My lock-and-sleep
locks the system and puts in back to sleep after 10 seconds
of idle as long as it remains locked.
Locking the system before going to sleep ensures that the system never shows an unlocked desktop when waking up from sleep. If the screen locker fails to start for some reason, this fact becomes clear sooner rather than later.
Going back to sleep automatically prevents the system from remaining active if woken up by accident. For example, if someone (some person, or some cat) presses any key during the night the system wakes up. I don’t want it to remain awake indefinitely in such situations.
When the system wakes up, it won’t always reset the idle timer. If I press the space-bar when the system is asleep, this key-press event is “swallowed” by the firmware (which wakes up the systems) and never delivered to the operating system or the compositor. The idle timer continues running since it originally started, and the compositor never send an event indicating that the system has resumed from idle.
The script
This script also exists in my dotfiles repository.
#!/usr/bin/env python3
#
# Requires swaylock > 1.7.2.
import os
import sys
import subprocess
(r, w) = os.pipe()
os.set_inheritable(r, False)
swaylock = subprocess.Popen(["swaylock", "--ready-fd", str(w)], pass_fds=(w,))
os.close(w)
# Wait for swaylock to be ready.
with os.fdopen(r, "rb") as ready_pipe:
while True:
pipe_data = ready_pipe.read(1)
if pipe_data == b"":
print("fatal: swaylock closed fd before locking")
sys.exit(1)
if pipe_data == b"\n":
break
while True:
subprocess.call(["powerctl", "mem"]) # blocks until wakeup from sleep.
wayidle = subprocess.Popen(["wayidle", "--timeout", "10"])
(pid, returncode) = os.wait()
if pid == swaylock.pid:
wayidle.kill()
sys.exit(returncode)
if pid == wayidle.pid and returncode != 0:
print("fatal: wayidle exited non-zero")
sys.exit(returncode) # swaylock will continue running
Let’s go over it bit by bit.
swaylock
> 1.7.2
This script requires swaylock > 1.7.2
. It relies on swaylock’s readiness
notification, which indicates once swaylock
has finished locking
the screen.
With previous releases, there exists no trivial way to do this and wait
for swaylock
to exit (more on this below).
Running swaylock
First comes the initial block which runs swaylock
:
(r, w) = os.pipe()
os.set_inheritable(r, False)
swaylock = subprocess.Popen(["swaylock", "--ready-fd", str(w)], pass_fds=(w,))
os.close(w)
os.pipe()
creates a pipe, a pair of file descriptors, w
and r
. Any data
written to w
gets read via r
. After closing all the copies of w
, r
will
yield an EOF
(end of file).
The child process (swaylock
) inherits a copy of w
, and the Python script
keeps the original copy. If the Python script keeps its the copy of the w
descriptor open, then the r
one will never reach its end. The script
explicitly closes it with the os.close(w)
call.
When swaylock
writes to on one of these file descriptors (w
), this Python
script will read from the other one (r
).
swaylock
will then start up, and as soon as it locks the system, it will write
\n
into w
and then close it.
Waiting for the system to lock
The next section now becomes relevant:
with os.fdopen(r, "rb") as ready_pipe:
while True:
pipe_data = ready_pipe.read(1)
if pipe_data == b"":
print("fatal: swaylock closed fd before locking")
sys.exit(1)
if pipe_data == b"\n":
break
This reads from r
in an infinite loop (while True
). If swaylock
writes a
\n
, this indicates that locking the screen has finished. The loop breaks and
the program continues executing in the next section.
If swaylock
does not print a \n
, this indicates that it has exited before
locking the screen. If this happens, the script exits with an error. I avoid
putting the system to sleep in this case, since that would hide the fact that
something went wrong with the screen locker.
The main loop
Lastly, the “main loop”. Again, an infinite loop:
while True:
subprocess.call(["powerctl", "mem"]) # blocks until wakeup from sleep.
wayidle = subprocess.Popen(["wayidle", "--timeout", "10"])
(pid, returncode) = os.wait()
if pid == swaylock.pid:
wayidle.kill()
sys.exit(returncode)
if pid == wayidle.pid and returncode != 0:
print("fatal: wayidle exited non-zero")
sys.exit(returncode) # swaylock will continue running
This starts off by calling powerctl mem
, which puts the system to sleep. This
command exits after the system has resumed from sleep. This guarantees that the
next line of this script executes after the system has woken up again.
At this point, wayidle
runs. wayidle
sets an idle timer (for 10 seconds in
this case) and exits once the timer expires. I tried using swayidle
here, but
it didn’t quite fit, because swayidle
doesn’t know when the system wakes up
again, so it won’t run another timer and the system will remain awake.
The script will then wait for one of its children (wayidle
or swaylock
) to
exit.
If swaylock
exits first, then it has unlocked the system (or crashed). In this
case wayidle
gets explicitly kill
ed, and the script exits (with the status
code of swaylock
, to reflect any errors).
If wayidle
exits first and without any errors, this means that the system
reached the idle timeout. The loop starts over, executing powerctl mem
and
putting the system back to sleep.
If wayidle
exits first but with an error, then the script exits with the same
error. This shouldn’t happen, and implies an unexpected error with the idle
timeout. The systems stays awake; if it went to sleep again, then it could end
up in an irrecoverable sleep-loop.
Closing notes
I configured sway
to run this script whenever I tap the power button on my
computer:
bindsym --release XF86PowerOff exec lock-and-sleep
I don’t think I can make this script any simpler, and I welcome any feedback on it.
Feel free to re-use any bits or ideas to your liking.