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 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontsleep
4
+ VERSION = "0.1.0"
5
+ 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: []