dontsleep 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +75 -0
- data/Rakefile +4 -0
- data/exe/dontsleep +42 -0
- data/lib/dontsleep/version.rb +5 -0
- data/lib/dontsleep.rb +85 -0
- data/test/test_dontsleep.rb +87 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 80681d16f63de71323f7c06ccaa0ac303281a92f6328e0bdd0ec1365249aba39
|
|
4
|
+
data.tar.gz: cce2043955e99caf1b6da08f29cc7547c250febc89368df3b6b9207b620e45a5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 687a3a4adb246381321e6649145039c773e01044d1cd53452c88d12c4470c9fe4c7d96ae7b410452c63daf227177d1d6a4a72d54641a6c50fd86a44071e2293d
|
|
7
|
+
data.tar.gz: dd61512c3bbc18301ae3500dc020bd9c2bf4ebdc70d6dbf953ffd6b45b2f6ba80bdf30c29c79d1243663fb2959982d7eeeefb3946196415c5b340a11e17925c4
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andreas Freier
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# dontsleep
|
|
2
|
+
|
|
3
|
+
Keep your Mac awake by moving the mouse a couple of pixels.
|
|
4
|
+
|
|
5
|
+
macOS decides you're "idle" when the mouse and keyboard have been still for a
|
|
6
|
+
while — and then it sleeps, killing your SSH sessions, your builds, your
|
|
7
|
+
downloads, and whatever else was patiently running. `dontsleep` prevents that
|
|
8
|
+
by nudging the cursor **2 pixels** now and then: invisible to you, convincing
|
|
9
|
+
to the operating system.
|
|
10
|
+
|
|
11
|
+
> **macOS only.** The gem depends on `rumouse`, which drives the mouse through
|
|
12
|
+
> macOS' Core Graphics events. On other platforms it installs fine but refuses
|
|
13
|
+
> to run, with a clear message. (Linux/Windows support: open an issue or send
|
|
14
|
+
> a patch — we'd love that.)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Gemfile
|
|
20
|
+
gem "dontsleep"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or simply:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
gem install dontsleep
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires macOS and a Ruby ≥ 2.5 with the `ffi` gem (pulled in automatically).
|
|
30
|
+
You will need to grant your terminal **Accessibility** permission under
|
|
31
|
+
*System Settings → Privacy & Security → Accessibility* — that's macOS asking
|
|
32
|
+
nicely that an app may move the mouse. It asks once.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
The executable does the job:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
dontsleep # 2px nudge every 60s, runs until you press Ctrl-C
|
|
40
|
+
dontsleep --minutes 120 # …and stops itself after 2 hours
|
|
41
|
+
dontsleep --interval 30 # more frequent, smaller heartbeats
|
|
42
|
+
dontsleep --wander # old-school mode: teleport to a random spot —
|
|
43
|
+
# for unattended machines you're not touching
|
|
44
|
+
dontsleep --quiet # no heartbeat dots
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
From Ruby:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "dontsleep"
|
|
51
|
+
Dontsleep.start(minutes: 60) # e.g. wrapped around a long build
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A `.` is printed with each nudge, so you can see it's alive. Ctrl-C always
|
|
55
|
+
stops it cleanly.
|
|
56
|
+
|
|
57
|
+
## Why a nudge and not a wander?
|
|
58
|
+
|
|
59
|
+
The original idea — move the mouse to a random position — works, but every
|
|
60
|
+
minute it *yanks your cursor away* from wherever you're actually clicking.
|
|
61
|
+
A 2-pixel ping-pong (right, left, down, up…) resets the idle timer while
|
|
62
|
+
staying out of your way. `--wander` keeps the classic behavior for machines
|
|
63
|
+
you've left alone.
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
bundle exec rake # run tests
|
|
69
|
+
bundle exec rake build # build the gem
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
[MIT](LICENSE.txt) — do whatever you like, no warranty, as with all
|
|
75
|
+
small utilities.
|
data/Rakefile
ADDED
data/exe/dontsleep
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dontsleep"
|
|
4
|
+
|
|
5
|
+
if Dontsleep.on_mac?
|
|
6
|
+
args = ARGV.dup
|
|
7
|
+
interval = 60
|
|
8
|
+
minutes = nil
|
|
9
|
+
wander = false
|
|
10
|
+
nudge = 2
|
|
11
|
+
quiet = false
|
|
12
|
+
|
|
13
|
+
while (a = args.shift)
|
|
14
|
+
case a
|
|
15
|
+
when "--interval" then interval = args.shift.to_f
|
|
16
|
+
when "--minutes" then minutes = args.shift.to_i
|
|
17
|
+
when "--nudge" then nudge = args.shift.to_i
|
|
18
|
+
when "--wander" then wander = true
|
|
19
|
+
when "--quiet" then quiet = true
|
|
20
|
+
when "-h", "--help"
|
|
21
|
+
puts <<~HELP
|
|
22
|
+
dontsleep — keep your Mac awake by moving the mouse a little.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--interval N seconds between moves (default 60)
|
|
26
|
+
--minutes N stop automatically after N minutes (default: run until Ctrl-C)
|
|
27
|
+
--nudge N pixels to move (default 2)
|
|
28
|
+
--wander teleport to a random spot instead (for unattended machines)
|
|
29
|
+
--quiet no heartbeat output
|
|
30
|
+
HELP
|
|
31
|
+
exit 0
|
|
32
|
+
else
|
|
33
|
+
warn "unknown option: #{a} (see --help)"
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Dontsleep.start(interval: interval, nudge: nudge, minutes: minutes, wander: wander, quiet: quiet)
|
|
39
|
+
else
|
|
40
|
+
warn "dontsleep only works on macOS (this is #{RbConfig::CONFIG["host_os"]})."
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
data/lib/dontsleep.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "dontsleep/version"
|
|
5
|
+
|
|
6
|
+
# rumouse (and its ffi dependency) only exists where it can be built —
|
|
7
|
+
# effectively macOS. Keep the require lenient so the gem *installs and loads*
|
|
8
|
+
# anywhere; on other platforms `Dontsleep.start` (and the executable) refuse
|
|
9
|
+
# with a clear message before the mouse is ever needed.
|
|
10
|
+
begin
|
|
11
|
+
if RbConfig::CONFIG["host_os"] =~ /darwin/
|
|
12
|
+
require "rumouse"
|
|
13
|
+
else
|
|
14
|
+
# Don't even try to load rumouse off-macOS: on Linux it probes X11
|
|
15
|
+
# libraries and dies with an ugly stack trace. Refuse cleanly instead.
|
|
16
|
+
module RuMouse
|
|
17
|
+
def initialize
|
|
18
|
+
raise Dontsleep::Error,
|
|
19
|
+
"rumouse could not be loaded — dontsleep needs macOS (and the ffi gem)"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
rescue LoadError
|
|
24
|
+
module RuMouse
|
|
25
|
+
def initialize
|
|
26
|
+
raise Dontsleep::Error,
|
|
27
|
+
"rumouse could not be loaded — dontsleep needs macOS (and the ffi gem)"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Dontsleep keeps a macOS machine awake by nudging the cursor a couple of
|
|
33
|
+
# pixels now and then, so the system never considers you idle — while your
|
|
34
|
+
# SSH sessions, builds, and downloads keep going through your breaks.
|
|
35
|
+
#
|
|
36
|
+
# Dontsleep.start # 2px ping-pong nudge, every 60s, until Ctrl-C
|
|
37
|
+
# Dontsleep.start(minutes: 120) # stop automatically after 120 minutes
|
|
38
|
+
# Dontsleep.start(wander: true) # old-school: teleport to a random spot (unattended boxes)
|
|
39
|
+
module Dontsleep
|
|
40
|
+
class Error < StandardError; end
|
|
41
|
+
|
|
42
|
+
def self.start(interval: 60, nudge: 2, minutes: nil, wander: false, quiet: false)
|
|
43
|
+
raise Error, "dontsleep only supports macOS" unless on_mac?
|
|
44
|
+
|
|
45
|
+
mouse = build_mouse
|
|
46
|
+
deadline = minutes ? Time.now + minutes * 60 : nil
|
|
47
|
+
positive = true
|
|
48
|
+
|
|
49
|
+
until (deadline && Time.now > deadline)
|
|
50
|
+
real_sleep(interval)
|
|
51
|
+
move_mouse(mouse, nudge: nudge, wander: wander, positive: positive)
|
|
52
|
+
positive = !positive # +2, −2, +2, −2 … on one fixed axis: pure ping-pong,
|
|
53
|
+
# zero drift — the cursor jitters in place instead of walking away
|
|
54
|
+
print(".") { $stdout.flush } unless quiet
|
|
55
|
+
end
|
|
56
|
+
puts
|
|
57
|
+
"stopped (auto-stop)"
|
|
58
|
+
rescue Interrupt
|
|
59
|
+
puts
|
|
60
|
+
"stopped (goodbye)"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.on_mac?
|
|
64
|
+
RbConfig::CONFIG["host_os"] =~ /darwin/ ? true : false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Indirection points so tests can run the loop on a fake mouse / fake clock.
|
|
68
|
+
def self.build_mouse
|
|
69
|
+
RuMouse.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.real_sleep(seconds)
|
|
73
|
+
sleep(seconds)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.move_mouse(mouse, nudge:, wander:, positive:)
|
|
77
|
+
if wander
|
|
78
|
+
mouse.move(100 + rand(200), 100 + rand(200))
|
|
79
|
+
else
|
|
80
|
+
pos = mouse.position
|
|
81
|
+
delta = positive ? nudge : -nudge
|
|
82
|
+
mouse.move(pos[:x] + delta, pos[:y]) # fixed axis (x): the twitch is invisible
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "dontsleep/version"
|
|
6
|
+
require "dontsleep"
|
|
7
|
+
|
|
8
|
+
# A mouse double: records moves, reports its current position.
|
|
9
|
+
class FakeMouse
|
|
10
|
+
attr_reader :moves
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@pos = { x: 500.0, y: 300.0 }
|
|
14
|
+
@moves = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def position
|
|
18
|
+
@pos
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def move(x, y)
|
|
22
|
+
@pos = { x: x, y: y }
|
|
23
|
+
@moves << { x: x, y: y }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run N ticks of the *real* move_mouse decision logic against a fake mouse.
|
|
28
|
+
# Mirrors Dontsleep.start's loop: the sign flips every tick on one fixed axis.
|
|
29
|
+
def drive_ticks(mouse, n, wander: false, nudge: 2)
|
|
30
|
+
positive = true
|
|
31
|
+
n.times do
|
|
32
|
+
Dontsleep.move_mouse(mouse, nudge: nudge, wander: wander, positive: positive)
|
|
33
|
+
positive = !positive
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
class DontsleepTest < Minitest::Test
|
|
37
|
+
def test_nudge_cancels_out_over_many_ticks
|
|
38
|
+
mouse = FakeMouse.new
|
|
39
|
+
start = { x: 500.0, y: 300.0 }
|
|
40
|
+
drive_ticks(mouse, 1000) # ~16 hours of ticking
|
|
41
|
+
assert_in_delta start[:x], mouse.position[:x], 2.0 # at most one step off
|
|
42
|
+
assert_equal start[:y], mouse.position[:y] # y never moves at all
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_wander_stays_in_its_rectangle
|
|
46
|
+
mouse = FakeMouse.new
|
|
47
|
+
drive_ticks(mouse, 50, wander: true)
|
|
48
|
+
mouse.moves.each do |m|
|
|
49
|
+
assert_operator m[:x], :>=, 100
|
|
50
|
+
assert_operator m[:x], :<=, 300
|
|
51
|
+
assert_operator m[:y], :>=, 100
|
|
52
|
+
assert_operator m[:y], :<=, 300
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_platform_guard_is_a_boolean
|
|
57
|
+
assert [true, false].include?(Dontsleep.on_mac?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Exercise the real loop with a fake mouse and no waiting.
|
|
61
|
+
def test_start_loop_auto_stops
|
|
62
|
+
fake = FakeMouse.new
|
|
63
|
+
Dontsleep.define_singleton_method(:build_mouse) { fake }
|
|
64
|
+
Dontsleep.define_singleton_method(:real_sleep) { |_s| }
|
|
65
|
+
Dontsleep.define_singleton_method(:on_mac?) { true }
|
|
66
|
+
|
|
67
|
+
result = Dontsleep.start(interval: 1, minutes: 0.001, quiet: true)
|
|
68
|
+
|
|
69
|
+
assert_equal "stopped (auto-stop)", result
|
|
70
|
+
assert_operator fake.moves.length, :>=, 1
|
|
71
|
+
ensure
|
|
72
|
+
# restore the real implementations for any later tests / runs
|
|
73
|
+
Dontsleep.singleton_class.class_eval <<~RUBY
|
|
74
|
+
def self.build_mouse
|
|
75
|
+
RuMouse.new
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.real_sleep(seconds)
|
|
79
|
+
sleep(seconds)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.on_mac?
|
|
83
|
+
RbConfig::CONFIG["host_os"] =~ /darwin/ ? true : false
|
|
84
|
+
end
|
|
85
|
+
RUBY
|
|
86
|
+
end
|
|
87
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dontsleep
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andreas Freier
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ffi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rumouse
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.0.7
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.0.7
|
|
41
|
+
description: Prevents macOS sleep — and the broken SSH sessions, builds and downloads
|
|
42
|
+
that come with it — by nudging the cursor. macOS only.
|
|
43
|
+
email:
|
|
44
|
+
- andreas.freier@googlemail.com
|
|
45
|
+
executables:
|
|
46
|
+
- dontsleep
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- Rakefile
|
|
53
|
+
- exe/dontsleep
|
|
54
|
+
- lib/dontsleep.rb
|
|
55
|
+
- lib/dontsleep/version.rb
|
|
56
|
+
- test/test_dontsleep.rb
|
|
57
|
+
homepage: https://github.com/andreasfreier/dontsleep
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
allowed_push_host: https://rubygems.org
|
|
62
|
+
homepage_uri: https://github.com/andreasfreier/dontsleep
|
|
63
|
+
source_code_uri: https://github.com/andreasfreier/dontsleep
|
|
64
|
+
post_install_message:
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 2.5.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 3.3.15
|
|
80
|
+
signing_key:
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Keep your Mac awake by moving the mouse a couple of pixels.
|
|
83
|
+
test_files: []
|