summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs1
-rw-r--r--src/error.rs4
-rw-r--r--src/main.rs38
-rw-r--r--src/stackexchange.rs2
-rw-r--r--src/tui/app.rs192
-rw-r--r--src/tui/list.rs60
-rw-r--r--src/tui/markdown.hs3
-rw-r--r--src/tui/markdown.rs330
-rw-r--r--src/tui/mod.rs2
9 files changed, 314 insertions, 318 deletions
diff --git a/src/config.rs b/src/config.rs
index af51de3..58b51f6 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -45,6 +45,7 @@ fn write_config(config: &Config) -> Result<()> {
Ok(serde_yaml::to_writer(file, config)?)
}
+// TODO consider switching to .toml to be consistent with colors.toml
fn config_file_name() -> Result<PathBuf> {
Ok(project_dir()?.config_dir().join("config.yml"))
}
diff --git a/src/error.rs b/src/error.rs
index 86fb55f..35847c3 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -26,8 +26,8 @@ pub enum Error {
ProjectDir,
#[error("Empty sites file in cache")]
EmptySites,
- #[error("Sorry, couldn't find any answers for your query")]
- NoResults,
+ //#[error("Sorry, couldn't find any answers for your query")]
+ //NoResults,
}
#[derive(Debug)]
diff --git a/src/main.rs b/src/main.rs
index f66e192..e099d21 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,3 @@
-#![allow(dead_code, unused_imports, unused_mut, unused_variables)]
mod cli;
mod config;
mod error;
@@ -65,40 +64,13 @@ fn main() {
}
if let Some(q) = opts.query {
- //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
- // TODO do the below for --lucky with option to continue
- //let md = ans.body.replace("<kbd>", "**[").replace("</kbd>", "]**");
+ let se = StackExchange::new(config);
+ //TODO async
+ let qs = se.search(&q)?;
+ //TODO do the print_text below for --lucky with option to continue
//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,
- },
- ],
- }])?;
+ tui::run(qs)?;
}
-
Ok(())
})()
.or_else(|e: Error| {
diff --git a/src/stackexchange.rs b/src/stackexchange.rs
index 8c65942..183e64f 100644
--- a/src/stackexchange.rs
+++ b/src/stackexchange.rs
@@ -1,4 +1,3 @@
-#![allow(dead_code, unused_imports, unused_mut, unused_variables)]
use flate2::read::GzDecoder;
use reqwest::blocking::Client;
use reqwest::Url;
@@ -25,6 +24,7 @@ const SE_SITES_PAGESIZE: u16 = 10000;
/// This structure allows interacting with parts of the StackExchange
/// API, using the `Config` struct to determine certain API settings and options.
+// TODO should my se structs have &str instead of String?
pub struct StackExchange {
client: Client,
config: Config,
diff --git a/src/tui/app.rs b/src/tui/app.rs
index b02da8c..c079828 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -1,24 +1,22 @@
-#![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 cursive::views::TextView;
+use super::markdown;
use crate::error::Result;
-use crate::stackexchange::{Answer, Question};
-use crate::tui::enumerable::Enum;
-use crate::tui::list;
+use crate::stackexchange::Question;
+
// -----------------------------------------
// |question title list|answer preview list| 1/3
// -----------------------------------------
// |question body |answer body | 2/3
// -----------------------------------------
+// TODO <shift+HJKL> moves layout boundaries
+// TODO <hjkl> to move focus? at least for lists..
+// TODO <space> to cycle layout
+// TODO <?> to bring up key mappings
+// TODO query initial term size to choose initial layout
+
+// TODO Circular Focus handles layout & focus & stuff
+// TODO these might be "layers" ?
pub enum Layout {
BothColumns,
SingleColumn,
@@ -26,6 +24,7 @@ pub enum Layout {
}
// Tab to cycle focus
+// TODO use NamedView
pub enum Focus {
QuestionList,
AnswerList,
@@ -55,106 +54,81 @@ 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 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()?;
+ let mut siv = cursive::default();
+
+ //TODO eventually do this in the right place, e.g. abstract out md
+ //parser, write benches, & do within threads
+ let md = markdown::parse(
+ qs[0].answers[0]
+ .body
+ .clone()
+ .replace("<kbd>", "**[")
+ .replace("</kbd>", "]**"),
+ );
+ siv.add_layer(TextView::new(md));
+
+ siv.run();
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,
- }
+// TODO prettier and more valuable tests
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::stackexchange::{Answer, Question};
+ #[test]
+ fn test_app() {
+ let ans_body = r#"
+Also try the iter:
+1. asdf
+2. asfd
+0. asdfa sfsdf
+
+but
+
+ cargo build --example stderr
+
+and then you run it with
+
+ cd "$(target/debug/examples/stderr)"
+ cd `(target/debug/examples/stderr)`
+
+what the application prints on stdout is used as argument to `cd`.
+
+Try it out.
+
+Hit any key to quit this screen:
+
+* **1** will print `..`
+* **2** will print `/`
+* **3** will print `~`
+* or anything else to print this text (so that you may copy-paste)
+"#;
+ let qs = 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: ans_body.to_string(),
+ is_accepted: false,
+ },
+ Answer {
+ id: 423,
+ score: 23,
+ body: "this is a *good* answer tho".to_string(),
+ is_accepted: true,
+ },
+ ],
+ }];
+
+ assert_eq!(run(qs).unwrap(), ());
}
}
diff --git a/src/tui/list.rs b/src/tui/list.rs
deleted file mode 100644
index 81d992d..0000000
--- a/src/tui/list.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-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
deleted file mode 100644
index 3627d23..0000000
--- a/src/tui/markdown.hs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub struct Markdown {
-
-
diff --git a/src/tui/markdown.rs b/src/tui/markdown.rs
index c3dbfc9..64d3141 100644
--- a/src/tui/markdown.rs
+++ b/src/tui/markdown.rs
@@ -1,135 +1,247 @@
-// possibly make this a separate package for others
-pub struct Markdown {};
+//! Parse markdown text.
+//!
+//! Extended from cursive::utils::markup::markdown to add code styles
+//! TODO: Bring in the full power of termimad (e.g. md tables) in a View;
+//! implementation of those features (.e.g automatic wrapping within each table
+//! cell) might be easier in this setting anyway.
-// Below is Paragraph; adjust to Markdown!
+use std::borrow::Cow;
-// style <-> termimad::MadSkin
+use cursive::theme::{Effect, PaletteColor, Style};
+use cursive::utils::markup::{StyledIndexedSpan, StyledString};
+use cursive::utils::span::IndexedCow;
-// Wrap/Truncate <-> termimad::DisplayableLine
-// and the stuff in termimad::composite{fit_width, extend_width}
+use pulldown_cmark::{self, CowStr, Event, Tag};
+use unicode_width::UnicodeWidthStr;
-// Looping over line_composer.next_line() <-> write_fmt_line
-// (see also text_view.write_on)
-
-#[derive(Debug, Clone)]
-pub struct Paragraph<'a, 't, T>
+/// Parses the given string as markdown text.
+pub fn parse<S>(input: S) -> StyledString
where
- T: Iterator<Item = &'t Text<'t>>,
+ S: Into<String>,
{
- /// 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,
-}
+ let input = input.into();
-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,
- }
- }
+ let spans = parse_spans(&input);
- pub fn block(mut self, block: Block<'a>) -> Paragraph<'a, 't, T> {
- self.block = Some(block);
- self
- }
+ StyledString::with_spans(input, spans)
+}
- pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> {
- self.style = style;
- self
- }
+/// Iterator that parse a markdown text and outputs styled spans.
+pub struct Parser<'a> {
+ first: bool,
+ item: Option<u64>,
+ in_list: bool,
+ stack: Vec<Style>,
+ input: &'a str,
+ parser: pulldown_cmark::Parser<'a>,
+}
- pub fn wrap(mut self, flag: bool) -> Paragraph<'a, 't, T> {
- self.wrapping = flag;
- self
+impl<'a> Parser<'a> {
+ /// Creates a new parser with the given input text.
+ pub fn new(input: &'a str) -> Self {
+ Parser {
+ input,
+ item: None,
+ in_list: false,
+ first: true,
+ parser: pulldown_cmark::Parser::new(input),
+ stack: Vec::new(),
+ }
}
- pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> {
- self.raw = flag;
- self
+ /// Creates a new span with the given value
+ fn literal<S>(&self, text: S) -> StyledIndexedSpan
+ where
+ S: Into<String>,
+ {
+ StyledIndexedSpan::simple_owned(text.into(), Style::merge(&self.stack))
}
- pub fn scroll(mut self, offset: u16) -> Paragraph<'a, 't, T> {
- self.scroll = offset;
- self
+ fn cow_to_span(&self, text: Cow<str>, style: Option<Style>) -> StyledIndexedSpan {
+ let width = text.width();
+ StyledIndexedSpan {
+ content: IndexedCow::from_cow(text, self.input),
+ attr: style.unwrap_or_else(|| Style::merge(&self.stack)),
+ width,
+ }
}
- pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a, 't, T> {
- self.alignment = alignment;
- self
+ fn cowstr_to_span(&self, text: CowStr, style: Option<Style>) -> StyledIndexedSpan {
+ let text = match text {
+ CowStr::Boxed(text) => Cow::Owned(text.into()),
+ CowStr::Borrowed(text) => Cow::Borrowed(text),
+ CowStr::Inlined(text) => Cow::Owned(text.to_string()),
+ };
+ self.cow_to_span(text, style)
}
}
-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)
+impl<'a> Iterator for Parser<'a> {
+ type Item = StyledIndexedSpan;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let next = match self.parser.next() {
+ None => return None,
+ Some(event) => event,
+ };
+
+ // TODO fix list tag
+ match next {
+ Event::Start(tag) => match tag {
+ // Add to the stack!
+ Tag::Emphasis => self.stack.push(Style::from(Effect::Italic)),
+ Tag::Heading(level) if level == 1 => {
+ self.stack.push(Style::from(PaletteColor::TitlePrimary))
+ }
+ Tag::Heading(_) => self.stack.push(Style::from(PaletteColor::TitleSecondary)),
+ // TODO style quote?
+ Tag::BlockQuote => return Some(self.literal("> ")),
+ Tag::Link(_, _, _) => return Some(self.literal("[")),
+ Tag::CodeBlock(_) => {
+ self.stack.push(Style::from(PaletteColor::Secondary));
+ return Some(self.literal("\n\n"));
+ }
+ Tag::Strong => self.stack.push(Style::from(Effect::Bold)),
+ Tag::Paragraph if !self.first && !self.in_list => {
+ return Some(self.literal("\n\n"))
+ }
+ Tag::List(ix) => {
+ self.item = ix;
+ self.in_list = true;
+ return Some(self.literal("\n\n"));
+ }
+ Tag::Item => match self.item {
+ Some(ix) => {
+ let pre = ix.to_string() + ". ";
+ return Some(
+ self.cow_to_span(Cow::Owned(pre), Some(Style::from(Effect::Bold))),
+ );
+ }
+ None => {
+ return Some(self.literal("• ".to_string()));
+ }
+ },
+ _ => (),
+ },
+ Event::End(tag) => match tag {
+ // Remove from stack!
+ Tag::Paragraph if self.first => self.first = false,
+ Tag::Heading(_) => {
+ self.stack.pop().unwrap();
+ return Some(self.literal("\n\n"));
+ }
+ // TODO underline the link
+ Tag::Link(_, link, _) => return Some(self.literal(format!("]({})", link))),
+ Tag::CodeBlock(_) => {
+ self.stack.pop().unwrap();
+ }
+ Tag::Emphasis | Tag::Strong => {
+ self.stack.pop().unwrap();
+ }
+ Tag::List(_) => {
+ self.item = None;
+ self.in_list = false;
+ }
+ Tag::Item => {
+ self.item = self.item.map(|ix| ix + 1);
+ return Some(self.literal("\n"));
+ }
+ _ => (),
+ },
+ Event::Rule => return Some(self.literal("---")),
+ Event::SoftBreak => return Some(self.literal("\n")),
+ Event::HardBreak => return Some(self.literal("\n")),
+ Event::Code(text) => {
+ return Some(
+ self.cowstr_to_span(text, Some(Style::from(PaletteColor::Secondary))),
+ );
+ }
+ // Treat all other texts the same
+ Event::FootnoteReference(text) | Event::Html(text) | Event::Text(text) => {
+ return Some(self.cowstr_to_span(text, None));
+ }
+ Event::TaskListMarker(checked) => {
+ let mark = if checked { "[x]" } else { "[ ]" };
+ return Some(self.cow_to_span(
+ Cow::Owned(mark.to_string()),
+ Some(Style::from(Effect::Bold)),
+ ));
+ }
}
- 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)))
- }
- });
+/// Parse the given markdown text into a list of spans.
+///
+/// This is a shortcut for `Parser::new(input).collect()`.
+pub fn parse_spans(input: &str) -> Vec<StyledIndexedSpan> {
+ Parser::new(input).collect()
+}
- 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;
+// TODO update these tests (some expectations will be different now)
+// TODO and add more! bang on the code, lists, etc.
+// use this as an opportunity to see how pulldown_cmark splits things up
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use cursive::utils::span::Span;
+
+ #[test]
+ fn test_parse() {
+ let input = r"
+Attention
+====
+I *really* love __Cursive__!";
+ let spans = parse_spans(input);
+ let spans: Vec<_> = spans.iter().map(|span| span.resolve(input)).collect();
+
+ // println!("{:?}", spans);
+ assert_eq!(
+ &spans[..],
+ &[
+ Span {
+ content: "# ",
+ width: 2,
+ attr: &Style::none(),
+ },
+ Span {
+ content: "Attention",
+ width: 9,
+ attr: &Style::none(),
+ },
+ Span {
+ content: "\n\n",
+ width: 0,
+ attr: &Style::none(),
+ },
+ Span {
+ content: "I ",
+ width: 2,
+ attr: &Style::none(),
+ },
+ Span {
+ content: "really",
+ width: 6,
+ attr: &Style::from(Effect::Italic),
+ },
+ Span {
+ content: " love ",
+ width: 6,
+ attr: &Style::none(),
+ },
+ Span {
+ content: "Cursive",
+ width: 7,
+ attr: &Style::from(Effect::Bold),
+ },
+ Span {
+ content: "!",
+ width: 1,
+ attr: &Style::none(),
}
- }
- y += 1;
- if y >= text_area.height + self.scroll {
- break;
- }
- }
+ ]
+ );
}
}
diff --git a/src/tui/mod.rs b/src/tui/mod.rs
index f74434f..12d415a 100644
--- a/src/tui/mod.rs
+++ b/src/tui/mod.rs
@@ -1,6 +1,6 @@
mod app;
mod enumerable;
-mod list;
+mod markdown;
mod ui;
pub use app::run;