forked from NotAShelf/beer
vt: desktop notifications (OSC 9/777/99) and a configurable bell
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I43ad1f9892ecec1f32c03c67e863b1746a6a6964
This commit is contained in:
parent
2161d7250f
commit
69ba5fb30c
3 changed files with 170 additions and 3 deletions
64
src/vt.rs
64
src/vt.rs
|
|
@ -34,6 +34,14 @@ pub enum ClipboardOp {
|
|||
Query { primary: bool },
|
||||
}
|
||||
|
||||
/// A desktop notification an application requested (OSC 9 / 777 / 99), for the
|
||||
/// front-end to deliver via the configured notifier.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Notification {
|
||||
pub title: Option<String>,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// Which dynamic colour an OSC 10/11/17/19 escape targets.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Dynamic {
|
||||
|
|
@ -106,6 +114,8 @@ pub struct Term {
|
|||
bell: bool,
|
||||
/// Working directory reported by the shell via OSC 7, for new windows.
|
||||
cwd: Option<String>,
|
||||
/// Desktop notifications requested via OSC 9/777/99, drained by the front-end.
|
||||
notifications: Vec<Notification>,
|
||||
}
|
||||
|
||||
impl Term {
|
||||
|
|
@ -123,6 +133,7 @@ impl Term {
|
|||
theme: Theme::default(),
|
||||
bell: false,
|
||||
cwd: None,
|
||||
notifications: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +142,11 @@ impl Term {
|
|||
self.cwd.as_deref()
|
||||
}
|
||||
|
||||
/// Drain the desktop notifications requested since the last call.
|
||||
pub fn take_notifications(&mut self) -> Vec<Notification> {
|
||||
std::mem::take(&mut self.notifications)
|
||||
}
|
||||
|
||||
/// Take and clear the pending bell flag.
|
||||
pub fn take_bell(&mut self) -> bool {
|
||||
std::mem::take(&mut self.bell)
|
||||
|
|
@ -691,6 +707,27 @@ impl Perform for Term {
|
|||
let uri = std::str::from_utf8(&uri_bytes).unwrap_or("");
|
||||
self.grid.set_link((!uri.is_empty()).then_some(uri));
|
||||
}
|
||||
// OSC 9: iTerm2-style notification (`OSC 9 ; body`).
|
||||
Some(&n) if n == b"9" => {
|
||||
if let Some(body) = osc_text(params.get(1)) {
|
||||
self.notifications.push(Notification { title: None, body });
|
||||
}
|
||||
}
|
||||
// OSC 777: `OSC 777 ; notify ; title ; body`.
|
||||
Some(&n) if n == b"777" && params.get(1) == Some(&&b"notify"[..]) => {
|
||||
let title = osc_text(params.get(2));
|
||||
if let Some(body) = osc_text(params.get(3)) {
|
||||
self.notifications.push(Notification { title, body });
|
||||
}
|
||||
}
|
||||
// OSC 99: kitty desktop-notification protocol. We honour the common
|
||||
// single-chunk form, taking the payload as the body and ignoring the
|
||||
// metadata key=value field.
|
||||
Some(&n) if n == b"99" => {
|
||||
if let Some(body) = osc_text(params.get(2)).filter(|b| !b.is_empty()) {
|
||||
self.notifications.push(Notification { title: None, body });
|
||||
}
|
||||
}
|
||||
// OSC 4: set/query palette entries (pairs of index;spec).
|
||||
Some(&n) if n == b"4" => self.osc_palette(params, bell),
|
||||
// OSC 104: reset palette (all, or the listed indices).
|
||||
|
|
@ -834,6 +871,11 @@ fn base64_decode(data: &[u8]) -> Option<Vec<u8>> {
|
|||
Some(out)
|
||||
}
|
||||
|
||||
/// Decode an OSC string field to UTF-8 (lossy), or `None` if absent.
|
||||
fn osc_text(field: Option<&&[u8]>) -> Option<String> {
|
||||
field.map(|b| String::from_utf8_lossy(b).into_owned())
|
||||
}
|
||||
|
||||
/// Map an OSC 133 mark letter to a [`PromptKind`].
|
||||
fn prompt_kind(b: u8) -> Option<PromptKind> {
|
||||
match b {
|
||||
|
|
@ -1158,6 +1200,28 @@ mod tests {
|
|||
assert_eq!(t.grid().last_command_output().as_deref(), Some("hi\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_notifications_collected() {
|
||||
let mut t = Term::new(20, 2);
|
||||
feed(&mut t, b"\x1b]9;hello\x07");
|
||||
feed(&mut t, b"\x1b]777;notify;Title;Body\x07");
|
||||
let n = t.take_notifications();
|
||||
assert_eq!(
|
||||
n,
|
||||
vec![
|
||||
Notification {
|
||||
title: None,
|
||||
body: "hello".into()
|
||||
},
|
||||
Notification {
|
||||
title: Some("Title".into()),
|
||||
body: "Body".into()
|
||||
},
|
||||
]
|
||||
);
|
||||
assert!(t.take_notifications().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc7_tracks_cwd_and_decodes_percent() {
|
||||
let mut t = Term::new(20, 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue