diff options
Diffstat (limited to '')
211 files changed, 15584 insertions, 6078 deletions
diff --git a/pkgs/by-name/at/atuin-dvorak/package.nix b/pkgs/by-name/at/atuin-dvorak/package.nix index c651eb75..d3f92961 100644 --- a/pkgs/by-name/at/atuin-dvorak/package.nix +++ b/pkgs/by-name/at/atuin-dvorak/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. {atuin}: atuin.overrideAttrs (finalAttrs: previousAttrs: { pname = previousAttrs.pname + "-dvorak"; diff --git a/pkgs/by-name/at/atuin-dvorak/set-dvorak-keybindings.patch.license b/pkgs/by-name/at/atuin-dvorak/set-dvorak-keybindings.patch.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/at/atuin-dvorak/set-dvorak-keybindings.patch.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/au/aumo/aumo.sh b/pkgs/by-name/au/aumo/aumo.sh index 84d39deb..dba5ef63 100755 --- a/pkgs/by-name/au/aumo/aumo.sh +++ b/pkgs/by-name/au/aumo/aumo.sh @@ -1,28 +1,73 @@ #! /usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +NAME="aumo" + +error() { + printf "\033[1;91m==> ERROR:\033[0m \033[1;93m%s\033[0m\n" "$*" >&2 +} + +die() { + error "$1" + exit "${2-1}" +} + +usage() { + echo "Usage: $NAME mount|unmount" +} + +get_mounted_labels() { + findmnt --output label --json | jq '.filesystems | map(.label) | sort | unique | map(select(. != null))' +} +get_unmounted_labels() { + first=true + + find /dev/disk/by-label -printf "%P\n" | while read -r label; do + if ! get_mounted_labels | jq 'join("\n")' --raw-output | grep "$label" --quiet; then + if [ "$first" = "true" ]; then + first=false + else + printf "|" + fi + printf "%s" "$label" + fi + done +} unmounting() { - disk_name="$(find /dev/disk/by-label -type l -printf "%P|" | rofi -sep "|" -dmenu -p "Select disk to mount")" + disk_name="$(get_mounted_labels | jq 'join("|")' --join-output | rofi -sep "|" -dmenu -p "Select disk to unmount")" udisksctl unmount --block-device "/dev/disk/by-label/$disk_name" } mounting() { - disk_name="$(find /dev/disk/by-label -type l -printf "%P|" | rofi -sep "|" -dmenu -p "Select disk to mount")" + disk_name="$(get_unmounted_labels | rofi -sep "|" -dmenu -p "Select disk to mount")" udisksctl mount --block-device "/dev/disk/by-label/$disk_name" } -case "$1" in +case "${1-unset}" in "mount") mounting ;; "unmount" | "umount") unmounting ;; +"unset") + usage + die "You need to provide one argument." + ;; *) - die "Usage: $NAME mount|unmount" + usage + die "Unknown command: '$1'" ;; esac diff --git a/pkgs/by-name/au/aumo/package.nix b/pkgs/by-name/au/aumo/package.nix index 20054bb5..8132a15a 100644 --- a/pkgs/by-name/au/aumo/package.nix +++ b/pkgs/by-name/au/aumo/package.nix @@ -1,17 +1,32 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies udisks, findutils, rofi, + jq, + gnugrep, + util-linux, }: -sysLib.writeShellScript { +writeShellApplication { name = "aumo"; - src = ./aumo.sh; - generateCompletions = false; - keepPath = false; - dependencies = [ + text = builtins.readFile ./aumo.sh; + inheritPath = false; + runtimeInputs = [ udisks findutils rofi + jq + gnugrep + util-linux # for findmnt ]; } diff --git a/pkgs/by-name/ba/battery/battery.sh b/pkgs/by-name/ba/battery/battery.sh index e650ba5d..5ccb303d 100755 --- a/pkgs/by-name/ba/battery/battery.sh +++ b/pkgs/by-name/ba/battery/battery.sh @@ -1,7 +1,14 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. capacity="$(cat /sys/class/power_supply/BAT0/capacity)" status="$(cat /sys/class/power_supply/BAT0/status)" diff --git a/pkgs/by-name/ba/battery/package.nix b/pkgs/by-name/ba/battery/package.nix index 9c0e194b..560ec59b 100644 --- a/pkgs/by-name/ba/battery/package.nix +++ b/pkgs/by-name/ba/battery/package.nix @@ -1,9 +1,20 @@ -{sysLib}: -sysLib.writeShellScript { +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + writeShellApplication, + # Dependencies + coreutils, +}: +writeShellApplication { name = "battery"; - src = ./battery.sh; - generateCompletions = false; - keepPath = false; - dependencies = [ - ]; + text = builtins.readFile ./battery.sh; + inheritPath = false; + runtimeInputs = [coreutils]; } diff --git a/pkgs/by-name/be/beetsExtraPlugins/package.nix b/pkgs/by-name/be/beetsExtraPlugins/package.nix index f019922e..b310715b 100644 --- a/pkgs/by-name/be/beetsExtraPlugins/package.nix +++ b/pkgs/by-name/be/beetsExtraPlugins/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. {...}: { # xtractor = pkgs.callPackage ./xtractor.nix {beets = pkgs.beetsPackages.beets-minimal;}; } diff --git a/pkgs/by-name/be/beetsExtraPlugins/xtractor.nix b/pkgs/by-name/be/beetsExtraPlugins/xtractor.nix index 17981cdb..c6b942fd 100644 --- a/pkgs/by-name/be/beetsExtraPlugins/xtractor.nix +++ b/pkgs/by-name/be/beetsExtraPlugins/xtractor.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { lib, fetchFromGitHub, diff --git a/pkgs/by-name/br/brightness/brightness.sh b/pkgs/by-name/br/brightness/brightness.sh index 887dbb1e..f38bb5a5 100755 --- a/pkgs/by-name/br/brightness/brightness.sh +++ b/pkgs/by-name/br/brightness/brightness.sh @@ -1,24 +1,33 @@ -#!/usr/bin/env dash - -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +#!/usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +die() { + echo "ERROR: $1" + exit 1 +} help() { cat <<EOF This is a system brightness manager USAGE: - $NAME up [VALUE] | down [VALUE] + $NAME set VALUE OPTIONS: --help | -h Output this help and exit. - --version | -v - Output the version and exit. - COMMANDS: - set [VALUE] + set VALUE Set the brightness to the specified percentage. ARGUMENTS: @@ -27,17 +36,14 @@ ARGUMENTS: EOF } -BACKLIGHT="/sys/class/backlight/%BACKLIGHT_NAME" +BACKLIGHT="/sys/class/backlight/$BACKLIGHT_NAME" brightness() { perc="$1" - max="$(cat $BACKLIGHT/max_brightness)" - + max="$(cat "$BACKLIGHT/max_brightness")" new="$(echo | awk --assign=perc="$perc" '{printf (perc / 100)}')" - output="$(echo | awk --assign=new="$new" --assign=max="$max" '{printf max * new}')" - echo "$output" >"$BACKLIGHT/brightness" } @@ -47,10 +53,6 @@ for arg in "$@"; do help exit 0 ;; - "--version" | "-v") - version - exit 0 - ;; esac done diff --git a/pkgs/by-name/br/brightness/package.nix b/pkgs/by-name/br/brightness/package.nix index c2e31a0c..f3797d43 100644 --- a/pkgs/by-name/br/brightness/package.nix +++ b/pkgs/by-name/br/brightness/package.nix @@ -1,14 +1,29 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Arguments backlightName ? "intel_backlight", # nixosConfig.soispha.laptop.backlight + # Dependencies + gawk, + coreutils, }: -sysLib.writeShellScript { +writeShellApplication { name = "brightness"; - src = ./brightness.sh; - generateCompletions = true; - keepPath = false; + text = builtins.readFile ./brightness.sh; + inheritPath = false; - replacementStrings = {BACKLIGHT_NAME = backlightName;}; + runtimeEnv = {BACKLIGHT_NAME = backlightName;}; - dependencies = []; + runtimeInputs = [ + gawk + coreutils + ]; } diff --git a/pkgs/by-name/co/con2pdf/con2pdf.sh b/pkgs/by-name/co/con2pdf/con2pdf.sh index 27c9d092..ebe35ad3 100755 --- a/pkgs/by-name/co/con2pdf/con2pdf.sh +++ b/pkgs/by-name/co/con2pdf/con2pdf.sh @@ -1,19 +1,18 @@ #! /usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -# needed for help() and version -# shellcheck disable=2034 -AUTHORS="Soispha" -# shellcheck disable=2034 -YEARS="2023" -# shellcheck disable=2034 -VERSION="1.0.0" - -# NAME is from the wrapper -# shellcheck disable=SC2269 -NAME="$NAME" +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +# TODO(@bpeetz): This should probably be rewritten in rust. <2025-04-14> + +NAME="con2pdf" help() { cat <<EOF Scan images and turn them into a pdf. @@ -52,7 +51,7 @@ ARGUMENTS: NUM | *([0-9]) := 0 | 1 | 2 | 3 | 4 Possible numbers of pages, can be more than 4 - DEVICE := [[$(cat %DEVICE_FUNCTION)]] + DEVICE := [[$(cat "$DEVICE_FUNCTION")]] Possible scanner names METHOD := ADF | Flatbed diff --git a/pkgs/by-name/co/con2pdf/package.nix b/pkgs/by-name/co/con2pdf/package.nix index 8eb994fd..11d45ab5 100644 --- a/pkgs/by-name/co/con2pdf/package.nix +++ b/pkgs/by-name/co/con2pdf/package.nix @@ -1,24 +1,34 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, writeText, - # dependencies + # Dependencies sane-backends, imagemagick, coreutils, fd, }: -sysLib.writeShellScript { +writeShellApplication { name = "con2pdf"; - src = ./con2pdf.sh; - generateCompletions = true; - keepPath = false; - dependencies = [ + text = builtins.readFile ./con2pdf.sh; + inheritPath = false; + + runtimeInputs = [ sane-backends imagemagick coreutils fd ]; - replacementStrings = { + + runtimeEnv = { DEVICE_FUNCTION = # This is here, because escaping the whole function, to use it in the shell script # directly just isn't possible diff --git a/pkgs/by-name/ct/ctpv-64-types/allow-up-to-64-types.patch.license b/pkgs/by-name/ct/ctpv-64-types/allow-up-to-64-types.patch.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/ct/ctpv-64-types/allow-up-to-64-types.patch.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/ct/ctpv-64-types/package.nix b/pkgs/by-name/ct/ctpv-64-types/package.nix index 763b0325..087e1d28 100644 --- a/pkgs/by-name/ct/ctpv-64-types/package.nix +++ b/pkgs/by-name/ct/ctpv-64-types/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. {ctpv}: ctpv.overrideAttrs (finalAttrs: previousAttrs: { pname = previousAttrs.pname + "-64-types"; diff --git a/pkgs/by-name/fd/fd_list/fd_list.sh b/pkgs/by-name/fd/fd_list/fd_list.sh new file mode 100755 index 00000000..568d73c8 --- /dev/null +++ b/pkgs/by-name/fd/fd_list/fd_list.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +# Lists all fds in use with their respective associated programs. +# This can be useful, if you need to debug, why your system is out of fds. + +# This script is not POSIX shell compatible, as the `ulimit` flags `-H` and `-n` are not defined by POSIX (thus we use `bash` instead). + +find /proc/ -maxdepth 1 | while read -r path; do + pid="${path#/proc/}" + fd_dir="$path/fd/" + + [ -d "$fd_dir" ] && echo "PID = $pid with $(find "$fd_dir" | wc -l) file descriptors" +done | sort -rn -k5 | head | while read -r _ _ pid _ fdcount _; do + command_execution="$(ps -o cmd -p "$pid" -h)" + + command_arguments="$(echo "$command_execution" | awk '{$1=""; print $0}')" + command_name="$(basename "$(echo "$command_execution" | awk '{print $1}')")" + + command="$command_name$command_arguments" + + printf "PID %8d with %4d file descriptors: %s\n" "$pid" "$fdcount" "$command" +done || true +# ^ Ignore SIGPIPE + +printf "\nThe Kernel has %7d file handles allocated.\n" "$(awk '{print $1}' /proc/sys/fs/file-nr)" +printf " -> The per-process fd maxium is: %d\n" "$(ulimit -Hn)" +printf " -> The per-process fd soft-maximum is: %d\n" "$(ulimit -n)" +printf " -> The global fd maximum is: %d\n" "$(cat /proc/sys/fs/file-max)" + +# vim: ft=sh diff --git a/pkgs/by-name/fd/fd_list/package.nix b/pkgs/by-name/fd/fd_list/package.nix new file mode 100644 index 00000000..34129b10 --- /dev/null +++ b/pkgs/by-name/fd/fd_list/package.nix @@ -0,0 +1,28 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + writeShellApplication, + findutils, + gawk, + coreutils, + procps, +}: +writeShellApplication { + name = "fd_list"; + text = builtins.readFile ./fd_list.sh; + + inheritPath = false; + runtimeInputs = [ + findutils + gawk + coreutils + procps + ]; +} diff --git a/pkgs/by-name/fu/fupdate-flake/fupdate-flake.sh b/pkgs/by-name/fu/fupdate-flake/fupdate-flake.sh new file mode 100755 index 00000000..00c1e443 --- /dev/null +++ b/pkgs/by-name/fu/fupdate-flake/fupdate-flake.sh @@ -0,0 +1,194 @@ +#! /usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +UPDATE_SCRIPT_NAME="update.sh" + +info() { + echo "Info: $1" +} +dbg() { + if [ "${DEBUG_ENABLE-unset}" != "unset" ]; then + echo "Debug: $1" >&2 + fi +} +die() { + echo "Error: $1" + exit 1 +} + +# Search for a file “upwards”. +# This will return the relative path from "$1" to the found file. +# +# # Type +# upfind :: Path -> String -> Path +# +# # Arguments +# $1 +# : The directory to use as start of your search. +# +# $2 +# : The file to search for. +# +# # Example +# upfind "/home/user1" "/usr" +# => /usr +upfind() { + starting_directory="$(readlink --canonicalize "$1")" + search_string="$2" + + current_directory="$starting_directory" + + while + search_result=$(fd "$search_string" "$current_directory/" --max-depth 1) + dbg "upfind - search in $current_directory gives: $search_result" + [ -z "$search_result" ] && [ "$current_directory" != "/" ] + do current_directory=$(dirname "$current_directory"); done + + realpath --relative-to="$1" "$search_result" +} + +# Construct the storage path for the update script allowed hashes. +# +# # Type +# get_storage_path :: Path -> Path +# +# # Arguments +# $1 +# : The path to the update script +# +# # Returns +# The constructed storage path. +get_storage_path() { + update_script="$(realpath "$1")" + + storage_path="$XDG_DATA_HOME/fupdate-flake/$update_script" + echo "$storage_path" +} + +# Checks if a given path to the update script is allowed. +# +# # Type +# is_allowed :: Path -> bool +# +# # Arguments +# $1 +# : The path to the update script to check. +# +# # Return exit code +# 0 +# : If the update script is allowed +# +# 1 +# : If it is not. +is_allowed() { + update_script="$(realpath "$1")" + + storage_path="$(get_storage_path "$update_script")" + + # Use this invocation, to also include the path to the `$update_script` + update_script_hash="$(sha256sum "$update_script")" + + if [ -f "$storage_path" ]; then + if [ "$(cat "$storage_path")" = "$update_script_hash" ]; then + return 0 + else + return 1 + fi + else + return 1 + fi +} + +# Asks the user if they want to allow a given script. +# +# # Type +# ask_to_allow_update_script :: Path +# +# # Arguments +# $1 +# : The path to the update script to ask for. +ask_to_allow_update_script() { + update_script="$(realpath "$1")" + + printf "\033[2J" # clear the screen + cat "$update_script" + + printf "Do you want to allow this script?[N/y]: " + read -r allow + + case "$allow" in + [yY]) + info "Update script allowed." + + storage_path="$(get_storage_path "$update_script")" + update_script_hash="$(sha256sum "$update_script")" + + mkdir --parents "$(dirname "$storage_path")" + printf "%s" "$update_script_hash" >"$storage_path" + ;; + *) + info "Update script not allowed." + ;; + esac +} + +# Performs a full update. +# This consists of running an update script. +# Additionally, it also checks for duplicated inputs in a `flake.lock` file, if it exists. +# +# # Type +# update :: Path -> Path -> [String] +# +# # Arguments +# $1 +# : The path to the update script to execute. +# +# $2 +# : The base directory from which to start the update. +# +# $3 +# : Arguments to pass to the update script. +update() { + update_script="$1" + base_directory="$2" + shift 2 + + cd "$base_directory" || die "The provided base directory '$base_directory' cannot be accessed" + dbg "Changed directory to: $base_directory" + + dbg "Executing update script ('$update_script') following args: '$*'" + "$update_script" "$@" + + if [ -f "flake.lock" ] && grep '[^0-9]_[0-9]' flake.lock --quiet; then + grep '[^0-9]_[0-9]' flake.lock + die "Your flake.nix contains duplicate inputs!" + fi +} + +main() { + base_directory="$(git rev-parse --show-toplevel)" + update_script="$(upfind "$PWD" "$UPDATE_SCRIPT_NAME")" + dbg "update_script is: $update_script" + + if [ "$update_script" = "" ]; then + die "Failed to find update script." + elif is_allowed "$update_script"; then + update "$update_script" "$base_directory" "$@" + else + ask_to_allow_update_script "$update_script" + is_allowed "$update_script" && main "$@" + fi +} + +main "$@" + +# vim: ft=sh diff --git a/pkgs/by-name/fu/fupdate-flake/package.nix b/pkgs/by-name/fu/fupdate-flake/package.nix new file mode 100644 index 00000000..4e21cd23 --- /dev/null +++ b/pkgs/by-name/fu/fupdate-flake/package.nix @@ -0,0 +1,31 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + writeShellApplication, + # Dependencies + coreutils, + fd, + gnugrep, + git, +}: +writeShellApplication { + name = "fupdate-flake"; + text = builtins.readFile ./fupdate-flake.sh; + + # The `update.sh` script might actually want to keep the path. + inheritPath = true; + + runtimeInputs = [ + coreutils + fd + gnugrep + git + ]; +} diff --git a/pkgs/by-name/fu/fupdate-sys/fupdate-sys.sh b/pkgs/by-name/fu/fupdate-sys/fupdate-sys.sh new file mode 100755 index 00000000..57ced766 --- /dev/null +++ b/pkgs/by-name/fu/fupdate-sys/fupdate-sys.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env dash + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +# FIXME(@bpeetz): Ideally I could replace this script with a deployment tool. Thus we +# would have the same tool on the server as I use in my config. <2025-04-14> + +# Shell library {{{ +die() { + error "$1" + if [ -n "$2" ]; then + exit "$2" + else + exit 1 + fi +} +print() { + # shellcheck disable=SC2059 + printf "$*" +} +println() { + # shellcheck disable=SC2059 + printf "$*\n" +} +eprint() { + >&2 print "$@" +} +eprintln() { + >&2 println "$@" +} +if [ "${NO_COLOR-unset}" != "unset" ]; then + error() { + eprintln "==> ERROR:" "$*" + } + warning() { + eprintln "==> WARNING:" "$*" + } + debug() { + [ -n "$SHELL_LIBRARY_DEBUG" ] && eprintln "==> [Debug:]" "$*" + } + debug2() { + [ -n "$SHELL_LIBRARY_DEBUG" ] && eprintln " -> [Debug:]" "$*" + } + msg() { + eprintln "==>" "$*" + } + msg2() { + eprintln " ->" "$*" + } + prompt() { + eprint "..>" "$*" + } +else + error() { + eprintln "\033[1;91m==> ERROR:\033[0m" "\033[1;93m$*\033[0m" + } + warning() { + eprintln "\033[1;91m==> WARNING:\033[0m" "\033[1;93m$*\033[0m" + } + debug() { + [ -n "$SHELL_LIBRARY_DEBUG" ] && eprintln "\033[1;94m==> [Debug:]\033[0m" "\033[1;93m$*\033[0m" + } + debug2() { + [ -n "$SHELL_LIBRARY_DEBUG" ] && eprintln "\033[1;94m -> [Debug:]\033[0m" "\033[1;93m$*\033[0m" + } + msg() { + eprintln "\033[1;96m==>\033[0m" "\033[1;93m$*\033[0m" + } + msg2() { + eprintln "\033[1;96m ->\033[0m" "\033[1;93m$*\033[0m" + } + prompt() { + eprint "\033[1;96m..>\033[0m" "\033[1;93m$*\033[0m" + } +fi +# }}} + +NAME="update-sys" +help() { + cat <<EOF +This is a NixOS System flake update manager. + +USAGE: + $NAME [--branch <branchname>] [--help] + +OPTIONS: + --branch | -b BRANCHNAME + select a branch to update from. + + --mode | -m MODE + select a mode to update with + + --help | -h + output this help. +ARGUMENTS: + BRANCHNAME := [[ git branch --list --format '%(refname:short)' ]] + The name of the branch to deploy the config from + + MODE := switch|boot|test|build|dry-build|dry-activate|edit|repl|build-vm|build-vm-with-bootloader + See the 'nixos-rebuild' manpage for more information about these modes. +EOF + exit "$1" +} +BRANCH="" + +while [ "$#" -gt 0 ]; do + case "$1" in + "--help" | "-h") + help 0 + ;; + "--branch" | "-b") + if [ "${2-unset}" != "unset" ]; then + BRANCH="$2" + else + error "$1 requires an argument" + help 1 + fi + shift 2 + ;; + "--mode" | "-m") + if [ "${2-unset}" != "unset" ]; then + MODE="$2" + else + error "$1 requires an argument" + help 1 + fi + shift 2 + ;; + *) + error "the option $1 does not exist!" + help 1 + ;; + esac +done + +cd /etc/nixos || die "No /etc/nixos" +msg "Starting system update..." +git remote update origin --prune >/dev/null 2>&1 +if ! [ "$BRANCH" = "" ]; then + git switch "$BRANCH" >/dev/null 2>&1 && msg2 "Switched to branch '$BRANCH'" +fi +msg2 "Updating git repository..." +git pull --rebase + +# We use a tempfile, to make this truly async. +default_branch=$(mktemp -t fupdate_flake_XXXX) +cleanup() { + rm "$default_branch" +} +trap cleanup EXIT + +git remote show origin | grep 'HEAD' | cut -d':' -f2 | sed -e 's/^ *//g' -e 's/ *$//g' >"$default_branch" & + +msg2 "Updating system..." +nixos-rebuild "${MODE-switch}" + +git switch "$(cat "$default_branch")" >/dev/null 2>&1 && msg2 "Switched to branch '$(cat "$default_branch")'" +msg "Finished Update!" + +# vim: ft=sh diff --git a/pkgs/by-name/fu/fupdate-sys/package.nix b/pkgs/by-name/fu/fupdate-sys/package.nix new file mode 100644 index 00000000..7fd4674b --- /dev/null +++ b/pkgs/by-name/fu/fupdate-sys/package.nix @@ -0,0 +1,38 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + writeShellApplication, + # Dependencies + git, + nixos-rebuild, + sudo, + openssh, + coreutils, + mktemp, + gnugrep, + gnused, + systemd, +}: +writeShellApplication { + name = "fupdate-sys"; + text = builtins.readFile ./fupdate-sys.sh; + inheritPath = false; + runtimeInputs = [ + git + nixos-rebuild + sudo + openssh + coreutils + mktemp + gnugrep + gnused + systemd + ]; +} diff --git a/pkgs/by-name/fu/fupdate/.envrc b/pkgs/by-name/fu/fupdate/.envrc new file mode 100644 index 00000000..294de504 --- /dev/null +++ b/pkgs/by-name/fu/fupdate/.envrc @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use flake diff --git a/pkgs/by-name/fu/fupdate/.gitignore b/pkgs/by-name/fu/fupdate/.gitignore new file mode 100644 index 00000000..f255eebd --- /dev/null +++ b/pkgs/by-name/fu/fupdate/.gitignore @@ -0,0 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +/target +.direnv diff --git a/pkgs/by-name/fu/fupdate/Cargo.lock b/pkgs/by-name/fu/fupdate/Cargo.lock new file mode 100644 index 00000000..9e72636a --- /dev/null +++ b/pkgs/by-name/fu/fupdate/Cargo.lock @@ -0,0 +1,304 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +dependencies = [ + "clap", + "clap_lex", + "is_executable", + "shlex", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "fupdate" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "clap_complete", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/pkgs/by-name/fu/fupdate/Cargo.toml b/pkgs/by-name/fu/fupdate/Cargo.toml new file mode 100644 index 00000000..b62cee51 --- /dev/null +++ b/pkgs/by-name/fu/fupdate/Cargo.toml @@ -0,0 +1,81 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +[package] +name = "fupdate" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.100" +clap = { version = "4.5.54", features = ["derive"] } +clap_complete = { version = "4.5.65", features = ["unstable-dynamic"] } + +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +split-debuginfo = "off" + +[lints.rust] +# rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html +warnings = "warn" +future_incompatible = { level = "warn", priority = -1 } +let_underscore = { level = "warn", priority = -1 } +nonstandard_style = { level = "warn", priority = -1 } +rust_2018_compatibility = { level = "warn", priority = -1 } +rust_2018_idioms = { level = "warn", priority = -1 } +rust_2021_compatibility = { level = "warn", priority = -1 } +unused = { level = "warn", priority = -1 } +# rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html +# missing_docs = "warn" +macro_use_extern_crate = "warn" +meta_variable_misuse = "warn" +missing_abi = "warn" +missing_copy_implementations = "warn" +missing_debug_implementations = "warn" +non_ascii_idents = "warn" +noop_method_call = "warn" +single_use_lifetimes = "warn" +trivial_casts = "warn" +trivial_numeric_casts = "warn" +unreachable_pub = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_crate_dependencies = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" +variant_size_differences = "warn" + +[lints.rustdoc] +# rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html +broken_intra_doc_links = "warn" +private_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +private_doc_tests = "warn" +invalid_codeblock_attributes = "warn" +invalid_rust_codeblocks = "warn" +bare_urls = "warn" + +[lints.clippy] +# clippy allowed by default +dbg_macro = "warn" +# clippy categories https://doc.rust-lang.org/clippy/ +all = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +missing_panics_doc = "allow" +missing_errors_doc = "allow" diff --git a/pkgs/by-name/fu/fupdate/flake.lock b/pkgs/by-name/fu/fupdate/flake.lock new file mode 100644 index 00000000..1e997998 --- /dev/null +++ b/pkgs/by-name/fu/fupdate/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1768661221, + "narHash": "sha256-MJwOjrIISfOpdI9x4C+5WFQXvHtOuj5mqLZ4TMEtk1M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3327b113f2ef698d380df83fbccefad7e83d7769", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/pkgs/by-name/fu/fupdate/flake.lock.license b/pkgs/by-name/fu/fupdate/flake.lock.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/fu/fupdate/flake.lock.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/fu/fupdate/flake.nix b/pkgs/by-name/fu/fupdate/flake.nix new file mode 100644 index 00000000..f06e27ec --- /dev/null +++ b/pkgs/by-name/fu/fupdate/flake.nix @@ -0,0 +1,34 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + description = "This is a Nix flake update manager."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = {nixpkgs, ...}: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages."${system}"; + in { + devShells."${system}".default = pkgs.mkShell { + packages = [ + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt + + pkgs.cargo-edit + ]; + }; + }; +} +# vim: ts=2 + diff --git a/pkgs/by-name/fu/fupdate/fupdate.1.md b/pkgs/by-name/fu/fupdate/fupdate.1.md deleted file mode 100644 index 710e8fb7..00000000 --- a/pkgs/by-name/fu/fupdate/fupdate.1.md +++ /dev/null @@ -1,70 +0,0 @@ -% FUPDATE(1) fupdate 1.0.0 -% Soispha -% May 2023 - -# NAME - -fupdate - updates your flake, while checking for common mistakes - -# SYNOPSIS - -**fupdate** list of \[*flake*|*\<some word>*|*--help*|*-h*\] - -# DESCRIPTION - -Argument can be stacked, this makes it possible to specify multiple targets to be updated in succession. See the Examples section for further details. - -No argument or *flake* -: **fupdate**, when executed without arguments or with *flake*, will update your *flake.lock*, check for duplicate flake inputs, i.e., an input has an input declared, which you have also declared as input, and will run a script called *update.sh*, if you allow it. -The allowance for the script is asked, when you run **fupdate** and the found script is not yet allowed. Furthermore, the allowance is based on the concrete sha256 hash of the script, so any changes will require another allowance. - -**\<some word>** as argument -: If the executable **update-\<some word>** is reachable thought the PATH variable, than this is run. Otherwise, the program will exit. - -# OPTIONS - -**--help**, **-h** -: Displays a help message and exit. - -**--version**, **-v** -: Displays the software version and exit. - -# EXAMPLES - -**fupdate** or **fupdate flake** -: Updates your *flake.lock*. See the Description section for further details. - -**fupdate sys** -: Run the executable **update-sys**, if it exists. See the Description section for further details. - -**fupdate flake sys docs** -: First updates your flake, then, if the command succeeded, runs **update-sys**, afterweich **update-docs** is run. - -# FILES - -*update.sh* -: This is supposed to be a shell script located in your flake base directory, i.e., the directory which contains both a *flake.nix* and a *flake.lock* file. - -*~/.local/share/flake-update/* -: **fupdate** will store the hashes to the allowed *update.sh* files here. - -# BUGS - -Report bugs to <https://codeberg.org/soispha/flake_update/issues>. - -# COPYRIGHT - -Copyright (C) 2023 Soispha - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see <https://www.gnu.org/licenses/>. diff --git a/pkgs/by-name/fu/fupdate/fupdate.sh b/pkgs/by-name/fu/fupdate/fupdate.sh deleted file mode 100755 index 4322610a..00000000 --- a/pkgs/by-name/fu/fupdate/fupdate.sh +++ /dev/null @@ -1,197 +0,0 @@ -#! /usr/bin/env dash - -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -UPDATE_SCRIPT_NAME="update.sh" -CONFIG_DIRECTORY_PATH="$HOME/.local/share/flake-update" - -# Both are used in version() -# shellcheck disable=SC2034 -AUTHORS="Soispha" -# shellcheck disable=SC2034 -YEARS="2023" - -UPDATE_SCRIPT_NOT_WANTED=false - -# Searches upward for a `UPDATE_SCRIPT_NAME` script -# Returns a path to the script if it exists, otherwise nothing is returned -check_for_update_script() { - dirname="$(search_upward_files "$UPDATE_SCRIPT_NAME")" - if [ "$dirname" ]; then - printf "%s/%s" "$dirname" "$UPDATE_SCRIPT_NAME" - fi -} - -# Checks if a given path to the update script is allowed. -# Takes the path as input -# Return 0, if allowed, 1 if not. -check_for_allowed_update_script() { - update_script="$1" - config_path="${CONFIG_DIRECTORY_PATH}${update_script}" - update_script_hash="$(sha256sum "$update_script")" - if [ -f "$config_path" ]; then - if [ "$(cat "$config_path")" = "$update_script_hash" ]; then - dbg "Recorded hash matches" - return 0 - else - dbg "Recorded hash \'$(cat "$config_path")\' does not match real hash \'$update_script_hash\', assuming not allowed" - return 1 - fi - else - dbg "Path \'$config_path\' does not exist, assuming not allowed" - return 1 - fi -} - -# Asks the user if they want to allow a given script. -# Takes the path as input -ask_to_allow_update_script() { - update_script="$1" - config_path="${CONFIG_DIRECTORY_PATH}${update_script}" - update_script_hash="$(sha256sum "$update_script")" - println "\033[2J" # clear the screen - cat "$update_script" - readp "Do you want to allow this script?[N/y]: " allow - # shellcheck disable=SC2154 - dbg "allow is: $allow" - case "$allow" in - [yY]) - dbg "allowed script" - dbg "storing contents in: $config_path" - mkdir --parents "$(dirname "$config_path")" - print "$update_script_hash" >"$config_path" - ;; - *) - UPDATE_SCRIPT_NOT_ALLOWED=true - ;; - esac -} - -# Runs the provided script and continues to update the nix flake -# Takes the path to the script and the directory to the flake as arguments -# If the path to the update script is empty, it will be ignored -update() { - update_script="$1" - flake_base_dir="$2" - shift 2 - dbg "Provided following args to update script: '$*'" - - cd "$flake_base_dir" || die "Provided dir \'$flake_base_dir\' can not be accessed" - dbg "changed directory to: $flake_base_dir" - - nix flake update - - if ! [ "$update_script" = "" ] && ! [ "$UPDATE_SCRIPT_NOT_WANTED" = "true" ]; then - "$update_script" "$@" - fi - - if grep '[^0-9]_[0-9]' flake.lock >/dev/null; then - batgrep '[^0-9]_[0-9]' flake.lock - die "Your flake.nix contains duplicate inputs!" - fi -} - -help() { - cat <<EOF -This is a Nix flake update manager. - -USAGE: - $NAME [--help | --version] [flake [--no-script] | <some other command>] - -OPTIONS: - --help | -h - Display this help and exit. - - --version | -v - Display version and copyright information and exit. - - --no-script - Avoid running the 'update.sh' script -COMMANDS: - flake - update the flake project - - <some other command> - runs a executable called "update-<some other command>", if it exists -EOF -} - -main() { - if ! [ "$UPDATE_SCRIPT_NOT_ALLOWED" = true ]; then - update_script="$(check_for_update_script)" - flake_base_dir="$(search_flake_base_dir)" # Assume, that the update script is in the base dir - dbg "update_script is: $update_script" - dbg "flake_base_dir is: $flake_base_dir" - - if [ "$update_script" = "" ]; then - update "" "$flake_base_dir" "$@" - elif check_for_allowed_update_script "$update_script" && ! [ "$update_script" = "" ]; then - update "$update_script" "$flake_base_dir" "$@" - else - ask_to_allow_update_script "$update_script" - main "$@" - fi - fi -} - -if [ "$#" -eq 0 ]; then - main - exit 0 -fi - -for input in "$@"; do - case "$input" in - "--help" | "-h") - help - exit 0 - ;; - "--version" | "-v") - version - exit 0 - ;; - "--no-script" | "-n") - UPDATE_SCRIPT_NOT_WANTED=true - ;; - "--") - end_of_cli_options=true - - # Stop processing args after that marker. - break - ;; - esac - [ "$end_of_cli_options" = "true" ] && break -done - -case "$1" in -"flake") - shift 1 - - # Filter out fupdate specific flags - while [ "$1" != "--" ]; do - # FIXME: This check allows to add a flag multiple times, but this should probably - # not be allowed <2024-03-29> - case "$1" in - "--no-script" | "-n") - shift 1 - ;; - *) - break - ;; - esac - done - - [ "$1" = "--" ] && shift 1 - main "$@" - ;; -*) - command="$1" - shift 1 - [ "$1" = "--" ] && shift 1 - if which update-"$command" >/dev/null 2>&1; then - update-"$command" "$@" - else - die "command \"update-$command\" is not executable, or does not exist" - fi - ;; -esac diff --git a/pkgs/by-name/fu/fupdate/package.nix b/pkgs/by-name/fu/fupdate/package.nix index 66372add..86eccaf7 100644 --- a/pkgs/by-name/fu/fupdate/package.nix +++ b/pkgs/by-name/fu/fupdate/package.nix @@ -1,29 +1,41 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, - dash, - lix, - gnugrep, - fd, - coreutils, - bat, # used by batgrep - bat-extras, - gnused, # required by batgrep - git, # needed to fetch through git + rustPlatform, + installShellFiles, + makeWrapper, }: -sysLib.writeShellScript { - name = "fupdate"; - src = ./fupdate.sh; - generateCompletions = true; - keepPath = true; - dependencies = [ - dash - lix - gnugrep - fd - coreutils - bat # used by batgrep - bat-extras.batgrep - gnused # required by batgrep - git # needed to fetch through git +rustPlatform.buildRustPackage (finalAttrs: { + pname = "fupdate"; + version = "0.1.0"; + + src = ./.; + cargoLock = { + lockFile = ./Cargo.lock; + }; + + buildInputs = []; + + nativeBuildInputs = [ + installShellFiles + makeWrapper ]; -} + + postInstall = '' + installShellCompletion --cmd fupdate \ + --bash <(COMPLETE=bash $out/bin/fupdate) \ + --fish <(COMPLETE=fish $out/bin/fupdate) \ + --zsh <(COMPLETE=zsh $out/bin/fupdate) + ''; + + meta = { + mainProgram = "fupdate"; + }; +}) diff --git a/pkgs/by-name/fu/fupdate/src/cli.rs b/pkgs/by-name/fu/fupdate/src/cli.rs new file mode 100644 index 00000000..6ebd1bc4 --- /dev/null +++ b/pkgs/by-name/fu/fupdate/src/cli.rs @@ -0,0 +1,56 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{env, ffi::OsStr, fs::read_dir}; + +use clap::Parser; +use clap_complete::{engine::ArgValueCompleter, CompletionCandidate}; + +/// This is a Nix flake update manager. +#[derive(Parser, Debug)] +#[command(author, version, about)] +pub struct CliArgs { + /// The command to execute. + #[arg(add = ArgValueCompleter::new(get_fupdate_commands))] + pub command: Vec<String>, +} + +fn get_fupdate_commands(current: &OsStr) -> Vec<CompletionCandidate> { + let mut output = vec![]; + let path = env::var("PATH").unwrap_or_default(); + + let Some(current) = current.to_str() else { + return output; + }; + + for directory in path.split(':') { + if let Ok(mut read) = read_dir(directory) { + for value in read.by_ref().flatten() { + let file_name = value.file_name(); + let name = file_name.to_string_lossy(); + let Some(stripped) = name.strip_prefix("fupdate-") else { + continue; + }; + + if stripped.starts_with(current) { + output.push(CompletionCandidate::new( + value + .file_name() + .to_string_lossy() + .strip_prefix("fupdate-") + .expect("Exists"), + )); + } + } + } + } + + output +} diff --git a/pkgs/by-name/fu/fupdate/src/main.rs b/pkgs/by-name/fu/fupdate/src/main.rs new file mode 100644 index 00000000..b4af6cd6 --- /dev/null +++ b/pkgs/by-name/fu/fupdate/src/main.rs @@ -0,0 +1,57 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use clap::{CommandFactory, Parser}; + +pub mod cli; + +use crate::cli::CliArgs; + +fn main() -> Result<(), anyhow::Error> { + clap_complete::CompleteEnv::with_factory(CliArgs::command).complete(); + + let args = CliArgs::parse(); + + let command = args.command.first().map_or("flake", String::as_str); + + { + let args = if args.command.len() > 1 { + &args.command[1..] + } else { + &[] + }; + + // println!("Running: `fupdate-{command} {}`", args.join(" ")); + + let child = Command::new(format!("fupdate-{command}")) + .args(args) + .status() + .with_context(|| format!("Failed to spawn `fupdate-{command}`"))?; + + if !child.success() { + bail!("Command `fupdate-{command} {}` failed!", args.join(" ")); + } + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use clap::CommandFactory; + + #[test] + fn verify_cli() { + super::CliArgs::command().debug_assert(); + } +} diff --git a/pkgs/by-name/fu/fupdate/update.sh b/pkgs/by-name/fu/fupdate/update.sh new file mode 100755 index 00000000..8e36e13e --- /dev/null +++ b/pkgs/by-name/fu/fupdate/update.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +[ "$1" = "upgrade" ] && cargo upgrade +cargo update diff --git a/pkgs/by-name/ge/generate_moz_extension/.envrc b/pkgs/by-name/ge/generate_moz_extension/.envrc deleted file mode 100644 index 2f9f1a81..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/.envrc +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -use flake diff --git a/pkgs/by-name/ge/generate_moz_extension/.gitignore b/pkgs/by-name/ge/generate_moz_extension/.gitignore deleted file mode 100644 index f717ddd7..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -/result -.direnv diff --git a/pkgs/by-name/ge/generate_moz_extension/Cargo.lock b/pkgs/by-name/ge/generate_moz_extension/Cargo.lock deleted file mode 100644 index 10970fa6..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/Cargo.lock +++ /dev/null @@ -1,1651 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "anyhow" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cc" -version = "1.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generate_extensions" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures", - "reqwest", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "h2" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "linux-raw-sys" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" - -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "openssl" -version = "0.10.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" - -[[package]] -name = "reqwest" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-registry", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustix" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - -[[package]] -name = "rustls-webpki" -version = "0.103.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" -dependencies = [ - "fastrand", - "getrandom 0.3.2", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/pkgs/by-name/ge/generate_moz_extension/Cargo.toml b/pkgs/by-name/ge/generate_moz_extension/Cargo.toml deleted file mode 100644 index 1b3ff4f0..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "generate_extensions" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.97" -futures = "0.3.31" -reqwest = "0.12.15" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } diff --git a/pkgs/by-name/ge/generate_moz_extension/examples/generate_extensions.sh b/pkgs/by-name/ge/generate_moz_extension/examples/generate_extensions.sh deleted file mode 100755 index 96802992..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/examples/generate_extensions.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -tmp=$(mktemp) -cat <<EOF | awk '!/^\s*#/' >"$tmp" - darkreader:navbar - keepassxc-browser:navbar - vhack-libredirect:navbar - # torproject-snowflake:navbar - tridactyl-vim:menupanel - ublock-origin:menupanel -EOF - -# The cat execution should be unquoted; -# shellcheck disable=SC2046 -cargo run -- $(cat "$tmp") - -rm "$tmp" diff --git a/pkgs/by-name/ge/generate_moz_extension/flake.lock b/pkgs/by-name/ge/generate_moz_extension/flake.lock deleted file mode 100644 index 9d72fb5b..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/flake.lock +++ /dev/null @@ -1,98 +0,0 @@ -{ - "nodes": { - "crane": { - "locked": { - "lastModified": 1742394900, - "narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1743076231, - "narHash": "sha256-yQugdVfi316qUfqzN8JMaA2vixl+45GxNm4oUfXlbgw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6c5963357f3c1c840201eda129a99d455074db04", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "crane": "crane", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1743215516, - "narHash": "sha256-52qbrkG65U1hyrQWltgHTgH4nm0SJL+9TWv2UDCEPNI=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "524463199fdee49338006b049bc376b965a2cfed", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/pkgs/by-name/ge/generate_moz_extension/flake.nix b/pkgs/by-name/ge/generate_moz_extension/flake.nix deleted file mode 100644 index 5575f90b..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/flake.nix +++ /dev/null @@ -1,75 +0,0 @@ -{ - description = "A simple way to query the mozialla api for extension data"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - - crane = { - url = "github:ipetkov/crane"; - inputs = { - nixpkgs.follows = "nixpkgs"; - }; - }; - - flake-utils.url = "github:numtide/flake-utils"; - - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs = { - nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; - }; - }; - }; - - outputs = { - self, - nixpkgs, - crane, - flake-utils, - rust-overlay, - ... - }: - flake-utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { - inherit system; - overlays = [(import rust-overlay)]; - }; - - rust-stable = pkgs.rust-bin.stable.latest.default; - rust-minimal = pkgs.rust-bin.stable.latest.minimal; - - craneLib = (crane.mkLib pkgs).overrideToolchain rust-minimal; - - buildInputs = [ - pkgs.openssl # needed for openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config # needed for openssl - ]; - - craneBuild = craneLib.buildPackage { - src = craneLib.cleanCargoSource ./.; - inherit buildInputs nativeBuildInputs; - - doCheck = true; - }; - in { - packages.default = craneBuild; - app.default = { - type = "app"; - program = "${self.packages.${system}.default}/bin/generate_extensions"; - }; - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - cocogitto - - rust-stable - cargo-edit - ]; - inherit buildInputs nativeBuildInputs; - }; - }); -} -# vim: ts=2 - diff --git a/pkgs/by-name/ge/generate_moz_extension/package.nix b/pkgs/by-name/ge/generate_moz_extension/package.nix deleted file mode 100644 index abd95c77..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/package.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ - rustPlatform, - openssl, - pkg-config, -}: -rustPlatform.buildRustPackage { - pname = "generate_firefox_extensions"; - version = "0.1.0"; - - src = ./.; - cargoLock = { - lockFile = ./Cargo.lock; - }; - buildInputs = [ - openssl # needed for openssl-sys crate - ]; - nativeBuildInputs = [ - pkg-config # needed for openssl dependency - ]; -} diff --git a/pkgs/by-name/ge/generate_moz_extension/res/generate_extensions.py b/pkgs/by-name/ge/generate_moz_extension/res/generate_extensions.py deleted file mode 100644 index ee8cc966..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/res/generate_extensions.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# source: https://github.com/etu/nixconfig/blob/ba47d577c8bfb4a1c06927c34ece34118f4a0460/modules/graphical/firefox/generate.py - -from concurrent.futures import ThreadPoolExecutor -import json -import requests - -EXTENSIONS = sorted( - [ - "darkreader", - "firenvim", - "keepassxc-browser", - "simple-tab-groups", - ] -) - - -def index_ext(ext: str): - # print(f"Indexing {ext}...") - - resp = requests.get(f"https://addons.mozilla.org/api/v5/addons/addon/{ext}/").json() - rel = resp["current_version"] - - if not rel["file"]["hash"].startswith("sha256:"): - raise ValueError("Unhandled hash type") - - return { - "pname": ext, - "version": rel["version"], - "addonId": resp["guid"], - "url": rel["file"]["url"], - "sha256": rel["file"]["hash"], - } - - -if __name__ == "__main__": - # outfile = os.path.dirname(os.path.realpath(__file__)) + "/extensions.json" - - with ThreadPoolExecutor() as e: - extensions = {ext: e.submit(index_ext, ext) for ext in EXTENSIONS} - extensions = {k: v.result() for k, v in extensions.items()} - - # with open(outfile, "w") as f: - print(json.dumps(extensions, indent=2)) diff --git a/pkgs/by-name/ge/generate_moz_extension/res/reference.json b/pkgs/by-name/ge/generate_moz_extension/res/reference.json deleted file mode 100644 index f46ea8ec..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/res/reference.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "darkreader": { - "pname": "darkreader", - "version": "4.9.62", - "addonId": "addon@darkreader.org", - "url": "https://addons.mozilla.org/firefox/downloads/file/4053589/darkreader-4.9.62.xpi", - "sha256": "sha256:e537a2cee45ed7c26f79ecd3ed362620e3f00d24c158532a58e163a63a3d60cc" - }, - "firenvim": { - "pname": "firenvim", - "version": "0.2.14", - "addonId": "firenvim@lacamb.re", - "url": "https://addons.mozilla.org/firefox/downloads/file/4026386/firenvim-0.2.14.xpi", - "sha256": "sha256:a8c495a59e30eaabbb3fcd188db9b5e28b40bffefe41a3f0fa22ecc58c80c2b6" - }, - "keepassxc-browser": { - "pname": "keepassxc-browser", - "version": "1.8.4", - "addonId": "keepassxc-browser@keepassxc.org", - "url": "https://addons.mozilla.org/firefox/downloads/file/4045866/keepassxc_browser-1.8.4.xpi", - "sha256": "sha256:cc39aa058cb8915cfc88424e2e1cebe3ccfc3f95d7bddb2abd0c4905d2b17719" - }, - "simple-tab-groups": { - "pname": "simple-tab-groups", - "version": "4.7.2.1", - "addonId": "simple-tab-groups@drive4ik", - "url": "https://addons.mozilla.org/firefox/downloads/file/3873608/simple_tab_groups-4.7.2.1.xpi", - "sha256": "sha256:75077589098ca62c00b86cf9554c6120bf8dc04c5f916fe26f84915f5147b2a4" - } -} diff --git a/pkgs/by-name/ge/generate_moz_extension/res/test.json b/pkgs/by-name/ge/generate_moz_extension/res/test.json deleted file mode 100644 index daa1d19a..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/res/test.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "darkreader": { - "addon_id": "addon@darkreader.org", - "pname": "darkreader", - "sha256": "sha256:e537a2cee45ed7c26f79ecd3ed362620e3f00d24c158532a58e163a63a3d60cc", - "url": "https://addons.mozilla.org/firefox/downloads/file/4053589/darkreader-4.9.62.xpi", - "version": "4.9.62" - }, - "firenvim": { - "addon_id": "firenvim@lacamb.re", - "pname": "firenvim", - "sha256": "sha256:a8c495a59e30eaabbb3fcd188db9b5e28b40bffefe41a3f0fa22ecc58c80c2b6", - "url": "https://addons.mozilla.org/firefox/downloads/file/4026386/firenvim-0.2.14.xpi", - "version": "0.2.14" - }, - "keepassxc-browser": { - "addon_id": "keepassxc-browser@keepassxc.org", - "pname": "keepassxc-browser", - "sha256": "sha256:cc39aa058cb8915cfc88424e2e1cebe3ccfc3f95d7bddb2abd0c4905d2b17719", - "url": "https://addons.mozilla.org/firefox/downloads/file/4045866/keepassxc_browser-1.8.4.xpi", - "version": "1.8.4" - }, - "simple-tab-groups": { - "addon_id": "simple-tab-groups@drive4ik", - "pname": "simple-tab-groups", - "sha256": "sha256:75077589098ca62c00b86cf9554c6120bf8dc04c5f916fe26f84915f5147b2a4", - "url": "https://addons.mozilla.org/firefox/downloads/file/3873608/simple_tab_groups-4.7.2.1.xpi", - "version": "4.7.2.1" - } -} diff --git a/pkgs/by-name/ge/generate_moz_extension/src/main.rs b/pkgs/by-name/ge/generate_moz_extension/src/main.rs deleted file mode 100644 index bde986a3..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/src/main.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::env::args; - -use anyhow::{bail, Context}; -use futures::StreamExt; -use reqwest::Client; -use serde_json::{json, Map, Value}; - -pub mod types; - -macro_rules! get_json_value { - ($key:expr, $json_value:ident, $type:ident, $get:ident) => { - match $json_value.get($key) { - Some(resp) => { - let resp = resp.to_owned(); - if resp.$type() { - resp.$get().expect( - "The should have been checked in the if guard, so unpacking here is fine", - ).to_owned() - } else { - bail!( - "Value {} => \n{}\n is not of type: {}", - $key, - resp, - stringify!($type) - ); - } - } - None => { - bail!( - "There seems to be no '{}' in your json data (json value: '{}')\n Has the api changend?", - $key, serde_json::to_string_pretty(&$json_value).expect("Will always work") - ); - } - } - }; -} - -use futures::stream::futures_unordered::FuturesUnordered; -use types::{Extension, InputExtension}; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let mut extensions: Vec<InputExtension> = vec![]; - for input_extension in args() - .skip(1) - .map(|str| InputExtension::try_from(str)) - .collect::<Vec<anyhow::Result<InputExtension>>>() - { - extensions.push(input_extension?); - } - - let resulting_extensions = process_extensions(extensions).await?; - - let mut output = Map::new(); - for extension in resulting_extensions { - output.insert(extension.pname.clone(), json!(extension)); - } - - println!( - "{}", - serde_json::to_string_pretty(&serde_json::Value::Object(output)).expect( - "This is constructed from json, it should also be possible to serialize it again" - ) - ); - Ok(()) -} - -async fn process_extensions(extensions: Vec<InputExtension>) -> anyhow::Result<Vec<Extension>> { - let mut output = Vec::with_capacity(extensions.len()); - - let client = Client::new(); - for extension in extensions - .iter() - .map(|ext| { - let local_client = &client; - index_extension(ext, local_client) - }) - .collect::<FuturesUnordered<_>>() - .collect::<Vec<_>>() - .await - { - output.push(extension?); - } - Ok(output) -} - -async fn index_extension(extension: &InputExtension, client: &Client) -> anyhow::Result<Extension> { - let response = client - .get(format!( - "https://addons.mozilla.org/api/v5/addons/addon/{}", - extension, - )) - .send() - .await - .context("Accessing the mozzila extenios api failed with error: {e}")?; - - eprintln!("Indexing {} ({})...", extension, response.status()); - let response: Value = serde_json::from_str( - &response - .text() - .await - .context("Turning the response to text fail with error: {e}")?, - ) - .context("Deserializing the response failed! Error: {e}")?; - - if let Some(detail) = response.get("detail") { - if detail == "Not found." { - bail!("Your extension ('{}') was not found!", extension); - } - }; - - let release = { get_json_value!("current_version", response, is_object, as_object) }; - - #[allow(non_snake_case)] - let addonId = { get_json_value!("guid", response, is_string, as_str) }; - - let version = { get_json_value!("version", release, is_string, as_str) }; - let file = { get_json_value!("file", release, is_object, as_object) }; - - let url = { get_json_value!("url", file, is_string, as_str) }; - let sha256 = { - let hash = get_json_value!("hash", file, is_string, as_str); - if hash.starts_with("sha256:") { - hash - } else { - bail!("This hash type is unhandled: {}", hash); - } - }; - - Ok(Extension { - pname: extension.moz_name.clone(), - default_area: extension.default_area, - version, - addonId, - url, - sha256, - }) -} diff --git a/pkgs/by-name/ge/generate_moz_extension/src/types.rs b/pkgs/by-name/ge/generate_moz_extension/src/types.rs deleted file mode 100644 index b830fe0d..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/src/types.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::fmt::Display; - -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct Extension { - pub pname: String, - pub default_area: DefaultArea, - pub version: String, - pub addonId: String, - pub url: String, - pub sha256: String, -} - -#[derive(Debug, Clone)] -pub struct InputExtension { - pub moz_name: String, - pub default_area: DefaultArea, -} -#[derive(Debug, Clone, Copy, Deserialize, Serialize)] -#[allow(non_camel_case_types)] -pub enum DefaultArea { - navbar, - menupanel, -} - -impl Display for DefaultArea { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DefaultArea::navbar => f.write_str("navbar"), - DefaultArea::menupanel => f.write_str("menupanel"), - } - } -} - -impl TryFrom<&str> for DefaultArea { - type Error = anyhow::Error; - - fn try_from(value: &str) -> Result<Self, Self::Error> { - match value { - "navbar" => Ok(Self::navbar), - "menupanel" => Ok(Self::menupanel), - _ => Err(anyhow!( - "Your <default_area> needs to be one of 'navbar' or 'menupanel', but is: '{}'", - value - )), - } - } -} - -impl Display for InputExtension { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.moz_name) - } -} -impl TryFrom<String> for InputExtension { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result<Self, Self::Error> { - if let Some((moz_name, default_area)) = value.split_once(':') { - Ok(Self { - moz_name: moz_name.to_owned(), - default_area: default_area.try_into()?, - }) - } else { - Err(anyhow!("Can't parse the input string as a InputExtension!\n Needs to be: '<moz_name>:<default_area>'")) - } - } -} diff --git a/pkgs/by-name/ge/generate_moz_extension/update.sh b/pkgs/by-name/ge/generate_moz_extension/update.sh deleted file mode 100755 index b9404867..00000000 --- a/pkgs/by-name/ge/generate_moz_extension/update.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env sh - -[ "$1" = "upgrade" ] && cargo upgrade -cargo update - -# vim: ft=sh diff --git a/pkgs/by-name/gi/git-cm/git-cm.sh b/pkgs/by-name/gi/git-cm/git-cm.sh index 2204e4d6..7ab957df 100755 --- a/pkgs/by-name/gi/git-cm/git-cm.sh +++ b/pkgs/by-name/gi/git-cm/git-cm.sh @@ -1,7 +1,14 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. ROOT="$(git rev-parse --show-toplevel)" @@ -13,6 +20,6 @@ else fi sed '1d' "$(git config commit.template)" >>"$ROOT/.git/COMMIT_TEMPLATE" -git commit --template "$ROOT/.git/COMMIT_TEMPLATE" --verbose "$@" +git commit --template "$ROOT/.git/COMMIT_TEMPLATE" "$@" # vim: ft=sh diff --git a/pkgs/by-name/gi/git-cm/package.nix b/pkgs/by-name/gi/git-cm/package.nix index a9949783..f124ce90 100644 --- a/pkgs/by-name/gi/git-cm/package.nix +++ b/pkgs/by-name/gi/git-cm/package.nix @@ -1,13 +1,26 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies git, gnused, }: -sysLib.writeShellScript { +writeShellApplication { name = "git-cm"; - src = ./git-cm.sh; - keepPath = true; - dependencies = [ + text = builtins.readFile ./git-cm.sh; + + # We need access to the $EDITOR + inheritPath = true; + + runtimeInputs = [ git gnused ]; diff --git a/pkgs/by-name/gi/git-edit-index/git-edit-index.sh b/pkgs/by-name/gi/git-edit-index/git-edit-index.sh index e73dc53c..a9434381 100755 --- a/pkgs/by-name/gi/git-edit-index/git-edit-index.sh +++ b/pkgs/by-name/gi/git-edit-index/git-edit-index.sh @@ -1,19 +1,20 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -# needed for help() and version -# shellcheck disable=2034 -AUTHORS="Soispha" -# shellcheck disable=2034 -YEARS="2024" -# shellcheck disable=2034 -VERSION="1.0.0" - -# NAME is from the wrapper -# shellcheck disable=SC2269 -NAME="$NAME" +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +NAME="git-edit-index" + +warn() { + echo "WARNING: $1" +} help() { cat <<EOF @@ -55,37 +56,35 @@ materialize_file() { } edit() { - files_to_add="$(mktmp)" - realpath --relative-to=. "$@" >"$files_to_add" + files_to_add="$(mktemp -t git_edit_index_XXXXX)" + cleanup() { + rm "$files_to_add" + } + trap cleanup EXIT - index_files="$(mktmp)" - git diff --name-only --cached --diff-filter=AM >"$index_files" + realpath --relative-to=. "$@" >"$files_to_add" - while read -r file; do - if grep -q "$file" "$files_to_add"; then - sed -i "s|$file||" "$files_to_add" - materialize_file "$file" + git diff --name-only --cached --diff-filter=AM | while read -r index_file; do + if grep -q "$index_file" "$files_to_add"; then + sed -i "s|$index_file||" "$files_to_add" + materialize_file "$index_file" fi - done <"$index_files" + done - files_to_check="$(mktmp)" - clean "$files_to_add" >"$files_to_check" - if [ "$(wc -l <"$files_to_check")" -gt 0 ]; then - warn "Could not edit every file:" + unedided_files="$(sed '/^\s*$/d' "$files_to_add" | wc -l)" + if [ "$unedided_files" -gt 0 ]; then + warn "Failed to edit $unedided_files file(s):" cat "$files_to_add" fi } +end_of_cli_options=false for arg in "$@"; do case "$arg" in "--help" | "-h") help exit 0 ;; - "--version" | "-v") - version - exit 0 - ;; "--") end_of_cli_options=true ;; diff --git a/pkgs/by-name/gi/git-edit-index/package.nix b/pkgs/by-name/gi/git-edit-index/package.nix index 8ac085bf..5e855b49 100644 --- a/pkgs/by-name/gi/git-edit-index/package.nix +++ b/pkgs/by-name/gi/git-edit-index/package.nix @@ -1,19 +1,29 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies + coreutils, git, gnused, }: -sysLib.writeShellScript { +writeShellApplication { name = "git-edit-index"; - src = ./git-edit-index.sh; - generateCompletions = true; + text = builtins.readFile ./git-edit-index.sh; # `git-edit-index` starts neovim, wich might want to shell out from - keepPath = true; + inheritPath = true; - dependencies = [ + runtimeInputs = [ + coreutils git gnused - # $EDITOR ]; } diff --git a/pkgs/by-name/hi/hibernate/hibernate.sh b/pkgs/by-name/hi/hibernate/hibernate.sh index 30868fd1..4a68e0d7 100755 --- a/pkgs/by-name/hi/hibernate/hibernate.sh +++ b/pkgs/by-name/hi/hibernate/hibernate.sh @@ -1,7 +1,16 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +# TODO(@bpeetz): This functionality could be moved to `tskm`. <2025-04-14> context="$(task _get rc.context)" [ "$context" ] && task context none diff --git a/pkgs/by-name/hi/hibernate/package.nix b/pkgs/by-name/hi/hibernate/package.nix index 54716606..c2e8c0b6 100644 --- a/pkgs/by-name/hi/hibernate/package.nix +++ b/pkgs/by-name/hi/hibernate/package.nix @@ -1,14 +1,22 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, systemd, taskwarrior3, }: -sysLib.writeShellScript { +writeShellApplication { name = "hibernate"; - src = ./hibernate.sh; - generateCompletions = false; - keepPath = false; - dependencies = [ + text = builtins.readFile ./hibernate.sh; + inheritPath = false; + runtimeInputs = [ systemd taskwarrior3 ]; diff --git a/pkgs/by-name/i3/i3bar-river-patched/0001-feat-crate-bar-Put-the-leftmost-block-in-the-middle-.patch b/pkgs/by-name/i3/i3bar-river-patched/0001-feat-crate-bar-Put-the-leftmost-block-in-the-middle-.patch new file mode 100644 index 00000000..7bfdd7bc --- /dev/null +++ b/pkgs/by-name/i3/i3bar-river-patched/0001-feat-crate-bar-Put-the-leftmost-block-in-the-middle-.patch @@ -0,0 +1,110 @@ +From 8ae692a461fad2f23231d50b78bb706408facfe6 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Tue, 20 May 2025 19:58:57 +0200 +Subject: [PATCH] feat(crate::bar): Put the leftmost block in the middle of the + bar + +This is a workaround for the limitation in the i3 blocks protocol, as +this does not allow for centred blocks. +--- + src/bar.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++---------- + 1 file changed, 52 insertions(+), 11 deletions(-) + +diff --git a/src/bar.rs b/src/bar.rs +index 96533e3..76f8025 100644 +--- a/src/bar.rs ++++ b/src/bar.rs +@@ -338,16 +338,55 @@ impl Bar { + } + + // Display the blocks +- render_blocks( +- &cairo_ctx, +- &ss.config, +- palette, +- ss.blocks_cache.get_computed(), +- &mut self.blocks_btns, +- offset_left, +- width_f, +- height_f, +- ); ++ { ++ if !ss.blocks_cache.get_computed().is_empty() { ++ let first_block = &ss.blocks_cache.get_computed()[0]; ++ ++ let blocks = &ss.blocks_cache.get_computed()[1..]; ++ ++ let other_start = render_blocks( ++ &cairo_ctx, ++ &ss.config, ++ palette, ++ blocks, ++ &mut self.blocks_btns, ++ offset_left, ++ width_f, ++ height_f, ++ ); ++ ++ // Draw the first block _after_ the other ones, so that we can nudge it more to the ++ // left, if the others are spanning over the middle. ++ let mut start = (width_f / 2.0) - (first_block.full.width / 2.0); ++ if start + first_block.full.width > other_start { ++ start = other_start ++ - first_block.full.width ++ - first_block.block.separator_block_width as f64; ++ } ++ ++ first_block.full.render( ++ &cairo_ctx, ++ RenderOptions { ++ x_offset: start, ++ bar_height: height_f, ++ fg_color: first_block.block.color.unwrap_or(palette.color), ++ bg_color: first_block.block.background, ++ r_left: ss.config.blocks_r, ++ r_right: ss.config.blocks_r, ++ overlap: ss.config.blocks_overlap, ++ }, ++ ); ++ ++ self.blocks_btns.push( ++ start, ++ first_block.full.width, ++ ( ++ first_block.block.name.clone(), ++ first_block.block.instance.clone(), ++ ), ++ ); ++ } ++ } + + self.viewport + .set_destination(conn, self.width as i32, self.height as i32); +@@ -422,7 +461,7 @@ fn render_blocks( + offset_left: f64, + full_width: f64, + full_height: f64, +-) { ++) -> f64 { + context.rectangle(offset_left, 0.0, full_width - offset_left, full_height); + context.clip(); + +@@ -507,6 +546,7 @@ fn render_blocks( + } + + // Render blocks ++ let leftmost_start = full_width - blocks_width; + buttons.clear(); + for series in blocks_computed { + let s_len = series.blocks.len(); +@@ -550,6 +590,7 @@ fn render_blocks( + } + + context.reset_clip(); ++ leftmost_start + } + + fn layer_surface_cb(ctx: EventCtx<State, ZwlrLayerSurfaceV1>) { +-- +2.49.0 + diff --git a/pkgs/by-name/i3/i3bar-river-patched/package.nix b/pkgs/by-name/i3/i3bar-river-patched/package.nix new file mode 100644 index 00000000..26f11ab3 --- /dev/null +++ b/pkgs/by-name/i3/i3bar-river-patched/package.nix @@ -0,0 +1,54 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + lib, + fetchFromGitHub, + rustPlatform, + pkg-config, + pango, +}: +rustPlatform.buildRustPackage { + pname = "i3bar-river-patched"; + version = "1.1.0-unstable-2025-05-20"; + + src = fetchFromGitHub { + owner = "bpeetz"; + repo = "i3bar-river"; + rev = "d460a9a283426e9474a0034a146d09816e92f571"; + hash = "sha256-E04b2FzEhOX5NyE/VpEGdg27Sg+1+lSSRZbGyX6PXrk="; + }; + + cargoHash = "sha256-jIB4XH67FmtPxAatHkuW8v5mNgr/KsyriaBNZ5t2dLo="; + + cargoPatches = [ + # TODO(@bpeetz): Open an issues, whether something like that could be up-streamed. <2025-05-20> + ./0001-feat-crate-bar-Put-the-leftmost-block-in-the-middle-.patch + ]; + + # Remove the WMs that I don't use. + buildNoDefaultFeatures = true; + buildFeatures = [ + # "hyprland" + # "niri" + "river" + ]; + + nativeBuildInputs = [pkg-config]; + buildInputs = [pango]; + + meta = with lib; { + description = "Port of i3bar for river"; + homepage = "https://github.com/MaxVerevkin/i3bar-river"; + license = licenses.gpl3Only; + maintainers = with maintainers; [nicegamer7]; + mainProgram = "i3bar-river"; + platforms = platforms.linux; + }; +} diff --git a/pkgs/by-name/i3/i3status-rust-patched/package.nix b/pkgs/by-name/i3/i3status-rust-patched/package.nix new file mode 100644 index 00000000..a103e275 --- /dev/null +++ b/pkgs/by-name/i3/i3status-rust-patched/package.nix @@ -0,0 +1,22 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + i3status-rust, +}: +i3status-rust.overrideAttrs (final: prev: { + pname = "${prev.pname}-patched"; + + patches = + (prev.patches or []) + ++ [ + # Btrfs support for disk_space block. + ./patches/0001-disk_space-Support-btrfs-backend.patch + ]; +}) diff --git a/pkgs/by-name/i3/i3status-rust-patched/patches/0001-disk_space-Support-btrfs-backend.patch b/pkgs/by-name/i3/i3status-rust-patched/patches/0001-disk_space-Support-btrfs-backend.patch new file mode 100644 index 00000000..8ef0af2e --- /dev/null +++ b/pkgs/by-name/i3/i3status-rust-patched/patches/0001-disk_space-Support-btrfs-backend.patch @@ -0,0 +1,190 @@ +From 78d2936b67064e3b5e700a2859d00ea3dd6eda4c Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Sun, 18 May 2025 20:22:04 +0200 +Subject: [PATCH 1/3] disk_space: Support btrfs backend + +Btrfs is too smart for the statvfs based backend (i.e., only counting +blocks leads to wrong numbers). + +For example, a btrfs disk with a lot of de-duplicated blocks (via the copy +on write mechanism) might have a drastically over-reported disk usage. + +The btrfs backend is currently implemented by parsing the output of the +`btrfs filesystem usage --raw` command. This is suboptimal, as this now +relies on the command output not changing. + +Vendoring the algorithm used internally by the `btrfs` command does not +seem to be a reasonable alternative, considering that the code[1] is +rather complex, low level and would require semi-constant maintenance. +Additionally, the c code would need bindings to be usable from rust. + +I assume, that the `btrfs` command output will stay rather similar in +the future, as a lot of tools rely on directly parsing it (see the +various scripts in the issue, this commit fixes). + +[1]: https://github.com/kdave/btrfs-progs/blob/eeab081e9d9fbdf4583122ed1caedf541383cf2d/cmds/filesystem-usage.c#L442 + +Fixes: #1654 +--- + src/blocks/disk_space.rs | 112 +++++++++++++++++++++++++++++++++++---- + 1 file changed, 101 insertions(+), 11 deletions(-) + +diff --git a/src/blocks/disk_space.rs b/src/blocks/disk_space.rs +index 79bfebd27..da0d3f518 100644 +--- a/src/blocks/disk_space.rs ++++ b/src/blocks/disk_space.rs +@@ -12,6 +12,7 @@ + //! `alert` | A value which will trigger critical block state | `10.0` + //! `info_type` | Determines which information will affect the block state. Possible values are `"available"`, `"free"` and `"used"` | `"available"` + //! `alert_unit` | The unit of `alert` and `warning` options. If not set, percents are used. Possible values are `"B"`, `"KB"`, `"KiB"`, `"MB"`, `"MiB"`, `"GB"`, `"Gib"`, `"TB"` and `"TiB"` | `None` ++//! `backend` | The backend to use when querying disk usage. Possible values are `"vfs"` (like `du(1)`) and `"btrfs"` | `"vfs"` + //! + //! Placeholder | Value | Type | Unit + //! -------------|--------------------------------------------------------------------|--------|------- +@@ -63,9 +64,12 @@ + + // make_log_macro!(debug, "disk_space"); + ++use std::cell::OnceCell; ++ + use super::prelude::*; + use crate::formatting::prefix::Prefix; + use nix::sys::statvfs::statvfs; ++use tokio::process::Command; + + #[derive(Copy, Clone, Debug, Deserialize, SmartDefault)] + #[serde(rename_all = "lowercase")] +@@ -76,11 +80,20 @@ pub enum InfoType { + Used, + } + ++#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)] ++#[serde(rename_all = "lowercase")] ++pub enum Backend { ++ #[default] ++ Vfs, ++ Btrfs, ++} ++ + #[derive(Deserialize, Debug, SmartDefault)] + #[serde(deny_unknown_fields, default)] + pub struct Config { + #[default("/".into())] + pub path: ShellString, ++ pub backend: Backend, + pub info_type: InfoType, + pub format: FormatConfig, + pub format_alt: Option<FormatConfig>, +@@ -128,17 +141,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { + loop { + let mut widget = Widget::new().with_format(format.clone()); + +- let statvfs = statvfs(&*path).error("failed to retrieve statvfs")?; +- +- // Casting to be compatible with 32-bit systems +- #[allow(clippy::unnecessary_cast)] +- let (total, used, available, free) = { +- let total = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64); +- let used = ((statvfs.blocks() as u64) - (statvfs.blocks_free() as u64)) +- * (statvfs.fragment_size() as u64); +- let available = (statvfs.blocks_available() as u64) * (statvfs.block_size() as u64); +- let free = (statvfs.blocks_free() as u64) * (statvfs.block_size() as u64); +- (total, used, available, free) ++ let (total, used, available, free) = match config.backend { ++ Backend::Vfs => get_vfs(&*path)?, ++ Backend::Btrfs => get_btrfs(&path).await?, + }; + + let result = match config.info_type { +@@ -205,3 +210,88 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { + } + } + } ++ ++fn get_vfs<P>(path: &P) -> Result<(u64, u64, u64, u64)> ++where ++ P: ?Sized + nix::NixPath, ++{ ++ let statvfs = statvfs(path).error("failed to retrieve statvfs")?; ++ ++ // Casting to be compatible with 32-bit systems ++ #[allow(clippy::unnecessary_cast)] ++ { ++ let total = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64); ++ let used = ((statvfs.blocks() as u64) - (statvfs.blocks_free() as u64)) ++ * (statvfs.fragment_size() as u64); ++ let available = (statvfs.blocks_available() as u64) * (statvfs.block_size() as u64); ++ let free = (statvfs.blocks_free() as u64) * (statvfs.block_size() as u64); ++ ++ Ok((total, used, available, free)) ++ } ++} ++ ++async fn get_btrfs(path: &str) -> Result<(u64, u64, u64, u64)> { ++ const OUTPUT_CHANGED: &str = "Btrfs filesystem usage output format changed"; ++ ++ fn remove_estimate_min(estimate_str: &str) -> Result<&str> { ++ estimate_str.trim_matches('\t') ++ .split_once("\t") ++ .ok_or(Error::new(OUTPUT_CHANGED)) ++ .map(|v| v.0) ++ } ++ ++ macro_rules! get { ++ ($source:expr, $name:expr, $variable:ident) => { ++ get!(@pre_op (|a| {Ok::<_, Error>(a)}), $source, $name, $variable) ++ }; ++ (@pre_op $function:expr, $source:expr, $name:expr, $variable:ident) => { ++ if $source.starts_with(concat!($name, ":")) { ++ let (found_name, variable_str) = ++ $source.split_once(":").ok_or(Error::new(OUTPUT_CHANGED))?; ++ ++ let variable_str = $function(variable_str)?; ++ ++ debug_assert_eq!(found_name, $name); ++ $variable ++ .set(variable_str.trim().parse().error(OUTPUT_CHANGED)?) ++ .map_err(|_| Error::new(OUTPUT_CHANGED))?; ++ } ++ }; ++ } ++ ++ let filesystem_usage = Command::new("btrfs") ++ .args(["filesystem", "usage", "--raw", path]) ++ .output() ++ .await ++ .error("Failed to collect btrfs filesystem usage info")? ++ .stdout; ++ ++ { ++ let final_total = OnceCell::new(); ++ let final_used = OnceCell::new(); ++ let final_free = OnceCell::new(); ++ ++ let mut lines = filesystem_usage.lines(); ++ while let Some(line) = lines ++ .next_line() ++ .await ++ .error("Failed to read output of btrfs filesystem usage")? ++ { ++ let line = line.trim(); ++ ++ // See btrfs-filesystem(8) for an explanation for the rows. ++ get!(line, "Device size", final_total); ++ get!(line, "Used", final_used); ++ get!(@pre_op remove_estimate_min, line, "Free (estimated)", final_free); ++ } ++ ++ Ok(( ++ *final_total.get().ok_or(Error::new(OUTPUT_CHANGED))?, ++ *final_used.get().ok_or(Error::new(OUTPUT_CHANGED))?, ++ // HACK(@bpeetz): We also return the free disk space as the available one, because btrfs ++ // does not tell us which disk space is reserved for the fs. <2025-05-18> ++ *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?, ++ *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?, ++ )) ++ } ++} +-- +2.49.0 + diff --git a/pkgs/by-name/lf/lf-make-map/.envrc b/pkgs/by-name/lf/lf-make-map/.envrc index 2b742cf6..880b1809 100644 --- a/pkgs/by-name/lf/lf-make-map/.envrc +++ b/pkgs/by-name/lf/lf-make-map/.envrc @@ -1,4 +1,15 @@ #!/usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use flake || use nix watch_file flake.nix diff --git a/pkgs/by-name/lf/lf-make-map/.gitignore b/pkgs/by-name/lf/lf-make-map/.gitignore index cb87f36f..8f29eabf 100644 --- a/pkgs/by-name/lf/lf-make-map/.gitignore +++ b/pkgs/by-name/lf/lf-make-map/.gitignore @@ -1,3 +1,13 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + # build /target /result diff --git a/pkgs/by-name/lf/lf-make-map/Cargo.lock b/pkgs/by-name/lf/lf-make-map/Cargo.lock index d68f7492..1e293fde 100644 --- a/pkgs/by-name/lf/lf-make-map/Cargo.lock +++ b/pkgs/by-name/lf/lf-make-map/Cargo.lock @@ -1,14 +1,17 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. version = 4 [[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -19,9 +22,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -34,79 +37,79 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -116,9 +119,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -126,9 +129,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -138,9 +141,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -150,15 +153,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "core-foundation-sys" @@ -167,6 +170,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -174,15 +183,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -204,9 +213,9 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", @@ -215,26 +224,36 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] +name = "keymaps" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea59e8e461942cf1d6a7ad938848d6fd2e40eb43799c21192c09226ecc86710f" +dependencies = [ + "thiserror", +] + +[[package]] name = "lf-make-map" version = "0.1.0" dependencies = [ "anyhow", "clap", + "keymaps", "log", "stderrlog", "walkdir", @@ -242,15 +261,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "num-traits" @@ -268,28 +287,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -327,9 +352,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -346,20 +371,39 @@ dependencies = [ ] [[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "utf8parse" @@ -379,35 +423,22 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -415,119 +446,99 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-sys" -version = "0.59.0" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "windows-targets", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/pkgs/by-name/lf/lf-make-map/Cargo.toml b/pkgs/by-name/lf/lf-make-map/Cargo.toml index d2427d73..15d48115 100644 --- a/pkgs/by-name/lf/lf-make-map/Cargo.toml +++ b/pkgs/by-name/lf/lf-make-map/Cargo.toml @@ -1,14 +1,25 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + [package] name = "lf-make-map" description = "An automatic lf cd mapping generator" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.97" -clap = { version = "4.5.34", features = ["derive", "env"] } -log = "0.4.27" +anyhow = "1.0.100" +clap = { version = "4.5.54", features = ["derive", "env"] } +keymaps = "1.2.0" +log = "0.4.29" stderrlog = "0.6.0" walkdir = "2.5.0" diff --git a/pkgs/by-name/lf/lf-make-map/README.md b/pkgs/by-name/lf/lf-make-map/README.md index 0c57cede..4d5dc95c 100644 --- a/pkgs/by-name/lf/lf-make-map/README.md +++ b/pkgs/by-name/lf/lf-make-map/README.md @@ -1,3 +1,15 @@ +<!-- +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +--> + # Lf make map > An automatic lf cd mapping generator diff --git a/pkgs/by-name/lf/lf-make-map/flake.lock b/pkgs/by-name/lf/lf-make-map/flake.lock index c9c523d9..1e997998 100644 --- a/pkgs/by-name/lf/lf-make-map/flake.lock +++ b/pkgs/by-name/lf/lf-make-map/flake.lock @@ -1,63 +1,12 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1742394900, - "narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": [ - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1743076231, - "narHash": "sha256-yQugdVfi316qUfqzN8JMaA2vixl+45GxNm4oUfXlbgw=", + "lastModified": 1768661221, + "narHash": "sha256-MJwOjrIISfOpdI9x4C+5WFQXvHtOuj5mqLZ4TMEtk1M=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6c5963357f3c1c840201eda129a99d455074db04", + "rev": "3327b113f2ef698d380df83fbccefad7e83d7769", "type": "github" }, "original": { @@ -69,68 +18,7 @@ }, "root": { "inputs": { - "crane": "crane", - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay", - "systems": "systems", - "treefmt-nix": "treefmt-nix" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1743215516, - "narHash": "sha256-52qbrkG65U1hyrQWltgHTgH4nm0SJL+9TWv2UDCEPNI=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "524463199fdee49338006b049bc376b965a2cfed", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1680978846, - "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", - "owner": "nix-systems", - "repo": "x86_64-linux", - "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "x86_64-linux", - "type": "github" - } - }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1743081648, - "narHash": "sha256-WRAylyYptt6OX5eCEBWyTwOEqEtD6zt33rlUkr6u3cE=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "29a3d7b768c70addce17af0869f6e2bd8f5be4b7", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/pkgs/by-name/lf/lf-make-map/flake.lock.license b/pkgs/by-name/lf/lf-make-map/flake.lock.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/flake.lock.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/lf/lf-make-map/flake.nix b/pkgs/by-name/lf/lf-make-map/flake.nix index dc8c24cc..8ce8ff0f 100644 --- a/pkgs/by-name/lf/lf-make-map/flake.nix +++ b/pkgs/by-name/lf/lf-make-map/flake.nix @@ -1,125 +1,34 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { description = "An automatic lf cd mapping generator"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; - treefmt-nix = { - url = "github:numtide/treefmt-nix"; - inputs = { - nixpkgs.follows = "nixpkgs"; - }; - }; - - crane = { - url = "github:ipetkov/crane"; - inputs = { - nixpkgs.follows = "nixpkgs"; - }; - }; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs = { - nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; - }; - }; - - # inputs for following - systems = { - url = "github:nix-systems/x86_64-linux"; # only evaluate for this system - }; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; - flake-utils = { - url = "github:numtide/flake-utils"; - inputs = { - systems.follows = "systems"; - }; + outputs = {nixpkgs, ...}: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages."${system}"; + in { + devShells."${system}".default = pkgs.mkShell { + packages = [ + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt + + pkgs.cargo-edit + ]; }; }; - - outputs = { - self, - nixpkgs, - flake-utils, - treefmt-nix, - crane, - rust-overlay, - ... - }: - flake-utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { - inherit system; - overlays = [(import rust-overlay)]; - }; - - nightly = false; - rust_minimal = - if nightly - then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal) - else pkgs.rust-bin.stable.latest.minimal; - rust_default = - if nightly - then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default) - else pkgs.rust-bin.stable.latest.default; - - cargo_toml = craneLib.cleanCargoToml {cargoToml = ./Cargo.toml;}; - pname = cargo_toml.package.name; - - craneLib = (crane.mkLib pkgs).overrideToolchain rust_minimal; - craneBuild = craneLib.buildPackage { - src = craneLib.cleanCargoSource ./.; - - doCheck = true; - }; - - manual = pkgs.stdenv.mkDerivation { - name = "${pname}-manual"; - inherit (cargo_toml.package) version; - - src = ./docs; - nativeBuildInputs = with pkgs; [pandoc]; - - buildPhase = '' - mkdir --parents $out/docs; - - pandoc "./${pname}.1.md" -s -t man > $out/docs/${pname}.1 - ''; - - installPhase = '' - install -D $out/docs/${pname}.1 $out/share/man/man1/${pname}; - ''; - }; - - treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;}; - in { - packages.default = pkgs.symlinkJoin { - inherit (cargo_toml.package) name; - - paths = [manual craneBuild]; - }; - - checks = { - inherit craneBuild; - formatting = treefmtEval.config.build.check self; - }; - - formatter = treefmtEval.config.build.wrapper; - - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - cocogitto - - rust_default - cargo-edit - - licensure - ]; - }; - }); } # vim: ts=2 diff --git a/pkgs/by-name/lf/lf-make-map/package.nix b/pkgs/by-name/lf/lf-make-map/package.nix index 8404927f..8f77c843 100644 --- a/pkgs/by-name/lf/lf-make-map/package.nix +++ b/pkgs/by-name/lf/lf-make-map/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. {rustPlatform}: rustPlatform.buildRustPackage { pname = "lf-make-map"; diff --git a/pkgs/by-name/lf/lf-make-map/src/cli.rs b/pkgs/by-name/lf/lf-make-map/src/cli.rs index a398e451..70746984 100644 --- a/pkgs/by-name/lf/lf-make-map/src/cli.rs +++ b/pkgs/by-name/lf/lf-make-map/src/cli.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::path::PathBuf; use clap::{ArgAction, Parser, Subcommand}; diff --git a/pkgs/by-name/lf/lf-make-map/src/main.rs b/pkgs/by-name/lf/lf-make-map/src/main.rs index aaf79b20..d5d934e1 100644 --- a/pkgs/by-name/lf/lf-make-map/src/main.rs +++ b/pkgs/by-name/lf/lf-make-map/src/main.rs @@ -1,13 +1,23 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::Parser; use cli::{Args, Command}; use log::trace; -use mapping::map_tree::MappingTree; +use mapping::map_key::MapKey; use walkdir::{DirEntry, WalkDir}; -use crate::mapping::MapKey; +use crate::mapping::MappingsTrie; mod cli; mod mapping; @@ -24,7 +34,7 @@ fn main() -> anyhow::Result<()> { .timestamp(stderrlog::Timestamp::Off) .init()?; - let mut mappings = MappingTree::new(); + let mut mappings = MappingsTrie::new(); let relevant_directories = match &args.command { Command::Visualize { options } => &options.relevant_directories, @@ -32,68 +42,62 @@ fn main() -> anyhow::Result<()> { }; for dir in relevant_directories { - trace!("Processing '{}'..", dir.display()); - let path = strip_path(&dir, &args.home_name)?; + trace!("START Processing '{}'..", dir.display()); + let path = strip_path(dir, &args.home_name)?; mappings .include(path_to_str(path)?) .with_context(|| format!("Failed to include path: '{}'", path.display()))?; + trace!("END Finished processing {}.", dir.display()); + } + + trace!("Generated mappings for the relevant directories. Starting expanding to max depth."); + if log::log_enabled!(log::Level::Trace) { + eprintln!("{mappings}"); } let home = path_to_str(&args.home_name)?.to_owned(); let mut current_depth = 1; while current_depth != args.depth { - for (key, value) in mappings.iter(false) { - trace!( - "Adding to child ('{}' -> '{}')", - MapKey::display(&key), - value - ); - - let mut local_mappings = MappingTree::new(); - for dir in WalkDir::new(extend(&home, &value)?) + for (keys, child) in mappings.0.iter().filter(|(_, child)| child.expendable) { + trace!("Adding to child '{}' ('{}')", MapKey::display(&keys), child); + + let mut local_mappings = MappingsTrie::new(); + for dir in WalkDir::new(extend(&home, &child.path)?) .min_depth(1) .max_depth(1) .into_iter() .filter_entry(|e| is_dir(e) && !is_hidden(e)) { - let directory = dir - .with_context(|| format!("Failed to read dir ('{}')", home.clone() + &value))?; - let path_to_strip = &PathBuf::from(extend(&home, &value)?); - let path = strip_path(&directory.path(), &path_to_strip)?; + let directory = dir.with_context(|| { + format!("Failed to read dir ('{}')", home.clone() + &child.path) + })?; + let path_to_strip = &PathBuf::from(extend(&home, &child.path)?); + let path = strip_path(directory.path(), path_to_strip)?; trace!( - "Including: '{}' (after stripping '{}' from '{}' -> '{}' + '/' + '{}')", + "Including: '{}' (after stripping '{}' from '{}')", path.display(), - directory.path().display(), path_to_strip.display(), - home, - value + directory.path().display(), ); let gen_key = MapKey::new_ones_from_path(path_to_str(path)?, 1); local_mappings .insert( &gen_key, - path_to_str(strip_path(&directory.path(), &PathBuf::from(&home))?)?, + path_to_str(strip_path(directory.path(), &PathBuf::from(&home))?)?, ) .with_context(|| format!("Failed to include path: '{}'", path.display()))?; } - trace!("{}", local_mappings); - - trace!( - "'{}' -> '{:#?}'", - MapKey::display(&key), - local_mappings.root_node() - ); - mappings.interleave(&key, local_mappings.root_node().to_owned())?; + mappings.add_trie(&keys, local_mappings)?; } current_depth += 1; } match args.command { - Command::Visualize { .. } => println!("{}", mappings), + Command::Visualize { .. } => println!("{}", mappings.0), Command::Generate { .. } => println!("{}", mappings.to_lf_mappings(args.home_name)), } @@ -120,7 +124,7 @@ fn is_dir(entry: &DirEntry) -> bool { } fn strip_path<'a>(path: &'a Path, to_strip: &Path) -> Result<&'a Path> { - path.strip_prefix(&to_strip).with_context(|| { + path.strip_prefix(to_strip).with_context(|| { format!( "'{}' is not under the specified home path ('{}')!", path.display(), diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/lf_mapping.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/lf_mapping.rs new file mode 100644 index 00000000..f8a6182e --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/src/mapping/lf_mapping.rs @@ -0,0 +1,35 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::path::PathBuf; + +use crate::mapping::MapKey; + +use super::MappingsTrie; + +impl MappingsTrie { + pub fn to_lf_mappings(&self, home_path: PathBuf) -> String { + let mut raw = self + .0 + .iter() + .map(|(key, value)| { + format!( + "map g{} cd \"{}\"\n", + MapKey::display(&key), + home_path.join(&value.path).display() + ) + }) + .collect::<Vec<String>>(); + + raw.sort(); + + raw.into_iter().collect() + } +} diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/map_key.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/map_key.rs new file mode 100644 index 00000000..10b612fd --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/src/mapping/map_key.rs @@ -0,0 +1,274 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::fmt::Write; + +use anyhow::bail; +use log::debug; + +#[derive(Clone, Debug)] +pub struct MapKey { + pub key: char, + + pub(crate) resolution: usize, + + /// Part of the path, used to derive the key + pub(crate) part_path: String, +} + +impl std::hash::Hash for MapKey { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.key.hash(state) + } +} + +impl Eq for MapKey {} +impl PartialEq for MapKey { + fn eq(&self, other: &Self) -> bool { + self.key == other.key + } +} + +impl Ord for MapKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.key.cmp(&other.key) + } +} +impl PartialOrd for MapKey { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl MapKey { + pub fn new_from_part_path(part_path: &str, resolution: usize) -> Vec<Self> { + let key = Self::part_path_to_key(part_path, resolution); + + key.chars() + .map(|ch| Self { + key: ch, + resolution, + part_path: part_path.to_owned(), + }) + .collect() + } + + pub fn new_ones_from_path(path: &str, number_of_chars: usize) -> Vec<Self> { + let key: Vec<MapKey> = path + .split('/') + .flat_map(|part| Self::new_from_part_path(part, number_of_chars)) + .collect(); + + debug!( + "Generated full MapKeys: '{}' -> '{}'", + path, + MapKey::display(&key) + ); + key + } + + pub fn increment(&self, target_resolution: usize) -> Vec<Self> { + let new_resolution = target_resolution; + + // debug!("Incrementing: '{}' ('{}')", &self, &self.part_path); + + let added_chars = if new_resolution < self.part_path.len() { + MapKey::part_path_to_key(&self.part_path, new_resolution) + } else { + let mut generated_chars = + MapKey::part_path_to_key(&self.part_path, self.part_path.len()); + + generated_chars.extend( + (0..(new_resolution - self.part_path.len())) + .into_iter() + .map(|_| self.part_path.chars().last().expect("This will exists")), + ); + + generated_chars + }; + + let part_path = self.part_path.clone(); + let output: Vec<Self> = added_chars + .chars() + .enumerate() + .map(|(res, ch)| MapKey { + key: ch, + resolution: res + 1, + part_path: part_path.clone(), + }) + .collect(); + + // debug!("Finished increment: '{}' ('{}')", MapKey::display(&output), output[0].part_path); + output + } + + pub fn display(values: &[Self]) -> String { + values.iter().map(|value| value.key.clone()).collect() + } + + fn part_path_to_key(part: &str, number_of_chars: usize) -> String { + fn make(pat: char, part: &str, number_of_chars: usize) -> String { + let mut acc = String::new(); + + if !part.split(pat).all(|part| part.len() > 0) { + panic!( + "\ +Can't turn this path '{}' to a mapping. +This should not happen, please report the bug!", + part + ) + } + + let mut last_working = None; + for i in 0..number_of_chars { + for str in part.split(pat) { + if acc.len() != number_of_chars { + acc.push(match str.chars().nth(i) { + Some(ch) => ch, + None => { + if let Some(last) = last_working { + str.chars().nth(last).expect("This should always exist") + } else { + last_working = Some(i - 1); + str.chars().nth(i - 1).expect("This should always exist") + } + } + }) + } + } + } + + acc + } + + let value = if part.contains('_') && !part.starts_with('_') && !part.ends_with('_') { + make('_', part, number_of_chars) + } else if part.contains('-') && !part.starts_with('-') && !part.ends_with('-') { + make('-', part, number_of_chars) + } else { + part.chars().take(number_of_chars).collect::<String>() + }; + + assert_eq!( + value.len(), + number_of_chars, + "'{}' does not have expected length of: {}", + value, + number_of_chars + ); + value + } + + /// Checks whether a tiebreak via the [`Self::increment`] function can result in unique keys. + pub fn can_tiebreak_with(&self, other: &Self) -> anyhow::Result<()> { + /// Check whether the `input` &str is composed of only one character. + /// If so, returns this character, otherwise returns None. + fn reduce_string(input: &str) -> Option<char> { + let first_char = input + .chars() + .take(1) + .last() + .expect("Should contain one char"); + + if input.chars().all(|ch| ch == first_char) { + Some(first_char) + } else { + None + } + } + + /// Check whether `a` is a subset of `b` or `b` is a subset of `a`. + fn is_subset_either(a: &str, b: &str) -> bool { + /// Checks if `subset` is a subset of `set`. + /// + /// # Examples + /// ``` + /// let a = "a"; + /// let b = "aa"; + /// assert!(is_subset(a, b)) + /// ``` + /// + /// ``` + /// let a = "abc"; + /// let b = "def"; + /// assert!(!is_subset(a, b)) + /// ``` + fn is_subset(subset: &str, set: &str) -> bool { + let prefix: String = set.chars().take(subset.len()).collect(); + let suffix: String = set.chars().skip(subset.len()).collect(); + + if prefix == subset { + let clean_suffix = reduce_string(&suffix); + if let Some(ch) = clean_suffix { + ch == subset.chars().last().expect("Will exists") + } else { + false + } + } else { + false + } + } + + match a.len().cmp(&b.len()) { + std::cmp::Ordering::Less => { + // `b` is the longer string. As such we need to check if `a` is a subset of `b`. + is_subset(a, b) + } + std::cmp::Ordering::Greater => { + // `a` is the longer string. As such we need to check if `b` is a subset of `a`. + is_subset(b, a) + } + std::cmp::Ordering::Equal => a == b, + } + } + + if reduce_string(&other.part_path) + .is_some_and(|a| Some(a) == reduce_string(&self.part_path)) + { + bail!( + "\ +The foreign_key ('{}', path_part: '{}' -> '{}') and our_key ('{}', path_part: '{}' -> '{}') \ +have an identical path_part (when duplicated chars are removed)! +I cannot extended them via incrementation. +Please rename the paths to fix this. + ", + other, + &other.part_path, + reduce_string(&other.part_path).expect("Is some here"), + self, + &self.part_path, + reduce_string(&self.part_path).expect("Is some here"), + ); + } + + if is_subset_either(&other.part_path, &self.part_path) { + bail!( + "\ +The foreign_key ('{}', path_part: '{}') and our_key ('{}', path_part: '{}') \ +are subsets of one another! +A discrimination through incrementation will not work! +Please rename the paths to fix this. + ", + other, + &other.part_path, + self, + &self.part_path, + ); + } + + Ok(()) + } +} + +impl std::fmt::Display for MapKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_char(self.key) + } +} diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/display.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/display.rs deleted file mode 100644 index 65302e1e..00000000 --- a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/display.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::fmt::Display; - -use crate::mapping::{ - map_tree::{Node, NodeValue}, - MapKey, -}; - -use super::MappingTree; - -impl Display for MappingTree { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fn write_node( - f: &mut std::fmt::Formatter<'_>, - node: &Node, - indention: String, - location: Vec<MapKey>, - is_last: bool, - is_root: bool, - ) -> std::fmt::Result { - let node_value = match &node.value { - NodeValue::Parent { children: _ } => "<Parent>".to_owned(), - NodeValue::Child { path, extandable } => { - path.to_owned() + if *extandable { " [exten.]" } else { " [stop]" } - } - }; - - let new_idention = indention.clone() - + if is_root { - "" - } else { - match is_last { - true => " ", - false => "│ ", - } - }; - - let bullet = match is_last { - true => String::from("└── "), - false => String::from("├── "), - }; - - if is_root { - write!(f, ": {}\n", node_value)?; - } else { - write!( - f, - "{}{}\x1b[1;33m{}\x1b[0m: {}\n", - indention, - bullet, - MapKey::display(&location), - node_value, - )?; - }; - - match &node.value { - NodeValue::Parent { children } => { - let mut children_vec: Vec<(&MapKey, &Node)> = children.iter().collect(); - children_vec.sort_by(|(a, _), (b, _)| a.key.cmp(&b.key)); - - let mut counter = 1; - for (key, child) in &children_vec { - let mut new_location = location.clone(); - new_location.push((*key).to_owned()); - - write_node( - f, - child, - new_idention.clone(), - new_location.clone(), - counter == children_vec.len(), - false, - )?; - counter += 1; - } - } - NodeValue::Child { - path: _, - extandable: _, - } => { - // Do nothing and stop the recursion - } - } - - Ok(()) - } - - write_node(f, &self.root, String::new(), vec![], false, true)?; - - Ok(()) - } -} diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/iterator.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/iterator.rs deleted file mode 100644 index 4364bb2b..00000000 --- a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/iterator.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::mapping::MapKey; - -use super::{MappingTree, Node, NodeValue}; - -pub struct MappingTreeIterator { - children: Vec<(Vec<MapKey>, String)>, -} - -impl MappingTreeIterator { - pub fn new(tree: &MappingTree, ignore_extendable: bool) -> Self { - let children = extract_child(vec![], &tree.root, ignore_extendable); - - Self { children } - } -} - -fn extract_child( - current_key: Vec<MapKey>, - node: &Node, - ignore_extendable: bool, -) -> Vec<(Vec<MapKey>, String)> { - match &node.value { - NodeValue::Parent { children } => children - .iter() - .map(|(key, value)| { - let mut new_key = current_key.clone(); - new_key.push(key.to_owned()); - - extract_child(new_key, value, ignore_extendable) - }) - .flatten() - .collect(), - NodeValue::Child { path, extandable } => { - if ignore_extendable { - vec![(current_key, path.to_string())] - } else { - if *extandable { - vec![(current_key, path.to_string())] - } else { - vec![] - } - } - } - } -} - -impl Iterator for MappingTreeIterator { - type Item = (Vec<MapKey>, String); - - fn next(&mut self) -> Option<Self::Item> { - self.children.pop() - } -} diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/lf_mapping.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/lf_mapping.rs deleted file mode 100644 index ba485dc2..00000000 --- a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/lf_mapping.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::path::PathBuf; - -use crate::mapping::MapKey; - -use super::MappingTree; - -impl MappingTree { - pub fn to_lf_mappings(self, home_path: PathBuf) -> String { - let mut raw = self - .iter(true) - .map(|(key, value)| { - format!( - "map g{} cd \"{}\"\n", - MapKey::display(&key), - home_path.join(&value).display() - ) - }) - .collect::<Vec<String>>(); - - raw.sort(); - - raw.into_iter().collect() - } -} diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/mod.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/mod.rs deleted file mode 100644 index 35e6d91d..00000000 --- a/pkgs/by-name/lf/lf-make-map/src/mapping/map_tree/mod.rs +++ /dev/null @@ -1,402 +0,0 @@ -use std::{collections::HashMap, mem}; - -use anyhow::{bail, Result}; -use log::debug; - -use self::iterator::MappingTreeIterator; - -use super::MapKey; - -pub mod display; -pub mod iterator; -pub mod lf_mapping; - -/// A prefix tree -#[derive(Debug)] -pub struct MappingTree { - root: Node, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum NodeValue { - Parent { children: HashMap<MapKey, Node> }, - Child { path: String, extandable: bool }, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Node { - value: NodeValue, -} - -impl MappingTree { - pub fn new() -> Self { - Self { - root: Node::new_parent(), - } - } - - pub fn root_node(&self) -> &Node { - &self.root - } - - pub fn iter(&self, ignore_extendable: bool) -> MappingTreeIterator { - MappingTreeIterator::new(&self, ignore_extendable) - } - - /// Returns the node at the key, otherwise None. The node can be changed - pub fn get_mut(&mut self, key: &[MapKey]) -> Option<&mut Node> { - let mut current_node = &mut self.root; - for ch in key.iter() { - if let NodeValue::Parent { children } = &mut current_node.value { - current_node = children.get_mut(&ch)? - } else { - return None; - } - } - - Some(current_node) - } - - /// Returns the node at the key, otherwise the last node that matched. - pub fn try_get(&self, key: &[MapKey]) -> (&Node, Vec<MapKey>) { - let mut current_node = &self.root; - let mut current_key = vec![]; - - for ch in key.iter() { - if let NodeValue::Parent { children } = ¤t_node.value { - current_node = if let Some(node) = children.get(&ch) { - let (key, _value) = children - .get_key_value(&ch) - .expect("This exists, we checked"); - current_key.push(key.clone()); - - node - } else { - return (current_node, current_key); - }; - } else { - return (current_node, current_key); - } - } - - (current_node, current_key) - } - - pub fn include(&mut self, path: &str) -> Result<()> { - let associated_key = MapKey::new_ones_from_path(path, 1); - self.insert(&associated_key, path) - } - - pub fn insert(&mut self, key: &[MapKey], path: &str) -> Result<()> { - self.insert_node(key, Node::new_child(path.to_owned())) - } - - pub fn interleave(&mut self, key: &[MapKey], node: Node) -> Result<()> { - let want_to_be_parent = self.get_mut(&key).expect("This value exists"); - let (parent_value, _parent_children) = if let NodeValue::Parent { children } = node.value { - ( - NodeValue::Parent { - children: children.clone(), - }, - children, - ) - } else { - unreachable!("This value will be a parent") - }; - - let child_value = mem::replace(&mut want_to_be_parent.value, parent_value); - assert!(matches!( - child_value, - NodeValue::Child { - path: _, - extandable: _ - } - )); - - let child_value = if let NodeValue::Child { - path, - extandable: _, - } = child_value - { - NodeValue::Child { - path, - extandable: false, - } - } else { - unreachable!("This is only a child value") - }; - - let child = Node { value: child_value }; - - let mut new_key = key.to_vec(); - new_key.push(MapKey { - key: '.', - part_path: ".".to_owned(), - resolution: 1, - }); - self.insert_node(&new_key, child)?; - Ok(()) - } - - pub fn insert_node(&mut self, key: &[MapKey], node: Node) -> Result<()> { - let (_node, found_key) = self.try_get(key).clone(); - - if found_key != key { - let needed_nodes_key = key - .strip_prefix(&found_key[..]) - .expect("The node's location is a prefix"); - - let needed_nodes_length = needed_nodes_key.iter().count(); - - let mut current_node = self - .get_mut(&found_key[..]) - .expect("This should always exists"); - let mut current_location = found_key.clone(); - let mut counter = 1; - - for ch in needed_nodes_key.iter() { - current_location.push(ch.to_owned()); - - let next_node = if counter == needed_nodes_length { - node.clone() - } else { - Node::new_parent() - }; - - current_node = match ¤t_node.value { - NodeValue::Parent { children } => { - assert_eq!(children.get(&ch), None); - - let children = - if let NodeValue::Parent { children } = &mut current_node.value { - children - } else { - unreachable!("This is a parent, we cheched") - }; - - children.insert(ch.to_owned(), next_node); - children.get_mut(&ch).expect("Was just inserted") - } - NodeValue::Child { - path, - extandable: _, - } => { - // A node that should be a parent was classified - // as child before: - // - // 1. Remove the child node and replace it with a parent one. - // 2. Add the child node to the parent node as child, but with a '.' as MapKey. - // 3. Add the original node also as child to the parent node. - - let mut children = HashMap::new(); - let move_child_node = Node::new_child(path.to_owned()); - - children.insert( - MapKey { - key: '.', - part_path: ".".to_owned(), - resolution: 1, - }, - move_child_node, - ); - children.insert(ch.to_owned(), next_node); - - current_node.value = NodeValue::Parent { children }; - - let children = - if let NodeValue::Parent { children } = &mut current_node.value { - children - } else { - unreachable!("We just inserted the parent value.") - }; - - children.get_mut(&ch).expect("Was just inserted") - } - }; - - counter += 1; - } - } else { - fn reduce_string(a: &str) -> Option<char> { - let first_char = a.chars().take(1).last().expect("Should contain one char"); - - if a.chars().all(|ch| ch == first_char) { - return Some(first_char); - } else { - return None; - } - } - fn check_subset(a: &str, b: &str) -> bool { - if a.len() > b.len() { - let a_prefix: String = a.chars().take(b.len()).collect(); - let a_suffix: String = a.chars().skip(b.len()).collect(); - - if a_prefix == b { - let clean_suffix = reduce_string(&a_suffix); - if let Some(ch) = clean_suffix { - ch == b.chars().last().expect("Will match") - } else { - false - } - } else { - false - } - } else if b.len() > a.len() { - let b_prefix: String = b.chars().take(a.len()).collect(); - let b_suffix: String = b.chars().skip(a.len()).collect(); - - if b_prefix == a { - let clean_suffix = reduce_string(&b_suffix); - if let Some(ch) = clean_suffix { - ch == a.chars().last().expect("Will match") - } else { - false - } - } else { - false - } - } else { - a == b - } - } - - // Another node was already inserted with the same key! - // So we simple increase the resolution of the other node and this node, until their - // keys are not the same anymore. - // This only includes the last segment of the `MapKey` - // - // 1. Change both keys, until they are not equal any more - // 2. Move the wrongly placed node to the new place. - // 3. Insert our node. - let mut foreign_key = vec![found_key.last().expect("This will exist").clone()]; - let mut our_key = vec![key.last().expect("This will exist").clone()]; - - debug!( - "'{}' ('{}') and '{}' ('{}') are the same, try to find a better combination!", - MapKey::display(&our_key), - our_key[0].part_path, - MapKey::display(&foreign_key), - foreign_key[0].part_path, - ); - - // The 'a' and 'b' stuff is here, to ensure that both returning None will not match - // this condition. - if reduce_string(&foreign_key[0].part_path).unwrap_or('a') - == reduce_string(&our_key[0].part_path).unwrap_or('b') - { - bail!( - "\ -The foreign_key ('{}', path_part: '{}' -> '{}') and our_key ('{}', path_part: '{}' -> '{}') \ -have an identical path_part (when duplicated chars are removed)! -I cannot extended them via incrementation. -Please rename the paths to fix this. - ", - MapKey::display(&foreign_key), - &foreign_key[0].part_path, - reduce_string(&foreign_key[0].part_path).expect("Is some here"), - MapKey::display(&our_key), - &our_key[0].part_path, - reduce_string(&our_key[0].part_path).expect("Is some here"), - ); - } - - if check_subset(&foreign_key[0].part_path, &our_key[0].part_path) { - bail!( - "\ -The foreign_key ('{}', path_part: '{}') and our_key ('{}', path_part: '{}') \ -are subsets of one another! -A discrimination through incrementation will not work! -Please rename the paths to fix this. - ", - MapKey::display(&foreign_key), - &foreign_key[0].part_path, - MapKey::display(&our_key), - &our_key[0].part_path, - ); - } - - while our_key == foreign_key { - our_key = our_key[0].increment(our_key[our_key.len() - 1].resolution + 1); - foreign_key = - foreign_key[0].increment(foreign_key[foreign_key.len() - 1].resolution + 1); - debug!( - "Now its: '{}' ('{}') and '{}' ('{}')", - MapKey::display(&our_key), - our_key[0].part_path, - MapKey::display(&foreign_key), - foreign_key[0].part_path, - ); - } - - debug!( - "Found a better one: '{}' ('{}') and '{}' ('{}')", - MapKey::display(&our_key), - our_key[0].part_path, - MapKey::display(&foreign_key), - foreign_key[0].part_path, - ); - - let parent = self - .get_mut(&found_key[..&found_key.len() - 1]) - .expect("This will exist"); - - if let NodeValue::Parent { children } = &mut parent.value { - if let NodeValue::Child { - path: _, - extandable: _, - } = children - .get(found_key.last().expect("Exists")) - .expect("This node also exists") - .value - { - let old = children - .remove(found_key.last().expect("This will exist")) - .expect("This will be there"); - - let full_foreign_key: Vec<_> = found_key - .clone() - .into_iter() - .rev() - .skip(1) - .rev() - .chain(foreign_key.clone().into_iter()) - .collect(); - self.insert_node(&full_foreign_key, old.clone())?; - } - - let full_our_key: Vec<_> = key - .to_vec() - .into_iter() - .rev() - .skip(1) - .rev() - .chain(our_key.clone().into_iter()) - .collect(); - - self.insert_node(&full_our_key, node.clone())?; - } else { - unreachable!("This node will be a parent"); - } - } - - Ok(()) - } -} - -impl Node { - pub fn new_child(path: String) -> Self { - Self { - value: NodeValue::Child { - path, - extandable: true, - }, - } - } - pub fn new_parent() -> Self { - Self { - value: NodeValue::Parent { - children: HashMap::new(), - }, - } - } -} diff --git a/pkgs/by-name/lf/lf-make-map/src/mapping/mod.rs b/pkgs/by-name/lf/lf-make-map/src/mapping/mod.rs index 114fdca0..21392388 100644 --- a/pkgs/by-name/lf/lf-make-map/src/mapping/mod.rs +++ b/pkgs/by-name/lf/lf-make-map/src/mapping/mod.rs @@ -1,156 +1,231 @@ -use std::{ - fmt::{Display, Write}, - hash::Hash, -}; - -use log::debug; - -pub mod map_tree; - -#[derive(Clone, Debug, Eq)] -pub struct MapKey { - pub key: char, +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use anyhow::Result; +use keymaps::map_tree::{Node, Trie}; +use log::{Level, debug, log_enabled, trace}; +use map_key::MapKey; + +pub mod lf_mapping; +pub mod map_key; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MapChild { + pub path: String, + pub expendable: bool, +} - resolution: usize, +impl std::fmt::Display for MapChild { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.path)?; + if !self.expendable { + f.write_str(" [stop]")?; + } - /// Part of the path, used to derive the key - part_path: String, + Ok(()) + } } -impl Hash for MapKey { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - self.key.hash(state) +pub struct MappingsTrie(pub Trie<MapKey, MapChild>); +impl std::fmt::Display for MappingsTrie { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) } } -impl PartialEq for MapKey { - fn eq(&self, other: &Self) -> bool { - self.key == other.key +impl MappingsTrie { + pub fn new() -> Self { + Self(Trie::new()) } -} -impl MapKey { - pub fn new_from_part_path(part_path: &str, resolution: usize) -> Vec<Self> { - let key = Self::part_path_to_key(&part_path, resolution); - - key.chars() - .map(|ch| Self { - key: ch, - resolution, - part_path: part_path.to_owned(), - }) - .collect() + pub(crate) fn include(&mut self, path: &str) -> Result<()> { + let associated_key = MapKey::new_ones_from_path(path, 1); + self.insert(&associated_key, path) } - pub fn new_ones_from_path(path: &str, number_of_chars: usize) -> Vec<Self> { - let key: Vec<MapKey> = path - .split('/') - .map(|part| Self::new_from_part_path(part, number_of_chars)) - .flatten() - .collect(); - - debug!( - "Generated full MapKeys: '{}' -> '{}'", - path, - MapKey::display(&key) - ); - key + pub(crate) fn insert(&mut self, keys: &[MapKey], path: &str) -> Result<()> { + let value = Node::new_child(MapChild { + path: path.to_owned(), + expendable: true, + }); + self.insert_node(keys, value) } - pub fn increment(&self, target_resolution: usize) -> Vec<Self> { - let new_resolution = target_resolution; + pub(crate) fn insert_node( + &mut self, + keys: &[MapKey], + node: Node<MapKey, MapChild>, + ) -> Result<()> { + if let Err(err) = self.0.insert_node(keys, &node) { + match err { + keymaps::error::TrieInsert::KeyAlreadySet(found_keys) => { + // Another node was already inserted with the same key! + // So we simple increase the resolution of the other node and this node, until their + // keys are no longer equal. + // This only includes the last segment of the `MapKey` + // + // 1. Change both keys, until they are not equal any more + // 2. Move the wrongly placed node to the new place. + // 3. Insert our node. + assert_eq!(keys, found_keys); + + let mut foreign_keys = + vec![found_keys.last().expect("This will exist").clone()]; + let mut our_keys = vec![keys.last().expect("This will exist").clone()]; + + debug!( + "'{}' ('{}') and '{}' ('{}') are the same, trying to find a better combination!", + MapKey::display(&our_keys), + our_keys[0].part_path, + MapKey::display(&foreign_keys), + foreign_keys[0].part_path, + ); + + our_keys[0].can_tiebreak_with(&foreign_keys[0])?; + + while our_keys == foreign_keys { + our_keys = + our_keys[0].increment(our_keys[our_keys.len() - 1].resolution + 1); + foreign_keys = foreign_keys[0] + .increment(foreign_keys[foreign_keys.len() - 1].resolution + 1); + debug!( + "Now its: '{}' ('{}') and '{}' ('{}')", + MapKey::display(&our_keys), + our_keys[0].part_path, + MapKey::display(&foreign_keys), + foreign_keys[0].part_path, + ); + } + debug!( + "Found a better one: '{}' ('{}') and '{}' ('{}')", + MapKey::display(&our_keys), + our_keys[0].part_path, + MapKey::display(&foreign_keys), + foreign_keys[0].part_path, + ); + + let parent_keys = &found_keys[..&found_keys.len() - 1]; + + { + if self + .0 + .get(&found_keys) + .expect("This will exist") + .value() + .is_some() + { + // This is a child, we must replace it with a parent. + let other_node = self + .0 + .replace_node(&found_keys, Node::new_parent()) + .expect("This node exists"); + + { + let mut full_foreign_keys = parent_keys.to_vec(); + full_foreign_keys.append(&mut foreign_keys); + self.insert_node(&full_foreign_keys, other_node)?; + } + } + } - // debug!("Incrementing: '{}' ('{}')", &self, &self.part_path); + { + let mut full_our_keys = parent_keys.to_vec(); + full_our_keys.append(&mut our_keys); + self.insert_node(&full_our_keys, node)?; + } - let added_chars = if new_resolution < self.part_path.len() { - MapKey::part_path_to_key(&self.part_path, new_resolution) + Ok(()) + } + keymaps::error::TrieInsert::KeyIncludesChild { + child_key: key, + child_value, + } => { + // A node that should be a parent was classified + // as child before: + // + // 1. Remove the child node and replace it with a parent one. + // 2. Add the child node to the parent node as child, but with a '.' as MapKey. + // 3. Add the original node also as child to the parent node. + + assert_eq!(key, keys); + + let (fetched_child_value, mut child_key) = self.0.try_get(keys); + assert_eq!(fetched_child_value.value(), Some(&child_value)); + + trace!( + "Replacing child ('{}') with a parent, so that we can continue from this point.", + MapKey::display(&child_key) + ); + + let child = self + .0 + .replace_node(&child_key, Node::new_parent()) + .expect("Node exists"); + assert_eq!(child.value(), Some(&child_value)); + + child_key.push(MapKey { + key: '.', + part_path: ".".to_owned(), + resolution: 1, + }); + self.0 + .insert_node(&child_key, &child) + .expect("We just created a parent here"); + + // Recursive call, because this key could have hit the previous child directly + // (thus it will now trigger the `KeyAlreadySet` error.) + self.insert_node(keys, node) + } + } } else { - let mut generated_chars = - MapKey::part_path_to_key(&self.part_path, self.part_path.len()); - - generated_chars.extend( - (0..(new_resolution - self.part_path.len())) - .into_iter() - .map(|_| self.part_path.chars().last().expect("This will exists")), - ); - - generated_chars - }; - - let part_path = self.part_path.clone(); - let output: Vec<Self> = added_chars - .chars() - .enumerate() - .map(|(res, ch)| MapKey { - key: ch, - resolution: res + 1, - part_path: part_path.clone(), - }) - .collect(); - - // debug!("Finished increment: '{}' ('{}')", MapKey::display(&output), output[0].part_path); - output + Ok(()) + } } - pub fn display(values: &[Self]) -> String { - values.iter().map(|value| value.key.clone()).collect() - } - fn part_path_to_key(part: &str, number_of_chars: usize) -> String { - fn make(pat: char, part: &str, number_of_chars: usize) -> String { - let mut acc = String::new(); - - if !part.split(pat).all(|part| part.len() > 0) { - panic!( - "\ -Can't turn this path '{}' to a mapping. -This should not happen, please report the bug!", - part - ) - } + /// Add a new [`MappingsTrie`] at the position `keys` into this Trie. + pub(crate) fn add_trie(&mut self, keys: &[MapKey], trie: Self) -> Result<()> { + if log_enabled!(Level::Trace) { + trace!("Adding mappings under '{}':", MapKey::display(keys)); + eprintln!("{trie}"); - let mut last_working = None; - for i in 0..number_of_chars { - for str in part.split(pat) { - if acc.len() != number_of_chars { - acc.push(match str.chars().nth(i) { - Some(ch) => ch, - None => { - if let Some(last) = last_working { - str.chars().nth(last).expect("This should always exist") - } else { - last_working = Some(i - 1); - str.chars().nth(i - 1).expect("This should always exist") - } - } - }) - } - } - } + trace!("Self is:"); + eprintln!("{self}"); + } + + let replaced = self + .0 + .replace_node(keys, trie.0.root_node().to_owned()) + .expect("This value exists"); - acc + if log_enabled!(Level::Trace) { + trace!("After replace adding the new trie"); + eprintln!("{self}"); } - let value = if part.contains('_') && !part.starts_with('_') && !part.ends_with('_') { - make('_', part, number_of_chars) - } else if part.contains('-') && !part.starts_with('-') && !part.ends_with('-') { - make('-', part, number_of_chars) - } else { - part.chars().take(number_of_chars).collect::<String>() - }; - - assert_eq!( - value.len(), - number_of_chars, - "'{}' does not have expected length of: {}", - value, - number_of_chars - ); - value - } -} + { + let mut new_keys = keys.to_vec(); + new_keys.push(MapKey { + key: '.', + part_path: ".".to_owned(), + resolution: 1, + }); + + let mut value = replaced.value().expect("Is a child").clone(); + value.expendable = false; + + trace!("Re-inserting '{}' into self.", MapKey::display(keys)); + self.0 + .insert_node(&new_keys, &Node::new_child(value)) + .expect("This key is not used."); + } -impl Display for MapKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_char(self.key) + Ok(()) } } diff --git a/pkgs/by-name/lf/lf-make-map/tests/base.sh b/pkgs/by-name/lf/lf-make-map/tests/base.sh new file mode 100755 index 00000000..c7694985 --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/tests/base.sh @@ -0,0 +1,25 @@ +#! /usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +cd "$(dirname "$0")" || exit 2 + +cargo build +execute_make_maps() { + ../target/debug/lf-make-map "$@" +} + +fd . cases --max-depth 1 --type directory | while read -r case; do + echo "Executing '$case/test.sh'" + + # shellcheck source=/dev/null + LOCATION="$case/test.sh" . "$case/test.sh" +done diff --git a/pkgs/by-name/lf/lf-make-map/tests/cases/child_insert/test.sh b/pkgs/by-name/lf/lf-make-map/tests/cases/child_insert/test.sh new file mode 100755 index 00000000..af6a1391 --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/tests/cases/child_insert/test.sh @@ -0,0 +1,27 @@ +#! /usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +test="$(mktemp --directory -t lf_make_map_test_XXXX)" + +cleanup() { + rm --recursive "$test" +} +trap cleanup EXIT + +cat <<EOF | while read -r name; do mkdir --parents "$test/media/books/${name}"; done +Andre A +Anton B +Andon C +Anton D +EOF + +execute_make_maps --home-name "$test" --depth 100 generate "$test"/* >/dev/null diff --git a/pkgs/by-name/lf/lf-make-map/tests/cases/simple/output.old b/pkgs/by-name/lf/lf-make-map/tests/cases/simple/output.old new file mode 100644 index 00000000..90591f16 --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/tests/cases/simple/output.old @@ -0,0 +1,46 @@ +map gdd. cd "/tmp/tmp.DfcgjemfCG/d" +map gddcc. cd "/tmp/tmp.DfcgjemfCG/d/c" +map gddccb. cd "/tmp/tmp.DfcgjemfCG/d/c/b" +map gddccbc. cd "/tmp/tmp.DfcgjemfCG/d/c/b/c" +map gddccbcf. cd "/tmp/tmp.DfcgjemfCG/d/c/b/c/file.test" +map gddccd. cd "/tmp/tmp.DfcgjemfCG/d/c/d" +map gddccdf. cd "/tmp/tmp.DfcgjemfCG/d/c/d/f" +map gddccdff. cd "/tmp/tmp.DfcgjemfCG/d/c/d/f/file.test2" +map gddcco. cd "/tmp/tmp.DfcgjemfCG/d/c/other" +map gddccof. cd "/tmp/tmp.DfcgjemfCG/d/c/other/file.test3" +map gddct. cd "/tmp/tmp.DfcgjemfCG/d/cll_the-things" +map gddcto. cd "/tmp/tmp.DfcgjemfCG/d/cll_the-things/other" +map gddctof. cd "/tmp/tmp.DfcgjemfCG/d/cll_the-things/other/file.test4" +map gddm. cd "/tmp/tmp.DfcgjemfCG/d/mcybe some whitespcce" +map gddmt. cd "/tmp/tmp.DfcgjemfCG/d/mcybe some whitespcce/test.file5" +map gdi. cd "/tmp/tmp.DfcgjemfCG/dir" +map gdicc. cd "/tmp/tmp.DfcgjemfCG/dir/c" +map gdiccb. cd "/tmp/tmp.DfcgjemfCG/dir/c/b" +map gdiccbc. cd "/tmp/tmp.DfcgjemfCG/dir/c/b/c" +map gdiccbcf. cd "/tmp/tmp.DfcgjemfCG/dir/c/b/c/file.test" +map gdiccd. cd "/tmp/tmp.DfcgjemfCG/dir/c/d" +map gdiccdf. cd "/tmp/tmp.DfcgjemfCG/dir/c/d/f" +map gdiccdff. cd "/tmp/tmp.DfcgjemfCG/dir/c/d/f/file.test2" +map gdicco. cd "/tmp/tmp.DfcgjemfCG/dir/c/other" +map gdiccof. cd "/tmp/tmp.DfcgjemfCG/dir/c/other/file.test3" +map gdict. cd "/tmp/tmp.DfcgjemfCG/dir/cll_the-things" +map gdicto. cd "/tmp/tmp.DfcgjemfCG/dir/cll_the-things/other" +map gdictof. cd "/tmp/tmp.DfcgjemfCG/dir/cll_the-things/other/file.test4" +map gdim. cd "/tmp/tmp.DfcgjemfCG/dir/mcybe some whitespcce" +map gdimt. cd "/tmp/tmp.DfcgjemfCG/dir/mcybe some whitespcce/test.file5" +map gdo. cd "/tmp/tmp.DfcgjemfCG/dor" +map gdocc. cd "/tmp/tmp.DfcgjemfCG/dor/c" +map gdoccb. cd "/tmp/tmp.DfcgjemfCG/dor/c/b" +map gdoccbc. cd "/tmp/tmp.DfcgjemfCG/dor/c/b/c" +map gdoccbcf. cd "/tmp/tmp.DfcgjemfCG/dor/c/b/c/file.test" +map gdoccd. cd "/tmp/tmp.DfcgjemfCG/dor/c/d" +map gdoccdf. cd "/tmp/tmp.DfcgjemfCG/dor/c/d/f" +map gdoccdff. cd "/tmp/tmp.DfcgjemfCG/dor/c/d/f/file.test2" +map gdocco. cd "/tmp/tmp.DfcgjemfCG/dor/c/other" +map gdoccof. cd "/tmp/tmp.DfcgjemfCG/dor/c/other/file.test3" +map gdoct. cd "/tmp/tmp.DfcgjemfCG/dor/cll_the-things" +map gdocto. cd "/tmp/tmp.DfcgjemfCG/dor/cll_the-things/other" +map gdoctof. cd "/tmp/tmp.DfcgjemfCG/dor/cll_the-things/other/file.test4" +map gdom. cd "/tmp/tmp.DfcgjemfCG/dor/mcybe some whitespcce" +map gdomt. cd "/tmp/tmp.DfcgjemfCG/dor/mcybe some whitespcce/test.file5" + diff --git a/pkgs/by-name/lf/lf-make-map/tests/cases/simple/output.old.license b/pkgs/by-name/lf/lf-make-map/tests/cases/simple/output.old.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/tests/cases/simple/output.old.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/lf/lf-make-map/tests/cases/simple/test.sh b/pkgs/by-name/lf/lf-make-map/tests/cases/simple/test.sh new file mode 100755 index 00000000..22f97009 --- /dev/null +++ b/pkgs/by-name/lf/lf-make-map/tests/cases/simple/test.sh @@ -0,0 +1,49 @@ +#! /usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +# We need to hard code this, so that our output matches the golden sample. +base="/tmp/tmp.DfcgjemfCG" +test="$(mktemp --directory -t lf_make_temp_test_XXXXX)" + +[ -d "$base" ] && { + echo "$base already exists!" + exit 1 +} + +mkdir "$base" + +cleanup() { + rm --recursive "$base" "$test" +} +trap cleanup EXIT + +mkdir --parents "$base/dir/c/b/c/file.test" +mkdir --parents "$base/dir/c/d/f/file.test2" +mkdir --parents "$base/dir/c/other/file.test3" +mkdir --parents "$base/dir/cll_the-things/other/file.test4" +mkdir --parents "$base/dir/mcybe some whitespcce/test.file5" + +mkdir --parents "$base/dor/c/b/c/file.test" +mkdir --parents "$base/dor/c/d/f/file.test2" +mkdir --parents "$base/dor/c/other/file.test3" +mkdir --parents "$base/dor/cll_the-things/other/file.test4" +mkdir --parents "$base/dor/mcybe some whitespcce/test.file5" + +mkdir --parents "$base/d/c/b/c/file.test" +mkdir --parents "$base/d/c/d/f/file.test2" +mkdir --parents "$base/d/c/other/file.test3" +mkdir --parents "$base/d/cll_the-things/other/file.test4" +mkdir --parents "$base/d/mcybe some whitespcce/test.file5" + +execute_make_maps --home-name "$base" --depth 100 generate "$base"/* >"$test/output.new" + +diff "$test/output.new" "$(dirname "$LOCATION")/output.old" diff --git a/pkgs/by-name/lf/lf-make-map/update.sh b/pkgs/by-name/lf/lf-make-map/update.sh index 7d517e8b..23d90a86 100755 --- a/pkgs/by-name/lf/lf-make-map/update.sh +++ b/pkgs/by-name/lf/lf-make-map/update.sh @@ -1,4 +1,14 @@ #!/usr/bin/env sh +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + [ "$1" = "upgrade" ] && cargo upgrade cargo update diff --git a/pkgs/by-name/ll/ll/ll.sh b/pkgs/by-name/ll/ll/ll.sh deleted file mode 100755 index 73328e3e..00000000 --- a/pkgs/by-name/ll/ll/ll.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env dash - -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -last_directory="$(mktemp)" - -command lf -last-dir-path="$last_directory" "$@" - -dir="$(cat "$last_directory")" -if cd "$dir"; then - [ -d "$XDG_RUNTIME_DIR/ll" ] || mkdir "$XDG_RUNTIME_DIR/ll" - echo "$dir" >"$XDG_RUNTIME_DIR/ll/last_directory" -else - die "$dir does not exist!" -fi - -rm "$last_directory" -# vim: ft=sh diff --git a/pkgs/by-name/ll/ll/package.nix b/pkgs/by-name/ll/ll/package.nix deleted file mode 100644 index 4c13b40e..00000000 --- a/pkgs/by-name/ll/ll/package.nix +++ /dev/null @@ -1,9 +0,0 @@ -{sysLib}: -sysLib.writeShellScript { - name = "ll"; - src = ./ll.sh; - generateCompletions = false; - - # `ll` must be able to change the path of the running shell. - wrap = false; -} diff --git a/pkgs/by-name/lm/lm/lm.sh b/pkgs/by-name/lm/lm/lm.sh deleted file mode 100755 index d5fdca10..00000000 --- a/pkgs/by-name/lm/lm/lm.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env dash - -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -if [ -f "$XDG_RUNTIME_DIR/ll/last_directory" ]; then - last_dir="$(cat "$XDG_RUNTIME_DIR/ll/last_directory")" - cd "$last_dir" || die "$last_dir does not exist!" -else - msg "No last directory saved (try using ll instead)." -fi -# vim: ft=sh diff --git a/pkgs/by-name/lm/lm/package.nix b/pkgs/by-name/lm/lm/package.nix deleted file mode 100644 index ef417cd2..00000000 --- a/pkgs/by-name/lm/lm/package.nix +++ /dev/null @@ -1,9 +0,0 @@ -{sysLib}: -sysLib.writeShellScript { - name = "lm"; - src = ./lm.sh; - generateCompletions = false; - - # `ll` must be able to change the path of the running shell. - wrap = false; -} diff --git a/pkgs/by-name/lo/lock/lock.sh b/pkgs/by-name/lo/lock/lock.sh index 4c5e1c01..1c068e57 100755 --- a/pkgs/by-name/lo/lock/lock.sh +++ b/pkgs/by-name/lo/lock/lock.sh @@ -1,3 +1,13 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + # shellcheck shell=bash context="$(task _get rc.context)" diff --git a/pkgs/by-name/lo/lock/package.nix b/pkgs/by-name/lo/lock/package.nix index a59fbdd0..572ebb0b 100644 --- a/pkgs/by-name/lo/lock/package.nix +++ b/pkgs/by-name/lo/lock/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { writeShellApplication, taskwarrior3, diff --git a/pkgs/by-name/mp/mpdpopm/.envrc b/pkgs/by-name/mp/mpdpopm/.envrc new file mode 100644 index 00000000..9f477e71 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/.envrc @@ -0,0 +1,22 @@ +#!/usr/bin/env sh + +# Mpdpopm - A mpd rating tracker +# +# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com> +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This file is part of Mpdpopm. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/agpl.txt>. + +use flake || use nix +watch_file flake.nix + +PATH_add ./scripts +PATH_add ./target/debug/ +PATH_add ./target/release/ + +if on_git_branch; then + echo && git status --short --branch +fi diff --git a/pkgs/by-name/mp/mpdpopm/.gitignore b/pkgs/by-name/mp/mpdpopm/.gitignore new file mode 100644 index 00000000..c80d7eef --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/.gitignore @@ -0,0 +1,16 @@ +# Mpdpopm - A mpd rating tracker +# +# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com> +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This file is part of Mpdpopm. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/agpl.txt>. + +# build +/target +/result + +# dev env +.direnv diff --git a/pkgs/by-name/mp/mpdpopm/Cargo.lock b/pkgs/by-name/mp/mpdpopm/Cargo.lock new file mode 100644 index 00000000..96909646 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/Cargo.lock @@ -0,0 +1,1356 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mpdpopm" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "boolinator", + "chrono", + "clap", + "errno", + "futures", + "lalrpop", + "lalrpop-util", + "lazy_static", + "os_str_bytes", + "pin-project", + "regex", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "os_str_bytes" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/pkgs/by-name/mp/mpdpopm/Cargo.toml b/pkgs/by-name/mp/mpdpopm/Cargo.toml new file mode 100644 index 00000000..c82537e6 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/Cargo.toml @@ -0,0 +1,44 @@ +# Mpdpopm - A mpd rating tracker +# +# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com> +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This file is part of Mpdpopm. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/agpl.txt>. + +[package] +name = "mpdpopm" +description = "Maintain ratings & playcounts for your mpd server" +version = "0.1.0" +edition = "2024" +license = "AGPL-3.0-or-later" +homepage = "" +repository = "https://git.vhack.eu/bpeetz/nixos-config" +authors = ["Benedikt Peetz", "Mechael Herstine"] +keywords = ["mpd", "music", "daemon"] +categories = ["multimedia", "network-programming", "database"] + +[build-dependencies] +lalrpop = { version = "0.22", features = ["lexer"] } + +[dependencies] +async-trait = "0.1" +boolinator = "2.4" +chrono = "0.4" +clap = {version = "4.5", features = ["derive"]} +errno = "0.3" +futures = "0.3" +lalrpop-util = { version = "0.22", features = ["lexer"] } +lazy_static = "1.5" +os_str_bytes = "7.1" +pin-project = "1.1" +regex = "1.12" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.149" +toml = "0.9" +tokio = { version = "1.49", features = ["io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"]} +anyhow = "1.0.100" diff --git a/pkgs/by-name/mp/mpdpopm/README.md b/pkgs/by-name/mp/mpdpopm/README.md new file mode 100644 index 00000000..3c2d961b --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/README.md @@ -0,0 +1,260 @@ + +# Table of Contents + +1. [Introduction](#orgb2618c9) +2. [What Can You Do With It?](#orgf1adf2c) +3. [Licsense](#org3f75b89) +4. [Prerequisites](#org67de102) +5. [Installing](#installing) + 1. [Use the pre-built binaries](#orgb2e3434) + 2. [Crates.io](#org971a8b3) + 3. [Use the Debian package](#org55e51f8) + 4. [Use the Arch package](#org49ada47) + 5. [Autotools source distributions](#org9c94559) + 6. [Building from source](#org64bc5dd) +6. [Getting Started](#getting_started) + 1. [Program Structure](#org4a22fae) + 2. [Getting Set-up](#orgfbd2d7d) + 1. [MPD](#orgb37b483) + 2. [mppopmd](#org38f4b69) + 3. [mppopm](#orgfa9dacf) +7. [Status & Roadmap](#orgd90c7da) + + + +<a id="orgb2618c9"></a> + +# Introduction + +[mpdpopm](https://github.com/sp1ff/mpdpopm) provides a companion daemon to [MPD](https://www.musicpd.org/) for maintaining play counts, ratings and last-played timestamps, along with an associated CLI for talking to the companion daemon. Similar to [mpdfav](https://github.com/vincent-petithory/mpdfav), but written in Rust (which I prefer to Go), it will maintain this information in your sticker database. Along the lines of [mpdcron](https://alip.github.io/mpdcron), it will also allow you to keep that information up-to-date in your tags by invoking external (user-provided & -configured) commands. + +This README focuses on obtaining & installing [mpdpopm](https://github.com/sp1ff/mpdpopm); the user manual is distributed with the package in [Texinfo](https://www.gnu.org/software/texinfo/) format. The HTML version of the user manual is hosted on my personal [site](https://www.unwoundstack.com/doc/mpdpopm/curr). + + +<a id="orgf1adf2c"></a> + +# What Can You Do With It? + +Once you've [installed](#installing) & [started](#getting_started) [mpdpopm](https://github.com/sp1ff/mpdpopm), its daemon (`mppopmd`) will sit in the background noting the songs you play and updating play counts & last played timestamps in your [MPD](https://www.musicpd.org/) sticker database. If you'd like to rate a song, you can send `mppopmd` a message using your favorte MPD client, or with the `mppopm` CLI that comes along with this package; `mppopmd` will note the rating, as well. + +If you'd like to make use of this information in your song selection, you can ask `mppopmd` to queue-up songs on this basis by saying things like: + + mppopm findadd "(rating > 128)" + +to add all songs with a rating greater than 128 to the play queue, or + + mppopm findadd "(lastplayed <= \"2022-12-28\")" + +to add all songs that haven't been played in the last year. + + +<a id="org3f75b89"></a> + +# Licsense + +[mpdpopm](https://github.com/sp1ff/mpdpopm) is GPL v3 software. + + +<a id="org67de102"></a> + +# Prerequisites + +[Music Player Daemon](https://www.musicpd.org/): "Music Player Daemon (MPD) is a flexible, powerful, server-side application for playing music. Through plugins and libraries it can play a variety of sound files while being controlled by its network protocol." If you're reading this, I assume you're already running MPD, so this document won't have much to say on installing & configuring it other than that you **do** need to setup the sticker database by setting `sticker_file` in your configuration. + +If you choose to use the pre-built binaries or the Debian or Arch packages (available under [releases](https://github.com/sp1ff/mpdpopm/releases)), that's all you'll need– you can jump ahead to the section entitled [Installing](#getting_started), below. + +If you would prefer to download [mpdpopm](https://github.com/sp1ff/mpdpopm) from [crates.io](https://crates.io/crates/mpdpopm), you'll need need the [Rust](https://www.rust-lang.org/tools/install) toolchain ("Rust is a memory- & thread-safe language with no runtime or garbage collector"). Installing the toolchain is easy: + + curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh + +[mpdpopm](https://github.com/sp1ff/mpdpopm) is also available as an Autotools source distribution (also under [releases](https://github.com/sp1ff/mpdpopm/releases)), and of course you can just clone the repo & build the project from source. In either of those two cases you'll need the Gnu [Autotools](https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html) installed in addition to Rust. In the former case, grab the tarball in the format of your choice & perform the usual "./configure && make && make install" incantation. In the latter, you'll need to invoke "./bootstrap" after you clone the repo. Again, if you're considering that route, I assume you're familiar with the Autotools & won't say much about them here. + + +<a id="installing"></a> + +# Installing + +As mentioned above, you can install [mpdpopm](https://github.com/sp1ff/mpdpopm) in a few different ways. In increasing order of complexity: + + +<a id="orgb2e3434"></a> + +## Use the pre-built binaries + +Thanks to a suggestion by [m040601](https://github.com/m040601), you can download pre-built binaries for each [release](https://github.com/sp1ff/mpdpopm/releases). At the time of this writing, only Linux & MacOS are supported, and only on x86<sub>64</sub> at that. If that works for you, you can do something like: + + cd /tmp + curl -L --output mpdpopm-0.3.5.tar.gz https://github.com/sp1ff/mpdpopm/releases/download/0.3.5/mpdpopm-0.3.5-x86_64-unknown-linux.tar.gz + tar xf mpdpopm-0.3.5.tar.gz + tree mpdpopm-0.3.5-x86_64-unknown-linux/ + mpdpopm-0.3.5-x86_64-unknown-linux/ + ├── bin + │ ├── mppopm + │ └── mppopmd + └── doc + ├── AUTHORS + ├── ChangeLog + ├── COPYING + ├── NEWS + ├── README.org + ├── THANKS + ├── mppopmd.conf + ├── mppopmd.info + └── mppopmd.service + + 2 directories, 10 files + +Copy the binaries `mppopmd` (the daemon) and `mppopm` (the CLI) to a convenient place (e.g. `/usr/local/bin` or `$HOME/.local/bin`) and proceed to [Getting Started](#getting_started), below. + + +<a id="org971a8b3"></a> + +## Crates.io + +If you've got the Rust toolchain installed, just say `cargo install mpdpopm`. The binaries will now be in `$HOME/.cargo/bin`, and you can proceed to [Getting Started](#getting_started), below. + + +<a id="org55e51f8"></a> + +## Use the Debian package + +If you're running on a Debian-based Linux distribution, and you're on an x86<sub>64</sub> processor, I've begun providing a Debian binary package, courtesy of the very cool [cargo-deb](https://github.com/mmstick/cargo-deb) Cargo helper command. Just do: + + cd /tmp + curl -L -O https://github.com/sp1ff/mpdpopm/releases/download/0.3.5/mpdpopm_0.3.5_amd64.deb + sudo dpkg -i mpdpopm_0.3.5_amd64.deb + +The binaries will be placed in `/usr/local/bin`, and you can proceed to [Getting Started](#getting_started), below. + + +<a id="org49ada47"></a> + +## Use the Arch package + +If you're running on an Arch-based Linux distribution, I maintain a few packages in the [AUR](https://aur.archlinux.org/): + +- [mpdpopm](https://aur.archlinux.org/packages/mpdpopm): which will grab the latest release & build it locally +- [mpdpopm-git](https://aur.archlinux.org/packages/mpdpopm-git): grab `HEAD` from `master` & build it locally +- [mpdpopm-bin](https://aur.archlinux.org/packages/mpdpopm-bin): grab the pre-built binaries from the latest release & install 'em + +You can clone the git repo for whichever package you'd like to use (remotes at link), do `makepkg` & then use pacman to install the package you just built, or use an AUR package manager (I use `yay`, e.g.) + + +<a id="org9c94559"></a> + +## Autotools source distributions + +If you've got the Rust toolchain as well as Autotools installed, you can build from source via Autotools: + + cd /tmp + curl -L -O https://github.com/sp1ff/mpdpopm/releases/download/0.3.5/mpdpopm-0.3.5.tar.xz + tar xf mpdpopm-0.3.5.tar.xz + cd mpdpopm-0.3.5 + ./configure + make + make check + sudo make install + +All the usual `configure` options apply (`--prefix`, e.g.) In particular, you can say `--enable-debug` to produce debug builds. + + +<a id="org64bc5dd"></a> + +## Building from source + +Finally, and again if you have the build toolchain (Rust & Autotools) installed, you can build from source: + + git clone git@github.com:sp1ff/mpdpopm.git + cd mpdpopm + ./bootstrap + ./configure + make + make check + sudo make install + +Notice the call to `./bootstrap`, in this case. + + +<a id="getting_started"></a> + +# Getting Started + +This README provides a "quick-start" guide to getting mpdpopm up & running. For detailed user docs, refer to the [manual](https://www.unwoundstack.com/doc/mpdpopm/curr). + + +<a id="org4a22fae"></a> + +## Program Structure + +[mpdpopm](https://github.com/sp1ff/mpdpopm) provides two programs: + +1. `mppopmd` is the companion daemon process +2. `mppopm` is the associated command-line interface to the daemon + +`mppopmd` will monitor `mpd` for song playback & note when songs complete; this is how it knows to increment the playcount & update the last played timestamp for each song to which you listen. `mppopmd` records this information (i.e. play counts, last played and ratings) using `mpd` [stickers](https://www.musicpd.org/doc/html/protocol.html#stickers). A sticker is a little bit of textual information which clients can attach to songs in the form of a name-value pair. [mpdpopm](https://github.com/sp1ff/mpdpopm) defines a new sticker name for each of these items & udpates the values for each song when & as requested. + +Of course, other `mpd` clients will not, in general, be aware of `mppopmd` or the stickers it sets: you the user will have to bridge that gap. You could of course just fire-up `netcat` & start sending commands over the MPD protocol using `sendmessage`, but that's not particularly convenient– that's where `mppopm` comes in. `mppopm` is the client interface; one can through it instruct `mppopmd` to set ratings, get & set the various stickers mpdpopm knows about, and even search for songs in terms of mpdpopm attributes & add them to the play queue. + + +<a id="orgfbd2d7d"></a> + +## Getting Set-up + + +<a id="orgb37b483"></a> + +### MPD + +If you're reading this, I assume you already have MPD up & running, so this section will be brief. One note, prompted by user [m040601](https://github.com/m040601), however: as mentioned above, [mpdpopm](https://github.com/sp1ff/mpdpopm) leverages the MPD sticker database. I was chagrined to find that if you do not configure MPD to maintain a sticker database, all sticker commands will simply be disabled. Therefore, before setting up [mpdpopm](https://github.com/sp1ff/mpdpopm), find your `mpd` configuration file and check to be sure you have a `sticker_file` entry; something like this: + + sticker_file "/home/sp1ff/lib/mpd/sticker.sql" + +Check also that the you have write access to the named file & its parent directory. + + +<a id="org38f4b69"></a> + +### mppopmd + +The daemon depends on a configuration file that you'll need to provide. Most `mppopmd` configuration items have sensible defaults, but there are a few that will need to be customized to your MPD setup. A sample configuration file is provided with all distributions; see also the user [manual](https://www.unwoundstack.com/doc/mpdpopm/curr#mppopmd-Configuration) for detailed documentation. + +You'll likely want to run the program in the foreground initially for ease of trouble-shooting, but after that you'll probably want to run it as a daemon. Again see the [manual](https://www.unwoundstack.com/doc/mpdopmd/curr#mppopmd-as-a-Daemon) for detailed instructions. + +Once you've got the daemon running to your satisfaction, if you're on a systemd-based Linux distribution, have a look at the sample systemd unit file thanks to [tanshoku](https://github.com/tanshoku). + +[tanshoku](https://github.com/tanshoku) was kind enough to contribute a systemd unit for this purpose. At present, the build does not install it, but provides it as an example and leaves it to the user to install should they desire (and after they have edited it to suit their configuration). You can find it in `${prefix}/share/mpdpopm/examples` for the Autotools distribution, `/usr/local/share/mpdpopm/examples` for the Debian package, and in the `doc` folder for the pre-built binaries. + + +<a id="orgfa9dacf"></a> + +### mppopm + +At this point, [mpdpopm](https://github.com/sp1ff/mpdpopm) will happily monitor your playback history & keep play counts & last played timestamps for you. If you would like to rate tracks, however, you will need to somehow induce your favorite mpd client to send a "rating" message to the [mpdpopm](https://github.com/sp1ff/mpdpopm) commands channel ("unwoundstack.com:commands" by default). Since this is unlikely to be convenient, I wrote an mpd client for the purpose: a little CLI called `mppopm`. You can simply execute + + mppopm set-rating '*****' + +to set the current track's rating to five "stars" (say `mppopm --help` for an explanation of the rating system; in brief– it's Winamp's). NB. the set rating command by default produces no output; if you want confirmation that something's happening, use the `-v` flag. + +The CLI offers "get" & "set" commands for play counts, last played timestamps & the rating. It also provides commands for searching your songs on the basis of play count, rating & last played times in addition to the usual artist, title &c. Say `mppopm --help` for a full list of options, including how to tell it where the mpd server can be found on your network. + + +<a id="orgd90c7da"></a> + +# Status & Roadmap + +I am currently using [mpdpopm](https://github.com/sp1ff/mpdpopm) day in & day out with my music collection, but it's early days; I have chosen the version number (0.n) in the hopes of indicating that. Right now, mpdpopm is the bare-bones of an app: it's plumbing, not the sink. + +Heretofore, you could use the `mppopm` CLI to, say, rate the current song, but in order to actually <span class="underline">do</span> anything with that rating in the future, you'd have had to write some kind of mpd client for yourself. With the 0.2 release, I've added support for extended MPD filter syntax that allows queries that include the stickers that [mpdpopm](https://github.com/sp1ff/mpdpopm) manages– so you can now, for instance, say: + + mppopm findadd "(artist =~ \"foo\") and (rating > 175)" + +MPD will handle the "artist =~" clause & [mpdpopm](https://github.com/sp1ff/mpdpopm) the "rating >" clause, as well as combining the results. + +This will hopefully be a start to making [mpdpopm](https://github.com/sp1ff/mpdpopm) into a more of a user-facing application than a developer-facing utlity. + +Windows support may be some time coming; the daemon depends on Unix signal handling, the MPD Unix socket, and the Unix daemon logic, especially `fork` & `exec`… if you'd like to run it on Windows, let me know– if there's enough interest, and I can get some kind of Windows VM setup, I'll look at a port. + +Longer-term, I see [mpdpopm](https://github.com/sp1ff/mpdpopm) as a "dual" to mpd– mpd commits to never altering your files. mpdpopm will take on that task in terms of tags, at least. To address the "plumbing, not the sink" problem, I'd like to author a client that will handle player control (of course), but also visualization & tag editing– a complete music library solution. + +Suggestions, bug reports & PRs welcome! + diff --git a/pkgs/by-name/mp/mpdpopm/README.org b/pkgs/by-name/mp/mpdpopm/README.org new file mode 100644 index 00000000..ebc91262 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/README.org @@ -0,0 +1,214 @@ +#+TITLE: README +#+AUTHOR: Michael Herstine +#+DESCRIPTION: mpdpopm +#+EMAIL: sp1ff@pobox.com +#+DATE: <2025-10-19 Sun 19:17> +#+AUTODATE: t + +* Introduction + +[[https://github.com/sp1ff/mpdpopm][mpdpopm]] provides a companion daemon to [[https://www.musicpd.org/][MPD]] for maintaining play counts, ratings and last-played timestamps, along with an associated CLI for talking to the companion daemon. Similar to [[https://github.com/vincent-petithory/mpdfav][mpdfav]], but written in Rust (which I prefer to Go), it will maintain this information in your sticker database. Along the lines of [[https://alip.github.io/mpdcron][mpdcron]], it will also allow you to keep that information up-to-date in your tags by invoking external (user-provided & -configured) commands. + +This README focuses on obtaining & installing [[https://github.com/sp1ff/mpdpopm][mpdpopm]]; the user manual is distributed with the package in [[https://www.gnu.org/software/texinfo/][Texinfo]] format. The HTML version of the user manual is hosted on my personal [[https://www.unwoundstack.com/doc/mpdpopm/curr][site]]. + +* What Can You Do With It? + +Once you've [[#installing][installed]] & [[#getting_started][started]] [[https://github.com/sp1ff/mpdpopm][mpdpopm]], its daemon (=mppopmd=) will sit in the background noting the songs you play and updating play counts & last played timestamps in your [[https://www.musicpd.org/][MPD]] sticker database. If you'd like to rate a song, you can send =mppopmd= a message using your favorte MPD client, or with the =mppopm= CLI that comes along with this package; =mppopmd= will note the rating, as well. + +If you'd like to make use of this information in your song selection, you can ask =mppopmd= to queue-up songs on this basis by saying things like: + +#+BEGIN_SRC bash +mppopm findadd "(rating > 128)" +#+END_SRC + +to add all songs with a rating greater than 128 to the play queue, or + +#+BEGIN_SRC bash +mppopm findadd "(lastplayed <= \"2022-12-28\")" +#+END_SRC + +to add all songs that haven't been played in the last year. + +* Licsense + +[[https://github.com/sp1ff/mpdpopm][mpdpopm]] is GPL v3 software. + +* Prerequisites + +[[https://www.musicpd.org/][Music Player Daemon]]: "Music Player Daemon (MPD) is a flexible, powerful, server-side application for playing music. Through plugins and libraries it can play a variety of sound files while being controlled by its network protocol." If you're reading this, I assume you're already running MPD, so this document won't have much to say on installing & configuring it other than that you *do* need to setup the sticker database by setting =sticker_file= in your configuration. + +If you choose to use the pre-built binaries or the Debian or Arch packages (available under [[https://github.com/sp1ff/mpdpopm/releases][releases]]), that's all you'll need-- you can jump ahead to the section entitled [[#getting_started][Installing]], below. + +If you would prefer to download [[https://github.com/sp1ff/mpdpopm][mpdpopm]] from [[https://crates.io/crates/mpdpopm][crates.io]], you'll need need the [[https://www.rust-lang.org/tools/install][Rust]] toolchain ("Rust is a memory- & thread-safe language with no runtime or garbage collector"). Installing the toolchain is easy: + +#+BEGIN_SRC bash +curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh +#+END_SRC + +[[https://github.com/sp1ff/mpdpopm][mpdpopm]] is also available as an Autotools source distribution (also under [[https://github.com/sp1ff/mpdpopm/releases][releases]]), and of course you can just clone the repo & build the project from source. In either of those two cases you'll need the Gnu [[https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html][Autotools]] installed in addition to Rust. In the former case, grab the tarball in the format of your choice & perform the usual "./configure && make && make install" incantation. In the latter, you'll need to invoke "./bootstrap" after you clone the repo. Again, if you're considering that route, I assume you're familiar with the Autotools & won't say much about them here. + +* Installing + :PROPERTIES: + :CUSTOM_ID: installing + :END: + +As mentioned above, you can install [[https://github.com/sp1ff/mpdpopm][mpdpopm]] in a few different ways. In increasing order of complexity: + +** Use the pre-built binaries + +Thanks to a suggestion by [[https://github.com/m040601][m040601]], you can download pre-built binaries for each [[https://github.com/sp1ff/mpdpopm/releases][release]]. At the time of this writing, only Linux & MacOS are supported, and only on x86_64 at that. If that works for you, you can do something like: + +#+BEGIN_SRC bash +cd /tmp +curl -L --output mpdpopm-0.3.5.tar.gz https://github.com/sp1ff/mpdpopm/releases/download/0.3.5/mpdpopm-0.3.5-x86_64-unknown-linux.tar.gz +tar xf mpdpopm-0.3.5.tar.gz +tree mpdpopm-0.3.5-x86_64-unknown-linux/ +mpdpopm-0.3.5-x86_64-unknown-linux/ +├── bin +│ ├── mppopm +│ └── mppopmd +└── doc + ├── AUTHORS + ├── ChangeLog + ├── COPYING + ├── NEWS + ├── README.org + ├── THANKS + ├── mppopmd.conf + ├── mppopmd.info + └── mppopmd.service + +2 directories, 10 files +#+END_SRC + +Copy the binaries =mppopmd= (the daemon) and =mppopm= (the CLI) to a convenient place (e.g. =/usr/local/bin= or =$HOME/.local/bin=) and proceed to [[#getting_started][Getting Started]], below. + +** Crates.io + +If you've got the Rust toolchain installed, just say =cargo install mpdpopm=. The binaries will now be in =$HOME/.cargo/bin=, and you can proceed to [[#getting_started][Getting Started]], below. + +** Use the Debian package + +If you're running on a Debian-based Linux distribution, and you're on an x86_64 processor, I've begun providing a Debian binary package, courtesy of the very cool [[https://github.com/mmstick/cargo-deb][cargo-deb]] Cargo helper command. Just do: + +#+BEGIN_SRC bash +cd /tmp +curl -L -O https://github.com/sp1ff/mpdpopm/releases/download/0.3.5/mpdpopm_0.3.5_amd64.deb +sudo dpkg -i mpdpopm_0.3.5_amd64.deb +#+END_SRC + +The binaries will be placed in =/usr/local/bin=, and you can proceed to [[#getting_started][Getting Started]], below. + +** Use the Arch package + +If you're running on an Arch-based Linux distribution, I maintain a few packages in the [[https://aur.archlinux.org/][AUR]]: + + - [[https://aur.archlinux.org/packages/mpdpopm][mpdpopm]]: which will grab the latest release & build it locally + - [[https://aur.archlinux.org/packages/mpdpopm-git][mpdpopm-git]]: grab =HEAD= from =master= & build it locally + - [[https://aur.archlinux.org/packages/mpdpopm-bin][mpdpopm-bin]]: grab the pre-built binaries from the latest release & install 'em + +You can clone the git repo for whichever package you'd like to use (remotes at link), do =makepkg= & then use pacman to install the package you just built, or use an AUR package manager (I use =yay=, e.g.) +** Autotools source distributions + +If you've got the Rust toolchain as well as Autotools installed, you can build from source via Autotools: + +#+BEGIN_SRC bash +cd /tmp +curl -L -O https://github.com/sp1ff/mpdpopm/releases/download/0.3.5/mpdpopm-0.3.5.tar.xz +tar xf mpdpopm-0.3.5.tar.xz +cd mpdpopm-0.3.5 +./configure +make +make check +sudo make install +#+END_SRC + +All the usual =configure= options apply (=--prefix=, e.g.) In particular, you can say =--enable-debug= to produce debug builds. + +** Building from source + +Finally, and again if you have the build toolchain (Rust & Autotools) installed, you can build from source: + +#+BEGIN_SRC bash +git clone git@github.com:sp1ff/mpdpopm.git +cd mpdpopm +./bootstrap +./configure +make +make check +sudo make install +#+END_SRC + +Notice the call to =./bootstrap=, in this case. + +* Getting Started + :PROPERTIES: + :CUSTOM_ID: getting_started + :END: + +This README provides a "quick-start" guide to getting mpdpopm up & running. For detailed user docs, refer to the [[https://www.unwoundstack.com/doc/mpdpopm/curr][manual]]. + +** Program Structure + +[[https://github.com/sp1ff/mpdpopm][mpdpopm]] provides two programs: + + 1. =mppopmd= is the companion daemon process + 2. =mppopm= is the associated command-line interface to the daemon + +=mppopmd= will monitor =mpd= for song playback & note when songs complete; this is how it knows to increment the playcount & update the last played timestamp for each song to which you listen. =mppopmd= records this information (i.e. play counts, last played and ratings) using =mpd= [[https://www.musicpd.org/doc/html/protocol.html#stickers][stickers]]. A sticker is a little bit of textual information which clients can attach to songs in the form of a name-value pair. [[https://github.com/sp1ff/mpdpopm][mpdpopm]] defines a new sticker name for each of these items & udpates the values for each song when & as requested. + +Of course, other =mpd= clients will not, in general, be aware of =mppopmd= or the stickers it sets: you the user will have to bridge that gap. You could of course just fire-up =netcat= & start sending commands over the MPD protocol using =sendmessage=, but that's not particularly convenient-- that's where =mppopm= comes in. =mppopm= is the client interface; one can through it instruct =mppopmd= to set ratings, get & set the various stickers mpdpopm knows about, and even search for songs in terms of mpdpopm attributes & add them to the play queue. + +** Getting Set-up + +*** MPD + +If you're reading this, I assume you already have MPD up & running, so this section will be brief. One note, prompted by user [[https://github.com/m040601][m040601]], however: as mentioned above, [[https://github.com/sp1ff/mpdpopm][mpdpopm]] leverages the MPD sticker database. I was chagrined to find that if you do not configure MPD to maintain a sticker database, all sticker commands will simply be disabled. Therefore, before setting up [[https://github.com/sp1ff/mpdpopm][mpdpopm]], find your =mpd= configuration file and check to be sure you have a =sticker_file= entry; something like this: + +#+BEGIN_EXAMPLE + sticker_file "/home/sp1ff/lib/mpd/sticker.sql" +#+END_EXAMPLE + +Check also that the you have write access to the named file & its parent directory. + +*** mppopmd + +The daemon depends on a configuration file that you'll need to provide. Most =mppopmd= configuration items have sensible defaults, but there are a few that will need to be customized to your MPD setup. A sample configuration file is provided with all distributions; see also the user [[https://www.unwoundstack.com/doc/mpdpopm/curr#mppopmd-Configuration][manual]] for detailed documentation. + +You'll likely want to run the program in the foreground initially for ease of trouble-shooting, but after that you'll probably want to run it as a daemon. Again see the [[https://www.unwoundstack.com/doc/mpdopmd/curr#mppopmd-as-a-Daemon][manual]] for detailed instructions. + +Once you've got the daemon running to your satisfaction, if you're on a systemd-based Linux distribution, have a look at the sample systemd unit file thanks to [[https://github.com/tanshoku][tanshoku]]. + +[[https://github.com/tanshoku][tanshoku]] was kind enough to contribute a systemd unit for this purpose. At present, the build does not install it, but provides it as an example and leaves it to the user to install should they desire (and after they have edited it to suit their configuration). You can find it in =${prefix}/share/mpdpopm/examples= for the Autotools distribution, =/usr/local/share/mpdpopm/examples= for the Debian package, and in the =doc= folder for the pre-built binaries. + +*** mppopm + +At this point, [[https://github.com/sp1ff/mpdpopm][mpdpopm]] will happily monitor your playback history & keep play counts & last played timestamps for you. If you would like to rate tracks, however, you will need to somehow induce your favorite mpd client to send a "rating" message to the [[https://github.com/sp1ff/mpdpopm][mpdpopm]] commands channel ("unwoundstack.com:commands" by default). Since this is unlikely to be convenient, I wrote an mpd client for the purpose: a little CLI called =mppopm=. You can simply execute + +#+BEGIN_SRC bash +mppopm set-rating '*****' +#+END_SRC + +to set the current track's rating to five "stars" (say =mppopm --help= for an explanation of the rating system; in brief-- it's Winamp's). NB. the set rating command by default produces no output; if you want confirmation that something's happening, use the =-v= flag. + +The CLI offers "get" & "set" commands for play counts, last played timestamps & the rating. It also provides commands for searching your songs on the basis of play count, rating & last played times in addition to the usual artist, title &c. Say =mppopm --help= for a full list of options, including how to tell it where the mpd server can be found on your network. + +* Status & Roadmap + +I am currently using [[https://github.com/sp1ff/mpdpopm][mpdpopm]] day in & day out with my music collection, but it's early days; I have chosen the version number (0.n) in the hopes of indicating that. Right now, mpdpopm is the bare-bones of an app: it's plumbing, not the sink. + +Heretofore, you could use the =mppopm= CLI to, say, rate the current song, but in order to actually _do_ anything with that rating in the future, you'd have had to write some kind of mpd client for yourself. With the 0.2 release, I've added support for extended MPD filter syntax that allows queries that include the stickers that [[https://github.com/sp1ff/mpdpopm][mpdpopm]] manages-- so you can now, for instance, say: + +#+BEGIN_EXAMPLE +mppopm findadd "(artist =~ \"foo\") and (rating > 175)" +#+END_EXAMPLE + +MPD will handle the "artist =~" clause & [[https://github.com/sp1ff/mpdpopm][mpdpopm]] the "rating >" clause, as well as combining the results. + +This will hopefully be a start to making [[https://github.com/sp1ff/mpdpopm][mpdpopm]] into a more of a user-facing application than a developer-facing utlity. + +Windows support may be some time coming; the daemon depends on Unix signal handling, the MPD Unix socket, and the Unix daemon logic, especially =fork= & =exec=... if you'd like to run it on Windows, let me know-- if there's enough interest, and I can get some kind of Windows VM setup, I'll look at a port. + +Longer-term, I see [[https://github.com/sp1ff/mpdpopm][mpdpopm]] as a "dual" to mpd-- mpd commits to never altering your files. mpdpopm will take on that task in terms of tags, at least. To address the "plumbing, not the sink" problem, I'd like to author a client that will handle player control (of course), but also visualization & tag editing-- a complete music library solution. + +Suggestions, bug reports & PRs welcome! diff --git a/pkgs/by-name/mp/mpdpopm/build.rs b/pkgs/by-name/mp/mpdpopm/build.rs new file mode 100644 index 00000000..3d8bbd48 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/build.rs @@ -0,0 +1,12 @@ +extern crate lalrpop; +fn main() { + let out_dir = std::env::var("OUT_DIR").unwrap(); + + lalrpop::Configuration::new() + .emit_comments(true) + .emit_whitespace(true) + .log_verbose() + .set_out_dir(out_dir) + .process_dir("./") + .unwrap(); +} diff --git a/pkgs/by-name/mp/mpdpopm/config.lsp b/pkgs/by-name/mp/mpdpopm/config.lsp new file mode 100644 index 00000000..4b657226 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/config.lsp @@ -0,0 +1,13 @@ +;; SAMPLE MPPOPMD CONFIGURATION FILE -*- mode: lisp; -*- +;; You will need to edit this to suit your particular installation. +;; In particular, examine the `log' & `local_music_dir' values since those +;; are difficult to guess. Also check the `host' & `port' settings. +((version . "1") + (log . "/home/soispha/.local/share/mppopmd/log") + ;; (conn TCP (host . "localhost") (port . 6600)) + ;; Replace the above line with this to use the local socket + (conn Local (path . "/run/user/1000/mpd/socket")) + (local_music_dir . "/home/soispha/media/music/beets") + (played_thresh . 0.6) + (poll_interval_ms . 5000) + (commands_chan . "unwoundstack.com:commands")) diff --git a/pkgs/by-name/mp/mpdpopm/flake.lock b/pkgs/by-name/mp/mpdpopm/flake.lock new file mode 100644 index 00000000..c1d50dc3 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769237874, + "narHash": "sha256-saOixpqPT4fiE/M8EfHv9I98f3sSEvt6nhMJ/z0a7xI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "523257564973361cc3e55e3df3e77e68c20b0b80", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768158989, + "narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/pkgs/by-name/mp/mpdpopm/flake.nix b/pkgs/by-name/mp/mpdpopm/flake.nix new file mode 100644 index 00000000..f6b622fe --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/flake.nix @@ -0,0 +1,66 @@ +# Mpdpopm - A mpd rating tracker +# +# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com> +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This file is part of Mpdpopm. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/agpl.txt>. +{ + description = "A mpd rating tracker"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs = { + nixpkgs.follows = "nixpkgs"; + }; + }; + }; + + outputs = { + self, + nixpkgs, + treefmt-nix, + ... + }: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages."${system}"; + + treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;}; + in { + checks."${system}" = { + formatting = treefmtEval.config.build.check self; + }; + + formatter."${system}" = treefmtEval.config.build.wrapper; + + devShells."${system}".default = pkgs.mkShell { + packages = [ + # rust stuff + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt + pkgs.mold + + pkgs.cargo-edit + pkgs.cargo-expand + pkgs.cargo-flamegraph + + # Releng + pkgs.git-bug + pkgs.reuse + pkgs.cocogitto + + # Perf + pkgs.hyperfine + ]; + }; + }; +} +# vim: ts=2 + diff --git a/pkgs/by-name/mp/mpdpopm/package.nix b/pkgs/by-name/mp/mpdpopm/package.nix new file mode 100644 index 00000000..907bb1cf --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/package.nix @@ -0,0 +1,29 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + rustPlatform, + lib, +}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "mpdpopm"; + version = "0.1.0"; + + buildInputs = []; + nativeBuildInputs = [ ]; + + src = ./.; + cargoLock = { + lockFile = ./Cargo.lock; + }; + + meta = { + mainProgram = "mpdpopm"; + }; +}) diff --git a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs new file mode 100644 index 00000000..d9d607d5 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs @@ -0,0 +1,597 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! # mppopm +//! +//! mppopmd client +//! +//! # Introduction +//! +//! `mppopmd` is a companion daemon for [mpd](https://www.musicpd.org/) that maintains play counts & +//! ratings. Similar to [mpdfav](https://github.com/vincent-petithory/mpdfav), but written in Rust +//! (which I prefer to Go), it will allow you to maintain that information in your tags, as well as +//! the sticker database, by invoking external commands to keep your tags up-to-date (something +//! along the lines of [mpdcron](https://alip.github.io/mpdcron)). `mppopm` is a command-line client +//! for `mppopmd`. Run `mppopm --help` for detailed usage. + +use mpdpopm::{ + clients::{Client, PlayerStatus, quote}, + config::{self, Config}, + storage::{last_played, play_count, rating_count}, +}; + +use anyhow::{Context, Result, anyhow, bail}; +use clap::{Parser, Subcommand}; +use tracing::{debug, info, level_filters::LevelFilter, trace}; +use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt}; + +use std::path::PathBuf; + +/// Map `tracks' argument(s) to a Vec of String containing one or more mpd URIs +/// +/// Several sub-commands take zero or more positional arguments meant to name tracks, with the +/// convention that zero indicates that the sub-command should use the currently playing track. +/// This is a convenience function for mapping the value returned by [`get_many`] to a +/// convenient representation of the user's intentions. +/// +/// [`get_many`]: [`clap::ArgMatches::get_many`] +async fn map_tracks(client: &mut Client, args: Option<Vec<String>>) -> Result<Vec<String>> { + let files = match args { + Some(iter) => iter, + None => { + let file = provide_file(client, None).await?; + vec![file] + } + }; + Ok(files) +} + +async fn provide_file(client: &mut Client, maybe_file: Option<String>) -> Result<String> { + let file = match maybe_file { + Some(file) => file, + None => { + match client + .status() + .await + .context("Failed to get status of client")? + { + PlayerStatus::Play(curr) | PlayerStatus::Pause(curr) => curr + .file + .to_str() + .ok_or_else(|| anyhow!("Path is not utf8: `{}`", curr.file.display()))? + .to_string(), + PlayerStatus::Stopped => { + bail!("Player is stopped"); + } + } + } + }; + + Ok(file) +} + +/// Retrieve ratings for one or more tracks +async fn get_ratings( + client: &mut Client, + tracks: Option<Vec<String>>, + with_uri: bool, +) -> Result<()> { + let mut ratings: Vec<(String, i8)> = Vec::new(); + + for file in map_tracks(client, tracks).await? { + let rating = rating_count::get(client, &file).await?; + + ratings.push((file, rating.unwrap_or_default())); + } + + if ratings.len() == 1 && !with_uri { + println!("{}", ratings[0].1); + } else { + for pair in ratings { + println!("{}: {}", pair.0, pair.1); + } + } + + Ok(()) +} + +/// Rate a track +async fn set_rating(client: &mut Client, rating: i8, arg: Option<String>) -> Result<()> { + let is_current = arg.is_none(); + let file = provide_file(client, arg).await?; + + rating_count::set(client, &file, rating).await?; + + match is_current { + false => info!("Set the rating for \"{}\" to \"{}\".", file, rating), + true => info!("Set the rating for the current song to \"{}\".", rating), + } + + Ok(()) +} + +/// Rate a track by incrementing the current rating +async fn inc_rating(client: &mut Client, arg: Option<String>) -> Result<()> { + let is_current = arg.is_none(); + let file = provide_file(client, arg).await?; + + let now = rating_count::get(client, &file).await?; + + rating_count::set(client, &file, now.unwrap_or_default().saturating_add(1)).await?; + + match is_current { + false => info!("Incremented the rating for \"{}\".", file), + true => info!("Incremented the rating for the current song."), + } + + Ok(()) +} + +/// Rate a track by decrementing the current rating +async fn decr_rating(client: &mut Client, arg: Option<String>) -> Result<()> { + let is_current = arg.is_none(); + let file = provide_file(client, arg).await?; + + let now = rating_count::get(client, &file).await?; + + rating_count::set(client, &file, now.unwrap_or_default().saturating_sub(1)).await?; + + match is_current { + false => info!("Decremented the rating for \"{}\".", file), + true => info!("Decremented the rating for the current song."), + } + + Ok(()) +} + +/// Retrieve the playcount for one or more tracks +async fn get_play_counts( + client: &mut Client, + tracks: Option<Vec<String>>, + with_uri: bool, +) -> Result<()> { + let mut playcounts: Vec<(String, usize)> = Vec::new(); + for file in map_tracks(client, tracks).await? { + let playcount = play_count::get(client, &file).await?.unwrap_or_default(); + playcounts.push((file, playcount)); + } + + if playcounts.len() == 1 && !with_uri { + println!("{}", playcounts[0].1); + } else { + for pair in playcounts { + println!("{}: {}", pair.0, pair.1); + } + } + + Ok(()) +} + +/// Set the playcount for a track +async fn set_play_counts(client: &mut Client, playcount: usize, arg: Option<String>) -> Result<()> { + let is_current = arg.is_none(); + let file = provide_file(client, arg).await?; + + play_count::set(client, &file, playcount).await?; + + match is_current { + false => info!("Set the playcount for \"{}\" to \"{}\".", file, playcount), + true => info!( + "Set the playcount for the current song to \"{}\".", + playcount + ), + } + + Ok(()) +} + +/// Retrieve the last played time for one or more tracks +async fn get_last_playeds( + client: &mut Client, + tracks: Option<Vec<String>>, + with_uri: bool, +) -> Result<()> { + let mut lastplayeds: Vec<(String, Option<u64>)> = Vec::new(); + for file in map_tracks(client, tracks).await? { + let lastplayed = last_played::get(client, &file).await?; + lastplayeds.push((file, lastplayed)); + } + + if lastplayeds.len() == 1 && !with_uri { + println!( + "{}", + match lastplayeds[0].1 { + Some(t) => format!("{}", t), + None => String::from("N/A"), + } + ); + } else { + for pair in lastplayeds { + println!( + "{}: {}", + pair.0, + match pair.1 { + Some(t) => format!("{}", t), + None => String::from("N/A"), + } + ); + } + } + + Ok(()) +} + +/// Set the playcount for a track +async fn set_last_playeds(client: &mut Client, lastplayed: u64, arg: Option<String>) -> Result<()> { + let is_current = arg.is_none(); + let file = provide_file(client, arg).await?; + + last_played::set(client, &file, lastplayed).await?; + + match is_current { + false => info!("Set last played for \"{}\" to \"{}\".", file, lastplayed), + true => info!( + "Set last played for the current song to \"{}\".", + lastplayed + ), + } + + Ok(()) +} + +/// Retrieve the list of stored playlists +async fn get_playlists(client: &mut Client) -> Result<()> { + let mut pls = client.get_stored_playlists().await?; + pls.sort(); + println!("Stored playlists:"); + for pl in pls { + println!("{}", pl); + } + Ok(()) +} + +/// Add songs selected by filter to the queue +async fn findadd(client: &mut Client, chan: &str, filter: &str, case: bool) -> Result<()> { + let qfilter = quote(filter); + debug!("findadd: got ``{}'', quoted to ``{}''.", filter, qfilter); + let cmd = format!("{} {}", if case { "findadd" } else { "searchadd" }, qfilter); + client.send_message(chan, &cmd).await?; + Ok(()) +} + +/// Send an arbitrary command +async fn send_command(client: &mut Client, chan: &str, args: Vec<String>) -> Result<()> { + client + .send_message( + chan, + args.iter() + .map(String::as_str) + .map(quote) + .collect::<Vec<String>>() + .join(" ") + .as_str(), + ) + .await?; + Ok(()) +} + +/// `mppopmd' client +#[derive(Parser)] +struct Args { + /// path to configuration file + #[arg(short, long)] + config: Option<PathBuf>, + + /// enable verbose logging + #[arg(short, long)] + verbose: bool, + + /// enable debug loggin (implies --verbose) + #[arg(short, long)] + debug: bool, + + #[command(subcommand)] + command: SubCommand, +} + +#[derive(Subcommand)] +enum RatingCommand { + /// retrieve the rating for one or more tracks + /// + /// With no arguments, retrieve the rating of the current song & print it + /// on stdout. With one argument, retrieve that track's rating & print it + /// on stdout. With multiple arguments, print their ratings on stdout, one + /// per line, prefixed by the track name. + /// + /// Ratings are expressed as an integer between 0 & 255, inclusive, with + /// the convention that 0 denotes "un-rated". + #[clap(verbatim_doc_comment)] + Get { + /// Always show the song URI, even when there is only one track + #[arg(short, long)] + with_uri: bool, + + tracks: Option<Vec<String>>, + }, + + /// set the rating for one track + /// + /// With one argument, set the rating of the current song to that argument. + /// With a second argument, rate that song at the first argument. Ratings + /// may be expressed a an integer between 0 & 255, inclusive. + #[clap(verbatim_doc_comment)] + Set { rating: i8, track: Option<String> }, + + /// increment the rating for one track + /// + /// With one argument, increment the rating of the current song. + /// With a second argument, rate that song at the first argument. + #[clap(verbatim_doc_comment)] + Inc { track: Option<String> }, + + /// decrement the rating for one track + /// + /// With one argument, decrement the rating of the current song. + /// With a second argument, rate that song at the first argument. + #[clap(verbatim_doc_comment)] + Decr { track: Option<String> }, +} + +#[derive(Subcommand)] +enum PlayCountCommand { + /// retrieve the play count for one or more tracks + /// + /// With no arguments, retrieve the play count of the current song & print it + /// on stdout. With one argument, retrieve that track's play count & print it + /// on stdout. With multiple arguments, print their play counts on stdout, one + /// per line, prefixed by the track name. + #[clap(verbatim_doc_comment)] + Get { + /// Always show the song URI, even when there is only one track + #[arg(short, long)] + with_uri: bool, + + tracks: Option<Vec<String>>, + }, + + /// set the play count for one track + /// + /// With one argument, set the play count of the current song to that argument. With a + /// second argument, set the play count for that song to the first. + #[clap(verbatim_doc_comment)] + Set { + play_count: usize, + track: Option<String>, + }, +} + +#[derive(Subcommand)] +enum LastPlayedCommand { + /// retrieve the last played timestamp for one or more tracks + /// + /// With no arguments, retrieve the last played timestamp of the current + /// song & print it on stdout. With one argument, retrieve that track's + /// last played time & print it on stdout. With multiple arguments, print + /// their last played times on stdout, one per line, prefixed by the track + /// name. + /// + /// The last played timestamp is expressed in seconds since Unix epoch. + #[clap(verbatim_doc_comment)] + Get { + /// Always show the song URI, even when there is only one track + #[arg(short, long)] + with_uri: bool, + + tracks: Option<Vec<String>>, + }, + + /// set the last played timestamp for one track + /// + /// With one argument, set the last played time of the current song. With two + /// arguments, set the last played time for the second argument to the first. + /// The last played timestamp is expressed in seconds since Unix epoch. + #[clap(verbatim_doc_comment)] + Set { + last_played: u64, + track: Option<String>, + }, +} + +#[derive(Subcommand)] +enum PlaylistsCommand { + /// retrieve the list of stored playlists + #[clap(verbatim_doc_comment)] + Get {}, +} + +#[derive(Subcommand)] +enum SubCommand { + /// Change details about rating. + Rating { + #[command(subcommand)] + command: RatingCommand, + }, + + /// Change details about play count. + PlayCount { + #[command(subcommand)] + command: PlayCountCommand, + }, + + /// Change details about last played date. + LastPlayed { + #[command(subcommand)] + command: LastPlayedCommand, + }, + + /// Change details about generated playlists. + Playlists { + #[command(subcommand)] + command: PlaylistsCommand, + }, + + /// search case-sensitively for songs matching matching a filter and add them to the queue + /// + /// This command extends the MPD command `findadd' (which will search the MPD database) to allow + /// searches on attributes managed by mpdpopm: rating, playcount & last played time. + /// + /// The MPD `findadd' <https://www.musicpd.org/doc/html/protocol.html#command-findadd> will search the + /// MPD database for songs that match a given filter & add them to the play queue. The filter syntax is + /// documented here <https://www.musicpd.org/doc/html/protocol.html#filter-syntax>. + /// + /// This command adds three new terms on which you can filter: rating, playcount & lastplayed. Each is + /// expressed as an unsigned integer, with zero interpreted as "not set". For instance: + /// + /// mppopm findadd "(rating > 128)" + /// + /// Will add all songs in the library with a rating sticker > 128 to the play queue. + /// + /// mppopm also introduces OR clauses (MPD only supports AND), so that: + /// + /// mppopm findadd "((rating > 128) AND (artist =~ \"pogues\"))" + /// + /// will add all songs whose artist tag matches the regexp "pogues" with a rating greater than + /// 128. + /// + /// `findadd' is case-sensitive; for case-insensitive searching see the `searchadd' command. + #[clap(verbatim_doc_comment)] + Findadd { filter: String }, + + /// search case-insensitively for songs matching matching a filter and add them to the queue + /// + /// This command extends the MPD command `searchadd' (which will search the MPD database) to allow + /// searches on attributes managed by mpdpopm: rating, playcount & last played time. + /// + /// The MPD `searchadd' <https://www.musicpd.org/doc/html/protocol.html#command-searchadd> will search + /// the MPD database for songs that match a given filter & add them to the play queue. The filter syntax + /// is documented here <https://www.musicpd.org/doc/html/protocol.html#filter-syntax>. + /// + /// This command adds three new terms on which you can filter: rating, playcount & lastplayed. Each is + /// expressed as an unsigned integer, with zero interpreted as "not set". For instance: + /// + /// mppopm searchadd "(rating > 128)" + /// + /// Will add all songs in the library with a rating sticker > 128 to the play queue. + /// + /// mppopm also introduces OR clauses (MPD only supports AND), so that: + /// + /// mppopm searchadd "((rating > 128) AND (artist =~ \"pogues\"))" + /// + /// will add all songs whose artist tag matches the regexp "pogues" with a rating greater than + /// 128. + /// + /// `searchadd' is case-insensitive; for case-sensitive searching see the `findadd' command. + #[clap(verbatim_doc_comment)] + Searchadd { filter: String }, + + /// Send a command to mpd. + #[clap(verbatim_doc_comment)] + SendCommand { args: Vec<String> }, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + let config = if let Some(configpath) = &args.config { + match std::fs::read_to_string(configpath) { + Ok(text) => config::from_str(&text).with_context(|| { + format!("Failed to parse config file at: `{}`", configpath.display()) + })?, + Err(err) => { + // Either they did _not_, in which case they probably want to know that the config + // file they explicitly asked for does not exist, or there was some other problem, + // in which case we're out of options, anyway. Either way: + bail!( + "Failed to read config file at: `{}`, because: {err}", + configpath.display() + ) + } + } + } else { + Config::default() + }; + + // Handle log verbosity: debug => verbose + let lf = match (args.verbose, args.debug) { + (_, true) => LevelFilter::TRACE, + (true, false) => LevelFilter::DEBUG, + _ => LevelFilter::WARN, + }; + + tracing::subscriber::set_global_default( + Registry::default() + .with( + tracing_subscriber::fmt::Layer::default() + .compact() + .with_writer(std::io::stdout), + ) + .with( + EnvFilter::builder() + .with_default_directive(lf.into()) + .from_env() + .unwrap(), + ), + ) + .unwrap(); + + trace!("logging configured."); + + let mut client = match config.conn { + config::Connection::Local { path } => Client::open(path).await?, + config::Connection::TCP { host, port } => { + Client::connect(format!("{}:{}", host, port)).await? + } + }; + + match args.command { + SubCommand::Rating { command } => match command { + RatingCommand::Get { with_uri, tracks } => { + get_ratings(&mut client, tracks, with_uri).await + } + RatingCommand::Set { rating, track } => set_rating(&mut client, rating, track).await, + RatingCommand::Inc { track } => inc_rating(&mut client, track).await, + RatingCommand::Decr { track } => decr_rating(&mut client, track).await, + }, + SubCommand::PlayCount { command } => match command { + PlayCountCommand::Get { with_uri, tracks } => { + get_play_counts(&mut client, tracks, with_uri).await + } + PlayCountCommand::Set { play_count, track } => { + set_play_counts(&mut client, play_count, track).await + } + }, + SubCommand::LastPlayed { command } => match command { + LastPlayedCommand::Get { with_uri, tracks } => { + get_last_playeds(&mut client, tracks, with_uri).await + } + LastPlayedCommand::Set { last_played, track } => { + set_last_playeds(&mut client, last_played, track).await + } + }, + SubCommand::Playlists { command } => match command { + PlaylistsCommand::Get {} => get_playlists(&mut client).await, + }, + SubCommand::Findadd { filter } => { + findadd(&mut client, &config.commands_chan, &filter, true).await + } + SubCommand::Searchadd { filter } => { + findadd(&mut client, &config.commands_chan, &filter, false).await + } + SubCommand::SendCommand { args } => { + send_command(&mut client, &config.commands_chan, args).await + } + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopmd.rs b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopmd.rs new file mode 100644 index 00000000..643611d6 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopmd.rs @@ -0,0 +1,150 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! # mppopmd +//! +//! Maintain ratings & playcounts for your mpd server. +//! +//! # Introduction +//! +//! This is a companion daemon for [mpd](https://www.musicpd.org/) that maintains play counts & +//! ratings. Similar to [mpdfav](https://github.com/vincent-petithory/mpdfav), but written in Rust +//! (which I prefer to Go), it will allow you to maintain that information in your tags, as well as +//! the sticker database, by invoking external commands to keep your tags up-to-date (something +//! along the lines of [mpdcron](https://alip.github.io/mpdcron)). + +use mpdpopm::{ + config::{self, Config}, + mpdpopm, +}; + +use anyhow::{Context, Result, bail}; +use clap::Parser; +use tracing::{info, level_filters::LevelFilter}; +use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; + +use std::{io, path::PathBuf, sync::MutexGuard}; + +pub struct MyMutexGuardWriter<'a>(MutexGuard<'a, std::fs::File>); + +impl io::Write for MyMutexGuardWriter<'_> { + #[inline] + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.0.write(buf) + } + + #[inline] + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } + + #[inline] + fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> { + self.0.write_vectored(bufs) + } + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + self.0.write_all(buf) + } + + #[inline] + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> { + self.0.write_fmt(fmt) + } +} + +/// mpd + POPM +/// +/// `mppopmd' is a companion daemon for `mpd' that maintains playcounts & ratings, +/// as well as implementing some handy functions. It maintains ratings & playcounts in the sticker +/// database, but it allows you to keep that information in your tags, as well, by invoking external +/// commands to keep your tags up-to-date. +#[derive(Parser)] +struct Args { + /// path to configuration file + #[arg(short, long)] + config: Option<PathBuf>, + + /// enable verbose logging + #[arg(short, long)] + verbose: bool, + + /// enable debug loggin (implies --verbose) + #[arg(short, long)] + debug: bool, +} + +/// Entry point for `mpdopmd'. +/// +/// Do *not* use the #[tokio::main] attribute here! If this program is asked to daemonize (the usual +/// case), we will fork after tokio has started its thread pool, with disastrous consequences. +/// Instead, stay synchronous until we've daemonized (or figured out that we don't need to), and +/// only then fire-up the tokio runtime. +fn main() -> Result<()> { + use mpdpopm::vars::VERSION; + + let args = Args::parse(); + + let config = if let Some(cfgpath) = &args.config { + match std::fs::read_to_string(cfgpath) { + Ok(text) => config::from_str(&text).with_context(|| { + format!("Failed to parse config file at: `{}`", cfgpath.display()) + })?, + // The config file (defaulted or not) either didn't exist, or we were unable to read its + // contents... + Err(err) => { + // Either they did _not_, in which case they probably want to know that the config + // file they explicitly asked for does not exist, or there was some other problem, + // in which case we're out of options, anyway. Either way: + bail!( + "No config file could be read at: `{}`, because: {err}", + cfgpath.display() + ) + } + } + } else { + Config::default() + }; + + // `--verbose' & `--debug' work as follows: if `--debug' is present, log at level Trace, no + // matter what. Else, if `--verbose' is present, log at level Debug. Else, log at level Info. + let lf = match (args.verbose, args.debug) { + (_, true) => LevelFilter::TRACE, + (true, false) => LevelFilter::DEBUG, + _ => LevelFilter::INFO, + }; + + let filter = EnvFilter::builder() + .with_default_directive(lf.into()) + .from_env() + .context("Failed to construct env filter")?; + + let formatter: Box<dyn Layer<Registry> + Send + Sync> = { + Box::new( + tracing_subscriber::fmt::Layer::default() + .compact() + .with_writer(io::stdout), + ) + }; + + tracing::subscriber::set_global_default(Registry::default().with(formatter).with(filter)) + .unwrap(); + + info!("mppopmd {VERSION} logging at level {lf:#?}."); + let rt = tokio::runtime::Runtime::new().unwrap(); + + rt.block_on(mpdpopm(config)).context("Main mpdpopm failed") +} diff --git a/pkgs/by-name/mp/mpdpopm/src/clients.rs b/pkgs/by-name/mp/mpdpopm/src/clients.rs new file mode 100644 index 00000000..b88e4041 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/clients.rs @@ -0,0 +1,1202 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! mpd clients and associated utilities. +//! +//! # Introduction +//! +//! This module contains basic types implementing various MPD client operations (cf. the [mpd +//! protocol](http://www.musicpd.org/doc/protocol/)). Since issuing the "idle" command will tie up +//! the connection, MPD clients often use multiple connections to the server (one to listen for +//! updates, one or more on which to issue commands). This modules provides two different client +//! types: [Client] for general-purpose use and [IdleClient] for long-lived connections listening +//! for server notifiations. +//! +//! Note that there *is* another idiom (used in [libmpdel](https://github.com/mpdel/libmpdel), +//! e.g.): open a single connection & issue an "idle" command. When you want to issue a command, +//! send a "noidle", then the command, then "idle" again. This isn't a race condition, as the +//! server will buffer any changes that took place when you were not idle & send them when you +//! re-issue the "idle" command. This crate however takes the approach of two channels (like +//! [mpdfav](https://github.com/vincent-petithory/mpdfav)). + +use anyhow::{Context, Error, Result, anyhow, bail, ensure}; +use async_trait::async_trait; +use regex::Regex; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::net::{TcpStream, ToSocketAddrs, UnixStream}; +use tracing::{debug, info}; + +use lazy_static::lazy_static; + +use std::{ + collections::HashMap, + convert::TryFrom, + fmt, + marker::{Send, Unpin}, + path::{Path, PathBuf}, + str::FromStr, +}; + +// Some default error context messages +const ENCODING_SNAFU: &str = "Failed to interpete text as utf8"; +const IO_SNAFU: &str = "Failed read from mpd socket"; + +/// A description of the current track, suitable for our purposes (as in, it only tracks the +/// attributes needed for this module's functionality). +#[derive(Clone, Debug)] +pub struct CurrentSong { + /// Identifier, unique within the play queue, identifying this particular track; if the same + /// file is listed twice in the `mpd' play queue each instance will get a distinct songid + pub songid: u64, + + /// Path, relative to `mpd' music directory root of this track + pub file: std::path::PathBuf, + + /// Elapsed time, in seconds, in this track + pub elapsed: f64, + + /// Total track duration, in seconds + pub duration: f64, +} + +impl CurrentSong { + fn new(songid: u64, file: std::path::PathBuf, elapsed: f64, duration: f64) -> CurrentSong { + CurrentSong { + songid, + file, + elapsed, + duration, + } + } + /// Compute the ratio of the track that has elapsed, expressed as a floating point between 0 & 1 + pub fn played_pct(&self) -> f64 { + self.elapsed / self.duration + } +} + +/// The MPD player itself can be in one of three states: playing, paused or stopped. In the first +/// two there is a "current" song. +#[derive(Clone, Debug)] +pub enum PlayerStatus { + Play(CurrentSong), + Pause(CurrentSong), + Stopped, +} + +impl PlayerStatus { + pub fn current_song(&self) -> Option<&CurrentSong> { + match self { + PlayerStatus::Play(curr) | PlayerStatus::Pause(curr) => Some(curr), + PlayerStatus::Stopped => None, + } + } +} + +/// A trait representing a simple, textual request/response protocol like that +/// [employed](https://www.musicpd.org/doc/html/protocol.html) by [MPD](https://www.musicpd.org/): +/// the caller sends a textual command & the server responds with a (perhaps multi-line) textual +/// response. +/// +/// This trait also enables unit testing client implementations. Note that it is async-- cf. +/// [async_trait](https://docs.rs/async-trait/latest/async_trait/). +#[async_trait] +pub trait RequestResponse { + async fn req(&mut self, msg: &str) -> Result<String>; + /// The hint is used to size the buffer prior to reading the response + async fn req_w_hint(&mut self, msg: &str, hint: usize) -> Result<String>; +} + +#[cfg(test)] +pub mod test_mock { + use super::*; + + /// Mock is an implementation of [`RequestRespone`] that checks expected requests & responses, + /// and will panic if it sees anything unexpected + pub struct Mock { + inmsgs: Vec<String>, + outmsgs: Vec<String>, + } + + impl Mock { + pub fn new(convo: &[(&str, &str)]) -> Mock { + let (left, right): (Vec<&str>, Vec<&str>) = convo.iter().copied().rev().unzip(); + Mock { + inmsgs: left.iter().map(|x| x.to_string()).collect(), + outmsgs: right.iter().map(|x| x.to_string()).collect(), + } + } + } + + #[async_trait] + impl RequestResponse for Mock { + async fn req(&mut self, msg: &str) -> Result<String> { + self.req_w_hint(msg, 512).await + } + async fn req_w_hint(&mut self, msg: &str, _hint: usize) -> Result<String> { + assert_eq!(msg, self.inmsgs.pop().unwrap()); + Ok(self.outmsgs.pop().unwrap()) + } + } + + #[tokio::test] + async fn mock_smoke_test() { + let mut mock = Mock::new(&[("ping", "pong"), ("from", "to")]); + assert_eq!(mock.req("ping").await.unwrap(), "pong"); + assert_eq!(mock.req("from").await.unwrap(), "to"); + } + + #[tokio::test] + #[should_panic] + async fn mock_negative_test() { + let mut mock = Mock::new(&[("ping", "pong")]); + assert_eq!(mock.req("ping").await.unwrap(), "pong"); + let _should_panic = mock.req("not there!").await.unwrap(); + } +} + +/// [MPD](https://www.musicpd.org/) connections talk the same +/// [protocol](https://www.musicpd.org/doc/html/protocol.html) over either a TCP or a Unix socket. +/// +/// # Examples +/// +/// Implementations are provided for tokio [UnixStream] and [TcpStream], but [MpdConnection] is a +/// trait that can work in terms of any asynchronous communications channel (so long as it is also +/// [Send] and [Unpin] so async executors can pass them between threads. +/// +/// To create a connection to an `MPD` server over a Unix domain socket: +/// +/// ```no_run +/// use std::path::Path; +/// use tokio::net::UnixStream; +/// use mpdpopm::clients::MpdConnection; +/// let local_conn = MpdConnection::<UnixStream>::connect(Path::new("/var/run/mpd/mpd.sock")); +/// ``` +/// +/// In this example, `local_conn` is a Future that will resolve to a Result containing the +/// [MpdConnection] Unix domain socket implementation once the socket has been established, the MPD +/// server greets us & the protocol version has been parsed. +/// +/// or over a TCP socket: +/// +/// ```no_run +/// use std::net::SocketAddrV4; +/// use tokio::net::{TcpStream, ToSocketAddrs}; +/// use mpdpopm::clients::MpdConnection; +/// let tcp_conn = MpdConnection::<TcpStream>::connect("localhost:6600".parse::<SocketAddrV4>().unwrap()); +/// ``` +/// +/// Here, `tcp_conn` is a Future that will resolve to a Result containing the [MpdConnection] TCP +/// implementation on successful connection to the MPD server (i.e. the connection is established, +/// the server greets us & we parse the protocol version). +/// +/// +pub struct MpdConnection<T: AsyncRead + AsyncWrite + Send + Unpin> { + sock: T, + _protocol_ver: String, +} + +/// MpdConnection implements RequestResponse using the usual (async) socket I/O +/// +/// The callers need not include the trailing newline in their requests; the implementation will +/// append it. +#[async_trait] +impl<T> RequestResponse for MpdConnection<T> +where + T: AsyncRead + AsyncWrite + Send + Unpin, +{ + async fn req(&mut self, msg: &str) -> Result<String> { + self.req_w_hint(msg, 512).await + } + async fn req_w_hint(&mut self, msg: &str, hint: usize) -> Result<String> { + self.sock + .write_all(format!("{}\n", msg).as_bytes()) + .await + .context(IO_SNAFU)?; + let mut buf = Vec::with_capacity(hint); + + // Given the request/response nature of the MPD protocol, our callers expect a complete + // response. Therefore we need to loop here until we see either "...^OK\n" or + // "...^ACK...\n". + let mut cb = 0; // # bytes read so far + let mut more = true; // true as long as there is more to read + while more { + cb += self.sock.read_buf(&mut buf).await.context(IO_SNAFU)?; + + // The shortest complete response has three bytes. If the final byte in `buf' is not a + // newline, then don't bother looking further. + if cb > 2 && char::from(buf[cb - 1]) == '\n' { + // If we're here, `buf' *may* contain a complete response. Search backward for the + // previous newline. It may not exist: many responses are of the form "OK\n". + let mut idx = cb - 2; + while idx > 0 { + if char::from(buf[idx]) == '\n' { + idx += 1; + break; + } + idx -= 1; + } + + if (idx + 2 < cb && char::from(buf[idx]) == 'O' && char::from(buf[idx + 1]) == 'K') + || (idx + 3 < cb + && char::from(buf[idx]) == 'A' + && char::from(buf[idx + 1]) == 'C' + && char::from(buf[idx + 2]) == 'K') + { + more = false; + } + } + } + + // Only doing this to trouble-shoot issue 11 + String::from_utf8(buf.clone()).context(ENCODING_SNAFU) + } +} + +/// Utility function to parse the initial response to a connection from mpd +async fn parse_connect_rsp<T>(sock: &mut T) -> Result<String> +where + T: AsyncReadExt + AsyncWriteExt + Send + Unpin, +{ + let mut buf = Vec::with_capacity(32); + let _cb = sock.read_buf(&mut buf).await.context(IO_SNAFU)?; + + // Only doing this to trouble-shoot issue 11 + let text = String::from_utf8(buf.clone()).context(ENCODING_SNAFU)?; + + ensure!( + text.starts_with("OK MPD "), + "failed to connect: {}", + text.trim() + ); + info!("Connected {}.", text[7..].trim()); + Ok(text[7..].trim().to_string()) +} + +impl MpdConnection<TcpStream> { + pub async fn connect<A: ToSocketAddrs>(addr: A) -> Result<Box<dyn RequestResponse>> { + let mut sock = TcpStream::connect(addr).await.context(IO_SNAFU)?; + let proto_ver = parse_connect_rsp(&mut sock).await?; + Ok(Box::new(MpdConnection::<TcpStream> { + sock, + _protocol_ver: proto_ver, + })) + } +} + +impl MpdConnection<UnixStream> { + // NTS: we have to box the return value because a `dyn RequestResponse` isn't Sized. + pub async fn connect<P: AsRef<Path>>(pth: P) -> Result<Box<dyn RequestResponse>> { + let mut sock = UnixStream::connect(pth).await.context(IO_SNAFU)?; + let proto_ver = parse_connect_rsp(&mut sock).await?; + Ok(Box::new(MpdConnection::<UnixStream> { + sock, + _protocol_ver: proto_ver, + })) + } +} + +/// Quote an argument by backslash-escaping " & \ characters +pub fn quote(text: &str) -> String { + if text.contains(&[' ', '\t', '\'', '"'][..]) { + let mut s = String::from("\""); + for c in text.chars() { + if c == '"' || c == '\\' { + s.push('\\'); + } + s.push(c); + } + s.push('"'); + s + } else { + text.to_string() + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Client // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/// General-purpose [mpd](https://www.musicpd.org) +/// [client](https://www.musicpd.org/doc/html/protocol.html): "general-purpose" in the sense that we +/// send commands through it; the interface is narrowly scoped to this program's needs. +/// +/// # Introduction +/// +/// This is the primary abstraction of the MPD client protocol, written for the convenience of +/// [mpdpopm](crate). Construct instances with a TCP socket, a Unix socket, or any [RequestResponse] +/// implementation. You can then carry out assorted operations in the MPD client protocol by +/// invoking its methods. +/// +/// ```no_run +/// use std::path::Path; +/// use mpdpopm::clients::Client; +/// let client = Client::open(Path::new("/var/run/mpd.sock")); +/// ``` +/// +/// `client` is now a [Future](https://doc.rust-lang.org/stable/std/future/trait.Future.html) that +/// resolves to a [Client] instance talking to `/var/run/mpd.sock`. +/// +/// ```no_run +/// use mpdpopm::clients::Client; +/// let client = Client::connect("localhost:6600"); +/// ``` +/// +/// `client` is now a [Future](https://doc.rust-lang.org/stable/std/future/trait.Future.html) that +/// resolves to a [Client] instance talking TCP to the MPD server on localhost at port 6600. +pub struct Client { + stream: Box<dyn RequestResponse>, +} + +// Thanks to <https://stackoverflow.com/questions/35169259/how-to-make-a-compiled-regexp-a-global-variable> +lazy_static! { + static ref RE_STATE: regex::Regex = Regex::new(r"(?m)^state: (play|pause|stop)$").unwrap(); + static ref RE_SONGID: regex::Regex = Regex::new(r"(?m)^songid: ([0-9]+)$").unwrap(); + static ref RE_ELAPSED: regex::Regex = Regex::new(r"(?m)^elapsed: ([.0-9]+)$").unwrap(); + static ref RE_FILE: regex::Regex = Regex::new(r"(?m)^file: (.*)$").unwrap(); + static ref RE_DURATION: regex::Regex = Regex::new(r"(?m)^duration: (.*)$").unwrap(); +} + +impl Client { + pub async fn connect<A: ToSocketAddrs>(addrs: A) -> Result<Client> { + Self::new(MpdConnection::<TcpStream>::connect(addrs).await?) + } + + pub async fn open<P: AsRef<Path>>(pth: P) -> Result<Client> { + Self::new(MpdConnection::<UnixStream>::connect(pth).await?) + } + + pub fn new(stream: Box<dyn RequestResponse>) -> Result<Client> { + Ok(Client { stream }) + } +} + +impl Client { + /// Retrieve the current server status. + pub async fn status(&mut self) -> Result<PlayerStatus> { + // We begin with sending the "status" command: "Reports the current status of the player and + // the volume level." Per the docs, "MPD may omit lines which have no (known) value", so I + // can't really count on particular lines being there. Tho nothing is said in the docs, I + // also don't want to depend on the order. + let text = self.stream.req("status").await?; + + let proto = || -> Error { anyhow!("Failed to parse mpd status output (with regexes)") }; + + // I first thought to avoid the use (and cost) of regular expressions by just doing + // sub-string searching on "state: ", but when I realized I needed to only match at the + // beginning of a line I bailed & just went ahead. This makes for more succinct code, since + // I can't count on order, either. + let state = RE_STATE + .captures(&text) + .ok_or_else(proto)? + .get(1) + .ok_or_else(proto)? + .as_str(); + + match state { + "stop" => Ok(PlayerStatus::Stopped), + "play" | "pause" => { + let songid = RE_SONGID + .captures(&text) + .ok_or_else(proto)? + .get(1) + .ok_or_else(proto)? + .as_str() + .parse::<u64>() + .context("Failed to parse songid as u64")?; + + let elapsed = RE_ELAPSED + .captures(&text) + .ok_or_else(proto)? + .get(1) + .ok_or_else(proto)? + .as_str() + .parse::<f64>() + .context("failed to parse `elapsed` as f64")?; + + // navigate from `songid'-- don't send a "currentsong" message-- the current song + // could have changed + let text = self.stream.req(&format!("playlistid {}", songid)).await?; + + let file = RE_FILE + .captures(&text) + .ok_or_else(proto)? + .get(1) + .ok_or_else(proto)? + .as_str(); + let duration = RE_DURATION + .captures(&text) + .ok_or_else(proto)? + .get(1) + .ok_or_else(proto)? + .as_str() + .parse::<f64>() + .context("Failed to parse `duration` as f64")?; + + let curr = CurrentSong::new(songid, PathBuf::from(file), elapsed, duration); + + if state == "play" { + Ok(PlayerStatus::Play(curr)) + } else { + Ok(PlayerStatus::Pause(curr)) + } + } + _ => bail!("Encountered unknow state `{}`", state), + } + } + + /// Retrieve a song sticker by name + pub async fn get_sticker<T: FromStr>( + &mut self, + file: &str, + sticker_name: &str, + ) -> Result<Option<T>> + where + <T as FromStr>::Err: std::error::Error + Sync + Send + 'static, + { + let msg = format!("sticker get song {} {}", quote(file), quote(sticker_name)); + let text = self.stream.req(&msg).await?; + debug!("Sent message `{}'; got `{}'", &msg, &text); + + let prefix = format!("sticker: {}=", sticker_name); + if text.starts_with(&prefix) { + let s = text[prefix.len()..] + .split('\n') + .next() + .with_context(|| format!("Failed to parse `{}` as get_sticker response", text))?; + Ok(Some(T::from_str(s).with_context(|| { + format!( + "Failed to parse sticker value as correct type: `{}`", + sticker_name + ) + })?)) + } else { + // ACK_ERROR_NO_EXIST = 50 (Ack.hxx:17) + ensure!( + text.starts_with("ACK [50@0]"), + "Missing no sticker response" + ); + Ok(None) + } + } + + /// Set a song sticker by name + pub async fn set_sticker<T: std::fmt::Display>( + &mut self, + file: &str, + sticker_name: &str, + sticker_value: &T, + ) -> Result<()> { + let value_as_str = format!("{}", sticker_value); + let msg = format!( + "sticker set song {} {} {}", + quote(file), + quote(sticker_name), + quote(&value_as_str) + ); + let text = self.stream.req(&msg).await?; + debug!("Sent `{}'; got `{}'", &msg, &text); + + ensure!(text.starts_with("OK"), "Set sticker, not acknowledged"); + Ok(()) + } + + /// Send a file to a playlist + pub async fn send_to_playlist(&mut self, file: &str, pl: &str) -> Result<()> { + let msg = format!("playlistadd {} {}", quote(pl), quote(file)); + let text = self.stream.req(&msg).await?; + debug!("Sent `{}'; got `{}'.", &msg, &text); + ensure!(text.starts_with("OK"), "send_to_playlist not acknowledged"); + Ok(()) + } + + /// Send an arbitrary message + pub async fn send_message(&mut self, chan: &str, msg: &str) -> Result<()> { + let msg = format!("sendmessage {} {}", chan, quote(msg)); + let text = self.stream.req(&msg).await?; + debug!("Sent `{}'; got `{}'.", &msg, &text); + + ensure!(text.starts_with("OK"), "Send_message not acknowledged"); + Ok(()) + } + + /// Update a URI + pub async fn update(&mut self, uri: &str) -> Result<u64> { + let msg = format!("update \"{}\"", uri); + let text = self.stream.req(&msg).await?; + debug!("Sent `{}'; got `{}'.", &msg, &text); + + // We expect a response of the form: + // updating_db: JOBID + // OK + // on success, and + // ACK ERR + // on failure. + + let prefix = "updating_db: "; + ensure!( + text.starts_with(prefix), + "update response doesn't start with correct prefix" + ); + text[prefix.len()..].split('\n').collect::<Vec<&str>>()[0] + .to_string() + .parse::<u64>() + .context("Failed to treat update job id as u64") + } + + /// Get the list of stored playlists + pub async fn get_stored_playlists(&mut self) -> Result<std::vec::Vec<String>> { + let text = self.stream.req("listplaylists").await?; + debug!("Sent listplaylists; got `{}'.", &text); + + // We expect a response of the form: + // playlist: a + // Last-Modified: 2020-03-13T17:20:16Z + // playlsit: b + // Last-Modified: 2020-03-13T17:20:16Z + // ... + // OK + // + // or + // + // ACK... + ensure!( + !text.starts_with("ACK"), + "get_stored_playlists response not acknowledged" + ); + Ok(text + .lines() + .filter_map(|x| x.strip_prefix("playlist: ").map(String::from)) + .collect::<Vec<String>>()) + } + + /// Process a search (either find or search) response + fn search_rsp_to_uris(&self, text: &str) -> Result<std::vec::Vec<String>> { + // We expect a response of the form: + // file: P/Pogues, The - A Pistol For Paddy Garcia.mp3 + // Last-Modified: 2007-12-26T19:18:00Z + // Format: 44100:24:2 + // ... + // file: P/Pogues, The - Billy's Bones.mp3 + // ... + // OK + // + // or + // + // ACK... + ensure!(!text.starts_with("ACK"), "rsp_to_uris not acknowledged"); + Ok(text + .lines() + .filter_map(|x| x.strip_prefix("file: ").map(String::from)) + .collect::<Vec<String>>()) + } + + /// Search the database for songs matching filter (unary operator) + /// + /// Set `case` to true to request a case-sensitive search (false yields case-insensitive) + pub async fn find1( + &mut self, + cond: &str, + val: &str, + case: bool, + ) -> Result<std::vec::Vec<String>> { + let cmd = format!( + "{} {}", + if case { "find" } else { "search" }, + quote(&format!("({} {})", cond, val)) + ); + let text = self.stream.req(&cmd).await?; + self.search_rsp_to_uris(&text) + } + + /// Search the database for songs matching filter (case-sensitive, binary operator) + /// + /// Set `case` to true to request a case-sensitive search (false yields case-insensitive) + pub async fn find2( + &mut self, + attr: &str, + op: &str, + val: &str, + case: bool, + ) -> Result<std::vec::Vec<String>> { + let cmd = format!( + "{} {}", + if case { "find" } else { "search" }, + quote(&format!("({} {} {})", attr, op, val)) + ); + debug!("find2 sending ``{}''", cmd); + let text = self.stream.req(&cmd).await?; + self.search_rsp_to_uris(&text) + } + + /// Retrieve all instances of a given sticker under the music directory + /// + /// Return a mapping from song URI to textual sticker value + pub async fn get_stickers(&mut self, sticker: &str) -> Result<HashMap<String, String>> { + let text = self + .stream + .req(&format!("sticker find song \"\" {}", sticker)) + .await?; + + // We expect a response of the form: + // + // file: U-Z/Zafari - Addis Adaba.mp3 + // sticker: unwoundstack.com:rating=64 + // ... + // file: U-Z/Zero 7 - In Time (Album Version).mp3 + // sticker: unwoundstack.com:rating=255 + // OK + // + // or + // + // ACK ... + ensure!(!text.starts_with("ACK"), "get_stickers not ACKed"); + let mut m = HashMap::new(); + let mut lines = text.lines(); + loop { + let file = lines.next().context("get_stickers no new line")?; + if "OK" == file { + break; + } + let val = lines.next().context("get_stickers no val")?; + + m.insert( + String::from(&file[6..]), + String::from(&val[10 + sticker.len()..]), + ); + } + Ok(m) + } + + /// Retrieve the song URIs of all songs in the database + /// + /// Returns a vector of String + pub async fn get_all_songs(&mut self) -> Result<std::vec::Vec<String>> { + let text = self.stream.req("find \"(base '')\"").await?; + // We expect a response of the form: + // file: 0-A/A Positive Life - Lighten Up!.mp3 + // Last-Modified: 2020-11-18T22:47:07Z + // Format: 44100:24:2 + // Time: 399 + // duration: 398.550 + // Artist: A Positive Life + // Title: Lighten Up! + // Genre: Electronic + // file: 0-A/A Positive Life - Pleidean Communication.mp3 + // ... + // OK + // + // or "ACK..." + ensure!(!text.starts_with("ACK"), "get_all_songs not ACKed"); + Ok(text + .lines() + .filter_map(|x| x.strip_prefix("file: ").map(String::from)) + .collect::<Vec<String>>()) + } + + pub async fn add(&mut self, uri: &str) -> Result<()> { + let msg = format!("add {}", quote(uri)); + let text = self.stream.req(&msg).await?; + debug!("Sent `{}'; got `{}'.", &msg, &text); + + ensure!(text.starts_with("OK"), "add not Oked"); + Ok(()) + } +} + +#[cfg(test)] +/// Let's test Client! +mod client_tests { + + use super::test_mock::Mock; + use super::*; + + /// Some basic "smoke" tests + #[tokio::test] + async fn client_smoke_test() { + let mock = Box::new(Mock::new(&[( + "sticker get song foo.mp3 stick", + "sticker: stick=splat\nOK\n", + )])); + let mut cli = Client::new(mock).unwrap(); + let val = cli + .get_sticker::<String>("foo.mp3", "stick") + .await + .unwrap() + .unwrap(); + assert_eq!(val, "splat"); + } + + /// Test the `status' method + #[tokio::test] + async fn test_status() { + let mock = Box::new(Mock::new(&[ + ( + "status", + // When the server is playing or paused, the response will look something like this: + "volume: -1 +repeat: 0 +random: 0 +single: 0 +consume: 0 +playlist: 3 +playlistlength: 87 +mixrampdb: 0.000000 +state: play +song: 14 +songid: 15 +time: 141:250 +bitrate: 128 +audio: 44100:24:2 +nextsong: 15 +nextsongid: 16 +elapsed: 140.585 +OK", + ), + // Should respond with a playlist id request + ( + "playlistid 15", + // Should look something like this: + "file: U-Z/U2 - Who's Gonna RIDE Your WILD HORSES.mp3 +Last-Modified: 2004-12-24T19:26:13Z +Artist: U2 +Title: Who's Gonna RIDE Your WILD HOR +Genre: Pop +Time: 316 +Pos: 41 +Id: 42 +duration: 249.994 +OK", + ), + ( + "status", + // But if the state is "stop", much of that will be missing; it will look more like: + "volume: -1 +repeat: 0 +random: 0 +single: 0 +consume: 0 +playlist: 84 +playlistlength: 27 +mixrampdb: 0.000000 +state: stop +OK", + ), + // Finally, let's simulate something being really wrong + ( + "status", + "volume: -1 +repeat: 0 +state: no-idea!?", + ), + ])); + let mut cli = Client::new(mock).unwrap(); + let stat = cli.status().await.unwrap(); + match stat { + PlayerStatus::Play(curr) => { + assert_eq!(curr.songid, 15); + assert_eq!( + curr.file.to_str().unwrap(), + "U-Z/U2 - Who's Gonna RIDE Your WILD HORSES.mp3" + ); + assert_eq!(curr.elapsed, 140.585); + assert_eq!(curr.duration, 249.994); + } + _ => panic!(), + } + + let stat = cli.status().await.unwrap(); + match stat { + PlayerStatus::Stopped => (), + _ => panic!(), + } + + let stat = cli.status().await; + match stat { + Err(_) => (), + Ok(_) => panic!(), + } + } + + /// Test the `get_sticker' method + #[tokio::test] + async fn test_get_sticker() { + let mock = Box::new(Mock::new(&[ + ( + "sticker get song foo.mp3 stick", + // On success, should get something like this... + "sticker: stick=2\nOK\n", + ), + ( + "sticker get song foo.mp3 stick", + // and on failure, something like this: + "ACK [50@0] {sticker} no such sticker\n", + ), + ( + "sticker get song foo.mp3 stick", + // Finally, let's try something nuts + "", + ), + ( + "sticker get song \"filename_with\\\"doublequotes\\\".flac\" unwoundstack.com:playcount", + "sticker: unwoundstack.com:playcount=11\nOK\n", + ), + ])); + let mut cli = Client::new(mock).unwrap(); + let val = cli + .get_sticker::<String>("foo.mp3", "stick") + .await + .unwrap() + .unwrap(); + assert_eq!(val, "2"); + let _val = cli + .get_sticker::<String>("foo.mp3", "stick") + .await + .unwrap() + .is_none(); + let _val = cli + .get_sticker::<String>("foo.mp3", "stick") + .await + .unwrap_err(); + let val = cli + .get_sticker::<String>( + "filename_with\"doublequotes\".flac", + "unwoundstack.com:playcount", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(val, "11"); + } + + /// Test the `set_sticker' method + #[tokio::test] + async fn test_set_sticker() { + let mock = Box::new(Mock::new(&[ + ("sticker set song foo.mp3 stick 2", "OK\n"), + ( + "sticker set song foo.mp3 stick 2", + "ACK [50@0] {sticker} some error", + ), + ( + "sticker set song foo.mp3 stick 2", + "this makes no sense as a response", + ), + ])); + let mut cli = Client::new(mock).unwrap(); + let () = cli.set_sticker("foo.mp3", "stick", &"2").await.unwrap(); + let _val = cli.set_sticker("foo.mp3", "stick", &"2").await.unwrap_err(); + let _val = cli.set_sticker("foo.mp3", "stick", &"2").await.unwrap_err(); + } + + /// Test the `send_to_playlist' method + #[tokio::test] + async fn test_send_to_playlist() { + let mock = Box::new(Mock::new(&[ + ("playlistadd foo.m3u foo.mp3", "OK\n"), + ( + "playlistadd foo.m3u foo.mp3", + "ACK [101@0] {playlist} some error\n", + ), + ])); + let mut cli = Client::new(mock).unwrap(); + let () = cli.send_to_playlist("foo.mp3", "foo.m3u").await.unwrap(); + let _val = cli + .send_to_playlist("foo.mp3", "foo.m3u") + .await + .unwrap_err(); + } + + /// Test the `update' method + #[tokio::test] + async fn test_update() { + let mock = Box::new(Mock::new(&[ + ("update \"foo.mp3\"", "updating_db: 2\nOK\n"), + ("update \"foo.mp3\"", "ACK [50@0] {update} blahblahblah"), + ("update \"foo.mp3\"", "this makes no sense as a response"), + ])); + let mut cli = Client::new(mock).unwrap(); + let _val = cli.update("foo.mp3").await.unwrap(); + let _val = cli.update("foo.mp3").await.unwrap_err(); + let _val = cli.update("foo.mp3").await.unwrap_err(); + } + + /// Test retrieving stored playlists + #[tokio::test] + async fn test_get_stored_playlists() { + let mock = Box::new(Mock::new(&[ + ( + "listplaylists", + "playlist: saturday-afternoons-in-santa-cruz +Last-Modified: 2020-03-13T17:20:16Z +playlist: gaelic-punk +Last-Modified: 2020-05-24T00:36:02Z +playlist: morning-coffee +Last-Modified: 2020-03-13T17:20:16Z +OK +", + ), + ("listplaylists", "ACK [1@0] {listplaylists} blahblahblah"), + ])); + + let mut cli = Client::new(mock).unwrap(); + let val = cli.get_stored_playlists().await.unwrap(); + assert_eq!( + val, + vec![ + String::from("saturday-afternoons-in-santa-cruz"), + String::from("gaelic-punk"), + String::from("morning-coffee") + ] + ); + let _val = cli.get_stored_playlists().await.unwrap_err(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// IdleClient // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#[non_exhaustive] +#[derive(Debug, PartialEq, Eq)] +pub enum IdleSubSystem { + Player, + Message, +} + +impl TryFrom<&str> for IdleSubSystem { + type Error = Error; + fn try_from(text: &str) -> std::result::Result<Self, Self::Error> { + let x = text.to_lowercase(); + if x == "player" { + Ok(IdleSubSystem::Player) + } else if x == "message" { + Ok(IdleSubSystem::Message) + } else { + bail!("{}", text) + } + } +} + +impl fmt::Display for IdleSubSystem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + IdleSubSystem::Player => write!(f, "Player"), + IdleSubSystem::Message => write!(f, "Message"), + } + } +} + +/// [MPD](https://www.musicpd.org) client for "idle" connections. +/// +/// # Introduction +/// +/// This is an MPD client designed to "idle": it opens a long-lived connection to the MPD server and +/// waits for MPD to respond with a message indicating that there's been a change to a subsystem of +/// interest. At present, there are only two subsystems in which [mpdpopm](crate) is interested: the player +/// & messages (cf. [IdleSubSystem]). +/// +/// ```no_run +/// use std::path::Path; +/// use tokio::runtime::Runtime; +/// use mpdpopm::clients::IdleClient; +/// +/// let mut rt = Runtime::new().unwrap(); +/// rt.block_on( async { +/// let mut client = IdleClient::open(Path::new("/var/run/mpd.sock")).await.unwrap(); +/// client.subscribe("player").await.unwrap(); +/// client.idle().await.unwrap(); +/// // Arrives here when the player's state changes +/// }) +/// ``` +/// +/// `client` is now a [Future](https://doc.rust-lang.org/stable/std/future/trait.Future.html) that +/// resolves to an [IdleClient] instance talking to `/var/run/mpd.sock`. +/// +pub struct IdleClient { + conn: Box<dyn RequestResponse>, +} + +impl IdleClient { + /// Create a new [mpdpopm::client::IdleClient][IdleClient] instance from something that + /// implements [ToSocketAddrs] + pub async fn connect<A: ToSocketAddrs>(addr: A) -> Result<IdleClient> { + Self::new(MpdConnection::<TcpStream>::connect(addr).await?) + } + + pub async fn open<P: AsRef<Path>>(pth: P) -> Result<IdleClient> { + Self::new(MpdConnection::<UnixStream>::connect(pth).await?) + } + + pub fn new(stream: Box<dyn RequestResponse>) -> Result<IdleClient> { + Ok(IdleClient { conn: stream }) + } + + /// Subscribe to an mpd channel + pub async fn subscribe(&mut self, chan: &str) -> Result<()> { + let text = self.conn.req(&format!("subscribe {}", chan)).await?; + debug!("Sent subscribe message for {}; got `{}'.", chan, text); + ensure!(text.starts_with("OK"), "subscribe not Ok: `{}`", text); + debug!("Subscribed to {}.", chan); + Ok(()) + } + + /// Enter idle state-- return the subsystem that changed, causing the connection to return. NB + /// this may block for some time. + pub async fn idle(&mut self) -> Result<IdleSubSystem> { + let text = self.conn.req("idle player message").await?; + debug!("Sent idle message; got `{}'.", text); + + // If the player state changes, we'll get: "changed: player\nOK\n" + // + // If a ratings message is sent, we'll get: "changed: message\nOK\n", to which we respond + // "readmessages", which should give us something like: + // + // channel: ratings + // message: 255 + // OK + // + // We remain subscribed, but we need to send a new idle message. + + ensure!(text.starts_with("changed: "), "idle not OK: `{}`", text); + let idx = text.find('\n').context("idle has no newline")?; + + let result = IdleSubSystem::try_from(&text[9..idx])?; + let text = text[idx + 1..].to_string(); + ensure!(text.starts_with("OK"), "idle not OKed"); + + Ok(result) + } + + /// This method simply returns the results of a "readmessages" as a HashMap of channel name to + /// Vec of (String) messages for that channel + pub async fn get_messages(&mut self) -> Result<HashMap<String, Vec<String>>> { + let text = self.conn.req("readmessages").await?; + debug!("Sent readmessages; got `{}'.", text); + + // We expect something like: + // + // channel: ratings + // message: 255 + // OK + // + // We remain subscribed, but we need to send a new idle message. + + let mut m: HashMap<String, Vec<String>> = HashMap::new(); + + // Populate `m' with a little state machine: + enum State { + Init, + Running, + Finished, + } + let mut state = State::Init; + let mut chan = String::new(); + let mut msgs: Vec<String> = Vec::new(); + for line in text.lines() { + match state { + State::Init => { + ensure!(line.starts_with("channel: "), "no `channel: ` given"); + chan = String::from(&line[9..]); + state = State::Running; + } + State::Running => { + if let Some(stripped) = line.strip_prefix("message: ") { + msgs.push(String::from(stripped)); + } else if let Some(stripped) = line.strip_prefix("channel: ") { + match m.get_mut(&chan) { + Some(v) => v.append(&mut msgs), + None => { + m.insert(chan.clone(), msgs.clone()); + } + } + chan = String::from(stripped); + msgs = Vec::new(); + } else if line == "OK" { + match m.get_mut(&chan) { + Some(v) => v.append(&mut msgs), + None => { + m.insert(chan.clone(), msgs.clone()); + } + } + state = State::Finished; + } else { + bail!("Failed to get messages: `{}`", text) + } + } + State::Finished => { + // Should never be here! + bail!("Failed to get messages: `{}`", text) + } + } + } + + Ok(m) + } +} + +#[cfg(test)] +/// Let's test IdleClient! +mod idle_client_tests { + + use super::test_mock::Mock; + use super::*; + + /// Some basic "smoke" tests + #[tokio::test] + async fn test_get_messages() { + let mock = Box::new(Mock::new(&[( + "readmessages", + // If a ratings message is sent, we'll get: "changed: message\nOK\n", to which we + // respond "readmessages", which should give us something like: + // + // channel: ratings + // message: 255 + // OK + // + // We remain subscribed, but we need to send a new idle message. + "channel: ratings +message: 255 +message: 128 +channel: send-to-playlist +message: foo.m3u +OK +", + )])); + let mut cli = IdleClient::new(mock).unwrap(); + let hm = cli.get_messages().await.unwrap(); + let val = hm.get("ratings").unwrap(); + assert_eq!(val.len(), 2); + let val = hm.get("send-to-playlist").unwrap(); + assert!(val.len() == 1); + } + + /// Test issue #1 + #[tokio::test] + async fn test_issue_1() { + let mock = Box::new(Mock::new(&[( + "readmessages", + "channel: playcounts +message: a +channel: playcounts +message: b +OK +", + )])); + let mut cli = IdleClient::new(mock).unwrap(); + let hm = cli.get_messages().await.unwrap(); + let val = hm.get("playcounts").unwrap(); + assert_eq!(val.len(), 2); + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/config.rs b/pkgs/by-name/mp/mpdpopm/src/config.rs new file mode 100644 index 00000000..2d9c466b --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/config.rs @@ -0,0 +1,277 @@ +// Copyright (C) 2021-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! # mpdpopm Configuration +//! +//! ## Introduction +//! +//! This module defines the configuration struct & handles deserialization thereof. +//! +//! ## Discussion +//! +//! In the first releases of [mpdpopm](crate) I foolishly forgot to add a version field to the +//! configuration structure. I am now paying for my sin by having to attempt serializing two +//! versions until one succeeds. +//! +//! The idiomatic approach to versioning [serde](https://docs.serde.rs/serde/) structs seems to be +//! using an +//! [enumeration](https://www.reddit.com/r/rust/comments/44dds3/handling_multiple_file_versions_with_serde_or/). This +//! implementation *now* uses that, but that leaves us with the problem of handling the initial, +//! un-tagged version. I proceed as follows: +//! +//! 1. attempt to deserialize as a member of the modern enumeration +//! 2. if that succeeds, with the most-recent version, we're good +//! 3. if that succeeds with an archaic version, convert to the most recent and warn the user +//! 4. if that fails, attempt to deserialize as the initial struct version +//! 5. if that succeeds, convert to the most recent & warn the user +//! 6. if that fails, I'm kind of stuck because I don't know what the user was trying to express; +//! bundle-up all the errors, report 'em & urge the user to use the most recent version +use crate::vars::{LOCALSTATEDIR, PREFIX}; + +use anyhow::{Result, bail}; +use serde::{Deserialize, Serialize}; + +use std::{env, path::PathBuf}; + +/// [mpdpopm](crate) can communicate with MPD over either a local Unix socket, or over regular TCP +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub enum Connection { + /// Local Unix socket-- payload is the path to the socket + Local { path: PathBuf }, + /// TCP-- payload is the hostname & port number + TCP { host: String, port: u16 }, +} + +impl Connection { + pub fn new() -> Result<Self> { + let env = env::var("MPD_HOST")?; + + if env.starts_with("/") { + // We assume that this is a path to a local socket + Ok(Self::Local { + path: PathBuf::from(env), + }) + } else { + todo!("Not yet able to auto-parse, MPD_HOST for remote connection") + } + } +} + +impl Default for Connection { + fn default() -> Self { + Self::new().expect("Could not generate default connection") + } +} + +#[cfg(test)] +mod test_connection { + use super::Connection; + + #[test] + fn test_serde() { + use serde_json::to_string; + + use std::path::PathBuf; + + let text = to_string(&Connection::Local { + path: PathBuf::from("/var/run/mpd.sock"), + }) + .unwrap(); + + assert_eq!( + text, + String::from(r#"{"Local":{"path":"/var/run/mpd.sock"}}"#) + ); + + let text = to_string(&Connection::TCP { + host: String::from("localhost"), + port: 6600, + }) + .unwrap(); + assert_eq!( + text, + String::from(r#"{"TCP":{"host":"localhost","port":6600}}"#) + ); + } +} + +/// This is the most recent `mppopmd` configuration struct. +#[derive(Deserialize, Debug, Serialize)] +#[serde(default)] +pub struct Config { + /// Configuration format version-- must be "1" + // Workaround to https://github.com/rotty/lexpr-rs/issues/77 + // When this gets fixed, I can remove this element from the struct & deserialize as + // a Configurations element-- the on-disk format will be the same. + #[serde(rename = "version")] + _version: String, + + /// Location of log file + pub log: PathBuf, + + /// How to connect to mpd + pub conn: Connection, + + /// The `mpd' root music directory, relative to the host on which *this* daemon is running + pub local_music_dir: PathBuf, + + /// Percentage threshold, expressed as a number between zero & one, for considering a song to + /// have been played + pub played_thresh: f64, + + /// The interval, in milliseconds, at which to poll `mpd' for the current state + pub poll_interval_ms: u64, + + /// Channel to setup for assorted commands-- channel names must satisfy "[-a-zA-Z-9_.:]+" + pub commands_chan: String, +} + +impl Default for Config { + fn default() -> Self { + Self::new().unwrap() + } +} + +impl Config { + fn new() -> Result<Self> { + Ok(Self { + _version: String::from("1"), + log: [LOCALSTATEDIR, "log", "mppopmd.log"].iter().collect(), + conn: Connection::new()?, + local_music_dir: [PREFIX, "Music"].iter().collect(), + played_thresh: 0.6, + poll_interval_ms: 5000, + commands_chan: String::from("unwoundstack.com:commands"), + }) + } +} + +pub fn from_str(text: &str) -> Result<Config> { + let cfg: Config = match serde_json::from_str(text) { + Ok(cfg) => cfg, + Err(err_outer) => { + bail!("Failed to parse config: `{}`", err_outer) + } + }; + Ok(cfg) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[ignore = "We changed the config format to json"] + fn test_from_str() { + let cfg = Config::default(); + assert_eq!(cfg.commands_chan, String::from("unwoundstack.com:commands")); + + assert_eq!( + serde_json::to_string(&cfg).unwrap(), + format!( + r#"((version . "1") (log . "{}/log/mppopmd.log") (conn TCP (host . "localhost") (port . 6600)) (local_music_dir . "{}/Music") (playcount_sticker . "unwoundstack.com:playcount") (lastplayed_sticker . "unwoundstack.com:lastplayed") (played_thresh . 0.6) (poll_interval_ms . 5000) (commands_chan . "unwoundstack.com:commands") (playcount_command . "") (playcount_command_args) (rating_sticker . "unwoundstack.com:rating") (ratings_command . "") (ratings_command_args) (gen_cmds))"#, + LOCALSTATEDIR, PREFIX + ) + ); + + let cfg: Config = serde_json::from_str( + r#" +((version . "1") + (log . "/usr/local/var/log/mppopmd.log") + (conn TCP (host . "localhost") (port . 6600)) + (local_music_dir . "/usr/local/Music") + (playcount_sticker . "unwoundstack.com:playcount") + (lastplayed_sticker . "unwoundstack.com:lastplayed") + (played_thresh . 0.6) + (poll_interval_ms . 5000) + (commands_chan . "unwoundstack.com:commands") + (playcount_command . "") + (playcount_command_args) + (rating_sticker . "unwoundstack.com:rating") + (ratings_command . "") + (ratings_command_args) + (gen_cmds)) +"#, + ) + .unwrap(); + assert_eq!(cfg._version, String::from("1")); + + let cfg: Config = serde_json::from_str( + r#" +((version . "1") + (log . "/usr/local/var/log/mppopmd.log") + (conn Local (path . "/home/mgh/var/run/mpd/mpd.sock")) + (local_music_dir . "/usr/local/Music") + (playcount_sticker . "unwoundstack.com:playcount") + (lastplayed_sticker . "unwoundstack.com:lastplayed") + (played_thresh . 0.6) + (poll_interval_ms . 5000) + (commands_chan . "unwoundstack.com:commands") + (playcount_command . "") + (playcount_command_args) + (rating_sticker . "unwoundstack.com:rating") + (ratings_command . "") + (ratings_command_args) + (gen_cmds)) +"#, + ) + .unwrap(); + assert_eq!(cfg._version, String::from("1")); + assert_eq!( + cfg.conn, + Connection::Local { + path: PathBuf::from("/home/mgh/var/run/mpd/mpd.sock") + } + ); + + // Test fallback to "v0" of the config struct + let cfg = from_str(r#" +((log . "/home/mgh/var/log/mppopmd.log") + (host . "192.168.1.14") + (port . 6600) + (local_music_dir . "/space/mp3") + (playcount_sticker . "unwoundstack.com:playcount") + (lastplayed_sticker . "unwoundstack.com:lastplayed") + (played_thresh . 0.6) + (poll_interval_ms . 5000) + (playcount_command . "/usr/local/bin/scribbu") + (playcount_command_args . ("popm" "-v" "-a" "-f" "-o" "sp1ff@pobox.com" "-C" "%playcount" "%full-file")) + (commands_chan . "unwoundstack.com:commands") + (rating_sticker . "unwoundstack.com:rating") + (ratings_command . "/usr/local/bin/scribbu") + (ratings_command_args . ("popm" "-v" "-a" "-f" "-o" "sp1ff@pobox.com" "-r" "%rating" "%full-file")) + (gen_cmds . + (((name . "set-genre") + (formal_parameters . (Literal Track)) + (default_after . 1) + (cmd . "/usr/local/bin/scribbu") + (args . ("genre" "-a" "-C" "-g" "%1" "%full-file")) + (update . TrackOnly)) + ((name . "set-xtag") + (formal_parameters . (Literal Track)) + (default_after . 1) + (cmd . "/usr/local/bin/scribbu") + (args . ("xtag" "-A" "-o" "sp1ff@pobox.com" "-T" "%1" "%full-file")) + (update . TrackOnly)) + ((name . "merge-xtag") + (formal_parameters . (Literal Track)) + (default_after . 1) + (cmd . "/usr/local/bin/scribbu") + (args . ("xtag" "-m" "-o" "sp1ff@pobox.com" "-T" "%1" "%full-file")) + (update . TrackOnly))))) +"#).unwrap(); + assert_eq!(cfg.log, PathBuf::from("/home/mgh/var/log/mppopmd.log")); + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop b/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop new file mode 100644 index 00000000..a591a3ba --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop @@ -0,0 +1,143 @@ +// Copyright (C) 2020-2025 Michael Herstine <sp1ff@pobox.com> -*- mode: rust; rust-format-on-save: nil -*- +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +use lalrpop_util::ParseError; + +use crate::filters_ast::{Conjunction, Disjunction, Expression, OpCode, Selector, Term, Value, + expect_quoted, parse_iso_8601}; + +grammar; + +pub ExprOp: OpCode = { + "==" => OpCode::Equality, + "!=" => OpCode::Inequality, + "contains" => OpCode::Contains, + "=~" => OpCode::RegexMatch, + "!~" => OpCode::RegexExclude, + ">" => OpCode::GreaterThan, + "<" => OpCode::LessThan, + ">=" => OpCode::GreaterThanEqual, + "<=" => OpCode::LessThanEqual, +}; + +pub ExprSel: Selector = { + r"(?i)artist" => Selector::Artist, + r"(?i)album" => Selector::Album, + r"(?i)albumartist" => Selector::AlbumArtist, + r"(?i)titile" => Selector::Title, + r"(?i)track" => Selector::Track, + r"(?i)name" => Selector::Name, + r"(?i)genre" => Selector::Genre, + r"(?i)date" => Selector::Date, + r"(?i)originaldate" => Selector::OriginalDate, + r"(?i)composer" => Selector::Composer, + r"(?i)performer" => Selector::Performer, + r"(?i)conductor" => Selector::Conductor, + r"(?i)work" => Selector::Work, + r"(?i)grouping" => Selector::Grouping, + r"(?i)comment" => Selector::Comment, + r"(?i)disc" => Selector::Disc, + r"(?i)label" => Selector::Label, + r"(?i)musicbrainz_aristid" => Selector::MusicbrainzAristID, + r"(?i)musicbrainz_albumid" => Selector::MusicbrainzAlbumID, + r"(?i)musicbrainz_albumartistid" => Selector::MusicbrainzAlbumArtistID, + r"(?i)musicbrainz_trackid" => Selector::MusicbrainzTrackID, + r"(?i)musicbrainz_releasetrackid" => Selector::MusicbrainzReleaseTrackID, + r"(?i)musicbrainz_workid" => Selector::MusicbrainzWorkID, + r"(?i)file" => Selector::File, + r"(?i)base" => Selector::Base, + r"(?i)modified-since" => Selector::ModifiedSince, + r"(?i)audioformat" => Selector::AudioFormat, + r"(?i)rating" => Selector::Rating, + r"(?i)playcount" => Selector::PlayCount, + r"(?i)lastplayed" => Selector::LastPlayed, +}; + +pub Token: Value = { + <s:r"[0-9]+"> =>? { + eprintln!("matched token: ``{}''.", s); + // We need to yield a Result<Value, ParseError> + match s.parse::<usize>() { + Ok(n) => Ok(Value::Uint(n)), + Err(_) => Err(ParseError::User { + error: "Internal parse error while parsing unsigned int" }) + } + }, + <s:r#""([ \t'a-zA-Z0-9~!@#$%^&*()-=_+\[\]{}|;:<>,./?]|\\\\|\\"|\\')+""#> => { + eprintln!("matched token: ``{}''.", s); + let s = expect_quoted(s).unwrap(); + match parse_iso_8601(&mut s.as_bytes()) { + Ok(x) => Value::UnixEpoch(x), + Err(_) => Value::Text(s), + } + }, + <s:r#"'([ \t"a-zA-Z0-9~!@#$%^&*()-=_+\[\]{}|;:<>,./?]|\\\\|\\'|\\")+'"#> => { + eprintln!("matched token: ``{}''.", s); + let s = expect_quoted(s).unwrap(); + match parse_iso_8601(&mut s.as_bytes()) { + Ok(x) => Value::UnixEpoch(x), + Err(_) => Value::Text(s), + } + }, +}; + +pub Term: Box<Term> = { + <t:ExprSel> <u:Token> => { + eprintln!("matched unary condition: ``({}, {:#?})''", t, u); + Box::new(Term::UnaryCondition(t, u)) + }, + <t:ExprSel> <o:ExprOp> <u:Token> => { + eprintln!("matched binary condition: ``({}, {:#?}, {:#?})''", t, o, u); + Box::new(Term::BinaryCondition(t, o, u)) + }, +} + +pub Conjunction: Box<Conjunction> = { + <e1:Expression> "AND" <e2:Expression> => { + eprintln!("matched conjunction: ``({:#?}, {:#?})''", e1, e2); + Box::new(Conjunction::Simple(e1, e2)) + }, + <c:Conjunction> "AND" <e:Expression> => { + eprintln!("matched conjunction: ``({:#?}, {:#?})''", c, e); + Box::new(Conjunction::Compound(c, e)) + }, +} + +pub Disjunction: Box<Disjunction> = { + <e1:Expression> "OR" <e2:Expression> => { + eprintln!("matched disjunction: ``({:#?}, {:#?})''", e1, e2); + Box::new(Disjunction::Simple(e1, e2)) + }, + <c:Disjunction> "OR" <e:Expression> => { + eprintln!("matched disjunction: ``({:#?}, {:#?})''", c, e); + Box::new(Disjunction::Compound(c, e)) + }, +} + +pub Expression: Box<Expression> = { + "(" <t:Term> ")" => { + eprintln!("matched parenthesized term: ``({:#?})''", t); + Box::new(Expression::Simple(t)) + }, + "(" "!" <e:Expression> ")" => Box::new(Expression::Negation(e)), + "(" <c:Conjunction> ")" => { + eprintln!("matched parenthesized conjunction: ``({:#?})''", c); + Box::new(Expression::Conjunction(c)) + }, + "(" <c:Disjunction> ")" => { + eprintln!("matched parenthesized disjunction: ``({:#?})''", c); + Box::new(Expression::Disjunction(c)) + }, +} diff --git a/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs b/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs new file mode 100644 index 00000000..bd1a67d6 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs @@ -0,0 +1,1005 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! Types for building the Abstract Syntax Tree when parsing filters +//! +//! This module provides support for our [lalrpop](https://github.com/lalrpop/lalrpop) grammar. + +use crate::clients::Client; +use crate::storage::{last_played, play_count, rating_count}; + +use anyhow::{Context, Error, Result, anyhow, bail}; +use boolinator::Boolinator; +use chrono::prelude::*; +use tracing::debug; + +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +/// The operations that can appear in a filter term +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum OpCode { + Equality, + Inequality, + Contains, + RegexMatch, + RegexExclude, + GreaterThan, + LessThan, + GreaterThanEqual, + LessThanEqual, +} + +impl std::fmt::Display for OpCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + OpCode::Equality => "==", + OpCode::Inequality => "!=", + OpCode::Contains => "contains", + OpCode::RegexMatch => "=~", + OpCode::RegexExclude => "!~", + OpCode::GreaterThan => ">", + OpCode::LessThan => "<", + OpCode::GreaterThanEqual => ">=", + OpCode::LessThanEqual => "<=", + } + ) + } +} + +/// The song attributes that can appear on the LHS of a filter term +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Selector { + Artist, + Album, + AlbumArtist, + Title, + Track, + Name, + Genre, + Date, + OriginalDate, + Composer, + Performer, + Conductor, + Work, + Grouping, + Comment, + Disc, + Label, + MusicbrainzAristID, + MusicbrainzAlbumID, + MusicbrainzAlbumArtistID, + MusicbrainzTrackID, + MusicbrainzReleaseTrackID, + MusicbrainzWorkID, + File, + Base, + ModifiedSince, + AudioFormat, + Rating, + PlayCount, + LastPlayed, +} + +impl std::fmt::Display for Selector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Selector::Artist => "artist", + Selector::Album => "album", + Selector::AlbumArtist => "albumartist", + Selector::Title => "title", + Selector::Track => "track", + Selector::Name => "name", + Selector::Genre => "genre", + Selector::Date => "date", + Selector::OriginalDate => "originaldate", + Selector::Composer => "composer", + Selector::Performer => "performer", + Selector::Conductor => "conductor", + Selector::Work => "work", + Selector::Grouping => "grouping", + Selector::Comment => "comment", + Selector::Disc => "disc", + Selector::Label => "label", + Selector::MusicbrainzAristID => "musicbrainz_aristid", + Selector::MusicbrainzAlbumID => "musicbrainz_albumid", + Selector::MusicbrainzAlbumArtistID => "musicbrainz_albumartistid", + Selector::MusicbrainzTrackID => "musicbrainz_trackid", + Selector::MusicbrainzReleaseTrackID => "musicbrainz_releasetrackid", + Selector::MusicbrainzWorkID => "musicbrainz_workid", + Selector::File => "file", + Selector::Base => "base", + Selector::ModifiedSince => "modified-since", + Selector::AudioFormat => "AudioFormat", + Selector::Rating => "rating", + Selector::PlayCount => "playcount", + Selector::LastPlayed => "lastplayed", + } + ) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + Text(String), + UnixEpoch(i64), + Uint(usize), +} + +fn quote_value(x: &Value) -> String { + match x { + Value::Text(s) => { + let mut ret = String::new(); + + ret.push('"'); + for c in s.chars() { + if c == '"' || c == '\\' { + ret.push('\\'); + } + ret.push(c); + } + ret.push('"'); + ret + } + Value::UnixEpoch(n) => { + format!("'{}'", n) + } + Value::Uint(n) => { + format!("'{}'", n) + } + } +} + +#[derive(Clone, Debug)] +pub enum Term { + UnaryCondition(Selector, Value), + BinaryCondition(Selector, OpCode, Value), +} + +#[derive(Clone, Debug)] +pub enum Conjunction { + Simple(Box<Expression>, Box<Expression>), + Compound(Box<Conjunction>, Box<Expression>), +} + +#[derive(Clone, Debug)] +pub enum Disjunction { + Simple(Box<Expression>, Box<Expression>), + Compound(Box<Disjunction>, Box<Expression>), +} + +#[derive(Clone, Debug)] +pub enum Expression { + Simple(Box<Term>), + Negation(Box<Expression>), + Conjunction(Box<Conjunction>), + Disjunction(Box<Disjunction>), +} + +#[cfg(test)] +mod smoke_tests { + use super::*; + use crate::filters::*; + + #[test] + fn test_opcodes() { + assert!(ExprOpParser::new().parse("==").unwrap() == OpCode::Equality); + assert!(ExprOpParser::new().parse("!=").unwrap() == OpCode::Inequality); + assert!(ExprOpParser::new().parse("contains").unwrap() == OpCode::Contains); + assert!(ExprOpParser::new().parse("=~").unwrap() == OpCode::RegexMatch); + assert!(ExprOpParser::new().parse("!~").unwrap() == OpCode::RegexExclude); + assert!(ExprOpParser::new().parse(">").unwrap() == OpCode::GreaterThan); + assert!(ExprOpParser::new().parse("<").unwrap() == OpCode::LessThan); + assert!(ExprOpParser::new().parse(">=").unwrap() == OpCode::GreaterThanEqual); + assert!(ExprOpParser::new().parse("<=").unwrap() == OpCode::LessThanEqual); + } + + #[test] + fn test_conditions() { + assert!(TermParser::new().parse("base 'foo'").is_ok()); + assert!(TermParser::new().parse("artist == 'foo'").is_ok()); + assert!( + TermParser::new() + .parse(r#"artist =~ "foo bar \"splat\"!""#) + .is_ok() + ); + assert!(TermParser::new().parse("artist =~ 'Pogues'").is_ok()); + + match *TermParser::new() + .parse(r#"base "/Users/me/My Music""#) + .unwrap() + { + Term::UnaryCondition(a, b) => { + assert!(a == Selector::Base); + assert!(b == Value::Text(String::from(r#"/Users/me/My Music"#))); + } + _ => { + unreachable!(); + } + } + + match *TermParser::new() + .parse(r#"artist =~ "foo bar \"splat\"!""#) + .unwrap() + { + Term::BinaryCondition(t, op, s) => { + assert!(t == Selector::Artist); + assert!(op == OpCode::RegexMatch); + assert!(s == Value::Text(String::from(r#"foo bar "splat"!"#))); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_expressions() { + assert!(ExpressionParser::new().parse("( base 'foo' )").is_ok()); + assert!(ExpressionParser::new().parse("(base \"foo\")").is_ok()); + assert!( + ExpressionParser::new() + .parse("(!(artist == 'value'))") + .is_ok() + ); + assert!( + ExpressionParser::new() + .parse(r#"((!(artist == "foo bar")) AND (base "/My Music"))"#) + .is_ok() + ); + } + + #[test] + fn test_quoted_expr() { + eprintln!("test_quoted_expr"); + assert!( + ExpressionParser::new() + .parse(r#"(artist =~ "foo\\bar\"")"#) + .is_ok() + ); + } + + #[test] + fn test_real_expression() { + let result = ExpressionParser::new() + .parse(r#"(((Artist =~ 'Flogging Molly') OR (artist =~ 'Dropkick Murphys') OR (artist =~ 'Pogues')) AND ((rating > 128) OR (rating == 0)))"#); + eprintln!("{:#?}", result); + assert!(result.is_ok()); + } + + #[test] + fn test_conjunction() { + assert!(ExpressionParser::new() + .parse( + r#"((base "foo") AND (artist == "foo bar") AND (!(file == '/net/mp3/A/a.mp3')))"# + ) + .is_ok()); + + eprintln!("=============================================================================="); + eprintln!("{:#?}", ExpressionParser::new() + .parse( + r#"((base 'foo') AND (artist == "foo bar") AND ((!(file == "/net/mp3/A/a.mp3")) OR (file == "/pub/mp3/A/a.mp3")))"# + )); + assert!(ExpressionParser::new() + .parse( + r#"((base 'foo') AND (artist == "foo bar") AND ((!(file == '/net/mp3/A/a.mp3')) OR (file == '/pub/mp3/A/a.mp3')))"# + ) + .is_ok()); + } + + #[test] + fn test_disjunction() { + assert!(ExpressionParser::new(). + parse(r#"((artist =~ 'Flogging Molly') OR (artist =~ 'Dropkick Murphys') OR (artist =~ 'Pogues'))"#) + .is_ok()); + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum EvalOp { + And, + Or, + Not, +} + +impl std::fmt::Display for EvalOp { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + EvalOp::And => write!(f, "And"), + EvalOp::Or => write!(f, "Or"), + EvalOp::Not => write!(f, "Not"), + } + } +} + +fn peek(buf: &[u8]) -> Option<char> { + match buf.len() { + 0 => None, + _ => Some(buf[0] as char), + } +} + +// advancing a slice by `i` indicies can *not* be this difficult +/// Pop a single byte off of `buf` +fn take1(buf: &mut &[u8], i: usize) -> Result<()> { + if i > buf.len() { + bail!("Bad iso-8601 string: `{:#?}`", buf); + } + let (_first, second) = buf.split_at(i); + *buf = second; + Ok(()) +} + +/// Pop `i` bytes off of `buf` & parse them as a T +fn take2<T>(buf: &mut &[u8], i: usize) -> Result<T> +where + T: FromStr, + <T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static, +{ + // 1. check len + if i > buf.len() { + bail!("Bad iso-8601 string: `{:#?}`", buf); + } + + let (first, second) = buf.split_at(i); + *buf = second; + // 2. convert to a string + let s = std::str::from_utf8(first).context("Bad iso-8601 string")?; + // 3. parse as a T + s.parse::<T>() + .context("Failed to parse iso-8601 string as T") +} + +/// Parse a timestamp in ISO 8601 format to a chrono DateTime instance +/// +/// Surprisingly, I was unable to find an ISO 8601 parser in Rust. I *did* find a crate named +/// iso-8601 that promised to do this, but it seemed derelict & I couldn't see what to do with the +/// parse output in any event. The ISO 8601 format is simple enough that I've chosen to simply +/// hand-parse it. +pub fn parse_iso_8601(buf: &mut &[u8]) -> Result<i64> { + // I wonder if `nom` would be a better choice? + + // The first four characters must be the year (expanded year representation is not supported by + // this parser). + + let year: i32 = take2(buf, 4)?; + + // Now at this point: + // 1. we may be done (i.e. buf.len() == 0) + // 2. we may have the timestamp (peek(buf) => Some('T')) + // - day & month := 0, consume the 'T', move on to parsing the time + // 3. we may have a month in extended format (i.e. peek(buf) => Some('-') + // - consume the '-', parse the month & move on to parsing the day + // 4. we may have a month in basic format (take(buf, 2) => Some('\d\d') + // - parse the month & move on to parsing the day + let mut month = 1; + let mut day = 1; + let mut hour = 0; + let mut minute = 0; + let mut second = 0; + if !buf.is_empty() { + let next = peek(buf); + if next != Some('T') { + let mut ext_fmt = false; + if next == Some('-') { + take1(buf, 1)?; + ext_fmt = true; + } + month = take2(buf, 2)?; + + // At this point: + // 1. we may be done (i.e. buf.len() == 0) + // 2. we may have the timestamp (peek(buf) => Some('T')) + // 3. we may have the day (in basic or extended format) + if !buf.is_empty() && peek(buf) != Some('T') { + if ext_fmt { + take1(buf, 1)?; + } + day = take2(buf, 2)?; + } + } + + // Parse time: at this point, buf will either be empty or begin with 'T' + if !buf.is_empty() { + take1(buf, 1)?; + // If there's a T, there must at least be an hour + hour = take2(buf, 2)?; + if !buf.is_empty() { + let mut ext_fmt = false; + if peek(buf) == Some(':') { + take1(buf, 1)?; + ext_fmt = true; + } + minute = take2(buf, 2)?; + if !buf.is_empty() { + if ext_fmt { + take1(buf, 1)?; + } + second = take2(buf, 2)?; + } + } + } + + // At this point, there may be a timezone + if !buf.is_empty() { + if peek(buf) == Some('Z') { + return Ok(Utc + .with_ymd_and_hms(year, month, day, hour, minute, second) + .single() + .ok_or(anyhow!("bad iso-8601 string"))? + .timestamp()); + } else { + let next = peek(buf); + if next != Some('-') && next != Some('+') { + bail!("bad iso-8601 string") + } + let west = next == Some('-'); + take1(buf, 1)?; + + let hours: i32 = take2(buf, 2)?; + let mut minutes = 0; + + if !buf.is_empty() { + if peek(buf) == Some(':') { + take1(buf, 1)?; + } + minutes = take2(buf, 2)?; + } + + if west { + return Ok(FixedOffset::west_opt(hours * 3600 + minutes * 60) + .ok_or(anyhow!("Bad iso-8601 string"))? + .with_ymd_and_hms(year, month, day, hour, minute, second) + .single() + .ok_or(anyhow!("Bad iso-8601 string"))? + .timestamp()); + } else { + return Ok(FixedOffset::east_opt(hours * 3600 + minutes * 60) + .ok_or(anyhow!("Bad iso-8601 string"))? + .with_ymd_and_hms(year, month, day, hour, minute, second) + .single() + .ok_or(anyhow!("Bad iso-8601 string"))? + .timestamp()); + } + } + } + } + Ok(Local + .with_ymd_and_hms(year, month, day, hour, minute, second) + .single() + .ok_or(anyhow!("Bad iso-8601 string"))? + .timestamp()) +} + +#[cfg(test)] +mod iso_8601_tests { + + use super::*; + + #[test] + fn smoke_tests() { + let mut b = "19700101T00:00:00Z".as_bytes(); + let t = parse_iso_8601(&mut b).unwrap(); + assert!(t == 0); + + let mut b = "19700101T00:00:01Z".as_bytes(); + let t = parse_iso_8601(&mut b).unwrap(); + assert!(t == 1); + + let mut b = "20210327T02:26:53Z".as_bytes(); + let t = parse_iso_8601(&mut b).unwrap(); + assert_eq!(t, 1616812013); + + let mut b = "20210327T07:29:05-07:00".as_bytes(); + let t = parse_iso_8601(&mut b).unwrap(); + assert_eq!(t, 1616855345); + + let mut b = "2021".as_bytes(); + // Should resolve to midnight, Jan 1 2021 in local time; don't want to test against the + // timestamp; just make sure it parses + parse_iso_8601(&mut b).unwrap(); + } +} + +/// "Un-quote" a token +/// +/// Textual tokens must be quoted, and double-quote & backslashes within backslash-escaped. If the +/// string is quoted with single-quotes, then any single-quotes inside the string will also need +/// to be escaped. +/// +/// In fact, *any* characters within may be harmlessly backslash escaped; the MPD implementation +/// walks the the string, skipping backslashes as it goes, so this implementation will do the same. +/// I have named this method in imitation of the corresponding MPD function. +pub fn expect_quoted(qtext: &str) -> Result<String> { + let mut iter = qtext.chars(); + let quote = iter.next(); + if quote.is_none() { + return Ok(String::new()); + } + + if quote != Some('\'') && quote != Some('"') { + bail!("Expected text to be quoted: `{}`", qtext); + } + + let mut ret = String::new(); + + // Walk qtext[1..]; copying characters to `ret'. If a '\' is found, skip to the next character + // (even if that is a '\'). The last character in qtext should be the closing quote. + let mut this = iter.next(); + while this != quote { + if this == Some('\\') { + this = iter.next(); + } + match this { + Some(c) => ret.push(c), + None => { + bail!("Expected text to be quoted: `{}`", qtext); + } + } + this = iter.next(); + } + + Ok(ret) +} + +#[cfg(test)] +mod quoted_tests { + + use super::*; + + #[test] + fn smoke_tests() { + let b = r#""foo bar \"splat!\"""#; + let s = expect_quoted(b).unwrap(); + assert!(s == r#"foo bar "splat!""#); + } +} + +/// Create a closure that will carry out an operator on its argument +/// +/// Call this function with an [OpCode] and a value of type `T`. `T` must be [PartialEq], +/// [`PartialOrd`] and [`Copy`]-- an integral type will do. It will return a closure that will carry +/// out the given [OpCode] against the given value. For instance, +/// `make_numeric_closure::<u8>(OpCode::Equality, 11)` will return a closure that takes a `u8` & +/// will return true if its argument is 11 (and false otherwise). +/// +/// If [OpCode] is not pertinent to a numeric type, then this function will return Err. +fn make_numeric_closure<'a, T: 'a + PartialEq + PartialOrd + Copy>( + op: OpCode, + val: T, +) -> Result<impl Fn(T) -> bool + 'a> { + // Rust closures each have their own type, so this was the only way I could find to + // return them from match arms. This seems ugly; perhaps there's something I'm + // missing. + // + // I have no idea why I have to make these `move` closures; T is constrained to by Copy-able, + // so I would have expected the closure to just take a copy. + match op { + OpCode::Equality => Ok(Box::new(move |x: T| x == val) as Box<dyn Fn(T) -> bool>), + OpCode::Inequality => Ok(Box::new(move |x: T| x != val) as Box<dyn Fn(T) -> bool>), + OpCode::GreaterThan => Ok(Box::new(move |x: T| x > val) as Box<dyn Fn(T) -> bool>), + OpCode::LessThan => Ok(Box::new(move |x: T| x < val) as Box<dyn Fn(T) -> bool>), + OpCode::GreaterThanEqual => Ok(Box::new(move |x: T| x >= val) as Box<dyn Fn(T) -> bool>), + OpCode::LessThanEqual => Ok(Box::new(move |x: T| x <= val) as Box<dyn Fn(T) -> bool>), + _ => bail!("Invalid operant: `{op}`"), + } +} + +async fn eval_numeric_sticker_term< + // The `FromStr' trait bound is really weird, but if I don't constrain the associated + // Err type to be `ParseIntError' the compiler complains about not being able to convert + // it to type `Error'. I'm probably still "thinking in C++" and imagining the compiler + // instantiating this function for each type (u8, usize, &c) instead of realizing that the Rust + // compiler is processing this as a first-class function. + // + // For instance, I can do the conversion manually, so long as I constrain the Err type + // to implement std::error::Error. I should probably be doing that, but it clutters the + // code. I'll figure it out when I need to extend this function to handle non-integral types + // :) + T: PartialEq + PartialOrd + Copy + FromStr<Err = std::num::ParseIntError> + std::fmt::Display, +>( + sticker: &str, + client: &mut Client, + op: OpCode, + numeric_val: T, + default_val: T, +) -> Result<HashSet<String>> { + let cmp = make_numeric_closure(op, numeric_val)?; + // It would be better to idle on the sticker DB & just update our collection on change, but for + // a first impl. this will do. + // + // Call `get_stickers'; this will return a HashMap from song URIs to ratings expressed as text + // (as all stickers are). This stanza will drain that collection into a new one with the ratings + // expressed as T. + // + // The point is that conversion from text to rating, lastplayed, or whatever can fail; the + // invocation of `collect' will call `from_iter' to convert a collection of Result-s to a Result + // of a collection. + let mut m = client + .get_stickers(sticker) + .await + .context("Failed to get stickers from client")? + .drain() + .map(|(k, v)| v.parse::<T>().map(|x| (k, x))) + .collect::<std::result::Result<HashMap<String, T>, _>>() + .context("Failed to parse sticker as T")?; + // `m' is now a map of song URI to rating/playcount/wathever (expressed as a T)... for all songs + // that have the salient sticker. + // + // This seems horribly inefficient, but I'm going to fetch all the song URIs in the music DB, + // and augment `m' with entries of `default_val' for any that are not already there. + client + .get_all_songs() + .await + .context("Failed to get all songs from client")? + .drain(..) + .for_each(|song| { + m.entry(song).or_insert(default_val); + }); + // Now that we don't have to worry about operations that can fail, we can use + // `filter_map'. + Ok(m.drain() + .filter_map(|(k, v)| cmp(v).as_some(k)) + .collect::<HashSet<String>>()) +} + +/// Convenience struct collecting the names for assorted stickers on which one may search +/// +/// While the search terms 'rating', 'playcount' &c are fixed & part of the filter grammar offered +/// by mpdpopm, the precise names of the corresponding stickers are configurable & hence must be +/// passed in. Three references to str is already unweildy IMO, and since I expect the number of +/// stickers on which one can search to grow further, I decided to wrap 'em up in a struct. The +/// lifetime is there to support the caller just using a reference to an existing string rather than +/// making a copy. +pub struct FilterStickerNames<'a> { + rating: &'a str, + playcount: &'a str, + lastplayed: &'a str, +} + +impl FilterStickerNames<'static> { + pub fn new() -> FilterStickerNames<'static> { + Self::default() + } +} + +impl Default for FilterStickerNames<'static> { + fn default() -> Self { + Self { + rating: rating_count::STICKER, + playcount: play_count::STICKER, + lastplayed: last_played::STICKER, + } + } +} + +/// Evaluate a Term +/// +/// Take a Term from the Abstract Syntax tree & resolve it to a collection of song URIs. Set `case` +/// to `true` to search case-sensitively & `false` to make the search case-insensitive. +async fn eval_term<'a>( + term: &Term, + case: bool, + client: &mut Client, + stickers: &FilterStickerNames<'a>, +) -> Result<HashSet<String>> { + match term { + Term::UnaryCondition(op, val) => Ok(client + .find1(&format!("{}", op), "e_value(val), case) + .await + .context("Failed to find1 on client")? + .drain(..) + .collect()), + Term::BinaryCondition(attr, op, val) => { + if *attr == Selector::Rating { + match val { + Value::Uint(n) => { + if *n > 255 { + bail!("Rating of `{}` is greater than allowed!", n) + } + Ok( + eval_numeric_sticker_term(stickers.rating, client, *op, *n as u8, 0) + .await?, + ) + } + _ => bail!("filter ratings expect an unsigned int; got {:#?}", val), + } + } else if *attr == Selector::PlayCount { + match val { + Value::Uint(n) => { + Ok( + eval_numeric_sticker_term(stickers.playcount, client, *op, *n, 0) + .await?, + ) + } + _ => bail!("filter ratings expect an unsigned int; got {:#?}", val), + } + } else if *attr == Selector::LastPlayed { + match val { + Value::UnixEpoch(t) => { + Ok( + eval_numeric_sticker_term(stickers.lastplayed, client, *op, *t, 0) + .await?, + ) + } + _ => bail!("filter ratings expect an unsigned int; got {:#?}", val), + } + } else { + Ok(client + .find2( + &format!("{}", attr), + &format!("{}", op), + "e_value(val), + case, + ) + .await + .context("Failed to `find2` on client")? + .drain(..) + .collect()) + } + } + } +} + +/// The evaluation stack contains logical operators & sets of song URIs +#[derive(Debug)] +enum EvalStackNode { + Op(EvalOp), + Result(HashSet<String>), +} + +async fn negate_result( + res: &HashSet<String>, + client: &mut Client, +) -> std::result::Result<HashSet<String>, Error> { + Ok(client + .get_all_songs() + .await + .context("Failed to get all songs from client")? + .drain(..) + .filter_map(|song| { + // Some(thing) adds thing, None elides it + if !res.contains(&song) { + Some(song) + } else { + None + } + }) + .collect::<HashSet<String>>()) +} + +/// Reduce the evaluation stack as far as possible. +/// +/// We can pop the stack in two cases: +/// +/// 1. S.len() > 2 and S[-3] is either And or Or, and both S[-1] & S[-2] are Result-s +/// 2. S.len() > 1, S[-2] is Not, and S[-1] is a Result +async fn reduce(stack: &mut Vec<EvalStackNode>, client: &mut Client) -> Result<()> { + loop { + let mut reduced = false; + let n = stack.len(); + if n > 1 { + // Take care to compute the reduction *before* popping the stack-- thank you, borrow + // checker! + let reduction = if let (EvalStackNode::Op(EvalOp::Not), EvalStackNode::Result(r)) = + (&stack[n - 2], &stack[n - 1]) + { + Some(negate_result(r, client).await?) + } else { + None + }; + + if let Some(res) = reduction { + stack.pop(); + stack.pop(); + stack.push(EvalStackNode::Result(res)); + reduced = true; + } + } + let n = stack.len(); + if n > 2 { + // Take care to compute the reduction *before* popping the stack-- thank you, borrow + // checker! + let and_reduction = if let ( + EvalStackNode::Op(EvalOp::And), + EvalStackNode::Result(r1), + EvalStackNode::Result(r2), + ) = (&stack[n - 3], &stack[n - 2], &stack[n - 1]) + { + Some(r1.intersection(r2).cloned().collect()) + } else { + None + }; + + if let Some(res) = and_reduction { + stack.pop(); + stack.pop(); + stack.pop(); + stack.push(EvalStackNode::Result(res)); + reduced = true; + } + } + let n = stack.len(); + if n > 2 { + let or_reduction = if let ( + EvalStackNode::Op(EvalOp::Or), + EvalStackNode::Result(r1), + EvalStackNode::Result(r2), + ) = (&stack[n - 3], &stack[n - 2], &stack[n - 1]) + { + Some(r1.union(r2).cloned().collect()) + } else { + None + }; + + if let Some(res) = or_reduction { + stack.pop(); + stack.pop(); + stack.pop(); + stack.push(EvalStackNode::Result(res)); + reduced = true; + } + } + + if !reduced { + break; + } + } + + Ok(()) +} + +/// Evaluate an abstract syntax tree (AST) +pub async fn evaluate<'a>( + expr: &Expression, + case: bool, + client: &mut Client, + stickers: &FilterStickerNames<'a>, +) -> Result<HashSet<String>> { + // We maintain *two* stacks, one for parsing & one for evaluation. Let sp (for "stack(parse)") + // be a stack of references to nodes in the parse tree. + let mut sp = Vec::new(); + // Initialize it with the root; as we walk the tree, we'll pop the "most recent" node, and push + // children. + sp.push(expr); + + // Let se (for "stack(eval)") be a stack of operators & URIs. + let mut se = Vec::new(); + + // Simple DFS traversal of the AST: + while let Some(node) = sp.pop() { + // and dispatch based on what we've got: + match node { + // 1. we have a simple term: this can be immediately resolved to a set of song URIs. Do + // so & push the resulting set onto the evaluation stack. + Expression::Simple(bt) => se.push(EvalStackNode::Result( + eval_term(bt, case, client, stickers).await?, + )), + // 2. we have a negation: push the "not" operator onto the evaluation stack & the child + // onto the parse stack. + Expression::Negation(be) => { + se.push(EvalStackNode::Op(EvalOp::Not)); + sp.push(be); + } + // 3. conjunction-- push the "and" operator onto the evaluation stack & the children + // onto the parse stack (be sure to push the right-hand child first, so it will be + // popped second) + // bc is &Box<Conjunction<'a>>, so &**bc is &Conjunction<'a> + Expression::Conjunction(bc) => { + let mut conj = &**bc; + loop { + match conj { + Conjunction::Simple(bel, ber) => { + se.push(EvalStackNode::Op(EvalOp::And)); + sp.push(&**ber); + sp.push(&**bel); + break; + } + Conjunction::Compound(bc, be) => { + se.push(EvalStackNode::Op(EvalOp::And)); + sp.push(&**be); + conj = bc; + } + } + } + } + Expression::Disjunction(bt) => { + let mut disj = &**bt; + loop { + match disj { + Disjunction::Simple(bel, ber) => { + se.push(EvalStackNode::Op(EvalOp::Or)); + sp.push(ber); + sp.push(bel); + break; + } + Disjunction::Compound(bd, be) => { + se.push(EvalStackNode::Op(EvalOp::Or)); + sp.push(&**be); + disj = bd; + } + } + } + } + } + + reduce(&mut se, client).await?; + } + + // At this point, sp is empty, but there had better be something on se. Keep reducing the stack + // until either we can't any further (in which case we error) or there is only one element left + // (in which case we return that). + reduce(&mut se, client).await?; + + // Now, se had better have one element, and that element had better be a Result. + if 1 != se.len() { + debug!("Too many ({}) operands left on stack:", se.len()); + se.iter() + .enumerate() + .for_each(|(i, x)| debug!(" {}: {:#?}", i, x)); + bail!("The number of operants is too big `{}`", se.len()); + } + + let ret = se.pop().unwrap(); + match ret { + EvalStackNode::Result(result) => Ok(result), + EvalStackNode::Op(op) => { + debug!("Operator left on stack (!?): {:#?}", op); + bail!("Operator left on stack: {op}") + } + } +} + +#[cfg(test)] +mod evaluation_tests { + + use super::*; + use crate::filters::*; + + use crate::clients::Client; + use crate::clients::test_mock::Mock; + + #[tokio::test] + async fn smoke() { + let mock = Box::new(Mock::new(&[( + r#"find "(base \"foo\")""#, + "file: foo/a.mp3 +Artist: The Foobars +file: foo/b.mp3 +Title: b! +OK", + )])); + let mut cli = Client::new(mock).unwrap(); + + let stickers = FilterStickerNames::new(); + + let expr = ExpressionParser::new().parse(r#"(base "foo")"#).unwrap(); + let result = evaluate(&expr, true, &mut cli, &stickers).await; + assert!(result.is_ok()); + + let g: HashSet<String> = ["foo/a.mp3", "foo/b.mp3"] + .iter() + .map(|x| x.to_string()) + .collect(); + assert!(result.unwrap() == g); + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/lib.rs b/pkgs/by-name/mp/mpdpopm/src/lib.rs new file mode 100644 index 00000000..4fe523ea --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/lib.rs @@ -0,0 +1,207 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! # mpdpopm +//! +//! Maintain ratings & playcounts for your mpd server. +//! +//! # Introduction +//! +//! This is a companion daemon for [mpd](https://www.musicpd.org/) that maintains play counts & +//! ratings. Similar to [mpdfav](https://github.com/vincent-petithory/mpdfav), but written in Rust +//! (which I prefer to Go), it will allow you to maintain that information in your tags, as well as +//! the sticker database, by invoking external commands to keep your tags up-to-date (something +//! along the lines of [mpdcron](https://alip.github.io/mpdcron)). +//! +//! # Commands +//! +//! I'm currently sending all commands over one (configurable) channel. +//! + +#![recursion_limit = "512"] // for the `select!' macro + +pub mod clients; +pub mod config; +pub mod filters_ast; +pub mod messages; +pub mod playcounts; +pub mod storage; +pub mod vars; + +#[rustfmt::skip] +#[allow(clippy::extra_unused_lifetimes)] +#[allow(clippy::needless_lifetimes)] +#[allow(clippy::let_unit_value)] +#[allow(clippy::just_underscores_and_digits)] +pub mod filters { + include!(concat!(env!("OUT_DIR"), "/src/filters.rs")); +} + +use crate::{ + clients::{Client, IdleClient, IdleSubSystem}, + config::{Config, Connection}, + filters_ast::FilterStickerNames, + messages::MessageProcessor, + playcounts::PlayState, +}; + +use anyhow::{Context, Error}; +use futures::{future::FutureExt, pin_mut, select}; +use tokio::{ + signal, + signal::unix::{SignalKind, signal}, + time::{Duration, sleep}, +}; +use tracing::{debug, error, info}; + +/// Core `mppopmd' logic +pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> { + info!("mpdpopm {} beginning.", vars::VERSION); + + let filter_stickers = FilterStickerNames::new(); + + let mut client = + match cfg.conn { + Connection::Local { ref path } => Client::open(path) + .await + .with_context(|| format!("Failed to open socket at `{}`", path.display()))?, + Connection::TCP { ref host, port } => Client::connect(format!("{}:{}", host, port)) + .await + .with_context(|| format!("Failed to connect to client at `{}:{}`", host, port))?, + }; + + let mut state = PlayState::new(&mut client, cfg.played_thresh) + .await + .context("Failed to construct PlayState")?; + + let mut idle_client = match cfg.conn { + Connection::Local { ref path } => IdleClient::open(path) + .await + .context("Failed to open idle client")?, + Connection::TCP { ref host, port } => IdleClient::connect(format!("{}:{}", host, port)) + .await + .context("Failed to connect to TCP idle client")?, + }; + + idle_client + .subscribe(&cfg.commands_chan) + .await + .context("Failed to subscribe to idle_client")?; + + let mut hup = signal(SignalKind::hangup()).unwrap(); + let mut kill = signal(SignalKind::terminate()).unwrap(); + let ctrl_c = signal::ctrl_c().fuse(); + + let sighup = hup.recv().fuse(); + let sigkill = kill.recv().fuse(); + + let tick = sleep(Duration::from_millis(cfg.poll_interval_ms)).fuse(); + pin_mut!(ctrl_c, sighup, sigkill, tick); + + let mproc = MessageProcessor::new(); + + let mut done = false; + while !done { + debug!("selecting..."); + let mut msg_check_needed = false; + { + // `idle_client' mutably borrowed here + let mut idle = Box::pin(idle_client.idle().fuse()); + loop { + select! { + _ = ctrl_c => { + info!("got ctrl-C"); + done = true; + break; + }, + _ = sighup => { + info!("got SIGHUP"); + done = true; + break; + }, + _ = sigkill => { + info!("got SIGKILL"); + done = true; + break; + }, + _ = tick => { + tick.set(sleep(Duration::from_millis(cfg.poll_interval_ms)).fuse()); + state.update(&mut client) + .await + .context("PlayState update failed")? + }, + // next = cmds.next() => match next { + // Some(out) => { + // debug!("output status is {:#?}", out.out); + // match out.upd { + // Some(uri) => { + // debug!("{} needs to be updated", uri); + // client.update(&uri).await.map_err(|err| Error::Client { + // source: err, + // back: Backtrace::new(), + // })?; + // }, + // None => debug!("No database update needed"), + // } + // }, + // None => { + // debug!("No more commands to process."); + // } + // }, + res = idle => match res { + Ok(subsys) => { + debug!("subsystem {} changed", subsys); + if subsys == IdleSubSystem::Player { + state.update(&mut client) + .await + .context("PlayState update failed")? + } else if subsys == IdleSubSystem::Message { + msg_check_needed = true; + } + break; + }, + Err(err) => { + debug!("error {err:#?} on idle"); + done = true; + break; + } + } + } + } + } + + if msg_check_needed { + // Check for any messages that have come in; if there's an error there's not a lot we + // can do about it (suppose some client fat-fingers a command name, e.g.)-- just log it + // & move on. + if let Err(err) = mproc + .check_messages( + &mut client, + &mut idle_client, + state.last_status(), + &cfg.commands_chan, + &filter_stickers, + ) + .await + { + error!("Error while processing messages: {err:#?}"); + } + } + } + + info!("mpdpopm exiting."); + + Ok(()) +} diff --git a/pkgs/by-name/mp/mpdpopm/src/messages.rs b/pkgs/by-name/mp/mpdpopm/src/messages.rs new file mode 100644 index 00000000..171a246a --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/messages.rs @@ -0,0 +1,409 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! # messages +//! +//! Process incoming messages to the [mpdpopm](https://github.com/sp1ff/mpdpopm) daemon. +//! +//! # Introduction +//! +//! The [mpdpopm](https://github.com/sp1ff/mpdpopm) daemon accepts commands over a dedicated +//! [channel](https://www.musicpd.org/doc/html/protocol.html#client-to-client). It also provides for +//! a generalized framework in which the [mpdpopm](https://github.com/sp1ff/mpdpopm) administrator +//! can define new commands backed by arbitrary command execution server-side. +//! +//! # Commands +//! +//! The following commands are built-in: +//! +//! - set rating: `rate RATING( TRACK)?` +//! - set playcount: `setpc PC( TRACK)?` +//! - set lastplayed: `setlp TIMESTAMP( TRACK)?` +//! +//! There is no need to provide corresponding accessors since this functionality is already provided +//! via "sticker get". Dedicated accessors could provide the same functionality with slightly more +//! convenience since the sticker name would not have to be specified (as with "sticker get") & may +//! be added at a later date. +//! +//! I'm expanding the MPD filter functionality to include attributes tracked by mpdpopm: +//! +//! - findadd replacement: `findadd FILTER [sort TYPE] [window START:END]` +//! (cf. [here](https://www.musicpd.org/doc/html/protocol.html#the-music-database)) +//! +//! - searchadd replacement: `searchadd FILTER [sort TYPE] [window START:END]` +//! (cf. [here](https://www.musicpd.org/doc/html/protocol.html#the-music-database)) +//! +//! Additional commands may be added through the +//! [generalized commands](crate::commands#the-generalized-command-framework) feature. + +use crate::{ + clients::{Client, IdleClient, PlayerStatus}, + filters::ExpressionParser, + filters_ast::{FilterStickerNames, evaluate}, +}; + +use anyhow::{Context, Error, Result, anyhow, bail}; +use boolinator::Boolinator; +use tracing::debug; + +use std::collections::VecDeque; + +/// Break `buf` up into individual tokens while removing MPD-style quoting. +/// +/// When a client sends a command to [mpdpopm](crate), it will look like this on the wire: +/// +/// ```text +/// sendmessage ${CHANNEL} "some-command \"with space\" simple \"'with single' and \\\\\"" +/// ``` +/// +/// In other words, the MPD "sendmessage" command takes two parameters: the channel and the +/// message. The recipient (i.e. us) is responsible for breaking up the message into its constituent +/// parts (a command name & its arguments in our case). +/// +/// The message will perforce be quoted according ot the MPD rules: +/// +/// 1. an un-quoted token may contain any printable ASCII character except space, tab, ' & " +/// +/// 2. to include spaces, tabs, '-s or "-s, the token must be enclosed in "-s, and any "-s or \\-s +/// therein must be backslash escaped +/// +/// When the messages is delivered to us, it has already been un-escaped; i.e. we will see the +/// string: +/// +/// ```text +/// some-command "with space" simple "'with single' and \\" +/// ``` +/// +/// This function will break that string up into individual tokens with one more level +/// of escaping removed; i.e. it will return an iterator that will yield the four tokens: +/// +/// 1. some-command +/// 2. with space +/// 3. simple +/// 4. 'with single' and \\ +/// +/// [MPD](https://github.com/MusicPlayerDaemon/MPD) has a nice +/// [implementation](https://github.com/MusicPlayerDaemon/MPD/blob/master/src/util/Tokenizer.cxx#L170) +/// that modifies the string in place by copying subsequent characters on top of escape characters +/// in the same buffer, inserting nulls in between the resulting tokens,and then working in terms of +/// pointers to the resulting null-terminated strings. +/// +/// Once I realized that I could split slices I saw how to implement an Iterator that do the same +/// thing (an idiomatic interface to the tokenization backed by a zero-copy implementation). I was +/// inspired by [My Favorite Rust Function +/// Signature](<https://www.brandonsmith.ninja/blog/favorite-rust-function>). +/// +/// NB. This method works in terms of a slice of [`u8`] because we can't index into Strings in +/// Rust, and MPD deals only in terms of ASCII at any rate. +pub fn tokenize(buf: &mut [u8]) -> impl Iterator<Item = Result<&[u8]>> { + TokenIterator::new(buf) +} + +struct TokenIterator<'a> { + /// The slice on which we operate; modified in-place as we yield tokens + slice: &'a mut [u8], + /// Index into [`slice`] of the first non-whitespace character + input: usize, +} + +impl<'a> TokenIterator<'a> { + pub fn new(slice: &'a mut [u8]) -> TokenIterator<'a> { + let input = match slice.iter().position(|&x| x > 0x20) { + Some(n) => n, + None => slice.len(), + }; + TokenIterator { slice, input } + } +} + +impl<'a> Iterator for TokenIterator<'a> { + type Item = Result<&'a [u8]>; + + fn next(&mut self) -> Option<Self::Item> { + let nslice = self.slice.len(); + if self.slice.is_empty() || self.input == nslice { + None + } else if '"' == self.slice[self.input] as char { + // This is NextString in MPD: walk self.slice, un-escaping characters, until we find + // a closing ". Note that we un-escape by moving characters forward in the slice. + let mut inp = self.input + 1; + let mut out = self.input; + while self.slice[inp] as char != '"' { + if '\\' == self.slice[inp] as char { + inp += 1; + if inp == nslice { + return Some(Err(anyhow!("Trailing backslash"))); + } + } + self.slice[out] = self.slice[inp]; + out += 1; + inp += 1; + if inp == nslice { + return Some(Err(anyhow!("No closing quote"))); + } + } + // The next token is in self.slice[self.input..out] and self.slice[inp] is " + let tmp = std::mem::take(&mut self.slice); + let (_, tmp) = tmp.split_at_mut(self.input); + let (result, new_slice) = tmp.split_at_mut(out - self.input); + self.slice = new_slice; + // strip any leading whitespace + self.input = inp - out + 1; // +1 to skip the closing " + while self.input < self.slice.len() && self.slice[self.input] as char == ' ' { + self.input += 1; + } + Some(Ok(result)) + } else { + // This is NextUnquoted in MPD; walk self.slice, validating characters until the end + // or the next whitespace + let mut i = self.input; + while i < nslice { + if 0x20 >= self.slice[i] { + break; + } + if self.slice[i] as char == '"' || self.slice[i] as char == '\'' { + return Some(Err(anyhow!("Invalid char: `{}`", self.slice[i]))); + } + i += 1; + } + // The next token is in self.slice[self.input..i] & self.slice[i] is either one- + // past-the end or whitespace. + let tmp = std::mem::take(&mut self.slice); + let (_, tmp) = tmp.split_at_mut(self.input); + let (result, new_slice) = tmp.split_at_mut(i - self.input); + self.slice = new_slice; + // strip any leading whitespace + self.input = match self.slice.iter().position(|&x| x > 0x20) { + Some(n) => n, + None => self.slice.len(), + }; + Some(Ok(result)) + } + } +} + +/// Collective state needed for processing messages, both built-in & generalized +#[derive(Default)] +pub struct MessageProcessor {} + +impl MessageProcessor { + /// Whip up a new instance; other than cloning the iterators, should just hold references in the + /// enclosing scope + pub fn new() -> MessageProcessor { + Self::default() + } + + /// Read messages off the commands channel & dispatch 'em + pub async fn check_messages<'a>( + &self, + client: &mut Client, + idle_client: &mut IdleClient, + state: PlayerStatus, + command_chan: &str, + stickers: &FilterStickerNames<'a>, + ) -> Result<()> { + let m = idle_client + .get_messages() + .await + .context("Failed to `get_messages` from client")?; + + for (chan, msgs) in m { + // Only supporting a single channel, ATM + (chan == command_chan).ok_or_else(|| anyhow!("Unknown chanell: `{}`", chan))?; + for msg in msgs { + self.process(msg, client, &state, stickers).await?; + } + } + + Ok(()) + } + + /// Process a single command + pub async fn process<'a>( + &self, + msg: String, + client: &mut Client, + state: &PlayerStatus, + stickers: &FilterStickerNames<'a>, + ) -> Result<()> { + if let Some(stripped) = msg.strip_prefix("findadd ") { + self.findadd(stripped.to_string(), client, stickers, state) + .await + } else if let Some(stripped) = msg.strip_prefix("searchadd ") { + self.searchadd(stripped.to_string(), client, stickers, state) + .await + } else { + unreachable!("Unkonwn command") + } + } + + /// Handle `findadd': "FILTER [sort TYPE] [window START:END]" + async fn findadd<'a>( + &self, + msg: String, + client: &mut Client, + stickers: &FilterStickerNames<'a>, + _state: &PlayerStatus, + ) -> Result<()> { + let mut buf = msg.into_bytes(); + let args: VecDeque<&str> = tokenize(&mut buf) + .map(|r| match r { + Ok(buf) => Ok(std::str::from_utf8(buf) + .context("Failed to interpete `findadd` string as utf8")?), + Err(err) => Err(err), + }) + .collect::<Result<VecDeque<&str>>>()?; + + debug!("findadd arguments: {:#?}", args); + + // there should be 1, 3 or 5 arguments. `sort' & `window' are not supported, yet. + + // ExpressionParser's not terribly ergonomic: it returns a ParesError<L, T, E>; T is the + // offending token, which has the same lifetime as our input, which makes it tough to + // capture. Nor is there a convenient way in which to treat all variants other than the + // Error Trait. + let ast = match ExpressionParser::new().parse(args[0]) { + Ok(ast) => ast, + Err(err) => { + bail!("Failed to parse filter: `{}`", err) + } + }; + + debug!("ast: {:#?}", ast); + + let mut results = Vec::new(); + for song in evaluate(&ast, true, client, stickers) + .await + .context("Failed to evaluate filter")? + { + results.push(client.add(&song).await); + } + match results + .into_iter() + .collect::<std::result::Result<Vec<()>, Error>>() + { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } + + /// Handle `searchadd': "FILTER [sort TYPE] [window START:END]" + async fn searchadd<'a>( + &self, + msg: String, + client: &mut Client, + stickers: &FilterStickerNames<'a>, + _state: &PlayerStatus, + ) -> Result<()> { + // Tokenize the message + let mut buf = msg.into_bytes(); + let args: VecDeque<&str> = tokenize(&mut buf) + .map(|r| match r { + Ok(buf) => Ok(std::str::from_utf8(buf) + .context("Failed to interpete `searchadd` string as utf8")?), + Err(err) => Err(err), + }) + .collect::<Result<VecDeque<_>>>()?; + + debug!("searchadd arguments: {:#?}", args); + + // there should be 1, 3 or 5 arguments. `sort' & `window' are not supported, yet. + + // ExpressionParser's not terribly ergonomic: it returns a ParesError<L, T, E>; T is the + // offending token, which has the same lifetime as our input, which makes it tough to + // capture. Nor is there a convenient way in which to treat all variants other than the + // Error Trait. + let ast = match ExpressionParser::new().parse(args[0]) { + Ok(ast) => ast, + Err(err) => { + bail!("Failed to parse filter: `{err}`") + } + }; + + debug!("ast: {:#?}", ast); + + let mut results = Vec::new(); + for song in evaluate(&ast, false, client, stickers) + .await + .context("Failed to evaluate ast")? + { + results.push(client.add(&song).await); + } + match results + .into_iter() + .collect::<std::result::Result<Vec<()>, Error>>() + { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } +} + +#[cfg(test)] +mod tokenize_tests { + use super::Result; + use super::tokenize; + + #[test] + fn tokenize_smoke() { + let mut buf1 = String::from("some-command").into_bytes(); + let x1: Vec<&[u8]> = tokenize(&mut buf1).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x1[0], b"some-command"); + + let mut buf2 = String::from("a b").into_bytes(); + let x2: Vec<&[u8]> = tokenize(&mut buf2).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x2[0], b"a"); + assert_eq!(x2[1], b"b"); + + let mut buf3 = String::from("a \"b c\"").into_bytes(); + let x3: Vec<&[u8]> = tokenize(&mut buf3).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x3[0], b"a"); + assert_eq!(x3[1], b"b c"); + + let mut buf4 = String::from("a \"b c\" d").into_bytes(); + let x4: Vec<&[u8]> = tokenize(&mut buf4).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x4[0], b"a"); + assert_eq!(x4[1], b"b c"); + assert_eq!(x4[2], b"d"); + + let mut buf5 = String::from("simple-command \"with space\" \"with '\"").into_bytes(); + let x5: Vec<&[u8]> = tokenize(&mut buf5).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x5[0], b"simple-command"); + assert_eq!(x5[1], b"with space"); + assert_eq!(x5[2], b"with '"); + + let mut buf6 = String::from("cmd \"with\\\\slash and space\"").into_bytes(); + let x6: Vec<&[u8]> = tokenize(&mut buf6).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x6[0], b"cmd"); + assert_eq!(x6[1], b"with\\slash and space"); + + let mut buf7 = String::from(" cmd \"with\\\\slash and space\" ").into_bytes(); + let x7: Vec<&[u8]> = tokenize(&mut buf7).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(x7[0], b"cmd"); + assert_eq!(x7[1], b"with\\slash and space"); + } + + #[test] + fn tokenize_filter() { + let mut buf1 = String::from(r#""(artist =~ \"foo\\\\bar\\\"\")""#).into_bytes(); + let x1: Vec<&[u8]> = tokenize(&mut buf1).collect::<Result<Vec<&[u8]>>>().unwrap(); + assert_eq!(1, x1.len()); + eprintln!("x1[0] is ``{}''", std::str::from_utf8(x1[0]).unwrap()); + assert_eq!( + std::str::from_utf8(x1[0]).unwrap(), + r#"(artist =~ "foo\\bar\"")"# + ); + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/playcounts.rs b/pkgs/by-name/mp/mpdpopm/src/playcounts.rs new file mode 100644 index 00000000..7d646b4c --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/playcounts.rs @@ -0,0 +1,313 @@ +// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com> +// +// This file is part of mpdpopm. +// +// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along with mpdpopm. If not, +// see <http://www.gnu.org/licenses/>. + +//! playcounts -- managing play counts & lastplayed times +//! +//! # Introduction +//! +//! Play counts & last played timestamps are maintained so long as [PlayState::update] is called +//! regularly (every few seconds, say). For purposes of library maintenance, however, they can be +//! set explicitly: +//! +//! - `setpc PLAYCOUNT( TRACK)?` +//! - `setlp LASTPLAYED( TRACK)?` +//! + +use crate::clients::{Client, PlayerStatus}; +use crate::storage::{last_played, play_count, skipped}; + +use anyhow::{Context, Error, Result, anyhow}; +use tracing::{debug, info}; + +use std::time::SystemTime; + +/// Current server state in terms of the play status (stopped/paused/playing, current track, elapsed +/// time in current track, &c) +#[derive(Debug)] +pub struct PlayState { + /// Last known server status + last_server_stat: PlayerStatus, + + /// true if we have already incremented the last known track's playcount + have_incr_play_count: bool, + + /// Percentage threshold, expressed as a number between zero & one, for considering a song to + /// have been played + played_thresh: f64, + last_song_was_skipped: bool, +} + +impl PlayState { + /// Create a new PlayState instance; async because it will reach out to the mpd server + /// to get current status. + pub async fn new( + client: &mut Client, + played_thresh: f64, + ) -> std::result::Result<PlayState, Error> { + Ok(PlayState { + last_server_stat: client.status().await?, + have_incr_play_count: false, + last_song_was_skipped: false, + played_thresh, + }) + } + + /// Retrieve a copy of the last known player status + pub fn last_status(&self) -> PlayerStatus { + self.last_server_stat.clone() + } + + /// Poll the server-- update our status; maybe increment the current track's play count; the + /// caller must arrange to have this method invoked periodically to keep our state fresh + pub async fn update(&mut self, client: &mut Client) -> Result<()> { + let new_stat = client + .status() + .await + .context("Failed to get client status")?; + + match (&self.last_server_stat, &new_stat) { + (PlayerStatus::Play(last), PlayerStatus::Play(curr)) + | (PlayerStatus::Pause(last), PlayerStatus::Play(curr)) + | (PlayerStatus::Play(last), PlayerStatus::Pause(curr)) + | (PlayerStatus::Pause(last), PlayerStatus::Pause(curr)) => { + // Last we knew, we were playing, and we're playing now. + if last.songid != curr.songid { + debug!("New songid-- resetting PC incremented flag."); + + if !self.have_incr_play_count { + // We didn't mark the previous song as played. + // As such, the user must have skipped it :( + self.last_song_was_skipped = true; + } + + self.have_incr_play_count = false; + } else if last.elapsed > curr.elapsed + && self.have_incr_play_count + && curr.elapsed / curr.duration <= 0.1 + { + debug!("Re-play-- resetting PC incremented flag."); + self.have_incr_play_count = false; + } + } + (PlayerStatus::Stopped, PlayerStatus::Play(_)) + | (PlayerStatus::Stopped, PlayerStatus::Pause(_)) + | (PlayerStatus::Pause(_), PlayerStatus::Stopped) + | (PlayerStatus::Play(_), PlayerStatus::Stopped) => { + self.have_incr_play_count = false; + } + (PlayerStatus::Stopped, PlayerStatus::Stopped) => (), + } + + match &new_stat { + PlayerStatus::Play(curr) => { + let pct = curr.played_pct(); + debug!("Updating status: {:.3}% complete.", 100.0 * pct); + if !self.have_incr_play_count && pct >= self.played_thresh { + info!( + "Increment play count for '{}' (songid: {}) at {} played.", + curr.file.display(), + curr.songid, + curr.elapsed / curr.duration + ); + + let file = curr.file.to_str().ok_or_else(|| { + anyhow!("Failed to parse path as utf8: `{}`", curr.file.display()) + })?; + + let curr_pc = play_count::get(client, file).await?.unwrap_or_default(); + + debug!("Current PC is {}.", curr_pc); + + last_played::set( + client, + file, + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .context("Failed to get system time")? + .as_secs(), + ) + .await?; + self.have_incr_play_count = true; + + play_count::set(client, file, curr_pc + 1).await?; + } else if self.last_song_was_skipped { + self.last_song_was_skipped = false; + let last = self + .last_server_stat + .current_song() + .expect("To exist, as it was skipped"); + + info!( + "Marking '{}' (songid: {}) as skipped at {}.", + last.file.display(), + last.songid, + last.elapsed / last.duration + ); + + let file = last.file.to_str().ok_or_else(|| { + anyhow!("Failed to parse path as utf8: `{}`", last.file.display()) + })?; + + let skip_count = skipped::get(client, file).await?.unwrap_or_default(); + skipped::set(client, file, skip_count + 1).await?; + } + } + PlayerStatus::Pause(_) | PlayerStatus::Stopped => (), + }; + + self.last_server_stat = new_stat; + Ok(()) // No need to update the DB + } +} + +#[cfg(test)] +mod player_state_tests { + use super::*; + use crate::clients::test_mock::Mock; + + /// "Smoke" tests for player state + #[tokio::test] + async fn player_state_smoke() { + let mock = Box::new(Mock::new(&[ + ( + "status", + "repeat: 0 +random: 1 +single: 0 +consume: 1 +playlist: 2 +playlistlength: 66 +mixrampdb: 0.000000 +state: stop +xfade: 5 +song: 51 +songid: 52 +nextsong: 11 +nextsongid: 12 +OK +", + ), + ( + "status", + "volume: 100 +repeat: 0 +random: 1 +single: 0 +consume: 1 +playlist: 2 +playlistlength: 66 +mixrampdb: 0.000000 +state: play +xfade: 5 +song: 51 +songid: 52 +time: 5:228 +elapsed: 5.337 +bitrate: 192 +duration: 227.637 +audio: 44100:24:2 +nextsong: 11 +nextsongid: 12 +OK +", + ), + ( + "playlistid 52", + "file: E/Enya - Wild Child.mp3 +Last-Modified: 2008-11-09T00:06:30Z +Artist: Enya +Title: Wild Child +Album: A Day Without Rain (Japanese Retail) +Date: 2000 +Genre: Celtic +Time: 228 +duration: 227.637 +Pos: 51 +Id: 52 +OK +", + ), + ( + "status", + "volume: 100 +repeat: 0 +random: 1 +single: 0 +consume: 1 +playlist: 2 +playlistlength: 66 +mixrampdb: 0.000000 +state: play +xfade: 5 +song: 51 +songid: 52 +time: 5:228 +elapsed: 200 +bitrate: 192 +duration: 227.637 +audio: 44100:24:2 +nextsong: 11 +nextsongid: 12 +OK +", + ), + ( + "playlistid 52", + "file: E/Enya - Wild Child.mp3 +Last-Modified: 2008-11-09T00:06:30Z +Artist: Enya +Title: Wild Child +Album: A Day Without Rain (Japanese Retail) +Date: 2000 +Genre: Celtic +Time: 228 +duration: 227.637 +Pos: 51 +Id: 52 +OK +", + ), + ( + "sticker get song \"E/Enya - Wild Child.mp3\" unwoundstack.com:playcount", + "sticker: unwoundstack.com:playcount=11\nOK\n", + ), + ( + &format!( + "sticker set song \"E/Enya - Wild Child.mp3\" unwoundstack.com:lastplayed {}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ), + "OK\n", + ), + ( + "sticker set song \"E/Enya - Wild Child.mp3\" unwoundstack.com:playcount 12", + "OK\n", + ), + ])); + + let mut cli = Client::new(mock).unwrap(); + let mut ps = PlayState::new(&mut cli, 0.6).await.unwrap(); + let check = match ps.last_status() { + PlayerStatus::Play(_) | PlayerStatus::Pause(_) => false, + PlayerStatus::Stopped => true, + }; + assert!(check); + + ps.update(&mut cli).await.unwrap(); + ps.update(&mut cli).await.unwrap() + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs b/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs new file mode 100644 index 00000000..24d8dcb5 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs @@ -0,0 +1,145 @@ +use anyhow::{Error, Result}; + +pub mod play_count { + use anyhow::Context; + + use crate::clients::Client; + + use super::Result; + + pub const STICKER: &str = "unwoundstack.com:playcount"; + + /// Retrieve the play count for a track + pub async fn get(client: &mut Client, file: &str) -> Result<Option<usize>> { + match client + .get_sticker::<usize>(file, STICKER) + .await + .context("Failed to get sticker from client")? + { + Some(n) => Ok(Some(n)), + None => Ok(None), + } + } + + /// Set the play count for a track-- this will run the associated command, if any + pub async fn set(client: &mut Client, file: &str, play_count: usize) -> Result<()> { + client + .set_sticker(file, STICKER, &format!("{}", play_count)) + .await + .context("Failed to set_sticker on client")?; + + Ok(()) + } + + #[cfg(test)] + mod pc_lp_tests { + use super::*; + use crate::{clients::test_mock::Mock, storage::play_count}; + + /// "Smoke" tests for play counts & last played times + #[tokio::test] + async fn pc_smoke() { + let mock = Box::new(Mock::new(&[ + ( + "sticker get song a unwoundstack.com:playcount", + "sticker: unwoundstack.com:playcount=11\nOK\n", + ), + ( + "sticker get song a unwoundstack.com:playcount", + "ACK [50@0] {sticker} no such sticker\n", + ), + ("sticker get song a unwoundstack.com:playcount", "splat!"), + ])); + let mut cli = Client::new(mock).unwrap(); + + assert_eq!(play_count::get(&mut cli, "a").await.unwrap().unwrap(), 11); + let val = play_count::get(&mut cli, "a").await.unwrap(); + assert!(val.is_none()); + play_count::get(&mut cli, "a").await.unwrap_err(); + } + } +} + +pub mod skipped { + use anyhow::Context; + + use crate::clients::Client; + + use super::Result; + + const STICKER: &str = "unwoundstack.com:skipped_count"; + + /// Retrieve the skip count for a track + pub async fn get(client: &mut Client, file: &str) -> Result<Option<usize>> { + match client + .get_sticker::<usize>(file, STICKER) + .await + .context("Failed to get_sticker on client")? + { + Some(n) => Ok(Some(n)), + None => Ok(None), + } + } + + /// Set the skip count for a track + pub async fn set(client: &mut Client, file: &str, skip_count: usize) -> Result<()> { + client + .set_sticker(file, STICKER, &format!("{}", skip_count)) + .await + .context("Failed to set_sticker on client") + } +} + +pub mod last_played { + use anyhow::Context; + + use crate::clients::Client; + + use super::Result; + + pub const STICKER: &str = "unwoundstack.com:lastplayed"; + + /// Retrieve the last played timestamp for a track (seconds since Unix epoch) + pub async fn get(client: &mut Client, file: &str) -> Result<Option<u64>> { + client + .get_sticker::<u64>(file, STICKER) + .await + .context("Falied to get_sticker on client") + } + + /// Set the last played for a track + pub async fn set(client: &mut Client, file: &str, last_played: u64) -> Result<()> { + client + .set_sticker(file, STICKER, &format!("{}", last_played)) + .await + .context("Failed to set_sticker on client")?; + Ok(()) + } +} + +pub mod rating_count { + use anyhow::Context; + + use crate::clients::Client; + + use super::Result; + + pub const STICKER: &str = "unwoundstack.com:ratings_count"; + + /// Retrieve the rating count for a track + pub async fn get(client: &mut Client, file: &str) -> Result<Option<i8>> { + client + .get_sticker::<i8>(file, STICKER) + .await + .context("Failed to get_sticker on client") + } + + /// Set the rating count for a track + pub async fn set(client: &mut Client, file: &str, rating_count: i8) -> Result<()> { + client + .set_sticker(file, STICKER, &format!("{}", rating_count)) + .await + .context("Failed to set_sticker on client")?; + Ok(()) + } +} diff --git a/pkgs/by-name/mp/mpdpopm/src/vars.rs b/pkgs/by-name/mp/mpdpopm/src/vars.rs new file mode 100644 index 00000000..7cacec66 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/vars.rs @@ -0,0 +1,4 @@ +pub static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub static AUTHOR: &str = env!("CARGO_PKG_AUTHORS"); +pub static LOCALSTATEDIR: &str = "/home/soispha/.local/state"; +pub static PREFIX: &str = "/home/soispha/.local/share/mpdpopm"; diff --git a/pkgs/by-name/mp/mpdpopm/update.sh b/pkgs/by-name/mp/mpdpopm/update.sh new file mode 100755 index 00000000..cf04b837 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/update.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +# Mpdpopm - A mpd rating tracker +# +# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com> +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This file is part of Mpdpopm. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/agpl.txt>. + +nix flake update + +[ "$1" = "upgrade" ] && cargo upgrade +cargo update diff --git a/pkgs/by-name/mp/mpp-beetrm/mpp-beetrm.sh b/pkgs/by-name/mp/mpp-beetrm/mpp-beetrm.sh index 3209503c..83784743 100755 --- a/pkgs/by-name/mp/mpp-beetrm/mpp-beetrm.sh +++ b/pkgs/by-name/mp/mpp-beetrm/mpp-beetrm.sh @@ -1,7 +1,14 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. beet remove --delete \ title:"$(mpc --format '%title%' current)" \ diff --git a/pkgs/by-name/mp/mpp-beetrm/package.nix b/pkgs/by-name/mp/mpp-beetrm/package.nix index 24b56606..00672838 100644 --- a/pkgs/by-name/mp/mpp-beetrm/package.nix +++ b/pkgs/by-name/mp/mpp-beetrm/package.nix @@ -1,15 +1,24 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies mpc, beets, }: -sysLib.writeShellScript { +writeShellApplication { name = "mpp-beetrm"; - src = ./mpp-beetrm.sh; - generateCompletions = false; - keepPath = false; + text = builtins.readFile ./mpp-beetrm.sh; + inheritPath = false; - dependencies = [ + runtimeInputs = [ mpc beets ]; diff --git a/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh b/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh index 004c67c7..fa1cac49 100755 --- a/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh +++ b/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh @@ -1,11 +1,21 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. -( - cd "%MPD_MUSIC_DIR" || die "No music dir!" - exiftool "$(mpc --format '%file%' current)" -json | jq '.[0].Lyrics' -r | less -) +die() { + echo "Error: $1" + exit 1 +} + +cd "$XDG_MUSIC_DIR/beets" || die "No music dir!" +exiftool "$(mpc --format '%file%' current)" -json | jq '.[0].Lyrics' --raw-output | less # vim: ft=sh diff --git a/pkgs/by-name/mp/mpp-lyrics/package.nix b/pkgs/by-name/mp/mpp-lyrics/package.nix index 76b590c7..0b6d8993 100644 --- a/pkgs/by-name/mp/mpp-lyrics/package.nix +++ b/pkgs/by-name/mp/mpp-lyrics/package.nix @@ -1,23 +1,27 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies exiftool, mpc, jq, less, locale, # dependency of less - mpd_music_dir ? "\${XDG_MUSIC_DIR}", }: -sysLib.writeShellScript { +writeShellApplication { name = "mpp-lyrics"; - src = ./mpp-lyrics.sh; - generateCompletions = false; - keepPath = false; + text = builtins.readFile ./mpp-lyrics.sh; + inheritPath = false; - replacementStrings = { - MPD_MUSIC_DIR = mpd_music_dir; - }; - - dependencies = [ + runtimeInputs = [ exiftool mpc jq diff --git a/pkgs/by-name/mp/mpp-searchadd/mpp-searchadd.sh b/pkgs/by-name/mp/mpp-searchadd/mpp-searchadd.sh index 3fe9a6b6..d76e73b8 100755 --- a/pkgs/by-name/mp/mpp-searchadd/mpp-searchadd.sh +++ b/pkgs/by-name/mp/mpp-searchadd/mpp-searchadd.sh @@ -1,14 +1,18 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. -tracks="$(mktmp)" -beet list "$@" --path >"$tracks" - -while read -r track; do +beet list "$@" --path | while read -r track; do mpc add "$track" -done <"$tracks" +done mpc playlist # vim: ft=sh diff --git a/pkgs/by-name/mp/mpp-searchadd/package.nix b/pkgs/by-name/mp/mpp-searchadd/package.nix index a98472d1..91ff05e9 100644 --- a/pkgs/by-name/mp/mpp-searchadd/package.nix +++ b/pkgs/by-name/mp/mpp-searchadd/package.nix @@ -1,15 +1,24 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies. mpc, beets, }: -sysLib.writeShellScript { +writeShellApplication { name = "mpp-searchadd"; - src = ./mpp-searchadd.sh; - generateCompletions = false; - keepPath = false; + text = builtins.readFile ./mpp-searchadd.sh; + inheritPath = false; - dependencies = [ + runtimeInputs = [ mpc beets ]; diff --git a/pkgs/by-name/mp/mpp/mpp.sh b/pkgs/by-name/mp/mpp/mpp.sh index 538a56ee..e52bfcdc 100755 --- a/pkgs/by-name/mp/mpp/mpp.sh +++ b/pkgs/by-name/mp/mpp/mpp.sh @@ -1,9 +1,16 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. -case "$1" in +case "${1-}" in "searchadd") shift 1 mpp-searchadd "$@" @@ -16,6 +23,10 @@ case "$1" in shift 1 mpp-beetrm "$@" ;; +"popm") + shift 1 + mpdpopm "$@" + ;; *) mpc "$@" ;; diff --git a/pkgs/by-name/mp/mpp/package.nix b/pkgs/by-name/mp/mpp/package.nix index 9c5315b0..fe08ba46 100644 --- a/pkgs/by-name/mp/mpp/package.nix +++ b/pkgs/by-name/mp/mpp/package.nix @@ -1,20 +1,37 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, - mpc, - fd, + writeShellApplication, symlinkJoin, stdenv, + # Dependencies + mpc, + mpp-searchadd, + mpp-lyrics, + mpp-beetrm, + mpdpopm, + # Build dependencies + fd, zsh, }: let - script = sysLib.writeShellScript { + script = writeShellApplication { name = "mpp"; - src = ./mpp.sh; - generateCompletions = false; - # We source the wrappers from the environment, to ensure that they have the same - # configurations (e.g. MPD_MUSIC_DIR in `mpc-lyrics`) - keepPath = true; - dependencies = [ + text = builtins.readFile ./mpp.sh; + inheritPath = false; + + runtimeInputs = [ mpc + mpp-searchadd + mpp-lyrics + mpp-beetrm + mpdpopm ]; }; @@ -55,6 +72,8 @@ fd "." --hidden --type file | while read -r file_path; do sed --in-place 's/mpc/mpp/g' "$file_path" done + + # TODO(@bpeetz): Also change this in man-pages. <2025-05-20> ''; installPhase = '' @@ -64,6 +83,8 @@ }; in symlinkJoin { - name = "mpp-merged"; + name = "mpp"; paths = [script mpcShare]; + + inherit (script) meta; } diff --git a/pkgs/by-name/no/notify-run/.envrc b/pkgs/by-name/no/notify-run/.envrc new file mode 100644 index 00000000..880b1809 --- /dev/null +++ b/pkgs/by-name/no/notify-run/.envrc @@ -0,0 +1,22 @@ +#!/usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use flake || use nix +watch_file flake.nix + +PATH_add ./target/debug +PATH_add ./target/release +PATH_add ./scripts + +if on_git_branch; then + echo && git status --short --branch +fi diff --git a/pkgs/by-name/no/notify-run/.gitignore b/pkgs/by-name/no/notify-run/.gitignore new file mode 100644 index 00000000..8f29eabf --- /dev/null +++ b/pkgs/by-name/no/notify-run/.gitignore @@ -0,0 +1,16 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +# build +/target +/result + +# dev env +.direnv diff --git a/pkgs/by-name/no/notify-run/Cargo.lock b/pkgs/by-name/no/notify-run/Cargo.lock new file mode 100644 index 00000000..1e065d25 --- /dev/null +++ b/pkgs/by-name/no/notify-run/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "notify-run" +version = "0.1.0" +dependencies = [ + "anyhow", +] diff --git a/pkgs/by-name/no/notify-run/Cargo.toml b/pkgs/by-name/no/notify-run/Cargo.toml new file mode 100644 index 00000000..c4b9a659 --- /dev/null +++ b/pkgs/by-name/no/notify-run/Cargo.toml @@ -0,0 +1,20 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +[package] +name = "notify-run" +description = "An safe way to run applications that might fail" +version = "0.1.0" +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.100" diff --git a/pkgs/by-name/no/notify-run/flake.lock b/pkgs/by-name/no/notify-run/flake.lock new file mode 100644 index 00000000..1e997998 --- /dev/null +++ b/pkgs/by-name/no/notify-run/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1768661221, + "narHash": "sha256-MJwOjrIISfOpdI9x4C+5WFQXvHtOuj5mqLZ4TMEtk1M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3327b113f2ef698d380df83fbccefad7e83d7769", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/pkgs/by-name/no/notify-run/flake.lock.license b/pkgs/by-name/no/notify-run/flake.lock.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/no/notify-run/flake.lock.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/no/notify-run/flake.nix b/pkgs/by-name/no/notify-run/flake.nix new file mode 100644 index 00000000..07be3258 --- /dev/null +++ b/pkgs/by-name/no/notify-run/flake.nix @@ -0,0 +1,34 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + description = "An safe way to run applications, that might fail"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = {nixpkgs, ...}: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages."${system}"; + in { + devShells."${system}".default = pkgs.mkShell { + packages = [ + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt + + pkgs.cargo-edit + ]; + }; + }; +} +# vim: ts=2 + diff --git a/pkgs/by-name/no/notify-run/package.nix b/pkgs/by-name/no/notify-run/package.nix new file mode 100644 index 00000000..1f9337be --- /dev/null +++ b/pkgs/by-name/no/notify-run/package.nix @@ -0,0 +1,41 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + rustPlatform, + lib, + libnotify, + makeWrapper, +}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "notify-run"; + version = "0.1.0"; + + buildInputs = [ + libnotify + ]; + nativeBuildInputs = [ + makeWrapper + ]; + + src = ./.; + cargoLock = { + lockFile = ./Cargo.lock; + }; + + postInstall = '' + # NOTE: We cannot clear the path, because we need access to the programs to start. <2025-12-03> + wrapProgram $out/bin/notify-run \ + --prefix PATH : ${lib.makeBinPath finalAttrs.buildInputs} + ''; + + meta = { + mainProgram = "notify-run"; + }; +}) diff --git a/pkgs/by-name/no/notify-run/src/main.rs b/pkgs/by-name/no/notify-run/src/main.rs new file mode 100644 index 00000000..a6a0165a --- /dev/null +++ b/pkgs/by-name/no/notify-run/src/main.rs @@ -0,0 +1,65 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{env::args, path::PathBuf, process::Command}; + +use anyhow::{Context, Result}; + +fn main() -> Result<()> { + let args = args().skip(1).collect::<Vec<_>>(); + + let mut cmd = Command::new(&args[0]); + if let Some(arguments) = args.get(1) { + cmd.args(arguments.split(" ").collect::<Vec<_>>().as_slice()); + } + + eprintln!("Spawning {:?}", cmd); + + let output = cmd + .output() + .with_context(|| format!("Failed to spawn and await output of {:?}", cmd))?; + + if !output.status.success() { + let mut notify_send = Command::new("notify-send"); + notify_send.args([ + format!("Command {:?} failed", cmd).as_str(), + &String::from_utf8_lossy(output.stderr.as_slice()), + ]); + + notify_send.status().with_context(|| { + format!( + "Failed to run `notify-send` to tell about failed command ({:?}).", + cmd + ) + })?; + } else { + let name = PathBuf::from(&args[0]) + .file_name() + .expect("this to be a command, and thus have a file_name") + .to_string_lossy() + .to_string(); + + print!("{}", append_name(&name, &output.stdout)); + eprint!("{}", append_name(&name, &output.stderr)); + } + + Ok(()) +} + +fn append_name(name: &str, base: &[u8]) -> String { + let base = String::from_utf8_lossy(base).to_string(); + + let mut output = String::new(); + for line in base.lines() { + output.push_str(format!("{name}> {line}\n").as_str()); + } + + output +} diff --git a/pkgs/by-name/no/notify-run/update.sh b/pkgs/by-name/no/notify-run/update.sh new file mode 100755 index 00000000..23d90a86 --- /dev/null +++ b/pkgs/by-name/no/notify-run/update.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +[ "$1" = "upgrade" ] && cargo upgrade +cargo update diff --git a/pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch b/pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch new file mode 100644 index 00000000..fa2e2482 --- /dev/null +++ b/pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch @@ -0,0 +1,54 @@ +From 8a0aa0e244fa565b8c55aab38cc5e84323c3b481 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Tue, 3 Jun 2025 12:43:44 +0200 +Subject: [PATCH] fix(standardpaths): Continue to work with xdg, while + `--basedir` is set + +This can be used to simulate firefox's profiles feature (i.e., completely separated +data/runtime/cache dirs), while still keeping to the xdg basedir standard. +--- + qutebrowser/utils/standarddir.py | 19 +++++++++++-------- + 1 file changed, 11 insertions(+), 8 deletions(-) + +diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py +index b82845a96..61319daed 100644 +--- a/qutebrowser/utils/standarddir.py ++++ b/qutebrowser/utils/standarddir.py +@@ -285,12 +285,11 @@ def _from_args( + The overridden path, or None if there is no override. + """ + basedir_suffix = { +- QStandardPaths.StandardLocation.ConfigLocation: 'config', +- QStandardPaths.StandardLocation.AppDataLocation: 'data', +- QStandardPaths.StandardLocation.AppLocalDataLocation: 'data', +- QStandardPaths.StandardLocation.CacheLocation: 'cache', +- QStandardPaths.StandardLocation.DownloadLocation: 'download', +- QStandardPaths.StandardLocation.RuntimeLocation: 'runtime', ++ QStandardPaths.StandardLocation.ConfigLocation: ('config', False), ++ QStandardPaths.StandardLocation.AppDataLocation: ('data', False), ++ QStandardPaths.StandardLocation.AppLocalDataLocation: ('data', False), ++ QStandardPaths.StandardLocation.CacheLocation: ('cache', True), ++ QStandardPaths.StandardLocation.RuntimeLocation: ('runtime', True), + } + + if getattr(args, 'basedir', None) is None: +@@ -298,10 +297,14 @@ def _from_args( + assert args is not None + + try: +- suffix = basedir_suffix[typ] ++ (suffix, extend) = basedir_suffix[typ] + except KeyError: # pragma: no cover + return None +- return os.path.abspath(os.path.join(args.basedir, suffix)) ++ ++ if extend: ++ return os.path.abspath(os.path.join(_writable_location(typ), os.path.basename(args.basedir))) ++ else: ++ return os.path.abspath(os.path.join(args.basedir, suffix)) + + + def _create(path: str) -> None: +-- +2.49.0 + diff --git a/pkgs/by-name/qu/qutebrowser-patched/package.nix b/pkgs/by-name/qu/qutebrowser-patched/package.nix new file mode 100644 index 00000000..1f2ea889 --- /dev/null +++ b/pkgs/by-name/qu/qutebrowser-patched/package.nix @@ -0,0 +1,6 @@ +{qutebrowser}: +qutebrowser.overrideAttrs (final: prev: { + pname = "${prev.pname}-patched"; + + patches = (prev.patches or []) ++ [./0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch]; +}) diff --git a/pkgs/by-name/ri/river-mk-keymap/.envrc b/pkgs/by-name/ri/river-mk-keymap/.envrc index fdd3e9d8..294de504 100644 --- a/pkgs/by-name/ri/river-mk-keymap/.envrc +++ b/pkgs/by-name/ri/river-mk-keymap/.envrc @@ -1,3 +1,13 @@ #!/usr/bin/env sh +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use flake diff --git a/pkgs/by-name/ri/river-mk-keymap/.gitignore b/pkgs/by-name/ri/river-mk-keymap/.gitignore index 2d5df85d..f255eebd 100644 --- a/pkgs/by-name/ri/river-mk-keymap/.gitignore +++ b/pkgs/by-name/ri/river-mk-keymap/.gitignore @@ -1,2 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + /target .direnv diff --git a/pkgs/by-name/ri/river-mk-keymap/Cargo.lock b/pkgs/by-name/ri/river-mk-keymap/Cargo.lock index 7fb009ef..ef1ffbf7 100644 --- a/pkgs/by-name/ri/river-mk-keymap/Cargo.lock +++ b/pkgs/by-name/ri/river-mk-keymap/Cargo.lock @@ -1,12 +1,37 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. version = 4 [[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -19,50 +44,90 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.34" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -70,9 +135,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -82,9 +147,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -94,15 +159,211 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "font-kit" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7e611d49285d4c4b2e1727b72cf05353558885cc5252f93707b845dfcaf3d3" +dependencies = [ + "bitflags 2.10.0", + "byteorder", + "core-foundation", + "core-graphics", + "core-text", + "dirs", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] [[package]] name = "heck" @@ -112,31 +373,84 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "keymaps" -version = "1.0.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cec33e805ecc09c4e4f91ca26e536978ad1ae28f2e1dc02fadafeec6d2f8504" +checksum = "ea59e8e461942cf1d6a7ad938848d6fd2e40eb43799c21192c09226ecc86710f" dependencies = [ "serde", "thiserror", ] [[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] [[package]] name = "once_cell" @@ -145,54 +459,171 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] name = "quote" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] name = "river-mk-keymap" version = "0.1.0" dependencies = [ + "ab_glyph", "anyhow", "clap", + "font-kit", "keymaps", + "memmap2", + "rustix", "serde", "serde_json", + "shlex", + "thiserror", + "vte", + "wayland-client", + "wayland-protocols-wlr", + "wayland-scanner", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", ] [[package]] -name = "ryu" -version = "1.0.20" +name = "rustix" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -201,17 +632,30 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -219,9 +663,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -230,18 +674,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -249,10 +693,16 @@ dependencies = [ ] [[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "utf8parse" @@ -261,74 +711,169 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "windows-sys" -version = "0.59.0" +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wayland-backend" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +dependencies = [ + "bitflags 2.10.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ - "windows-targets", + "pkg-config", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "wio" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "yeslogic-fontconfig-sys" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "zmij" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" diff --git a/pkgs/by-name/ri/river-mk-keymap/Cargo.toml b/pkgs/by-name/ri/river-mk-keymap/Cargo.toml index f14411f2..31247cd7 100644 --- a/pkgs/by-name/ri/river-mk-keymap/Cargo.toml +++ b/pkgs/by-name/ri/river-mk-keymap/Cargo.toml @@ -1,3 +1,13 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + [package] name = "river-mk-keymap" version = "0.1.0" @@ -6,11 +16,21 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.97" -clap = { version = "4.5.34", features = ["derive"] } -keymaps = { version = "1.0.0", features = ["serde", "mouse-keys"] } -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" +ab_glyph = "0.2.32" +anyhow = "1.0.100" +clap = { version = "4.5.54", features = ["derive"] } +font-kit = "0.14.3" +keymaps = { version = "1.2.0", features = ["serde", "mouse-keys", "modifier-keys"] } +memmap2 = "0.9.9" +rustix = { version = "1.1.3", features = ["fs", "shm"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +shlex = "1.3.0" +thiserror = "2.0.17" +vte = "0.15.0" +wayland-client = {version = "0.31.12", default-features = false} +wayland-protocols-wlr = { version = "0.3.10", features = ["client"] } +wayland-scanner = {version = "0.31.8", default-features = false} [profile.release] lto = true diff --git a/pkgs/by-name/ri/river-mk-keymap/TODO b/pkgs/by-name/ri/river-mk-keymap/TODO deleted file mode 100644 index be77953e..00000000 --- a/pkgs/by-name/ri/river-mk-keymap/TODO +++ /dev/null @@ -1 +0,0 @@ -Look at https://github.com/stefur/flow for river wayland inclusion diff --git a/pkgs/by-name/ri/river-mk-keymap/contrib/example.json b/pkgs/by-name/ri/river-mk-keymap/contrib/example.json index c8673f9a..bddd61c0 100644 --- a/pkgs/by-name/ri/river-mk-keymap/contrib/example.json +++ b/pkgs/by-name/ri/river-mk-keymap/contrib/example.json @@ -1,5 +1,8 @@ { - "<M-a>": { - "command": ["focus-view", "next"] + "Kbad": { + "command": ["spawn", "/nix/store/1xfyw9c5ala73y8sayrsf98vcrr3jrww-libnotify-0.8.6/bin/notify-send hi"] + }, + "Kbae": { + "command": ["e"] } } diff --git a/pkgs/by-name/ri/river-mk-keymap/contrib/example.json.license b/pkgs/by-name/ri/river-mk-keymap/contrib/example.json.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/contrib/example.json.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/ri/river-mk-keymap/contrib/init.json b/pkgs/by-name/ri/river-mk-keymap/contrib/init.json new file mode 100644 index 00000000..a5f24307 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/contrib/init.json @@ -0,0 +1,261 @@ +{ + "<Alt+Ctrl+Super+Shift-Z>": [ + "spawn", + "/nix/store/h71ca2rxlnlcyv4604ih2b2gla5ly27d-qmk-unicode-type-1.0.0/bin/qmk-unicode-type 106 65377" + ], + "<MEDIA_LOWERVOLUME>": { + "allow_locked": true, + "command": [ + "spawn", + "/nix/store/7h9w2sycqj3i2lp61nibgv7qhvwy3pi9-wireplumber-0.5.10/bin/wpctl set-volume @DEFAULT_SINK@ 5%-" + ] + }, + "<MEDIA_MUTEVOLUME>": { + "allow_locked": true, + "command": [ + "spawn", + "/nix/store/08bgv5x7gfhkczf0lgrpim1rw51jlxvn-mpp/bin/mpp toggle" + ] + }, + "<MEDIA_RAISEVOLUME>": { + "allow_locked": true, + "command": [ + "spawn", + "/nix/store/7h9w2sycqj3i2lp61nibgv7qhvwy3pi9-wireplumber-0.5.10/bin/wpctl set-volume @DEFAULT_SINK@ 5%+" + ] + }, + "<LEFT_SUPER>": { + "c": { + "<ENTER>": [ + "zoom" + ], + " ": [ + "toggle-float" + ], + "c": [ + "close" + ], + "f": [ + "toggle-fullscreen" + ], + "n": [ + "swap", + "previous" + ], + "o": [ + "send-to-output", + "next" + ], + "t": [ + "swap", + "next" + ] + }, + "f": { + "0": [ + "set-focused-tags", + "4294967295" + ], + "1": [ + "set-focused-tags", + "1" + ], + "2": [ + "set-focused-tags", + "2" + ], + "3": [ + "set-focused-tags", + "4" + ], + "4": [ + "set-focused-tags", + "8" + ], + "5": [ + "set-focused-tags", + "16" + ], + "6": [ + "set-focused-tags", + "32" + ], + "7": [ + "set-focused-tags", + "64" + ], + "8": [ + "set-focused-tags", + "128" + ], + "9": [ + "set-focused-tags", + "256" + ], + "<Ctrl-n>": [ + "focus-output", + "previous" + ], + "<Ctrl-t>": [ + "focus-output", + "next" + ], + "n": [ + "focus-view", + "previous" + ], + "p": [ + "focus-previous-tags" + ], + "t": [ + "focus-view", + "next" + ] + }, + "m": { + "l": { + "command": [ + "spawn", + "/nix/store/7h9w2sycqj3i2lp61nibgv7qhvwy3pi9-wireplumber-0.5.10/bin/wpctl set-volume @DEFAULT_SINK@ 5%-" + ], + "description": "wpctl set-volume @DEFAULT_SINK@ 5%-" + }, + "m": [ + "spawn", + "/nix/store/08bgv5x7gfhkczf0lgrpim1rw51jlxvn-mpp/bin/mpp toggle" + ], + "r": [ + "spawn", + "/nix/store/7h9w2sycqj3i2lp61nibgv7qhvwy3pi9-wireplumber-0.5.10/bin/wpctl set-volume @DEFAULT_SINK@ 5%+" + ] + }, + "r": { + "a": [ + "spawn", + "/nix/store/h601phmb09d9dwwziwsim6m0r31qajr3-alacritty-0.15.1/bin/alacritty" + ], + "b": [ + "spawn", + "/nix/store/k8gfhk1lglwr8k6477ygkr9hh037a4kw-tskm-0.1.0/bin/tskm open select" + ], + "k": [ + "spawn", + "/nix/store/xpinf75gxhl8aglw2z7631k89iiml7rz-keepassxc-2.7.10/bin/keepassxc" + ], + "p": [ + "spawn", + "/nix/store/skgvjhmqp3jbmaw70xlz86a66lg13395-screenshot_persistent/bin/screenshot_persistent" + ], + "s": [ + "spawn", + "/nix/store/zvzr8cj57jhxyrzjym2rv3w95w7zw901-signal-desktop-7.56.1/bin/signal-desktop" + ] + }, + "v": { + "0": [ + "set-view-tags", + "4294967295" + ], + "1": [ + "set-view-tags", + "1" + ], + "2": [ + "set-view-tags", + "2" + ], + "3": [ + "set-view-tags", + "4" + ], + "4": [ + "set-view-tags", + "8" + ], + "5": [ + "set-view-tags", + "16" + ], + "6": [ + "set-view-tags", + "32" + ], + "7": [ + "set-view-tags", + "64" + ], + "8": [ + "set-view-tags", + "128" + ], + "9": [ + "set-view-tags", + "256" + ], + "a": { + "1": [ + "toggle-view-tags", + "1" + ], + "2": [ + "toggle-view-tags", + "2" + ], + "3": [ + "toggle-view-tags", + "4" + ], + "4": [ + "toggle-view-tags", + "8" + ], + "5": [ + "toggle-view-tags", + "16" + ], + "6": [ + "toggle-view-tags", + "32" + ], + "7": [ + "toggle-view-tags", + "64" + ], + "8": [ + "toggle-view-tags", + "128" + ], + "9": [ + "toggle-view-tags", + "256" + ] + }, + "p": [ + "send-to-previous-tags" + ] + }, + "x": { + "l": [ + "spawn", + "/nix/store/4gp8yj8cz3d78hn01firv7dlqf4ap1fj-lock/bin/lock" + ], + "q": [ + "exit" + ] + } + }, + "<Super-<MOUSE_LEFT>>": [ + "move-view" + ], + "<Super-<MOUSE_RIGHT>>": [ + "resize-view" + ], + "<Super-L>": [ + "spawn", + "/nix/store/4gp8yj8cz3d78hn01firv7dlqf4ap1fj-lock/bin/lock" + ], + "<PRINTSCREEN>": [ + "spawn", + "/nix/store/skgvjhmqp3jbmaw70xlz86a66lg13395-screenshot_persistent/bin/screenshot_persistent" + ] +} diff --git a/pkgs/by-name/ri/river-mk-keymap/flake.lock b/pkgs/by-name/ri/river-mk-keymap/flake.lock index 82448387..1e997998 100644 --- a/pkgs/by-name/ri/river-mk-keymap/flake.lock +++ b/pkgs/by-name/ri/river-mk-keymap/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1743076231, - "narHash": "sha256-yQugdVfi316qUfqzN8JMaA2vixl+45GxNm4oUfXlbgw=", + "lastModified": 1768661221, + "narHash": "sha256-MJwOjrIISfOpdI9x4C+5WFQXvHtOuj5mqLZ4TMEtk1M=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6c5963357f3c1c840201eda129a99d455074db04", + "rev": "3327b113f2ef698d380df83fbccefad7e83d7769", "type": "github" }, "original": { diff --git a/pkgs/by-name/ri/river-mk-keymap/flake.lock.license b/pkgs/by-name/ri/river-mk-keymap/flake.lock.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/flake.lock.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/ri/river-mk-keymap/flake.nix b/pkgs/by-name/ri/river-mk-keymap/flake.nix index 95a14456..b338e4c9 100644 --- a/pkgs/by-name/ri/river-mk-keymap/flake.nix +++ b/pkgs/by-name/ri/river-mk-keymap/flake.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { description = "A smart way to configure river keybindings"; @@ -8,15 +17,27 @@ outputs = {nixpkgs, ...}: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages."${system}"; + + nativeBuildInputs = [ + pkgs.pkg-config + ]; + + buildInputs = [ + pkgs.wayland + pkgs.libxkbcommon + pkgs.fontconfig + ]; in { devShells."${system}".default = pkgs.mkShell { - packages = with pkgs; [ - cargo - clippy - rustc - rustfmt + inherit nativeBuildInputs buildInputs; + + packages = [ + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt - cargo-edit + pkgs.cargo-edit ]; }; }; diff --git a/pkgs/by-name/ri/river-mk-keymap/package.nix b/pkgs/by-name/ri/river-mk-keymap/package.nix index d9519d48..bb3dc285 100644 --- a/pkgs/by-name/ri/river-mk-keymap/package.nix +++ b/pkgs/by-name/ri/river-mk-keymap/package.nix @@ -1,4 +1,19 @@ -{rustPlatform}: +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + rustPlatform, + pkg-config, + wayland, + libxkbcommon, + fontconfig, +}: rustPlatform.buildRustPackage { pname = "river-mk-keymap"; version = "0.1.0"; @@ -8,6 +23,16 @@ rustPlatform.buildRustPackage { lockFile = ./Cargo.lock; }; + nativeBuildInputs = [ + pkg-config + ]; + + buildInputs = [ + wayland + libxkbcommon + fontconfig + ]; + meta = { mainProgram = "river-mk-keymap"; }; diff --git a/pkgs/by-name/ri/river-mk-keymap/resources/river-control-unstable-v1.xml b/pkgs/by-name/ri/river-mk-keymap/resources/river-control-unstable-v1.xml new file mode 100644 index 00000000..aa5fc4dc --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/resources/river-control-unstable-v1.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="river_control_unstable_v1"> + <copyright> + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + </copyright> + + <interface name="zriver_control_v1" version="1"> + <description summary="run compositor commands"> + This interface allows clients to run compositor commands and receive a + success/failure response with output or a failure message respectively. + + Each command is built up in a series of add_argument requests and + executed with a run_command request. The first argument is the command + to be run. + + A complete list of commands should be made available in the man page of + the compositor. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_control object"> + This request indicates that the client will not use the + river_control object any more. Objects that have been created + through this instance are not affected. + </description> + </request> + + <request name="add_argument"> + <description summary="add an argument to the current command"> + Arguments are stored by the server in the order they were sent until + the run_command request is made. + </description> + <arg name="argument" type="string" summary="the argument to add"/> + </request> + + <request name="run_command"> + <description summary="run the current command"> + Execute the command built up using the add_argument request for the + given seat. + </description> + <arg name="seat" type="object" interface="wl_seat"/> + <arg name="callback" type="new_id" interface="zriver_command_callback_v1" + summary="callback object"/> + </request> + </interface> + + <interface name="zriver_command_callback_v1" version="1"> + <description summary="callback object"> + This object is created by the run_command request. Exactly one of the + success or failure events will be sent. This object will be destroyed + by the compositor after one of the events is sent. + </description> + + <event name="success" type="destructor"> + <description summary="command successful"> + Sent when the command has been successfully received and executed by + the compositor. Some commands may produce output, in which case the + output argument will be a non-empty string. + </description> + <arg name="output" type="string" summary="the output of the command"/> + </event> + + <event name="failure" type="destructor"> + <description summary="command failed"> + Sent when the command could not be carried out. This could be due to + sending a non-existent command, no command, not enough arguments, too + many arguments, invalid arguments, etc. + </description> + <arg name="failure_message" type="string" + summary="a message explaining why failure occurred"/> + </event> + </interface> +</protocol> diff --git a/pkgs/by-name/ri/river-mk-keymap/resources/river-status-unstable-v1.xml b/pkgs/by-name/ri/river-mk-keymap/resources/river-status-unstable-v1.xml new file mode 100644 index 00000000..e9629dde --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/resources/river-status-unstable-v1.xml @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="river_status_unstable_v1"> + <copyright> + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + </copyright> + + <interface name="zriver_status_manager_v1" version="4"> + <description summary="manage river status objects"> + A global factory for objects that receive status information specific + to river. It could be used to implement, for example, a status bar. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_status_manager object"> + This request indicates that the client will not use the + river_status_manager object any more. Objects that have been created + through this instance are not affected. + </description> + </request> + + <request name="get_river_output_status"> + <description summary="create an output status object"> + This creates a new river_output_status object for the given wl_output. + </description> + <arg name="id" type="new_id" interface="zriver_output_status_v1"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + + <request name="get_river_seat_status"> + <description summary="create a seat status object"> + This creates a new river_seat_status object for the given wl_seat. + </description> + <arg name="id" type="new_id" interface="zriver_seat_status_v1"/> + <arg name="seat" type="object" interface="wl_seat"/> + </request> + </interface> + + <interface name="zriver_output_status_v1" version="4"> + <description summary="track output tags and focus"> + This interface allows clients to receive information about the current + windowing state of an output. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_output_status object"> + This request indicates that the client will not use the + river_output_status object any more. + </description> + </request> + + <event name="focused_tags"> + <description summary="focused tags of the output"> + Sent once binding the interface and again whenever the tag focus of + the output changes. + </description> + <arg name="tags" type="uint" summary="32-bit bitfield"/> + </event> + + <event name="view_tags"> + <description summary="tag state of an output's views"> + Sent once on binding the interface and again whenever the tag state + of the output changes. + </description> + <arg name="tags" type="array" summary="array of 32-bit bitfields"/> + </event> + + <event name="urgent_tags" since="2"> + <description summary="tags of the output with an urgent view"> + Sent once on binding the interface and again whenever the set of + tags with at least one urgent view changes. + </description> + <arg name="tags" type="uint" summary="32-bit bitfield"/> + </event> + + <event name="layout_name" since="4"> + <description summary="name of the layout"> + Sent once on binding the interface should a layout name exist and again + whenever the name changes. + </description> + <arg name="name" type="string" summary="layout name"/> + </event> + + <event name="layout_name_clear" since="4"> + <description summary="name of the layout"> + Sent when the current layout name has been removed without a new one + being set, for example when the active layout generator disconnects. + </description> + </event> + </interface> + + <interface name="zriver_seat_status_v1" version="3"> + <description summary="track seat focus"> + This interface allows clients to receive information about the current + focus of a seat. Note that (un)focused_output events will only be sent + if the client has bound the relevant wl_output globals. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_seat_status object"> + This request indicates that the client will not use the + river_seat_status object any more. + </description> + </request> + + <event name="focused_output"> + <description summary="the seat focused an output"> + Sent on binding the interface and again whenever an output gains focus. + </description> + <arg name="output" type="object" interface="wl_output"/> + </event> + + <event name="unfocused_output"> + <description summary="the seat unfocused an output"> + Sent whenever an output loses focus. + </description> + <arg name="output" type="object" interface="wl_output"/> + </event> + + <event name="focused_view"> + <description summary="information on the focused view"> + Sent once on binding the interface and again whenever the focused + view or a property thereof changes. The title may be an empty string + if no view is focused or the focused view did not set a title. + </description> + <arg name="title" type="string" summary="title of the focused view"/> + </event> + + <event name="mode" since="3"> + <description summary="the active mode changed"> + Sent once on binding the interface and again whenever a new mode + is entered (e.g. with riverctl enter-mode foobar). + </description> + <arg name="name" type="string" summary="name of the mode"/> + </event> + </interface> +</protocol> diff --git a/pkgs/by-name/ri/river-mk-keymap/src/cli.rs b/pkgs/by-name/ri/river-mk-keymap/src/cli.rs index 55b87e1a..ad872cc9 100644 --- a/pkgs/by-name/ri/river-mk-keymap/src/cli.rs +++ b/pkgs/by-name/ri/river-mk-keymap/src/cli.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::path::PathBuf; use clap::Parser; @@ -6,6 +16,20 @@ use clap::Parser; #[command(author, version, about, long_about = None)] /// A tool to manage your key mappings for the river window manager pub(super) struct Args { - /// Path to mappings JSON file - pub path: PathBuf, + #[command(subcommand)] + pub command: SubCommand, + + #[arg(long, short)] + /// Path to mapping config JSON file + pub keymap: PathBuf, +} + +#[derive(clap::Subcommand, Clone, Debug)] +pub(super) enum SubCommand { + Init { + #[arg(short, long, default_value_t = false)] + /// Only show what would be done, don't actually perform the init. + dry_run: bool, + }, + ShowHelp {}, } diff --git a/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs b/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs index a4ac0ebd..8372b61d 100644 --- a/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs +++ b/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs @@ -1,109 +1,313 @@ -use std::process::Command; +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. -use keymaps::key_repr::{KeyValue, MediaKeyCode, MouseKeyValue}; +use std::{env::current_exe, path::Path, process::Command}; -use super::{KeyMap, MapMode}; +use anyhow::{bail, Result}; +use keymaps::key_repr::{Key, KeyValue, Keys, MediaKeyCode, ModifierKeyCode, MouseKeyValue}; +use rustix::path::Arg; + +use super::KeyMap; impl KeyMap { - #[must_use] - pub fn to_commands(self) -> Vec<Command> { - self.0 - .iter() - .flat_map(|(key, value)| { - let key = key.last().expect("Will exist"); - let mods = { - let modifiers = key.modifiers(); - let mut output = vec![]; - - if modifiers.alt() { - output.push("Alt"); - } - if modifiers.ctrl() { - output.push("Control"); - } - if modifiers.meta() { - output.push("Super"); - } - if modifiers.shift() { - output.push("Shift"); - } - if output.is_empty() { - "None".to_owned() - } else { - output.join("+") - } - }; - let key_value = match key.value() { - KeyValue::Backspace => "BackSpace".to_owned(), - KeyValue::Enter => "Enter".to_owned(), - KeyValue::Left => "Left".to_owned(), - KeyValue::Right => "Right".to_owned(), - KeyValue::Up => "Up".to_owned(), - KeyValue::Down => "Down".to_owned(), - KeyValue::Home => "Home".to_owned(), - KeyValue::End => "End".to_owned(), - KeyValue::PageUp => "Page_Up".to_owned(), - KeyValue::PageDown => "Page_Down".to_owned(), - KeyValue::Tab => "Tab".to_owned(), - KeyValue::BackTab => "BackTab".to_owned(), - KeyValue::Delete => "Delete".to_owned(), - KeyValue::Insert => "Insert".to_owned(), - KeyValue::F(num) => format!("F{num}"), - KeyValue::Char(a) => a.to_string(), - KeyValue::Null => "Null".to_owned(), - KeyValue::Esc => "Esc".to_owned(), - KeyValue::CapsLock => "CapsLock".to_owned(), - KeyValue::ScrollLock => "ScrollLock".to_owned(), - KeyValue::NumLock => "NumLock".to_owned(), - KeyValue::PrintScreen => "Print".to_owned(), - KeyValue::Pause => "Pause".to_owned(), - KeyValue::Menu => "Menu".to_owned(), - KeyValue::KeypadBegin => "KeypadBegin".to_owned(), - KeyValue::Media(media_key_code) => match media_key_code { - MediaKeyCode::Play => "XF86AudioPlay".to_owned(), - MediaKeyCode::Pause => "XF86AudioPause".to_owned(), - MediaKeyCode::PlayPause => "XF86AudioPlayPause".to_owned(), - MediaKeyCode::Reverse => "XF86AudioReverse".to_owned(), - MediaKeyCode::Stop => "XF86AudioStop".to_owned(), - MediaKeyCode::FastForward => "XF86AudioFastForward".to_owned(), - MediaKeyCode::Rewind => "XF86AudioRewind".to_owned(), - MediaKeyCode::TrackNext => "XF86AudioTrackNext".to_owned(), - MediaKeyCode::TrackPrevious => "XF86AudioTrackPrevious".to_owned(), - MediaKeyCode::Record => "XF86AudioRecord".to_owned(), - MediaKeyCode::LowerVolume => "XF86AudioLowerVolume".to_owned(), - MediaKeyCode::RaiseVolume => "XF86AudioRaiseVolume".to_owned(), - MediaKeyCode::MuteVolume => "XF86AudioMuteVolume".to_owned(), - }, - KeyValue::MouseKey(mouse_key_value) => match mouse_key_value { - MouseKeyValue::Left => "BTN_LEFT".to_owned(), - MouseKeyValue::Right => "BTN_RIGHT".to_owned(), - MouseKeyValue::Middle => "BTN_MIDDLE".to_owned(), - }, - _ => todo!(), + /// # Errors + /// If impossible requests are made. + /// + /// # Panics + /// If internal assertions fail. + #[allow(clippy::too_many_lines)] + pub fn to_commands(self, keymap_path: &Path) -> Result<Vec<Command>> { + self.0.iter().try_for_each(|(keys, value)| { + let (prefix, last) = keys.split_at(keys.len() - 1); + let prefix = prefix.to_owned(); + + if value.allow_locked && !prefix.is_empty() { + bail!( + "Only single key mappings can be used \ + in locked mode, but '{}' contains multiple ('{}').", + Keys::from(keys), + Keys::from(prefix), + ) + } + + if !prefix.is_empty() + && [ + "<ESC>".parse().expect("hardcoded"), + "<BACKSPACE>".parse().expect("hardcoded"), + ] + .contains(&last[0]) + { + bail!( + "You cannot use <ESC> or <BACKSPACE> as the final part of a \ + prefixed mapping, as that is used to return \ + to 'normal' or the upper mode; found in '{}'", + Keys::from(keys), + ) + } + + Ok(()) + })?; + + let mut output: Vec<_> = self + .0 + .into_iter() + .flat_map(|(keys, value)| { + let (prefix, mapping) = keys.split_at(keys.len() - 1); + + let (final_mode, mut base): (Option<String>, _) = + prefix + .iter() + .fold((None, vec![]), |(acc_mode, mut acc_vec), key| { + // Declare intermediate modes for each key. + let mode_name: String = { + let base = key.to_string_repr(); + + if let Some(result) = &acc_mode { + result.to_owned() + base.as_str() + } else { + base + } + }; + + let mut riverctl = Command::new("riverctl"); + riverctl.args(["declare-mode", mode_name.as_str()]); + + let mut output = vec![riverctl]; + + // Provide keymaps for entering and leaving the mode + if let Some(acc_mode) = acc_mode.clone() { + output.extend(key_to_command( + key.to_owned(), + &["enter-mode".to_owned(), mode_name.clone()], + &acc_mode, + false, + )); + } else { + // Also spawn the help display if we start from the “normal” mode. + output.extend(key_to_command( + key.to_owned(), + &[ + "spawn".to_owned(), + format!( + "{} && sleep 1 && {}", + shlex::try_join([ + "riverctl", + "enter-mode", + mode_name.as_str() + ]) + .expect("Should work"), + shlex::try_join([ + current_exe() + .expect("Should have a current exe") + .as_os_str() + .as_str() + .expect("Should be valid utf8"), + "--keymap", + keymap_path.to_str().expect("Should be valid utf8"), + "show-help", + ]) + .expect("Should work"), + ), + ], + "normal", + false, + )); + } + + // Provide a mapping for going up a mode + output.extend(key_to_command( + "<BACKSPACE>".parse().expect("Hardcoded"), + &[ + "enter-mode".to_owned(), + acc_mode.unwrap_or("normal".to_owned()), + ], + &mode_name, + false, + )); + + // Another one for going back to normal. + output.extend(key_to_command( + "<ESC>".parse().expect("Hardcoded"), + &["enter-mode".to_owned(), "normal".to_owned()], + &mode_name, + false, + )); + + acc_vec.extend(output); + + (Some(mode_name), acc_vec) + }); + + let command = if value.once { + vec![ + "spawn".to_owned(), + format!( + "riverctl {} && {}", + shlex::try_join(value.command.iter().map(String::as_str)) + .expect("Should work"), + shlex::try_join(["riverctl", "enter-mode", "normal"]) + .expect("Should work"), + ), + ] + } else { + value.command }; + base.extend(key_to_command( + mapping[0], + &command, + final_mode.as_ref().map_or("normal", |v| v.as_str()), + value.allow_locked, + )); - value - .modes - .iter() - .map(|mode| { - let mut riverctl = Command::new("riverctl"); - riverctl.args([value.map_mode.as_command(), mode, &mods, &key_value]); - - riverctl.args(value.command.iter().map(String::as_str)); - riverctl - }) - .collect::<Vec<_>>() + base }) - .collect() + .collect(); + + output.sort_by_cached_key(|cmd| format!("{cmd:?}")); + output.dedup_by_key(|cmd| format!("{cmd:?}")); + + Ok(output) } } -impl MapMode { - pub(crate) fn as_command(self) -> &'static str { - match self { - MapMode::Map => "map", - MapMode::MapMouse => "map-pointer", - MapMode::Unmap => "unmap", +fn key_value_to_xkb_common_name(value: KeyValue) -> (String, Vec<&'static str>) { + let mut extra_modifiers = vec![]; + + let output = match value { + KeyValue::Backspace => "BackSpace".to_owned(), + KeyValue::Enter => "Return".to_owned(), + KeyValue::Left => "Left".to_owned(), + KeyValue::Right => "Right".to_owned(), + KeyValue::Up => "Up".to_owned(), + KeyValue::Down => "Down".to_owned(), + KeyValue::Home => "Home".to_owned(), + KeyValue::End => "End".to_owned(), + KeyValue::PageUp => "Page_Up".to_owned(), + KeyValue::PageDown => "Page_Down".to_owned(), + KeyValue::Tab => "Tab".to_owned(), + KeyValue::BackTab => "BackTab".to_owned(), + KeyValue::Delete => "Delete".to_owned(), + KeyValue::Insert => "Insert".to_owned(), + KeyValue::F(num) => format!("F{num}"), + KeyValue::Char(a) => { + // River does not differentiate between 'a' and 'A', + // so we need to do it beforehand. + if a.is_ascii_uppercase() { + extra_modifiers.push("Shift"); + } + + if a == ' ' { + "Space".to_string() + } else { + a.to_string() + } + } + KeyValue::Null => "Null".to_owned(), + KeyValue::Esc => "Escape".to_owned(), + KeyValue::CapsLock => "CapsLock".to_owned(), + KeyValue::ScrollLock => "ScrollLock".to_owned(), + KeyValue::NumLock => "NumLock".to_owned(), + KeyValue::PrintScreen => "Print".to_owned(), + KeyValue::Pause => "Pause".to_owned(), + KeyValue::Menu => "Menu".to_owned(), + KeyValue::KeypadBegin => "KeypadBegin".to_owned(), + KeyValue::Media(media_key_code) => match media_key_code { + MediaKeyCode::Play => "XF86AudioPlay".to_owned(), + MediaKeyCode::Pause => "XF86AudioPause".to_owned(), + MediaKeyCode::PlayPause => "XF86AudioPlayPause".to_owned(), + MediaKeyCode::Reverse => "XF86AudioReverse".to_owned(), + MediaKeyCode::Stop => "XF86AudioStop".to_owned(), + MediaKeyCode::FastForward => "XF86AudioFastForward".to_owned(), + MediaKeyCode::Rewind => "XF86AudioRewind".to_owned(), + MediaKeyCode::TrackNext => "XF86AudioNext".to_owned(), + MediaKeyCode::TrackPrevious => "XF86AudioPrev".to_owned(), + MediaKeyCode::Record => "XF86AudioRecord".to_owned(), + MediaKeyCode::LowerVolume => "XF86AudioLowerVolume".to_owned(), + MediaKeyCode::RaiseVolume => "XF86AudioRaiseVolume".to_owned(), + MediaKeyCode::MuteVolume => "XF86AudioMute".to_owned(), + }, + KeyValue::MouseKey(mouse_key_value) => match mouse_key_value { + MouseKeyValue::Left => "BTN_LEFT".to_owned(), + MouseKeyValue::Right => "BTN_RIGHT".to_owned(), + MouseKeyValue::Middle => "BTN_MIDDLE".to_owned(), + }, + KeyValue::ModifierKey(modifier_key_code) => match modifier_key_code { + ModifierKeyCode::LeftAlt => "ALT_L".to_owned(), + ModifierKeyCode::RightAlt => "ALT_R".to_owned(), + ModifierKeyCode::LeftCtrl => "CTRL_L".to_owned(), + ModifierKeyCode::RightCtrl => "CTRL_R".to_owned(), + ModifierKeyCode::LeftMeta => "SUPER_L".to_owned(), + ModifierKeyCode::RightMeta => "SUPER_R".to_owned(), + ModifierKeyCode::LeftShift => "SHIFT_L".to_owned(), + ModifierKeyCode::RightShift => "SHIFT_R".to_owned(), + }, + other => todo!("Key value: {other} not known."), + }; + + (output, extra_modifiers) +} + +fn key_to_command(key: Key, command: &[String], mode: &str, allow_locked: bool) -> Vec<Command> { + let mut modifiers = { + let modifiers = key.modifiers(); + let mut output = vec![]; + + if modifiers.alt() { + output.push("Alt"); + } + if modifiers.ctrl() { + output.push("Control"); + } + if modifiers.meta() { + output.push("Super"); } + if modifiers.shift() { + output.push("Shift"); + } + output + }; + + let (key_value, extra_modifiers) = key_value_to_xkb_common_name(key.value()); + modifiers.extend(extra_modifiers); + + let map_mode = if let KeyValue::MouseKey(_) = key.value() { + "map-pointer" + } else { + "map" + }; + + let modifiers = if modifiers.is_empty() { + "None".to_owned() + } else { + modifiers.join("+") + }; + + let mut output = vec![{ + let mut riverctl = Command::new("riverctl"); + riverctl.args([map_mode, mode, &modifiers, &key_value]); + + riverctl.args(command.iter().map(String::as_str)); + + riverctl + }]; + + if allow_locked { + output.push({ + let mut riverctl = Command::new("riverctl"); + riverctl.args([map_mode, "locked", &modifiers, &key_value]); + + riverctl.args(command.iter().map(String::as_str)); + + riverctl + }); } + + output } diff --git a/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs index 84a16c9d..16dc02f4 100644 --- a/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs +++ b/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs @@ -1,37 +1,107 @@ -use std::{collections::HashMap, fmt::Display, ops::Deref, str::FromStr}; - -use anyhow::Context; -use keymaps::{key_repr::Key, map_tree::MapTrie}; +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{fmt::Display, ops::Deref, str::FromStr}; + +use anyhow::{anyhow, bail, Context, Result}; +use keymaps::{ + key_repr::{Key, Keys}, + map_tree::MapTrie, +}; use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; pub mod commands; -#[derive(Deserialize, Serialize, Debug)] -#[allow(clippy::module_name_repetitions)] -pub struct RawKeyMap(HashMap<Key, KeyConfig>); - #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, PartialOrd)] -/// What values to use for: `riverctl <map_mode> <mode> <mods> <key> <command..>` +/// What values to use for: `riverctl <command..>` +#[serde(deny_unknown_fields)] pub struct KeyConfig { command: Vec<String>, - #[serde(default = "default_mode")] - modes: Vec<String>, + /// Whether to allow this key mapping in the “locked” mode. + #[serde(default)] + allow_locked: bool, + + /// Whether to go back to the normal mode, after running this command. + #[serde(default)] + once: bool, - #[serde(default = "MapMode::default")] - map_mode: MapMode, + /// Use a different description to display this command, instead of the `command`. + description: Option<String>, } impl FromStr for KeyMap { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { - let raw: RawKeyMap = - serde_json::from_str(s).context("Failed to parse the keymap config file as json.")?; + fn decode_value( + output: &mut MapTrie<KeyConfig>, + current_key: Vec<Key>, + value: &Value, + ) -> Result<()> { + let key_config = if let Some(value) = value.as_array() { + KeyConfig { + command: value + .iter() + .map(|v| v.as_str().map(ToOwned::to_owned)) + .collect::<Option<_>>() + .ok_or(anyhow!("A array contained a non-string value: {value:#?}"))?, + allow_locked: false, + once: false, + description: None, + } + } else if let Some(object) = value.as_object() { + if object.contains_key("command") { + serde_json::from_value(value.to_owned()) + .with_context(|| format!("Failed to parse key config: {value:#?}"))? + } else { + for (key, value) in object { + let mut local_current_key = current_key.clone(); + local_current_key.push( + Key::from_str(key) + .with_context(|| format!("Failed to parse key '{key}'"))?, + ); + + decode_value(output, local_current_key, value)?; + } + return Ok(()); + } + } else { + bail!("Value ({}) is invalid (not array or object).", value) + }; + + output + .insert(¤t_key, key_config.clone()) + .with_context(|| { + format!( + "Failed to insert mapping {} -> {key_config}", + Keys::from(current_key) + ) + })?; + + Ok(()) + } + let mut out = MapTrie::<KeyConfig>::new(); - for (key, value) in raw.0 { - out.insert(&[key], value.clone()) - .with_context(|| format!("Failed to insert mapping {key} -> {value}"))?; + + let raw: Map<String, Value> = + serde_json::from_str(s).context("Failed to parse the keymap config file as json.")?; + + for (key, value) in raw { + decode_value( + &mut out, + vec![Key::from_str(&key) + .with_context(|| format!("Failed to parse key ('{key}')"))?], + &value, + )?; } Ok(Self(out)) @@ -39,25 +109,11 @@ impl FromStr for KeyMap { } impl Display for KeyConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.command.join(" ").as_str()) - } -} - -fn default_mode() -> Vec<String> { - vec!["normal".to_owned()] -} - -#[derive(Copy, Deserialize, Serialize, Debug, Clone, Default, PartialEq, PartialOrd)] -enum MapMode { - #[default] - Map, - MapMouse, - Unmap, -} - -impl Display for MapMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - <Self as std::fmt::Debug>::fmt(self, f) + if let Some(desc) = &self.description { + f.write_str(desc) + } else { + f.write_str(self.command.join(" ").as_str()) + } } } diff --git a/pkgs/by-name/ri/river-mk-keymap/src/main.rs b/pkgs/by-name/ri/river-mk-keymap/src/main.rs index 5cb99f74..18c291cf 100644 --- a/pkgs/by-name/ri/river-mk-keymap/src/main.rs +++ b/pkgs/by-name/ri/river-mk-keymap/src/main.rs @@ -1,33 +1,69 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::fs; use anyhow::Context; use clap::Parser; -mod cli; +pub mod cli; pub mod key_map; +pub mod wayland; use crate::{cli::Args, key_map::KeyMap}; fn main() -> Result<(), anyhow::Error> { let args = Args::parse(); - let keymap_file = fs::read_to_string(&args.path) - .with_context(|| format!("Failed to open keymap file at: '{}'.", args.path.display()))?; - - let keymap: KeyMap = keymap_file - .parse() - .with_context(|| format!("Failed to parse keymap file at: {}", args.path.display()))?; - - // println!("{keymap}"); - // println!("Commands:"); - for mut command in keymap.to_commands() { - // println!("Executing {command:?}"); - let status = command - .status() - .with_context(|| format!("Failed to run command: '{command:?}'"))?; - - if !status.success() { - eprintln!("Command ('{command:?}') returned with non zero exit code: {status}"); + + let keymap_path = &args.keymap.canonicalize().with_context(|| { + format!( + "Failed to canonicalize kepmay path: '{}'", + args.keymap.display() + ) + })?; + + let config = { + let keymap_file = fs::read_to_string(keymap_path).with_context(|| { + format!( + "Failed to open keymap file at: '{}'.", + keymap_path.display() + ) + })?; + + let keymap: KeyMap = keymap_file.parse().with_context(|| { + format!("Failed to parse keymap file at: {}", keymap_path.display()) + })?; + + keymap + }; + + match args.command { + cli::SubCommand::Init { dry_run } => { + println!("{config}"); + for mut command in config.to_commands(keymap_path)? { + if dry_run { + println!("{command:?}"); + } else { + let status = command + .status() + .with_context(|| format!("Failed to run command: '{command:?}'"))?; + + if !status.success() { + eprintln!( + "Command ('{command:?}') returned with non zero exit code: {status}" + ); + } + } + } } + cli::SubCommand::ShowHelp {} => wayland::main(config)?, } Ok(()) diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/ansi/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/ansi/mod.rs new file mode 100644 index 00000000..0517ecf2 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/ansi/mod.rs @@ -0,0 +1,173 @@ +use std::mem; + +use vte::{Params, Parser, Perform}; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Purple, + Cyan, + White, +} + +#[derive(Debug)] +struct Cleaner { + current_color: Option<Color>, + styles: StyledString, + current: String, +} + +#[derive(Debug)] +struct StyledStringInner { + val: String, + color: Option<Color>, +} + +pub(crate) struct StyledChar { + ch: char, + color: Option<Color>, +} + +impl StyledChar { + pub(crate) fn as_char(&self) -> char { + self.ch + } + + pub(crate) fn is_bold(&self) -> bool { + self.color.is_some() + } + + pub(crate) fn color(&self) -> Option<Color> { + self.color + } +} + +#[derive(Debug)] +pub(crate) struct StyledString { + inner: Vec<StyledStringInner>, +} + +impl StyledString { + fn push(&mut self, val: StyledStringInner) { + self.inner.push(val); + } + + pub(crate) fn chars(&self) -> impl Iterator<Item = StyledChar> + use<'_> { + self.inner.iter().flat_map(|inner| { + inner.val.chars().map(|ch| StyledChar { + ch, + color: inner.color, + }) + }) + } +} + +impl Cleaner { + fn reset_color(&mut self) { + self.styles.push(StyledStringInner { + val: mem::take(&mut self.current), + color: mem::take(&mut self.current_color), + }); + } + + fn set_color(&mut self, color: Color) { + self.current_color = Some(color); + } + + fn add_char(&mut self, c: char) { + self.current.push(c); + } +} + +impl Perform for Cleaner { + fn print(&mut self, c: char) { + self.add_char(c); + } + + fn execute(&mut self, byte: u8) { + if byte == b'\n' { + self.reset_color(); + self.add_char('\n'); + self.reset_color(); + } else { + eprintln!("Unknown [execute]: {byte:02x}"); + } + } + + fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) { + eprintln!( + "Unknown [hook] params={params:?}, intermediates={intermediates:?}, ignore={ignore:?}, char={c:?}" + ); + } + + fn put(&mut self, byte: u8) { + eprintln!("Unknonw [put] {byte:02x}"); + } + + fn unhook(&mut self) { + eprintln!("Unknown [unhook]"); + } + + fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { + eprintln!("Unkown [osc_dispatch] params={params:?} bell_terminated={bell_terminated}"); + } + + fn csi_dispatch(&mut self, params: &Params, _: &[u8], _: bool, c: char) { + let params: Vec<u16> = params.iter().flatten().copied().collect(); + + if c != 'm' { + return; + } + + // See: https://gist.github.com/JBlond/2fea43a3049b38287e5e9cefc87b2124 + match params[..] { + [0] => self.reset_color(), + // [0, regular] if matches!(regular, 30..=37) => {} + [1, bold] if matches!(bold, 30..=37) => match bold { + 30 => self.set_color(Color::Black), + 31 => self.set_color(Color::Red), + 32 => self.set_color(Color::Green), + 36 => self.set_color(Color::Yellow), + 34 => self.set_color(Color::Blue), + 35 => self.set_color(Color::Purple), + 33 => self.set_color(Color::Cyan), + 37 => self.set_color(Color::White), + _ => unreachable!("Was filtered out"), + }, + // [4, underline] if matches!(underline, 30..=37) => {} + // [background] if matches!(background, 40..=47) => {} + _ => todo!(), + } + + // println!( + // "[csi_dispatch] params={:#?}, intermediates={:?}, ignore={:?}, char={:?}", + // params, intermediates, ignore, c + // ); + } + + fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { + eprintln!( + "Unkown [esc_dispatch] intermediates={intermediates:?}, ignore={ignore:?}, byte={byte:02x}" + ); + } +} + +pub(crate) fn parse(input: &str) -> StyledString { + let mut statemachine = Parser::new(); + let mut performer = Cleaner { + current_color: None, + styles: StyledString { inner: vec![] }, + current: String::new(), + }; + + let buf: Vec<_> = input.bytes().collect(); + + statemachine.advance(&mut performer, &buf[..]); + + assert!(performer.current.is_empty() && performer.current_color.is_none()); + performer.styles +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/dispatches.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/dispatches.rs new file mode 100644 index 00000000..c6e04fdf --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/dispatches.rs @@ -0,0 +1,214 @@ +use std::num::NonZero; + +use keymaps::key_repr::Key; +use wayland_client::{ + globals::GlobalListContents, + protocol::{ + wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_seat::WlSeat, + wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface, + }, + Connection, Dispatch, QueueHandle, +}; + +use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::ZwlrLayerShellV1, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, +}; + +use crate::wayland::{ + ansi, render, + river::protocols::river_protocols::{ + zriver_seat_status_v1::{self, ZriverSeatStatusV1}, + zriver_status_manager_v1::ZriverStatusManagerV1, + }, + AppData, +}; + +impl Dispatch<ZriverSeatStatusV1, ()> for AppData { + fn event( + state: &mut Self, + _: &ZriverSeatStatusV1, + event: <ZriverSeatStatusV1 as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + if let zriver_seat_status_v1::Event::Mode { name } = event { + let new_text = { + if name == "normal" { + // We are back at the normal mode. + // There is no need to display the mappings anymore, exit. + state.should_exit = true; + return; + } else if let Ok(keys) = Key::parse_multiple(&name) { + if let Some(val) = state.config.get(&keys) { + ansi::parse(val.to_string().as_str()) + } else { + // Mode name not know, do nothing. + return; + } + } else { + // Mode name not valid, do nothing. + return; + } + }; + + let px_height; + (state.pixel_data, (state.max_px_width, px_height)) = + render::text(&new_text).expect("Works?"); + + // We add the `5` here, so that our letters don't stop exactly at the border. + state + .window + .0 + .set_size(state.max_px_width + 5, px_height + 5); + state.window.1.commit(); + + if state.configured { + state.draw(); + } + } + } +} + +impl Dispatch<ZwlrLayerSurfaceV1, ()> for AppData { + fn event( + state: &mut Self, + proxy: &ZwlrLayerSurfaceV1, + event: <ZwlrLayerSurfaceV1 as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + match event { + zwlr_layer_surface_v1::Event::Configure { + serial, + width, + height, + } => { + state.buffer = None; + + proxy.ack_configure(serial); + + state.width = NonZero::new(width).map_or_else(|| state.width, NonZero::get); + state.height = NonZero::new(height).map_or_else(|| state.height, NonZero::get); + + state.draw(); + + state.configured = true; + } + zwlr_layer_surface_v1::Event::Closed => { + state.should_exit = true; + } + _ => (), + } + } +} + +impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for AppData { + fn event( + _: &mut AppData, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle<AppData>, + ) { + } +} + +impl Dispatch<WlShmPool, ()> for AppData { + fn event( + _: &mut Self, + _: &WlShmPool, + _: <WlShmPool as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<WlShm, ()> for AppData { + fn event( + _: &mut Self, + _: &WlShm, + _: <WlShm as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<WlSurface, ()> for AppData { + fn event( + _: &mut Self, + _: &WlSurface, + _: <WlSurface as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<WlCompositor, ()> for AppData { + fn event( + _: &mut Self, + _: &WlCompositor, + _: <WlCompositor as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<WlSeat, ()> for AppData { + fn event( + _: &mut Self, + _: &WlSeat, + _: <WlSeat as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<WlBuffer, ()> for AppData { + fn event( + _: &mut Self, + _: &WlBuffer, + _: <WlBuffer as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<ZriverStatusManagerV1, ()> for AppData { + fn event( + _: &mut Self, + _: &ZriverStatusManagerV1, + _: <ZriverStatusManagerV1 as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} + +impl Dispatch<ZwlrLayerShellV1, ()> for AppData { + fn event( + _: &mut Self, + _: &ZwlrLayerShellV1, + _: <ZwlrLayerShellV1 as wayland_client::Proxy>::Event, + (): &(), + _: &Connection, + _: &QueueHandle<Self>, + ) { + } +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/mod.rs new file mode 100644 index 00000000..44c010d5 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/mod.rs @@ -0,0 +1,272 @@ +#![allow( + clippy::cast_sign_loss, + clippy::cast_possible_wrap, + clippy::cast_precision_loss, + clippy::cast_possible_truncation +)] + +use anyhow::Result; +use wayland_client::{ + globals::registry_queue_init, + protocol::{ + wl_compositor::WlCompositor, + wl_seat::WlSeat, + wl_shm::{self, WlShm}, + wl_surface::WlSurface, + }, + Connection, +}; +use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::{self, ZwlrLayerShellV1}, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, +}; + +use crate::{ + key_map::KeyMap, + wayland::{ + ansi::Color, + river::protocols::river_protocols::zriver_status_manager_v1::ZriverStatusManagerV1, + shm::slot::{Buffer, SlotPool}, + }, +}; + +mod ansi; +mod render; +mod river; +mod shm; + +mod dispatches; + +struct AppData { + pool: SlotPool, + window: (ZwlrLayerSurfaceV1, WlSurface), + + configured: bool, + buffer: Option<Buffer>, + + width: u32, + height: u32, + + max_px_width: u32, + pixel_data: (Vec<f32>, Vec<Option<Color>>), + + config: KeyMap, + should_exit: bool, +} + +impl AppData { + #[allow(clippy::too_many_lines)] + fn draw(&mut self) { + let width = self.width; + let height = self.height; + let stride = self.width as i32 * 4; + + let buffer = self.buffer.get_or_insert_with(|| { + self.pool + .create_buffer( + width as i32, + height as i32, + stride, + wl_shm::Format::Argb8888, + ) + .expect("Works?") + .0 + }); + + let canvas = if let Some(canvas) = self.pool.canvas(buffer) { + canvas + } else { + // This should be rare, but if the compositor has not released the previous + // buffer, we need double-buffering. + let (second_buffer, canvas) = self + .pool + .create_buffer( + self.width as i32, + self.height as i32, + stride, + wl_shm::Format::Argb8888, + ) + .expect("create buffer"); + *buffer = second_buffer; + canvas + }; + + // Draw to the window. + { + canvas + .chunks_exact_mut(stride as usize) + .enumerate() + .for_each(|(row_index, row)| { + // let row_slice = row_slice(self.height, row_index as u32, 0.97); + // let allowed_columns = (f64::from(self.width) * row_slice).ceil() as usize; + + row.chunks_exact_mut(4) + .enumerate() + .for_each(|(column_index, chunk)| { + // const BACKGROUND_COLOR: u32 = 0xee58_5b70; + const BACKGROUND_COLOR: u32 = 0xee00_0000; + + assert!(column_index as u32 <= self.width); + + // if column_index > allowed_columns + // || column_index < (self.width as usize - allowed_columns) + // { + // let array: &mut [u8; 4] = chunk.try_into().unwrap(); + // *array = 0u32.to_le_bytes(); + // return; + // } + + if column_index >= (self.max_px_width as usize) { + let array: &mut [u8; 4] = chunk.try_into().unwrap(); + *array = BACKGROUND_COLOR.to_le_bytes(); + } else { + assert!(column_index < self.max_px_width as usize); + + let position = + column_index + row_index * self.max_px_width as usize; + + if let Some(coverage) = &self.pixel_data.0.get(position) { + let a = (BACKGROUND_COLOR & (0xff << (6 * 4))) >> 24; + + let (r, g, b) = if let Some(color) = self + .pixel_data + .1 + .get(position) + .expect("If the pixel is set, the color will too") + { + let (r, g, b) = match color { + Color::Black => (0, 0, 0), + Color::Red => (0xff, 0, 0), + Color::Green => (0, 0xff, 0), + Color::Yellow => (0xff, 0xff, 0), + Color::Blue => (0, 0, 0xff), + Color::Purple => (0x80, 0, 0x80), + Color::Cyan => (0, 0xff, 0xff), + Color::White => (0xff, 0xff, 0xff), + }; + + let r = (r as f32 * **coverage).ceil() as u32; + let g = (g as f32 * **coverage).ceil() as u32; + let b = (b as f32 * **coverage).ceil() as u32; + + (r, g, b) + } else { + let r = (255.0 * **coverage).ceil() as u32; + let g = (255.0 * **coverage).ceil() as u32; + let b = (255.0 * **coverage).ceil() as u32; + + (r, g, b) + }; + + let color: u32 = (a << 24) + (r << 16) + (g << 8) + b; + + let array: &mut [u8; 4] = chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + } else { + let array: &mut [u8; 4] = chunk.try_into().unwrap(); + *array = BACKGROUND_COLOR.to_le_bytes(); + } + } + }); + }); + } + + self.window + .1 + .damage_buffer(0, 0, self.width as i32, self.height as i32); + + buffer.attach_to(&self.window.1).expect("works"); + self.window.1.commit(); + } +} + +/// # Errors +/// If a protocol error arises. +pub fn main(config: KeyMap) -> Result<()> { + let conn = Connection::connect_to_env()?; + let (globals, mut queue) = registry_queue_init::<AppData>(&conn)?; + let qh = queue.handle(); + + let seat: WlSeat = globals.bind(&qh, 9..=9, ())?; + let status_manager: ZriverStatusManagerV1 = globals.bind(&qh, 4..=4, ())?; + let _seat_status = status_manager.get_river_seat_status(&seat, &qh, ()); + + let compositor: WlCompositor = globals.bind(&qh, 6..=6, ())?; + let shm: WlShm = globals.bind(&qh, 1..=1, ())?; + // let xdg_wm: XdgWmBase = globals.bind(&qh, 5..=5, ())?; + + let surface = compositor.create_surface(&qh, ()); + let pool = SlotPool::new(1024 * 1024, &shm)?; + + let zwlr_layer_shell: ZwlrLayerShellV1 = globals.bind(&qh, 4..=4, ())?; + let layer_surface = zwlr_layer_shell.get_layer_surface( + &surface, + None, + zwlr_layer_shell_v1::Layer::Overlay, + "river-mk-keymap which-key".to_owned(), + &qh, + (), + ); + + layer_surface.set_size(256, 256); + layer_surface + .set_anchor(zwlr_layer_surface_v1::Anchor::Left | zwlr_layer_surface_v1::Anchor::Top); + + surface.commit(); + + let mut me = AppData { + config, + should_exit: false, + + configured: false, + buffer: None, + + width: 256, + height: 256, + + max_px_width: 0, + pixel_data: (vec![], vec![]), + + window: (layer_surface, surface), + + pool, + }; + + loop { + queue.blocking_dispatch(&mut me)?; + + if me.should_exit { + break; + } + } + + Ok(()) +} + +// /// Calculate which amount of the current row (`i`) should be painted, if we want a corner +// /// rounding of percent `p` and have an total of `n` rows. +// fn row_slice(n_u32: u32, i_u32: u32, p: f64) -> f64 { +// fn within_tolerance(a: f64, b: f64) -> bool { +// const ALLOWED_ERROR: f64 = 0.000_000_1; +// +// (a - b).abs() < ALLOWED_ERROR +// } +// +// let i = f64::from(i_u32); +// let n = f64::from(n_u32); +// +// let out = p + (1.0 - p) * (PI * i / n).sin(); +// +// assert!(out >= 0.0); +// assert!(out <= 1.0); +// +// if i_u32 == 0 || i_u32 == n_u32 { +// assert!(within_tolerance(out, p)); +// } +// +// if i_u32 < n_u32 / 2 { +// assert!(within_tolerance(out, row_slice(n_u32, n_u32 - i_u32, p))); +// } +// +// out +// } diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/render/layout.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/render/layout.rs new file mode 100644 index 00000000..7f0aaec9 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/render/layout.rs @@ -0,0 +1,57 @@ +use ab_glyph::{point, Font, Glyph, Point, ScaleFont}; + +use crate::wayland::ansi::{StyledChar, StyledString}; + +/// Simple paragraph layout for glyphs into `target`. +/// Starts at position `(0, ascent)`. +/// +/// This is for testing and examples. +pub(super) fn layout_paragraph<F, SF, BF, BSF>( + font: SF, + bold_font: BSF, + position: Point, + max_width: f32, + text: &StyledString, + target: &mut Vec<(Glyph, StyledChar)>, +) where + F: Font, + SF: ScaleFont<F>, + BF: Font, + BSF: ScaleFont<BF>, +{ + let v_advance = font.height() + font.line_gap(); + let mut caret = position + point(0.0, font.ascent()); + let mut last_glyph: Option<Glyph> = None; + + for c in text.chars() { + if c.as_char().is_control() { + if c.as_char() == '\n' { + caret = point(position.x, caret.y + v_advance); + last_glyph = None; + } + continue; + } + + let mut glyph = if c.is_bold() { + bold_font.scaled_glyph(c.as_char()) + } else { + font.scaled_glyph(c.as_char()) + }; + + if let Some(previous) = last_glyph.take() { + caret.x += font.kern(previous.id, glyph.id); + } + glyph.position = caret; + + last_glyph = Some(glyph.clone()); + caret.x += font.h_advance(glyph.id); + + if !c.as_char().is_whitespace() && caret.x > position.x + max_width { + caret = point(position.x, caret.y + v_advance); + glyph.position = caret; + last_glyph = None; + } + + target.push((glyph, c)); + } +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/render/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/render/mod.rs new file mode 100644 index 00000000..e92def3c --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/render/mod.rs @@ -0,0 +1,129 @@ +use std::{fs::File, io::Read}; + +use ab_glyph::{point, Font, FontVec, PxScale, ScaleFont}; +use anyhow::{Context, Result}; +use font_kit::{ + family_name::FamilyName, handle::Handle, properties::Properties, source::SystemSource, +}; + +use crate::wayland::ansi::{Color, StyledString}; + +mod layout; + +fn get_font(weight: f32) -> Result<impl Font> { + let handle = SystemSource::new() + .select_best_match( + &[FamilyName::Monospace], + Properties::new().weight(font_kit::properties::Weight(weight)), + ) + .context("Failed to find a monospace font")?; + + match handle { + Handle::Path { path, font_index } => { + let data = { + let mut buffer = vec![]; + + let mut file = File::open(&path)?; + file.read_to_end(&mut buffer)?; + buffer + }; + + FontVec::try_from_vec_and_index(data, font_index).with_context(|| { + format!( + "Failed to load font at '{}' with index {}", + path.display(), + font_index + ) + }) + } + Handle::Memory { .. } => unimplemented!(), + } +} + +pub(super) type ColorVec = (Vec<f32>, Vec<Option<Color>>); +pub(super) fn text(input: &StyledString) -> Result<(ColorVec, (u32, u32))> { + let normal_font = get_font(400.0)?; + let bold_font = get_font(600.0)?; + + let height: f32 = 15.0; + let px_height = height.ceil() as usize; + + let scale = PxScale { + x: height, + y: height, + }; + + let scaled_font = normal_font.into_scaled(scale); + let bold_scaled_font = bold_font.into_scaled(scale); + + let mut glyphs = Vec::new(); + layout::layout_paragraph( + &scaled_font, + &bold_scaled_font, + point(0.0, 0.0), + 9999.0, + input, + &mut glyphs, + ); + + let px_width = glyphs + .iter() + .fold(0.0, |acc, (g, c)| { + let next = g.position.x + + if c.is_bold() { + bold_scaled_font.h_advance(g.id) + } else { + scaled_font.h_advance(g.id) + }; + + if next > acc { + next + } else { + acc + } + }) + .ceil() as usize; + + // Rasterise to a f32 alpha vec + let mut pixel_data = vec![0.0; px_width * px_height]; + let mut color_data = vec![None; px_width * px_height]; + for (g, c) in glyphs { + let maybe_glyph = if c.is_bold() { + bold_scaled_font.outline_glyph(g) + } else { + scaled_font.outline_glyph(g) + }; + + if let Some(og) = maybe_glyph { + let bounds = og.px_bounds(); + og.draw(|x, y, v| { + let x = x as f32 + bounds.min.x; + let y = y as f32 + bounds.min.y; + let next_idx = x as usize + y as usize * px_width; + + assure_idx(&mut pixel_data, next_idx, 0.0); + assure_idx(&mut color_data, next_idx, None); + + // save the coverage alpha + pixel_data[next_idx] += v; + color_data[next_idx] = c.color(); + }); + } + } + + let len = pixel_data.len(); + Ok(( + (pixel_data, color_data), + (px_width as u32, (len / px_width) as u32), + )) +} + +fn assure_idx<T: Copy + Clone>(pixel_data: &mut Vec<T>, next_idx: usize, fill: T) { + let last = pixel_data.len() - 1; + + if next_idx > last { + let needed = next_idx - last; + + pixel_data.extend(vec![fill; needed]); + } +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/river/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/river/mod.rs new file mode 100644 index 00000000..f17c7ac8 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/river/mod.rs @@ -0,0 +1 @@ +pub(crate) mod protocols; diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/river/protocols.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/river/protocols.rs new file mode 100644 index 00000000..e54b65e1 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/river/protocols.rs @@ -0,0 +1,28 @@ +pub(crate) mod river_protocols { + use wayland_client; + // import objects from the core protocol if needed + use wayland_client::protocol::{wl_output, wl_seat}; + + // This module hosts a low-level representation of the protocol objects + // you will not need to interact with it yourself, but the code generated + // by the generate_client_code! macro will use it + // import the interfaces from the core protocol if needed + + #[allow(non_upper_case_globals)] + pub(crate) mod __status { + use wayland_client::backend as wayland_backend; + use wayland_client::protocol::__interfaces::{ + wl_output_interface, wl_seat_interface, WL_OUTPUT_INTERFACE, WL_SEAT_INTERFACE, + }; + wayland_scanner::generate_interfaces!("./resources/river-status-unstable-v1.xml"); + } + + use self::__status::{ + ZRIVER_OUTPUT_STATUS_V1_INTERFACE, ZRIVER_SEAT_STATUS_V1_INTERFACE, + ZRIVER_STATUS_MANAGER_V1_INTERFACE, + }; + + // This macro generates the actual types that represent the wayland objects of + // your custom protocol + wayland_scanner::generate_client_code!("./resources/river-status-unstable-v1.xml"); +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/mod.rs new file mode 100644 index 00000000..65d3c590 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/mod.rs @@ -0,0 +1,21 @@ +#![allow(dead_code)] + +pub(crate) mod multi; +pub(crate) mod raw; +pub(crate) mod slot; + +use std::io; + +use wayland_client::globals::GlobalError; + +/// An error that may occur when creating a pool. +#[derive(Debug, thiserror::Error)] +pub enum CreatePoolError { + /// The [`wl_shm`] global is not bound. + #[error(transparent)] + Global(#[from] GlobalError), + + /// Error while allocating the shared memory. + #[error(transparent)] + Create(#[from] io::Error), +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/multi.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/multi.rs new file mode 100644 index 00000000..0b1fdc1b --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/multi.rs @@ -0,0 +1,437 @@ +//! A pool implementation which automatically manage buffers. +//! +//! This pool is built on the [`RawPool`]. +//! +//! The [`MultiPool`] takes a key which is used to identify buffers and tries to return the buffer associated to the key +//! if possible. If no buffer in the pool is associated to the key, it will create a new one. +//! +//! # Example +//! +//! ```rust +//! use smithay_client_toolkit::reexports::client::{ +//! QueueHandle, +//! protocol::wl_surface::WlSurface, +//! protocol::wl_shm::Format, +//! }; +//! use smithay_client_toolkit::shm::multi::MultiPool; +//! +//! struct WlFoo { +//! // The surface we'll draw on and the index of buffer associated to it +//! surface: (WlSurface, usize), +//! pool: MultiPool<(WlSurface, usize)> +//! } +//! +//! impl WlFoo { +//! fn draw(&mut self, qh: &QueueHandle<WlFoo>) { +//! let surface = &self.surface.0; +//! // We'll increment "i" until the pool can create a new buffer +//! // if there's no buffer associated with our surface and "i" or if +//! // a buffer with the obuffer associated with our surface and "i" is free for use. +//! // +//! // There's no limit to the amount of buffers we can allocate to our surface but since +//! // shm buffers are released fairly fast, it's unlikely we'll need more than double buffering. +//! for i in 0..2 { +//! self.surface.1 = i; +//! if let Ok((offset, buffer, slice)) = self.pool.create_buffer( +//! 100, +//! 100 * 4, +//! 100, +//! &self.surface, +//! Format::Argb8888, +//! ) { +//! /* +//! insert drawing code here +//! */ +//! surface.attach(Some(buffer), 0, 0); +//! surface.commit(); +//! // We exit the function after the draw. +//! return; +//! } +//! } +//! /* +//! If there's no buffer available we can for example request a frame callback +//! and trigger a redraw when it fires. +//! (not shown in this example) +//! */ +//! } +//! } +//! +//! fn draw(slice: &mut [u8]) { +//! todo!() +//! } +//! +//! ``` +//! + +use std::borrow::Borrow; +use std::io; +use std::os::unix::io::OwnedFd; + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use wayland_client::backend::protocol::Message; +use wayland_client::backend::{Backend, ObjectData, ObjectId}; +use wayland_client::{ + protocol::{wl_buffer, wl_shm}, + Proxy, +}; + +use crate::wayland::shm::CreatePoolError; + +use super::raw::RawPool; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum PoolError { + #[error("buffer is currently used")] + InUse, + #[error("buffer is overlapping another")] + Overlap, + #[error("buffer could not be found")] + NotFound, +} + +/// This pool manages buffers associated with keys. +/// Only one buffer can be attributed to a given key. +#[derive(Debug)] +pub(crate) struct MultiPool<K> { + buffer_list: Vec<BufferSlot<K>>, + pub(crate) inner: RawPool, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) struct BufferSlot<K> { + free: Arc<AtomicBool>, + size: usize, + used: usize, + offset: usize, + buffer: Option<wl_buffer::WlBuffer>, + key: K, +} + +impl<K> Drop for BufferSlot<K> { + fn drop(&mut self) { + self.destroy().ok(); + } +} + +impl<K> BufferSlot<K> { + pub(crate) fn destroy(&self) -> Result<(), PoolError> { + self.buffer + .as_ref() + .ok_or(PoolError::NotFound) + .and_then(|buffer| { + self.free + .load(Ordering::Relaxed) + .then(|| buffer.destroy()) + .ok_or(PoolError::InUse) + }) + } +} + +impl<K> MultiPool<K> { + pub(crate) fn new(shm: &wl_shm::WlShm) -> Result<Self, CreatePoolError> { + Ok(Self { + inner: RawPool::new(4096, shm)?, + buffer_list: Vec::new(), + }) + } + + /// Resizes the memory pool, notifying the server the pool has changed in size. + /// + /// The [`wl_shm`] protocol only allows the pool to be made bigger. If the new size is smaller than the + /// current size of the pool, this function will do nothing. + pub(crate) fn resize(&mut self, size: usize) -> io::Result<()> { + self.inner.resize(size) + } + + /// Removes the buffer with the given key from the pool and rearranges the others. + pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<BufferSlot<K>> + where + Q: PartialEq, + K: Borrow<Q>, + { + self.buffer_list + .iter() + .enumerate() + .find(|(_, slot)| slot.key.borrow().eq(key)) + .map(|(i, _)| i) + .map(|i| self.buffer_list.remove(i)) + } + + /// Insert a buffer into the pool. + /// + /// The parameters are: + /// + /// - `width`: the width of this buffer (in pixels) + /// - `height`: the height of this buffer (in pixels) + /// - `stride`: distance (in bytes) between the beginning of a row and the next one + /// - `key`: a borrowed form of the stored key type + /// - `format`: the encoding format of the pixels. + pub(crate) fn insert<Q>( + &mut self, + width: i32, + stride: i32, + height: i32, + key: &Q, + format: wl_shm::Format, + ) -> Result<usize, PoolError> + where + K: Borrow<Q>, + Q: PartialEq + ToOwned<Owned = K>, + { + let mut offset = 0; + let mut found_key = false; + let size = (stride * height) as usize; + let mut index = Err(PoolError::NotFound); + + for (i, buf_slot) in self.buffer_list.iter_mut().enumerate() { + if buf_slot.key.borrow().eq(key) { + found_key = true; + if buf_slot.free.load(Ordering::Relaxed) { + // Destroys the buffer if it's resized + if size != buf_slot.used { + if let Some(buffer) = buf_slot.buffer.take() { + buffer.destroy(); + } + } + // Increases the size of the Buffer if it's too small and add 5% padding. + // It is possible this buffer overlaps the following but the else if + // statement prevents this buffer from being returned if that's the case. + buf_slot.size = buf_slot.size.max(size + size / 20); + index = Ok(i); + } else { + index = Err(PoolError::InUse); + } + // If a buffer is resized, it is likely that the followings might overlap + } else if offset > buf_slot.offset { + // When the buffer is free, it's safe to shift it because we know the compositor won't try to read it. + if buf_slot.free.load(Ordering::Relaxed) { + if offset != buf_slot.offset { + if let Some(buffer) = buf_slot.buffer.take() { + buffer.destroy(); + } + } + buf_slot.offset = offset; + } else { + // If one of the overlapping buffers is busy, then no buffer can be returned because it could result in a data race. + index = Err(PoolError::InUse); + } + } else if found_key { + break; + } + let size = (buf_slot.size + 63) & !63; + offset += size; + } + + if !found_key { + if let Err(err) = index { + return self + .dyn_resize(offset, width, stride, height, key.to_owned(), format) + .map(|()| self.buffer_list.len() - 1) + .ok_or(err); + } + } + + index + } + + /// Retreives the buffer associated with the given key. + /// + /// The parameters are: + /// + /// - `width`: the width of this buffer (in pixels) + /// - `height`: the height of this buffer (in pixels) + /// - `stride`: distance (in bytes) between the beginning of a row and the next one + /// - `key`: a borrowed form of the stored key type + /// - `format`: the encoding format of the pixels. + pub(crate) fn get<Q>( + &mut self, + width: i32, + stride: i32, + height: i32, + key: &Q, + format: wl_shm::Format, + ) -> Option<(usize, &wl_buffer::WlBuffer, &mut [u8])> + where + Q: PartialEq, + K: Borrow<Q>, + { + let len = self.inner.len(); + let size = (stride * height) as usize; + let buf_slot = self + .buffer_list + .iter_mut() + .find(|buf_slot| buf_slot.key.borrow().eq(key))?; + + if buf_slot.size >= size { + return None; + } + + buf_slot.used = size; + let offset = buf_slot.offset; + if buf_slot.buffer.is_none() { + if offset + size > len { + self.inner.resize(offset + size + size / 20).ok()?; + } + let free = Arc::new(AtomicBool::new(true)); + let data = BufferObjectData { free: free.clone() }; + let buffer = self.inner.create_buffer_raw( + offset as i32, + width, + height, + stride, + format, + Arc::new(data), + ); + buf_slot.free = free; + buf_slot.buffer = Some(buffer); + } + let buf = buf_slot.buffer.as_ref()?; + buf_slot.free.store(false, Ordering::Relaxed); + Some((offset, buf, &mut self.inner.mmap()[offset..][..size])) + } + + /// Returns the buffer associated with the given key and its offset (usize) in the mempool. + /// + /// The parameters are: + /// + /// - `width`: the width of this buffer (in pixels) + /// - `height`: the height of this buffer (in pixels) + /// - `stride`: distance (in bytes) between the beginning of a row and the next one + /// - `key`: a borrowed form of the stored key type + /// - `format`: the encoding format of the pixels. + /// + /// The offset can be used to determine whether or not a buffer was moved in the mempool + /// and by consequence if it should be damaged partially or fully. + pub(crate) fn create_buffer<Q>( + &mut self, + width: i32, + stride: i32, + height: i32, + key: &Q, + format: wl_shm::Format, + ) -> Result<(usize, &wl_buffer::WlBuffer, &mut [u8]), PoolError> + where + K: Borrow<Q>, + Q: PartialEq + ToOwned<Owned = K>, + { + let index = self.insert(width, stride, height, key, format)?; + self.get_at(index, width, stride, height, format) + } + + /// Retreives the buffer at the given index. + fn get_at( + &mut self, + index: usize, + width: i32, + stride: i32, + height: i32, + format: wl_shm::Format, + ) -> Result<(usize, &wl_buffer::WlBuffer, &mut [u8]), PoolError> { + let len = self.inner.len(); + let size = (stride * height) as usize; + let buf_slot = self.buffer_list.get_mut(index).ok_or(PoolError::NotFound)?; + + if size > buf_slot.size { + return Err(PoolError::Overlap); + } + + buf_slot.used = size; + let offset = buf_slot.offset; + if buf_slot.buffer.is_none() { + if offset + size > len { + self.inner + .resize(offset + size + size / 20) + .map_err(|_| PoolError::Overlap)?; + } + let free = Arc::new(AtomicBool::new(true)); + let data = BufferObjectData { free: free.clone() }; + let buffer = self.inner.create_buffer_raw( + offset as i32, + width, + height, + stride, + format, + Arc::new(data), + ); + buf_slot.free = free; + buf_slot.buffer = Some(buffer); + } + buf_slot.free.store(false, Ordering::Relaxed); + let buf = buf_slot.buffer.as_ref().unwrap(); + Ok((offset, buf, &mut self.inner.mmap()[offset..][..size])) + } + + /// Calcule the offet and size of a buffer based on its stride. + fn offset(mut offset: i32, stride: i32, height: i32) -> (usize, usize) { + // bytes per pixel + let size = stride * height; + // 5% padding. + offset += offset / 20; + offset = (offset + 63) & !63; + (offset as usize, size as usize) + } + + #[allow(clippy::too_many_arguments)] + /// Resizes the pool and appends a new buffer. + fn dyn_resize( + &mut self, + offset: usize, + width: i32, + stride: i32, + height: i32, + key: K, + format: wl_shm::Format, + ) -> Option<()> { + let (offset, size) = Self::offset(offset as i32, stride, height); + if self.inner.len() < offset + size { + self.resize(offset + size + size / 20).ok()?; + } + let free = Arc::new(AtomicBool::new(true)); + let data = BufferObjectData { free: free.clone() }; + let buffer = self.inner.create_buffer_raw( + offset as i32, + width, + height, + stride, + format, + Arc::new(data), + ); + self.buffer_list.push(BufferSlot { + offset, + used: 0, + free, + buffer: Some(buffer), + size, + key, + }); + Some(()) + } +} + +struct BufferObjectData { + free: Arc<AtomicBool>, +} + +impl ObjectData for BufferObjectData { + fn event( + self: Arc<Self>, + _backend: &Backend, + msg: Message<ObjectId, OwnedFd>, + ) -> Option<Arc<dyn ObjectData>> { + debug_assert!(wayland_client::backend::protocol::same_interface( + msg.sender_id.interface(), + wl_buffer::WlBuffer::interface() + )); + debug_assert!(msg.opcode == 0); + + // wl_buffer only has a single event: wl_buffer.release + self.free.store(true, Ordering::Relaxed); + + None + } + + fn destroyed(&self, _: ObjectId) {} +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/raw.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/raw.rs new file mode 100644 index 00000000..a12afaa0 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/raw.rs @@ -0,0 +1,290 @@ +//! A raw shared memory pool handler. +//! +//! This is intended as a safe building block for higher level shared memory pool abstractions and is not +//! encouraged for most library users. + +use rustix::{ + io::Errno, + shm::{Mode, OFlags}, +}; +use std::{ + fs::File, + io, + ops::Deref, + os::unix::prelude::{AsFd, BorrowedFd, OwnedFd}, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use memmap2::MmapMut; +use wayland_client::{ + backend::ObjectData, + protocol::{wl_buffer, wl_shm, wl_shm_pool}, + Dispatch, Proxy, QueueHandle, WEnum, +}; + +use super::CreatePoolError; + +/// A raw handler for file backed shared memory pools. +/// +/// This type of pool will create the SHM memory pool and provide a way to resize the pool. +/// +/// This pool does not release buffers. If you need this, use one of the higher level pools. +#[derive(Debug)] +pub struct RawPool { + pool: DestroyOnDropPool, + len: usize, + mem_file: File, + mmap: MmapMut, +} + +impl RawPool { + pub fn new(len: usize, shm: &wl_shm::WlShm) -> Result<RawPool, CreatePoolError> { + let shm_fd = RawPool::create_shm_fd()?; + let mem_file = File::from(shm_fd); + mem_file.set_len(len as u64)?; + + let pool = shm + .send_constructor( + wl_shm::Request::CreatePool { + fd: mem_file.as_fd(), + size: len as i32, + }, + Arc::new(ShmPoolData), + ) + .unwrap_or_else(|_| Proxy::inert(shm.backend().clone())); + let mmap = unsafe { MmapMut::map_mut(&mem_file)? }; + + Ok(RawPool { + pool: DestroyOnDropPool(pool), + len, + mem_file, + mmap, + }) + } + + /// Resizes the memory pool, notifying the server the pool has changed in size. + /// + /// The [`wl_shm`] protocol only allows the pool to be made bigger. If the new size is smaller than the + /// current size of the pool, this function will do nothing. + pub fn resize(&mut self, size: usize) -> io::Result<()> { + if size > self.len { + self.len = size; + self.mem_file.set_len(size as u64)?; + self.pool.resize(size as i32); + self.mmap = unsafe { MmapMut::map_mut(&self.mem_file) }?; + } + + Ok(()) + } + + /// Returns a reference to the underlying shared memory file using the memmap2 crate. + pub fn mmap(&mut self) -> &mut MmapMut { + &mut self.mmap + } + + /// Returns the size of the mempool + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.len + } + + /// Create a new buffer to this pool. + /// + /// ## Parameters + /// - `offset`: the offset (in bytes) from the beginning of the pool at which this buffer starts. + /// - `width` and `height`: the width and height of the buffer in pixels. + /// - `stride`: distance (in bytes) between the beginning of a row and the next one. + /// - `format`: the encoding format of the pixels. + /// + /// The encoding format of the pixels must be supported by the compositor or else a protocol error is + /// risen. You can ensure the format is supported by listening to [`Shm::formats`](crate::shm::Shm::formats). + /// + /// Note this function only creates the [`wl_buffer`] object, you will need to write to the pixels using the + /// [`io::Write`] implementation or [`RawPool::mmap`]. + #[allow(clippy::too_many_arguments)] + pub fn create_buffer<D, U>( + &mut self, + offset: i32, + width: i32, + height: i32, + stride: i32, + format: wl_shm::Format, + udata: U, + qh: &QueueHandle<D>, + ) -> wl_buffer::WlBuffer + where + D: Dispatch<wl_buffer::WlBuffer, U> + 'static, + U: Send + Sync + 'static, + { + self.pool + .create_buffer(offset, width, height, stride, format, qh, udata) + } + + /// Create a new buffer to this pool. + /// + /// This is identical to [`Self::create_buffer`], but allows using a custom [`ObjectData`] + /// implementation instead of relying on the [Dispatch] interface. + #[allow(clippy::too_many_arguments)] + pub fn create_buffer_raw( + &mut self, + offset: i32, + width: i32, + height: i32, + stride: i32, + format: wl_shm::Format, + data: Arc<dyn ObjectData + 'static>, + ) -> wl_buffer::WlBuffer { + self.pool + .send_constructor( + wl_shm_pool::Request::CreateBuffer { + offset, + width, + height, + stride, + format: WEnum::Value(format), + }, + data, + ) + .unwrap_or_else(|_| Proxy::inert(self.pool.backend().clone())) + } + + /// Returns the pool object used to communicate with the server. + pub fn pool(&self) -> &wl_shm_pool::WlShmPool { + &self.pool + } +} + +impl AsFd for RawPool { + fn as_fd(&self) -> BorrowedFd<'_> { + self.mem_file.as_fd() + } +} + +impl From<RawPool> for OwnedFd { + fn from(pool: RawPool) -> Self { + pool.mem_file.into() + } +} + +impl io::Write for RawPool { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + io::Write::write(&mut self.mem_file, buf) + } + + fn flush(&mut self) -> io::Result<()> { + io::Write::flush(&mut self.mem_file) + } +} + +impl io::Seek for RawPool { + fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> { + io::Seek::seek(&mut self.mem_file, pos) + } +} + +impl RawPool { + fn create_shm_fd() -> io::Result<OwnedFd> { + #[cfg(target_os = "linux")] + { + match RawPool::create_memfd() { + Ok(fd) => return Ok(fd), + + // Not supported, use fallback. + Err(Errno::NOSYS) => (), + + Err(err) => return Err(Into::<io::Error>::into(err)), + } + } + + let time = SystemTime::now(); + let mut mem_file_handle = format!( + "/smithay-client-toolkit-{}", + time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + + loop { + let flags = OFlags::CREATE | OFlags::EXCL | OFlags::RDWR; + + let mode = Mode::RUSR | Mode::WUSR; + + match rustix::shm::open(mem_file_handle.as_str(), flags, mode) { + Ok(fd) => match rustix::shm::unlink(mem_file_handle.as_str()) { + Ok(()) => return Ok(fd), + + Err(errno) => { + return Err(errno.into()); + } + }, + + Err(Errno::EXIST) => { + // Change the handle if we happen to be duplicate. + let time = SystemTime::now(); + + mem_file_handle = format!( + "/smithay-client-toolkit-{}", + time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + } + + Err(Errno::INTR) => (), + + Err(err) => return Err(err.into()), + } + } + } + + #[cfg(target_os = "linux")] + fn create_memfd() -> rustix::io::Result<OwnedFd> { + use rustix::fs::{MemfdFlags, SealFlags}; + + loop { + let name = c"smithay-client-toolkit"; + let flags = MemfdFlags::ALLOW_SEALING | MemfdFlags::CLOEXEC; + + match rustix::fs::memfd_create(name, flags) { + Ok(fd) => { + // We only need to seal for the purposes of optimization, ignore the errors. + let _ = rustix::fs::fcntl_add_seals(&fd, SealFlags::SHRINK | SealFlags::SEAL); + return Ok(fd); + } + + Err(Errno::INTR) => (), + + Err(err) => return Err(err), + } + } + } +} + +#[derive(Debug)] +struct DestroyOnDropPool(wl_shm_pool::WlShmPool); + +impl Deref for DestroyOnDropPool { + type Target = wl_shm_pool::WlShmPool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for DestroyOnDropPool { + fn drop(&mut self) { + self.0.destroy(); + } +} + +#[derive(Debug)] +struct ShmPoolData; + +impl ObjectData for ShmPoolData { + fn event( + self: Arc<Self>, + _: &wayland_client::backend::Backend, + _: wayland_client::backend::protocol::Message<wayland_client::backend::ObjectId, OwnedFd>, + ) -> Option<Arc<(dyn ObjectData + 'static)>> { + unreachable!("wl_shm_pool has no events") + } + + fn destroyed(&self, _: wayland_client::backend::ObjectId) {} +} diff --git a/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/slot.rs b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/slot.rs new file mode 100644 index 00000000..ab52c5f6 --- /dev/null +++ b/pkgs/by-name/ri/river-mk-keymap/src/wayland/shm/slot.rs @@ -0,0 +1,596 @@ +//! A pool implementation based on buffer slots + +use std::io; +use std::{ + os::unix::io::{AsRawFd, OwnedFd}, + sync::{ + atomic::{AtomicU8, AtomicUsize, Ordering}, + Arc, Mutex, Weak, + }, +}; + +use wayland_client::backend::protocol::Message; +use wayland_client::backend::{ObjectData, ObjectId}; +use wayland_client::{ + protocol::{wl_buffer, wl_shm, wl_surface}, + Proxy, +}; + +use crate::wayland::shm::raw::RawPool; +use crate::wayland::shm::CreatePoolError; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum CreateBufferError { + /// Slot creation error. + #[error(transparent)] + Io(#[from] io::Error), + + /// Pool mismatch. + #[error("Incorrect pool for slot")] + PoolMismatch, + + /// Slot size mismatch + #[error("Requested buffer size is too large for slot")] + SlotTooSmall, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum ActivateSlotError { + /// Buffer was already active + #[error("Buffer was already active")] + AlreadyActive, +} + +#[derive(Debug)] +pub(crate) struct SlotPool { + pub(crate) inner: RawPool, + free_list: Arc<Mutex<Vec<FreelistEntry>>>, +} + +#[derive(Debug)] +struct FreelistEntry { + offset: usize, + len: usize, +} + +/// A chunk of memory allocated from a [`SlotPool`] +/// +/// Retaining this object is only required if you wish to resize or change the buffer's format +/// without changing the contents of the backing memory. +#[derive(Debug)] +pub(crate) struct Slot { + inner: Arc<SlotInner>, +} + +#[derive(Debug)] +struct SlotInner { + free_list: Weak<Mutex<Vec<FreelistEntry>>>, + offset: usize, + len: usize, + active_buffers: AtomicUsize, + /// Count of all "real" references to this slot. This includes all Slot objects and any + /// [`BufferData`] object that is not in the DEAD state. When this reaches zero, the memory for + /// this slot will return to the [`free_list`]. It is not possible for it to reach zero and have a + /// Slot or Buffer referring to it. + all_refs: AtomicUsize, +} + +/// A wrapper around a [`wl_buffer::WlBuffer`] which has been allocated via a [`SlotPool`]. +/// +/// When this object is dropped, the buffer will be destroyed immediately if it is not active, or +/// upon the server's release if it is. +#[derive(Debug)] +pub(crate) struct Buffer { + inner: wl_buffer::WlBuffer, + height: i32, + stride: i32, + slot: Slot, +} + +/// [`ObjectData`] for the [`WlBuffer`] +#[derive(Debug)] +struct BufferData { + inner: Arc<SlotInner>, + state: AtomicU8, +} + +// These constants define the value of BufferData::state, since AtomicEnum does not exist. +impl BufferData { + /// Buffer is counted in [`active_buffers`] list; will return to INACTIVE on Release. + const ACTIVE: u8 = 0; + + /// Buffer is not counted in [`active_buffers`] list, but also has not been destroyed. + const INACTIVE: u8 = 1; + + /// Buffer is counted in [`active_buffers`] list; will move to DEAD on Release + const DESTROY_ON_RELEASE: u8 = 2; + + /// Buffer has been destroyed + const DEAD: u8 = 3; + + /// Value that is [`ORed`] on buffer release to transition to the next state + const RELEASE_SET: u8 = 1; + + /// Value that is [`ORed`] on buffer destroy to transition to the next state + const DESTROY_SET: u8 = 2; + + /// Call after successfully transitioning the state to DEAD + fn record_death(&self) { + drop(Slot { + inner: self.inner.clone(), + }); + } +} + +impl SlotPool { + pub(crate) fn new(len: usize, shm: &wl_shm::WlShm) -> Result<Self, CreatePoolError> { + let inner = RawPool::new(len, shm)?; + let free_list = Arc::new(Mutex::new(vec![FreelistEntry { + offset: 0, + len: inner.len(), + }])); + Ok(SlotPool { inner, free_list }) + } + + /// Create a new buffer in a new slot. + /// + /// This returns the buffer and the canvas. The parameters are: + /// + /// - `width`: the width of this buffer (in pixels) + /// - `height`: the height of this buffer (in pixels) + /// - `stride`: distance (in bytes) between the beginning of a row and the next one + /// - `format`: the encoding format of the pixels. Using a format that was not + /// advertised to the `wl_shm` global by the server is a protocol error and will + /// terminate your connection. + /// + /// The [Slot] for this buffer will have exactly the size required for the data. It can be + /// accessed via [`Buffer::slot`] to create additional buffers that point to the same data. This + /// is required if you wish to change formats, buffer dimensions, or attach a canvas to + /// multiple surfaces. + /// + /// For more control over sizing, use [`Self::new_slot`] and [`Self::create_buffer_in`]. + pub(crate) fn create_buffer( + &mut self, + width: i32, + height: i32, + stride: i32, + format: wl_shm::Format, + ) -> Result<(Buffer, &mut [u8]), CreateBufferError> { + let len = (height as usize) * (stride as usize); + let slot = self.new_slot(len)?; + let buffer = self.create_buffer_in(&slot, width, height, stride, format)?; + let canvas = self.raw_data_mut(&slot); + Ok((buffer, canvas)) + } + + /// Get the bytes corresponding to a given slot or buffer if drawing to the slot is permitted. + /// + /// Returns `None` if there are active buffers in the slot or if the slot does not correspond + /// to this pool. + pub(crate) fn canvas(&mut self, key: &impl CanvasKey) -> Option<&mut [u8]> { + key.canvas(self) + } + + /// Returns the size, in bytes, of this pool. + #[allow(clippy::len_without_is_empty)] + pub(crate) fn len(&self) -> usize { + self.inner.len() + } + + /// Resizes the memory pool, notifying the server the pool has changed in size. + /// + /// This is an optimization; the pool automatically resizes when you allocate new slots. + pub(crate) fn resize(&mut self, size: usize) -> io::Result<()> { + let old_len = self.inner.len(); + self.inner.resize(size)?; + let new_len = self.inner.len(); + if old_len == new_len { + return Ok(()); + } + // add the new memory to the freelist + let mut free = self.free_list.lock().unwrap(); + if let Some(FreelistEntry { offset, len }) = free.last_mut() { + if *offset + *len == old_len { + *len += new_len - old_len; + return Ok(()); + } + } + free.push(FreelistEntry { + offset: old_len, + len: new_len - old_len, + }); + Ok(()) + } + + fn alloc(&mut self, size: usize) -> io::Result<usize> { + let mut free = self.free_list.lock().unwrap(); + for FreelistEntry { offset, len } in free.iter_mut() { + if *len >= size { + let rv = *offset; + *len -= size; + *offset += size; + return Ok(rv); + } + } + let mut rv = self.inner.len(); + let mut pop_tail = false; + if let Some(FreelistEntry { offset, len }) = free.last() { + if offset + len == self.inner.len() { + rv -= len; + pop_tail = true; + } + } + // resize like Vec::reserve, always at least doubling + let target = std::cmp::max(rv + size, self.inner.len() * 2); + self.inner.resize(target)?; + // adjust the end of the freelist here + if pop_tail { + free.pop(); + } + if target > rv + size { + free.push(FreelistEntry { + offset: rv + size, + len: target - rv - size, + }); + } + Ok(rv) + } + + fn free(free_list: &Mutex<Vec<FreelistEntry>>, mut offset: usize, mut len: usize) { + let mut free = free_list.lock().unwrap(); + let mut nf = Vec::with_capacity(free.len() + 1); + for &FreelistEntry { + offset: ioff, + len: ilen, + } in free.iter() + { + if ioff + ilen == offset { + offset = ioff; + len += ilen; + continue; + } + if ioff == offset + len { + len += ilen; + continue; + } + if ioff > offset + len && len != 0 { + nf.push(FreelistEntry { offset, len }); + len = 0; + } + if ilen != 0 { + nf.push(FreelistEntry { + offset: ioff, + len: ilen, + }); + } + } + if len != 0 { + nf.push(FreelistEntry { offset, len }); + } + *free = nf; + } + + /// Create a new slot with the given size in bytes. + pub(crate) fn new_slot(&mut self, mut len: usize) -> io::Result<Slot> { + len = (len + 63) & !63; + let offset = self.alloc(len)?; + + Ok(Slot { + inner: Arc::new(SlotInner { + free_list: Arc::downgrade(&self.free_list), + offset, + len, + active_buffers: AtomicUsize::new(0), + all_refs: AtomicUsize::new(1), + }), + }) + } + + /// Get the bytes corresponding to a given slot. + /// + /// Note: prefer using [`Self::canvas`], which will prevent drawing to a buffer that has not been + /// released by the server. + /// + /// Returns an empty buffer if the slot does not belong to this pool. + pub(crate) fn raw_data_mut(&mut self, slot: &Slot) -> &mut [u8] { + if slot.inner.free_list.as_ptr() == Arc::as_ptr(&self.free_list) { + &mut self.inner.mmap()[slot.inner.offset..][..slot.inner.len] + } else { + &mut [] + } + } + + /// Create a new buffer corresponding to a slot. + /// + /// The parameters are: + /// + /// - `width`: the width of this buffer (in pixels) + /// - `height`: the height of this buffer (in pixels) + /// - `stride`: distance (in bytes) between the beginning of a row and the next one + /// - `format`: the encoding format of the pixels. Using a format that was not + /// advertised to the `wl_shm` global by the server is a protocol error and will + /// terminate your connection + pub(crate) fn create_buffer_in( + &mut self, + slot: &Slot, + width: i32, + height: i32, + stride: i32, + format: wl_shm::Format, + ) -> Result<Buffer, CreateBufferError> { + let offset = slot.inner.offset as i32; + let len = (height as usize) * (stride as usize); + if len > slot.inner.len { + return Err(CreateBufferError::SlotTooSmall); + } + + if slot.inner.free_list.as_ptr() != Arc::as_ptr(&self.free_list) { + return Err(CreateBufferError::PoolMismatch); + } + + let slot = slot.clone(); + // take a ref for the BufferData, which will be destroyed by BufferData::record_death + slot.inner.all_refs.fetch_add(1, Ordering::Relaxed); + let data = Arc::new(BufferData { + inner: slot.inner.clone(), + state: AtomicU8::new(BufferData::INACTIVE), + }); + let buffer = self + .inner + .create_buffer_raw(offset, width, height, stride, format, data); + Ok(Buffer { + inner: buffer, + height, + stride, + slot, + }) + } +} + +impl Clone for Slot { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + inner.all_refs.fetch_add(1, Ordering::Relaxed); + Slot { inner } + } +} + +impl Drop for Slot { + fn drop(&mut self) { + if self.inner.all_refs.fetch_sub(1, Ordering::Relaxed) == 1 { + if let Some(free_list) = self.inner.free_list.upgrade() { + SlotPool::free(&free_list, self.inner.offset, self.inner.len); + } + } + } +} + +impl Drop for SlotInner { + fn drop(&mut self) { + debug_assert_eq!(*self.all_refs.get_mut(), 0); + } +} + +/// A helper trait for [`SlotPool::canvas`]. +pub(crate) trait CanvasKey { + fn canvas<'pool>(&self, pool: &'pool mut SlotPool) -> Option<&'pool mut [u8]>; +} + +impl Slot { + /// Return true if there are buffers referencing this slot whose contents are being accessed + /// by the server. + pub(crate) fn has_active_buffers(&self) -> bool { + self.inner.active_buffers.load(Ordering::Relaxed) != 0 + } + + /// Returns the size, in bytes, of this slot. + #[allow(clippy::len_without_is_empty)] + pub(crate) fn len(&self) -> usize { + self.inner.len + } + + /// Get the bytes corresponding to a given slot if drawing to the slot is permitted. + /// + /// Returns `None` if there are active buffers in the slot or if the slot does not correspond + /// to this pool. + pub(crate) fn canvas<'pool>(&self, pool: &'pool mut SlotPool) -> Option<&'pool mut [u8]> { + if self.has_active_buffers() { + return None; + } + if self.inner.free_list.as_ptr() == Arc::as_ptr(&pool.free_list) { + Some(&mut pool.inner.mmap()[self.inner.offset..][..self.inner.len]) + } else { + None + } + } +} + +impl CanvasKey for Slot { + fn canvas<'pool>(&self, pool: &'pool mut SlotPool) -> Option<&'pool mut [u8]> { + self.canvas(pool) + } +} + +impl Buffer { + /// Attach a buffer to a surface. + /// + /// This marks the slot as active until the server releases the buffer, which will happen + /// automatically assuming the surface is committed without attaching a different buffer. + /// + /// Note: if you need to ensure that [`canvas()`](Buffer::canvas) calls never return data that + /// could be attached to a surface in a multi-threaded client, make this call while you have + /// exclusive access to the corresponding [`SlotPool`]. + pub(crate) fn attach_to(&self, surface: &wl_surface::WlSurface) -> Result<(), ActivateSlotError> { + self.activate()?; + surface.attach(Some(&self.inner), 0, 0); + Ok(()) + } + + /// Get the inner buffer. + pub(crate) fn wl_buffer(&self) -> &wl_buffer::WlBuffer { + &self.inner + } + + pub(crate) fn height(&self) -> i32 { + self.height + } + + pub(crate) fn stride(&self) -> i32 { + self.stride + } + + fn data(&self) -> Option<&BufferData> { + self.inner.object_data()?.downcast_ref() + } + + /// Get the bytes corresponding to this buffer if drawing is permitted. + /// + /// This may be smaller than the canvas associated with the slot. + pub(crate) fn canvas<'pool>(&self, pool: &'pool mut SlotPool) -> Option<&'pool mut [u8]> { + let len = (self.height as usize) * (self.stride as usize); + if self.slot.inner.active_buffers.load(Ordering::Relaxed) != 0 { + return None; + } + if self.slot.inner.free_list.as_ptr() == Arc::as_ptr(&pool.free_list) { + Some(&mut pool.inner.mmap()[self.slot.inner.offset..][..len]) + } else { + None + } + } + + /// Get the slot corresponding to this buffer. + pub(crate) fn slot(&self) -> Slot { + self.slot.clone() + } + + /// Manually mark a buffer as active. + /// + /// An active buffer prevents drawing on its slot until a Release event is received or until + /// manually deactivated. + pub(crate) fn activate(&self) -> Result<(), ActivateSlotError> { + let data = self.data().expect("UserData type mismatch"); + + // This bitwise AND will transition INACTIVE -> ACTIVE, or do nothing if the buffer was + // already ACTIVE. No other ordering is required, as the server will not send a Release + // until we send our attach after returning Ok. + match data + .state + .fetch_and(!BufferData::RELEASE_SET, Ordering::Relaxed) + { + BufferData::INACTIVE => { + data.inner.active_buffers.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + BufferData::ACTIVE => Err(ActivateSlotError::AlreadyActive), + _ => unreachable!("Invalid state in BufferData"), + } + } + + /// Manually mark a buffer as inactive. + /// + /// This should be used when the buffer was manually marked as active or when a buffer was + /// attached to a surface but not committed. Calling this function on a buffer that was + /// committed to a surface risks making the surface contents undefined. + pub(crate) fn deactivate(&self) -> Result<(), ActivateSlotError> { + let data = self.data().expect("UserData type mismatch"); + + // Same operation as the Release event, but we know the Buffer was not dropped. + match data + .state + .fetch_or(BufferData::RELEASE_SET, Ordering::Relaxed) + { + BufferData::ACTIVE => { + data.inner.active_buffers.fetch_sub(1, Ordering::Relaxed); + Ok(()) + } + BufferData::INACTIVE => Err(ActivateSlotError::AlreadyActive), + _ => unreachable!("Invalid state in BufferData"), + } + } +} + +impl CanvasKey for Buffer { + fn canvas<'pool>(&self, pool: &'pool mut SlotPool) -> Option<&'pool mut [u8]> { + self.canvas(pool) + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + if let Some(data) = self.data() { + match data + .state + .fetch_or(BufferData::DESTROY_SET, Ordering::Relaxed) + { + BufferData::ACTIVE => { + // server is using the buffer, let ObjectData handle the destroy + } + BufferData::INACTIVE => { + data.record_death(); + self.inner.destroy(); + } + _ => unreachable!("Invalid state in BufferData"), + } + } + } +} + +impl ObjectData for BufferData { + fn event( + self: Arc<Self>, + handle: &wayland_client::backend::Backend, + msg: Message<ObjectId, OwnedFd>, + ) -> Option<Arc<dyn ObjectData>> { + debug_assert!(wayland_client::backend::protocol::same_interface( + msg.sender_id.interface(), + wl_buffer::WlBuffer::interface() + )); + debug_assert!(msg.opcode == 0); + + match self + .state + .fetch_or(BufferData::RELEASE_SET, Ordering::Relaxed) + { + BufferData::ACTIVE => { + self.inner.active_buffers.fetch_sub(1, Ordering::Relaxed); + } + BufferData::INACTIVE => { + // possible spurious release, or someone called deactivate incorrectly + eprintln!("Unexpected WlBuffer::Release on an inactive buffer"); + } + BufferData::DESTROY_ON_RELEASE => { + self.record_death(); + self.inner.active_buffers.fetch_sub(1, Ordering::Relaxed); + + // The Destroy message is identical to Release message (no args, same ID), so just reply + handle + .send_request(msg.map_fd(|x| x.as_raw_fd()), None, None) + .expect("Unexpected invalid ID"); + } + BufferData::DEAD => { + // no-op, this object is already unusable + } + _ => unreachable!("Invalid state in BufferData"), + } + + None + } + + fn destroyed(&self, _: ObjectId) {} +} + +impl Drop for BufferData { + fn drop(&mut self) { + let state = *self.state.get_mut(); + if state == BufferData::ACTIVE || state == BufferData::DESTROY_ON_RELEASE { + // Release the active-buffer count + self.inner.active_buffers.fetch_sub(1, Ordering::Relaxed); + } + + if state != BufferData::DEAD { + // nobody has ever transitioned state to DEAD, so we are responsible for freeing the + // extra reference + self.record_death(); + } + } +} diff --git a/pkgs/by-name/ri/river-mk-keymap/update.sh b/pkgs/by-name/ri/river-mk-keymap/update.sh index 9268caf2..8e36e13e 100755 --- a/pkgs/by-name/ri/river-mk-keymap/update.sh +++ b/pkgs/by-name/ri/river-mk-keymap/update.sh @@ -1,3 +1,14 @@ #!/bin/sh -cargo update && cargo upgrade +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +[ "$1" = "upgrade" ] && cargo upgrade +cargo update diff --git a/pkgs/by-name/ri/river-start/package.nix b/pkgs/by-name/ri/river-start/package.nix deleted file mode 100644 index ab986708..00000000 --- a/pkgs/by-name/ri/river-start/package.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ - writeShellApplication, - river, -}: -writeShellApplication { - name = "river-start"; - text = builtins.readFile ./river-start.sh; - runtimeInputs = [ - river - ]; - meta = { - mainProgram = "river-start"; - }; -} diff --git a/pkgs/by-name/ri/river-start/river-start.sh b/pkgs/by-name/ri/river-start/river-start.sh deleted file mode 100755 index 4587ff96..00000000 --- a/pkgs/by-name/ri/river-start/river-start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env sh - -# NOTE: Keep this in sync with the file from `base_init.sh` <2025-02-03> -RIVER_LOG_FILE="$HOME/.local/share/river/log" - -[ -d "$(dirname "$RIVER_LOG_FILE")" ] || mkdir --parents "$(dirname "$RIVER_LOG_FILE")" - -exec river -log-level info >"$RIVER_LOG_FILE" 2>&1 - -# vim: ft=sh diff --git a/pkgs/by-name/sc/screenshot_persistent/package.nix b/pkgs/by-name/sc/screenshot_persistent/package.nix index 12bb3968..34054b0f 100644 --- a/pkgs/by-name/sc/screenshot_persistent/package.nix +++ b/pkgs/by-name/sc/screenshot_persistent/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { writeShellApplication, grim, diff --git a/pkgs/by-name/sc/screenshot_persistent/screenshot_persistent.sh b/pkgs/by-name/sc/screenshot_persistent/screenshot_persistent.sh index 2bea69e3..f67293cd 100755 --- a/pkgs/by-name/sc/screenshot_persistent/screenshot_persistent.sh +++ b/pkgs/by-name/sc/screenshot_persistent/screenshot_persistent.sh @@ -1,6 +1,16 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + # shellcheck shell=bash -tmp="$(mktemp)" +tmp="$(mktemp -t screenshot_persistent_XXXXX)" if grim -g "$(slurp)" "$tmp"; then name="$(rofi -dmenu -p "Name of screenshot: " -l 0)" diff --git a/pkgs/by-name/sc/screenshot_temporary/package.nix b/pkgs/by-name/sc/screenshot_temporary/package.nix index f3739b01..49bbeeb7 100644 --- a/pkgs/by-name/sc/screenshot_temporary/package.nix +++ b/pkgs/by-name/sc/screenshot_temporary/package.nix @@ -1,15 +1,24 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, + writeShellApplication, + # Dependencies grim, slurp, wl-clipboard, }: -sysLib.writeShellScript { +writeShellApplication { name = "screenshot_temporary"; - src = ./screenshot_temporary.sh; - generateCompletions = false; - keepPath = false; - dependencies = [ + text = builtins.readFile ./screenshot_temporary.sh; + inheritPath = false; + runtimeInputs = [ grim slurp wl-clipboard diff --git a/pkgs/by-name/sc/screenshot_temporary/screenshot_temporary.sh b/pkgs/by-name/sc/screenshot_temporary/screenshot_temporary.sh index 8968ca79..db1a79b7 100755 --- a/pkgs/by-name/sc/screenshot_temporary/screenshot_temporary.sh +++ b/pkgs/by-name/sc/screenshot_temporary/screenshot_temporary.sh @@ -1,7 +1,14 @@ #! /usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. grim -g "$(slurp)" | wl-copy diff --git a/pkgs/by-name/sn/snap-sync-forked/package.nix b/pkgs/by-name/sn/snap-sync-forked/package.nix deleted file mode 100644 index b3f40b24..00000000 --- a/pkgs/by-name/sn/snap-sync-forked/package.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ - sysLib, - bash, - btrfs-progs, - coreutils, - gawk, - gnugrep, - snapper, - util-linux, - # optional - libnotify, - openssh, - pv, - rsync, - sudo, -}: -sysLib.writeShellScript { - name = "snap-sync-forked"; - src = ./snap-sync-forked.sh; - dependencies = [ - bash - btrfs-progs - coreutils - gawk - gnugrep - snapper - util-linux - - # optional: - libnotify - openssh - pv - rsync - sudo - ]; -} diff --git a/pkgs/by-name/sn/snap-sync-forked/snap-sync-forked.sh b/pkgs/by-name/sn/snap-sync-forked/snap-sync-forked.sh deleted file mode 100755 index 3d9c1ac9..00000000 --- a/pkgs/by-name/sn/snap-sync-forked/snap-sync-forked.sh +++ /dev/null @@ -1,534 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -# -# snap-sync -# https://github.com/wesbarnett/snap-sync -# Copyright (C) 2016-2021 Wes Barnett - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., - -# ------------------------------------------------------------------------- - -# Takes snapshots of each snapper configuration. It then sends the snapshot to -# a location on an external drive. After the initial transfer, it does -# incremental snapshots on later calls. It's important not to delete the -# snapshot created on your system since that will be used to determine the -# difference for the next incremental snapshot. - -set -o errtrace - -version="0.7" -name="snap-sync" - -printf "\nsnap-sync version %s, Copyright (C) 2016-2021 Wes Barnett\n" "$version" -printf "snap-sync comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the license for more information. \n\n" - -# The following line is modified by the Makefile or -# find_snapper_config script -SNAPPER_CONFIG=/etc/sysconfig/snapper - -donotify=0 -if ! command -v notify-send &>/dev/null; then - donotify=1 -fi - -doprogress=0 -if ! command -v pv &>/dev/null; then - doprogress=1 -fi - -error() { - printf "==> ERROR: %s\n" "$@" - notify_error 'Error' 'Check journal for more information.' -} >&2 - -die() { - error "$@" - exit 1 -} - -traperror() { - printf "Exited due to error on line %s.\n" "$1" - printf "exit status: %s\n" "$2" - printf "command: %s\n" "$3" - printf "bash line: %s\n" "$4" - printf "function name: %s\n" "$5" - exit 1 -} - -trapkill() { - die "Exited due to user intervention." -} - -trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR -trap trapkill SIGTERM SIGINT - -usage() { - cat <<EOF -$name $version -Usage: $name [options] - -Options: - -c, --config <config> snapper configuration to backup - -d, --description <desc> snapper description - -h, --help print this message - -n, --noconfirm do not ask for confirmation - -k, --keepold keep old incremental snapshots instead of deleting them - after backup is performed - -p, --port <port> remote port; used with '--remote'. - -q, --quiet do not send notifications; instead print them. - -r, --remote <address> ip address of a remote machine to backup to - --sudo use sudo on the remote machine - -s, --subvolid <subvlid> subvolume id of the mounted BTRFS subvolume to back up to - -u, --UUID <UUID> UUID of the mounted BTRFS subvolume to back up to - -See 'man snap-sync' for more details. -EOF -} - -ssh="" -sudo=0 -while [[ $# -gt 0 ]]; do - key="$1" - case $key in - -d | --description) - description="$2" - shift 2 - ;; - -c | --config) - selected_configs="$2" - shift 2 - ;; - -u | --UUID) - uuid_cmdline="$2" - shift 2 - ;; - -s | --subvolid) - subvolid_cmdline="$2" - shift 2 - ;; - -k | --keepold) - keep="yes" - shift - ;; - -n | --noconfirm) - noconfirm="yes" - shift - ;; - -h | --help) - usage - exit 1 - ;; - -q | --quiet) - donotify=1 - shift - ;; - -r | --remote) - remote=$2 - shift 2 - ;; - -p | --port) - port=$2 - shift 2 - ;; - --sudo) - sudo=1 - shift - ;; - *) - die "Unknown option: '$key'. Run '$name -h' for valid options." - ;; - esac -done - -notify() { - for u in $(users | tr ' ' '\n' | sort -u); do - sudo -u "$u" DISPLAY=:0 \ - DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(sudo -u "$u" id -u)/bus" \ - notify-send -a $name "$1" "$2" --icon="dialog-$3" - done -} - -notify_info() { - if [[ $donotify -eq 0 ]]; then - notify "$1" "$2" "information" - else - printf '%s\n' "$1: $2" - fi -} - -notify_error() { - if [[ $donotify -eq 0 ]]; then - notify "$1" "$2" "error" - else - printf '%s\n' "$1: $2" - fi -} - -[[ $EUID -ne 0 ]] && die "Script must be run as root. See '$name -h' for a description of options" -! [[ -f $SNAPPER_CONFIG ]] && die "$SNAPPER_CONFIG does not exist." - -description=${description:-"latest incremental backup"} -uuid_cmdline=${uuid_cmdline:-"none"} -subvolid_cmdline=${subvolid_cmdline:-"5"} -noconfirm=${noconfirm:-"no"} - -if [[ -z $remote ]]; then - if ! command -v rsync &>/dev/null; then - die "--remote specified but rsync command not found" - fi -fi - -if [[ $uuid_cmdline != "none" ]]; then - if [[ -z $remote ]]; then - notify_info "Backup started" "Starting backups to $uuid_cmdline subvolid=$subvolid_cmdline..." - else - notify_info "Backup started" "Starting backups to $uuid_cmdline subvolid $subvolid_cmdline at $remote..." - fi -else - if [[ -z $remote ]]; then - notify_info "Backup started" "Starting backups. Use command line menu to select disk." - else - notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote." - fi -fi - -if [[ -n $remote ]]; then - ssh="ssh $remote" - if [[ -n $port ]]; then - ssh="$ssh -p $port" - fi - if [[ $sudo -eq 1 ]]; then - ssh="$ssh sudo" - fi -fi - -if [[ "$($ssh findmnt -n -v --target / -o FSTYPE)" == "btrfs" ]]; then - EXCLUDE_UUID=$($ssh findmnt -n -v -t btrfs --target / -o UUID) - TARGETS=$($ssh findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v "$EXCLUDE_UUID" | awk '{print $2}') - UUIDS=$($ssh findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v "$EXCLUDE_UUID" | awk '{print $1}') -else - TARGETS=$($ssh findmnt -n -v -t btrfs -o TARGET --list) - UUIDS=$($ssh findmnt -n -v -t btrfs -o UUID --list) -fi - -declare -a TARGETS_ARRAY -declare -a UUIDS_ARRAY -declare -a SUBVOLIDS_ARRAY - -i=0 -for x in $TARGETS; do - SUBVOLIDS_ARRAY[i]=$($ssh btrfs subvolume show "$x" | awk '/Subvolume ID:/ { print $3 }') - TARGETS_ARRAY[i]=$x - i=$((i + 1)) -done - -i=0 -disk=-1 -disk_count=0 -for x in $UUIDS; do - UUIDS_ARRAY[i]=$x - if [[ $x == "$uuid_cmdline" && ${SUBVOLIDS_ARRAY[$((i))]} == "$subvolid_cmdline" ]]; then - disk=$i - disk_count=$((disk_count + 1)) - fi - i=$((i + 1)) -done - -if [[ ${#UUIDS_ARRAY[$@]} -eq 0 ]]; then - die "No external btrfs subvolumes found to backup to. Run '$name -h' for more options." -fi - -if [[ $disk_count -gt 1 ]]; then - printf "Multiple mount points were found with UUID %s and subvolid %s.\n" "$uuid_cmdline" "$subvolid_cmdline" - disk="-1" -fi - -if [[ $disk == -1 ]]; then - if [[ $disk_count == 0 && $uuid_cmdline != "none" ]]; then - error "A device with UUID $uuid_cmdline and subvolid $subvolid_cmdline was not found to be mounted, or it is not a BTRFS device." - fi - if [[ -z $ssh ]]; then - printf "Select a mounted BTRFS device on your local machine to backup to.\nFor more options, exit and run '%s -h'.\n" "$name" - else - printf "Select a mounted BTRFS device on %s to backup to.\nFor more options, exit and run '%s -h'.\n" "$remote" "$name" - fi - while [[ $disk -lt 0 || $disk -gt $i ]]; do - for x in "${!TARGETS_ARRAY[@]}"; do - printf "%4s) %s (uuid=%s, subvolid=%s)\n" "$((x + 1))" "${TARGETS_ARRAY[$x]}" "${UUIDS_ARRAY[$x]}" "${SUBVOLIDS_ARRAY[$x]}" - done - printf "%4s) Exit\n" "0" - read -e -r -p "Enter a number: " disk - if ! [[ $disk == ?(-)+([0-9]) ]] || [[ $disk -lt 0 || $disk -gt $i ]]; then - printf "\nNo disk selected. Select a disk to continue.\n" - disk=-1 - fi - done - if [[ $disk == 0 ]]; then - exit 0 - fi - disk=$((disk - 1)) -fi - -selected_subvolid="${SUBVOLIDS_ARRAY[$((disk))]}" -selected_uuid="${UUIDS_ARRAY[$((disk))]}" -selected_mnt="${TARGETS_ARRAY[$((disk))]}" -printf "\nYou selected the disk with uuid=%s, subvolid=%s.\n" "$selected_uuid" "$selected_subvolid" -if [[ -z $ssh ]]; then - printf "The disk is mounted at '%s'.\n" "$selected_mnt" -else - printf "The disk is mounted at '%s:%s'.\n" "$remote" "$selected_mnt" -fi - -# shellcheck source=/dev/null -source "$SNAPPER_CONFIG" - -if [[ -z $selected_configs ]]; then - printf "\nInteractively cycling through all snapper configurations...\n" -fi -selected_configs=${selected_configs:-$SNAPPER_CONFIGS} - -declare -a BACKUPDIRS_ARRAY -declare -a MYBACKUPDIR_ARRAY -declare -a OLD_NUM_ARRAY -declare -a OLD_SNAP_ARRAY -declare -a NEW_NUM_ARRAY -declare -a NEW_SNAP_ARRAY -declare -a NEW_INFO_ARRAY -declare -a BACKUPLOC_ARRAY -declare -a CONT_BACKUP_ARRAY - -# Initial configuration of where backup directories are -i=0 -for x in $selected_configs; do - - if [[ "$(snapper -c "$x" list --disable-used-space -t single | awk '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {cnt++} END {print cnt}')" -gt 1 ]]; then - error "More than one snapper entry found with UUID $selected_uuid subvolid $selected_subvolid for configuration $x. Skipping configuration $x." - continue - fi - - if [[ "$(snapper -c "$x" list --disable-used-space -t single | awk '/'$name' backup in progress/ {cnt++} END {print cnt}')" -gt 0 ]]; then - printf "\nNOTE: Previous failed %s backup snapshots found for '%s'.\n" "$name" "$x" - if [[ $noconfirm == "yes" ]]; then - printf "'noconfirm' option passed. Failed backups will not be deleted.\n" - else - read -e -r -p "Delete failed backup snapshot(s)? (These local snapshots from failed backups are not used.) [y/N]? " delete_failed - while [[ -n $delete_failed && $delete_failed != [Yy]"es" && - $delete_failed != [Yy] && $delete_failed != [Nn]"o" && - $delete_failed != [Nn] ]]; do - read -e -r -p "Delete failed backup snapshot(s)? (These local snapshots from failed backups are not used.) [y/N] " delete_failed - if [[ -n $delete_failed && $delete_failed != [Yy]"es" && - $delete_failed != [Yy] && $delete_failed != [Nn]"o" && - $delete_failed != [Nn] ]]; then - printf "Select 'y' or 'N'.\n" - fi - done - if [[ $delete_failed == [Yy]"es" || $delete_failed == [Yy] ]]; then - # explicit split list of snapshots (on whitespace) into multiple arguments - # shellcheck disable=SC2046 - snapper -c "$x" delete $(snapper -c "$x" list --disable-used-space | awk '/'$name' backup in progress/ {print $1}') - fi - fi - fi - - SNAP_SYNC_EXCLUDE=no - - if [[ -f "/etc/snapper/configs/$x" ]]; then - # shellcheck source=/dev/null - source "/etc/snapper/configs/$x" - else - die "Selected snapper configuration $x does not exist." - fi - - if [[ $SNAP_SYNC_EXCLUDE == "yes" ]]; then - continue - fi - - printf "\n" - - old_num=$(snapper -c "$x" list --disable-used-space -t single | awk '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {print $1}') - old_snap=$SUBVOLUME/.snapshots/$old_num/snapshot - - OLD_NUM_ARRAY[i]=$old_num - OLD_SNAP_ARRAY[i]=$old_snap - - if [[ -z $old_num ]]; then - printf "No backups have been performed for '%s' on this disk.\n" "$x" - read -e -r -p "Enter name of subvolume to store backups, relative to $selected_mnt (to be created if not existing): " mybackupdir - printf "This will be the initial backup for snapper configuration '%s' to this disk. This could take awhile.\n" "$x" - BACKUPDIR="$selected_mnt/$mybackupdir" - $ssh test -d "$BACKUPDIR" || $ssh btrfs subvolume create "$BACKUPDIR" - else - mybackupdir=$(snapper -c "$x" list --disable-used-space -t single | awk -F"|" '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}') - BACKUPDIR="$selected_mnt/$mybackupdir" - $ssh test -d "$BACKUPDIR" || die "%s is not a directory on %s.\n" "$BACKUPDIR" "$selected_uuid" - fi - BACKUPDIRS_ARRAY[i]="$BACKUPDIR" - MYBACKUPDIR_ARRAY[i]="$mybackupdir" - - printf "Creating new local snapshot for '%s' configuration...\n" "$x" - new_num=$(snapper -c "$x" create --print-number -d "$name backup in progress") - new_snap=$SUBVOLUME/.snapshots/$new_num/snapshot - new_info=$SUBVOLUME/.snapshots/$new_num/info.xml - sync - backup_location=$BACKUPDIR/$x/$new_num/ - if [[ -z $ssh ]]; then - printf "Will backup %s to %s\n" "$new_snap" "$backup_location/snapshot" - else - printf "Will backup %s to %s\n" "$new_snap" "$remote":"$backup_location/snapshot" - fi - - if ($ssh test -d "$backup_location/snapshot"); then - printf "WARNING: Backup directory '%s' already exists. This configuration will be skipped!\n" "$backup_location/snapshot" - printf "Move or delete destination directory and try backup again.\n" - fi - - NEW_NUM_ARRAY[i]="$new_num" - NEW_SNAP_ARRAY[i]="$new_snap" - NEW_INFO_ARRAY[i]="$new_info" - BACKUPLOC_ARRAY[i]="$backup_location" - - cont_backup="K" - CONT_BACKUP_ARRAY[i]="yes" - if [[ $noconfirm == "yes" ]]; then - cont_backup="yes" - else - while [[ -n $cont_backup && $cont_backup != [Yy]"es" && - $cont_backup != [Yy] && $cont_backup != [Nn]"o" && - $cont_backup != [Nn] ]]; do - read -e -r -p "Proceed with backup of '$x' configuration [Y/n]? " cont_backup - if [[ -n $cont_backup && $cont_backup != [Yy]"es" && - $cont_backup != [Yy] && $cont_backup != [Nn]"o" && - $cont_backup != [Nn] ]]; then - printf "Select 'Y' or 'n'.\n" - fi - done - fi - - if [[ $cont_backup != [Yy]"es" && $cont_backup != [Yy] && -n $cont_backup ]]; then - CONT_BACKUP_ARRAY[i]="no" - printf "Not backing up '%s' configuration.\n" "$x" - snapper -c "$x" delete "$new_num" - fi - - i=$((i + 1)) - -done - -# Actual backing up -printf "\nPerforming backups...\n" -i=-1 -for x in $selected_configs; do - - i=$((i + 1)) - - SNAP_SYNC_EXCLUDE=no - - if [[ -f "/etc/snapper/configs/$x" ]]; then - # shellcheck source=/dev/null - source "/etc/snapper/configs/$x" - else - die "Selected snapper configuration $x does not exist." - fi - - cont_backup=${CONT_BACKUP_ARRAY[$i]} - if [[ $cont_backup == "no" || $SNAP_SYNC_EXCLUDE == "yes" ]]; then - notify_info "Backup in progress" "NOTE: Skipping $x configuration." - continue - fi - - notify_info "Backup in progress" "Backing up $x configuration." - - printf "\n" - - old_num="${OLD_NUM_ARRAY[$i]}" - old_snap="${OLD_SNAP_ARRAY[$i]}" - BACKUPDIR="${BACKUPDIRS_ARRAY[$i]}" - mybackupdir="${MYBACKUPDIR_ARRAY[$i]}" - new_num="${NEW_NUM_ARRAY[$i]}" - new_snap="${NEW_SNAP_ARRAY[$i]}" - new_info="${NEW_INFO_ARRAY[$i]}" - backup_location="${BACKUPLOC_ARRAY[$i]}" - - if ($ssh test -d "$backup_location/snapshot"); then - printf "ERROR: Backup directory '%s' already exists. Skipping backup of this configuration!\n" "$backup_location/snapshot" - continue - fi - - $ssh mkdir -p "$backup_location" - - if [[ -z $old_num ]]; then - printf "Sending first snapshot for '%s' configuration...\n" "$x" - if [[ $doprogress -eq 0 ]]; then - btrfs send "$new_snap" | pv | $ssh btrfs receive "$backup_location" &>/dev/null - else - btrfs send "$new_snap" | $ssh btrfs receive "$backup_location" &>/dev/null - fi - else - - printf "Sending incremental snapshot for '%s' configuration...\n" "$x" - # Sends the difference between the new snapshot and old snapshot to the - # backup location. Using the -c flag instead of -p tells it that there - # is an identical subvolume to the old snapshot at the receiving - # location where it can get its data. This helps speed up the transfer. - - if [[ $doprogress -eq 0 ]]; then - btrfs send -c "$old_snap" "$new_snap" | pv | $ssh btrfs receive "$backup_location" - else - btrfs send -c "$old_snap" "$new_snap" | $ssh btrfs receive "$backup_location" - fi - - if [[ $keep == "yes" ]]; then - printf "Modifying data for old local snapshot for '%s' configuration...\n" "$x" - snapper -v -c "$x" modify -d "old snap-sync snapshot (you may remove)" -u "backupdir=,subvolid=,uuid=" -c "number" "$old_num" - else - printf "Deleting old snapshot for %s...\n" "$x" - snapper -c "$x" delete "$old_num" - fi - - fi - - if [[ -z $remote ]]; then - cp "$new_info" "$backup_location" - else - if [[ -z $port ]]; then - rsync -avzq "$new_info" "$remote":"$backup_location" - else - rsync -avzqe "ssh -p $port" "$new_info" "$remote":"$backup_location" - fi - fi - - # It's important not to change this userdata in the snapshots, since that's how - # we find the previous one. - - userdata="backupdir=$mybackupdir, subvolid=$selected_subvolid, uuid=$selected_uuid" - - # Tag new snapshot as the latest - printf "Tagging local snapshot as latest backup for '%s' configuration...\n" "$x" - snapper -v -c "$x" modify -d "$description" -u "$userdata" "$new_num" - - printf "Backup complete for '%s' configuration.\n" "$x" - -done - -printf "\nDone!\n" - -if [[ $uuid_cmdline != "none" ]]; then - notify_info "Finished" "Backups to $uuid_cmdline complete!" -else - notify_info "Finished" "Backups complete!" -fi diff --git a/pkgs/by-name/st/stamp/package.nix b/pkgs/by-name/st/stamp/package.nix index 703f73e3..871d531c 100644 --- a/pkgs/by-name/st/stamp/package.nix +++ b/pkgs/by-name/st/stamp/package.nix @@ -1,20 +1,29 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { - sysLib, - findutils, + writeShellApplication, + # Dependencies + coreutils, fd, - reuse, git, + reuse, }: -sysLib.writeShellScript { +writeShellApplication { name = "stamp"; - src = ./stamp.sh; - generateCompletions = false; - keepPath = false; + text = builtins.readFile ./stamp.sh; + inheritPath = false; - dependencies = [ - findutils + runtimeInputs = [ + coreutils fd - reuse git + reuse ]; } diff --git a/pkgs/by-name/st/stamp/stamp.sh b/pkgs/by-name/st/stamp/stamp.sh index 0aa6c281..5f17b2aa 100755 --- a/pkgs/by-name/st/stamp/stamp.sh +++ b/pkgs/by-name/st/stamp/stamp.sh @@ -1,7 +1,19 @@ #!/usr/bin/env dash -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +die() { + echo "Error: $1" + exit 1 +} help() { cat <<EOF diff --git a/pkgs/by-name/sw/swallow/package.nix b/pkgs/by-name/sw/swallow/package.nix new file mode 100644 index 00000000..16608143 --- /dev/null +++ b/pkgs/by-name/sw/swallow/package.nix @@ -0,0 +1,25 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. +{ + writeShellApplication, + # Dependencies + river-classic, +}: +writeShellApplication { + name = "swallow"; + text = builtins.readFile ./swallow.sh; + + # We need to inherit the path, so that we can spawn stuff in a swallowed mode. + inheritPath = true; + + runtimeInputs = [ + river-classic + ]; +} diff --git a/pkgs/by-name/sw/swallow/swallow.sh b/pkgs/by-name/sw/swallow/swallow.sh new file mode 100755 index 00000000..922a21b3 --- /dev/null +++ b/pkgs/by-name/sw/swallow/swallow.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env dash +# Based on: https://codeberg.org/nirodhvana/reservoir/src/commit/2fa8c14877799a03bb927f048c2907dbb418fd68/dot-local/bin/gobble + +# Inspired by https://github.com/swindlesmccoop/not-just-dotfiles/blob/master/.local/bin/swallow + +swallow_tag=$((1 << 9)) +eat() { + riverctl set-view-tags $swallow_tag +} + +throwup() { + riverctl set-focused-tags $swallow_tag && + riverctl send-to-previous-tags && + riverctl focus-previous-tags +} + +if [ -z "$*" ]; then + printf "ERROR: No Arguments Supplied\n" +else + eat && "$@" + + throwup +fi diff --git a/pkgs/by-name/tr/tree-sitter-yts/package.nix b/pkgs/by-name/tr/tree-sitter-yts/package.nix index 3a97530d..eef65714 100644 --- a/pkgs/by-name/tr/tree-sitter-yts/package.nix +++ b/pkgs/by-name/tr/tree-sitter-yts/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. # taken from nixpgks: pkgs/development/tools/parsing/tree-sitter/grammar.nix { yt, @@ -5,46 +14,10 @@ nodejs, tree-sitter, }: -stdenv.mkDerivation { - pname = "yts-grammar"; - version = "1.0.0"; +tree-sitter.buildGrammar { + language = "yts"; + version = "0.0.1+rev=0bb9a60"; inherit (yt) src; sourceRoot = "yt/tree-sitter-yts"; - - nativeBuildInputs = [nodejs tree-sitter]; - - CFLAGS = ["-Isrc" "-O2"]; - CXXFLAGS = ["-Isrc" "-O2"]; - - stripDebugList = ["parser"]; - - configurePhase = '' - tree-sitter generate - ''; - - # When both scanner.{c,cc} exist, we should not link both since they may be the same but in - # different languages. Just randomly prefer C++ if that happens. - buildPhase = '' - runHook preBuild - if [[ -e src/scanner.cc ]]; then - $CXX -fPIC -c src/scanner.cc -o scanner.o $CXXFLAGS - elif [[ -e src/scanner.c ]]; then - $CC -fPIC -c src/scanner.c -o scanner.o $CFLAGS - fi - $CC -fPIC -c src/parser.c -o parser.o $CFLAGS - rm -rf parser - $CXX -shared -o parser *.o - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - mkdir $out - mv parser $out/ - if [[ -d queries ]]; then - cp -r queries $out - fi - runHook postInstall - ''; } diff --git a/pkgs/by-name/ts/tskm/.envrc b/pkgs/by-name/ts/tskm/.envrc index d21a17fc..a84d550d 100644 --- a/pkgs/by-name/ts/tskm/.envrc +++ b/pkgs/by-name/ts/tskm/.envrc @@ -1,8 +1,18 @@ #!/usr/bin/env sh -SHELL_COMPLETION_DIR="$(pwd)/target/shell" -export SHELL_COMPLETION_DIR +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. export TSKM_PROJECT_FILE=/home/soispha/repos/nix/config/modules/common/projects.json use flake + +PATH_add ./target/debug +PATH_add ./target/release diff --git a/pkgs/by-name/ts/tskm/.gitignore b/pkgs/by-name/ts/tskm/.gitignore index 2d5df85d..f255eebd 100644 --- a/pkgs/by-name/ts/tskm/.gitignore +++ b/pkgs/by-name/ts/tskm/.gitignore @@ -1,2 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + /target .direnv diff --git a/pkgs/by-name/ts/tskm/Cargo.lock b/pkgs/by-name/ts/tskm/Cargo.lock index 68823d3c..2e253ebd 100644 --- a/pkgs/by-name/ts/tskm/Cargo.lock +++ b/pkgs/by-name/ts/tskm/Cargo.lock @@ -1,18 +1,27 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. version = 4 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", @@ -21,12 +30,6 @@ dependencies = [ ] [[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -37,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -52,62 +55,68 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arraydeque" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -117,26 +126,26 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -147,9 +156,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -157,9 +166,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -169,18 +178,21 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.47" +version = "4.5.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" +checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" dependencies = [ "clap", + "clap_lex", + "is_executable", + "shlex", ] [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -190,15 +202,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "core-foundation-sys" @@ -208,9 +220,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -233,7 +245,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -248,6 +260,15 @@ dependencies = [ ] [[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "fallible-iterator" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -260,45 +281,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] name = "flate2" -version = "1.1.1" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] [[package]] @@ -311,12 +344,30 @@ dependencies = [ ] [[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", ] [[package]] @@ -327,15 +378,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -357,21 +408,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -381,103 +433,65 @@ dependencies = [ ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - -[[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -486,9 +500,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -496,32 +510,41 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -529,15 +552,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", @@ -555,38 +578,36 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "lz4_flex" -version = "0.11.3" +name = "md5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" -dependencies = [ - "twox-hash", -] +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -605,6 +626,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -612,9 +639,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" @@ -623,36 +650,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror", ] @@ -666,22 +702,16 @@ dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", - "hashlink", + "hashlink 0.9.1", "libsqlite3-sys", "smallvec", ] [[package]] name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -694,18 +724,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -714,14 +754,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -731,22 +772,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "smallvec" -version = "1.14.0" +name = "simd-adler32" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stderrlog" @@ -754,7 +795,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b" dependencies = [ - "chrono", "is-terminal", "log", "termcolor", @@ -769,28 +809,27 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn", ] [[package]] name = "syn" -version = "2.0.100" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -799,9 +838,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -839,18 +878,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -859,19 +898,18 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -886,50 +924,36 @@ dependencies = [ "clap_complete", "dirs", "log", - "lz4_flex", + "md5", "serde", "serde_json", "stderrlog", "taskchampion", "url", "walkdir", -] - -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", + "yaml-rust2", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -943,12 +967,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.2", - "serde", + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", ] [[package]] @@ -975,50 +1001,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1026,65 +1039,119 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-targets" -version = "0.52.6" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -1097,80 +1164,81 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] -name = "write16" -version = "1.0.0" +name = "writeable" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] -name = "writeable" -version = "0.5.5" +name = "yaml-rust2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.10.0", +] [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1178,9 +1246,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -1190,18 +1258,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -1230,10 +1298,21 @@ dependencies = [ ] [[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1242,11 +1321,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" diff --git a/pkgs/by-name/ts/tskm/Cargo.toml b/pkgs/by-name/ts/tskm/Cargo.toml index 41fc5888..49774037 100644 --- a/pkgs/by-name/ts/tskm/Cargo.toml +++ b/pkgs/by-name/ts/tskm/Cargo.toml @@ -1,3 +1,13 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + [package] name = "tskm" version = "0.1.0" @@ -6,17 +16,19 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.97" -clap = { version = "4.5.34", features = ["derive"] } -dirs = "6.0.0" -log = "0.4.27" -lz4_flex = "0.11.3" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -stderrlog = "0.6.0" +anyhow = { version = "1.0.100", default-features = false } +clap = { version = "4.5.54", features = [ "derive", "std", "color", "help", "usage", "error-context", "suggestions", ], default-features = false } +clap_complete = { version = "4.5.65", features = ["unstable-dynamic"] } +dirs = { version = "6.0.0", default-features = false } +log = { version = "0.4.29", default-features = false } +serde = { version = "1.0.228", features = ["derive"], default-features = false } +serde_json = { version = "1.0.149", default-features = false } +stderrlog = { version = "0.6.0", default-features = false } taskchampion = { version = "2.0.3", default-features = false } -url = { version = "2.5.4", features = ["serde"] } -walkdir = "2.5.0" +url = { version = "2.5.8", features = ["serde", "std"], default-features = false } +walkdir = { version = "2.5.0", default-features = false } +md5 = { version = "0.8.0", default-features = false } +yaml-rust2 = "0.10.4" [profile.release] lto = true @@ -77,15 +89,3 @@ perf = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } missing_panics_doc = "allow" missing_errors_doc = "allow" - -[build-dependencies] -anyhow = "1.0.97" -clap = { version = "4.5.34", features = ["derive"] } -clap_complete = "4.5.47" -dirs = "6.0.0" -log = "0.4.27" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -taskchampion = { version = "2.0.3", default-features = false } -url = "2.5.4" -walkdir = "2.5.0" diff --git a/pkgs/by-name/ts/tskm/build.rs b/pkgs/by-name/ts/tskm/build.rs deleted file mode 100644 index e3b60bb9..00000000 --- a/pkgs/by-name/ts/tskm/build.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::{Context, Result}; -use clap::{CommandFactory, ValueEnum}; -use clap_complete::generate_to; -use clap_complete::Shell; - -use std::env; -use std::fs; -use std::path::PathBuf; - -use crate::cli::CliArgs; - -pub mod task { - include!("src/task/mod.rs"); -} -pub mod state { - include!("src/state.rs"); -} - -pub mod interface { - pub mod input { - include!("src/interface/input/mod.rs"); - } - pub mod project { - include!("src/interface/project/mod.rs"); - } -} - -pub mod cli { - include!("src/cli.rs"); -} - -fn main() -> Result<()> { - let outdir = match env::var_os("SHELL_COMPLETION_DIR") { - None => return Ok(()), - Some(outdir) => outdir, - }; - - if !PathBuf::from(&outdir).exists() { - fs::create_dir_all(&outdir)?; - } - - let mut cmd = CliArgs::command(); - - for &shell in Shell::value_variants() { - let path = generate_to(shell, &mut cmd, "tskm", &outdir).with_context(|| { - format!("Failed to output shell completion for {shell} to {outdir:?}") - })?; - println!("cargo:warning=completion file for {shell} is generated at: {path:?}"); - } - - Ok(()) -} diff --git a/pkgs/by-name/ts/tskm/flake.lock b/pkgs/by-name/ts/tskm/flake.lock index 82448387..1e997998 100644 --- a/pkgs/by-name/ts/tskm/flake.lock +++ b/pkgs/by-name/ts/tskm/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1743076231, - "narHash": "sha256-yQugdVfi316qUfqzN8JMaA2vixl+45GxNm4oUfXlbgw=", + "lastModified": 1768661221, + "narHash": "sha256-MJwOjrIISfOpdI9x4C+5WFQXvHtOuj5mqLZ4TMEtk1M=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6c5963357f3c1c840201eda129a99d455074db04", + "rev": "3327b113f2ef698d380df83fbccefad7e83d7769", "type": "github" }, "original": { diff --git a/pkgs/by-name/ts/tskm/flake.lock.license b/pkgs/by-name/ts/tskm/flake.lock.license new file mode 100644 index 00000000..eae6a84c --- /dev/null +++ b/pkgs/by-name/ts/tskm/flake.lock.license @@ -0,0 +1,9 @@ +nixos-config - My current NixOS configuration + +Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +SPDX-License-Identifier: GPL-3.0-or-later + +This file is part of my nixos-config. + +You should have received a copy of the License along with this program. +If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. diff --git a/pkgs/by-name/ts/tskm/flake.nix b/pkgs/by-name/ts/tskm/flake.nix index 5a5f628b..6217f942 100644 --- a/pkgs/by-name/ts/tskm/flake.nix +++ b/pkgs/by-name/ts/tskm/flake.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { description = "This is the core interface to the system-integrated task management"; @@ -14,13 +23,13 @@ pkgs.sqlite ]; - packages = with pkgs; [ - cargo - clippy - rustc - rustfmt + packages = [ + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt - cargo-edit + pkgs.cargo-edit ]; }; }; diff --git a/pkgs/by-name/ts/tskm/package.nix b/pkgs/by-name/ts/tskm/package.nix index 3d320772..ad10865f 100644 --- a/pkgs/by-name/ts/tskm/package.nix +++ b/pkgs/by-name/ts/tskm/package.nix @@ -1,3 +1,12 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { rustPlatform, installShellFiles, @@ -7,7 +16,6 @@ taskwarrior3, git, rofi, - firefox, sqlite, }: rustPlatform.buildRustPackage (finalAttrs: { @@ -19,15 +27,10 @@ rustPlatform.buildRustPackage (finalAttrs: { lockFile = ./Cargo.lock; }; - env = { - SHELL_COMPLETION_DIR = "./shell"; - }; - buildInputs = [ taskwarrior3 git rofi - firefox sqlite ]; @@ -38,9 +41,9 @@ rustPlatform.buildRustPackage (finalAttrs: { postInstall = '' installShellCompletion --cmd tskm \ - --bash ./shell/tskm.bash \ - --fish ./shell/tskm.fish \ - --zsh ./shell/_tskm + --bash <(COMPLETE=bash $out/bin/tskm) \ + --fish <(COMPLETE=fish $out/bin/tskm) \ + --zsh <(COMPLETE=zsh $out/bin/tskm) # NOTE: We cannot clear the path, because we need access to the $EDITOR. <2025-04-04> wrapProgram $out/bin/tskm \ diff --git a/pkgs/by-name/ts/tskm/src/browser/mod.rs b/pkgs/by-name/ts/tskm/src/browser/mod.rs new file mode 100644 index 00000000..2129982f --- /dev/null +++ b/pkgs/by-name/ts/tskm/src/browser/mod.rs @@ -0,0 +1,202 @@ +use std::{ + env, fs, + io::Write, + os::unix::net::UnixStream, + path::PathBuf, + process::{self, ExitStatus}, +}; + +use anyhow::{Context, Result}; +use log::{error, info}; +use serde_json::json; +use url::Url; + +use crate::{state::State, task}; + +#[allow(clippy::too_many_lines)] +pub fn open_in_browser<U>( + selected_project: &task::Project, + state: &mut State, + urls: Option<Vec<U>>, +) -> Result<()> +where + U: Into<Url>, +{ + let old_project: Option<task::Project> = + task::Project::get_current().context("Failed to get currently active project")?; + let old_task: Option<task::Task> = + task::Task::get_current(state).context("Failed to get currently active task")?; + + selected_project.activate().with_context(|| { + format!( + "Failed to active project: '{}'", + selected_project.to_project_display() + ) + })?; + + let tracking_task = { + let all_tasks = selected_project.get_tasks(state).with_context(|| { + format!( + "Failed to get assoctiated tasks for project: '{}'", + selected_project.to_project_display() + ) + })?; + + let tracking_task = all_tasks.into_iter().find(|t| { + let maybe_desc = t.description(state); + if let Ok(desc) = maybe_desc { + desc == "tracking" + } else { + error!( + "Getting task description returned error: {}", + maybe_desc.expect_err("We already check for Ok") + ); + false + } + }); + + if let Some(task) = tracking_task { + info!( + "Starting task {} -> tracking", + selected_project.to_project_display() + ); + task.start(state) + .with_context(|| format!("Failed to start task {task}"))?; + } + tracking_task + }; + + let status = { + // #!/bin/sh + // # initial idea: Florian Bruhin (The-Compiler) + // # author: Thore Bödecker (foxxx0) + // + // _url="$1" + // _qb_version='1.0.4' + // _proto_version=1 + // _ipc_socket="${XDG_RUNTIME_DIR}/qutebrowser/ipc-$(printf '%s' "$USER" | md5sum | cut -d' ' -f1)" + // _qute_bin="/usr/bin/qutebrowser" + // + // printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version": %d, "cwd": "%s"}\n' \ + // "${_url}" \ + // "${_qb_version}" \ + // "${_proto_version}" \ + // "${PWD}" | socat -lf /dev/null - UNIX-CONNECT:"${_ipc_socket}" || "$_qute_bin" "$@" & + + let ipc_socket_path = PathBuf::from( + env::var("XDG_RUNTIME_DIR").context("Failed to access XDG_RUNTIME_DIR var")?, + ) + .join("qutebrowser") + .join(selected_project.to_project_display()) + .join(format!("ipc-{:x}", { + let user_name = env::var("USER").context("Failed to get USER var")?; + let base_dir = env::var("XDG_DATA_HOME").context("Failed to get XDG_DATA_HOME")?; + + md5::compute( + format!( + "{user_name}-{}", + PathBuf::from(base_dir) + .join("qutebrowser") + .join(selected_project.to_project_display()) + .display() + ) + .as_bytes(), + ) + })); + + let socket = if ipc_socket_path.exists() { + match UnixStream::connect(&ipc_socket_path) { + Ok(ok) => Some(ok), + Err(err) => match err.kind() { + std::io::ErrorKind::ConnectionRefused => { + // There is no qutebrowser listening to our connection. + fs::remove_file(&ipc_socket_path).with_context(|| { + format!( + "Failed to remove orphaned qutebrowser socket: {}", + ipc_socket_path.display() + ) + })?; + None + } + _ => Err(err).with_context(|| { + format!( + "Failed to connect to qutebrowser's ipc socket at: {}", + ipc_socket_path.display() + ) + })?, + }, + } + } else { + None + }; + + if let Some(mut stream) = socket { + let real_url = if let Some(urls) = urls { + urls.into_iter().map(|url| url.into().to_string()).collect() + } else { + // Always add a new tab, so that qutebrowser is marked as “urgent”. + vec!["qute://start".to_owned()] + }; + + stream.write_all( + json! { + { + "args": real_url, + "target_arg": null, + "version": "1.0.4", + "protocol_version": 1, + "cwd": "/" + } + } + .to_string() + .as_bytes(), + )?; + stream.write_all(b"\n")?; + + ExitStatus::default() + } else { + let args = if let Some(urls) = urls { + urls.into_iter() + .map(Into::<Url>::into) + .map(|u| u.to_string()) + .collect() + } else { + vec![] + }; + + process::Command::new(format!( + "qutebrowser-{}", + selected_project.to_project_display() + )) + .args(args) + .status() + .context("Failed to start qutebrowser")? + } + }; + + if !status.success() { + error!("Qutebrowser run exited with error."); + } + + if let Some(task) = tracking_task { + task.stop(state) + .with_context(|| format!("Failed to stop task {task}"))?; + } + if let Some(task) = old_task { + task.start(state) + .with_context(|| format!("Failed to start task {task}"))?; + } + + if let Some(project) = old_project { + project.activate().with_context(|| { + format!( + "Failed to activate project {}", + project.to_project_display() + ) + })?; + } else { + task::Project::clear().context("Failed to clear currently focused project")?; + } + + Ok(()) +} diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs index bd389ca5..359c1050 100644 --- a/pkgs/by-name/ts/tskm/src/cli.rs +++ b/pkgs/by-name/ts/tskm/src/cli.rs @@ -1,12 +1,26 @@ -use std::path::PathBuf; +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{ffi::OsStr, path::PathBuf}; use anyhow::{bail, Result}; -use clap::{ArgAction, Parser, Subcommand}; +use clap::{builder::StyledStr, ArgAction, Parser, Subcommand, ValueEnum}; +use clap_complete::{ArgValueCompleter, CompletionCandidate}; use crate::{ - interface::{input::Input, project::ProjectName}, - state::State, - task, + interface::{ + input::{Input, Tag}, + open::UrlLike, + project::ProjectName, + }, + state, task, }; #[derive(Parser, Debug)] @@ -14,10 +28,12 @@ use crate::{ /// This is the core interface to the system-integrated task management /// /// `tskm` effectively combines multiple applications together: -/// - `taskwarrior` projects are raised connected to `firefox` profiles, making it possible to “open” +/// - `taskwarrior` projects are connected to `qutebrowser` profiles, making it possible to “open” /// a project. +/// /// - Every `taskwarrior` project has a determined `neorg` path, so that extra information for a /// `project` can be stored in this `norg` file. +/// /// - `tskm` can track inputs for you. These are URLs with optional tags which you can that /// “review” to open tasks based on them. pub struct CliArgs { @@ -53,7 +69,7 @@ pub enum Command { command: NeorgCommand, }, - /// Interface with the Firefox profile of each project. + /// Interface with the Qutebrowser profile of each project. Open { #[command(subcommand)] command: OpenCommand, @@ -78,14 +94,14 @@ pub enum NeorgCommand { /// Open the `neorg` project associated with id of the task. Task { /// The working set id of the task - #[arg(value_parser = task_from_working_set_id)] - id: task::Task, + #[arg(value_name = "ID", value_parser = task_from_working_set_id, add = ArgValueCompleter::new(complete_task_id))] + task: task::Task, }, } fn task_from_working_set_id(id: &str) -> Result<task::Task> { let id: usize = id.parse()?; - let mut state = State::new_ro()?; + let mut state = state::State::new_ro()?; let Some(task) = task::Task::from_working_set(id, &mut state)? else { bail!("Working set id '{id}' is not valid!") @@ -95,52 +111,222 @@ fn task_from_working_set_id(id: &str) -> Result<task::Task> { #[derive(Subcommand, Debug)] pub enum OpenCommand { - /// Open each project's Firefox profile consecutively, that was opened since the last review. + /// Open each project's Qutebrowser profile consecutively, that was opened since the last review. /// /// This allows you to remove stale opened tabs and to commit open tabs to the `inputs`. - Review, + Review { + /// Review all projects, if they contain tabs + #[arg(short, long, default_value_t)] + non_empty: bool, + }, - /// Opens Firefox with either the supplied project or the currently active project profile. + /// Opens Qutebrowser with either the supplied project or the currently active project profile. Project { /// The project to open. - #[arg(value_parser = task::Project::from_project_string)] - project: Option<task::Project>, + #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] + project: task::Project, + + /// The URLs to open. + urls: Option<Vec<UrlLike>>, }, - /// Open a selected project in it's Firefox profile. + /// Open a selected project in it's Qutebrowser profile. /// /// This will use rofi's dmenu mode to select one project from the list of all registered /// projects. - Select, + Select { + /// The URLs to open. + urls: Option<Vec<UrlLike>>, + }, /// List all open tabs in the project. ListTabs { - /// The project to open. - #[arg(value_parser = task::Project::from_project_string)] - project: Option<task::Project>, + /// The projects to open. + #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] + projects: Option<Vec<task::Project>>, + + /// Only show the tabs, that are in this mode + #[arg(short, long, conflicts_with = "projects")] + mode: Option<ListMode>, }, } +#[derive(Clone, Copy, ValueEnum, Debug)] +pub enum ListMode { + // The tab contains no tabs. + Empty, + + // The tab contains tabs. + NonEmpty, +} + #[derive(Subcommand, Debug)] pub enum InputCommand { /// Add URLs as inputs to be categorized. Add { inputs: Vec<Input> }, /// Remove URLs - Remove { inputs: Vec<Input> }, + Remove { + #[arg(add = ArgValueCompleter::new(complete_input_url))] + inputs: Vec<Input>, + }, /// Add all URLs in the file as inputs to be categorized. /// /// This expects each line to contain one URL. - File { file: PathBuf }, + File { + /// The file to read from. + file: PathBuf, + + /// Additional tags to apply to every read URL in the file. + #[arg(add = ArgValueCompleter::new(complete_tag))] + tags: Vec<Tag>, + }, /// Like 'review', but for the inputs that have previously been added. /// It takes a project in which to open the URLs. Review { /// Opens all the URLs in this project. - #[arg(value_parser = task::Project::from_project_string)] + #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] project: task::Project, }, /// List all the previously added inputs. - List, + List { + /// Only list the inputs that have all the specified tags + #[arg(add = ArgValueCompleter::new(complete_tag))] + tags: Vec<Tag>, + }, + + /// Show all the available tags. + Tags {}, +} + +fn complete_task_id(current: &OsStr) -> Vec<CompletionCandidate> { + fn format_task( + task: task::Task, + current: &str, + state: &mut state::State, + ) -> Option<CompletionCandidate> { + let id = { + let Ok(base) = task.working_set_id(state) else { + return None; + }; + base.to_string() + }; + + if !id.starts_with(current) { + return None; + } + + let description = { + let Ok(base) = task.description(state) else { + return None; + }; + StyledStr::from(base) + }; + + Some(CompletionCandidate::new(id).help(Some(description))) + } + + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + let Ok(mut state) = state::State::new_ro() else { + return output; + }; + + let Ok(pending) = state.replica().pending_tasks() else { + return output; + }; + + let Ok(current_project) = task::Project::get_current() else { + return output; + }; + + if let Some(current_project) = current_project { + for t in pending { + let task = task::Task::from(&t); + if let Ok(project) = task.project(&mut state) { + if project == current_project { + if let Some(out) = format_task(task, current, &mut state) { + output.push(out); + } + } + } + } + } else { + for t in pending { + let task = task::Task::from(&t); + if let Some(out) = format_task(task, current, &mut state) { + output.push(out); + } + } + } + + output +} +fn complete_project(current: &OsStr) -> Vec<CompletionCandidate> { + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + let Ok(all) = task::Project::all() else { + return output; + }; + + for a in all { + if a.to_project_display().starts_with(current) { + output.push(CompletionCandidate::new(a.to_project_display())); + } + } + + output +} +fn complete_input_url(current: &OsStr) -> Vec<CompletionCandidate> { + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + let Ok(all) = Input::all() else { + return output; + }; + + for a in all { + if a.to_string().starts_with(current) { + output.push(CompletionCandidate::new(a.to_string())); + } + } + + output +} +fn complete_tag(current: &OsStr) -> Vec<CompletionCandidate> { + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + if !current.starts_with('+') { + output.push(CompletionCandidate::new(format!("+{current}"))); + } + + output +} + +#[cfg(test)] +mod test { + use clap::CommandFactory; + + use super::CliArgs; + #[test] + fn verify_cli() { + CliArgs::command().debug_assert(); + } } diff --git a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs index 0ff0e56e..76eea6dc 100644 --- a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs @@ -1,23 +1,33 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::{ - fs, process, + collections::{HashMap, HashSet}, + fs, str::FromStr, - thread::{self, sleep}, - time::Duration, }; use anyhow::{Context, Result}; -use log::{error, info}; +use log::info; -use crate::cli::InputCommand; +use crate::{browser::open_in_browser, cli::InputCommand, state::State}; -use super::Input; +use super::{Input, Tag}; /// # Errors /// When command handling fails. /// /// # Panics /// When internal assertions fail. -pub fn handle(command: InputCommand) -> Result<()> { +#[allow(clippy::too_many_lines)] +pub fn handle(command: InputCommand, state: &mut State) -> Result<()> { match command { InputCommand::Add { inputs } => { for input in inputs { @@ -33,47 +43,37 @@ pub fn handle(command: InputCommand) -> Result<()> { })?; } } - InputCommand::File { file } => { - let file = fs::read_to_string(file)?; - for line in file.lines() { - let input = Input::from_str(line)?; + InputCommand::File { file, tags } => { + let file = fs::read_to_string(&file) + .with_context(|| format!("Failed to read input file '{}'", file.display()))?; + + let mut tag_set = HashSet::with_capacity(tags.len()); + for tag in tags { + tag_set.insert(tag); + } + + for line in file.lines().map(str::trim) { + if line.is_empty() { + continue; + } + + let mut input = Input::from_str(line)?; + input.tags = input.tags.union(&tag_set).cloned().collect(); + input.commit().with_context(|| { format!("Failed to add input ('{input}') to the input storage.") })?; } } InputCommand::Review { project } => { - let project = project.to_project_display(); - - let local_project = project.clone(); - let handle = thread::spawn(move || { - // We assume that the project is not yet open. - let mut firefox = process::Command::new("firefox") - .args(["-P", local_project.as_str(), "about:newtab"]) - .spawn()?; - - Ok::<_, anyhow::Error>(firefox.wait()?) - }); - // Give Firefox some time to start. - info!("Waiting on firefox to start"); - sleep(Duration::from_secs(4)); - - let project_str = project.as_str(); 'outer: for all in Input::all()?.chunks(100) { info!("Starting review for the first hundred URLs."); - for input in all { - info!("-> '{input}'"); - let status = process::Command::new("firefox") - .args(["-P", project_str, input.url().to_string().as_str()]) - .status()?; - - if status.success() { - input.remove()?; - } else { - error!("Adding `{input}` to Firefox failed!"); - } - } + open_in_browser( + &project, + state, + Some(all.iter().map(|f| f.url.clone()).collect()), + )?; { use std::io::{stdin, stdout, Write}; @@ -98,15 +98,51 @@ pub fn handle(command: InputCommand) -> Result<()> { } } } - - info!("Waiting for firefox to stop"); - handle.join().expect("Should be joinable")?; } - InputCommand::List => { - for url in Input::all()? { + InputCommand::List { tags } => { + let mut tag_set = HashSet::with_capacity(tags.len()); + for tag in tags { + tag_set.insert(tag); + } + + for url in Input::all()? + .iter() + .filter(|input| tag_set.is_subset(&input.tags)) + { println!("{url}"); } } + InputCommand::Tags {} => { + let mut without_tags = 0; + let mut tag_set: HashMap<Tag, u64> = HashMap::new(); + + for input in Input::all()? { + if input.tags.is_empty() { + without_tags += 1; + } + + for tag in input.tags { + if let Some(number) = tag_set.get_mut(&tag) { + *number += 1; + } else { + tag_set.insert(tag, 1); + } + } + } + + let mut tags: Vec<(Tag, u64)> = tag_set.into_iter().collect(); + tags.sort_by_key(|(_, number)| *number); + tags.reverse(); + + for (tag, number) in tags { + println!("{tag} {number}"); + } + + if without_tags != 0 { + println!(); + println!("Witohut tags: {without_tags}"); + } + } } Ok(()) } diff --git a/pkgs/by-name/ts/tskm/src/interface/input/mod.rs b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs index 9ece7a3a..1d1d67f4 100644 --- a/pkgs/by-name/ts/tskm/src/interface/input/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs @@ -1,7 +1,17 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fmt::Display, - fs::{self, read_to_string, File}, + fs, io::Write, path::PathBuf, process::Command, @@ -16,38 +26,47 @@ pub mod handle; pub use handle::handle; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct NoWhitespaceString(String); +pub struct Tag(String); -impl NoWhitespaceString { - /// # Panics - /// If the input contains whitespace. - #[must_use] - pub fn new(input: String) -> Self { - if input.contains(' ') { - panic!("Your input '{input}' contains whitespace. I did not expect that.") +impl Tag { + pub fn new(input: &str) -> Result<Self> { + Self::from_str(input) + } +} + +impl FromStr for Tag { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { + if let Some(tag) = s.strip_prefix('+') { + if tag.contains(' ') { + bail!("Your tag '{s}' should not whitespace.") + } + + Ok(Self(tag.to_owned())) } else { - Self(input) + bail!("Your tag '{s}' does not start with the required '+'"); } } } -impl Display for NoWhitespaceString { +impl Display for Tag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + write!(f, "+{}", self.0) } } -impl NoWhitespaceString { +impl Tag { #[must_use] pub fn as_str(&self) -> &str { &self.0 } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Input { url: Url, - tags: HashSet<NoWhitespaceString>, + tags: HashSet<Tag>, } impl FromStr for Input { @@ -61,13 +80,7 @@ impl FromStr for Input { tags: { tags.trim() .split(' ') - .map(|tag| { - if let Some(tag) = tag.strip_prefix('+') { - Ok(NoWhitespaceString::new(tag.to_owned())) - } else { - bail!("Your tag '{tag}' does not start with the required '+'"); - } - }) + .map(Tag::new) .collect::<Result<_, _>>()? }, }) @@ -91,13 +104,9 @@ impl Display for Input { self.url, self.tags .iter() - .fold(String::new(), |mut acc, tag| { - acc.push('+'); - acc.push_str(tag.as_str()); - acc.push(' '); - acc - }) - .trim() + .map(ToString::to_string) + .collect::<Vec<_>>() + .join(" ") ) } } @@ -113,7 +122,10 @@ impl Input { fn url_path(url: &Url) -> Result<PathBuf> { let base_path = Self::base_path(); - let url_path = base_path.join(url.to_string()); + let url_path = base_path + .join(url.scheme()) + .join(url.host_str().unwrap_or("<No Host>")) + .join(url.path().trim_matches('/')); fs::create_dir_all(&url_path) .with_context(|| format!("Failed to open file: '{}'", url_path.display()))?; @@ -132,17 +144,12 @@ impl Input { pub fn commit(&self) -> Result<()> { let url_path = Self::url_path(&self.url)?; - let url_content = { - if url_path.exists() { - read_to_string(&url_path)? - } else { - String::new() - } - }; - - let mut file = File::create(&url_path) + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&url_path) .with_context(|| format!("Failed to open file: '{}'", url_path.display()))?; - writeln!(file, "{url_content}{self}")?; + writeln!(file, "{self}")?; Self::git_commit(&format!("Add new url: '{self}'"))?; @@ -173,28 +180,7 @@ impl Input { Ok(()) } - /// Commit your changes - fn git_commit(message: &str) -> Result<()> { - let status = Command::new("git") - .args(["add", "."]) - .current_dir(Self::base_path()) - .status()?; - if !status.success() { - bail!("Git add . failed!"); - } - - let status = Command::new("git") - .args(["commit", "--message", message, "--no-gpg-sign"]) - .current_dir(Self::base_path()) - .status()?; - if !status.success() { - bail!("Git commit failed!"); - } - - Ok(()) - } - - /// Get all previously [`Self::commit`]ed inputs. + /// Get all previously [committed][`Self::commit`] inputs. /// /// # Errors /// When IO handling fails. @@ -217,41 +203,58 @@ impl Input { continue; } - let url_value_file = entry - .path() - .to_str() - .expect("All of these should be URLs and thus valid strings"); - assert!(url_value_file.ends_with("/url_value")); - - let url = { - let base = url_value_file - .strip_prefix(&format!("{}/", Self::base_path().display())) - .expect("This will exist"); - - let (proto, path) = base.split_once(':').expect("This will countain a :"); - - let path = path.strip_suffix("/url_value").expect("Will exist"); - - Url::from_str(&format!("{proto}:/{path}")) - .expect("This was a URL, it should still be one") - }; - let tags = { - let url_values = read_to_string(PathBuf::from(url_value_file))?; - url_values - .lines() - .map(|line| { - let input = Self::from_str(line)?; - Ok::<_, anyhow::Error>(input.tags) - }) - .collect::<Result<Vec<HashSet<NoWhitespaceString>>, _>>()? - .into_iter() - .flatten() - .collect() - }; - - output.push(Self { url, tags }); + let url_value_file = entry.path(); + assert!(url_value_file.ends_with("url_value")); + + let url_values = fs::read_to_string(PathBuf::from(url_value_file))?; + + let mut inputs: HashMap<Url, Self> = HashMap::new(); + for input in url_values + .lines() + .map(Self::from_str) + .collect::<Result<Vec<Self>, _>>()? + { + if let Some(found) = inputs.get_mut(&input.url) { + found.tags = found.tags.union(&input.tags).cloned().collect(); + } else { + assert_eq!(inputs.insert(input.url.clone(), input), None); + } + } + + output.extend(inputs.drain().map(|(_, value)| value)); } Ok(output) } + + /// Commit your changes + fn git_commit(message: &str) -> Result<()> { + if !Self::base_path().join(".git").exists() { + let status = Command::new("git") + .args(["init"]) + .current_dir(Self::base_path()) + .status()?; + if !status.success() { + bail!("Git init failed!"); + } + } + + let status = Command::new("git") + .args(["add", "."]) + .current_dir(Self::base_path()) + .status()?; + if !status.success() { + bail!("Git add . failed!"); + } + + let status = Command::new("git") + .args(["commit", "--message", message, "--no-gpg-sign"]) + .current_dir(Self::base_path()) + .status()?; + if !status.success() { + bail!("Git commit failed!"); + } + + Ok(()) + } } diff --git a/pkgs/by-name/ts/tskm/src/interface/mod.rs b/pkgs/by-name/ts/tskm/src/interface/mod.rs index 1a0d934c..513ca317 100644 --- a/pkgs/by-name/ts/tskm/src/interface/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/mod.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + pub mod input; pub mod neorg; pub mod open; diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs index 577de02c..ea3a89ae 100644 --- a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::{ env, fs::{self, read_to_string, File, OpenOptions}, @@ -11,12 +21,12 @@ use crate::{cli::NeorgCommand, state::State}; pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> { match command { - NeorgCommand::Task { id } => { - let project = id.project(state)?; - let path = dirs::data_local_dir() + NeorgCommand::Task { task } => { + let project = task.project(state)?; + let base = dirs::data_local_dir() .expect("This should exists") - .join("tskm/notes") - .join(project.get_neorg_path()?); + .join("tskm/notes"); + let path = base.join(project.get_neorg_path()?); fs::create_dir_all(path.parent().expect("This should exist"))?; @@ -30,15 +40,17 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> { String::new() }; - if !contents.contains(format!("% {}", id.uuid()).as_str()) { + if !contents.contains(format!("% {}", task.uuid()).as_str()) { let mut options = OpenOptions::new(); options.append(true).create(false); let mut file = options.open(&path)?; - file.write_all(format!("* TITLE (% {})", id.uuid()).as_bytes()) - .with_context(|| { - format!("Failed to write task uuid to file: '{}'", path.display()) - })?; + file.write_all( + format!("* {} (% {})", task.description(state)?, task.uuid()).as_bytes(), + ) + .with_context(|| { + format!("Failed to write task uuid to file: '{}'", path.display()) + })?; file.flush() .with_context(|| format!("Failed to flush file: '{}'", path.display()))?; } @@ -49,7 +61,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> { .args([ path.to_str().expect("Should be a utf-8 str"), "-c", - format!("/% {}", id.uuid()).as_str(), + format!("/% {}", task.uuid()).as_str(), ]) .status()?; if !status.success() { @@ -69,11 +81,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> { .args([ "commit", "--message", - format!( - "chore({}): Update", - path.parent().expect("Should have a parent").display() - ) - .as_str(), + format!("chore({}): Update", project.get_neorg_path()?.display()).as_str(), "--no-gpg-sign", ]) .current_dir(path.parent().expect("Will exist")) @@ -84,7 +92,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> { } { - id.mark_neorg_data(state)?; + task.mark_neorg_data(state)?; } } } diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs index dc5cdf19..6bed1e39 100644 --- a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs @@ -1,18 +1,35 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::path::PathBuf; use anyhow::Result; -use crate::task::{run_task, Project}; +use crate::task::{Project, run_task}; pub mod handle; pub use handle::handle; impl Project { + /// Return the stored neorg path of this project. + /// The returned path will never start with a slash (/). pub(super) fn get_neorg_path(&self) -> Result<PathBuf> { let project_path = run_task(&[ "_get", format!("rc.context.{}.rc.neorg_path", self.to_context_display()).as_str(), ])?; - Ok(PathBuf::from(project_path.as_str())) + + let final_path = project_path + .strip_prefix('/') + .unwrap_or(project_path.as_str()); + + Ok(PathBuf::from(final_path)) } } diff --git a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs index 0b565abd..3897a63b 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs @@ -1,48 +1,73 @@ -use std::process; +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::str::FromStr; use anyhow::{bail, Context, Result}; use log::{error, info}; +use url::Url; -use crate::{cli::OpenCommand, rofi, state::State, task}; +use crate::{browser::open_in_browser, cli::OpenCommand, rofi, state::State, task}; +fn is_empty(project: &task::Project) -> Result<bool> { + let tabs = get_tabs(project)?; + + if tabs.is_empty() { + Ok(true) + } else if tabs.len() > 1 { + Ok(false) + } else { + let url = &tabs[0].1; + + Ok(url == &Url::from_str("qute://start/").expect("Hardcoded")) + } +} + +#[allow(clippy::too_many_lines)] pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> { match command { - OpenCommand::Review => { + OpenCommand::Review { non_empty } => { for project in task::Project::all().context("Failed to get all project files")? { - if project.is_touched() { - info!("Reviewing project: '{}'", project.to_project_display()); - open_in_browser(project, state).with_context(|| { - format!( - "Failed to open project ('{}') in Firefox", - project.to_project_display() - ) - })?; - project.untouch().with_context(|| { + let is_empty = is_empty(project)?; + + if project.is_touched() || (non_empty && !is_empty) { + info!( + "Reviewing project: '{}' ({})", + project.to_project_display(), + if is_empty { "is empty" } else { "is not empty" } + ); + open_in_browser(project, state, None::<Vec<Url>>).with_context(|| { format!( - "Failed to untouch project ('{}')", + "Failed to open project ('{}') in qutebrowser", project.to_project_display() ) })?; + + if project.is_touched() { + project.untouch().with_context(|| { + format!( + "Failed to untouch project ('{}')", + project.to_project_display() + ) + })?; + } } } } - OpenCommand::Project { project } => { - let project = if let Some(p) = project { - p - } else if let Some(p) = - task::Project::get_current().context("Failed to get currently focused project")? - { - p - } else { - bail!("You need to either supply a project or have a project active!"); - }; - + OpenCommand::Project { project, urls } => { project.touch().context("Failed to touch project")?; - open_in_browser(&project, state).with_context(|| { + open_in_browser(&project, state, urls).with_context(|| { format!("Failed to open project: {}", project.to_project_display()) })?; } - OpenCommand::Select => { + OpenCommand::Select { urls } => { let selected_project: task::Project = task::Project::from_project_string( &rofi::select( task::Project::all() @@ -60,129 +85,106 @@ pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> { .touch() .context("Failed to touch project")?; - open_in_browser(&selected_project, state).context("Failed to open project")?; + open_in_browser(&selected_project, state, urls).context("Failed to open project")?; } - OpenCommand::ListTabs { project } => { - let project = if let Some(p) = project { - p - } else if let Some(p) = - task::Project::get_current().context("Failed to get currently focused project")? - { - p - } else { - bail!("You need to either supply a project or have a project active!"); + OpenCommand::ListTabs { projects, mode } => { + let projects = { + if let Some(p) = projects { + p + } else if mode.is_some() { + task::Project::all() + .context("Failed to get all projects")? + .to_owned() + } else if let Some(p) = task::Project::get_current() + .context("Failed to get currently focused project")? + { + vec![p] + } else { + bail!("You need to either select projects or pass --mode"); + } }; - let session_store = project.get_sessionstore().with_context(|| { - format!( - "Failed to get session store for project: '{}'", - project.to_project_display() - ) - })?; + for project in &projects { + if let Some(mode) = mode { + match mode { + crate::cli::ListMode::Empty => { + if !is_empty(project)? { + continue; + } + + // We do not need to print, tabs they are always empty. + if projects.len() > 1 { + println!("/* {} */", project.to_project_display()); + } + continue; + } + crate::cli::ListMode::NonEmpty => { + if is_empty(project)? { + continue; + } + } + } + } - let selected = session_store - .windows - .iter() - .map(|w| w.selected) - .collect::<Vec<_>>(); + if projects.len() > 1 { + println!("/* {} */", project.to_project_display()); + } - let tabs = session_store - .windows - .iter() - .flat_map(|window| window.tabs.iter()) - .map(|tab| tab.entries.get(tab.index - 1).expect("This should be Some")) - .collect::<Vec<_>>(); - - for (index, entry) in tabs.iter().enumerate() { - let index = index + 1; - let is_selected = { - if selected.contains(&index) { - "🔻 " - } else { - " " + let tabs = match get_tabs(project) { + Ok(ok) => ok, + Err(err) => { + if projects.len() > 1 { + error!( + "While trying to get the sessionstore for {}: {:?}", + project.to_project_display(), + err + ); + continue; + } + + return Err(err).with_context(|| { + format!( + "While trying to get the sessionstore for {}", + project.to_project_display() + ) + }); } }; - println!("{}{}", is_selected, entry.url); - } - } - } - Ok(()) -} -fn open_in_browser(selected_project: &task::Project, state: &mut State) -> Result<()> { - let old_project: Option<task::Project> = - task::Project::get_current().context("Failed to get currently active project")?; - let old_task: Option<task::Task> = - task::Task::get_current(state).context("Failed to get currently active task")?; - - selected_project.activate().with_context(|| { - format!( - "Failed to active project: '{}'", - selected_project.to_project_display() - ) - })?; - - let tracking_task = { - let all_tasks = selected_project.get_tasks(state).with_context(|| { - format!( - "Failed to get assoctiated tasks for project: '{}'", - selected_project.to_project_display() - ) - })?; - - let tracking_task = all_tasks.into_iter().find(|t| { - let maybe_desc = t.description(state); - if let Ok(desc) = maybe_desc { - desc == "tracking" - } else { - error!( - "Getting task description returned error: {}", - maybe_desc.expect_err("We already check for Ok") - ); - false + for (active, url) in tabs { + let is_selected = { + if active { + "🔻 " + } else { + " " + } + }; + println!("{is_selected}{url}"); + } } - }); - - if let Some(task) = tracking_task { - info!( - "Starting task {} -> tracking", - selected_project.to_project_display() - ); - task.start(state) - .with_context(|| format!("Failed to start task {task}"))?; } - tracking_task - }; - - let status = process::Command::new("firefox") - .args([ - "-P", - selected_project.to_project_display().as_str(), - "about:newtab", - ]) - .status() - .context("Failed to start firefox")?; - - if !status.success() { - error!("Firefox run exited with error."); } - if let Some(task) = tracking_task { - task.stop(state) - .with_context(|| format!("Failed to stop task {task}"))?; - } - if let Some(task) = old_task { - task.start(state) - .with_context(|| format!("Failed to start task {task}"))?; - } + Ok(()) +} - if let Some(project) = old_project { - project.activate().with_context(|| { - format!("Failed to active project {}", project.to_project_display()) - })?; - } else { - task::Project::clear().context("Failed to clear currently focused project")?; - } +fn get_tabs(project: &task::Project) -> Result<Vec<(bool, Url)>> { + let session_store = project.get_sessionstore()?; - Ok(()) + let tabs = session_store + .windows + .iter() + .flat_map(|window| window.tabs.iter()) + .filter_map(|tab| { + tab.history + .iter() + .find(|hist| hist.active) + .map(|hist| (tab.active, hist)) + }) + .collect::<Vec<_>>(); + + Ok(tabs + .into_iter() + .map(|(active, hist)| (active, hist.url.clone())) + .collect()) } diff --git a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs index 2dc75957..e403b4a8 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs @@ -1,106 +1,198 @@ -use std::{collections::HashMap, fs::File, io}; - -use anyhow::{Context, Result}; -use lz4_flex::decompress_size_prepended; -use serde::Deserialize; -use serde_json::Value; +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{ + fs::{self, File}, + io::Read, + str::FromStr, +}; + +use anyhow::{anyhow, Context, Result}; +use taskchampion::chrono::NaiveDateTime; use url::Url; +use yaml_rust2::Yaml; use crate::task::Project; pub mod handle; pub use handle::handle; -impl Project { - pub(super) fn get_sessionstore(&self) -> Result<SessionStore> { - let path = dirs::home_dir() - .expect("Will exist") - .join(".mozilla/firefox") - .join(self.to_project_display()) - .join("sessionstore-backups/recovery.jsonlz4"); - let file = decompress_mozlz4( - File::open(&path) - .with_context(|| format!("Failed to open path '{}'", path.display()))?, - ) - .with_context(|| format!("Failed to decompress file as mozlzh '{}'", path.display()))?; - - let contents: SessionStore = serde_json::from_str(&file).with_context(|| { - format!( - "Failed to deserialize file ('{}') as session store.", - path.display() - ) - })?; - Ok(contents) +/// An Url that also accepts file paths +#[derive(Debug, Clone)] +pub struct UrlLike(Url); + +impl FromStr for UrlLike { + type Err = url::ParseError; + + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { + if let Ok(u) = fs::canonicalize(s) { + Ok(Self(Url::from_file_path(u).expect( + "The path could be canonicalized, as such it is valid for this", + ))) + } else { + Url::from_str(s).map(Self) + } } } -fn decompress_mozlz4<P: io::Read>(mut file: P) -> Result<String> { - const MOZLZ4_MAGIC_NUMBER: &[u8] = b"mozLz40\0"; +impl From<UrlLike> for Url { + fn from(value: UrlLike) -> Self { + value.0 + } +} + +impl Project { + pub(super) fn get_sessionstore(&self) -> Result<SessionStore> { + let path = dirs::data_local_dir() + .context("Failed to get data dir")? + .join("qutebrowser") + .join(self.to_project_display()) + .join("data/sessions/default.yml"); - let mut buf = [0u8; 8]; - file.read_exact(&mut buf) - .context("Failed to read the mozlz40 header.")?; + let mut file = File::open(&path) + .with_context(|| format!("Failed to open path '{}'", path.display()))?; - assert_eq!(buf, MOZLZ4_MAGIC_NUMBER); + let mut yaml_str = String::new(); + file.read_to_string(&mut yaml_str) + .context("Failed to read _autosave.yml path")?; + let yaml = yaml_rust2::YamlLoader::load_from_str(&yaml_str)?; - let mut buf = vec![]; - file.read_to_end(&mut buf).context("Failed to read file")?; + let store = qute_store_from_yaml(&yaml).context("Failed to read yaml store")?; - let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?; + Ok(store) + } +} - Ok(String::from_utf8(uncompressed).expect("This should be valid json and thus utf8")) +fn qute_store_from_yaml(yaml: &[Yaml]) -> Result<SessionStore> { + assert_eq!(yaml.len(), 1); + let doc = &yaml[0]; + + let hash = doc.as_hash().context("Invalid yaml")?; + let windows = hash + .get(&Yaml::String("windows".to_owned())) + .ok_or(anyhow!("Missing windows"))? + .as_vec() + .ok_or(anyhow!("Windows not vector"))?; + + Ok(SessionStore { + windows: windows + .iter() + .map(|window| { + let hash = window.as_hash().ok_or(anyhow!("Windows not hashmap"))?; + + Ok::<_, anyhow::Error>(Window { + geometry: hash + .get(&Yaml::String("geometry".to_owned())) + .ok_or(anyhow!("Missing window geometry"))? + .as_str() + .ok_or(anyhow!("geometry not string"))? + .to_owned(), + tabs: hash + .get(&Yaml::String("tabs".to_owned())) + .ok_or(anyhow!("Missing window tabs"))? + .as_vec() + .ok_or(anyhow!("Tabs not vec"))? + .iter() + .map(|tab| { + let hash = tab.as_hash().ok_or(anyhow!("Tab not hashmap"))?; + + Ok::<_, anyhow::Error>(Tab { + history: hash + .get(&Yaml::String("history".to_owned())) + .ok_or(anyhow!("Missing tab history"))? + .as_vec() + .ok_or(anyhow!("tab history not vec"))? + .iter() + .map(|history| { + let hash = history + .as_hash() + .ok_or(anyhow!("Tab history not hashmap"))?; + + Ok::<_, anyhow::Error>(TabHistory { + active: hash + .get(&Yaml::String("active".to_owned())) + .unwrap_or(&Yaml::Boolean(false)) + .as_bool() + .ok_or(anyhow!("tab history active not bool"))?, + last_visited: NaiveDateTime::from_str( + hash.get(&Yaml::String("last_visited".to_owned())) + .ok_or(anyhow!( + "Missing tab history last_visited" + ))? + .as_str() + .ok_or(anyhow!( + "tab history last_visited not string" + ))?, + ) + .context("Failed to parse last_visited")?, + pinned: hash + .get(&Yaml::String("pinned".to_owned())) + .ok_or(anyhow!("Missing tab history pinned"))? + .as_bool() + .ok_or(anyhow!("tab history pinned not bool"))?, + title: hash + .get(&Yaml::String("title".to_owned())) + .ok_or(anyhow!("Missing tab history title"))? + .as_str() + .ok_or(anyhow!("tab history title not string"))? + .to_owned(), + url: Url::parse( + hash.get(&Yaml::String("url".to_owned())) + .ok_or(anyhow!("Missing tab history url"))? + .as_str() + .ok_or(anyhow!("tab history url not string"))?, + ) + .context("Failed to parse url")?, + zoom: hash + .get(&Yaml::String("zoom".to_owned())) + .unwrap_or(&Yaml::Real("1.0".to_owned())) + .as_f64() + .ok_or(anyhow!("tab history zoom not 64"))?, + }) + }) + .collect::<Result<Vec<_>, _>>()?, + active: hash + .get(&Yaml::String("active".to_owned())) + .unwrap_or(&Yaml::Boolean(false)) + .as_bool() + .ok_or(anyhow!("active not bool"))?, + }) + }) + .collect::<Result<Vec<_>, _>>()?, + }) + }) + .collect::<Result<Vec<_>, _>>()?, + }) } -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct SessionStore { pub windows: Vec<Window>, } - -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct Window { + pub geometry: String, pub tabs: Vec<Tab>, - pub selected: usize, } - -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct Tab { - pub entries: Vec<TabEntry>, - #[serde(rename = "lastAccessed")] - pub last_accessed: u64, - pub hidden: bool, - #[serde(rename = "searchMode")] - pub search_mode: Option<Value>, - #[serde(rename = "userContextId")] - pub user_context_id: u32, - pub attributes: TabAttributes, - #[serde(rename = "extData")] - pub ext_data: Option<HashMap<String, Value>>, - pub index: usize, - #[serde(rename = "requestedIndex")] - pub requested_index: Option<u32>, - pub image: Option<Url>, + pub history: Vec<TabHistory>, + pub active: bool, } - -#[derive(Deserialize, Debug)] -pub struct TabEntry { - pub url: Url, +#[derive(Debug)] +pub struct TabHistory { + pub active: bool, + pub last_visited: NaiveDateTime, + pub pinned: bool, + // pub scroll-pos: pub title: String, - #[serde(rename = "cacheKey")] - pub cache_key: u32, - #[serde(rename = "ID")] - pub id: u32, - #[serde(rename = "docshellUUID")] - pub docshell_uuid: Value, - #[serde(rename = "resultPrincipalURI")] - pub result_principal_uri: Option<Url>, - #[serde(rename = "hasUserInteraction")] - pub has_user_interaction: bool, - #[serde(rename = "triggeringPrincipal_base64")] - pub triggering_principal_base64: Value, - #[serde(rename = "docIdentifier")] - pub doc_identifier: u32, - pub persist: bool, + pub url: Url, + pub zoom: f64, } - -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct TabAttributes {} diff --git a/pkgs/by-name/ts/tskm/src/interface/project/handle.rs b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs index 2b01f5d1..6d44b340 100644 --- a/pkgs/by-name/ts/tskm/src/interface/project/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs @@ -1,6 +1,16 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::{env, fs::File, io::Write}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use log::trace; use crate::{cli::ProjectCommand, task}; @@ -60,10 +70,12 @@ pub fn handle(command: ProjectCommand) -> Result<()> { let new_definition = ProjectDefinition::default(); - assert!(definition - .subprojects - .insert(segment.clone(), new_definition) - .is_none()); + assert!( + definition + .subprojects + .insert(segment.clone(), new_definition) + .is_none() + ); definition = definition .subprojects diff --git a/pkgs/by-name/ts/tskm/src/interface/project/mod.rs b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs index 62069746..8a7fa1b0 100644 --- a/pkgs/by-name/ts/tskm/src/interface/project/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::collections::HashMap; use anyhow::Result; diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs index f4416c6d..e6113111 100644 --- a/pkgs/by-name/ts/tskm/src/main.rs +++ b/pkgs/by-name/ts/tskm/src/main.rs @@ -1,47 +1,32 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use anyhow::Result; -use clap::Parser; -use state::State; +use clap::{CommandFactory, Parser}; -use crate::interface::{input, neorg, open, project}; +use crate::{ + cli::{CliArgs, Command}, + interface::{input, neorg, open, project}, + state::State, +}; +pub mod browser; pub mod cli; pub mod interface; pub mod rofi; pub mod state; pub mod task; -use crate::cli::{CliArgs, Command}; - fn main() -> Result<(), anyhow::Error> { - // TODO: Support these completions for the respective types <2025-04-04> - // - // ID_GENERATION_FUNCTION - // ```sh - // context="$(task _get rc.context)" - // if [ "$context" ]; then - // filter="project:$context" - // else - // filter="0-10000" - // fi - // tasks="$(task "$filter" _ids)" - // - // if [ "$tasks" ]; then - // echo "$tasks" | xargs task _zshids | awk -F: -v q="'" '{gsub(/'\''/, q "\\" q q ); print $1 ":" q $2 q}' - // fi - // ``` - // - // ARGUMENTS: - // ID | *([0-9]) := [[%ID_GENERATION_FUNCTION]] - // The function displays all possible IDs of the eligible tasks. - // - // WS := %ALL_WORKSPACES - // All possible workspaces. - // - // P := %ALL_PROJECTS_PIPE - // The possible project. - // - // F := [[fd . --max-depth 3]] - // A URL-Input file to use as source. + clap_complete::CompleteEnv::with_factory(CliArgs::command).complete(); + let args = CliArgs::parse(); stderrlog::new() @@ -50,14 +35,13 @@ fn main() -> Result<(), anyhow::Error> { .show_module_names(true) .color(stderrlog::ColorChoice::Auto) .verbosity(usize::from(args.verbosity)) - .timestamp(stderrlog::Timestamp::Off) .init() .expect("Let's just hope that this does not panic"); let mut state = State::new_rw()?; match args.command { - Command::Inputs { command } => input::handle(command)?, + Command::Inputs { command } => input::handle(command, &mut state)?, Command::Neorg { command } => neorg::handle(command, &mut state)?, Command::Open { command } => open::handle(command, &mut state)?, Command::Projects { command } => project::handle(command)?, diff --git a/pkgs/by-name/ts/tskm/src/rofi/mod.rs b/pkgs/by-name/ts/tskm/src/rofi/mod.rs index a0591b7f..37c2eafa 100644 --- a/pkgs/by-name/ts/tskm/src/rofi/mod.rs +++ b/pkgs/by-name/ts/tskm/src/rofi/mod.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::{ io::Write, process::{Command, Stdio}, diff --git a/pkgs/by-name/ts/tskm/src/state.rs b/pkgs/by-name/ts/tskm/src/state.rs index 175a7f03..ae71764e 100644 --- a/pkgs/by-name/ts/tskm/src/state.rs +++ b/pkgs/by-name/ts/tskm/src/state.rs @@ -1,7 +1,17 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::path::PathBuf; use anyhow::Result; -use taskchampion::{storage::AccessMode, Replica, StorageConfig}; +use taskchampion::{Replica, StorageConfig, storage::AccessMode}; pub struct State { replica: Replica, diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs index 03a12faa..9c671273 100644 --- a/pkgs/by-name/ts/tskm/src/task/mod.rs +++ b/pkgs/by-name/ts/tskm/src/task/mod.rs @@ -1,3 +1,13 @@ +// nixos-config - My current NixOS configuration +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of my nixos-config. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + use std::{ fmt::Display, fs::{self, read_to_string, File}, @@ -66,6 +76,13 @@ impl Task { pub fn uuid(&self) -> &taskchampion::Uuid { &self.uuid } + pub fn working_set_id(&self, state: &mut State) -> Result<usize> { + Ok(state + .replica() + .working_set()? + .by_uuid(self.uuid) + .expect("The task should be in the working set")) + } fn as_task(&self, state: &mut State) -> Result<taskchampion::Task> { Ok(state @@ -121,7 +138,7 @@ impl Task { .expect("Every task should have a project") .to_owned() }; - let project = Project::from_project_string(output.as_str()) + let project = Project::from_project_string(output.as_str().trim()) .expect("This comes from tw, it should be valid"); Ok(project) } @@ -338,5 +355,13 @@ pub(crate) fn run_task(args: &[&str]) -> Result<String> { trace!("Output (stdout): '{}'", stdout.trim()); trace!("Output (stderr): '{}'", stderr.trim()); + if !output.status.success() { + bail!( + "Command `task {}` failed with status: {}", + args.join(" "), + output.status + ); + } + Ok(stdout.trim().to_owned()) } diff --git a/pkgs/by-name/ts/tskm/update.sh b/pkgs/by-name/ts/tskm/update.sh index 9268caf2..8e36e13e 100755 --- a/pkgs/by-name/ts/tskm/update.sh +++ b/pkgs/by-name/ts/tskm/update.sh @@ -1,3 +1,14 @@ #!/bin/sh -cargo update && cargo upgrade +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +[ "$1" = "upgrade" ] && cargo upgrade +cargo update diff --git a/pkgs/by-name/up/update-sys/package.nix b/pkgs/by-name/up/update-sys/package.nix deleted file mode 100644 index 8777f82d..00000000 --- a/pkgs/by-name/up/update-sys/package.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ - sysLib, - git, - nixos-rebuild, - sudo, - openssh, - coreutils, - mktemp, - gnugrep, - gnused, - systemd, -}: -sysLib.writeShellScript { - name = "update-sys"; - src = ./update-sys.sh; - generateCompletions = true; - keepPath = false; - dependencies = [ - git - nixos-rebuild - sudo - openssh - coreutils - mktemp - gnugrep - gnused - systemd - ]; -} diff --git a/pkgs/by-name/up/update-sys/update-sys.sh b/pkgs/by-name/up/update-sys/update-sys.sh deleted file mode 100755 index d28247f6..00000000 --- a/pkgs/by-name/up/update-sys/update-sys.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env dash - -# shellcheck source=/dev/null -SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH - -help() { - cat <<EOF -This is a NixOS System flake update manager. - -USAGE: - $NAME [--branch <branchname>] [--help] - -OPTIONS: - --branch | -b BRANCHNAME - select a branch to update from. - - --mode | -m MODE - select a mode to update with - - --help | -h - output this help. -ARGUMENTS: - BRANCHNAME := [[ git branch --list --format '%(refname:short)' ]] - The name of the branch to deploy the config from - - MODE := switch|boot|test|build|dry-build|dry-activate|edit|repl|build-vm|build-vm-with-bootloader - See the 'nixos-rebuild' manpage for more information about these modes. -EOF - exit "$1" -} -default_branch=$(mktmp) -BRANCH="" - -while [ "$#" -gt 0 ]; do - case "$1" in - "--help" | "-h") - help 0 - ;; - "--branch" | "-b") - if [ -n "$2" ]; then - BRANCH="$2" - else - error "$1 requires an argument" - help 1 - fi - shift 2 - ;; - "--mode" | "-m") - if [ -n "$2" ]; then - MODE="$2" - else - error "$1 requires an argument" - help 1 - fi - shift 2 - ;; - *) - error "the option $1 does not exist!" - help 1 - ;; - esac -done - -cd /etc/nixos || die "No /etc/nixos" -msg "Starting system update..." -git remote update origin --prune >/dev/null 2>&1 -if ! [ "$BRANCH" = "" ]; then - git switch "$BRANCH" >/dev/null 2>&1 && msg2 "Switched to branch '$BRANCH'" -fi -msg2 "Updating git repository..." -git pull --rebase - -git remote show origin | grep 'HEAD' | cut -d':' -f2 | sed -e 's/^ *//g' -e 's/ *$//g' >"$default_branch" & - -msg2 "Updating system..." -if [ -n "$MODE" ]; then - nixos-rebuild "$MODE" -else - nixos-rebuild switch -fi - -git switch "$(cat "$default_branch")" >/dev/null 2>&1 && msg2 "Switched to branch '$(cat "$default_branch")'" -msg "Finished Update!" - -# vim: ft=sh diff --git a/pkgs/by-name/ya/yambar-modules/.envrc b/pkgs/by-name/ya/yambar-modules/.envrc deleted file mode 100644 index 2f9f1a81..00000000 --- a/pkgs/by-name/ya/yambar-modules/.envrc +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -use flake diff --git a/pkgs/by-name/ya/yambar-modules/.gitignore b/pkgs/by-name/ya/yambar-modules/.gitignore deleted file mode 100644 index ea8c4bf7..00000000 --- a/pkgs/by-name/ya/yambar-modules/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/pkgs/by-name/ya/yambar-modules/Cargo.lock b/pkgs/by-name/ya/yambar-modules/Cargo.lock deleted file mode 100644 index 6b689d29..00000000 --- a/pkgs/by-name/ya/yambar-modules/Cargo.lock +++ /dev/null @@ -1,131 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "libc" -version = "0.2.167" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" - -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "sysinfo" -version = "0.28.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "winapi", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "yambar-modules" -version = "0.1.0" -dependencies = [ - "sysinfo", -] diff --git a/pkgs/by-name/ya/yambar-modules/Cargo.toml b/pkgs/by-name/ya/yambar-modules/Cargo.toml deleted file mode 100644 index 8e3995fe..00000000 --- a/pkgs/by-name/ya/yambar-modules/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "yambar-modules" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -sysinfo = "0.28.4" diff --git a/pkgs/by-name/ya/yambar-modules/flake.lock b/pkgs/by-name/ya/yambar-modules/flake.lock deleted file mode 100644 index 8043448e..00000000 --- a/pkgs/by-name/ya/yambar-modules/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1732617236, - "narHash": "sha256-PYkz6U0bSEaEB1al7O1XsqVNeSNS+s3NVclJw7YC43w=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "af51545ec9a44eadf3fe3547610a5cdd882bc34e", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/pkgs/by-name/ya/yambar-modules/flake.nix b/pkgs/by-name/ya/yambar-modules/flake.nix deleted file mode 100644 index e3d0cd49..00000000 --- a/pkgs/by-name/ya/yambar-modules/flake.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - description = "Extension modules for yambar(1)"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { - nixpkgs, - flake-utils, - ... - }: - flake-utils.lib.eachDefaultSystem (system: let - pkgs = nixpkgs.legacyPackages."${system}"; - in { - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - # rust stuff - cargo - clippy - rustc - rustfmt - - cargo-edit - cargo-expand - ]; - }; - }); -} -# vim: ts=2 - diff --git a/pkgs/by-name/ya/yambar-modules/package.nix b/pkgs/by-name/ya/yambar-modules/package.nix deleted file mode 100644 index 79281429..00000000 --- a/pkgs/by-name/ya/yambar-modules/package.nix +++ /dev/null @@ -1,14 +0,0 @@ -{rustPlatform}: -rustPlatform.buildRustPackage { - pname = "yambar-modules"; - version = "0.1.0"; - - src = ./.; - cargoLock = { - lockFile = ./Cargo.lock; - }; - - meta = { - mainProgram = "yambar-modules"; - }; -} diff --git a/pkgs/by-name/ya/yambar-modules/src/cpu.rs b/pkgs/by-name/ya/yambar-modules/src/cpu.rs deleted file mode 100644 index 5a6dd084..00000000 --- a/pkgs/by-name/ya/yambar-modules/src/cpu.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::{thread, time::Duration}; - -use sysinfo::{CpuExt, System, SystemExt}; - -pub fn cpu() { - let mut sys = System::new(); - - loop { - sys.refresh_cpu(); - let cpu_usage: f32 = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).sum(); - println!( - "cpu|range:0-100|{:.0}", - cpu_usage / sys.cpus().iter().count() as f32 - ); - println!(); - - // Sleeping to give the system time to run for long - // enough to have useful information. - thread::sleep(Duration::from_secs(3)); - } -} diff --git a/pkgs/by-name/ya/yambar-modules/src/main.rs b/pkgs/by-name/ya/yambar-modules/src/main.rs deleted file mode 100644 index 315c3be7..00000000 --- a/pkgs/by-name/ya/yambar-modules/src/main.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::{env::args, process}; - -mod cpu; -mod memory; - -fn main() { - let args: Vec<String> = args().collect(); - - if args.len() != 2 { - eprintln!("Usage: yambar-modules cpu|memory"); - process::exit(1); - } - - match args[1].as_str() { - "cpu" => { - cpu::cpu(); - } - "memory" => { - memory::memory(); - } - other => { - eprintln!("'{other}' is not a valid command. Only 'cpu' or 'memory'."); - process::exit(1); - } - } -} diff --git a/pkgs/by-name/ya/yambar-modules/src/memory.rs b/pkgs/by-name/ya/yambar-modules/src/memory.rs deleted file mode 100644 index 6da714cc..00000000 --- a/pkgs/by-name/ya/yambar-modules/src/memory.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::{thread, time::Duration}; - -use sysinfo::{System, SystemExt}; - -pub fn memory() { - let mut sys = System::new(); - - loop { - sys.refresh_memory(); - - let memory_percentage: f64 = - 100 as f64 * (sys.used_memory() as f64 / sys.total_memory() as f64); - - println!("memperc|string|{:.0}", memory_percentage); - if sys.total_swap() > 0 { - let swap_percentage: f64 = - 100 as f64 * (sys.used_swap() as f64 / sys.total_swap() as f64); - println!("swapperc|string|{:.0}", swap_percentage); - println!("swapstate|bool|true"); - } else { - println!("swapstate|bool|false"); - } - println!(""); - - // Sleeping to give the system time to run for long - // enough to have useful information. - thread::sleep(Duration::from_secs(3)); - } -} diff --git a/pkgs/by-name/yt/yt/package.nix b/pkgs/by-name/yt/yt/package.nix index 3c780374..4e1b2ecb 100644 --- a/pkgs/by-name/yt/yt/package.nix +++ b/pkgs/by-name/yt/yt/package.nix @@ -1,39 +1,75 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { + lib, + rustPlatform, + installShellFiles, fetchgit, - ffmpeg, gitUpdater, - glibc, - lib, - llvmPackages_latest, - makeWrapper, + # buildInputs mpv-unwrapped, + ffmpeg-headless, + openssl, + libffi, + zlib, + curl, + deno, + # NativeBuildInputs python3, - rustPlatform, + makeWrapper, + llvmPackages_latest, + glibc, + mold, sqlite, fd, + pkg-config, + SDL2, }: let - version = "1.5.0"; - - src = fetchgit { - url = "https://git.foss-syndicate.org/bpeetz/clients/yt"; - rev = "v${version}"; - hash = "sha256-P/mpF2KPjoC7XZ6juJubeGEHhL2ajdOeiuIEb5sYrS0="; - }; - - buildInputs = [ - (python3.withPackages (ps: [ps.yt-dlp])) - mpv-unwrapped.dev - ffmpeg - ]; + version = "1.9.0"; + python = python3.withPackages (ps: [ps.yt-dlp]); in - rustPlatform.buildRustPackage { - inherit version src buildInputs; + rustPlatform.buildRustPackage (finalAttrs: { + inherit version; pname = "yt"; + src = fetchgit { + url = "https://git.foss-syndicate.org/bpeetz/clients/yt"; + tag = "v${version}"; + hash = "sha256-/Isgqe7Hda/1kwYY+ciQH/NBAcWvM92vDxWZ9svlQAM="; + }; + + cargoHash = "sha256-U0alYK9mhz6esVf0mad9o7Ra6tRaL9HKCOftyOg34HE="; + + buildInputs = [ + mpv-unwrapped.dev + ffmpeg-headless + openssl + libffi + zlib + curl.dev + python + deno + ]; + nativeBuildInputs = [ + installShellFiles makeWrapper sqlite fd + pkg-config + mold + ]; + + checkInputs = [ + # Needed for the tests in `libmpv2` + SDL2 ]; passthru.updateScript = gitUpdater {rev-prefix = "v";}; @@ -43,27 +79,46 @@ in lib.versions.major llvmPackages_latest.clang-unwrapped.version; in { - FFMPEG_LOCATION = "${lib.getExe ffmpeg}"; - PYO3_PYTHON = lib.getExe (python3.withPackages (ps: [ps.yt-dlp])); + # Needed for the compile time sqlite checks. + DATABASE_URL = "sqlite://database.sqlx"; + + # Required by yt_dlp + FFMPEG_LOCATION = "${lib.getExe ffmpeg-headless}"; + + # Tell pyo3 which python to use. + PYO3_PYTHON = lib.getExe python; + # Needed for the libmpv2. C_INCLUDE_PATH = "${glibc.dev}/include"; - DATABASE_URL = "sqlite://target/database.sqlx"; LIBCLANG_INCLUDE_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include"; LIBCLANG_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so"; }; - # Some tests depend on network access, others require a special library. - doCheck = false; + doCheck = true; + checkFlags = [ + # All of these tests try to connect to the internet to download test data. + "--skip=select::base::test_base" + "--skip=select::file::test_file" + "--skip=select::options::test_options" + "--skip=subscriptions::import_export::test_import_export" + "--skip=subscriptions::naming_subscriptions::test_naming_subscriptions" + "--skip=videos::downloading::test_downloading" + ]; prePatch = '' + # Generate the sqlite db, so that we can run the comp-time sqlite checks. bash ./scripts/mkdb.sh ''; - useFetchCargoVendor = true; - cargoHash = "sha256-0XTbC+mFsczUFXqAtiQ+BIsCfKilerhlzE41pzVjLVs="; - postInstall = '' + installShellCompletion --cmd yt \ + --bash <(COMPLETE=bash $out/bin/yt) \ + --fish <(COMPLETE=fish $out/bin/yt) \ + --zsh <(COMPLETE=zsh $out/bin/yt) + + # NOTE: We cannot clear the path, because we need access to the $EDITOR. <2025-04-04> wrapProgram $out/bin/yt \ - --prefix PATH : ${lib.makeBinPath buildInputs} + --prefix PATH : ${lib.makeBinPath finalAttrs.buildInputs} \ + --set YTDLP_NO_PLUGINS 1 ''; - } + }) |
