feat(installer): enhance NixOS installer with SSH access and encryption password input

This commit is contained in:
2026-07-02 14:17:49 -04:00
committed by Richie
parent b252b0bf6e
commit ddea13b93a
4 changed files with 125 additions and 15 deletions
+72 -10
View File
@@ -10,6 +10,8 @@ from python.process import run_output
logger = logging.getLogger(__name__)
BYTE_MAX = 255
class Cursor:
"""Cursor class."""
@@ -101,6 +103,9 @@ class State:
self.reserve_size = 0
self.show_reserve_input = False
self.encryption_password = None
self.show_encryption_password_input = False
self.selected_device_ids: set[str] = set()
def get_selected_devices(self) -> tuple[str, ...]:
@@ -155,7 +160,22 @@ def debug_menu(std_screen: curses.window, key: int) -> None:
std_screen.addstr(height - 2, i * 3, f"{i}██", curses.color_pair(i))
def get_text_input(std_screen: curses.window, prompt: str, y: int, x: int) -> str:
def draw_input_line(std_screen: curses.window, prompt: str, input_str: str, y: int, x: int, mask: str | None) -> None:
"""Draw an input line without leaking masked values."""
_, width = std_screen.getmaxyx()
displayed_input = input_str if mask is None else mask * len(input_str)
line = f"{prompt}{displayed_input}"
available_width = max(0, width - x - 1)
std_screen.move(y, x)
if available_width > 0:
std_screen.addstr(y, x, line[:available_width])
std_screen.clrtoeol()
std_screen.move(y, min(x + len(line), width - 1))
std_screen.refresh()
def get_text_input(std_screen: curses.window, prompt: str, y: int, x: int, mask: str | None = None) -> str | None:
"""Get text input.
Args:
@@ -163,27 +183,27 @@ def get_text_input(std_screen: curses.window, prompt: str, y: int, x: int) -> st
prompt (str): The prompt.
y (int): The y position.
x (int): The x position.
mask (str | None, optional): The character used to mask displayed input. Defaults to None.
Returns:
str: The input string.
str | None: The input string, or None if input was cancelled.
"""
esc_key = 27
curses.echo()
std_screen.addstr(y, x, prompt)
curses.noecho()
input_str = ""
draw_input_line(std_screen, prompt, input_str, y, x, mask)
while True:
key = std_screen.getch()
if key == ord("\n"):
if key in (ord("\n"), ord("\r")):
break
if key == esc_key:
input_str = ""
break
curses.noecho()
return None
if key in (curses.KEY_BACKSPACE, ord("\b"), 127):
input_str = input_str[:-1]
std_screen.addstr(y, x + len(prompt), input_str + " ")
else:
elif 0 <= key <= BYTE_MAX:
input_str += chr(key)
std_screen.refresh()
draw_input_line(std_screen, prompt, input_str, y, x, mask)
curses.noecho()
return input_str
@@ -210,6 +230,9 @@ def swap_size_input(
if state.show_swap_input:
swap_size_str = get_text_input(std_screen, swap_size_text, swap_offset, 0)
if swap_size_str is None:
state.show_swap_input = False
return state
try:
state.swap_size = int(swap_size_str)
state.show_swap_input = False
@@ -243,6 +266,9 @@ def reserve_size_input(
if state.show_reserve_input:
reserve_size_str = get_text_input(std_screen, reserve_size_text, reserve_offset, 0)
if reserve_size_str is None:
state.show_reserve_input = False
return state
try:
state.reserve_size = int(reserve_size_str)
state.show_reserve_input = False
@@ -254,6 +280,37 @@ def reserve_size_input(
return state
def encryption_password_input(
std_screen: curses.window,
state: State,
password_offset: int,
) -> State:
"""Encryption password input.
Args:
std_screen (curses.window): The curses window.
state (State): The state object.
password_offset (int): The password offset.
Returns:
State: The updated state object.
"""
password_status = "set" if state.encryption_password else "unset"
encryption_label = "Encryption password: "
encryption_prompt = "Encryption password (blank disables LUKS): "
std_screen.addstr(password_offset, 0, f"{encryption_label}{password_status}")
if state.key == ord("\n") and state.cursor.get_y() == password_offset:
state.show_encryption_password_input = True
if state.show_encryption_password_input:
password = get_text_input(std_screen, encryption_prompt, password_offset, 0)
if password is not None:
state.encryption_password = password
state.show_encryption_password_input = False
return state
def status_bar(
std_screen: curses.window,
cursor: Cursor,
@@ -464,6 +521,11 @@ def draw_menu(std_screen: curses.window) -> State:
state=state,
reserve_offset=swap_offset + 1,
)
encryption_password_input(
std_screen=std_screen,
state=state,
password_offset=swap_offset + 2,
)
status_bar(std_screen, state.cursor, width, height)