add notify option

This commit is contained in:
Robin Appelman 2025-11-11 01:21:47 +01:00
commit ea24479757
7 changed files with 878 additions and 19 deletions

817
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,8 @@ ctrlc = "3.5.0"
sha2 = "0.11.0-rc.2" sha2 = "0.11.0-rc.2"
hex = "0.4.3" hex = "0.4.3"
xrust = "1.3.0" xrust = "1.3.0"
notify-rust = "4.11.7"
open = "5.3.2"
[dev-dependencies] [dev-dependencies]
maplit = "1.0.2" maplit = "1.0.2"

View file

@ -115,3 +115,10 @@ delete the newly download file if a duplicate is found.
[watch] [watch]
remove-duplicates = true remove-duplicates = true
``` ```
## Notifications
Since having the downloaded file being moved or deleted right after downloading
makes the default desktop notifications less useful (e.g. you can't open the
newly download file trough it). Galton supports sending it's own notifications
whenever it performs an action. Providing an easy way to interact with the file.

View file

@ -1,6 +1,7 @@
[watch] [watch]
symlink = "~/Downloads/last" symlink = "~/Downloads/last"
remove-duplicates = true remove-duplicates = true
notify = true
[[rule]] [[rule]]
name = "\\.(csv|CSV)" name = "\\.(csv|CSV)"

View file

@ -10,7 +10,7 @@ with lib; let
removeNulls = filterAttrs (_: val: val != null); removeNulls = filterAttrs (_: val: val != null);
configFile = format.generate "galton.toml" { configFile = format.generate "galton.toml" {
watch = removeNulls { watch = removeNulls {
inherit (cfg) symlink; inherit (cfg) symlink notify;
remove-duplicates = cfg.removeDuplicates; remove-duplicates = cfg.removeDuplicates;
}; };
rule = map removeNulls cfg.rules; rule = map removeNulls cfg.rules;
@ -37,6 +37,12 @@ in {
description = "Remove duplicate downloads"; description = "Remove duplicate downloads";
}; };
notify = mkOption {
type = types.bool;
default = false;
description = "Show notifications for moved downloads";
};
rules = mkOption { rules = mkOption {
default = []; default = [];
type = types.listOf (types.submodule { type = types.listOf (types.submodule {

View file

@ -35,6 +35,8 @@ pub struct WatchConfig {
symlink: Option<String>, symlink: Option<String>,
#[serde(rename = "remove-duplicates", default)] #[serde(rename = "remove-duplicates", default)]
pub remove_duplicates: bool, pub remove_duplicates: bool,
#[serde(default)]
pub notify: bool,
} }
impl WatchConfig { impl WatchConfig {

View file

@ -8,12 +8,13 @@ use main_error::MainResult;
use notify_debouncer_full::notify::event::{AccessKind, AccessMode, ModifyKind, RenameMode}; use notify_debouncer_full::notify::event::{AccessKind, AccessMode, ModifyKind, RenameMode};
use notify_debouncer_full::notify::{EventKind, RecursiveMode}; use notify_debouncer_full::notify::{EventKind, RecursiveMode};
use notify_debouncer_full::{new_debouncer, DebounceEventResult}; use notify_debouncer_full::{new_debouncer, DebounceEventResult};
use notify_rust::{Hint, Notification};
use std::fs::{copy, create_dir_all, read_dir, remove_file, rename}; use std::fs::{copy, create_dir_all, read_dir, remove_file, rename};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::thread::sleep; use std::thread::{sleep, spawn};
use std::time::Duration; use std::time::Duration;
use tracing::{debug, error, info, instrument}; use tracing::{debug, error, info, instrument};
@ -94,6 +95,7 @@ fn main() -> MainResult {
&rules, &rules,
symlink.as_deref(), symlink.as_deref(),
config.watch.remove_duplicates, config.watch.remove_duplicates,
config.watch.notify,
); );
} }
} }
@ -107,6 +109,7 @@ fn handle_watch_event(
rules: &[Rule], rules: &[Rule],
link_target: Option<&str>, link_target: Option<&str>,
remove_duplicates: bool, remove_duplicates: bool,
notify: bool,
) { ) {
let handle_path = |path: &Path| { let handle_path = |path: &Path| {
// give originfox time to set xattr // give originfox time to set xattr
@ -117,10 +120,14 @@ fn handle_watch_event(
} }
match FileInfo::load(path) { match FileInfo::load(path) {
Ok(file) => maybe_link( Ok(file) => {
handle_file(&file, rules, remove_duplicates).as_deref(), if let Some(new_path) = handle_file(&file, rules, remove_duplicates) {
link_target, maybe_link_target(&new_path, link_target);
), if notify {
show_notification(new_path);
}
};
}
Err(error) => { Err(error) => {
error!(%error, "failed to load file info"); error!(%error, "failed to load file info");
} }
@ -166,8 +173,47 @@ fn is_part(path: &Path) -> bool {
path.extension().and_then(|ext| ext.to_str()) == Some("part") path.extension().and_then(|ext| ext.to_str()) == Some("part")
} }
fn maybe_link(source: Option<&Path>, target: Option<&str>) { fn show_notification(source: PathBuf) {
if let (Some(source), Some(target)) = (source, target) { debug!(file = %source.display(), "showing notification for file");
match Notification::new()
.summary("Download moved")
.appname("Galton")
.body(&format!(
"<a href=\"{}\">{}</a>",
source.display(),
source.file_name().unwrap().to_string_lossy()
))
.hint(Hint::ActionIcons(true))
.hint(Hint::Category("transfer.complete".into()))
.action("document-open", "Open")
.action("folder-open", "Open Containing folder")
.show()
{
Ok(notification) => {
spawn(move || {
notification.wait_for_action(|action| match action {
"document-open" => {
if let Err(error) = open::that(&source) {
error!(%error, file = %source.display(), "failed to open file from notification");
}
}
"folder-open" => {
if let Err(error) = open::that(source.parent().unwrap()) {
error!(%error, file = %source.display(), "failed to open parent folder from notification");
}
}
_ => (),
});
});
}
Err(error) => {
error!(%error, "Failed to show notification");
}
}
}
fn maybe_link_target(source: &Path, target: Option<&str>) {
if let Some(target) = target {
if Path::new(target).is_symlink() { if Path::new(target).is_symlink() {
if let Err(error) = remove_file(target) { if let Err(error) = remove_file(target) {
error!(%error, "failed to remove link target"); error!(%error, "failed to remove link target");