mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
extract tui support code to tek_tui
This commit is contained in:
parent
1a9077427c
commit
1faf5bb6df
22 changed files with 477 additions and 450 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -1419,8 +1419,7 @@ dependencies = [
|
|||
"quanta",
|
||||
"rand",
|
||||
"symphonia",
|
||||
"tek_edn",
|
||||
"tek_layout",
|
||||
"tek_tui",
|
||||
"toml",
|
||||
"uuid",
|
||||
"wavers",
|
||||
|
|
@ -1439,11 +1438,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "tek_engine"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"better-panic",
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_layout"
|
||||
|
|
@ -1452,6 +1446,20 @@ dependencies = [
|
|||
"tek_engine",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_tui"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"better-panic",
|
||||
"crossterm",
|
||||
"palette",
|
||||
"rand",
|
||||
"ratatui",
|
||||
"tek_edn",
|
||||
"tek_engine",
|
||||
"tek_layout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ edition = "2021"
|
|||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek_layout = { path = "./layout" }
|
||||
tek_edn = { optional = true, path = "./edn" }
|
||||
tek_tui = { path = "./tui" }
|
||||
|
||||
atomic_float = "1.0.0"
|
||||
backtrace = "0.3.72"
|
||||
|
|
@ -29,7 +28,7 @@ wavers = "1.4.3"
|
|||
|
||||
[features]
|
||||
default = ["edn"]
|
||||
edn = ["tek_edn"]
|
||||
edn = ["tek_tui/edn"]
|
||||
|
||||
[[bin]]
|
||||
name = "tek_arranger"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
*,
|
||||
jack::*,
|
||||
tek_layout::Measure,
|
||||
tek_engine::{Usually, tui::{Tui, TuiRun, ratatui::prelude::Color}}
|
||||
tek_engine::Usually,
|
||||
tek_tui::{Tui, TuiRun, ItemPalette, ItemColor, ratatui::prelude::Color}
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,3 @@ edition = "2021"
|
|||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.28.1"
|
||||
ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
better-panic = "0.3.0"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ mod engine; pub use self::engine::*;
|
|||
mod input; pub use self::input::*;
|
||||
mod output; pub use self::output::*;
|
||||
|
||||
pub mod tui;
|
||||
|
||||
pub use std::error::Error;
|
||||
|
||||
/// Standard result type.
|
||||
|
|
@ -15,6 +13,16 @@ pub type Usually<T> = Result<T, Box<dyn Error>>;
|
|||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
|
||||
/// Prototypal case of implementor macro.
|
||||
/// Saves 4loc per data pats.
|
||||
#[macro_export] macro_rules! from {
|
||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
||||
fn from ($state:$Source) -> Self { $cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_dimensions () {
|
||||
assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::fmt::{Debug, Display};
|
||||
use std::ops::{Add, Sub, Mul, Div};
|
||||
|
||||
impl Coordinate for u16 {}
|
||||
|
||||
/// A linear coordinate.
|
||||
pub trait Coordinate: Send + Sync + Copy
|
||||
+ Add<Self, Output=Self>
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
mod tui_output; pub use self::tui_output::*;
|
||||
mod tui_input; pub use self::tui_input::*;
|
||||
mod tui_run; pub use self::tui_run::*;
|
||||
|
||||
use crate::*;
|
||||
use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||
use std::io::{stdout, Stdout};
|
||||
|
||||
pub use ::better_panic;
|
||||
pub(crate) use better_panic::{Settings, Verbosity};
|
||||
|
||||
pub use ::crossterm;
|
||||
pub(crate) use crossterm::{
|
||||
ExecutableCommand,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||
event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState},
|
||||
};
|
||||
|
||||
pub use ::ratatui;
|
||||
pub(crate) use ratatui::{
|
||||
prelude::{Color, Style, Buffer},
|
||||
style::Modifier,
|
||||
backend::{Backend, CrosstermBackend, ClearType},
|
||||
layout::{Size, Rect},
|
||||
buffer::Cell
|
||||
};
|
||||
|
||||
impl Coordinate for u16 {}
|
||||
|
||||
pub struct Tui {
|
||||
pub exited: Arc<AtomicBool>,
|
||||
pub buffer: Buffer,
|
||||
pub backend: CrosstermBackend<Stdout>,
|
||||
pub area: [u16;4], // FIXME auto resize
|
||||
}
|
||||
|
||||
impl Engine for Tui {
|
||||
type Unit = u16;
|
||||
type Size = [Self::Unit;2];
|
||||
type Area = [Self::Unit;4];
|
||||
type Input = TuiIn;
|
||||
type Handled = bool;
|
||||
type Output = TuiOut;
|
||||
fn exited (&self) -> bool {
|
||||
self.exited.fetch_and(true, Relaxed)
|
||||
}
|
||||
fn setup (&mut self) -> Usually<()> {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
self.backend.hide_cursor()?;
|
||||
enable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
fn teardown (&mut self) -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
self.backend.show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
/// Construct a new TUI engine and wrap it for shared ownership.
|
||||
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let Size { width, height } = backend.size()?;
|
||||
Ok(Arc::new(RwLock::new(Self {
|
||||
exited: Arc::new(AtomicBool::new(false)),
|
||||
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
||||
area: [0, 0, width, height],
|
||||
backend,
|
||||
})))
|
||||
}
|
||||
/// Update the display buffer.
|
||||
fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
||||
if self.buffer.area != size {
|
||||
self.backend.clear_region(ClearType::All).unwrap();
|
||||
self.buffer.resize(size);
|
||||
self.buffer.reset();
|
||||
}
|
||||
let updates = self.buffer.diff(&buffer);
|
||||
self.backend.draw(updates.into_iter()).expect("failed to render");
|
||||
self.backend.flush().expect("failed to flush output buffer");
|
||||
std::mem::swap(&mut self.buffer, &mut buffer);
|
||||
buffer.reset();
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
use crate::{*, tui::*};
|
||||
pub use crossterm::event::Event;
|
||||
use Event as CrosstermEvent;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TuiIn(pub Arc<AtomicBool>, pub CrosstermEvent);
|
||||
|
||||
impl Input<Tui> for TuiIn {
|
||||
type Event = Event;
|
||||
fn event (&self) -> &CrosstermEvent { &self.1 }
|
||||
fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) }
|
||||
fn done (&self) { self.0.store(true, Relaxed); }
|
||||
}
|
||||
|
||||
/// Define a key
|
||||
pub const fn key (code: KeyCode) -> Event {
|
||||
let modifiers = KeyModifiers::NONE;
|
||||
let kind = KeyEventKind::Press;
|
||||
let state = KeyEventState::NONE;
|
||||
Event::Key(KeyEvent { code, modifiers, kind, state })
|
||||
}
|
||||
|
||||
/// Add Ctrl modifier to key
|
||||
pub const fn ctrl (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::CONTROL)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
/// Add Alt modifier to key
|
||||
pub const fn alt (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::ALT)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
/// Add Shift modifier to key
|
||||
pub const fn shift (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::SHIFT)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! kpat {
|
||||
(Ctrl-Alt-$code:pat) => { kpat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
|
||||
(Ctrl-$code:pat) => { kpat!($code, KeyModifiers::CONTROL) };
|
||||
(Alt-$code:pat) => { kpat!($code, KeyModifiers::ALT) };
|
||||
(Shift-$code:pat) => { kpat!($code, KeyModifiers::SHIFT) };
|
||||
($code:pat) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
($code:pat, $modifiers: pat) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: $modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! kexp {
|
||||
(Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::from_bits(0b0000_0110).unwrap()) };
|
||||
(Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) };
|
||||
(Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) };
|
||||
(Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) };
|
||||
($code:ident) => { key_event_expr!($code) };
|
||||
($code:expr) => { key_event_expr!($code) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! key_event_expr {
|
||||
($code:expr, $modifiers: expr) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: $modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
($code:expr) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
use crate::{*, tui::*};
|
||||
|
||||
pub struct TuiOut {
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4]
|
||||
}
|
||||
|
||||
impl Output<Tui> for TuiOut {
|
||||
#[inline] fn area (&self) -> [u16;4] { self.area }
|
||||
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
|
||||
#[inline] fn place (&mut self, area: [u16;4], content: &impl Render<Tui>) {
|
||||
let last = self.area();
|
||||
*self.area_mut() = area;
|
||||
content.render(self);
|
||||
*self.area_mut() = last;
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiOut {
|
||||
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
buffer_update(&mut self.buffer, area, callback);
|
||||
}
|
||||
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
|
||||
if on {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
|
||||
} else {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
|
||||
}
|
||||
}
|
||||
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||
cell.underline_color = color;
|
||||
})
|
||||
}
|
||||
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
||||
}
|
||||
pub fn make_dim (&mut self) {
|
||||
for cell in self.buffer.content.iter_mut() {
|
||||
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
||||
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
}
|
||||
pub fn blit (
|
||||
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
||||
) {
|
||||
let text = text.as_ref();
|
||||
let buf = &mut self.buffer;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for &str {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
to.center_xy([self.chars().count() as u16, 1])
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.blit(self, to.area.x(), to.area.y(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for String {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
to.center_xy([self.chars().count() as u16, 1])
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.blit(self, to.area.x(), to.area.y(), None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
for row in 0..area.h() {
|
||||
let y = area.y() + row;
|
||||
for col in 0..area.w() {
|
||||
let x = area.x() + col;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
callback(buf.get_mut(x, y), col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//impl Area<u16> for Rect {
|
||||
//fn x (&self) -> u16 { self.x }
|
||||
//fn y (&self) -> u16 { self.y }
|
||||
//fn w (&self) -> u16 { self.width }
|
||||
//fn h (&self) -> u16 { self.height }
|
||||
//}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
//impl<T: Content<Tui>> Render<Tui> for T {}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
use crate::{*, tui::*};
|
||||
use ratatui::prelude::Size;
|
||||
use std::time::Duration;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
|
||||
pub trait TuiRun<R: Render<Tui> + Handle<Tui> + Sized + 'static> {
|
||||
/// Run an app in the main loop.
|
||||
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
|
||||
/// Spawn the input thread.
|
||||
fn run_input (&self, state: &Arc<RwLock<R>>, poll: Duration) -> JoinHandle<()>;
|
||||
/// Spawn the output thread.
|
||||
fn run_output (&self, state: &Arc<RwLock<R>>, sleep: Duration) -> JoinHandle<()>;
|
||||
}
|
||||
|
||||
impl<T: Render<Tui> + Handle<Tui> + Sized + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = self.run_input(state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = self.run_output(state, Duration::from_millis(10));
|
||||
render_thread.join().expect("main thread failed");
|
||||
self.write().unwrap().teardown()?;
|
||||
Ok(())
|
||||
}
|
||||
fn run_input (&self, state: &Arc<RwLock<T>>, poll: Duration) -> JoinHandle<()> {
|
||||
let exited = self.read().unwrap().exited.clone();
|
||||
let state = state.clone();
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
if ::crossterm::event::poll(poll).is_ok() {
|
||||
let event = ::crossterm::event::read().unwrap();
|
||||
match event {
|
||||
kpat!(Ctrl-KeyCode::Char('c')) => {
|
||||
exited.store(true, Relaxed);
|
||||
},
|
||||
_ => {
|
||||
let exited = exited.clone();
|
||||
if let Err(e) = state.write().unwrap().handle(&TuiIn(exited, event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
fn run_output (&self, state: &Arc<RwLock<T>>, sleep: Duration) -> JoinHandle<()> {
|
||||
let exited = self.read().unwrap().exited.clone();
|
||||
let engine = self.clone();
|
||||
let state = state.clone();
|
||||
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
|
||||
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
let Size { width, height } = engine.read().unwrap().backend.size()
|
||||
.expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
let size = Rect { x: 0, y: 0, width, height };
|
||||
if buffer.area != size {
|
||||
engine.write().unwrap().backend.clear_region(ClearType::All)
|
||||
.expect("clear failed");
|
||||
buffer.resize(size);
|
||||
buffer.reset();
|
||||
}
|
||||
let mut output = TuiOut { buffer, area: [0, 0, width, height] };
|
||||
state.render(&mut output);
|
||||
buffer = engine.write().unwrap().flip(output.buffer, size);
|
||||
}
|
||||
std::thread::sleep(sleep);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
42
src/lib.rs
42
src/lib.rs
|
|
@ -5,19 +5,19 @@
|
|||
#![feature(impl_trait_in_assoc_type)]
|
||||
#![feature(associated_type_defaults)]
|
||||
|
||||
pub use ::tek_layout;
|
||||
pub use ::tek_layout::tek_engine;
|
||||
|
||||
pub(crate) use ::tek_layout::{
|
||||
pub use ::tek_tui::{self, tek_engine, tek_layout};
|
||||
pub(crate) use ::tek_tui::{
|
||||
*,
|
||||
tek_edn::*,
|
||||
tek_layout::*,
|
||||
tek_engine::{
|
||||
from,
|
||||
Usually, Perhaps,
|
||||
Output, Content, Render, Thunk, render, Engine, Size, Area,
|
||||
Input, handle, Handle, command, Command, input_to_command, InputToCommand,
|
||||
keymap, kexp, kpat, EventMap,
|
||||
tui::{
|
||||
Input, handle, Handle, command, Command, input_to_command, InputToCommand, keymap, EventMap,
|
||||
},
|
||||
Tui,
|
||||
TuiIn, key, ctrl, shift, alt,
|
||||
TuiIn, key, ctrl, shift, alt, kexp, kpat,
|
||||
TuiOut,
|
||||
crossterm::{
|
||||
self,
|
||||
|
|
@ -31,13 +31,8 @@ pub(crate) use ::tek_layout::{
|
|||
prelude::{Color, Style, Stylize, Buffer, Modifier},
|
||||
buffer::Cell,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub use ::tek_edn;
|
||||
pub(crate) use ::tek_edn::*;
|
||||
|
||||
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
||||
pub(crate) use std::collections::BTreeMap;
|
||||
pub(crate) use std::error::Error;
|
||||
|
|
@ -53,9 +48,7 @@ pub(crate) use std::thread::{spawn, JoinHandle};
|
|||
pub(crate) use std::time::Duration;
|
||||
|
||||
pub mod arranger; pub use self::arranger::*;
|
||||
pub mod border; pub use self::border::*;
|
||||
pub mod clock; pub use self::clock::*;
|
||||
pub mod color; pub use self::color::*;
|
||||
pub mod field; pub use self::field::*;
|
||||
pub mod file; pub use self::file::*;
|
||||
pub mod focus; pub use self::focus::*;
|
||||
|
|
@ -70,8 +63,6 @@ pub mod pool; pub use self::pool::*;
|
|||
pub mod sampler; pub use self::sampler::*;
|
||||
pub mod sequencer; pub use self::sequencer::*;
|
||||
pub mod status; pub use self::status::*;
|
||||
pub mod style; pub use self::style::*;
|
||||
pub mod theme; pub use self::theme::*;
|
||||
|
||||
pub use ::atomic_float;
|
||||
pub(crate) use atomic_float::*;
|
||||
|
|
@ -84,13 +75,6 @@ pub(crate) use ::midly::{
|
|||
live::LiveEvent,
|
||||
};
|
||||
|
||||
pub use ::palette;
|
||||
pub(crate) use ::palette::{
|
||||
*,
|
||||
convert::*,
|
||||
okhsl::*
|
||||
};
|
||||
|
||||
testmod! { test }
|
||||
|
||||
/// Define test modules.
|
||||
|
|
@ -98,16 +82,6 @@ testmod! { test }
|
|||
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
||||
}
|
||||
|
||||
/// Prototypal case of implementor macro.
|
||||
/// Saves 4loc per data pats.
|
||||
#[macro_export] macro_rules! from {
|
||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
||||
fn from ($state:$Source) -> Self { $cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
|
|
|
|||
18
tui/Cargo.toml
Normal file
18
tui/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "tek_tui"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek_engine = { path = "../engine" }
|
||||
tek_layout = { path = "../layout" }
|
||||
tek_edn = { optional = true, path = "../edn" }
|
||||
palette = { version = "0.7.6", features = [ "random" ] }
|
||||
rand = "0.8.5"
|
||||
crossterm = "0.28.1"
|
||||
ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
better-panic = "0.3.0"
|
||||
|
||||
[features]
|
||||
default = ["edn"]
|
||||
edn = ["tek_edn"]
|
||||
3
tui/README.md
Normal file
3
tui/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `tek_tui`
|
||||
|
||||
tui utilities.
|
||||
44
tui/src/lib.rs
Normal file
44
tui/src/lib.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
pub use ::tek_engine;
|
||||
pub use ::tek_layout;
|
||||
pub use ::tek_edn;
|
||||
pub(crate) use tek_layout::*;
|
||||
pub(crate) use tek_engine::*;
|
||||
|
||||
mod tui_engine; pub use self::tui_engine::*;
|
||||
mod tui_input; pub use self::tui_input::*;
|
||||
mod tui_output; pub use self::tui_output::*;
|
||||
mod tui_run; pub use self::tui_run::*;
|
||||
|
||||
mod tui_color; pub use self::tui_color::*;
|
||||
mod tui_style; pub use self::tui_style::*;
|
||||
mod tui_theme; pub use self::tui_theme::*;
|
||||
mod tui_border; pub use self::tui_border::*;
|
||||
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||
pub(crate) use std::io::{stdout, Stdout};
|
||||
|
||||
pub use ::better_panic;
|
||||
pub(crate) use better_panic::{Settings, Verbosity};
|
||||
|
||||
pub use ::crossterm;
|
||||
pub(crate) use crossterm::{
|
||||
ExecutableCommand,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||
event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState},
|
||||
};
|
||||
|
||||
pub use ::ratatui;
|
||||
pub(crate) use ratatui::{
|
||||
prelude::{Color, Style, Buffer},
|
||||
style::Modifier,
|
||||
backend::{Backend, CrosstermBackend, ClearType},
|
||||
layout::{Size, Rect},
|
||||
buffer::Cell
|
||||
};
|
||||
|
||||
pub use ::palette;
|
||||
pub(crate) use ::palette::{
|
||||
*,
|
||||
convert::*,
|
||||
okhsl::*
|
||||
};
|
||||
65
tui/src/tui_engine.rs
Normal file
65
tui/src/tui_engine.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Tui {
|
||||
pub exited: Arc<AtomicBool>,
|
||||
pub buffer: Buffer,
|
||||
pub backend: CrosstermBackend<Stdout>,
|
||||
pub area: [u16;4], // FIXME auto resize
|
||||
}
|
||||
|
||||
impl Engine for Tui {
|
||||
type Unit = u16;
|
||||
type Size = [Self::Unit;2];
|
||||
type Area = [Self::Unit;4];
|
||||
type Input = TuiIn;
|
||||
type Handled = bool;
|
||||
type Output = TuiOut;
|
||||
fn exited (&self) -> bool {
|
||||
self.exited.fetch_and(true, Relaxed)
|
||||
}
|
||||
fn setup (&mut self) -> Usually<()> {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
self.backend.hide_cursor()?;
|
||||
enable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
fn teardown (&mut self) -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
self.backend.show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
/// Construct a new TUI engine and wrap it for shared ownership.
|
||||
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let Size { width, height } = backend.size()?;
|
||||
Ok(Arc::new(RwLock::new(Self {
|
||||
exited: Arc::new(AtomicBool::new(false)),
|
||||
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
||||
area: [0, 0, width, height],
|
||||
backend,
|
||||
})))
|
||||
}
|
||||
/// Update the display buffer.
|
||||
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
||||
if self.buffer.area != size {
|
||||
self.backend.clear_region(ClearType::All).unwrap();
|
||||
self.buffer.resize(size);
|
||||
self.buffer.reset();
|
||||
}
|
||||
let updates = self.buffer.diff(&buffer);
|
||||
self.backend.draw(updates.into_iter()).expect("failed to render");
|
||||
self.backend.flush().expect("failed to flush output buffer");
|
||||
std::mem::swap(&mut self.buffer, &mut buffer);
|
||||
buffer.reset();
|
||||
buffer
|
||||
}
|
||||
}
|
||||
106
tui/src/tui_input.rs
Normal file
106
tui/src/tui_input.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use crate::*;
|
||||
pub use crossterm::event::Event;
|
||||
use Event as CrosstermEvent;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TuiIn(pub Arc<AtomicBool>, pub CrosstermEvent);
|
||||
|
||||
impl Input<Tui> for TuiIn {
|
||||
type Event = Event;
|
||||
fn event (&self) -> &CrosstermEvent { &self.1 }
|
||||
fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) }
|
||||
fn done (&self) { self.0.store(true, Relaxed); }
|
||||
}
|
||||
|
||||
/// Define a key
|
||||
pub const fn key (code: KeyCode) -> Event {
|
||||
let modifiers = KeyModifiers::NONE;
|
||||
let kind = KeyEventKind::Press;
|
||||
let state = KeyEventState::NONE;
|
||||
Event::Key(KeyEvent { code, modifiers, kind, state })
|
||||
}
|
||||
|
||||
/// Add Ctrl modifier to key
|
||||
pub const fn ctrl (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::CONTROL)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
/// Add Alt modifier to key
|
||||
pub const fn alt (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::ALT)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
/// Add Shift modifier to key
|
||||
pub const fn shift (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::SHIFT)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! kpat {
|
||||
(Ctrl-Alt-$code:pat) => { kpat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
|
||||
(Ctrl-$code:pat) => { kpat!($code, KeyModifiers::CONTROL) };
|
||||
(Alt-$code:pat) => { kpat!($code, KeyModifiers::ALT) };
|
||||
(Shift-$code:pat) => { kpat!($code, KeyModifiers::SHIFT) };
|
||||
($code:pat) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
($code:pat, $modifiers: pat) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: $modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! kexp {
|
||||
(Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::from_bits(0b0000_0110).unwrap()) };
|
||||
(Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) };
|
||||
(Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) };
|
||||
(Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) };
|
||||
($code:ident) => { key_event_expr!($code) };
|
||||
($code:expr) => { key_event_expr!($code) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! key_event_expr {
|
||||
($code:expr, $modifiers: expr) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: $modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
($code:expr) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
116
tui/src/tui_output.rs
Normal file
116
tui/src/tui_output.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct TuiOut {
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4]
|
||||
}
|
||||
|
||||
impl Output<Tui> for TuiOut {
|
||||
#[inline] fn area (&self) -> [u16;4] { self.area }
|
||||
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
|
||||
#[inline] fn place (&mut self, area: [u16;4], content: &impl Render<Tui>) {
|
||||
let last = self.area();
|
||||
*self.area_mut() = area;
|
||||
content.render(self);
|
||||
*self.area_mut() = last;
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiOut {
|
||||
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
buffer_update(&mut self.buffer, area, callback);
|
||||
}
|
||||
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
|
||||
if on {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
|
||||
} else {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
|
||||
}
|
||||
}
|
||||
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||
cell.underline_color = color;
|
||||
})
|
||||
}
|
||||
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
||||
}
|
||||
pub fn make_dim (&mut self) {
|
||||
for cell in self.buffer.content.iter_mut() {
|
||||
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
||||
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
}
|
||||
pub fn blit (
|
||||
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
||||
) {
|
||||
let text = text.as_ref();
|
||||
let buf = &mut self.buffer;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for &str {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
to.center_xy([self.chars().count() as u16, 1])
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.blit(self, to.area.x(), to.area.y(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for String {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
to.center_xy([self.chars().count() as u16, 1])
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.blit(self, to.area.x(), to.area.y(), None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
for row in 0..area.h() {
|
||||
let y = area.y() + row;
|
||||
for col in 0..area.w() {
|
||||
let x = area.x() + col;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
callback(buf.get_mut(x, y), col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//impl Area<u16> for Rect {
|
||||
//fn x (&self) -> u16 { self.x }
|
||||
//fn y (&self) -> u16 { self.y }
|
||||
//fn w (&self) -> u16 { self.width }
|
||||
//fn h (&self) -> u16 { self.height }
|
||||
//}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
//impl<T: Content<Tui>> Render<Tui> for T {}
|
||||
74
tui/src/tui_run.rs
Normal file
74
tui/src/tui_run.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use crate::*;
|
||||
use ratatui::prelude::Size;
|
||||
use std::time::Duration;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
|
||||
pub trait TuiRun<R: Render<Tui> + Handle<Tui> + Sized + 'static> {
|
||||
/// Run an app in the main loop.
|
||||
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
|
||||
/// Spawn the input thread.
|
||||
fn run_input (&self, state: &Arc<RwLock<R>>, poll: Duration) -> JoinHandle<()>;
|
||||
/// Spawn the output thread.
|
||||
fn run_output (&self, state: &Arc<RwLock<R>>, sleep: Duration) -> JoinHandle<()>;
|
||||
}
|
||||
|
||||
impl<T: Render<Tui> + Handle<Tui> + Sized + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = self.run_input(state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = self.run_output(state, Duration::from_millis(10));
|
||||
render_thread.join().expect("main thread failed");
|
||||
self.write().unwrap().teardown()?;
|
||||
Ok(())
|
||||
}
|
||||
fn run_input (&self, state: &Arc<RwLock<T>>, poll: Duration) -> JoinHandle<()> {
|
||||
let exited = self.read().unwrap().exited.clone();
|
||||
let state = state.clone();
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
if ::crossterm::event::poll(poll).is_ok() {
|
||||
let event = ::crossterm::event::read().unwrap();
|
||||
match event {
|
||||
kpat!(Ctrl-KeyCode::Char('c')) => {
|
||||
exited.store(true, Relaxed);
|
||||
},
|
||||
_ => {
|
||||
let exited = exited.clone();
|
||||
if let Err(e) = state.write().unwrap().handle(&TuiIn(exited, event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
fn run_output (&self, state: &Arc<RwLock<T>>, sleep: Duration) -> JoinHandle<()> {
|
||||
let exited = self.read().unwrap().exited.clone();
|
||||
let engine = self.clone();
|
||||
let state = state.clone();
|
||||
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
|
||||
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
let Size { width, height } = engine.read().unwrap().backend.size()
|
||||
.expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
let size = Rect { x: 0, y: 0, width, height };
|
||||
if buffer.area != size {
|
||||
engine.write().unwrap().backend.clear_region(ClearType::All)
|
||||
.expect("clear failed");
|
||||
buffer.resize(size);
|
||||
buffer.reset();
|
||||
}
|
||||
let mut output = TuiOut { buffer, area: [0, 0, width, height] };
|
||||
state.render(&mut output);
|
||||
buffer = engine.write().unwrap().flip(output.buffer, size);
|
||||
}
|
||||
std::thread::sleep(sleep);
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue