Settings from terminal view, scrollback truncation dialog, drawer about footer
- Add "Settings" item to tab bar and drawer bar kebab menus — opens full SettingsScreen from the terminal view via NavHost navigation - Add Settings row + compact about footer (app icon, version, developer) to drawer content panel - Scrollback truncation: when user lowers scrollback value with active SSH/Telnet/Local sessions, confirmation dialog warns about history loss. ScreenBuffer.truncateHistory() trims oldest lines and clamps scrollOffset. - TerminalService.truncateAllScrollback() iterates all session buffers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ca4359a996
commit
e0cbcc6d43
13 changed files with 179 additions and 18 deletions
|
|
@ -265,6 +265,17 @@ class TerminalService : Service() {
|
|||
|
||||
fun getScreenBuffer(sessionId: Long): ScreenBuffer? = sessions[sessionId]?.screenBuffer
|
||||
|
||||
/** Truncate scrollback history of all active terminal sessions to [newMax] lines. */
|
||||
fun truncateAllScrollback(newMax: Int) {
|
||||
for (entry in sessions.values) {
|
||||
entry.screenBuffer.truncateHistory(newMax)
|
||||
}
|
||||
FileLogger.log(TAG, "Truncated scrollback to $newMax for ${sessions.size} sessions")
|
||||
}
|
||||
|
||||
/** Count of active terminal sessions (SSH, Telnet, Local — not SFTP). */
|
||||
fun terminalSessionCount(): Int = sessions.size
|
||||
|
||||
fun hasActiveSession(sessionId: Long): Boolean {
|
||||
val state = _activeSessions.value[sessionId]
|
||||
return state is SessionState.Connected || state is SessionState.Connecting
|
||||
|
|
|
|||
|
|
@ -606,6 +606,10 @@ class MainActivity : AppCompatActivity() {
|
|||
mainViewModel.switchToNavHost()
|
||||
navController.navigate(Routes.PICK_HOST) { launchSingleTop = true }
|
||||
}
|
||||
val onOpenSettings = {
|
||||
mainViewModel.switchToNavHost()
|
||||
navController.navigate(Routes.SETTINGS) { launchSingleTop = true }
|
||||
}
|
||||
val onSwitchNavStyle = { style: String ->
|
||||
lifecycleScope.launch { mainViewModel.terminalPrefs.setSessionNavStyle(style) }
|
||||
Unit
|
||||
|
|
@ -633,6 +637,7 @@ class MainActivity : AppCompatActivity() {
|
|||
tabType = tabTypes[appState.activeSessionId] ?: TabType.TERMINAL,
|
||||
onHamburger = { drawerScope.launch { drawerState.open() } },
|
||||
onPlusTap = onNewSession,
|
||||
onSettings = onOpenSettings,
|
||||
onKbSettings = {
|
||||
if (isCustomKeyboard) showKbSettings = true
|
||||
else showAqbSettings = true
|
||||
|
|
@ -652,6 +657,7 @@ class MainActivity : AppCompatActivity() {
|
|||
sessionLabels = sessionLabels,
|
||||
onSessionTap = { sid -> mainViewModel.switchToTerminal(sid) },
|
||||
onPlusTap = onNewSession,
|
||||
onSettings = onOpenSettings,
|
||||
onDuplicate = { sid -> mainViewModel.duplicateSession(sid) },
|
||||
onSftp = { sid ->
|
||||
val connId = mainViewModel.terminalService?.getSession(sid)?.savedConnectionId ?: return@SessionTabBar
|
||||
|
|
@ -963,6 +969,10 @@ class MainActivity : AppCompatActivity() {
|
|||
drawerScope.launch { drawerState.close() }
|
||||
onNewSession()
|
||||
},
|
||||
onSettings = {
|
||||
drawerScope.launch { drawerState.close() }
|
||||
onOpenSettings()
|
||||
},
|
||||
onSwitchToTopBar = {
|
||||
drawerScope.launch { drawerState.close() }
|
||||
onSwitchNavStyle("top_bar")
|
||||
|
|
@ -1039,7 +1049,11 @@ class MainActivity : AppCompatActivity() {
|
|||
onVaultScreenChanged = { mainViewModel.setOnVaultScreen(it) },
|
||||
onImportVault = { mainViewModel.requestImportVault() },
|
||||
pendingImportVault = mainViewModel.pendingImportVault.collectAsStateWithLifecycle().value,
|
||||
onPendingImportConsumed = { mainViewModel.consumeImportVault() }
|
||||
onPendingImportConsumed = { mainViewModel.consumeImportVault() },
|
||||
terminalSessionCount = mainViewModel.terminalService?.terminalSessionCount() ?: 0,
|
||||
onTruncateScrollback = { newMax ->
|
||||
mainViewModel.terminalService?.truncateAllScrollback(newMax)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ fun SshWorkbenchNavGraph(
|
|||
onImportVault: () -> Unit = {},
|
||||
onSaveVaultLocally: () -> Unit = {},
|
||||
pendingImportVault: Boolean = false,
|
||||
onPendingImportConsumed: () -> Unit = {}
|
||||
onPendingImportConsumed: () -> Unit = {},
|
||||
terminalSessionCount: Int = 0,
|
||||
onTruncateScrollback: (Int) -> Unit = {}
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
|
|
@ -143,7 +145,9 @@ fun SshWorkbenchNavGraph(
|
|||
proFeatures = proFeatures,
|
||||
onNavigateToSubscription = { navController.navigate(Routes.SUBSCRIPTION) },
|
||||
onNavigateToLanguage = { navController.navigate(Routes.LANGUAGE) },
|
||||
onNavigateToActions = { navController.navigate(Routes.ACTIONS) }
|
||||
onNavigateToActions = { navController.navigate(Routes.ACTIONS) },
|
||||
terminalSessionCount = terminalSessionCount,
|
||||
onTruncateScrollback = onTruncateScrollback
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.roundingmobile.sshworkbench.ui.screens
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -56,6 +57,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.roundingmobile.ssh.SessionState
|
||||
|
|
@ -103,6 +105,7 @@ fun SessionTabBar(
|
|||
onSessionTap: (Long) -> Unit,
|
||||
onPlusTap: () -> Unit,
|
||||
onKbSettings: () -> Unit = {},
|
||||
onSettings: () -> Unit = {},
|
||||
kbSettingsLabel: String = "",
|
||||
ckbVisible: Boolean = false,
|
||||
onToggleCkb: () -> Unit = {},
|
||||
|
|
@ -210,6 +213,11 @@ fun SessionTabBar(
|
|||
onClick = { showBarMenu = false; onKbSettings() },
|
||||
leadingIcon = { Icon(Icons.Filled.Settings, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.settings)) },
|
||||
onClick = { showBarMenu = false; onSettings() },
|
||||
leadingIcon = { Icon(Icons.Filled.Settings, contentDescription = null, tint = AccentTeal) }
|
||||
)
|
||||
if (onSwitchToDrawer != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.switch_to_drawer)) },
|
||||
|
|
@ -486,6 +494,7 @@ fun SessionDrawerBar(
|
|||
onHamburger: () -> Unit,
|
||||
onPlusTap: () -> Unit,
|
||||
onKbSettings: () -> Unit = {},
|
||||
onSettings: () -> Unit = {},
|
||||
kbSettingsLabel: String = "",
|
||||
ckbVisible: Boolean = false,
|
||||
onToggleCkb: () -> Unit = {},
|
||||
|
|
@ -564,6 +573,11 @@ fun SessionDrawerBar(
|
|||
onClick = { showBarMenu = false; onKbSettings() },
|
||||
leadingIcon = { Icon(Icons.Filled.Settings, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.settings)) },
|
||||
onClick = { showBarMenu = false; onSettings() },
|
||||
leadingIcon = { Icon(Icons.Filled.Settings, contentDescription = null, tint = AccentTeal) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -582,6 +596,7 @@ fun SessionDrawerContent(
|
|||
sessionLabels: Map<Long, String>,
|
||||
onSessionTap: (Long) -> Unit,
|
||||
onPlusTap: () -> Unit,
|
||||
onSettings: () -> Unit = {},
|
||||
onSwitchToTopBar: () -> Unit,
|
||||
onDuplicate: (Long) -> Unit = {},
|
||||
onSftp: (Long) -> Unit = {},
|
||||
|
|
@ -668,20 +683,62 @@ fun SessionDrawerContent(
|
|||
}
|
||||
}
|
||||
|
||||
// Footer divider + switch to top bar
|
||||
// Footer
|
||||
HorizontalDivider(color = Color(0xFF2A2A2A))
|
||||
|
||||
// Settings
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onSettings)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Filled.Settings, contentDescription = null, tint = AccentTeal, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(stringResource(R.string.settings), color = TextPrimary, fontSize = 14.sp)
|
||||
}
|
||||
|
||||
// Switch to top bar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onSwitchToTopBar)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.switch_to_top_bar),
|
||||
color = DrawerSectionHeader,
|
||||
fontSize = 13.sp
|
||||
Text(stringResource(R.string.switch_to_top_bar), color = DrawerSectionHeader, fontSize = 13.sp)
|
||||
}
|
||||
|
||||
// Compact about
|
||||
HorizontalDivider(color = Color(0xFF2A2A2A))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_pro_about),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column {
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
color = DrawerSectionHeader,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"v1.0.0 — ${stringResource(R.string.developer_name)}",
|
||||
color = DrawerSectionHeader.copy(alpha = 0.6f),
|
||||
fontSize = 9.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import androidx.compose.material3.MenuAnchorType
|
|||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -81,6 +80,7 @@ import androidx.fragment.app.FragmentActivity
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.roundingmobile.sshworkbench.R
|
||||
import com.roundingmobile.sshworkbench.ui.theme.AppColors
|
||||
import com.roundingmobile.sshworkbench.ui.theme.AppSwitch
|
||||
import com.roundingmobile.sshworkbench.auth.BiometricAuthManager
|
||||
import com.roundingmobile.sshworkbench.pro.ProFeatures
|
||||
import com.roundingmobile.sshworkbench.terminal.AqbSettingsScreen
|
||||
|
|
@ -102,7 +102,11 @@ fun SettingsScreen(
|
|||
proFeatures: ProFeatures? = null,
|
||||
onNavigateToSubscription: (() -> Unit)? = null,
|
||||
onNavigateToLanguage: () -> Unit = {},
|
||||
onNavigateToActions: () -> Unit = {}
|
||||
onNavigateToActions: () -> Unit = {},
|
||||
/** Number of active terminal sessions (SSH/Telnet/Local). 0 = no truncation prompt needed. */
|
||||
terminalSessionCount: Int = 0,
|
||||
/** Called after user confirms scrollback truncation for active sessions. */
|
||||
onTruncateScrollback: (Int) -> Unit = {}
|
||||
) {
|
||||
val fontSize by viewModel.fontSize.collectAsStateWithLifecycle()
|
||||
val fontFamily by viewModel.fontFamily.collectAsStateWithLifecycle()
|
||||
|
|
@ -125,6 +129,9 @@ fun SettingsScreen(
|
|||
val appLanguage by viewModel.appLanguage.collectAsStateWithLifecycle()
|
||||
val sessionNavStyle by viewModel.sessionNavStyle.collectAsStateWithLifecycle()
|
||||
|
||||
// Scrollback truncation dialog
|
||||
var pendingScrollbackValue by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
Scaffold(
|
||||
containerColor = AppColors.Background,
|
||||
topBar = {
|
||||
|
|
@ -399,8 +406,12 @@ fun SettingsScreen(
|
|||
DropdownMenuItem(
|
||||
text = { Text(lines.toString()) },
|
||||
onClick = {
|
||||
viewModel.setScrollbackLines(lines)
|
||||
scrollbackExpanded = false
|
||||
if (lines < scrollbackLines && terminalSessionCount > 0) {
|
||||
pendingScrollbackValue = lines
|
||||
} else {
|
||||
viewModel.setScrollbackLines(lines)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -519,7 +530,7 @@ fun SettingsScreen(
|
|||
color = AppColors.OnSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
AppSwitch(
|
||||
checked = wifiLockDefault,
|
||||
onCheckedChange = { viewModel.setWifiLockDefault(it) }
|
||||
)
|
||||
|
|
@ -771,6 +782,33 @@ fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollback truncation confirmation dialog
|
||||
pendingScrollbackValue?.let { newValue ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { pendingScrollbackValue = null },
|
||||
title = { Text(stringResource(R.string.scrollback_truncate_title)) },
|
||||
text = {
|
||||
Text(stringResource(
|
||||
R.string.scrollback_truncate_message,
|
||||
terminalSessionCount,
|
||||
newValue
|
||||
))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.setScrollbackLines(newValue)
|
||||
onTruncateScrollback(newValue)
|
||||
pendingScrollbackValue = null
|
||||
}) { Text(stringResource(R.string.scrollback_truncate_confirm)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { pendingScrollbackValue = null }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reusable components ──
|
||||
|
|
@ -832,7 +870,7 @@ private fun SwitchRow(
|
|||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Switch(
|
||||
AppSwitch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled
|
||||
|
|
@ -866,7 +904,7 @@ private fun SwitchRowWithHint(
|
|||
)
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled)
|
||||
AppSwitch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@
|
|||
<string name="font_size">Schriftgröße</string>
|
||||
<string name="font_size_value">%1$.0f sp</string>
|
||||
<string name="scrollback_lines">Scrollverlauf</string>
|
||||
<string name="scrollback_truncate_title">Verlauf kürzen?</string>
|
||||
<string name="scrollback_truncate_message">Sie haben %1$d aktive Sitzungen. Die Reduzierung des Verlaufs auf %2$d Zeilen verwirft älteren Verlauf aller Sitzungen.</string>
|
||||
<string name="scrollback_truncate_confirm">Kürzen</string>
|
||||
<string name="keep_screen_on">Bildschirm anlassen</string>
|
||||
<string name="time_format">Zeitformat</string>
|
||||
<string name="time_format_system">Gerätestandard</string>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@
|
|||
<string name="font_size">Tamaño de fuente</string>
|
||||
<string name="font_size_value">%1$.0f sp</string>
|
||||
<string name="scrollback_lines">Líneas de historial</string>
|
||||
<string name="scrollback_truncate_title">¿Truncar historial?</string>
|
||||
<string name="scrollback_truncate_message">Tienes %1$d sesiones activas. Reducir el historial a %2$d líneas descartará el historial antiguo de todas las sesiones.</string>
|
||||
<string name="scrollback_truncate_confirm">Truncar</string>
|
||||
<string name="keep_screen_on">Mantener pantalla encendida</string>
|
||||
<string name="time_format">Formato de hora</string>
|
||||
<string name="time_format_system">Predeterminado del dispositivo</string>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@
|
|||
<string name="font_size">Taille de police</string>
|
||||
<string name="font_size_value">%1$.0f sp</string>
|
||||
<string name="scrollback_lines">Historique de défilement</string>
|
||||
<string name="scrollback_truncate_title">Tronquer l\'historique ?</string>
|
||||
<string name="scrollback_truncate_message">Vous avez %1$d sessions actives. Réduire l\'historique à %2$d lignes supprimera l\'historique ancien de toutes les sessions.</string>
|
||||
<string name="scrollback_truncate_confirm">Tronquer</string>
|
||||
<string name="keep_screen_on">Garder l\'écran allumé</string>
|
||||
<string name="time_format">Format de l\'heure</string>
|
||||
<string name="time_format_system">Par défaut de l\'appareil</string>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@
|
|||
<string name="font_size">Teckenstorlek</string>
|
||||
<string name="font_size_value">%1$.0f sp</string>
|
||||
<string name="scrollback_lines">Rullningshistorik</string>
|
||||
<string name="scrollback_truncate_title">Trunkera historik?</string>
|
||||
<string name="scrollback_truncate_message">Du har %1$d aktiva sessioner. Att minska historiken till %2$d rader kommer att ta bort äldre historik från alla sessioner.</string>
|
||||
<string name="scrollback_truncate_confirm">Trunkera</string>
|
||||
<string name="keep_screen_on">Håll skärmen på</string>
|
||||
<string name="time_format">Tidsformat</string>
|
||||
<string name="time_format_system">Enhetens standard</string>
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@
|
|||
<string name="font_size">Font Size</string>
|
||||
<string name="font_size_value">%1$.0f sp</string>
|
||||
<string name="scrollback_lines">Scrollback Lines</string>
|
||||
<string name="scrollback_truncate_title">Truncate scrollback?</string>
|
||||
<string name="scrollback_truncate_message">You have %1$d active sessions. Reducing scrollback to %2$d lines will discard older history from all sessions.</string>
|
||||
<string name="scrollback_truncate_confirm">Truncate</string>
|
||||
<string name="keep_screen_on">Keep Screen On</string>
|
||||
<string name="time_format">Time Format</string>
|
||||
<string name="time_format_system">Device default</string>
|
||||
|
|
|
|||
|
|
@ -299,11 +299,11 @@ The TopAppBar actions include a kebab menu (MoreVert) with three items:
|
|||
[16 bytes] GCM authentication tag
|
||||
```
|
||||
|
||||
**Encrypted JSON payload**: version, exported_at, hosts (SavedConnection), credentials (passwords), keys (SshKey + private PEM), snippets, port_forwards.
|
||||
**Encrypted JSON payload**: version, exported_at, hosts (SavedConnection), credentials (passwords), keys (SshKey + private PEM), snippets, port_forwards, settings (optional — `SettingsData` with string/boolean/int/float maps from `TerminalPrefsKeys.EXPORTABLE_*_KEYS`).
|
||||
|
||||
**Export flow** (`VaultExportScreen` → `VaultExportViewModel`):
|
||||
1. Full-screen Scaffold (navigated via NavGraph route `vaultExport`)
|
||||
2. Select items: hosts (with credentials toggle), SSH keys, snippets
|
||||
2. Select items: hosts (with credentials toggle), SSH keys, snippets, settings (optional, unchecked by default)
|
||||
3. Choose protection mode: Password or QR Code
|
||||
4. Password mode: min 12 chars, uppercase + lowercase + digit + special char required. Argon2id derives 32-byte key from password + random salt
|
||||
5. QR mode: random 32-byte key shown as QR (Base64URL, generated on Dispatchers.Default with progress spinner), must Save or Share QR before export is enabled. Confirmation dialog reminds user the vault is useless without the QR
|
||||
|
|
@ -318,6 +318,9 @@ The TopAppBar actions include a kebab menu (MoreVert) with three items:
|
|||
5. Duplicate detection by name across all entity types
|
||||
6. Conflict resolution: Skip / Overwrite / Import as New (appends " (imported)" suffix)
|
||||
7. Re-links credentials and port forwards to newly inserted host IDs
|
||||
8. Restores settings to DataStore if present (overwrites current values in a single transaction)
|
||||
|
||||
**Free tier import gate**: Free users can only import local vault saves (MODE_LOCAL). Pro-exported vaults (MODE_PASSWORD/MODE_QR) require a Pro subscription. Checked in `VaultImportViewModel.selectFile()` after parsing the `.swb` header.
|
||||
|
||||
**Serialization**: `VaultExportSerializer.kt` using `org.json.JSONObject` (Android built-in). No external serialization dependency.
|
||||
|
||||
|
|
@ -616,6 +619,8 @@ Key operations: cursor movement (CUU/CUD/CUF/CUB/CUP/HVP), scroll (SU/SD/IND/RI)
|
|||
|
||||
`reflowResize(newRows, newCols)` — creates new buffer, reflows wrapped lines.
|
||||
|
||||
`truncateHistory(newMax)` — synchronized, trims oldest scrollback lines to `newMax`, clamps `scrollOffset`. Used when the user lowers the global scrollback setting while sessions are active.
|
||||
|
||||
#### Parser Hierarchy
|
||||
```
|
||||
BaseTermParser → Vt220Parser → XtermParser (ANSI mode — inheritance chain)
|
||||
|
|
|
|||
11
docs/TODO.md
11
docs/TODO.md
|
|
@ -1,8 +1,16 @@
|
|||
# SSH Workbench — TODO
|
||||
|
||||
> Updated: 2026-04-11
|
||||
> Updated: 2026-04-12
|
||||
> Status: Active development. Future ideas in `FUTURE.md`.
|
||||
|
||||
## Recently Completed (2026-04-12)
|
||||
|
||||
- ~~Vault settings export/import~~ — optional "Include settings" checkbox (unchecked by default) in both Save Vault Locally and Export Vault. Exports 56 DataStore prefs (keyboard, display, QuickBar customization, HW actions). Import auto-restores settings. `EXPORTABLE_*_KEYS` lists in `TerminalPrefsKeys` define what's backed up.
|
||||
- ~~Jump chain pro message fix~~ — upgrade dialog now says "Jump host chaining" instead of "Jump Host" so free users understand single jump hosts work, only chaining is pro-gated
|
||||
- ~~Free vault import gate~~ — free users can only import local vault saves (MODE_LOCAL), not pro-exported vaults (MODE_PASSWORD/MODE_QR). Clear error message in 5 locales.
|
||||
- ~~Settings from terminal view~~ — "Settings" item in tab bar + drawer bar kebab menus opens full SettingsScreen. Drawer content panel gets Settings row + compact about footer (app icon + version + developer).
|
||||
- ~~Scrollback truncation dialog~~ — when user lowers scrollback value with active SSH/Telnet/Local sessions, confirmation dialog warns about history loss. `ScreenBuffer.truncateHistory()` trims oldest lines and clamps scroll offset.
|
||||
|
||||
## Recently Completed (2026-04-11)
|
||||
|
||||
- ~~Security audit 2026-04-11~~ — fourth full security audit (prod scope only). Fixed: FileLogger redacts PEM/Bearer; telnet cleartext warning card in EditConnectionScreen; `migrateFromProApk()` made internal + idempotent; host key fingerprints stored with `<keyType>:<fingerprint>` prefix (backward-compatible); URL scheme allowlist in `MainActivity.onUrlTapped`. Deferred: password String→CharArray refactor (HIGH, multi-file API change); purchase signature verification (already in TODO, needs Play Console RSA key); clipboard auto-clear timer (HIGH, needs UX). Full report in `SecurityAudit.md`.
|
||||
|
|
@ -73,4 +81,5 @@
|
|||
- Implement purchase signature verification (needs Play Console RSA key) — HIGH security finding from audit 2026-04-11
|
||||
- Refactor SSHAuth.Password from String to CharArray — HIGH security finding from audit 2026-04-11, multi-file API change crossing lib-ssh and app
|
||||
- Add clipboard auto-clear timer for sensitive copies — HIGH security finding from audit 2026-04-11, needs UX decisions
|
||||
- Implement session logging — per-session toggle (tab 3-dot menu), global default OFF, ANSI-stripped text to ZIP, SAF folder picker, organized by connection alias. Solves tmux/screen buffer limitation. Design agreed 2026-04-12, see `project_session_logging_design.md`.
|
||||
- Register dev app in Firebase Console for analytics/crashlytics on dev builds
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ class ScreenBuffer(
|
|||
/** Number of lines in scrollback history. Thread-safe for render thread access. */
|
||||
val scrollbackLineCount: Int @Synchronized get() = history.size
|
||||
|
||||
/** Truncate scrollback history to [newMax] lines, removing oldest lines first. */
|
||||
@Synchronized fun truncateHistory(newMax: Int) {
|
||||
while (history.size > newMax) {
|
||||
history.removeFirst()
|
||||
}
|
||||
if (scrollOffset > history.size) scrollOffset = history.size
|
||||
}
|
||||
|
||||
// Scroll offset: 0 = live terminal view, >0 = scrolled into history
|
||||
@Volatile var scrollOffset: Int = 0; private set
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue