155 lines
7.8 KiB
Markdown
155 lines
7.8 KiB
Markdown
# 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`:
|
|
|
|
```rust
|
|
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
|
|
```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:
|
|
|
|
```rust
|
|
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 |