try to sort TuiKey/TuiEvent mess

This commit is contained in:
facile pop culture reference 2026-06-29 19:56:32 +03:00
parent dd7091bda1
commit 53c9438f7d
3 changed files with 108 additions and 113 deletions

View file

@ -1,7 +1,7 @@
mod border; mod border;
pub use self::border::*; pub use self::border::*;
use crate::{*, lang::*, draw::*, task::*}; use crate::{*, lang::*, draw::*, task::*, exit::*};
//use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
//use rand::distributions::uniform::UniformSampler; //use rand::distributions::uniform::UniformSampler;
use ::{ use ::{
@ -22,46 +22,37 @@ use ::{
//event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
} }
}, },
crossterm::event::{
read, Event, KeyEvent, KeyModifiers, KeyCode, KeyEventKind, KeyEventState
},
}; };
/// Implement standard [main] entrypoint for TUI apps.
#[macro_export] macro_rules! tui_main { #[macro_export] macro_rules! tui_main {
($state:expr) => { ($state:expr) => { pub fn main () -> Usually<()> { tui_run_main($state) } }
pub fn main () -> Usually<()> {
use ::{
std::sync::{
Arc,
RwLock
},
std::panic::{
set_hook,
PanicHookInfo,
},
std::time::{
Duration
},
better_panic::{
Settings,
Verbosity
},
ratatui::{
backend::{
Backend,
CrosstermBackend
},
crossterm::{
ExecutableCommand,
terminal::{
disable_raw_mode,
LeaveAlternateScreen
} }
pub fn tui_run_main <T> (state: T) -> Usually<()> where
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static
{
tui_setup_panic();
Exit::run(|exit|{
let state = Arc::new(RwLock::new(state));
let scan = Duration::from_millis(100);
let input = tui_input(exit.as_ref(), &state, scan)?;
let frame = Duration::from_millis(10);
let output = tui_output(exit.as_ref(), &state, frame, std::io::stdout())?;
output.join();
stdout().execute(LeaveAlternateScreen)?;
CrosstermBackend::new(stdout()).show_cursor()?;
disable_raw_mode()?;
Ok(())
})
} }
},
tengri::{ pub fn tui_setup_panic () {
exit::Exit, use ::std::panic::{set_hook, PanicHookInfo};
keys::tui_input, use ::better_panic::{Settings, Verbosity};
term::tui_output,
}
};
let panic = Settings::auto() let panic = Settings::auto()
.verbosity(Verbosity::Full) .verbosity(Verbosity::Full)
.create_panic_handler(); .create_panic_handler();
@ -71,21 +62,45 @@ use ::{
disable_raw_mode().unwrap(); disable_raw_mode().unwrap();
panic(info); panic(info);
})); }));
Exit::run(|exit|{ }
let state = Arc::new(RwLock::new($state));
let scan = Duration::from_millis(100); /// Enable TUI keyboard input for main state struct.
let input = tui_input(exit.as_ref(), &state, scan)?; #[macro_export] macro_rules! tui_keys {
let frame = Duration::from_millis(10); (|$self:ident:$State:ty,$input:ident|$body:block) => {
let output = tui_output(stdout(), exit.as_ref(), &state, frame)?; impl Apply<TuiEvent, Usually<()>> for $State {
output.join(); fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $body
stdout().execute(LeaveAlternateScreen)?; }
CrosstermBackend::new(stdout()).show_cursor()?; };
disable_raw_mode()?; }
Ok(())
/// Spawn the TUI input thread which reads keys from the terminal.
pub fn tui_input <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
) -> Result<Task, std::io::Error> {
let exited = exited.clone();
let state = state.clone();
Task::new_poll(exited.clone(), poll, move |_| {
let event = read().unwrap();
match event {
// Hardcoded exit.
Event::Key(KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('c'),
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}) => { exited.store(true, Relaxed); },
// Handle all other events by the state:
event => {
if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
panic!("{e}")
}
},
}
}) })
} }
}
}
/// Enable TUI output for state struct. /// Enable TUI output for state struct.
#[macro_export] macro_rules! tui_view { #[macro_export] macro_rules! tui_view {
@ -549,6 +564,8 @@ use self::colors::*; mod colors {
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) } pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
} }
/// Terminal keyboard input. #[cfg(feature = "term")] pub mod event;
#[cfg(feature = "term")] pub use self::event::*;
#[cfg(feature = "term")] pub mod keys; #[cfg(feature = "term")] pub mod keys;
#[cfg(feature = "term")] pub use self::keys::*; #[cfg(feature = "term")] pub use self::keys::*;

25
src/term/event.rs Normal file
View file

@ -0,0 +1,25 @@
use ::dizzle::impl_from;
use ::crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
/// TUI input loop event.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
pub struct TuiEvent(
/// Wrapped [crossterm::event::Event].
pub Event
);
impl_from!(TuiEvent: |e: Event| TuiEvent(
e));
impl_from!(TuiEvent: |e: KeyEvent| TuiEvent(
Event::Key(e)));
impl_from!(TuiEvent: |c: KeyCode| TuiEvent(
Event::Key(KeyEvent::new(c, KeyModifiers::NONE))));
impl_from!(TuiEvent: |c: char| TuiEvent(
Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))));
impl Ord for TuiEvent {
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other)
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
}
}

View file

@ -1,69 +1,22 @@
use crate::task::Task; use crate::{task::Task, term::TuiEvent};
use ::std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; use ::std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
use ::std::time::Duration; use ::std::time::Duration;
use ::dizzle::{Language, Symbol, Usually, Apply, impl_from}; use ::dizzle::{Language, Symbol, Usually, Apply};
use ::crossterm::event::{ use ::crossterm::event::{
read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState
}; };
/// Enable TUI keyboard input for main state struct. /// TUI key spec.
#[macro_export] macro_rules! tui_keys {
(|$self:ident:$State:ty,$input:ident|$body:block) => {
impl Apply<TuiEvent, Usually<()>> for $State {
fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $body
}
};
}
/// Spawn the TUI input thread which reads keys from the terminal.
pub fn tui_input <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
) -> Result<Task, std::io::Error> {
let exited = exited.clone();
let state = state.clone();
Task::new_poll(exited.clone(), poll, move |_| {
let event = read().unwrap();
match event {
// Hardcoded exit.
Event::Key(KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('c'),
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}) => { exited.store(true, Relaxed); },
// Handle all other events by the state:
event => {
if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
panic!("{e}")
}
},
}
})
}
/// TUI input loop event.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] #[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
pub struct TuiEvent(pub Event); pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
impl_from!(TuiEvent: |e: Event| TuiEvent(e)); impl Ord for TuiKey {
impl_from!(TuiEvent: |c: char| TuiEvent(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))));
impl Ord for TuiEvent {
fn cmp (&self, other: &Self) -> std::cmp::Ordering { fn cmp (&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other) self.partial_cmp(other)
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf .unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
} }
} }
/// TUI key spec.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
impl TuiKey { impl TuiKey {
const SPLIT: char = '/'; const SPLIT: char = '/';
pub fn from_crossterm (event: KeyEvent) -> Self { pub fn from_crossterm (event: KeyEvent) -> Self {