diff --git a/configuration-desktop.nix b/configuration-desktop.nix index d1499d3..1ff67db 100644 --- a/configuration-desktop.nix +++ b/configuration-desktop.nix @@ -27,6 +27,8 @@ in }; # sway stuff + programs.ydotool.enable = true; + users.users.audrey.extraGroups = [ "ydotool" ]; programs.regreet.enable = true; services.greetd.settings = { default_session.command = "${pkgs.dbus}/bin/dbus-run-session ${lib.getExe config.programs.sway.package} -c /etc/sway/greeter-config"; @@ -74,8 +76,8 @@ in xwayland.enable = true; extraPackages = with pkgs; [ swaylock + swayr pavucontrol - pasystray libnotify wdisplays playerctl @@ -88,7 +90,6 @@ in adwaita-icon-theme glib kdePackages.kwallet - swaynotificationcenter ]; extraSessionCommands = '' export ELECTRON_OZONE_PLATFORM_HINT=wayland @@ -165,6 +166,16 @@ in partOf = [ "graphical-environment.target" ]; wantedBy = [ "graphical-environment.target" ]; }; + systemd.user.services.pasystray = lib.mkIf config.programs.sway.enable { + description = "Pulseaudio system tray icon"; + serviceConfig = { + Type = "simple"; + ExecStart = "${lib.getExe pkgs.pasystray} --notify source --notify sink -m 100"; + }; + path = [ "/run/current-system/sw" ]; + partOf = [ "graphical-environment.target" ]; + wantedBy = [ "graphical-environment.target" ]; + }; systemd.user.services.kdeconnect-indicator = lib.mkIf config.programs.sway.enable { description = "KDE connect indicator"; serviceConfig = { @@ -175,6 +186,16 @@ in partOf = [ "graphical-environment.target" ]; wantedBy = [ "graphical-environment.target" ]; }; + systemd.user.services.swayr = lib.mkIf config.programs.sway.enable { + description = "Sway MRU window switcher"; + serviceConfig = { + Type = "simple"; + ExecStart = "${lib.getBin pkgs.swayr}/bin/swayrd"; + }; + path = [ "/run/current-system/sw" ]; + partOf = [ "graphical-environment.target" ]; + wantedBy = [ "graphical-environment.target" ]; + }; virtualisation.docker = { enable = true; @@ -200,6 +221,7 @@ in ]; }; }; + environment.sessionVariables.TERMINAL = "foot"; environment.systemPackages = with pkgs; [ dino diff --git a/configuration.nix b/configuration.nix index a822cc6..607b546 100644 --- a/configuration.nix +++ b/configuration.nix @@ -13,7 +13,7 @@ in { description = "kakoune packages to include in the global editor"; }; }; - imports = [ ./overlays/packages.nix ./configuration-cross.nix ]; + imports = [ ./overlays/packages.nix ./overlays/lix.nix ./configuration-cross.nix ]; config = { nixpkgs.config.allowUnfree = true; @@ -23,7 +23,6 @@ in { nix.settings.cores = 0; nix.settings.secret-key-files = [ "/var/lib/nix/binary-cache-key" ]; nix.settings.trusted-public-keys = builtins.filter (f: f != "") <| lib.strings.splitString "\n" <| builtins.readFile ./keys/nix; - nix.package = pkgs.lixPackageSets.stable.lix; # Select internationalisation properties. i18n.defaultLocale = "en_US.UTF-8"; @@ -45,9 +44,9 @@ in { environment.systemPackages = with pkgs; [ man-pages man-pages-posix - bat gnumake wget + moar ripgrep fd curl @@ -55,11 +54,14 @@ in { file nettools psmisc + units + units-desktop patchelf gdb kubectl p7zip unzip + zip foremost binwalk jq @@ -117,7 +119,7 @@ in { pag = "ps aux | grep -v grep | grep -i"; hd = "hexdump -C"; hdc = "hexdump -ve '\"\\\x\" 1/1 \"%02x\"'"; - man = "MAN_POSIXLY_CORRECT=1 man"; + man = "batman"; nose = "pytest -v --capture=no --pdbcls=IPython.terminal.debugger:TerminalPdb"; mkvirtualenv = "mkvirtualenv -r /etc/venv-default.txt"; }; @@ -140,8 +142,22 @@ in { url."ssh://git@".insteadOf = "git://"; }; }; + bat = { + enable = true; + extraPackages = with pkgs.bat-extras; [ + batdiff + batman + prettybat + ]; + settings = { + italic-text = "always"; + wrap = "never"; + style = "plain"; + }; + }; }; + environment.variables.PAGER = "moar"; environment.etc.zinputrc.text = lib.mkForce (builtins.readFile ./dotfiles/zsh-input.sh); environment.etc."gdb/gdbinit".source = ./dotfiles/gdb-init.gdb; diff --git a/dotfiles/zsh-init.sh b/dotfiles/zsh-init.sh index 079481d..2827a36 100644 --- a/dotfiles/zsh-init.sh +++ b/dotfiles/zsh-init.sh @@ -97,7 +97,7 @@ function precmd-osc-title() { print -Pn "\e]2;%n@%M | %~\a" } function preexec-osc-title() { - print -Pn "\e]2;%n@%M | $1\a" + print -Pn "\e]2;%n@%M | ${~1:gs/%/%%}\a" } autoload -Uz add-zsh-hook @@ -128,6 +128,8 @@ export PYTHONBREAKPOINT="ipdb.set_trace" export COLORTERM=1 export SHELL=$(which zsh) export npm_config_prefix=~/.local +export HISTSIZE=100000 +export SAVEHIST=100000 # site vars, functions, and aliases if [ -e ~/.site_aliases.sh ]; then diff --git a/flake.lock b/flake.lock index 70b8417..861bc95 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "bingosync": { "locked": { - "lastModified": 1751571764, - "narHash": "sha256-KEII5eCIBsBJy3eowAmmtRbdmV2aREOjU8AHypPz5y8=", + "lastModified": 1760739933, + "narHash": "sha256-yhgeHqfn0ZlXYkhZMqccTzWDvzre+DpcYzpk/7a7xec=", "owner": "rhelmot", "repo": "bingosync", - "rev": "08ab441fee60360435c263430610fc10406b0602", + "rev": "59f0555be4e7679750bc15396dc1b3e559325951", "type": "github" }, "original": { @@ -23,11 +23,11 @@ ] }, "locked": { - "lastModified": 1745193236, - "narHash": "sha256-1hG3it3DDdn+VXmKUgF/KCbO3VMteEZ8xuh20jXqFus=", + "lastModified": 1759863336, + "narHash": "sha256-H8NRd03xQVKVunTYsd95pMzZS5nfYTDUw6R78dJESrs=", "ref": "refs/heads/main", - "rev": "5cb010a404c5a6c9fe2257aee2afe4aa3b288974", - "revCount": 8, + "rev": "bc6337d8f649f5afdc281b64fad2891bb2067a51", + "revCount": 11, "type": "git", "url": "https://git.lain.faith/rhelmot/blog.rhelmot.io" }, @@ -45,11 +45,11 @@ "utils": "utils" }, "locked": { - "lastModified": 1745191548, - "narHash": "sha256-z8BhZIXNNNfGop+k/6+psSKp9BLz5eiuE16fDICdnyE=", + "lastModified": 1759863318, + "narHash": "sha256-6yXyEllmvAFgSg4KzFqJ3bx6K1+ZBsqOOdX08F29k08=", "owner": "rhelmot", "repo": "coricamu", - "rev": "225536eafef6d8de6bdad3bc756587ad3ebaecb7", + "rev": "f109bad2add146f3001805a8600b198473b3c9c2", "type": "github" }, "original": { diff --git a/overlays/lix.nix b/overlays/lix.nix new file mode 100644 index 0000000..a2aeb1f --- /dev/null +++ b/overlays/lix.nix @@ -0,0 +1,15 @@ +{ + pkgs, + ... +}: +{ + nixpkgs.overlays = [ (final: prev: { + inherit (prev.lixPackageSets.latest) + nixpkgs-review + nix-eval-jobs + nix-fast-build + colmena; + }) ]; + + nix.package = pkgs.lixPackageSets.latest.lix; +} diff --git a/overlays/packages.nix b/overlays/packages.nix index bf16f98..a77bf3f 100644 --- a/overlays/packages.nix +++ b/overlays/packages.nix @@ -12,6 +12,7 @@ let overlay = final: prev: { }); idapro9 = pkgs.callPackage ../pkgs/idapro9.nix {}; condition-unmetered-network = pkgs.callPackage ../pkgs/condition-unmetered-network {}; + units-desktop = pkgs.callPackage ../pkgs/units-desktop.nix {}; }; in { diff --git a/pkgs/units-desktop.nix b/pkgs/units-desktop.nix new file mode 100644 index 0000000..ab1bc84 --- /dev/null +++ b/pkgs/units-desktop.nix @@ -0,0 +1,18 @@ +{ + lib, + writeTextFile, + units, +}: +writeTextFile { + name = "units-desktop"; + destination = "/share/applications/units.desktop"; + text = '' + [Desktop Entry] + Encoding=UTF-8 + Version=${units.version} + Type=Application + Terminal=true + Exec=${lib.getExe units} + Name=Units + ''; +} diff --git a/sites/daisy/configuration.nix b/sites/daisy/configuration.nix index b527d76..36707eb 100644 --- a/sites/daisy/configuration.nix +++ b/sites/daisy/configuration.nix @@ -48,6 +48,7 @@ #} ); environment.systemPackages = [ + pkgs.racket pkgs.idapro9 pkgs.qemu_kvm (pkgs.runCommand "OVMF-fd" {} '' @@ -183,4 +184,19 @@ # enable = true; # nginxHost = "noscope"; #}; + + boot.binfmt.emulatedSystems = [ + "aarch64-linux" + "mips-linux" + "mipsel-linux" + "armv7l-linux" + ]; + boot.binfmt.preferStaticEmulators = true; + + programs.steam.enable = true; + programs.steam.gamescopeSession.enable = true; + programs.gamescope.enable = true; + programs.gamescope.capSysNice = true; + services.pulseaudio.support32Bit = true; + hardware.graphics.enable32Bit = true; } diff --git a/sites/sunflower/configuration.nix b/sites/sunflower/configuration.nix index d478be1..3431861 100644 --- a/sites/sunflower/configuration.nix +++ b/sites/sunflower/configuration.nix @@ -93,10 +93,6 @@ }; }; - services.keycloak = { - enable = true; - }; - services.postgresql = { enable = true; ensureDatabases = [ @@ -246,30 +242,20 @@ }; }; - systemd.services.spamkick = let - src = pkgs.fetchFromGitHub { - owner = "maddie480"; - repo = "SpamKick"; - rev = "9dd5b5e3cc78e2520b13a0875ae7ef264a5a52c5"; - hash = "sha256-ZjxnqIiXBaxrZwrCfDPVTpGmRxtrL5kc5ZcDUaQtbZo="; - }; - env = pkgs.python3.withPackages (ps: with ps; [ discordpy ]); - in { - path = [ env ]; + users.users.reminder-bot = { + isSystemUser = true; + group = "nogroup"; + }; + systemd.services.reminder-bot = { + path = [ (pkgs.python3.withPackages (p: with p; [ discordpy aiocron aiosqlite cronsim ])) ]; script = '' - export TOKEN="$(cat /var/lib/spamkick/token.txt)" - exec python ${src}/main.py + exec python ${./reminder_bot.py} ''; serviceConfig = { Type = "simple"; Restart = "always"; + User = "reminder-bot"; }; wantedBy = [ "multi-user.target" ]; - environment = { - LOG_CHANNEL_ID = "532689319350108160"; - CHANNEL_COUNT = "4"; - DELAY_SECONDS = "5"; - DEBUG = "0"; - }; }; } diff --git a/sites/sunflower/reminder_bot.py b/sites/sunflower/reminder_bot.py new file mode 100644 index 0000000..097c5b7 --- /dev/null +++ b/sites/sunflower/reminder_bot.py @@ -0,0 +1,173 @@ +from cronsim import CronSimError +import discord +from discord.ext import commands +import aiosqlite +import aiocron +from datetime import datetime, timedelta, timezone +import logging + +log = logging.getLogger("reminder_bot") + +DATABASE = '/var/lib/reminder-bot/db' + +with open('/var/lib/reminder-bot/token') as fp: + TOKEN = fp.read().strip() +BOTSPAM_CHANNEL_ID = 1428820508178124820 + +intents = discord.Intents.default() +intents.message_content = True + +client = commands.Bot('/', intents=intents) +tree = client.tree + +# ------------------------------------------------------------ +# DATABASE SETUP +# ------------------------------------------------------------ +async def init_db(): + async with aiosqlite.connect(DATABASE) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message TEXT NOT NULL, + cron TEXT NOT NULL + ) + """) + await db.commit() + +# ------------------------------------------------------------ +# DISMISS BUTTON +# ------------------------------------------------------------ +class DismissButton(discord.ui.View): + def __init__(self, reminder_id: int): + super().__init__(timeout=None) + self.reminder_id = reminder_id + + @discord.ui.button(label="Dismiss", style=discord.ButtonStyle.red) + async def dismiss(self, interaction: discord.Interaction, button: discord.ui.Button): + user = interaction.user.display_name + timestamp = datetime.now(tz=timezone(timedelta(hours=-7))).strftime("%a %b %-m %-I:%M %p") + assert interaction.message is not None + button.disabled = True + await interaction.response.edit_message(content=interaction.message.content + f"\nCompleted by {user} at {timestamp}", view=self) + + +# ------------------------------------------------------------ +# GLOBAL JOB REGISTRY +# ------------------------------------------------------------ +scheduled_jobs = {} + +async def send_reminder(message: str, reminder_id: int): + channel = client.get_channel(BOTSPAM_CHANNEL_ID) + assert isinstance(channel, discord.TextChannel) + await channel.send( + f"⏰ **Reminder:** {message}", + view=DismissButton(reminder_id), + allowed_mentions=discord.AllowedMentions(everyone=True, users=True, roles=True) + ) + +async def schedule_reminder(reminder_id: int, message: str, cron_expr: str): + assert reminder_id not in scheduled_jobs + job = aiocron.crontab(cron_expr, func=send_reminder, args=(message, reminder_id)) + scheduled_jobs[reminder_id] = job + +# ------------------------------------------------------------ +# COMMANDS +# ------------------------------------------------------------ +@tree.command(name="reminder-add") +async def add_reminder(ctx: discord.Interaction, cron_expr: str, *, message: str): + """ + Add a new reminder using a cron expression. + """ + try: + aiocron.CronSim(cron_expr, datetime.now()) + except CronSimError as e: + await ctx.response.send_message(f"Your cron expression did not parse: {e}", ephemeral=True) + else: + async with aiosqlite.connect(DATABASE) as db: + cursor = await db.execute( + "INSERT INTO reminders (message, cron) VALUES (?, ?)", + (message, cron_expr) + ) + await db.commit() + reminder_id = cursor.lastrowid + assert reminder_id is not None + + await schedule_reminder(reminder_id, message, cron_expr) + await ctx.response.send_message(f"✅ Reminder {reminder_id} added: '{message}' `{cron_expr}`") + +@tree.command(name="reminder-list") +async def list_reminders(ctx: discord.Interaction): + """List all active reminders.""" + async with aiosqlite.connect(DATABASE) as db: + async with db.execute("SELECT id, message, cron FROM reminders") as cursor: + rows = await cursor.fetchall() + + if not rows: + await ctx.response.send_message("No active reminders.", ephemeral=True) + return + + lines = [f"**{r[0]}**: {r[1]} `{r[2]}`" for r in rows] + await ctx.response.send_message("\n".join(lines), ephemeral=True) + +@tree.command(name="reminder-delete") +async def delete_reminder(ctx: discord.Interaction, reminder_id: int): + """Delete a reminder by ID.""" + async with aiosqlite.connect(DATABASE) as db: + async with db.execute("SELECT message FROM reminders WHERE id = ?", (reminder_id,)) as cursor: + rows = list(await cursor.fetchall()) + if not rows: + await ctx.response.send_message(f"No such reminder {reminder_id}") + return + + await db.execute("DELETE FROM reminders WHERE id = ?", (reminder_id,)) + await db.commit() + + job = scheduled_jobs.pop(reminder_id, None) + if job: + job.stop() + + await ctx.response.send_message(f"🗑️ Reminder {reminder_id} ({rows[0][0]}) deleted.") + +@tree.command(name="reminder-edit") +async def edit_reminder(ctx: discord.Interaction, reminder_id: int, message: str, cron_expr: str): + try: + aiocron.CronSim(cron_expr, datetime.now()) + except CronSimError as e: + await ctx.response.send_message(f"Your cron expression did not parse: {e}", ephemeral=True) + else: + async with aiosqlite.connect(DATABASE) as db: + async with db.execute("SELECT message FROM reminders WHERE id = ?", (reminder_id,)) as cursor: + rows = list(await cursor.fetchall()) + if not rows: + await ctx.response.send_message(f"No such reminder {reminder_id}") + return + + await db.execute("UPDATE reminders SET message = ?, cron = ? WHERE id = ?", (message, cron_expr, reminder_id)) + await db.commit() + job = scheduled_jobs.pop(reminder_id, None) + if job: + job.stop() + await schedule_reminder(reminder_id, message, cron_expr) + await ctx.response.send_message(f"✅ Reminder {reminder_id} updated: {message} `{cron_expr}`") + + +# ------------------------------------------------------------ +# STARTUP +# ------------------------------------------------------------ +@client.event +async def on_ready(): + await init_db() + + # Load all reminders from DB and schedule them + async with aiosqlite.connect(DATABASE) as db: + async with db.execute("SELECT id, message, cron FROM reminders") as cursor: + async for reminder_id, message, cron_expr in cursor: + await schedule_reminder(reminder_id, message, cron_expr) + + log.info(f"Loaded {len(scheduled_jobs)} scheduled reminders.") + await tree.sync() + +# ------------------------------------------------------------ +# RUN +# ------------------------------------------------------------ +client.run(TOKEN)