use component::{Component, LineArea, Plan, Widget}; use std::{ io::{Stdout, Write}, time::{Duration, Instant}, }; use termion::{ clear, cursor, event::{Key, MouseButton, MouseEvent}, input::{MouseTerminal, TermRead}, raw::{IntoRawMode, RawTerminal}, screen::{AlternateScreen, IntoAlternateScreen}, }; use theme::{Color, ColorSet}; use view::Event; type Result = std::result::Result; type Screen = MouseTerminal>>; extern crate termion; pub mod component; pub mod theme; pub mod token; pub mod view; struct PlanState { plans: Vec, layers: Vec, last_term_size: (u16, u16), base_colorset: ColorSet, last_render: Option, } impl PlanState { fn new( plans: PlanLayers, base_colorset: ColorSet, (term_w, term_h): (u16, u16), last_render: Option, ) -> Self { let layers = plans .clone() .into_iter() .map(|plan| { Layer::make( plan, base_colorset, (term_w as usize, term_h as usize), ) }) .collect(); Self { plans, layers, base_colorset, last_render, last_term_size: (term_w, term_h), } } fn match_click(&self, click: (u16, u16)) -> Option { for layer in (&self.layers).into_iter().rev() { let (_, h) = termion::terminal_size().unwrap(); for link in &layer.links { if link.1.matches(click) { return Some(link.0.clone()); } } } None } fn from_plans( plans: PlanLayers, base_colorset: ColorSet, ) -> Result { Ok(Self::new( plans, base_colorset, termion::terminal_size()?, None, )) } fn resized(self) -> Result { let term_size = termion::terminal_size()?; if term_size == self.last_term_size { return Ok(self); } Ok(Self::new( self.plans, self.base_colorset, term_size, self.last_render, )) } fn scroll_up(self) -> Result { Ok(Self::new( self.plans .into_iter() .map(|layer| layer.scroll_up()) .collect(), self.base_colorset, termion::terminal_size()?, self.last_render, )) } fn scroll_down(self) -> Result { Ok(Self::new( self.plans .into_iter() .map(|layer| layer.scroll_down()) .collect(), self.base_colorset, termion::terminal_size()?, self.last_render, )) } fn render(&mut self, scr: &mut Screen) -> Result<()> { const RENDER_COOLDOWN: u128 = 1000 / 60; if let Some(last) = self.last_render { if last.elapsed().as_millis() < RENDER_COOLDOWN { return Ok(()); } } write!( scr, "{color}{clear}{goto}", color = self.base_colorset.to_string(), clear = clear::All, goto = cursor::Goto(1, 1) )?; eprintln!("im rendeeeeeering"); for layer in (&self.layers).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)?; } scr.flush()?; self.last_render = Some(Instant::now()); Ok(()) } fn act_on( self, action: Action, scr: &mut Screen, ) -> Result { let term_size = termion::terminal_size()?; let mut new = match action { Action::ReplaceAll(layers) => Self::new( layers, self.base_colorset, term_size, self.last_render, ), Action::ReplaceLayer(layer, index) => { if index >= self.plans.len() { return Err(anyhow::anyhow!( "cannot replace layer at index {}, have: {}", index, self.plans.len() )); } else { todo!() } } Action::PushLayer(layer) => { let mut layers = self.plans; layers.push(layer); Self::new( layers, self.base_colorset, term_size, self.last_render, ) } Action::PopLayer => { let mut layers = self.plans; layers.pop(); Self::new( layers, self.base_colorset, term_size, self.last_render, ) } Action::Nothing => return Ok(self), }; new.render(scr)?; Ok(new) } } pub async fn run(view: V) -> Result<()> where V: View, { let base_colorset = ColorSet { fg: Color::WHITE, bg: Color::BLACK, }; let mut view = view; let mut last_ctrlc: Option = None; 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 = PlanState::from_plans(view.init().await?, base_colorset)?; plans.render(&mut screen)?; // FIXME: current loop means that pasting a string with len >1 // results in only the first character being rendered, until // something else triggers a render loop { if let Some(msg) = view.query() { plans = plans.act_on( view.update(Event::Message(msg)).await?, &mut screen, )?; continue; } let event = match events_iter.next() { Some(e) => { let e = e?; match e { termion::event::Event::Mouse(mus) => match mus { MouseEvent::Press(press, x, y) => match press { MouseButton::WheelUp => { plans = plans.scroll_up()?; plans.render(&mut screen)?; None } MouseButton::WheelDown => { plans = plans.scroll_down()?; plans.render(&mut screen)?; None } _ => match plans.match_click((x, y)) { Some(name) => Some(Event::Link(name)), None => None, }, }, // Not doing release/whatever mouse events rn _ => None, }, termion::event::Event::Key(key) => { if let Key::Ctrl(c) = key { if c == 'c' { match last_ctrlc { Some(last) => { if last.elapsed().as_millis() < 300 { break; } } None => {} }; last_ctrlc = Some(Instant::now()); continue; } } Some(Event::Input(key.into())) } _ => None, } } None => { std::thread::sleep(Duration::from_millis(1)); continue; } }; // if let termion::event::Event::Mouse(m) = &event { // match m { // MouseEvent::Press(btn, x, y) => { // eprintln!("got {:#?} at {} {}", btn, x, y); // } // _ => {} // } // } else { // eprintln!("got event {:#?}", &event); // } if let Some(event) = event { plans = plans .act_on(view.update(event).await?, &mut screen)?; } } 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 enum Action { ReplaceAll(PlanLayers), ReplaceLayer(Plan, usize), PushLayer(Plan), PopLayer, Nothing, } #[async_trait::async_trait] pub trait View { type Message; async fn init( &mut self, ) -> std::result::Result; // query is called at the beginning of every loop, and should // return quickly. Ideally, it should simply return a message // from some sort of queue, or None if there's an empty queue. fn query(&mut self) -> Option; async fn update( &mut self, event: Event, ) -> std::result::Result; }