summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
10 files changed, 434 insertions, 23 deletions
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