From b56ab0a02e8ca45497454932147ce0ad4592739e Mon Sep 17 00:00:00 2001 From: "Rene D. Obermueller" Date: Sun, 12 Jul 2026 08:26:07 +0200 Subject: [PATCH 1/3] refactor: use disable_notifications centrally Bunch of places used `!APP_CONFIG.read().disable_notifications()`, this is actually better placed in the `log_result` itself. --- src/notification.rs | 3 ++- src/sketch_board.rs | 42 +++++++++--------------------------------- 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/src/notification.rs b/src/notification.rs index d5112570..1f30a309 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,11 +1,12 @@ use relm4::gtk::gio::FileIcon; use relm4::gtk::gio::{Notification, prelude::ApplicationExt}; +use crate::configuration::APP_CONFIG; use relm4::gtk::{IconLookupFlags, IconTheme, TextDirection}; pub fn log_result(msg: &str, notify: bool) { eprintln!("{msg}"); - if notify { + if notify && !APP_CONFIG.read().disable_notifications() { show_notification(msg); } } diff --git a/src/sketch_board.rs b/src/sketch_board.rs index a2e59255..d0182d7c 100644 --- a/src/sketch_board.rs +++ b/src/sketch_board.rs @@ -417,10 +417,7 @@ impl SketchBoard { home_dir.push(tilde_stripped); output_filename = home_dir.to_string_lossy().into_owned(); } else { - log_result( - "~ found but could not determine homedir", - !APP_CONFIG.read().disable_notifications(), - ); + log_result("~ found but could not determine homedir", true); return None; } } @@ -505,7 +502,7 @@ impl SketchBoard { if output_filename != "-" && !output_filename.ends_with(".png") { log_result( "The only supported format is png, but the filename does not end in png", - !APP_CONFIG.read().disable_notifications(), + true, ); return; } @@ -528,17 +525,11 @@ impl SketchBoard { return; } match fs::write(&output_filename, data) { - Err(e) => log_result( - &format!("Error while saving file: {e}"), - !APP_CONFIG.read().disable_notifications(), - ), + Err(e) => log_result(&format!("Error while saving file: {e}"), true), Ok(_) => { // Store the filepath for copy-filepath action *self.last_saved_filepath.borrow_mut() = Some(output_filename.clone()); - log_result( - &format!("File saved to '{}'.", output_filename), - !APP_CONFIG.read().disable_notifications(), - ) + log_result(&format!("File saved to '{}'.", output_filename), true) } }; } @@ -607,18 +598,12 @@ impl SketchBoard { }; match fs::write(&output_filename, &data) { - Err(e) => log_result( - &format!("Error while saving file: {e}"), - !APP_CONFIG.read().disable_notifications(), - ), + Err(e) => log_result(&format!("Error while saving file: {e}"), true), Ok(_) => { exit_app = APP_CONFIG.read().early_exit_save_as(); filename = Some(output_filename.clone()); Self::remember_save_as_dir(Path::new(&output_filename)); - log_result( - &format!("File saved to '{}'.", output_filename), - !APP_CONFIG.read().disable_notifications(), - ) + log_result(&format!("File saved to '{}'.", output_filename), true) } }; } @@ -687,10 +672,7 @@ impl SketchBoard { match result { Err(e) => eprintln!("Error saving {e}"), Ok(()) => { - log_result( - "Copied to clipboard.", - !APP_CONFIG.read().disable_notifications(), - ); + log_result("Copied to clipboard.", true); // TODO: rethink order and messaging patterns if APP_CONFIG.read().save_after_copy() { @@ -726,14 +708,8 @@ impl SketchBoard { }; match result { - Err(e) => log_result( - &format!("Error copying filepath: {e}"), - !APP_CONFIG.read().disable_notifications(), - ), - Ok(()) => log_result( - &format!("Filepath copied to clipboard: {}", filepath), - !APP_CONFIG.read().disable_notifications(), - ), + Err(e) => log_result(&format!("Error copying filepath: {e}"), true), + Ok(()) => log_result(&format!("Filepath copied to clipboard: {}", filepath), true), } } From 4d3b24ef0fc413613849cabb041fc536196e5b3f Mon Sep 17 00:00:00 2001 From: "Rene D. Obermueller" Date: Sun, 12 Jul 2026 08:40:58 +0200 Subject: [PATCH 2/3] feat: configuration for NotificationThumbnail --- cli/src/command_line.rs | 20 ++++++++++++++++++++ src/configuration.rs | 16 +++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/cli/src/command_line.rs b/cli/src/command_line.rs index 65de45cf..3c1d94a0 100644 --- a/cli/src/command_line.rs +++ b/cli/src/command_line.rs @@ -159,6 +159,10 @@ pub struct CommandLine { #[arg(long)] pub app_id: Option, + /// Experimental feature (NEXTRELEASE): use preview thumbnail in notifications where available + #[arg(long)] + pub notification_thumbnail: Option, + // --- deprecated options --- /// Right click to copy. /// Preferably use the `action_on_right_click` option instead. @@ -215,6 +219,22 @@ pub enum EarlyExitTriggers { SaveAs, } +// notifications can use image-path or image-data, see https://specifications.freedesktop.org/notification/latest-single/#icons-and-images +// But gtk does not support it https://gitlab.gnome.org/GNOME/glib/-/work_items/1457 at this point. +// an image lib dependency in glib is not wanted so options for an implementation there are +// very limited. +// options are: use different notification library or implement manually. For now, we +// just offer image-path (which uses a temporary file). +#[derive(Debug, Clone, Copy, Default, ValueEnum, Deserialize, PartialEq, Eq)] +#[value(rename_all = "kebab-case")] +#[serde(rename_all = "kebab-case")] +pub enum NotificationThumbnail { + #[default] + AppIcon, + ThumbnailFileIcon, + //ThumbnailIcon +} + #[derive(Debug, Clone, Copy, Default, ValueEnum)] pub enum Tools { #[default] diff --git a/src/configuration.rs b/src/configuration.rs index 2f4ba9f6..493f16f7 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -20,7 +20,8 @@ use crate::{ }; use satty_cli::command_line::{ - Action as CommandLineAction, CommandLine, EarlyExitTriggers, Fullscreen, Resize, + Action as CommandLineAction, CommandLine, EarlyExitTriggers, Fullscreen, NotificationThumbnail, + Resize, }; pub static APP_CONFIG: SharedState = SharedState::new(); @@ -72,6 +73,7 @@ pub struct Configuration { input_scale: Option, title: Option, app_id: Option, + notification_thumbnail: NotificationThumbnail, } pub struct Keybinds { @@ -423,6 +425,9 @@ impl Configuration { if let Some(v) = general.app_id { self.app_id = Some(v); } + if let Some(v) = general.notification_thumbnail { + self.notification_thumbnail = v; + } // --- deprecated options --- if let Some(v) = general.right_click_copy @@ -556,6 +561,9 @@ impl Configuration { if let Some(v) = command_line.app_id { self.app_id = Some(v); } + if let Some(v) = command_line.notification_thumbnail { + self.notification_thumbnail = v; + } // --- deprecated options --- if command_line.right_click_copy @@ -718,6 +726,10 @@ impl Configuration { pub fn app_id(&self) -> Option<&String> { self.app_id.as_ref() } + + pub fn notification_thumbnail(&self) -> &NotificationThumbnail { + &self.notification_thumbnail + } } impl Default for Configuration { @@ -757,6 +769,7 @@ impl Default for Configuration { input_scale: None, title: None, app_id: None, + notification_thumbnail: NotificationThumbnail::default(), } } } @@ -841,6 +854,7 @@ struct ConfigurationFileGeneral { input_scale: Option, title: Option, app_id: Option, + notification_thumbnail: Option, // --- deprecated options --- right_click_copy: Option, From 528630c4235d52f2f69129ae1283c3fac6f0e0bb Mon Sep 17 00:00:00 2001 From: "Rene D. Obermueller" Date: Sun, 12 Jul 2026 09:35:43 +0200 Subject: [PATCH 3/3] feat: add notification-thumbnail option to offer thumbnail in notifications --- README.md | 5 +++ config.toml | 3 ++ src/configuration.rs | 4 +- src/main.rs | 15 +++++-- src/notification.rs | 102 +++++++++++++++++++++++++++++++++++-------- src/sketch_board.rs | 14 ++++-- 6 files changed, 114 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 6711ebea..d3d6f992 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,9 @@ input-scale = 1.0 title = "Satty" # experimental feature (0.21.0): set app_id, note this has to match D-Bus well-known name format, otherwise GTK does not accept it. app-id = "org.satty.satty" +# experimental feature (NEXTRELEASE): show thumbnail as notifcation icon +# notification-thumbnail = "thumbnail-file--icon" +notification-thumbnail = "app-icon" # Tool selection keyboard shortcuts (since 0.20.0) [keybinds] @@ -354,6 +357,8 @@ Options: Experimental feature (0.21.0): Set window title --app-id Experimental feature (0.21.0): Set toplevel app_id. Note that this has to match D-Bus well known name format, otherwise GTK does not accept it + --notification-thumbnail + Experimental feature (NEXTRELEASE): use preview thumbnail in notifications where available [possible values: app-icon, thumbnail-file-icon] --right-click-copy Right click to copy. Preferably use the `action_on_right_click` option instead --action-on-enter diff --git a/config.toml b/config.toml index f596fb9f..881885a2 100644 --- a/config.toml +++ b/config.toml @@ -76,6 +76,9 @@ input-scale = 1.0 title = "Satty" # experimental feature (0.21.0): set app_id, note this has to match D-Bus well-known name format, otherwise GTK does not accept it. app-id = "org.satty.satty" +# experimental feature (NEXTRELEASE): show thumbnail as notifcation icon +# notification-thumbnail = "thumbnail-file--icon" +notification-thumbnail = "app-icon" # Tool selection keyboard shortcuts (since 0.20.0) [keybinds] diff --git a/src/configuration.rs b/src/configuration.rs index 493f16f7..dabc9110 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -727,8 +727,8 @@ impl Configuration { self.app_id.as_ref() } - pub fn notification_thumbnail(&self) -> &NotificationThumbnail { - &self.notification_thumbnail + pub fn notification_thumbnail(&self) -> NotificationThumbnail { + self.notification_thumbnail } } diff --git a/src/main.rs b/src/main.rs index dcf71e06..211a6900 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ use configuration::{APP_CONFIG, Configuration}; +use relm4::gtk::gdk_pixbuf::{Pixbuf, PixbufLoader}; +use relm4::gtk::gio::{Application, ApplicationFlags}; +use relm4::gtk::prelude::*; use std::io::Read; use std::ops::Deref; use std::process::exit; @@ -6,10 +9,6 @@ use std::sync::LazyLock; use std::{fs, ptr}; use std::{io, time::Duration}; -use relm4::gtk::gdk_pixbuf::{Pixbuf, PixbufLoader}; -use relm4::gtk::gio::{Application, ApplicationFlags}; -use relm4::gtk::prelude::*; - use relm4::gtk::gdk::Rectangle; use relm4::{ @@ -20,6 +19,7 @@ use relm4::{ use anyhow::{Context, Result, anyhow}; use satty_cli::command_line::{Fullscreen, Resize}; +use crate::notification::NOTIFICATION_THUMBNAIL_PATH; use sketch_board::SketchBoardOutput; use ui::toolbars::{StyleToolbar, StyleToolbarInput, ToolsToolbar, ToolsToolbarInput}; use xdg::BaseDirectories; @@ -515,6 +515,13 @@ fn run_satty() -> Result<()> { icons::icon_names::GRESOURCE_BYTES, icons::icon_names::RESOURCE_PREFIX, ); + + relm4::main_application().connect_shutdown(move |_| { + if let Err(e) = std::fs::remove_file(&*NOTIFICATION_THUMBNAIL_PATH) { + eprintln!("Failed to remove thumbnail temp path: {e}"); + } + }); + app.run::(image); Ok(()) } diff --git a/src/notification.rs b/src/notification.rs index 1f30a309..1d7d3962 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,36 +1,100 @@ -use relm4::gtk::gio::FileIcon; +use relm4::gtk::gio::{FileIcon, Icon}; use relm4::gtk::gio::{Notification, prelude::ApplicationExt}; +use std::path::PathBuf; +use std::sync::LazyLock; use crate::configuration::APP_CONFIG; -use relm4::gtk::{IconLookupFlags, IconTheme, TextDirection}; +use relm4::gtk::gdk_pixbuf::{InterpType, Pixbuf}; +use relm4::gtk::prelude::Cast; +use relm4::gtk::{IconLookupFlags, IconTheme, TextDirection, gio}; +use satty_cli::command_line::NotificationThumbnail; + +pub static NOTIFICATION_THUMBNAIL_PATH: LazyLock = LazyLock::new(|| { + std::env::temp_dir().join(format!( + "satty-notification-thumbnail-{}.png", + std::process::id() + )) +}); pub fn log_result(msg: &str, notify: bool) { eprintln!("{msg}"); if notify && !APP_CONFIG.read().disable_notifications() { - show_notification(msg); + show_notification(msg, None); } } -fn show_notification(msg: &str) { +pub fn log_result_with_pixbuf(msg: &str, pixbuf: Pixbuf) { + eprintln!("{msg}"); + + if APP_CONFIG.read().disable_notifications() { + return; + } + + let notification_icon_kind = APP_CONFIG.read().notification_thumbnail(); + + let pixbuf = match notification_icon_kind { + NotificationThumbnail::AppIcon => None, + _ => { + let src_w = pixbuf.width(); + let src_h = pixbuf.height(); + + if src_w == 0 || src_h == 0 { + None + } else { + let scale = f64::min(96.0 / src_w as f64, 96.0 / src_h as f64); + + let new_w = ((src_w as f64) * scale).round().max(1.0) as i32; + let new_h = ((src_h as f64) * scale).round().max(1.0) as i32; + + pixbuf.scale_simple(new_w, new_h, InterpType::Bilinear) + } + } + }; + + let icon = match pixbuf { + // glib doesn't support image-data at this point + /* + Some(p) if notification_icon_kind == NotificationThumbnail::ThumbnailIcon => { + Some(p.upcast::()) + }*/ + Some(p) if notification_icon_kind == NotificationThumbnail::ThumbnailFileIcon => { + if p.savev(&*NOTIFICATION_THUMBNAIL_PATH, "png", &[]).is_err() { + None + } else { + let file = gio::File::for_path(&*NOTIFICATION_THUMBNAIL_PATH); + Some(FileIcon::new(&file).upcast::()) + } + } + _ => None, + }; + + show_notification(msg, icon); +} + +fn show_notification(msg: &str, icon: Option) { // construct let notification = Notification::new("Satty"); notification.set_body(Some(msg)); - // lookup sattys icon - let theme = IconTheme::default(); - if theme.has_icon("satty") - && let Some(icon_file) = theme - .lookup_icon( - "satty", - &[], - 96, - 1, - TextDirection::Ltr, - IconLookupFlags::empty(), - ) - .file() - { - notification.set_icon(&FileIcon::new(&icon_file)); + if let Some(i) = icon { + notification.set_icon(&i); + } else { + // lookup sattys icon + let theme = IconTheme::default(); + if theme.has_icon("satty") + && let Some(icon_file) = theme + .lookup_icon( + "satty", + &[], + 96, + 1, + TextDirection::Ltr, + IconLookupFlags::empty(), + ) + .file() + { + notification.set_icon(&FileIcon::new(&icon_file)); + } } // send notification diff --git a/src/sketch_board.rs b/src/sketch_board.rs index d0182d7c..4ce3a76e 100644 --- a/src/sketch_board.rs +++ b/src/sketch_board.rs @@ -22,7 +22,7 @@ use crate::configuration::{APP_CONFIG, Action}; use crate::femtovg_area::FemtoVGArea; use crate::ime::pango_adapter::spans_from_pango_attrs; use crate::math::Vec2D; -use crate::notification::log_result; +use crate::notification::{log_result, log_result_with_pixbuf}; use crate::style::Style; use crate::tools::{Tool, ToolEvent, ToolUpdateResult, Tools, ToolsManager}; use crate::ui::toolbars::ToolbarEvent; @@ -529,7 +529,10 @@ impl SketchBoard { Ok(_) => { // Store the filepath for copy-filepath action *self.last_saved_filepath.borrow_mut() = Some(output_filename.clone()); - log_result(&format!("File saved to '{}'.", output_filename), true) + log_result_with_pixbuf( + &format!("File saved to '{}'.", output_filename), + image.clone(), + ) } }; } @@ -603,7 +606,10 @@ impl SketchBoard { exit_app = APP_CONFIG.read().early_exit_save_as(); filename = Some(output_filename.clone()); Self::remember_save_as_dir(Path::new(&output_filename)); - log_result(&format!("File saved to '{}'.", output_filename), true) + log_result_with_pixbuf( + &format!("File saved to '{}'.", output_filename), + pixbuf.clone(), + ) } }; } @@ -672,7 +678,7 @@ impl SketchBoard { match result { Err(e) => eprintln!("Error saving {e}"), Ok(()) => { - log_result("Copied to clipboard.", true); + log_result_with_pixbuf("Copied to clipboard.", image.clone()); // TODO: rethink order and messaging patterns if APP_CONFIG.read().save_after_copy() {