add multiline-hidden prompt for both ui and cli

This commit is contained in:
Jörg Thalheim
2025-05-13 17:50:41 +02:00
parent 0e50e47f16
commit d397c8ad39
4 changed files with 106 additions and 19 deletions

View File

@@ -364,11 +364,13 @@ in
- hidden: A hidden text (e.g. password)
- line: A single line of text
- multiline: A multiline text
- multiline-hidden: A multiline text
'';
type = enum [
"hidden"
"line"
"multiline"
"multiline-hidden"
];
default = "line";
};

View File

@@ -1,6 +1,8 @@
import enum
import logging
import sys
import termios
import tty
from dataclasses import dataclass
from getpass import getpass
from typing import Any
@@ -15,6 +17,7 @@ class PromptType(enum.Enum):
LINE = "line"
HIDDEN = "hidden"
MULTILINE = "multiline"
MULTILINE_HIDDEN = "multiline-hidden"
@dataclass
@@ -36,6 +39,63 @@ class Prompt:
)
def get_multiline_hidden_input() -> str:
"""
Get multiline input from the user without echoing the input.
This function allows the user to enter multiple lines of text,
and it will return the concatenated string of all lines entered.
The user can finish the input by pressing Ctrl-D (EOF).
"""
# Save terminal settings
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
lines = []
current_line: list[str] = []
try:
# Change terminal settings - disable echo
tty.setraw(fd)
while True:
char = sys.stdin.read(1)
# Check for Ctrl-D (ASCII value 4 or EOF)
if not char or ord(char) == 4:
# Add last line if not empty
if current_line:
lines.append("".join(current_line))
break
# Check for Ctrl-C (KeyboardInterrupt)
if ord(char) == 3:
raise KeyboardInterrupt
# Handle Enter key
if char == "\r" or char == "\n":
lines.append("".join(current_line))
current_line = []
# Print newline for visual feedback
sys.stdout.write("\r\n")
sys.stdout.flush()
# Handle backspace
elif ord(char) == 127 or ord(char) == 8:
if current_line:
current_line.pop()
# Regular character
else:
current_line.append(char)
finally:
# Restore terminal settings
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
# Print a final newline for clean display
print()
return "\n".join(lines)
def ask(
ident: str,
input_type: PromptType,
@@ -53,6 +113,9 @@ def ask(
case PromptType.MULTILINE:
print(f"{text} (Finish with Ctrl-D): ")
result = sys.stdin.read()
case PromptType.MULTILINE_HIDDEN:
print("Enter multiple lines (press Ctrl-D to finish or Ctrl-C to cancel):")
result = get_multiline_hidden_input()
case PromptType.HIDDEN:
result = getpass(f"{text} (hidden): ")

View File

@@ -33,19 +33,19 @@ def set_var(machine: str | Machine, var: str | Var, value: bytes, flake: Flake)
)
def set_via_stdin(machine: str, var_id: str, flake: Flake) -> None:
var = get_var(str(flake.path), machine, var_id)
def set_via_stdin(machine_name: str, var_id: str, flake: Flake) -> None:
machine = Machine(name=machine_name, flake=flake)
var = get_var(str(flake.path), machine_name, var_id)
if sys.stdin.isatty():
new_value = ask(
var.id,
PromptType.HIDDEN,
PromptType.MULTILINE_HIDDEN,
None,
).encode("utf-8")
else:
new_value = sys.stdin.buffer.read()
_machine = Machine(name=machine, flake=flake)
set_var(_machine, var, new_value, flake)
set_var(machine, var, new_value, flake)
def _set_command(args: argparse.Namespace) -> None:

View File

@@ -1,9 +1,9 @@
import { callApi } from "@/src/api";
import {
createForm,
FieldValues,
SubmitHandler,
validate,
FieldValues,
} from "@modular-forms/solid";
import { createQuery, useQueryClient } from "@tanstack/solid-query";
import { Typography } from "@/src/components/Typography";
@@ -113,21 +113,43 @@ export const VarsStep = (props: VarsStepProps) => {
</Typography>
{/* Avoid nesting issue in case of a "." */}
<Field
name={`${generator.name.replaceAll(".", "__dot__")}.${prompt.name.replaceAll(".", "__dot__")}`}
name={`${generator.name.replaceAll(
".",
"__dot__",
)}.${prompt.name.replaceAll(".", "__dot__")}`}
>
{(field, props) => (
<TextInput
inputProps={{
...props,
type:
prompt.prompt_type === "hidden"
? "password"
: "text",
}}
label={prompt.description}
value={prompt.previous_value ?? ""}
error={field.error}
/>
<Switch
fallback={
<TextInput
inputProps={{
...props,
type:
prompt.prompt_type === "hidden"
? "password"
: "text",
}}
label={prompt.description}
value={prompt.previous_value ?? ""}
error={field.error}
/>
}
>
<Match
when={
prompt.prompt_type === "multiline" ||
prompt.prompt_type === "multiline-hidden"
}
>
<textarea
{...props}
class="w-full h-32 border border-gray-300 rounded-md p-2"
placeholder={prompt.description}
value={prompt.previous_value ?? ""}
name={prompt.description}
/>
</Match>
</Switch>
)}
</Field>
</Group>