From acb7773889c8f7612ad07eff9a497351a7e0be32 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Sat, 22 Feb 2025 17:52:53 +0100 Subject: [PATCH] init --- README.md | 56 ++++++++++++++++++++ res/source.txt | 1 + res/watch-icon.svg | 73 ++++++++++++++++++++++++++ shell.nix | 19 +++++++ src/dndbuster/app.py | 115 +++++++++++++++++++++++++++++++++++++++++ src/dndbuster/timer.py | 58 +++++++++++++++++++++ 6 files changed, 322 insertions(+) create mode 100644 README.md create mode 100644 res/source.txt create mode 100644 res/watch-icon.svg create mode 100644 shell.nix create mode 100644 src/dndbuster/app.py create mode 100644 src/dndbuster/timer.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccbc853 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# DnDBuster + +A timer that notifies you - even if "do not disturb" is enabled. + +## Application IDs + +https://developer.gnome.org/documentation/tutorials/application-id.html + +Used by + +- GtkApplication + +> identifying your application to the system, for ensuring that only one instance of your application is running at a given time, and as a way of passing messages to your application + +- D-Bus + +> to name your application on the message bus. This is the primary means of communicating between applications + +- name of the .desktop file for your application + +> This file is how you describe your application to the system (so that it can be displayed in and launched by GNOME). + +## .desktop entry + +https://wiki.gnome.org/HowDoI/GNotification + +The GNOME desktop environment needs a `.desktop` file that matches your applications ID. Otherwise, sent notifications are not displayed! + +> Warning: gnome-shell uses desktop files to find extra information (app icon, name) about the sender of the notification. If you don't have a desktop file whose base name matches the application id, then your notification will not show up. + +During development, we created a dummy vile at `~/.local/share/applications/de.p1st.dndbuster.desktop` with content: + +``` +[Desktop Entry] +Name=DnDBuster +Comment=A simple timer app +Exec=steam steam://rungameid/2707930 +Icon=dndbuster-stopwatch +Terminal=false +Type=Application +Categories=Game; +``` + +Copy `watch-icon.svg` to `~/.local/share/icons/hicolor/scalable/apps/dndbuster-stopwatch.svg` + +## Alternatives + +https://man.archlinux.org/man/notify-send.1.en + +```shell +nix-shell -p libnotify +``` +```shell +sleep 30m +notify-send -u critical -a PleaseDisturbTimer "Alert" "Time is over!" +``` diff --git a/res/source.txt b/res/source.txt new file mode 100644 index 0000000..30933d3 --- /dev/null +++ b/res/source.txt @@ -0,0 +1 @@ +https://openclipart.org/detail/20502/pocket-watch-icon \ No newline at end of file diff --git a/res/watch-icon.svg b/res/watch-icon.svg new file mode 100644 index 0000000..34deeb8 --- /dev/null +++ b/res/watch-icon.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Pocket watch icon + 2008-12-01T15:14:04 + Pocket watch icon Originally developed for www.studenti.unige.it + https://openclipart.org/detail/20502/pocket-watch-icon-by-cod_fsfe + + + CoD_fsfe + + + + + clock + color + icon + inkscape + pocket watch + time + watch + web + webdesign + + + + + + + + + + + \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d392682 --- /dev/null +++ b/shell.nix @@ -0,0 +1,19 @@ +{ pkgs ? import {} }: + pkgs.mkShell { + buildInputs = [ + (pkgs.python3.withPackages (python-pkgs: with python-pkgs; [ + # Python bindings for Glib + pygobject3 + # Typing Stubs for PyGObject + pygobject-stubs + + # Not sure if required + #dbus-python + ])) + + # Not sure if required + #pkgs.glib + #pkgs.gtk4 + #pkgs.gobject-introspection + ]; +} diff --git a/src/dndbuster/app.py b/src/dndbuster/app.py new file mode 100644 index 0000000..fa2e0d8 --- /dev/null +++ b/src/dndbuster/app.py @@ -0,0 +1,115 @@ +import gi + +from timer import Timer + +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import GLib, Gio, Gtk, Adw + + +def main(): + # Create a new application + app = Application() + # Run the application + app.run(None) + + +class Application(Adw.Application): + def __init__(self): + super().__init__(application_id='de.p1st.dndbuster') + self.window: AppWindow | None = None + + def do_activate(self): + # Windows are associated with the application. + # When the last one is closed the application shuts down. + self.window = AppWindow(application=self) + self.window.present() + + def notify_timer(self): + notification = Gio.Notification() + notification.set_title('Timeout') + notification.set_body('The time is over and your do-not-disturb mode just got busted!') + notification.set_priority(Gio.NotificationPriority.URGENT) + self.send_notification(id=f'{self.get_application_id()}.{Timer.current_time()}', notification=notification) + + +class AppWindow(Gtk.ApplicationWindow): + def __init__(self, application: Application): + super().__init__(application=application, title='DnDBuster') + + self.app = application + self.set_default_size(400, 200) + + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.box.set_spacing(10) + self.box.set_margin_top(10) + self.box.set_margin_bottom(10) + self.box.set_margin_start(10) + self.box.set_margin_end(10) + self.set_child(self.box) + + self.button_start_restart = Gtk.Button(label='Start') + self.button_start_restart.connect('clicked', self.on_button_start_restart_clicked) + self.box.append(self.button_start_restart) + + self.button_pause_resume = Gtk.Button(label='Pause', visible=False) + self.button_pause_resume.connect('clicked', self.on_button_pause_resume_clicked) + self.box.append(self.button_pause_resume) + + self.progress_bar = Gtk.ProgressBar(visible=False) + self.box.append(self.progress_bar) + + self.timer = None + + def update_progress(self) -> bool: + total_min = 25 + total_sec = total_min * 60 + delta_sec = self.timer.read() + progress = delta_sec / total_sec + self.progress_bar.set_fraction(progress) + + if progress >= 1: + self.app.notify_timer() + # Stop regularly calling update_progress() + return False + + # Weather to continue regularly calling update_progress() + return not self.timer.is_paused() + + def on_button_start_restart_clicked(self, _widget): + if self.timer is None: + self.add_timer() + + self.button_start_restart.set_label("Restart") + self.button_pause_resume.set_visible(True) + else: + self.add_timer() + + self.update_label() + + def add_timer(self): + self.timer = Timer() + self.timer.start() + self.progress_bar.set_visible(True) + self.add_timeout() + + def add_timeout(self): + GLib.timeout_add_seconds(1, self.update_progress) + + def on_button_pause_resume_clicked(self, _widget): + if self.timer.is_paused(): + self.timer.resume() + self.add_timeout() + else: + self.timer.pause() + self.update_label() + + def update_label(self): + if self.timer.is_paused(): + self.button_pause_resume.set_label("Resume") + else: + self.button_pause_resume.set_label("Pause") + + +if __name__ == '__main__': + main() diff --git a/src/dndbuster/timer.py b/src/dndbuster/timer.py new file mode 100644 index 0000000..6f47390 --- /dev/null +++ b/src/dndbuster/timer.py @@ -0,0 +1,58 @@ +import time + + +class Timer: + def __init__(self): + self.start_seconds = None + self.pause_timer = None + + def start(self): + """ + Start time measurement + """ + self.start_seconds = self.current_time() + + + def is_started(self) -> bool: + """ + :return: Weather time measurement has been started. + """ + return self.start_seconds is not None + + def pause(self): + """ + Pause time measurement. + """ + self.pause_timer = Timer() + self.pause_timer.start() + + def is_paused(self): + """ + :return: Weather time measurement is paused. + """ + return self.pause_timer is not None + + def resume(self): + """ + Resumes time measurement. + """ + if self.pause_timer: + pause_delta = self.pause_timer.read() + self.start_seconds += pause_delta + self.pause_timer = None + + def read(self): + """ + Return measured time in seconds. + """ + end_seconds = self.current_time() + delta = end_seconds - self.start_seconds + + if self.is_paused(): + delta -= self.pause_timer.read() + + return delta + + @classmethod + def current_time(cls): + return time.time()