kkx/kkdisp/src/lib.rs

390 lines
12 KiB
Rust

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<T> = std::result::Result<T, anyhow::Error>;
type Screen = MouseTerminal<RawTerminal<AlternateScreen<Stdout>>>;
extern crate termion;
pub mod component;
pub mod theme;
pub mod token;
pub mod view;
struct PlanState {
plans: Vec<Plan>,
layers: Vec<Layer>,
last_term_size: (u16, u16),
base_colorset: ColorSet,
last_render: Option<Instant>,
}
impl PlanState {
fn new(
plans: PlanLayers,
base_colorset: ColorSet,
(term_w, term_h): (u16, u16),
last_render: Option<Instant>,
) -> 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<String> {
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<Self> {
Ok(Self::new(
plans,
base_colorset,
termion::terminal_size()?,
None,
))
}
fn resized(self) -> Result<Self> {
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<Self> {
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<Self> {
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::<String>()
}) {
write!(scr, "{}", layer)?;
}
scr.flush()?;
self.last_render = Some(Instant::now());
Ok(())
}
fn act_on(
self,
action: Action,
scr: &mut Screen,
) -> Result<Self> {
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<V>(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<Instant> = 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<Component>,
}
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<Plan>;
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<PlanLayers, anyhow::Error>;
// 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<Self::Message>;
async fn update(
&mut self,
event: Event<Self::Message>,
) -> std::result::Result<Action, anyhow::Error>;
}