mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-15 19:28:00 +01:00
Add desktop notifications (notify-send, osascript, custom command)
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
85
src/notifier.rs
Normal 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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user