From 7b78196dddb13e6976539147adec9b622f1bf899 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sat, 13 Jan 2024 11:10:10 +0100 Subject: [PATCH] Add desktop notifications (notify-send, osascript, custom command) --- Cargo.lock | 20 ++++++++++++ Cargo.toml | 1 + src/cmd/rec.rs | 13 +++++++- src/config.rs | 9 ++++++ src/main.rs | 1 + src/notifier.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ src/recorder.rs | 32 +++++++++++++++---- 7 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/notifier.rs diff --git a/Cargo.lock b/Cargo.lock index 15bbd1a..dbd4459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,7 @@ dependencies = [ "signal-hook", "termion", "uuid", + "which", ] [[package]] @@ -290,6 +291,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1379,6 +1386,19 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 4f59d5e..063f249 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ uuid = { version = "1.6.1", features = ["v4"] } reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "rustls-tls", "multipart", "gzip", "json"] } rustyline = "13.0.0" config = { version = "0.13.4", default-features = false, features = ["toml", "ini"] } +which = "5.0.0" diff --git a/src/cmd/rec.rs b/src/cmd/rec.rs index f654902..4e20782 100644 --- a/src/cmd/rec.rs +++ b/src/cmd/rec.rs @@ -1,6 +1,7 @@ use crate::config::Config; use crate::format::{asciicast, raw}; use crate::locale; +use crate::notifier; use crate::pty; use crate::recorder::{self, KeyBindings}; use crate::tty; @@ -109,8 +110,10 @@ impl Cli { }; let keys = get_key_bindings(config)?; + let notifier = get_notifier(config); - let mut recorder = recorder::Recorder::new(writer, append, self.stdin, metadata, keys); + let mut recorder = + recorder::Recorder::new(writer, append, self.stdin, metadata, keys, notifier); let exec_command = build_exec_command(self.command); let exec_extra_env = build_exec_extra_env(); @@ -160,6 +163,14 @@ fn get_key_bindings(config: &Config) -> Result { Ok(keys) } +fn get_notifier(config: &Config) -> Box { + if config.notifications.enabled { + notifier::get_notifier(config.notifications.command.clone()) + } else { + Box::new(notifier::NullNotifier) + } +} + fn capture_env(vars: &str) -> HashMap { let vars = vars.split(',').collect::>(); diff --git a/src/config.rs b/src/config.rs index 547b187..04ba489 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,6 +17,7 @@ pub type Key = Option>; pub struct Config { server: Server, cmd: Cmd, + pub notifications: Notifications, } #[derive(Debug, Deserialize)] @@ -54,6 +55,13 @@ pub struct Play { pub next_marker_key: Option, } +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Notifications { + pub enabled: bool, + pub command: Option, +} + impl Config { pub fn new(server_url: Option) -> Result { let mut config = config::Config::builder() @@ -61,6 +69,7 @@ impl Config { .set_default("cmd.rec.input", false)? .set_default("cmd.rec.env", "SHELL,TERM")? .set_default("cmd.play.speed", 1.0)? + .set_default("notifications.enabled", true)? .add_source( config::File::with_name(&user_defaults_path()?.to_string_lossy()).required(false), ) diff --git a/src/main.rs b/src/main.rs index 0b74213..ae97cc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod config; mod format; mod io; mod locale; +mod notifier; mod player; mod pty; mod recorder; diff --git a/src/notifier.rs b/src/notifier.rs new file mode 100644 index 0000000..1f34c9f --- /dev/null +++ b/src/notifier.rs @@ -0,0 +1,85 @@ +use anyhow::Result; +use std::{ + ffi::OsStr, + path::PathBuf, + process::{Command, Stdio}, +}; +use which::which; + +pub trait Notifier: Send { + fn notify(&self, message: String) -> Result<()>; +} + +pub fn get_notifier(custom_command: Option) -> Box { + if let Some(command) = custom_command { + Box::new(CustomNotifier(command)) + } else { + LibNotifyNotifier::get() + .map(|n| Box::new(n) as Box) + .or_else(|| AppleScriptNotifier::get().map(|n| Box::new(n) as Box)) + .unwrap_or_else(|| Box::new(NullNotifier)) + } +} + +pub struct LibNotifyNotifier(PathBuf); + +impl LibNotifyNotifier { + fn get() -> Option { + which("notify-send").ok().map(LibNotifyNotifier) + } +} + +impl Notifier for LibNotifyNotifier { + fn notify(&self, message: String) -> Result<()> { + exec(&mut Command::new(&self.0), &["asciinema", &message]) + } +} + +pub struct AppleScriptNotifier(PathBuf); + +impl AppleScriptNotifier { + fn get() -> Option { + which("osascript").ok().map(AppleScriptNotifier) + } +} + +impl Notifier for AppleScriptNotifier { + fn notify(&self, message: String) -> Result<()> { + let text = message.replace('\"', "\\\""); + let script = format!("display notification \"{text}\" with title \"asciinema\""); + + exec(&mut Command::new(&self.0), &["-e", &script]) + } +} + +pub struct CustomNotifier(String); + +impl Notifier for CustomNotifier { + fn notify(&self, text: String) -> Result<()> { + exec::<&str>( + Command::new("/bin/sh") + .args(["-c", &self.0]) + .env("TEXT", text), + &[], + ) + } +} + +pub struct NullNotifier; + +impl Notifier for NullNotifier { + fn notify(&self, _text: String) -> Result<()> { + Ok(()) + } +} + +fn exec>(command: &mut Command, args: &[S]) -> Result<()> { + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(args) + .status()?; + + Ok(()) +} diff --git a/src/recorder.rs b/src/recorder.rs index 8187cf7..f6c1f9e 100644 --- a/src/recorder.rs +++ b/src/recorder.rs @@ -1,4 +1,5 @@ use crate::config::Key; +use crate::notifier::Notifier; use crate::pty; use std::collections::HashMap; use std::io; @@ -14,6 +15,7 @@ pub struct Recorder { record_input: bool, metadata: Metadata, keys: KeyBindings, + notifier: Option>, sender: mpsc::Sender, receiver: Option>, handle: Option, @@ -50,6 +52,7 @@ enum Message { Input(u64, Vec), Resize(u64, (u16, u16)), Marker(u64), + Notification(String), } struct JoinHandle(Option>); @@ -61,6 +64,7 @@ impl Recorder { record_input: bool, metadata: Metadata, keys: KeyBindings, + notifier: Box, ) -> Self { let (sender, receiver) = mpsc::channel(); @@ -72,6 +76,7 @@ impl Recorder { record_input, metadata, keys, + notifier: Some(notifier), sender, receiver: Some(receiver), handle: None, @@ -86,6 +91,14 @@ impl Recorder { self.start_time.elapsed().as_micros() as u64 } } + + fn notify(&self, text: S) { + let msg = Message::Notification(text.to_string()); + + self.sender + .send(msg) + .expect("notification send should succeed"); + } } impl pty::Recorder for Recorder { @@ -109,25 +122,32 @@ impl pty::Recorder for Recorder { }; writer.start(&header, self.append)?; + let notifier = self.notifier.take().unwrap(); let handle = thread::spawn(move || { + use Message::*; + for msg in receiver { match msg { - Message::Output(time, data) => { + Output(time, data) => { let _ = writer.output(time, &data); } - Message::Input(time, data) => { + Input(time, data) => { let _ = writer.input(time, &data); } - Message::Resize(time, size) => { + Resize(time, size) => { let _ = writer.resize(time, size); } - Message::Marker(time) => { + Marker(time) => { let _ = writer.marker(time); } + + Notification(text) => { + let _ = notifier.notify(text); + } } } }); @@ -164,10 +184,10 @@ impl pty::Recorder for Recorder { if let Some(pt) = self.pause_time { self.start_time = Instant::now() - Duration::from_micros(pt); self.pause_time = None; - // notify("Resumed recording") + self.notify("Resumed recording"); } else { self.pause_time = Some(self.elapsed_time()); - // notify("Paused recording") + self.notify("Paused recording"); } return false;