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:
- Write
\r\nat end (raw mode needs explicit CR) - Parse
^_/^*/^:/^^sequences - Use
\x1b[4m/\x1b[1m/\x1b[0mANSI 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:
- Remove
tui::init()/cleanup()calls — usetui_continuous::init()/cleanup() - Use
tui_continuous::render_top_frame()andtui_continuous::render_table()for the status screen - Use
tui_continuous::get_key()instead oftui::poll_key() - Add
tui_continuous::install_resize_handler()at startup - Add
tui_continuous::resize_pending()check in the main loop - Add 'r' (refresh) and 'l' (lock/reunlock) key handlers
- Add activity log
Dependencies
No new dependencies needed. Uses:
crossterm(already in Cargo.toml) for terminal size, raw modelibc(already in Cargo.toml) for SIGWINCH, isattystd::sync::atomicfor resize flag
Phase plan
Phase 1: Core types and utilities
TuiSize,TuiMenuItem,TuiFrame,TuiMenu,TuiStatus,TuiColumn,TuiTabletypesterminal_size(),install_resize_handler(),resize_pending()init(),cleanup(),get_key()
Phase 2: Print and rendering
print()with hotkey markupclear_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 fallbackcompute_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.rsto usetui_continuous - Add activity log, lock/reunlock, refresh, SIGWINCH handling
- Remove or reduce
tui.rsto just the high-level app screens