package solutions.tretter.githugandroid import android.os.SystemClock import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable fun GitHugApp() { MaterialTheme(colorScheme = GitHugColorScheme) { val context = LocalContext.current val configuration = LocalConfiguration.current val density = LocalDensity.current val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) } if (!runtime.isNativeGitAvailable()) { MissingNativeGitScreen(message = runtime.unavailableMessage()) return@MaterialTheme } val levels = remember { sampleLevels() } val paneLayoutStore = remember(context) { PaneLayoutStore(context.applicationContext) } val gameProgressStore = remember(context) { GameProgressStore(context.applicationContext) } val persistedPaneLayout by paneLayoutStore.layoutFlow.collectAsState(initial = defaultPaneLayout()) val persistedProgress by gameProgressStore.progressFlow.collectAsState(initial = null) val scope = rememberCoroutineScope() val screenScrollState = rememberScrollState() val screenHeightDp = configuration.screenHeightDp var paneLayout by remember { mutableStateOf(defaultPaneLayout()) } var currentLevelIndex by remember { mutableStateOf(0) } var repo by remember { mutableStateOf(runtime.prepareLevel(levels.first())) } var commandInput by remember { mutableStateOf(TextFieldValue("")) } var inputFieldVersion by remember { mutableStateOf(0) } var suppressedImeEcho by remember { mutableStateOf(null) } var output by remember { mutableStateOf(listOf(runtime.startupBanner())) } var hintIndex by remember { mutableStateOf(0) } var activeExerciseDetail by remember { mutableStateOf(null) } var visibleHint by remember { mutableStateOf(null) } var completedLevels by remember { mutableStateOf(setOf()) } var commandHistory by remember { mutableStateOf(listOf()) } var historyIndex by remember { mutableStateOf(-1) } var historyDraft by remember { mutableStateOf("") } var hasRestoredProgress by remember { mutableStateOf(false) } var isPreparingLevel by remember { mutableStateOf(false) } var levelLoadRequestId by remember { mutableStateOf(0) } var editorState by remember { mutableStateOf(null) } var gitMessageEditorState by remember { mutableStateOf(null) } var manPageState by remember { mutableStateOf(null) } var solvedCelebrationTitle by remember { mutableStateOf(null) } var showTerminalInputHint by remember { mutableStateOf(true) } var showExerciseDescriptionHint by remember { mutableStateOf(true) } var showHelpOverlay by rememberSaveable { mutableStateOf(false) } var showHelpOnStart by rememberSaveable { mutableStateOf(true) } var helpPreferenceInitialized by rememberSaveable { mutableStateOf(false) } var showDiagnosticDialog by rememberSaveable { mutableStateOf(false) } var diagnosticSkinEnabled by rememberSaveable { mutableStateOf(false) } var headerTapTimes by remember { mutableStateOf(emptyList()) } var exerciseBounds by remember { mutableStateOf(null) } var promptBounds by remember { mutableStateOf(null) } val currentLevel = levels[currentLevelIndex] val appBackground = if (diagnosticSkinEnabled) DiagnosticAppBackground else AppBackground val imeBottom = WindowInsets.ime.getBottom(density) val navigationBottom = WindowInsets.navigationBars.getBottom(density) val bottomSystemPadding = with(density) { if (imeBottom > navigationBottom) 0.dp else navigationBottom.toDp() } LaunchedEffect(solvedCelebrationTitle) { if (solvedCelebrationTitle != null) { delay(3_000) solvedCelebrationTitle = null } } LaunchedEffect(persistedPaneLayout) { paneLayout = persistedPaneLayout } LaunchedEffect(currentLevelIndex) { AppLog.d("GitHugApp", "Current level index changed to $currentLevelIndex id=${levels[currentLevelIndex].id}") delay(100) screenScrollState.animateScrollTo(0) AppLog.d("GitHugApp", "Scrolled to top for level index=$currentLevelIndex id=${levels[currentLevelIndex].id}") } fun updatePaneLayout(transform: (PaneLayout) -> PaneLayout, persist: Boolean = true) { val updated = transform(paneLayout) paneLayout = updated if (persist) { scope.launch { paneLayoutStore.save(updated) } } } fun clearCommandInput(recreateField: Boolean = false) { commandInput = TextFieldValue(text = "", selection = TextRange.Zero) if (recreateField) { inputFieldVersion += 1 } } suspend fun persistProgress(completed: Set, activeLevelId: String?) { gameProgressStore.saveProgress(completed, activeLevelId) } LaunchedEffect(persistedProgress) { val restoredProgress = persistedProgress ?: return@LaunchedEffect val restoredCompletedLevels = restoredProgress.completedLevels completedLevels = restoredCompletedLevels showHelpOnStart = restoredProgress.showHelpOnStart if (!helpPreferenceInitialized) { helpPreferenceInitialized = true showHelpOverlay = restoredProgress.showHelpOnStart } AppLog.d( "GitHugApp", "Observed persisted progress completed=${restoredCompletedLevels.sorted()} activeLevelId=${restoredProgress.activeLevelId} restored=$hasRestoredProgress", ) if (!hasRestoredProgress) { hasRestoredProgress = true val resumeIndex = restoredProgress.activeLevelId ?.let { activeId -> levels.indexOfFirst { it.id == activeId }.takeIf { it >= 0 } } ?: levels.indexOfFirst { it.id !in restoredCompletedLevels }.takeIf { it >= 0 } ?: levels.lastIndex AppLog.d( "GitHugApp", "Restoring app to levelIndex=$resumeIndex levelId=${levels[resumeIndex].id}", ) currentLevelIndex = resumeIndex repo = runtime.prepareLevel(levels[resumeIndex]) output = listOf( if (restoredCompletedLevels.size == levels.size) { "🏁 All Githug levels completed." } else if (restoredCompletedLevels.isEmpty()) { runtime.startupBanner() } else { "Resumed at level: ${levels[resumeIndex].title}" } ) commandInput = TextFieldValue(text = "", selection = TextRange.Zero) suppressedImeEcho = null hintIndex = 0 activeExerciseDetail = null visibleHint = null historyIndex = -1 historyDraft = "" paneLayout = paneLayout.copy( weights = paneLayout.weights + recommendedPaneWeights( heights = recommendedPaneHeights( level = levels[resumeIndex], levelCount = levels.size, outputLineCount = 1, screenHeightDp = screenHeightDp, suggestionsVisible = false, visibleHint = null, ), ) ) } } fun applyRecommendedPaneWeights( persist: Boolean = true, outputLineCount: Int = terminalOutputLineCount(output), ) { updatePaneLayout( transform = { layout -> layout.copy( weights = layout.weights + recommendedPaneWeights( heights = recommendedPaneHeights( level = currentLevel, levelCount = levels.size, outputLineCount = outputLineCount, screenHeightDp = screenHeightDp, suggestionsVisible = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS, visibleHint = visibleHint, ), ) ) }, persist = persist, ) } fun resetCurrentLevel(message: String = "Level reset.") { val startedAt = System.nanoTime() val resetLevelId = currentLevel.id AppLog.d("GitHugApp", "Reset requested level=$resetLevelId") val updatedCompletedLevels = completedLevels - resetLevelId completedLevels = updatedCompletedLevels scope.launch { persistProgress(updatedCompletedLevels, resetLevelId) } repo = runtime.prepareLevel(currentLevel) clearCommandInput() suppressedImeEcho = null output = listOf(message) hintIndex = 0 activeExerciseDetail = null visibleHint = null historyIndex = -1 historyDraft = "" editorState = null gitMessageEditorState = null manPageState = null applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(listOf(message))) AppLog.d( "GitHugApp", "Reset finished level=$resetLevelId durationMs=${elapsedMillisSince(startedAt)} repo=${repo.diagnosticSnapshot()}", ) } fun loadLevel( index: Int, message: List = listOf("Loaded level: ${levels[index].title}"), keepSuppressedImeEcho: Boolean = false, prepareInBackground: Boolean = false, ) { val startedAt = System.nanoTime() val level = levels[index] val requestId = levelLoadRequestId + 1 levelLoadRequestId = requestId AppLog.d("GitHugApp", "Loading level index=$index id=${level.id} title=${level.title} background=$prepareInBackground") currentLevelIndex = index output = if (prepareInBackground) listOf("Preparing level: ${level.title}") else message clearCommandInput(recreateField = true) if (!keepSuppressedImeEcho) { suppressedImeEcho = null } hintIndex = 0 activeExerciseDetail = null visibleHint = null historyIndex = -1 historyDraft = "" editorState = null gitMessageEditorState = null manPageState = null if (prepareInBackground) { isPreparingLevel = true repo = RepoState() AppLog.d( "GitHugApp", "Level load state updated index=$index id=${level.id} requestId=$requestId durationMs=${elapsedMillisSince(startedAt)} preparing=true", ) scope.launch { val prepareStartedAt = System.nanoTime() AppLog.d("GitHugApp", "Background prepare started index=$index id=${level.id} requestId=$requestId") val preparedRepo = withContext(Dispatchers.Default) { runtime.prepareLevel(level) } val prepareMs = elapsedMillisSince(prepareStartedAt) if (levelLoadRequestId == requestId) { AppLog.d( "GitHugApp", "Background prepare applying index=$index id=${level.id} requestId=$requestId prepareMs=$prepareMs " + "repo=${preparedRepo.diagnosticSnapshot()}", ) repo = preparedRepo output = message isPreparingLevel = false clearCommandInput(recreateField = true) AppLog.d( "GitHugApp", "Background prepare applied index=$index id=${level.id} requestId=$requestId totalMs=${elapsedMillisSince(startedAt)}", ) } else { AppLog.d( "GitHugApp", "Background prepare discarded index=$index id=${level.id} requestId=$requestId activeRequestId=$levelLoadRequestId prepareMs=$prepareMs", ) } } } else { isPreparingLevel = false val prepareStartedAt = System.nanoTime() repo = runtime.prepareLevel(level) AppLog.d( "GitHugApp", "Synchronous prepare applied index=$index id=${level.id} prepareMs=${elapsedMillisSince(prepareStartedAt)}", ) } paneLayout = paneLayout.copy( weights = paneLayout.weights + recommendedPaneWeights( heights = recommendedPaneHeights( level = level, levelCount = levels.size, outputLineCount = 1, screenHeightDp = screenHeightDp, suggestionsVisible = false, visibleHint = null, ), ) ) AppLog.d( "GitHugApp", "Level load returned index=$index id=${level.id} requestId=$requestId durationMs=${elapsedMillisSince(startedAt)} preparing=$isPreparingLevel", ) } 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) val leadingCommand = beforeCursor.trimStart().substringBefore(' ') val isCdCompletion = leadingCommand == "cd" if (token.isBlank() && !isCdCompletion) return val candidates = runtime.completionCandidates(currentLevel, repo, directoriesOnly = isCdCompletion) val matches = contextualCompletionCandidates( candidates = candidates, commandBeforeToken = beforeCursor.substring(0, tokenStart), token = token, directoriesOnly = isCdCompletion, ) if (matches.isEmpty()) return val replacement = if (matches.size == 1) matches.first() else commonPrefix(matches) if (replacement == token && matches.size > 1) { val newOutput = output + "completion> ${matches.joinToString(" ")}" output = newOutput applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(newOutput)) return } val newText = commandInput.text.replaceRange(tokenStart, cursor, replacement) val newCursor = tokenStart + replacement.length commandInput = TextFieldValue(newText, selection = TextRange(newCursor)) } fun applyCommandResult(raw: String, newRepo: RepoState, lines: List, echoCommand: Boolean) { val startedAt = System.nanoTime() val levelForResult = currentLevel val validationBlockedByEditor = editorState != null || gitMessageEditorState != null || newRepo.nativeGitSession != null val solvedAfterCommand = if (validationBlockedByEditor) { false } else { levelForResult.validator(newRepo, raw) } val wasAlreadyCompleted = currentLevel.id in completedLevels AppLog.d( "GitHugApp", "Command='$raw' level=${levelForResult.id} solved=$solvedAfterCommand validationBlockedByEditor=$validationBlockedByEditor alreadyCompleted=$wasAlreadyCompleted " + "completedBefore=${completedLevels.sorted()} outputLineCount=${lines.size} repo=${newRepo.diagnosticSnapshot()}", ) val newOutput = buildList { addAll(output) if (echoCommand) { add("$ $raw") } addAll(lines) if (solvedAfterCommand && !wasAlreadyCompleted) { add("") add("👍 Level solved: ${levelForResult.title}") add("Nice. ${completedLevels.size + 1}/${levels.size} levels complete.") } } if (solvedAfterCommand && !wasAlreadyCompleted) { val updatedCompletedLevels = completedLevels + levelForResult.id completedLevels = updatedCompletedLevels solvedCelebrationTitle = levelForResult.title val nextLevelIndex = levels.indexOfFirst { it.id !in updatedCompletedLevels } if (nextLevelIndex >= 0) { val nextLevelId = levels[nextLevelIndex].id AppLog.d( "GitHugApp", "Level solved ${levelForResult.id}; advancing to next incomplete index=$nextLevelIndex id=$nextLevelId", ) scope.launch { persistProgress(updatedCompletedLevels, nextLevelId) } loadLevel(nextLevelIndex, message = emptyList(), keepSuppressedImeEcho = true) } else { AppLog.d("GitHugApp", "All levels completed") scope.launch { persistProgress(updatedCompletedLevels, levels.lastOrNull()?.id) } repo = newRepo val completedOutput = newOutput + listOf("🏁 All Githug levels completed.") output = completedOutput clearCommandInput() suppressedImeEcho = null applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(completedOutput)) } } else { AppLog.d("GitHugApp", "Staying on level=${levelForResult.id}") repo = newRepo output = newOutput applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(newOutput)) } AppLog.d( "GitHugApp", "Command result applied level=${levelForResult.id} solved=$solvedAfterCommand durationMs=${elapsedMillisSince(startedAt)}", ) } fun openEditor(invocation: VisualEditorInvocation) { val path = invocation.path.orEmpty() val (content, lines) = if (path.isBlank()) { "" to emptyList() } else { runtime.readEditorFile(currentLevel, repo, path) } val newOutput = buildList { addAll(output) add("$ ${invocation.command}") addAll(lines) if (lines.isEmpty()) { add("Opened ${invocation.editor} editor${if (path.isBlank()) "" else " for $path"}") } } output = newOutput if (lines.isEmpty()) { editorState = TextEditorState( editor = invocation.editor, originalCommand = invocation.command, path = path, content = content, saveAsPath = path, ) } applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(newOutput)) } fun openManPage(invocation: GitHelpInvocation) { val content = runtime.gitManPage(currentLevel, repo, invocation.topic) val newOutput = buildList { addAll(output) add("$ ${invocation.command}") add("Opened git help viewer for ${invocation.topic}") } output = newOutput manPageState = ManPageState(topic = invocation.topic, content = content) applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(newOutput)) } fun openGitMessageEditor(invocation: GitEditorInvocation) { val initialContent = runtime.gitEditorInitialContent(currentLevel, repo, invocation) val openedMessage = when (invocation.kind) { GitEditorCommandKind.REBASE_TODO -> "Opened Git rebase editor" else -> "Opened Git message editor" } val newOutput = buildList { addAll(output) add("$ ${invocation.command}") add(openedMessage) } output = newOutput gitMessageEditorState = GitMessageEditorState( invocation = invocation.copy(initialContent = initialContent), content = initialContent, ) applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(newOutput)) } fun saveEditor() { val state = editorState ?: return val targetPath = state.saveAsPath.trim() if (targetPath.isBlank()) return val (newRepo, lines) = runtime.writeEditorFile(currentLevel, repo, targetPath, state.content) editorState = null applyCommandResult(state.originalCommand, newRepo, lines, echoCommand = false) } fun saveGitMessageEditor() { val state = gitMessageEditorState ?: return val result = runtime.executeGitEditorCommandWithResult( level = currentLevel, currentRepo = repo, invocation = state.invocation, message = state.content, ) gitMessageEditorState = result.nextEditor?.let { nextEditor -> GitMessageEditorState( invocation = nextEditor.invocation, content = nextEditor.content, ) } applyCommandResult(state.invocation.command, result.repo, result.outputLines, echoCommand = false) } fun runCommand() { val startedAt = System.nanoTime() val submittedText = commandInput.text val raw = submittedText.trim() if (raw.isBlank()) return if (isPreparingLevel) { AppLog.d("GitHugApp", "Command blocked while preparing level=${currentLevel.id} raw='$raw'") output = output + "Still preparing ${currentLevel.title}. Try again in a moment." clearCommandInput(recreateField = true) applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(output)) return } val submittedLevelId = currentLevel.id val isInteractiveInput = repo.nativeGitSession != null AppLog.d( "GitHugApp", "Command submitted level=$submittedLevelId raw='$raw' interactive=$isInteractiveInput repo=${repo.diagnosticSnapshot()}", ) showHelpOverlay = false showTerminalInputHint = false showExerciseDescriptionHint = false suppressedImeEcho = submittedText clearCommandInput(recreateField = true) if (!isInteractiveInput && commandHistory.lastOrNull() != raw) { commandHistory = commandHistory + raw } historyIndex = -1 historyDraft = "" val editorInvocation = parseVisualEditorInvocation(raw) if (editorInvocation != null) { openEditor(editorInvocation) return } val helpInvocation = parseGitHelpInvocation(raw) if (helpInvocation != null) { openManPage(helpInvocation) return } val gitEditorInvocation = parseGitEditorInvocation(raw) if (gitEditorInvocation != null) { openGitMessageEditor(gitEditorInvocation) return } val (newRepo, lines) = runtime.execute(currentLevel, repo, raw) applyCommandResult(raw, newRepo, lines, echoCommand = !isInteractiveInput) AppLog.d("GitHugApp", "Command handling finished level=$submittedLevelId raw='$raw' durationMs=${elapsedMillisSince(startedAt)}") } fun showCommandHelp() { val newOutput = output + runtime.commandReferenceLines() output = newOutput applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(newOutput)) } fun registerHeaderTap() { val now = SystemClock.elapsedRealtime() val recentTaps = (headerTapTimes + now).filter { tapTime -> now - tapTime <= 15_000L } if (recentTaps.size >= 7) { headerTapTimes = emptyList() showDiagnosticDialog = true } else { headerTapTimes = recentTaps } } Scaffold( containerColor = appBackground, contentWindowInsets = WindowInsets(0.dp), ) { padding -> Surface( modifier = Modifier .fillMaxSize() .padding(padding), color = appBackground, ) { Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxSize() .background(appBackground) .statusBarsPadding() .verticalScroll(screenScrollState) .padding( start = 10.dp, top = 8.dp, end = 10.dp, bottom = 8.dp + bottomSystemPadding, ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { FixedHeader(onTap = { registerHeaderTap() }) Box( modifier = Modifier.fillMaxWidth(), ) { PaneWorkspace( modifier = Modifier.fillMaxWidth(), paneLayout = paneLayout, diagnosticSkinEnabled = diagnosticSkinEnabled, onMovePane = { paneId, delta -> updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) }) }, onTogglePane = { paneId -> updatePaneLayout(transform = { layout -> val collapsed = layout.collapsed.toMutableSet() if (!collapsed.add(paneId)) collapsed.remove(paneId) layout.copy(collapsed = collapsed) }) }, levelsContent = { LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index, prepareInBackground = true) } }, visualContent = { VisualPane(repo) }, exerciseContent = { ExercisePane( level = currentLevel, visibleHint = if (activeExerciseDetail == ExerciseDetailPanel.HINT) visibleHint else null, suggestionsExpanded = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS, showDescriptionHint = false, onBoundsChanged = { exerciseBounds = it }, onToggleSuggestions = { showExerciseDescriptionHint = false showHelpOverlay = false activeExerciseDetail = if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) null else ExerciseDetailPanel.SUGGESTIONS if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) { visibleHint = null } applyRecommendedPaneWeights(persist = false) }, onHint = { showExerciseDescriptionHint = false showHelpOverlay = false val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level." visibleHint = hint activeExerciseDetail = ExerciseDetailPanel.HINT hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size) applyRecommendedPaneWeights(persist = false) }, onReset = { showExerciseDescriptionHint = false showHelpOverlay = false resetCurrentLevel() }, ) }, terminalContent = { TerminalPane( output = output, inputFieldVersion = inputFieldVersion, commandInput = commandInput, showInputHint = false, onInputHintDismiss = { showTerminalInputHint = false showHelpOverlay = false }, onValueChange = { val blockedEcho = suppressedImeEcho if (blockedEcho != null && commandInput.text.isEmpty()) { val blockedTrimmed = blockedEcho.trim() if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) { return@TerminalPane } } suppressedImeEcho = null if (it.text.isNotEmpty()) { showTerminalInputHint = false showExerciseDescriptionHint = false showHelpOverlay = false } commandInput = it }, onRun = { runCommand() }, onTab = { tabComplete() }, onHelp = { showCommandHelp() }, onCursorLeft = { moveCursor(-1) }, onCursorRight = { moveCursor(1) }, onHistoryUp = { historyUp() }, onHistoryDown = { historyDown() }, onPromptBoundsChanged = { promptBounds = it }, ) }, ) } } if (showHelpOverlay) { HelpCalloutOverlay( showHelpOnStart = showHelpOnStart, onShowHelpOnStartChange = { showHelpOnStart = it }, onOk = { showHelpOverlay = false scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) } }, onClose = { showHelpOverlay = false }, exerciseBounds = exerciseBounds, promptBounds = promptBounds, ) } } } } editorState?.let { state -> TextEditorDialog( state = state, onContentChange = { editorState = state.copy(content = it) }, onSaveAsPathChange = { editorState = state.copy(saveAsPath = it) }, onClose = { output = output + "Editor closed without saving" editorState = null }, onSave = { saveEditor() }, ) } gitMessageEditorState?.let { state -> GitMessageEditorDialog( state = state, onContentChange = { gitMessageEditorState = state.copy(content = it) }, onClose = { output = output + "Git editor closed without saving" gitMessageEditorState = null }, onSave = { saveGitMessageEditor() }, ) } manPageState?.let { state -> ManPageDialog( state = state, onClose = { manPageState = null }, ) } if (showDiagnosticDialog) { DiagnosticSkinDialog( diagnosticSkinEnabled = diagnosticSkinEnabled, onDiagnosticSkinEnabledChange = { diagnosticSkinEnabled = it }, onDismiss = { showDiagnosticDialog = false }, ) } solvedCelebrationTitle?.let { title -> SolvedCelebrationOverlay(title = title) } } }