use cursive::{ event::Key, menu::MenuTree, theme::{Effect, Style, Color, PaletteColor, Theme}, traits::*, utils::markup::StyledString, views::{Dialog, EditView, Panel, ResizedView, TextView}, Cursive, }; use maj::{self, Response, StatusCode}; use std::str; fn main() { cursive::logger::init(); let mut siv = cursive::default(); let theme = custom_theme_from_cursive(&siv); siv.set_theme(theme); siv.add_global_callback('c', |s| { s.pop_layer(); }); siv.add_global_callback('q', cursive::Cursive::quit); siv.add_global_callback('~', cursive::Cursive::toggle_debug_console); siv.add_global_callback('o', open_prompt); siv.add_global_callback('?', help); siv.menubar() .add_subtree( "majc", MenuTree::new() .leaf("About", move |s| { s.add_layer(Dialog::info(format!( "{} {}\n\nby {}\n\nSee https://tulpa.dev/cadey/maj for more information", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_AUTHORS"), ))); }) .leaf("Help", move |s| { help(s); }), ) .add_leaf("Open", |s| open_prompt(s)); siv.set_autohide_menu(false); siv.add_global_callback(Key::Esc, |s| s.select_menubar()); help(&mut siv); siv.run(); } fn help(siv: &mut Cursive) { let content = include_str!("./help.gmi"); siv.add_layer( Dialog::around(Panel::new( TextView::new(render_gemini(content)).scrollable(), )) .title("Help") .dismiss_button("Ok"), ); } fn open_prompt(siv: &mut Cursive) { siv.add_layer( Dialog::around( EditView::new() .on_submit(open) .with_name("url") .fixed_width(50), ) .title("Enter a Gemini URL") .button("Ok", |s| { let url = s .call_on_name("url", |view: &mut EditView| view.get_content()) .unwrap(); open(s, &url); }) .button("Cancel", |s| { s.pop_layer(); }), ); } fn open(siv: &mut Cursive, url: &str) { siv.pop_layer(); log::debug!("got URL: {}", url); match maj::get(url.to_string()) { Ok(resp) => { show(siv, url, resp); } Err(why) => { log::error!("got response error: {:?}", why); siv.add_layer(Dialog::info(format!("Error fetching response: {:?}", why))); } } } fn show(siv: &mut Cursive, url: &str, resp: Response) { use StatusCode::*; match resp.status { Success => match str::from_utf8(&resp.body) { Ok(content) => { let content: StyledString = if resp.meta.starts_with("text/gemini") { render_gemini(content) } else { StyledString::plain(content) }; siv.add_fullscreen_layer(ResizedView::with_full_screen( Dialog::around(TextView::new(content).scrollable()) .title(format!("{}: {}", url, resp.meta)), )); } Err(why) => { siv.add_layer(Dialog::info(format!( "UTF/8 decoding error for {}: {:?}", url, why ))); } }, TemporaryRedirect => { open(siv, resp.meta.as_str()); } PermanentRedirect => { open(siv, resp.meta.as_str()); } Input => { siv.add_layer(Dialog::info("needs input support")); return; // siv.add_layer( // Dialog::around(EditView::new().with_name("input").fixed_width(50)) // .title(resp.meta) // .button("Ok", |s| { // let inp = s // .call_on_name("input", |view: &mut EditView| view.get_content()) // .unwrap(); // let url = { // let mut u = url::Url::parse(url).unwrap(); // u.set_query(Some(&inp)); // u.as_str() // }; // open(s, url); // }), // ); } _ => { siv.add_layer(Dialog::info(format!("{:?}: {}", resp.status, resp.meta))); return; } } } fn render_gemini(body: &str) -> StyledString { let doc = maj::gemini::parse(body); let mut styled = StyledString::new(); use maj::gemini::Node::*; for node in doc { match node { Text(line) => styled.append(StyledString::plain(line)), Link { to, name } => match name { None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), Some(name) => styled.append(StyledString::styled( format!("{}: {}", to, name), Style::from(Effect::Underline), )), }, Preformatted(data) => styled.append(StyledString::plain(data)), Heading { level: _, body } => { styled.append(StyledString::styled(body, Style::from(Effect::Bold))) } ListItem(item) => styled.append(StyledString::plain(format!("* {}", item))), Quote(quote) => styled.append(StyledString::plain(format!("> {}", quote))), } styled.append(StyledString::plain("\n")); } styled } fn custom_theme_from_cursive(siv: &Cursive) -> Theme { // We'll return the current theme with a small modification. let mut theme = siv.current_theme().clone(); theme.palette[PaletteColor::Background] = Color::TerminalDefault; theme }