Files
kiln/kiln-tui/src/main.rs
T

113 lines
4.2 KiB
Rust
Raw Normal View History

//! `kiln-tui` — a terminal "player" for kiln boards.
//!
//! Pass a `.toml` map file as the single command-line argument; kiln-tui loads
//! that board and lets you walk the player around it with the arrow keys
//! (or HJKL). There is no editor — this is a play-only front-end.
//!
//! Rendering is text-based: kiln's bitmap-font tile indices are reinterpreted
//! as characters (see [`cp437`]) and drawn with each cell's RGB colors.
mod cp437;
mod render;
mod term;
use kiln_core::game::GameState;
use kiln_core::map_file;
use ratatui::Frame;
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::widgets::Block;
use render::BoardWidget;
use std::process::ExitCode;
use term::TerminalCaps;
/// Entry point: parse the map path, load the board, then run the play loop with
/// the terminal in raw/alternate-screen mode (restored on every exit path).
fn main() -> ExitCode {
// The single positional argument is the map file to play.
let Some(path) = std::env::args().nth(1) else {
eprintln!("usage: kiln-tui <map.toml>");
return ExitCode::from(2);
};
// Load before touching the terminal so load errors print to a normal screen.
let board = match map_file::load(&path) {
Ok(board) => board,
Err(e) => {
eprintln!("error loading {path}: {e}");
return ExitCode::FAILURE;
}
};
let mut game = GameState::new(board);
// `init` enters the alternate screen and raw mode; `restore` always undoes it.
let mut terminal = ratatui::init();
// Detect terminal capabilities (now that we're in raw mode) and enable the
// Kitty keyboard protocol when it's available, so scripts can rely on the
// richer bindings it provides. Disabled again before restoring the terminal.
let caps = TerminalCaps::detect();
if caps.keyboard_enhancement {
let _ = term::push_kitty_flags();
}
let result = run(&mut terminal, &mut game, &caps);
if caps.keyboard_enhancement {
let _ = term::pop_kitty_flags();
}
ratatui::restore();
if let Err(e) = result {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
/// Draw-then-handle-input loop. Returns once the user asks to quit.
fn run(
terminal: &mut ratatui::DefaultTerminal,
game: &mut GameState,
caps: &TerminalCaps,
) -> std::io::Result<()> {
loop {
// Redraw the board every iteration; ratatui diffs against the previous
// frame so only changed cells are actually written to the terminal.
terminal.draw(|frame| draw(frame, game, caps))?;
// Block until the next input event.
if let Event::Key(key) = event::read()? {
// Act on Press and Repeat so holding a key moves the player
// continuously (the Kitty protocol emits Repeat events while a key is
// held). Ignore Release, which that protocol also emits and which
// would otherwise fire a second, spurious move per keypress.
if key.kind == KeyEventKind::Release {
continue;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Up | KeyCode::Char('k') => game.try_move(0, -1),
KeyCode::Down | KeyCode::Char('j') => game.try_move(0, 1),
KeyCode::Left | KeyCode::Char('h') => game.try_move(-1, 0),
KeyCode::Right | KeyCode::Char('l') => game.try_move(1, 0),
_ => {}
}
}
}
}
/// Render a single frame: a bordered block titled with the board name and
/// controls, containing the board itself.
fn draw(frame: &mut Frame, game: &GameState, caps: &TerminalCaps) {
let board = &game.board;
let block = Block::bordered()
.title(format!(" {} — arrows move · q quit ", board.name))
// Surface detected terminal capabilities in the bottom-right border.
.title_bottom(caps.summary_line().right_aligned());
// Draw the board inside the border, not over it.
let inner = block.inner(frame.area());
frame.render_widget(block, frame.area());
frame.render_widget(BoardWidget::new(board), inner);
}