summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-06-10 11:44:17 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-06-10 11:44:17 -0700
commit7ac11eee2b38522f22fa44227ffb6411b4f758e4 (patch)
tree5e98d915600fcf3df87a4b9870e8a9789f5e5e35
parent0056c5a93feabf96f432c5a50bd492742483acbd (diff)
Trying out tui-rs...
-rw-r--r--Cargo.lock100
-rw-r--r--Cargo.toml3
-rw-r--r--TODO.md2
-rw-r--r--src/main.rs50
-rw-r--r--src/stackexchange.rs2
-rw-r--r--src/term.rs29
-rw-r--r--src/tui/app.rs160
-rw-r--r--src/tui/enumerable.rs12
-rw-r--r--src/tui/list.rs60
-rw-r--r--src/tui/markdown.hs3
-rw-r--r--src/tui/markdown.rs135
-rw-r--r--src/tui/mod.rs6
-rw-r--r--src/tui/ui.rs0
13 files changed, 537 insertions, 25 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c9f920d..765562d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index eccd5dc..bfd9fb9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/TODO.md b/TODO.md
index 2d06bce..26c87fd 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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