From 79eef9f043c09c59b026fc195a6d5289806d3862 Mon Sep 17 00:00:00 2001 From: Thomas Eppers Date: Mon, 23 Aug 2021 14:06:00 +0200 Subject: [PATCH] WIP --- Cargo.lock | 60 +++++++++++++++++++ Cargo.toml | 4 +- src/main.rs | 5 ++ src/states.rs | 1 + src/ui.rs | 121 +++++++++++++++++++++++++++++++++++++++ src/widget/mod.rs | 61 ++++++++++++++++++++ src/widget/repo_entry.rs | 56 ++++++++++++++++++ src/widget/tag_list.rs | 56 ++++++++++++++++++ 8 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 src/states.rs create mode 100644 src/ui.rs create mode 100644 src/widget/mod.rs create mode 100644 src/widget/repo_entry.rs create mode 100644 src/widget/tag_list.rs diff --git a/Cargo.lock b/Cargo.lock index 478c07f..66904bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.69" @@ -437,6 +443,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "once_cell" version = "1.8.0" @@ -523,6 +535,8 @@ dependencies = [ "reqwest", "serde", "serde_json", + "termion", + "tui", ] [[package]] @@ -583,6 +597,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -750,6 +773,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "time" version = "0.1.44" @@ -848,6 +883,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tui" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" +dependencies = [ + "bitflags", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-bidi" version = "0.3.6" @@ -863,6 +911,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index fc971f9..e379cad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,5 @@ serde = { version = "1.0.127", features = ["derive"] } serde_json = "1.0.66" reqwest = { version = "0.11.4", features = ["blocking", "json"] } chrono = "0.4.19" -# crossterm = "0.17" -# tui = { version = "0.12", default-features = false, features = ['crossterm'] } +tui = "0.16" +termion = "1.5" diff --git a/src/main.rs b/src/main.rs index 449108f..d76ccf1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ use chrono::DateTime; use serde::Deserialize; +mod ui; +mod widget; + #[derive(Deserialize)] struct Image { architecture: String, @@ -52,6 +55,8 @@ fn main() { let dif = now - rfc3339.with_timezone(&chrono::Utc); println!("{} vor {}", result.tag_name, format_time_nice(dif)); } + + ui::Ui::new(); } fn format_time_nice(time: chrono::Duration) -> String { diff --git a/src/states.rs b/src/states.rs new file mode 100644 index 0000000..40daac6 --- /dev/null +++ b/src/states.rs @@ -0,0 +1 @@ +pub trait Screen {} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..f05ec42 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,121 @@ +use core::time::Duration; +use std::sync::mpsc; +use std::{io, thread}; + +use termion::event::{Event, Key}; +use termion::input::TermRead; +use termion::raw::IntoRawMode; +use tui::backend::TermionBackend; +use tui::layout::{Alignment, Constraint, Direction, Layout}; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, Paragraph}; +use tui::Terminal; + +use crate::widget::Widget; + +pub struct Ui { + state: State, + repo: crate::widget::repo_entry::RepoEntry, + tags: crate::widget::tag_list::TagList, +} + +#[derive(PartialEq, Clone)] +pub enum State { + EditRepo, + SelectTag, +} + +impl Ui { + pub fn new() -> Result { + let mut ui = Ui { + state: State::EditRepo, + repo: crate::widget::repo_entry::RepoEntry::new("This is a text"), + tags: crate::widget::tag_list::TagList::new(vec![ + String::from("first"), + String::from("second"), + String::from("third"), + String::from("sdfs"), + ]), + }; + + //setup tui + let stdout = io::stdout().into_raw_mode()?; + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + //setup input thread + let receiver = ui.spawn_stdin_channel(); + + //core interaction loop + 'core: loop { + //draw + terminal.draw(|rect| { + let chunks = Layout::default() + .direction(Direction::Vertical) + // .margin(1) + .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) + .split(rect.size()); + + rect.render_widget(ui.repo.render(), chunks[0]); + let (list, state) = ui.tags.render(); + rect.render_stateful_widget(list, chunks[1], state); + })?; + + //handle input + match receiver.try_recv() { + Ok(Key::Ctrl('q')) => break 'core, //quit program without saving + Ok(Key::Ctrl('s')) => { + if ui.state == State::SelectTag { + //TODO save currently selected tag + } + } + Ok(Key::Char('\n')) => { + if ui.state == State::EditRepo { + ui.state = State::SelectTag; + ui.repo.confirm(); + //TODO query tags and show them switch + } + } + Ok(Key::Down) => { + if ui.state == State::SelectTag { + //TODO select tag + ui.tags.next(); + } + } + Ok(Key::Up) => { + if ui.state == State::SelectTag { + //TODO select tag + ui.tags.previous(); + } + } + Ok(Key::Backspace) => { + ui.state = State::EditRepo; + ui.repo.input(Key::Backspace); + } + Ok(key) => { + ui.state = State::EditRepo; + ui.repo.input(key); + } + _ => (), + } + + //sleep for 64ms (15 fps) + thread::sleep(std::time::Duration::from_millis(64)); + } + + Ok(ui) + } + + pub fn spawn_stdin_channel(&self) -> mpsc::Receiver { + let (tx, rx) = mpsc::channel::(); + + thread::spawn(move || loop { + let stdin = io::stdin(); + for c in stdin.keys() { + tx.send(c.unwrap()).unwrap(); + } + }); + thread::sleep(std::time::Duration::from_millis(64)); + rx + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs new file mode 100644 index 0000000..05bb3a3 --- /dev/null +++ b/src/widget/mod.rs @@ -0,0 +1,61 @@ +use tui::widgets::ListState; + +pub mod repo_entry; +pub mod tag_list; + +pub trait Widget { + fn input(&mut self, event: termion::event::Key); +} + +pub struct StatefulList { + pub state: ListState, + pub items: Vec, +} + +impl StatefulList { + pub fn new() -> StatefulList { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } + + pub fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } +} diff --git a/src/widget/repo_entry.rs b/src/widget/repo_entry.rs new file mode 100644 index 0000000..d31d888 --- /dev/null +++ b/src/widget/repo_entry.rs @@ -0,0 +1,56 @@ +use termion::event::Key; +use tui::layout::Alignment; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, Paragraph}; + +pub struct RepoEntry { + text: String, + old_text: String, + changed: bool, +} + +impl RepoEntry { + pub fn new(text: &str) -> Self { + Self { + text: String::from(text), + old_text: String::from(text), + changed: false, + } + } + + pub fn render(&self) -> Paragraph { + let title = match self.changed { + true => "Repository*", + false => "Repository", + }; + Paragraph::new(self.text.clone()) + .block(Block::default().title(title).borders(Borders::ALL)) + .style(Style::default().fg(Color::White).bg(Color::Black)) + .alignment(Alignment::Left) + } + + pub fn confirm(&mut self) { + self.old_text = self.text.clone(); + self.changed = false; + } +} + +impl super::Widget for RepoEntry { + fn input(&mut self, key: termion::event::Key) { + match key { + Key::Esc => { + self.text = self.old_text.clone(); //TODO return to other structure + self.changed = false; + } + Key::Backspace => { + self.text.pop(); + self.changed = true; + } + Key::Char(c) => { + self.text.push(c); + self.changed = true; + } + _ => (), + } + } +} diff --git a/src/widget/tag_list.rs b/src/widget/tag_list.rs new file mode 100644 index 0000000..73dacb9 --- /dev/null +++ b/src/widget/tag_list.rs @@ -0,0 +1,56 @@ +use termion::event::Key; +use tui::layout::Alignment; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, List, ListState}; + +pub struct TagList { + list: super::StatefulList, +} + +impl TagList { + pub fn new(items: Vec) -> Self { + Self { + list: super::StatefulList::with_items(items), + } + } + + pub fn render(&mut self) -> (List, &mut ListState) { + let items: Vec = self + .list + .items + .iter() + .enumerate() + .map(|i| { + let mut lines = vec![tui::text::Spans::from(i.1.as_ref())]; + for _ in 0..i.0 { + lines.push(tui::text::Spans::from(tui::text::Span::styled( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + Style::default().add_modifier(tui::style::Modifier::ITALIC), + ))); + } + tui::widgets::ListItem::new(lines) + .style(Style::default().fg(Color::Black).bg(Color::White)) + }) + .collect(); + + // Create a List from all list items and highlight the currently selected one + let items = List::new(items) + .block(Block::default().borders(Borders::ALL).title("List")) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .add_modifier(tui::style::Modifier::BOLD), + ) + .highlight_symbol(">> "); + + (items, &mut self.list.state) + } + + pub fn next(&mut self) { + self.list.next(); + } + + pub fn previous(&mut self) { + self.list.previous(); + } +}