diff --git a/Cargo.lock b/Cargo.lock index 0926f2a..1a0daf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,39 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "kkdisp" version = "0.1.0" @@ -29,6 +56,7 @@ version = "0.1.0" dependencies = [ "anyhow", "kkdisp", + "tokio", ] [[package]] @@ -37,12 +65,88 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "proc-macro2" version = "1.0.50" @@ -79,6 +183,12 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.152" @@ -99,6 +209,31 @@ dependencies = [ "syn", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "syn" version = "1.0.107" @@ -122,8 +257,124 @@ dependencies = [ "redox_termios", ] +[[package]] +name = "tokio" +version = "1.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/kkdisp/src/component.rs b/kkdisp/src/component.rs index caa67ab..bc1a9b7 100644 --- a/kkdisp/src/component.rs +++ b/kkdisp/src/component.rs @@ -9,6 +9,7 @@ use std::io::{Stdout, Write}; #[derive(Eq, Clone, Debug)] pub enum Component { NextLine, + Link(String, usize), X(usize), String(String), Fg(Color), @@ -27,46 +28,27 @@ impl Component { Component::Bg(c) => format!("BG({:#?})", c) .replace('\n', "") .replace(' ', ""), + Component::Link(name, ln) => { + format!("Clickable[{}:{}]", name, ln) + } } } } -pub trait Cursed { - fn scene( - &mut self, - comp: Vec, - ) -> Result<(), anyhow::Error>; +pub struct LineArea { + pos: (u16, u16), + length: u16, } -impl Cursed for RawTerminal { - fn scene( - &mut self, - comp: Vec, - ) -> Result<(), anyhow::Error> { - let mut line = 1; - let screen_str = comp - .into_iter() - .map(|c| match c { - Component::NextLine => { - line += 1; - String::from("\r\n") - } - Component::X(x) => { - termion::cursor::Goto((x + 1) as u16, line).into() - } - Component::String(s) => s, - Component::Fg(c) => c.fg(), - Component::Bg(c) => c.bg(), - }) - .collect::(); - write!( - self, - "{clear}{start}{scene}", - clear = termion::clear::All, - start = termion::cursor::Goto(1, 1), - scene = screen_str, - )?; - Ok(()) +impl LineArea { + pub fn new(pos: (u16, u16), length: u16) -> Self { + Self { pos, length } + } + + pub fn matches(&self, pos: (u16, u16)) -> bool { + self.pos.1 == pos.1 + && pos.0 >= self.pos.0 + && pos.0 <= (self.pos.0 + self.length - 1) } } @@ -93,6 +75,12 @@ impl PartialEq for Component { Self::NextLine => true, _ => false, }, + Self::Link(name, length) => match other { + Self::Link(other_name, other_length) => { + name == other_name && length == other_length + } + _ => false, + }, } } } diff --git a/kkdisp/src/lib.rs b/kkdisp/src/lib.rs index 746b9ec..2a11503 100644 --- a/kkdisp/src/lib.rs +++ b/kkdisp/src/lib.rs @@ -1,27 +1,200 @@ -use crate::component::Cursed; -use component::{Plan, Widget}; +use component::{Component, LineArea, Plan, Widget}; use std::{ io::{Stdout, Write}, time::Duration, }; -use termion::raw::{IntoRawMode, RawTerminal}; +use termion::{ + clear, cursor, + input::{MouseTerminal, TermRead}, + raw::{IntoRawMode, RawTerminal}, + screen::{AlternateScreen, IntoAlternateScreen}, + AsyncReader, +}; use theme::{Color, ColorSet}; use token::Token; +use view::Event; + +type Result = std::result::Result; +type Screen = MouseTerminal>>; extern crate termion; -mod component; -mod theme; -mod token; +pub mod component; +pub mod theme; +pub mod token; +pub mod view; -pub struct Display { - // needs to hold the termion display - screen: RawTerminal, -} +pub async fn run(view: V) -> Result<()> +where + V: View, +{ + let mut view = view; + let mut screen: Screen = MouseTerminal::from( + std::io::stdout().into_alternate_screen()?.into_raw_mode()?, + ); + let mut events_iter = termion::async_stdin().events(); + let mut plans = view.init()?; + let (term_w, term_h) = termion::terminal_size()?; + let mut layers = make_layers( + plans, + ColorSet { + fg: Color::WHITE, + bg: Color::BLACK, + }, + (term_w as usize, term_h as usize), + ); + render_layers(&layers, &mut screen)?; + screen.flush()?; + loop { + let event = match events_iter.next() { + Some(e) => e, + None => { + std::thread::sleep(Duration::from_millis(50)); + continue; + } + }?; -impl Display { - pub fn new() -> Result { - Ok(Self { - screen: std::io::stdout().into_raw_mode()?, - }) + let event = match event { + termion::event::Event::Mouse(mus) => match mus { + termion::event::MouseEvent::Press(_, x, y) => { + // TODO: scrolling later + match match_click(&layers, (x, y)) { + Some(yay) => Event::Link(yay), + None => continue, + } + } + _ => Event::Input(event), + }, + _ => Event::Input(event), + }; + + plans = view.update(event)?; + let (term_w, term_h) = termion::terminal_size()?; + layers = make_layers( + plans, + ColorSet { + fg: Color::WHITE, + bg: Color::BLACK, + }, + (term_w as usize, term_h as usize), + ); + write!(screen, "{}", clear::All)?; + render_layers(&layers, &mut screen)?; + screen.flush()?; } } + +fn match_click( + layers: &Vec, + click: (u16, u16), +) -> Option { + for layer in layers.into_iter().rev() { + for link in &layer.links { + if link.1.matches(click) { + return Some(link.0.clone()); + } + } + } + None +} + +fn make_layers( + plans: PlanLayers, + base_colorset: ColorSet, + term_dimensions: (usize, usize), +) -> Vec { + plans + .into_iter() + .map(|plan| Layer::make(plan, base_colorset, term_dimensions)) + .collect() +} + +fn render_layers( + layered: &Vec, + scr: &mut Screen, +) -> Result<()> { + for layer in layered.into_iter().map(|layer| { + let mut line = 1; + (&layer.components) + .into_iter() + .map(|component| match component { + Component::NextLine => { + line += 1; + "\n\r".into() + } + Component::Link(_, _) => panic!( + "links should be filtered out at the layer" + ), + Component::X(x) => { + cursor::Goto((x + 1) as u16, line).into() + } + Component::String(s) => s.clone(), + Component::Fg(c) => c.fg(), + Component::Bg(c) => c.bg(), + }) + .collect::() + }) { + write!(scr, "{}", layer)?; + } + Ok(()) +} + +pub struct Layer { + links: Vec<(String, LineArea)>, + components: Vec, +} + +impl Layer { + pub fn make( + plan: Plan, + base_colorset: ColorSet, + term_dimensions: (usize, usize), + ) -> Self { + let mut line = 1; + let mut line_offset = 1; + let mut links = Vec::new(); + let components = plan + .make(base_colorset, term_dimensions) + .into_iter() + .filter(|comp| match comp { + Component::NextLine => { + line += 1; + line_offset = 1; + true + } + Component::X(x) => { + line_offset = (x + 1) as u16; + true + } + Component::String(s) => { + line_offset += s.len() as u16; + true + } + Component::Link(name, length) => { + links.push(( + name.clone(), + LineArea::new( + (line_offset, line), + *length as u16, + ), + )); + false + } + _ => true, + }) + .collect(); + Self { links, components } + } +} +// Simple type alias to be clear that the plans are going to +// be layered on top of eachother +pub type PlanLayers = Vec; + +pub trait View { + fn init( + &mut self, + ) -> std::result::Result; + fn update( + &mut self, + event: Event, + ) -> std::result::Result; +} diff --git a/kkdisp/src/token.rs b/kkdisp/src/token.rs index 638dd3f..406a1aa 100644 --- a/kkdisp/src/token.rs +++ b/kkdisp/src/token.rs @@ -7,6 +7,7 @@ pub enum Token { CharPad(Box, char, usize), Fg(Box, Color), Bg(Box, Color), + Link(Box, String), String(Box, String), End, } @@ -55,6 +56,7 @@ impl Token { Token::String(t, s) => s.len() + t.str_len(), Token::Fg(t, _) => t.str_len(), Token::Bg(t, _) => t.str_len(), + Token::Link(t, _) => t.str_len(), Token::Centered(t) => t.str_len(), Token::Limited(t, lim) => (*lim).min(t.str_len()), Token::CharPad(t, _, pad_to) => { @@ -64,6 +66,13 @@ impl Token { } } + pub fn link(self, name: S) -> Self + where + S: Into, + { + Self::Link(Box::new(self), name.into()) + } + pub fn pad_char(self, c: char, pad_to: usize) -> Self { Self::CharPad(Box::new(self), c, pad_to) } @@ -82,6 +91,12 @@ impl Token { .into_iter() .chain(t.with_width(width)) .collect(), + Token::Link(t, name) => { + vec![Component::Link(name, t.str_len())] + .into_iter() + .chain(t.with_width(width)) + .collect() + } Token::Centered(cnt) => { let mut str_len = cnt.str_len(); let components = if str_len > width { @@ -234,6 +249,17 @@ mod tests { Component::String("end".into()), ], ), + ( + "contains a link", + Token::text("learn more") + .centered() + .link("string_link"), + vec![ + Component::Link("string_link".into(), 10), + Component::X((WIDTH - 10) / 2), + Component::String("learn more".into()), + ], + ), ] .into_iter() .for_each(|(name, token, expected)| { diff --git a/kkdisp/src/view.rs b/kkdisp/src/view.rs new file mode 100644 index 0000000..968bb8a --- /dev/null +++ b/kkdisp/src/view.rs @@ -0,0 +1,9 @@ +use crate::{ + component::{Component, LineArea, Plan}, + theme::ColorSet, +}; + +pub enum Event { + Link(String), + Input(termion::event::Event), +} diff --git a/kkx/Cargo.toml b/kkx/Cargo.toml index b932acb..c437e0d 100644 --- a/kkx/Cargo.toml +++ b/kkx/Cargo.toml @@ -8,3 +8,7 @@ edition = "2021" [dependencies] anyhow = "1.0.68" kkdisp = { version = "0.1.0", path = "../kkdisp" } + +[dependencies.tokio] +version = "1.24.2" +features = ["full"] diff --git a/kkx/src/main.rs b/kkx/src/main.rs index e202cf2..6952c48 100644 --- a/kkx/src/main.rs +++ b/kkx/src/main.rs @@ -1,7 +1,78 @@ -use kkdisp::Display; +use kkdisp::{ + component::{Plan, Widget}, + theme::Color, + token::Token, + view::Event, + View, +}; -fn main() { - // let disp = Display::new(); - // let widget_idk = Widget::new() - Display::test(); +#[tokio::main] +async fn main() { + kkdisp::run(App::default()).await.unwrap() +} + +pub struct App { + states: Vec, + index: usize, +} + +impl Default for App { + fn default() -> Self { + Self { + index: 0, + states: vec![ + Plan::start().fill(vec![]).fixed( + 4, + vec![Widget::new( + kkdisp::component::SectionWidth::Full, + vec![ + Token::End, + Token::text("click me") + .bg(Color::BLUE) + .link("click"), + ], + )], + ), + Plan::start().fill(vec![]).fixed( + 4, + vec![Widget::new( + kkdisp::component::SectionWidth::Full, + vec![ + Token::End, + Token::text( + "you did it! now undo it u fuck", + ) + .bg(Color::RED) + .link("click"), + ], + )], + ), + ], + } + } +} + +impl View for App { + fn init( + &mut self, + ) -> std::result::Result { + Ok(vec![self.states[self.index].clone()]) + } + + fn update( + &mut self, + event: Event, + ) -> std::result::Result { + match event { + Event::Link(lnk) => { + if lnk == "click" { + self.index ^= 1; + Ok(vec![self.states[self.index].clone()]) + } else { + panic!("bad link"); + } + } + _ => Ok(vec![self.states[self.index].clone()]), + } + } }