Add terminal builtins for raw input mode #276

Closed
opened 2026-01-18 17:06:08 +00:00 by navicore · 2 comments
navicore commented 2026-01-18 17:06:08 +00:00 (Migrated from github.com)

Summary

Add built-in terminal functions to the runtime for raw input mode, enabling vim-style line editing and TUI applications in pure Seq.

Proposed API

terminal.raw-mode   ( Bool -- )     # Enable/disable raw mode
terminal.read-char  ( -- Int )      # Blocking read single byte (0-255, -1 on EOF)
terminal.read-char? ( -- Int )      # Non-blocking read (-1 if none available)
terminal.width      ( -- Int )      # Terminal width in columns
terminal.height     ( -- Int )      # Terminal height in rows
terminal.flush      ( -- )          # Flush stdout

Use Cases

  • Vim-style line editing in pure Seq (seq-lisp REPL)
  • TUI applications
  • Games with keyboard input
  • Single-key command interfaces

Implementation Notes

Add to crates/runtime/src/ using direct libc calls:

  • tcgetattr / tcsetattr for raw mode
  • read(STDIN_FILENO, ...) for character input
  • fcntl for non-blocking mode
  • ioctl(TIOCGWINSZ) for terminal size

Safety considerations:

  • Install atexit handler to restore terminal on exit
  • Signal handlers for SIGINT/SIGTERM to restore terminal

Why Builtins vs FFI

Originally considered as FFI, but that required users to build and distribute a custom shared library. Builtins are the right approach because:

  • Just works - no library path management
  • Consistent with other I/O operations (io.write-line, etc.)
  • Small footprint (~200 lines of Rust)
  • Issue #274 (original request for terminal FFI - superseded by this approach)
## Summary Add built-in terminal functions to the runtime for raw input mode, enabling vim-style line editing and TUI applications in pure Seq. ## Proposed API ```seq terminal.raw-mode ( Bool -- ) # Enable/disable raw mode terminal.read-char ( -- Int ) # Blocking read single byte (0-255, -1 on EOF) terminal.read-char? ( -- Int ) # Non-blocking read (-1 if none available) terminal.width ( -- Int ) # Terminal width in columns terminal.height ( -- Int ) # Terminal height in rows terminal.flush ( -- ) # Flush stdout ``` ## Use Cases - Vim-style line editing in pure Seq (seq-lisp REPL) - TUI applications - Games with keyboard input - Single-key command interfaces ## Implementation Notes Add to `crates/runtime/src/` using direct `libc` calls: - `tcgetattr` / `tcsetattr` for raw mode - `read(STDIN_FILENO, ...)` for character input - `fcntl` for non-blocking mode - `ioctl(TIOCGWINSZ)` for terminal size Safety considerations: - Install `atexit` handler to restore terminal on exit - Signal handlers for SIGINT/SIGTERM to restore terminal ## Why Builtins vs FFI Originally considered as FFI, but that required users to build and distribute a custom shared library. Builtins are the right approach because: - Just works - no library path management - Consistent with other I/O operations (`io.write-line`, etc.) - Small footprint (~200 lines of Rust) ## Related - Issue #274 (original request for terminal FFI - superseded by this approach)
navicore commented 2026-01-18 17:18:01 +00:00 (Migrated from github.com)

Tests

Add to crates/runtime/src/terminal.rs (or similar):

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_terminal_size_not_tty() {
        // When not a TTY, should return 0x0
        // (CI environments typically aren't TTYs)
    }

    #[test]
    fn test_raw_mode_toggle() {
        // Enable then disable - should not panic
        // Can't fully test without TTY, but can verify no crash
    }

    #[test]
    fn test_read_char_nonblock_no_input() {
        // Non-blocking read with no input should return -1
    }
}

Note: Full interactive testing requires a TTY, so unit tests are limited. Manual testing with the example is important.

Example

Add examples/terminal-demo.seq:

# Terminal raw mode demo
# Run: seqc build examples/terminal-demo.seq -o terminal-demo && ./terminal-demo

# Print a key code
: print-key ( Int -- )
  dup int->string io.write
  " = '" io.write
  char->string io.write
  "'" io.write-line ;

# Show terminal info
: show-info ( -- )
  "Terminal size: " io.write
  terminal.width int->string io.write
  "x" io.write
  terminal.height int->string io.write-line
  "" io.write-line
  "Press keys to see their codes." io.write-line
  "Press 'q' to quit, ESC=27, arrows start with 27." io.write-line
  "" io.write-line ;

# Main input loop (recursive)
: input-loop ( -- )
  terminal.read-char
  dup 113 i.= if    # 'q' = quit
    drop
    "" io.write-line
    "Goodbye!" io.write-line
  else
    print-key
    input-loop
  then ;

: main ( -- )
  show-info
  true terminal.raw-mode
  input-loop
  false terminal.raw-mode ;

No FFI includes, no library paths - just works.

## Tests Add to `crates/runtime/src/terminal.rs` (or similar): ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_terminal_size_not_tty() { // When not a TTY, should return 0x0 // (CI environments typically aren't TTYs) } #[test] fn test_raw_mode_toggle() { // Enable then disable - should not panic // Can't fully test without TTY, but can verify no crash } #[test] fn test_read_char_nonblock_no_input() { // Non-blocking read with no input should return -1 } } ``` Note: Full interactive testing requires a TTY, so unit tests are limited. Manual testing with the example is important. ## Example Add `examples/terminal-demo.seq`: ```seq # Terminal raw mode demo # Run: seqc build examples/terminal-demo.seq -o terminal-demo && ./terminal-demo # Print a key code : print-key ( Int -- ) dup int->string io.write " = '" io.write char->string io.write "'" io.write-line ; # Show terminal info : show-info ( -- ) "Terminal size: " io.write terminal.width int->string io.write "x" io.write terminal.height int->string io.write-line "" io.write-line "Press keys to see their codes." io.write-line "Press 'q' to quit, ESC=27, arrows start with 27." io.write-line "" io.write-line ; # Main input loop (recursive) : input-loop ( -- ) terminal.read-char dup 113 i.= if # 'q' = quit drop "" io.write-line "Goodbye!" io.write-line else print-key input-loop then ; : main ( -- ) show-info true terminal.raw-mode input-loop false terminal.raw-mode ; ``` No FFI includes, no library paths - just works.
navicore commented 2026-01-18 18:00:17 +00:00 (Migrated from github.com)
https://github.com/navicore/patch-seq/pull/277
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-seq#276
No description provided.