forked from cadey/maj
Merge pull request 'majc: support history' (#3) from majc-history into main
Reviewed-on: cadey/maj#3
This commit is contained in:
commit
0217fa2792
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,5 +1,17 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### maj
|
||||||
|
|
||||||
|
- `maj::get` unfortunately had to become async in order for fetching `gus.guru` to work
|
||||||
|
|
||||||
|
### majc
|
||||||
|
|
||||||
|
- keep track of loaded pages as history
|
||||||
|
- `l` to view/navigate to links
|
||||||
|
- use [smol](https://github.com/stjepang/smol) for doing the async operations synchronously
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### maj
|
### maj
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "maj"
|
name = "maj"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
authors = ["Christine Dodrill <me@christine.website>"]
|
authors = ["Christine Dodrill <me@christine.website>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "0BSD"
|
license = "0BSD"
|
||||||
|
@ -18,6 +18,7 @@ rustls = { version = "0.18", optional = true, features = ["dangerous_configurati
|
||||||
webpki = { version = "0.21.0", optional = true }
|
webpki = { version = "0.21.0", optional = true }
|
||||||
webpki-roots = { version = "0.20", optional = true }
|
webpki-roots = { version = "0.20", optional = true }
|
||||||
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true }
|
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true }
|
||||||
|
tokio-io-timeout = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
url = "2"
|
url = "2"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
@ -42,7 +43,7 @@ optional = true
|
||||||
[features]
|
[features]
|
||||||
default = ["client", "server"]
|
default = ["client", "server"]
|
||||||
|
|
||||||
client = ["rustls", "webpki", "webpki-roots"]
|
client = ["rustls", "webpki", "webpki-roots", "tokio", "tokio-rustls"]
|
||||||
server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"]
|
server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "majc"
|
name = "majc"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
authors = ["Christine Dodrill <me@christine.website>"]
|
authors = ["Christine Dodrill <me@christine.website>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
@ -10,5 +10,10 @@ edition = "2018"
|
||||||
cursive = "0.15"
|
cursive = "0.15"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
webpki = "0.21.0"
|
||||||
|
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] }
|
||||||
|
rustls = { version = "0.18", features = ["dangerous_configuration"] }
|
||||||
|
smol = { version = "0.3", features = ["tokio02"] }
|
||||||
|
|
||||||
maj = { path = ".." }
|
maj = { path = ".." }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
* History works (h/l)
|
||||||
|
* Added changelog screen
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
* Base functionality implemented
|
|
@ -0,0 +1,34 @@
|
||||||
|
use cursive::{
|
||||||
|
traits::*,
|
||||||
|
views::{Dialog, TextView},
|
||||||
|
Cursive,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn help(siv: &mut Cursive) {
|
||||||
|
let content = include_str!("./help.gmi");
|
||||||
|
|
||||||
|
siv.add_layer(
|
||||||
|
Dialog::around(TextView::new(crate::gemini::render(content)).scrollable())
|
||||||
|
.title("Help")
|
||||||
|
.dismiss_button("Ok"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn changelog(siv: &mut Cursive) {
|
||||||
|
let content = include_str!("./changelog.gmi");
|
||||||
|
|
||||||
|
siv.add_layer(
|
||||||
|
Dialog::around(TextView::new(crate::gemini::render(content)).scrollable())
|
||||||
|
.title("Changelog")
|
||||||
|
.dismiss_button("Ok"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn about(siv: &mut Cursive) {
|
||||||
|
siv.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"),
|
||||||
|
)));
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
use cursive::{
|
||||||
|
theme::{Effect, Style},
|
||||||
|
traits::*,
|
||||||
|
utils::markup::StyledString,
|
||||||
|
views::{Dialog, EditView, ResizedView, SelectView, TextView},
|
||||||
|
Cursive,
|
||||||
|
};
|
||||||
|
use maj::{self, Response};
|
||||||
|
use std::str;
|
||||||
|
use tokio_rustls::rustls::ClientConfig;
|
||||||
|
|
||||||
|
/// The state of the browser.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct State {
|
||||||
|
url: Option<url::Url>,
|
||||||
|
history: Vec<url::Url>,
|
||||||
|
links: Vec<(url::Url, Option<String>)>,
|
||||||
|
cfg: ClientConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
State {
|
||||||
|
url: None,
|
||||||
|
history: vec![],
|
||||||
|
links: vec![],
|
||||||
|
cfg: crate::tls::config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn history(siv: &mut Cursive) {
|
||||||
|
let st = siv.user_data::<State>().unwrap();
|
||||||
|
|
||||||
|
let mut select = SelectView::new().autojump();
|
||||||
|
|
||||||
|
for entry in st.history.iter() {
|
||||||
|
select.add_item(
|
||||||
|
format!("{}{}", entry.host_str().unwrap(), entry.path()),
|
||||||
|
entry.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
select.set_on_submit(open);
|
||||||
|
|
||||||
|
siv.add_layer(
|
||||||
|
Dialog::around(select.scrollable())
|
||||||
|
.title("History")
|
||||||
|
.button("Cancel", |s| {
|
||||||
|
s.pop_layer();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn links(siv: &mut Cursive) {
|
||||||
|
let st = siv.user_data::<State>().unwrap();
|
||||||
|
|
||||||
|
let mut select = SelectView::new().autojump();
|
||||||
|
|
||||||
|
for link in st.links.iter() {
|
||||||
|
select.add_item(
|
||||||
|
match &link.1 {
|
||||||
|
None => link.0.to_string(),
|
||||||
|
Some(name) => format!("{}: {}", name, link.0.to_string()),
|
||||||
|
},
|
||||||
|
link.0.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
select.set_on_submit(open);
|
||||||
|
|
||||||
|
siv.add_layer(
|
||||||
|
Dialog::around(select.scrollable())
|
||||||
|
.title("Links")
|
||||||
|
.button("Cancel", |s| {
|
||||||
|
s.pop_layer();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(siv: &mut Cursive, url: &str) {
|
||||||
|
siv.pop_layer();
|
||||||
|
log::debug!("got URL: {}", url);
|
||||||
|
let mut st = siv.user_data::<State>().unwrap();
|
||||||
|
|
||||||
|
let mut url: String = url.to_string();
|
||||||
|
if !url.starts_with("gemini://") {
|
||||||
|
url = format!("gemini://{}", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = url::Url::parse(url.as_str()).unwrap();
|
||||||
|
|
||||||
|
st.url = Some(url.clone());
|
||||||
|
|
||||||
|
match smol::run(maj::get(url.to_string(), st.cfg.clone())) {
|
||||||
|
Ok(resp) => {
|
||||||
|
st.history.push(url.clone());
|
||||||
|
show(siv, url.to_string().as_str(), resp);
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
log::error!("got response error: {:?}", why);
|
||||||
|
siv.add_layer(Dialog::info(format!("Error fetching response: {:?}", why)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(siv: &mut Cursive, url: &str, resp: Response) {
|
||||||
|
use maj::StatusCode::*;
|
||||||
|
|
||||||
|
match resp.status {
|
||||||
|
Success => match str::from_utf8(&resp.body) {
|
||||||
|
Ok(content) => {
|
||||||
|
let content: StyledString = if resp.meta.starts_with("text/gemini") {
|
||||||
|
populate_links(siv, content);
|
||||||
|
render(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 => {
|
||||||
|
let st = siv.user_data::<State>().unwrap();
|
||||||
|
st.history.pop();
|
||||||
|
open(siv, resp.meta.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
PermanentRedirect => {
|
||||||
|
let st = siv.user_data::<State>().unwrap();
|
||||||
|
st.history.pop();
|
||||||
|
open(siv, resp.meta.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
Input => {
|
||||||
|
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 st = s.user_data::<State>().unwrap();
|
||||||
|
let url = st.url.as_ref().unwrap();
|
||||||
|
let mut u = url.clone();
|
||||||
|
u.set_query(Some(&inp));
|
||||||
|
&u.to_string()
|
||||||
|
};
|
||||||
|
open(s, url);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
siv.add_layer(Dialog::info(format!("{:?}: {}", resp.status, resp.meta)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn populate_links(siv: &mut Cursive, body: &str) {
|
||||||
|
use maj::gemini::Node::*;
|
||||||
|
let doc = maj::gemini::parse(body);
|
||||||
|
let st = siv.user_data::<State>().unwrap();
|
||||||
|
st.links.clear();
|
||||||
|
let u = st.url.as_ref().unwrap();
|
||||||
|
|
||||||
|
for node in doc {
|
||||||
|
match node {
|
||||||
|
Link { to, name } => st.links.push((u.join(&to).unwrap(), name)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(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(name, Style::from(Effect::Underline)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Preformatted(data) => styled.append(StyledString::plain(data)),
|
||||||
|
Heading { level, body } => styled.append(StyledString::styled(
|
||||||
|
format!("{} {}", "#".repeat(level as usize), 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
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
# majc
|
|
||||||
```
|
```
|
||||||
__
|
__
|
||||||
_____ _____ |__| ____
|
_____ _____ |__| ____
|
||||||
|
@ -10,17 +9,16 @@
|
||||||
|
|
||||||
A curses client for Gemini!
|
A curses client for Gemini!
|
||||||
|
|
||||||
=> gemini://gemini.circumlunar.space/ Gemini homepage
|
|
||||||
|
|
||||||
## Homepage
|
## Homepage
|
||||||
The main homepage for majc is on tulpa.dev:
|
The main homepage for majc is on tulpa.dev:
|
||||||
|
|
||||||
=> https://tulpa.dev/cadey/maj
|
=> https://tulpa.dev/cadey/maj
|
||||||
|
|
||||||
## Important Keys
|
## Important Keys
|
||||||
<esc>: opens the menubar
|
<esc>: opens the menubar
|
||||||
c: closes the active window
|
c: closes the active window
|
||||||
o: prompts to open a URL
|
o: prompts to open a URL
|
||||||
|
h: shows history
|
||||||
|
l: shows active links in the page
|
||||||
q: quits majc
|
q: quits majc
|
||||||
?: shows this screen
|
?: shows this screen
|
||||||
~: toggles the debug logging pane
|
~: toggles the debug logging pane
|
||||||
|
|
194
majc/src/main.rs
194
majc/src/main.rs
|
@ -1,198 +1,46 @@
|
||||||
use cursive::{
|
use cursive::{event::Key, menu::MenuTree};
|
||||||
event::Key,
|
|
||||||
menu::MenuTree,
|
pub(crate) mod commands;
|
||||||
theme::{Effect, Style, Color, PaletteColor, Theme},
|
pub(crate) mod gemini;
|
||||||
traits::*,
|
pub(crate) mod theme;
|
||||||
utils::markup::StyledString,
|
pub(crate) mod tls;
|
||||||
views::{Dialog, EditView, Panel, ResizedView, TextView},
|
|
||||||
Cursive,
|
|
||||||
};
|
|
||||||
use maj::{self, Response, StatusCode};
|
|
||||||
use std::str;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
cursive::logger::init();
|
cursive::logger::init();
|
||||||
|
|
||||||
let mut siv = cursive::default();
|
let mut siv = cursive::default();
|
||||||
|
|
||||||
let theme = custom_theme_from_cursive(&siv);
|
theme::apply(&mut siv);
|
||||||
siv.set_theme(theme);
|
|
||||||
|
siv.set_user_data(gemini::State::default());
|
||||||
|
|
||||||
siv.add_global_callback('c', |s| {
|
siv.add_global_callback('c', |s| {
|
||||||
s.pop_layer();
|
s.pop_layer();
|
||||||
});
|
});
|
||||||
siv.add_global_callback('q', cursive::Cursive::quit);
|
siv.add_global_callback('q', cursive::Cursive::quit);
|
||||||
siv.add_global_callback('~', cursive::Cursive::toggle_debug_console);
|
siv.add_global_callback('~', cursive::Cursive::toggle_debug_console);
|
||||||
siv.add_global_callback('o', open_prompt);
|
siv.add_global_callback('h', gemini::history);
|
||||||
siv.add_global_callback('?', help);
|
siv.add_global_callback('l', gemini::links);
|
||||||
|
siv.add_global_callback('o', gemini::open_prompt);
|
||||||
|
siv.add_global_callback('?', commands::help);
|
||||||
|
|
||||||
siv.menubar()
|
siv.menubar()
|
||||||
.add_subtree(
|
.add_subtree(
|
||||||
"majc",
|
"majc",
|
||||||
MenuTree::new()
|
MenuTree::new()
|
||||||
.leaf("About", move |s| {
|
.leaf("About", commands::about)
|
||||||
s.add_layer(Dialog::info(format!(
|
.leaf("Changelog", commands::changelog)
|
||||||
"{} {}\n\nby {}\n\nSee https://tulpa.dev/cadey/maj for more information",
|
.leaf("Help", commands::help)
|
||||||
env!("CARGO_PKG_NAME"),
|
.leaf("Quit", cursive::Cursive::quit),
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
env!("CARGO_PKG_AUTHORS"),
|
|
||||||
)));
|
|
||||||
})
|
|
||||||
.leaf("Help", move |s| {
|
|
||||||
help(s);
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.add_leaf("Open", |s| open_prompt(s));
|
.add_leaf("Open", gemini::open_prompt)
|
||||||
|
.add_leaf("Links", gemini::links)
|
||||||
|
.add_leaf("History", gemini::history);
|
||||||
|
|
||||||
siv.set_autohide_menu(false);
|
siv.set_autohide_menu(false);
|
||||||
siv.add_global_callback(Key::Esc, |s| s.select_menubar());
|
siv.add_global_callback(Key::Esc, |s| s.select_menubar());
|
||||||
|
|
||||||
help(&mut siv);
|
commands::help(&mut siv);
|
||||||
|
|
||||||
siv.run();
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
use cursive::{
|
||||||
|
theme::{BaseColor, BorderStyle, Color, PaletteColor},
|
||||||
|
Cursive,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn apply(siv: &mut Cursive) {
|
||||||
|
// We'll return the current theme with a small modification.
|
||||||
|
let mut theme = siv.current_theme().clone();
|
||||||
|
|
||||||
|
theme.palette[PaletteColor::Background] = Color::TerminalDefault;
|
||||||
|
theme.palette[PaletteColor::View] = Color::TerminalDefault;
|
||||||
|
theme.palette[PaletteColor::Primary] = Color::Dark(BaseColor::White);
|
||||||
|
theme.palette[PaletteColor::TitlePrimary] = Color::Light(BaseColor::White);
|
||||||
|
|
||||||
|
theme.shadow = false;
|
||||||
|
theme.borders = BorderStyle::Simple;
|
||||||
|
|
||||||
|
siv.set_theme(theme);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
use tokio_rustls::rustls;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn config() -> rustls::ClientConfig {
|
||||||
|
let mut config = rustls::ClientConfig::new();
|
||||||
|
config
|
||||||
|
.dangerous()
|
||||||
|
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoCertificateVerification {}
|
||||||
|
|
||||||
|
impl rustls::ServerCertVerifier for NoCertificateVerification {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
_roots: &rustls::RootCertStore,
|
||||||
|
_presented_certs: &[rustls::Certificate],
|
||||||
|
_dns_name: webpki::DNSNameRef<'_>,
|
||||||
|
_ocsp: &[u8],
|
||||||
|
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
|
||||||
|
Ok(rustls::ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,15 @@
|
||||||
use crate::Response;
|
use crate::Response;
|
||||||
use rustls::{ClientConfig, ClientSession, Stream, TLSError};
|
use std::{io::Cursor, sync::Arc};
|
||||||
use std::{io::prelude::*, net::TcpStream, sync::Arc};
|
use tokio::{
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
net::TcpStream,
|
||||||
|
};
|
||||||
|
use tokio_rustls::{
|
||||||
|
rustls::{TLSError},
|
||||||
|
TlsConnector,
|
||||||
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
fn config() -> ClientConfig {
|
|
||||||
let mut config = ClientConfig::new();
|
|
||||||
config.dangerous().set_certificate_verifier(Arc::new(NoCertificateVerification{}));
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoCertificateVerification {}
|
|
||||||
|
|
||||||
impl rustls::ServerCertVerifier for NoCertificateVerification {
|
|
||||||
fn verify_server_cert(&self,
|
|
||||||
_roots: &rustls::RootCertStore,
|
|
||||||
_presented_certs: &[rustls::Certificate],
|
|
||||||
_dns_name: webpki::DNSNameRef<'_>,
|
|
||||||
_ocsp: &[u8]) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
|
|
||||||
Ok(rustls::ServerCertVerified::assertion())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("TLS error: {0:?}")]
|
#[error("TLS error: {0:?}")]
|
||||||
|
@ -43,7 +31,7 @@ pub enum Error {
|
||||||
InvalidScheme(String),
|
InvalidScheme(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<T>(u: T) -> Result<crate::Response, Error>
|
pub async fn get<T>(u: T, cfg: tokio_rustls::rustls::ClientConfig) -> Result<crate::Response, Error>
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<String>,
|
||||||
{
|
{
|
||||||
|
@ -53,38 +41,73 @@ where
|
||||||
ur.set_port(Some(1965)).unwrap();
|
ur.set_port(Some(1965)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ur.scheme() == "" {
|
|
||||||
let _ = ur.set_scheme("gemini");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ur.scheme() != "gemini" {
|
if ur.scheme() != "gemini" {
|
||||||
return Err(Error::InvalidScheme(ur.scheme().to_string()));
|
return Err(Error::InvalidScheme(ur.scheme().to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cfg = Arc::new(config());
|
let cfg = Arc::new(cfg);
|
||||||
let host = ur.host_str().unwrap();
|
let host = ur.host_str().unwrap();
|
||||||
let mut sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap()))?;
|
|
||||||
let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?;
|
let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?;
|
||||||
let mut client = ClientSession::new(&cfg, name_ref);
|
let config = TlsConnector::from(cfg);
|
||||||
let mut tls = Stream::new(&mut client, &mut sock);
|
|
||||||
|
let sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap())).await?;
|
||||||
|
let mut tls = config.connect(name_ref, sock).await?;
|
||||||
|
|
||||||
let req = format!("{}\r\n", u);
|
let req = format!("{}\r\n", u);
|
||||||
log::trace!("writing request {:?}", req);
|
log::trace!("writing request {:?}", req);
|
||||||
tls.write(req.as_bytes())?;
|
tls.write(req.as_bytes()).await?;
|
||||||
Ok(Response::parse(&mut tls)?)
|
let mut buf: Vec<u8> = vec![];
|
||||||
|
tls.read_to_end(&mut buf).await?;
|
||||||
|
Ok(Response::parse(&mut Cursor::new(buf))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use tokio_rustls::rustls;
|
||||||
|
|
||||||
|
fn config() -> rustls::ClientConfig {
|
||||||
|
let mut config = rustls::ClientConfig::new();
|
||||||
|
config
|
||||||
|
.dangerous()
|
||||||
|
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoCertificateVerification {}
|
||||||
|
|
||||||
|
impl rustls::ServerCertVerifier for NoCertificateVerification {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
_roots: &rustls::RootCertStore,
|
||||||
|
_presented_certs: &[rustls::Certificate],
|
||||||
|
_dns_name: webpki::DNSNameRef<'_>,
|
||||||
|
_ocsp: &[u8],
|
||||||
|
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
|
||||||
|
Ok(rustls::ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn gemini_homepage() -> Result<(), Error> {
|
async fn gemini_homepage() -> Result<(), Error> {
|
||||||
let _ = pretty_env_logger::try_init();
|
let _ = pretty_env_logger::try_init();
|
||||||
let resp = get("gemini://gemini.circumlunar.space/".to_string())?;
|
let resp = get("gemini://gemini.circumlunar.space/".to_string(), config()).await?;
|
||||||
|
|
||||||
assert_eq!(resp.status, StatusCode::Success);
|
assert_eq!(resp.status, crate::StatusCode::Success);
|
||||||
|
assert_eq!(resp.meta, "text/gemini");
|
||||||
|
assert_ne!(resp.body.len(), 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gus() -> Result<(), Error> {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let resp = get("gemini://gus.guru/".to_string(), config()).await?;
|
||||||
|
|
||||||
|
assert_eq!(resp.status, crate::StatusCode::Success);
|
||||||
assert_eq!(resp.meta, "text/gemini");
|
assert_eq!(resp.meta, "text/gemini");
|
||||||
assert_ne!(resp.body.len(), 0);
|
assert_ne!(resp.body.len(), 0);
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ impl Response {
|
||||||
result.body = data;
|
result.body = data;
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
panic!("got here: {}, {:?}", n, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue