371 lines
11 KiB
Rust
371 lines
11 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 mut new = match action {
|
|
Action::ReplaceAll(layers) => {
|
|
Self::from_plans(layers, self.base_colorset)?
|
|
}
|
|
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::from_plans(layers, self.base_colorset)?
|
|
}
|
|
Action::PopLayer => {
|
|
let mut layers = self.plans;
|
|
layers.pop();
|
|
Self::from_plans(layers, self.base_colorset)?
|
|
}
|
|
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()?, base_colorset)?;
|
|
plans.render(&mut screen)?;
|
|
loop {
|
|
if let Some(msg) = view.query() {
|
|
plans = plans.act_on(
|
|
view.update(Event::Message(msg))?,
|
|
&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)?, &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,
|
|
}
|
|
|
|
pub trait View {
|
|
type Message;
|
|
|
|
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>;
|
|
|
|
fn update(
|
|
&mut self,
|
|
event: Event<Self::Message>,
|
|
) -> std::result::Result<Action, anyhow::Error>;
|
|
}
|