diff --git a/.bash_aliases b/.bash_aliases index 8c96a9f..17c5753 100755 --- a/.bash_aliases +++ b/.bash_aliases @@ -1,7 +1,8 @@ #! /usr/bin/bash alias em="emacs --no-init-file --no-splash --no-window-system" -alias ll="exa --binary --group --long" +alias ll="eza --binary --group --long" alias la="ll --all" alias tree="ll --tree" -alias llblk="lsblk -o NAME,TYPE,FSTYPE,SIZE,MOUNTPOINT,LABEL,PARTLABEL,UUID,PARTUUID" +alias llblk="lsblk --output NAME,TYPE,FSTYPE,SIZE,MOUNTPOINT,LABEL,PARTLABEL,UUID,PARTUUID" +alias sudo="run0" diff --git a/.config/containers/systemd/ollama.container b/.config/containers/systemd/ollama.container new file mode 100644 index 0000000..98929ac --- /dev/null +++ b/.config/containers/systemd/ollama.container @@ -0,0 +1,24 @@ +[Unit] +Description=A local LLM server + +[Container] +# keep-sorted start +AutoUpdate=registry +ContainerName=ollama +Environment=OLLAMA_KEEP_ALIVE=10m +Image=docker.io/ollama/ollama:latest +Network=ollama.network +PodmanArgs=--transient-store +PublishPort=11434:11434 +ReadOnly=true +Volume=%h/.local/share/ollama:/root/.ollama:ro,z +# keep-sorted end + +[Install] +WantedBy=default.target + +[Service] +# keep-sorted start +ExecStartPre=mkdir --parents %h/.local/share/ollama +Restart=always +# keep-sorted end diff --git a/.config/containers/systemd/ollama.network b/.config/containers/systemd/ollama.network new file mode 100644 index 0000000..5f8c30b --- /dev/null +++ b/.config/containers/systemd/ollama.network @@ -0,0 +1,5 @@ +[Unit] +Description=Isolated network for my local LLM server + +[Network] +Internal=true diff --git a/.config/containers/systemd/plantuml.container b/.config/containers/systemd/plantuml.container new file mode 100644 index 0000000..aa8057d --- /dev/null +++ b/.config/containers/systemd/plantuml.container @@ -0,0 +1,19 @@ +[Unit] +Description=A local PlantUML server + +[Container] +# keep-sorted start +AutoUpdate=registry +ContainerName=plantuml +Image=docker.io/plantuml/plantuml-server:jetty +Network=private +PodmanArgs=--transient-store +PublishPort=8080:8080 +ReadOnly=true +# keep-sorted end + +[Install] +WantedBy=default.target + +[Service] +Restart=always diff --git a/.config/containers/systemd/transmission.container b/.config/containers/systemd/transmission.container new file mode 100644 index 0000000..6d83357 --- /dev/null +++ b/.config/containers/systemd/transmission.container @@ -0,0 +1,34 @@ +[Unit] +Description=Transmission client service with web interface on localhost:9091 + +[Container] +# keep-sorted start +AutoUpdate=registry +ContainerName=transmission +Environment=PGID=1000 +Environment=PUID=1000 +Image=lscr.io/linuxserver/transmission:latest +Network=private +PodmanArgs=--transient-store +PublishPort=51413:51413 +PublishPort=51413:51413/udp +PublishPort=9091:9091 +ReadOnly=true +UserNS=keep-id +Volume=%h/.config/transmission:/config:Z +Volume=%h/Downloads/transmission/watch:/watch:ro,Z +Volume=%h/Downloads/transmission:/downloads:Z +# keep-sorted end + +[Install] +WantedBy=default.target + +[Service] +# keep-sorted start +ExecStartPre=mkdir --parents %h/.config/transmission +ExecStartPre=mkdir --parents %h/Downloads/transmission +ExecStartPre=mkdir --parents %h/Downloads/transmission/complete +ExecStartPre=mkdir --parents %h/Downloads/transmission/incomplete +ExecStartPre=mkdir --parents %h/Downloads/transmission/watch +Restart=always +# keep-sorted end diff --git a/.config/emacs/init.el b/.config/emacs/init.el index 0be5a4f..bec67ef 100644 --- a/.config/emacs/init.el +++ b/.config/emacs/init.el @@ -23,8 +23,12 @@ :bind (("C-z" . nil) ("C-z i" . find-init-file) ("C-z f" . ffap)) - :hook ((after-save . executable-make-buffer-file-executable-if-script-p) - (xref-after-update . outline-minor-mode)) + :hook ( + ;; keep-sorted start + (after-save . executable-make-buffer-file-executable-if-script-p) + (xref-after-update . outline-minor-mode) + ;; keep-sorted end + ) :init (setq load-path (append (list (expand-file-name "~/.config/emacs/site-lisp/") @@ -32,7 +36,7 @@ ) load-path)) :config - ; keep-sorted start + ;; keep-sorted start (defalias 'yes-or-no-p 'y-or-n-p) (display-battery-mode) (display-time-mode) @@ -42,9 +46,9 @@ (put 'dired-find-alternate-file 'disabled nil) (set-default-file-modes #o750) (windmove-default-keybindings 'super) - ; keep-sorted end + ;; keep-sorted end :custom - ; keep-sorted start + ;; keep-sorted start (auto-save-interval 20) (auto-save-visited-mode t) (auto-save-visited-predicate #'should-auto-save-current-buffer) @@ -56,8 +60,21 @@ (global-auto-revert-non-file-buffers t) (inhibit-startup-screen t) (show-paren-context-when-offscreen 'overlay) + (treesit-enabled-modes t) (xref-search-program 'ripgrep) - ; keep-sorted end + ;; keep-sorted end + ) + +(use-package flyspell + :ensure nil + :bind (:map flyspell-mode-map + ("C-;" . nil)) + :hook ( + ;; keep-sorted start + (prog-mode . flyspell-prog-mode) + (text-mode . flyspell-mode) + ;; keep-sorted end + ) ) (use-package diminish) @@ -75,10 +92,10 @@ ("" . dired-subtree-cycle) ("" . dired-subtree-remove)) :config - ; keep-sorted start + ;; keep-sorted start (defadvice dired-subtree-cycle (after add-icons activate) (revert-buffer)) (defadvice dired-subtree-toggle (after add-icons activate) (revert-buffer)) - ; keep-sorted end + ;; keep-sorted end :custom (dired-subtree-use-backgrounds nil)) @@ -157,7 +174,7 @@ ("M-." . org-open-at-point) ("M-," . org-mark-ring-goto)) :custom - ; keep-sorted start block=yes + ;; keep-sorted start block=yes (org-agenda-start-on-weekday 0) (org-agenda-weekend-days '(5 6)) (org-default-notes-file "~/Documents/notes.org") @@ -172,13 +189,29 @@ "** TODO %?\n %U") )) (org-clock-sound "~/Music/single-ding.wav") - ; keep-sorted end + ;; keep-sorted end ) (use-package org-contrib) (use-package org-contacts :after org-contrib) +(use-package deflate + :ensure (:repo "https://github.com/skuro/deflate")) +(use-package plantuml-mode + :ensure (:repo "https://github.com/skuro/plantuml-mode") + :custom + ;; keep-sorted start + (plantuml-default-exec-mode 'server) + (plantuml-output-type "svg") + (plantuml-server-url "http://localhost:8080") + ;; keep-sorted end + :init + (add-to-list 'auto-mode-alist '("\\.puml\\'" . plantuml-mode)) + (add-to-list 'org-src-lang-modes '("plantuml" . plantuml)) + (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))) + ) + (use-package age :custom (age-default-identity "~/.age/key") @@ -196,7 +229,7 @@ ) (use-package company - :init (global-company-mode) + :config (global-company-mode) :diminish company-mode) (use-package apt-mode @@ -217,7 +250,6 @@ (use-package apheleia :config (apheleia-global-mode) - (setf (alist-get 'emacs-lisp-mode apheleia-mode-alist nil 'remove) nil) (setf (alist-get 'ruff-isort apheleia-formatters) '("ruff" "check" "-n" @@ -236,18 +268,18 @@ (use-package flymake :ensure nil :bind (:map flymake-mode-map - ("C-c C-l" . flymake-show-buffer-diagnostics) - ("C-x C-l" . flymake-show-project-diagnostics) - ("C-c C-n" . flymake-goto-next-error) - ("C-c C-p" . flymake-goto-prev-error)) + ("C-c C-l" . flymake-show-buffer-diagnostics) + ("C-x C-l" . flymake-show-project-diagnostics) + ("C-c C-n" . flymake-goto-next-error) + ("C-c C-p" . flymake-goto-prev-error)) :custom (flymake-show-diagnostics-at-end-of-line t) ) -; Note: debugging Python in a virtualenv requires debugpy to be installed inside the venv +;; Note: debugging Python in a virtualenv requires debugpy to be installed inside the venv (use-package dape) -; Requires poetry to be installed +;; Requires poetry to be installed (use-package poetry) (defun load-python-env () @@ -259,7 +291,7 @@ (eglot-ensure)))) (use-package python - :bind (:map python-mode-map + :bind (:map python-ts-mode-map ("C-c C-p" . nil) ("C-c C-l" . nil) ("C-c t" . elpy-test)) @@ -278,10 +310,10 @@ :config (require 'ein-notebook) :custom - ; keep-sorted start + ;; keep-sorted start (ein:jupyter-default-notebook-directory "~/Projects/notebooks") (ein:output-area-inlined-images t) - ; keep-sorted end + ;; keep-sorted end ) (use-package direnv @@ -306,18 +338,52 @@ auto-mode-alist)) ) +(defun list-ollama-models () + "Query the local Ollama server for the list of installed models." + (let* ((tags-buffer (url-retrieve-synchronously "http://ollama:11434/api/tags")) + (raw-response (with-current-buffer tags-buffer (buffer-string))) + (tags-payload (nth 1 (split-string raw-response "\n\n"))) + (models (gethash "models" (json-parse-string tags-payload))) + (model-names (mapcar (lambda (model) (gethash "name" model)) models))) + (mapcar #'intern model-names))) + +(defun enrich-ollama-models (available library) + "Enrich the available models with metadata from the library of known models." + (mapcar + (lambda (model) + (seq-find (lambda (x) (eq (car x) model)) library model)) + available)) + +(defun generate-ollama-declaration () + (gptel-make-ollama "Ollama" + :host "ollama:11434" + :stream t + :models (enrich-ollama-models (list-ollama-models) gptel--local-models))) + (use-package gptel + :hook + ;; keep-sorted start + (gptel-mode . gptel-highlight-mode) + (gptel-mode . visual-line-mode) + (gptel-post-response . gptel-end-of-response) + (gptel-post-stream . gptel-auto-scroll) + ;; keep-sorted end :custom - ; keep-sorted start - (gptel-backend (gptel-get-backend "Claude")) + ;; keep-sorted start + (gptel--system-message "You are a sassy, sharp-tongued personal assistant. I need you to assist me in crafting responses to questions, dripping with acerbic wit and sarcasm.") + (gptel-backend (gptel-get-backend "Ollama")) (gptel-default-mode 'org-mode) - (gptel-model 'claude-3-5-haiku-20241022) - ; keep-sorted end + (gptel-expert-commands t) + (gptel-highlight-methods '(face margin)) + (gptel-model 'hf.co/unsloth/Devstral-Small-2507-GGUF:latest) + ;; keep-sorted end :preface + (load "local-models.el") (gptel-make-anthropic "Claude" :stream t :key 'gptel-api-key-from-auth-source ) + (generate-ollama-declaration) ) (use-package power-mode) @@ -327,10 +393,10 @@ (require 'emms-setup) (emms-all) :custom - ; keep-sorted start + ;; keep-sorted start (emms-info-functions '(emms-info-native)) (emms-player-list '(emms-player-mpv)) - ; keep-sorted end + ;; keep-sorted end ) (defun my-qr-selection () diff --git a/.config/emacs/site-lisp/local-models.el b/.config/emacs/site-lisp/local-models.el new file mode 100644 index 0000000..ac695eb --- /dev/null +++ b/.config/emacs/site-lisp/local-models.el @@ -0,0 +1,55 @@ +;; -*- lexical-binding: t; -*- + +(defconst gptel--local-models + '( + ;; keep-sorted start + ( + gemma3:latest + :description "A model from Google built on Gemini technology" + :capabilities (media tool-use cache) + :mime-types ("image/bmp" "image/gif" "image/jpeg" "image/png" "image/tiff" "image/webp") + :context-window 128 + :cutoff-date "2024-08" + ) + ( + hf.co/Orenguteng/Llama-3.1-8B-Lexi-Uncensored-V2-GGUF:latest + :description "Uncensored model based on Llama-3.1-8b-Instruct" + :capabilities (tool-use cache) + :context-window 128 + :cutoff-date "2023-12" + ) + ( + hf.co/TheBloke/MythoMax-L2-13B-GGUF:latest + :description "Proficient at both roleplaying and storywriting" + :context-window 32 + ) + ( + hf.co/bartowski/cognitivecomputations_Dolphin-Mistral-24B-Venice-Edition-GGUF:latest + :description "Uncensored version of Mistral 24B" + :context-window 32 + :cutoff-date "2023-10" + ) + ( + hf.co/unsloth/Devstral-Small-2507-GGUF:latest + :capabilities (json tool-use cache) + :description "Agentic LLM for software engineering tasks" + :context-window 128 + ) + ( + llama3.2:latest + :description "Instruction-tuned model optimized for multilingual dialogue" + :capabilities (tool-use cache) + :context-window 128 + :cutoff-date "2023-12" + ) + ( + mollysama/rwkv-7-g0a3:13.3b + :description "Pure RNN reasoning model, suitable for post-training and fine-tuning" + :context-window 1000 + :cutoff-date "2023-10" + ) + ;; keep-sorted end + ) + "List of known local models and associated properties. +Refer to https://gptel.org/manual.html#models for a description of supported properties" + ) diff --git a/.config/emacs/transient/values.el b/.config/emacs/transient/values.el index c77d63e..9dc81a5 100644 --- a/.config/emacs/transient/values.el +++ b/.config/emacs/transient/values.el @@ -1,9 +1,10 @@ ( - ; keep-sorted start + ;; keep-sorted start (magit-commit "--gpg-sign=") (magit-diff:magit-revision-mode "--no-ext-diff" "--stat" "--show-signature") + (magit-fetch "--force" "--prune" "--tags") (magit-log:magit-log-mode "-n256" "--graph" "--decorate" "--show-signature") (magit-merge "--gpg-sign=") (magit-tag "--sign") - ; keep-sorted end + ;; keep-sorted end ) diff --git a/.config/environment.d/ollama.conf b/.config/environment.d/ollama.conf new file mode 100644 index 0000000..6105efa --- /dev/null +++ b/.config/environment.d/ollama.conf @@ -0,0 +1,3 @@ +# keep-sorted start +HOSTALIASES=~/.config/hosts +# keep-sorted end diff --git a/.config/environment.d/testcontainers.conf b/.config/environment.d/testcontainers.conf new file mode 100644 index 0000000..ada45f6 --- /dev/null +++ b/.config/environment.d/testcontainers.conf @@ -0,0 +1,4 @@ +# keep-sorted start +DOCKER_HOST=unix:///run/user/1000/podman/podman.sock +TESTCONTAINERS_RYUK_DISABLED=true +# keep-sorted end diff --git a/.config/foot/foot.ini b/.config/foot/foot.ini index 611e95b..b28aaf2 100644 --- a/.config/foot/foot.ini +++ b/.config/foot/foot.ini @@ -40,9 +40,9 @@ force_color_prompt=true [bell] -# urgent=no +urgent=yes # notify=no -# visual=no +visual=yes # command= # command-focused=no diff --git a/.config/hosts b/.config/hosts new file mode 100644 index 0000000..4aa6d0b --- /dev/null +++ b/.config/hosts @@ -0,0 +1 @@ +ollama localhost diff --git a/.config/mimeapps.list b/.config/mimeapps.list index ff1ce92..ddbdff7 100644 --- a/.config/mimeapps.list +++ b/.config/mimeapps.list @@ -10,6 +10,8 @@ application/xhtml+xml=userapp-Firefox-RJEWT1.desktop application/x-extension-xhtml=userapp-Firefox-RJEWT1.desktop application/x-extension-xht=userapp-Firefox-RJEWT1.desktop x-scheme-handler/magnet=userapp-transmission-gtk-BAUQU2.desktop +audio/ogg=mpv.desktop; +audio/x-opus+ogg=mpv.desktop; [Added Associations] x-scheme-handler/http=userapp-Firefox-RJEWT1.desktop; diff --git a/.config/setup/02-install-debian-testing.sh b/.config/setup/02-install-debian-testing.sh index 7cf2c21..0b55046 100755 --- a/.config/setup/02-install-debian-testing.sh +++ b/.config/setup/02-install-debian-testing.sh @@ -3,8 +3,8 @@ set -euo pipefail IFS=$'\n\t' -sudo cp "$(systemd-path user-configuration)"/setup/sources.list.d/debian.sources /etc/apt/sources.list.d/ -sudo rm /etc/apt/sources.list +run0 cp "$(systemd-path user-configuration)"/setup/sources.list.d/debian.sources /etc/apt/sources.list.d/ +run0 rm /etc/apt/sources.list -sudo apt update -sudo apt --yes full-upgrade +run0 apt update +run0 apt --yes full-upgrade diff --git a/.config/setup/04-install-deb-packages.sh b/.config/setup/04-install-deb-packages.sh index d1e8af7..70370d6 100755 --- a/.config/setup/04-install-deb-packages.sh +++ b/.config/setup/04-install-deb-packages.sh @@ -16,6 +16,7 @@ DEB_PKGS=( build-essential cargo cargo-doc + catatonit curl direnv emacs-mozc @@ -58,8 +59,8 @@ DEB_PKGS=( network-manager-openvpn nmap ntfs-3g + nvtop pipewire-audio - pipx postfix pre-commit pulseaudio-utils @@ -83,7 +84,6 @@ DEB_PKGS=( syncthing thunderbolt-tools timidity - transmission udisks2 unar unrar-free @@ -106,5 +106,6 @@ META_PKGS=( # keep-sorted end ) -sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --yes "${DEB_PKGS[@]}" -sudo DEBIAN_FRONTEND=noninteractive apt-get install --install-recommends --yes "${META_PKGS[@]}" +run0 --setenv=DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --yes "${DEB_PKGS[@]}" +run0 --setenv=DEBIAN_FRONTEND=noninteractive apt-get install --install-recommends --yes "${META_PKGS[@]}" +run0 --setenv=DEBIAN_FRONTEND=noninteractive apt-get remove --yes sudo diff --git a/.config/setup/06-install-pipx-packages.sh b/.config/setup/06-install-pipx-packages.sh deleted file mode 100755 index 91d04f3..0000000 --- a/.config/setup/06-install-pipx-packages.sh +++ /dev/null @@ -1,30 +0,0 @@ -#! /usr/bin/bash - -set -euo pipefail -IFS=$'\n\t' - -PIPX_PKGS=( - # keep-sorted start - python-lsp-server - rshell - ruff - # keep-sorted end -) - -if (( "${#PIPX_PKGS[@]}" != 0 )); then - pipx install "${PIPX_PKGS[@]}" -fi - -PYLSP_PLUGINS=( - # keep-sorted start - fs - podman - pydantic - pylsp-mypy - pylsp-rope - pytest - types-pyxdg - # keep-sorted end -) - -pipx inject python-lsp-server "${PYLSP_PLUGINS[@]}" diff --git a/.config/setup/12-setup-packages.sh b/.config/setup/12-setup-packages.sh index 6c3ad50..ded26af 100755 --- a/.config/setup/12-setup-packages.sh +++ b/.config/setup/12-setup-packages.sh @@ -19,6 +19,6 @@ mkdir --parents ~/Pictures/screenshots pre-commit install -sudo usermod --append --groups dialout "${USER}" +run0 usermod --append --groups dialout "${USER}" -echo 'kernel.perf_event_paranoid=1' | sudo tee '/etc/sysctl.d/51-enable-perf-events.conf' +echo 'kernel.perf_event_paranoid=1' | run0 tee '/etc/sysctl.d/51-enable-perf-events.conf' diff --git a/.config/setup/16-install-tor.sh b/.config/setup/16-install-tor.sh index 44bf3f5..def32bf 100755 --- a/.config/setup/16-install-tor.sh +++ b/.config/setup/16-install-tor.sh @@ -3,12 +3,12 @@ set -euo pipefail IFS=$'\n\t' -sudo cp "$(systemd-path user-configuration)"/setup/sources.list.d/tor.sources /etc/apt/sources.list.d/ -sudo chmod 644 /etc/apt/sources.list.d/tor.sources +run0 cp "$(systemd-path user-configuration)"/setup/sources.list.d/tor.sources /etc/apt/sources.list.d/ +run0 chmod 644 /etc/apt/sources.list.d/tor.sources outfile=/usr/share/keyrings/deb.torproject.org-keyring.pgp -curl --silent https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | sudo tee "${outfile}" >/dev/null -sudo chmod 644 "${outfile}" +curl --silent https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | run0 tee "${outfile}" >/dev/null +run0 chmod 644 "${outfile}" TOR_PKGS=( # keep-sorted start @@ -18,5 +18,5 @@ TOR_PKGS=( # keep-sorted end ) -sudo apt update -sudo apt install --no-install-recommends --yes "${TOR_PKGS[@]}" +run0 apt update +run0 apt install --no-install-recommends --yes "${TOR_PKGS[@]}" diff --git a/.config/setup/18-install-live-git-repo-packages.sh b/.config/setup/18-install-live-git-repo-packages.sh new file mode 100755 index 0000000..97c5a78 --- /dev/null +++ b/.config/setup/18-install-live-git-repo-packages.sh @@ -0,0 +1,6 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +resync-live-git-repo-packages diff --git a/.config/setup/20-install-zoom.sh b/.config/setup/20-install-zoom.sh index d1260cc..c5e6fe1 100755 --- a/.config/setup/20-install-zoom.sh +++ b/.config/setup/20-install-zoom.sh @@ -4,4 +4,4 @@ set -euo pipefail IFS=$'\n\t' curl --location --output-dir /tmp --remote-name https://zoom.us/client/latest/zoom_amd64.deb -sudo apt-get install --no-install-recommends --yes /tmp/zoom_amd64.deb +run0 apt-get install --no-install-recommends --yes /tmp/zoom_amd64.deb diff --git a/.config/setup/31-initialize-backup-repository.sh b/.config/setup/31-initialize-backup-repository.sh index fe2c0cf..81441d6 100755 --- a/.config/setup/31-initialize-backup-repository.sh +++ b/.config/setup/31-initialize-backup-repository.sh @@ -3,8 +3,8 @@ set -euo pipefail IFS=$'\n\t' -sudo mkdir --parents "${BORG_REPO}" -sudo chown "${USER}":"${USER}" "${BORG_REPO}" -sudo chmod 700 "${BORG_REPO}" +run0 mkdir --parents "${BORG_REPO}" +run0 chown "${USER}":"${USER}" "${BORG_REPO}" +run0 chmod 700 "${BORG_REPO}" borg init --encryption=repokey diff --git a/.config/setup/41-download-container-images.sh b/.config/setup/41-download-container-images.sh new file mode 100755 index 0000000..07f44a6 --- /dev/null +++ b/.config/setup/41-download-container-images.sh @@ -0,0 +1,16 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +IMAGES=( + # keep-sorted start + docker.io/ollama/ollama:latest + docker.io/plantuml/plantuml-server:jetty + ghcr.io/hadolint/hadolint:latest + ghcr.io/mongodb/kingfisher:latest + lscr.io/linuxserver/transmission:latest + # keep-sorted end +) + +podman pull "${IMAGES[@]}" diff --git a/.config/setup/46-install-uv-packages.sh b/.config/setup/46-install-uv-packages.sh new file mode 100755 index 0000000..0c09810 --- /dev/null +++ b/.config/setup/46-install-uv-packages.sh @@ -0,0 +1,30 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +UV_PKGS=( + # keep-sorted start + rshell + ruff + # keep-sorted end +) + +for package in "${UV_PKGS[@]}"; do + uv tool install "${package}" +done + +PYLSP_PLUGINS=( + # keep-sorted start + --with fs + --with podman + --with pydantic + --with pylsp-mypy + --with pylsp-rope + --with pytest + --with testcontainers + --with types-docker + --with types-pyxdg + # keep-sorted end +) +uv tool install "${PYLSP_PLUGINS[@]}" python-lsp-server diff --git a/.config/sway/config b/.config/sway/config index 1054a60..8d8adba 100644 --- a/.config/sway/config +++ b/.config/sway/config @@ -32,15 +32,15 @@ include /etc/sway/config-vars.d/* # # output HDMI-A-1 resolution 1920x1080 position 1920,0 # -# You can get the names of your outputs by running: swaymsg -t get_outputs +# You can get the names of your outputs by running: swaymsg --type get_outputs ### Idle configuration # exec swayidle -w \ - timeout 300 'swaylock -f -c 000000' \ + timeout 300 'swaylock --daemonize --color 000000' \ timeout 600 'swaymsg "output * dpms off"' resume 'swaymsg "output * dpms on"' \ - before-sleep 'swaylock -f -c 000000' + before-sleep 'swaylock --daemonize --color 000000' # This will lock your screen after 300 seconds of inactivity, then turn off # your displays after another 300 seconds, and turn your screens back on when @@ -58,7 +58,7 @@ input type:touchpad { tap enabled } -# You can get the names of your inputs by running: swaymsg -t get_inputs +# You can get the names of your inputs by running: swaymsg --type get_inputs # Read `man 5 sway-input` for more information about this section. ### Key bindings @@ -99,9 +99,9 @@ input type:touchpad { # Lock screen # Black - bindsym $wm_mod+l exec swaylock -f -c 000000 + bindsym $wm_mod+l exec swaylock --daemonize --color 000000 # Random color - bindsym $wm_mod+semicolon exec swaylock -f -c $(dd if=/dev/urandom bs=1 count=3 2>/dev/null | hexdump -e '"%02x"') + bindsym $wm_mod+semicolon exec swaylock --daemonize --color $(dd if=/dev/urandom bs=1 count=3 2>/dev/null | hexdump --format '"%02x"') # Exit sway (logs you out of your Wayland session) bindsym $wm_mod+Delete exec leave @@ -188,6 +188,9 @@ input type:touchpad { # Move focus to the parent container bindsym $wm_mod+bracketleft focus parent bindsym $wm_mod+bracketright focus child + + # Select urgent window to move focus to + bindsym $wm_mod+backslash exec select-urgent # # Scratchpad: # diff --git a/.local/bin/clear-buildah b/.local/bin/clear-buildah new file mode 100755 index 0000000..591aebd --- /dev/null +++ b/.local/bin/clear-buildah @@ -0,0 +1,11 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +buildah_containers="$(buildah list | tail --lines +2 | cut --delimiter ' ' --fields 1)" +if [ -z "${buildah_containers}" ]; then + echo "No containers to remove" +else + buildah rm "${buildah_containers}" +fi diff --git a/.config/setup/18-install-git-sync.sh b/.local/bin/docker similarity index 75% rename from .config/setup/18-install-git-sync.sh rename to .local/bin/docker index c5824c1..abd7414 100755 --- a/.config/setup/18-install-git-sync.sh +++ b/.local/bin/docker @@ -3,4 +3,4 @@ set -euo pipefail IFS=$'\n\t' -resync-git-sync +podman "${@:1}" diff --git a/.local/bin/no-ipv6 b/.local/bin/no-ipv6 index 3ad8edb..18495df 100755 --- a/.local/bin/no-ipv6 +++ b/.local/bin/no-ipv6 @@ -3,6 +3,6 @@ set -euo pipefail IFS=$'\n\t' -sudo sysctl net.ipv6.conf.all.disable_ipv6=1 +run0 sysctl net.ipv6.conf.all.disable_ipv6=1 "$@" -sudo sysctl net.ipv6.conf.all.disable_ipv6=0 +run0 sysctl net.ipv6.conf.all.disable_ipv6=0 diff --git a/.local/bin/podllama b/.local/bin/podllama new file mode 100755 index 0000000..39b8e63 --- /dev/null +++ b/.local/bin/podllama @@ -0,0 +1,57 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +if ! PARSED_OPTIONS=$(getopt --options "" --long "offline,volatile" --name "$0" -- "$@"); then + echo "Error parsing options." >&2 + exit 1 +fi + +eval set -- "$PARSED_OPTIONS" + +offline=false +volatile=false + +while true; do + case "$1" in + --offline) + offline=true + shift + ;; + --volatile) + volatile=true + shift + ;; + --) + shift + break + ;; + *) + echo "Internal error!" >&2 + exit 1 + ;; + esac +done + +mkdir --parents ~/.local/share/ollama/ + +if [[ "true" == "${volatile}" ]]; then + PODMAN=(podman --transient-store) + MOUNTS=() +else + PODMAN=(podman) + MOUNTS=(--volume ~/.local/share/ollama/:/root/.ollama) +fi + +if [[ "true" == "${offline}" ]]; then + NETWORK=(--network none) +else + NETWORK=() +fi + +pod_id=$("${PODMAN[@]}" run --detach --rm "${MOUNTS[@]}" "${NETWORK[@]}" ollama:latest) +"${PODMAN[@]}" exec --interactive --tty "${pod_id}" ollama "$@" +"${PODMAN[@]}" kill "${pod_id}" > /dev/null + +emacsclient --eval "(generate-ollama-declaration)" diff --git a/.local/bin/print-status b/.local/bin/print-status index 6192f29..a9e5ab7 100755 --- a/.local/bin/print-status +++ b/.local/bin/print-status @@ -1,6 +1,6 @@ #! /usr/bin/sh -recordings=$(pgrep wf-recorder | wc -l) +recordings=$(pgrep wf-recorder | wc --lines) if [ "${recordings}" = "0" ]; then recs="" else @@ -19,7 +19,7 @@ else numlock="⮔" fi -touchpad=$(swaymsg -t get_inputs | jq -r '[.[] | select(.type == "touchpad")][0] | .libinput.send_events') +touchpad=$(swaymsg --type get_inputs | jq --raw-output '[.[] | select(.type == "touchpad")][0] | .libinput.send_events') if [ "${touchpad}" = "enabled" ]; then touchpad_active="✅" else @@ -29,8 +29,8 @@ fi today=$(date +'%Y-%m-%d') now=$(date +'%H:%M:%S') -hour=$(echo "${now}" | cut -d: -f1 -) -minute=$(echo "${now}" | cut -d: -f2 -) +hour=$(echo "${now}" | cut --delimiter ":" --fields 1 -) +minute=$(echo "${now}" | cut --delimiter ":" --fields 2 -) if [ "${minute}" -lt 30 ]; then case "${hour}" in "00"|"12") clock_face="🕛";; @@ -64,7 +64,7 @@ else fi audio_mute=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}') -audio_volume=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]\+%' | head -n 1 | tr -d %) +audio_volume=$(pactl get-sink-volume @DEFAULT_SINK@ | grep --only-matching '[0-9]\+%' | head --lines 1 | tr --delete %) audio_symbol="❔" if [ "${audio_mute}" = "yes" ]; then audio_symbol="🔇" @@ -81,7 +81,7 @@ else fi mike_mute=$(pactl get-source-mute @DEFAULT_SOURCE@ | awk '{print $2}') -mike_volume=$(pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]\+%' | head -n 1 | tr -d %) +mike_volume=$(pactl get-source-volume @DEFAULT_SOURCE@ | grep --only-matching '[0-9]\+%' | head --lines 1 | tr --delete %) mike_symbol="❔" if [ "${mike_mute}" = "yes" ]; then mike_symbol="🍌" diff --git a/.local/bin/resync-git-sync b/.local/bin/resync-git-sync deleted file mode 100755 index f167ca8..0000000 --- a/.local/bin/resync-git-sync +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/bash - -set -euo pipefail -IFS=$'\n\t' - -REPO_DIR="$(systemd-path user-state-private)"/git-sync -if [ -d "${REPO_DIR}" ] -then - git -C "${REPO_DIR}" pull -else - cd "$(systemd-path user-state-private)" || exit - git clone https://github.com/simonthum/git-sync -fi diff --git a/.local/bin/resync-live-git-repo-packages b/.local/bin/resync-live-git-repo-packages new file mode 100755 index 0000000..bf0229a --- /dev/null +++ b/.local/bin/resync-live-git-repo-packages @@ -0,0 +1,24 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +REPO_DIR="$(systemd-path user-state-private)"/agentbox +if [ -d "${REPO_DIR}" ] +then + git -C "${REPO_DIR}" pull +else + cd "$(systemd-path user-state-private)" || exit + git clone https://github.com/libohad-dev/agentbox + git -C "${REPO_DIR}" checkout podman + ln --symbolic "${REPO_DIR}"/agentbox ~/.local/bin/ +fi + +REPO_DIR="$(systemd-path user-state-private)"/git-sync +if [ -d "${REPO_DIR}" ] +then + git -C "${REPO_DIR}" pull +else + cd "$(systemd-path user-state-private)" || exit + git clone https://github.com/simonthum/git-sync +fi diff --git a/.local/bin/select-urgent b/.local/bin/select-urgent new file mode 100755 index 0000000..69c44c0 --- /dev/null +++ b/.local/bin/select-urgent @@ -0,0 +1,61 @@ +#! /usr/bin/python3 + +import json +import re +import subprocess +from collections.abc import Iterable, Iterator +from typing import Any + + +def urgent_descendents(node: dict[str, Any]) -> Iterator[dict[str, Any]]: + for subnode in node["nodes"]: + yield from find_urgent(subnode) + + +def find_urgent(node: dict[str, Any]) -> Iterator[dict[str, Any]]: + if node["type"] in ("root", "output"): + yield from urgent_descendents(node) + elif node["type"] == "workspace": + for unode in urgent_descendents(node): + yield unode | {"workspace": node["name"]} + elif node["type"] == "con": + if node["focus"]: + yield from urgent_descendents(node) + elif node["urgent"]: + yield {"id": node["id"], "name": node["name"]} + + +def display_option(unode: dict[str, Any]) -> str: + return f"[#{unode['id']} @ WS {unode['workspace']}] {unode['name']}" + + +def select_option(unodes: Iterable[dict[str, Any]]) -> str: + joined = "".join(display_option(unode) + "\n" for unode in unodes) + proc = subprocess.run( + ["fuzzel", "--dmenu"], + input=joined, + capture_output=True, + text=True, + ) + + return proc.stdout + + +def parse_id_from_option(opt: str) -> int: + m = re.match("^\\[#(?P[0-9]+) ", opt) + if m is None: + raise ValueError() + + return int(m["id"]) + + +def main() -> None: + proc = subprocess.run(["swaymsg", "-t", "get_tree"], capture_output=True, text=True) + sway_tree = json.loads(proc.stdout) + opt = select_option(find_urgent(sway_tree)) + con_id = parse_id_from_option(opt) + subprocess.run(["swaymsg", "--raw", f"[con_id={con_id}]", "focus"]) + + +if __name__ == "__main__": + main() diff --git a/.local/bin/take-screenshot b/.local/bin/take-screenshot index 32f4eaf..8a71bea 100755 --- a/.local/bin/take-screenshot +++ b/.local/bin/take-screenshot @@ -37,15 +37,15 @@ else fi get_focused_monitor() { - swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name' + swaymsg --type get_outputs | jq --raw-output '.[] | select(.focused) | .name' } get_focused_window() { - swaymsg -t get_tree | jq -j '.. | select(.type?) | select(.focused).rect | "\(.x),\(.y) \(.width)x\(.height)"' + swaymsg --type get_tree | jq --join-output '.. | select(.type?) | select(.focused).rect | "\(.x),\(.y) \(.width)x\(.height)"' } select_window() { - swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp + swaymsg --type get_tree | jq --raw-output '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp } case "${option}" in @@ -68,11 +68,11 @@ case "${option}" in "📷 Select window → 📋") grim -g "$(select_window)" - | wl-copy;; "📽 Record focused monitor") - wf-recorder -o "$(get_focused_monitor)" -f "$(recording_filename)";; + wf-recorder --output "$(get_focused_monitor)" -f "$(recording_filename)";; "📽 Record select region") - wf-recorder -g "$(slurp)" -f "$(recording_filename)";; + wf-recorder --geometry "$(slurp)" -f "$(recording_filename)";; "📽 Record focused window") - wf-recorder -g "$(get_focused_window)" -f "$(recording_filename)";; + wf-recorder --geometry "$(get_focused_window)" -f "$(recording_filename)";; "📽 Record select window") - wf-recorder -g "$(select_window)" -f "$(recording_filename)";; + wf-recorder --geometry "$(select_window)" -f "$(recording_filename)";; esac diff --git a/.local/bin/temper b/.local/bin/temper new file mode 100755 index 0000000..2fb087f --- /dev/null +++ b/.local/bin/temper @@ -0,0 +1,81 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +default_editor() { + if [ -n "${SUDO_EDITOR+set}" ]; then + echo "${SUDO_EDITOR}" + elif [ -n "${VISUAL+set}" ]; then + echo "${VISUAL}" + elif [ -n "${EDITOR+set}" ]; then + echo "${EDITOR}" + else + echo "No editor configured" 1>&2 + exit 1 + fi +} + +try_command() { + if "$@" 2> /dev/null; then + return + elif run0 "$@" > /dev/null; then + return + else + exit 2 + fi +} + +make_editable_copy() { + orig_file="$1" + tempfile="$2" + + try_command cp -- "${orig_file}" "${tempfile}" +} + +elevate_permissions() { + tempfile="$1" + orig_file="$2" + + try_command chown --reference "${orig_file}" "${tempfile}" + try_command chmod --reference "${orig_file}" "${tempfile}" +} + +update_file() { + tempfile="$1" + orig_file="$2" + + try_command mv --force -- "${tempfile}" "${orig_file}" +} + +clean_tempfiles() { + tempfile="$1" + + rm --force -- "${tempfile}" "${tempfile}.orig" +} + +if [ "$#" -ne "1" ]; then + echo "Expected exactly one input file. Usage:" 1>&2 + echo " $0 FILE" 1>&2 + exit 1 +fi + +editor_cmd="$(default_editor)" +echo "Editing using the command \"${editor_cmd}\"" + +IFS=' ' read -r -a editor <<< "${editor_cmd}" + +orig_file="$1" +tempfile="$(mktemp)" +echo "Using temporary file ${tempfile}" +make_editable_copy "${orig_file}" "${tempfile}" +cp "${tempfile}" "${tempfile}.orig" +"${editor[@]}" "${tempfile}" +if cmp --quiet "${tempfile}" "${tempfile}.orig"; then + echo "No change to file" +else + elevate_permissions "${tempfile}" "${orig_file}" + update_file "${tempfile}" "${orig_file}" +fi + +clean_tempfiles "${tempfile}" diff --git a/.local/bin/upgrade b/.local/bin/upgrade index 95784aa..023622a 100755 --- a/.local/bin/upgrade +++ b/.local/bin/upgrade @@ -4,11 +4,14 @@ set -euo pipefail IFS=$'\n\t' apt_update() { - sudo --reset-timestamp - sudo apt update --audit && apt list --upgradable - sudo apt full-upgrade - sudo apt autoremove - sudo --reset-timestamp + run0 apt update --audit && apt list --upgradable + run0 apt full-upgrade + run0 apt autoremove +} + +emacs_update() { + echo Queuing updates for Emacs packages... + emacsclient --alternate-editor "" --reuse-frame --eval "(elpaca-pull-all t)" --no-wait > /dev/null } cargo_update() { @@ -16,20 +19,27 @@ cargo_update() { cargo install $(cargo install --list | grep '^[a-z0-9_-]\+ v[0-9.]\+:$' | cut --delimiter=' ' --fields=1) } -pipx_update() { - for venv in $(pipx list --json | jq --raw-output ".venvs | keys[]") - do - pipx upgrade "${venv}" - done +uv_update() { + uv tool upgrade --all } -git_sync_update() { - echo Updating git-sync... - resync-git-sync +live_git_repo_update() { + echo Updating packages installed directly via git repos... + resync-live-git-repo-packages +} + +podman_update() { + echo Updating container service images... + podman auto-update + podman --transient-store auto-update + echo Removing dangling images... + podman image prune --force } apt_update -pipx_update +emacs_update +uv_update cargo_update ghup -git_sync_update +live_git_repo_update +podman_update diff --git a/.local/bin/vaulter b/.local/bin/vaulter index f12dd73..0ebad34 100755 --- a/.local/bin/vaulter +++ b/.local/bin/vaulter @@ -3,13 +3,14 @@ set -euo pipefail IFS=$'\n\t' -podman --transient-store run --rm -ti \ +podman --transient-store run --interactive --rm --tty \ --env WAYLAND_DISPLAY \ --env XDG_RUNTIME_DIR=/tmp \ --hostname localhost \ --mount type=bind,source="${XDG_RUNTIME_DIR}"/"${WAYLAND_DISPLAY}",target=/tmp/"${WAYLAND_DISPLAY}" \ + --mount type=bind,source="$(systemd-path user-configuration)"/emacs/site-lisp/local-models.el,target=/root/.config/emacs/site-lisp/local-models.el \ --mount type=bind,readonly=true,source="$(systemd-path user)"/.keys/vaults,target=/root/.age/key \ --mount type=bind,readonly=true,source="$(systemd-path user)"/.keys/vaults.pub,target=/root/.age/key.pub \ --mount type=bind,source="$(systemd-path user)"/Vaults,target=/root/Vaults \ - --network none \ + --network systemd-ollama \ vaulter:latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d32394..d5b6fff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,13 +10,13 @@ repos: exclude: ^\.config/fcitx5/ - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.14.8 hooks: - id: ruff args: [ --fix ] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy - repo: https://github.com/syntaqx/git-hooks @@ -29,6 +29,6 @@ repos: hooks: - id: detect-secrets - repo: https://github.com/google/keep-sorted - rev: v0.6.0 + rev: v0.7.1 hooks: - id: keep-sorted diff --git a/.profile b/.profile index c1e9129..1e7ee3e 100644 --- a/.profile +++ b/.profile @@ -30,6 +30,10 @@ if [ -d "$HOME/.local/bin" ] ; then PATH="$HOME/.local/bin:$PATH" fi +if [ -d "$(go env GOBIN)" ] ; then + PATH="$(go env GOBIN):$PATH" +fi + # Install the git-sync script if [ -d "$(systemd-path user-state-private)"/git-sync ] ; then PATH="$(systemd-path user-state-private)/git-sync:$PATH"