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 a97173f148
commit 75fa7ac609
4 changed files with 106 additions and 19 deletions

View File

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

View File

@@ -1,6 +1,8 @@
import enum import enum
import logging import logging
import sys import sys
import termios
import tty
from dataclasses import dataclass from dataclasses import dataclass
from getpass import getpass from getpass import getpass
from typing import Any from typing import Any
@@ -15,6 +17,7 @@ class PromptType(enum.Enum):
LINE = "line" LINE = "line"
HIDDEN = "hidden" HIDDEN = "hidden"
MULTILINE = "multiline" MULTILINE = "multiline"
MULTILINE_HIDDEN = "multiline-hidden"
@dataclass @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( def ask(
ident: str, ident: str,
input_type: PromptType, input_type: PromptType,
@@ -53,6 +113,9 @@ def ask(
case PromptType.MULTILINE: case PromptType.MULTILINE:
print(f"{text} (Finish with Ctrl-D): ") print(f"{text} (Finish with Ctrl-D): ")
result = sys.stdin.read() 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: case PromptType.HIDDEN:
result = getpass(f"{text} (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: def set_via_stdin(machine_name: str, var_id: str, flake: Flake) -> None:
var = get_var(str(flake.path), machine, var_id) machine = Machine(name=machine_name, flake=flake)
var = get_var(str(flake.path), machine_name, var_id)
if sys.stdin.isatty(): if sys.stdin.isatty():
new_value = ask( new_value = ask(
var.id, var.id,
PromptType.HIDDEN, PromptType.MULTILINE_HIDDEN,
None, None,
).encode("utf-8") ).encode("utf-8")
else: else:
new_value = sys.stdin.buffer.read() 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: def _set_command(args: argparse.Namespace) -> None:

View File

@@ -1,9 +1,9 @@
import { callApi } from "@/src/api"; import { callApi } from "@/src/api";
import { import {
createForm, createForm,
FieldValues,
SubmitHandler, SubmitHandler,
validate, validate,
FieldValues,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { createQuery, useQueryClient } from "@tanstack/solid-query"; import { createQuery, useQueryClient } from "@tanstack/solid-query";
import { Typography } from "@/src/components/Typography"; import { Typography } from "@/src/components/Typography";
@@ -113,9 +113,14 @@ export const VarsStep = (props: VarsStepProps) => {
</Typography> </Typography>
{/* Avoid nesting issue in case of a "." */} {/* Avoid nesting issue in case of a "." */}
<Field <Field
name={`${generator.name.replaceAll(".", "__dot__")}.${prompt.name.replaceAll(".", "__dot__")}`} name={`${generator.name.replaceAll(
".",
"__dot__",
)}.${prompt.name.replaceAll(".", "__dot__")}`}
> >
{(field, props) => ( {(field, props) => (
<Switch
fallback={
<TextInput <TextInput
inputProps={{ inputProps={{
...props, ...props,
@@ -128,6 +133,23 @@ export const VarsStep = (props: VarsStepProps) => {
value={prompt.previous_value ?? ""} value={prompt.previous_value ?? ""}
error={field.error} 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> </Field>
</Group> </Group>