From 0ccf788d7b5aa26757cf99c97caad4c1b82e590f Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 7 Jul 2022 03:41:44 +0200 Subject: [PATCH] Add option to export plain text room logs --- Cargo.lock | 136 +++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/export.rs | 72 ++++++++++++++++++++++++ src/main.rs | 52 +++++++++++++++--- src/vault/euph.rs | 10 ++-- 5 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 src/export.rs diff --git a/Cargo.lock b/Cargo.lock index 8865f4a..c110dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -119,6 +130,45 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "3.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -142,6 +192,7 @@ dependencies = [ "anyhow", "async-trait", "chrono", + "clap", "crossterm", "directories", "edit", @@ -157,6 +208,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "toss", + "unicode-width", ] [[package]] @@ -414,15 +466,27 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + [[package]] name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown", + "hashbrown 0.11.2", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -460,6 +524,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown 0.12.1", +] + [[package]] name = "instant" version = "0.1.12" @@ -590,6 +664,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + [[package]] name = "palette" version = "0.6.0" @@ -666,6 +746,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.40" @@ -979,6 +1083,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.98" @@ -1004,6 +1114,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.31" @@ -1346,6 +1471,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 1d90f33..b538d8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = "1.0.58" async-trait = "0.1.56" chrono = { version = "0.4.19", features = ["serde"] } +clap = { version = "3.2.8", features = ["derive"] } crossterm = "0.24.0" directories = "4.0.1" edit = "0.1.4" @@ -20,6 +21,7 @@ serde = { version = "1.0.138", features = ["derive"] } serde_json = "1.0.82" thiserror = "1.0.31" tokio = { version = "1.19.2", features = ["full"] } +unicode-width = "0.1.9" [dependencies.tokio-tungstenite] version = "0.17.1" diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..5eea8f2 --- /dev/null +++ b/src/export.rs @@ -0,0 +1,72 @@ +//! Export logs from the vault to plain text files. + +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +use unicode_width::UnicodeWidthStr; + +use crate::euph::api::Snowflake; +use crate::store::{MsgStore, Tree}; +use crate::vault::{EuphMsg, Vault}; + +const TIME_FORMAT: &str = "%F %T"; +const TIME_EMPTY: &str = " "; + +pub async fn export(vault: &Vault, room: String, file: &Path) -> anyhow::Result<()> { + let mut file = BufWriter::new(File::create(file)?); + let vault = vault.euph(room); + + let mut tree_id = vault.first_tree().await; + while let Some(some_tree_id) = tree_id { + let tree = vault.tree(&some_tree_id).await; + write_tree(&mut file, &tree, some_tree_id, 0)?; + tree_id = vault.next_tree(&some_tree_id).await; + } + + Ok(()) +} + +fn write_tree( + file: &mut BufWriter, + tree: &Tree, + id: Snowflake, + indent: usize, +) -> anyhow::Result<()> { + let indent_string = "| ".repeat(indent); + + if let Some(msg) = tree.msg(&id) { + write_msg(file, &indent_string, msg)?; + } else { + write_placeholder(file, &indent_string)?; + } + + if let Some(children) = tree.children(&id) { + for child in children { + write_tree(file, tree, *child, indent + 1)?; + } + } + + Ok(()) +} + +fn write_msg(file: &mut BufWriter, indent_string: &str, msg: &EuphMsg) -> anyhow::Result<()> { + let nick = &msg.nick; + let nick_empty = " ".repeat(nick.width()); + + for (i, line) in msg.content.lines().enumerate() { + if i == 0 { + let time = msg.time.0.format(TIME_FORMAT); + writeln!(file, "{time} {indent_string}[{nick}] {line}")?; + } else { + writeln!(file, "{TIME_EMPTY} {indent_string} {nick_empty} {line}")?; + } + } + + Ok(()) +} + +fn write_placeholder(file: &mut BufWriter, indent_string: &str) -> anyhow::Result<()> { + writeln!(file, "{TIME_EMPTY} {indent_string}...")?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index fbf35c3..5efbed6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,21 +3,65 @@ // TODO Clean up use and manipulation of toss Pos and Size mod euph; +mod export; mod logger; mod replies; mod store; mod ui; mod vault; +use std::path::PathBuf; + +use clap::Parser; use directories::ProjectDirs; use log::info; use toss::terminal::Terminal; use ui::Ui; +use vault::Vault; use crate::logger::Logger; +#[derive(Debug, clap::Subcommand)] +enum Command { + /// Run the client interactively (default). + Run, + /// Export logs for a single room as a plain text file. + Export { room: String, file: PathBuf }, +} + +impl Default for Command { + fn default() -> Self { + Self::Run + } +} + +#[derive(Debug, clap::Parser)] +struct Args { + #[clap(subcommand)] + command: Option, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + let dirs = ProjectDirs::from("de", "plugh", "cove").expect("unable to determine directories"); + println!("Data dir: {}", dirs.data_dir().to_string_lossy()); + + let vault = vault::launch(&dirs.data_dir().join("vault.db"))?; + + match args.command.unwrap_or_default() { + Command::Run => run(&vault).await?, + Command::Export { room, file } => export::export(&vault, room, &file).await?, + } + + vault.close().await; + + println!("Goodbye!"); + Ok(()) +} + +async fn run(vault: &Vault) -> anyhow::Result<()> { let (logger, logger_rx) = Logger::init(log::Level::Debug); info!( "Welcome to {} {}", @@ -25,18 +69,10 @@ async fn main() -> anyhow::Result<()> { env!("CARGO_PKG_VERSION") ); - let dirs = ProjectDirs::from("de", "plugh", "cove").expect("unable to determine directories"); - println!("Data dir: {}", dirs.data_dir().to_string_lossy()); - - let vault = vault::launch(&dirs.data_dir().join("vault.db"))?; - let mut terminal = Terminal::new()?; // terminal.set_measuring(true); Ui::run(&mut terminal, vault.clone(), logger, logger_rx).await?; drop(terminal); // So the vault can print again - vault.close().await; - - println!("Goodbye!"); Ok(()) } diff --git a/src/vault/euph.rs b/src/vault/euph.rs index 4cc69fe..69197f1 100644 --- a/src/vault/euph.rs +++ b/src/vault/euph.rs @@ -41,11 +41,11 @@ impl FromSql for Time { #[derive(Debug, Clone)] pub struct EuphMsg { - id: Snowflake, - parent: Option, - time: Time, - nick: String, - content: String, + pub id: Snowflake, + pub parent: Option, + pub time: Time, + pub nick: String, + pub content: String, } impl Msg for EuphMsg {