From 344ff99f0a048233c4ac62e7a21b91257776bb2c Mon Sep 17 00:00:00 2001 From: Joe Tretter Date: Tue, 5 May 2026 19:54:33 -0500 Subject: [PATCH] Add terminal input onboarding bubble --- app/build.gradle.kts | 4 +- .../tretter/githugandroid/GitHugApp.kt | 7 +++ .../tretter/githugandroid/Terminal.kt | 51 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 880ba16..7642e3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 113 - versionName = "0.1.112" + versionCode = 114 + versionName = "0.1.113" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt b/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt index 32dc786..b0dcf64 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt @@ -77,6 +77,7 @@ fun GitHugApp() { var gitMessageEditorState by remember { mutableStateOf(null) } var manPageState by remember { mutableStateOf(null) } var solvedCelebrationTitle by remember { mutableStateOf(null) } + var showTerminalInputHint by remember { mutableStateOf(true) } val currentLevel = levels[currentLevelIndex] LaunchedEffect(solvedCelebrationTitle) { @@ -412,6 +413,7 @@ fun GitHugApp() { val raw = submittedText.trim() if (raw.isBlank()) return + showTerminalInputHint = false suppressedImeEcho = submittedText clearCommandInput(recreateField = true) @@ -509,6 +511,8 @@ fun GitHugApp() { output = output, inputFieldVersion = inputFieldVersion, commandInput = commandInput, + showInputHint = showTerminalInputHint, + onInputHintDismiss = { showTerminalInputHint = false }, onValueChange = { val blockedEcho = suppressedImeEcho if (blockedEcho != null && commandInput.text.isEmpty()) { @@ -518,6 +522,9 @@ fun GitHugApp() { } } suppressedImeEcho = null + if (it.text.isNotEmpty()) { + showTerminalInputHint = false + } commandInput = it }, onRun = { runCommand() }, diff --git a/app/src/main/java/solutions/tretter/githugandroid/Terminal.kt b/app/src/main/java/solutions/tretter/githugandroid/Terminal.kt index df0db32..9375da0 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/Terminal.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/Terminal.kt @@ -1,6 +1,7 @@ package solutions.tretter.githugandroid import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -37,6 +38,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalFocusManager @@ -57,6 +59,8 @@ fun TerminalPane( output: List, inputFieldVersion: Int, commandInput: TextFieldValue, + showInputHint: Boolean, + onInputHintDismiss: () -> Unit, onValueChange: (TextFieldValue) -> Unit, onRun: () -> Unit, onTab: () -> Unit, @@ -150,6 +154,9 @@ fun TerminalPane( onHistoryDown = onHistoryDown, onInsertText = { insertLiteral(it) }, ) + if (showInputHint) { + TerminalInputHintBubble() + } Row( modifier = Modifier .fillMaxWidth() @@ -171,6 +178,7 @@ fun TerminalPane( onValueChange = onValueChange, focusRequester = focusRequester, keyboardController = keyboardController, + onFocused = onInputHintDismiss, onSubmit = { submitCommand() }, ) } @@ -186,6 +194,7 @@ private fun RowScope.TerminalInputField( onValueChange: (TextFieldValue) -> Unit, focusRequester: FocusRequester, keyboardController: androidx.compose.ui.platform.SoftwareKeyboardController?, + onFocused: () -> Unit, onSubmit: () -> Unit, ) { key(inputFieldVersion) { @@ -197,6 +206,7 @@ private fun RowScope.TerminalInputField( .focusRequester(focusRequester) .onFocusChanged { state -> if (state.isFocused) { + onFocused() keyboardController?.show() } }, @@ -237,6 +247,47 @@ private fun RowScope.TerminalInputField( } } +@Composable +private fun TerminalInputHintBubble() { + val bubbleColor = Color(0xFFFFF1A8) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 46.dp, end = 14.dp), + horizontalAlignment = Alignment.Start, + ) { + Box( + modifier = Modifier + .background(bubbleColor, RoundedCornerShape(18.dp)) + .padding(horizontal = 14.dp, vertical = 10.dp), + ) { + Text( + text = "Tap here to enter a command", + color = Color(0xFF161000), + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + ) + } + Canvas( + modifier = Modifier + .padding(start = 26.dp) + .width(26.dp) + .height(13.dp), + ) { + drawPath( + path = Path().apply { + moveTo(0f, 0f) + lineTo(size.width, 0f) + lineTo(size.width * 0.25f, size.height) + close() + }, + color = bubbleColor, + ) + } + } +} + @Composable private fun SpecialKeyBar( onTab: () -> Unit,