260615.3 · 98792b1dab553d0ac9a206e40c10643d28cc57f5
*: a near complete overhaul of some fundamental components
Particularly, the `video` script and how Wingo boots. We consolidate much here and avoid separate init scripts for each machine. They were a huge pain to maintain and frequently got out of sync from one another.
We do still have some case analysis, which is annoying. But a foolish consistency is the hobgoblin of little minds.
reading declarations
@@ -69,8 +69,8 @@ nh() { } reenv() {- ssh_reagent- eval $(tmux show-environment -s | rg DISPLAY)+ relink-ssh-auth-sock+ eval $(tmux show-environment -s DISPLAY) } ssh_reagent() {@@ -149,5 +149,5 @@ o() { p() { pattern="$1"- pgrep -a "$1" | rg "$1"+ pgrep -a "$pattern" | rg "$1" }
@@ -29,6 +29,9 @@ trust_level = "trusted" [projects."/home/andrew/code/rust/regex/master"] trust_level = "trusted" +[projects."/home/andrew"]+trust_level = "trusted"+ [tui.model_availability_nux] "gpt-5.5" = 4
@@ -14,9 +14,11 @@ /coc /Code /Code\ -\ OSS+/composer /configstore /dconf /dolphinrc+/dolphin.kmessagebox /Element /emaildefaults /enchant@@ -66,8 +68,10 @@ /mirage /monitors.xml* /mozilla+/naps2 /nautilus /nitrogen+/nushell /okularpartrc /okularrc /op@@ -88,11 +92,12 @@ /session /snes9x /sniffnet-/systemd+/spotify /totem /trashrc /Unknown Organization /user-dirs.*+/variety /vivaldi /vivaldi-snapshot /vlc
.config/ag/alacritty/bart/alacritty.toml
@@ -6,7 +6,7 @@ TERM = "xterm-256color" [font]-size = 7.0+size = 7.5 [font.normal] family = "Hack Nerd Font"
.config/ag/gohead/bart/config.ini
@@ -1,3 +1,3 @@ [Monitors]-laptop = eDP-1+main = eDP-1 topleft = DP-3
.config/ag/gohead/flanders/config.ini
@@ -1,5 +1,5 @@ [Monitors]-tv = DP1+main = DP1 -[tv]+[main] mode = 1920x1080
.config/ag/gohead/milhouse/config.ini
@@ -1,2 +1,2 @@ [Monitors]-tv = DP1+main = DP1
.config/ag/gohead/otto/config.ini
@@ -1,5 +1,5 @@ [Monitors]-tv = DP1+main = DP1 -[tv]+[main] mode = 1920x1080
.config/ag/gohead/wiggum/config.ini
@@ -1,5 +1,5 @@ [Monitors]-tv = DP1+main = DP1 -[tv]+[main] mode = 1920x1080
.config/ag/startup/wingo-bart.xinitrc
@@ -1,24 +0,0 @@-#!/bin/sh--set -x-set -e--startup-nitrogen --restore &-video laptop &-dunst &-nm-applet &-ag-blueberry-tray &-(volume mute ; volume mute) &-restart-compositor &-autobright &--# An agent to make the fingerprint sensor work.-/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 &-# Run the 1password tray.-1password \- --silent \- --force-device-scale-factor=1.5 \- --log=debug &> /tmp/1password.log &--exec run-wingo-environment
.config/ag/startup/wingo-duff.xinitrc
@@ -1,8 +0,0 @@-#!/bin/sh--startup &-media-video left middle right-nitrogen --restore-dunst &-exec run-wingo-environment
.config/ag/startup/wingo-flanders.xinitrc
@@ -1,14 +0,0 @@-#!/bin/sh--startup &-sleep 2-video tv-nitrogen --restore-idesk &-no-blanking-loop &-dunst &-nm-applet &-restart-compositor &-(sleep 2 && video tv) &--exec bash -c '(dbus-launch run-wingo ; (while true; do sleep 1000; done))'
.config/ag/startup/wingo-kang.xinitrc
@@ -1,13 +0,0 @@-#!/bin/sh--startup &-nitrogen --restore-(- sleep 0.3- video laptop- dunst &- nm-applet &- blueberry-tray &- (volume mute ; volume mute) &-) &-exec bash -c '(dbus-launch run-wingo ; (while true; do sleep 1000; done))'
.config/ag/startup/wingo-milhouse.xinitrc
@@ -1,12 +0,0 @@-#!/bin/sh--startup &-nitrogen --restore-idesk &-no-blanking-loop &-dunst &-nm-applet &-restart-compositor &-(sleep 1 && video tv) &--exec bash -c '(dbus-launch run-wingo ; (while true; do sleep 1000; done))'
.config/ag/startup/wingo-nas2.xinitrc
@@ -1,15 +0,0 @@-#!/bin/sh--startup-video main &-dunst &-ag-blueberry-tray &-restart-compositor &--# Run the 1password tray.-1password \- --silent \- --force-device-scale-factor=1.5 \- --log=debug &> /tmp/1password.log &--exec run-wingo-environment
.config/ag/startup/wingo-otto.xinitrc
@@ -1,30 +0,0 @@-#!/usr/bin/env zsh--# startup &-# sleep 2-# video tv-# nitrogen --restore-# gkrellm &-# idesk &-# no-blanking-loop &-# dunst &-# nm-applet &-# restart-compositor &-#-# exec bash -c '(dbus-launch run-wingo ; (while true; do sleep 1000; done))'--startup &-nitrogen --restore &-video laptop &-dunst &-nm-applet &-ag-blueberry-tray &-restart-compositor &--# Run the 1password tray.-1password \- --silent \- --force-device-scale-factor=1.5 \- --log=debug &> /tmp/1password.log &--exec run-wingo-environment
.config/ag/startup/wingo-wiggum.xinitrc
@@ -1,15 +0,0 @@-#!/bin/sh--startup &-sleep 2-video tv-nitrogen --restore-gkrellm &-idesk &-no-blanking-loop &-dunst &-nm-applet &-restart-compositor &-(sleep 2 && video tv) &--exec bash -c '(dbus-launch run-wingo ; (while true; do sleep 1000; done))'
.config/ag/startup/wingo.xinitrc
@@ -1,31 +1,3 @@-#!/bin/sh+#!/usr/bin/env bash -# vim: ft=sh sw=2 ts=2 sts=2--base="$HOME/.config/ag/startup"-case "$(myhostname)" in- bart)- exec "$base/wingo-bart.xinitrc" &> /tmp/run-wingo-environment.log- ;;- kang)- exec "$base/wingo-kang.xinitrc"- ;;- nas2)- exec "$base/wingo-nas2.xinitrc"- ;;- duff)- exec "$base/wingo-duff.xinitrc"- ;;- flanders)- exec "$base/wingo-flanders.xinitrc"- ;;- milhouse)- exec "$base/wingo-milhouse.xinitrc"- ;;- wiggum)- exec "$base/wingo-wiggum.xinitrc"- ;;- otto)- exec "$base/wingo-otto.xinitrc"- ;;-esac+x11-init &> /tmp/x11-init.log
@@ -204,7 +204,7 @@ vim.api.nvim_create_autocmd('FileType', { end, }) vim.api.nvim_create_autocmd('FileType', {- pattern = 'sh',+ pattern = {'sh', 'bash', 'zsh'}, callback = function() spacing.space2() vim.b.comment = '#'@@ -278,10 +278,3 @@ vim.api.nvim_create_autocmd('FileType', { -- inoremap # X# end, })-vim.api.nvim_create_autocmd('FileType', {- pattern = 'zsh',- callback = function()- spacing.space4()- vim.b.comment = '#'- end,-})
@@ -2,6 +2,11 @@ -- https://neovim.io/doc/user/lua.html#vim.hl -- https://neovim.io/doc/user/undo.html#undo-persistence +-- Enable project local configuration. Just drop in a `.nvim.lua` in a+-- repository root directory.+vim.o.exrc = true+vim.o.secure = true+ -- Show line numbers. vim.opt.number = true @@ -18,9 +23,29 @@ spacing.space2() -- the current one isn't saved. vim.opt.hidden = true +-- This makes neovim use OSC 52 escapes for clipboard integration. The nice+-- benefit here is that it doesn't require X11 forwarding. Yay!+-- vim.g.clipboard = 'osc52'++vim.g.clipboard = {+ name = 'OSC52 copy, xcp paste',++ copy = {+ ['+'] = require('vim.ui.clipboard.osc52').copy('+'),+ ['*'] = require('vim.ui.clipboard.osc52').copy('*'),+ },++ paste = {+ ['+'] = { 'xcp', '--output' },+ ['*'] = { 'xcp', '--output' },+ },++ cache_enabled = 0,+}+ -- This causes neovim to use the system clipboard for all yanking operations, -- instead of needing to use the '+' or '*' registers explicitly.-vim.opt.clipboard:append('unnamedplus')+vim.opt.clipboard = 'unnamedplus' -- Always disable code folding. vim.opt.foldenable = false@@ -78,21 +103,3 @@ vim.opt.completeopt = 'noselect' -- [1]: https://old.reddit.com/r/neovim/comments/f0qx2y/automatically_reload_file_if_contents_changed/ -- [2]: https://github.com/neovim/neovim/issues/1936 vim.opt.autoread = true---- Make copy/paste from the unnamed register work well.--- I don't remember exactly why I need this.-if vim.fn.executable('xsel') == 1 then- -- Adapted from runtime/autoload/provider/clipboard.vim.- vim.g.clipboard = {- name = 'myxsel',- copy = {- ['+'] = 'xsel --nodetach --input --clipboard',- ['*'] = 'xsel --nodetach --input --primary',- },- paste = {- ['+'] = 'xsel --output --clipboard',- ['*'] = 'xsel --output --primary',- },- cache_enabled = 1,- }-end
.config/systemd/user/.gitignore
@@ -0,0 +1 @@+/default.target.wants
.config/systemd/user/mount-nas.service
@@ -0,0 +1,20 @@+[Unit]+Description=Mount NAS+After=network-online.target++[Service]+Type=simple+Environment=PATH=/home/andrew/bin:/usr/local/bin:/usr/bin:/bin+ExecStart=/home/andrew/bin/mount-nas+ExecStop=/usr/bin/fusermount3 -u /m+Restart=on-failure+RestartSec=30++# Let the wrapper wait instead of failing quickly.+TimeoutStartSec=0++# Avoid tripping the limiter during transient boot weirdness.+StartLimitBurst=20++[Install]+WantedBy=default.target
@@ -134,3 +134,7 @@ if is-mac; then # Our TERM is typically tmux-256color. macOS doesn't seem to recognize it. export TERM=xterm fi++# Bring in secret environment variables. These live in a separate encrypted+# file because I don't really want them to be public.+. "$HOME/.secretenv"
@@ -1,3 +1,4 @@ /.ssh/config filter=git-crypt-home diff=git-crypt-home /.msmtprc filter=git-crypt-home diff=git-crypt-home /.secretastralrc filter=git-crypt-home diff=git-crypt-home+/.secretenv filter=git-crypt-home diff=git-crypt-home
@@ -34,6 +34,7 @@ /.emscripten_cache /.falcon.* /.fceux+/.fehbg /.fontconfig /.fzf* /.ghcup@@ -55,6 +56,8 @@ /.ICEauthority /.isheadless /.islaptop+/.islowerres+/.ismedia /.iswork /.java /.k5login
@@ -3,6 +3,6 @@ table Icon Icon: /usr/share/icons/oxygen/base/64x64/places/folder-blue.png X: 502 Y: 62- Command[0]: dolphin /m/media/kids+ Command[0]: dolphin --new-window /m/media/kids & end
@@ -3,6 +3,6 @@ table Icon Icon: /usr/share/icons/oxygen/base/64x64/places/folder-blue.png X: 402 Y: 62- Command[0]: dolphin /m/media/movies+ Command[0]: dolphin --new-window /m/media/movies & end
@@ -3,6 +3,6 @@ table Icon Icon: /usr/share/icons/oxygen/base/64x64/places/folder-blue.png X: 202 Y: 62- Command[0]: dolphin /m/media/stage+ Command[0]: dolphin --new-window /m/media/stage & end
@@ -3,6 +3,6 @@ table Icon Icon: /usr/share/icons/oxygen/base/64x64/places/folder-blue.png X: 302 Y: 62- Command[0]: dolphin /m/media/tv+ Command[0]: dolphin --new-window /m/media/tv & end
@@ -13,6 +13,9 @@ prependpath() { esac } +# Set the Cargo installation directory.+export CARGO_INSTALL_ROOT="$HOME/.local/cargo"+ # Setup PATH. # shellcheck disable=SC2123 PATH=""@@ -26,38 +29,24 @@ PATH="$(prependpath "$PATH" /usr/bin/vendor_perl)" PATH="$(prependpath "$PATH" /usr/local/sbin)" PATH="$(prependpath "$PATH" /usr/local/bin)" PATH="$(prependpath "$PATH" /usr/local/go/bin)"-# Home specific paths.-PATH="$(prependpath "$PATH" "$HOME/.local/bin")"-PATH="$(prependpath "$PATH" "$HOME/.gem/ruby/2.5.0/bin")"-PATH="$(prependpath "$PATH" "$HOME/go/bin")"-PATH="$(prependpath "$PATH" "$CARGO_INSTALL_ROOT/bin")"-PATH="$(prependpath "$PATH" "$HOME/.cargo/bin")"-PATH="$(prependpath "$PATH" "$HOME/.local/cargo/bin")"-PATH="$(prependpath "$PATH" "$HOME/.ghcup/bin")"+# This is for emscripten. Whhhyyyyyyyyyyy is it in /usr/lib? Wtf?+PATH="$(prependpath "$PATH" "/usr/lib/emscripten")"+# macOS paths+PATH="$(prependpath "$PATH" "$HOME/tmp/nvim-osx64/bin")"+# These come from my m2 mac mini via homebrew.+if [ -r "/opt/homebrew" ]; then+ PATH="$(prependpath "$PATH" "/opt/homebrew/bin")"+ PATH="$(prependpath "$PATH" "/opt/homebrew/sbin")"+fi # My paths.+PATH="$(prependpath "$PATH" "$CARGO_INSTALL_ROOT/bin")" PATH="$(prependpath "$PATH" "$HOME/bin")" PATH="$(prependpath "$PATH" "$HOME/bin/x86_64")" PATH="$(prependpath "$PATH" "$HOME/bin/aarch64")" PATH="$(prependpath "$PATH" "$HOME/rust/ripgrep/target/release")"-PATH="$(prependpath "$PATH" "$HOME/rust/ripgrep/target/release/examples")"-PATH="$(prependpath "$PATH" "$HOME/rust/xsv/target/release")"-# macOS paths-PATH="$(prependpath "$PATH" "$HOME/tmp/nvim-osx64/bin")"-# These come from my m2 mac mini.-PATH="$(prependpath "$PATH" "/opt/homebrew/bin")"-PATH="$(prependpath "$PATH" "/opt/homebrew/sbin")"--# This is for pyenv.-PATH="$(prependpath "$PATH" "$HOME/.pyenv/shims")"--# This is for emscripten. Whhhyyyyyyyyyyy is it in /usr/lib? Wtf?-PATH="$(prependpath "$PATH" "/usr/lib/emscripten")" export PATH -# Set the Cargo installation directory.-export CARGO_INSTALL_ROOT="$HOME/.local/cargo"- # If homebrew was installed in /opt, then it wants all these vars set to # point it in the right direction. if [ -r "/opt/homebrew" ]; then@@ -66,8 +55,7 @@ if [ -r "/opt/homebrew" ]; then export HOMEBREW_REPOSITORY="/opt/homebrew"; export MANPATH="/opt/homebrew/share/man${MANPATH+:$MANPATH}:"; export INFOPATH="/opt/homebrew/share/info:${INFOPATH:-}";- # This apparently tells C/C++ compiles where to find header files?+ # This apparently tells C/C++ compilers where to find header files? # Not sure if it's appropriate to set this unconditionally, alas... export CPATH="/opt/homebrew/include" fi-
@@ -3,6 +3,7 @@ /id_rsa.pub /keys /known_hosts*+/ssh_auth_sock # I can't seem to figure out how to stop Google's tooling from writing SSH # keys to this directory.
@@ -2,78 +2,114 @@ # simplified systemd command, for instance # "sudo systemctl stop xxx" - > "0.stop xxx"-if ! cmd-exists systemd-notify || ! systemd-notify --booted;-then # for not systemd- 0.start() {- sudo rc.d start "$@"- }+0.reboot() {+ systemctl reboot "$@"+} - 0.restart() {- sudo rc.d restart "$@"- }+0.off() {+ systemctl poweroff "$@"+} - 0.stop() {- sudo rc.d stop "$@"- }-else- 0.reboot() {- systemctl reboot "$@"- }+# start systemd service+0.start() {+ sudo systemctl start "$@"+}+# restart systemd service+0.restart() {+ sudo systemctl restart "$@"+}+# stop systemd service+0.stop() {+ sudo systemctl stop "$@"+}+# enable systemd service+0.enable() {+ sudo systemctl enable "$@"+}+# disable a systemd service+0.disable() {+ sudo systemctl disable "$@"+}+# show the status of a service+0.status() {+ systemctl status "$@"+}+# reload a service configuration+0.reload() {+ sudo systemctl --system daemon-reload+}+# list all running service+0.list() {+ systemctl+}+# list all failed service+0.failed () {+ systemctl --failed+}+# list all systemd available unit files+0.list-files() {+ systemctl list-unit-files+}+# check the log+0.log() {+ sudo journalctl --follow --this-boot "$@"+}+# show wants+0.wants() {+ systemctl show -p "Wants" $1.target+}+# analyze the system+0.analyze() {+ systemd-analyze "$@"+} - 0.off() {- systemctl poweroff "$@"- }+# Same as above, but for user services. - # start systemd service- 0.start() {- sudo systemctl start "$@"- }- # restart systemd service- 0.restart() {- sudo systemctl restart "$@"- }- # stop systemd service- 0.stop() {- sudo systemctl stop "$@"- }- # enable systemd service- 0.enable() {- sudo systemctl enable "$@"- }- # disable a systemd service- 0.disable() {- sudo systemctl disable "$@"- }- # show the status of a service- 0.status() {- systemctl status "$@"- }- # reload a service configuration- 0.reload() {- sudo systemctl --system daemon-reload- }- # list all running service- 0.list() {- systemctl- }- # list all failed service- 0.failed () {- systemctl --failed- }- # list all systemd available unit files- 0.list-files() {- systemctl list-unit-files- }- # check the log- 0.log() {- sudo journalctl --follow --this-boot "$@"- }- # show wants- 0.wants() {- systemctl show -p "Wants" $1.target- }- # analyze the system- 0.analyze() {- systemd-analyze "$@"- }-fi+# start systemd service+1.start() {+ systemctl --user start "$@"+}+# restart systemd service+1.restart() {+ systemctl --user restart "$@"+}+# stop systemd service+1.stop() {+ systemctl --user stop "$@"+}+# enable systemd service+1.enable() {+ systemctl --user enable "$@"+}+# disable a systemd service+1.disable() {+ systemctl --user disable "$@"+}+# show the status of a service+1.status() {+ systemctl --user status "$@"+}+# reload a service configuration+1.reload() {+ systemctl --user daemon-reload+}+# list all running service+1.list() {+ systemctl --user+}+# list all failed service+1.failed () {+ systemctl --user --failed+}+# list all systemd available unit files+1.list-files() {+ systemctl --user list-unit-files+}+# check the log+1.log() {+ journalctl --user --follow --this-boot "$@"+}+# show wants+1.wants() {+ systemctl show -p "Wants" $1.target+}
@@ -27,6 +27,10 @@ set-option -sa terminal-overrides ',*-256color:RGB' # Archlinux.) set-option -sa terminal-features ",*:hyperlinks" +# Let OSC 52 escape sequences work for copy/paste.+set -g set-clipboard on+set -g allow-passthrough on+ # Set history limit ridiculously high. set -g history-limit 1073741824 @@ -104,6 +108,5 @@ set-hook -g session-closed "run-shell $HOME/bin/tmux-cleanup \; run-shell $HOME/ # Open a new terminal in the CWD. bind C-n run-shell -b "$HOME/bin/terminal -d #{pane_current_path} -- tmux -u new -c #{pane_current_path} zsh" -# set -g update-environment "SSH_ASKPASS SSH_AGENT_PID SSH_CONNECTION"-# setenv -g SSH_AUTH_SOCK "/tmp/andrew_ssh_auth_sock"-# if-shell 'ls /tmp/andrew_ssh_auth_sock' 'setenv -g SSH_AUTH_SOCK "/tmp/andrew_ssh_auth_sock"'+# Track fresh values for new windows.+set -g update-environment "DISPLAY SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION"
@@ -1,3 +1,6 @@+# shellcheck disable=SC2034,SC2206+# vim: ft=zsh sw=2 ts=2 sts=2+ # Define aliases and generic shell variables. [[ -f ~/.pathrc ]] && . ~/.pathrc [[ -f ~/.aliasrc ]] && . ~/.aliasrc@@ -19,13 +22,8 @@ else umask 0002 fi -# Disable beeping.-setterm --blength 0 > /dev/null 2>&1-# Disable software flow control (Ctrl+Q/Ctrl+S)-# See: https://unix.stackexchange.com/questions/545045/what-is-the-difference-between-ixon-and-ixoff-tty-attributes-stty -ixon- # Don't remove a space before the pipe symbol.+# shellcheck disable=SC2034 ZLE_REMOVE_SUFFIX_CHARS=$' \t\n;&' # Setup LS_COLORS.@@ -36,6 +34,7 @@ fi # Set fpath for custom completions. fpath=(~/.zsh-complete $fpath) if [[ -f /usr/local/share/zsh-completions ]]; then+ # shellcheck disable=SC2128 fpath=(/usr/local/share/zsh-completions $fpath) fi @@ -43,6 +42,7 @@ fi # See: https://superuser.com/questions/458906/zsh-tab-completion-of-git-commands-is-very-slow-how-can-i-turn-it-off __git_other_files() { if [[ "$PWD" = "$HOME" ]]; then+ # shellcheck disable=SC2034 local -a expl _wanted files expl 'other file' _files fi@@ -78,30 +78,6 @@ bindkey '\e[4~' end-of-line # SHIFT-TAB should go backwards during auto-completion. bindkey '\e[Z' reverse-menu-complete -# Setup history paging. We bind UP/DOWN to a special variant of history-# searching that matches according to prefix (not just the first word).-# This will also move the cursor to the end of the line.-autoload -Uz history-search-end-zle -N history-beginning-search-backward-end history-search-end-zle -N history-beginning-search-forward-end history-search-end-# UP does history prefix search, backwards.-# DOWN does history prefix search, forwards.-if is-work; then- # For Ubuntu machines, the normal '\u[A' combo doesn't seem to work for- # arrow keys. It's not clear why. This formulation was taken from:- # https://github.com/zsh-users/zsh-history-substring-search/issues/92- bindkey "$terminfo[kcuu1]" history-beginning-search-backward-end- bindkey "$terminfo[kcud1]" history-beginning-search-forward-end-else- bindkey '\e[A' history-beginning-search-backward-end- bindkey '\e[B' history-beginning-search-forward-end-fi--# CTRL-X CTRL-E edits current command in editor.-autoload -Uz edit-command-line-zle -N edit-command-line-bindkey '^x^e' edit-command-line- # Make it so `command $foo` will potential split "$foo" into multiple # parameters, which is how most other shells work. setopt shwordsplit@@ -162,17 +138,6 @@ add-zsh-hook -Uz preexec preexec # Enable fzf integration. if [[ -f ~/.fzf.zsh ]]; then . ~/.fzf.zsh-else- fzf=(- /usr/share/fzf- /usr/share/doc/fzf/examples- )- for p in $fzf; do- if [[ -d "$p" ]]; then- . "$p/key-bindings.zsh" && . "$p/completion.zsh"- break- fi- done fi # Enable auto suggestions when typing commands.@@ -183,7 +148,7 @@ zshauto=( /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh )-for p in $zshauto; do+for p in "${zshauto[@]}"; do if [[ -f "$p" ]]; then . "$p" break@@ -204,21 +169,23 @@ zshsyntax=( /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh )-for p in $zshsyntax; do+for p in "${zshsyntax[@]}"; do if [[ -f "$p" ]]; then . "$p" break fi done -# Add gvm environment. gvm is used for managing Go installations.-if [[ -f ~/.gvm/scripts/gvm ]]; then- . ~/.gvm/scripts/gvm+# Run commands for configuring tty related options.+# We guard this to ensure it won't run for shells+# not connected to a tty.+if [ -t 0 ]; then+ . ~/.zshtty fi -# Try to reconnect to ssh agent?-# if is-work; then- # if ! ssh-add -L > /dev/null 2>&1; then- # ssh_reagent- # fi-# fi+# Try to setup a symlink for our ssh agent authorization socket. This gives+# it a stable path so that we can reference it in various contexts and+# without needing to change the SSH_AUTH_SOCK environment variable. e.g., For+# reconstructing a tmux session or running sshfs via a systemd user service+# that utilizes the currently running agent.+relink-ssh-auth-sock &> /dev/null
@@ -0,0 +1,32 @@+# vim: ft=zsh sw=2 ts=2 sts=2++# Disable beeping.+setterm --blength 0 > /dev/null 2>&1+# Disable software flow control (Ctrl+Q/Ctrl+S)+# See: https://unix.stackexchange.com/questions/545045/what-is-the-difference-between-ixon-and-ixoff-tty-attributes+stty -ixon++# Setup history paging. We bind UP/DOWN to a special variant of history+# searching that matches according to prefix (not just the first word).+# This will also move the cursor to the end of the line.+autoload -Uz history-search-end+zle -N history-beginning-search-backward-end history-search-end+zle -N history-beginning-search-forward-end history-search-end+# UP does history prefix search, backwards.+# DOWN does history prefix search, forwards.+if is-work; then+ # For Ubuntu machines, the normal '\u[A' combo doesn't seem to work for+ # arrow keys. It's not clear why. This formulation was taken from:+ # https://github.com/zsh-users/zsh-history-substring-search/issues/92+ # shellcheck disable=SC2154+ bindkey "${terminfo[kcuu1]}" history-beginning-search-backward-end+ bindkey "${terminfo[kcud1]}" history-beginning-search-forward-end+else+ bindkey '\e[A' history-beginning-search-backward-end+ bindkey '\e[B' history-beginning-search-forward-end+fi++# CTRL-X CTRL-E edits current command in editor.+autoload -Uz edit-command-line+zle -N edit-command-line+bindkey '^x^e' edit-command-line
@@ -1,9 +1,9 @@-#!/bin/sh+#!/usr/bin/env bash host="$1" summary="$2" body="$3"-if [ -z "$host" ]; then+if [ -z "$host" ] || [ "$host" = "self" ]; then host=$(myhostname) fi if [ -z "$summary" ]; then@@ -11,8 +11,13 @@ if [ -z "$summary" ]; then fi if [ "$host" = "$(myhostname)" ]; then notify-send "$summary" "$body" &- audio-play /usr/share/sounds/freedesktop/stereo/complete.oga &+ pw-play /usr/share/sounds/freedesktop/stereo/complete.oga & wait else- sshrun "$host" A "$host" "$summary" "$body"+ if timeout 2 ssh-add -l &> /dev/null; then+ sshrun "$host" A "$host" "$summary" "$body"+ else+ echo $MY_ALERT_URL+ curl -H "Title: $summary" -d "$body" "$MY_ALERT_URL" &> /dev/null+ fi fi
@@ -197,7 +197,7 @@ pkgs() { terminus-font \ words \ keyd \- bash-language-server+ bash-language-server lua-language-server typescript-language-server pkgfile --update systemctl enable keyd }@@ -207,7 +207,7 @@ xpkgs() { pacman --noconfirm -S \ xterm \ alacritty \- k3b konversation konsole dolphin okular \+ k3b konversation konsole dolphin okular ffmpegthumbs \ qt6ct oxygen oxygen-icons kde-cli-tools \ xbindkeys \ libnotify \@@ -223,10 +223,13 @@ xpkgs() { scrot \ freerdp \ gvim \+ vorbis-tools \ noto-fonts noto-fonts-cjk noto-fonts-emoji noto-fonts-extra \ pipewire-audio pipewire-pulse pipewire-alsa wireplumber \ pamixer pavucontrol \ recordmydesktop \+ feh variety \+ stalonetray \ firefox systemctl --user enable wireplumber }@@ -296,30 +299,13 @@ aurpkgs() { } aurxpkgs() {- # It looks like gkrellm has finally gone the way of the dodo. This is what- # I used to install:- #- # gkrellm- # gkleds- # gkrellsun- # gkrellweather- #- # Also, these programs require gtk2. Which now requires going through the AUR.- # This is bad. Let's try and use stalonetray instead. And find an alternative- # program to set wallpaper. No reason to use gtk2 for that.- #- # trayer- # nitrogen- #- # The only other thing on my system requiring gtk2 is XSANE, but it looks like- # there are many alternatives there too. NAPS2 looks promising:- # https://wiki.archlinux.org/title/SANE yay --noconfirm -S \ dzen2-xft-xpm-xinerama-git \ google-chrome \ 1password 1password-cli \ idesk \ gmrun \+ naps2-bin \ ttf-hack-nerd \ ttf-dejavu \ ttf-fira-mono \@@ -391,7 +377,7 @@ xenvinit() { # in order to make file associations in Dolphin work. WTF. # # See: https://old.reddit.com/r/kde/comments/1bd313p/dolphin_not_recognizing_file_associations/l26i3m2/- mkdir "$HOME/.config/menus"+ mkdir -p "$HOME/.config/menus" url="https://github.com/KDE/plasma-workspace/raw/11e7f5306fa013ec5c2b894a28457dabf5c42bad/menu/desktop/plasma-applications.menu" curl -L "$url" -o "$HOME/.config/menus/applications.menu" }
bin/create-bluetooth-speaker-sinks
@@ -0,0 +1,76 @@+#!/usr/bin/env bash++set -euo pipefail++VIRTUAL_SINK="bluetooth_speakers"+DESC="Bluetooth-Speakers"+TAG="bluetooth-speaker-loopback"++find_speaker_sinks() {+ pactl list sinks |+ awk '+ /^Sink #/ { sink="" }+ /^\s*Name:/ { sink=$2 }+ /^\s*Description:/ {+ desc=$0+ if (sink ~ /^bluez_output\./ && desc ~ /Denon|JBL Flip/) {+ print sink+ }+ }+ '+}++ensure_virtual_sink() {+ if ! pactl list short sinks | awk '{print $2}' | rg -qx "$VIRTUAL_SINK"; then+ pactl load-module module-null-sink \+ sink_name="$VIRTUAL_SINK" \+ sink_properties="device.description=\"$DESC\"" >/dev/null+ fi+ # pactl set-default-sink "$VIRTUAL_SINK"+ # pactl set-sink-volume "$VIRTUAL_SINK" 100%+ pactl set-sink-mute "$VIRTUAL_SINK" 0+}++unload_loopbacks() {+ pactl list modules |+ awk -v tag="$TAG" '+ /^Module #/ {+ id = $2+ sub(/^#/, "", id)+ }+ $0 ~ tag {+ print id+ }+ ' |+ while read -r id; do+ pactl unload-module "$id" || true+ done+}++create_loopbacks() {+ local sinks=()+ mapfile -t sinks < <(find_speaker_sinks)++ if ((${#sinks[@]} == 0)); then+ echo "No connected bluetooth speaker sinks found."+ exit 0+ fi++ for sink in "${sinks[@]}"; do+ # pactl set-sink-volume "$sink" 100% || true+ pactl set-sink-mute "$sink" 0 || true++ pactl load-module module-loopback \+ source="${VIRTUAL_SINK}.monitor" \+ sink="$sink" \+ latency_msec=100 \+ sink_input_properties="application.name=\"$TAG\"" >/dev/null+ done++ echo "Routing $VIRTUAL_SINK to:"+ printf ' %s\n' "${sinks[@]:0:4}"+}++ensure_virtual_sink+unload_loopbacks+create_loopbacks
@@ -0,0 +1,3 @@+#!/usr/bin/env bash++dzen2 -dock -xs "$monitor" -h "$height" -fn "$font" -ta l -bg '#ffffff' -y "$y"
@@ -0,0 +1,19 @@+#!/usr/bin/env bash++if [ $# != 5 ]; then+ echo "$(basename "$0") <font-size> <x> <y> <w> <h>" >&2+ exit 1+fi++font_size="$1"+x="$2"+y="$3"+w="$4"+h="$5"+(+ while "true"; do+ laptop-status "$font_size"+ sleep 10s+ done+) | dzen2 -dock -fn Cousine -fg black -bg white -ta l -sa c \+ -x "$x" -y "$y" -w "$w" -h "$h"
@@ -0,0 +1,19 @@+#!/usr/bin/env bash++. "$HOME/bin/x11-go"++if [ $# != 5 ]; then+ echo "$(basename "$0") <font-size> <x> <y> <w> <h>" >&2+ exit 1+fi++font_size="$1"+x="$2"+y="$3"+w="$4"+h="$5"++script="$HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces"+"$script" | \+ dzen2 -dock -fn "Cousine:size=$font_size" -fg black -bg white -ta l -sa c \+ -x "$x" -y "$y" -w "$w" -h "$h"
@@ -2,12 +2,13 @@ # This used to do more when I used different versions of Chrome on different # machines. But now I just use stable when I use Chrome.-case "$(myhostname)" in- bart)- # And we set the scaling factor for when the DPI is too high.- exec google-chrome-stable --force-device-scale-factor=1.5 "$@"- ;;- *)- exec google-chrome-stable "$@"- ;;-esac+#+# And we set the scaling factor for when the DPI is too high.+# Or when the screen is set far away.+if is-laptop; then+ exec google-chrome-stable --force-device-scale-factor=1.5 "$@"+elif is-media; then+ exec google-chrome-stable --force-device-scale-factor=1.25 "$@"+else+ exec google-chrome-stable "$@"+fi
@@ -31,7 +31,7 @@ # Play volume ding when it changes. ding() { sound=/usr/share/sounds/freedesktop/stereo/audio-volume-change.oga- audio-play "$sound"+ pw-play "$sound" } # Get the volume as a percentage.
@@ -0,0 +1,6 @@+#!/usr/bin/env bash++if [ -f "$HOME/.islowerres" ]; then+ exit 0+fi+exit 1
@@ -0,0 +1,6 @@+#!/bin/sh++if [ -f "$HOME/.ismedia" ]; then+ exit 0+fi+exit 1
@@ -1,12 +1,26 @@ #!/bin/sh -battery="$(batrem -s | tr -d '\n')"-if echo "$battery" | rg -q 'AC'; then- batcolor=77dd77-else- batcolor=ff6961+if [ $# != 1 ]; then+ echo "$(basename "$0") <font-size>" >&2+ exit 1 fi+font_size="$1"+ nowcolor=3b82ff-font='^fn(Cousine:bold:size=18)' now="$(bttf time fmt now -f "^fg(#$nowcolor)%a, %B %d, %Y, %H:%M %Z")"-exec printf "%s%s ^fg(#$batcolor)%s\n" "$font" "$now" "$battery"+if is-laptop; then+ battery="$(batrem -s | tr -d '\n')"+ if echo "$battery" | rg -q 'AC'; then+ batcolor=77dd77+ else+ batcolor=ff6961+ fi+ exec printf \+ "^fn(Cousine:size=$font_size)%s ^fn(Cousine:bold:size=$font_size)^fg(#$batcolor)%s\n" \+ "$now" \+ "$battery"+else+ exec printf \+ "^fn(Cousine:size=$font_size)%s\n" \+ "$now"+fi
@@ -0,0 +1,64 @@+#!/usr/bin/env bash++# This should generally be run via `systemctl --user start mount-nas` instead+# of run directly. See `~/.config/systemd/user/mount-nas.service`. ---AG++set -euo pipefail++export SSH_AUTH_SOCK=/home/andrew/.ssh/ssh_auth_sock++# Wait until the ssh agent socket exists.+until [ -S "$SSH_AUTH_SOCK" ]; do+ echo "waiting for SSH_AUTH_SOCK=$SSH_AUTH_SOCK"+ sleep 1+done++# Wait until the agent has at least one key loaded.+until ssh-add -l >/dev/null 2>&1; do+ echo "waiting for ssh-agent keys"+ sleep 1+done++# Wait until the host resolves.+until getent ahosts home.burntsushi.net >/dev/null 2>&1; do+ echo "waiting for DNS: home.burntsushi.net"+ sleep 2+done++# Wait until SSH auth actually works.+until ssh -o BatchMode=yes -o ConnectTimeout=3 nas true >/dev/null 2>&1; do+ echo "waiting for SSH auth to nas"+ sleep 2+done++# For our media PCs, we mount the NAS as read-only.+# Why? Because I have a 5.5 year old (as of 2026-06-15)+# and sometimes he does wild things.+read_only=+if is-media; then+ read_only="-o ro"+fi+sshfs nas:/m /m \+ -f \+ $read_only \+ -o allow_other \+ -o reconnect \+ -o follow_symlinks \+ -o writeback_cache=no \+ -o kernel_cache \+ -o ServerAliveInterval=15 \+ -o ServerAliveCountMax=3 \+ -o BatchMode=yes &+sshfs_pid=$!++until findmnt -T /m >/dev/null 2>&1; do+ if ! kill -0 "$sshfs_pid" 2>/dev/null; then+ wait "$sshfs_pid"+ exit $?+ fi+ echo "waiting for /m to be mounted"+ sleep 1+done++A self 'NAS is mounted'+wait "$sshfs_pid"
bin/mountnas
@@ -0,0 +1,23 @@+#!/usr/bin/env bash++down="$1"+exist="$(findmnt /m 2>&1)"++case "$(myhostname)" in+ nas|nas2)+ echo "cannot mount media on nas; the nas IS the media!" >&2+ exit 1+ ;;+ duff)+ [ $down = down ] && [ -n "$exist" ] && sudo umount /m+ [ $down != down ] && [ -z "$exist" ] && sudo mount nas2:/m /m+ ;;+ flanders|wiggum|milhouse|otto)+ [ $down = down ] && [ -n "$exist" ] && msshfs-readonly down /m+ [ $down != down ] && [ -z "$exist" ] && msshfs-readonly nas2:/m /m+ ;;+ *)+ [[ $down = down ]] && [[ -n "$exist" ]] && msshfs down /m+ [[ $down != down ]] && [[ -z "$exist" ]] && msshfs nas2:/m /m+ ;;+esac
@@ -0,0 +1,13 @@+#!/usr/bin/env bash++# A better version of `killall` that seems to work more reliably. The key trick+# here is pgrep's `-f` flag, which will search the full command name.+#+# This probably isn't fully robust, but good enough for me.++process="$1"+if [ -z "$process" ]; then+ echo "Usage: $(basename "$0") <process-name>" >&2+ exit 1+fi+pgrep -f "(^|/)$process($|\\s+)" | xargs -r kill
@@ -0,0 +1,19 @@+#!/usr/bin/env bash++if [ $# != 3 ]; then+ echo "$(basename "$0") <x> <y> <height>" >&2+ exit 1+fi+x="$1"+y="$2"+h="$3"+shift 3++stalonetray \+ --background '#ffffff' \+ --geometry 1x1-$x-$y \+ --icon-size "$h" \+ --icon-gravity E \+ --grow-gravity E \+ --window-strut bottom \+ "$@"
@@ -3,11 +3,15 @@ case "$1" in todo) passwords="$(- (cd ~/.password-store && rg --files --sort path) \- | rg -or '$1' '(.*)\.gpg$'+ (+ cd ~/.password-store || exit 1+ rg --files --sort path \+ | rg -v '^astral/' \+ | rg -v '^(andrew|kait)/(machine|ssh)'+ ) | rg -or '$1' '(.*)\.gpg$' )" for f in $passwords; do- (pass "$f" | rg -q '1PASS') && echo $f+ (pass "$f" | rg -q -e 1PASS -e 1STALE) || echo $f done ;; untagged)
@@ -0,0 +1,34 @@+#!/usr/bin/env zsh++append() {+ local entry="$1"+ local append="$2"+ { pass show $entry && echo "$append" } \+ | pass insert --multiline --force $entry+}++stale() {+ # local entry="$1"+ for entry; do+ append "$entry" 1STALE+ done+}++fin() {+ local entry="$1"+ append "$entry" 1PASS+}++_entry() {+ local store="${PASSWORD_STORE_DIR:-$HOME/.password-store}"+ local -a entries++ entries=("${store}"/**/*.gpg(N))+ entries=("${entries[@]#$store/}")+ entries=("${entries[@]%.gpg}")++ _describe 'pass entry' entries+}++compdef _entry stale+compdef _entry fin
bin/place
@@ -1,69 +0,0 @@-#!/bin/sh--if [ $# != 5 ]; then- echo "Usage: $(basename): monitor-number x y width height" >&2- exit 1-fi--# Nice names for args.-monitor=$1-mx=$2-my=$3-mwidth=$4-mheight=$5-font="Cousine:size=18"--# Calculate the height of the "panel" and the y position of the "panel."-height=40-y=$((mheight - height + my))--# Bump font size for some machines.-case "$(myhostname)" in- kang|bart)- font="Cousine:size=25"- height=50- ;;- *)- ;;-esac--# Width constants for the system tray and battery indicator.-tray_width=300-bat_width=650--# Determine the width of the pager based on the width of the system tray and-# battery indicator. If it's a laptop, include the battery indicator. Otherwise,-# leave it out.-if is-laptop; then- pager_width=$((mwidth - bat_width - tray_width))-else- pager_width=$((mwidth - tray_width))-fi--# Always kill the current pager and system tray.-# shellcheck disable=SC2009-ps aux | grep -v grep | grep dzen-workspaces | awk '{print $2}' | xargs -r kill-killall trayer-killall dzen2--# Restart the pager and system tray with appropriate geometry.-# (pager-multihead -x $mx -y $y --width $pager_width --height $height &)--($HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \- | dzen2 -dock -xs 2 -h "$height" -fn "$font" -ta l -bg '#ffffff' -y "$y") &-(trayer --widthtype pixel --width $tray_width \- --heighttype pixel --height $height \- --edge bottom --align right \- --monitor $monitor \- --SetDockType true &)--# If this is a laptop, kill the battery notifier and restart it too.-if is-laptop; then- # shellcheck disable=SC2009- ps aux | grep -v grep | grep batrem | awk '{print $2}' | xargs -r kill- ((while true; do laptop-status && sleep 10s; done) | \- dzen2 -dock -fn "Cousine:size=18" -fg black -bg white \- -x $((mx + pager_width)) -y $y -w $bat_width -h $height &)-fi--exit 0
@@ -6,5 +6,5 @@ if [ -z "$process" ]; then echo "Usage: $(basename "$0") <process-name>" >&2 exit 1 fi-pgrep -f "(^|/)$process($|\\s+)" | xargs -r kill+my-killall "$process" exec "$process" "$@"
@@ -0,0 +1,32 @@+#!/usr/bin/env bash++set -eu++link="$HOME/.ssh/ssh_auth_sock"++if [[+ -n "${SSH_CONNECTION:-}" &&+ -n "${SSH_AUTH_SOCK:-}" &&+ "$SSH_AUTH_SOCK" != "$link" &&+ -S "$SSH_AUTH_SOCK"+]]; then+ mkdir -p "$HOME/.ssh"+ chmod 700 "$HOME/.ssh"+ ln -snf "$SSH_AUTH_SOCK" "$link"+fi++# When our link isn't valid, try to make it so.+if ! [ -S "$link" ]; then+ for agent in "$HOME/.ssh/agent"/*; do+ if SSH_AUTH_SOCK="$agent" timeout 2s ssh-add -l > /dev/null 2>&1; then+ ln -snf "$agent" "$link"+ break+ fi+ done+fi++if [ -S "$link" ]; then+ printf '%s\n' "$link"+else+ exit 1+fi
@@ -10,10 +10,7 @@ Usage: See `freq --help` for more detail. */ -#![allow(warnings)]- use std::{- borrow::Cow, io::Write, path::{Path, PathBuf}, };@@ -368,8 +365,6 @@ impl FreqsOtherMany { fn into_table(self, config: &Config) -> Table { let mut table = Table::default(); for (units, freq) in self.map {- use std::fmt::Write;- table.total += freq; if !units.iter().all(|unit| config.include_unit(&unit)) { continue;
bin/rust/setup-system-links/Cargo.lock
@@ -1,39 +1,35 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing.+version = 4+ [[package]] name = "anyhow"-version = "1.0.32"+version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags"-version = "1.2.1"+version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr"-version = "0.2.13"+version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931"+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "lazy_static", "memchr", "regex-automata", ] -[[package]]-name = "byteorder"-version = "1.3.4"-source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"- [[package]] name = "clap"-version = "2.33.3"+version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", "textwrap",@@ -42,24 +38,21 @@ dependencies = [ [[package]] name = "lazy_static"-version = "1.4.0"+version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "memchr"-version = "2.3.3"+version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "regex-automata"-version = "0.1.9"+version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"-dependencies = [- "byteorder",-]+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "same-file"@@ -92,48 +85,40 @@ dependencies = [ [[package]] name = "unicode-width"-version = "0.1.8"+version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "walkdir"-version = "2.3.1"+version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file",- "winapi", "winapi-util", ] [[package]]-name = "winapi"-version = "0.3.9"+name = "winapi-util"+version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [- "winapi-i686-pc-windows-gnu",- "winapi-x86_64-pc-windows-gnu",+ "windows-sys", ] [[package]]-name = "winapi-i686-pc-windows-gnu"-version = "0.4.0"+name = "windows-link"+version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]]-name = "winapi-util"-version = "0.1.5"+name = "windows-sys"+version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [- "winapi",+ "windows-link", ]--[[package]]-name = "winapi-x86_64-pc-windows-gnu"-version = "0.4.0"-source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
bin/rust/setup-system-links/Cargo.toml
@@ -4,6 +4,10 @@ version = "0.0.1" authors = ["Andrew Gallant <jamslam@gmail.com>"] edition = "2018" +[[bin]]+name = "setup-system-links"+path = "main.rs"+ [dependencies] anyhow = "1.0.32" bstr = "0.2.13"
bin/rust/setup-system-links/main.rs
@@ -0,0 +1,446 @@+// Add a convention that lets us avoid doing symlinks. e.g., For PAM.++#[cfg(not(unix))]+compile_error!("setup-system-links only works on Unix systems");++use std::collections::{BTreeMap, BTreeSet};+use std::env;+use std::ffi::{OsStr, OsString};+use std::io::{self, Write};+#[cfg(unix)]+use std::os::unix;+use std::path::{Path, PathBuf};++use anyhow::Context;+use bstr::{BString, ByteSlice, ByteVec};+use walkdir::WalkDir;++fn main() -> anyhow::Result<()> {+ let args = Args::parse()?;+ let mut links = Links::discover(&args.system, &args.hostname)?;+ if let Some(ref backup_dir) = args.backup {+ links.backup(backup_dir.to_owned());+ }+ if args.dry_run {+ links.dry_run(std::io::stdout())?;+ } else {+ links.run(std::io::stdout())?;+ }+ Ok(())+}++/// Represents a collection of links derived from this machine's ~/system+/// directory.+#[derive(Debug)]+struct Links {+ links: Vec<Link>,+ backup: Option<PathBuf>,+}++impl Links {+ /// Determine the full set of system config links for this machine given+ /// the system configuration directory and this machine's hostname.+ fn discover(system: &Path, hostname: &str) -> anyhow::Result<Links> {+ // Collect all leaf files in ~/system, creating a map, e.g.,+ // etc/default/grub |--> {default, duff, frink, ...}+ // Note that the key lacks a leading slash. We add that below.+ #[derive(Debug)]+ struct Leaf {+ machine_config_name: String,+ copy: bool,+ }++ impl Eq for Leaf {}++ impl PartialEq for Leaf {+ fn eq(&self, rhs: &Leaf) -> bool {+ self.machine_config_name == rhs.machine_config_name+ }+ }++ impl Ord for Leaf {+ fn cmp(&self, rhs: &Leaf) -> std::cmp::Ordering {+ self.machine_config_name.cmp(&rhs.machine_config_name)+ }+ }++ impl PartialOrd for Leaf {+ fn partial_cmp(&self, rhs: &Leaf) -> Option<std::cmp::Ordering> {+ Some(self.cmp(rhs))+ }+ }++ let mut all: BTreeMap<PathBuf, BTreeSet<Leaf>> = BTreeMap::new();+ for result in WalkDir::new(system) {+ let dent = result.with_context(|| {+ format!("{}: failed to walk directory", system.display())+ })?;+ if dent.file_type().is_dir() {+ continue;+ }++ // Should never fail since 'system' is our root and is guaranteed+ // to be non-empty.+ let dst = dent+ .path()+ .strip_prefix(system)+ .unwrap()+ .parent()+ .unwrap()+ .to_path_buf();+ if dst.as_os_str().is_empty() {+ // These are files directly in ~/system/ like+ // ~/system/README.md and are not part of our config.+ continue;+ }++ let machine_config_name =+ dent.file_name().to_str().with_context(|| {+ format!("{} is not valid UTF-8", dent.path().display())+ })?;+ let mut leaf = Leaf {+ machine_config_name: machine_config_name.to_string(),+ copy: false,+ };+ if let Some(hostname) = machine_config_name.strip_prefix("copy:") {+ leaf.machine_config_name = hostname.to_string();+ leaf.copy = true;+ }+ all.entry(dst).or_insert_with(|| BTreeSet::new()).insert(leaf);+ }+ // Now organize them into links by choosing the appropriate specific+ // config for this machine.+ let mut links = vec![];+ let attempts = [+ Leaf { machine_config_name: hostname.to_string(), copy: false },+ Leaf { machine_config_name: "default".to_string(), copy: false },+ ];+ for (relative_dst, configs) in all {+ let dst = Path::new("/").join(&relative_dst);+ for attempt in &attempts {+ let Some(config) = configs.get(attempt) else { continue };+ let src = system.join(relative_dst).join(if !config.copy {+ attempt.machine_config_name.to_string()+ } else {+ format!("copy:{}", attempt.machine_config_name)+ });+ links.push(Link { src, dst, copy: config.copy });+ break;+ }+ }+ Ok(Links { links, backup: None })+ }++ /// Indicates that the destination of each link should be backed up+ /// first.+ fn backup(&mut self, dir: PathBuf) {+ self.backup = Some(dir);+ }++ /// Create the links that do not yet exist.+ ///+ /// When backup is enabled (the default), then all config files will be+ /// backed up before attempting any link. If any backup fails, then no+ /// link will be created.+ ///+ /// The equivalent Unix command to each operation will be written to the+ /// given writer (like during a dry run).+ fn run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {+ let mut to_link = vec![];+ for link in &self.links {+ if link.exists()? {+ continue;+ }+ to_link.push(link);+ }+ if let Some(ref backup_dir) = self.backup {+ for link in &to_link {+ link.backup(&mut wtr, backup_dir)?;+ }+ }+ for link in to_link {+ link.install(&mut wtr)?;+ }+ Ok(())+ }++ /// Print the equivalent Unix commands that this would execute if we were+ /// running for real, but make no changes to the file system.+ fn dry_run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {+ let mut to_link = vec![];+ for link in &self.links {+ if link.exists()? {+ continue;+ }+ to_link.push(link);+ }+ if let Some(ref backup_dir) = self.backup {+ for link in &to_link {+ link.dry_backup(&mut wtr, backup_dir)?;+ }+ }+ for link in to_link {+ link.dry_install(&mut wtr)?;+ }+ Ok(())+ }+}++/// Represents a link between a file in ~/system and an actual system+/// configuration path on the system. e.g., ~/system/etc/default/grub/krusty+/// will link to /etc/default/grub on my 'krusty' laptop.+#[derive(Debug)]+struct Link {+ /// The ~/system/ path to this machine's config.+ src: PathBuf,+ /// The path to where this config lives on the system.+ dst: PathBuf,+ /// When true, we create a completely new file that is+ /// an identical copy of the source. This is needed in+ /// some cases (like for `/etc/pam.d/system-auth`) where+ /// symlinks are tricky because of permissions. Namely,+ /// the permission of the source file are relevant.+ ///+ /// We can't use hard-links either, because for PAM, files+ /// in `/etc/pam.d` need to be owned by root. But we don't+ /// want this for `~/system`, where files should be owned by+ /// the user. Hardlinks don't allow it.+ ///+ /// So... we copy.+ copy: bool,+}++impl Link {+ /// Write the equivalent Unix command we would execute if we were running+ /// for real.+ fn dry_install<W: Write>(&self, mut wtr: W) -> io::Result<()> {+ let (prog, flags) = if self.copy { ("cp", "") } else { ("ln", " -s") };+ writeln!(+ wtr,+ "{prog}{flags} \"{}\" \"{}\"",+ self.src.display(),+ self.dst.display()+ )+ }++ /// Write the equivalent Unix command we would execute to backup the config+ /// file if we were running for real.+ fn dry_backup<W: Write>(&self, mut wtr: W, dir: &Path) -> io::Result<()> {+ if !self.dst.exists() {+ return Ok(());+ }+ let backup_dst = dir.join(self.relative_dst());+ // OK since self.dst is guaranteed to be non-empty.+ let parent = backup_dst.parent().unwrap();+ writeln!(wtr, "mkdir -p \"{}\"", parent.display(),)?;+ writeln!(+ wtr,+ "cp \"{}\" \"{}\"",+ self.dst.display(),+ backup_dst.display()+ )+ }++ /// Link (or install) the source to the destination, overwriting the+ /// destination if it exists. Callers should ensure that the destination is+ /// backed up before executing.+ fn install<W: Write>(&self, wtr: W) -> anyhow::Result<()> {+ if self.dst.exists() {+ std::fs::remove_file(&self.dst).with_context(|| {+ format!(+ "failed to delete {} before creating symlink from {}",+ self.dst.display(),+ self.src.display()+ )+ })?;+ }+ if self.copy {+ std::fs::copy(&self.src, &self.dst).with_context(|| {+ format!(+ "failed to copy {} to {}",+ self.src.display(),+ self.dst.display()+ )+ })?;+ } else {+ unix::fs::symlink(&self.src, &self.dst).with_context(|| {+ format!(+ "failed to link {} to {}",+ self.src.display(),+ self.dst.display()+ )+ })?;+ }+ self.dry_install(wtr)?;+ Ok(())+ }++ /// Backup the destination to the directory given.+ fn backup<W: Write>(&self, wtr: W, dir: &Path) -> anyhow::Result<()> {+ if !self.dst.exists() {+ return Ok(());+ }++ let backup_dst = dir.join(self.relative_dst());+ // OK since self.dst is guaranteed to be non-empty.+ let parent = backup_dst.parent().unwrap();+ std::fs::create_dir_all(parent).with_context(|| {+ format!(+ "failed to create parent directory {} for backup",+ parent.display()+ )+ })?;+ std::fs::copy(&self.dst, &backup_dst).with_context(|| {+ format!(+ "failed to backup {} to {}",+ self.dst.display(),+ backup_dst.display()+ )+ })?;+ self.dry_backup(wtr, dir)?;+ Ok(())+ }++ /// Returns true if and only if this link already exists in the file+ /// system. That is, when the src and dst refer to the same underlying+ /// file.+ fn exists(&self) -> anyhow::Result<bool> {+ if !self.dst.exists() {+ return Ok(false);+ }+ same_file::is_same_file(&self.src, &self.dst).with_context(|| {+ format!(+ "failed to determine if {} == {}",+ self.src.display(),+ self.dst.display(),+ )+ })+ }++ /// Returns the destination path without its leading /.+ fn relative_dst(&self) -> &Path {+ // OK because dst is always absolute, by construction.+ self.dst.strip_prefix("/").unwrap()+ }+}++#[derive(Debug)]+struct Args {+ /// The directory in which to copy the contents of system configuration+ /// files before removing them and putting a symlink in its place.+ backup: Option<PathBuf>,+ /// When true, make no changes to the file system and instead print what+ /// we would do instead.+ dry_run: bool,+ /// The hostname of this machine (possibly derived from ~/.myhostname).+ hostname: String,+ /// The path to the directory containing this machine's system+ /// configuration. This is currently hard-coded to ~/system.+ system: PathBuf,+}++impl Args {+ fn parse() -> anyhow::Result<Args> {+ use clap::{crate_authors, crate_version, App, Arg};++ let parsed = App::new("Install system configuration symlinks.")+ .author(crate_authors!())+ .version(crate_version!())+ .max_term_width(80)+ .arg(Arg::with_name("no-backup").long("no-backup"))+ .arg(Arg::with_name("dry-run").long("dry-run"))+ .get_matches();++ let home = match env::var_os("HOME") {+ None => anyhow::bail!("HOME environment variable not set"),+ Some(home) => PathBuf::from(home),+ };+ let system = home.join("system");+ if !system.is_dir() {+ anyhow::bail!(+ "{}: system directory does not exist",+ system.display()+ );+ }++ let dry_run = parsed.is_present("dry-run");+ let hostname = runcmd(&["myhostname"])?;+ let backup = if parsed.is_present("no-backup") {+ None+ } else if dry_run {+ let parent = home.join("tmp").join("setup-system-links-backup");+ Some(parent.join("backup-{timestamp}"))+ } else {+ let parent = home.join("tmp").join("setup-system-links-backup");+ Some(+ create_unique_dir_in(parent, "backup")+ .context("failed to create backup directory")?,+ )+ };+ Ok(Args { backup, dry_run, hostname, system })+ }+}++fn runcmd<A: AsRef<OsStr>>(+ argv: impl IntoIterator<Item = A>,+) -> anyhow::Result<String> {+ let args: Vec<OsString> =+ argv.into_iter().map(|a| a.as_ref().to_os_string()).collect();+ let output = std::process::Command::new(&args[0])+ .args(&args[1..])+ .output()+ .context("failed to run 'myhostname'")?;+ if !output.status.success() {+ let stderr = BString::from(output.stderr.trim().to_vec());+ let args: Vec<Vec<u8>> = args+ .into_iter()+ .map(|a| Vec::from_os_str_lossy(&a).into_owned())+ .collect();+ let mut command = BString::from(bstr::join(" ", args));+ if let Some(code) = output.status.code() {+ command =+ BString::from(format!("`{}` (exit code {})", command, code));+ } else {+ command = BString::from(format!("`{}`", command));+ }+ anyhow::bail!("failed to run {}, stderr:\n\n{}", command, stderr)+ }++ let stdout = output+ .stdout+ .to_str()+ .context("hostname is not valid UTF-8")?+ .trim()+ .to_string();+ Ok(stdout)+}++fn create_unique_dir_in<P: AsRef<Path>, S: AsRef<str>>(+ parent: P,+ prefix: S,+) -> anyhow::Result<PathBuf> {+ use std::time::{Duration, SystemTime};++ let parent = parent.as_ref();+ let prefix = prefix.as_ref();+ std::fs::create_dir_all(&parent).with_context(|| {+ format!("{}: failed to create parent directory", parent.display())+ })?;+ for _ in 0..100 {+ let ts = SystemTime::now()+ .duration_since(SystemTime::UNIX_EPOCH)+ .context("system time is borked")?;+ let name =+ format!("{}-{}-{}", prefix, ts.as_secs(), ts.subsec_nanos());+ let dir = parent.join(name);+ if std::fs::create_dir(&dir).is_err() {+ std::thread::sleep(Duration::from_secs(1));+ continue;+ }+ return Ok(dir);+ }+ anyhow::bail!(+ "failed to create unique directory inside of {}",+ parent.display()+ );+}
bin/rust/setup-system-links/src/main.rs
@@ -1,446 +0,0 @@-// Add a convention that lets us avoid doing symlinks. e.g., For PAM.--#[cfg(not(unix))]-compile_error!("setup-system-links only works on Unix systems");--use std::collections::{BTreeMap, BTreeSet};-use std::env;-use std::ffi::{OsStr, OsString};-use std::io::{self, Write};-#[cfg(unix)]-use std::os::unix;-use std::path::{Path, PathBuf};--use anyhow::Context;-use bstr::{BString, ByteSlice, ByteVec};-use walkdir::WalkDir;--fn main() -> anyhow::Result<()> {- let args = Args::parse()?;- let mut links = Links::discover(&args.system, &args.hostname)?;- if let Some(ref backup_dir) = args.backup {- links.backup(backup_dir.to_owned());- }- if args.dry_run {- links.dry_run(std::io::stdout())?;- } else {- links.run(std::io::stdout())?;- }- Ok(())-}--/// Represents a collection of links derived from this machine's ~/system-/// directory.-#[derive(Debug)]-struct Links {- links: Vec<Link>,- backup: Option<PathBuf>,-}--impl Links {- /// Determine the full set of system config links for this machine given- /// the system configuration directory and this machine's hostname.- fn discover(system: &Path, hostname: &str) -> anyhow::Result<Links> {- // Collect all leaf files in ~/system, creating a map, e.g.,- // etc/default/grub |--> {default, duff, frink, ...}- // Note that the key lacks a leading slash. We add that below.- #[derive(Debug)]- struct Leaf {- machine_config_name: String,- copy: bool,- }-- impl Eq for Leaf {}-- impl PartialEq for Leaf {- fn eq(&self, rhs: &Leaf) -> bool {- self.machine_config_name == rhs.machine_config_name- }- }-- impl Ord for Leaf {- fn cmp(&self, rhs: &Leaf) -> std::cmp::Ordering {- self.machine_config_name.cmp(&rhs.machine_config_name)- }- }-- impl PartialOrd for Leaf {- fn partial_cmp(&self, rhs: &Leaf) -> Option<std::cmp::Ordering> {- Some(self.cmp(rhs))- }- }-- let mut all: BTreeMap<PathBuf, BTreeSet<Leaf>> = BTreeMap::new();- for result in WalkDir::new(system) {- let dent = result.with_context(|| {- format!("{}: failed to walk directory", system.display())- })?;- if dent.file_type().is_dir() {- continue;- }-- // Should never fail since 'system' is our root and is guaranteed- // to be non-empty.- let dst = dent- .path()- .strip_prefix(system)- .unwrap()- .parent()- .unwrap()- .to_path_buf();- if dst.as_os_str().is_empty() {- // These are files directly in ~/system/ like- // ~/system/README.md and are not part of our config.- continue;- }-- let machine_config_name =- dent.file_name().to_str().with_context(|| {- format!("{} is not valid UTF-8", dent.path().display())- })?;- let mut leaf = Leaf {- machine_config_name: machine_config_name.to_string(),- copy: false,- };- if let Some(hostname) = machine_config_name.strip_prefix("copy:") {- leaf.machine_config_name = hostname.to_string();- leaf.copy = true;- }- all.entry(dst).or_insert_with(|| BTreeSet::new()).insert(leaf);- }- // Now organize them into links by choosing the appropriate specific- // config for this machine.- let mut links = vec![];- let attempts = [- Leaf { machine_config_name: hostname.to_string(), copy: false },- Leaf { machine_config_name: "default".to_string(), copy: false },- ];- for (relative_dst, configs) in all {- let dst = Path::new("/").join(&relative_dst);- for attempt in &attempts {- let Some(config) = configs.get(attempt) else { continue };- let src = system.join(relative_dst).join(if !config.copy {- attempt.machine_config_name.to_string()- } else {- format!("copy:{}", attempt.machine_config_name)- });- links.push(Link { src, dst, copy: config.copy });- break;- }- }- Ok(Links { links, backup: None })- }-- /// Indicates that the destination of each link should be backed up- /// first.- fn backup(&mut self, dir: PathBuf) {- self.backup = Some(dir);- }-- /// Create the links that do not yet exist.- ///- /// When backup is enabled (the default), then all config files will be- /// backed up before attempting any link. If any backup fails, then no- /// link will be created.- ///- /// The equivalent Unix command to each operation will be written to the- /// given writer (like during a dry run).- fn run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {- let mut to_link = vec![];- for link in &self.links {- if link.exists()? {- continue;- }- to_link.push(link);- }- if let Some(ref backup_dir) = self.backup {- for link in &to_link {- link.backup(&mut wtr, backup_dir)?;- }- }- for link in to_link {- link.install(&mut wtr)?;- }- Ok(())- }-- /// Print the equivalent Unix commands that this would execute if we were- /// running for real, but make no changes to the file system.- fn dry_run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {- let mut to_link = vec![];- for link in &self.links {- if link.exists()? {- continue;- }- to_link.push(link);- }- if let Some(ref backup_dir) = self.backup {- for link in &to_link {- link.dry_backup(&mut wtr, backup_dir)?;- }- }- for link in to_link {- link.dry_install(&mut wtr)?;- }- Ok(())- }-}--/// Represents a link between a file in ~/system and an actual system-/// configuration path on the system. e.g., ~/system/etc/default/grub/krusty-/// will link to /etc/default/grub on my 'krusty' laptop.-#[derive(Debug)]-struct Link {- /// The ~/system/ path to this machine's config.- src: PathBuf,- /// The path to where this config lives on the system.- dst: PathBuf,- /// When true, we create a completely new file that is- /// an identical copy of the source. This is needed in- /// some cases (like for `/etc/pam.d/system-auth`) where- /// symlinks are tricky because of permissions. Namely,- /// the permission of the source file are relevant.- ///- /// We can't use hard-links either, because for PAM, files- /// in `/etc/pam.d` need to be owned by root. But we don't- /// want this for `~/system`, where files should be owned by- /// the user. Hardlinks don't allow it.- ///- /// So... we copy.- copy: bool,-}--impl Link {- /// Write the equivalent Unix command we would execute if we were running- /// for real.- fn dry_install<W: Write>(&self, mut wtr: W) -> io::Result<()> {- let (prog, flags) = if self.copy { ("cp", "") } else { ("ln", " -s") };- writeln!(- wtr,- "{prog}{flags} \"{}\" \"{}\"",- self.src.display(),- self.dst.display()- )- }-- /// Write the equivalent Unix command we would execute to backup the config- /// file if we were running for real.- fn dry_backup<W: Write>(&self, mut wtr: W, dir: &Path) -> io::Result<()> {- if !self.dst.exists() {- return Ok(());- }- let backup_dst = dir.join(self.relative_dst());- // OK since self.dst is guaranteed to be non-empty.- let parent = backup_dst.parent().unwrap();- writeln!(wtr, "mkdir -p \"{}\"", parent.display(),)?;- writeln!(- wtr,- "cp \"{}\" \"{}\"",- self.dst.display(),- backup_dst.display()- )- }-- /// Link (or install) the source to the destination, overwriting the- /// destination if it exists. Callers should ensure that the destination is- /// backed up before executing.- fn install<W: Write>(&self, wtr: W) -> anyhow::Result<()> {- if self.dst.exists() {- std::fs::remove_file(&self.dst).with_context(|| {- format!(- "failed to delete {} before creating symlink from {}",- self.dst.display(),- self.src.display()- )- })?;- }- if self.copy {- std::fs::copy(&self.src, &self.dst).with_context(|| {- format!(- "failed to copy {} to {}",- self.src.display(),- self.dst.display()- )- })?;- } else {- unix::fs::symlink(&self.src, &self.dst).with_context(|| {- format!(- "failed to link {} to {}",- self.src.display(),- self.dst.display()- )- })?;- }- self.dry_install(wtr)?;- Ok(())- }-- /// Backup the destination to the directory given.- fn backup<W: Write>(&self, wtr: W, dir: &Path) -> anyhow::Result<()> {- if !self.dst.exists() {- return Ok(());- }-- let backup_dst = dir.join(self.relative_dst());- // OK since self.dst is guaranteed to be non-empty.- let parent = backup_dst.parent().unwrap();- std::fs::create_dir_all(parent).with_context(|| {- format!(- "failed to create parent directory {} for backup",- parent.display()- )- })?;- std::fs::copy(&self.dst, &backup_dst).with_context(|| {- format!(- "failed to backup {} to {}",- self.dst.display(),- backup_dst.display()- )- })?;- self.dry_backup(wtr, dir)?;- Ok(())- }-- /// Returns true if and only if this link already exists in the file- /// system. That is, when the src and dst refer to the same underlying- /// file.- fn exists(&self) -> anyhow::Result<bool> {- if !self.dst.exists() {- return Ok(false);- }- same_file::is_same_file(&self.src, &self.dst).with_context(|| {- format!(- "failed to determine if {} == {}",- self.src.display(),- self.dst.display(),- )- })- }-- /// Returns the destination path without its leading /.- fn relative_dst(&self) -> &Path {- // OK because dst is always absolute, by construction.- self.dst.strip_prefix("/").unwrap()- }-}--#[derive(Debug)]-struct Args {- /// The directory in which to copy the contents of system configuration- /// files before removing them and putting a symlink in its place.- backup: Option<PathBuf>,- /// When true, make no changes to the file system and instead print what- /// we would do instead.- dry_run: bool,- /// The hostname of this machine (possibly derived from ~/.myhostname).- hostname: String,- /// The path to the directory containing this machine's system- /// configuration. This is currently hard-coded to ~/system.- system: PathBuf,-}--impl Args {- fn parse() -> anyhow::Result<Args> {- use clap::{crate_authors, crate_version, App, Arg};-- let parsed = App::new("Install system configuration symlinks.")- .author(crate_authors!())- .version(crate_version!())- .max_term_width(80)- .arg(Arg::with_name("no-backup").long("no-backup"))- .arg(Arg::with_name("dry-run").long("dry-run"))- .get_matches();-- let home = match env::var_os("HOME") {- None => anyhow::bail!("HOME environment variable not set"),- Some(home) => PathBuf::from(home),- };- let system = home.join("system");- if !system.is_dir() {- anyhow::bail!(- "{}: system directory does not exist",- system.display()- );- }-- let dry_run = parsed.is_present("dry-run");- let hostname = runcmd(&["myhostname"])?;- let backup = if parsed.is_present("no-backup") {- None- } else if dry_run {- let parent = home.join("tmp").join("setup-system-links-backup");- Some(parent.join("backup-{timestamp}"))- } else {- let parent = home.join("tmp").join("setup-system-links-backup");- Some(- create_unique_dir_in(parent, "backup")- .context("failed to create backup directory")?,- )- };- Ok(Args { backup, dry_run, hostname, system })- }-}--fn runcmd<A: AsRef<OsStr>>(- argv: impl IntoIterator<Item = A>,-) -> anyhow::Result<String> {- let args: Vec<OsString> =- argv.into_iter().map(|a| a.as_ref().to_os_string()).collect();- let output = std::process::Command::new(&args[0])- .args(&args[1..])- .output()- .context("failed to run 'myhostname'")?;- if !output.status.success() {- let stderr = BString::from(output.stderr.trim().to_vec());- let args: Vec<Vec<u8>> = args- .into_iter()- .map(|a| Vec::from_os_str_lossy(&a).into_owned())- .collect();- let mut command = BString::from(bstr::join(" ", args));- if let Some(code) = output.status.code() {- command =- BString::from(format!("`{}` (exit code {})", command, code));- } else {- command = BString::from(format!("`{}`", command));- }- anyhow::bail!("failed to run {}, stderr:\n\n{}", command, stderr)- }-- let stdout = output- .stdout- .to_str()- .context("hostname is not valid UTF-8")?- .trim()- .to_string();- Ok(stdout)-}--fn create_unique_dir_in<P: AsRef<Path>, S: AsRef<str>>(- parent: P,- prefix: S,-) -> anyhow::Result<PathBuf> {- use std::time::{Duration, SystemTime};-- let parent = parent.as_ref();- let prefix = prefix.as_ref();- std::fs::create_dir_all(&parent).with_context(|| {- format!("{}: failed to create parent directory", parent.display())- })?;- for _ in 0..100 {- let ts = SystemTime::now()- .duration_since(SystemTime::UNIX_EPOCH)- .context("system time is borked")?;- let name =- format!("{}-{}-{}", prefix, ts.as_secs(), ts.subsec_nanos());- let dir = parent.join(name);- if std::fs::create_dir(&dir).is_err() {- std::thread::sleep(Duration::from_secs(1));- continue;- }- return Ok(dir);- }- anyhow::bail!(- "failed to create unique directory inside of {}",- parent.display()- );-}
@@ -8,4 +8,4 @@ fi d="/tmp/screenshots" mkdir -p "$d" scrot -s "$d/$1"-exec audio-play /usr/share/sounds/freedesktop/stereo/screen-capture.oga+exec pw-play /usr/share/sounds/freedesktop/stereo/screen-capture.oga
@@ -1,6 +1,5 @@ #!/bin/sh -set -x set -e process-running xconfig-self-healing &
@@ -12,4 +12,5 @@ fi exec \ terminal \ -e ssh -AY "$host" -t \- '. ~/.pathrc ; tmux' -u new -A -s "$tmux_session_name"+ '. ~/.pathrc ; export SSH_AUTH_SOCK="$(relink-ssh-auth-sock)" ; tmux' \+ -u new -A -s "$tmux_session_name"
@@ -21,7 +21,7 @@ notify_high_fidelity() { } beep() {- audio-play /usr/share/sounds/freedesktop/stereo/audio-volume-change.oga+ pw-play /usr/share/sounds/freedesktop/stereo/audio-volume-change.oga } case "$(myhostname)" in
@@ -1,109 +1,88 @@-#!/bin/bash+#!/usr/bin/env bash -. $HOME/bin/x11-go+. "$HOME/bin/x11-go" -function headNum {- headName="$1"- gohead tabs | cut -f1-2 | grep "$headName" | cut -f1-}--function placeHead {- headName="$1"- geom=$(gohead query "$headName" | cut -f2-5)- x=$(echo "$geom" | cut -f1)- y=$(echo "$geom" | cut -f2)- w=$(echo "$geom" | cut -f3)- h=$(echo "$geom" | cut -f4)+# Place the pager, status bar and system try on the head given.+#+# The head given should be a name recognized by `gohead`. This also sets the+# "primary" head to be the one given.+place_head() {+ local head_name="$1"+ local x y w h - place $(headNum "$headName") $x $y $w $h+ IFS=$'\t' read -r x y w h < <(gohead query "$head_name" | cut -f2-5)+ # This is important because stalonetray bases its positioning on the primary+ # RandR monitor.+ gohead primary "$head_name" &> /dev/null+ place_by_head_geometry "$x" "$y" "$w" "$h" } -function array_exists {- search="$1"- haystack=($2)+# Places the pager, status bar and system tray based on the head geometry+# given.+#+# This routine doesn't know what head it's being place on, despite the fact+# that dzen2 and stalonetray purportedly support positioning based on RandR+# head number. The problem is that they each use different systems. So we just+# position everything ourselves in the context of the root window geometry.+place_by_head_geometry() {+ local mx="$1"+ local my="$2"+ local mwidth="$3"+ local mheight="$4" - for needle in ${haystack[@]}; do- if [ "$search" == "$needle" ]; then- return 0- fi- done+ local height=35+ local font_size=14+ local status_width=450+ local tray_width=100+ if [[ $mheight -gt 1200 ]] || is-media; then+ height=50+ font_size=21+ status_width=650+ tray_width=300+ fi - return 1+ local extended_status_width=$((status_width + tray_width))+ local pager_width=$((mwidth - extended_status_width))+ local y=$((mheight - height + my))+ dzen-wingo-workspaces "$font_size" "$mx" "$y" "$pager_width" "$height" &+ dzen-laptop-status \+ "$font_size" \+ "$((mx + pager_width))" "$y" "$extended_status_width" "$height" &+ # stalonetray has weird positioning. It's relative to the *primary* RandR+ # head, which we set in `pl`. So we give it -0-0 to indicate the+ # bottom right corner.+ my-stalonetray "0" "0" "$height" & } -function array_key {- search="$1"- keys=($2)- values=($3)+has_arg() {+ local search="$1"+ shift - for (( i=0; i<${#values[*]}; i++ )); do- if [ "$search" == "${values[$i]}" ]; then- echo "${keys[$i]}"+ local arg+ for arg in "$@"; do+ if [ "$search" = "$arg" ]; then+ return 0 fi done+ return 1 } -if ! gohead set $*; then+# Actually run the necessary `xrandr` command.+if ! gohead set "$@" &> /dev/null; then exit 1 fi-sleep 1 -case $(myhostname) in- duff|frink)- ps aux | grep -v grep | grep dzen-workspaces | awk '{print $2}' | xargs -r kill- killall trayer- (sleep 1 && \- trayer \- --widthtype pixel --width 150 \- --heighttype pixel --height 40 \- --edge bottom --align right --monitor 1 \- --SetDockType true) &- (sleep 1 && \- $HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \- | dzen2 -dock -xs 3 -h 40 -fn 'Cousine-18' -ta l -bg '#ffffff' -y 1170) &- ;;- nas2)- ps aux | grep -v grep | grep dzen-workspaces | awk '{print $2}' | xargs -r kill- killall stalonetray- (- stalonetray \- --background '#ffffff' \- --icon-size 40 --geometry 1x1-0-0 --grow-gravity SE --icon-gravity SE- ) &- (- $HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \- | dzen2 -dock -xs 3 -h 40 -fn 'Cousine-18' -ta l -bg '#ffffff' -y 1170- ) &- ;;- bart|kang)- if array_exists laptop "$*"; then- placeHead laptop- else- placeHead "$1"- fi- ;;- flanders|milhouse|wiggum)- placeHead "$1"- ;;- otto)- pgrep dzen-workspaces | xargs -r kill- killall trayer- (sleep 1 && \- trayer \- --widthtype pixel --width 150 \- --heighttype pixel --height 40 \- --edge bottom --align right \- --SetDockType true) &+# Kill any pager stuff we had.+killall dzen-workspaces &> /dev/null+killall dzen2 &> /dev/null+killall stalonetray &> /dev/null - headHeight="$(gohead query tv | cut -f4)"- y="$((headHeight - 40))"- (sleep 1 && \- $HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \- | dzen2 -dock -xs 2 -h 40 -fn 'Cousine-18' -ta l -bg '#ffffff' -y "$y") &- ;;- *)- echo "Unrecognized machine '$(hostname)'." >&2- exit 1- ;;-esac-exit 0+# Look for a special head to place our stuff on first.+for special in main middle; do+ if has_arg "$special" "$@"; then+ place_head "$special"+ exit 0+ fi+done+# Otherwise fall back to the first one given.+place_head "$1"
@@ -30,7 +30,7 @@ notify_volume() { # Play a little beep. beep() {- audio-play /usr/share/sounds/freedesktop/stereo/audio-volume-change.oga+ pw-play /usr/share/sounds/freedesktop/stereo/audio-volume-change.oga } # Gets an icon for the provided volume. The volume provided should be a@@ -51,49 +51,26 @@ get_volume_icon() { echo "$icon" } -case "$(myhostname)" in- nas2|duff|kang|bart|flanders|milhouse|otto|wiggum)- case $1 in- up)- pamixer --increase 5- notify_volume- ;;- down)- pamixer --decrease 5- notify_volume- ;;- mute)- if ! eval $(pamixer --get-mute); then- notify_muted- fi- pamixer --toggle-mute- if ! eval $(pamixer --get-mute); then- notify_volume- fi- ;;- *)- echo "Unrecognized command: $1" >&2- exit 1- ;;- esac+case $1 in+ up)+ pamixer --increase 5+ notify_volume+ ;;+ down)+ pamixer --decrease 5+ notify_volume+ ;;+ mute)+ if ! eval $(pamixer --get-mute); then+ notify_muted+ fi+ pamixer --toggle-mute+ if ! eval $(pamixer --get-mute); then+ notify_volume+ fi ;; *)- case "$(hostname)" in- *) control='Master' mcontrol='Master' ;;- esac- case "$(hostname)" in- monty|Turtle)- card=0- ;;- *)- card=1- ;;- esac-- case $1 in- up) amixer -c$card set "$control" 2+ unmute ;;- down) amixer -c$card set "$control" 2- unmute ;;- mute) amixer -q set "$mcontrol" toggle ;;- esac+ echo "Unrecognized command: $1" >&2+ exit 1 ;; esac
@@ -0,0 +1,74 @@+#!/usr/bin/env bash++set -e++. "$HOME/bin/x11-go"++if is-lower-res; then+ sleep 2s+ case "$(myhostname)" in+ duff)+ gohead set left middle right+ ;;+ *)+ gohead set main+ ;;+ esac+fi++run-wingo-environment &+# Wait for Wingo to start before doing other things that generally require+# a WM to be present below. e.g., Starting the bottom dock in `video`.+while ! pgrep '^wingo$'; do+ sleep 0.1s+done++# I'm not sure why this is needed, but when the mode needs to be+# changed, we need to wait a bit more time. Otherwise the mode doesn't+# actually seem to be changed. It ends up being too quick after X11+# starts? I suppose we could change the default mode in a static X11+# configuration, but this is fine in practice.+# if is-lower-res; then+ # sleep 2s+# fi++startup+# Most things jsut want to boot to one main monitor, but duff is my special+# triple monitor setup.+case "$(myhostname)" in+ duff)+ video left middle right &+ ;;+ *)+ video main &+ ;;+esac+variety &+dunst &+ag-blueberry-tray &+(volume mute ; volume mute) &+restart-compositor &+if cmd-exists nm-applet; then+ nm-applet &+fi+if is-laptop; then+ autobright &++ # An agent to make the fingerprint sensor work.+ /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 &+fi+if is-media; then+ idesk &+fi++# Run the 1password tray.+1password \+ --silent \+ --force-device-scale-factor=1.5 \+ --log=debug &> /tmp/1password.log &++if is-laptop; then+ (ssh-add-keys && relink-ssh-auth-sock && systemctl --user start mount-nas) &+fi++while true; do sleep 86400s; done
system/etc/NetworkManager/dispatcher.d/50-mount-nas/copy:bart
@@ -0,0 +1 @@+run
system/etc/NetworkManager/dispatcher.d/50-mount-nas/run
@@ -0,0 +1,39 @@+#!/usr/bin/env bash++target_user=andrew+target_uid="$(id -u "$target_user")"+unit=mount-nas.service+tag=NetworkManager-mount-nas++iface="${1:-}"+action="${2:-}"++case "$action" in+ up|vpn-up)+ ;;+ connectivity-change)+ # Only react when NetworkManager says connectivity is fully online.+ [ "${CONNECTIVITY_STATE:-}" = "FULL" ] || exit 0+ ;;+ *)+ exit 0+ ;;+esac++runtime_dir="/run/user/$target_uid"+bus="unix:path=$runtime_dir/bus"++if [ ! -S "$runtime_dir/bus" ]; then+ logger -t "$tag" \+ "user systemd bus not available for $target_user; not starting $unit"+ exit 0+fi++logger -t "$tag" \+ "NetworkManager action=$action iface=$iface" \+ "connection=${CONNECTION_ID:-unknown}; starting $unit"++runuser -u "$target_user" -- \+ env XDG_RUNTIME_DIR="$runtime_dir" \+ DBUS_SESSION_BUS_ADDRESS="$bus" \+ systemctl --user start --no-block "$unit"