1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-22 12:51:31 +03:00

toast: hook notifications up to OSC 9

refs: #489
This commit is contained in:
Wez Furlong 2021-02-17 09:33:58 -08:00
parent e10b3cf6db
commit 83da7216c3
8 changed files with 128 additions and 61 deletions

View File

@ -33,6 +33,10 @@ use crate::activity::Activity;
pub enum MuxNotification {
PaneOutput(PaneId),
WindowCreated(WindowId),
ToastNotification {
pane_id: PaneId,
notification: wezterm_term::ToastNotification,
},
}
static SUB_ID: AtomicUsize = AtomicUsize::new(0);

View File

@ -2,7 +2,7 @@ use crate::domain::DomainId;
use crate::pane::{Pane, PaneId, Pattern, SearchResult};
use crate::renderable::*;
use crate::tmux::{TmuxDomain, TmuxDomainState};
use crate::{Domain, Mux};
use crate::{Domain, Mux, MuxNotification};
use anyhow::Error;
use async_trait::async_trait;
use config::keyassignment::ScrollbackEraseMode;
@ -17,7 +17,7 @@ use url::Url;
use wezterm_term::color::ColorPalette;
use wezterm_term::{
CellAttributes, Clipboard, KeyCode, KeyModifiers, MouseEvent, SemanticZone, StableRowIndex,
Terminal,
Terminal, ToastNotification, ToastNotificationHandler,
};
pub struct LocalPane {
@ -367,6 +367,21 @@ impl wezterm_term::DeviceControlHandler for LocalPaneDCSHandler {
}
}
struct LocalPaneNotifHandler {
pane_id: PaneId,
}
impl ToastNotificationHandler for LocalPaneNotifHandler {
fn show_notification(&mut self, notification: ToastNotification) {
if let Some(mux) = Mux::get() {
mux.notify(MuxNotification::ToastNotification {
pane_id: self.pane_id,
notification,
});
}
}
}
impl LocalPane {
pub fn new(
pane_id: PaneId,
@ -379,6 +394,7 @@ impl LocalPane {
pane_id,
tmux_domain: None,
}));
terminal.set_notification_handler(Box::new(LocalPaneNotifHandler { pane_id }));
Self {
pane_id,
terminal: RefCell::new(terminal),

View File

@ -35,6 +35,21 @@ pub trait DeviceControlHandler {
fn handle_device_control(&mut self, _control: termwiz::escape::DeviceControlMode);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToastNotification {
/// The title text for the notification.
pub title: Option<String>,
/// The message body
pub body: String,
/// Whether clicking on the notification should focus the
/// window/tab/pane that generated it
pub focus: bool,
}
pub trait ToastNotificationHandler {
fn show_notification(&mut self, notif: ToastNotification);
}
/// Represents an instance of a terminal emulator.
pub struct Terminal {
/// The terminal model/state

View File

@ -289,6 +289,7 @@ pub struct TerminalState {
clipboard: Option<Arc<dyn Clipboard>>,
device_control_handler: Option<Box<dyn DeviceControlHandler>>,
notification_handler: Option<Box<dyn ToastNotificationHandler>>,
current_dir: Option<Url>,
@ -433,6 +434,7 @@ impl TerminalState {
pixel_width: size.pixel_width,
clipboard: None,
device_control_handler: None,
notification_handler: None,
current_dir: None,
term_program: term_program.to_string(),
term_version: term_version.to_string(),
@ -449,6 +451,10 @@ impl TerminalState {
self.device_control_handler.replace(handler);
}
pub fn set_notification_handler(&mut self, handler: Box<dyn ToastNotificationHandler>) {
self.notification_handler.replace(handler);
}
/// Returns the title text associated with the terminal session.
/// The title can be changed by the application using a number
/// of escape sequences:
@ -3176,7 +3182,15 @@ impl<'a> Performer<'a> {
}
OperatingSystemCommand::SystemNotification(message) => {
error!("Application sends SystemNotification: {}", message);
if let Some(handler) = self.notification_handler.as_mut() {
handler.show_notification(ToastNotification {
title: None,
body: message,
focus: true,
});
} else {
log::info!("Application sends SystemNotification: {}", message);
}
}
OperatingSystemCommand::CurrentWorkingDirectory(url) => {
self.current_dir = Url::parse(&url).ok();

View File

@ -4,6 +4,7 @@ pub use config::FrontEndSelection;
use mux::{Mux, MuxNotification};
use std::cell::RefCell;
use std::rc::Rc;
use wezterm_toast_notification::*;
mod glyphcache;
mod overlay;
@ -63,6 +64,21 @@ impl GuiFrontEnd {
}
}
MuxNotification::PaneOutput(_) => {}
MuxNotification::ToastNotification {
pane_id: _,
notification,
} => {
let title = notification.title.as_ref().unwrap_or(&notification.body);
let message = if notification.title.is_none() {
""
} else {
&notification.body
};
// FIXME: if notification.focus is true, we should do
// something here to arrange to focus pane_id when the
// notification is clicked
persistent_toast_notification(title, message);
}
}
true
} else {

View File

@ -81,6 +81,13 @@ where
Ok(Item::Notif(MuxNotification::PaneOutput(pane_id))) => {
handler.schedule_pane_push(pane_id);
}
Ok(Item::Notif(MuxNotification::ToastNotification {
pane_id,
notification: _,
})) => {
// FIXME: queue notification to send to client!
handler.schedule_pane_push(pane_id);
}
Ok(Item::Notif(MuxNotification::WindowCreated(_window_id))) => {}
Err(err) => {
log::error!("process_async Err {}", err);

View File

@ -1,4 +1,4 @@
#![cfg(all(not(target_os = "macos"), not(windows), not(target_os="freebsd")))]
#![cfg(all(not(target_os = "macos"), not(windows), not(target_os = "freebsd")))]
//! See <https://developer.gnome.org/notification-spec/>
use serde::{Deserialize, Serialize};
@ -91,7 +91,7 @@ pub fn show_notif(title: &str, message: &str, url: Option<&str>) -> Result<(), z
let proxy = NotificationsProxy::new(&connection)?;
let caps = proxy.get_capabilities()?;
if !caps.iter().any(|cap| cap == "actions") {
if url.is_some() && !caps.iter().any(|cap| cap == "actions") {
// Server doesn't support actions, so skip showing this notification
// because it might have text that says "click to see more"
// and that just wouldn't work.
@ -99,8 +99,7 @@ pub fn show_notif(title: &str, message: &str, url: Option<&str>) -> Result<(), z
}
let mut hints = HashMap::new();
let resident = Value::Bool(true);
hints.insert("resident", resident);
hints.insert("urgency", Value::U8(2 /* Critical */));
let notification = proxy.notify(
"wezterm",
0,
@ -116,60 +115,56 @@ pub fn show_notif(title: &str, message: &str, url: Option<&str>) -> Result<(), z
0, // Never timeout
)?;
// If we have a URL, we need to listen for signals to know when/if
// the user clicks on it. The thread will stick around until an
// error is encountered or the user clicks/dismisses the notification.
if let Some(url) = &url {
let url = url.to_string();
let url = url.map(|s| s.to_string());
struct State {
notification: u32,
done: bool,
url: String,
}
let state = Arc::new(Mutex::new(State {
notification,
done: false,
url,
}));
proxy.connect_action_invoked({
let state = Arc::clone(&state);
move |nid, _action_name| {
let mut state = state.lock().unwrap();
if nid == state.notification {
let _ = open::that(&state.url);
state.done = true;
}
Ok(())
}
})?;
proxy.connect_notification_closed({
let state = Arc::clone(&state);
move |nid, reason| {
let _reason = Reason::new(reason);
let mut state = state.lock().unwrap();
if nid == state.notification {
state.done = true;
}
Ok(())
}
})?;
std::thread::spawn(move || {
while !state.lock().unwrap().done {
match proxy.next_signal() {
Err(err) => {
log::error!("next_signal: {:#}", err);
break;
}
Ok(_) => {}
}
}
});
struct State {
notification: u32,
done: bool,
url: Option<String>,
}
let state = Arc::new(Mutex::new(State {
notification,
done: false,
url,
}));
proxy.connect_action_invoked({
let state = Arc::clone(&state);
move |nid, _action_name| {
let state = state.lock().unwrap();
if nid == state.notification {
if let Some(url) = state.url.as_ref() {
let _ = open::that(url);
}
}
Ok(())
}
})?;
proxy.connect_notification_closed({
let state = Arc::clone(&state);
move |nid, reason| {
let _reason = Reason::new(reason);
let mut state = state.lock().unwrap();
if nid == state.notification {
state.done = true;
}
Ok(())
}
})?;
std::thread::spawn(move || {
while !state.lock().unwrap().done {
match proxy.next_signal() {
Err(err) => {
log::error!("next_signal: {:#}", err);
break;
}
Ok(_) => {}
}
}
});
Ok(())
}

View File

@ -8,7 +8,7 @@ pub fn persistent_toast_notification_with_click_to_open_url(title: &str, message
macos::show_notif(title, message, Some(url));
}
#[cfg(all(not(target_os = "macos"), not(windows), not(target_os="freebsd")))]
#[cfg(all(not(target_os = "macos"), not(windows), not(target_os = "freebsd")))]
{
if let Err(err) = dbus::show_notif(title, message, Some(url)) {
log::error!("Failed to show notification: {}", err);
@ -24,7 +24,7 @@ pub fn persistent_toast_notification(title: &str, message: &str) {
macos::show_notif(title, message, None);
}
#[cfg(all(not(target_os = "macos"), not(windows), not(target_os="freebsd")))]
#[cfg(all(not(target_os = "macos"), not(windows), not(target_os = "freebsd")))]
{
if let Err(err) = dbus::show_notif(title, message, None) {
log::error!("Failed to show notification: {}", err);