Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -354,6 +357,8 @@ Options:
Experimental feature (0.21.0): Set window title
--app-id <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 <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 <ACTION_ON_ENTER>
Expand Down
20 changes: 20 additions & 0 deletions cli/src/command_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ pub struct CommandLine {
#[arg(long)]
pub app_id: Option<String>,

/// Experimental feature (NEXTRELEASE): use preview thumbnail in notifications where available
#[arg(long)]
pub notification_thumbnail: Option<NotificationThumbnail>,

// --- deprecated options ---
/// Right click to copy.
/// Preferably use the `action_on_right_click` option instead.
Expand Down Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
16 changes: 15 additions & 1 deletion src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Configuration> = SharedState::new();
Expand Down Expand Up @@ -72,6 +73,7 @@ pub struct Configuration {
input_scale: Option<f32>,
title: Option<String>,
app_id: Option<String>,
notification_thumbnail: NotificationThumbnail,
}

pub struct Keybinds {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -757,6 +769,7 @@ impl Default for Configuration {
input_scale: None,
title: None,
app_id: None,
notification_thumbnail: NotificationThumbnail::default(),
}
}
}
Expand Down Expand Up @@ -841,6 +854,7 @@ struct ConfigurationFileGeneral {
input_scale: Option<f32>,
title: Option<String>,
app_id: Option<String>,
notification_thumbnail: Option<NotificationThumbnail>,

// --- deprecated options ---
right_click_copy: Option<bool>,
Expand Down
15 changes: 11 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
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;
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::{
Expand All @@ -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;
Expand Down Expand Up @@ -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::<App>(image);
Ok(())
}
Expand Down
105 changes: 85 additions & 20 deletions src/notification.rs
Original file line number Diff line number Diff line change
@@ -1,35 +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 relm4::gtk::{IconLookupFlags, IconTheme, TextDirection};
use crate::configuration::APP_CONFIG;
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<PathBuf> = 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 {
show_notification(msg);
if notify && !APP_CONFIG.read().disable_notifications() {
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::<Icon>())
}*/
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::<Icon>())
}
}
_ => None,
};

show_notification(msg, icon);
}

fn show_notification(msg: &str, icon: Option<Icon>) {
// 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
Expand Down
42 changes: 12 additions & 30 deletions src/sketch_board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -528,16 +525,13 @@ 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(
log_result_with_pixbuf(
&format!("File saved to '{}'.", output_filename),
!APP_CONFIG.read().disable_notifications(),
image.clone(),
)
}
};
Expand Down Expand Up @@ -607,17 +601,14 @@ 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(
log_result_with_pixbuf(
&format!("File saved to '{}'.", output_filename),
!APP_CONFIG.read().disable_notifications(),
pixbuf.clone(),
)
}
};
Expand Down Expand Up @@ -687,10 +678,7 @@ impl SketchBoard {
match result {
Err(e) => eprintln!("Error saving {e}"),
Ok(()) => {
log_result(
"Copied to clipboard.",
!APP_CONFIG.read().disable_notifications(),
);
log_result_with_pixbuf("Copied to clipboard.", image.clone());

// TODO: rethink order and messaging patterns
if APP_CONFIG.read().save_after_copy() {
Expand Down Expand Up @@ -726,14 +714,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),
}
}

Expand Down
Loading