diff options
author | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-10 11:44:17 -0700 |
---|---|---|
committer | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-10 11:44:17 -0700 |
commit | 7ac11eee2b38522f22fa44227ffb6411b4f758e4 (patch) | |
tree | 5e98d915600fcf3df87a4b9870e8a9789f5e5e35 | |
parent | 0056c5a93feabf96f432c5a50bd492742483acbd (diff) |
Trying out tui-rs...
-rw-r--r-- | Cargo.lock | 100 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | TODO.md | 2 | ||||
-rw-r--r-- | src/main.rs | 50 | ||||
-rw-r--r-- | src/stackexchange.rs | 2 | ||||
-rw-r--r-- | src/term.rs | 29 | ||||
-rw-r--r-- | src/tui/app.rs | 160 | ||||
-rw-r--r-- | src/tui/enumerable.rs | 12 | ||||
-rw-r--r-- | src/tui/list.rs | 60 | ||||
-rw-r--r-- | src/tui/markdown.hs | 3 | ||||
-rw-r--r-- | src/tui/markdown.rs | 135 | ||||
-rw-r--r-- | src/tui/mod.rs | 6 | ||||
-rw-r--r-- | src/tui/ui.rs | 0 |
13 files changed, 537 insertions, 25 deletions
@@ -92,6 +92,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" [[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.54" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -237,6 +243,7 @@ checksum = "9851d20b9809e561297ec3ca85d7cba3a57507fe8d01d07ba7b52469e1c89a11" dependencies = [ "bitflags", "crossterm_winapi", + "futures", "lazy_static", "libc", "mio", @@ -283,6 +290,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" [[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] name = "encoding_rs" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -341,12 +354,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-channel" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -356,12 +385,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" [[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-io" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" [[package]] +name = "futures-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "futures-sink" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -382,12 +434,17 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project", "pin-utils", + "proc-macro-hack", + "proc-macro-nested", "slab", ] @@ -524,6 +581,15 @@ dependencies = [ ] [[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] name = "itoa" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -829,6 +895,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" [[package]] +name = "proc-macro-hack" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" + +[[package]] +name = "proc-macro-nested" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0afe1bd463b9e9ed51d0e0f0b50b6b146aec855c56fd182bb242388710a9b6de" + +[[package]] name = "proc-macro2" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1108,6 +1186,7 @@ dependencies = [ "serde_yaml", "termimad", "thiserror", + "tui", ] [[package]] @@ -1260,6 +1339,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" [[package]] +name = "tui" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9533d39bef0ae8f510e8a99d78702e68d1bbf0b98a78ec9740509d287010ae1e" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "either", + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] name = "unicase" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1287,6 +1381,12 @@ dependencies = [ ] [[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] name = "unicode-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -crossterm = "0.17" +crossterm = { version = "0.17", features = ["event-stream"] } clap = "2.33" directories = "2.0" flate2 = "1.0" @@ -17,5 +17,6 @@ serde_json = "1.0" serde_yaml = "0.8" termimad = "0.8" minimad = "0.6" +tui = { version = "0.9", default-features = false, features = ['crossterm'] } lazy_static = "1.4" thiserror = "1.0" @@ -1,5 +1,5 @@ # TODO - +1. maybe <query> is optional, and leaving blank starts up TUI? ### v0.2.0 0. Termimad interface for viewing questions and answers 0. use [par_iter](https://github.com/rayon-rs/rayon) for parsing markdown and manual <kbd> removers, etc. diff --git a/src/main.rs b/src/main.rs index 7d1dd26..f66e192 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ +#![allow(dead_code, unused_imports, unused_mut, unused_variables)] mod cli; mod config; mod error; mod stackexchange; mod term; +mod tui; mod utils; use crossterm::style::Color; @@ -10,7 +12,7 @@ use error::Error; use lazy_static::lazy_static; use minimad::mad_inline; use stackexchange::{LocalStorage, StackExchange}; -use term::with_error_style; +use term::mk_print_error; use termimad::{CompoundStyle, MadSkin}; fn main() { @@ -18,6 +20,7 @@ fn main() { // TODO style configuration skin.inline_code = CompoundStyle::with_fg(Color::Cyan); skin.code_block.set_fgbg(Color::Cyan, termimad::gray(20)); + let mut print_error = mk_print_error(&skin); (|| { let opts = cli::get_opts()?; let config = opts.config; @@ -48,7 +51,7 @@ fn main() { if !ls.validate_site(site)? { print_error!(skin, "$0 is not a valid StackExchange site.\n\n", site)?; - // TODO what about using text wrapping feature? + // TODO should only use inline for single lines; use termimad::text stuff print_notice!( skin, "If you think this is incorrect, try running\n\ @@ -62,23 +65,44 @@ fn main() { } if let Some(q) = opts.query { - let se = StackExchange::new(config); - let que = se.search(&q)?; - let ans = que.first().ok_or(Error::NoResults)?.answers.first().expect( - "StackExchange returned a question with no answers; \ - this shouldn't be possible!", - ); + //let se = StackExchange::new(config); + // TODO async + //let qs = se.search(&q)?; + //let ans = que.first().ok_or(Error::NoResults)?.answers.first().expect( + //"StackExchange returned a question with no answers; \ + //this shouldn't be possible!", + //); // TODO eventually do this in the right place, e.g. abstract out md parser, write benches, & do within threads - let md = ans.body.replace("<kbd>", "**[").replace("</kbd>", "]**"); - skin.print_text(&md); + // TODO do the below for --lucky with option to continue + //let md = ans.body.replace("<kbd>", "**[").replace("</kbd>", "]**"); + //skin.print_text(&md); + use crate::stackexchange::{Answer, Question}; + tui::run(vec![Question { + id: 42, + score: 323, + title: "How do I exit Vim?".to_string(), + body: "yo this be my problem dawg but don't say **do** `this`".to_string(), + answers: vec![ + Answer { + id: 422, + score: -4, + body: "# bad\nthis is my bad answer".to_string(), + is_accepted: false, + }, + Answer { + id: 423, + score: 23, + body: "this is a *good* answer tho".to_string(), + is_accepted: true, + }, + ], + }])?; } Ok(()) })() .or_else(|e: Error| { - with_error_style(&mut skin, |err_skin, stderr| { - err_skin.write_text_on(stderr, &e.to_string()) - })?; + print_error(&e.to_string())?; match e { Error::EmptySites => { print_notice!(skin, "This can likely be fixed by `so --update-sites`.") diff --git a/src/stackexchange.rs b/src/stackexchange.rs index cf03399..8c65942 100644 --- a/src/stackexchange.rs +++ b/src/stackexchange.rs @@ -1,3 +1,4 @@ +#![allow(dead_code, unused_imports, unused_mut, unused_variables)] use flate2::read::GzDecoder; use reqwest::blocking::Client; use reqwest::Url; @@ -55,6 +56,7 @@ pub struct Answer { /// Represents a StackExchange question with a custom selection of fields from /// the [StackExchange docs](https://api.stackexchange.com/docs/types/question) +// TODO let body be a generic that implements Display! #[derive(Deserialize, Debug)] pub struct Question { #[serde(rename = "question_id")] diff --git a/src/term.rs b/src/term.rs index a2c9433..8297a6a 100644 --- a/src/term.rs +++ b/src/term.rs @@ -9,17 +9,26 @@ pub fn with_error_style<R, F>(skin: &mut MadSkin, f: F) -> Result<R> where F: FnOnce(&MadSkin, &mut Stderr) -> Result<R, termimad::Error>, { - (|| { - let err = &mut std::io::stderr(); - let p = skin.paragraph.clone(); - skin.paragraph.set_fg(Color::Red); - mad_write_inline!(err, skin, "✖ ")?; - let r: R = f(&skin, err)?; - skin.paragraph = p; - Ok::<R, termimad::Error>(r) - })() - .map_err(Error::from) + let err = &mut std::io::stderr(); + let p = skin.paragraph.clone(); + skin.paragraph.set_fg(Color::Red); + mad_write_inline!(err, skin, "✖ ")?; + let r: R = f(&skin, err)?; + skin.paragraph = p; + Ok::<R, Error>(r) +} + +/// This makes code much more convenient, but would require each style to own +/// its own skin clone. Not sure if it is worth it. +pub fn mk_print_error(skin: &MadSkin) -> impl FnMut(&str) -> Result<()> + 'static { + let mut skin = skin.clone(); + move |text: &str| { + with_error_style(&mut skin, |err_skin, stderr| { + err_skin.write_text_on(stderr, text) + }) + } } + #[macro_export] macro_rules! print_error { ($skin: expr, $md: literal $(, $value: expr )* $(,)? ) => {{ diff --git a/src/tui/app.rs b/src/tui/app.rs new file mode 100644 index 0000000..b02da8c --- /dev/null +++ b/src/tui/app.rs @@ -0,0 +1,160 @@ +#![allow(dead_code, unused_imports, unused_mut, unused_variables)] +use crossterm::execute; +use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; +use std::io; +use std::io::Write; +use tui::backend::CrosstermBackend; +use tui::buffer::Buffer; +use tui::layout::{Alignment, Constraint, Direction, Layout as TuiLayout, Rect}; +use tui::style::Style; +use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; +use tui::Terminal; + +use crate::error::Result; +use crate::stackexchange::{Answer, Question}; +use crate::tui::enumerable::Enum; +use crate::tui::list; +// ----------------------------------------- +// |question title list|answer preview list| 1/3 +// ----------------------------------------- +// |question body |answer body | 2/3 +// ----------------------------------------- +pub enum Layout { + BothColumns, + SingleColumn, + FullScreen, +} + +// Tab to cycle focus +pub enum Focus { + QuestionList, + AnswerList, + Question, + Answer, +} + +pub enum Mode { + /// Akin to vim, keys are treated as commands + Normal, + /// Akin to vim, user is typing in bottom prompt + Insert, + // TODO if adding a search feature, that will be anther mode +} + +//pub struct App<'a> { +//pub stackexchange: StackExchange, +///// the questions matching the current query +//pub question_list: StatefulList<Question>, +///// the answers to a single question (i.e. the answer list currently shown) +//pub answer_list: StatefulList<Answer>, +//pub questions: Vec<Question>, +//pub layout: Layout, +//pub focus: Focus, +//pub mode: Mode, +//pub ratio: (u32, u32), +//} + +// TODO maybe a struct like Tui::new(stackexchange) creates App::new and impls tui.run()? + +// TODO take async questions +// TODO take the entire SE struct for future questions +pub fn run(qs: Vec<Question>) -> Result<()> { + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; + + //terminal.draw(|mut f| ui::draw(&mut f, &mut app))?; + terminal.draw(|mut f| { + let chunks = TuiLayout::default() + .direction(Direction::Horizontal) + .constraints( + [ + // TODO this depends on app.ratio and app.layout + Constraint::Ratio(1, 2), + Constraint::Ratio(1, 2), + ] + .as_ref(), + ) + .split(f.size()); + // TODO this depends on app.layout + let question_pane = TuiLayout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref()) + .split(chunks[0]); + let answer_pane = TuiLayout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref()) + .split(chunks[1]); + let block = Block::default().title("Questions").borders(Borders::ALL); + f.render_widget(block, question_pane[0]); + let block = Block::default().title("Answers").borders(Borders::ALL); + f.render_widget(block, answer_pane[0]); + // for now, just text + let t = qs[0].body.clone(); + let text = [Text::raw(t)]; + let p = Paragraph::new(text.iter()) + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Left) + .wrap(true); + f.render_widget(p, question_pane[1]); + let t = qs[0].answers[0].body.clone(); + let text = [Text::raw(t)]; + let p = Paragraph::new(text.iter()) + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Left) + .wrap(true); + f.render_widget(p, answer_pane[1]); + })?; + //disable_raw_mode()?; + std::thread::sleep(std::time::Duration::from_millis(10000)); + execute!(terminal.backend_mut(), LeaveAlternateScreen,)?; + terminal.show_cursor()?; + Ok(()) +} + +//fmttext = termimad::FmtText::from(madskin, md, mwidth) +//// whose dispaly instance just calls +//for line in &self.lines { +//self.skin.write_fmt_line(f, line, self.width, false)?; +//writeln!(f)?; +//} + +////OR directly with madskin +//skin.write_in_area_on(w: Writer, md: &str, area: &Area) + +// lowest level +//skin.write_fmt_composite: actually applies styles to pieces of md text + +// little higher +//skin.write_fmt_line: also handles lines such as table borders + +// higher +//text.write_on: actually queues stuff up, cycling through its self.text.lines() and +//handling scrollbar + +// TODO shift HJKL moves layout boundaries +// TODO hjkl to move focus? at least for lists.. + +// TODO should my se structs have &str instead of String? + +// Space to cycle layout +// TODO query initial term size to init layout + +impl Enum for Layout { + fn to_enum(&self) -> u8 { + match self { + Layout::BothColumns => 0, + Layout::SingleColumn => 1, + Layout::FullScreen => 2, + } + } + fn from_enum(i: u8) -> Self { + match i % 3 { + 0 => Layout::BothColumns, + 1 => Layout::SingleColumn, + _ => Layout::FullScreen, + } + } +} diff --git a/src/tui/enumerable.rs b/src/tui/enumerable.rs new file mode 100644 index 0000000..2b31dc9 --- /dev/null +++ b/src/tui/enumerable.rs @@ -0,0 +1,12 @@ +/// Borrowed from Haskell +/// Should be possible to auto derive +pub trait Enum: Sized { + fn to_enum(&self) -> u8; + fn from_enum(i: u8) -> Self; + fn succ(&self) -> Self { + Self::from_enum(self.to_enum() + 1) + } + fn pred(&self) -> Self { + Self::from_enum(self.to_enum() - 1) + } +} diff --git a/src/tui/list.rs b/src/tui/list.rs new file mode 100644 index 0000000..81d992d --- /dev/null +++ b/src/tui/list.rs @@ -0,0 +1,60 @@ +use tui::widgets::ListState; + +pub struct StatefulList<T> { + pub state: ListState, + pub items: Vec<T>, +} + +impl<T> StatefulList<T> { + pub fn new() -> StatefulList<T> { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } + + pub fn with_items(items: Vec<T>) -> StatefulList<T> { + 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); + } +} + +impl<T> From<Vec<T>> for StatefulList<T> { + fn from(v: Vec<T>) -> Self { + StatefulList::with_items(v) + } +} diff --git a/src/tui/markdown.hs b/src/tui/markdown.hs new file mode 100644 index 0000000..3627d23 --- /dev/null +++ b/src/tui/markdown.hs @@ -0,0 +1,3 @@ +pub struct Markdown { + + diff --git a/src/tui/markdown.rs b/src/tui/markdown.rs new file mode 100644 index 0000000..c3dbfc9 --- /dev/null +++ b/src/tui/markdown.rs @@ -0,0 +1,135 @@ +// possibly make this a separate package for others +pub struct Markdown {}; + +// Below is Paragraph; adjust to Markdown! + +// style <-> termimad::MadSkin + +// Wrap/Truncate <-> termimad::DisplayableLine +// and the stuff in termimad::composite{fit_width, extend_width} + +// Looping over line_composer.next_line() <-> write_fmt_line +// (see also text_view.write_on) + +#[derive(Debug, Clone)] +pub struct Paragraph<'a, 't, T> +where + T: Iterator<Item = &'t Text<'t>>, +{ + /// A block to wrap the widget in + block: Option<Block<'a>>, + /// Widget style + style: Style, + /// Wrap the text or not + wrapping: bool, + /// The text to display + text: T, + /// Should we parse the text for embedded commands + raw: bool, + /// Scroll + scroll: u16, + /// Aligenment of the text + alignment: Alignment, +} + +impl<'a, 't, T> Paragraph<'a, 't, T> +where + T: Iterator<Item = &'t Text<'t>>, +{ + pub fn new(text: T) -> Paragraph<'a, 't, T> { + Paragraph { + block: None, + style: Default::default(), + wrapping: false, + raw: false, + text, + scroll: 0, + alignment: Alignment::Left, + } + } + + pub fn block(mut self, block: Block<'a>) -> Paragraph<'a, 't, T> { + self.block = Some(block); + self + } + + pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> { + self.style = style; + self + } + + pub fn wrap(mut self, flag: bool) -> Paragraph<'a, 't, T> { + self.wrapping = flag; + self + } + + pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> { + self.raw = flag; + self + } + + pub fn scroll(mut self, offset: u16) -> Paragraph<'a, 't, T> { + self.scroll = offset; + self + } + + pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a, 't, T> { + self.alignment = alignment; + self + } +} + +impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T> +where + T: Iterator<Item = &'t Text<'t>>, +{ + fn render(mut self, area: Rect, buf: &mut Buffer) { + let text_area = match self.block { + Some(ref mut b) => { + b.render(area, buf); + b.inner(area) + } + None => area, + }; + + if text_area.height < 1 { + return; + } + + buf.set_background(text_area, self.style.bg); + + let style = self.style; + let mut styled = self.text.by_ref().flat_map(|t| match *t { + Text::Raw(ref d) => { + let data: &'t str = d; // coerce to &str + Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style))) + } + Text::Styled(ref d, s) => { + let data: &'t str = d; // coerce to &str + Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s))) + } + }); + + let mut line_composer: Box<dyn LineComposer> = if self.wrapping { + Box::new(WordWrapper::new(&mut styled, text_area.width)) + } else { + Box::new(LineTruncator::new(&mut styled, text_area.width)) + }; + let mut y = 0; + while let Some((current_line, current_line_width)) = line_composer.next_line() { + if y >= self.scroll { + let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); + for Styled(symbol, style) in current_line { + buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll) + .set_symbol(symbol) + .set_style(*style); + x += symbol.width() as u16; + } + } + y += 1; + if y >= text_area.height + self.scroll { + break; + } + } + } +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..f74434f --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,6 @@ +mod app; +mod enumerable; +mod list; +mod ui; + +pub use app::run; diff --git a/src/tui/ui.rs b/src/tui/ui.rs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/tui/ui.rs |