summaryrefslogtreecommitdiffstats
path: root/src/tui/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui/app.rs')
-rw-r--r--src/tui/app.rs286
1 files changed, 167 insertions, 119 deletions
diff --git a/src/tui/app.rs b/src/tui/app.rs
index 6658f16..3c2fedf 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -20,134 +20,181 @@ use super::views::{
};
use crate::config::Config;
use crate::error::Result;
-use crate::stackexchange::{Answer, Question};
+use crate::stackexchange::{Answer, Id, Question, Search};
pub const NAME_HELP_VIEW: &str = "help_view";
-// TODO an Arc<Mutex> app state that gets auto updated with new selections would
-// be convenient
-
-pub fn run(qs: Vec<Question<Markdown>>, cfg: Config) -> Result<()> {
- let mut siv = cursive::default();
- siv.load_theme_file(Config::theme_file_path()?).unwrap(); // TODO dont unwrap
-
- let question_map: HashMap<u32, Question<Markdown>> =
- qs.clone().into_iter().map(|q| (q.id, q)).collect();
- let question_map = Arc::new(question_map);
- let answer_map: HashMap<u32, Answer<Markdown>> = qs
- .clone()
- .into_iter()
- .flat_map(|q| q.answers.into_iter().map(|a| (a.id, a)))
- .collect();
- let answer_map = Arc::new(answer_map);
-
- let question_view = MdView::new(Name::QuestionView);
- let answer_view = MdView::new(Name::AnswerView);
-
- 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),
- );
-
- 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));
- });
-
- siv.add_layer(
- LayoutView::new(
- 1,
- question_list_view,
- question_view,
- answer_list_view,
- answer_view,
- )
- .add_vim_bindings(),
- );
-
- let cb = siv.call_on_name(NAME_QUESTION_LIST, |v: &mut ListView| v.select(0));
- if let Some(cb) = cb {
- cb(&mut siv)
+pub struct App {
+ questions: HashMap<Id, Question<Markdown>>,
+ answers: HashMap<Id, Answer<Markdown>>,
+ config: Config,
+ sites: HashMap<String, String>,
+}
+
+impl App {
+ pub async fn from_search(search: Search) -> Result<Self> {
+ let qs = search.search_md().await?;
+ let questions: HashMap<u32, Question<Markdown>> =
+ qs.clone().into_iter().map(|q| (q.id, q)).collect();
+ let answers: HashMap<u32, Answer<Markdown>> = qs
+ .into_iter()
+ .flat_map(|q| q.answers.into_iter().map(|a| (a.id, a)))
+ .collect();
+ Ok(Self {
+ config: search.config,
+ sites: search.sites,
+ questions,
+ answers,
+ })
}
- // Help / View keymappings
- siv.add_global_callback('?', |s| {
- if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_HELP_VIEW) {
- s.screen_mut().remove_layer(pos);
- } else {
- s.add_layer(help());
- }
- });
-
- // Reload theme
- siv.add_global_callback(Event::CtrlChar('r'), |s| {
- s.load_theme_file(Config::theme_file_path().unwrap())
- .unwrap()
- });
-
- // Copy contents to sys clipboard
- siv.add_global_callback('y', move |s| {
- let mut v: ViewRef<LayoutView> = s
- .find_name(NAME_FULL_LAYOUT)
- .expect("bug: layout view should exist");
- let md = v.get_focused_content();
- if let Some(mut copy_cmd) = cfg.get_copy_cmd() {
- let res = (|| {
- let mut child = copy_cmd.spawn().map_err(|e| {
- if e.kind() == io::ErrorKind::NotFound {
- io::Error::new(
- io::ErrorKind::Other,
- "couldn't exec copy cmd; you may need to configure it manually",
- )
- } else {
- e
- }
- })?;
- let mut stdin = child.stdin.take().ok_or_else(|| {
- io::Error::new(io::ErrorKind::Other, "couldn't get stdin of copy cmd")
- })?;
- stdin.write_all(md.source().as_bytes())?;
- Ok("copied to clipboard!".to_string())
- })();
- temp_feedback_msg(s, res);
- }
- });
+ // TODO a <Mutex> app field that gets auto updated with new selections would be convenient
+ pub fn run(self) -> Result<()> {
+ // The underlying fields of self are just static data that we
+ // borrow from various places and callbacks; wrap in Arc to just have
+ // one allocation that gets referenced from wherever.
+ let arc = Arc::new(self);
- siv.add_global_callback(Event::Key(Key::Esc), |s| {
- if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_HELP_VIEW) {
- s.screen_mut().remove_layer(pos);
- }
- if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_TEMP_MSG) {
- s.screen_mut().remove_layer(pos);
+ let mut siv = cursive::default();
+ siv.load_theme_file(Config::theme_file_path()?).unwrap(); // TODO dont unwrap
+
+ let question_view = MdView::new(Name::QuestionView);
+ let answer_view = MdView::new(Name::AnswerView);
+
+ let arc2 = arc.clone();
+ let question_list_view = ListView::new_with_items(
+ Name::QuestionList,
+ arc.questions
+ .clone()
+ .into_values()
+ .map(|q| (preview_question(&q), q.id)),
+ move |s, qid| arc2.question_selected_callback(s, *qid),
+ );
+
+ let arc2 = arc.clone();
+ let answer_list_view = ListView::new(Name::AnswerList, move |s, aid| {
+ let a = arc2.answers.get(aid).unwrap();
+ s.call_on_name(NAME_ANSWER_VIEW, |v: &mut MdView| v.set_content(&a.body));
+ });
+
+ siv.add_layer(
+ LayoutView::new(
+ 1,
+ question_list_view,
+ question_view,
+ answer_list_view,
+ answer_view,
+ )
+ .add_vim_bindings(),
+ );
+
+ let cb = siv.call_on_name(NAME_QUESTION_LIST, |v: &mut ListView| v.select(0));
+ if let Some(cb) = cb {
+ cb(&mut siv)
}
- });
- // Run the app
- siv.run();
- Ok(())
-}
+ // Help / View keymappings
+ siv.add_global_callback('?', |s| {
+ if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_HELP_VIEW) {
+ s.screen_mut().remove_layer(pos);
+ } else {
+ s.add_layer(help());
+ }
+ });
+
+ // Reload theme
+ siv.add_global_callback(Event::CtrlChar('r'), |s| {
+ s.load_theme_file(Config::theme_file_path().unwrap())
+ .unwrap()
+ });
+
+ // Copy contents to sys clipboard
+ let arc2 = arc.clone();
+ siv.add_global_callback('y', move |s| {
+ let mut v: ViewRef<LayoutView> = s
+ .find_name(NAME_FULL_LAYOUT)
+ .expect("bug: layout view should exist");
+ let md = v.get_focused_content();
+ if let Some(mut copy_cmd) = arc2.config.get_copy_cmd() {
+ let res = (|| {
+ let mut child = copy_cmd.spawn().map_err(|e| {
+ if e.kind() == io::ErrorKind::NotFound {
+ io::Error::new(
+ io::ErrorKind::Other,
+ "couldn't exec copy cmd; you may need to configure it manually",
+ )
+ } else {
+ e
+ }
+ })?;
+ let mut stdin = child.stdin.take().ok_or_else(|| {
+ io::Error::new(io::ErrorKind::Other, "couldn't get stdin of copy cmd")
+ })?;
+ stdin.write_all(md.source().as_bytes())?;
+ Ok("copied to clipboard!".to_string())
+ })();
+ temp_feedback_msg(s, res);
+ }
+ });
+
+ // Open in browser
+ let arc2 = arc;
+ siv.add_global_callback('o', move |s| {
+ let mut v: ViewRef<LayoutView> = s
+ .find_name(NAME_FULL_LAYOUT)
+ .expect("bug: layout view should exist");
+ if let Some((qid, aid)) = v.get_focused_ids() {
+ let res = webbrowser::open(&arc2.mk_url("stackoverflow".to_string(), qid, aid))
+ .map(|_| "opened stackexchange in the browser!".to_string());
+ dbg!(&res);
+ temp_feedback_msg(s, res);
+ }
+ });
+
+ siv.add_global_callback(Event::Key(Key::Esc), |s| {
+ if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_HELP_VIEW) {
+ s.screen_mut().remove_layer(pos);
+ }
+ if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_TEMP_MSG) {
+ s.screen_mut().remove_layer(pos);
+ }
+ });
-fn question_selected_callback(
- question_map: Arc<HashMap<u32, Question<Markdown>>>,
- s: &mut Cursive,
- qid: u32,
-) {
- let q = question_map.get(&qid).unwrap();
- let body = &q.body;
- let XY { x, y: _y } = s.screen_size();
- // Update question view
- s.call_on_name(NAME_QUESTION_VIEW, |v: &mut MdView| {
- v.set_content(body);
- })
- .expect("Panic: setting question view content failed");
- // 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)))
+ // Run the app
+ siv.run();
+ Ok(())
+ }
+
+ fn mk_url(&self, site: String, question_id: Id, answer_id_opt: Option<Id>) -> String {
+ // answer link actually doesn't need question id
+ let site_url = self
+ .sites
+ .get(&site)
+ .expect("we lost track of a site?!")
+ .to_string();
+ match answer_id_opt {
+ Some(answer_id) => format!("https://{site_url}/a/{answer_id}"),
+ None => format!("https://{site_url}/a/{question_id}"),
+ }
+ }
+
+ pub fn question_selected_callback(&self, s: &mut Cursive, qid: u32) {
+ let q = self.questions.get(&qid).unwrap();
+ let body = &q.body;
+ let XY { x, y: _y } = s.screen_size();
+ // Update question view
+ s.call_on_name(NAME_QUESTION_VIEW, |v: &mut MdView| {
+ v.set_content(body);
})
- .expect("Panic: setting answer list content failed");
- cb(s)
+ .expect("Panic: setting question view content failed");
+ // 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)))
+ })
+ .expect("Panic: setting answer list content failed");
+ cb(s)
+ }
}
fn preview_question(q: &Question<Markdown>) -> StyledString {
@@ -201,6 +248,7 @@ pub fn help() -> Dialog {
**G**: Scroll To Bottom
## Misc
+**o**: Open current q/a in the browser
**y**: Copy current q/a to the clipboard
**q, ZZ, Ctrl<c>**: Exit
**Ctrl<r>**: Reload theme
@@ -216,7 +264,7 @@ pub fn help() -> Dialog {
}
pub fn temp_feedback_msg(siv: &mut Cursive, msg: io::Result<String>) {
- // TODO semaphore to close existing msg before displaying new one
+ // TODO semaphore to close existing msg before displaying new one?
let style = if msg.is_ok() {
Color::Light(BaseColor::Green)
} else {