summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pkgs/default.nix2
-rw-r--r--pkgs/still-awake/default.nix38
-rw-r--r--pkgs/still-awake/still_awake.py102
3 files changed, 142 insertions, 0 deletions
diff --git a/pkgs/default.nix b/pkgs/default.nix
index b84e5b4..998aef3 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -3,6 +3,8 @@ pkgs:
{
# duksebot = pkgs.callPackage ./duksebot { };
+ still-awake = pkgs.callPackage ./still-awake { };
+
# Use patched version from Karl.
smu = (pkgs.smu.overrideAttrs (old: {
version = "2022-08-01";
diff --git a/pkgs/still-awake/default.nix b/pkgs/still-awake/default.nix
new file mode 100644
index 0000000..a83dd63
--- /dev/null
+++ b/pkgs/still-awake/default.nix
@@ -0,0 +1,38 @@
+{ stdenv, pypy3, lib }:
+
+let
+ # Needs python interpreter with tkinter support.
+ python3' = pypy3;
+in
+stdenv.mkDerivation {
+ pname = "still-awake";
+ version = "10-09-2023";
+
+ src = builtins.readFile ./still_awake.py;
+ passAsFile = [ "buildCommand" "src" ];
+
+ # Building basically boils down to writing source to a file
+ # and making it executable.
+ buildCommand = ''
+ mkdir -p $out/bin
+
+ echo "#!${python3'.interpreter}" >$out/bin/still-awake
+
+ if [ -e "$srcPath" ]; then
+ cat "$srcPath" >>$out/bin/still-awake
+ else
+ echo -n "$src" >>$out/bin/still-awake
+ fi
+
+ chmod +x $out/bin/still-awake
+ '';
+
+ # It doesn't make sense to do this remotely.
+ preferLocalBuild = true;
+ allowSubstitute = false;
+
+ meta = with lib; {
+ description = "Small program which shuts down Mac, if user is asleep";
+ platforms = platforms.darwin;
+ };
+}
diff --git a/pkgs/still-awake/still_awake.py b/pkgs/still-awake/still_awake.py
new file mode 100644
index 0000000..b5d1008
--- /dev/null
+++ b/pkgs/still-awake/still_awake.py
@@ -0,0 +1,102 @@
+from tkinter import *
+from typing import *
+from os import system
+from time import asctime, localtime
+
+def position(root: Tk):
+ """
+ Position the window at the top center of the screen.
+
+ Must be called after all widgets have been added.
+ """
+
+ # Apparently a common hack to get the window size. Temporarily hide the
+ # window to avoid update_idletasks() drawing the window in the wrong
+ # position.
+ root.withdraw()
+ root.update_idletasks() # Update "requested size" from geometry manager
+
+ x = (root.winfo_screenwidth() - root.winfo_reqwidth()) / 2
+ y = (root.winfo_screenheight() - root.winfo_reqheight()) * 0.2
+ root.geometry("+%d+%d" % (x, y))
+
+ # Raise this window and give it focus
+ root.deiconify()
+
+def countdown(root: Tk, label: StringVar, counter: int, on_end: Callable):
+ if counter > 0:
+ content = str(counter) + (" second" if counter == 1 else " seconds")
+ label.set(content)
+ root.after(1000, countdown, root, label, counter - 1, on_end)
+ else:
+ on_end()
+
+def sleep(root: Tk):
+ """Send the device into hibernation mode."""
+
+ root.destroy()
+
+ print("Shutting down device...")
+
+ # NOTE: This will ask whether python3 should be allowed to control system
+ # events on first invocation. Give it a test run, when you first
+ # install it.
+ # system("/usr/bin/osascript -e 'tell app \"System Events\" to sleep'")
+ # system("shutdown -s now")
+ system("pmset sleepnow")
+
+def dont_sleep(root: Tk):
+ """End the application run loop."""
+
+ print("User is still awake. Ending application without shutdown.")
+ root.destroy()
+
+def main():
+ root = Tk()
+ root.title("Still awake?")
+ root.attributes("-topmost", True)
+ root.resizable(width=False, height=False)
+
+ top_frame = Frame(root, relief=RIDGE, borderwidth=0)
+ top_frame.pack(padx=10, pady=(10, 5), expand=1)
+
+ label = Label(top_frame, text="Are you still awake? If not, the device will go to sleep in...")
+ label.pack(side=LEFT)
+
+ counter = StringVar()
+ counter_label = Label(top_frame, textvariable=counter)
+ counter_label.pack(side=LEFT)
+ countdown(root, counter, 30, on_end=lambda: sleep(root))
+
+ bottom_frame = Frame(root, relief=RIDGE, borderwidth=0)
+ bottom_frame.pack(padx=10, pady=(5, 10), expand=1)
+
+ button = Button(bottom_frame, text="Yes, I'm awake!", command=lambda: dont_sleep(root))
+ button.pack(side=BOTTOM)
+
+ position(root)
+ root.mainloop()
+
+ print("Finished window loop. End of run.")
+
+if __name__ == "__main__":
+ # According to the Daemons and Services Programming Guide:
+ #
+ # If you schedule a launchd job by setting the StartCalendarInterval
+ # key and the computer is asleep when the job should have run, your job
+ # will run when the computer wakes up. However, if the machine is off
+ # when the job should have run, the job does not execute until the next
+ # designated time occurs.
+ #
+ # This is not the behavior we want, so we explicitly check that we are
+ # being run in the allowed time span. The definition of "allowed time span"
+ # must match that of the job definition in `default.nix`.
+ # FIXME: If you turn the computer on at, say, 22:15 the window pops up.
+ # That happens becuase we are within the allowed timspan but it's
+ # not the behavior I want.
+ now = localtime()
+ if now.tm_hour <= 5 or now.tm_hour >= 21:
+ print(f"Starting at {asctime(now)}.")
+ main()
+ else:
+ print(f"Skipping, as invoked outside allowed hours: {asctime(now)}")