package solutions.tretter.githugandroid import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.SolidColor import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @OptIn(ExperimentalFoundationApi::class) @Composable fun TerminalPane( output: List, inputFieldVersion: Int, commandInput: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, onRun: () -> Unit, onTab: () -> Unit, onHelp: () -> Unit, onCursorLeft: () -> Unit, onCursorRight: () -> Unit, onHistoryUp: () -> Unit, onHistoryDown: () -> Unit, ) { val configuration = LocalConfiguration.current val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current val outputHorizontalScroll = rememberScrollState() val outputVerticalScroll = rememberScrollState() val terminalMinWidth = 80.dp * 7.2f val terminalPaneMaxHeight = configuration.screenHeightDp.dp * 0.7f val reservedControlsHeight = 86.dp val maxOutputHeight = (terminalPaneMaxHeight - reservedControlsHeight).coerceAtLeast(120.dp) val outputLineHeight = 20.dp val desiredOutputHeight = (output.size.coerceAtLeast(6) * outputLineHeight.value).dp.coerceAtMost(maxOutputHeight) LaunchedEffect(inputFieldVersion) { outputHorizontalScroll.scrollTo(0) } LaunchedEffect(output.size) { outputVerticalScroll.animateScrollTo(outputVerticalScroll.maxValue) } fun submitCommand() { focusManager.clearFocus(force = true) keyboardController?.hide() onRun() } fun insertLiteral(value: String) { val selectionStart = commandInput.selection.start.coerceIn(0, commandInput.text.length) val selectionEnd = commandInput.selection.end.coerceIn(0, commandInput.text.length) val rangeStart = minOf(selectionStart, selectionEnd) val rangeEnd = maxOf(selectionStart, selectionEnd) val newText = commandInput.text.replaceRange(rangeStart, rangeEnd, value) val newCursor = rangeStart + value.length onValueChange(TextFieldValue(newText, selection = TextRange(newCursor))) } Box( modifier = Modifier .fillMaxSize() .background(TerminalBackground, RoundedCornerShape(12.dp)) .padding(8.dp) ) { Column( modifier = Modifier .fillMaxHeight() .widthIn(min = terminalMinWidth), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Box( modifier = Modifier .heightIn(min = 200.dp, max = maxOutputHeight) .height(desiredOutputHeight) .fillMaxWidth() .background(PanelPrimary, RoundedCornerShape(10.dp)) .padding(8.dp) .horizontalScroll(outputHorizontalScroll) .verticalScroll(outputVerticalScroll), ) { SelectionContainer { Column( modifier = Modifier.widthIn(min = terminalMinWidth), verticalArrangement = Arrangement.spacedBy(4.dp), ) { output.forEach { line -> Text( text = line, color = if (line.startsWith("✔") || line.startsWith("🏁")) Success else TextSecondary, fontFamily = FontFamily.Monospace, softWrap = false, ) } } } } SpecialKeyBar( onTab = onTab, onHelp = onHelp, onCursorLeft = onCursorLeft, onCursorRight = onCursorRight, onHistoryUp = onHistoryUp, onHistoryDown = onHistoryDown, onInsertText = { insertLiteral(it) }, ) Row( modifier = Modifier .fillMaxWidth() .widthIn(min = terminalMinWidth) .background(PanelPrimary, RoundedCornerShape(8.dp)) .padding(horizontal = 8.dp, vertical = 3.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = "$", color = Accent, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 15.sp, ) TerminalInputField( inputFieldVersion = inputFieldVersion, commandInput = commandInput, onValueChange = onValueChange, focusRequester = focusRequester, keyboardController = keyboardController, onSubmit = { submitCommand() }, ) } } } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun RowScope.TerminalInputField( inputFieldVersion: Int, commandInput: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, focusRequester: FocusRequester, keyboardController: androidx.compose.ui.platform.SoftwareKeyboardController?, onSubmit: () -> Unit, ) { key(inputFieldVersion) { BasicTextField( value = commandInput, onValueChange = onValueChange, modifier = Modifier .weight(1f) .focusRequester(focusRequester) .onFocusChanged { state -> if (state.isFocused) { keyboardController?.show() } }, singleLine = true, textStyle = TextStyle( color = TextPrimary, fontFamily = FontFamily.Monospace, fontSize = 15.sp, fontWeight = FontWeight.Normal, ), cursorBrush = SolidColor(Accent), keyboardOptions = KeyboardOptions( autoCorrect = false, keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions(onDone = { onSubmit() }), decorationBox = { innerTextField -> Box( modifier = Modifier .fillMaxWidth() .background(Color.Transparent) .padding(vertical = 2.dp), contentAlignment = Alignment.CenterStart, ) { if (commandInput.text.isEmpty()) { Text( text = "Enter git command", color = TextMuted.copy(alpha = 0.7f), fontFamily = FontFamily.Monospace, fontSize = 15.sp, ) } innerTextField() } }, ) } } @Composable private fun SpecialKeyBar( onTab: () -> Unit, onHelp: () -> Unit, onCursorLeft: () -> Unit, onCursorRight: () -> Unit, onHistoryUp: () -> Unit, onHistoryDown: () -> Unit, onInsertText: (String) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(1.dp), ) { TerminalKeyButton(label = "?", onClick = onHelp, highlight = true, fontFamily = FontFamily.Default) TerminalKeyButton(label = "↹", onClick = onTab, fontFamily = FontFamily.Default) TerminalKeyButton(label = ".", onClick = { onInsertText(".") }) TerminalKeyButton(label = "-", onClick = { onInsertText("-") }) TerminalKeyButton(label = "/", onClick = { onInsertText("/") }) TerminalKeyButton(label = "←", onClick = onCursorLeft) TerminalKeyButton(label = "→", onClick = onCursorRight) TerminalKeyButton(label = "↑", onClick = onHistoryUp) TerminalKeyButton(label = "↓", onClick = onHistoryDown) } } @Composable private fun TerminalKeyButton( label: String, onClick: () -> Unit, highlight: Boolean = false, fontFamily: FontFamily = FontFamily.Monospace, ) { Button( onClick = onClick, modifier = Modifier .width(32.dp) .height(26.dp), shape = RoundedCornerShape(0.dp), contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp), colors = ButtonDefaults.buttonColors( containerColor = if (highlight) Color(0xFF1976D2) else PanelSecondary, contentColor = TextPrimary, ), ) { Text( text = label, fontFamily = fontFamily, fontWeight = FontWeight.Bold, fontSize = 11.sp, ) } }