- Split core game models from sandbox engine, native level setup, and repo state saving. - Move app overlays and completion helpers out of GitHugApp. - Extract the terminal special key bar into its own component.
630 lines
29 KiB
Kotlin
630 lines
29 KiB
Kotlin
package solutions.tretter.githugandroid
|
|
|
|
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.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.imePadding
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.rememberScrollState
|
|
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.ui.Modifier
|
|
import androidx.compose.ui.geometry.Rect
|
|
import androidx.compose.ui.platform.LocalConfiguration
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.text.TextRange
|
|
import androidx.compose.ui.text.input.TextFieldValue
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.layout.boundsInRoot
|
|
import androidx.compose.ui.layout.onGloballyPositioned
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
|
|
@Composable
|
|
fun GitHugApp() {
|
|
MaterialTheme(colorScheme = GitHugColorScheme) {
|
|
val context = LocalContext.current
|
|
val configuration = LocalConfiguration.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<String?>(null) }
|
|
var output by remember { mutableStateOf(listOf(runtime.startupBanner())) }
|
|
var hintIndex by remember { mutableStateOf(0) }
|
|
var activeExerciseDetail by remember { mutableStateOf<ExerciseDetailPanel?>(null) }
|
|
var visibleHint by remember { mutableStateOf<String?>(null) }
|
|
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("") }
|
|
var hasRestoredProgress by remember { mutableStateOf(false) }
|
|
var editorState by remember { mutableStateOf<TextEditorState?>(null) }
|
|
var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) }
|
|
var manPageState by remember { mutableStateOf<ManPageState?>(null) }
|
|
var solvedCelebrationTitle by remember { mutableStateOf<String?>(null) }
|
|
var showTerminalInputHint by remember { mutableStateOf(true) }
|
|
var showExerciseDescriptionHint by remember { mutableStateOf(true) }
|
|
var showHelpOverlay by remember { mutableStateOf(false) }
|
|
var showHelpOnStart by remember { mutableStateOf(true) }
|
|
var helpPreferenceInitialized by remember { mutableStateOf(false) }
|
|
var workspaceBounds by remember { mutableStateOf<Rect?>(null) }
|
|
var exerciseBounds by remember { mutableStateOf<Rect?>(null) }
|
|
var promptBounds by remember { mutableStateOf<Rect?>(null) }
|
|
val currentLevel = levels[currentLevelIndex]
|
|
|
|
LaunchedEffect(solvedCelebrationTitle) {
|
|
if (solvedCelebrationTitle != null) {
|
|
delay(3_000)
|
|
solvedCelebrationTitle = null
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(persistedPaneLayout) {
|
|
paneLayout = persistedPaneLayout
|
|
}
|
|
|
|
LaunchedEffect(currentLevelIndex) {
|
|
delay(100)
|
|
screenScrollState.animateScrollTo(0)
|
|
}
|
|
|
|
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<String>, 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) {
|
|
updatePaneLayout(
|
|
transform = { layout ->
|
|
layout.copy(
|
|
weights = layout.weights + recommendedPaneWeights(
|
|
heights = recommendedPaneHeights(
|
|
level = currentLevel,
|
|
levelCount = levels.size,
|
|
outputLineCount = output.size,
|
|
screenHeightDp = screenHeightDp,
|
|
suggestionsVisible = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
|
|
visibleHint = visibleHint,
|
|
),
|
|
)
|
|
)
|
|
},
|
|
persist = persist,
|
|
)
|
|
}
|
|
|
|
fun resetCurrentLevel(message: String = "Level reset.") {
|
|
val resetLevelId = currentLevel.id
|
|
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)
|
|
}
|
|
|
|
fun loadLevel(index: Int, message: List<String> = listOf("Loaded level: ${levels[index].title}")) {
|
|
AppLog.d("GitHugApp", "Loading level index=$index id=${levels[index].id} title=${levels[index].title}")
|
|
currentLevelIndex = index
|
|
repo = runtime.prepareLevel(levels[index])
|
|
output = message
|
|
clearCommandInput()
|
|
suppressedImeEcho = null
|
|
hintIndex = 0
|
|
activeExerciseDetail = null
|
|
visibleHint = null
|
|
historyIndex = -1
|
|
historyDraft = ""
|
|
editorState = null
|
|
gitMessageEditorState = null
|
|
manPageState = null
|
|
paneLayout = paneLayout.copy(
|
|
weights = paneLayout.weights + recommendedPaneWeights(
|
|
heights = recommendedPaneHeights(
|
|
level = levels[index],
|
|
levelCount = levels.size,
|
|
outputLineCount = 1,
|
|
screenHeightDp = screenHeightDp,
|
|
suggestionsVisible = false,
|
|
visibleHint = null,
|
|
),
|
|
)
|
|
)
|
|
}
|
|
|
|
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 = candidates.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 applyCommandResult(raw: String, newRepo: RepoState, lines: List<String>, echoCommand: Boolean) {
|
|
val levelForResult = currentLevel
|
|
val solvedAfterCommand = levelForResult.validator(newRepo, raw)
|
|
val wasAlreadyCompleted = currentLevel.id in completedLevels
|
|
AppLog.d(
|
|
"GitHugApp",
|
|
"Command='$raw' level=${levelForResult.id} solved=$solvedAfterCommand alreadyCompleted=$wasAlreadyCompleted completedBefore=${completedLevels.sorted()}",
|
|
)
|
|
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())
|
|
} else {
|
|
AppLog.d("GitHugApp", "All levels completed")
|
|
scope.launch { persistProgress(updatedCompletedLevels, levels.lastOrNull()?.id) }
|
|
repo = newRepo
|
|
output = newOutput + listOf("🏁 All Githug levels completed.")
|
|
clearCommandInput()
|
|
suppressedImeEcho = null
|
|
}
|
|
} else {
|
|
AppLog.d("GitHugApp", "Staying on level=${levelForResult.id}")
|
|
repo = newRepo
|
|
output = newOutput
|
|
applyRecommendedPaneWeights(persist = false)
|
|
}
|
|
}
|
|
|
|
fun openEditor(invocation: VisualEditorInvocation) {
|
|
val path = invocation.path.orEmpty()
|
|
val (content, lines) = if (path.isBlank()) {
|
|
"" to emptyList()
|
|
} else {
|
|
runtime.readEditorFile(currentLevel, repo, path)
|
|
}
|
|
output = buildList {
|
|
addAll(output)
|
|
add("$ ${invocation.command}")
|
|
addAll(lines)
|
|
if (lines.isEmpty()) {
|
|
add("Opened ${invocation.editor} editor${if (path.isBlank()) "" else " for $path"}")
|
|
}
|
|
}
|
|
if (lines.isEmpty()) {
|
|
editorState = TextEditorState(
|
|
editor = invocation.editor,
|
|
originalCommand = invocation.command,
|
|
path = path,
|
|
content = content,
|
|
saveAsPath = path,
|
|
)
|
|
}
|
|
applyRecommendedPaneWeights(persist = false)
|
|
}
|
|
|
|
fun openManPage(invocation: GitHelpInvocation) {
|
|
val content = runtime.gitManPage(currentLevel, repo, invocation.topic)
|
|
output = buildList {
|
|
addAll(output)
|
|
add("$ ${invocation.command}")
|
|
add("Opened git help viewer for ${invocation.topic}")
|
|
}
|
|
manPageState = ManPageState(topic = invocation.topic, content = content)
|
|
applyRecommendedPaneWeights(persist = false)
|
|
}
|
|
|
|
fun openGitMessageEditor(invocation: GitEditorInvocation) {
|
|
output = buildList {
|
|
addAll(output)
|
|
add("$ ${invocation.command}")
|
|
add("Opened Git message editor")
|
|
}
|
|
gitMessageEditorState = GitMessageEditorState(
|
|
invocation = invocation,
|
|
content = invocation.initialContent,
|
|
)
|
|
applyRecommendedPaneWeights(persist = false)
|
|
}
|
|
|
|
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 (newRepo, lines) = runtime.executeGitEditorCommand(
|
|
level = currentLevel,
|
|
currentRepo = repo,
|
|
invocation = state.invocation,
|
|
message = state.content,
|
|
)
|
|
gitMessageEditorState = null
|
|
applyCommandResult(state.invocation.command, newRepo, lines, echoCommand = false)
|
|
}
|
|
|
|
fun runCommand() {
|
|
val submittedText = commandInput.text
|
|
val raw = submittedText.trim()
|
|
if (raw.isBlank()) return
|
|
|
|
showHelpOverlay = false
|
|
showTerminalInputHint = false
|
|
showExerciseDescriptionHint = false
|
|
suppressedImeEcho = submittedText
|
|
clearCommandInput(recreateField = true)
|
|
|
|
if (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 = true)
|
|
}
|
|
|
|
fun showCommandHelp() {
|
|
output = output + runtime.commandReferenceLines()
|
|
}
|
|
|
|
Scaffold(containerColor = AppBackground) { padding ->
|
|
Surface(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(padding),
|
|
color = AppBackground,
|
|
) {
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(AppBackground)
|
|
.imePadding()
|
|
.verticalScroll(screenScrollState)
|
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
) {
|
|
FixedHeader()
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.onGloballyPositioned { workspaceBounds = it.boundsInRoot() },
|
|
) {
|
|
PaneWorkspace(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
paneLayout = paneLayout,
|
|
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) }
|
|
},
|
|
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 },
|
|
workspaceBounds = workspaceBounds,
|
|
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 },
|
|
)
|
|
}
|
|
|
|
solvedCelebrationTitle?.let { title ->
|
|
SolvedCelebrationOverlay(title = title)
|
|
}
|
|
}
|
|
}
|