diff options
-rw-r--r-- | pkgs/default.nix | 2 | ||||
-rw-r--r-- | pkgs/still-awake/default.nix | 38 | ||||
-rw-r--r-- | pkgs/still-awake/still_awake.py | 102 |
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)}") |