slow-coffee 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PLNech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: slow-coffee
3
+ Version: 1.0.0
4
+ Summary: A take-your-time keep-awake indicator for your system tray - on by default.
5
+ Author: PLNech
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/PLNech/slow-coffee
8
+ Project-URL: Source, https://github.com/PLNech/slow-coffee
9
+ Project-URL: Issues, https://github.com/PLNech/slow-coffee/issues
10
+ Keywords: caffeine,caffeinate,keep-awake,idle,screensaver,tray,appindicator,gnome,wayland,presentation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: X11 Applications :: GTK
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Desktop Environment
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: PyGObject>=3.42
23
+ Dynamic: license-file
24
+
25
+ <div align="center">
26
+
27
+ # ☕ slow-coffee
28
+
29
+ **A take-your-time keep-awake indicator for your system tray. On by default.**
30
+
31
+ A small coffee cup lives in your top bar and stops the screen blanking or the
32
+ session going idle. The cup boots *full*, because the whole point is to stay
33
+ awake until you say otherwise. No daemon to babysit, no battery to drain - it
34
+ just holds a D-Bus idle inhibitor and gets out of the way.
35
+
36
+ [![CI](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml/badge.svg)](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/slow-coffee.svg)](https://pypi.org/project/slow-coffee/)
38
+ [![AUR](https://img.shields.io/aur/version/slow-coffee.svg)](https://aur.archlinux.org/packages/slow-coffee)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+
41
+ <img src="docs/menu.png" alt="slow-coffee tray menu" width="260">
42
+
43
+ </div>
44
+
45
+ ## Why another caffeine?
46
+
47
+ Most keep-awake tools assume you want a *trigger*: stay awake while a video
48
+ plays, while music plays, while an app is fullscreen. slow-coffee assumes the
49
+ opposite. You opened it because you want to stay awake **now**, so it starts
50
+ caffeinated and stays that way. One click (or a timer) to stop.
51
+
52
+ - **On by default.** Cup full at launch. `--start-inactive` if you disagree.
53
+ - **Honest icon.** Full = really inhibiting; empty = really not. No lying cup.
54
+ - **Idle-only by default.** The screen stays on, but closing the lid still
55
+ suspends - safer for a laptop than blocking all sleep.
56
+ - **Timers.** 30 min / 1 hour / 2 hours / indefinitely, from the menu or
57
+ `--minutes N`.
58
+ - **Self-contained.** Bundles its own icons; no theme or extra data package.
59
+
60
+ ## Install
61
+
62
+ slow-coffee needs the GObject introspection runtime (GTK 3 + Ayatana
63
+ AppIndicator). On a tray that hides AppIndicators (e.g. vanilla GNOME), enable
64
+ the *AppIndicator support* extension.
65
+
66
+ ### Arch (AUR)
67
+
68
+ ```sh
69
+ yay -S slow-coffee # or: paru -S slow-coffee
70
+ ```
71
+
72
+ ### Debian / Ubuntu (.deb)
73
+
74
+ Grab `slow-coffee_*.deb` from the [latest release](https://github.com/PLNech/slow-coffee/releases) and:
75
+
76
+ ```sh
77
+ sudo apt install ./slow-coffee_1.0.0_all.deb
78
+ ```
79
+
80
+ ### pip (PyPI)
81
+
82
+ The GIR libraries come from your distro, the rest from pip:
83
+
84
+ ```sh
85
+ # Debian/Ubuntu
86
+ sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1
87
+ # Fedora
88
+ sudo dnf install python3-gobject gtk3 libayatana-appindicator-gtk3
89
+
90
+ pip install --user slow-coffee
91
+ slow-coffee --install-desktop # add it to your applications menu (no root)
92
+ ```
93
+
94
+ ### From source
95
+
96
+ ```sh
97
+ git clone https://github.com/PLNech/slow-coffee
98
+ cd slow-coffee
99
+ sudo make install # PREFIX=/usr/local by default
100
+ ```
101
+
102
+ ## Usage
103
+
104
+ Run `slow-coffee` (or launch **Slow Coffee** from your applications menu). The
105
+ cup appears in the tray, already caffeinated. Click it for the menu:
106
+
107
+ | Item | Does |
108
+ |------|------|
109
+ | Activate / Deactivate | Toggle the inhibitor by hand |
110
+ | Indefinitely / 30 min / 1 h / 2 h | Pick a duration; it auto-stops when time is up |
111
+ | About / Quit | The usual |
112
+
113
+ ```sh
114
+ slow-coffee # start caffeinated, forever
115
+ slow-coffee --minutes 90 # caffeinated for 90 minutes, then off
116
+ slow-coffee --start-inactive # start decaf; toggle on from the tray
117
+ slow-coffee --install-desktop --autostart # menu entry + start at login
118
+ slow-coffee --uninstall-desktop # undo the above
119
+ ```
120
+
121
+ ## How it works
122
+
123
+ slow-coffee asks the session to not go idle, preferring whatever is actually
124
+ running and falling back gracefully:
125
+
126
+ 1. **`org.gnome.SessionManager.Inhibit`** (flag `8`, idle) - GNOME.
127
+ *X11 verified; GNOME-Wayland expected but untested by the author.*
128
+ 2. **`org.freedesktop.ScreenSaver.Inhibit`** - KDE and other freedesktop shells.
129
+ 3. **`systemd-inhibit --what=idle`** - logind fallback for bare window managers.
130
+
131
+ It holds one persistent session-bus connection and releases the inhibitor on
132
+ quit. It does **not** add the "suspend" flag, so lid-close still sleeps. Wayland
133
+ note: the idle-inhibit protocol is per-surface, so a global manual inhibitor is
134
+ shell-dependent; under GNOME the SessionManager call covers it, under wlroots
135
+ your mileage varies and the `systemd-inhibit` fallback handles sleep.
136
+
137
+ ## Prior art
138
+
139
+ This is an independent implementation, written from scratch, but it stands on
140
+ the shoulders of two lovely projects worth knowing:
141
+
142
+ - [caffeine](https://launchpad.net/caffeine) by Reuben Thomas - the classic
143
+ fullscreen-auto daemon + manual indicator (GPL-3, X11).
144
+ - [caffeine-ng](https://codeberg.org/WhyNotHugo/caffeine-ng) by Hugo Barrera -
145
+ the modern rewrite with presentation-mode and audio awareness.
146
+
147
+ slow-coffee differs by being *on by default*, using D-Bus inhibition (rather
148
+ than an unmapped X window), shipping as a single self-contained package, and
149
+ living under MIT.
150
+
151
+ ## Development
152
+
153
+ ```sh
154
+ python3 -m slow_coffee # run from a checkout (needs system PyGObject)
155
+ python3 make_icons.py # regenerate the cup icons (needs Pillow)
156
+ /usr/bin/python3 docs/render_menu.py docs/menu.png # re-render the screenshot
157
+ ```
158
+
159
+ ## License
160
+
161
+ [MIT](LICENSE) (c) 2026 PLNech.
@@ -0,0 +1,137 @@
1
+ <div align="center">
2
+
3
+ # ☕ slow-coffee
4
+
5
+ **A take-your-time keep-awake indicator for your system tray. On by default.**
6
+
7
+ A small coffee cup lives in your top bar and stops the screen blanking or the
8
+ session going idle. The cup boots *full*, because the whole point is to stay
9
+ awake until you say otherwise. No daemon to babysit, no battery to drain - it
10
+ just holds a D-Bus idle inhibitor and gets out of the way.
11
+
12
+ [![CI](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml/badge.svg)](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml)
13
+ [![PyPI](https://img.shields.io/pypi/v/slow-coffee.svg)](https://pypi.org/project/slow-coffee/)
14
+ [![AUR](https://img.shields.io/aur/version/slow-coffee.svg)](https://aur.archlinux.org/packages/slow-coffee)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
16
+
17
+ <img src="docs/menu.png" alt="slow-coffee tray menu" width="260">
18
+
19
+ </div>
20
+
21
+ ## Why another caffeine?
22
+
23
+ Most keep-awake tools assume you want a *trigger*: stay awake while a video
24
+ plays, while music plays, while an app is fullscreen. slow-coffee assumes the
25
+ opposite. You opened it because you want to stay awake **now**, so it starts
26
+ caffeinated and stays that way. One click (or a timer) to stop.
27
+
28
+ - **On by default.** Cup full at launch. `--start-inactive` if you disagree.
29
+ - **Honest icon.** Full = really inhibiting; empty = really not. No lying cup.
30
+ - **Idle-only by default.** The screen stays on, but closing the lid still
31
+ suspends - safer for a laptop than blocking all sleep.
32
+ - **Timers.** 30 min / 1 hour / 2 hours / indefinitely, from the menu or
33
+ `--minutes N`.
34
+ - **Self-contained.** Bundles its own icons; no theme or extra data package.
35
+
36
+ ## Install
37
+
38
+ slow-coffee needs the GObject introspection runtime (GTK 3 + Ayatana
39
+ AppIndicator). On a tray that hides AppIndicators (e.g. vanilla GNOME), enable
40
+ the *AppIndicator support* extension.
41
+
42
+ ### Arch (AUR)
43
+
44
+ ```sh
45
+ yay -S slow-coffee # or: paru -S slow-coffee
46
+ ```
47
+
48
+ ### Debian / Ubuntu (.deb)
49
+
50
+ Grab `slow-coffee_*.deb` from the [latest release](https://github.com/PLNech/slow-coffee/releases) and:
51
+
52
+ ```sh
53
+ sudo apt install ./slow-coffee_1.0.0_all.deb
54
+ ```
55
+
56
+ ### pip (PyPI)
57
+
58
+ The GIR libraries come from your distro, the rest from pip:
59
+
60
+ ```sh
61
+ # Debian/Ubuntu
62
+ sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1
63
+ # Fedora
64
+ sudo dnf install python3-gobject gtk3 libayatana-appindicator-gtk3
65
+
66
+ pip install --user slow-coffee
67
+ slow-coffee --install-desktop # add it to your applications menu (no root)
68
+ ```
69
+
70
+ ### From source
71
+
72
+ ```sh
73
+ git clone https://github.com/PLNech/slow-coffee
74
+ cd slow-coffee
75
+ sudo make install # PREFIX=/usr/local by default
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ Run `slow-coffee` (or launch **Slow Coffee** from your applications menu). The
81
+ cup appears in the tray, already caffeinated. Click it for the menu:
82
+
83
+ | Item | Does |
84
+ |------|------|
85
+ | Activate / Deactivate | Toggle the inhibitor by hand |
86
+ | Indefinitely / 30 min / 1 h / 2 h | Pick a duration; it auto-stops when time is up |
87
+ | About / Quit | The usual |
88
+
89
+ ```sh
90
+ slow-coffee # start caffeinated, forever
91
+ slow-coffee --minutes 90 # caffeinated for 90 minutes, then off
92
+ slow-coffee --start-inactive # start decaf; toggle on from the tray
93
+ slow-coffee --install-desktop --autostart # menu entry + start at login
94
+ slow-coffee --uninstall-desktop # undo the above
95
+ ```
96
+
97
+ ## How it works
98
+
99
+ slow-coffee asks the session to not go idle, preferring whatever is actually
100
+ running and falling back gracefully:
101
+
102
+ 1. **`org.gnome.SessionManager.Inhibit`** (flag `8`, idle) - GNOME.
103
+ *X11 verified; GNOME-Wayland expected but untested by the author.*
104
+ 2. **`org.freedesktop.ScreenSaver.Inhibit`** - KDE and other freedesktop shells.
105
+ 3. **`systemd-inhibit --what=idle`** - logind fallback for bare window managers.
106
+
107
+ It holds one persistent session-bus connection and releases the inhibitor on
108
+ quit. It does **not** add the "suspend" flag, so lid-close still sleeps. Wayland
109
+ note: the idle-inhibit protocol is per-surface, so a global manual inhibitor is
110
+ shell-dependent; under GNOME the SessionManager call covers it, under wlroots
111
+ your mileage varies and the `systemd-inhibit` fallback handles sleep.
112
+
113
+ ## Prior art
114
+
115
+ This is an independent implementation, written from scratch, but it stands on
116
+ the shoulders of two lovely projects worth knowing:
117
+
118
+ - [caffeine](https://launchpad.net/caffeine) by Reuben Thomas - the classic
119
+ fullscreen-auto daemon + manual indicator (GPL-3, X11).
120
+ - [caffeine-ng](https://codeberg.org/WhyNotHugo/caffeine-ng) by Hugo Barrera -
121
+ the modern rewrite with presentation-mode and audio awareness.
122
+
123
+ slow-coffee differs by being *on by default*, using D-Bus inhibition (rather
124
+ than an unmapped X window), shipping as a single self-contained package, and
125
+ living under MIT.
126
+
127
+ ## Development
128
+
129
+ ```sh
130
+ python3 -m slow_coffee # run from a checkout (needs system PyGObject)
131
+ python3 make_icons.py # regenerate the cup icons (needs Pillow)
132
+ /usr/bin/python3 docs/render_menu.py docs/menu.png # re-render the screenshot
133
+ ```
134
+
135
+ ## License
136
+
137
+ [MIT](LICENSE) (c) 2026 PLNech.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "slow-coffee"
7
+ dynamic = ["version"]
8
+ description = "A take-your-time keep-awake indicator for your system tray - on by default."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "PLNech" }]
13
+ keywords = ["caffeine", "caffeinate", "keep-awake", "idle", "screensaver",
14
+ "tray", "appindicator", "gnome", "wayland", "presentation"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Environment :: X11 Applications :: GTK",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Desktop Environment",
23
+ "Topic :: Utilities",
24
+ ]
25
+ # PyGObject + AyatanaAppIndicator3 GIR come from the system (see README); pip can
26
+ # build PyGObject too if the GObject-introspection dev headers are present.
27
+ dependencies = ["PyGObject>=3.42"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/PLNech/slow-coffee"
31
+ Source = "https://github.com/PLNech/slow-coffee"
32
+ Issues = "https://github.com/PLNech/slow-coffee/issues"
33
+
34
+ [project.scripts]
35
+ slow-coffee = "slow_coffee.__main__:main"
36
+
37
+ [tool.setuptools]
38
+ packages = ["slow_coffee"]
39
+
40
+ [tool.setuptools.dynamic]
41
+ version = { attr = "slow_coffee.__version__" }
42
+
43
+ [tool.setuptools.package-data]
44
+ slow_coffee = ["icons/*.png"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """slow-coffee - a take-your-time keep-awake indicator for the system tray."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,70 @@
1
+ """Command-line entry point for slow-coffee."""
2
+
3
+ import sys
4
+ import signal
5
+ import argparse
6
+
7
+ from . import __version__
8
+
9
+
10
+ def _run_indicator(start_active, minutes):
11
+ # Import the indicator first: it pins gi.require_version("Gtk", "3.0") before
12
+ # anything pulls in gi.repository.Gtk (which would otherwise default to 4.0).
13
+ # Kept lazy so --install-desktop / --version work without a display.
14
+ from .indicator import SlowCoffee
15
+ from gi.repository import Gtk, GLib
16
+
17
+ app = SlowCoffee(start_active=start_active, minutes=minutes)
18
+
19
+ def handler(*_a):
20
+ app.deactivate()
21
+ Gtk.main_quit()
22
+ return False
23
+
24
+ for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
25
+ GLib.unix_signal_add(GLib.PRIORITY_HIGH, sig, handler)
26
+
27
+ Gtk.main()
28
+
29
+
30
+ def main(argv=None):
31
+ p = argparse.ArgumentParser(
32
+ prog="slow-coffee",
33
+ description="A take-your-time keep-awake tray indicator (on by default).")
34
+ p.add_argument("-V", "--version", action="version",
35
+ version="%(prog)s " + __version__)
36
+ p.add_argument("--start-inactive", action="store_true",
37
+ help="boot decaf (cup empty); default is on")
38
+ p.add_argument("--minutes", type=int, default=0, metavar="N",
39
+ help="stay awake N minutes then auto-off (0 = forever)")
40
+ p.add_argument("--install-desktop", action="store_true",
41
+ help="add the launcher + icon to your app menu (no root)")
42
+ p.add_argument("--autostart", action="store_true",
43
+ help="with --install-desktop, also start at login")
44
+ p.add_argument("--uninstall-desktop", action="store_true",
45
+ help="remove the launcher/icon/autostart entries")
46
+ args = p.parse_args(argv)
47
+
48
+ if args.uninstall_desktop:
49
+ from . import desktop
50
+ removed = desktop.uninstall()
51
+ print("Removed:" if removed else "Nothing to remove.")
52
+ for path in removed:
53
+ print(" -", path)
54
+ return 0
55
+
56
+ if args.install_desktop:
57
+ from . import desktop
58
+ written = desktop.install(autostart=args.autostart)
59
+ print("Installed:")
60
+ for path in written:
61
+ print(" +", path)
62
+ print("\nLook for \"Slow Coffee\" in your applications menu.")
63
+ return 0
64
+
65
+ _run_indicator(start_active=not args.start_inactive, minutes=args.minutes)
66
+ return 0
67
+
68
+
69
+ if __name__ == "__main__":
70
+ sys.exit(main())
@@ -0,0 +1,86 @@
1
+ """Self-integration: drop a .desktop entry + icon into the user's XDG dirs.
2
+
3
+ This is what makes ``slow-coffee`` appear in "installed applications" even from a
4
+ ``pip install --user`` - no root, no distro package required. Distro packages
5
+ (deb/AUR) install the same files system-wide via their own rules instead.
6
+ """
7
+
8
+ import os
9
+ import shutil
10
+ from os.path import join, dirname, abspath, exists
11
+
12
+ from .indicator import APP_ID
13
+
14
+ DESKTOP_BODY = """\
15
+ [Desktop Entry]
16
+ Type=Application
17
+ Name=Slow Coffee
18
+ GenericName=Keep Awake
19
+ Comment=Keep your screen awake - on by default, sip at your own pace
20
+ Exec=slow-coffee
21
+ Icon=slow-coffee
22
+ Terminal=false
23
+ Categories=Utility;GTK;
24
+ Keywords=caffeine;caffeinate;awake;idle;screensaver;inhibit;presentation;
25
+ StartupNotify=false
26
+ """
27
+
28
+ AUTOSTART_EXTRA = "X-GNOME-Autostart-enabled=true\n"
29
+
30
+
31
+ def _xdg(*parts):
32
+ base = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
33
+ return join(base, *parts)
34
+
35
+
36
+ def _config(*parts):
37
+ base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
38
+ return join(base, *parts)
39
+
40
+
41
+ def _src_icon():
42
+ return join(dirname(abspath(__file__)), "icons", "slow-coffee-full.png")
43
+
44
+
45
+ def install(autostart=False):
46
+ """Install the launcher + icon (+ optional autostart). Returns paths written."""
47
+ written = []
48
+
49
+ icon_dst = _xdg("icons", "hicolor", "128x128", "apps", "slow-coffee.png")
50
+ os.makedirs(dirname(icon_dst), exist_ok=True)
51
+ shutil.copyfile(_src_icon(), icon_dst)
52
+ written.append(icon_dst)
53
+
54
+ desktop_dst = _xdg("applications", "slow-coffee.desktop")
55
+ os.makedirs(dirname(desktop_dst), exist_ok=True)
56
+ with open(desktop_dst, "w") as f:
57
+ f.write(DESKTOP_BODY)
58
+ written.append(desktop_dst)
59
+
60
+ if autostart:
61
+ auto_dst = _config("autostart", "slow-coffee.desktop")
62
+ os.makedirs(dirname(auto_dst), exist_ok=True)
63
+ with open(auto_dst, "w") as f:
64
+ f.write(DESKTOP_BODY + AUTOSTART_EXTRA)
65
+ written.append(auto_dst)
66
+
67
+ # refresh the menu cache if the tool is around (best-effort)
68
+ try:
69
+ import subprocess
70
+ subprocess.run(["update-desktop-database", _xdg("applications")],
71
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
72
+ except (OSError, FileNotFoundError):
73
+ pass
74
+ return written
75
+
76
+
77
+ def uninstall():
78
+ """Remove everything install() created. Returns paths removed."""
79
+ removed = []
80
+ for path in (_xdg("icons", "hicolor", "128x128", "apps", "slow-coffee.png"),
81
+ _xdg("applications", "slow-coffee.desktop"),
82
+ _config("autostart", "slow-coffee.desktop")):
83
+ if exists(path):
84
+ os.remove(path)
85
+ removed.append(path)
86
+ return removed
@@ -0,0 +1,251 @@
1
+ """The tray indicator and the idle-inhibition backends.
2
+
3
+ Idle inhibition is requested over D-Bus, preferring the running session daemon
4
+ and falling back to logind:
5
+
6
+ 1. org.gnome.SessionManager.Inhibit (GNOME; X11 verified, Wayland expected)
7
+ 2. org.freedesktop.ScreenSaver.Inhibit (KDE / freedesktop)
8
+ 3. systemd-inhibit --what=idle (logind fallback for bare WMs)
9
+
10
+ Default flag is *idle only* (8): the screen stays on, but closing the lid still
11
+ suspends - safer for a laptop than blocking all sleep.
12
+
13
+ This is an independent implementation; see the README's "Prior art" note.
14
+ """
15
+
16
+ import os
17
+ import subprocess
18
+ from os.path import join, dirname, abspath, isdir
19
+
20
+ import gi
21
+ gi.require_version("Gtk", "3.0")
22
+ gi.require_version("AyatanaAppIndicator3", "0.1")
23
+ from gi.repository import Gtk, Gio, GLib, AyatanaAppIndicator3 as AppIndicator3
24
+
25
+ from . import __version__
26
+
27
+ APP_ID = "slow-coffee"
28
+ REASON = "User asked to stay awake (slow-coffee)"
29
+
30
+ # GNOME SessionManager inhibit flag: 8 = "session marked idle". We deliberately
31
+ # do NOT add 4 ("suspend") so lid-close still sleeps the laptop.
32
+ GSM_INHIBIT_IDLE = 8
33
+
34
+ # (label, minutes) for the duration radio group; 0 == indefinitely
35
+ DURATIONS = (("Indefinitely", 0), ("30 minutes", 30), ("1 hour", 60), ("2 hours", 120))
36
+
37
+
38
+ def find_icons_dir():
39
+ """Locate the cup icons, whether pip-installed or run from a checkout."""
40
+ here = dirname(abspath(__file__))
41
+ for path in (join(here, "icons"),
42
+ join("/usr/share", APP_ID, "icons"),
43
+ join(os.path.expanduser("~/.local/share"), APP_ID, "icons")):
44
+ if isdir(path):
45
+ return path
46
+ return here
47
+
48
+
49
+ class Inhibitor:
50
+ """Holds at most one idle inhibitor, trying backends in order of preference.
51
+
52
+ One persistent session-bus connection is kept for the whole run: the
53
+ freedesktop ScreenSaver inhibit is bound to the D-Bus sender, so dropping the
54
+ connection would silently release it.
55
+ """
56
+
57
+ def __init__(self):
58
+ self.backend = None # "gnome" | "screensaver" | "systemd"
59
+ self.cookie = None # for the D-Bus backends
60
+ self.proc = None # for the systemd-inhibit child
61
+ try:
62
+ self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
63
+ except GLib.Error:
64
+ self.bus = None
65
+
66
+ @property
67
+ def active(self):
68
+ return self.backend is not None
69
+
70
+ def _dbus_inhibit(self, name, path, iface, method, args, sig):
71
+ if self.bus is None:
72
+ return None
73
+ try:
74
+ res = self.bus.call_sync(
75
+ name, path, iface, method,
76
+ GLib.Variant(sig, args), GLib.VariantType("(u)"),
77
+ Gio.DBusCallFlags.NONE, 4000, None)
78
+ return res.unpack()[0]
79
+ except GLib.Error:
80
+ return None
81
+
82
+ def _dbus_uninhibit(self, name, path, iface, method, cookie):
83
+ try:
84
+ self.bus.call_sync(
85
+ name, path, iface, method,
86
+ GLib.Variant("(u)", (cookie,)), None,
87
+ Gio.DBusCallFlags.NONE, 4000, None)
88
+ except GLib.Error:
89
+ pass
90
+
91
+ def acquire(self):
92
+ if self.active:
93
+ return self.backend
94
+ cookie = self._dbus_inhibit(
95
+ "org.gnome.SessionManager", "/org/gnome/SessionManager",
96
+ "org.gnome.SessionManager", "Inhibit",
97
+ (APP_ID, 0, REASON, GSM_INHIBIT_IDLE), "(susu)")
98
+ if cookie is not None:
99
+ self.backend, self.cookie = "gnome", cookie
100
+ return self.backend
101
+ cookie = self._dbus_inhibit(
102
+ "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver",
103
+ "org.freedesktop.ScreenSaver", "Inhibit",
104
+ (APP_ID, REASON), "(ss)")
105
+ if cookie is not None:
106
+ self.backend, self.cookie = "screensaver", cookie
107
+ return self.backend
108
+ try:
109
+ self.proc = subprocess.Popen(
110
+ ["systemd-inhibit", "--what=idle", "--who=slow-coffee",
111
+ "--why=" + REASON, "--mode=block", "sleep", "infinity"],
112
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
113
+ self.backend = "systemd"
114
+ return self.backend
115
+ except (OSError, FileNotFoundError):
116
+ return None
117
+
118
+ def release(self):
119
+ if self.backend == "gnome":
120
+ self._dbus_uninhibit(
121
+ "org.gnome.SessionManager", "/org/gnome/SessionManager",
122
+ "org.gnome.SessionManager", "Uninhibit", self.cookie)
123
+ elif self.backend == "screensaver":
124
+ self._dbus_uninhibit(
125
+ "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver",
126
+ "org.freedesktop.ScreenSaver", "UnInhibit", self.cookie)
127
+ elif self.backend == "systemd" and self.proc is not None:
128
+ self.proc.terminate()
129
+ try:
130
+ self.proc.wait(timeout=2)
131
+ except subprocess.TimeoutExpired:
132
+ self.proc.kill()
133
+ self.backend = self.cookie = self.proc = None
134
+
135
+
136
+ class SlowCoffee:
137
+ def __init__(self, start_active=True, minutes=0):
138
+ self.inhibitor = Inhibitor()
139
+ self.timer_id = None
140
+ self.icons = find_icons_dir()
141
+
142
+ self.ind = AppIndicator3.Indicator.new_with_path(
143
+ APP_ID, "slow-coffee-empty",
144
+ AppIndicator3.IndicatorCategory.APPLICATION_STATUS, self.icons)
145
+ self.ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
146
+ self.ind.set_title("slow-coffee")
147
+ self.ind.set_menu(self._build_menu())
148
+
149
+ if start_active:
150
+ self.activate(minutes)
151
+ else:
152
+ self._refresh()
153
+
154
+ # --- menu -------------------------------------------------------------
155
+ def _build_menu(self):
156
+ menu = Gtk.Menu()
157
+
158
+ self.toggle_item = Gtk.MenuItem(label="Deactivate")
159
+ self.toggle_item.connect("activate", self.on_toggle)
160
+ menu.append(self.toggle_item)
161
+
162
+ menu.append(Gtk.SeparatorMenuItem())
163
+
164
+ self.duration_items = []
165
+ for label, mins in DURATIONS:
166
+ item = Gtk.RadioMenuItem(label=label)
167
+ if self.duration_items:
168
+ item.join_group(self.duration_items[0])
169
+ item.set_active(mins == 0)
170
+ item.connect("activate", self.on_pick_duration, mins)
171
+ self.duration_items.append(item)
172
+ menu.append(item)
173
+
174
+ menu.append(Gtk.SeparatorMenuItem())
175
+
176
+ about = Gtk.MenuItem(label="About slow-coffee")
177
+ about.connect("activate", self.on_about)
178
+ menu.append(about)
179
+
180
+ quit_item = Gtk.MenuItem(label="Quit")
181
+ quit_item.connect("activate", self.on_quit)
182
+ menu.append(quit_item)
183
+
184
+ menu.show_all()
185
+ return menu
186
+
187
+ # --- state ------------------------------------------------------------
188
+ def _clear_timer(self):
189
+ if self.timer_id is not None:
190
+ GLib.source_remove(self.timer_id)
191
+ self.timer_id = None
192
+
193
+ def activate(self, minutes=0):
194
+ backend = self.inhibitor.acquire()
195
+ self._clear_timer()
196
+ if minutes > 0:
197
+ self.timer_id = GLib.timeout_add_seconds(minutes * 60, self._expire)
198
+ self._refresh(backend)
199
+
200
+ def deactivate(self):
201
+ self._clear_timer()
202
+ self.inhibitor.release()
203
+ self._refresh()
204
+
205
+ def _expire(self):
206
+ self.timer_id = None
207
+ self.deactivate()
208
+ if self.duration_items:
209
+ self.duration_items[0].set_active(True)
210
+ return False # one-shot
211
+
212
+ def _refresh(self, backend=None):
213
+ on = self.inhibitor.active
214
+ self.ind.set_icon_full(
215
+ "slow-coffee-full" if on else "slow-coffee-empty",
216
+ "caffeinated" if on else "decaf")
217
+ self.toggle_item.set_label("Deactivate" if on else "Activate")
218
+ via = " via %s" % backend if backend else ""
219
+ self.ind.set_title("slow-coffee: %s%s" % ("awake" if on else "off", via))
220
+
221
+ # --- callbacks --------------------------------------------------------
222
+ def on_toggle(self, _item):
223
+ if self.inhibitor.active:
224
+ self.deactivate()
225
+ else:
226
+ self.activate(self._selected_minutes())
227
+
228
+ def on_pick_duration(self, item, minutes):
229
+ if item.get_active():
230
+ self.activate(minutes)
231
+
232
+ def _selected_minutes(self):
233
+ for item, (_label, mins) in zip(self.duration_items, DURATIONS):
234
+ if item.get_active():
235
+ return mins
236
+ return 0
237
+
238
+ def on_about(self, _item):
239
+ d = Gtk.AboutDialog()
240
+ d.set_program_name("slow-coffee")
241
+ d.set_version(__version__)
242
+ d.set_comments("A take-your-time keep-awake indicator.\n"
243
+ "On by default; sips D-Bus, not your battery.")
244
+ d.set_website("https://github.com/PLNech/slow-coffee")
245
+ d.set_license_type(Gtk.License.MIT_X11)
246
+ d.run()
247
+ d.destroy()
248
+
249
+ def on_quit(self, _item):
250
+ self.deactivate()
251
+ Gtk.main_quit()
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: slow-coffee
3
+ Version: 1.0.0
4
+ Summary: A take-your-time keep-awake indicator for your system tray - on by default.
5
+ Author: PLNech
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/PLNech/slow-coffee
8
+ Project-URL: Source, https://github.com/PLNech/slow-coffee
9
+ Project-URL: Issues, https://github.com/PLNech/slow-coffee/issues
10
+ Keywords: caffeine,caffeinate,keep-awake,idle,screensaver,tray,appindicator,gnome,wayland,presentation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: X11 Applications :: GTK
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Desktop Environment
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: PyGObject>=3.42
23
+ Dynamic: license-file
24
+
25
+ <div align="center">
26
+
27
+ # ☕ slow-coffee
28
+
29
+ **A take-your-time keep-awake indicator for your system tray. On by default.**
30
+
31
+ A small coffee cup lives in your top bar and stops the screen blanking or the
32
+ session going idle. The cup boots *full*, because the whole point is to stay
33
+ awake until you say otherwise. No daemon to babysit, no battery to drain - it
34
+ just holds a D-Bus idle inhibitor and gets out of the way.
35
+
36
+ [![CI](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml/badge.svg)](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/slow-coffee.svg)](https://pypi.org/project/slow-coffee/)
38
+ [![AUR](https://img.shields.io/aur/version/slow-coffee.svg)](https://aur.archlinux.org/packages/slow-coffee)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+
41
+ <img src="docs/menu.png" alt="slow-coffee tray menu" width="260">
42
+
43
+ </div>
44
+
45
+ ## Why another caffeine?
46
+
47
+ Most keep-awake tools assume you want a *trigger*: stay awake while a video
48
+ plays, while music plays, while an app is fullscreen. slow-coffee assumes the
49
+ opposite. You opened it because you want to stay awake **now**, so it starts
50
+ caffeinated and stays that way. One click (or a timer) to stop.
51
+
52
+ - **On by default.** Cup full at launch. `--start-inactive` if you disagree.
53
+ - **Honest icon.** Full = really inhibiting; empty = really not. No lying cup.
54
+ - **Idle-only by default.** The screen stays on, but closing the lid still
55
+ suspends - safer for a laptop than blocking all sleep.
56
+ - **Timers.** 30 min / 1 hour / 2 hours / indefinitely, from the menu or
57
+ `--minutes N`.
58
+ - **Self-contained.** Bundles its own icons; no theme or extra data package.
59
+
60
+ ## Install
61
+
62
+ slow-coffee needs the GObject introspection runtime (GTK 3 + Ayatana
63
+ AppIndicator). On a tray that hides AppIndicators (e.g. vanilla GNOME), enable
64
+ the *AppIndicator support* extension.
65
+
66
+ ### Arch (AUR)
67
+
68
+ ```sh
69
+ yay -S slow-coffee # or: paru -S slow-coffee
70
+ ```
71
+
72
+ ### Debian / Ubuntu (.deb)
73
+
74
+ Grab `slow-coffee_*.deb` from the [latest release](https://github.com/PLNech/slow-coffee/releases) and:
75
+
76
+ ```sh
77
+ sudo apt install ./slow-coffee_1.0.0_all.deb
78
+ ```
79
+
80
+ ### pip (PyPI)
81
+
82
+ The GIR libraries come from your distro, the rest from pip:
83
+
84
+ ```sh
85
+ # Debian/Ubuntu
86
+ sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1
87
+ # Fedora
88
+ sudo dnf install python3-gobject gtk3 libayatana-appindicator-gtk3
89
+
90
+ pip install --user slow-coffee
91
+ slow-coffee --install-desktop # add it to your applications menu (no root)
92
+ ```
93
+
94
+ ### From source
95
+
96
+ ```sh
97
+ git clone https://github.com/PLNech/slow-coffee
98
+ cd slow-coffee
99
+ sudo make install # PREFIX=/usr/local by default
100
+ ```
101
+
102
+ ## Usage
103
+
104
+ Run `slow-coffee` (or launch **Slow Coffee** from your applications menu). The
105
+ cup appears in the tray, already caffeinated. Click it for the menu:
106
+
107
+ | Item | Does |
108
+ |------|------|
109
+ | Activate / Deactivate | Toggle the inhibitor by hand |
110
+ | Indefinitely / 30 min / 1 h / 2 h | Pick a duration; it auto-stops when time is up |
111
+ | About / Quit | The usual |
112
+
113
+ ```sh
114
+ slow-coffee # start caffeinated, forever
115
+ slow-coffee --minutes 90 # caffeinated for 90 minutes, then off
116
+ slow-coffee --start-inactive # start decaf; toggle on from the tray
117
+ slow-coffee --install-desktop --autostart # menu entry + start at login
118
+ slow-coffee --uninstall-desktop # undo the above
119
+ ```
120
+
121
+ ## How it works
122
+
123
+ slow-coffee asks the session to not go idle, preferring whatever is actually
124
+ running and falling back gracefully:
125
+
126
+ 1. **`org.gnome.SessionManager.Inhibit`** (flag `8`, idle) - GNOME.
127
+ *X11 verified; GNOME-Wayland expected but untested by the author.*
128
+ 2. **`org.freedesktop.ScreenSaver.Inhibit`** - KDE and other freedesktop shells.
129
+ 3. **`systemd-inhibit --what=idle`** - logind fallback for bare window managers.
130
+
131
+ It holds one persistent session-bus connection and releases the inhibitor on
132
+ quit. It does **not** add the "suspend" flag, so lid-close still sleeps. Wayland
133
+ note: the idle-inhibit protocol is per-surface, so a global manual inhibitor is
134
+ shell-dependent; under GNOME the SessionManager call covers it, under wlroots
135
+ your mileage varies and the `systemd-inhibit` fallback handles sleep.
136
+
137
+ ## Prior art
138
+
139
+ This is an independent implementation, written from scratch, but it stands on
140
+ the shoulders of two lovely projects worth knowing:
141
+
142
+ - [caffeine](https://launchpad.net/caffeine) by Reuben Thomas - the classic
143
+ fullscreen-auto daemon + manual indicator (GPL-3, X11).
144
+ - [caffeine-ng](https://codeberg.org/WhyNotHugo/caffeine-ng) by Hugo Barrera -
145
+ the modern rewrite with presentation-mode and audio awareness.
146
+
147
+ slow-coffee differs by being *on by default*, using D-Bus inhibition (rather
148
+ than an unmapped X window), shipping as a single self-contained package, and
149
+ living under MIT.
150
+
151
+ ## Development
152
+
153
+ ```sh
154
+ python3 -m slow_coffee # run from a checkout (needs system PyGObject)
155
+ python3 make_icons.py # regenerate the cup icons (needs Pillow)
156
+ /usr/bin/python3 docs/render_menu.py docs/menu.png # re-render the screenshot
157
+ ```
158
+
159
+ ## License
160
+
161
+ [MIT](LICENSE) (c) 2026 PLNech.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ slow_coffee/__init__.py
5
+ slow_coffee/__main__.py
6
+ slow_coffee/desktop.py
7
+ slow_coffee/indicator.py
8
+ slow_coffee.egg-info/PKG-INFO
9
+ slow_coffee.egg-info/SOURCES.txt
10
+ slow_coffee.egg-info/dependency_links.txt
11
+ slow_coffee.egg-info/entry_points.txt
12
+ slow_coffee.egg-info/requires.txt
13
+ slow_coffee.egg-info/top_level.txt
14
+ slow_coffee/icons/slow-coffee-empty.png
15
+ slow_coffee/icons/slow-coffee-full.png
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ slow-coffee = slow_coffee.__main__:main
@@ -0,0 +1 @@
1
+ PyGObject>=3.42
@@ -0,0 +1 @@
1
+ slow_coffee