From 8984959452a5aa7e5657ca969a1808fd67787847 Mon Sep 17 00:00:00 2001 From: bhoffman20 Date: Mon, 13 Jul 2026 16:10:37 -0500 Subject: [PATCH 1/3] feat: customizable accessory keys and a termux-style bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the accessory bar on top of the grouped-keys model from #55. The chuchu, files, and settings buttons were fixed trailing buttons that could not be moved or removed. They are ordinary catalog entries now, so they take part in the layout editor like any other key. SettingsRepository backfills them once into layouts saved before the change, since those layouts do not list them and the keys would otherwise vanish on upgrade — taking the only route to settings with them. Add a termux-style bar behind a setting. Where the default bar draws bordered, content-sized keys that wrap across two rows, the termux style divides the width into equal, borderless columns so keys line up on a grid. Labels shrink to fit, since those keys are sized off the bar width rather than their text. Groups keep working in both styles. --- .../data/repository/SettingsRepository.kt | 40 +- .../chuchu/ui/ApplicationNavController.kt | 3 + .../ui/screens/Settings/SettingsScreen.kt | 4 + .../Settings/TerminalAccessorySettings.kt | 25 + .../ui/screens/Terminal/CommandPalette.kt | 8 +- .../ui/screens/Terminal/TerminalScreen.kt | 78 ++- .../ui/terminal/KeyboardAccessoryBar.kt | 492 ++++++++++-------- .../chuchu/ui/terminal/TerminalAccessory.kt | 50 ++ .../chuchu/ui/terminal/AccessoryLogicTest.kt | 83 +++ 9 files changed, 541 insertions(+), 242 deletions(-) diff --git a/android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt b/android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt index 1c8f4d1b..493f25bf 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt @@ -37,6 +37,9 @@ class SettingsRepository(context: Context) { private val _accessoryBarSingleRow = MutableStateFlow(prefs.getBoolean(KEY_ACCESSORY_BAR_SINGLE_ROW, false)) val accessoryBarSingleRow: StateFlow = _accessoryBarSingleRow.asStateFlow() + private val _termuxStyleAccessoryBar = MutableStateFlow(prefs.getBoolean(KEY_TERMUX_STYLE_ACCESSORY_BAR, false)) + val termuxStyleAccessoryBar: StateFlow = _termuxStyleAccessoryBar.asStateFlow() + private val _appLockEnabled = MutableStateFlow(prefs.getBoolean(KEY_APP_LOCK_ENABLED, false)) val appLockEnabled: StateFlow = _appLockEnabled.asStateFlow() @@ -93,6 +96,11 @@ class SettingsRepository(context: Context) { _accessoryBarSingleRow.value = enabled } + fun setTermuxStyleAccessoryBar(enabled: Boolean) { + prefs.edit().putBoolean(KEY_TERMUX_STYLE_ACCESSORY_BAR, enabled).apply() + _termuxStyleAccessoryBar.value = enabled + } + fun setTerminalTabMode(mode: TerminalTabMode) { prefs.edit().putString(KEY_TAB_MODE, mode.name).apply() _terminalTabMode.value = mode @@ -144,13 +152,38 @@ class SettingsRepository(context: Context) { private fun loadAccessoryLayoutIds(): List { val stored = prefs.getString(KEY_ACCESSORY_LAYOUT, null) - ?: return TerminalAccessoryLayoutStore.defaultLayoutIds() + if (stored == null) { + markScreenActionsBackfilled() + return TerminalAccessoryLayoutStore.defaultLayoutIds() + } if (stored.isBlank()) { + markScreenActionsBackfilled() return emptyList() } - return TerminalAccessoryLayoutStore.normalizeIds( + val ids = TerminalAccessoryLayoutStore.normalizeIds( stored.split(',').map(String::trim).filter(String::isNotEmpty), ) + return backfillScreenActions(ids) + } + + /** + * Runs the screen-action backfill once, then records that it happened so a user who later + * removes those keys on purpose does not get them back on the next launch. + */ + private fun backfillScreenActions(ids: List): List { + if (prefs.getBoolean(KEY_ACCESSORY_SCREEN_ACTIONS_BACKFILLED, false)) { + return ids + } + val migrated = TerminalAccessoryLayoutStore.backfillScreenActions(ids) + prefs.edit() + .putString(KEY_ACCESSORY_LAYOUT, migrated.joinToString(separator = ",")) + .putBoolean(KEY_ACCESSORY_SCREEN_ACTIONS_BACKFILLED, true) + .apply() + return migrated + } + + private fun markScreenActionsBackfilled() { + prefs.edit().putBoolean(KEY_ACCESSORY_SCREEN_ACTIONS_BACKFILLED, true).apply() } private fun loadTerminalCustomKeyGroups(): List { @@ -164,6 +197,9 @@ class SettingsRepository(context: Context) { private const val KEY_ACCESSORY_LAYOUT = "terminal_accessory_layout" private const val KEY_TERMINAL_CUSTOM_ACTIONS = "terminal_custom_actions" private const val KEY_ACCESSORY_BAR_SINGLE_ROW = "terminal_accessory_bar_single_row" + private const val KEY_TERMUX_STYLE_ACCESSORY_BAR = "terminal_termux_style_accessory_bar" + private const val KEY_ACCESSORY_SCREEN_ACTIONS_BACKFILLED = + "terminal_accessory_screen_actions_backfilled" private const val KEY_TAB_MODE = "terminal_tab_mode" private const val KEY_APP_LOCK_ENABLED = "app_lock_enabled" private const val KEY_REQUIRE_AUTH_ON_CONNECT = "require_auth_on_connect" diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt index 45096d71..5b91a752 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt @@ -122,6 +122,7 @@ fun ApplicationNavController() { val requireAuthOnConnect by settingsRepo.requireAuthOnConnect.collectAsStateWithLifecycle() val accessoryLayoutIds by settingsRepo.accessoryLayoutIds.collectAsStateWithLifecycle() val accessoryBarSingleRow by settingsRepo.accessoryBarSingleRow.collectAsStateWithLifecycle() + val termuxStyleAccessoryBar by settingsRepo.termuxStyleAccessoryBar.collectAsStateWithLifecycle() val customKeyGroups by settingsRepo.terminalCustomKeyGroups.collectAsStateWithLifecycle() val tabMode by settingsRepo.terminalTabMode.collectAsStateWithLifecycle() val localShellEnabled by settingsRepo.localShellEnabled.collectAsStateWithLifecycle() @@ -136,6 +137,7 @@ fun ApplicationNavController() { localShellEnabled = localShellEnabled, currentAccessoryLayoutIds = accessoryLayoutIds, accessoryBarSingleRow = accessoryBarSingleRow, + termuxStyleAccessoryBar = termuxStyleAccessoryBar, currentTerminalCustomKeyGroups = customKeyGroups, currentTabMode = tabMode, onTabModeChanged = settingsRepo::setTerminalTabMode, @@ -150,6 +152,7 @@ fun ApplicationNavController() { onLocalShellEnabledChanged = settingsRepo::setLocalShellEnabled, onAccessoryLayoutChanged = settingsRepo::setAccessoryLayoutIds, onAccessoryBarSingleRowChanged = settingsRepo::setAccessoryBarSingleRow, + onTermuxStyleAccessoryBarChanged = settingsRepo::setTermuxStyleAccessoryBar, currentTerminalFontSize = terminalFontSize, onTerminalFontSizeChanged = settingsRepo::setTerminalFontSize, onTerminalCustomActionsChanged = settingsRepo::setTerminalCustomKeyGroups, diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt index c330c1cd..d40f6256 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt @@ -48,6 +48,7 @@ fun SettingsScreen( localShellEnabled: Boolean, currentAccessoryLayoutIds: List, accessoryBarSingleRow: Boolean, + termuxStyleAccessoryBar: Boolean, currentTerminalCustomKeyGroups: List, currentTabMode: TerminalTabMode = TerminalTabMode.Classic, onTabModeChanged: (TerminalTabMode) -> Unit = {}, @@ -62,6 +63,7 @@ fun SettingsScreen( onLocalShellEnabledChanged: (Boolean) -> Unit, onAccessoryLayoutChanged: (List) -> Unit, onAccessoryBarSingleRowChanged: (Boolean) -> Unit, + onTermuxStyleAccessoryBarChanged: (Boolean) -> Unit, currentTerminalFontSize: Float = 14f, onTerminalFontSizeChanged: (Float) -> Unit = {}, onTerminalCustomActionsChanged: (List) -> Unit, @@ -169,6 +171,8 @@ fun SettingsScreen( onEditAccessoryLayout = { showAccessoryEditor = true }, accessoryBarSingleRow = accessoryBarSingleRow, onAccessoryBarSingleRowChanged = onAccessoryBarSingleRowChanged, + termuxStyleAccessoryBar = termuxStyleAccessoryBar, + onTermuxStyleAccessoryBarChanged = onTermuxStyleAccessoryBarChanged, currentTerminalFontSize = currentTerminalFontSize, onTerminalFontSizeChanged = onTerminalFontSizeChanged, currentTerminalCustomKeyGroups = currentTerminalCustomKeyGroups, diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt index 19de7c07..616de9c5 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt @@ -78,6 +78,8 @@ internal fun TerminalSettings( onEditAccessoryLayout: () -> Unit, accessoryBarSingleRow: Boolean, onAccessoryBarSingleRowChanged: (Boolean) -> Unit, + termuxStyleAccessoryBar: Boolean, + onTermuxStyleAccessoryBarChanged: (Boolean) -> Unit, currentTerminalFontSize: Float = 14f, onTerminalFontSizeChanged: (Float) -> Unit = {}, currentTerminalCustomKeyGroups: List, @@ -331,6 +333,28 @@ internal fun TerminalSettings( ) } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + ChuText("termux-style accessory bar", style = typography.label) + ChuText( + "equal-width keys, no borders, tighter spacing", + style = typography.bodySmall, + color = colors.textMuted, + ) + } + ChuSwitch( + checked = termuxStyleAccessoryBar, + onCheckedChange = onTermuxStyleAccessoryBarChanged, + ) + } + if (selectedEntries.isEmpty()) { ChuText( "choose the accessory keys you want in the terminal bar.", @@ -348,6 +372,7 @@ internal fun TerminalSettings( modifierState = ModifierState(), onAction = {}, useSingleRow = accessoryBarSingleRow, + termuxStyle = termuxStyleAccessoryBar, modifier = Modifier.fillMaxWidth(), ) } diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt index d9a1b131..aa5f95e9 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt @@ -62,11 +62,9 @@ fun CommandPalette( accessoryEntries: List, accessoryModifierState: ModifierState, onAccessoryAction: (AccessoryAction) -> Unit, - onChuchuKey: () -> Unit, chuchuKeyActive: Boolean, - onOpenFiles: () -> Unit, - onOpenSettings: () -> Unit, useSingleRowAccessoryBar: Boolean, + termuxStyleAccessoryBar: Boolean = false, onSelectTab: (String) -> Unit, onCloseTab: (String) -> Unit, onAddTab: () -> Unit, @@ -254,11 +252,9 @@ fun CommandPalette( entries = accessoryEntries, modifierState = accessoryModifierState, onAction = onAccessoryAction, - onSettings = onOpenSettings, - onChuchuKey = onChuchuKey, chuchuKeyActive = chuchuKeyActive, - onOpenFiles = onOpenFiles, useSingleRow = useSingleRowAccessoryBar, + termuxStyle = termuxStyleAccessoryBar, modifier = Modifier.align(Alignment.BottomCenter) .windowInsetsPadding(WindowInsets.safeDrawing) diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt index b20e7090..246c732f 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt @@ -289,6 +289,7 @@ fun TerminalScreen( val tabMode by settingsRepo.terminalTabMode.collectAsStateWithLifecycle() val currentAccessoryLayoutIds by settingsRepo.accessoryLayoutIds.collectAsStateWithLifecycle() val useSingleRowAccessoryBar by settingsRepo.accessoryBarSingleRow.collectAsStateWithLifecycle() + val useTermuxStyleAccessoryBar by settingsRepo.termuxStyleAccessoryBar.collectAsStateWithLifecycle() val currentTerminalCustomKeyGroups by settingsRepo.terminalCustomKeyGroups.collectAsStateWithLifecycle() val settingsFontSize by settingsRepo.terminalFontSize.collectAsStateWithLifecycle() @@ -826,6 +827,26 @@ fun TerminalScreen( fun dispatchAccessoryAction(action: AccessoryAction) { + when (action) { + AccessoryAction.Settings -> { + onOpenSettings() + return + } + AccessoryAction.ChuchuKey -> { + chuchuKeys.togglePrefix() + requestInputFocus() + return + } + AccessoryAction.OpenFiles -> { + if (filesSupported) { + vm.selectConnectionTab(ConnectionTab.Files) + } else { + showLocalShellFilesUnsupported() + } + return + } + else -> Unit + } if ( action is AccessoryAction.SendText && chuchuKeys.handleText(action.text) ) { @@ -1502,7 +1523,10 @@ fun TerminalScreen( } } - Spacer(modifier = Modifier.height(6.dp)) + // The termux-style bar sits flush against the terminal, so it drops the gap. + if (!(selectedTab == ConnectionTab.Terminal && useTermuxStyleAccessoryBar)) { + Spacer(modifier = Modifier.height(6.dp)) + } if (selectedTab == ConnectionTab.Terminal) { if (activeHostCount > 1 && currentHostName != null) { Row( @@ -1560,21 +1584,14 @@ fun TerminalScreen( entries = accessoryLayout, modifierState = modifierState, onAction = ::dispatchAccessoryAction, - onSettings = onOpenSettings, - onChuchuKey = { - chuchuKeys.togglePrefix() - requestInputFocus() - }, chuchuKeyActive = chuchuKeys.isPrefixActive, - onOpenFiles = { - if (filesSupported) { - vm.selectConnectionTab(ConnectionTab.Files) - } else { - showLocalShellFilesUnsupported() - } - }, useSingleRow = useSingleRowAccessoryBar, - modifier = Modifier.padding(bottom = 2.dp), + termuxStyle = useTermuxStyleAccessoryBar, + modifier = if (useTermuxStyleAccessoryBar) { + Modifier + } else { + Modifier.padding(bottom = 2.dp) + }, ) } } @@ -1585,7 +1602,27 @@ fun TerminalScreen( UploadProgressDialog(progress = uploadProgress) } if (showTabSheet) { - val paletteAccessoryAction: (AccessoryAction) -> Unit = { action -> + val paletteAccessoryAction: (AccessoryAction) -> Unit = fun(action: AccessoryAction) { + when (action) { + AccessoryAction.Settings -> { + onOpenSettings() + return + } + AccessoryAction.ChuchuKey -> { + chuchuKeys.togglePrefix() + return + } + AccessoryAction.OpenFiles -> { + if (filesSupported) { + vm.selectConnectionTab(ConnectionTab.Files) + showTabSheet = false + } else { + showLocalShellFilesUnsupported() + } + return + } + else -> Unit + } if ( !(action is AccessoryAction.SendText && chuchuKeys.handleText(action.text)) @@ -1655,18 +1692,9 @@ fun TerminalScreen( accessoryEntries = accessoryLayout, accessoryModifierState = modifierState, onAccessoryAction = paletteAccessoryAction, - onChuchuKey = { chuchuKeys.togglePrefix() }, chuchuKeyActive = chuchuKeys.isPrefixActive, - onOpenFiles = { - if (filesSupported) { - vm.selectConnectionTab(ConnectionTab.Files) - showTabSheet = false - } else { - showLocalShellFilesUnsupported() - } - }, - onOpenSettings = onOpenSettings, useSingleRowAccessoryBar = useSingleRowAccessoryBar, + termuxStyleAccessoryBar = useTermuxStyleAccessoryBar, onSelectTab = { vm.selectTab(it) showTabSheet = false diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/KeyboardAccessoryBar.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/KeyboardAccessoryBar.kt index 818dcafe..07a49cce 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/KeyboardAccessoryBar.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/KeyboardAccessoryBar.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -29,8 +31,10 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription @@ -38,6 +42,9 @@ import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -46,59 +53,105 @@ import com.jossephus.chuchu.ui.components.ChuButtonSurface import com.jossephus.chuchu.ui.components.ChuButtonVariant import com.jossephus.chuchu.ui.components.ChuText import com.jossephus.chuchu.ui.theme.ChuColors -import com.jossephus.chuchu.ui.theme.ChuSymbolsFontFamily import com.jossephus.chuchu.ui.theme.ChuTypography import kotlinx.coroutines.withTimeoutOrNull private const val INITIAL_REPEAT_DELAY_MS = 400L private const val REPEAT_INTERVAL_MS = 50L +/** Keys visible at once before the termux-style single-row bar starts scrolling. */ +private const val TERMUX_SINGLE_ROW_KEYS_PER_SCREEN = 8 + +private const val MIN_LABEL_FONT_SIZE_SP = 8f +private const val LABEL_FONT_SIZE_STEP_SP = 0.5f + +private val ButtonHeight = 30.dp +private val DefaultButtonPadding = PaddingValues(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) +private val TermuxButtonPadding = PaddingValues(horizontal = 0.dp, vertical = 2.dp) + +/** + * The accessory key bar that sits above the keyboard. + * + * The default style draws bordered, content-sized keys that wrap across up to two rows. The termux + * style instead divides the bar into equal, borderless columns so keys line up on a grid, the way + * Termux's extra-keys row does. + */ @OptIn(ExperimentalLayoutApi::class) @Composable fun KeyboardAccessoryBar( entries: List, modifierState: ModifierState, onAction: (AccessoryAction) -> Unit, - onSettings: (() -> Unit)? = null, - onChuchuKey: (() -> Unit)? = null, chuchuKeyActive: Boolean = false, - onOpenFiles: (() -> Unit)? = null, useSingleRow: Boolean = false, + termuxStyle: Boolean = false, horizontalPadding: Dp = 8.dp, verticalPadding: Dp = 6.dp, modifier: Modifier = Modifier, ) { - val buttonHeight = 30.dp - val buttonPadding = PaddingValues(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) + val buttonPadding = if (termuxStyle) TermuxButtonPadding else DefaultButtonPadding + val keySpacing = if (termuxStyle) 0.dp else 6.dp + val barHorizontalPadding = if (termuxStyle) 0.dp else horizontalPadding + val barVerticalPadding = if (termuxStyle) 2.dp else verticalPadding + var expandedGroupId by remember { mutableStateOf(null) } - val onChildAction: (AccessoryAction) -> Unit = { action -> - onAction(action) + val toggleGroup: (String) -> Unit = { id -> + expandedGroupId = if (expandedGroupId == id) null else id } Column(modifier = modifier) { - expandedGroupId?.let { groupId -> - val expandedGroup = - entries.firstOrNull { it is ResolvedAccessoryEntry.Group && it.group.id == groupId } - as? ResolvedAccessoryEntry.Group - if (expandedGroup != null) { - GroupPopover( - group = expandedGroup.group, - modifierState = modifierState, - onAction = onChildAction, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - horizontalPadding = horizontalPadding, - ) - } + val expandedGroup = expandedGroupId?.let { groupId -> + entries.filterIsInstance() + .firstOrNull { it.group.id == groupId } + } + if (expandedGroup != null) { + GroupPopover( + group = expandedGroup.group, + modifierState = modifierState, + onAction = onAction, + chuchuKeyActive = chuchuKeyActive, + horizontalPadding = barHorizontalPadding, + ) } - if (useSingleRow) { - Row( + when { + useSingleRow && termuxStyle -> BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = barVerticalPadding), + ) { + // Size keys off the bar width instead of their content so they stay on the grid, + // then let the row scroll once they overflow. + val keyWidth = maxWidth / TERMUX_SINGLE_ROW_KEYS_PER_SCREEN + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(keySpacing), + verticalAlignment = Alignment.CenterVertically, + ) { + entries.forEach { entry -> + AccessoryEntry( + entry = entry, + expanded = expandedGroupId == entry.id, + modifierState = modifierState, + onAction = onAction, + onToggleGroup = toggleGroup, + chuchuKeyActive = chuchuKeyActive, + termuxStyle = true, + buttonPadding = buttonPadding, + modifier = Modifier.width(keyWidth), + ) + } + } + } + + useSingleRow -> Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = horizontalPadding, vertical = verticalPadding) + .padding(horizontal = barHorizontalPadding, vertical = barVerticalPadding) .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(keySpacing), verticalAlignment = Alignment.CenterVertically, ) { entries.forEach { entry -> @@ -107,62 +160,67 @@ fun KeyboardAccessoryBar( expanded = expandedGroupId == entry.id, modifierState = modifierState, onAction = onAction, - onToggleGroup = { id -> - expandedGroupId = if (expandedGroupId == id) null else id - }, - buttonHeight = buttonHeight, + onToggleGroup = toggleGroup, + chuchuKeyActive = chuchuKeyActive, + termuxStyle = false, buttonPadding = buttonPadding, ) } - FilesButton( - onChuchuKey = onChuchuKey, - chuchuKeyActive = chuchuKeyActive, - onOpenFiles = onOpenFiles, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) - SettingsButton( - onSettings = onSettings, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) } - return - } - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = horizontalPadding, vertical = verticalPadding), - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - maxLines = 2, - ) { - entries.forEach { entry -> - AccessoryEntry( - entry = entry, - expanded = expandedGroupId == entry.id, - modifierState = modifierState, - onAction = onAction, - onToggleGroup = { id -> - expandedGroupId = if (expandedGroupId == id) null else id - }, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) + termuxStyle -> { + val (topRow, bottomRow) = TerminalAccessoryLayoutStore.splitIntoTwoRows(entries) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = barVerticalPadding), + verticalArrangement = Arrangement.spacedBy(keySpacing), + ) { + listOf(topRow, bottomRow).filter { it.isNotEmpty() }.forEach { rowEntries -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(keySpacing), + verticalAlignment = Alignment.CenterVertically, + ) { + rowEntries.forEach { entry -> + AccessoryEntry( + entry = entry, + expanded = expandedGroupId == entry.id, + modifierState = modifierState, + onAction = onAction, + onToggleGroup = toggleGroup, + chuchuKeyActive = chuchuKeyActive, + termuxStyle = true, + buttonPadding = buttonPadding, + modifier = Modifier.weight(1f), + ) + } + } + } + } + } + + else -> FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = barHorizontalPadding, vertical = barVerticalPadding), + horizontalArrangement = Arrangement.spacedBy(keySpacing), + verticalArrangement = Arrangement.spacedBy(keySpacing), + maxLines = 2, + ) { + entries.forEach { entry -> + AccessoryEntry( + entry = entry, + expanded = expandedGroupId == entry.id, + modifierState = modifierState, + onAction = onAction, + onToggleGroup = toggleGroup, + chuchuKeyActive = chuchuKeyActive, + termuxStyle = false, + buttonPadding = buttonPadding, + ) + } } - FilesButton( - onChuchuKey = onChuchuKey, - chuchuKeyActive = chuchuKeyActive, - onOpenFiles = onOpenFiles, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) - SettingsButton( - onSettings = onSettings, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) } } } @@ -172,8 +230,7 @@ private fun GroupPopover( group: AccessoryKeyGroup, modifierState: ModifierState, onAction: (AccessoryAction) -> Unit, - buttonHeight: Dp, - buttonPadding: PaddingValues, + chuchuKeyActive: Boolean, horizontalPadding: Dp, ) { val colors = ChuColors.current @@ -193,12 +250,15 @@ private fun GroupPopover( verticalAlignment = Alignment.CenterVertically, ) { group.children.forEach { child -> + // The popover scrolls freely, so its keys keep the default look even when the bar + // underneath is termux-styled. AccessoryButton( item = child, modifierState = modifierState, onAction = onAction, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, + chuchuKeyActive = chuchuKeyActive, + termuxStyle = false, + buttonPadding = DefaultButtonPadding, ) } } @@ -212,26 +272,30 @@ private fun AccessoryEntry( modifierState: ModifierState, onAction: (AccessoryAction) -> Unit, onToggleGroup: (String) -> Unit, - buttonHeight: Dp, + chuchuKeyActive: Boolean, + termuxStyle: Boolean, buttonPadding: PaddingValues, + modifier: Modifier = Modifier, ) { when (entry) { - is ResolvedAccessoryEntry.Single -> - AccessoryButton( - item = entry.item, - modifierState = modifierState, - onAction = onAction, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) - is ResolvedAccessoryEntry.Group -> - GroupButton( - group = entry.group, - expanded = expanded, - onToggle = { onToggleGroup(entry.group.id) }, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) + is ResolvedAccessoryEntry.Single -> AccessoryButton( + item = entry.item, + modifierState = modifierState, + onAction = onAction, + chuchuKeyActive = chuchuKeyActive, + termuxStyle = termuxStyle, + buttonPadding = buttonPadding, + modifier = modifier, + ) + + is ResolvedAccessoryEntry.Group -> GroupButton( + group = entry.group, + expanded = expanded, + onToggle = { onToggleGroup(entry.group.id) }, + termuxStyle = termuxStyle, + buttonPadding = buttonPadding, + modifier = modifier, + ) } } @@ -240,21 +304,23 @@ private fun GroupButton( group: AccessoryKeyGroup, expanded: Boolean, onToggle: () -> Unit, - buttonHeight: Dp, + termuxStyle: Boolean, buttonPadding: PaddingValues, + modifier: Modifier = Modifier, ) { val colors = ChuColors.current val typography = ChuTypography.current ChuButton( onClick = onToggle, - variant = if (expanded) ChuButtonVariant.Filled else ChuButtonVariant.Outlined, - modifier = Modifier.height(buttonHeight), + variant = keyVariant(active = expanded, termuxStyle = termuxStyle), + modifier = modifier.height(ButtonHeight), contentPadding = buttonPadding, ) { - ChuText( - group.label, + AccessoryLabel( + text = group.label, style = typography.label, color = if (expanded) colors.onAccent else colors.textPrimary, + termuxStyle = termuxStyle, ) } } @@ -264,101 +330,71 @@ private fun AccessoryButton( item: AccessoryKeyItem, modifierState: ModifierState, onAction: (AccessoryAction) -> Unit, - buttonHeight: Dp, + chuchuKeyActive: Boolean, + termuxStyle: Boolean, buttonPadding: PaddingValues, + modifier: Modifier = Modifier, ) { + val colors = ChuColors.current val typography = ChuTypography.current - val toggleModifier = (item.action as? AccessoryAction.ToggleModifier)?.modifier - if (toggleModifier != null) { - ToggleButton( - label = item.label, - enabled = modifierState.isEnabled(toggleModifier), - onClick = { onAction(item.action) }, - modifier = Modifier.height(buttonHeight), - contentPadding = buttonPadding, + val labelStyle = item.labelFontFamily?.let { family -> + TextStyle( + fontFamily = family, + fontWeight = typography.label.fontWeight, + fontStyle = typography.label.fontStyle, + fontSize = 16.sp, + lineHeight = 16.sp, ) - } else { - val specialKey = (item.action as? AccessoryAction.SendSpecialKey)?.key - if (specialKey != null && specialKey.isRepeatable) { - RepeatableAccessoryButton( - item = item, - onAction = onAction, - buttonHeight = buttonHeight, - buttonPadding = buttonPadding, - ) - } else { + } ?: typography.label + + val toggleModifier = (item.action as? AccessoryAction.ToggleModifier)?.modifier + val specialKey = (item.action as? AccessoryAction.SendSpecialKey)?.key + + when { + toggleModifier != null -> { + val active = modifierState.isEnabled(toggleModifier) ChuButton( onClick = { onAction(item.action) }, - variant = ChuButtonVariant.Outlined, - modifier = Modifier.height(buttonHeight), + variant = keyVariant(active = active, termuxStyle = termuxStyle), + modifier = modifier.height(ButtonHeight), contentPadding = buttonPadding, ) { - ChuText(item.label, style = typography.label) + // A leading dot marks a sticky modifier. Termux-style keys are too narrow for the + // extra glyph, so there the fill alone carries the state. + AccessoryLabel( + text = if (active && !termuxStyle) "• ${item.label}" else item.label, + style = typography.label, + color = if (active) colors.onAccent else colors.textSecondary, + termuxStyle = termuxStyle, + ) } } - } -} -@Composable -private fun FilesButton( - onChuchuKey: (() -> Unit)?, - chuchuKeyActive: Boolean, - onOpenFiles: (() -> Unit)?, - buttonHeight: Dp, - buttonPadding: PaddingValues, -) { - val colors = ChuColors.current - val typography = ChuTypography.current - if (onChuchuKey != null) { - ChuButton( - onClick = onChuchuKey, - variant = if (chuchuKeyActive) ChuButtonVariant.Filled else ChuButtonVariant.Outlined, - modifier = Modifier.height(buttonHeight), - contentPadding = buttonPadding, - ) { - ChuText( - "⌘", - style = typography.label, - color = if (chuchuKeyActive) colors.onAccent else colors.textPrimary, - ) - } - } - if (onOpenFiles == null) return - ChuButton( - onClick = onOpenFiles, - variant = ChuButtonVariant.Outlined, - modifier = Modifier.height(buttonHeight), - contentPadding = buttonPadding, - ) { - ChuText( - text = "", - style = TextStyle( - fontFamily = ChuSymbolsFontFamily, - fontWeight = typography.label.fontWeight, - fontStyle = typography.label.fontStyle, - fontSize = 16.sp, - lineHeight = 16.sp, - ), - color = colors.textPrimary, + specialKey?.isRepeatable == true -> RepeatableAccessoryButton( + item = item, + onAction = onAction, + termuxStyle = termuxStyle, + buttonPadding = buttonPadding, + labelStyle = labelStyle, + modifier = modifier, ) - } -} -@Composable -private fun SettingsButton( - onSettings: (() -> Unit)?, - buttonHeight: Dp, - buttonPadding: PaddingValues, -) { - val typography = ChuTypography.current - if (onSettings == null) return - ChuButton( - onClick = onSettings, - variant = ChuButtonVariant.Outlined, - modifier = Modifier.height(buttonHeight), - contentPadding = buttonPadding, - ) { - ChuText("⚙", style = typography.label) + else -> { + val active = item.action is AccessoryAction.ChuchuKey && chuchuKeyActive + ChuButton( + onClick = { onAction(item.action) }, + variant = keyVariant(active = active, termuxStyle = termuxStyle), + modifier = modifier.height(ButtonHeight), + contentPadding = buttonPadding, + ) { + AccessoryLabel( + text = item.label, + style = labelStyle, + color = if (active) colors.onAccent else colors.textPrimary, + termuxStyle = termuxStyle, + ) + } + } } } @@ -366,17 +402,19 @@ private fun SettingsButton( private fun RepeatableAccessoryButton( item: AccessoryKeyItem, onAction: (AccessoryAction) -> Unit, - buttonHeight: Dp, + termuxStyle: Boolean, buttonPadding: PaddingValues, + labelStyle: TextStyle, + modifier: Modifier = Modifier, ) { - val typography = ChuTypography.current + val colors = ChuColors.current val haptics = LocalHapticFeedback.current var pressed by remember { mutableStateOf(false) } val currentOnAction by rememberUpdatedState(onAction) Box( - modifier = Modifier - .height(buttonHeight) + modifier = modifier + .height(ButtonHeight) .pointerInput(item.action) { awaitEachGesture { awaitFirstDown().consume() @@ -413,37 +451,73 @@ private fun RepeatableAccessoryButton( }, ) { ChuButtonSurface( - modifier = Modifier.height(buttonHeight), + modifier = if (termuxStyle) Modifier.matchParentSize() else Modifier.height(ButtonHeight), pressed = pressed, - variant = ChuButtonVariant.Outlined, + variant = keyVariant(active = false, termuxStyle = termuxStyle), contentPadding = buttonPadding, ) { - ChuText(item.label, style = typography.label) + AccessoryLabel( + text = item.label, + style = labelStyle, + color = colors.textPrimary, + termuxStyle = termuxStyle, + ) } } } +private fun keyVariant(active: Boolean, termuxStyle: Boolean): ChuButtonVariant = when { + active -> ChuButtonVariant.Filled + termuxStyle -> ChuButtonVariant.Ghost + else -> ChuButtonVariant.Outlined +} + +/** + * Draws a key label, shrinking the font until it fits when the key is termux-styled. Those keys are + * sized off the bar width rather than their text, so a label like "Enter" would otherwise wrap onto + * a second line in a narrow column. + */ @Composable -private fun ToggleButton( - label: String, - enabled: Boolean, - onClick: () -> Unit, - modifier: Modifier, - contentPadding: PaddingValues, +private fun AccessoryLabel( + text: String, + style: TextStyle, + color: Color, + termuxStyle: Boolean, ) { - val colors = ChuColors.current - val typography = ChuTypography.current - val activeLabel = if (enabled) "• $label" else label - ChuButton( - onClick = onClick, - modifier = modifier, - contentPadding = contentPadding, - variant = if (enabled) ChuButtonVariant.Filled else ChuButtonVariant.Outlined, - ) { + if (!termuxStyle) { + ChuText(text, style = style, color = color) + return + } + + val density = LocalDensity.current + val textMeasurer = rememberTextMeasurer() + + BoxWithConstraints(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + val maxWidthPx = with(density) { maxWidth.toPx() } + val fittedStyle = remember(text, style, maxWidthPx) { + var candidate = style + while (candidate.fontSize.value > MIN_LABEL_FONT_SIZE_SP + LABEL_FONT_SIZE_STEP_SP) { + val measured = textMeasurer.measure( + text = text, + style = candidate, + maxLines = 1, + overflow = TextOverflow.Clip, + constraints = Constraints(maxWidth = maxWidthPx.toInt()), + ) + if (measured.lineCount <= 1 && measured.size.width <= maxWidthPx) break + candidate = candidate.copy( + fontSize = (candidate.fontSize.value - LABEL_FONT_SIZE_STEP_SP).sp, + ) + } + candidate + } + ChuText( - activeLabel, - style = typography.label, - color = if (enabled) colors.onAccent else colors.textSecondary, + text = text, + style = fittedStyle, + color = color, + maxLines = 1, + overflow = TextOverflow.Clip, ) } } diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt index 41860cfd..96cd8ead 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt @@ -1,5 +1,8 @@ package com.jossephus.chuchu.ui.terminal +import androidx.compose.ui.text.font.FontFamily +import com.jossephus.chuchu.ui.theme.ChuSymbolsFontFamily + enum class TerminalModifier { Ctrl, Alt, @@ -103,12 +106,22 @@ sealed interface AccessoryAction { data class SendText(val text: String) : AccessoryAction data object Paste : AccessoryAction + + /** Opens the settings screen. Handled by the host screen, not the dispatcher. */ + data object Settings : AccessoryAction + + /** Toggles the chuchu prefix. Handled by the host screen, not the dispatcher. */ + data object ChuchuKey : AccessoryAction + + /** Switches to the file browser tab. Handled by the host screen, not the dispatcher. */ + data object OpenFiles : AccessoryAction } data class AccessoryKeyItem( val id: String, val label: String, val action: AccessoryAction, + val labelFontFamily: FontFamily? = null, ) data class AccessoryKeyGroup( @@ -164,6 +177,13 @@ object TerminalAccessoryDispatcher { modifierState = modifierState, shouldPaste = true, ) + + // Screen-level actions are intercepted by the host before reaching the dispatcher. + AccessoryAction.Settings, + AccessoryAction.ChuchuKey, + AccessoryAction.OpenFiles -> AccessoryDispatchResult( + modifierState = modifierState, + ) } } @@ -203,8 +223,25 @@ object TerminalAccessoryLayoutStore { AccessoryKeyItem("f11", TerminalSpecialKey.F11.label, AccessoryAction.SendSpecialKey(TerminalSpecialKey.F11)), AccessoryKeyItem("f12", TerminalSpecialKey.F12.label, AccessoryAction.SendSpecialKey(TerminalSpecialKey.F12)), AccessoryKeyItem("paste", "Paste", AccessoryAction.Paste), + AccessoryKeyItem("chuchu_key", "⌘", AccessoryAction.ChuchuKey), + AccessoryKeyItem("open_files", "", AccessoryAction.OpenFiles, labelFontFamily = ChuSymbolsFontFamily), + AccessoryKeyItem("settings", "⚙", AccessoryAction.Settings), ) + /** + * Keys that used to be fixed trailing buttons on the bar, before they became ordinary catalog + * entries the user can reorder or remove. + */ + private val screenActionIds: List = listOf("chuchu_key", "open_files", "settings") + + /** + * Appends any missing screen-action key to a saved layout. Layouts stored before those keys + * entered the catalog do not list them, and without this they would disappear from the bar on + * upgrade — taking the only route to settings with them. Callers must apply this exactly once. + */ + fun backfillScreenActions(ids: List): List = + ids + screenActionIds.filterNot { it in ids } + private val compositeGroups: List = listOf( AccessoryKeyGroup( id = "digits", @@ -235,10 +272,23 @@ object TerminalAccessoryLayoutStore { "right", "enter", "space", + "chuchu_key", + "open_files", + "settings", ) fun defaultEntries(): List = resolveSelectedLayout(defaultLayoutIds) + /** + * Splits entries into two rows as evenly as possible. The first row takes the extra entry when + * the count is odd, so the second row is never the longer one. Only the termux-style bar uses + * this; the default bar lets [FlowRow] wrap. + */ + fun splitIntoTwoRows(items: List): Pair, List> { + val splitIndex = (items.size + 1) / 2 + return items.take(splitIndex) to items.drop(splitIndex) + } + fun defaultLayoutIds(): List = defaultLayoutIds fun catalog(): List = catalogItems diff --git a/android/app/src/test/java/com/jossephus/chuchu/ui/terminal/AccessoryLogicTest.kt b/android/app/src/test/java/com/jossephus/chuchu/ui/terminal/AccessoryLogicTest.kt index b03e6733..fe18ded2 100644 --- a/android/app/src/test/java/com/jossephus/chuchu/ui/terminal/AccessoryLogicTest.kt +++ b/android/app/src/test/java/com/jossephus/chuchu/ui/terminal/AccessoryLogicTest.kt @@ -311,4 +311,87 @@ class AccessoryLogicTest { val item = TerminalAccessoryLayoutStore.catalog().first { it.id == "paste" } assertTrue(item.action is AccessoryAction.Paste) } + + // ── Screen actions ──────────────────────────────────────────────────── + + @Test + fun `screen actions are catalog keys so they can be reordered or removed`() { + val ids = TerminalAccessoryLayoutStore.catalog().map { it.id } + assertTrue("catalog must contain chuchu_key", "chuchu_key" in ids) + assertTrue("catalog must contain open_files", "open_files" in ids) + assertTrue("catalog must contain settings", "settings" in ids) + } + + @Test + fun `default layout offers the screen actions`() { + val ids = TerminalAccessoryLayoutStore.defaultLayoutIds() + assertTrue("chuchu_key" in ids) + assertTrue("open_files" in ids) + assertTrue("settings" in ids) + } + + @Test + fun `open_files renders with the symbol font`() { + val item = TerminalAccessoryLayoutStore.catalog().first { it.id == "open_files" } + assertTrue(item.action is AccessoryAction.OpenFiles) + assertNotNull("open_files label needs the symbol font to render", item.labelFontFamily) + } + + @Test + fun `dispatching a screen action leaves terminal state untouched`() { + // The host screen intercepts these; reaching the dispatcher must be a no-op, and in + // particular must not clear sticky modifiers. + val sticky = ModifierState(ctrl = true) + listOf( + AccessoryAction.Settings, + AccessoryAction.ChuchuKey, + AccessoryAction.OpenFiles, + ).forEach { action -> + val result = TerminalAccessoryDispatcher.dispatch(action, sticky) + assertEquals(sticky, result.modifierState) + assertNull(result.text) + assertNull(result.specialKey) + assertFalse(result.shouldPaste) + assertFalse(result.suppressImeInput) + } + } + + // ── Layout migration ────────────────────────────────────────────────── + + @Test + fun `backfill appends screen actions missing from a saved layout`() { + val saved = listOf("escape", "tab", "ctrl") + val migrated = TerminalAccessoryLayoutStore.backfillScreenActions(saved) + assertEquals(listOf("escape", "tab", "ctrl", "chuchu_key", "open_files", "settings"), migrated) + } + + @Test + fun `backfill keeps screen actions the layout already places`() { + val saved = listOf("settings", "escape", "chuchu_key", "open_files") + val migrated = TerminalAccessoryLayoutStore.backfillScreenActions(saved) + assertEquals("an already-migrated layout must be left alone", saved, migrated) + } + + // ── Termux-style two-row split ──────────────────────────────────────── + + @Test + fun `two-row split never leaves the bottom row longer than the top`() { + val (top, bottom) = TerminalAccessoryLayoutStore.splitIntoTwoRows(listOf(1, 2, 3, 4, 5)) + assertEquals(listOf(1, 2, 3), top) + assertEquals(listOf(4, 5), bottom) + } + + @Test + fun `two-row split halves an even layout`() { + val (top, bottom) = TerminalAccessoryLayoutStore.splitIntoTwoRows(listOf(1, 2, 3, 4)) + assertEquals(listOf(1, 2), top) + assertEquals(listOf(3, 4), bottom) + } + + @Test + fun `two-row split handles an empty layout`() { + val (top, bottom) = TerminalAccessoryLayoutStore.splitIntoTwoRows(emptyList()) + assertTrue(top.isEmpty()) + assertTrue(bottom.isEmpty()) + } } From 343a6db71073710102726c921b51b3e0019dcb71 Mon Sep 17 00:00:00 2001 From: bhoffman20 Date: Mon, 13 Jul 2026 17:21:07 -0500 Subject: [PATCH 2/3] fix: label the paste key with an icon The bar is tight on horizontal room, and Paste is the only word-length label among the punctuation and symbol keys it sits beside. --- .../java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt index 96cd8ead..997a18a6 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt @@ -222,7 +222,7 @@ object TerminalAccessoryLayoutStore { AccessoryKeyItem("f10", TerminalSpecialKey.F10.label, AccessoryAction.SendSpecialKey(TerminalSpecialKey.F10)), AccessoryKeyItem("f11", TerminalSpecialKey.F11.label, AccessoryAction.SendSpecialKey(TerminalSpecialKey.F11)), AccessoryKeyItem("f12", TerminalSpecialKey.F12.label, AccessoryAction.SendSpecialKey(TerminalSpecialKey.F12)), - AccessoryKeyItem("paste", "Paste", AccessoryAction.Paste), + AccessoryKeyItem("paste", "⎘", AccessoryAction.Paste), // U+2398 AccessoryKeyItem("chuchu_key", "⌘", AccessoryAction.ChuchuKey), AccessoryKeyItem("open_files", "", AccessoryAction.OpenFiles, labelFontFamily = ChuSymbolsFontFamily), AccessoryKeyItem("settings", "⚙", AccessoryAction.Settings), From 88bf5cb1ae96ef7e3862df8328b3cd23bbd56f46 Mon Sep 17 00:00:00 2001 From: bhoffman20 Date: Mon, 13 Jul 2026 18:34:16 -0500 Subject: [PATCH 3/3] fix: restore the files key glyph Folding the files button into the catalog carried over an older codepoint (U+E5FE) instead of the one the fixed button rendered (U+E5FF). --- .../java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt index 997a18a6..b097adb5 100644 --- a/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt +++ b/android/app/src/main/java/com/jossephus/chuchu/ui/terminal/TerminalAccessory.kt @@ -224,7 +224,7 @@ object TerminalAccessoryLayoutStore { AccessoryKeyItem("f12", TerminalSpecialKey.F12.label, AccessoryAction.SendSpecialKey(TerminalSpecialKey.F12)), AccessoryKeyItem("paste", "⎘", AccessoryAction.Paste), // U+2398 AccessoryKeyItem("chuchu_key", "⌘", AccessoryAction.ChuchuKey), - AccessoryKeyItem("open_files", "", AccessoryAction.OpenFiles, labelFontFamily = ChuSymbolsFontFamily), + AccessoryKeyItem("open_files", "", AccessoryAction.OpenFiles, labelFontFamily = ChuSymbolsFontFamily), AccessoryKeyItem("settings", "⚙", AccessoryAction.Settings), )