mirror of
https://codeberg.org/icewind/galton.git
synced 2026-06-03 10:24:07 +02:00
add notify option
This commit is contained in:
parent
b423323473
commit
ea24479757
7 changed files with 878 additions and 19 deletions
817
Cargo.lock
generated
817
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
60
src/main.rs
60
src/main.rs
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue