WIP: Simple frontend and backend to search notmuch mail.

This commit is contained in:
Bill Thiede 2021-10-24 10:21:25 -07:00
parent 9085d7f0d2
commit 8f2e14049c
11 changed files with 1377 additions and 145 deletions

1118
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,5 @@
[package]
version = "0.1.0"
name = "letterbox"
repository = "https://github.com/seed-rs/seed-quickstart"
authors = ["Bill Thiede <git@xinu.tv>"]
description = "App Description"
categories = ["category"]
license = "MIT"
readme = "./README.md"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dev-dependencies]
wasm-bindgen-test = "0.3.18"
[dependencies]
log = "0.4.14"
seed = "0.8.0"
[profile.release]
lto = true
opt-level = 'z'
codegen-units = 1
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os']
[workspace]
members = [
"web",
"server",
]

11
server/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = "0.5.0-rc.1"
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }

9
server/Rocket.toml Normal file
View File

@ -0,0 +1,9 @@
[release]
address = "0.0.0.0"
port = 9345
[debug]
address = "0.0.0.0"
port = 9345
# Uncomment to make it production like.
#log_level = "critical"

59
server/src/main.rs Normal file
View File

@ -0,0 +1,59 @@
#[macro_use]
extern crate rocket;
use std::error::Error;
use std::process::{Command, Output};
use std::str::FromStr;
use rocket::{http::Method, Route};
use rocket_cors::{AllowedHeaders, AllowedOrigins, Guard, Responder};
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[get("/search/<query>")]
async fn search(query: &str) -> std::io::Result<Vec<u8>> {
//format!("Search for '{}'", query)
let mut cmd = Command::new("notmuch");
let cmd = cmd.args(["search", "--format=json", "--limit=20", query]);
dbg!(&cmd);
let out = cmd.output()?;
Ok(out.stdout)
}
#[get("/show/<query>")]
async fn show(query: &str) -> std::io::Result<Vec<u8>> {
//format!("Search for '{}'", query)
let mut cmd = Command::new("notmuch");
let cmd = cmd.args(["show", "--format=json", "--body=true", query]);
dbg!(&cmd);
let out = cmd.output()?;
Ok(out.stdout)
}
#[rocket::main]
async fn main() -> Result<(), Box<dyn Error>> {
let allowed_origins = AllowedOrigins::all();
// You can also deserialize this
let cors = rocket_cors::CorsOptions {
allowed_origins,
allowed_methods: vec!["Get"]
.into_iter()
.map(|s| FromStr::from_str(s).unwrap())
.collect(),
allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]),
allow_credentials: true,
..Default::default()
}
.to_cors()?;
rocket::build()
.mount("/", routes![hello, search, show])
.attach(cors)
.launch()
.await?;
Ok(())
}

View File

@ -1,99 +0,0 @@
// (Lines like the one below ignore selected Clippy rules
// - it's useful when you want to check your code with `cargo make verify`
// but some rules are too "annoying" or are not applicable for your case.)
#![allow(clippy::wildcard_imports)]
use log::info;
use seed::{prelude::*, *};
// ------ ------
// Init
// ------ ------
// `init` describes what should happen when your app started.
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
Model { counter: 0 }
}
// ------ ------
// Model
// ------ ------
// `Model` describes our app state.
struct Model {
counter: i32,
}
// ------ ------
// Update
// ------ ------
// (Remove the line below once any of your `Msg` variants doesn't implement `Copy`.)
// `Msg` describes the different events you can modify state with.
enum Msg {
Increment,
SearchRequest(String),
// TODO(wathiede): replace String with serde json decoded struct
SearchResult(fetch::Result<String>),
}
// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Increment => model.counter += 1,
Msg::SearchRequest(query) => {
orders
.skip()
.perform_cmd(async { Msg::SearchResult(search_request(query).await) });
}
Msg::SearchResult(Ok(response_data)) => {
info!("fetch ok {}", response_data);
}
Msg::SearchResult(Err(fetch_error)) => {
info!("fetch failed {:?}", fetch_error);
}
}
}
async fn search_request(query: String) -> fetch::Result<String> {
Request::new(get_search_request_url(query))
.method(Method::Get)
.fetch()
.await?
.check_status()?
.json()
.await
}
fn get_search_request_url(query: String) -> String {
format!("http://localhost:1234/search?q={}", query)
}
// ------ ------
// View
// ------ ------
// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
div![
"Modified This is a counter: ",
C!["counter"],
button![
model.counter,
ev(Ev::Click, |_| Msg::SearchRequest("hello".to_string()))
],
]
}
// ------ ------
// Start
// ------ ------
// (This function is invoked by `init` function in `index.html`.)
#[wasm_bindgen(start)]
pub fn start() {
// Mount the `app` to the element with the `id` "app".
App::start("app", init, update, view);
}

32
web/Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
version = "0.1.0"
name = "letterbox"
repository = "https://github.com/seed-rs/seed-quickstart"
authors = ["Bill Thiede <git@xinu.tv>"]
description = "App Description"
categories = ["category"]
license = "MIT"
readme = "./README.md"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dev-dependencies]
wasm-bindgen-test = "0.3.18"
[dependencies]
console_error_panic_hook = "0.1.6"
log = "0.4.14"
seed = "0.8.0"
console_log = {git = "http://git.z.xinu.tv/wathiede/console_log"}
serde = "1.0.126"
[profile.release]
lto = true
opt-level = 'z'
codegen-units = 1
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os']

161
web/src/lib.rs Normal file
View File

@ -0,0 +1,161 @@
// (Lines like the one below ignore selected Clippy rules
// - it's useful when you want to check your code with `cargo make verify`
// but some rules are too "annoying" or are not applicable for your case.)
#![allow(clippy::wildcard_imports)]
use log::{error, info, Level};
use seed::{prelude::*, *};
use serde::Deserialize;
use web_sys::HtmlInputElement;
// ------ ------
// Init
// ------ ------
// `init` describes what should happen when your app started.
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
orders
.skip()
.perform_cmd(async { Msg::SearchResult(search_request("is:unread").await) });
Model {
search_results: None,
}
}
// ------ ------
// Model
// ------ ------
// `Model` describes our app state.
struct Model {
search_results: Option<Vec<SearchResult>>,
}
/*
[
{
"thread": "0000000000022fa5",
"timestamp": 1634947104,
"date_relative": "Yest. 16:58",
"matched": 1,
"total": 1,
"authors": "Blue Cross Blue Shield Claims Administrator",
"subject": "BCBS Settlement Claim Received",
"query": [
"id:20211022235824.232afe38e3e07950@bcbssettlement.com",
null
],
"tags": [
"inbox"
]
}
]
*/
#[derive(Deserialize, Debug)]
struct SearchResult {
thread: String,
timestamp: u64,
date_relative: String,
matched: isize,
total: isize,
authors: String,
subject: String,
// TODO(wathiede): what are these?
//query:Vec<_>,
tags: Vec<String>,
}
// ------ ------
// Update
// ------ ------
// (Remove the line below once any of your `Msg` variants doesn't implement `Copy`.)
// `Msg` describes the different events you can modify state with.
enum Msg {
SearchRequest(String),
// TODO(wathiede): replace String with serde json decoded struct
SearchResult(fetch::Result<Vec<SearchResult>>),
}
// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::SearchRequest(query) => {
orders
.skip()
.perform_cmd(async move { Msg::SearchResult(search_request(&query).await) });
}
Msg::SearchResult(Ok(response_data)) => {
info!("fetch ok {:?}", response_data);
model.search_results = Some(response_data);
}
Msg::SearchResult(Err(fetch_error)) => {
error!("fetch failed {:?}", fetch_error);
}
}
}
async fn search_request(query: &str) -> fetch::Result<Vec<SearchResult>> {
Request::new(get_search_request_url(query))
.method(Method::Get)
.fetch()
.await?
.check_status()?
.json()
.await
}
fn get_search_request_url(query: &str) -> String {
format!("http://nixos-07:9345/search/{}", query)
}
// ------ ------
// View
// ------ ------
// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
let results = if let Some(res) = &model.search_results {
div![res.iter().map(|r| p![
h3![&r.subject, " ", small![&r.date_relative]],
div![span![&r.authors]]
])]
} else {
div![]
};
div![
"Do Something: ",
button![
"Unread",
ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())),
],
input![
attrs! {
At::Placeholder => "Search";
At::AutoFocus => true.as_at_value();
},
input_ev(Ev::Input, Msg::SearchRequest),
],
results,
]
}
// ------ ------
// Start
// ------ ------
// (This function is invoked by `init` function in `index.html`.)
#[wasm_bindgen(start)]
pub fn start() {
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat up the file size.
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
let lvl = Level::Info;
console_log::init_with_level(lvl).expect("failed to initialize console logging");
// Mount the `app` to the element with the `id` "app".
App::start("app", init, update, view);
}