From 44bd87d5366107c86c9a2eee8a686f2b70d74532 Mon Sep 17 00:00:00 2001 From: Joe Tretter Date: Wed, 22 Apr 2026 13:42:59 -0500 Subject: [PATCH] Auto-commit after successful build: update app gameplay/UI, improve build setup Changed files:\napp/build.gradle.kts app/src/main/java/com/kawomi/githugandroid/GameModels.kt app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt --- app/build.gradle.kts | 4 +- .../com/kawomi/githugandroid/GameModels.kt | 174 ++++++++++++++++-- .../com/kawomi/githugandroid/GitHugApp.kt | 24 ++- 3 files changed, 178 insertions(+), 24 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9e48fed..c528f19 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.kawomi.githugandroid" minSdk = 26 targetSdk = 34 - versionCode = 6 - versionName = "0.1.5" + versionCode = 7 + versionName = "0.1.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/com/kawomi/githugandroid/GameModels.kt b/app/src/main/java/com/kawomi/githugandroid/GameModels.kt index c2a2af8..64223b4 100644 --- a/app/src/main/java/com/kawomi/githugandroid/GameModels.kt +++ b/app/src/main/java/com/kawomi/githugandroid/GameModels.kt @@ -11,6 +11,7 @@ data class GitFile( val name: String, val content: String = "", val staged: Boolean = false, + val tracked: Boolean = false, ) data class CommitNode( @@ -71,7 +72,7 @@ val RepoStateSaver = listSaver( listOf( state.initialized, state.headBranch, - state.files.flatMap { listOf(it.name, it.content, it.staged.toString()) }, + state.files.flatMap { listOf(it.name, it.content, it.staged.toString(), it.tracked.toString()) }, state.commits.flatMap { listOf(it.id, it.message) }, state.branches.flatMap { listOf(it.key, it.value.toString()) }, ) @@ -85,8 +86,13 @@ val RepoStateSaver = listSaver( RepoState( initialized = initialized, headBranch = headBranch, - files = fileParts.chunked(3).map { - GitFile(it[0] as String, it[1] as String, (it[2] as String).toBoolean()) + files = fileParts.chunked(4).map { + GitFile( + name = it[0] as String, + content = it[1] as String, + staged = (it[2] as String).toBoolean(), + tracked = (it[3] as String).toBoolean(), + ) }, commits = commitParts.chunked(2).map { CommitNode(it[0] as String, it[1] as String) @@ -97,10 +103,28 @@ val RepoStateSaver = listSaver( ) object GitSandboxEngine { + fun commandReferenceLines(): List = listOf( + "Available sandbox commands:", + " git init", + " git status", + " git add ", + " git add .", + " git commit -m \"message\"", + " git commit -am \"message\"", + " git log", + " git branch ", + " git checkout ", + " ls", + " touch ", + " help", + " git help", + ) + fun execute(repo: RepoState, command: String): Pair> { - val parts = command.split(" ").filter { it.isNotBlank() } + val parts = tokenizeCommand(command) if (parts.isEmpty()) return repo to emptyList() return when { + parts[0] == "help" || parts[0] == "?" -> repo to commandReferenceLines() parts[0] == "touch" && parts.size >= 2 -> { val name = parts[1] if (repo.files.any { it.name == name }) repo to listOf("$name already exists") @@ -110,6 +134,7 @@ object GitSandboxEngine { parts[0] != "git" -> repo to listOf("Command not supported in sandbox. Try a git command or 'touch'.") parts.size >= 2 && parts[1] == "init" -> repo.copy(initialized = true, branches = mapOf("master" to repo.commits.size)) to listOf("Initialized empty Git repository") !repo.initialized -> repo to listOf("fatal: not a git repository") + parts.size >= 2 && parts[1] == "help" -> repo to commandReferenceLines() parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo) parts.size >= 3 && parts[1] == "add" -> { val target = parts[2] @@ -120,7 +145,7 @@ object GitSandboxEngine { 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.drop(2)) 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") else repo.commits.reversed().flatMap { listOf("commit ${it.id}", " ${it.message}") } @@ -139,26 +164,94 @@ object GitSandboxEngine { } } - private fun commit(repo: RepoState, parts: List): Pair> { - val messageIndex = parts.indexOf("-m") - if (messageIndex == -1 || messageIndex == parts.lastIndex) { - return repo to listOf("error: commit message required. Use git commit -m \"message\"") + private fun commit(repo: RepoState, arguments: List): Pair> { + val parsed = parseCommitArguments(arguments) + if (parsed.error != null) { + return repo to listOf(parsed.error) } - val message = parts.drop(messageIndex + 1).joinToString(" ").trim('"') - val staged = repo.files.filter { it.staged } + + val repoForCommit = if (parsed.stageAllTracked) { + repo.copy(files = repo.files.map { file -> if (file.tracked) file.copy(staged = true) else file }) + } else { + repo + } + + val message = parsed.message.orEmpty() + val staged = repoForCommit.files.filter { it.staged } if (staged.isEmpty()) return repo to listOf("nothing to commit") + val nextId = "${repo.commits.size + 1}".padStart(7, '0') - val cleanedFiles = repo.files.map { it.copy(staged = false) } - return repo.copy( + val cleanedFiles = repoForCommit.files.map { file -> + if (file.staged) file.copy(staged = false, tracked = true) else file + } + + return repoForCommit.copy( files = cleanedFiles, commits = repo.commits + CommitNode(nextId, message), branches = repo.branches + (repo.headBranch to (repo.commits.size + 1)), ) to listOf("[$nextId] $message") } + private fun parseCommitArguments(arguments: List): ParsedCommitArguments { + var message: String? = null + var stageAllTracked = false + var index = 0 + + while (index < arguments.size) { + val argument = arguments[index] + when { + argument == "-a" || argument == "--all" -> { + stageAllTracked = true + } + argument == "-m" || argument == "--message" -> { + val next = arguments.getOrNull(index + 1) + ?: return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"") + message = next + index += 1 + } + argument.startsWith("--message=") -> { + message = argument.substringAfter('=') + } + argument.startsWith("-") && !argument.startsWith("--") && argument.length > 2 -> { + val shortFlags = argument.drop(1) + var shortIndex = 0 + while (shortIndex < shortFlags.length) { + when (val flag = shortFlags[shortIndex]) { + 'a' -> stageAllTracked = true + 'm' -> { + val attachedValue = shortFlags.substring(shortIndex + 1) + if (attachedValue.isNotEmpty()) { + message = attachedValue + } else { + val next = arguments.getOrNull(index + 1) + ?: return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"") + message = next + index += 1 + } + break + } + else -> return ParsedCommitArguments(error = "error: unsupported commit option '-$flag'") + } + shortIndex += 1 + } + } + else -> return ParsedCommitArguments(error = "error: unsupported commit argument '$argument'") + } + index += 1 + } + + if (message.isNullOrBlank()) { + return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"") + } + + return ParsedCommitArguments(message = message, stageAllTracked = stageAllTracked) + } + private fun statusLines(repo: RepoState): List { - val staged = repo.files.filter { it.staged }.map { "new file: ${it.name}" } - val unstaged = repo.files.filterNot { it.staged }.map { "untracked: ${it.name}" } + val staged = repo.files.filter { it.staged }.map { + if (it.tracked) "modified: ${it.name}" else "new file: ${it.name}" + } + val unstaged = repo.files.filterNot { it.staged || it.tracked }.map { "untracked: ${it.name}" } return buildList { add("On branch ${repo.headBranch}") if (staged.isEmpty() && unstaged.isEmpty()) { @@ -175,4 +268,55 @@ object GitSandboxEngine { } } } + + private fun tokenizeCommand(command: String): List { + val result = mutableListOf() + val current = StringBuilder() + var quoteChar: Char? = null + var escaping = false + + command.forEach { char -> + when { + escaping -> { + current.append(char) + escaping = false + } + char == '\\' && quoteChar != '\'' -> { + escaping = true + } + quoteChar != null -> { + if (char == quoteChar) { + quoteChar = null + } else { + current.append(char) + } + } + char == '"' || char == '\'' -> { + quoteChar = char + } + char.isWhitespace() -> { + if (current.isNotEmpty()) { + result += current.toString() + current.clear() + } + } + else -> current.append(char) + } + } + + if (escaping) { + current.append('\\') + } + if (current.isNotEmpty()) { + result += current.toString() + } + + return result + } + + private data class ParsedCommitArguments( + val message: String? = null, + val stageAllTracked: Boolean = false, + val error: String? = null, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt b/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt index cc16f38..8f0d5bd 100644 --- a/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt +++ b/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt @@ -211,6 +211,10 @@ fun GitHugApp() { } } + fun showCommandHelp() { + output = output + GitSandboxEngine.commandReferenceLines() + } + Scaffold { padding -> Surface( modifier = Modifier @@ -265,6 +269,7 @@ fun GitHugApp() { }, onRun = { runCommand() }, onTab = { tabComplete() }, + onHelp = { showCommandHelp() }, onCursorLeft = { moveCursor(-1) }, onCursorRight = { moveCursor(1) }, onHistoryUp = { historyUp() }, @@ -383,6 +388,7 @@ private fun TerminalPanel( onValueChange: (TextFieldValue) -> Unit, onRun: () -> Unit, onTab: () -> Unit, + onHelp: () -> Unit, onCursorLeft: () -> Unit, onCursorRight: () -> Unit, onHistoryUp: () -> Unit, @@ -419,6 +425,7 @@ private fun TerminalPanel( } SpecialKeyBar( onTab = onTab, + onHelp = onHelp, onCursorLeft = onCursorLeft, onCursorRight = onCursorRight, onHistoryUp = onHistoryUp, @@ -464,6 +471,7 @@ private fun TerminalPanel( @Composable private fun SpecialKeyBar( onTab: () -> Unit, + onHelp: () -> Unit, onCursorLeft: () -> Unit, onCursorRight: () -> Unit, onHistoryUp: () -> Unit, @@ -471,13 +479,14 @@ private fun SpecialKeyBar( ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - TerminalKeyButton(label = "TAB", onClick = onTab, modifier = Modifier.width(68.dp)) - TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(48.dp)) - TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(48.dp)) - TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(48.dp)) - TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(48.dp)) + TerminalKeyButton(label = "↹", onClick = onTab, modifier = Modifier.width(56.dp), fontFamily = FontFamily.Default) + TerminalKeyButton(label = "?", onClick = onHelp, modifier = Modifier.width(40.dp), fontFamily = FontFamily.Default) + TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(40.dp)) + TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(40.dp)) + TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(40.dp)) + TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(40.dp)) } } @@ -486,6 +495,7 @@ private fun TerminalKeyButton( label: String, onClick: () -> Unit, modifier: Modifier = Modifier, + fontFamily: FontFamily = FontFamily.Monospace, ) { Button( onClick = onClick, @@ -498,7 +508,7 @@ private fun TerminalKeyButton( ) { Text( text = label, - fontFamily = FontFamily.Monospace, + fontFamily = fontFamily, fontWeight = FontWeight.Bold, fontSize = 13.sp )