This commit is contained in:
Daniel Langbein 2025-02-22 17:52:53 +01:00
commit acb7773889
Signed by: langfingaz
GPG Key ID: 6C47C753F0823002
6 changed files with 322 additions and 0 deletions

56
README.md Normal file
View File

@ -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!"
```

1
res/source.txt Normal file
View File

@ -0,0 +1 @@
https://openclipart.org/detail/20502/pocket-watch-icon

73
res/watch-icon.svg Normal file
View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" id="svg2845" sodipodi:docname="pocket_watch.svg" viewBox="0 0 180 180" sodipodi:version="0.32" version="1.0" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46">
<sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" inkscape:window-y="0" pagecolor="#ffffff" inkscape:window-height="727" inkscape:zoom="1.76" inkscape:window-x="0" showgrid="false" borderopacity="1.0" inkscape:current-layer="layer2" inkscape:cx="-60.513955" inkscape:cy="68.432376" inkscape:window-width="1280" inkscape:pageopacity="0.0" inkscape:document-units="px"/>
<g id="layer1" inkscape:label="background" transform="translate(176.1 -59.114)" inkscape:groupmode="layer" sodipodi:insensitive="true">
<rect id="rect2160" style="stroke:#4d4d4d;stroke-width:1.0479;fill:#ccccff" transform="translate(-176.1 59.114)" rx="8.907" ry="8.907" height="174.02" width="174.02" y="2.9904" x="2.9904"/>
</g>
<g id="layer2" inkscape:label="icon" inkscape:groupmode="layer">
<g id="g3588" transform="matrix(1.5037 0 0 1.5037 -114.71 -105.18)">
<path id="rect3466" style="stroke-linejoin:round;stroke:#000000;stroke-width:1.5;fill:#000000" d="m127.81 88.812v4.844h4.38v4.844h7.9v-4.844h4.41v-4.844h-16.69z"/>
<path id="path3464" sodipodi:rx="47" sodipodi:ry="47" style="stroke-linejoin:round;stroke:#000000;stroke-width:5.2928;fill:#ffff90" sodipodi:type="arc" d="m138 86a47 47 0 1 1 -94 0 47 47 0 1 1 94 0z" transform="matrix(.75575 0 0 .75575 67.372 69.029)" sodipodi:cy="86" sodipodi:cx="91"/>
<g id="g3522" transform="matrix(.75575 0 0 .75575 68.127 50.052)">
<path id="path3474" style="fill:#000000" d="m89.989 79.487l-3.688 30.443h-0.042l0.042 0.11-0.042 0.3h0.147l3.583 10.17 3.605-10.17h0.147l-0.042-0.3 0.042-0.11h-0.042l-3.71-30.443z"/>
<path id="path3480" inkscape:transform-center-x="-10.123637" style="fill:#000000" inkscape:transform-center-y="7.2278985" d="m122.6 134.73l-28.431-24.87 0.025-0.03-0.126-0.04-0.281-0.25-0.086 0.12-12.302-4.39 8.121 10.24-0.086 0.12 0.33 0.18 0.077 0.11 0.025-0.04 32.734 18.85z"/>
</g>
<g id="g3508" transform="matrix(.75575 0 0 .75575 67.535 49.693)">
<path id="path3482" inkscape:transform-center-x="0.70063" style="stroke:#000000;stroke-width:1.3232px;fill:none" inkscape:transform-center-y="-37.718461" d="m90.284 68.851l-0.402 10.031"/>
<use id="use3486" inkscape:transform-center-y="-0.70063" inkscape:transform-center-x="-37.71846" xlink:href="#path3482" transform="matrix(0 1 -1 0 202.37 20.802)" height="180" width="180" y="0" x="0"/>
<use id="use3488" xlink:href="#path3482" inkscape:transform-center-x="-32.314828" inkscape:transform-center-y="-19.465994" transform="matrix(.5 .86603 -.86603 .5 142.03 -22.828)" height="180" width="180" y="0" x="0"/>
<use id="use3490" inkscape:transform-center-y="-33.015459" inkscape:transform-center-x="-18.252464" xlink:href="#path3482" transform="matrix(.86603 .5 -.5 .86603 67.955 -30.442)" height="180" width="180" y="0" x="0"/>
<use id="use3492" xlink:href="#path3482" inkscape:transform-center-x="-33.015458" inkscape:transform-center-y="18.252464" transform="matrix(-.5 .86603 -.86603 -.5 232.81 88.757)" height="180" width="180" y="0" x="0"/>
<use id="use3494" inkscape:transform-center-y="32.314827" inkscape:transform-center-x="-19.465989" xlink:href="#path3482" transform="matrix(-.86603 .5 -.5 -.86603 225.2 162.83)" height="180" width="180" y="0" x="0"/>
<use id="use3496" xlink:href="#path3482" inkscape:transform-center-x="-0.70063083" inkscape:transform-center-y="37.718459" transform="matrix(-1 0 0 -1 181.57 223.17)" height="180" width="180" y="0" x="0"/>
<use id="use3498" inkscape:transform-center-y="33.015457" inkscape:transform-center-x="18.252461" xlink:href="#path3482" transform="matrix(-.86603 -.5 .5 -.86603 113.61 253.61)" height="180" width="180" y="0" x="0"/>
<use id="use3500" xlink:href="#path3482" inkscape:transform-center-x="32.314824" inkscape:transform-center-y="19.465998" transform="matrix(-.5 -.86603 .86603 -.5 39.54 246)" height="180" width="180" y="0" x="0"/>
<use id="use3502" inkscape:transform-center-y="0.70063983" inkscape:transform-center-x="37.718457" xlink:href="#path3482" transform="matrix(0 -1 1 0 -20.802 202.37)" height="180" width="180" y="0" x="0"/>
<use id="use3504" xlink:href="#path3482" inkscape:transform-center-x="33.015463" inkscape:transform-center-y="-18.252452" transform="matrix(.5 -.86603 .86603 .5 -51.244 134.41)" height="180" width="180" y="0" x="0"/>
<use id="use3506" inkscape:transform-center-y="-32.314819" inkscape:transform-center-x="19.466004" xlink:href="#path3482" transform="matrix(.86603 -.5 .5 .86603 -43.63 60.341)" height="180" width="180" y="0" x="0"/>
</g>
</g>
</g>
<metadata>
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
<dc:publisher>
<cc:Agent rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
<dc:title>Pocket watch icon</dc:title>
<dc:date>2008-12-01T15:14:04</dc:date>
<dc:description>Pocket watch icon Originally developed for www.studenti.unige.it</dc:description>
<dc:source>https://openclipart.org/detail/20502/pocket-watch-icon-by-cod_fsfe</dc:source>
<dc:creator>
<cc:Agent>
<dc:title>CoD_fsfe</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>clock</rdf:li>
<rdf:li>color</rdf:li>
<rdf:li>icon</rdf:li>
<rdf:li>inkscape</rdf:li>
<rdf:li>pocket watch</rdf:li>
<rdf:li>time</rdf:li>
<rdf:li>watch</rdf:li>
<rdf:li>web</rdf:li>
<rdf:li>webdesign</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
</cc:License>
</rdf:RDF>
</metadata>
<script xmlns=""/></svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

19
shell.nix Normal file
View File

@ -0,0 +1,19 @@
{ pkgs ? import <nixpkgs> {} }:
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
];
}

115
src/dndbuster/app.py Normal file
View File

@ -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()

58
src/dndbuster/timer.py Normal file
View File

@ -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()