gopher://gopher.someodd.zip:70/1/catalog/tech/workstation/WorkstationLaptop and desktop user-facing tooling: window managers, browsers, password managers, dev utilities.2026-06-12T00:00:00Zgopher.someodd.zipgopher://gopher.someodd.zip:70/0/tech/workstation/hibernation-nightmares.txtHibernation Nightmares (Framework 13, Ryzen AI 300)2026-03-13T00:00:00Z2026-06-12T00:00:00ZThe full saga, now CLOSED: resume was TWO bugs. The firmware-S4 reb... amdgpu RDNA 3.5 hang; kernel
version is the dial. Recover with SysRq, not power-off.
(UPDATE 2026-06-12: caught it. Async device-resume race;
pm_async=0 kills it. Kernel version was never the dial
here either. Smoking-gun chapter below. closed.)
* reboot to a fresh LUKS prompt -> firmware S4 flaking out
(PM: Image not found, NO S4 wake). Fix is HibernateMode=
shutdown PLUS aligning the initramfs resume target to the
swapfile; shutdown alone wasn't enough. (closed 2026-06-03.)
- Don't force-power-off a blanked resume; it invalidates the
swap header. Use SysRq S-U-B.
- Lid now does DIRECT hibernate on battery, nothing on AC.
suspend-then-hibernate made resume worse, not better.
- Bluetooth (MT7925) is dead on kernel 7.0.7, fine on 6.19.13.
The hardware (so I stop misdiagnosing it)
-----------------------------------------
Ryzen AI 7 350 / Radeon 860M, codename "Krackan", RDNA 3.5,
gfx1152, PCI 1002:1114, Display Core 3.5. Wi-Fi and Bluetooth
are one MediaTek MT7925 combo: Wi-Fi mt7925e on PCIe, BT
btusb/btmtk on USB 0e8d:0717. Framework board FRANMGCP07,
BIOS 03.05.
It is NOT a Phoenix 7040 -- I assumed that for a while and it
sent me chasing the wrong fixes. Also: this platform offers
s2idle only, no "deep" S3, so mem_sleep_default=deep in my grub
line is inert.
The core nightmare: resume
--------------------------
The image writes fine (battery barely moves overnight), but
RESUME comes up to a black screen -- often with the machine
alive underneath -- and sometimes reboots instead. The crash
happens before journald persists anything, so there is no log
of it. The only trace is on the next cold boot:
PM: Image not found (code -22)
A part-way resume crash invalidates the swap header (so it
won't retry into a wedged state), which is exactly why the
retry boot comes up clean and empty-handed.
(UPDATE 2026-05-26: that story is right for the black-screen
variant. For the REBOOT variant it's backwards -- the image was
never restored at all, because the firmware never came back in
S4. See "the lever I never pulled" below.)
Cause of the black-screen variant: an upstream amdgpu
regression on RDNA 3.5. Framework's own AI 300 threads put the
failure rate at 20-50% and quote support saying hibernate-to-
disk on these "is spotty at best ... wait for the bug to be
patched." The lever that moves it is kernel version -- people
report specific kernels working where neighbours fail.
The setup (still valid)
-----------------------
1. Big swapfile to hold the image:
fallocate -l 64G /swapfile-hibernate
chmod 600 /swapfile-hibernate
mkswap /swapfile-hibernate
swapon /swapfile-hibernate
/etc/fstab:
/swapfile-hibernate none swap defaults 0 0
2. Resume from it. Find the offset:
filefrag -v /swapfile-hibernate | grep " 0:"
/etc/default/grub:
resume=/dev/mapper/framework--vg-root resume_offset=
then: update-grub && reboot
Swap must be >= RAM or hibernate fails with "Not enough
suitable swap space".
Gotcha I left myself: the initramfs resume target DISAGREES
with the cmdline --
cmdline: resume=/dev/mapper/framework--vg-root + offset
initramfs: RESUME=/dev/mapper/framework--vg-swap_1 (1G LV)
It works only because the cmdline wins. The 1G LV couldn't hold
an image anyway. Should be aligned; left as a TODO.
(UPDATE 2026-06-03: NOT harmless under shutdown mode -- this
was the bug. See below.)
Lid policy: the part that flipped
---------------------------------
History:
s2idle only -> overnigh]]>gopher://gopher.someodd.zip:70/0/tech/workstation/wireguard-nm.txtwireguard-nm.txt2026-06-10T00:00:00Z2026-06-10T00:00:00ZWireGuard + NetworkManager (nm-applet) — setup notes ==============... home server (friend0) over WireGuard on the LAN.
DDNS (friend0.crabdance.com) for reaching it from outside.
TOPOLOGY
--------
server (friend0 @ 192.168.1.210) wg0 10.8.0.1/32 listen 51820
laptop friend0 10.8.0.2/32
Keys cross over: each side's [Peer] PublicKey = the OTHER side's
PUBLIC key. A private key NEVER leaves its own box.
SERVER /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/32
ListenPort = 51820
PrivateKey =
[Peer] # laptop
PublicKey =
AllowedIPs = 10.8.0.2/32 # /32 per peer
LAPTOP /etc/wireguard/friend0.conf
[Interface]
Address = 10.8.0.2/32
PrivateKey =
[Peer] # server
PublicKey =
Endpoint = 192.168.1.210:51820
AllowedIPs = 10.8.0.0/32
PersistentKeepalive = 25
NM-APPLET: SET THE WG IPv4 ADDRESS (the big trap)
-------------------------------------------------
NM stores the tunnel IP in IPv4 settings, NOT the WireGuard
section. New WG connections default IPv4 to Automatic (DHCP).
WG has no DHCP -> you get NO ipv4 addr (only fe80:: link-local).
GUI fix:
nm-applet > Edit Connections > > IPv4 Settings
Method: Manual
Add Address 10.8.0.2 Netmask 32 Gateway (blank)
Save, toggle the connection off/on.
CLI:
nmcli connection modify friend0 ipv4.method manual \
ipv4.addresses 10.8.0.2/32
nmcli connection up friend0
Best: import the conf, it sets IPv4 Manual for you:
nmcli connection import type wireguard file friend0.conf
Split tunnel: IPv4 > Routes > tick "use this connection only for
resources on its network".
MISTAKES I MADE (the derp log)
------------------------------
1. Put the LAPTOP's private key into the SERVER [Interface].
-> server's identity became the laptop's; pubkey mismatch;
no handshake. Each box uses ITS OWN private key.
2. AllowedIPs = x/24 on a peer. Must be /32 per peer (server
side). /24 collides with the interface's own subnet route.
3. No IPv4 on the tunnel (the real blocker): NM "Automatic" left
only fe80::. ping had no source -> server couldn't route the
reply back. Set IPv4 Manual = 10.8.0.2/24.
4. Fought the wrong interface: laptop's wg0 is a DIFFERENT VPN.
The friend0 tunnel is iface "friend0". Don't down/up wg0.
5. wg syncconf does NOT set Address (wg-quick-only directive,
stripped by wg-quick strip). Use wg-quick up, or add the IP
by hand.
TROUBLESHOOTING (in order)
--------------------------
Keys pair? on each box:
echo "" | wg pubkey
-> must equal what the OTHER box lists as [Peer] PublicKey.
Server listening?
sudo ss -ulnp | grep 51820
Firewall? (no ufw here; check nft/iptables)
sudo nft list ruleset
sudo iptables -L INPUT -n -v
allow: sudo iptables -I INPUT -p udp --dport 51820 -j ACCEPT
Handshake + transfer:
sudo wg show
- "latest handshake" present = crypto OK
- X sent / 0 received = reply blocked (firewall, return route,
wrong AllowedIPs, or no IP on the iface)
- counters are cumulative; old failed retries inflate "sent"
Does the iface actually have its IP?
ip -br addr show
- only fe80:: = NO ipv4 (the NM-Automatic trap)
LAN reachable at all?
ping
Apply a config change:
edit file, then
sudo wg syncconf <(wg-quick strip ) # peers/keys only
(NOT Address) — or a full sudo wg-quick up
DDNS note: WG resolves Endpoint ONCE at bring-up. When the home
IP changes the tunnel goes stale -> use the reresolve-dns timer
(wireguard-tools examples) or just restart the tunnel.
]]>gopher://gopher.someodd.zip:70/1/tech/workstation/window_maker/rofi-omnirofi-omni: a unified rofi launcher for Window Maker (windows + apps + files)2026-06-09T00:00:00Z2026-06-09T00:00:00ZCompanion to the rofi-windows switcher: a single rofi prompt that f...Companion to the rofi-windows switcher: a single rofi prompt that fuzzy-filters across open windows (minimized ones included), installed apps, and recently-modified files under $HOME, ranked windows-then-apps-then-files with files newest-first. Bash + wmctrl + xdotool + fd, with background-refreshed caches so it stays instant -- files are mtime-sorted and capped to keep the prompt snappy. Bound to Mod4+s.gopher://gopher.someodd.zip:70/0/tech/workstation/keepass/keepassxc-no-plaintext-secrets.txtStop Storing API Keys in Plaintext Config Files (KeepassXC + Secret Service)2026-03-31T00:00:00Z2026-03-31T00:00:00ZStop storing API keys in plaintext by pulling them from KeepassXC t... /usr/share/dbus-1/services/org.freedesktop.secrets.service << EOF
[D-BUS Service]
Name=org.freedesktop.secrets
Exec=/usr/bin/false
EOF'
# ALSO stop D-Bus auto-activation via org.gnome.keyring
# This is a separate service file that launches gnome-keyring-daemon
# with --components=secrets. If anything pokes the org.gnome.keyring
# D-Bus name, gnome-keyring starts and claims org.freedesktop.secrets
# too. Blocking only org.freedesktop.secrets is not enough.
sudo bash -c 'cat > /usr/share/dbus-1/services/org.gnome.keyring.service << EOF
[D-BUS Service]
Name=org.gnome.keyring
Exec=/usr/bin/false
EOF'
# Hide the autostart entry (idempotent -- safe to run again)
mkdir -p ~/.config/autostart
cp /etc/xdg/autostart/gnome-keyring-secrets.desktop ~/.config/autostart/
grep -q '^Hidden=true' ~/.config/autostart/gnome-keyring-secrets.desktop \
|| echo "Hidden=true" >> ~/.config/autostart/gnome-keyring-secrets.desktop
# Kill any running instance
killall gnome-keyring-daemon
# KeepassXC won't automatically reclaim org.freedesktop.secrets --
# it tried at startup, lost to gnome-keyring, and gave up.
# Restart it (or toggle Secret Service off/on in its settings).
```
Since apt upgrade will silently restore these service files (see caveats
below), I keep a script that re-neuters both in one shot. Save as
~/fix-gnome-keyring-secrets.sh:
```
#!/bin/bash
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "Run this with sudo." >&2
exit 1
fi
REAL_USER="${SUDO_USER:?}"
cat > /usr/share/dbus-1/services/org.freedesktop.secrets.service << 'EOF'
[D-BUS Service]
Name=org.freedesktop.secrets
Exec=/usr/bin/false
EOF
cat > /usr/share/dbus-1/services/org.gnome.keyring.service << 'EOF'
[D-BUS Service]
Name=org.gnome.keyring
Exec=/usr/bin/false
EOF
sudo -u "$REAL_USER" killall gnome-keyring-daemon 2>/dev/null || true
if pgrep -u "$REAL_USER" keepassxc >/dev/null 2>&1; then
echo "Restart KeePassXC to reclaim org.freedesktop.secrets."
else
echo "Start KeePassXC to claim org.freedesktop.secrets."
fi
```
Run with `sudo ~/fix-gnome-keyring-secrets.sh` after any gnome-keyring
package update.
Then delete the old gnome-keyring data if you don't need it:
```
rm ~/.local/share/keyrings/login.keyring
```
## 3. Enable KeepassXC Secret Servi]]>gopher://gopher.someodd.zip:70/0/tech/workstation/mcp-claude-habitica-trello.txtComputer, merge all my redundant habitica todos, then identify which should be trello cards instead.2026-01-15T00:00:00Z2026-01-15T00:00:00ZExperiments with an MCP setup that uses Claude to merge redundant H...gopher://gopher.someodd.zip:70/0/tech/workstation/phone-files.txtphone files2025-12-31T00:00:00Z2025-12-31T00:00:00ZTroubleshooting notes on getting CalyxOS and Android files to show .../dev/null)
if [[ -n "$exif_date" ]]; then
echo "$exif_date"
return
fi
fi
# Fallback to modification date
echo "$(date -r "$file" +"%Y:%m")"
}
# Function to copy files if they are missing or newer
copy_files() {
local file_type="$1"
local target_dir="$2"
shift 2
local extensions=("$@")
for src_dir in "${SOURCE_DIRECTORIES[@]}"; do
for ext in "${extensions[@]}"; do
find "$src_dir" -type f -iname "*.$ext" | while read -r file; do
# Extract year and month from EXIF or modification date
year_month=$(get_file_date "$file" "$file_type")
year=${year_month%%:*}
month=${year_month##*:}
# Determine target directory
target_path="$target_dir/$year/$month"
mkdir -p "$target_path"
# Check if the file needs to be copied
dest_file="$target_path/$(basename "$file")"
if [[ ! -e "$dest_file" ]] || [[ "$file" -nt "$dest_file" ]]; then
cp "$file" "$des]]>gopher://gopher.someodd.zip:70/1/tech/workstation/keepass/keepass-keyring-managerKeepassXC as Key Ring Manager for Minimal DEs & WMs2025-10-15T00:00:00Z2025-10-15T00:00:00ZUses KeepassXC as an SSH agent and Secret Service provider for mini...Uses KeepassXC as an SSH agent and Secret Service provider for minimal window managers like Window Maker. Targets setups where a full DE keyring (GNOME/KDE) isn't available.gopher://gopher.someodd.zip:70/0/tech/workstation/restic-on-my-laptop.txtRestic + Custom TUI on the Laptop2025-09-16T00:00:00Z2025-09-16T00:00:00ZAdapting the server's restic-based backups for the laptop after mov...gopher://gopher.someodd.zip:70/0/tech/workstation/projectm-audio-visualizations.txtprojectm: audio visualizer (classic milkdrop/old winamp visualizations)2025-03-13T00:00:00Z2025-03-13T00:00:00ZNotes on installing projectM, a modern port of classic Milkdrop/Win...gopher://gopher.someodd.zip:70/0/tech/workstation/gpg_messages.txtUsing GPG to encrypt messages (like email)2024-12-30T00:00:00Z2024-12-30T00:00:00ZStep-by-step guide to encrypting and signing arbitrary text with GP...
```
I'm not going to get into keyservers.
Set key expiration.
You may also want to add your key for decrypting the message you sent as well.
]]>gopher://gopher.someodd.zip:70/0/tech/workstation/monero.txtCashing out Monero in Debian2024-11-26T00:00:00Z2024-11-26T00:00:00ZCashing out Monero (XMR) from a long-neglected Debian server. Colle...gopher://gopher.someodd.zip:70/0/tech/workstation/terminals_linux_vt320.txtUsing old terminals (like the VT320) with Linux2024-10-20T00:00:00Z2024-10-20T00:00:00ZConnecting a VT320 terminal to a Debian server over serial via a US... key`
* `n key
Tab setup:
* clear all tabs
* set 8 column tabs
I think this is a reference to the tab setup:
```
T T T T T T T T
12345678901234567890123456789012345678901234567890123456789012345678901234567890
```
### Server setup
Let's get the server to accept serial login/TTY. As you can see the device I'm
dealing with is a USB/serial adapter/TTY.
```
systemctl enable serial-getty@ttyUSB0.service
reboot
systemctl start getty@ttyUSB0
ps -ef | grep agetty
```
I think I also edited `/etc/default/grub` (yikes!):
```
#GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX_DEFAULT="console=ttyUSB0 console=tty0"
GRUB_CMDLINE_LINUX=""
```
After editing grub, run `updategrub`.
I think I may have set the speed of the TTY, but I'm not sure if that actually
helped much: `stty -F /dev/ttyUSB0 speed 9600`.
A handy test command: `cat /dev/ttyUSB0` and then try typing stuff on the
terminal.
I got a tip to set `$TERM` variable on the server. Try `echo $TERM`. I think
it may help to set `TERM=vt220` or whatever you have a terminfo entry for.
Try checking `/lib/terminfo/v/` and see the entries.
You don't want to set that in your login script, since it'll be different if
you're in an xterm versus on the terminal itself. Set it in the systemd script
for the getty on the serial line.
Edit `/lib/systemd/system/serial-getty@.service` and change:
```
ExecStart=-/sbin/agetty -o '-p -- \\u' --keep-baud 115200,38400,9600 %I $TERM
```
to:
```
ExecStart=-/sbin/agetty -o '-p -- \\u' --keep-baud 115200,38400,9600 %I vt220
```
### Updated instructions
I did some extra stuff like adding to grub I'll document later...
## Terminal advice: living inside screen
This advice comes from someone (sorry I forget their name, so no credit).
Type 'screen', and then you get basically a fresh terminal instance. Now, you
can run whatever programs you want. When you want a new "window", hit
Control-a, then press c. Tapping control-a a will switch back and forth
between this window and the window you were in. You can have lots of windows
open, and switch between them with control-a #, where # is the window number
you want to go to. (it starts at 0) There are a whole ton of other things you
can do with screen, of course, but one of the more powerful things you can do
is to type control-a d - which detaches the screen session. Then you can log
out - and when you log back in, you can type 'screen -r' to reattach to the
session you detached. And... it gets better. If you connect to this machine
from another terminal, or from the network, you can type 'screen -x' on the
other terminal, and attach to the same running screen session again. So you
have two terminals, both interacting with the same set of "windows".
And you can have different wi]]>gopher://gopher.someodd.zip:70/0/tech/workstation/lyx.txtLyX: What You See is What You Mean2024-10-02T00:00:00Z2024-10-02T00:00:00ZQuick notes on LyX, a WYSIWYM front end for LaTeX. Covers exporting...gopher://gopher.someodd.zip:70/0/tech/workstation/using-gnu-screen.txtUsing GNU screen2024-07-03T00:00:00Z2024-07-03T00:00:00ZWhy the author prefers GNU screen for persistent sessions — especia...gopher://gopher.someodd.zip:70/1/tech/workstation/window_maker/window_maker_megapostProductivity in Window Maker2024-05-17T00:00:00Z2024-05-17T00:00:00ZLiving inside the lightweight Window Maker window manager on Debian...Living inside the lightweight Window Maker window manager on Debian Unstable, with tips for making it feel like a complete environment. Covers dockapps, app selection, and open TODOs the author still wants to solve.gopher://gopher.someodd.zip:70/0/tech/workstation/dillo.txtDillo Browser2024-04-01T00:00:00Z2024-04-01T00:00:00ZCompiling the very lightweight Dillo browser (with Gemini and Gophe...