Skip to main content

gitlab_tracker_notify/
lib.rs

1//! Desktop notification plugin for gitlab-tracker.
2//!
3//! Compiled with the `desktop` feature (on by default) to send OS notifications
4//! via `notify-rust`. Build with `--no-default-features` for a zero-dependency
5//! stub suitable for headless / CI environments.
6//!
7//! When the user clicks a notification, the MR URL is opened in the default browser.
8
9// ── Internal helper ───────────────────────────────────────────────────────────
10
11/// Show a notification and, if the user clicks it, open `url` in the default browser.
12/// The D-Bus action wait is performed in a detached thread to avoid blocking the caller.
13#[cfg(feature = "desktop")]
14fn show_with_url(notification: notify_rust::Notification, url: String) {
15    if let Ok(handle) = notification.show() {
16        std::thread::spawn(move || {
17            handle.wait_for_action(|action| {
18                if action == "default" {
19                    let _ = open::that(&url);
20                }
21            });
22        });
23    }
24}
25
26// ── MR notification events ───────────────────────────────────────────────────
27
28/// Notify that an MR has appeared on a branch it was not previously seen on.
29#[cfg(feature = "desktop")]
30pub fn mr_on_new_branch(mr_id: &str, title: &str, branch: &str, web_url: &str) {
31    let notification = notify_rust::Notification::new()
32        .summary("GitLab MR Tracker")
33        .body(&format!(
34            "MR !{} ({}) is now present on branch '{}'!",
35            mr_id, title, branch
36        ))
37        .icon("dialog-information")
38        .action("default", "Open MR")
39        .finalize();
40    show_with_url(notification, web_url.to_owned());
41}
42
43/// Notify that an MR's `updated_at` field has changed (i.e. the MR was modified).
44#[cfg(feature = "desktop")]
45pub fn mr_updated(mr_id: &str, title: &str, updated_at: Option<&str>, web_url: &str) {
46    let notification = notify_rust::Notification::new()
47        .summary(&format!("MR !{} updated", mr_id))
48        .body(&format!(
49            "{}\n{}",
50            title,
51            updated_at.unwrap_or("unknown date")
52        ))
53        .icon("dialog-information")
54        .action("default", "Open MR")
55        .finalize();
56    show_with_url(notification, web_url.to_owned());
57}
58
59/// Notify that an MR's mergeability status has changed.
60/// Accepts string labels so this crate stays independent of gitlab-tracker model types.
61#[cfg(feature = "desktop")]
62pub fn mr_mergeability_changed(mr_id: &str, title: &str, old: &str, new: &str, web_url: &str) {
63    let notification = notify_rust::Notification::new()
64        .summary(&format!("MR !{} — mergeability changed", mr_id))
65        .body(&format!("{}\n{} → {}", title, old, new))
66        .icon("dialog-warning")
67        .action("default", "Open MR")
68        .finalize();
69    show_with_url(notification, web_url.to_owned());
70}
71
72/// Notify that an MR's review complexity category has changed (e.g. EASY → COMPLEX).
73///
74/// Fires when the computed difficulty score crosses a category boundary during a refresh.
75/// `old` and `new` are human-readable labels matching the UI badges (e.g. `"🟢 EASY"`,
76/// `"🟡 MEDIUM"`, `"🔴 COMPLEX"`).
77#[cfg(feature = "desktop")]
78pub fn mr_complexity_changed(mr_id: &str, title: &str, old: &str, new: &str, web_url: &str) {
79    let notification = notify_rust::Notification::new()
80        .summary(&format!("MR !{} — complexity changed", mr_id))
81        .body(&format!("{}\n{} → {}", title, old, new))
82        .icon("dialog-warning")
83        .action("default", "Open MR")
84        .finalize();
85    show_with_url(notification, web_url.to_owned());
86}
87
88/// Notify that an MR's milestone has changed.
89#[cfg(feature = "desktop")]
90pub fn mr_milestone_changed(mr_id: &str, title: &str, old: &str, new: &str, web_url: &str) {
91    let notification = notify_rust::Notification::new()
92        .summary(&format!("MR !{} — milestone changed", mr_id))
93        .body(&format!("{}\n{} → {}", title, old, new))
94        .icon("dialog-information")
95        .action("default", "Open MR")
96        .finalize();
97    show_with_url(notification, web_url.to_owned());
98}
99
100// ── Tracker ticket notification events ───────────────────────────────────────
101
102/// Notify that a tracked field on a linked tracker ticket has changed.
103///
104/// This is a **single generic entry point** for all ticket field changes.
105/// The `field` parameter is a human-readable label (e.g. `"priority"`, `"status"`),
106/// sourced from [`gitlab_tracker_core::TicketChange::field_label`].
107///
108/// Using one function instead of per-field functions means that adding a new tracked
109/// field in `core` (e.g. `Sprint`) requires **zero changes** to this crate.
110/// The orchestrator maps `TicketChange` variants to this function directly.
111///
112/// # Icon selection
113/// Priority changes use `"dialog-warning"` (yellow); all others use `"dialog-information"`.
114#[cfg(feature = "desktop")]
115pub fn ticket_field_changed(
116    ticket_id: &str,
117    mr_title: &str,
118    field: &str,
119    old: &str,
120    new: &str,
121    ticket_url: &str,
122) {
123    let icon = if field == "priority" || field == "effort" {
124        "dialog-warning"
125    } else {
126        "dialog-information"
127    };
128    let notification = notify_rust::Notification::new()
129        .summary(&format!("Ticket #{} — {} changed", ticket_id, field))
130        .body(&format!("{}\n{} → {}", mr_title, old, new))
131        .icon(icon)
132        .action("default", "Open ticket")
133        .finalize();
134    show_with_url(notification, ticket_url.to_owned());
135}
136
137// ── No-op stubs when the `desktop` feature is disabled ───────────────────────
138
139#[cfg(not(feature = "desktop"))]
140#[inline(always)]
141pub fn mr_on_new_branch(_mr_id: &str, _title: &str, _branch: &str, _web_url: &str) {}
142
143#[cfg(not(feature = "desktop"))]
144#[inline(always)]
145pub fn mr_updated(_mr_id: &str, _title: &str, _updated_at: Option<&str>, _web_url: &str) {}
146
147#[cfg(not(feature = "desktop"))]
148#[inline(always)]
149pub fn mr_mergeability_changed(_mr_id: &str, _title: &str, _old: &str, _new: &str, _web_url: &str) {
150}
151
152#[cfg(not(feature = "desktop"))]
153#[inline(always)]
154pub fn mr_complexity_changed(_mr_id: &str, _title: &str, _old: &str, _new: &str, _web_url: &str) {}
155
156#[cfg(not(feature = "desktop"))]
157#[inline(always)]
158pub fn mr_milestone_changed(_mr_id: &str, _title: &str, _old: &str, _new: &str, _web_url: &str) {}
159
160#[cfg(not(feature = "desktop"))]
161#[inline(always)]
162pub fn ticket_field_changed(
163    _ticket_id: &str,
164    _mr_title: &str,
165    _field: &str,
166    _old: &str,
167    _new: &str,
168    _ticket_url: &str,
169) {
170}