WIP: Simple frontend and backend to search notmuch mail.
This commit is contained in:
parent
9085d7f0d2
commit
8f2e14049c
1118
Cargo.lock
generated
1118
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
@ -1,28 +1,5 @@
|
|||||||
[package]
|
[workspace]
|
||||||
version = "0.1.0"
|
members = [
|
||||||
name = "letterbox"
|
"web",
|
||||||
repository = "https://github.com/seed-rs/seed-quickstart"
|
"server",
|
||||||
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']
|
|
||||||
|
|||||||
11
server/Cargo.toml
Normal file
11
server/Cargo.toml
Normal 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
9
server/Rocket.toml
Normal 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
59
server/src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
99
src/lib.rs
99
src/lib.rs
@ -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
32
web/Cargo.toml
Normal 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
161
web/src/lib.rs
Normal 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);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user