Auto-commit after successful build (2026-04-21 21:28:05)
This commit is contained in:
@@ -113,8 +113,12 @@ 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]
|
||||||
val updated = repo.files.map { if (target == "." || it.name == target) it.copy(staged = true) else it }
|
if (target != "." && repo.files.none { it.name == target }) {
|
||||||
repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else 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] == "commit" -> commit(repo, parts)
|
||||||
parts.size >= 2 && parts[1] == "log" -> {
|
parts.size >= 2 && parts[1] == "log" -> {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user