Files
signer/plans/port_tui_continuous.md
T

7.8 KiB

Port tui_continuous C library to Rust

Overview

Port the vendored C library resources/tui_continuous/tui_continuous.c (536 lines, ~16 public functions) to Rust as a standalone, well-documented module. Keep it separate from the main tui.rs so it can be spun out as its own crate later.

File structure

src/
  tui_continuous.rs     <-- The ported library (new file)
  tui.rs                <-- Existing app-level TUI code (will call tui_continuous)

The existing tui.rs will be refactored to call tui_continuous primitives instead of using raw println!/tprint!/crossterm.

C API surface (16 functions)

Types to port

C type Rust equivalent
TuiSize { width, height } pub struct TuiSize { pub width: u16, pub height: u16 }
TuiMenuItem { label, shortcut } pub struct TuiMenuItem { pub label: &'static str, pub shortcut: char }
TuiFrame { app_name, app_version, breadcrumb } pub struct TuiFrame { pub app_name: &'static str, pub app_version: &'static str, pub breadcrumb: &'static str }
TuiMenu { items, count } pub struct TuiMenu<'a> { pub items: &'a [TuiMenuItem] }
TuiStatus { text } pub struct TuiStatus<'a> { pub text: Option<&'a str> }
TuiColumn { name, width, right_align } pub struct TuiColumn { pub name: &'static str, pub width: u16, pub right_align: bool }
TuiTable { columns, get_cell, ... } pub struct TuiTable<'a, F> { pub columns: &'a [TuiColumn], pub row_count: usize, pub get_cell: F } where F: Fn(usize, usize, &mut [u8])

Functions to port (in order)

# C function Rust signature Notes
1 tui_terminal_size() pub fn terminal_size() -> TuiSize Use crossterm::terminal::size()
2 tui_install_resize_handler() pub fn install_resize_handler() Register SIGWINCH → set g_resize_pending flag
3 tui_resize_pending() pub fn resize_pending() -> bool Check and clear g_resize_pending
4 tui_init() pub fn init() Calls crossterm::terminal::enable_raw_mode()
5 tui_cleanup() pub fn cleanup() Calls crossterm::terminal::disable_raw_mode()
6 tui_get_key() pub fn get_key() -> Result<TuiKey, Error> Returns TuiKey::Char(c), TuiKey::Resize, TuiKey::Eof
7 tui_print() pub fn print(fmt: std::fmt::Arguments) Parse ^_→underline, ^*→bold, ^:→reset, ^^→literal ^
8 tui_clear_continuous() pub fn clear_continuous(term_height: u16) ANSI escape sequence to clear scrollback region
9 tui_render_top_frame() pub fn render_top_frame(frame: &TuiFrame) Draw ==== header with centered title + breadcrumb
10 tui_menu_left_col() pub fn menu_left_col(frame: &TuiFrame) -> u16 Compute left column for centered menu
11 tui_render_menu() pub fn render_menu(menu: &TuiMenu, left_col: u16) Render each menu item via tui_print()
12 tui_render_status() pub fn render_status_line(status: &TuiStatus) Print status text if non-empty
13 tui_anchor_prompt() pub fn anchor_prompt(filler_lines: u16, left_col: u16) Fill blank lines and position cursor
14 tui_render_content_screen() pub fn render_content_screen(frame: &TuiFrame, title: Option<&str>) clear_continuous + render_top_frame + optional title
15 tui_render_screen() pub fn render_screen(frame: &TuiFrame, menu: Option<&TuiMenu>, status: Option<&TuiStatus>) Full screen layout with filler lines
16 tui_render_table() pub fn render_table(table: &TuiTable) Column-aligned table with compact mode fallback
17 tui_read_line() pub fn read_line(buf: &mut [u8]) -> Result<usize, Error> Read line with fgets-like semantics (cooked mode)
18 tui_is_escape_input() pub fn is_escape_input(input: &str) -> bool Check for q/x/exit/quit/esc
19 tui_menu_match_key() pub fn menu_match_key(menu: &TuiMenu, input: &str) -> Option<usize> Match single-char input to menu item shortcut
20 tui_compute_unique_prefixes() pub fn compute_unique_prefixes(ids: &[&str]) -> Vec<usize> Compute minimal unique prefix lengths
21 tui_confirm() pub fn confirm(prompt: &str) -> bool [y/n] with single-key in raw mode, line fallback
22 tui_prompt_default() pub fn prompt_default(prompt: &str, default: &str, out: &mut String) -> io::Result<()> Prompt with default value
23 tui_press_enter() pub fn press_enter(message: Option<&str>) Wait for any key with tui_get_key()
24 tui_has_stdin_pipe() pub fn has_stdin_pipe() -> bool Check isatty(STDIN_FILENO) via libc

Implementation details

Hotkey markup (tui_print)

The ^_X^ → \033[4mX\033[0m (underline) and ^*X^ → \033[1mX\033[0m (bold) markup is central to how the C TUI renders labels. The Rust tui_print must:

  1. Write \r\n at end (raw mode needs explicit CR)
  2. Parse ^_ / ^* / ^: / ^^ sequences
  3. Use \x1b[4m / \x1b[1m / \x1b[0m ANSI escape codes

SIGWINCH handling

The g_resize_pending static flag is set by a signal handler. In Rust, use std::sync::atomic::AtomicBool:

use std::sync::atomic::{AtomicBool, Ordering};
static RESIZE_PENDING: AtomicBool = AtomicBool::new(false);

extern "C" fn handle_sigwinch(_: i32) {
    RESIZE_PENDING.store(true, Ordering::SeqCst);
}

tui_get_key() in Rust

pub enum TuiKey {
    Char(char),
    Eof,
    Resize,
}

pub fn get_key() -> io::Result<TuiKey> {
    use crossterm::event::{read, Event, KeyCode, KeyEvent};
    // Check for SIGWINCH first
    if RESIZE_PENDING.swap(false, Ordering::SeqCst) {
        return Ok(TuiKey::Resize);
    }
    // Block on crossterm::event::read()
    match read()? {
        Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) => Ok(TuiKey::Char(c)),
        Event::Resize(..) => Ok(TuiKey::Resize),
        _ => Ok(TuiKey::Eof),
    }
}

tui_render_table() with callbacks

The C version uses a callback get_cell(row, col, out, out_size, user_data). In Rust, use a closure:

pub struct TuiTable<'a, F: Fn(usize, usize) -> String> {
    pub columns: &'a [TuiColumn],
    pub row_count: usize,
    pub get_cell: F,
}

Refactoring main.rs

After the port is done, update main.rs to:

  1. Remove tui::init()/cleanup() calls — use tui_continuous::init()/cleanup()
  2. Use tui_continuous::render_top_frame() and tui_continuous::render_table() for the status screen
  3. Use tui_continuous::get_key() instead of tui::poll_key()
  4. Add tui_continuous::install_resize_handler() at startup
  5. Add tui_continuous::resize_pending() check in the main loop
  6. Add 'r' (refresh) and 'l' (lock/reunlock) key handlers
  7. Add activity log

Dependencies

No new dependencies needed. Uses:

  • crossterm (already in Cargo.toml) for terminal size, raw mode
  • libc (already in Cargo.toml) for SIGWINCH, isatty
  • std::sync::atomic for resize flag

Phase plan

Phase 1: Core types and utilities

  • TuiSize, TuiMenuItem, TuiFrame, TuiMenu, TuiStatus, TuiColumn, TuiTable types
  • terminal_size(), install_resize_handler(), resize_pending()
  • init(), cleanup(), get_key()

Phase 2: Print and rendering

  • print() with hotkey markup
  • clear_continuous(), render_top_frame(), menu_left_col()
  • render_menu(), render_status_line(), anchor_prompt()
  • render_content_screen(), render_screen()

Phase 3: Table rendering

  • render_table() with compact mode fallback
  • compute_unique_prefixes()

Phase 4: Input helpers

  • read_line(), is_escape_input(), menu_match_key()
  • confirm(), prompt_default(), press_enter(), has_stdin_pipe()

Phase 5: Integration

  • Update main.rs to use tui_continuous
  • Add activity log, lock/reunlock, refresh, SIGWINCH handling
  • Remove or reduce tui.rs to just the high-level app screens