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.
- donotsleep-0.3.0/LICENSE +21 -0
- donotsleep-0.3.0/PKG-INFO +140 -0
- donotsleep-0.3.0/README.md +108 -0
- donotsleep-0.3.0/pyproject.toml +52 -0
- donotsleep-0.3.0/pyproject.toml.orig +45 -0
- donotsleep-0.3.0/src/donotsleep/__init__.py +5 -0
- donotsleep-0.3.0/src/donotsleep/__main__.py +4 -0
- donotsleep-0.3.0/src/donotsleep/donotsleep.py +398 -0
donotsleep-0.3.0/LICENSE
ADDED
|
@@ -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,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.")
|