summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-06-11 16:29:10 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-06-11 17:01:53 -0700
commit020a9390db41da75233e41db1c58549a600e1011 (patch)
tree299fb081780658c3b6545bc98a18452cafb19e21
parent37ab4c85f9d8bf1e113c4b1b419c0d17ec72b84e (diff)
Visible question & answer list
-rw-r--r--TODO.md1
-rw-r--r--src/config.rs72
-rw-r--r--src/stackexchange.rs5
-rw-r--r--src/tui/app.rs93
-rw-r--r--src/tui/markdown.rs8
5 files changed, 155 insertions, 24 deletions
diff --git a/TODO.md b/TODO.md
index a442a30..bb66b4c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -7,7 +7,6 @@ benefit of incorporating termimad features will not be felt. But, this is
changing [soon](https://meta.stackexchange.com/q/348746).
### v0.2.0
-0. remove cruft from playing with tui-rs
0. Cursive interface for viewing questions and answers
1. Focus in one of four areas
3. arrows and vim bindings to manipulate lists and scroll, depending on focus
diff --git a/src/config.rs b/src/config.rs
index 58b51f6..7fc3e83 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,7 @@
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
+use std::io::Write;
use std::path::PathBuf;
use crate::error::{Error, Result};
@@ -13,6 +14,7 @@ pub struct Config {
pub site: String,
}
+// TODO make a friender config file, like the colors.toml below
impl Default for Config {
fn default() -> Self {
Config {
@@ -39,6 +41,26 @@ pub fn user_config() -> Result<Config> {
}
}
+pub fn set_api_key(key: String) -> Result<()> {
+ let mut cfg = user_config()?;
+ cfg.api_key = Some(key);
+ write_config(&cfg)
+}
+
+/// Get project directory
+pub fn project_dir() -> Result<ProjectDirs> {
+ ProjectDirs::from("io", "Sam Tay", "so").ok_or_else(|| Error::ProjectDir)
+}
+
+pub fn theme_file_name() -> Result<PathBuf> {
+ let name = project_dir()?.config_dir().join("colors.toml");
+ if !name.as_path().exists() {
+ let mut file = utils::create_file(&name)?;
+ file.write_all(DEFAULT_COLORS_TOML.as_bytes())?;
+ }
+ Ok(name)
+}
+
fn write_config(config: &Config) -> Result<()> {
let filename = config_file_name()?;
let file = utils::create_file(&filename)?;
@@ -50,13 +72,45 @@ fn config_file_name() -> Result<PathBuf> {
Ok(project_dir()?.config_dir().join("config.yml"))
}
-/// Get project directory
-pub fn project_dir() -> Result<ProjectDirs> {
- ProjectDirs::from("io", "Sam Tay", "so").ok_or_else(|| Error::ProjectDir)
-}
+// TODO either try to respect terminal defaults, or go all in one solarized or
+// tomorrow night 80s
+static DEFAULT_COLORS_TOML: &str = r##"
+# Every field in a theme file is optional.
-pub fn set_api_key(key: String) -> Result<()> {
- let mut cfg = user_config()?;
- cfg.api_key = Some(key);
- write_config(&cfg)
-}
+shadow = false
+borders = "outset" # Alternatives are "none" and "simple"
+
+# Base colors are red, green, blue,
+# cyan, magenta, yellow, white and black.
+[colors]
+ # There are 3 ways to select a color:
+ # - The 16 base colors are selected by name:
+ # "blue", "light red", "magenta", ...
+ # - Low-resolution colors use 3 characters, each <= 5:
+ # "541", "003", ...
+ # - Full-resolution colors start with '#' and can be 3 or 6 hex digits:
+ # "#1A6", "#123456", ...
+
+ # If the value is an array, the first valid
+ # and supported color will be used.
+ background = ["default"]
+
+ # If the terminal doesn't support custom color (like the linux TTY),
+ # non-base colors will be skipped.
+ shadow = []
+ view = "111"
+
+ # An array with a single value has the same effect as a simple value.
+ primary = []
+ secondary = "cyan"
+ tertiary = "green"
+
+ # Hex values can use lower or uppercase.
+ # (base color MUST be lowercase)
+ title_primary = ["BLUE", "yellow"] # `BLUE` will be skipped.
+ title_secondary = "#ffff55"
+
+ # Lower precision values can use only 3 digits.
+ highlight = "#F88"
+ highlight_inactive = "#5555FF"
+"##;
diff --git a/src/stackexchange.rs b/src/stackexchange.rs
index 183e64f..941aad6 100644
--- a/src/stackexchange.rs
+++ b/src/stackexchange.rs
@@ -44,7 +44,7 @@ pub struct Site {
/// Represents a StackExchange answer with a custom selection of fields from
/// the [StackExchange docs](https://api.stackexchange.com/docs/types/answer)
-#[derive(Deserialize, Debug)]
+#[derive(Clone, Deserialize, Debug)]
pub struct Answer {
#[serde(rename = "answer_id")]
pub id: u32,
@@ -56,8 +56,9 @@ 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 container over answers should be generic iterator
// TODO let body be a generic that implements Display!
-#[derive(Deserialize, Debug)]
+#[derive(Clone, Deserialize, Debug)]
pub struct Question {
#[serde(rename = "question_id")]
pub id: u32,
diff --git a/src/tui/app.rs b/src/tui/app.rs
index c079828..261dd4a 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -1,8 +1,12 @@
-use cursive::views::TextView;
+use cursive::traits::Nameable;
+use cursive::views::{LinearLayout, NamedView, SelectView, TextContent, TextView};
+use std::collections::HashMap;
+use std::sync::Arc;
use super::markdown;
+use crate::config;
use crate::error::Result;
-use crate::stackexchange::Question;
+use crate::stackexchange::{Answer, Question};
// -----------------------------------------
// |question title list|answer preview list| 1/3
@@ -59,23 +63,90 @@ pub enum Mode {
// 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
+
+ //app state
+ //put this in siv.set_user_data? hmm
+ //TODO maybe this isn't necessary until multithreading
+
+ let question_map: HashMap<u32, Question> = qs.clone().into_iter().map(|q| (q.id, q)).collect();
+ let question_map = Arc::new(question_map);
+ let answer_map: HashMap<u32, Answer> = qs
+ .clone()
+ .into_iter()
+ .map(|q| q.answers.into_iter().map(|a| (a.id, a)))
+ .flatten()
+ .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");
+
+ // 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");
+
+ // question list view
+ //let question_map_ = question_map.clone();
+ //let current_question_ = current_question.clone();
+ let question_list_view: NamedView<SelectView<u32>> = SelectView::new()
+ .autojump() // ? probably not...
+ .with_all(qs.into_iter().map(|q| (q.title, q.id)))
+ .on_select(move |s, qid| {
+ let q = question_map.get(qid).unwrap().clone();
+ let q_body = q.body;
+ let q_ans = q.answers;
+ current_question.set_content(markdown::parse(q_body));
+ s.call_on_name("answer_list", move |v: &mut SelectView<u32>| {
+ v.clear();
+ v.add_all(q_ans.into_iter().map(|a| {
+ // TODO dedup newlines, split newlines, join with spaces
+ // add ellipses
+ // set const for cutoff
+ // add score & accepted checkmark
+ let mut a_body = a.body.clone();
+ a_body.truncate(50);
+ (markdown::parse(a_body), a.id)
+ }));
+ }); // TODO select initial answer
+ }) // TODO select initial question
+ .with_name("question_list");
+
+ // answer list view
+ //let answer_map_ = answer_map.clone();
+ //let current_answer_ = current_question.clone();
+ let answer_list_view: NamedView<SelectView<u32>> = SelectView::new()
+ .autojump()
+ .on_select(move |_, aid| {
+ let a = answer_map.get(aid).unwrap().clone();
+ current_answer.set_content(markdown::parse(a.body));
+ })
+ .with_name("answer_list");
//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(
+ LinearLayout::horizontal()
+ .child(
+ LinearLayout::vertical()
+ .child(question_list_view)
+ .child(question_view),
+ )
+ .child(
+ LinearLayout::vertical()
+ .child(answer_list_view)
+ .child(answer_view),
+ ),
);
- siv.add_layer(TextView::new(md));
-
siv.run();
Ok(())
}
-// TODO prettier and more valuable tests
+// TODO see cursive/examples/src/bin/select_test.rs for how to test the interface!
+// maybe see if we can conditionally run when --nocapture is passed?
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/tui/markdown.rs b/src/tui/markdown.rs
index 64d3141..de031ed 100644
--- a/src/tui/markdown.rs
+++ b/src/tui/markdown.rs
@@ -19,7 +19,12 @@ pub fn parse<S>(input: S) -> StyledString
where
S: Into<String>,
{
- let input = input.into();
+ // TODO handle other stackexchange oddities here
+ // TODO then benchmark
+ let input = input
+ .into()
+ .replace("<kbd>", "**[")
+ .replace("</kbd>", "]**");
let spans = parse_spans(&input);
@@ -183,6 +188,7 @@ pub fn parse_spans(input: &str) -> Vec<StyledIndexedSpan> {
// 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
+// TODO: how to reverse a list in Python answer is broken; test it here!
#[cfg(test)]
mod tests {
use super::*;