tawasuyu

sergio/humanoid

HTML 50Markdown 1TOML 10 Definitions

consola: claudificar — Claude por defecto, modo danger, PATH y teclas

- spawn vía login shell que prepende los bin del usuario al PATH
  (~/.npm-global/bin, donde vive claude; el daemon arranca con PATH
  mínimo). Antes 'ni siquiera encontraba claude'.
- nueva sesión por defecto: 'claude --dangerously-skip-permissions'
  (modo danger) + botón flotante '+ claude' de un toque.
- fila de teclas del terminal: ctrl/alt pegajosos (modifican la
  siguiente tecla), PgUp/PgDn/Home/End además de esc/tab/flechas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

b4a7a20b3823f0754a284038fc7f55dad68f9ea3 · by Sergio · (4 months ago) · parent f973f9c17 · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

3 files · +109 −28

diff --git a/consola/src/main/java/com/rizoma/consola/ConsolaViewModel.kt b/consola/src/main/java/com/rizoma/consola/ConsolaViewModel.kt
index d915e3e..5b88c2e 100644
--- a/consola/src/main/java/com/rizoma/consola/ConsolaViewModel.kt
+++ b/consola/src/main/java/com/rizoma/consola/ConsolaViewModel.kt
@@ -130,18 +130,27 @@ class ConsolaViewModel(app: Application) : AndroidViewModel(app) {
 
     // ---- Terminal -----------------------------------------------------
 
-    /** Crea una sesión nueva (spawn) y se adjunta. */
+    /**
+     * Crea una sesión nueva (spawn) y se adjunta. El comando corre dentro de un
+     * login shell que **prepende los bin del usuario al PATH** —`~/.npm-global/
+     * bin` (donde vive `claude`), `~/.local/bin`, `~/bin`— porque el daemon
+     * (lanzado por OpenRC) arranca con un PATH mínimo y, en su entorno limpio,
+     * ni los perfiles de shell traen esos directorios. El `exec` reemplaza al
+     * shell para que cerrar el programa cierre la sesión.
+     */
     fun spawn(command: String, cwd: String, label: String) {
-        val parts = command.trim().split(Regex("\\s+")).filter { it.isNotEmpty() }
-        if (parts.isEmpty()) return
+        val cmd = command.trim()
+        if (cmd.isEmpty()) return
+        val niceLabel = label.ifBlank { cmd.substringBefore(' ').substringAfterLast('/') }
+        val userPath = "\$HOME/.npm-global/bin:\$HOME/.local/bin:\$HOME/bin:\$PATH"
         startTerminal { svc ->
             svc.openTerminal(
                 session = null,
-                program = parts.first(),
-                args = parts.drop(1),
+                program = "bash",
+                args = listOf("-lc", "export PATH=\"$userPath\"; exec $cmd"),
                 cwd = cwd.ifBlank { "." },
-                label = label,
-                title = label.ifBlank { command },
+                label = niceLabel,
+                title = niceLabel.ifBlank { cmd },
             )
         }
     }
diff --git a/consola/src/main/java/com/rizoma/consola/ui/SessionListScreen.kt b/consola/src/main/java/com/rizoma/consola/ui/SessionListScreen.kt
index 06c3932..e2d3fb0 100644
--- a/consola/src/main/java/com/rizoma/consola/ui/SessionListScreen.kt
+++ b/consola/src/main/java/com/rizoma/consola/ui/SessionListScreen.kt
@@ -90,18 +90,31 @@ fun SessionListScreen(
                 }
             }
 
-            // Botón "nueva sesión".
-            Text(
-                "+",
-                color = Vacio,
-                modifier = Modifier
-                    .align(Alignment.BottomEnd)
-                    .padding(24.dp)
-                    .clip(RoundedCornerShape(50))
-                    .background(NeonVerde)
-                    .clickable { showSpawn = true }
-                    .padding(horizontal = 22.dp, vertical = 14.dp),
-            )
+            // Acciones flotantes: un Claude de un toque (danger) + el genérico.
+            Row(
+                Modifier.align(Alignment.BottomEnd).padding(24.dp),
+                horizontalArrangement = Arrangement.spacedBy(12.dp),
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                Text(
+                    "+ claude",
+                    color = Vacio,
+                    modifier = Modifier
+                        .clip(RoundedCornerShape(50))
+                        .background(NeonVerde)
+                        .clickable { onSpawn(CLAUDE_CMD, CLAUDE_CWD, "claude") }
+                        .padding(horizontal = 18.dp, vertical = 14.dp),
+                )
+                Text(
+                    "+",
+                    color = NeonVerde,
+                    modifier = Modifier
+                        .clip(RoundedCornerShape(50))
+                        .background(Superficie)
+                        .clickable { showSpawn = true }
+                        .padding(horizontal = 18.dp, vertical = 14.dp),
+                )
+            }
         }
     }
 
@@ -161,10 +174,14 @@ private fun meta(s: PtySession): String {
     return "$estado · ${s.cols}×${s.rows}$adj$dir"
 }
 
+private const val CLAUDE_CMD = "claude --dangerously-skip-permissions"
+private const val CLAUDE_CWD = "/home/sergio"
+
 @Composable
 private fun SpawnDialog(onDismiss: () -> Unit, onCreate: (String, String, String) -> Unit) {
-    var command by remember { mutableStateOf("bash") }
-    var cwd by remember { mutableStateOf(".") }
+    // Claudificado: por defecto un Claude en modo danger; editable para un shell.
+    var command by remember { mutableStateOf(CLAUDE_CMD) }
+    var cwd by remember { mutableStateOf(CLAUDE_CWD) }
     var label by remember { mutableStateOf("") }
 
     AlertDialog(
diff --git a/consola/src/main/java/com/rizoma/consola/ui/TerminalScreen.kt b/consola/src/main/java/com/rizoma/consola/ui/TerminalScreen.kt
index 5073d36..1aa75f4 100644
--- a/consola/src/main/java/com/rizoma/consola/ui/TerminalScreen.kt
+++ b/consola/src/main/java/com/rizoma/consola/ui/TerminalScreen.kt
@@ -83,6 +83,20 @@ fun TerminalScreen(
     // Abre la bitácora sola cuando algo falla: ahí es cuando hace falta verla.
     LaunchedEffect(status) { if (status == TermStatus.Error) showDebug = true }
 
+    // Modificadores pegajosos: se arman con un toque y se aplican a la SIGUIENTE
+    // tecla escrita (ctrl+letra, alt+letra), como en un teclado de terminal.
+    var ctrlArmed by remember { mutableStateOf(false) }
+    var altArmed by remember { mutableStateOf(false) }
+    val emitText: (String) -> Unit = { typed ->
+        if (ctrlArmed || altArmed) {
+            onBytes(applyMods(typed, ctrlArmed, altArmed))
+            ctrlArmed = false
+            altArmed = false
+        } else {
+            onText(typed.replace('\n', '\r'))
+        }
+    }
+
     Column(Modifier.fillMaxSize().background(Vacio)) {
         // Barra superior.
         Row(
@@ -116,11 +130,18 @@ fun TerminalScreen(
         ) {
             TerminalCanvas(frame = frame, onSized = onSized)
             // Campo oculto que captura las teclas del teclado virtual.
-            HiddenInput(focus = focus, onText = onText, onBytes = onBytes)
+            HiddenInput(focus = focus, onText = emitText, onBytes = onBytes)
         }
 
         // Fila de teclas especiales (lo que un teclado virtual no manda).
-        FunctionKeyRow(onBytes = onBytes, onFocus = { focus.requestFocus() })
+        FunctionKeyRow(
+            onBytes = onBytes,
+            onFocus = { focus.requestFocus() },
+            ctrlArmed = ctrlArmed,
+            altArmed = altArmed,
+            onToggleCtrl = { ctrlArmed = !ctrlArmed; altArmed = false },
+            onToggleAlt = { altArmed = !altArmed; ctrlArmed = false },
+        )
     }
 }
 
@@ -275,8 +296,8 @@ private fun HiddenInput(
         onValueChange = { nv ->
             val typed = nv.text
             if (typed.isNotEmpty()) {
-                // Enter del IME llega como '\n'; el terminal quiere CR.
-                onText(typed.replace('\n', '\r'))
+                // El padre (emitText) aplica modificadores y traduce '\n'→CR.
+                onText(typed)
             }
             // Reset: el campo solo sirve para capturar deltas, no acumula.
             value = TextFieldValue("")
@@ -305,7 +326,14 @@ private fun HiddenInput(
 }
 
 @Composable
-private fun FunctionKeyRow(onBytes: (ByteArray) -> Unit, onFocus: () -> Unit) {
+private fun FunctionKeyRow(
+    onBytes: (ByteArray) -> Unit,
+    onFocus: () -> Unit,
+    ctrlArmed: Boolean,
+    altArmed: Boolean,
+    onToggleCtrl: () -> Unit,
+    onToggleAlt: () -> Unit,
+) {
     val scroll = rememberScrollState()
     Row(
         Modifier
@@ -316,6 +344,9 @@ private fun FunctionKeyRow(onBytes: (ByteArray) -> Unit, onFocus: () -> Unit) {
         horizontalArrangement = Arrangement.spacedBy(6.dp),
         verticalAlignment = Alignment.CenterVertically,
     ) {
+        // Modificadores pegajosos: se iluminan cuando están armados.
+        KeyCap("ctrl", accent = if (ctrlArmed) Vacio else NeonRosa, fill = if (ctrlArmed) NeonRosa else Superficie) { onToggleCtrl() }
+        KeyCap("alt", accent = if (altArmed) Vacio else NeonRosa, fill = if (altArmed) NeonRosa else Superficie) { onToggleAlt() }
         KeyCap("esc") { onBytes(byteArrayOf(0x1b)) }
         KeyCap("tab") { onBytes(byteArrayOf(0x09)) }
         KeyCap("^C") { onBytes(byteArrayOf(0x03)) }
@@ -325,6 +356,10 @@ private fun FunctionKeyRow(onBytes: (ByteArray) -> Unit, onFocus: () -> Unit) {
         KeyCap("↑") { onBytes(esc('A')) }
         KeyCap("↓") { onBytes(esc('B')) }
         KeyCap("→") { onBytes(esc('C')) }
+        KeyCap("PgUp") { onBytes(csi("5~")) }
+        KeyCap("PgDn") { onBytes(csi("6~")) }
+        KeyCap("Home") { onBytes(csi("H")) }
+        KeyCap("End") { onBytes(csi("F")) }
         KeyCap("⌫") { onBytes(byteArrayOf(0x7f)) }
         KeyCap("↩") { onBytes(byteArrayOf(0x0d)) }
         KeyCap("⌨", accent = NeonCian) { onFocus() }
@@ -332,12 +367,12 @@ private fun FunctionKeyRow(onBytes: (ByteArray) -> Unit, onFocus: () -> Unit) {
 }
 
 @Composable
-private fun KeyCap(label: String, accent: Color = NeonVerde, onClick: () -> Unit) {
+private fun KeyCap(label: String, accent: Color = NeonVerde, fill: Color = Superficie, onClick: () -> Unit) {
     Text(
         label,
         color = accent,
         modifier = Modifier
-            .background(Superficie)
+            .background(fill)
             .clickable { onClick() }
             .padding(horizontal = 12.dp, vertical = 8.dp),
     )
@@ -345,6 +380,26 @@ private fun KeyCap(label: String, accent: Color = NeonVerde, onClick: () -> Unit
 
 private fun esc(final: Char): ByteArray = byteArrayOf(0x1b, '['.code.toByte(), final.code.toByte())
 
+/** Secuencia CSI (`ESC [` + [tail]), p.ej. `csi("5~")` = PgUp. */
+private fun csi(tail: String): ByteArray =
+    byteArrayOf(0x1b, '['.code.toByte()) + tail.toByteArray(Charsets.US_ASCII)
+
+/**
+ * Aplica los modificadores pegajosos al texto escrito. ctrl pliega la primera
+ * letra a su carácter de control (ctrl+A=0x01…); alt antepone ESC. El resto
+ * del texto va literal.
+ */
+private fun applyMods(text: String, ctrl: Boolean, alt: Boolean): ByteArray {
+    if (text.isEmpty()) return ByteArray(0)
+    val out = ArrayList<Byte>(text.length + 1)
+    if (alt) out.add(0x1b)
+    val first = text[0]
+    val b = if (ctrl) (first.uppercaseChar().code and 0x1f) else first.code
+    out.add(b.toByte())
+    for (i in 1 until text.length) out.add(text[i].code.toByte())
+    return out.toByteArray()
+}
+
 private fun statusLine(status: TermStatus, message: String?): String = when (status) {
     TermStatus.Connecting -> "conectando…"
     TermStatus.Live -> "en vivo"