//! hexwork-term: terminal backend abstraction (docs/04 §4.6, docs/05 §5.3). //! Normalizes kitty/wezterm/xterm/gnome-terminal + SSH behind one trait. pub mod backend; pub mod keymap; pub mod remap; /// Terminal capabilities probed at boot. #[derive(Clone, Debug)] pub struct Capabilities { pub truecolor: bool, pub unicode: bool, pub mouse: bool, pub size: (u16, u16), } impl Default for Capabilities { fn default() -> Self { Self { truecolor: true, unicode: true, mouse: false, size: (120, 36), } } } /// Minimum supported size: 80×24 with a kind refusal below it. pub const MIN_W: u16 = 80; pub const MIN_H: u16 = 24; pub fn size_ok(w: u16, h: u16) -> bool { w >= MIN_W && h >= MIN_H } /// Normalized input events. Every mouse action has a key twin. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Input { Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight, Wait, Quit, Look, Inventory, Contracts, Map, Chapel, /// Transfer verbs (Phase 12 T.2): pick up a loose item at the player's /// own cell / drop the explicitly selected carried item. Bound `g` and /// `-` by default; optional in custom keymaps, so they may be unbound. Pickup, Drop, CameraToggle, TiltDown, TiltUp, SliceUp, SliceDown, Recenter, LookReset, /// Terminal dimensions, columns then rows; never a canonical action. Resize(u16, u16), /// Zero-based terminal column and row of a left-button event. MouseDown(u16, u16), MouseDrag(u16, u16), MouseUp, FocusLost, PauseStep, /// Contextual interaction/menu confirmation (Enter). Confirm, /// Menu/chargen digit pick (1-6 → 0-5). Play treats as no-op; menus /// consume. Digits 7-9 map to Unknown (reserved). ChargenPick(usize), /// Explicitly toggle continuous travel (toggle-walk). A second press, or an /// active stop key (`Space`/`Esc`), clears it. Needed for terminals that do /// not report key repeats/releases, so continuous travel never depends on /// guessing a release. ToggleWalk, Unknown, } /// A movement direction currently held down, carried by the terminal's key /// repeat (`KeyEventKind::Repeat`). This is a physical-input fact, not a game /// action: the frontend converts it to a world direction and paces one Move per /// tick. Releases clear it; terminals that never report repeats simply leave it /// `None`, and continuous travel falls back to [`Input::ToggleWalk`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HeldMove { Right, DownRight, Down, DownLeft, Left, UpLeft, Up, UpRight, } impl HeldMove { /// The discrete movement [`Input`] this held key mirrors. pub fn as_input(self) -> Input { match self { HeldMove::Right => Input::Right, HeldMove::DownRight => Input::DownRight, HeldMove::Down => Input::Down, HeldMove::DownLeft => Input::DownLeft, HeldMove::Left => Input::Left, HeldMove::UpLeft => Input::UpLeft, HeldMove::Up => Input::Up, HeldMove::UpRight => Input::UpRight, } } /// The held key mirroring `input`, if `input` is a movement key. pub fn from_input(input: Input) -> Option { Some(match input { Input::Right => HeldMove::Right, Input::DownRight => HeldMove::DownRight, Input::Down => HeldMove::Down, Input::DownLeft => HeldMove::DownLeft, Input::Left => HeldMove::Left, Input::UpLeft => HeldMove::UpLeft, Input::Up => HeldMove::Up, Input::UpRight => HeldMove::UpRight, _ => return None, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn refuses_tiny_terminals() { assert!(!size_ok(79, 24)); assert!(!size_ok(80, 23)); assert!(size_ok(80, 24)); } #[test] fn held_move_round_trips_through_input() { for held in [ HeldMove::Right, HeldMove::DownRight, HeldMove::Down, HeldMove::DownLeft, HeldMove::Left, HeldMove::UpLeft, HeldMove::Up, HeldMove::UpRight, ] { assert_eq!(HeldMove::from_input(held.as_input()), Some(held)); } for input in [ Input::Wait, Input::Look, Input::ToggleWalk, Input::PauseStep, Input::Unknown, ] { assert_eq!(HeldMove::from_input(input), None); } } }