Add desktop notifications (notify-send, osascript, custom command)

This commit is contained in:
Marcin Kulik
2024-01-13 11:10:10 +01:00
parent 12df12d4c3
commit 7b78196ddd
7 changed files with 154 additions and 7 deletions

20
Cargo.lock generated
View File

@@ -97,6 +97,7 @@ dependencies = [
"signal-hook", "signal-hook",
"termion", "termion",
"uuid", "uuid",
"which",
] ]
[[package]] [[package]]
@@ -290,6 +291,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@@ -1379,6 +1386,19 @@ version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -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"] } reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "rustls-tls", "multipart", "gzip", "json"] }
rustyline = "13.0.0" rustyline = "13.0.0"
config = { version = "0.13.4", default-features = false, features = ["toml", "ini"] } config = { version = "0.13.4", default-features = false, features = ["toml", "ini"] }
which = "5.0.0"

View File

@@ -1,6 +1,7 @@
use crate::config::Config; use crate::config::Config;
use crate::format::{asciicast, raw}; use crate::format::{asciicast, raw};
use crate::locale; use crate::locale;
use crate::notifier;
use crate::pty; use crate::pty;
use crate::recorder::{self, KeyBindings}; use crate::recorder::{self, KeyBindings};
use crate::tty; use crate::tty;
@@ -109,8 +110,10 @@ impl Cli {
}; };
let keys = get_key_bindings(config)?; 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_command = build_exec_command(self.command);
let exec_extra_env = build_exec_extra_env(); let exec_extra_env = build_exec_extra_env();
@@ -160,6 +163,14 @@ fn get_key_bindings(config: &Config) -> Result<KeyBindings> {
Ok(keys) Ok(keys)
} }
fn get_notifier(config: &Config) -> Box<dyn notifier::Notifier> {
if config.notifications.enabled {
notifier::get_notifier(config.notifications.command.clone())
} else {
Box::new(notifier::NullNotifier)
}
}
fn capture_env(vars: &str) -> HashMap<String, String> { fn capture_env(vars: &str) -> HashMap<String, String> {
let vars = vars.split(',').collect::<HashSet<_>>(); let vars = vars.split(',').collect::<HashSet<_>>();

View File

@@ -17,6 +17,7 @@ pub type Key = Option<Vec<u8>>;
pub struct Config { pub struct Config {
server: Server, server: Server,
cmd: Cmd, cmd: Cmd,
pub notifications: Notifications,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -54,6 +55,13 @@ pub struct Play {
pub next_marker_key: Option<String>, pub next_marker_key: Option<String>,
} }
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Notifications {
pub enabled: bool,
pub command: Option<String>,
}
impl Config { impl Config {
pub fn new(server_url: Option<String>) -> Result<Self> { pub fn new(server_url: Option<String>) -> Result<Self> {
let mut config = config::Config::builder() let mut config = config::Config::builder()
@@ -61,6 +69,7 @@ impl Config {
.set_default("cmd.rec.input", false)? .set_default("cmd.rec.input", false)?
.set_default("cmd.rec.env", "SHELL,TERM")? .set_default("cmd.rec.env", "SHELL,TERM")?
.set_default("cmd.play.speed", 1.0)? .set_default("cmd.play.speed", 1.0)?
.set_default("notifications.enabled", true)?
.add_source( .add_source(
config::File::with_name(&user_defaults_path()?.to_string_lossy()).required(false), config::File::with_name(&user_defaults_path()?.to_string_lossy()).required(false),
) )

View File

@@ -3,6 +3,7 @@ mod config;
mod format; mod format;
mod io; mod io;
mod locale; mod locale;
mod notifier;
mod player; mod player;
mod pty; mod pty;
mod recorder; mod recorder;

85
src/notifier.rs Normal file
View File

@@ -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<String>) -> Box<dyn Notifier> {
if let Some(command) = custom_command {
Box::new(CustomNotifier(command))
} else {
LibNotifyNotifier::get()
.map(|n| Box::new(n) as Box<dyn Notifier>)
.or_else(|| AppleScriptNotifier::get().map(|n| Box::new(n) as Box<dyn Notifier>))
.unwrap_or_else(|| Box::new(NullNotifier))
}
}
pub struct LibNotifyNotifier(PathBuf);
impl LibNotifyNotifier {
fn get() -> Option<Self> {
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<Self> {
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<S: AsRef<OsStr>>(command: &mut Command, args: &[S]) -> Result<()> {
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.args(args)
.status()?;
Ok(())
}

View File

@@ -1,4 +1,5 @@
use crate::config::Key; use crate::config::Key;
use crate::notifier::Notifier;
use crate::pty; use crate::pty;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
@@ -14,6 +15,7 @@ pub struct Recorder {
record_input: bool, record_input: bool,
metadata: Metadata, metadata: Metadata,
keys: KeyBindings, keys: KeyBindings,
notifier: Option<Box<dyn Notifier>>,
sender: mpsc::Sender<Message>, sender: mpsc::Sender<Message>,
receiver: Option<mpsc::Receiver<Message>>, receiver: Option<mpsc::Receiver<Message>>,
handle: Option<JoinHandle>, handle: Option<JoinHandle>,
@@ -50,6 +52,7 @@ enum Message {
Input(u64, Vec<u8>), Input(u64, Vec<u8>),
Resize(u64, (u16, u16)), Resize(u64, (u16, u16)),
Marker(u64), Marker(u64),
Notification(String),
} }
struct JoinHandle(Option<thread::JoinHandle<()>>); struct JoinHandle(Option<thread::JoinHandle<()>>);
@@ -61,6 +64,7 @@ impl Recorder {
record_input: bool, record_input: bool,
metadata: Metadata, metadata: Metadata,
keys: KeyBindings, keys: KeyBindings,
notifier: Box<dyn Notifier>,
) -> Self { ) -> Self {
let (sender, receiver) = mpsc::channel(); let (sender, receiver) = mpsc::channel();
@@ -72,6 +76,7 @@ impl Recorder {
record_input, record_input,
metadata, metadata,
keys, keys,
notifier: Some(notifier),
sender, sender,
receiver: Some(receiver), receiver: Some(receiver),
handle: None, handle: None,
@@ -86,6 +91,14 @@ impl Recorder {
self.start_time.elapsed().as_micros() as u64 self.start_time.elapsed().as_micros() as u64
} }
} }
fn notify<S: ToString>(&self, text: S) {
let msg = Message::Notification(text.to_string());
self.sender
.send(msg)
.expect("notification send should succeed");
}
} }
impl pty::Recorder for Recorder { impl pty::Recorder for Recorder {
@@ -109,25 +122,32 @@ impl pty::Recorder for Recorder {
}; };
writer.start(&header, self.append)?; writer.start(&header, self.append)?;
let notifier = self.notifier.take().unwrap();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
use Message::*;
for msg in receiver { for msg in receiver {
match msg { match msg {
Message::Output(time, data) => { Output(time, data) => {
let _ = writer.output(time, &data); let _ = writer.output(time, &data);
} }
Message::Input(time, data) => { Input(time, data) => {
let _ = writer.input(time, &data); let _ = writer.input(time, &data);
} }
Message::Resize(time, size) => { Resize(time, size) => {
let _ = writer.resize(time, size); let _ = writer.resize(time, size);
} }
Message::Marker(time) => { Marker(time) => {
let _ = writer.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 { if let Some(pt) = self.pause_time {
self.start_time = Instant::now() - Duration::from_micros(pt); self.start_time = Instant::now() - Duration::from_micros(pt);
self.pause_time = None; self.pause_time = None;
// notify("Resumed recording") self.notify("Resumed recording");
} else { } else {
self.pause_time = Some(self.elapsed_time()); self.pause_time = Some(self.elapsed_time());
// notify("Paused recording") self.notify("Paused recording");
} }
return false; return false;