@@ -10,6 +10,7 @@ import androidx.activity.compose.BackHandler
1010import androidx.compose.animation.AnimatedVisibility
1111import androidx.compose.animation.fadeIn
1212import androidx.compose.animation.fadeOut
13+ import androidx.compose.foundation.BorderStroke
1314import androidx.compose.foundation.background
1415import androidx.compose.foundation.layout.*
1516import androidx.compose.foundation.rememberScrollState
@@ -20,6 +21,8 @@ import androidx.compose.material.icons.Icons
2021import androidx.compose.material.icons.automirrored.filled.ArrowBack
2122import androidx.compose.material.icons.filled.Add
2223import androidx.compose.material.icons.filled.Close
24+ import androidx.compose.material.icons.filled.Keyboard
25+ import androidx.compose.material.icons.filled.KeyboardHide
2326import androidx.compose.material3.*
2427import androidx.compose.runtime.*
2528import androidx.compose.ui.Alignment
@@ -371,6 +374,13 @@ private fun TerminalTabView(
371374 // Terminal-only dark mode: renders the terminal page dark even when the rest
372375 // of the app follows the light theme. Read once at entry; re-enter to apply.
373376 val terminalDarkTheme = remember { prefsManager.terminalDarkTheme }
377+ // When taps no longer raise the keyboard, the button on the terminal is the
378+ // only way to get it back, so both are read together at entry.
379+ val tapOpensKeyboard = remember { prefsManager.terminalTapKeyboard }
380+ val keyboardButtonLeft = remember { prefsManager.terminalKeyboardLeft }
381+ // Holds this tab's client so the button acts on the terminal it sits on,
382+ // rather than on whichever view TerminalScreenState last saw.
383+ var backEnd by remember { mutableStateOf<TerminalBackEnd ?>(null ) }
374384 // Termux TerminalColors indices: 256 = default foreground, 257 = background,
375385 // 258 = cursor. Dark mode uses the classic termux white-on-black scheme:
376386 // pure white foreground on pure black background.
@@ -392,68 +402,85 @@ private fun TerminalTabView(
392402 ) {
393403 Column (modifier = Modifier .fillMaxSize()) {
394404
395- AndroidView (
396- factory = { ctx ->
397- TerminalView (ctx, null ).apply {
398- TerminalScreenState .terminalView = WeakReference (this )
399- setTextSize(fontSizePx) // must run first, initializes mRenderer
400- setTypeface(terminalTypeface) // JetBrains Mono; null = system default
401- keepScreenOn = true
402- isFocusableInTouchMode = true
403- // The renderer only paints cell backgrounds; the full-screen
404- // default background comes from the View itself.
405- if (terminalDarkTheme) {
406- setBackgroundColor(terminalBackground)
407- }
405+ // The button rides inside the terminal's own Box, which is what keeps
406+ // it clear of the virtual keys row without hardcoding its height.
407+ Box (modifier = Modifier .fillMaxWidth().weight(1f )) {
408+ AndroidView (
409+ factory = { ctx ->
410+ TerminalView (ctx, null ).apply {
411+ TerminalScreenState .terminalView = WeakReference (this )
412+ setTextSize(fontSizePx) // must run first, initializes mRenderer
413+ setTypeface(terminalTypeface) // JetBrains Mono; null = system default
414+ keepScreenOn = true
415+ isFocusableInTouchMode = true
416+ // The renderer only paints cell backgrounds; the full-screen
417+ // default background comes from the View itself.
418+ if (terminalDarkTheme) {
419+ setBackgroundColor(terminalBackground)
420+ }
408421
409- if (activity != null ) {
410- val client = TerminalBackEnd (
411- terminal = this ,
412- activity = activity,
413- initialFontSizePx = fontSizePx,
414- onSessionFinished = onSessionFinished,
415- onFontSizeChanged = { newSize ->
416- TerminalSessionService .globalSessionList[tab.id]?.let { info ->
417- TerminalSessionService .globalSessionList[tab.id] = info.copy(fontSizePx = newSize)
418- }
419- },
420- )
421- val session: TerminalSession =
422- binder.getSession(tab.id) ? : binder.createSession(
423- containerName = containerName,
424- client = client,
425- containerUser = tab.user,
426- sessionId = tab.id,
422+ if (activity != null ) {
423+ val client = TerminalBackEnd (
424+ terminal = this ,
425+ activity = activity,
426+ initialFontSizePx = fontSizePx,
427+ onSessionFinished = onSessionFinished,
428+ onFontSizeChanged = { newSize ->
429+ TerminalSessionService .globalSessionList[tab.id]?.let { info ->
430+ TerminalSessionService .globalSessionList[tab.id] = info.copy(fontSizePx = newSize)
431+ }
432+ },
427433 )
428- session.updateTerminalSessionClient(client)
429- attachSession(session)
430- setTerminalViewClient(client)
431- }
434+ val session: TerminalSession =
435+ binder.getSession(tab.id) ? : binder.createSession(
436+ containerName = containerName,
437+ client = client,
438+ containerUser = tab.user,
439+ sessionId = tab.id,
440+ )
441+ session.updateTerminalSessionClient(client)
442+ attachSession(session)
443+ setTerminalViewClient(client)
444+ backEnd = client
445+ }
432446
433- post {
434- requestFocus()
435- (mClient as ? TerminalBackEnd )?.activate12KeyInputMethodIfNeeded()
436- mEmulator?.mColors?.mCurrentColors?.apply {
437- set(256 , terminalForeground)
438- set(258 , terminalForeground)
439- if (terminalDarkTheme) {
440- set(257 , terminalBackground)
447+ post {
448+ requestFocus()
449+ (mClient as ? TerminalBackEnd )?.activate12KeyInputMethodIfNeeded()
450+ mEmulator?.mColors?.mCurrentColors?.apply {
451+ set(256 , terminalForeground)
452+ set(258 , terminalForeground)
453+ if (terminalDarkTheme) {
454+ set(257 , terminalBackground)
455+ }
441456 }
442457 }
443458 }
444- }
445- },
446- update = { tv ->
447- if (isVisible) {
448- // Re-apply before setTextSize: termux renderer resets typeface on size changes
449- tv.setTypeface(terminalTypeface)
450- tv.setTextSize(fontSizePx)
451- tv.onScreenUpdated()
452- TerminalScreenState .terminalView = WeakReference (tv)
453- }
454- },
455- modifier = Modifier .fillMaxWidth().weight(1f )
456- )
459+ },
460+ update = { tv ->
461+ if (isVisible) {
462+ // Re-apply before setTextSize: termux renderer resets typeface on size changes
463+ tv.setTypeface(terminalTypeface)
464+ tv.setTextSize(fontSizePx)
465+ tv.onScreenUpdated()
466+ TerminalScreenState .terminalView = WeakReference (tv)
467+ }
468+ },
469+ modifier = Modifier .fillMaxSize()
470+ )
471+
472+ if (! tapOpensKeyboard) {
473+ KeyboardToggleButton (
474+ darkTheme = terminalDarkTheme,
475+ onToggle = { imeVisible ->
476+ if (imeVisible) backEnd?.hideSoftInput() else backEnd?.showSoftInput()
477+ },
478+ modifier = Modifier
479+ .align(if (keyboardButtonLeft) Alignment .BottomStart else Alignment .BottomEnd )
480+ .padding(16 .dp)
481+ )
482+ }
483+ }
457484
458485 AndroidView (
459486 factory = { ctx ->
@@ -485,6 +512,47 @@ private fun TerminalTabView(
485512 }
486513}
487514
515+ /* *
516+ * Manual keyboard control for when taps are handed to the terminal instead. It
517+ * toggles, so the same button also puts the keyboard away.
518+ */
519+ @OptIn(ExperimentalLayoutApi ::class )
520+ @Composable
521+ private fun KeyboardToggleButton (
522+ darkTheme : Boolean ,
523+ onToggle : (Boolean ) -> Unit ,
524+ modifier : Modifier = Modifier ,
525+ ) {
526+ val context = LocalContext .current
527+ val imeVisible = WindowInsets .isImeVisible
528+ // Literals for the same reason virtualKeysBackground above uses them, and the
529+ // same decided exception in DESIGN.md: this sits on the terminal's own black,
530+ // so it has to read dark even when the app itself is in the light theme.
531+ val fill = if (darkTheme) Color (0xFF1A1A1E ) else MaterialTheme .colorScheme.surfaceContainerHighest
532+ val accent = if (darkTheme) Color .White else MaterialTheme .colorScheme.onSurface
533+ val edge = if (darkTheme) Color .White .copy(alpha = 0.15f )
534+ else MaterialTheme .colorScheme.outlineVariant.copy(alpha = 0.35f )
535+
536+ Surface (
537+ onClick = { onToggle(imeVisible) },
538+ modifier = modifier.size(56 .dp),
539+ shape = RoundedCornerShape (16 .dp),
540+ // Slightly transparent so it never fully hides a line of output under it
541+ color = fill.copy(alpha = 0.9f ),
542+ border = BorderStroke (1 .dp, edge),
543+ tonalElevation = 0 .dp
544+ ) {
545+ Box (Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
546+ Icon (
547+ imageVector = if (imeVisible) Icons .Default .KeyboardHide else Icons .Default .Keyboard ,
548+ contentDescription = context.getString(R .string.terminal_toggle_keyboard),
549+ tint = accent,
550+ modifier = Modifier .size(28 .dp)
551+ )
552+ }
553+ }
554+ }
555+
488556@Composable
489557private fun UserPickerDialog (
490558 users : List <String >,
0 commit comments