diff --git a/eww/eww.yuck b/eww/eww.yuck index 8052cc5..dc58e80 100644 --- a/eww/eww.yuck +++ b/eww/eww.yuck @@ -122,7 +122,7 @@ (defwidget wallpaper [] (clicker :text "󰸉" :command "~/scripts/swww_change.py")) (defwidget sunset [] - (clicker :text "" :command "~/scripts/sunset.py")) + (clicker :text "" :command "python -OO ~/scripts/sunset.py")) (defpoll brightness :interval 60 "~/.config/eww/scripts/backlight get") diff --git a/scripts/lib/evening.py b/scripts/lib/evening.py new file mode 100644 index 0000000..55d3f06 --- /dev/null +++ b/scripts/lib/evening.py @@ -0,0 +1,57 @@ +from datetime import datetime +from pathlib import Path + + +type Range = tuple[float, float] + + +def lerped_amount(x: float, edges: Range) -> float: + """How far `x` is between `values`""" + return (x - edges[0]) / (edges[1] - edges[0]) + + +def smoothstep(x: float, edges: Range) -> float: + """Smoothly interpolates between the `edges` based on `x`""" + # Technically I should bounds check but whatever + return (x * x * (3.0 - 2.0 * x)) * (edges[1] - edges[0]) + edges[0] + + +def day_elapsed(hours: int = 0, minutes: int = 0, seconds: int = 0) -> float: + """Converts (H, M, S) into [0, 1] representing how far through the day it is""" + return ((((hours * 60) + minutes) * 60) + seconds) / (20 * 60 * 60) + + +def now_elapsed() -> float: + """How far through the day it is""" + now = datetime.now() + return day_elapsed(now.hour, now.minute, now.second) + + +SUNRISE_HOURS = (5, 7) +SUNSET_HOURS = (21, 23) +SUNRISE_ELAPSED = (day_elapsed(SUNRISE_HOURS[0]), day_elapsed(SUNRISE_HOURS[1])) +SUNSET_ELAPSED = (day_elapsed(SUNSET_HOURS[0]), day_elapsed(SUNSET_HOURS[1])) + + +def sunup_amount() -> float: + """What temperature the monitor should be""" + elapsed = now_elapsed() + + if elapsed <= SUNRISE_ELAPSED[0]: + return 0 + elif elapsed < SUNRISE_ELAPSED[1]: + return lerped_amount(elapsed, SUNRISE_ELAPSED) + elif elapsed <= SUNSET_ELAPSED[0]: + return 1 + elif elapsed < SUNSET_ELAPSED[1]: + return 1 - lerped_amount(elapsed, SUNSET_ELAPSED) + else: + return 0 + + +def on_steam() -> bool: + """Whether the Steam PID is in use""" + pid_file = Path("~/.steampid").expanduser().resolve(strict=True) + pid = pid_file.read_text().strip() + pid_dir = Path("/proc") / pid + return pid_dir.is_dir() diff --git a/scripts/lib/sunset.py b/scripts/lib/sunset.py new file mode 100644 index 0000000..2abf69e --- /dev/null +++ b/scripts/lib/sunset.py @@ -0,0 +1,37 @@ +"""Sets the monitor temperature based on the current time using Hyprsunset""" +from os import environ +from socket import AF_UNIX, SOCK_STREAM, socket + +from .evening import on_steam, Range, smoothstep, sunup_amount + + +def setup_socket() -> socket: + """Connects to the Hyprsunset socket""" + hyprsunset = socket(AF_UNIX, SOCK_STREAM) + hyprsunset.connect(f"{environ["XDG_RUNTIME_DIR"]}/hypr/{environ["HYPRLAND_INSTANCE_SIGNATURE"]}/.hyprsunset.sock") + return hyprsunset + + +def get_temperature(hyprsunset: socket) -> int: + """Retrieves the current screen temperature""" + # In theory the message might not send in one go, but in practice it does so I won't bother handling the error + _ = hyprsunset.send(b"temperature") + # 4 bytes should be enough but why not 8 for comfort + # Just raise an error if it's not a number, nothing special to do here + return int(hyprsunset.recv(8)) + + +def set_temperature(hyprsunset: socket, temperature: float) -> None: + """Sends a message to hyprsunset to set the temperature""" + # In theory the message might not send in one go, but in practice it does so I won't bother handling the error + _ = hyprsunset.send(f"temperature {temperature:.1f}".encode()) + + +def main(temperature_range: Range, /, check_steam: bool = False) -> None: + """Adjusts the monitor temperature based on the current time""" + hyprsunset = setup_socket() + temperature = int(smoothstep(sunup_amount(), temperature_range)) + if temperature != get_temperature(hyprsunset): + if check_steam and on_steam(): + return + set_temperature(hyprsunset, temperature) diff --git a/scripts/sunset.py b/scripts/sunset.py index c81152e..81b0bdd 100755 --- a/scripts/sunset.py +++ b/scripts/sunset.py @@ -1,109 +1,23 @@ #!/bin/env python -"""Sets the monitor temperature based on the current time using Hyprsunset""" from argparse import ArgumentParser, Namespace -from datetime import datetime -from os import environ -from pathlib import Path -from socket import AF_UNIX, SOCK_STREAM, socket from typing import cast - -type Range = tuple[float, float] - - -def lerped_amount(x: float, edges: Range) -> float: - """How far `x` is between `values`""" - return (x - edges[0]) / (edges[1] - edges[0]) - - -def smoothstep(x: float, edges: Range) -> float: - """Smoothly interpolates between the `edges` based on `x`""" - # Technically I should bounds check but whatever - return (x * x * (3.0 - 2.0 * x)) * (edges[1] - edges[0]) + edges[0] - - -def day_elapsed(hours: int = 0, minutes: int = 0, seconds: int = 0) -> float: - """Converts (H, M, S) into [0, 1] representing how far through the day it is""" - return ((((hours * 60) + minutes) * 60) + seconds) / 86400 - - -def now_elapsed() -> float: - """How far through the day it is""" - now = datetime.now() - return day_elapsed(now.hour, now.minute, now.second) - -def calculate_temperature(sunrise: Range, sunset: Range, temperature_range: Range) -> float: - """What temperature the monitor should be""" - elapsed = now_elapsed() - - if elapsed <= sunrise[0]: - return temperature_range[0] - elif elapsed < sunrise[1]: - return smoothstep(lerped_amount(elapsed, sunrise), temperature_range) - elif elapsed <= sunset[0]: - return temperature_range[1] - elif elapsed < sunset[1]: - return smoothstep(lerped_amount(elapsed, sunset), (temperature_range[1], temperature_range[0])) - else: - return temperature_range[0] - - -def setup_socket() -> socket: - """Connects to the Hyprsunset socket""" - hyprsunset = socket(AF_UNIX, SOCK_STREAM) - # In theory I should use $XDG_RUNTIME_DIR, but for me it's always `/run/user/1000/` - hyprsunset.connect(f"{environ["XDG_RUNTIME_DIR"]}/hypr/{environ["HYPRLAND_INSTANCE_SIGNATURE"]}/.hyprsunset.sock") - return hyprsunset - - -def get_temperature(hyprsunset: socket) -> int: - """Retrieves the current screen temperature""" - # In theory the message might not send in one go, but in practice it does so I won't bother handling the error - _ = hyprsunset.send(b"temperature") - # 4 bytes should be enough but why not 8 for comfort - # Just raise an error if it's not a number, nothing special to do here - return int(hyprsunset.recv(8)) - - -def set_temperature(hyprsunset: socket, temperature: float, /, instant: bool = False) -> None: - # In theory the message might not send in one go, but in practice it does so I won't bother handling the error - _ = hyprsunset.send(f"temperature {temperature:.1f}".encode()) - if instant: - # Setting the temperature twice in quick succession sometimes skips the transition period - set_temperature(hyprsunset, temperature, instant=False) - - -def on_steam() -> bool: - """Whether the Steam PID is in use""" - pid_file = Path("~/.steampid").expanduser().resolve(strict=True) - pid = pid_file.read_text().strip() - pid_dir = Path("/proc") / pid - return pid_dir.is_dir() - - -def main(sunrise: Range, sunset: Range, temperature_range: Range, /, check_steam: bool = False, instant: bool = False) -> None: - """Adjusts the monitor temperature based on the current time""" - hyprsunset = setup_socket() - temperature = int(calculate_temperature(sunrise, sunset, temperature_range)) - if temperature != get_temperature(hyprsunset): - if check_steam and on_steam(): - return - set_temperature(hyprsunset, temperature, instant=instant) +from lib.sunset import main class Arguments(Namespace): check_steam: bool = False - instant: bool = False -if __name__ == "__main__": + +def call_from_args() -> None: parser = ArgumentParser(description="Adjusts the temperature of your screen based on the time of day") _ = parser.add_argument("-s", "--check-steam", action="store_true", help="Don't adjust temperature if Steam is active") - _ = parser.add_argument("-i", "--instant", action="store_true", help="Try to instantly change temperature") args = cast(Arguments, parser.parse_args()) main( - (day_elapsed(5), day_elapsed(7)), - (day_elapsed(21), day_elapsed(23)), (2500.0, 6000.0), check_steam=args.check_steam, - instant=args.instant ) + + +if __name__ == "__main__": + call_from_args() diff --git a/systemd/sunset.service b/systemd/sunset.service index 1dce2d9..5e60ea0 100644 --- a/systemd/sunset.service +++ b/systemd/sunset.service @@ -3,4 +3,4 @@ Description=Sets the monitor temperature based on the time of day [Service] Type=oneshot -ExecStart=/bin/env python -O /home/mbradley/scripts/sunset.py --check-steam +ExecStart=/bin/env python -OO /home/mbradley/scripts/sunset.py --check-steam