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",
"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"

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"] }
rustyline = "13.0.0"
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::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<KeyBindings> {
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> {
let vars = vars.split(',').collect::<HashSet<_>>();

View File

@@ -17,6 +17,7 @@ pub type Key = Option<Vec<u8>>;
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<String>,
}
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Notifications {
pub enabled: bool,
pub command: Option<String>,
}
impl Config {
pub fn new(server_url: Option<String>) -> Result<Self> {
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),
)

View File

@@ -3,6 +3,7 @@ mod config;
mod format;
mod io;
mod locale;
mod notifier;
mod player;
mod pty;
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::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<Box<dyn Notifier>>,
sender: mpsc::Sender<Message>,
receiver: Option<mpsc::Receiver<Message>>,
handle: Option<JoinHandle>,
@@ -50,6 +52,7 @@ enum Message {
Input(u64, Vec<u8>),
Resize(u64, (u16, u16)),
Marker(u64),
Notification(String),
}
struct JoinHandle(Option<thread::JoinHandle<()>>);
@@ -61,6 +64,7 @@ impl Recorder {
record_input: bool,
metadata: Metadata,
keys: KeyBindings,
notifier: Box<dyn Notifier>,
) -> 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<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 {
@@ -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;