Fix interactive input by using Console.readLine instead of Process.exec

Process.exec uses popen(cmd, "r") which creates a read-only pipe where
the subprocess stdin is disconnected from the terminal. Console.readLine
reads directly from the parent process stdin via fgets, fixing all
interactive prompts (askConfirm, askInput, choice menus).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 23:48:33 -05:00
parent d30d2efa4e
commit 4a5fa4415c

View File

@@ -128,10 +128,10 @@ fn printStatusLine(name: String, state: String, detail: String): Unit with {Proc
} }
// Ask for confirmation: returns true if user confirms // Ask for confirmation: returns true if user confirms
fn askConfirm(question: String, defaultYes: Bool): Bool with {Process} = { fn askConfirm(question: String, defaultYes: Bool): Bool with {Console, Process} = {
let prompt = if defaultYes then " [Y/n] " else " [y/N] " let prompt = if defaultYes then " [Y/n] " else " [y/N] "
let ignore = Process.exec("printf '%s%s' \"" + question + "\" \"" + prompt + "\" >&2") let ignore = Process.exec("printf '%s%s' \"" + question + "\" \"" + prompt + "\" >&2")
let response = String.trim(Process.exec("head -n1 </dev/tty")) let response = String.trim(Console.readLine())
let lower = execQuiet("echo '" + response + "' | tr A-Z a-z") let lower = execQuiet("echo '" + response + "' | tr A-Z a-z")
if String.length(lower) == 0 then if String.length(lower) == 0 then
defaultYes defaultYes
@@ -148,13 +148,13 @@ fn askConfirm(question: String, defaultYes: Bool): Bool with {Process} = {
} }
// Ask for text input with a default value // Ask for text input with a default value
fn askInput(prompt: String, defaultVal: String): String with {Process} = { fn askInput(prompt: String, defaultVal: String): String with {Console, Process} = {
let display = if String.length(defaultVal) > 0 then let display = if String.length(defaultVal) > 0 then
"printf '%s [%s]: ' \"" + prompt + "\" \"" + defaultVal + "\" >&2" "printf '%s [%s]: ' \"" + prompt + "\" \"" + defaultVal + "\" >&2"
else else
"printf '%s: ' \"" + prompt + "\" >&2" "printf '%s: ' \"" + prompt + "\" >&2"
let ignore = Process.exec(display) let ignore = Process.exec(display)
let input = String.trim(Process.exec("head -n1 </dev/tty")) let input = String.trim(Console.readLine())
if String.length(input) == 0 then defaultVal else input if String.length(input) == 0 then defaultVal else input
} }
@@ -302,7 +302,7 @@ fn showMiniLogo(): Unit with {Process} = {
// Welcome flow (Interactive) // Welcome flow (Interactive)
// ============================================================================= // =============================================================================
fn showInteractiveWelcome(): Unit with {Process} = { fn showInteractiveWelcome(): Unit with {Console, Process} = {
print("") print("")
showLogo() showLogo()
print("") print("")
@@ -325,7 +325,7 @@ fn showInteractiveWelcome(): Unit with {Process} = {
print("") print("")
let ignore8 = Process.exec("printf 'Choice [1]: ' >&2") let ignore8 = Process.exec("printf 'Choice [1]: ' >&2")
let choice = String.trim(Process.exec("head -n1 </dev/tty")) let choice = String.trim(Console.readLine())
match choice { match choice {
"2" => { "2" => {
@@ -765,7 +765,7 @@ fn doServerUnmount(): Unit with {Process} = {
// Export/Import // Export/Import
// ============================================================================= // =============================================================================
fn doExport(): Unit with {Process} = { fn doExport(): Unit with {Console, Process} = {
print("") print("")
let timestamp = execQuiet("date +%Y-%m-%d") let timestamp = execQuiet("date +%Y-%m-%d")
let exportFile = "pal-export-" + timestamp + ".tar.zst" let exportFile = "pal-export-" + timestamp + ".tar.zst"
@@ -790,7 +790,7 @@ fn doExport(): Unit with {Process} = {
print("") print("")
} }
fn doImport(archivePath: String): Unit with {Process} = { fn doImport(archivePath: String): Unit with {Console, Process} = {
print("") print("")
if String.length(archivePath) == 0 then { if String.length(archivePath) == 0 then {
@@ -974,7 +974,7 @@ fn showCompactStatus(): Unit with {Process} = {
// Health check (default command) // Health check (default command)
// ============================================================================= // =============================================================================
fn showHealthCheck(): Unit with {Process} = { fn showHealthCheck(): Unit with {Console, Process} = {
// Check if this is an interactive terminal // Check if this is an interactive terminal
let isTty = execQuiet("test -t 0 && echo yes || echo no") let isTty = execQuiet("test -t 0 && echo yes || echo no")
@@ -1225,7 +1225,7 @@ fn showOnboardSummary(deviceName: String): Unit with {Process} = {
print("") print("")
} }
fn doOnboardSteps(): Unit with {Process} = { fn doOnboardSteps(): Unit with {Console, Process} = {
// ── Step 1: Device ────────────────────────────────────────────────── // ── Step 1: Device ──────────────────────────────────────────────────
let ignore = Process.exec("printf '\\033[1m── Step 1: Device ──\\033[0m\\n\\n' >&2") let ignore = Process.exec("printf '\\033[1m── Step 1: Device ──\\033[0m\\n\\n' >&2")
@@ -1273,7 +1273,7 @@ fn doOnboardSteps(): Unit with {Process} = {
print("") print("")
let ignore8 = Process.exec("printf 'Choice [3]: ' >&2") let ignore8 = Process.exec("printf 'Choice [3]: ' >&2")
let configChoice = String.trim(Process.exec("head -n1 </dev/tty")) let configChoice = String.trim(Console.readLine())
print("") print("")
match configChoice { match configChoice {
@@ -1451,7 +1451,7 @@ fn doOnboardSteps(): Unit with {Process} = {
} else () } else ()
} }
fn doOnboard(): Unit with {Process} = { fn doOnboard(): Unit with {Console, Process} = {
print("") print("")
if isInitialized() then { if isInitialized() then {
@@ -1620,7 +1620,7 @@ fn showServerHelp(): Unit with {Process} = {
// Main // Main
// ============================================================================= // =============================================================================
fn main(): Unit with {Process} = { fn main(): Unit with {Console, Process} = {
let args = Process.args() let args = Process.args()
let cmd = match List.get(args, 1) { let cmd = match List.get(args, 1) {
Some(c) => c, Some(c) => c,