Auto-commit after successful build (2026-04-21 21:28:05)

This commit is contained in:
Joe Tretter
2026-04-21 21:28:05 -05:00
parent 8303cfdea6
commit 45e2452015
2 changed files with 149 additions and 11 deletions

View File

@@ -113,9 +113,13 @@ object GitSandboxEngine {
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo) parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
parts.size >= 3 && parts[1] == "add" -> { parts.size >= 3 && parts[1] == "add" -> {
val target = parts[2] val target = parts[2]
if (target != "." && repo.files.none { it.name == target }) {
repo to listOf("fatal: pathspec '$target' did not match any files")
} else {
val updated = repo.files.map { if (target == "." || it.name == target) it.copy(staged = true) else it } val updated = repo.files.map { if (target == "." || it.name == target) it.copy(staged = true) else it }
repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}") repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
} }
}
parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts) parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts)
parts.size >= 2 && parts[1] == "log" -> { parts.size >= 2 && parts[1] == "log" -> {
repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet") repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet")

View File

@@ -31,11 +31,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
private val AppBackground = Color(0xFF000000) private val AppBackground = Color(0xFF000000)
@@ -71,10 +73,13 @@ fun GitHugApp() {
var currentLevelIndex by remember { mutableStateOf(0) } var currentLevelIndex by remember { mutableStateOf(0) }
var mode by remember { mutableStateOf(PlayMode.CLI_ONLY) } var mode by remember { mutableStateOf(PlayMode.CLI_ONLY) }
var repo by remember { mutableStateOf(levels.first().setup()) } var repo by remember { mutableStateOf(levels.first().setup()) }
var commandInput by remember { mutableStateOf("") } var commandInput by remember { mutableStateOf(TextFieldValue("")) }
var output by remember { mutableStateOf(listOf("Welcome to GitHug Android.")) } var output by remember { mutableStateOf(listOf("Welcome to GitHug Android.")) }
var hintIndex by remember { mutableStateOf(0) } var hintIndex by remember { mutableStateOf(0) }
var completedLevels by remember { mutableStateOf(setOf<String>()) } var completedLevels by remember { mutableStateOf(setOf<String>()) }
var commandHistory by remember { mutableStateOf(listOf<String>()) }
var historyIndex by remember { mutableStateOf(-1) }
var historyDraft by remember { mutableStateOf("") }
val currentLevel = levels[currentLevelIndex] val currentLevel = levels[currentLevelIndex]
val solved = currentLevel.validator(repo) val solved = currentLevel.validator(repo)
@@ -85,14 +90,75 @@ fun GitHugApp() {
fun resetCurrentLevel(message: String = "Level reset.") { fun resetCurrentLevel(message: String = "Level reset.") {
repo = currentLevel.setup() repo = currentLevel.setup()
commandInput = "" commandInput = TextFieldValue("")
output = listOf(message) output = listOf(message)
hintIndex = 0 hintIndex = 0
historyIndex = -1
historyDraft = ""
}
fun setCommandText(text: String) {
commandInput = TextFieldValue(text = text, selection = TextRange(text.length))
}
fun moveCursor(delta: Int) {
val next = (commandInput.selection.start + delta).coerceIn(0, commandInput.text.length)
commandInput = commandInput.copy(selection = TextRange(next))
}
fun historyUp() {
if (commandHistory.isEmpty()) return
if (historyIndex == -1) {
historyDraft = commandInput.text
historyIndex = commandHistory.lastIndex
} else {
historyIndex = (historyIndex - 1).coerceAtLeast(0)
}
setCommandText(commandHistory[historyIndex])
}
fun historyDown() {
if (commandHistory.isEmpty() || historyIndex == -1) return
if (historyIndex >= commandHistory.lastIndex) {
historyIndex = -1
setCommandText(historyDraft)
} else {
historyIndex += 1
setCommandText(commandHistory[historyIndex])
}
}
fun tabComplete() {
val cursor = commandInput.selection.start.coerceIn(0, commandInput.text.length)
val beforeCursor = commandInput.text.substring(0, cursor)
val tokenStart = beforeCursor.lastIndexOf(' ').let { if (it == -1) 0 else it + 1 }
val token = commandInput.text.substring(tokenStart, cursor)
if (token.isBlank()) return
val matches = repo.files.map { it.name }.sorted().filter { it.startsWith(token) }
if (matches.isEmpty()) return
val replacement = if (matches.size == 1) matches.first() else commonPrefix(matches)
if (replacement == token && matches.size > 1) {
output = output + "completion> ${matches.joinToString(" ")}"
return
}
val newText = commandInput.text.replaceRange(tokenStart, cursor, replacement)
val newCursor = tokenStart + replacement.length
commandInput = TextFieldValue(newText, selection = TextRange(newCursor))
} }
fun runCommand() { fun runCommand() {
val raw = commandInput.trim() val raw = commandInput.text.trim()
if (raw.isBlank()) return if (raw.isBlank()) return
if (commandHistory.lastOrNull() != raw) {
commandHistory = commandHistory + raw
}
historyIndex = -1
historyDraft = ""
val (newRepo, lines) = GitSandboxEngine.execute(repo, raw) val (newRepo, lines) = GitSandboxEngine.execute(repo, raw)
val solvedAfterCommand = currentLevel.validator(newRepo) val solvedAfterCommand = currentLevel.validator(newRepo)
val wasAlreadyCompleted = currentLevel.id in completedLevels val wasAlreadyCompleted = currentLevel.id in completedLevels
@@ -113,17 +179,17 @@ fun GitHugApp() {
currentLevelIndex = nextLevelIndex currentLevelIndex = nextLevelIndex
repo = nextLevel.setup() repo = nextLevel.setup()
output = listOf("Loaded level: ${nextLevel.title}") output = listOf("Loaded level: ${nextLevel.title}")
commandInput = "" commandInput = TextFieldValue("")
hintIndex = 0 hintIndex = 0
} else { } else {
repo = newRepo repo = newRepo
output = listOf("🏁 All available MVP levels completed.") output = listOf("🏁 All available MVP levels completed.")
commandInput = "" commandInput = TextFieldValue("")
} }
} else { } else {
repo = newRepo repo = newRepo
output = newOutput output = newOutput
commandInput = "" commandInput = TextFieldValue("")
} }
} }
@@ -145,8 +211,10 @@ fun GitHugApp() {
currentLevelIndex = index currentLevelIndex = index
repo = levels[index].setup() repo = levels[index].setup()
output = listOf("Loaded level: ${levels[index].title}") output = listOf("Loaded level: ${levels[index].title}")
commandInput = "" commandInput = TextFieldValue("")
hintIndex = 0 hintIndex = 0
historyIndex = -1
historyDraft = ""
} }
ModeBar(mode = mode, onModeSelected = { mode = it }) ModeBar(mode = mode, onModeSelected = { mode = it })
LevelCard( LevelCard(
@@ -166,6 +234,11 @@ fun GitHugApp() {
commandInput = commandInput, commandInput = commandInput,
onValueChange = { commandInput = it }, onValueChange = { commandInput = it },
onRun = { runCommand() }, onRun = { runCommand() },
onTab = { tabComplete() },
onCursorLeft = { moveCursor(-1) },
onCursorRight = { moveCursor(1) },
onHistoryUp = { historyUp() },
onHistoryDown = { historyDown() },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 260.dp) .heightIn(min = 260.dp)
@@ -275,9 +348,14 @@ private fun InfoCard(title: String, content: String, modifier: Modifier = Modifi
@Composable @Composable
private fun TerminalPanel( private fun TerminalPanel(
output: List<String>, output: List<String>,
commandInput: String, commandInput: TextFieldValue,
onValueChange: (String) -> Unit, onValueChange: (TextFieldValue) -> Unit,
onRun: () -> Unit, onRun: () -> Unit,
onTab: () -> Unit,
onCursorLeft: () -> Unit,
onCursorRight: () -> Unit,
onHistoryUp: () -> Unit,
onHistoryDown: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Card(modifier = modifier, colors = CardDefaults.cardColors(containerColor = TerminalBackground)) { Card(modifier = modifier, colors = CardDefaults.cardColors(containerColor = TerminalBackground)) {
@@ -291,6 +369,13 @@ private fun TerminalPanel(
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
) )
} }
SpecialKeyBar(
onTab = onTab,
onCursorLeft = onCursorLeft,
onCursorRight = onCursorRight,
onHistoryUp = onHistoryUp,
onHistoryDown = onHistoryDown,
)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -323,3 +408,52 @@ private fun TerminalPanel(
} }
} }
} }
@Composable
private fun SpecialKeyBar(
onTab: () -> Unit,
onCursorLeft: () -> Unit,
onCursorRight: () -> Unit,
onHistoryUp: () -> Unit,
onHistoryDown: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TerminalKeyButton(label = "TAB", onClick = onTab, modifier = Modifier.weight(1.3f))
TerminalKeyButton(label = "", onClick = onCursorLeft)
TerminalKeyButton(label = "", onClick = onCursorRight)
TerminalKeyButton(label = "", onClick = onHistoryUp)
TerminalKeyButton(label = "", onClick = onHistoryDown)
}
}
@Composable
private fun TerminalKeyButton(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = PanelSecondary,
contentColor = TextPrimary
)
) {
Text(label, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold)
}
}
private fun commonPrefix(values: List<String>): String {
if (values.isEmpty()) return ""
var prefix = values.first()
values.drop(1).forEach { value ->
while (!value.startsWith(prefix) && prefix.isNotEmpty()) {
prefix = prefix.dropLast(1)
}
}
return prefix
}