summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-06-14 22:41:13 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-06-14 22:41:13 -0700
commit812cfc8c3e3b878ae11b4c79d468e718ad583689 (patch)
tree44a8317ef8dd7ea903071c4b4ef3683ebe0898e4
parent9049d2d09116d544ec4c35a41b769a7296bfecc3 (diff)
Refactor: split up tui::app::run
-rw-r--r--TODO.md3
-rw-r--r--src/tui/app.rs218
-rw-r--r--src/tui/mod.rs1
-rw-r--r--src/tui/views.rs248
4 files changed, 331 insertions, 139 deletions
diff --git a/TODO.md b/TODO.md
index ffbcf3d..f245dff 100644
--- a/TODO.md
+++ b/TODO.md
@@ -9,9 +9,6 @@ changing [soon](https://meta.stackexchange.com/q/348746).
### v0.2.0
#### Cursive interface for viewing questions and answers
-0. Split up `run` function with many little helper functions defining callbacks
- and stull general to both the question and answer panes
-1. Make each pane scrollable
2. Handle focus with tab and h,l
4. Allow cycling layouts
5. Init with smaller layout if terminal size smaller
diff --git a/src/tui/app.rs b/src/tui/app.rs
index 62a3db9..35e3ef1 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -1,18 +1,17 @@
-use cursive::event::EventResult;
use cursive::theme::{BaseColor, Color, Effect, Style};
-use cursive::traits::{Nameable, Resizable, Scrollable};
+use cursive::utils::markup::StyledString;
use cursive::utils::span::SpannedString;
-use cursive::view::Margins;
-use cursive::views::{
- LinearLayout, NamedView, OnEventView, PaddedView, Panel, ResizedView, ScrollView, SelectView,
- TextContent, TextView,
-};
+use cursive::Cursive;
use cursive::XY;
use std::cmp;
use std::collections::HashMap;
use std::sync::Arc;
use super::markdown;
+use super::views::{
+ FullLayout, ListView, MdView, Name, NAME_ANSWER_LIST, NAME_ANSWER_VIEW, NAME_QUESTION_LIST,
+ NAME_QUESTION_VIEW,
+};
use crate::config;
use crate::error::Result;
use crate::stackexchange::{Answer, Question};
@@ -31,29 +30,6 @@ use crate::stackexchange::{Answer, Question};
// TODO Circular Focus handles layout & focus & stuff
// TODO these might be "layers" ?
-pub enum Layout {
- BothColumns,
- SingleColumn,
- FullScreen,
-}
-
-// Tab to cycle focus
-// TODO use NamedView
-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
-}
-
// TODO make my own views for lists, md, etc, and use cursive::inner_getters!
// (or at least type synonyms)
// and abstract out the common builder funcs
@@ -72,13 +48,11 @@ pub enum Mode {
//}
// TODO maybe a struct like Tui::new(stackexchange) creates App::new and impls tui.run()?
-// TODO views::SelectView?
// TODO take async questions
// TODO take the entire SE struct for future questions
pub fn run(qs: Vec<Question>) -> Result<()> {
let mut siv = cursive::default();
siv.load_theme_file(config::theme_file_name()?).unwrap(); // TODO dont unwrap
- let XY { x, y } = siv.screen_size();
//app state
//put this in siv.set_user_data? hmm
@@ -94,124 +68,96 @@ pub fn run(qs: Vec<Question>) -> Result<()> {
.collect();
let answer_map = Arc::new(answer_map);
- // question view
- let current_question = TextContent::new(""); // init would be great
- let question_view: NamedView<TextView> =
- TextView::new_with_content(current_question.clone()).with_name("question");
+ let question_view = MdView::new(Name::QuestionView);
+ let answer_view = MdView::new(Name::AnswerView);
- // answer view
- let current_answer = TextContent::new(""); // init would be great
- let answer_view: NamedView<TextView> =
- TextView::new_with_content(current_answer.clone()).with_name("answer");
+ let question_list_view = ListView::new_with_items(
+ Name::QuestionList,
+ qs.into_iter().map(|q| (preview_question(&q), q.id)),
+ move |s, qid| question_selected_callback(question_map.clone(), s, qid),
+ );
- // question list view
- //let question_map_ = question_map.clone();
- //let current_question_ = current_question.clone();
- // TODO fuck select view has indexing capabilities :facepalm:
- let mut question_list_view: NamedView<SelectView<u32>> = SelectView::new()
- .with_all(qs.into_iter().map(|q| (q.title, q.id)))
- .on_select(move |mut s, qid| {
- let q = question_map.get(qid).unwrap();
- let XY { x, y: _y } = s.screen_size();
- current_question.set_content(markdown::parse(&q.body));
- let cb = s.call_on_name("answer_list", |v: &mut SelectView<u32>| {
- v.clear();
- v.add_all(q.answers.iter().map(|a| {
- // TODO make a damn func for this
- // add score & accepted checkmark
- let width = cmp::min(a.body.len(), x / 2);
- let a_body = a.body[..width].to_owned();
- let md = markdown::preview(x, a_body);
- let color = if a.score > 0 {
- Color::Light(BaseColor::Green)
- } else {
- Color::Light(BaseColor::Red)
- };
- let mut preview = SpannedString::styled(
- format!("({}) ", a.score),
- Style::merge(&[Style::from(color), Style::from(Effect::Bold)]),
- );
- if a.is_accepted {
- preview.append_styled(
- "\u{2713} ", // "✔ "
- Style::merge(&[
- Style::from(Color::Light(BaseColor::Green)),
- Style::from(Effect::Bold),
- ]),
- );
- }
- preview.append(md);
- (preview, a.id)
- }));
- v.set_selection(0)
- });
- if let Some(cb) = cb {
- cb(&mut s)
- }
- })
- .with_name("question_list");
- let question_list_view = make_select_scrollable(question_list_view);
- let question_list_view = Panel::new(question_list_view).title("Questions");
-
- // answer list view
- //let answer_map_ = answer_map.clone();
- //let current_answer_ = current_question.clone();
- let answer_list_view: NamedView<SelectView<u32>> = SelectView::new()
- .on_select(move |_, aid| {
- let a = answer_map.get(aid).unwrap();
- current_answer.set_content(markdown::parse(&a.body));
- })
- .with_name("answer_list");
- let answer_list_view = make_select_scrollable(answer_list_view);
- let answer_list_view = Panel::new(answer_list_view).title("Answers");
+ // TODO init with qs[0].answers ?
+ let answer_list_view = ListView::new(Name::AnswerList, move |s, aid| {
+ let a = answer_map.get(aid).unwrap();
+ s.call_on_name(NAME_ANSWER_VIEW, |v: &mut MdView| v.set_content(&a.body));
+ });
- //TODO eventually do this in the right place, e.g. abstract out md
- //parser, write benches, & do within threads
- let margin = 1;
- let x = if x % 2 == 0 { x - 1 } else { x };
- siv.add_layer(PaddedView::new(
- Margins::lrtb(margin, margin, 0, 0),
- LinearLayout::horizontal()
- .child(ResizedView::with_fixed_width(
- (x - 2 * margin) / 2,
- LinearLayout::vertical()
- .child(question_list_view.scrollable().fixed_height(y / 3))
- .child(Panel::new(question_view.scrollable()).fixed_height(2 * y / 3)),
- ))
- .child(ResizedView::with_fixed_width(
- (x - 2 * margin) / 2,
- LinearLayout::vertical()
- .child(answer_list_view.scrollable().fixed_height(y / 3))
- .child(Panel::new(answer_view.scrollable()).fixed_height(2 * y / 3)),
- )),
+ siv.add_layer(FullLayout::new(
+ 1,
+ siv.screen_size(),
+ question_list_view,
+ question_view,
+ answer_list_view,
+ answer_view,
));
- let cb = siv.call_on_name("question_list", |v: &mut SelectView<u32>| {
- v.set_selection(0)
- });
+
+ let cb = siv.call_on_name(NAME_QUESTION_LIST, |v: &mut ListView| v.select(0));
if let Some(cb) = cb {
cb(&mut siv)
}
cursive::logger::init();
siv.add_global_callback('?', cursive::Cursive::toggle_debug_console);
+ println!("{:?}", siv.debug_name(NAME_QUESTION_VIEW));
siv.run();
Ok(())
}
-// TODO move this out to utils
-// use LastSizeView if i want to resize things with shift <HJKL>
-// Also, it might be that we control all scrolling from the top
-fn make_select_scrollable(
- view: NamedView<SelectView<u32>>,
-) -> ScrollView<OnEventView<NamedView<SelectView<u32>>>> {
- // Clobber existing functionality:
- OnEventView::new(view)
- .on_pre_event_inner('k', |s, _| {
- Some(EventResult::Consumed(Some(s.get_mut().select_up(1))))
+// TODO need to get size of question list view, as this will change depending on layout
+fn question_selected_callback(
+ question_map: Arc<HashMap<u32, Question>>,
+ mut s: &mut Cursive,
+ qid: &u32,
+) {
+ let q = question_map.get(qid).unwrap();
+ let XY { x, y: _y } = s.screen_size();
+ // Update question view
+ s.call_on_name(NAME_QUESTION_VIEW, |v: &mut MdView| {
+ v.set_content(&q.body);
+ })
+ .expect("TODO: make sure this is callable: setting question body on view");
+ // Update answer list view
+ let cb = s
+ .call_on_name(NAME_ANSWER_LIST, |v: &mut ListView| {
+ v.reset_with_all(q.answers.iter().map(|a| (preview_answer(x, a), a.id)))
})
- .on_pre_event_inner('j', |s, _| {
- Some(EventResult::Consumed(Some(s.get_mut().select_down(1))))
- })
- .scrollable()
+ .expect("TODO why would this ever fail");
+ cb(&mut s)
+}
+
+fn preview_question(q: &Question) -> StyledString {
+ let mut preview = pretty_score(q.score);
+ preview.append_plain(&q.title);
+ preview
+}
+
+fn preview_answer(screen_width: usize, a: &Answer) -> StyledString {
+ let width = cmp::min(a.body.len(), screen_width / 2);
+ let md = markdown::preview(width, a.body.to_owned());
+ let mut preview = pretty_score(a.score);
+ if a.is_accepted {
+ preview.append_styled(
+ "\u{2713} ", // "✔ "
+ Style::merge(&[
+ Style::from(Color::Light(BaseColor::Green)),
+ Style::from(Effect::Bold),
+ ]),
+ );
+ }
+ preview.append(md);
+ preview
+}
+
+fn pretty_score(score: i32) -> StyledString {
+ let color = if score > 0 {
+ Color::Light(BaseColor::Green)
+ } else {
+ Color::Light(BaseColor::Red)
+ };
+ SpannedString::styled(
+ format!("({}) ", score),
+ Style::merge(&[Style::from(color), Style::from(Effect::Bold)]),
+ )
}
// TODO see cursive/examples/src/bin/select_test.rs for how to test the interface!
diff --git a/src/tui/mod.rs b/src/tui/mod.rs
index 03df443..634cb29 100644
--- a/src/tui/mod.rs
+++ b/src/tui/mod.rs
@@ -3,5 +3,6 @@ mod entities;
mod enumerable;
mod markdown;
mod ui;
+mod views;
pub use app::run;
diff --git a/src/tui/views.rs b/src/tui/views.rs
new file mode 100644
index 0000000..9571341
--- /dev/null
+++ b/src/tui/views.rs
@@ -0,0 +1,248 @@
+use cursive::event::{Callback, EventResult};
+use cursive::traits::{Finder, Nameable, Resizable, Scrollable};
+use cursive::utils::markup::StyledString;
+use cursive::view::{Margins, SizeConstraint, View, ViewWrapper};
+use cursive::views::{
+ LinearLayout, NamedView, OnEventView, PaddedView, Panel, ScrollView, SelectView, TextView,
+};
+use cursive::{Cursive, Vec2};
+use std::fmt;
+use std::fmt::Display;
+
+use super::markdown;
+use crate::error::Result;
+
+pub const NAME_QUESTION_LIST: &str = "question_list";
+pub const NAME_ANSWER_LIST: &str = "answer_list";
+pub const NAME_QUESTION_VIEW: &str = "question_view";
+pub const NAME_ANSWER_VIEW: &str = "answer_view";
+
+// TODO might need resizable wrappers in types
+
+pub enum Name {
+ QuestionList,
+ AnswerList,
+ QuestionView,
+ AnswerView,
+}
+
+impl Display for Name {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match &self {
+ Name::QuestionList => write!(f, "Questions"),
+ Name::AnswerList => write!(f, "Answers"),
+ Name::QuestionView => write!(f, "Question"),
+ Name::AnswerView => write!(f, "Answer"),
+ }
+ }
+}
+
+impl From<Name> for String {
+ fn from(name: Name) -> Self {
+ match name {
+ Name::QuestionList => String::from(NAME_QUESTION_LIST),
+ Name::AnswerList => String::from(NAME_ANSWER_LIST),
+ Name::QuestionView => String::from(NAME_QUESTION_VIEW),
+ Name::AnswerView => String::from(NAME_ANSWER_VIEW),
+ }
+ }
+}
+
+// TODO maybe I should use cursive's ListView over SelectView ?
+pub type ListView = ListViewT<Panel<ScrollView<OnEventView<NamedView<SelectView<u32>>>>>>;
+
+pub struct ListViewT<T: View> {
+ inner_name: String,
+ view: T,
+}
+
+impl<T: View> ViewWrapper for ListViewT<T> {
+ cursive::wrap_impl!(self.view: T);
+}
+
+impl ListView {
+ pub fn new<F>(name: Name, on_select: F) -> NamedView<Self>
+ where
+ F: Fn(&mut Cursive, &u32) + 'static,
+ {
+ ListView::make_new::<StyledString, Vec<_>, _>(name, None, on_select)
+ }
+
+ pub fn new_with_items<S, I, F>(name: Name, items: I, on_select: F) -> NamedView<Self>
+ where
+ S: Into<StyledString>,
+ I: IntoIterator<Item = (S, u32)>,
+ F: Fn(&mut Cursive, &u32) + 'static,
+ {
+ ListView::make_new(name, Some(items), on_select)
+ }
+
+ fn make_new<S, I, F>(name: Name, items: Option<I>, on_select: F) -> NamedView<Self>
+ where
+ S: Into<StyledString>,
+ I: IntoIterator<Item = (S, u32)>,
+ F: Fn(&mut Cursive, &u32) + 'static,
+ {
+ let inner_name = name.to_string() + "_inner";
+ let mut view = SelectView::new().on_select(on_select);
+ if let Some(items) = items {
+ view.add_all(items);
+ }
+ let view = view.with_name(&inner_name);
+ let view = add_vim_bindings(view);
+ let view = view.scrollable();
+ let view = Panel::new(view).title(format!("{}", name));
+ let view = ListViewT { view, inner_name };
+ view.with_name(name)
+ }
+
+ pub fn reset_with_all<S, I>(&mut self, iter: I) -> Callback
+ where
+ S: Into<StyledString>,
+ I: IntoIterator<Item = (S, u32)>,
+ {
+ self.call_on_inner(|s| {
+ s.clear();
+ s.add_all(iter);
+ s.set_selection(0)
+ })
+ }
+
+ pub fn select(&mut self, i: usize) -> Callback {
+ self.call_on_inner(|sv| sv.set_selection(i))
+ }
+
+ fn call_on_inner<F, R>(&mut self, cb: F) -> R
+ where
+ F: FnOnce(&mut SelectView<u32>) -> R,
+ {
+ self.view.call_on_name(&self.inner_name, cb).expect("TODO")
+ }
+}
+
+pub type MdView = MdViewT<Panel<ScrollView<NamedView<TextView>>>>;
+
+pub struct MdViewT<T: View> {
+ inner_name: String,
+ view: T,
+}
+
+impl<T: View> ViewWrapper for MdViewT<T> {
+ cursive::wrap_impl!(self.view: T);
+}
+
+impl MdView {
+ pub fn new(name: Name) -> NamedView<Self> {
+ let inner_name = name.to_string() + "_inner";
+ let view = TextView::empty().with_name(&inner_name);
+ let view = view.scrollable();
+ let view = Panel::new(view);
+ let view = MdViewT { view, inner_name };
+ view.with_name(name)
+ }
+
+ /// Panics for now, to explore when result is None
+ pub fn set_content<S>(&mut self, content: S)
+ where
+ S: Into<String>,
+ {
+ self.view
+ .call_on_name(&self.inner_name, |tv: &mut TextView| {
+ tv.set_content(markdown::parse(content))
+ })
+ .expect("unwrap failed in MdView.set_content")
+ }
+}
+
+pub type FullLayout = FullLayoutT<PaddedView<LinearLayout>>;
+
+pub struct FullLayoutT<T: View> {
+ view: T,
+ lr_margin: usize,
+}
+
+// TODO set child widths based on parent
+impl ViewWrapper for FullLayoutT<PaddedView<LinearLayout>> {
+ cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
+
+ fn wrap_layout(&mut self, size: Vec2) {
+ let margin = self.lr_margin;
+ let horiz_xy = size.map_x(|x| x / 2 - margin);
+ for ix in 0..2 {
+ self.view
+ .get_inner_mut()
+ .get_child_mut(ix)
+ .and_then(|pane| {
+ // Set top level horizontal constraints
+ pane.layout(horiz_xy);
+ // Then coerce the inner linear layouts
+ pane.downcast_mut()
+ })
+ // And get their children
+ .and_then(|v: &mut LinearLayout| v.get_child_mut(0))
+ // And set the inner vertical constraints
+ .map(|v| v.layout(horiz_xy.map_y(|y| (ix + 1) * y / 3)));
+ }
+ }
+}
+
+impl FullLayout {
+ pub fn new(
+ lr_margin: usize,
+ screen_size: Vec2,
+ q_list: NamedView<ListView>,
+ q_view: NamedView<MdView>,
+ a_list: NamedView<ListView>,
+ a_view: NamedView<MdView>,
+ ) -> Self {
+ let heuristic = 1;
+ let x = SizeConstraint::Fixed(screen_size.x / 2 - lr_margin - heuristic);
+ let y_list = SizeConstraint::AtMost(screen_size.y / 3);
+ let y_view = SizeConstraint::Full; //AtLeast(2 * screen_size.y / 3);
+ let view = LinearLayout::horizontal()
+ .child(
+ // TODO decide whats better, horizontal sizing on the outside,
+ // or keeping both sizings on the 4 internal views
+ LinearLayout::vertical()
+ .child(q_list.resized(x, y_list))
+ .child(q_view.resized(x, y_view))
+ .with_name("question-pane"), // TODO constants
+ )
+ .child(
+ LinearLayout::vertical()
+ .child(a_list.resized(x, y_list))
+ .child(a_view.resized(x, y_view))
+ .with_name("answer-pane"),
+ );
+ let view = PaddedView::new(Margins::lrtb(lr_margin, lr_margin, 0, 0), view);
+ FullLayoutT { view, lr_margin }
+ }
+}
+
+fn add_vim_bindings<T: 'static>(
+ view: NamedView<SelectView<T>>,
+) -> OnEventView<NamedView<SelectView<T>>>
+where
+{
+ OnEventView::new(view)
+ .on_pre_event_inner('k', |s, _| {
+ Some(EventResult::Consumed(Some(s.get_mut().select_up(1))))
+ })
+ .on_pre_event_inner('j', |s, _| {
+ Some(EventResult::Consumed(Some(s.get_mut().select_down(1))))
+ })
+}
+
+pub enum Layout {
+ BothColumns,
+ SingleColumn,
+ FullScreen,
+}
+
+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
+}