donotsleep 0.3.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 Bipin Subedi
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,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: donotsleep
3
+ Version: 0.3.0
4
+ Summary: Prevent your computer from sleeping by simulating safe media key activity.
5
+ Keywords: sleep,automation,desktop,utility,productivity
6
+ Author: Bipin Subedi
7
+ Author-email: Bipin Subedi <contact@bipinsubedi.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Desktop Environment
24
+ Classifier: Topic :: Utilities
25
+ Requires-Dist: customtkinter>=5.2.0
26
+ Requires-Dist: pyautogui>=0.9.54
27
+ Requires-Python: >=3.8
28
+ Project-URL: Homepage, https://github.com/bipinsubedi/donotsleep
29
+ Project-URL: Repository, https://github.com/bipinsubedi/donotsleep
30
+ Project-URL: Issues, https://github.com/bipinsubedi/donotsleep/issues
31
+ Description-Content-Type: text/markdown
32
+
33
+ # donotsleep
34
+
35
+ `donotsleep` is a lightweight desktop utility that helps keep your system awake by sending periodic media key events.
36
+
37
+ It opens a simple GUI where you can:
38
+
39
+ - Set the activity interval
40
+ - Add randomized timing variation
41
+ - Start/stop behavior immediately
42
+
43
+ ## Why This Exists
44
+
45
+ Some workloads (long downloads, streaming dashboards, remote sessions, monitoring windows) are interrupted when the host device sleeps. This tool provides a minimal way to keep activity alive while you are away.
46
+
47
+ ## Features
48
+
49
+ - GUI-first usage with instant start
50
+ - Optional CLI interval argument
51
+ - Randomized interval variation to reduce strict timing patterns
52
+ - Background launch on both Windows and Linux (terminal can be closed after start)
53
+ - Native Linux sleep inhibition (`systemd-inhibit` and `xdg-screensaver`) supporting both Wayland and X11 out of the box
54
+ - Zero extra configuration or OS-specific settings required
55
+
56
+ ## Requirements
57
+
58
+ - Python 3.8+
59
+ - Windows or Linux (supports Ubuntu, Fedora, Debian, Arch, etc. under Wayland & X11)
60
+
61
+ ## Installation & Running
62
+
63
+ ### Run directly with `uvx` (no install needed)
64
+
65
+ ```bash
66
+ uvx donotsleep
67
+ ```
68
+
69
+ ### From PyPI
70
+
71
+ ```bash
72
+ pip install donotsleep
73
+ ```
74
+
75
+ ### Local Development
76
+
77
+ ```bash
78
+ uv sync
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ ### Start With Defaults (Detached in Background)
84
+
85
+ ```bash
86
+ donotsleep
87
+ ```
88
+
89
+ Or with `uvx`:
90
+
91
+ ```bash
92
+ uvx donotsleep
93
+ ```
94
+
95
+ ### Start With Custom Interval
96
+
97
+ ```bash
98
+ donotsleep 120
99
+ ```
100
+
101
+ ### Run In Foreground (debugging)
102
+
103
+ ```bash
104
+ donotsleep --no-detach
105
+ ```
106
+
107
+ ## Behavior Notes
108
+
109
+ - Default interval is `100` seconds.
110
+ - Interval range is `10-300` seconds.
111
+ - Randomization range is `0-20%`.
112
+ - **Windows**: Simulates periodic `volumedown` and `volumeup` key events.
113
+ - **Linux**: Automatically engages native system sleep inhibition (`systemd-inhibit`, `xdg-screensaver`) to prevent idle timeout, screen lock, and system sleep across both Wayland and X11 desktop sessions without requiring manual permissions. Also sends simulated media keys when supported.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ uv sync
119
+ uv run donotsleep 100 --no-detach
120
+ ```
121
+
122
+ ## Build
123
+
124
+ ```bash
125
+ uv build
126
+ ```
127
+
128
+ ## Publish (PyPI)
129
+
130
+ ```bash
131
+ uv publish
132
+ ```
133
+
134
+ ## Security and Responsibility
135
+
136
+ Use this tool only on systems you own or are authorized to manage. Some managed environments may restrict synthetic input automation.
137
+
138
+ ## License
139
+
140
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,108 @@
1
+ # donotsleep
2
+
3
+ `donotsleep` is a lightweight desktop utility that helps keep your system awake by sending periodic media key events.
4
+
5
+ It opens a simple GUI where you can:
6
+
7
+ - Set the activity interval
8
+ - Add randomized timing variation
9
+ - Start/stop behavior immediately
10
+
11
+ ## Why This Exists
12
+
13
+ Some workloads (long downloads, streaming dashboards, remote sessions, monitoring windows) are interrupted when the host device sleeps. This tool provides a minimal way to keep activity alive while you are away.
14
+
15
+ ## Features
16
+
17
+ - GUI-first usage with instant start
18
+ - Optional CLI interval argument
19
+ - Randomized interval variation to reduce strict timing patterns
20
+ - Background launch on both Windows and Linux (terminal can be closed after start)
21
+ - Native Linux sleep inhibition (`systemd-inhibit` and `xdg-screensaver`) supporting both Wayland and X11 out of the box
22
+ - Zero extra configuration or OS-specific settings required
23
+
24
+ ## Requirements
25
+
26
+ - Python 3.8+
27
+ - Windows or Linux (supports Ubuntu, Fedora, Debian, Arch, etc. under Wayland & X11)
28
+
29
+ ## Installation & Running
30
+
31
+ ### Run directly with `uvx` (no install needed)
32
+
33
+ ```bash
34
+ uvx donotsleep
35
+ ```
36
+
37
+ ### From PyPI
38
+
39
+ ```bash
40
+ pip install donotsleep
41
+ ```
42
+
43
+ ### Local Development
44
+
45
+ ```bash
46
+ uv sync
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Start With Defaults (Detached in Background)
52
+
53
+ ```bash
54
+ donotsleep
55
+ ```
56
+
57
+ Or with `uvx`:
58
+
59
+ ```bash
60
+ uvx donotsleep
61
+ ```
62
+
63
+ ### Start With Custom Interval
64
+
65
+ ```bash
66
+ donotsleep 120
67
+ ```
68
+
69
+ ### Run In Foreground (debugging)
70
+
71
+ ```bash
72
+ donotsleep --no-detach
73
+ ```
74
+
75
+ ## Behavior Notes
76
+
77
+ - Default interval is `100` seconds.
78
+ - Interval range is `10-300` seconds.
79
+ - Randomization range is `0-20%`.
80
+ - **Windows**: Simulates periodic `volumedown` and `volumeup` key events.
81
+ - **Linux**: Automatically engages native system sleep inhibition (`systemd-inhibit`, `xdg-screensaver`) to prevent idle timeout, screen lock, and system sleep across both Wayland and X11 desktop sessions without requiring manual permissions. Also sends simulated media keys when supported.
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ uv sync
87
+ uv run donotsleep 100 --no-detach
88
+ ```
89
+
90
+ ## Build
91
+
92
+ ```bash
93
+ uv build
94
+ ```
95
+
96
+ ## Publish (PyPI)
97
+
98
+ ```bash
99
+ uv publish
100
+ ```
101
+
102
+ ## Security and Responsibility
103
+
104
+ Use this tool only on systems you own or are authorized to manage. Some managed environments may restrict synthetic input automation.
105
+
106
+ ## License
107
+
108
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "donotsleep"
3
+ version = "0.3.0"
4
+ description = "Prevent your computer from sleeping by simulating safe media key activity."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "sleep",
10
+ "automation",
11
+ "desktop",
12
+ "utility",
13
+ "productivity",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "Operating System :: Microsoft :: Windows",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Desktop Environment",
30
+ "Topic :: Utilities",
31
+ ]
32
+ requires-python = ">=3.8"
33
+ dependencies = [
34
+ "customtkinter>=5.2.0",
35
+ "pyautogui>=0.9.54",
36
+ ]
37
+
38
+ [[project.authors]]
39
+ name = "Bipin Subedi"
40
+ email = "contact@bipinsubedi.com"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/bipinsubedi/donotsleep"
44
+ Repository = "https://github.com/bipinsubedi/donotsleep"
45
+ Issues = "https://github.com/bipinsubedi/donotsleep/issues"
46
+
47
+ [project.scripts]
48
+ donotsleep = "donotsleep.donotsleep:main"
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.11.28,<0.15"]
52
+ build-backend = "uv_build"
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "donotsleep"
3
+ version = "0.3.0"
4
+ description = "Prevent your computer from sleeping by simulating safe media key activity."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Bipin Subedi", email = "contact@bipinsubedi.com" }
10
+ ]
11
+ keywords = ["sleep", "automation", "desktop", "utility", "productivity"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: End Users/Desktop",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Desktop Environment",
27
+ "Topic :: Utilities",
28
+ ]
29
+ requires-python = ">=3.8"
30
+ dependencies = [
31
+ "customtkinter>=5.2.0",
32
+ "pyautogui>=0.9.54",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/bipinsubedi/donotsleep"
37
+ Repository = "https://github.com/bipinsubedi/donotsleep"
38
+ Issues = "https://github.com/bipinsubedi/donotsleep/issues"
39
+
40
+ [project.scripts]
41
+ donotsleep = "donotsleep.donotsleep:main"
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.11.28,<0.15"]
45
+ build-backend = "uv_build"
@@ -0,0 +1,5 @@
1
+ from .donotsleep import main, run_ui
2
+
3
+ __version__ = "0.3.0"
4
+
5
+ __all__ = ["run_ui", "main", "__version__"]
@@ -0,0 +1,4 @@
1
+ from donotsleep.donotsleep import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,398 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import atexit
5
+ import ctypes
6
+ import multiprocessing
7
+ import os
8
+ import random
9
+ import shutil
10
+ import signal
11
+ import subprocess
12
+ import sys
13
+ import threading
14
+ import time
15
+ from pathlib import Path
16
+ from tkinter import messagebox
17
+
18
+ import customtkinter as ctk
19
+
20
+ try:
21
+ import pyautogui
22
+
23
+ pyautogui.FAILSAFE = False
24
+ except Exception:
25
+ pyautogui = None
26
+
27
+ # Constants
28
+ DEFAULT_INTERVAL = 100
29
+ MIN_INTERVAL = 10
30
+ MAX_INTERVAL = 300
31
+ MAX_VARIATION = 20
32
+
33
+
34
+ def _set_pdeathsig() -> None:
35
+ """Ensure child process terminates when parent process dies."""
36
+ try:
37
+ libc = ctypes.CDLL("libc.so.6")
38
+ PR_SET_PDEATHSIG = 1
39
+ libc.prctl(PR_SET_PDEATHSIG, signal.SIGTERM)
40
+ except Exception:
41
+ pass
42
+
43
+
44
+ class LinuxSleepInhibitor:
45
+ """Manages system sleep inhibition on Linux via systemd-inhibit."""
46
+
47
+ def __init__(self) -> None:
48
+ self.process: subprocess.Popen | None = None
49
+ atexit.register(self.stop)
50
+
51
+ def start(self) -> None:
52
+ if sys.platform != "linux":
53
+ return
54
+ inhibit_bin = shutil.which("systemd-inhibit")
55
+ if not inhibit_bin:
56
+ return
57
+
58
+ for what in ("idle:sleep:handle-lid-switch", "idle:sleep"):
59
+ try:
60
+ proc = subprocess.Popen(
61
+ [
62
+ inhibit_bin,
63
+ f"--what={what}",
64
+ "--who=Do Not Sleep",
65
+ "--why=Preventing sleep",
66
+ "--mode=block",
67
+ sys.executable,
68
+ "-c",
69
+ "import time; time.sleep(10**8)",
70
+ ],
71
+ stdout=subprocess.DEVNULL,
72
+ stderr=subprocess.DEVNULL,
73
+ preexec_fn=_set_pdeathsig,
74
+ )
75
+ time.sleep(0.1)
76
+ if proc.poll() is None:
77
+ self.process = proc
78
+ return
79
+ except Exception:
80
+ pass
81
+ self.process = None
82
+
83
+ def stop(self) -> None:
84
+ if self.process:
85
+ if self.process.poll() is None:
86
+ try:
87
+ self.process.terminate()
88
+ self.process.wait(timeout=1)
89
+ except Exception:
90
+ try:
91
+ self.process.kill()
92
+ except Exception:
93
+ pass
94
+ self.process = None
95
+
96
+
97
+ def _simulate_activity_down() -> None:
98
+ """Simulate keypress or reset screensaver for volumedown."""
99
+ if sys.platform == "linux" and shutil.which("xdg-screensaver"):
100
+ try:
101
+ subprocess.run(["xdg-screensaver", "reset"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
102
+ except Exception:
103
+ pass
104
+
105
+ if pyautogui is not None:
106
+ try:
107
+ pyautogui.press("volumedown")
108
+ except Exception:
109
+ pass
110
+
111
+
112
+ def _simulate_activity_up() -> None:
113
+ """Simulate keypress or reset screensaver for volumeup."""
114
+ if sys.platform == "linux" and shutil.which("xdg-screensaver"):
115
+ try:
116
+ subprocess.run(["xdg-screensaver", "reset"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
117
+ except Exception:
118
+ pass
119
+
120
+ if pyautogui is not None:
121
+ try:
122
+ pyautogui.press("volumeup")
123
+ except Exception:
124
+ pass
125
+
126
+
127
+ def run_ui(interval: int = DEFAULT_INTERVAL) -> None:
128
+ """Main GUI function for Do Not Sleep settings and control."""
129
+ ctk.set_appearance_mode("dark")
130
+ ctk.set_default_color_theme("blue")
131
+
132
+ try:
133
+ app = ctk.CTk()
134
+ except Exception as e:
135
+ print(f"Error: Unable to initialize graphical display: {e}", file=sys.stderr)
136
+ return
137
+
138
+ app.title("Do Not Sleep")
139
+ app.geometry("500x320")
140
+ app.resizable(False, False)
141
+
142
+ process = None
143
+ worker_thread = None
144
+ stop_event = None
145
+ linux_inhibitor = None
146
+ initial_interval = max(MIN_INTERVAL, min(MAX_INTERVAL, int(interval)))
147
+
148
+ status_var = ctk.StringVar(value="STOPPED")
149
+ interval_var = ctk.DoubleVar(value=initial_interval)
150
+ variation_var = ctk.DoubleVar(value=10)
151
+ interval_display_var = ctk.StringVar(value=f"{initial_interval} s")
152
+ variation_display_var = ctk.StringVar(value="10%")
153
+
154
+ def terminate_worker() -> None:
155
+ nonlocal process, worker_thread, stop_event, linux_inhibitor
156
+ if sys.platform == "win32":
157
+ if process and process.is_alive():
158
+ process.terminate()
159
+ process.join(timeout=1)
160
+ process = None
161
+ else:
162
+ if stop_event:
163
+ stop_event.set()
164
+ if linux_inhibitor:
165
+ linux_inhibitor.stop()
166
+ linux_inhibitor = None
167
+ if worker_thread and worker_thread.is_alive():
168
+ worker_thread.join(timeout=1)
169
+ worker_thread = None
170
+ stop_event = None
171
+
172
+ def update_interval_display(value: float) -> None:
173
+ interval_display_var.set(f"{int(round(value))} s")
174
+
175
+ def update_variation_display(value: float) -> None:
176
+ variation_display_var.set(f"{int(round(value))}%")
177
+
178
+ def set_running_ui(running: bool) -> None:
179
+ if running:
180
+ status_var.set("RUNNING")
181
+ status_badge.configure(fg_color="#14532D", text_color="#D1FAE5")
182
+ content.configure(border_color="#22C55E")
183
+ primary_button.configure(text="Apply Settings")
184
+ else:
185
+ status_var.set("STOPPED")
186
+ status_badge.configure(fg_color="#7F1D1D", text_color="#FEE2E2")
187
+ content.configure(border_color="#EF4444")
188
+ primary_button.configure(text="Start")
189
+
190
+ def start_or_apply() -> None:
191
+ nonlocal process, worker_thread, stop_event, linux_inhibitor
192
+ new_interval = int(round(interval_var.get()))
193
+ variation_percent = int(round(variation_var.get()))
194
+
195
+ if new_interval < MIN_INTERVAL or new_interval > MAX_INTERVAL:
196
+ messagebox.showerror("Invalid Interval", f"Interval must be between {MIN_INTERVAL} and {MAX_INTERVAL} seconds.")
197
+ return
198
+ if variation_percent < 0 or variation_percent > MAX_VARIATION:
199
+ messagebox.showerror("Invalid Variation", f"Variation must be between 0 and {MAX_VARIATION}%.")
200
+ return
201
+
202
+ # Restart worker with the latest settings so slider changes can be applied at any time.
203
+ terminate_worker()
204
+ if sys.platform == "win32":
205
+ process = multiprocessing.Process(target=prevent_sleep, args=(new_interval, variation_percent))
206
+ process.start()
207
+ else:
208
+ stop_event = threading.Event()
209
+ linux_inhibitor = LinuxSleepInhibitor()
210
+ linux_inhibitor.start()
211
+ worker_thread = threading.Thread(
212
+ target=prevent_sleep,
213
+ args=(new_interval, variation_percent, stop_event),
214
+ daemon=True,
215
+ )
216
+ worker_thread.start()
217
+ set_running_ui(True)
218
+
219
+ def on_stop() -> None:
220
+ terminate_worker()
221
+ set_running_ui(False)
222
+
223
+ def on_close() -> None:
224
+ on_stop()
225
+ app.destroy()
226
+
227
+ app.protocol("WM_DELETE_WINDOW", on_close)
228
+
229
+ app.grid_columnconfigure(0, weight=1)
230
+
231
+ content = ctk.CTkFrame(app, border_width=2, border_color="#EF4444")
232
+ content.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
233
+ content.grid_columnconfigure(1, weight=1)
234
+
235
+ ctk.CTkLabel(content, text="Do Not Sleep", font=ctk.CTkFont(size=22, weight="bold")).grid(
236
+ row=0, column=0, columnspan=3, pady=(10, 16)
237
+ )
238
+
239
+ ctk.CTkLabel(content, text="Interval (seconds):").grid(row=1, column=0, padx=(12, 8), pady=8, sticky="w")
240
+ ctk.CTkSlider(
241
+ content,
242
+ from_=MIN_INTERVAL,
243
+ to=MAX_INTERVAL,
244
+ number_of_steps=MAX_INTERVAL - MIN_INTERVAL,
245
+ variable=interval_var,
246
+ command=update_interval_display,
247
+ ).grid(row=1, column=1, padx=8, pady=8, sticky="ew")
248
+ ctk.CTkLabel(content, textvariable=interval_display_var, width=50).grid(row=1, column=2, padx=(8, 12), pady=8)
249
+
250
+ ctk.CTkLabel(content, text="Randomization (%):").grid(row=2, column=0, padx=(12, 8), pady=8, sticky="w")
251
+ ctk.CTkSlider(
252
+ content,
253
+ from_=0,
254
+ to=MAX_VARIATION,
255
+ number_of_steps=MAX_VARIATION,
256
+ variable=variation_var,
257
+ command=update_variation_display,
258
+ ).grid(row=2, column=1, padx=8, pady=8, sticky="ew")
259
+ ctk.CTkLabel(content, textvariable=variation_display_var, width=50).grid(row=2, column=2, padx=(8, 12), pady=8)
260
+
261
+ buttons = ctk.CTkFrame(content, fg_color="transparent")
262
+ buttons.grid(row=3, column=0, columnspan=3, pady=(14, 8))
263
+ primary_button = ctk.CTkButton(buttons, text="Start", width=150, command=start_or_apply)
264
+ primary_button.grid(row=0, column=0, padx=8)
265
+ ctk.CTkButton(buttons, text="Stop", width=120, fg_color="#991B1B", hover_color="#7F1D1D", command=on_stop).grid(row=0, column=1, padx=8)
266
+
267
+ status_badge = ctk.CTkLabel(
268
+ content,
269
+ textvariable=status_var,
270
+ anchor="center",
271
+ corner_radius=10,
272
+ fg_color="#7F1D1D",
273
+ text_color="#FEE2E2",
274
+ width=160,
275
+ height=34,
276
+ font=ctk.CTkFont(size=14, weight="bold"),
277
+ )
278
+ status_badge.grid(row=4, column=0, columnspan=3, pady=(8, 14))
279
+
280
+ # Start immediately when the app opens using the current/default slider values.
281
+ start_or_apply()
282
+
283
+ app.mainloop()
284
+
285
+
286
+ def prevent_sleep(
287
+ interval: int,
288
+ variation_percent: int,
289
+ stop_event: threading.Event | None = None,
290
+ ) -> None:
291
+ """Simulate activity by pressing volume keys and inhibiting sleep at randomized intervals."""
292
+ # If invoked directly (not via run_ui on Linux), manage inhibitor here
293
+ inhibitor = LinuxSleepInhibitor() if (sys.platform == "linux" and stop_event is None) else None
294
+ if inhibitor:
295
+ inhibitor.start()
296
+
297
+ try:
298
+ while True:
299
+ if stop_event and stop_event.is_set():
300
+ break
301
+
302
+ _simulate_activity_down()
303
+
304
+ if variation_percent > 0:
305
+ sleep_time = random.uniform(interval * (1 - variation_percent / 100), interval * (1 + variation_percent / 100))
306
+ else:
307
+ sleep_time = interval
308
+
309
+ if stop_event:
310
+ if stop_event.wait(sleep_time):
311
+ break
312
+ else:
313
+ time.sleep(sleep_time)
314
+
315
+ if stop_event and stop_event.is_set():
316
+ break
317
+
318
+ _simulate_activity_up()
319
+
320
+ if variation_percent > 0:
321
+ sleep_time = random.uniform(interval * (1 - variation_percent / 100), interval * (1 + variation_percent / 100))
322
+ else:
323
+ sleep_time = interval
324
+
325
+ if stop_event:
326
+ if stop_event.wait(sleep_time):
327
+ break
328
+ else:
329
+ time.sleep(sleep_time)
330
+ finally:
331
+ if inhibitor:
332
+ inhibitor.stop()
333
+
334
+
335
+ def main() -> None:
336
+ """Entry point for the Do Not Sleep CLI tool."""
337
+ parser = argparse.ArgumentParser(
338
+ description="Do Not Sleep - Prevent sleep by pressing volume keys with randomized intervals (mutes volume to avoid buzzing)"
339
+ )
340
+ parser.add_argument(
341
+ "interval",
342
+ type=int,
343
+ nargs="?",
344
+ default=DEFAULT_INTERVAL,
345
+ help=f"Approximate interval in seconds between key presses (default: {DEFAULT_INTERVAL})",
346
+ )
347
+ parser.add_argument(
348
+ "--no-detach",
349
+ action="store_true",
350
+ help="Run GUI in the current process (useful for debugging).",
351
+ )
352
+ args = parser.parse_args()
353
+
354
+ if args.no_detach:
355
+ run_ui(args.interval)
356
+ return
357
+
358
+ # On Windows, run using pythonw in a detached process so the terminal can be closed.
359
+ if sys.platform == "win32":
360
+ python_path = Path(sys.executable)
361
+ pythonw_path = python_path.with_name("pythonw.exe")
362
+ python_exe = str(pythonw_path if pythonw_path.exists() else python_path)
363
+ creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
364
+ subprocess.Popen(
365
+ [python_exe, "-c", f"from donotsleep.donotsleep import run_ui; run_ui({args.interval})"],
366
+ creationflags=creationflags,
367
+ stdout=subprocess.DEVNULL,
368
+ stderr=subprocess.DEVNULL,
369
+ )
370
+ print("Do Not Sleep GUI started in background. It begins running immediately.")
371
+ return
372
+
373
+ # Non-Windows (Linux / macOS): start detached where possible.
374
+ env = os.environ.copy()
375
+ package_root = str(Path(__file__).resolve().parent.parent)
376
+ cur_pythonpath = env.get("PYTHONPATH", "")
377
+ if cur_pythonpath:
378
+ env["PYTHONPATH"] = f"{package_root}{os.pathsep}{cur_pythonpath}"
379
+ else:
380
+ env["PYTHONPATH"] = package_root
381
+
382
+ proc = subprocess.Popen(
383
+ [sys.executable, "-c", f"from donotsleep.donotsleep import run_ui; run_ui({args.interval})"],
384
+ stdout=subprocess.DEVNULL,
385
+ stderr=subprocess.DEVNULL,
386
+ start_new_session=True,
387
+ env=env,
388
+ )
389
+ try:
390
+ proc.wait(timeout=0.2)
391
+ if proc.returncode != 0:
392
+ print("Failed to start Do Not Sleep GUI in background. Running with --no-detach for diagnostic output:")
393
+ run_ui(args.interval)
394
+ return
395
+ except subprocess.TimeoutExpired:
396
+ pass
397
+
398
+ print("Do Not Sleep GUI started in background. It begins running immediately.")