maj/majc/src/main.rs

199 lines
5.9 KiB
Rust

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
}