Compare commits
5 commits
9a03bb684b
...
97295856a3
Author | SHA1 | Date | |
---|---|---|---|
97295856a3 | |||
8bee11d5fb | |||
c1c8a83ec5 | |||
310b879416 | |||
ff9b012217 |
27 changed files with 410 additions and 176 deletions
|
@ -19,15 +19,17 @@ discord = { source = "config/electron-flags.conf", target = ".config/discord-fla
|
|||
1password = { source = "autostart/1password.desktop", target = ".config/autostart/1password.desktop", direct = true }
|
||||
megasync = { source = "autostart/megasync.desktop", target = ".config/autostart/megasync.desktop", direct = true }
|
||||
|
||||
# Also needs a one-time `systemctl --user enable --now eww-daemon.service eww-bars.service ip-geolocation.service sunset.timer swww-daemon.service'
|
||||
# Also needs a one-time `systemctl --user enable --now eww-daemon.service eww-bars.service ip-geolocation.service sunset.timer swww-daemon.service wallpaper.timer'
|
||||
eww-daemon = { source = "systemd/eww-daemon.service", target = ".config/systemd/user/eww-daemon.service", direct = true }
|
||||
eww-bars = { source = "systemd/eww-bars.service", target = ".config/systemd/user/eww-bars.service" }
|
||||
ip-geolocation = { source = "systemd/ip-geolocation.service", target = ".config/systemd/user/ip-geolocation.service", direct = true }
|
||||
sunset-service = { source = "systemd/sunset.service", target = ".config/systemd/user/sunset.service", direct = true }
|
||||
sunset-timer = { source = "systemd/sunset.timer", target = ".config/systemd/user/sunset.timer", direct = true }
|
||||
swww-daemon = { source = "systemd/swww-daemon.service", target = ".config/systemd/user/swww-daemon.service", direct = true }
|
||||
wallpaper-service = { source = "systemd/wallpaper.service", target = ".config/systemd/user/wallpaper.service", direct = true }
|
||||
wallpaper-timer = { source = "systemd/wallpaper.timer", target = ".config/systemd/user/wallpaper.timer", direct = true }
|
||||
|
||||
btop = { source = "btop", target = ".config/btop", direct = true }
|
||||
btop = { source = "btop", target = ".config/btop" }
|
||||
dunst = { source = "dunst", target = ".config/dunst", direct = true }
|
||||
eww = { source = "eww", target = ".config/eww" }
|
||||
fastfetch = { source = "fastfetch", target = ".config/fastfetch", direct = true }
|
||||
|
|
|
@ -50,7 +50,11 @@ graph_symbol_net = "default"
|
|||
graph_symbol_proc = "default"
|
||||
|
||||
#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
|
||||
{% if host.name == "chonk" %}
|
||||
shown_boxes = "proc cpu mem net gpu0"
|
||||
{% else %}
|
||||
shown_boxes = "cpu mem net proc"
|
||||
{% endif %}
|
||||
|
||||
#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
|
||||
update_ms = 1500
|
||||
|
|
27
eww/eww.yuck
27
eww/eww.yuck
|
@ -120,9 +120,9 @@
|
|||
;(defwidget colour_selector []
|
||||
;(clicker :text "" :command "uwsm-app -- hyprpicker -a"))
|
||||
(defwidget wallpaper []
|
||||
(clicker :text "" :command "~/scripts/swww_change.py"))
|
||||
(clicker :text "" :command "~/.config/eww/scripts/wallpaper-shell"))
|
||||
(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")
|
||||
|
@ -234,4 +234,25 @@
|
|||
:exclusive true
|
||||
:focusable false
|
||||
(laptop_bar))
|
||||
|
||||
(defwindow laptop_left
|
||||
:monitor 1
|
||||
:geometry (geometry :x "0px"
|
||||
:y "4px"
|
||||
:width "2552px"
|
||||
:height "24px"
|
||||
:anchor "top center")
|
||||
:stacking "fg"
|
||||
:exclusive true
|
||||
:focusable false
|
||||
(laptop_bar))
|
||||
(defwindow laptop_right
|
||||
:monitor 2
|
||||
:geometry (geometry :x "0px"
|
||||
:y "4px"
|
||||
:width "1912px"
|
||||
:height "24px"
|
||||
:anchor "top center")
|
||||
:stacking "fg"
|
||||
:exclusive true
|
||||
:focusable false
|
||||
(laptop_bar))
|
||||
|
|
|
@ -10,4 +10,3 @@ case "$1" in
|
|||
*) exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
|
@ -12,4 +12,3 @@ case "$1" in
|
|||
*) echo "Unrecognized command"; exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
|
@ -70,4 +70,3 @@ pactl subscribe | while read -r LINE; do
|
|||
get_ids
|
||||
fi
|
||||
done
|
||||
|
||||
|
|
|
@ -22,4 +22,3 @@ case "$1" in
|
|||
*) echo "Device name '$1' not recognized"; exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
|
@ -17,4 +17,3 @@ charging() {
|
|||
}
|
||||
|
||||
echo "{\"charge\":$CHARGE,\"icon\":\"$CHARGE_ICON\",\"charging\":$(charging)}"
|
||||
|
||||
|
|
|
@ -115,4 +115,3 @@ if __name__ == "__main__":
|
|||
if len(argv) != 2:
|
||||
exit("Must provide 1 arg: Polling interval")
|
||||
main(float(argv[1]))
|
||||
|
||||
|
|
|
@ -2,4 +2,3 @@
|
|||
|
||||
# EWW doesn't seem to like listening to Python scripts directly, but this wrapper seems to work fine
|
||||
"$(dirname "$0")"/network-statistics $@
|
||||
|
||||
|
|
|
@ -8,4 +8,3 @@ if [ "$(pactl get-default-sink)" = "${SPEAKERS}" ] ; then
|
|||
else
|
||||
pactl set-default-sink "${SPEAKERS}"
|
||||
fi
|
||||
|
||||
|
|
|
@ -8,4 +8,3 @@ if [ "$(pactl get-default-source)" = "${BLUE_MIC}" ] ; then
|
|||
else
|
||||
pactl set-default-source "${BLUE_MIC}"
|
||||
fi
|
||||
|
||||
|
|
4
eww/scripts/wallpaper-shell
Executable file
4
eww/scripts/wallpaper-shell
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/env sh
|
||||
|
||||
# EWW doesn't seem to like listening to Python scripts directly, but this wrapper seems to work fine
|
||||
python -OO ~/scripts/wallpaper.py
|
|
@ -1,5 +1,6 @@
|
|||
monitor = DP-3, 1920x1080@60, -4480x0, 1
|
||||
monitor = DP-2, 2560x1440@100, -2560x0, 1
|
||||
monitor = eDP-1, 2880x1920@120, 0x0, 1.5
|
||||
monitor = DP-2, 1920x1080@60, -1920x0, 1
|
||||
|
||||
decoration {
|
||||
blur {
|
||||
|
@ -31,6 +32,13 @@ gestures {
|
|||
workspace_swipe_cancel_ratio = 0.1
|
||||
}
|
||||
|
||||
workspace = 1, monitor:eDP-1, default:true
|
||||
workspace = 2, monitor:eDP-1,
|
||||
workspace = 3, monitor:eDP-1,
|
||||
workspace = 1, monitor:DP-3, default:true
|
||||
workspace = 2, monitor:DP-2, default:true
|
||||
workspace = 3, monitor:eDP-1, default:true
|
||||
workspace = 4, monitor:DP-3
|
||||
workspace = 5, monitor:DP-2
|
||||
workspace = 6, monitor:eDP-1
|
||||
workspace = 7, monitor:DP-3
|
||||
workspace = 8, monitor:DP-2
|
||||
workspace = 9, monitor:eDP-1
|
||||
workspace = 10, monitor:DP-3
|
||||
|
|
|
@ -30,7 +30,7 @@ listener {
|
|||
}
|
||||
|
||||
listener {
|
||||
timeout = 330
|
||||
timeout = 600
|
||||
on-timeout = systemctl suspend
|
||||
}
|
||||
{% endif %}
|
||||
|
|
|
@ -74,6 +74,11 @@ device {
|
|||
#sensitivity = -0.9157894737 # Rough calc for max DPI - issues exist
|
||||
}
|
||||
|
||||
device {
|
||||
name = logitech-usb-receiver-mouse
|
||||
sensitivity = 0.0
|
||||
}
|
||||
|
||||
windowrulev2 = suppressevent maximize, class:.*
|
||||
windowrulev2 = opacity 1.0 0.9, class:.*
|
||||
|
||||
|
|
57
scripts/lib/evening.py
Normal file
57
scripts/lib/evening.py
Normal file
|
@ -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()
|
37
scripts/lib/sunset.py
Normal file
37
scripts/lib/sunset.py
Normal file
|
@ -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)
|
|
@ -1,45 +0,0 @@
|
|||
"""Basic functions for interacting with swww"""
|
||||
from json import loads
|
||||
from pathlib import Path
|
||||
from random import sample
|
||||
from shlex import split
|
||||
from socket import gethostname
|
||||
from subprocess import run
|
||||
|
||||
|
||||
IS_DESKTOP = gethostname() == "chonk"
|
||||
|
||||
MONITORS = ("DP-1", "DP-2") if IS_DESKTOP else ("eDP-1",)
|
||||
|
||||
ANGLE_TRANSITION_ARGS = [
|
||||
"--transition-type", "wipe",
|
||||
"--transition-angle", "30",
|
||||
"--transition-step", "45",
|
||||
"--transition-fps", "165" if IS_DESKTOP else "120",
|
||||
]
|
||||
IMMEDIATE_TRANSITION_ARGS = [
|
||||
"--transition-step", "255",
|
||||
]
|
||||
|
||||
|
||||
def _run_cmd(arg0: str, *args: str) -> str:
|
||||
return run([arg0, *args] if args else split(arg0), capture_output=True).stdout.decode("utf-8")
|
||||
|
||||
|
||||
def monitors() -> list[str]:
|
||||
return [monitor["name"] for monitor in loads(_run_cmd("hyprctl monitors -j"))]
|
||||
|
||||
|
||||
def run_swww(monitor: str, image: Path, extra_args: list[str] | None = None) -> None:
|
||||
run(["swww", "img", "-o", monitor, image, *(extra_args or [])])
|
||||
|
||||
|
||||
def get_wallpapers() -> list[Path]:
|
||||
return list(Path("~/Pictures/wallpapers/").expanduser().glob("*"))
|
||||
|
||||
|
||||
def sample_wallpapers(displays: list[str] | None = None) -> "zip[tuple[str, Path]]":
|
||||
if displays is None:
|
||||
displays = monitors()
|
||||
return zip(displays, sample(get_wallpapers(), len(displays)))
|
||||
|
216
scripts/lib/wallpaper.py
Normal file
216
scripts/lib/wallpaper.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
"""Script to update the wallpaper"""
|
||||
from abc import ABC, abstractmethod
|
||||
from json import dumps, loads
|
||||
from math import ceil, floor, isnan, nan
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from random import sample
|
||||
from re import Pattern, compile, match
|
||||
from subprocess import run
|
||||
from time import sleep
|
||||
from typing import cast, override
|
||||
|
||||
from .evening import on_steam, sunup_amount
|
||||
|
||||
|
||||
# Python sets aren't insertion-ordered, but dict keys are
|
||||
type OrderedSet[T] = dict[T, None]
|
||||
|
||||
|
||||
class Background(ABC):
|
||||
_colour: Pattern[str] = compile(r"^[a-fA-F0-9]{6}$")
|
||||
|
||||
@abstractmethod
|
||||
def blue_light(self) -> float: ...
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def __eq__(self, value: object, /) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def __hash__(self) -> int: ...
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def __str__(self) -> str: ...
|
||||
|
||||
@classmethod
|
||||
def parse(cls, arg: str) -> "Background":
|
||||
if match(cls._colour, arg):
|
||||
return Colour(int(arg, 16))
|
||||
else:
|
||||
return Wallpaper(Path(arg))
|
||||
|
||||
|
||||
class Wallpaper(Background):
|
||||
_cache: Path = Path(environ["XDG_CACHE_HOME"]) / "wallpaper_blue_light.json"
|
||||
_wallpapers: Path = Path("~/Pictures/wallpapers/").expanduser()
|
||||
|
||||
def __init__(self, image: Path) -> None:
|
||||
self._path: Path = image
|
||||
self._blue_light: float = nan
|
||||
|
||||
def _calculate_blue_light(self) -> float:
|
||||
from PIL import Image
|
||||
data = cast(list[int], Image.open(self._path).getdata(2))
|
||||
return sum(data) / len(data)
|
||||
|
||||
@classmethod
|
||||
def _read_cache(cls) -> dict[str, float]:
|
||||
if cls._cache.exists():
|
||||
if (text := cls._cache.read_text().strip()):
|
||||
if isinstance((obj := loads(text)), dict):
|
||||
return cast(dict[str, float], obj)
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def _write_cache(cls, cache: dict[str, float]) -> None:
|
||||
stringified = dumps(cache)
|
||||
if len(stringified) != cls._cache.write_text(stringified):
|
||||
raise Exception("Did not fully write blue light cache to file")
|
||||
|
||||
@classmethod
|
||||
def available(cls) -> "OrderedSet[Background]":
|
||||
cache = cls._read_cache()
|
||||
modified = False
|
||||
wallpapers = {str(path): Wallpaper(path) for path in cls._wallpapers.iterdir()}
|
||||
for wallpaper in wallpapers.values():
|
||||
cached = cache.get(str(wallpaper._path))
|
||||
if cached is None or isnan(cached):
|
||||
print("New file:", wallpaper._path)
|
||||
cached = wallpaper._calculate_blue_light()
|
||||
modified = True
|
||||
cache[str(wallpaper._path)] = cached
|
||||
wallpaper._blue_light = cached
|
||||
if modified:
|
||||
cache = {path: score for (path, score) in sorted(cache.items(), key=lambda pair: pair[1])}
|
||||
cls._write_cache(cache)
|
||||
return {wallpapers[path]: None for path in cache.keys()}
|
||||
|
||||
@override
|
||||
def blue_light(self) -> float:
|
||||
return self._blue_light
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object, /) -> bool:
|
||||
if isinstance(value, Wallpaper):
|
||||
return self._path == value._path
|
||||
return False
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._path)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return str(self._path)
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"Wallpaper({repr(self._path)})"
|
||||
|
||||
|
||||
class Colour(Background):
|
||||
def __init__(self, value: int) -> None:
|
||||
self._value: int = value
|
||||
|
||||
@override
|
||||
def blue_light(self) -> float:
|
||||
return self._value & 0xFF
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object, /) -> bool:
|
||||
if isinstance(value, Colour):
|
||||
return self._value == value._value
|
||||
return False
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._value)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return hex(self._value)
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"Colour({hex(self._value)})"
|
||||
|
||||
|
||||
def _convert_bytes(output: bytes) -> str:
|
||||
return output.decode("utf-8").strip()
|
||||
|
||||
|
||||
def _swww_cmd(*args: str) -> str:
|
||||
output = run(["swww", *args], capture_output=True)
|
||||
if output.stderr:
|
||||
raise Exception(f"swww returned: '{_convert_bytes(output.stderr)}'")
|
||||
return _convert_bytes(output.stdout)
|
||||
|
||||
|
||||
class Monitor:
|
||||
_query: Pattern[str] = compile(r"^([^:]+): (\d+)x(\d+), scale: ([\d.]+), currently displaying: (?:image|color): (.+)$")
|
||||
_transition_args: list[str] = [
|
||||
"--transition-type=wipe",
|
||||
"--transition-angle=30",
|
||||
"--transition-step=45",
|
||||
"--transition-fps=120",
|
||||
]
|
||||
|
||||
def __init__(self, line: str) -> None:
|
||||
if not (values := match(self._query, line)):
|
||||
raise Exception(f"Failed to extract information from: '{line}'")
|
||||
self._name: str = values[1]
|
||||
self._resolution: tuple[int, int] = (int(values[2]), int(values[3]))
|
||||
self._scale: float = float(values[4])
|
||||
self._background: Background = Background.parse(values[5])
|
||||
|
||||
def is_colour(self) -> bool:
|
||||
return isinstance(self._background, Colour)
|
||||
|
||||
def set_wallpaper(self, background: Background) -> None:
|
||||
self._background = background
|
||||
_ = _swww_cmd("img", *self._transition_args, f"--outputs={self._name}", str(self._background))
|
||||
|
||||
def get_background(self) -> Background:
|
||||
return self._background
|
||||
|
||||
@classmethod
|
||||
def current(cls) -> "list[Monitor]":
|
||||
return [Monitor(line) for line in _swww_cmd("query").splitlines()]
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object, /) -> bool:
|
||||
if isinstance(value, Monitor):
|
||||
return self._background == value._background
|
||||
return False
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"Monitor('{self._name}: {self._resolution[0]}x{self._resolution[1]}, scale: {self._scale}, currently displaying: {"color" if self.is_colour() else "image"}: {self._background}')"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self._name}: {self._background}"
|
||||
|
||||
|
||||
def select_wallpapers(available: list[Background]) -> list[Background]:
|
||||
radius = len(available) / 3
|
||||
centre = sunup_amount() * len(available)
|
||||
return available[max(0, floor(centre - radius)): min(len(available) + 1, ceil(centre + radius))]
|
||||
|
||||
|
||||
def main(check_steam: bool = False) -> None:
|
||||
if check_steam and on_steam():
|
||||
return
|
||||
|
||||
monitors = Monitor.current()
|
||||
wallpapers = Wallpaper.available()
|
||||
|
||||
for monitor in monitors:
|
||||
wallpapers.pop(monitor.get_background(), None)
|
||||
|
||||
for monitor, wallpaper in zip(monitors, sample(select_wallpapers(list(wallpapers.keys())), len(monitors))):
|
||||
monitor.set_wallpaper(wallpaper)
|
||||
sleep(1.25)
|
|
@ -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()
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/env python
|
||||
"""Selects new backgrounds for the monitors"""
|
||||
|
||||
from lib.swww import ANGLE_TRANSITION_ARGS, run_swww, sample_wallpapers
|
||||
|
||||
|
||||
for monitor, wallpaper in sample_wallpapers():
|
||||
run_swww(monitor, wallpaper, ANGLE_TRANSITION_ARGS)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/env python
|
||||
"""Sets a new background for the monitors immediately"""
|
||||
|
||||
from lib.swww import IMMEDIATE_TRANSITION_ARGS, run_swww, sample_wallpapers
|
||||
|
||||
|
||||
for monitor, wallpaper in sample_wallpapers():
|
||||
run_swww(monitor, wallpaper, IMMEDIATE_TRANSITION_ARGS)
|
||||
|
20
scripts/wallpaper.py
Executable file
20
scripts/wallpaper.py
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/env python
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from typing import cast
|
||||
|
||||
from lib.wallpaper import main
|
||||
|
||||
|
||||
class Arguments(Namespace):
|
||||
check_steam: bool = False
|
||||
|
||||
|
||||
def call_from_args() -> None:
|
||||
parser = ArgumentParser(description="Adjusts the wallpaper based on the time of day")
|
||||
_ = parser.add_argument("-s", "--check-steam", action="store_true", help="Don't adjust the wallpaper if Steam is active")
|
||||
args = cast(Arguments, parser.parse_args())
|
||||
main(args.check_steam)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
call_from_args()
|
|
@ -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
|
||||
|
|
6
systemd/wallpaper.service
Normal file
6
systemd/wallpaper.service
Normal file
|
@ -0,0 +1,6 @@
|
|||
[Unit]
|
||||
Description=Sets the wallpaper based on the time of day
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/env python -OO /home/mbradley/scripts/wallpaper.py --check-steam
|
12
systemd/wallpaper.timer
Normal file
12
systemd/wallpaper.timer
Normal file
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=Changes the wallpaper over the course of the day to ease eye strain
|
||||
After=graphical-session.target
|
||||
After=swww-daemon.service
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*:0/15
|
||||
Persistent=true
|
||||
AccuracySec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
Loading…
Add table
Add a link
Reference in a new issue