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,8 +113,12 @@ object GitSandboxEngine {
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
parts.size >= 3 && parts[1] == "add" -> {
val target = parts[2]
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}")
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 }
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] == "log" -> {

View File

@@ -31,11 +31,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
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.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
private val AppBackground = Color(0xFF000000)
@@ -71,10 +73,13 @@ fun GitHugApp() {
var currentLevelIndex by remember { mutableStateOf(0) }
var mode by remember { mutableStateOf(PlayMode.CLI_ONLY) }
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 hintIndex by remember { mutableStateOf(0) }
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 solved = currentLevel.validator(repo)
@@ -85,14 +90,75 @@ fun GitHugApp() {
fun resetCurrentLevel(message: String = "Level reset.") {
repo = currentLevel.setup()
commandInput = ""
commandInput = TextFieldValue("")
output = listOf(message)
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() {
val raw = commandInput.trim()
val raw = commandInput.text.trim()
if (raw.isBlank()) return
if (commandHistory.lastOrNull() != raw) {
commandHistory = commandHistory + raw
}
historyIndex = -1
historyDraft = ""
val (newRepo, lines) = GitSandboxEngine.execute(repo, raw)
val solvedAfterCommand = currentLevel.validator(newRepo)
val wasAlreadyCompleted = currentLevel.id in completedLevels
@@ -113,17 +179,17 @@ fun GitHugApp() {
currentLevelIndex = nextLevelIndex
repo = nextLevel.setup()
output = listOf("Loaded level: ${nextLevel.title}")
commandInput = ""
commandInput = TextFieldValue("")
hintIndex = 0
} else {
repo = newRepo
output = listOf("🏁 All available MVP levels completed.")
commandInput = ""
commandInput = TextFieldValue("")
}
} else {
repo = newRepo
output = newOutput
commandInput = ""
commandInput = TextFieldValue("")
}
}
@@ -145,8 +211,10 @@ fun GitHugApp() {
currentLevelIndex = index
repo = levels[index].setup()
output = listOf("Loaded level: ${levels[index].title}")
commandInput = ""
commandInput = TextFieldValue("")
hintIndex = 0
historyIndex = -1
historyDraft = ""
}
ModeBar(mode = mode, onModeSelected = { mode = it })
LevelCard(
@@ -166,6 +234,11 @@ fun GitHugApp() {
commandInput = commandInput,
onValueChange = { commandInput = it },
onRun = { runCommand() },
onTab = { tabComplete() },
onCursorLeft = { moveCursor(-1) },
onCursorRight = { moveCursor(1) },
onHistoryUp = { historyUp() },
onHistoryDown = { historyDown() },
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 260.dp)
@@ -275,9 +348,14 @@ private fun InfoCard(title: String, content: String, modifier: Modifier = Modifi
@Composable
private fun TerminalPanel(
output: List<String>,
commandInput: String,
onValueChange: (String) -> Unit,
commandInput: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
onRun: () -> Unit,
onTab: () -> Unit,
onCursorLeft: () -> Unit,
onCursorRight: () -> Unit,
onHistoryUp: () -> Unit,
onHistoryDown: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(modifier = modifier, colors = CardDefaults.cardColors(containerColor = TerminalBackground)) {
@@ -291,6 +369,13 @@ private fun TerminalPanel(
fontFamily = FontFamily.Monospace
)
}
SpecialKeyBar(
onTab = onTab,
onCursorLeft = onCursorLeft,
onCursorRight = onCursorRight,
onHistoryUp = onHistoryUp,
onHistoryDown = onHistoryDown,
)
Row(
modifier = Modifier
.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
}