diff --git a/nixosModules/clanCore/vars/interface.nix b/nixosModules/clanCore/vars/interface.nix index 81453fd15..c34ada565 100644 --- a/nixosModules/clanCore/vars/interface.nix +++ b/nixosModules/clanCore/vars/interface.nix @@ -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"; }; diff --git a/pkgs/clan-cli/clan_cli/vars/prompt.py b/pkgs/clan-cli/clan_cli/vars/prompt.py index bd47e53b6..f1a3aa0da 100644 --- a/pkgs/clan-cli/clan_cli/vars/prompt.py +++ b/pkgs/clan-cli/clan_cli/vars/prompt.py @@ -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): ") diff --git a/pkgs/clan-cli/clan_cli/vars/set.py b/pkgs/clan-cli/clan_cli/vars/set.py index ccef00e92..dd9163299 100644 --- a/pkgs/clan-cli/clan_cli/vars/set.py +++ b/pkgs/clan-cli/clan_cli/vars/set.py @@ -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: diff --git a/pkgs/webview-ui/app/src/routes/machines/install/vars-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/vars-step.tsx index 044337d30..3796b8c7b 100644 --- a/pkgs/webview-ui/app/src/routes/machines/install/vars-step.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/install/vars-step.tsx @@ -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) => { {/* Avoid nesting issue in case of a "." */} {(field, props) => ( - + + } + > + +