Initial commit of ferret: command-line search
There are still some issues with launching a sub-program from within Cursive that have not been solved (see: https://github.com/gyscos/Cursive/issues/199). But, it works for the most part.
This commit is contained in:
commit
aef4f0d76a
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
1289
Cargo.lock
generated
Normal file
1289
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "ferret"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Tyler Hallada <tyler@hallada.net>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
termion = "1.5.1"
|
||||||
|
tui = "0.2.0"
|
||||||
|
liner = "0.4.4"
|
||||||
|
reqwest = "0.8.4"
|
||||||
|
scraper = "0.4.0"
|
||||||
|
chan = "0.1.20"
|
||||||
|
|
||||||
|
[dependencies.cursive]
|
||||||
|
version = "0.7.5"
|
||||||
|
default-features = false
|
||||||
|
features = ["termion-backend"]
|
158
src/main.rs
Normal file
158
src/main.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
extern crate chan;
|
||||||
|
extern crate cursive;
|
||||||
|
extern crate reqwest;
|
||||||
|
extern crate scraper;
|
||||||
|
extern crate termion;
|
||||||
|
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use cursive::Cursive;
|
||||||
|
use cursive::align::HAlign;
|
||||||
|
use cursive::event::{EventResult, Key};
|
||||||
|
use cursive::theme::{BaseColor, Color, Theme};
|
||||||
|
use cursive::traits::*;
|
||||||
|
use cursive::views::{Dialog, EditView, OnEventView, SelectView};
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
struct SearchResult {
|
||||||
|
title: String,
|
||||||
|
domain: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DDG_HTML_URL: &str = "https://duckduckgo.com";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut siv = Cursive::new();
|
||||||
|
siv.add_active_screen();
|
||||||
|
|
||||||
|
let theme = get_dark_theme(&siv);
|
||||||
|
siv.set_theme(theme);
|
||||||
|
|
||||||
|
siv.add_layer(
|
||||||
|
Dialog::new()
|
||||||
|
.title("Ferret")
|
||||||
|
// Padding is (left, right, top, bottom)
|
||||||
|
.padding((1, 1, 1, 0))
|
||||||
|
.content(
|
||||||
|
EditView::new()
|
||||||
|
.on_submit(search)
|
||||||
|
.with_id("query")
|
||||||
|
.fixed_width(50),
|
||||||
|
)
|
||||||
|
.button("Ok", |s| {
|
||||||
|
// This will run the given closure, *ONLY* if a view with the
|
||||||
|
// correct type and the given ID is found.
|
||||||
|
let query = s.call_on_id("query", |view: &mut EditView| {
|
||||||
|
// We can return content from the closure!
|
||||||
|
view.get_content()
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
// Run the next step
|
||||||
|
search(s, &query);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
siv.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will make the search and display results in a new popup.
|
||||||
|
// If the query is empty, we'll show an error message instead.
|
||||||
|
fn search(s: &mut Cursive, query: &str) {
|
||||||
|
if query.is_empty() {
|
||||||
|
s.add_layer(Dialog::info("Please enter a search query!"));
|
||||||
|
} else {
|
||||||
|
let client = Client::new();
|
||||||
|
let url = format!("{}/html/?q={}", DDG_HTML_URL, query);
|
||||||
|
let mut resp = client.get(&url).send().unwrap();
|
||||||
|
let document = Html::parse_document(&resp.text().unwrap());
|
||||||
|
let result_selector = Selector::parse(".web-result").unwrap();
|
||||||
|
let result_title_selector = Selector::parse(".result__a").unwrap();
|
||||||
|
let result_url_selector = Selector::parse(".result__url").unwrap();
|
||||||
|
|
||||||
|
let mut results: Vec<SearchResult> = Vec::new();
|
||||||
|
for result in document.select(&result_selector) {
|
||||||
|
let result_title = result.select(&result_title_selector).next().unwrap();
|
||||||
|
let result_url = result.select(&result_url_selector).next().unwrap();
|
||||||
|
results.push(SearchResult {
|
||||||
|
title: result_title.text().collect::<Vec<_>>().join(""),
|
||||||
|
domain: result_url.text().collect::<Vec<_>>().join(""),
|
||||||
|
url: String::from(result_url.value().attr("href").unwrap()),
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
s.pop_layer();
|
||||||
|
s.add_layer(
|
||||||
|
Dialog::new()
|
||||||
|
.title(query)
|
||||||
|
.button("Exit", |s| s.quit())
|
||||||
|
.content(build_list(results))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_list(results: Vec<SearchResult>) -> OnEventView<SelectView> {
|
||||||
|
let mut result_view = SelectView::new().h_align(HAlign::Left);
|
||||||
|
|
||||||
|
for (i, result) in results.into_iter().enumerate() {
|
||||||
|
let url = format!("{}{}", DDG_HTML_URL, result.url);
|
||||||
|
result_view = result_view.item(
|
||||||
|
format!("{}. {}", i, result.title),
|
||||||
|
url.clone()
|
||||||
|
)
|
||||||
|
.item(
|
||||||
|
format!(" {}", result.domain.replace("\n", "").trim()),
|
||||||
|
url.clone()
|
||||||
|
)
|
||||||
|
.item(
|
||||||
|
" ",
|
||||||
|
url.clone()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result_view.set_on_submit(open_url);
|
||||||
|
|
||||||
|
let result_view = OnEventView::new(result_view)
|
||||||
|
.on_pre_event_inner(Key::Up, |s| {
|
||||||
|
let from_bottom = (s.len() - 1) - s.selected_id().unwrap();
|
||||||
|
if from_bottom < 2 {
|
||||||
|
s.select_up(2 - from_bottom);
|
||||||
|
} else {
|
||||||
|
s.select_up(3);
|
||||||
|
}
|
||||||
|
Some(EventResult::Consumed(None))
|
||||||
|
})
|
||||||
|
.on_pre_event_inner(Key::Down, |s| {
|
||||||
|
if s.selected_id().unwrap() != s.len() - 3 {
|
||||||
|
s.select_down(4);
|
||||||
|
s.select_up(1);
|
||||||
|
}
|
||||||
|
Some(EventResult::Consumed(None))
|
||||||
|
});
|
||||||
|
|
||||||
|
result_view
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_url(_: &mut Cursive, url: &str) {
|
||||||
|
Command::new("w3m")
|
||||||
|
.args(&[url])
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.output().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dark_theme(siv: &Cursive) -> Theme {
|
||||||
|
// We'll return the current theme with a small modification
|
||||||
|
let mut theme = siv.current_theme().clone();
|
||||||
|
|
||||||
|
theme.colors.background = Color::TerminalDefault;
|
||||||
|
theme.colors.view = Color::Dark(BaseColor::Black);
|
||||||
|
theme.colors.shadow = Color::Dark(BaseColor::Black);
|
||||||
|
theme.colors.primary = Color::Dark(BaseColor::White);
|
||||||
|
theme.colors.tertiary = Color::Dark(BaseColor::Black);
|
||||||
|
|
||||||
|
theme
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user