Personal system configuration for CachyOS + Omarchy (Hyprland on Wayland).
This repository contains the scripts, daemons, keybindings, and config files built on top of the Omarchy base. Everything solves a specific problem — nothing is here just because it looked interesting. The goal is to make a fresh install feel like home in under five minutes.
| Component | What it does |
|---|---|
claude-copy-response |
Copies Claude Code's last response to the clipboard with one button press |
claude-bypass |
Removes permission prompts from Claude Desktop |
mouse-dpi-daemon |
Shows a desktop notification when the DPI button is pressed on a SteelSeries mouse |
mouse-restore |
Restores DPI presets and polling rate after any hardware reset |
bindings.lua |
Hyprland keybindings — apps, workspaces, scratchpad, mouse shortcuts |
mako/config |
Notification styling with auto-dismiss |
- CachyOS with Omarchy installed
- WezTerm — required by
claude-copy-responsefor active session detection - Python 3.10+
wl-clipboard— Wayland clipboard utilitylibnotify— desktop notification support (notify-send)rivalcfg(optional) — only needed if you use a SteelSeries mouse
# Required
sudo pacman -S wl-clipboard libnotify
# Optional — SteelSeries mouse tools (AUR)
yay -S rivalcfggit clone https://github.com/YOUR_USERNAME/dotfiles.git ~/dotfiles
cd ~/dotfiles
bash install.shThe installer creates symlinks from this repo to the correct system paths. Existing files are backed up with a timestamp before being replaced — nothing is silently overwritten.
# Preview what would change without touching anything
bash install.sh --dry-runAfter installing:
hyprctl reload # apply Hyprland keybindings
mouse-restore # restore mouse config (SteelSeries only)
pkill mako && mako &disown # reload notification daemoninstall.sh uses symlinks rather than file copies. This means:
- Editing any file in
~/dotfiles/takes effect immediately — no re-run needed git pullto update the system to the latest versiongit diffto audit what changed between updates
The problem: Claude Code runs in the terminal and has no built-in button to copy its last response. Claude Desktop has a copy icon — the CLI doesn't.
The solution: A Python script triggered by a mouse side button (or any keybinding) that locates the active Claude session, extracts the last assistant response, and copies it to the Wayland clipboard.
Why it's not trivial:
WezTerm runs as a single process for all tabs — you can't identify which tab is focused by PID alone. The script uses WezTerm's IPC (wezterm cli list) to match the active Hyprland window title against open panes and recover the working directory of the focused tab.
Claude's .jsonl transcript has a subtlety: text blocks and tool_use blocks (internal tool calls Claude makes) carry different message IDs. Searching for "the last assistant message by ID" always lands on a tool invocation — never on readable text. The correct approach is to find the last real user message (not a tool result) and collect every text block that follows it.
Fallback: If WezTerm IPC fails, the script falls back to the globally most-recently-modified .jsonl across all Claude sessions.
Dependencies: Python 3, wl-copy, notify-send, hyprctl, wezterm
The problem: Claude Desktop prompts for confirmation before every file read, write, or shell command. In an active workflow this interrupts constantly.
The solution: A thin Bash wrapper that sits between the Claude .desktop launcher and the real binary. It intercepts session-start calls and injects --dangerously-skip-permissions --permission-mode bypassPermissions automatically, without touching non-session calls like claude --version.
Setup: Point CLAUDE_CODE_LOCAL_BINARY in Claude Desktop's .desktop entry to this wrapper instead of the real binary at /opt/claude-code/bin/claude.
Note: This disables all permission prompts. Only use it on a machine where you trust the agent's actions.
The problem: The SteelSeries Aerox 3 Wireless has a hardware button that cycles through DPI presets, but Linux provides no visual indicator of which preset is active.
The solution: A Python daemon that reads raw HID events from the mouse and fires a desktop notification when the DPI button is pressed.
The mouse sends a vendor-specific HID report 0xAD 0x02 <preset_index> on its if04 interface whenever the DPI button cycles. The daemon opens the device by its stable by-id symlink (which survives USB reconnects) and maps the index to a human-readable value from ~/.config/mouse-dpi/presets.
Why a daemon and not a one-shot script:
The mouse disconnects and reconnects on its own — wireless sleep, USB re-plug, port changes. A one-shot script dies on disconnect. The daemon wraps reads in an outer retry loop and simply waits for the device path to reappear. The systemd service adds a second layer: Restart=always with a 3-second backoff means the process always comes back, even on unexpected crashes.
DPI presets file (config/mouse-dpi/presets):
800 1400
Space-separated values, one per hardware preset slot. Update this file whenever you change presets with rivalcfg.
The problem: rivalcfg -r performs a full firmware reset that wipes all DPI presets and resets the polling rate to its default. This is easy to trigger accidentally during debugging.
The solution: A single script that re-applies the complete mouse configuration in the correct order.
rivalcfg -s "800, 1400" # restore DPI presets
rivalcfg -p 1000 # restore polling rate to 1000 Hz
systemctl --user restart mouse-dpi.service # restart the notification daemonRun it after: rivalcfg -r, a USB port change, or any time DPI notifications stop appearing.
config/hypr/bindings.lua is written in Lua using Omarchy's binding layer on top of Hyprland. Organized in sections — application launchers, web apps, workspace navigation, and utility shortcuts.
o.bind("F4", "Discord", "discord-toggle") -- open if closed, hide if open
o.bind("F5", "Spotify", "spotify-toggle")
o.bind("F8", "WhatsApp", "whatsapp-toggle")To change an app, replace the toggle name or use a launch + focus pair:
o.bind("F4", "Slack", { launch = "slack", focus = "^slack$" })
-- Find the window class of any running app:
-- hyprctl clients | grep classo.bind("SUPER + SHIFT + G", "Signal", { launch = "signal-desktop", focus = "^signal$" })
o.bind("SUPER + SHIFT + O", "Obsidian", { launch = "obsidian", focus = "^obsidian$" })
o.bind("SUPER + SHIFT + D", "Docker", { tui = "lazydocker" })To add your own:
o.bind("SUPER + SHIFT + T", "App Name", { launch = "binary-name", focus = "^window-class$" })Any URL opened as a webapp gets a focused browser window:
o.bind("SUPER + SHIFT + A", "ChatGPT", { webapp = "https://chatgpt.com" })
o.bind("SUPER + SHIFT + Y", "YouTube", { webapp = "https://youtube.com/" })To add more:
o.bind("SUPER + SHIFT + L", "Linear", { webapp = "https://linear.app" })SUPER + KP_1..9,0 → Switch to workspace 1–10
SUPER + SHIFT + KP_1..9,0 → Move focused window to workspace 1–10
Keycodes are used instead of key names for reliable Wayland compatibility.
ALT + S → Send focused window to scratchpad (hide it globally)
ALT + D → Pull window back into the current workspace
F1 → Toggle keybindings cheatsheet
SUPER + W → Close window (handles Discord/Vesktop separately)
ALT + KP_+ → Screenshot selected region → clipboard
mouse:275 → Copy last Claude Code response → clipboard
Mako is Omarchy's notification daemon. The config layers over Omarchy's base styles with two changes:
default-timeout=3000 # auto-dismiss after 3 seconds
[urgency=critical]
default-timeout=0 # critical alerts stay until manually dismissedApply after editing:
pkill mako && mako &disownNote: Omarchy symlinks
~/.config/mako/configto the current theme's file. The installer replaces that symlink with one pointing to this repo, so the timeout persists across theme changes.
Open config/hypr/bindings.lua, change the app name/binary, save. Hyprland reloads automatically. Verify with hyprctl configerrors.
# 1. Apply to hardware
rivalcfg -s "800, 1400, 3200"
# 2. Update the presets file to match
echo "800 1400 3200" > config/mouse-dpi/presets
# 3. Restart the daemon
systemctl --user restart mouse-dpi.serviceThe daemon watches for a specific HID path and byte pattern. To find the right values for a different mouse:
# List available HID devices
ls /dev/input/by-id/ | grep -i your-mouse
# Capture raw HID output while pressing the DPI button
sudo cat /dev/input/by-id/YOUR-DEVICE-hidraw | xxd | head -20Update HIDRAW and the 0xAD 0x02 byte check in bin/mouse-dpi-daemon accordingly.
This mouse has an intermittent hardware issue where BTN_LEFT gets stuck in a "pressed" state at the firmware level. The OS stops generating new click events because it believes the button is still held down.
# Confirm the stuck state
evtest /dev/input/by-id/usb-SteelSeries_*-event-mouseFix: Press the left button firmly several times. This mechanically resets the contact. There is no software workaround.
The mouse was likely reset. Run:
mouse-restoreIf that doesn't help:
systemctl --user status mouse-dpi.service
journalctl --user -u mouse-dpi.service -n 30A stale mako process is blocking the new config:
pkill mako && mako &disownhyprctl reload && hyprctl configerrorsDebug mode prints which pane was matched and which .jsonl was selected:
claude-copy-response --debug- Multi-terminal support — extend session detection beyond WezTerm to Kitty, Alacritty, Ghostty
--markdownflag — preserve code blocks and headers when pasting into rich-text contexts--codeflag — extract only the last code block from the response
- Auto-detect the connected SteelSeries model instead of requiring a hardcoded device path
- Support multiple simultaneously connected devices
| OS | CachyOS (Arch Linux, BORE-scheduled kernel) |
| Window Manager | Hyprland via Omarchy |
| GPU | NVIDIA (proprietary drivers) |
| Terminal | WezTerm |
| Mouse | SteelSeries Aerox 3 Wireless |
MIT — use it, fork it, adapt it freely.