mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 03:38:03 +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",
|
"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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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<_>>();
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
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::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;
|
||||||
|
|||||||
Reference in New Issue
Block a user