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.
- slow_coffee-1.0.0/LICENSE +21 -0
- slow_coffee-1.0.0/PKG-INFO +161 -0
- slow_coffee-1.0.0/README.md +137 -0
- slow_coffee-1.0.0/pyproject.toml +44 -0
- slow_coffee-1.0.0/setup.cfg +4 -0
- slow_coffee-1.0.0/slow_coffee/__init__.py +3 -0
- slow_coffee-1.0.0/slow_coffee/__main__.py +70 -0
- slow_coffee-1.0.0/slow_coffee/desktop.py +86 -0
- slow_coffee-1.0.0/slow_coffee/icons/slow-coffee-empty.png +0 -0
- slow_coffee-1.0.0/slow_coffee/icons/slow-coffee-full.png +0 -0
- slow_coffee-1.0.0/slow_coffee/indicator.py +251 -0
- slow_coffee-1.0.0/slow_coffee.egg-info/PKG-INFO +161 -0
- slow_coffee-1.0.0/slow_coffee.egg-info/SOURCES.txt +15 -0
- slow_coffee-1.0.0/slow_coffee.egg-info/dependency_links.txt +1 -0
- slow_coffee-1.0.0/slow_coffee.egg-info/entry_points.txt +2 -0
- slow_coffee-1.0.0/slow_coffee.egg-info/requires.txt +1 -0
- slow_coffee-1.0.0/slow_coffee.egg-info/top_level.txt +1 -0
|
@@ -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
|
+
[](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/slow-coffee/)
|
|
38
|
+
[](https://aur.archlinux.org/packages/slow-coffee)
|
|
39
|
+
[](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
|
+
[](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml)
|
|
13
|
+
[](https://pypi.org/project/slow-coffee/)
|
|
14
|
+
[](https://aur.archlinux.org/packages/slow-coffee)
|
|
15
|
+
[](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,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
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
[](https://github.com/PLNech/slow-coffee/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/slow-coffee/)
|
|
38
|
+
[](https://aur.archlinux.org/packages/slow-coffee)
|
|
39
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyGObject>=3.42
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
slow_coffee
|