1 Commits

Author SHA1 Message Date
d9231c3a0e Elpaca migration WIP 2024-06-25 03:00:19 -06:00
458 changed files with 7651 additions and 35797 deletions

View File

@@ -1,11 +0,0 @@
{
"permissions": {
"allow": [
"Bash(rg:*)",
"Bash(wmctrl:*)",
"Bash(grep:*)",
"Bash(hyprctl:*)"
],
"deny": []
}
}

View File

@@ -1,89 +0,0 @@
name: Build and Push Cachix (imalison-taffybar)
on:
push:
branches: [master]
paths:
- "dotfiles/config/taffybar/**"
- ".github/workflows/cachix.yml"
pull_request:
branches: [master]
paths:
- "dotfiles/config/taffybar/**"
- ".github/workflows/cachix.yml"
workflow_dispatch: {}
jobs:
imalison-taffybar:
runs-on: ubuntu-latest
permissions:
contents: read
env:
# Avoid flaky/stalled CI due to unreachable substituters referenced in flake config
# (e.g. LAN caches). We keep this list explicit for CI reliability.
NIX_CONFIG: |
experimental-features = nix-command flakes
connect-timeout = 5
substituters = https://cache.nixos.org https://colonelpanic8-dotfiles.cachix.org https://org-agenda-api.cachix.org https://taffybar.cachix.org https://codex-cli.cachix.org https://claude-code.cachix.org
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= colonelpanic8-dotfiles.cachix.org-1:O6GF3nptpeMFapX29okzO92eSWXR36zqW6ZF2C8P0eQ= org-agenda-api.cachix.org-1:liKFemKkOLV/rJt2txDNcpDjRsqLuBneBjkSw/UVXKA= taffybar.cachix.org-1:beZotJ1nVEsAnJxa3lWn0zwzZM7oeXmGh4ADRpHeeIo= codex-cli.cachix.org-1:1Br3H1hHoRYG22n//cGKJOk3cQXgYobUel6O8DgSing= claude-code.cachix.org-1:YeXf2aNu7UTX8Vwrze0za1WEDS+4DuI2kVeWEE4fsRk=
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Free disk space
run: |
set -euxo pipefail
df -h
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/share/boost || true
sudo apt-get clean || true
df -h
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v16
- name: Require Cachix config (push only)
if: github.event_name == 'push'
env:
CACHIX_CACHE_NAME: ${{ vars.CACHIX_CACHE_NAME }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${CACHIX_CACHE_NAME:-}" ]; then
echo "Missing repo variable CACHIX_CACHE_NAME (Settings -> Secrets and variables -> Actions -> Variables)." >&2
exit 1
fi
if [ -z "${CACHIX_AUTH_TOKEN:-}" ]; then
echo "Missing repo secret CACHIX_AUTH_TOKEN (Settings -> Secrets and variables -> Actions -> Secrets)." >&2
exit 1
fi
- name: Setup Cachix (push)
if: github.event_name == 'push'
uses: cachix/cachix-action@v15
with:
name: ${{ vars.CACHIX_CACHE_NAME }}
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
skipPush: false
- name: Setup Cachix (PR, no push)
if: github.event_name == 'pull_request' && vars.CACHIX_CACHE_NAME != ''
uses: cachix/cachix-action@v15
with:
name: ${{ vars.CACHIX_CACHE_NAME }}
skipPush: true
- name: Build imalison-taffybar
run: |
set -euxo pipefail
nix build \
--no-link \
--print-build-logs \
./dotfiles/config/taffybar#defaultPackage.x86_64-linux

View File

@@ -1,54 +0,0 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Emacs
uses: purcell/setup-emacs@master
with:
version: 29.1
- name: Setup Cask
uses: conao3/setup-cask@master
with:
version: snapshot
- name: Install dependencies
working-directory: gen-gh-pages
run: cask install
- name: Generate HTML
working-directory: gen-gh-pages
run: |
cask exec emacs --script generate-html.el
mv ../dotfiles/emacs.d/README.html ./index.html
- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./gen-gh-pages
publish_branch: gh-pages
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
commit_message: 'Deploy to GitHub Pages: ${{ github.sha }}'
keep_files: false

26
.gitignore vendored
View File

@@ -21,33 +21,7 @@
gotools gotools
/dotfiles/config/xmonad/result /dotfiles/config/xmonad/result
/dotfiles/config/taffybar/result /dotfiles/config/taffybar/result
/nix-darwin/result
/nixos/result
/dotfiles/emacs.d/*.sqlite /dotfiles/emacs.d/*.sqlite
/dotfiles/config/gtk-3.0/colors.css /dotfiles/config/gtk-3.0/colors.css
/dotfiles/config/gtk-3.0/settings.ini
/dotfiles/emacs.d/.cache/ /dotfiles/emacs.d/.cache/
/dotfiles/emacs.d/projectile.cache
/dotfiles/emacs.d/projectile-bookmarks.eld
/dotfiles/config/fontconfig/conf.d/10-hm-fonts.conf /dotfiles/config/fontconfig/conf.d/10-hm-fonts.conf
/dotfiles/config/fontconfig/conf.d/52-hm-default-fonts.conf
/dotfiles/config/taffybar/_scratch/
/dotfiles/config/taffybar/taffybar-*/
/dotfiles/config/taffybar/status-notifier-item/
/dotfiles/config/taffybar/.direnv/
/dotfiles/config/taffybar/dist-newstyle/
/dotfiles/config/taffybar/sni-priorities.dat
/dotfiles/config/xmonad/dist-newstyle/
/dotfiles/config/hypr/hyprscratch.conf
/.worktrees/
/result
# Secrets and machine-local state (managed via agenix/pass instead of git)
/dotfiles/config/asciinema/config
/dotfiles/config/remmina/remmina.pref
/dotfiles/config/screencloud/ScreenCloud.conf
# Local tool state
/.playwright-cli/
/nixos/action-cache-dir/
/dotfiles/config/taffybar/dbus-menu/

8
.travis.yml Normal file
View File

@@ -0,0 +1,8 @@
language: generic
script: bash ./gen-gh-pages/deploy.sh
env:
global:
- ENCRYPTION_LABEL: "73e6c870aa87"
- COMMIT_AUTHOR_EMAIL: "IvanMalison@gmail.com"
- COMMIT_AUTHOR_NAME: "Ivan Malison"

View File

@@ -1,228 +0,0 @@
# -*- mode: org; -*-
#+TITLE: colonelpanic8's Dotfiles
This repository is the source of truth for my machines, user environment, and a
large set of day-to-day workflow scripts. It started as an Emacs configuration,
and that is still here, but the repo is now mostly a Nix-managed personal
systems repo: NixOS hosts, a nix-darwin host, Home Manager link management,
desktop/window-manager configuration, shell tooling, agent configuration, and
org-agenda-api deployment glue.
The old literate Emacs README lives at [[file:dotfiles/emacs.d/README.org][dotfiles/emacs.d/README.org]]. The
published GitHub Pages site is still generated from that document.
* What This Manages
- NixOS systems under [[file:nixos/][nixos/]], with one flake configuration per file in
[[file:nixos/machines/][nixos/machines/]].
- A nix-darwin configuration under [[file:nix-darwin/][nix-darwin/]] for the macOS machine.
- Shared Nix modules and overlays under [[file:nix-shared/][nix-shared/]].
- Home Manager placement of files from [[file:dotfiles/][dotfiles/]] into =$HOME= and
=$XDG_CONFIG_HOME=.
- Shell functions and executable helpers in [[file:dotfiles/lib/][dotfiles/lib/]], added to
=PATH= and =fpath= by the NixOS environment module.
- Desktop environment and tiling-window-manager configuration for Hyprland,
XMonad, River/XMonad experiments, Taffybar, Waybar, Rofi, Alacritty,
autorandr, and related utilities.
- Emacs and org-mode configuration, including the tangled org configuration used
by the org-agenda-api container.
- Agent and tool configuration for Codex, Claude, project guides, and local
task-specific skills.
- Container and deployment configuration for personal org-agenda-api instances.
This is not intended to be a generic starter dotfiles repo. Many modules assume
my users, hostnames, hardware, SSH keys, secrets layout, and local checkout path
=(~/dotfiles=). It is still useful as a reference for how the pieces fit
together.
* Layout
| Path | Purpose |
|------+---------|
| [[file:nixos/][nixos/]] | Main NixOS flake. Imports feature modules, host files, agenix secrets, Home Manager, overlays, and package checks. |
| [[file:nixos/machines/][nixos/machines/]] | Per-host NixOS entrypoints such as =strixi-minaj=, =ryzen-shine=, =railbird-sf=, WSL hosts, and Raspberry Pi hosts. |
| [[file:nix-darwin/][nix-darwin/]] | macOS system flake using nix-darwin, nix-homebrew, Home Manager, agenix, and shared packages. |
| [[file:nix-shared/][nix-shared/]] | Shared package lists, overlays, Home Manager modules, and Syncthing fragments used by Linux and macOS. |
| [[file:dotfiles/][dotfiles/]] | Files that are linked into the home directory. Top-level entries become dotfiles; =dotfiles/config/*= becomes XDG config. |
| [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] | User commands and desktop helpers, including Rofi scripts, Hyprland helpers, audio controls, and Syncthing utilities. |
| [[file:dotfiles/lib/functions/][dotfiles/lib/functions/]] | Zsh autoload functions and shell helpers. |
| [[file:dotfiles/config/hypr/][dotfiles/config/hypr/]] | Hyprland Lua config, lock/idle config, workspace files, scripts, and plugin state. |
| [[file:dotfiles/config/xmonad/][dotfiles/config/xmonad/]] | XMonad configuration, local Cabal package, flake, and upstream submodules. |
| [[file:dotfiles/config/taffybar/][dotfiles/config/taffybar/]] | Personal Taffybar package/configuration, CSS themes, scripts, and local upstream checkout. |
| [[file:dotfiles/emacs.d/][dotfiles/emacs.d/]] | Emacs configuration, literate org config, org-mode setup, snippets, and generated/tangled Elisp. |
| [[file:dotfiles/agents/][dotfiles/agents/]] | Agent instructions, project constellation guides, and local Codex skills. |
| [[file:org-agenda-api/][org-agenda-api/]] | Instance-specific config and container/deploy glue for org-agenda-api. |
| [[file:docs/][docs/]] | Design notes for Cachix, tiling WM behavior, River evaluation, and org-agenda-api consolidation. |
| [[file:gen-gh-pages/][gen-gh-pages/]] | Legacy/publication pipeline that exports the Emacs README to GitHub Pages. |
* NixOS
The NixOS flake is [[file:nixos/flake.nix][nixos/flake.nix]]. It discovers host configurations from
[[file:nixos/machines/][nixos/machines/]] and exposes them as =nixosConfigurations.<hostname>=.
The broad feature set is assembled by [[file:nixos/configuration.nix][nixos/configuration.nix]], where
=features.full.enable= expands into the normal desktop/profile modules.
Common workflow:
#+begin_src sh
cd ~/dotfiles/nixos
just switch
#+end_src
The local =just switch= recipe wraps =nixos-rebuild switch --flake ".#"=, waits
for an already-running switch to finish, and overrides the Taffybar inputs to
the live checkout under this repo. Use it instead of running =nixos-rebuild=
directly.
Useful variants:
#+begin_src sh
cd ~/dotfiles/nixos
just switch-remote
just switch-local-taffybar
just remote-switch <host>
#+end_src
Build/check examples:
#+begin_src sh
nix flake check ~/dotfiles/nixos
nix build ~/dotfiles/nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel
#+end_src
The flake also exposes package/check outputs for Hyprland plugins and a
Hyprland Lua config syntax/verification check.
* nix-darwin
The macOS configuration lives in [[file:nix-darwin/flake.nix][nix-darwin/flake.nix]]. It uses
nix-darwin, nix-homebrew, Home Manager, agenix, and the shared package list in
[[file:nix-shared/system/essential.nix][nix-shared/system/essential.nix]].
Common workflow:
#+begin_src sh
cd ~/dotfiles/nix-darwin
just switch
#+end_src
The active host configuration is =mac-demarco-mini=. There is also a
=mac-demarco-mini-imalison= target used while migrating the primary macOS user.
* Home File Linking
The NixOS Home Manager module [[file:nixos/dotfiles-links.nix][nixos/dotfiles-links.nix]] reproduces the useful
part of =rcm/rcup=:
- files under [[file:dotfiles/][dotfiles/]] are linked into =$HOME= with a leading dot;
- directories under [[file:dotfiles/config/][dotfiles/config/]] are linked into =$XDG_CONFIG_HOME=;
- links are out-of-store symlinks, so editing the checkout updates runtime
config immediately;
- generated or special directories such as =codex=, =lib=, =config=, and
=emacs.d= are handled separately.
On NixOS, shell scripts belong in [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] and autoloaded shell
functions belong in [[file:dotfiles/lib/functions/][dotfiles/lib/functions/]]. [[file:nixos/environment.nix][nixos/environment.nix]] adds
those paths to the shell environment.
The nix-darwin Home Manager module in [[file:nix-darwin/home/common.nix][nix-darwin/home/common.nix]] uses the
same basic idea for macOS, with extra launchd, GPG, Raycast, Homebrew, and agent
setup.
* Desktop Stack
The desktop setup is modular. [[file:nixos/desktop.nix][nixos/desktop.nix]] enables the common desktop
surface, while individual modules layer in window managers, panels, launchers,
notifications, SNI/tray support, fonts, and app defaults.
The currently important pieces are:
- Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], with imported Lua
modules under [[file:dotfiles/config/hypr/hyprland/][dotfiles/config/hypr/hyprland/]], backed by custom plugin inputs in
the NixOS flake.
- XMonad configuration in [[file:dotfiles/config/xmonad/xmonad.hs][dotfiles/config/xmonad/xmonad.hs]], with upstream
=xmonad= and =xmonad-contrib= available as submodules/checkouts.
- Taffybar configuration in [[file:dotfiles/config/taffybar/taffybar.hs][dotfiles/config/taffybar/taffybar.hs]], plus a local
flake and scripts for restart, screenshots, and SNI debugging.
- Waybar, Rofi, autorandr, Alacritty, Zellij, and miscellaneous app configs
under [[file:dotfiles/config/][dotfiles/config/]].
The intended tiling-WM behavior is documented in
[[file:docs/tiling-wm-experience.md][docs/tiling-wm-experience.md]].
* Emacs And Org
Emacs is still a major part of the repo, just no longer the only thing here.
The main files are:
- [[file:dotfiles/emacs.d/README.org][dotfiles/emacs.d/README.org]]: the original literate Emacs README.
- [[file:dotfiles/emacs.d/init.el][dotfiles/emacs.d/init.el]] and [[file:dotfiles/emacs.d/early-init.el][early-init.el]]: runtime entrypoints.
- [[file:dotfiles/emacs.d/org-config.org][dotfiles/emacs.d/org-config.org]]: the org-mode configuration that is tangled
for normal Emacs and for org-agenda-api.
- [[file:gen-gh-pages/][gen-gh-pages/]] and [[file:.github/workflows/gh-pages.yml][.github/workflows/gh-pages.yml]]: export the Emacs README
to the public GitHub Pages site.
* org-agenda-api
The repo carries the personal integration layer for
[[https://github.com/colonelpanic8/org-agenda-api][org-agenda-api]].
[[file:nixos/org-agenda-api.nix][nixos/org-agenda-api.nix]] tangles the org-mode configuration from
[[file:dotfiles/emacs.d/org-config.org][dotfiles/emacs.d/org-config.org]]. [[file:org-agenda-api/container.nix][org-agenda-api/container.nix]] combines that
tangled config with per-instance loaders under [[file:org-agenda-api/configs/][org-agenda-api/configs/]] and
builds OCI containers exposed by the NixOS flake.
The host-side NixOS module [[file:nixos/org-agenda-api-host.nix][nixos/org-agenda-api-host.nix]] runs the container
behind nginx with ACME certificates and Podman.
To enter the deployment shell:
#+begin_src sh
nix develop ~/dotfiles/nixos#org-agenda-api
#+end_src
* Secrets
Secrets are intentionally not stored as plaintext in the repo. Nix-managed
secrets use agenix files under [[file:nixos/secrets/][nixos/secrets/]]. Runtime credentials and
personal service passwords live in =pass=. Modules and scripts should consume
secrets from those sources at runtime rather than checking derived values into
git.
* Submodules And Local Checkouts
Some third-party or upstream projects are tracked as submodules:
- =dotfiles/config/taffybar/taffybar=
- =dotfiles/config/xmonad/xmonad=
- =dotfiles/config/xmonad/xmonad-contrib=
- =dotfiles/config/alacritty/themes=
- =nixos/railbird.ai=
Clone with submodules when bootstrapping a new checkout:
#+begin_src sh
git clone --recurse-submodules git@github.com:IvanMalison/dotfiles.git ~/dotfiles
#+end_src
This repo also contains project-local git worktrees under =.worktrees/= during
active development. Those are machine-local working state and are ignored.
* CI And Caches
[[file:.github/workflows/cachix.yml][.github/workflows/cachix.yml]] can build the =strixi-minaj= NixOS closure and
push paths to Cachix.
The top-level [[file:justfile][justfile]] contains helper commands for populating the
=colonelpanic8-dotfiles= Cachix cache from a local machine.
* Working In This Repo
- Prefer Nix modules for system-level behavior and Home Manager modules for
user-level placement and services.
- Put user commands in [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] and shell functions in
[[file:dotfiles/lib/functions/][dotfiles/lib/functions/]].
- Run NixOS switches from [[file:nixos/][nixos/]] with =just switch=.
- Run macOS switches from [[file:nix-darwin/][nix-darwin/]] with =just switch=.
- Keep host-specific behavior in [[file:nixos/machines/][nixos/machines/]] where possible.
- Do not commit secrets or generated local state; use agenix, =pass=, or ignored
machine-local files.

1
README.org Symbolic link
View File

@@ -0,0 +1 @@
dotfiles/emacs.d/README.org

View File

@@ -1,437 +0,0 @@
# Tiling WM Experience Spec
This document describes the tiling window manager experience I am targeting.
## Priority Levels
- Required: daily-driver behavior.
- Important: expected for parity, but a rough first version is acceptable.
- Nice: useful polish or compatibility.
## Modifier Terminology
- `Super` names the physical modifier key often labeled Windows, Command, GUI,
or OS depending on the keyboard.
- `Hyper` means a higher-order logical modifier layer used for monitor,
workspace, utility, and cross-context operations.
- Prefer implementing `Hyper` as its own virtual modifier or equivalent logical
mask when the environment supports that.
- If a dedicated virtual `Hyper` mask is not practical, `Ctrl+Alt+Super` is the
fallback chord.
- The fallback `Hyper` chord intentionally does not include `Shift`; portable
`Hyper` bindings only use the plain `Hyper` layer and the `Hyper+Shift`
layer.
- Do not require `Hyper+Ctrl`, `Hyper+Alt`, or `Hyper+Super` bindings. Those
modifiers may already be part of the fallback `Hyper` chord.
- Binding descriptions should use `Super` and `Hyper` rather than
hardware-vendor names.
## Workspaces and Monitors
Required behavior:
- Workspaces are a shared global set, not independent per-monitor namespaces.
- Focusing workspace `N` shows workspace `N` on the currently focused monitor.
- Moving a window to workspace `N` does not require caring which monitor
currently owns that workspace.
- Sending the focused window to workspace `N` without following it is a
first-class operation.
- Moving the focused window to workspace `N` and following it is a first-class
operation.
- Sending the focused window to the next empty workspace without following it is
a first-class operation.
- Moving the focused window to the next empty workspace and following it is a
first-class operation.
- Normal workspaces are bounded to `1..9`.
Important behavior:
- Workspace history is tracked per monitor.
- Last-workspace toggle uses the current monitor's workspace history.
- Workspace history cycling works on the current monitor within the bounded
workspace set.
- Swapping the current workspace contents with another workspace is available.
- Moving a window to an empty workspace on another monitor is available.
- Moving the focused window to another monitor without following keeps keyboard
focus on the original monitor.
- Moving the focused window to another monitor and following it moves keyboard
focus to the destination monitor.
- Hidden/special workspaces exist for scratchpad state.
- Hidden/special workspaces exist for minimized state.
- Hidden/special workspaces are excluded from ordinary workspace cycling.
- Hidden/special workspaces are excluded from the status bar's normal workspace
list.
### Workspace History Cycling
Important behavior:
- The model is most-recently-used workspace switching, scoped to the monitor
where the action starts.
- Each monitor has its own ordered workspace history. The focused monitor's
history is not shared with other monitors.
- Only ordinary bounded workspaces are candidates. Special, scratchpad,
minimized, hidden, and out-of-range workspaces are excluded.
- Starting a cycle freezes the candidate list for that cycle. Previewing
workspaces while the cycle is active must not rewrite the history order.
- Starting a cycle previews the previous workspace for the current monitor.
- Repeating the forward cycle action continues farther back through that
monitor's frozen history.
- A reverse cycle action moves through the same frozen history in the opposite
direction.
- Releasing the initiating modifier key commits the currently previewed
workspace and updates history exactly once.
- A cancel path may return to the workspace where the cycle started.
This behavior is important for workflow continuity, but it is not a hard
requirement for a minimal daily-driver window manager.
## Directional Navigation
Required behavior:
- Directional window focus is available.
- Directional window swapping or movement is available.
- Directional move-to-monitor is available while preserving useful focus.
- Directional monitor focus is available.
- Directional window movement between monitors is available.
- Moving the focused window to an empty workspace on the monitor in a direction
remains required behavior, but it should not require an extra `Hyper`
modifier beyond `Shift`.
- `Super+w/a/s/d` focuses windows directionally.
- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally.
- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that
direction while preserving useful focus.
- `Super+Ctrl+Shift+w/a/s/d` moves the focused window to an empty workspace on
the monitor in that direction.
- `Hyper+w/a/s/d` focuses monitors directionally.
- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally.
- Directional focus in tabbed/fullscreen mode should cycle predictably through
windows even though their screen geometry overlaps.
Important behavior:
- Keyboard resize remains available, but it should not displace the directional
move-to-monitor binding.
## Pointer Focus
Required behavior:
- Focus-follows-mouse, or an equivalent pointer-driven focus model, is enabled.
- Moving the pointer over a managed window focuses that window without requiring
a click.
- Mouse-follows-focus is also enabled: keyboard or programmatic focus changes
move the pointer into the newly focused window.
## Layouts
Required behavior:
- Tiling is dynamic.
- Primary layout is equal-width vertical columns.
- Scrolling layouts are not acceptable.
- All ordinary splits are vertical.
- Adding windows dynamically redistributes all tiled windows evenly.
- Newly tiled windows are inserted near the currently focused tile, not
appended to the far end of the workspace.
- Removing windows dynamically redistributes all tiled windows evenly.
- Ordinary use should not require manually managing a split tree.
- Tabbed/fullscreen-style monocle layout is available.
- Directional window navigation bindings continue to switch windows in
tabbed/fullscreen mode.
- The important layouts are columns and tabbed/fullscreen.
- Dialogs float.
- Dialogs are centered.
- There is a command to jump directly to the columns layout and one to jump
directly to the tabbed/fullscreen layout.
- `Super+Ctrl+Space` jumps directly to the tabbed/fullscreen layout.
- Direct fullscreen or floating-fullscreen behavior should not have a
keybinding.
- Layout state is per workspace when the compositor supports it.
Important behavior:
- One-window workspaces should have no visible gaps or use smart gaps.
Nice behavior:
- Gaps can be toggled.
- Smart borders can be toggled.
- Layout-related modifiers remain available for experiments.
- Inactive windows are slightly dimmed when supported.
## Overview and Discovery
Required behavior:
- There is a visual window overview for inspecting open windows before jumping.
- There is a visual workspace expose for inspecting normal workspaces before
jumping.
- There is a rofi-style window picker.
- Window picker entries show icons.
- Window picker entries show titles.
- Window picker entries show workspace labels.
- Go-to-window focuses the selected window wherever it currently lives.
- Bring-window moves a selected non-visible window to the current workspace and
focuses it.
- Replace-window swaps the focused window with a selected window where feasible.
Important behavior:
- Overview supports both "go" and "bring" workflows.
- Window overview and workspace expose are distinct surfaces, because window
selection and workspace selection are different navigation tasks.
- Window overview supports directional keyboard selection with the same
`w/a/s/d` spatial model as ordinary window focus.
- Window overview supports direct go, bring, and replace-window actions from the
selection UI.
- Workspace expose shows bounded normal workspaces, including empty workspaces,
with visible workspace numbers.
- Workspace expose can be opened in a bring-window-oriented mode when supported.
- Window switchers hide scratchpad windows unless the user is explicitly using a
scratchpad picker.
- Window switchers hide minimized windows unless the user is explicitly using a
minimized picker.
- Window switchers hide internal windows.
- Go/bring actions unminimize selected windows when needed.
## Scratchpads
Required behavior:
- A named scratchpad exists for codex.
- A named scratchpad exists for element.
- A named scratchpad exists for htop.
- A named scratchpad exists for slack.
- A named scratchpad exists for spotify.
- A named scratchpad exists for transmission.
- A named scratchpad exists for volume.
- Scratchpads appear near-fullscreen and centered by default.
- Toggling a scratchpad deactivates fullscreen/tabbed state first.
- Scratchpads are hidden from normal workspace and window listings.
Important behavior:
- A dropdown terminal scratchpad exists.
- Scratchpad matching handles delayed class/title assignment.
- Scratchpad behavior is robust when the app is already running.
- Scratchpad behavior is robust when the app is minimized.
- Scratchpad behavior is robust when the app is on another workspace.
## Minimization
Required behavior:
- Focused window can be minimized.
- Last minimized window can be restored to the current workspace and focused.
- Minimized windows are excluded from normal layout.
- Minimized windows are excluded from ordinary go/bring lists.
Important behavior:
- A minimized picker mode exists.
- Restore-all-minimized exists.
- Other classes in the current workspace can be minimized.
- Windows of the focused class can be restored.
- All minimized windows can be restored.
## Class-Aware Workflows
Important behavior:
- Gather all windows of the focused class onto the current workspace.
- Raise-or-spawn exists for the browser.
- Window menus show class.
- Window menus show title.
- Window menus show workspace.
- Window menus show icon.
## Status Bar Contract
Required behavior:
- The status bar can list normal workspaces.
- The status bar can identify the active workspace per monitor.
- The status bar can list windows per workspace.
- The status bar can expose class hints for each listed window.
- The status bar can expose title for each listed window.
- The status bar can expose active state for each listed window.
- The status bar can expose minimized state when available.
- The status bar can expose urgency when available.
- The status bar can expose approximate window position when available.
- Scratchpad workspaces are marked as special or filtered out.
- Minimized workspaces are marked as special or filtered out.
- Internal workspaces are marked as special or filtered out.
Important behavior:
- Workspace labels are stable.
- Workspace icons are stable.
- Window positioning information is available enough for workspace icon strips
and future expose-like views.
- Layout information is available enough for workspace icon strips and future
expose-like views.
- Layout name is exposed if practical.
- Layout state is exposed if practical.
## Session and Utility Behavior
Important behavior:
- Terminal is `ghostty --gtk-single-instance=false`.
- Launcher is `rofi -show drun -show-icons`.
- Run menu is `rofi -show run`.
- Browser raise/spawn behavior exists.
- Border width is effectively zero.
- The status bar can be toggled per monitor.
- Session startup integrates with the normal graphical-session target.
- Session startup integrates with any required session-specific user target.
Nice behavior:
- Wallpaper behavior remains consistent.
- Wallpaper selection uses `Hyper+comma`; `Hyper+w/a/s/d` are reserved for
directional monitor focus.
- Idle behavior remains consistent.
- Lock behavior remains consistent.
- Clipboard history behavior remains consistent.
- Screenshot behavior remains consistent.
- Monitor DDC/input switching remains consistent.
- Rofi utility bindings remain consistent.
- Media keys remain consistent.
## Binding Appendix
Required behavior:
- `Hyper` bindings should remain available from a single physical key where
practical, even if that key emits the fallback chord internally.
- Extra modifiers on `Hyper` are limited to `Shift` for portable bindings.
Important behavior:
- `Hyper` utility bindings must not displace required directional monitor
bindings on `Hyper+w/a/s/d`.
### Core Bindings
Required behavior:
- `Super+p` opens the application launcher.
- `Super+Shift+p` opens the run menu.
- `Super+Shift+Return` opens a terminal.
- `Super+q` reloads the window manager config.
- `Super+Shift+c` closes the focused window.
- `Super+Shift+q` exits the window manager session.
- `Super+x` opens the command picker with `rofi_command.sh`.
- `Super+g` opens the go-to-window picker.
- `Super+b` opens the bring-window picker.
- `Super+Shift+b` opens the replace-window picker.
- `Super+Shift+e` moves the focused window to the next empty workspace and
follows it. This is the target replacement for the older `Super+Shift+h`
binding.
- `Hyper+e` focuses the next empty workspace.
- `Hyper+1` toggles inactive-window opacity reduction for the focused window.
- `Hyper+5` swaps the current workspace with a selected workspace.
- `Hyper+g` gathers windows of the focused class onto the current workspace.
Important behavior:
- `Super+Tab` opens the visual window overview.
- `Super+Shift+Tab` opens the visual window overview scoped to non-visible
windows or bring-window mode when supported.
- `Alt+Tab` opens the visual workspace expose.
- `Alt+Shift+Tab` opens the visual workspace expose in bring-window mode when
supported.
- Within visual window overview, `w/a/s/d`, `h/j/k/l`, and arrow keys move the
selection directionally.
- Within visual window overview, `Return`, `Space`, `g`, or `f` activates the
selected window.
- Within visual window overview, `b`, `Shift+Return`, or `Shift+Space` brings
the selected window to the current workspace.
- Within visual window overview, `Shift+b` replaces the focused window with the
selected window when supported.
- Within visual window overview, `Escape` or `q` closes the overview.
- `Super+\` starts or advances current-monitor workspace history cycling.
- `Super+/` reverses current-monitor workspace history cycling while the
initiating `Super` key is held.
- Releasing the initiating `Super` key commits the workspace history cycle.
### Directional Navigation Bindings
Required behavior:
- `Super+w/a/s/d` focuses windows directionally.
- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally.
- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that
direction while preserving useful focus.
- `Hyper+w/a/s/d` focuses monitors directionally.
- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally.
- Moving the focused window to an empty workspace on the monitor in a direction
remains required behavior, but it should not require a `Hyper+Ctrl` binding.
- `Super+z` focuses the next monitor.
- `Super+Shift+z` moves the focused window to the next monitor.
### Numbered Workspace Bindings
Required behavior:
- `Super+1..9` focuses workspace `1..9` on the current monitor.
- `Super+Shift+1..9` sends the focused window to workspace `1..9` without
following it.
- `Super+Ctrl+1..9` sends the focused window to workspace `1..9` and follows
it.
### Scratchpad Bindings
Required behavior:
- `Super+Alt+c` toggles the codex scratchpad.
- `Super+Alt+e` toggles the element scratchpad.
- `Super+Alt+h` toggles the htop scratchpad.
- `Super+Alt+k` toggles the slack scratchpad.
- `Super+Alt+s` toggles the spotify scratchpad.
- `Super+Alt+t` toggles the transmission scratchpad.
- `Super+Alt+v` toggles the volume scratchpad.
Important behavior:
- `Super+Alt+grave` toggles the dropdown terminal scratchpad.
- `Super+Alt+Return` enters the minimized-window picker or restores minimized
windows, depending on environment support.
- `Super+Alt` is reserved for app-specific raise/spawn, scratchpad, and
scratchpad-adjacent bindings.
### Utility Bindings
Required behavior:
- `Hyper+v` opens clipboard history with a rofi-backed clipboard command
such as `greenclip print` or `cliphist`.
- `Hyper+p` opens the password picker with `rofi-pass`.
- `Hyper+h` opens the screenshot tool with the compositor/session-appropriate
screenshot command.
- `Hyper+c` opens the Codex launcher with `rofi_tmcodex.sh`.
- `Hyper+Shift+c` opens the Codex launcher with `tmcodex resume`.
- `Hyper+k` opens the process killer with `rofi_kill_process.sh`.
- `Hyper+Shift+k` opens the kill-all/process-tree killer with
`rofi_kill_all.sh`.
- `Hyper+r` opens the systemd/service menu with `rofi-systemd`.
- `Hyper+slash` toggles the status bar with the status-bar-appropriate command.
- `Hyper+backslash` toggles the monitor input with `mpg341cx_input toggle`.
- `Hyper+i` opens the audio input selector with `rofi_select_input.hs`.
- `Hyper+o` opens the audio output selector with `rofi_paswitch`.
- `Hyper+y` opens the agentic skill picker with `rofi_agentic_skill`.
- `Hyper+Shift+l` locks the session with the compositor/session-appropriate
locker.
Important behavior:
- Wallpaper selection is available under `Hyper` via `rofi_wallpaper.sh`, but
its exact key must avoid the required `Hyper+w/a/s/d` directional monitor
bindings.
- Expose-style overview remains available as a utility binding using the
compositor-appropriate implementation.
- Session-destructive operations use shifted or otherwise harder-to-hit
variants.

View File

@@ -1,126 +0,0 @@
# Agentic Session Preferences
## Multiplexer session titling
- If the `TMUX` or `ZELLIJ` environment variable is set, treat this chat as the controller for the current tmux or zellij session.
- Use `set_multiplexer_title '<project> - <task>'` to update the title. The command detects tmux vs. zellij internally, prefers tmux when both are present, and no-ops outside a multiplexer.
- Maintain a session/window/pane title that describes the durable purpose of the overall exchange.
- Prefer automatic titling: infer a concise <task> from the current user request and the existing chat context without asking.
- Choose holistic titles over granular turn summaries. The title should answer "what has this chat been for?" rather than describe the latest command, substep, clarification, or follow-up message.
- Preserve the existing <task> when the new user turn is a continuation, status check, refinement, or implementation detail within the same broader objective.
- Title format: "<project> - <task>".
- <project> is the basename of the current project directory.
- Prefer git repo root basename if available; otherwise use basename of the current working directory.
- <task> is a short, user-friendly description of what we are doing.
- Ask for a short descriptive <task> only when the task is ambiguous or you are not confident in an inferred title.
- When the broader objective changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
- When a title is provided or updated, immediately run `set_multiplexer_title '<project> - <task>'`; do not call raw tmux or zellij rename commands unless debugging the helper itself.
- For Claude Code sessions, a UserPromptSubmit hook may initialize titles automatically from the first substantive prompt, but it should not keep overwriting an established same-project title with the latest prompt.
## Pane usage
- Do not create extra panes or windows unless the user asks.
## Git worktrees
- Default to creating git worktrees under a project-local `.worktrees/` directory at the repository root.
- For a repository at `<repo_root>`, use worktree paths like `<repo_root>/.worktrees/<task-or-branch>`.
- Create `.worktrees/` if needed before running `git worktree add`.
- Only use a non-`.worktrees/` location when the user explicitly asks for a different path.
## NixOS workflow
- This system is managed with a Nix flake at `~/dotfiles/nixos`.
- Use `just switch` from that directory for rebuilds instead of plain `nixos-rebuild`.
- Host configs live under `machines/`; choose the appropriate host when needed.
## Ad-hoc utilities via Nix
- If you want to use a CLI utility you know about but it is not currently available on PATH, prefer using `nix run` / `nix shell` to get it temporarily rather than installing it globally.
- Use `nix run` for a single command:
nix run nixpkgs#ripgrep -- rg -n "pattern" .
- Use `nix shell` when you need multiple tools available for a short sequence of commands:
nix shell nixpkgs#{jq,ripgrep} --command bash -lc 'rg -n "pattern" . | head'
- If you are not sure what the package is called in nixpkgs, use:
nix search nixpkgs <name-or-keyword>
## Personal Information
- Full Legal Name: Ivan Anthony Malison
- Email: IvanMalison@gmail.com
- Country of Citizenship: United States of America
- Birthday: August 2, 1990 (1990-08-02)
- Address: 100 Broderick St APT 401, San Francisco, CA 94117, United States
- Employer: Railbird Inc.
- GitHub: colonelpanic8
- Phone: 301-244-8534
- Primary Credit Card: Chase-Reserve
## Repository Overview
This is an org-mode repository containing personal task management, calendars, habits, and project tracking files. It serves as the central hub for Ivan's personal organization.
## Available Tools
### Chrome DevTools MCP
A browser automation MCP is available for interacting with web pages. Use it to:
- Navigate to websites and fill out forms
- Take screenshots and snapshots of pages
- Click elements, type text, and interact with web UIs
- Read page content and extract information
- Automate multi-step web workflows (booking, purchasing, form submission, etc.)
### Google Workspace CLI (`gws`)
The local `gws` CLI is available for Google Workspace operations. Use it to:
- Search, read, and send Gmail messages
- Manage Gmail labels and filters
- Download attachments and inspect message payloads
- Access Drive, Calendar, Docs, Sheets, and other Google Workspace APIs
## Credentials via `pass`
Many credentials and personal details are stored in `pass` (the standard unix password manager). There are hundreds of entries covering a wide range of things, so always search before asking the user for information. Use `pass find <keyword>` to search and `pass show <entry>` to retrieve values.
Examples of what's stored:
- Personal documents - driver's license, passport number, etc.
- Credit/debit cards - card numbers, expiration, CVV for various cards
- Banking - account numbers, online banking logins
- Travel & loyalty - airline accounts, hotel programs, CLEAR, etc.
- Website logins - credentials for hundreds of services
- API keys & tokens - GitHub, various services
- The store is regularly updated with new entries. Always do a dynamic lookup with `pass find` rather than assuming what's there.
- Provide credentials to tools/config at runtime via environment variables or inline `pass` usage instead of committing them.
- Never hardcode credentials or store them in plain text files.
## Guidelines
- When filling out forms or making purchases, pull personal info from this file and credentials from `pass` rather than asking the user to provide them.
- For web tasks, prefer using the Chrome DevTools MCP to automate interactions directly.
- For email tasks, prefer using `gws gmail` over navigating to Gmail in the browser.
- If a task requires a credential not found in `pass`, ask the user rather than guessing.
- This repo's org files (gtd.org, calendar.org, habits.org, projects.org) contain task and scheduling data. The org-agenda-api skill/service can also be used to query agenda data programmatically.
## Project links (local symlink index)
- Paths in this section are relative to this file's directory (`dotfiles/agents/`).
- Keep a local symlink index under `./project-links/` for projects that are frequently referenced.
- Treat these links as machine-local discovery state maintained by agents (do not commit machine-specific targets).
- Reuse existing symlinks first. If a link is missing or stale, search for the repo, then update the link with:
ln -sfn "<absolute-path-to-repo>" "./project-links/<link-name>"
- If a project cannot be found quickly, do a targeted search (starting from likely roots) and only then widen the search.
## Project constellation guides
- Keep per-constellation context in `./project-guides/` and keep this file minimal.
- When a request involves one of these projects:
- Open the guide first.
- If a mentioned repo/package name matches a guide's related-project list, open that guide even if the user did not name the constellation explicitly.
- Ensure required links exist under `./project-links/`.
- If links are missing, run a targeted search from likely roots, then create/update the symlink.
- Guide index:
- `./project-guides/mova-org-agenda-api.md`
- `./project-guides/taffybar.md`
- `./project-guides/railbird.md`
- `./project-guides/org-emacs-packages.md`

View File

@@ -1,108 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
mapfile -d '' -t parsed < <(PAYLOAD="$input" python3 - <<'PY'
import json, os, sys
try:
data = json.loads(os.environ.get("PAYLOAD", ""))
except Exception:
data = {}
cwd = data.get("cwd") or os.getcwd()
prompt = (data.get("prompt") or "").strip()
sys.stdout.write(cwd)
sys.stdout.write("\0")
sys.stdout.write(prompt)
sys.stdout.write("\0")
sys.stdout.write(str(data.get("session_id") or ""))
sys.stdout.write("\0")
PY
)
cwd="${parsed[0]:-}"
prompt="${parsed[1]:-}"
session_id="${parsed[2]:-}"
if [[ -z "${cwd}" ]]; then
cwd="$PWD"
fi
project_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)
if [[ -n "$project_root" ]]; then
project=$(basename "$project_root")
else
project=$(basename "$cwd")
fi
prompt_first_line=$(printf '%s' "$prompt" | head -n 1 | tr '\n' ' ' | sed -e 's/[[:space:]]\+/ /g' -e 's/^ *//; s/ *$//')
lower=$(printf '%s' "$prompt_first_line" | tr '[:upper:]' '[:lower:]')
case "$lower" in
""|"ok"|"okay"|"thanks"|"thx"|"cool"|"yep"|"yes"|"no"|"sure"|"done"|"k")
exit 0
;;
esac
task="$prompt_first_line"
if [[ -z "$task" ]]; then
task="work"
fi
explicit_retitle=false
case "$lower" in
"new task:"*|"new topic:"*|"switch topic:"*|"switch context:"*|"rename title:"*|"title:"*)
explicit_retitle=true
task=$(printf '%s' "$prompt_first_line" | sed -E 's/^[^:]+:[[:space:]]*//')
if [[ -z "$task" ]]; then
task="work"
fi
;;
esac
# Trim to a reasonable length for multiplexer UI labels.
if [[ ${#task} -gt 60 ]]; then
task="${task:0:57}..."
fi
title="$project - $task"
# The hook only sees the newest prompt, not the full conversation. Avoid
# degrading a useful same-project title into a granular follow-up summary.
if [[ -n "${TMUX:-}" ]]; then
multiplexer="tmux"
elif [[ -n "${ZELLIJ:-}" ]]; then
multiplexer="zellij"
else
multiplexer=""
fi
hook_state_file=""
if [[ -n "$multiplexer" ]]; then
state_dir="${HOME}/.agents/state"
if [[ -n "$session_id" ]]; then
safe_session_id=$(printf '%s' "$session_id" | tr -c '[:alnum:]_.-' '_')
hook_state_file="${state_dir}/${multiplexer}-title-hook-${safe_session_id}"
else
hook_state_file="${state_dir}/${multiplexer}-title"
fi
if [[ -f "$hook_state_file" ]]; then
established_title=$(cat "$hook_state_file" 2>/dev/null || true)
if [[ "$established_title" == "$project - "* && "$established_title" != "$title" && "$explicit_retitle" != true ]]; then
exit 0
fi
fi
fi
if command -v set_multiplexer_title >/dev/null 2>&1; then
set_multiplexer_title "$title"
else
hook_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
"$hook_dir/../../lib/functions/set_multiplexer_title" "$title"
fi
if [[ -n "$hook_state_file" ]]; then
mkdir -p "$(dirname "$hook_state_file")"
printf '%s' "$title" > "$hook_state_file"
fi

View File

@@ -1,29 +0,0 @@
# Mova / org-agenda-api constellation
## Scope
- Use this guide for requests involving the mova constellation, including `org-agenda-api`.
- Primary anchor is the mova root repo; start there and branch out.
## Related packages/projects (trigger list)
- If any of these names are mentioned, open this guide for context.
- `mova-dev`: coordination repo for the mova ecosystem and cross-repo workflows.
- `mova`: React Native app (iOS/Android/Web).
- `org-agenda-api`: Emacs Lisp HTTP API and deployment container.
- `org-window-habit`: habit-tracking logic used by org workflows.
- `org-wild-notifier`: org notification logic and scheduling behavior.
- `dotfiles` (within mova-dev context): infra/config and deployment glue for org-agenda-api.
## Symlink targets
- `./project-links/mova-dev` -> mova constellation root.
## Discovery hints
- Check likely roots first, especially `~/Projects`.
- Common local path is `~/Projects/mova-dev`, but do not assume it exists.
- If the symlink is missing or stale, search by directory name first, then by repo names.
## Read-first docs
- `./project-links/mova-dev/README.md`
- `./project-links/mova-dev/org-agenda-api/README.md` (if present)
## Notes
- Prefer treating mova root docs as canonical project context.

View File

@@ -1,25 +0,0 @@
# Org / Emacs package constellation
## Scope
- Use this guide for org-related package repos, including `org-window-habit`.
- This is especially relevant when repos are managed through local Emacs package trees.
## Related packages/projects (trigger list)
- If any of these names are mentioned, open this guide for context.
- `org-window-habit`: org habit-tracking package/repo.
- `org-wild-notifier`: org notification package/repo.
- `org-agenda-api`: Emacs Lisp HTTP API project that loads org package deps.
- `elpaca`: Emacs package manager tree where local checkouts may live.
- `elpa`: traditional Emacs package install tree (fallback search area).
## Symlink targets
- `./project-links/org-window-habit` -> org-window-habit repo/root.
## Discovery hints
- Start with Emacs roots, especially `~/.emacs.d`.
- Prefer checking package manager trees (including `elpaca`) before broader searches.
- Common pattern is nested repos under `~/.emacs.d` package directories.
## Read-first docs
- `./project-links/org-window-habit/README.md`
- `./project-links/org-window-habit/README.org` (if present)

View File

@@ -1,28 +0,0 @@
# Railbird constellation
## Scope
- Use this guide for requests involving railbird backend/main repo and railbird mobile app work.
## Related packages/projects (trigger list)
- If any of these names are mentioned, open this guide for context.
- `railbird`: primary backend/main railbird repository.
- `railbird-mobile`: primary mobile app repository.
- `railbird2`: alternate/new-generation backend repo.
- `railbird-mobile2`: alternate/new-generation mobile repo.
- `railbird-docs`: documentation repository.
- `railbird-landing-page`: marketing/landing site repository.
- `railbird-alert-tuning`: alert/tuning and operational experimentation repo.
- `railbird-agents-architecture`: architecture notes/prototypes for agent workflows.
## Symlink targets
- `./project-links/railbird` -> primary railbird repo.
- `./project-links/railbird-mobile` -> railbird mobile app repo.
## Discovery hints
- Start from `~/Projects`.
- Common backend location is `~/Projects/railbird`.
- Mobile repo often also lives under `~/Projects`, but name/path may vary by machine.
## Read-first docs
- `./project-links/railbird/README.md`
- `./project-links/railbird-mobile/README.md` (if present)

View File

@@ -1,30 +0,0 @@
# Taffybar constellation
## Scope
- Use this guide for requests involving taffybar itself or local taffybar configuration.
## Related packages/projects (trigger list)
- If any of these names are mentioned, open this guide for context.
- `taffybar`: top-level desktop bar library/app.
- `imalison-taffybar`: personal taffybar configuration package/repo.
- `gtk-sni-tray`: StatusNotifier tray integration for taffybar.
- `gtk-strut`: X11/WM strut handling used by taffybar ecosystem.
- `status-notifier-item`: StatusNotifier protocol/types library.
- `dbus-menu`: DBus menu protocol support used by tray integrations.
- `dbus-hslogger`: DBus logging helper used in ecosystem packages.
## Symlink targets
- `./project-links/taffybar-main` -> main taffybar repo.
- `./project-links/taffybar-config` -> local taffybar config root.
## Discovery hints
- Start with `~/.config/taffybar`.
- Common layout is:
- config root at `~/.config/taffybar`
- main repo at `~/.config/taffybar/taffybar`
- Other taffybar-related repos may exist elsewhere; find them from docs in the main repo.
## Read-first docs
- `./project-links/taffybar-main/README.md`
- `./project-links/taffybar-config/README.md` (if present)
- `./project-links/taffybar-config/AGENTS.md` (if present)

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -1,2 +0,0 @@
.system/
codex-primary-runtime/

View File

@@ -1,256 +0,0 @@
---
name: disk-space-cleanup
description: Investigate and safely reclaim disk space on this machine, especially on NixOS systems with heavy Nix, Rust/Haskell, Docker, and Podman usage. Use when disk is low, builds fail with no-space errors, /nix/store appears unexpectedly large, or the user asks for easy cleanup wins without deleting important data.
---
# Disk Space Cleanup
Reclaim disk space with a safety-first workflow: investigate first, run obvious low-risk cleanup wins, then do targeted analysis for larger opportunities.
Bundled helpers:
- `scripts/rust_target_dirs.py`: inventory and guarded deletion for explicit Rust `target/` directories
- `references/rust-target-roots.txt`: machine-specific roots for Rust artifact scans
- `references/ignore-paths.md`: machine-specific excludes for `du`/`ncdu`
## Execution Default
- Start with non-destructive investigation and quick sizing.
- Prioritize easy wins first (`nix-collect-garbage`, container prune, Cargo artifacts).
- Propose destructive actions with expected impact before running them.
- Run destructive actions only after confirmation, unless the user explicitly requests immediate execution of obvious wins.
- Capture new reusable findings by updating this skill before finishing.
## Workflow
1. Establish current pressure and biggest filesystems
2. Run easy cleanup wins
3. Inventory Rust build artifacts and clean the right kind of target
4. Investigate remaining heavy directories with `ncdu`/`du`
5. Investigate `/nix/store` roots when large toolchains still persist
6. Summarize reclaimed space and next candidate actions
7. Record new machine-specific ignore paths, Rust roots, or cleanup patterns in this skill
## Step 1: Baseline
Run a quick baseline before deleting anything:
```bash
df -h /
df -h /home
df -h /nix
```
Optionally add a quick home-level size snapshot:
```bash
du -xh --max-depth=1 "$HOME" 2>/dev/null | sort -h
```
## Step 2: Easy Wins
Use these first when the user wants fast, low-effort reclaiming:
```bash
sudo -n nix-collect-garbage -d
sudo -n docker system prune -a
sudo -n podman system prune -a
```
Notes:
- Add `--volumes` only when the user approves deleting unused volumes.
- Re-check free space after each command to show impact.
- Prefer `sudo -n` first so cleanup runs fail fast instead of hanging on password prompts.
- If root is still tight after these, run app cache cleaners before proposing raw `rm -rf`:
```bash
uv cache clean
pip cache purge
yarn cache clean
npm cache clean --force
```
## Step 3: Rust Build Artifact Cleanup
Do not start with a blind `find ~ -name target` or with hard-coded roots that may miss worktrees. Inventory explicit `target/` directories first using the bundled helper and the machine-specific root list in `references/rust-target-roots.txt`.
Inventory the biggest candidates:
```bash
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 500M --limit 30
```
Focus on stale targets only:
```bash
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 1G --older-than 14 --output tsv
```
Use `cargo-sweep` when the repo is still active and you want age/toolchain-aware cleanup inside a workspace:
```bash
nix run nixpkgs#cargo-sweep -- sweep -d -r -t 30 <workspace-root>
nix run nixpkgs#cargo-sweep -- sweep -r -t 30 <workspace-root>
nix run nixpkgs#cargo-sweep -- sweep -d -r -i <workspace-root>
nix run nixpkgs#cargo-sweep -- sweep -r -i <workspace-root>
```
Use direct `target/` deletion when inventory shows a discrete stale directory, especially for inactive repos or project-local worktrees. The helper only deletes explicit paths named `target` that are beneath configured roots and a Cargo project:
```bash
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target --yes
```
Recommended sequence:
1. Run `rust_target_dirs.py list` to see the largest `target/` directories across `~/Projects`, `~/org`, `~/dotfiles`, and other configured roots.
2. For active repos, prefer `cargo-sweep` from the workspace root.
3. For inactive repos, abandoned branches, and `.worktrees/*/target`, prefer guarded direct deletion of the explicit `target/` directory.
4. Re-run the list command after each deletion round to show reclaimed space.
Machine-specific note:
- Project-local `.worktrees/*/target` directories are common cleanup wins on this machine and are easy to miss with the old hard-coded workflow.
- `cargo-sweep` is installed through the NixOS `code.nix` package set, but stale manually-installed binaries under `~/.cargo/bin` can shadow `/run/current-system/sw/bin/cargo-sweep`. If `cargo sweep` fails with a missing loader or `No such file or directory`, run `type -a cargo-sweep` and remove the stale `~/.cargo/bin/cargo-sweep` entry.
- `nixos/imalison.nix` defines a daily user timer, `cargo-sweep-rust-targets.timer`, that runs `cargo-sweep sweep -r --hidden --maxsize 15GB` across `/home/imalison/Projects`, `/home/imalison/org`, and `/home/imalison/dotfiles`.
## Step 4: Investigation with `ncdu` and `du`
Avoid mounted or remote filesystems when profiling space. Load ignore patterns from `references/ignore-paths.md`.
Use one-filesystem scans to avoid crossing mounts:
```bash
ncdu -x "$HOME"
sudo ncdu -x /
```
When excluding known noisy mountpoints:
```bash
ncdu -x --exclude "$HOME/keybase" "$HOME"
sudo ncdu -x --exclude /keybase --exclude /var/lib/railbird /
```
If `ncdu` is missing, use:
```bash
nix run nixpkgs#ncdu -- -x "$HOME"
```
For reusable, mount-safe snapshots on this machine, prefer the local wrapper:
```bash
safe_ncdu /
sudo -n env HOME=/home/imalison safe_ncdu /
safe_ncdu /nix/store
safe_ncdu top ~/.cache/ncdu/latest-root.json.zst 30 /home/imalison
safe_ncdu open ~/.cache/ncdu/latest-root.json.zst
```
`safe_ncdu` writes compressed ncdu exports under `~/.cache/ncdu`, records the exclude list beside the export, excludes mounted descendants of the scan root, and supports follow-up `top` queries without rescanning.
For quick, non-blocking triage on very large trees, prefer bounded probes:
```bash
timeout 30s du -xh --max-depth=1 "$HOME/.cache" 2>/dev/null | sort -h
timeout 30s du -xh --max-depth=1 "$HOME/.local/share" 2>/dev/null | sort -h
```
Machine-specific heavy hitters seen in practice:
- `~/.cache/uv` can exceed 20G and is reclaimable with `uv cache clean`.
- `~/.cache/pypoetry` can exceed 7G across artifacts, repository cache, and virtualenvs; inspect first, then use Poetry cache commands or targeted virtualenv removal.
- `~/.cache/google-chrome` can exceed 8G across multiple Chrome profiles; close Chrome before clearing profile cache directories.
- `~/.cache/spotify` can exceed 10G; treat as optional app-cache cleanup.
- `~/.gradle` can exceed 8G, mostly under `caches/`; prefer Gradle-aware cleanup and expect dependency redownloads.
- `~/.local/share/picom/debug.log` can grow past 15G when verbose picom debugging is enabled or crashes leave a stale log behind; if `picom` is not running, deleting or truncating the log is a high-yield low-risk win.
- `~/.local/share/Trash` can exceed several GB; empty only with user approval.
- `/var/lib/private/gitea-runner` can exceed 50G and is not visible to an unprivileged `ncdu /` scan; use `sudo -n env HOME=/home/imalison safe_ncdu /` when `/var` looks undercounted.
- Validated cleanup pattern: stop `gitea-runner-nix.service`, remove cache/work directories under `/var/lib/private/gitea-runner` (`.cache`, `.gradle`, `action-cache-dir`, `workspace`, stale nested `gitea-runner`, and nested `nix/.cache`/`nix/.local`), recreate `action-cache-dir`, `workspace`, and `.cache` owned by `gitea-runner:gitea-runner`, then restart the service.
- Preserve registration/config-like files such as `/var/lib/private/gitea-runner/nix/.runner`, `/var/lib/private/gitea-runner/nix/.labels`, `/var/lib/private/gitea-runner/.docker/config.json`, and SSH/Kube material.
- `~/Projects/*/target` directories can dominate home usage. Recent example candidates included stale `target/` directories under `scrobble-scrubber`, `http-client-vcr`, `http-client`, `subtr-actor`, `http-types`, `subtr-actor-py`, `sdk`, and `async-h1`.
## Step 5: `/nix/store` Deep Dive
When `/nix/store` is still large after GC, inspect root causes instead of deleting random paths.
Useful commands:
```bash
nix path-info -Sh /nix/store/* 2>/dev/null | sort -h | tail -n 50
nix-store --gc --print-roots
```
Avoid `du -sh /nix/store` as a first diagnostic; it can be very slow on large stores.
For repeated GHC/Rust toolchain copies:
```bash
nix path-info -Sh /nix/store/* 2>/dev/null | rg '(ghc|rustc|rust-std|cargo)'
nix-store --gc --print-roots | rg '(ghc|rust)'
```
Resolve why a path is retained:
```bash
/home/imalison/dotfiles/dotfiles/lib/functions/find_store_path_gc_roots /nix/store/<store-path>
nix why-depends <consumer-store-path> <dependency-store-path>
```
Common retention pattern on this machine:
- Many `.direnv/flake-profile-*` symlinks under `~/Projects` and worktrees keep `nix-shell-env`/`ghc-shell-*` roots alive.
- Old taffybar constellation repos under `~/Projects` can pin large Haskell closures through `.direnv` and `result` symlinks. Deleting `gtk-sni-tray`, `status-notifier-item`, `dbus-menu`, `dbus-hslogger`, and `gtk-strut` and then rerunning `nix-collect-garbage -d` reclaimed about 11G of store data in one validated run.
- `find_store_path_gc_roots` is especially useful for proving GHC retention: many large `ghc-9.10.3-with-packages` paths are unique per project, while the base `ghc-9.10.3` and docs paths are shared.
- NixOS system generations and a repo-root `nixos/result` symlink can pin multiple Android Studio and Android SDK versions. Check `/nix/var/nix/profiles/system-*-link`, `/run/current-system`, `/run/booted-system`, and `~/dotfiles/nixos/result` before assuming Android paths are pinned by project shells.
- `~/Projects/railbird-mobile/.direnv/flake-profile-*` can pin large Android SDK system images. Removing stale direnv profiles there is a more targeted first step than deleting Android store paths directly.
- For a repeatable `/nix/store` `ncdu` snapshot without driving the TUI, export and inspect it:
```bash
ncdu -0 -x -c -o /tmp/nix-store.ncdu.json.zst /nix/store
zstdcat /tmp/nix-store.ncdu.json.zst | jq 'def sumd: if type=="array" then ((.[0].dsize // 0) + ([.[1:][] | sumd] | add // 0)) elif type=="object" then (.dsize // 0) else 0 end; .[3] | sumd'
```
- `nix-store --gc --print-dead` plus the Nix SQLite database is a fast way to estimate immediate GC wins before deleting anything:
```bash
nix-store --gc --print-dead > /tmp/nix-dead-paths.txt
printf '%s\n' '.mode list' '.separator |' 'create temp table dead(path text);' \
'.import /tmp/nix-dead-paths.txt dead' \
'select count(*), sum(narSize) from ValidPaths join dead using(path);' \
| nix shell nixpkgs#sqlite --command sqlite3 /nix/var/nix/db/db.sqlite
```
- Quantify before acting:
```bash
find ~/Projects -type l -path '*/.direnv/flake-profile-*' | wc -l
find ~/Projects -type d -name .direnv | wc -l
nix-store --gc --print-roots | rg '/\\.direnv/flake-profile-' | awk -F' -> ' '{print $1 \"|\" $2}' \
| while IFS='|' read -r root target; do \
nix-store -qR \"$target\" | rg '^/nix/store/.+-ghc-[0-9]'; \
done | sort | uniq -c | sort -nr | head
```
- If counts are high and the projects are inactive, propose targeted `.direnv` cleanup for user confirmation.
## Safety Rules
- Do not delete user files directly unless explicitly requested.
- Prefer cleanup tools that understand ownership/metadata (`nix`, `docker`, `podman`, `cargo-sweep`) over `rm -rf`.
- For Rust build artifacts, deleting an explicit directory literally named `target` is acceptable when it is discovered by the bundled helper; Cargo will rebuild it.
- Present a concise “proposed actions” list before high-impact deletes.
- If uncertain whether data is needed, stop at investigation and ask.
## Learning Loop (Required)
Treat this skill as a living playbook.
After each disk cleanup task:
1. Add newly discovered mountpoints or directories to ignore in `references/ignore-paths.md`.
2. Add newly discovered Rust repo roots in `references/rust-target-roots.txt`.
3. Add validated command patterns or caveats discovered during the run to this `SKILL.md`.
4. Keep instructions practical and machine-specific; remove stale guidance.

View File

@@ -1,3 +0,0 @@
interface:
display_name: "Disk Space Cleanup"
short_description: "Find safe disk-space wins on NixOS hosts"

View File

@@ -1,31 +0,0 @@
# Ignore Paths for Disk Investigation
Use this file to track mountpoints or directories that should be excluded from `ncdu`/`du` scans because they are remote, special-purpose, or noisy.
## Known Ignores
- `$HOME/keybase`
- `$HOME/.cache/keybase`
- `$HOME/.local/share/keybase`
- `$HOME/.config/keybase`
- `/keybase`
- `/var/lib/railbird`
- `/run/user/*/doc` (FUSE portal mount; machine-specific example observed: `/run/user/1004/doc`)
## Discovery Commands
List mounted filesystems and spot special mounts:
```bash
findmnt -rn -o TARGET,FSTYPE,SOURCE
```
Target likely remote/special mounts:
```bash
findmnt -rn -o TARGET,FSTYPE,SOURCE | rg '(keybase|fuse|rclone|s3|railbird)'
```
## Maintenance Rule
When a disk cleanup run encounters a mount or path that should be ignored in future runs, add it here immediately with a short note.

View File

@@ -1,6 +0,0 @@
# One absolute path per line. Comments are allowed.
# Keep this list machine-specific and update it when Rust repos move.
/home/imalison/Projects
/home/imalison/org
/home/imalison/dotfiles

View File

@@ -1,271 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_ROOTS_FILE = SCRIPT_DIR.parent / "references" / "rust-target-roots.txt"
def parse_size(value: str) -> int:
text = value.strip().upper()
units = {
"B": 1,
"K": 1024,
"KB": 1024,
"M": 1024**2,
"MB": 1024**2,
"G": 1024**3,
"GB": 1024**3,
"T": 1024**4,
"TB": 1024**4,
}
for suffix, multiplier in units.items():
if text.endswith(suffix):
number = text[: -len(suffix)].strip()
return int(float(number) * multiplier)
return int(float(text))
def human_size(num_bytes: int) -> str:
value = float(num_bytes)
for unit in ["B", "K", "M", "G", "T"]:
if value < 1024 or unit == "T":
if unit == "B":
return f"{int(value)}B"
return f"{value:.1f}{unit}"
value /= 1024
return f"{num_bytes}B"
def is_relative_to(path: Path, root: Path) -> bool:
try:
path.relative_to(root)
return True
except ValueError:
return False
def load_roots(roots_file: Path, cli_roots: list[str]) -> list[Path]:
roots: list[Path] = []
for raw in cli_roots:
candidate = Path(raw).expanduser().resolve()
if candidate.exists():
roots.append(candidate)
if roots_file.exists():
for line in roots_file.read_text().splitlines():
stripped = line.split("#", 1)[0].strip()
if not stripped:
continue
candidate = Path(stripped).expanduser().resolve()
if candidate.exists():
roots.append(candidate)
unique_roots: list[Path] = []
seen: set[Path] = set()
for root in roots:
if root not in seen:
unique_roots.append(root)
seen.add(root)
return unique_roots
def du_size_bytes(path: Path) -> int:
result = subprocess.run(
["du", "-sb", str(path)],
check=True,
capture_output=True,
text=True,
)
return int(result.stdout.split()[0])
def nearest_cargo_root(path: Path, stop_roots: list[Path]) -> str:
current = path.parent
stop_root_set = set(stop_roots)
while current != current.parent:
if (current / "Cargo.toml").exists():
return str(current)
if current in stop_root_set:
break
current = current.parent
return ""
def discover_targets(roots: list[Path]) -> list[dict]:
results: dict[Path, dict] = {}
now = time.time()
for root in roots:
for current, dirnames, _filenames in os.walk(root, topdown=True):
if "target" in dirnames:
target_dir = (Path(current) / "target").resolve()
dirnames.remove("target")
if target_dir in results or not target_dir.is_dir():
continue
stat_result = target_dir.stat()
size_bytes = du_size_bytes(target_dir)
age_days = int((now - stat_result.st_mtime) // 86400)
results[target_dir] = {
"path": str(target_dir),
"size_bytes": size_bytes,
"size_human": human_size(size_bytes),
"age_days": age_days,
"workspace": nearest_cargo_root(target_dir, roots),
}
return sorted(results.values(), key=lambda item: item["size_bytes"], reverse=True)
def print_table(rows: list[dict]) -> None:
if not rows:
print("No matching Rust target directories found.")
return
size_width = max(len(row["size_human"]) for row in rows)
age_width = max(len(str(row["age_days"])) for row in rows)
print(
f"{'SIZE'.ljust(size_width)} {'AGE'.rjust(age_width)} PATH"
)
for row in rows:
print(
f"{row['size_human'].ljust(size_width)} "
f"{str(row['age_days']).rjust(age_width)}d "
f"{row['path']}"
)
def filter_rows(rows: list[dict], min_size: int, older_than: int | None, limit: int | None) -> list[dict]:
filtered = [row for row in rows if row["size_bytes"] >= min_size]
if older_than is not None:
filtered = [row for row in filtered if row["age_days"] >= older_than]
if limit is not None:
filtered = filtered[:limit]
return filtered
def cmd_list(args: argparse.Namespace) -> int:
roots = load_roots(Path(args.roots_file).expanduser(), args.root)
if not roots:
print("No scan roots available.", file=sys.stderr)
return 1
rows = discover_targets(roots)
rows = filter_rows(rows, parse_size(args.min_size), args.older_than, args.limit)
if args.output == "json":
print(json.dumps(rows, indent=2))
elif args.output == "tsv":
for row in rows:
print(
"\t".join(
[
str(row["size_bytes"]),
str(row["age_days"]),
row["path"],
row["workspace"],
]
)
)
elif args.output == "paths":
for row in rows:
print(row["path"])
else:
print_table(rows)
return 0
def validate_delete_path(path_text: str, roots: list[Path]) -> Path:
target = Path(path_text).expanduser().resolve(strict=True)
if target.name != "target":
raise ValueError(f"{target} is not a target directory")
if target.is_symlink():
raise ValueError(f"{target} is a symlink")
if not target.is_dir():
raise ValueError(f"{target} is not a directory")
if not any(is_relative_to(target, root) for root in roots):
raise ValueError(f"{target} is outside configured scan roots")
if nearest_cargo_root(target, roots) == "":
raise ValueError(f"{target} is not beneath a Cargo project")
return target
def cmd_delete(args: argparse.Namespace) -> int:
roots = load_roots(Path(args.roots_file).expanduser(), args.root)
if not roots:
print("No scan roots available.", file=sys.stderr)
return 1
targets: list[Path] = []
for raw_path in args.path:
try:
targets.append(validate_delete_path(raw_path, roots))
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
total_size = sum(du_size_bytes(target) for target in targets)
print(f"Matched {len(targets)} target directories totaling {human_size(total_size)}:")
for target in targets:
print(str(target))
if not args.yes:
print("Dry run only. Re-run with --yes to delete these target directories.")
return 0
for target in targets:
shutil.rmtree(target)
print(f"Deleted {len(targets)} target directories.")
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Inventory and delete Rust target directories under configured roots."
)
parser.add_argument(
"--roots-file",
default=str(DEFAULT_ROOTS_FILE),
help="Path to the newline-delimited root list.",
)
parser.add_argument(
"--root",
action="append",
default=[],
help="Additional root to scan. May be provided multiple times.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
list_parser = subparsers.add_parser("list", help="List target directories.")
list_parser.add_argument("--min-size", default="0", help="Minimum size threshold, for example 500M or 2G.")
list_parser.add_argument("--older-than", type=int, help="Only include targets at least this many days old.")
list_parser.add_argument("--limit", type=int, help="Maximum number of rows to print.")
list_parser.add_argument(
"--output",
choices=["table", "tsv", "json", "paths"],
default="table",
help="Output format.",
)
list_parser.set_defaults(func=cmd_list)
delete_parser = subparsers.add_parser("delete", help="Delete explicit target directories.")
delete_parser.add_argument("path", nargs="+", help="One or more target directories to delete.")
delete_parser.add_argument("--yes", action="store_true", help="Actually delete the paths.")
delete_parser.set_defaults(func=cmd_delete)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,111 +0,0 @@
---
name: email-unsubscribe-check
description: Use when user wants to find promotional or unwanted recurring emails to unsubscribe from, or when doing periodic inbox hygiene to identify senders worth unsubscribing from
---
# Email Unsubscribe Check
Scan recent inbox emails to surface promotional, newsletter, and digest senders the user likely wants to unsubscribe from. Actually unsubscribe via browser automation.
## Workflow
```dot
digraph unsubscribe_check {
"Search recent inbox emails" -> "Group by sender domain";
"Group by sender domain" -> "Classify each sender";
"Classify each sender" -> "Obvious unsubscribe?";
"Obvious unsubscribe?" -> "Present to user for confirmation" [label="yes"];
"Obvious unsubscribe?" -> "Borderline?" [label="no"];
"Borderline?" -> "Ask user" [label="yes"];
"Borderline?" -> "Skip" [label="no, personal"];
"Present to user for confirmation" -> "User confirms?";
"User confirms?" -> "Actually unsubscribe" [label="yes"];
"User confirms?" -> "Skip" [label="no"];
"Actually unsubscribe" -> "Mark matching emails read + archive";
"Mark matching emails read + archive" -> "Create Gmail filter";
"Create Gmail filter" -> "Retroactively clean old emails";
}
```
## Execution Default
- Start the workflow immediately when this skill is invoked.
- Do not ask a kickoff question like "should I start now?".
- Default scan window is `newer_than:7d` unless the user already specified a different range.
- Only ask a follow-up question before starting if required information is missing and execution would otherwise be blocked.
- Default user preference: they generally do not want subscription-style email in their inbox.
- For obvious marketing/newsletter/digest mail with a working unsubscribe path, unsubscribe by default without asking for confirmation first.
- Still ask first for borderline cases such as creator subscriptions, professional communities, event platforms, or anything that appears transactional/security-sensitive.
## How to Scan
1. Search recent emails: `newer_than:7d` (or wider if user requests)
2. Identify senders that look promotional/automated/digest
3. Present findings grouped by confidence:
- **Clearly unsubscribeable**: marketing, promos, digests user never engages with
- **Ask user**: newsletters, community content, event platforms (might be wanted)
When the user's standing preference is to keep subscriptions out of the inbox, treat the **Clearly unsubscribeable** bucket as auto-actionable.
## Unsubscribe Execution
For each confirmed sender, do ALL of these:
### 1. Actually unsubscribe via browser (most important step)
Two approaches depending on the sender:
**For emails with unsubscribe links:**
- Read the email via `gws gmail` to find the unsubscribe URL (usually at bottom of email body)
- Navigate to the URL with Chrome DevTools MCP
- Take a snapshot, find the confirmation button/checkbox
- Click through to complete the unsubscribe
- Verify the confirmation page
**For services with email settings pages (Nextdoor, LinkedIn, etc.):**
- Navigate to the service's notification/email settings page
- Log in using credentials from `pass` if needed
- Find and disable all email notification toggles
- Check ALL categories (digests, alerts, promotions, etc.)
### 2. Create Gmail filter as backup
Even after unsubscribing, create a filter to catch stragglers:
```
gws gmail users settings filters create \
--params '{"userId":"me"}' \
--json '{"criteria":{"from":"domain.com"},"action":{"removeLabelIds":["INBOX"]}}'
```
### 3. Mark old emails as read and archive them (minimum hygiene)
After unsubscribing, clean up existing email from the sender.
- At minimum: mark them as read.
- Preferred/default: also archive them (remove `INBOX` label).
Example:
```
gws gmail users messages list --params '{"userId":"me","q":"from:domain.com","maxResults":50}'
gws gmail users messages batchModify \
--params '{"userId":"me"}' \
--json '{"ids":["..."],"removeLabelIds":["UNREAD","INBOX"]}'
```
## Signals That an Email is Unsubscribeable
- "no-reply@" or "newsletter@" sender addresses
- Marketing subject lines: sales, promotions, "don't miss", digests
- Bulk senders: Nextdoor, Yelp, LinkedIn digest, social media notifications
- Community digests the user doesn't engage with
- Financial marketing (not transactional alerts)
- "Your weekly/daily/monthly" summaries
- Messages with explicit unsubscribe/manage-preferences links whose primary purpose is promotional or newsletter delivery
## Signals to NOT Auto-Unsubscribe (Ask First)
- Patreon/creator content
- Event platforms (Luma, Eventbrite, Meetup)
- Professional communities
- Services the user actively uses (even if noisy)
- Transactional emails from wanted services

View File

@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,25 +0,0 @@
---
name: gh-address-comments
description: Help address review/issue comments on the open GitHub PR for the current branch using gh CLI; verify gh auth first and prompt the user to authenticate if not logged in.
metadata:
short-description: Address comments in a GitHub PR review
---
# PR Comment Handler
Guide to find the open PR for the current branch and address its comments with gh CLI. Run all `gh` commands with elevated network access.
Prereq: ensure `gh` is authenticated (for example, run `gh auth login` once), then run `gh auth status` with escalated permissions (include workflow/repo scopes) so `gh` commands succeed. If sandboxing blocks `gh auth status`, rerun it with `sandbox_permissions=require_escalated`.
## 1) Inspect comments needing attention
- Run scripts/fetch_comments.py which will print out all the comments and review threads on the PR
## 2) Ask the user for clarification
- Number all the review threads and comments and provide a short summary of what would be required to apply a fix for it
- Ask the user which numbered comments should be addressed
## 3) If user chooses comments
- Apply fixes for the selected comments
Notes:
- If gh hits auth/rate issues mid-run, prompt the user to re-authenticate with `gh auth login`, then retry.

View File

@@ -1,6 +0,0 @@
interface:
display_name: "GitHub Address Comments"
short_description: Address comments in a GitHub PR review"
icon_small: "./assets/github-small.svg"
icon_large: "./assets/github.png"
default_prompt: "Address all actionable GitHub PR review comments in this branch and summarize the updates."

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill="currentColor" d="M8 1.3a6.665 6.665 0 0 1 5.413 10.56 6.677 6.677 0 0 1-3.288 2.432c-.333.067-.458-.142-.458-.316 0-.226.008-.942.008-1.834 0-.625-.208-1.025-.45-1.233 1.483-.167 3.042-.734 3.042-3.292a2.58 2.58 0 0 0-.684-1.792c.067-.166.3-.85-.066-1.766 0 0-.559-.184-1.834.683a6.186 6.186 0 0 0-1.666-.225c-.567 0-1.134.075-1.667.225-1.275-.858-1.833-.683-1.833-.683-.367.916-.134 1.6-.067 1.766a2.594 2.594 0 0 0-.683 1.792c0 2.55 1.55 3.125 3.033 3.292-.192.166-.367.458-.425.891-.383.175-1.342.459-1.942-.55-.125-.2-.5-.691-1.025-.683-.558.008-.225.317.009.442.283.158.608.75.683.941.133.376.567 1.092 2.242.784 0 .558.008 1.083.008 1.242 0 .174-.125.374-.458.316a6.662 6.662 0 0 1-4.559-6.325A6.665 6.665 0 0 1 8 1.3Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,237 +0,0 @@
#!/usr/bin/env python3
"""
Fetch all PR conversation comments + reviews + review threads (inline threads)
for the PR associated with the current git branch, by shelling out to:
gh api graphql
Requires:
- `gh auth login` already set up
- current branch has an associated (open) PR
Usage:
python fetch_comments.py > pr_comments.json
"""
from __future__ import annotations
import json
import subprocess
import sys
from typing import Any
QUERY = """\
query(
$owner: String!,
$repo: String!,
$number: Int!,
$commentsCursor: String,
$reviewsCursor: String,
$threadsCursor: String
) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
number
url
title
state
# Top-level "Conversation" comments (issue comments on the PR)
comments(first: 100, after: $commentsCursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
body
createdAt
updatedAt
author { login }
}
}
# Review submissions (Approve / Request changes / Comment), with body if present
reviews(first: 100, after: $reviewsCursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
state
body
submittedAt
author { login }
}
}
# Inline review threads (grouped), includes resolved state
reviewThreads(first: 100, after: $threadsCursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
isResolved
isOutdated
path
line
diffSide
startLine
startDiffSide
originalLine
originalStartLine
resolvedBy { login }
comments(first: 100) {
nodes {
id
body
createdAt
updatedAt
author { login }
}
}
}
}
}
}
}
"""
def _run(cmd: list[str], stdin: str | None = None) -> str:
p = subprocess.run(cmd, input=stdin, capture_output=True, text=True)
if p.returncode != 0:
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{p.stderr}")
return p.stdout
def _run_json(cmd: list[str], stdin: str | None = None) -> dict[str, Any]:
out = _run(cmd, stdin=stdin)
try:
return json.loads(out)
except json.JSONDecodeError as e:
raise RuntimeError(f"Failed to parse JSON from command output: {e}\nRaw:\n{out}") from e
def _ensure_gh_authenticated() -> None:
try:
_run(["gh", "auth", "status"])
except RuntimeError:
print("run `gh auth login` to authenticate the GitHub CLI", file=sys.stderr)
raise RuntimeError("gh auth status failed; run `gh auth login` to authenticate the GitHub CLI") from None
def gh_pr_view_json(fields: str) -> dict[str, Any]:
# fields is a comma-separated list like: "number,headRepositoryOwner,headRepository"
return _run_json(["gh", "pr", "view", "--json", fields])
def get_current_pr_ref() -> tuple[str, str, int]:
"""
Resolve the PR for the current branch (whatever gh considers associated).
Works for cross-repo PRs too, by reading head repository owner/name.
"""
pr = gh_pr_view_json("number,headRepositoryOwner,headRepository")
owner = pr["headRepositoryOwner"]["login"]
repo = pr["headRepository"]["name"]
number = int(pr["number"])
return owner, repo, number
def gh_api_graphql(
owner: str,
repo: str,
number: int,
comments_cursor: str | None = None,
reviews_cursor: str | None = None,
threads_cursor: str | None = None,
) -> dict[str, Any]:
"""
Call `gh api graphql` using -F variables, avoiding JSON blobs with nulls.
Query is passed via stdin using query=@- to avoid shell newline/quoting issues.
"""
cmd = [
"gh",
"api",
"graphql",
"-F",
"query=@-",
"-F",
f"owner={owner}",
"-F",
f"repo={repo}",
"-F",
f"number={number}",
]
if comments_cursor:
cmd += ["-F", f"commentsCursor={comments_cursor}"]
if reviews_cursor:
cmd += ["-F", f"reviewsCursor={reviews_cursor}"]
if threads_cursor:
cmd += ["-F", f"threadsCursor={threads_cursor}"]
return _run_json(cmd, stdin=QUERY)
def fetch_all(owner: str, repo: str, number: int) -> dict[str, Any]:
conversation_comments: list[dict[str, Any]] = []
reviews: list[dict[str, Any]] = []
review_threads: list[dict[str, Any]] = []
comments_cursor: str | None = None
reviews_cursor: str | None = None
threads_cursor: str | None = None
pr_meta: dict[str, Any] | None = None
while True:
payload = gh_api_graphql(
owner=owner,
repo=repo,
number=number,
comments_cursor=comments_cursor,
reviews_cursor=reviews_cursor,
threads_cursor=threads_cursor,
)
if "errors" in payload and payload["errors"]:
raise RuntimeError(f"GitHub GraphQL errors:\n{json.dumps(payload['errors'], indent=2)}")
pr = payload["data"]["repository"]["pullRequest"]
if pr_meta is None:
pr_meta = {
"number": pr["number"],
"url": pr["url"],
"title": pr["title"],
"state": pr["state"],
"owner": owner,
"repo": repo,
}
c = pr["comments"]
r = pr["reviews"]
t = pr["reviewThreads"]
conversation_comments.extend(c.get("nodes") or [])
reviews.extend(r.get("nodes") or [])
review_threads.extend(t.get("nodes") or [])
comments_cursor = c["pageInfo"]["endCursor"] if c["pageInfo"]["hasNextPage"] else None
reviews_cursor = r["pageInfo"]["endCursor"] if r["pageInfo"]["hasNextPage"] else None
threads_cursor = t["pageInfo"]["endCursor"] if t["pageInfo"]["hasNextPage"] else None
if not (comments_cursor or reviews_cursor or threads_cursor):
break
assert pr_meta is not None
return {
"pull_request": pr_meta,
"conversation_comments": conversation_comments,
"reviews": reviews,
"review_threads": review_threads,
}
def main() -> None:
_ensure_gh_authenticated()
owner, repo, number = get_current_pr_ref()
result = fetch_all(owner, repo, number)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()

View File

@@ -1,65 +0,0 @@
---
name: hackage-release
description: Use when user asks to release, publish, or bump version of a Haskell package to Hackage
---
# Hackage Release
Bump version, build, validate, tag, push, and publish a Haskell package to Hackage.
## Workflow
1. **Bump version** in `package.yaml` (if using hpack) or `.cabal` file
2. **Update ChangeLog.md** with release notes
3. **Regenerate cabal** (if using hpack): `hpack`
4. **Build**: `cabal build`
5. **Check**: `cabal check` (must report zero warnings)
6. **Create sdist**: `cabal sdist`
7. **Commit & tag**: commit all changed files, `git tag vX.Y.Z.W`
8. **Push**: `git push && git push --tags`
9. **Get Hackage credentials**: `pass show hackage.haskell.org.gpg`
- Format: first line is password, `user:` line has username
10. **Publish package**: `cabal upload --publish <sdist-tarball> --username=<user> --password='<pass>'`
11. **Build & publish docs**: `cabal haddock --haddock-for-hackage` then `cabal upload --documentation --publish <docs-tarball> --username=<user> --password='<pass>'`
## Version Bumping (PVP)
Haskell uses the [Package Versioning Policy](https://pvp.haskell.org/) with format `A.B.C.D`:
| Component | When to Bump |
|-----------|-------------|
| A.B (major) | Breaking API changes |
| C (minor) | Backwards-compatible new features |
| D (patch) | Bug fixes, non-API changes |
## Nix-Based Projects
If the project uses a Nix flake, wrap cabal commands with `nix develop`:
```bash
nix develop --command cabal build
nix develop --command cabal check
nix develop --command hpack package.yaml
```
Prefer `nix develop` (flake) over `nix-shell` (legacy) to avoid ABI mismatches.
## PVP Dependency Bounds
Hackage warns about:
- **Missing upper bounds**: Every dependency should have an upper bound (e.g., `text >= 1.2 && < 2.2`)
- **Trailing zeros in upper bounds**: Use `< 2` not `< 2.0.0`; use `< 0.4` not `< 0.4.0.0`
Run `cabal check` to verify zero warnings before releasing.
## Checklist
- [ ] Version bumped in package.yaml / .cabal
- [ ] ChangeLog.md updated
- [ ] Cabal file regenerated (if hpack)
- [ ] `cabal build` succeeds
- [ ] `cabal check` reports no errors or warnings
- [ ] Changes committed and tagged
- [ ] Pushed to remote with tags
- [ ] Package published to Hackage
- [ ] Docs published to Hackage

View File

@@ -1,32 +0,0 @@
---
name: journaling
description: Use when user wants to journal, reflect, write a journal entry, or process thoughts. Also use when user mentions wanting to talk through what's on their mind.
---
# Journaling
## Overview
Guide the user through a freeform journaling conversation, then synthesize their thoughts into an organized `.org` file.
## How It Works
**1. Open the conversation.** Ask what's on their mind, how things have been going, or what they want to talk through. Keep it open-ended.
**2. Follow up naturally.** Listen for what seems important - dig into those threads. Don't rush through a checklist. One question at a time.
**3. Synthesize into a journal entry.** When the conversation winds down (or the user says they're done), write an organized `~/org/journal/YYYY-MM-DD.org` file with:
- A timestamp on the first line: `[YYYY-MM-DD Day HH:MM]`
- Org headings that emerge naturally from the conversation topics
- The user's thoughts in their own voice, but organized and cleaned up
- No rigid template - structure follows content
**4. Offer to review.** Show them the entry before writing, let them tweak it.
## Guidelines
- This is their space. Don't coach or advise unless asked.
- Reflect back what you hear - help them see their own patterns.
- If they seem stuck, gently prompt: recent events, feelings, goals, relationships, work.
- Keep the tone warm but not saccharine.
- Entries go in `~/org/journal/` as `YYYY-MM-DD.org`.

View File

@@ -1,124 +0,0 @@
---
name: logical-commits
description: Use when the user asks to split current git changes into logical commits, clean up commit history, create atomic commits, or stage by hunk. Review the whole worktree, group related changes, and produce ordered commits where each commit is a valid state (builds/tests pass with the project validation command).
---
# Logical Commits
Turn a mixed worktree into a clean sequence of atomic commits.
## Workflow
1. Inspect the full change set before staging anything.
2. Define commit boundaries by behavior or concern, not by file count.
3. Order commits so dependencies land first (types/api/schema/helpers before consumers).
4. Stage only the exact hunks for one commit.
5. Validate that staged commit state is healthy before committing.
6. Commit with a precise message.
7. Repeat until all intended changes are committed.
## 1) Inspect First
Run:
```bash
git status --short
git diff --stat
git diff
```
If there are staged changes already, inspect both views:
```bash
git diff --staged
git diff
```
## 2) Choose Validation Command Early
Select the fastest command that proves the repo is valid for this project. Prefer project-standard commands (for example: `just test`, `npm test`, `cargo test`, `go test ./...`, `nix flake check`, targeted build commands).
If no clear command exists:
1. Infer the best available command from repo scripts/config.
2. Tell the user what command you chose and why.
3. Do not claim full validation if coverage is partial.
## 3) Plan the Commit Stack
Before committing, write a short plan:
1. Commit title
2. Files and hunks included
3. Why this is a coherent unit
4. Validation command to run
If changes are intertwined, split by hunk (`git add -p`). If hunk splitting is not enough, use `git add -e` or perform a temporary refactor so each commit remains coherent and valid.
## 4) Stage Exactly One Commit
Preferred staging flow:
```bash
git add -p <file>
git diff --staged
```
Useful corrections:
```bash
git restore --staged -p <file> # unstage specific hunks
git reset -p <file> # alternate unstage flow
```
Never stage unrelated edits just to make the commit pass.
## 5) Validate Before Commit
Run the chosen validation command with the current staged/working tree state.
If validation fails:
1. Fix only what belongs in this logical commit, or
2. Unstage/re-split and revise the commit boundary.
Commit only after validation passes.
## 6) Commit and Verify
Commit:
```bash
git commit -m "<type>: <logical change>"
```
Then confirm:
```bash
git show --stat --oneline -1
```
Ensure remaining unstaged changes still make sense for later commits.
## 7) Final Checks
After finishing the stack:
```bash
git log --oneline --decorate -n <count>
git status
```
Report:
1. The commit sequence created
2. Validation command(s) run per commit
3. Any residual risks (for example, partial validation only)
## Guardrails
1. Keep commits atomic and reviewable.
2. Prefer hunk staging over broad file staging when a file contains multiple concerns.
3. Preserve user changes; do not discard unrelated work.
4. Avoid destructive commands unless the user explicitly requests them.
5. If a clean logical split is impossible without deeper refactor, explain the blocker and ask for direction.

View File

@@ -1,77 +0,0 @@
---
name: nixpkgs-review
description: Review or prepare nixpkgs package changes and PRs using a checklist distilled from review feedback on Ivan Malison's own NixOS/nixpkgs pull requests. Use when working in nixpkgs on package inits, updates, packaging fixes, or before opening or reviewing a nixpkgs PR.
---
# Nixpkgs Review
Use this skill when the task is specifically about reviewing or tightening a change in `NixOS/nixpkgs`.
The goal is not generic style review. The goal is to catch the kinds of issues that repeatedly came up in real nixpkgs feedback on Ivan's PRs: derivation structure, builder choice, metadata, PR hygiene, and JS packaging details.
## Workflow
1. Read the scope first.
Open the changed `package.nix` files, related metadata, and the PR title/body if there is one.
2. Run the historical checklist below.
Bias toward concrete review findings and actionable edits, not abstract style commentary.
3. Validate the package path.
Use the narrowest reasonable validation for the task: targeted build, package eval, or `nixpkgs-review` when appropriate.
4. If you are writing a review:
Lead with findings ordered by severity, include file references, and tie each point to a nixpkgs expectation.
5. If you are preparing a PR:
Fix the checklist items before opening it, then confirm title/body/commit hygiene.
## Historical Checklist
### Derivation structure
- Prefer `finalAttrs` over `rec` for derivations and nested derivations when self-references matter.
- Prefer `tag = "v${...}"` over `rev` when fetching a tagged upstream release.
- Check whether `strictDeps = true;` should be enabled.
- Use the narrowest builder/stdenv that matches the package. If no compiler is needed, consider `stdenvNoCC`.
- Put source modifications in `postPatch` or another appropriate hook, not inside `buildPhase`.
- Prefer `makeBinaryWrapper` over `makeWrapper` when a compiled wrapper is sufficient.
- Keep wrappers aligned with `meta.mainProgram` so overrides remain clean.
- Avoid `with lib;` in package expressions; prefer explicit `lib.*` references.
### Metadata and platform expectations
- For new packages, ensure maintainers are present and include the submitter when appropriate.
- Check whether platform restrictions are justified. Do not mark packages Linux-only or broken without evidence.
- If a package is only workable through patch accumulation and has no maintainer, call that out directly.
### JS, Bun, Electron, and wrapper-heavy packages
- Separate runtime deps from build-only deps. Large closures attract review attention.
- Remove redundant env vars and duplicated configuration if build hooks already cover them.
- Check bundled tool/runtime version alignment, especially browser/runtime pairs.
- Install completions, desktop files, or icons when upstream clearly ships them and the package already exposes the feature.
- Be careful with wrappers that hardcode env vars users may want to override.
### PR hygiene
- PR title should match nixpkgs naming and the package version.
- Keep the PR template intact unless there is a strong reason not to.
- Avoid unrelated commits in the PR branch.
- Watch for duplicate or overlapping PRs before investing in deeper review.
- If asked, squash fixup history before merge.
## Review Output
When producing a review, prefer this shape:
- Finding: what is wrong or risky.
- Why it matters in nixpkgs terms.
- Concrete fix, ideally with the exact attr/hook/builder to use.
If there are no findings, say so explicitly and mention remaining validation gaps.
## References
- Read [references/review-patterns.md](references/review-patterns.md) for the curated list of recurring review themes and concrete PR examples.
- Run `scripts/mine_pr_feedback.py --repo NixOS/nixpkgs --author colonelpanic8 --limit 20 --format markdown` to refresh the source material from newer PRs.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "Nixpkgs Review"
short_description: "Review nixpkgs changes with historical guidance"
default_prompt: "Use $nixpkgs-review to review this nixpkgs package change before I open the PR."

View File

@@ -1,105 +0,0 @@
# Nixpkgs Review Patterns
This reference is a curated summary of recurring feedback from Ivan Malison's `NixOS/nixpkgs` PRs. Use it to ground reviews in patterns that have already come up from nixpkgs reviewers.
## Most Repeated Themes
### 1. Prefer `finalAttrs` over `rec`
This came up repeatedly on both package init and update PRs.
- [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230) `playwright-cli`: reviewer asked for `buildNpmPackage (finalAttrs: { ... })` instead of `rec`.
- [PR #490033](https://github.com/NixOS/nixpkgs/pull/490033) `rumno`: same feedback for `rustPlatform.buildRustPackage`.
Practical rule:
- If the derivation self-references `version`, `src`, `pname`, `meta.mainProgram`, or nested outputs, default to `finalAttrs`.
### 2. Prefer `tag` when upstream release is a tag
This also repeated across multiple PRs.
- [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230) `playwright-cli`
- [PR #490033](https://github.com/NixOS/nixpkgs/pull/490033) `rumno`
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) `t3code`
Practical rule:
- If upstream publishes a named release tag, prefer `tag = "v${finalAttrs.version}";` or the exact tag format instead of a raw `rev`.
### 3. Use the right hook and builder
Reviewers often push on hook placement and builder/stdenv choice.
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) `t3code`: feedback to move work from `buildPhase` into `postPatch`.
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) `t3code`: feedback to consider `stdenvNoCC`.
- [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230) `playwright-cli`: prefer `makeBinaryWrapper` for a simple wrapper.
Practical rule:
- Check whether each mutation belongs in `postPatch`, `preConfigure`, `buildPhase`, or `installPhase`.
- Check whether the package genuinely needs a compiler toolchain.
- For simple env/arg wrappers, prefer `makeBinaryWrapper`.
### 4. Enable `strictDeps` unless there is a reason not to
This was called out explicitly on [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465).
Practical rule:
- For new derivations, ask whether `strictDeps = true;` should be present.
- If not, be ready to justify why the builder or package layout makes it unnecessary.
### 5. Keep metadata explicit and override-friendly
- [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230) `playwright-cli`: reviewer asked to avoid `with lib;`.
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) `t3code`: reviewer suggested deriving wrapper executable name from `finalAttrs.meta.mainProgram`.
Practical rule:
- Prefer `lib.licenses.mit` over `with lib;`.
- Keep `meta.mainProgram` authoritative and have wrappers follow it when practical.
### 6. Maintainers matter for new packages
- [PR #496806](https://github.com/NixOS/nixpkgs/pull/496806) `gws`: reviewer would not merge until the submitter appeared in maintainers.
Practical rule:
- For package inits, check maintainers early rather than waiting for review feedback.
### 7. PR title and template hygiene are review targets
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) `t3code`: asked to fix the PR title to match the version.
- [PR #490033](https://github.com/NixOS/nixpkgs/pull/490033) `rumno`: reviewer asked what happened to the PR template.
Practical rule:
- Before opening or updating a PR, verify the title, template, and branch scope.
### 8. Duplicate or overlapping PRs get noticed quickly
- [PR #490227](https://github.com/NixOS/nixpkgs/pull/490227) was replaced by [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230).
- [PR #490053](https://github.com/NixOS/nixpkgs/pull/490053) overlapped with [PR #490033](https://github.com/NixOS/nixpkgs/pull/490033).
- [PR #488606](https://github.com/NixOS/nixpkgs/pull/488606), [PR #488602](https://github.com/NixOS/nixpkgs/pull/488602), and [PR #488603](https://github.com/NixOS/nixpkgs/pull/488603) were closed after reviewers pointed to existing work.
Practical rule:
- Search for existing PRs on the package before spending time polishing a review.
- If a branch contains unrelated commits, fix that before asking for review.
### 9. JS/Bun/Electron packages draw runtime-layout scrutiny
This came up heavily on `t3code` and `playwright-cli`.
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) `t3code`: reviewers proposed trimming the runtime closure, removing unnecessary env vars, and adding shell completions and desktop integration.
- [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230) `playwright-cli`: reviewers called out mismatched bundled `playwright-core` and browser binaries, and wrapper behavior that prevented user overrides.
Practical rule:
- For JS-heavy packages, inspect closure size, runtime vs build-only deps, wrapper env vars, and version alignment between bundled libraries and external binaries.
### 10. Cross-platform evidence helps
- [PR #490230](https://github.com/NixOS/nixpkgs/pull/490230) received an approval explicitly noting Darwin success.
- [PR #497465](https://github.com/NixOS/nixpkgs/pull/497465) got feedback questioning platform restrictions and build behavior.
Practical rule:
- If the package plausibly supports Darwin, avoid premature Linux-only restrictions and mention what was or was not tested.
## How To Use This Reference
- Use these patterns as a focused checklist before submitting or reviewing nixpkgs changes.
- Do not blindly apply every point. Check whether the builder, language ecosystem, and upstream release model actually match.
- When in doubt, prefer concrete evidence from the current package diff over generic convention.

View File

@@ -1,2 +0,0 @@
__pycache__/
*.pyc

View File

@@ -1,208 +0,0 @@
#!/usr/bin/env python3
"""
Mine external feedback from recent GitHub PRs.
Examples:
python scripts/mine_pr_feedback.py --repo NixOS/nixpkgs --author colonelpanic8
python scripts/mine_pr_feedback.py --repo NixOS/nixpkgs --author colonelpanic8 --limit 30 --format json
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
def run(cmd: list[str]) -> str:
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or f"command failed: {' '.join(cmd)}")
return proc.stdout
def gh_json(args: list[str]) -> object:
return json.loads(run(["gh", *args]))
def fetch_prs(repo: str, author: str, limit: int) -> list[dict]:
prs: dict[int, dict] = {}
for state in ("open", "closed"):
data = gh_json(
[
"search",
"prs",
"--repo",
repo,
"--author",
author,
"--limit",
str(max(limit, 30)),
"--state",
state,
"--json",
"number,title,state,closedAt,updatedAt,url",
]
)
for pr in data:
prs[pr["number"]] = pr
return sorted(
prs.values(),
key=lambda pr: (pr["updatedAt"], pr["number"]),
reverse=True,
)[:limit]
def fetch_feedback(repo: str, author: str, pr: dict) -> dict:
owner, name = repo.split("/", 1)
number = pr["number"]
def api(path: str) -> list[dict]:
return gh_json(["api", f"repos/{owner}/{name}/{path}", "--paginate"])
issue_comments = api(f"issues/{number}/comments")
review_comments = api(f"pulls/{number}/comments")
reviews = api(f"pulls/{number}/reviews")
comments = []
for comment in issue_comments:
login = comment["user"]["login"]
body = (comment.get("body") or "").strip()
if login != author and body:
comments.append({"kind": "issue", "user": login, "body": body})
for comment in review_comments:
login = comment["user"]["login"]
body = (comment.get("body") or "").strip()
if login != author and body:
comments.append(
{
"kind": "review_comment",
"user": login,
"body": body,
"path": comment.get("path"),
"line": comment.get("line"),
}
)
for review in reviews:
login = review["user"]["login"]
body = (review.get("body") or "").strip()
if login != author and body:
comments.append(
{
"kind": "review",
"user": login,
"body": body,
"state": review.get("state"),
}
)
return {**pr, "comments": comments}
def is_bot(login: str) -> bool:
return login.endswith("[bot]") or login in {"github-actions", "app/dependabot"}
def render_markdown(results: list[dict], include_bots: bool) -> str:
commenters = Counter()
kept = []
for pr in results:
comments = [
comment
for comment in pr["comments"]
if include_bots or not is_bot(comment["user"])
]
if comments:
kept.append({**pr, "comments": comments})
commenters.update(comment["user"] for comment in comments)
lines = [
"# PR Feedback Summary",
"",
f"- PRs scanned: {len(results)}",
f"- PRs with external feedback: {len(kept)}",
"",
"## Top commenters",
"",
]
for user, count in commenters.most_common(10):
lines.append(f"- `{user}`: {count}")
for pr in kept:
lines.extend(
[
"",
f"## PR #{pr['number']}: {pr['title']}",
"",
f"- URL: {pr['url']}",
f"- State: {pr['state']}",
"",
]
)
for comment in pr["comments"]:
body = comment["body"].replace("\r", " ").replace("\n", " ").strip()
snippet = body[:280] + ("..." if len(body) > 280 else "")
lines.append(f"- `{comment['user']}` `{comment['kind']}`: {snippet}")
return "\n".join(lines) + "\n"
def main() -> int:
parser = argparse.ArgumentParser(description="Collect review feedback from recent GitHub PRs.")
parser.add_argument("--repo", required=True, help="GitHub repo in owner/name form")
parser.add_argument("--author", required=True, help="PR author to inspect")
parser.add_argument("--limit", type=int, default=20, help="How many recent PRs to inspect")
parser.add_argument(
"--format",
choices=("markdown", "json"),
default="markdown",
help="Output format",
)
parser.add_argument(
"--include-bots",
action="store_true",
help="Keep bot comments in the output",
)
parser.add_argument(
"--workers",
type=int,
default=6,
help="Maximum concurrent GitHub API workers",
)
args = parser.parse_args()
try:
run(["gh", "auth", "status"])
except RuntimeError as err:
print(err, file=sys.stderr)
return 1
prs = fetch_prs(args.repo, args.author, args.limit)
results = []
with ThreadPoolExecutor(max_workers=args.workers) as pool:
futures = [pool.submit(fetch_feedback, args.repo, args.author, pr) for pr in prs]
for future in as_completed(futures):
results.append(future.result())
results.sort(key=lambda pr: (pr["updatedAt"], pr["number"]), reverse=True)
if args.format == "json":
if not args.include_bots:
for pr in results:
pr["comments"] = [
comment for comment in pr["comments"] if not is_bot(comment["user"])
]
json.dump(results, sys.stdout, indent=2)
sys.stdout.write("\n")
else:
sys.stdout.write(render_markdown(results, args.include_bots))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,51 +0,0 @@
---
name: org-agenda-api-production
description: Use when investigating production org-agenda-api state, testing endpoints, or debugging production issues
---
# org-agenda-api Production Access
## Overview
Access the production org-agenda-api instance at https://colonelpanic-org-agenda.fly.dev/ for debugging, testing, or verification.
## Credentials
Get the password from `pass`:
```bash
pass show org-agenda-api/imalison
```
Username is currently `imalison`.
## Quick Access with just
This repo includes a `justfile` under `~/dotfiles/org-agenda-api` with pre-configured commands:
```bash
cd ~/dotfiles/org-agenda-api
just health
just get-all-todos
just get-todays-agenda
just agenda
just agenda-files
just todo-states
just create-todo "Test todo"
```
## Manual curl
Prefer using the `just` recipes above so we don't bake auth syntax into docs.
## Key Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| /health | GET | Health check |
| /version | GET | API version |
| /get-all-todos | GET | All TODO items |
| /agenda | GET | Agenda (span=day\|week) |
| /capture | POST | Create entry |
| /update | POST | Update heading |
| /complete | POST | Complete item |
| /delete | POST | Delete heading |

View File

@@ -1,312 +0,0 @@
---
name: org-agenda-api
description: Use when interacting with the org-agenda-api HTTP server to read/write org-mode agenda data
---
# Org Agenda API Reference
HTTP API for org-mode agenda data. Use this skill when you need to query or modify org agenda entries programmatically.
## Authentication
Get credentials from pass:
```bash
pass show colonelpanic-org-agenda.fly.dev
```
Returns: password on first line, then `user:` and `url:` fields.
**Note:** The `url` field in pass may be outdated. Use the base URL below.
## Base URL
`https://colonelpanic-org-agenda.fly.dev`
All requests use Basic Auth with the credentials from pass.
## Read Endpoints
### GET /agenda
Get agenda entries for a day or week.
Query params:
- `span`: `day` (default) or `week`
- `date`: `YYYY-MM-DD` (default: today)
- `include_overdue`: `true` to include overdue items from previous days
- `include_completed`: `true` to include items completed on the queried date
- `refresh`: `true` to git pull repos first
Response includes `span`, `date`, `entries` array, and optionally `gitRefresh` results.
### GET /get-all-todos
Get all TODO items from agenda files.
Query params:
- `refresh`: `true` to git pull first
Response includes `defaults` (with `notifyBefore`), `todos` array, and optionally `gitRefresh`.
### GET /metadata
Get all app metadata in a single request. Returns:
- `templates`: capture templates
- `filterOptions`: tags, categories, priorities, todoStates
- `todoStates`: active and done states
- `customViews`: available custom agenda views
- `errors`: any errors encountered fetching above
### GET /todo-states
Get configured TODO states. Returns:
- `active`: array of not-done states (TODO, NEXT, etc.)
- `done`: array of done states (DONE, CANCELLED, etc.)
### GET /filter-options
Get available filter options. Returns:
- `todoStates`: all states
- `priorities`: available priorities (A, B, C)
- `tags`: all tags from agenda files
- `categories`: all categories
### GET /custom-views
List available custom agenda views. Returns array of `{key, name}` objects.
### GET /custom-view
Run a custom agenda view.
Query params:
- `key` (required): custom agenda command key
- `refresh`: `true` to git pull first
### GET /agenda-files
Get list of org-agenda-files with existence and readability status.
### GET /capture-templates (alias: /templates)
List available capture templates with their prompts.
### GET /health
Health check. Returns `status`, `uptime`, `requests`, and `captureStatus` if unhealthy.
### GET /version
Version info. Returns `version` and `gitCommit`.
### GET /debug-config
Current org configuration for debugging.
## Write Endpoints
### POST /capture
Create a new entry using a capture template.
**Important:** Use `capture-g` (GTD Todo) for most tasks - it properly records creation time and logbook history. Only use `default` when you specifically don't want GTD tracking.
Body:
```json
{
"template": "capture-g",
"values": {
"Title": "Task title",
"scheduled": "2026-01-20",
"deadline": "2026-01-25",
"priority": "A",
"tags": ["work", "urgent"],
"todo": "TODO"
}
}
```
### POST /complete
Mark a TODO as complete.
Body (use any combination to identify the item):
```json
{
"id": "org-id-if-available",
"file": "/path/to/file.org",
"pos": 12345,
"title": "Task title",
"state": "DONE"
}
```
Lookup order: id -> file+pos+title -> file+title -> title only
### POST /update
Update a TODO's scheduled date, deadline, priority, tags, or properties.
Body:
```json
{
"id": "org-id",
"file": "/path/to/file.org",
"pos": 12345,
"title": "Task title",
"scheduled": "2026-01-20T10:00:00",
"deadline": "2026-01-25",
"priority": "B",
"tags": ["updated", "tags"],
"properties": {
"CUSTOM_PROP": "value"
}
}
```
Set value to `null` or empty string to clear. Response includes new `pos` for cache updates.
### POST /delete
Delete an org item permanently.
Body:
```json
{
"id": "org-id",
"file": "/path/to/file.org",
"position": 12345,
"include_children": true
}
```
Requires `include_children: true` if item has children, otherwise returns error.
### POST /restart
Restart the Emacs server (exits gracefully, supervisord restarts).
## Category Strategy Endpoints
These require org-category-capture to be configured.
### GET /category-types
List registered category strategy types. Returns array with:
- `name`: strategy type name
- `hasCategories`: boolean
- `captureTemplate`: template string
- `prompts`: array of prompt definitions
### GET /categories
Get categories for a strategy type.
Query params:
- `type` (required): strategy type name (e.g., "projects")
- `existing_only`: `true` to only return categories with capture locations
Returns `type`, `categories` array, `todoFiles` array.
### GET /category-tasks
Get tasks for a specific category.
Query params:
- `type` (required): strategy type name
- `category` (required): category name
### POST /category-capture
Capture a new entry to a category.
Body:
```json
{
"type": "projects",
"category": "my-project",
"title": "Task title",
"todo": "TODO",
"scheduled": "2026-01-20",
"deadline": "2026-01-25",
"priority": "A",
"tags": ["work"],
"properties": {"EFFORT": "1h"}
}
```
## Response Format
Agenda/todo entries include:
- `todo`: TODO state (TODO, NEXT, DONE, etc.)
- `title`: Heading text
- `scheduled`: ISO date or datetime
- `deadline`: ISO date or datetime
- `priority`: A, B, or C (only if explicitly set)
- `tags`: Array of tags
- `file`: Source file path
- `pos`: Position in file (may change after edits)
- `id`: Org ID if set (stable identifier)
- `olpath`: Outline path array
- `level`: Heading level
- `category`: Category of the item
- `properties`: All properties from the property drawer
- `completedAt`: ISO timestamp when completed (if applicable)
- `agendaLine`: Raw agenda display text (agenda endpoint only)
- `notifyBefore`: Array of minutes for notifications
- `isWindowHabit`: Boolean for window habits
- `habitSummary`: Summary object for habits (if applicable)
## Common Workflows
**View today's agenda:**
```bash
curl -s -u "$USER:$PASS" "$URL/agenda?span=day" | jq '.entries[] | {todo, title, scheduled}'
```
**View this week:**
```bash
curl -s -u "$USER:$PASS" "$URL/agenda?span=week" | jq .
```
**View completed tasks for a specific date:**
```bash
curl -s -u "$USER:$PASS" "$URL/agenda?date=2026-01-17&include_completed=true" | jq '.entries[] | select(.completedAt != null) | {title, completedAt}'
```
**Get all metadata at once:**
```bash
curl -s -u "$USER:$PASS" "$URL/metadata" | jq .
```
**Create a task:**
```bash
curl -s -u "$USER:$PASS" -X POST "$URL/capture" \
-H "Content-Type: application/json" \
-d '{"template":"capture-g","values":{"Title":"New task","scheduled":"2026-01-20"}}'
```
**Complete a task by title:**
```bash
curl -s -u "$USER:$PASS" -X POST "$URL/complete" \
-H "Content-Type: application/json" \
-d '{"title":"Task title"}'
```
**Update a task's schedule:**
```bash
curl -s -u "$USER:$PASS" -X POST "$URL/update" \
-H "Content-Type: application/json" \
-d '{"title":"Task title","scheduled":"2026-01-21T14:00:00"}'
```
**Clear a deadline:**
```bash
curl -s -u "$USER:$PASS" -X POST "$URL/update" \
-H "Content-Type: application/json" \
-d '{"title":"Task title","deadline":null}'
```
**Delete a task:**
```bash
curl -s -u "$USER:$PASS" -X POST "$URL/delete" \
-H "Content-Type: application/json" \
-d '{"title":"Task to delete","file":"/path/to/file.org","position":12345}'
```
## Error Handling
All endpoints return JSON. Errors include:
```json
{
"status": "error",
"message": "Error description"
}
```
Success responses include:
```json
{
"status": "created" | "completed" | "updated",
...additional fields
}
```

View File

@@ -1,122 +0,0 @@
---
name: password-reset
description: Use when the user wants to reset or rotate a website or service password end-to-end, including finding the right `pass` entry, generating a new password with `xkcdpassgen`, retrieving reset emails through `gws gmail` or a local mail CLI, completing the reset in the browser with Chrome DevTools MCP, and updating the password store safely without losing entry metadata.
---
# Password Reset
## Overview
Handle password resets end-to-end. Prefer `gws gmail` for reset-email retrieval, Chrome DevTools MCP for website interaction, and the local `xkcdpassgen` helper for password generation.
## Tool Priorities
- Prefer `gws gmail` over opening Gmail in the browser.
- If `gws` is unavailable, use an installed Gmail CLI or IMAP-based mail tool if one exists locally. Inspect the environment first instead of guessing command names.
- Prefer Chrome DevTools MCP for all browser interaction.
- Use `pass find` and `pass show` before asking the user for credentials or account details.
## Password Generation
The local password generator is `xkcdpassgen`, defined in `dotfiles/lib/functions/xkcdpassgen` and available in shell as an autoloaded function.
```bash
xkcdpassgen <pass-entry-name>
```
Behavior:
- Generates `xkcdpass -n 3 | tr -d ' '` as the base password.
- Appends one uppercase letter, one digit, and one symbol by default.
- Supports:
- `-U` to omit uppercase
- `-N` to omit number
- `-S` to omit symbol
Do not substitute a different password generator ungless the user explicitly asks.
## Safe `pass` Update Pattern
`xkcdpassgen` writes directly to the `pass` entry it is given. Do not run it against the canonical entry before the reset succeeds, because:
- it would overwrite the current password immediately
- it would replace any extra metadata lines in a multiline `pass` entry
Use this pattern instead:
```bash
entry="service/example"
tmp_entry="${entry}-password-reset-tmp"
existing_contents="$(pass show "$entry" 2>/dev/null || true)"
metadata="$(printf '%s\n' "$existing_contents" | tail -n +2)"
xkcdpassgen "$tmp_entry"
new_password="$(pass show "$tmp_entry" | head -1)"
# ... use $new_password in the reset flow ...
if [ -n "$metadata" ]; then
printf '%s\n%s\n' "$new_password" "$metadata" | pass insert -m -f "$entry"
else
printf '%s\n' "$new_password" | pass insert -m -f "$entry"
fi
pass rm -f "$tmp_entry"
```
If the site rejects the password because of policy constraints, keep the canonical entry unchanged, delete or reuse the temp entry, and generate another candidate with different flags only if needed.
## Reset Workflow
1. Identify the account and canonical `pass` entry.
2. Run `pass find <service>` and inspect likely matches with `pass show`.
3. Capture existing metadata before generating a new password.
4. Generate the candidate password into a temporary `pass` entry with `xkcdpassgen`.
5. Start the reset flow in Chrome DevTools MCP:
- navigate to the login or account page
- use the site's "forgot password" flow, or
- sign in and navigate to security settings if the user asked for a rotation rather than a reset
6. Use `gws gmail` to retrieve the reset email when needed:
- search recent mail by sender domain, subject, or reset-related keywords
- open the message and extract the reset link
- navigate to that link in Chrome DevTools MCP
7. Fill the new password from the temporary `pass` entry and complete the form.
8. Verify success:
- confirmation page, or
- successful login with the new password
9. Promote the temp password into the canonical `pass` entry while preserving metadata, then remove the temp entry.
## Email Guidance
Prefer `gws gmail` for reset-email handling. Typical pattern:
- list recent messages with `gws gmail users messages list --params '{"userId":"me","q":"from:service.example newer_than:7d"}'`
- bias toward reset keywords such as `reset`, `password`, `security`, `verify`, or `signin`
- read shortlisted messages with `gws gmail users messages get --params '{"userId":"me","id":"MESSAGE_ID","format":"full"}'` rather than browsing Gmail manually
If `gws` is unavailable, use an installed Gmail CLI or local mail helper only as a fallback. Keep that discovery lightweight and local to the current environment.
## Browser Guidance
Use Chrome DevTools MCP to complete the reset flow directly:
- navigate to the reset or security page
- take snapshots to identify the relevant inputs and buttons
- click, fill, and submit through the site UI
- verify the success state before updating the canonical `pass` entry
Prefer MCP interaction over describing steps for the user to perform manually.
## Credentials And Account Data
- Search `pass` before asking the user for usernames, recovery emails, or OTP-related entries.
- Preserve existing metadata lines in multiline `pass` entries whenever possible.
- Never print the new password in the final response unless the user explicitly asks for it.
## Failure Handling
- If account discovery is ambiguous, ask a short clarifying question only after checking `pass`.
- If the reset email does not arrive, search spam or alternate senders before giving up.
- If login or reset requires another secret that is not in `pass`, then ask the user.
- If the reset flow fails after temp-password generation, leave the canonical entry untouched.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "Password Reset"
short_description: "Reset passwords and update pass safely"
default_prompt: "Use $password-reset to reset this account password, complete the browser flow, and update pass safely."

View File

@@ -1,402 +0,0 @@
---
name: planning-coaching
description: Use when helping with daily planning, task prioritization, reviewing agenda, or when user seems stuck on what to do next
---
# Planning Coaching
Help Ivan with planning through question-driven coaching, honest feedback, and data-informed accountability.
## Persistent Files
**IMPORTANT:** Always read these at the start of planning sessions.
### Context File: `/home/imalison/org/planning/context.org`
Persistent context about Ivan's life, goals, struggles, and current focus. Claude maintains this file - update it when:
- Goals or priorities shift
- New patterns emerge
- Life circumstances change
- We learn something about what helps/doesn't help
Read this first. It's the "state of Ivan" that persists across sessions.
### Daily Journals: `/home/imalison/org/planning/dailies/YYYY-MM-DD.org`
One file per day we do planning. Contains:
- That day's plan (short list, focus areas)
- Stats table from the previous day review (inline)
- Notes from the session
- End-of-day reflection (if we do one)
Create a new file for each planning session day. Reference past dailies to see patterns.
### Stats File: `/home/imalison/org/planning/stats.org`
Running tables for trend analysis:
- **Daily Log**: One row per planning day with all metrics
- **Weekly Summary**: Aggregated weekly totals with notes
### Raw Logs: `/home/imalison/org/planning/logs.jsonl`
Detailed machine-readable log (one JSON object per line, per day). Captures full task data so we can calculate new metrics retroactively.
Each line contains:
```json
{
"date": "2026-01-20",
"planned": [{"title": "...", "friction": 3, "effort": 2, "id": "...", "file": "...", ...}],
"completed": [{"title": "...", "friction": 3, "effort": 2, "completedAt": "...", ...}],
"rescheduled": [{"title": "...", "from": "2026-01-20", "to": "2026-01-21", ...}],
"context": {"energy": "medium", "available_time": "full day", "notes": "..."}
}
```
When recording stats:
1. Append full JSON object to logs.jsonl
2. Add summary row to stats.org Daily Log table
3. Include inline stats table in that day's journal
4. Update Weekly Summary when a week ends
## Core Principles
1. **Question-driven**: Ask questions to help think through priorities rather than dictating
2. **Direct and honest**: Call out avoidance patterns directly - this is wanted
3. **Data-informed**: Use org-agenda-api to look at patterns, velocity, scheduling history
4. **Balance pressure**: Push on procrastination but don't overwhelm on decision-heavy tasks
5. **Lightweight and flexible**: Always offer option to skip parts if not feeling it
6. **No guilt**: If we fall off the wagon, make it easy and encouraging to get back on
## Planning Session Flow
```dot
digraph planning_session {
rankdir=TB;
"Read context.org" [shape=box];
"Yesterday review (skippable)" [shape=box];
"Capture new items" [shape=box];
"Check current state" [shape=box];
"Inbox processing (skippable)" [shape=box];
"Pick focus areas" [shape=box];
"Create short list" [shape=box];
"Meta check (optional)" [shape=box];
"Write daily journal" [shape=box];
"Read context.org" -> "Yesterday review (skippable)";
"Yesterday review (skippable)" -> "Capture new items";
"Capture new items" -> "Check current state";
"Check current state" -> "Inbox processing (skippable)";
"Inbox processing (skippable)" -> "Pick focus areas";
"Pick focus areas" -> "Create short list";
"Create short list" -> "Meta check (optional)";
"Meta check (optional)" -> "Write daily journal";
}
```
Every step marked "skippable" - offer it, but accept "let's skip that today" without question.
### 0. Read Context (Always)
Read `/home/imalison/org/planning/context.org` first. This grounds the session in what's currently going on.
### 1. Yesterday Review (Skippable)
Quick look back at the previous day. Keep it lightweight - a minute or two, not an interrogation.
**Subjective check-in:**
- "How do you feel about yesterday?" (open-ended, not demanding)
- "Anything you want to talk about - productivity or otherwise?"
**Objective stats (if wanted):**
- Completion rate: X of Y planned tasks done
- Friction conquered: total/average friction of completed tasks
- Rescheduled: N tasks bumped to today
- Effort accuracy: any tasks that took way more/less than estimated?
**Keep it encouraging:**
- Celebrate wins, especially high-friction completions
- If it was a rough day, acknowledge it without judgment
- "Yesterday was yesterday. What do we want today to look like?"
**If we haven't done this in a while:**
- "Hey, we haven't done a planning session in [X days]. No big deal - want to ease back in?"
- Don't guilt trip. Just pick up where we are.
### 2. Capture New Items
Before diving into today's state, ask: "Anything new come up that needs to be captured?"
- New tasks, ideas, commitments that surfaced since last session
- Things remembered overnight or during the day
- Add these to org before continuing
**Which capture command to use:**
- `just inbox "Task title"` - Default for new todos. Quick capture without setting properties. Items go to inbox for later triage (setting effort, friction, priority, category).
- `just capture "Task title"` - Only when we're setting effort, friction, priority, or category upfront during the planning session.
This prevents things from falling through the cracks and clears mental load before planning.
### 3. Check Current State
Ask about:
- Energy level right now (low/medium/high)
- Time available and structure of the day
- Any hard deadlines or commitments
- Mental state (scattered? focused? anxious?)
### 4. Inbox Processing (Skippable)
Process items captured to inbox since last session. These are quick captures (`just inbox`) that need triage.
**For each inbox item, decide:**
1. Is this actually actionable? (If not: delete, or convert to reference/someday)
2. Assign FRICTION and EFFORT estimates
3. Set priority if obvious
4. Schedule if it has a natural date, otherwise leave unscheduled for later prioritization
5. **IMPORTANT: Transition state from INBOX to NEXT** using `just set-state "Task title" "NEXT"`
**Process for property assignment:**
1. Both of us estimate FRICTION and EFFORT
2. Use Ivan's values unless we differ by 2+ points
3. If discrepancy >= 2, discuss: "I estimated this as [X] because [reason] - what makes you see it as [Y]?"
**Why this matters:** Items sitting in inbox create mental overhead. Regular processing keeps the system trustworthy.
### 5. Pick Focus Areas
Based on energy and context, choose what *types* of work to tackle:
- High friction tasks (if energy supports it)
- Quick wins (if need momentum)
- Deep work (if have focus time)
- Admin/shallow work (if low energy)
### 6. Create Short List
Curate 3-5 tasks that match the day's reality. Not a full dump - a focused list.
### 7. Meta Check (Optional)
Occasionally (weekly-ish, or when it feels right), ask:
- "Is this planning process working for you?"
- "Anything we should change about how we do this?"
- "Are the FRICTION/EFFORT scales making sense?"
This is how we iterate on the system itself.
## Task Properties
Store in org properties drawer via `just update` with a `properties` field in the JSON body.
### FRICTION (0-5)
Psychological resistance / avoidance tendency / decision paralysis factor.
| Value | Meaning |
|-------|---------|
| 0 | No friction - could start right now |
| 1 | Minimal - minor reluctance |
| 2 | Some - need to push a bit |
| 3 | Moderate - will procrastinate without intention |
| 4 | High - significant avoidance |
| 5 | Maximum - dread/paralysis |
### EFFORT (Fibonacci: 1, 2, 3, 5, 8)
Time/energy investment. Store as number, discuss as t-shirt size.
| Number | T-shirt | Meaning |
|--------|---------|---------|
| 1 | XS | Trivial, <30min |
| 2 | S | Small, ~1-2h |
| 3 | M | Medium, half-day |
| 5 | L | Large, full day |
| 8 | XL | Multi-day effort |
### Setting Properties
```bash
just update '{"title": "Task name", "properties": {"FRICTION": "3", "EFFORT": "5"}}'
```
## Priority Framework
When helping decide what to work on, weigh these factors:
1. **Energy/context match**: Does current energy support this task's friction level?
2. **Deadlines**: What's due soon or has external pressure?
3. **Impact**: What moves the needle most?
High-friction + high-impact tasks need the right conditions. Don't push these when energy is low.
## Handling Avoidance
**Be direct.** Ivan wants honest feedback.
When noticing avoidance patterns:
- "You've rescheduled X three times now. What's making this hard?"
- "This has been on your list for two weeks. Let's talk about what's blocking it."
- "I notice you keep picking small tasks over [big important thing]. What would make that more approachable?"
**Use data:**
- Look at scheduling history via `just agenda-day YYYY-MM-DD`
- Track how long tasks have been scheduled
- Notice patterns in what gets done vs. avoided
## Coaching Stance
**Do:**
- Ask "what's making this hard?" not "why haven't you done this?"
- Offer to break down high-friction tasks into smaller steps
- Notice and celebrate progress, especially on hard things
- Be honest about patterns you see
**Don't:**
- Overwhelm with too many decisions at once
- Push high-friction tasks when energy is clearly low
- Judge - observe and inquire instead
- Let things slide without comment (directness is wanted)
## Red Flags to Watch For
- Same task rescheduled 3+ times
- Consistently avoiding a category of work
- Taking on new commitments while existing ones slip
- Only doing low-friction tasks day after day
- Overcommitting (too many items scheduled for one day)
When you see these: name it directly and explore what's going on.
## Mid-Day Check-ins
These can happen impromptu - not every day, just when useful.
**When to offer:**
- If morning plan isn't working out
- Energy shifted significantly
- Got stuck or derailed
- Finished the short list early
**Keep it brief:**
- "How's it going with [today's focus]?"
- "Want to adjust the plan for the afternoon?"
- "Anything blocking you right now?"
## Metrics We Track
For the daily review, pull these from the API:
| Metric | How to calculate | Why it matters |
|--------|------------------|----------------|
| Completion rate | completed / planned for day | Overall follow-through |
| Friction conquered | sum of FRICTION on completed tasks | Are we tackling hard things? |
| Rescheduling count | tasks that moved from yesterday to today | Chronic rescheduling = avoidance |
| Effort accuracy | compare EFFORT estimate vs actual | Calibrate future estimates |
**Don't obsess over numbers.** They're conversation starters, not report cards.
## Queries for Planning
Use the `just` commands in `/home/imalison/org/justfile` for all API interactions.
**Tasks needing property assignment:**
```bash
just todos # Get all todos, filter for missing FRICTION or EFFORT in properties
```
**Today's agenda (including overdue):**
```bash
just agenda-overdue # Use this for planning - shows today + all overdue items
just agenda # Only today's scheduled items (misses overdue tasks)
```
**Note:** Always use `agenda-overdue` during planning sessions to see the full picture of what needs attention.
**Agenda for specific date:**
```bash
just agenda-day 2026-01-20
```
**Completed items for a specific date:**
```bash
just completed 2026-01-22 # Get items completed on a specific date
just completed-today # Get items completed today
```
**This week's agenda:**
```bash
just agenda-week
```
**Overdue/rescheduled items:**
```bash
just agenda-overdue
```
**Capture new items:**
```bash
just inbox "New task title" # Quick capture to inbox (default)
just capture "Task title" "2026-01-22" # With scheduling
```
**Update task properties:**
```bash
just update '{"title": "Task name", "properties": {"FRICTION": "3", "EFFORT": "5"}}'
```
**Reschedule a task:**
```bash
just reschedule "Task title" "2026-01-25"
```
**Complete a task:**
```bash
just complete "Task title"
```
**Change task state (e.g., INBOX -> NEXT):**
```bash
just set-state "Task title" "NEXT"
```
## Daily Journal Template
Create `/home/imalison/org/planning/dailies/YYYY-MM-DD.org` for each session:
```org
#+TITLE: Planning - YYYY-MM-DD
#+DATE: [YYYY-MM-DD Day]
* Yesterday Review
** Stats
| Metric | Value |
|-------------+-------|
| Planned | N |
| Completed | N |
| Rate | N% |
| Friction | N |
| Rescheduled | N |
** Reflection
[How Ivan felt about yesterday, anything discussed]
* Today's Context
- Energy: [low/medium/high]
- Available time: [description]
- Mental state: [notes]
* Focus Areas
- [What types of work we're tackling today]
* Today's Short List
Use org ID links to reference tasks - don't duplicate task definitions here.
- [[id:uuid-here][Task 1 title]]
- [[id:uuid-here][Task 2 title]]
- [[id:uuid-here][Task 3 title]]
* Notes
[Anything else from the session]
* End of Day (optional)
[If we do an evening check-in]
```
**Also add row to** `/home/imalison/org/planning/stats.org` Daily Log table.
## Updating Context File
Update `/home/imalison/org/planning/context.org` when:
- Ivan mentions a new goal or project
- We notice a recurring pattern
- Something significant changes in life/work
- We discover what helps or doesn't help
- The meta check reveals process adjustments
Don't ask permission to update it - just do it and mention what changed.

View File

@@ -1,47 +0,0 @@
---
name: playwright-cli
description: Automate browser interactions from the shell using Playwright via the `playwright-cli` command (open/goto/snapshot/click/type/screenshot, tabs/storage/network). Use when you need deterministic browser automation for web testing, form filling, screenshots/PDFs, or data extraction.
---
# Browser Automation With playwright-cli
This system provides `playwright-cli` via Nix (see `nixos/flake.nix` for the nixpkgs PR patch and `nixos/code.nix` for installation), so its available on `PATH` without any `npm -g` installs.
## Quick Start
```bash
# First run (downloads browser bits used by Playwright)
playwright-cli install-browser
# Open a new browser session (optionally with a URL)
playwright-cli open
playwright-cli open https://example.com/
# Navigate, inspect, and interact
playwright-cli goto https://playwright.dev
playwright-cli snapshot
playwright-cli click e15
playwright-cli type "search query"
playwright-cli press Enter
# Save artifacts
playwright-cli screenshot --filename=page.png
playwright-cli pdf --filename=page.pdf
# Close the browser
playwright-cli close
```
## Practical Workflow
1. `playwright-cli open` (or `open <url>`)
2. `playwright-cli snapshot`
3. Use element refs (`e1`, `e2`, ...) from the snapshot with `click`, `fill`, `hover`, `check`, etc.
4. Take `screenshot`/`pdf` as needed
5. `playwright-cli close`
## Tips
- Use `playwright-cli state-save auth.json` / `state-load auth.json` to persist login state across runs.
- Use named sessions with `-s=mysession` when you need multiple concurrent browsers.
- Set `PLAYWRIGHT_CLI_PACKAGE` to pin the npm package (default is `@playwright/cli@latest`).

View File

@@ -1,5 +0,0 @@
interface:
display_name: "Playwright CLI"
short_description: "Automate browser interactions"
default_prompt: "Use playwright-cli to automate browser actions (open/goto/snapshot/click/type/screenshot) and save useful artifacts (screenshots, PDFs, auth state)."

View File

@@ -1,54 +0,0 @@
---
name: release
description: Use when user asks to release, publish, bump version, or prepare a new version for deployment
---
# Release
Validate, format, bump version, and tag for release.
## Workflow
1. **Validate** - Run project's validation command
2. **Fix formatting** - Auto-fix prettier/formatting issues if any
3. **Bump version** - Ask user for bump type, update package.json
4. **Commit & tag** - Commit version bump, create git tag
5. **Optionally push** - Ask if user wants to push
## Commands
```bash
# 1. Validate
yarn validate # or: npm run validate
# 2. Fix formatting if needed
yarn prettier:fix # or: npm run prettier:fix
# 3. Bump version (edit package.json)
# patch: 1.2.3 → 1.2.4
# minor: 1.2.3 → 1.3.0
# major: 1.2.3 → 2.0.0
# 4. Commit and tag
git add package.json
git commit -m "chore: bump version to X.Y.Z"
git tag vX.Y.Z
# 5. Push (if requested)
git push && git push --tags
```
## Quick Reference
| Bump Type | When to Use |
|-----------|-------------|
| patch | Bug fixes, small changes |
| minor | New features, backwards compatible |
| major | Breaking changes |
## Before Release Checklist
- [ ] All tests pass
- [ ] No lint errors
- [ ] Formatting is clean
- [ ] Changes are committed

View File

@@ -1,86 +0,0 @@
---
name: taffybar-ecosystem-release
description: Use when releasing, version-bumping, or propagating changes across taffybar GitHub org packages (taffybar, gtk-sni-tray, gtk-strut, status-notifier-item, dbus-menu, dbus-hslogger)
---
# Taffybar Ecosystem Release
Release and propagate changes across the taffybar Haskell package ecosystem.
See also: `taffybar-nixos-flake-chain` for how these packages are consumed by the NixOS configuration and what flake.lock updates may be needed after a release.
## Package Dependency Graph
```
taffybar
├── gtk-sni-tray
│ ├── dbus-menu
│ ├── gtk-strut
│ └── status-notifier-item
├── dbus-menu
├── gtk-strut
├── status-notifier-item
└── dbus-hslogger
```
**Leaf packages** (no ecosystem deps): `gtk-strut`, `status-notifier-item`, `dbus-hslogger`, `dbus-menu`
**Mid-level**: `gtk-sni-tray` (depends on dbus-menu, gtk-strut, status-notifier-item)
**Top-level**: `taffybar` (depends on all above)
## Repositories & Local Checkouts
| Package | GitHub | Local Checkout |
|---------|--------|---------------|
| taffybar | taffybar/taffybar | `~/.config/taffybar/taffybar/` |
| gtk-sni-tray | taffybar/gtk-sni-tray | `~/Projects/gtk-sni-tray/` |
| gtk-strut | taffybar/gtk-strut | `~/Projects/gtk-strut/` |
| status-notifier-item | taffybar/status-notifier-item | `~/Projects/status-notifier-item/` |
| dbus-menu | taffybar/dbus-menu | `~/Projects/dbus-menu/` |
| dbus-hslogger | IvanMalison/dbus-hslogger | `~/Projects/dbus-hslogger/` |
## Releasing a Package
Always release leaf packages before their dependents. Changes propagate **upward** through the graph.
### 1. Release the Changed Package
Use the `hackage-release` skill for the full Hackage publish workflow. In the local checkout:
1. Bump version in `.cabal` file (PVP: A.B.C.D)
2. Update ChangeLog.md
3. `cabal build && cabal check`
4. `cabal sdist`
5. Commit, tag `vX.Y.Z.W`, push with tags
6. Publish to Hackage
7. Publish docs
**Manual doc upload required for GTK-dependent packages:** Hackage cannot build documentation for packages that depend on GTK/GI libraries (the build servers lack the system dependencies). This affects `taffybar`, `gtk-sni-tray`, `gtk-strut`, and `dbus-menu`. For these packages you must build haddocks locally and upload them yourself — see the `hackage-release` skill for the `cabal haddock --haddock-for-hackage` and `cabal upload --documentation` commands. Only `status-notifier-item` and `dbus-hslogger` (pure DBus/Haskell deps) can have their docs built by Hackage automatically.
### 2. Update Dependents' Version Bounds
For each package higher in the graph that depends on what you just released, update the dependency bound in its `.cabal` file. For example, if you bumped `gtk-strut` to 0.1.5.0:
- In `gtk-sni-tray.cabal`: update `gtk-strut >= 0.1.5 && < 0.2`
- In `taffybar.cabal`: update `gtk-strut >= 0.1.5 && < 0.2`
Then release those packages too if needed (repeat from step 1).
### 3. Update Flake Inputs
Each package's `flake.nix` references its ecosystem dependencies as inputs (typically `flake = false` pointing at GitHub). After pushing changes, update the flake.lock in any repo that directly references the changed package:
```bash
cd ~/Projects/gtk-sni-tray # if it depends on what changed
nix flake update gtk-strut
```
```bash
cd ~/.config/taffybar/taffybar # taffybar references all ecosystem pkgs
nix flake update gtk-strut
```
### Full Ecosystem Release Order
1. `gtk-strut`, `status-notifier-item`, `dbus-hslogger`, `dbus-menu` (leaves — parallel OK)
2. `gtk-sni-tray` (update bounds for any leaf changes first)
3. `taffybar` (update bounds for all changes)

View File

@@ -1,64 +0,0 @@
---
name: taffybar-nixos-flake-chain
description: Use when doing NixOS rebuilds involving taffybar, or when flake.lock updates are needed after changing taffybar ecosystem packages. Also use when debugging stale taffybar versions after `just switch`.
---
# Taffybar NixOS Flake Chain
How the taffybar ecosystem packages are consumed by the NixOS configuration through a chain of nested flakes, and what flake.lock updates may be needed when something changes.
See also: `taffybar-ecosystem-release` for the package dependency graph, release workflow, and Hackage publishing.
## The Flake Chain
The NixOS system build pulls in taffybar through the personal
`imalison-taffybar` config flake. The top-level NixOS flake should not declare
or override a direct `taffybar` input; the config flake owns its taffybar
version.
```
nixos/flake.nix (top - `just switch` reads this)
│ └── imalison-taffybar path:../dotfiles/config/taffybar
dotfiles/config/taffybar/flake.nix (middle - imalison-taffybar config)
│ ├── taffybar path:.../taffybar/taffybar
│ └── gtk-sni-tray, gtk-strut, etc. (GitHub inputs)
dotfiles/config/taffybar/taffybar/flake.nix (bottom - taffybar library)
│ └── gtk-sni-tray, gtk-strut, etc. (flake = false GitHub inputs)
```
The NixOS layer may make `imalison-taffybar` follow shared inputs such as
`nixpkgs`, `flake-utils`, and `xmonad`, but it should not set
`imalison-taffybar.inputs.taffybar.follows`.
## Why Bottom-Up Updates Matter
`path:` inputs snapshot the target flake **including its flake.lock** at lock time. If you only run `nix flake update` at the top (nixos) layer, the middle and bottom layers keep whatever was previously locked in their own flake.lock files.
So when propagating a change to a system rebuild, you generally need to update flake.lock files from the bottom up — the bottom layer first so the middle layer picks up fresh locks when it re-resolves, then the middle so the top picks up fresh locks.
```bash
# Bottom (if an ecosystem dep changed):
cd ~/.config/taffybar/taffybar && nix flake update <pkg>
# Middle:
cd ~/.config/taffybar && nix flake update <pkg> taffybar
# Top:
cd ~/dotfiles/nixos && nix flake update imalison-taffybar
```
Not every change requires touching all three layers. Think about which flake.lock files actually contain stale references:
- Changed **taffybar itself** — it's owned by the config flake, so start at the middle (`nix flake update taffybar`) then update `imalison-taffybar` at the top.
- Changed a **leaf ecosystem package** (e.g. gtk-strut) — start at the bottom since taffybar's flake.lock references it, then cascade up.
- The nixos flake can still have unrelated direct inputs such as `kanshi-sni`. Do not add a top-level `taffybar` input just to control the config flake's taffybar source.
## Rebuilding
```bash
cd ~/dotfiles/nixos && just switch
```
If taffybar seems stale after a rebuild, check whether the flake.lock at each layer actually points at the expected revision — a missed cascade step is the usual cause.

View File

@@ -31,6 +31,7 @@
"iterm2", "iterm2",
"java", "java",
"jumpcut", "jumpcut",
"karabiner",
"libreoffice", "libreoffice",
"macpass", "macpass",
"mirrordisplays", "mirrordisplays",
@@ -169,7 +170,6 @@
"tig", "tig",
"tmate", "tmate",
"tmux", "tmux",
"zellij",
"unoconv", "unoconv",
"vim", "vim",
"w3m", "w3m",

View File

@@ -1,6 +0,0 @@
*
!.gitignore
!CLAUDE.md
!settings.json
!settings.local.json
!settings.local.json.example

View File

@@ -1 +0,0 @@
../agents/AGENTS.md

View File

@@ -1,21 +0,0 @@
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.agents/hooks/tmux-title.sh"
}
]
}
]
},
"enabledPlugins": {
"superpowers@superpowers-marketplace": true,
"agent-browser@agent-browser": true
},
"effortLevel": "high",
"skipDangerousModePermissionPrompt": true,
"remoteControlAtStartup": true
}

View File

@@ -1,39 +0,0 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(cat:*)"
],
"deny": []
},
"mcp": {
"servers": {
"gitea-mcp": {
"command": "bash",
"args": [
"-lc",
"set -euo pipefail; export GITEA_BASE_URL='https://dev.railbird.ai'; export GITEA_ACCESS_TOKEN=\"$(pass show claude-mcp/gitea-access-token | head -1)\"; exec docker run -i --rm -e GITEA_ACCESS_TOKEN -e GITEA_BASE_URL docker.gitea.com/gitea-mcp-server"
]
},
"chrome-devtools": {
"command": "npx",
"args": [
"chrome-devtools-mcp@latest",
"--auto-connect"
]
},
"imap-email": {
"command": "bash",
"args": [
"-lc",
"set -euo pipefail; export IMAP_USER='IvanMalison@gmail.com'; export IMAP_HOST='imap.gmail.com'; export IMAP_PASSWORD=\"$(pass show claude-mcp/gmail-imap-app-password | head -1)\"; exec npx -y imap-email-mcp"
]
}
}
},
"enabledMcpjsonServers": [
"chrome-devtools",
"imap-email"
],
"enableAllProjectMcpServers": true
}

View File

@@ -1,43 +0,0 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(cat:*)"
],
"deny": []
},
"mcp": {
"servers": {
"gitea-mcp": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITEA_ACCESS_TOKEN",
"-e",
"GITEA_BASE_URL=https://dev.railbird.ai",
"docker.gitea.com/gitea-mcp-server"
]
},
"chrome-devtools": {
"command": "npx",
"args": [
"chrome-devtools-mcp@latest",
"--auto-connect"
]
},
"imap-email": {
"command": "npx",
"args": ["-y", "imap-email-mcp"],
"env": {}
}
}
},
"enabledMcpjsonServers": [
"chrome-devtools",
"imap-email"
],
"enableAllProjectMcpServers": true
}

View File

@@ -1,8 +0,0 @@
*
!.gitignore
!AGENTS.md
!config.toml
!skills
# Legacy generated/local Codex state under this repo stays ignored. Active
# host-local Codex fragments now live under ~/.codex.

View File

@@ -1 +0,0 @@
../agents/AGENTS.md

View File

@@ -1,55 +0,0 @@
model = "gpt-5.5"
model_reasoning_effort = "high"
service_tier = "fast"
personality = "pragmatic"
suppress_unstable_features_warning = true
# Portable Codex defaults. Home Manager regenerates ~/.codex/config.toml from
# this file, ~/.codex/config.local.toml, and Codex-owned sections preserved in
# ~/.codex/config.local-state.toml.
[mcp_servers.chrome-devtools]
command = "npx"
args = ["-y", "chrome-devtools-mcp@latest", "--auto-connect"]
[mcp_servers.observability]
command = "npx"
args = ["-y", "@google-cloud/observability-mcp"]
[mcp_servers.openaiDeveloperDocs]
url = "https://developers.openai.com/mcp"
[features]
unified_exec = true
apps = true
steer = true
goals = true
fast_mode = true
remote_control = true
[plugins."google-calendar@openai-curated"]
enabled = true
[plugins."gmail@openai-curated"]
enabled = true
[plugins."google-drive@openai-curated"]
enabled = true
[plugins."github@openai-curated"]
enabled = true
[plugins."computer-use@openai-bundled"]
enabled = true
[plugins."documents@openai-primary-runtime"]
enabled = true
[plugins."spreadsheets@openai-primary-runtime"]
enabled = true
[plugins."presentations@openai-primary-runtime"]
enabled = true
[plugins."browser-use@openai-bundled"]
enabled = true

View File

@@ -1 +0,0 @@
../agents/skills

View File

@@ -1,8 +1,7 @@
[general] import = ["/home/imalison/.config/alacritty/themes/themes/dracula.toml"]
import = ["~/.config/alacritty/themes/themes/dracula.toml"]
[font] [font]
size = 12 size = 8
[scrolling] [scrolling]
history = 10000 history = 10000

View File

@@ -0,0 +1,2 @@
[api]
token = 417ba97c-b532-4e4b-86df-a240314ae840

View File

@@ -1,18 +0,0 @@
output HDMI-0
off
output DP-1
off
output DP-2
off
output DP-3
off
output DP-4
off
output DP-5
off
output DP-0
crtc 0
mode 3440x1440
pos 0x0
rate 240.00
x-prop-non_desktop 0

View File

@@ -1 +0,0 @@
DP-0 00ffffffffffff003669d04d0000000033210104b55022783bac05b04d3db7250f5054bfcf00714f81c0814081809500b300d1c00101e77c70a0d0a0295030203a0020513100001a023a801871382d40582c450020513100001e000000fd0c30f0919196010a202020202020000000fc004d50473334314358204f4c45440257020339f14901030204901211133f2309070783010000e2002a741a0000030330f000a066024f03f0000000000000e305e201e6060701664b00565e00a0a0a029503020350020513100001a6fc200a0a0a055503020350020513100001a00000000000000000000000000000000000000000000000000000000000000000000fc7012790300030150a2e300086f0d9f002f801f009f05b20031000900520101086f0d9f002f801f009f05540002000900b76901086f0d9f002f801f009f057600020009006f0502086f0d8f002f801f009f0563001d00090000000000000000000000000000000000000000000000000000000000000000000000000000001590

View File

@@ -1,39 +0,0 @@
output DP-1
off
output HDMI-1
off
output DP-2
off
output HDMI-2
off
output DP-1-0
off
output DP-1-1
off
output DP-1-2
off
output DP-1-3
off
output DP-1-4
off
output DP-1-5
off
output DP-1-6
off
output eDP-1
crtc 0
mode 2560x1600
pos 0x0
primary
rate 240.00
x-prop-broadcast_rgb Automatic
x-prop-colorspace Default
x-prop-max_bpc 12
x-prop-non_desktop 0
x-prop-scaling_mode Full aspect
output HDMI-1-0
crtc 4
mode 3440x1440
pos 2560x0
rate 99.98
x-prop-non_desktop 0

View File

@@ -1,2 +0,0 @@
HDMI-1-0 00ffffffffffff0010ace3a1535a333016210103805123782a25a1b14d3db7250e505421080001010101010101010101010101010101e77c70a0d0a029503020350029623100001a000000ff00237442737a474441594542634e000000fd0018781e963c010a202020202020000000fc0044656c6c204157333432334457015f020337f148101f04130312013f230907018301000068030c002000383c006ad85dc401788000000278e305c000e2006ae60605018d4b004ed470a0d0a046503020350029623100001a9d6770a0d0a022503020350029623100001a565e00a0a0a029503020350029623100001a6fc200a0a0a055503020350029623100001a3c
eDP-1 00ffffffffffff0009e5580c0000000001210104b527187803bbc5ae503fb7250c515500000001010101010101010101010101010101c07200a0a040c8603020360084f21000001a000000fd0c30f0b1b176010a202020202020000000fe00424f452043510a202020202020000000fc004e4531383051444d2d4e4d310a029602030f00e3058080e606050195731000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa702079020021001d280f7409000a400680dd2a511824b249120e023554b060ec64662a1378220014ffed1185ff099f002f001f003f06c700020005002b000c27003cef00002700303b0000810015741a0000030b30f0006095107310f0000000008d00000000000000000000000000000000000000000000000000000000bc90

View File

@@ -1,39 +0,0 @@
output DP-1
off
output HDMI-1
off
output DP-2
off
output HDMI-2
off
output DP-1-0
off
output DP-1-1
off
output DP-1-2
off
output DP-1-3
off
output DP-1-4
off
output DP-1-5
off
output DP-1-6
off
output eDP-1
crtc 0
mode 2560x1600
pos 0x0
primary
rate 240.00
x-prop-broadcast_rgb Automatic
x-prop-colorspace Default
x-prop-max_bpc 12
x-prop-non_desktop 0
x-prop-scaling_mode Full aspect
output HDMI-1-0
crtc 4
mode 3440x1440
pos 2560x0
rate 99.98
x-prop-non_desktop 0

View File

@@ -1,2 +0,0 @@
HDMI-1-0 00ffffffffffff0010ace3a1535a333016210103805123782a25a1b14d3db7250e505421080001010101010101010101010101010101e77c70a0d0a029503020350029623100001a000000ff00237442737a474441594542634e000000fd0018781e963c010a202020202020000000fc0044656c6c204157333432334457015f020337f148101f04130312013f230907018301000068030c003000383c006ad85dc401788000000278e305c000e2006ae60605018d4b004ed470a0d0a046503020350029623100001a9d6770a0d0a022503020350029623100001a565e00a0a0a029503020350029623100001a6fc200a0a0a055503020350029623100001a2c
eDP-1 00ffffffffffff0009e5580c0000000001210104b527187803bbc5ae503fb7250c515500000001010101010101010101010101010101c07200a0a040c8603020360084f21000001a000000fd0c30f0b1b176010a202020202020000000fe00424f452043510a202020202020000000fc004e4531383051444d2d4e4d310a029602030f00e3058080e606050195731000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa702079020021001d280f7409000a400680dd2a511824b249120e023554b060ec64662a1378220014ffed1185ff099f002f001f003f06c700020005002b000c27003cef00002700303b0000810015741a0000030b30f0006095107310f0000000008d00000000000000000000000000000000000000000000000000000000bc90

View File

@@ -0,0 +1,27 @@
[rc]
use_copy=true
use_primary=true
synchronize=false
automatic_paste=false
show_indexes=true
save_uris=true
use_rmb_menu=false
save_history=true
history_limit=1000
history_timeout_seconds=30
history_timeout=false
items_menu=50
statics_show=true
statics_items=10
hyperlinks_only=false
confirm_clear=false
single_line=false
reverse_history=false
item_length=50
ellipsize=2
history_key=
actions_key=
menu_key=
search_key=
offline_key=
offline_mode=false

View File

@@ -0,0 +1,90 @@
@binding-set gtk-emacs-text-entry
{
bind "<ctrl>b" { "move-cursor" (logical-positions, -1, 0) };
bind "<shift><ctrl>b" { "move-cursor" (logical-positions, -1, 1) };
bind "<ctrl>f" { "move-cursor" (logical-positions, 1, 0) };
bind "<shift><ctrl>f" { "move-cursor" (logical-positions, 1, 1) };
bind "<alt>b" { "move-cursor" (words, -1, 0) };
bind "<shift><alt>b" { "move-cursor" (words, -1, 1) };
bind "<alt>f" { "move-cursor" (words, 1, 0) };
bind "<shift><alt>f" { "move-cursor" (words, 1, 1) };
bind "<ctrl>a" { "move-cursor" (paragraph-ends, -1, 0) };
bind "<shift><ctrl>a" { "move-cursor" (paragraph-ends, -1, 1) };
bind "<ctrl>e" { "move-cursor" (paragraph-ends, 1, 0) };
bind "<shift><ctrl>e" { "move-cursor" (paragraph-ends, 1, 1) };
bind "<ctrl>w" { "cut-clipboard" () };
bind "<ctrl>y" { "paste-clipboard" () };
bind "<ctrl>d" { "delete-from-cursor" (chars, 1) };
bind "<alt>d" { "delete-from-cursor" (word-ends, 1) };
bind "<alt>BackSpace" { "delete-from-cursor" (word-ends, -1) };
bind "<ctrl>k" { "delete-from-cursor" (paragraph-ends, 1) };
bind "<alt>space" { "delete-from-cursor" (whitespace, 1)
"insert-at-cursor" (" ") };
bind "<alt>KP_Space" { "delete-from-cursor" (whitespace, 1)
"insert-at-cursor" (" ") };
/*
* Some non-Emacs keybindings people are attached to
*/
bind "<ctrl>u" { "move-cursor" (paragraph-ends, -1, 0)
"delete-from-cursor" (paragraph-ends, 1) };
bind "<ctrl>h" { "delete-from-cursor" (chars, -1) };
bind "<ctrl>w" { "delete-from-cursor" (word-ends, -1) };
}
/*
* Bindings for GtkTextView
*/
@binding-set gtk-emacs-text-view
{
bind "<ctrl>p" { "move-cursor" (display-lines, -1, 0) };
bind "<shift><ctrl>p" { "move-cursor" (display-lines, -1, 1) };
bind "<ctrl>n" { "move-cursor" (display-lines, 1, 0) };
bind "<shift><ctrl>n" { "move-cursor" (display-lines, 1, 1) };
bind "<ctrl>space" { "set-anchor" () };
bind "<ctrl>KP_Space" { "set-anchor" () };
}
/*
* Bindings for GtkTreeView
*/
@binding-set gtk-emacs-tree-view
{
bind "<ctrl>s" { "start-interactive-search" () };
bind "<ctrl>f" { "move-cursor" (logical-positions, 1) };
bind "<ctrl>b" { "move-cursor" (logical-positions, -1) };
}
/*
* Bindings for menus
*/
@binding-set gtk-emacs-menu
{
bind "<ctrl>n" { "move-current" (next) };
bind "<ctrl>p" { "move-current" (prev) };
bind "<ctrl>f" { "move-current" (child) };
bind "<ctrl>b" { "move-current" (parent) };
}
entry {
-gtk-key-bindings: gtk-emacs-text-entry;
}
textview {
-gtk-key-bindings: gtk-emacs-text-entry, gtk-emacs-text-view;
}
treeview {
-gtk-key-bindings: gtk-emacs-tree-view;
}
GtkMenuShell {
-gtk-key-bindings: gtk-emacs-menu;
}
@import 'colors.css';

View File

@@ -1,10 +0,0 @@
general {
lock_cmd = pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session
}
listener {
timeout = 300
on-timeout = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver start
on-resume = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop
}

View File

@@ -1,42 +0,0 @@
local function config_dir()
local source = debug.getinfo(1, "S").source
if source:sub(1, 1) == "@" then
source = source:sub(2)
end
local dir = source:match("^(.*)/[^/]*$")
if dir and dir ~= "" then
return dir
end
return "."
end
local base_dir = config_dir()
package.path = table.concat({
base_dir .. "/?.lua",
base_dir .. "/?/init.lua",
package.path,
}, ";")
local modules = {
"hyprland.state",
"hyprland.scratchpads",
"hyprland.core",
"hyprland.layouts",
"hyprland.windows",
"hyprland.settings",
"hyprland.binds",
"hyprland.events",
}
for _, module in ipairs(modules) do
package.loaded[module] = nil
end
local ctx = require(modules[1])
setmetatable(ctx, { __index = _G })
for i = 2, #modules do
require(modules[i]).setup(ctx)
end

View File

@@ -1,332 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function desc(description, opts)
local bind_opts = {}
for key, value in pairs(opts or {}) do
bind_opts[key] = value
end
bind_opts.description = description
return bind_opts
end
local function setup_launcher_and_app_bindings()
bind(main_mod .. " + P", exec(launcher_command), desc("Open application launcher"))
bind(main_mod .. " + SHIFT + P", exec(run_menu), desc("Open command runner"))
bind(main_mod .. " + SHIFT + Return", exec(terminal), desc("Open terminal"))
bind(main_mod .. " + E", exec("emacsclient --eval '(emacs-everywhere)'"), desc("Open Emacs Everywhere"))
bind(main_mod .. " + V", exec("wl-paste --no-newline | ydotool type --file -"), desc("Type clipboard contents"))
end
local function setup_shell_and_session_bindings()
bind(hyper .. " + SHIFT + N", exec(shell_ui_command .. " control-center"), desc("Open control center"))
bind(hyper .. " + CTRL + N", exec(shell_ui_command .. " settings"), desc("Open system settings"))
bind(main_mod .. " + Q", exec("hyprctl reload"), desc("Reload Hyprland"))
bind(main_mod .. " + R", exec("hyprctl reload"), desc("Reload Hyprland"))
bind(hyper .. " + SHIFT + L", exec("hyprlock"), desc("Lock screen"))
bind(hyper .. " + SHIFT + V", toggle_visual_performance_mode, desc("Toggle Hyprland performance mode"))
bind(hyper .. " + slash", function()
hl.exec_cmd("toggle_taffybar")
refresh_monitor_reserved_cache(0.25)
refresh_active_scratchpad_geometries_later(600)
end, desc("Toggle taffybar"))
end
local function setup_audio_media_bindings()
bind(main_mod .. " + I", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
bind(main_mod .. " + K", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
bind(main_mod .. " + U", exec("set_volume --toggle-mute"), desc("Toggle mute"))
bind(main_mod .. " + semicolon", exec("playerctl play-pause"), desc("Play or pause media"))
bind(main_mod .. " + L", exec("playerctl next"), desc("Skip to next media track"))
bind(main_mod .. " + J", exec("playerctl previous"), desc("Skip to previous media track"))
bind("XF86AudioPlay", exec("playerctl play-pause"), desc("Play or pause media"))
bind("XF86AudioPause", exec("playerctl play-pause"), desc("Play or pause media"))
bind("XF86AudioNext", exec("playerctl next"), desc("Skip to next media track"))
bind("XF86AudioPrev", exec("playerctl previous"), desc("Skip to previous media track"))
bind("XF86AudioRaiseVolume", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
bind("XF86AudioLowerVolume", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
bind("XF86AudioMute", exec("set_volume --toggle-mute"), desc("Toggle mute"))
bind(hyper .. " + O", exec("/home/imalison/dotfiles/dotfiles/lib/functions/rofi_paswitch"), desc("Open PulseAudio output switcher"))
bind(hyper .. " + SHIFT + O", exec("/home/imalison/dotfiles/dotfiles/lib/bin/kef-optical"), desc("Switch KEF speakers to optical input"))
end
local function setup_display_wallpaper_and_capture_bindings()
bind("XF86MonBrightnessUp", exec("brightness.sh up"), desc("Raise display brightness", { repeating = true }))
bind("XF86MonBrightnessDown", exec("brightness.sh down"), desc("Lower display brightness", { repeating = true }))
bind("Print", exec("flameshot gui"), desc("Take screenshot"))
bind(hyper .. " + H", exec("flameshot gui"), desc("Take screenshot"))
bind(hyper .. " + backslash", exec("/home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle"), desc("Toggle monitor input"))
bind(hyper .. " + comma", exec("rofi_wallpaper.sh"), desc("Open wallpaper menu"))
bind(hyper .. " + SHIFT + comma", exec("/home/imalison/dotfiles/dotfiles/lib/bin/neowall-wallpaper toggle"), desc("Toggle neowall wallpaper"))
end
local function setup_rofi_and_tool_bindings()
bind(main_mod .. " + X", exec("rofi_command.sh"), desc("Open command menu"))
bind(hyper .. " + V", exec([[cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy]]), desc("Open clipboard history"))
bind(hyper .. " + P", exec("rofi-pass"), desc("Open password menu"))
bind(hyper .. " + C", exec("rofi_tmcodex.sh"), desc("Open Codex session menu"))
bind(hyper .. " + SHIFT + C", exec("rofi_tmcodex.sh resume"), desc("Resume Codex session"))
bind(hyper .. " + L", exec("hypr_rofi_layout"), desc("Open Hyprland layout menu"))
bind(hyper .. " + K", exec("rofi_kill_process.sh"), desc("Open process kill menu"))
bind(hyper .. " + SHIFT + K", exec("rofi_kill_all.sh"), desc("Open kill-all menu"))
bind(hyper .. " + R", exec("rofi_systemd_mono"), desc("Open systemd unit menu"))
bind(hyper .. " + X", exec("hypr_rofi_action"), desc("Open Hyprland action menu"))
bind(hyper .. " + I", exec("rofi_select_input.hs"), desc("Open input selection menu"))
bind(hyper .. " + Y", exec("rofi_agentic_skill"), desc("Open agentic skill menu"))
end
local function setup_external_command_bindings()
setup_launcher_and_app_bindings()
setup_shell_and_session_bindings()
setup_audio_media_bindings()
setup_display_wallpaper_and_capture_bindings()
setup_rofi_and_tool_bindings()
end
local function setup_window_overview_bindings()
bind(main_mod .. " + SHIFT + C", hl.dsp.window.close(), desc("Close active window"))
bind(main_mod .. " + SHIFT + Q", hl.dsp.exit(), desc("Exit Hyprland"))
bind(main_mod .. " + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + Tab", hyprwinview({
action = "show",
include_current_workspace = false,
start_in_filter_mode = true,
default_action = "bring",
}), desc("Show all-workspace window overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + slash", hyprwinview({ action = "toggle-filter" }), desc("Toggle window overview filter", overview_bind_opts))
bind("ALT + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
bind("ALT + SHIFT + Tab", hyprexpo("on"), desc("Open hyprexpo workspace overview", overview_bind_opts))
bind(main_mod .. " + G", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "select",
}), desc("Show window overview", overview_bind_opts))
bind(main_mod .. " + B", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring",
}), desc("Bring window from overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + B", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring-replace",
}), desc("Replace active window from overview", overview_bind_opts))
end
local function setup_window_focus_and_move_bindings()
bind(main_mod .. " + W", function()
focus_direction("up")
end, desc("Focus window above"))
bind(main_mod .. " + S", function()
focus_direction("down")
end, desc("Focus window below"))
bind(main_mod .. " + A", function()
focus_direction("left")
end, desc("Focus window to the left"))
bind(main_mod .. " + D", function()
focus_direction("right")
end, desc("Focus window to the right"))
bind(main_mod .. " + SHIFT + W", function()
swap_direction("up")
end, desc("Swap active window upward"))
bind(main_mod .. " + SHIFT + S", function()
swap_direction("down")
end, desc("Swap active window downward"))
bind(main_mod .. " + SHIFT + A", function()
swap_direction("left")
end, desc("Swap active window left"))
bind(main_mod .. " + SHIFT + D", function()
swap_direction("right")
end, desc("Swap active window right"))
bind(main_mod .. " + CTRL + W", function()
move_window_to_monitor("u", false)
end, desc("Move window to monitor above"))
bind(main_mod .. " + CTRL + S", function()
move_window_to_monitor("d", false)
end, desc("Move window to monitor below"))
bind(main_mod .. " + CTRL + A", function()
move_window_to_monitor("l", false)
end, desc("Move window to monitor on the left"))
bind(main_mod .. " + CTRL + D", function()
move_window_to_monitor("r", false)
end, desc("Move window to monitor on the right"))
bind(main_mod .. " + CTRL + SHIFT + W", function()
move_window_to_empty_workspace_on_monitor("u")
end, desc("Move window to empty workspace on monitor above"))
bind(main_mod .. " + CTRL + SHIFT + S", function()
move_window_to_empty_workspace_on_monitor("d")
end, desc("Move window to empty workspace on monitor below"))
bind(main_mod .. " + CTRL + SHIFT + A", function()
move_window_to_empty_workspace_on_monitor("l")
end, desc("Move window to empty workspace on left monitor"))
bind(main_mod .. " + CTRL + SHIFT + D", function()
move_window_to_empty_workspace_on_monitor("r")
end, desc("Move window to empty workspace on right monitor"))
end
local function setup_submap_bindings()
hl.define_submap("swap-workspace", function()
for i = 1, 9 do
local workspace_id = i
bind(tostring(i), function()
swap_current_workspace_with(workspace_id)
dispatch(hl.dsp.submap("reset"))
end, desc("Swap current workspace with workspace " .. workspace_id))
end
bind("Escape", hl.dsp.submap("reset"), desc("Exit workspace swap mode"))
bind("catchall", hl.dsp.submap("reset"), desc("Exit workspace swap mode"))
end)
hl.define_submap("window-picker", function()
for i = 1, 9 do
local index = i
bind(tostring(i), function()
activate_window_picker_candidate(index)
end, desc("Activate window picker candidate " .. index))
end
bind("Escape", hl.dsp.submap("reset"), desc("Exit window picker"))
bind("catchall", hl.dsp.submap("reset"), desc("Exit window picker"))
end)
end
local function setup_window_resize_and_monitor_bindings()
bind(mod_alt .. " + SHIFT + W", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), desc("Shrink window height upward", { repeating = true }))
bind(mod_alt .. " + SHIFT + S", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), desc("Grow window height downward", { repeating = true }))
bind(mod_alt .. " + SHIFT + A", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), desc("Shrink window width leftward", { repeating = true }))
bind(mod_alt .. " + SHIFT + D", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), desc("Grow window width rightward", { repeating = true }))
bind(hyper .. " + W", hl.dsp.focus({ monitor = "u" }), desc("Focus monitor above"))
bind(hyper .. " + S", hl.dsp.focus({ monitor = "d" }), desc("Focus monitor below"))
bind(hyper .. " + A", hl.dsp.focus({ monitor = "l" }), desc("Focus monitor on the left"))
bind(hyper .. " + D", hl.dsp.focus({ monitor = "r" }), desc("Focus monitor on the right"))
bind(hyper .. " + SHIFT + W", function()
move_window_to_monitor("u", true)
end, desc("Move window to monitor above and follow"))
bind(hyper .. " + SHIFT + S", function()
move_window_to_monitor("d", true)
end, desc("Move window to monitor below and follow"))
bind(hyper .. " + SHIFT + A", function()
move_window_to_monitor("l", true)
end, desc("Move window to left monitor and follow"))
bind(hyper .. " + SHIFT + D", function()
move_window_to_monitor("r", true)
end, desc("Move window to right monitor and follow"))
end
local function setup_layout_and_window_state_bindings()
bind(main_mod .. " + Space", cycle_layout_or_restore_tabbed_group, desc("Cycle workspace layout"))
bind(main_mod .. " + SHIFT + Space", force_columns_layout, desc("Force columns layout"))
bind(main_mod .. " + CTRL + Space", gather_workspace_into_tabbed_group, desc("Gather workspace into tabbed group"))
bind(main_mod .. " + bracketright", monocle_next, desc("Focus next monocle window"))
bind(main_mod .. " + bracketleft", monocle_prev, desc("Focus previous monocle window"))
bind(main_mod .. " + T", hl.dsp.window.float({ action = "disable" }), desc("Tile active window"))
bind(main_mod .. " + O", toggle_pinned_active_window, desc("Toggle pinned active window"))
bind(main_mod .. " + M", minimize_active_window, desc("Minimize active window"))
bind(main_mod .. " + SHIFT + M", restore_last_minimized, desc("Restore last minimized window"))
bind(main_mod .. " + CTRL + SHIFT + M", function()
enter_window_picker("minimized")
end, desc("Pick minimized window to restore"))
bind(main_mod .. " + SHIFT + equal", schedule_nstack_count_update, desc("Update nstack window count"))
bind(main_mod .. " + CTRL + M", hl.dsp.window.toggle_swallow(), desc("Toggle window swallowing"))
bind(main_mod .. " + SHIFT + E", function()
move_to_next_empty_workspace(true)
end, desc("Move to next empty workspace"))
bind(main_mod .. " + CTRL + E", function()
move_to_next_empty_workspace(false)
end, desc("Move window to next empty workspace"))
bind(main_mod .. " + apostrophe", focus_next_class, desc("Focus next window class"))
bind(hyper .. " + 1", toggle_inactive_opacity_for_active_window, desc("Toggle inactive opacity reduction for active window"))
bind(mod_alt .. " + W", show_active_window_info, desc("Show active window info"))
end
local function setup_scratchpad_bindings()
bind(main_mod .. " + SHIFT + X", hl.dsp.workspace.toggle_special("NSP"), desc("Toggle NSP special workspace"))
bind(mod_alt .. " + C", function()
toggle_scratchpad("codex")
end, desc("Toggle Codex scratchpad"))
bind(mod_alt .. " + E", function()
toggle_scratchpad("element")
end, desc("Toggle Element scratchpad"))
bind(mod_alt .. " + H", function()
toggle_scratchpad("htop")
end, desc("Toggle htop scratchpad"))
bind(mod_alt .. " + K", function()
toggle_scratchpad("slack")
end, desc("Toggle Slack scratchpad"))
bind(mod_alt .. " + M", function()
toggle_scratchpad("messages")
end, desc("Toggle Messages scratchpad"))
bind(mod_alt .. " + S", function()
toggle_scratchpad("spotify")
end, desc("Toggle Spotify scratchpad"))
bind(mod_alt .. " + T", function()
toggle_scratchpad("transmission")
end, desc("Toggle Transmission scratchpad"))
bind(mod_alt .. " + V", function()
toggle_scratchpad("volume")
end, desc("Toggle volume scratchpad"))
bind(mod_alt .. " + grave", function()
toggle_scratchpad("dropdown")
end, desc("Toggle dropdown scratchpad"))
bind(mod_alt .. " + Space", minimize_other_classes, desc("Minimize other window classes"))
bind(mod_alt .. " + SHIFT + Space", restore_focused_class, desc("Restore focused window class"))
bind(mod_alt .. " + Return", restore_all_minimized, desc("Restore all minimized windows"))
end
local function setup_workspace_bindings()
for i = 1, 9 do
local workspace = tostring(i)
bind(main_mod .. " + " .. workspace, hl.dsp.focus({ workspace = workspace, on_current_monitor = true }), desc("Focus workspace " .. workspace))
bind(main_mod .. " + SHIFT + " .. workspace, hl.dsp.window.move({ workspace = workspace, follow = false }), desc("Move window to workspace " .. workspace))
bind(main_mod .. " + CTRL + " .. workspace, function()
dispatch(hl.dsp.window.move({ workspace = workspace, follow = false }))
dispatch(hl.dsp.focus({ workspace = workspace, on_current_monitor = true }))
end, desc("Move window to workspace " .. workspace .. " and follow"))
end
bind(main_mod .. " + backslash", workspacehistory("cycle", 1), desc("Cycle to next workspace in history"))
bind(main_mod .. " + slash", workspacehistory("cycle", -1), desc("Cycle to previous workspace in history"))
bind(main_mod .. " + Escape", workspacehistory("cancel"), desc("Cancel workspace history cycle"))
bind(main_mod .. " + Z", hl.dsp.focus({ monitor = "+1" }), desc("Focus next monitor"))
bind(main_mod .. " + SHIFT + Z", hl.dsp.window.move({ monitor = "+1" }), desc("Move window to next monitor"))
bind(main_mod .. " + mouse_down", function()
cycle_workspace(1)
end, desc("Cycle to next workspace"))
bind(main_mod .. " + mouse_up", function()
cycle_workspace(-1)
end, desc("Cycle to previous workspace"))
bind(hyper .. " + E", focus_next_empty_workspace, desc("Focus next empty workspace"))
bind(hyper .. " + 5", enter_workspace_swap_mode, desc("Enter workspace swap mode"))
bind(hyper .. " + G", gather_focused_class, desc("Gather focused window class"))
bind(hyper .. " + SHIFT + backslash", workspacehistory("debug"), desc("Show workspace history debug info"))
end
local function setup_mouse_bindings()
bind(main_mod .. " + mouse:272", float_and_drag_active_window, desc("Float and drag active window"))
bind(main_mod .. " + mouse:273", float_and_resize_active_window, desc("Float and resize active window"))
end
local function setup_internal_window_manager_bindings()
setup_window_overview_bindings()
setup_window_focus_and_move_bindings()
setup_submap_bindings()
setup_window_resize_and_monitor_bindings()
setup_layout_and_window_state_bindings()
setup_scratchpad_bindings()
setup_workspace_bindings()
setup_mouse_bindings()
end
setup_external_command_bindings()
setup_internal_window_manager_bindings()
end
return M

View File

@@ -1,615 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function command_line_contains(needle)
local command_line = io.open("/proc/self/cmdline", "rb")
if not command_line then
return false
end
local contents = command_line:read("*a") or ""
command_line:close()
return contents:find(needle, 1, true) ~= nil
end
verify_config = command_line_contains("--verify-config")
dev_session = os.getenv("IMALISON_HYPRLAND_DEV_SESSION") == "1"
local function exec(command)
return hl.dsp.exec_cmd(command)
end
local function dispatch(dispatcher)
return hl.dispatch(dispatcher)
end
local action_registry = {}
local function action_text(value)
return tostring(value or ""):gsub("[\t\r\n]", " "):gsub(" +", " "):match("^%s*(.-)%s*$")
end
local function action_registry_path()
local runtime_dir = os.getenv("XDG_RUNTIME_DIR") or "/tmp"
return runtime_dir .. "/hyprland-actions.tsv"
end
local function register_action(keys, dispatcher, opts)
local description = opts and opts.description
if not description or description == "" then
return
end
local id = tostring(#action_registry + 1)
action_registry[#action_registry + 1] = {
id = id,
keys = action_text(keys),
description = action_text(description),
dispatcher = dispatcher,
}
end
local function bind(keys, dispatcher, opts)
hl.bind(keys, dispatcher, opts)
register_action(keys, dispatcher, opts)
end
_G.im_hyprland_write_actions = function()
local actions_file = io.open(action_registry_path(), "w")
if not actions_file then
return
end
for _, action in ipairs(action_registry) do
actions_file:write(action.id, "\t", action.description, "\t", action.keys, "\n")
end
actions_file:close()
end
_G.im_hyprland_run_action = function(id)
local action = action_registry[tonumber(id)]
if not action then
return
end
if type(action.dispatcher) == "function" then
action.dispatcher()
else
dispatch(action.dispatcher)
end
end
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function overview_trace(label)
local enabled = io.open(overview_trace_enabled_path, "r")
if not enabled then
return
end
enabled:close()
local trace = io.open(overview_trace_path, "a")
if trace then
trace:write(os.date("%Y-%m-%d %H:%M:%S "), label, "\n")
trace:close()
end
end
local function window_selector(window)
if not window or not window.address then
return nil
end
return "address:" .. tostring(window.address)
end
local function hyprexpo_call(method, arg)
return function()
overview_trace("hyprexpo:" .. method .. (arg and (" " .. tostring(arg)) or ""))
if hl.plugin and hl.plugin.hyprexpo and hl.plugin.hyprexpo[method] then
hl.plugin.hyprexpo[method](arg)
else
hl.notification.create({
text = "hyprexpo is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
end
local function hyprexpo(action)
return hyprexpo_call("expo", action or "toggle")
end
local function hyprwinview(action)
return function()
local label = "hyprwinview"
if type(action) == "table" and action.action then
label = label .. " " .. tostring(action.action)
elseif type(action) ~= "table" and action ~= nil then
label = label .. " " .. tostring(action)
end
local function invoke()
overview_trace(label)
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.overview then
hl.plugin.hyprwinview.overview(action)
else
hl.notification.create({
text = "hyprwinview is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
invoke()
end
end
local function workspacehistory(action, arg)
return function()
if hl.plugin and hl.plugin.workspacehistory and hl.plugin.workspacehistory[action] then
hl.plugin.workspacehistory[action](arg)
else
hl.notification.create({
text = "workspacehistory is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
end
local function apply_nstack_config()
if verify_config or not enable_nstack or not configure_nstack_plugin_from_lua then
return
end
hl.config({
plugin = {
nstack = {
layout = {
orientation = "left",
new_on_top = false,
new_near_focused = true,
new_is_master = false,
no_gaps_when_only = true,
special_scale_factor = 0.8,
inherit_fullscreen = true,
stacks = 1,
center_single_master = false,
mfact = 0.0,
single_mfact = 1.0,
},
},
},
})
end
local function apply_hyprexpo_config()
if verify_config or not enable_hyprexpo then
return
end
hl.config({
plugin = {
hyprexpo = {
columns = 3,
gap_size = 5,
gap_size_outer = 0,
bg_col = 0xff111111,
workspace_method = "first 1",
skip_empty = false,
max_workspace = max_workspace,
gesture_distance = 200,
keynav_wrap_h = 1,
keynav_wrap_v = 1,
keynav_reading_order = 0,
border_width = 2,
border_color_current = "rgb(66ccff)",
border_color_focus = "rgb(edb443)",
border_color_hover = "rgb(aabbcc)",
tile_rounding = 5,
tile_rounding_power = 2.0,
label_enable = 1,
label_font_size = 28,
label_text_mode = "id",
label_position = "center",
label_offset_x = 6,
label_offset_y = 6,
selection_label_enable = 0,
label_show = "always",
label_color_default = 0xffffffff,
label_color_hover = 0xffeeeeee,
label_color_focus = 0xffedb443,
label_color_current = 0xff66ccff,
label_bg_enable = 1,
label_bg_color = 0xcc000000,
label_bg_rounding = 10,
label_padding = 12,
label_font_bold = 1,
label_pixel_snap = 1,
},
},
})
end
local function apply_hyprwinview_config()
if verify_config or not enable_hyprwinview then
return
end
hl.config({
plugin = {
hyprwinview = {
gap_size = 24,
margin = 48,
background = "rgba(10101400)",
background_blur = 1,
border_col = "rgba(ffffff33)",
hover_border_col = "rgba(66ccffee)",
border_size = 3,
window_order = "application",
keys_default_action = "return,enter,space,g,f",
keys_filter_toggle = "/",
show_app_icon = 1,
app_icon_size = 48,
app_icon_theme_source = "auto",
app_icon_position = "bottom right",
app_icon_margin_x = 12,
app_icon_margin_y = 12,
app_icon_margin_relative_x = 0.0,
app_icon_margin_relative_y = 0.0,
app_icon_offset_x = 0,
app_icon_offset_y = 0,
app_icon_backplate_col = "rgba(00000066)",
app_icon_backplate_padding = 6,
show_window_text = 1,
window_text_font = "Sans",
window_text_size = 14,
window_text_color = "rgba(ffffffff)",
window_text_backplate_col = "rgba(00000099)",
window_text_padding = 6,
filter_animation_ms = 140,
animation = "workspace_zoom",
animation_in_ms = 280,
animation_out_ms = 220,
animation_speed = 1.0,
animation_scale = 0.94,
animation_stagger_ms = 16,
animation_stagger_max_ms = 120,
},
},
})
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.configure then
hl.plugin.hyprwinview.configure({
keys = {
left = { "a", "h", "left" },
right = { "d", "l", "right" },
up = { "w", "k", "up" },
down = { "s", "j", "down" },
default_action = { "return", "enter", "space", "g", "f" },
bring = { "b", "shift+return", "shift+space" },
bring_replace = { "shift + b" },
close = { "escape", "q" },
filter_toggle = { "/" },
},
})
end
end
local function active_workspace()
return hl.get_active_workspace()
end
local function active_workspace_id()
local workspace = active_workspace()
if workspace and type(workspace.id) == "number" and workspace.id >= 1 then
return math.min(max_workspace, math.max(1, workspace.id))
end
return 1
end
local function workspace_key(workspace)
workspace = workspace or active_workspace()
if workspace and workspace.id then
return tostring(workspace.id)
end
return tostring(active_workspace_id())
end
local function current_workspace_layout()
return workspace_layouts[workspace_key()] or columns_layout
end
local function write_layout_state()
local runtime_dir = os.getenv("XDG_RUNTIME_DIR")
if not runtime_dir then
return
end
local file = io.open(runtime_dir .. "/hyprland-layout-state", "w")
if not file then
return
end
local workspace = active_workspace()
file:write("workspace=", workspace_key(workspace), "\n")
file:write("layout=", current_layout, "\n")
for key, layout in pairs(workspace_layouts) do
file:write("workspace.", tostring(key), "=", tostring(layout), "\n")
end
file:close()
end
local function is_normal_workspace(workspace)
return workspace and not workspace.special and workspace.id and workspace.id >= 1
end
local function same_workspace(left, right)
if not left or not right then
return false
end
if left.name and right.name and tostring(left.name) == tostring(right.name) then
return true
end
return left.id and right.id and left.id == right.id
end
local function is_minimized_workspace(workspace)
if not workspace then
return false
end
local name = tostring(workspace.name or "")
return name == minimized_workspace or name == "minimized" or (workspace.special and name:find("minimized", 1, true) ~= nil)
end
local function is_minimized_window(window)
return window and is_minimized_workspace(window.workspace)
end
local function is_normal_window(window)
return window
and window.mapped ~= false
and not window.hidden
and window.workspace
and is_normal_workspace(window.workspace)
and not is_scratchpad_window(window)
and not is_minimized_window(window)
end
local function tiled_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if not window.floating and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function tiled_window_count(workspace)
return #tiled_windows(workspace)
end
local function sort_windows_by_focus_history(windows)
table.sort(windows, function(left, right)
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
end
local function window_address_set(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[window.address] = true
end
end
return addresses
end
local function window_address_list(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[#addresses + 1] = window.address
end
end
return addresses
end
local function window_address_in_set(window, addresses)
return window and window.address and addresses[window.address] or false
end
local function windows_by_address()
local windows = {}
for _, window in ipairs(hl.get_windows()) do
if window and window.address then
windows[window.address] = window
end
end
return windows
end
local function numeric_component(value, key, index)
if type(value) ~= "table" then
return 0
end
return tonumber(value[key] or value[index]) or 0
end
local function window_center(window)
local at = window and window.at or {}
local size = window and window.size or {}
return numeric_component(at, "x", 1) + numeric_component(size, "x", 1) / 2,
numeric_component(at, "y", 2) + numeric_component(size, "y", 2) / 2
end
local function tiled_window_geometry(window)
if not window or window.floating then
return nil
end
local selector = window_selector(window)
if not selector then
return nil
end
local at = window.at or {}
local size = window.size or {}
local width = math.floor(numeric_component(size, "x", 1))
local height = math.floor(numeric_component(size, "y", 2))
if width <= 0 or height <= 0 then
return nil
end
return {
selector = selector,
x = math.floor(numeric_component(at, "x", 1)),
y = math.floor(numeric_component(at, "y", 2)),
width = width,
height = height,
}
end
local function window_distance_squared(window, x, y)
local wx, wy = window_center(window)
local dx = wx - x
local dy = wy - y
return dx * dx + dy * dy
end
local function sort_windows_by_visual_position(windows)
table.sort(windows, function(left, right)
local left_x, left_y = window_center(left)
local right_x, right_y = window_center(right)
if math.abs(left_x - right_x) > 10 then
return left_x < right_x
end
if math.abs(left_y - right_y) > 10 then
return left_y < right_y
end
return tostring(left.address or "") < tostring(right.address or "")
end)
end
local function grouping_direction(window, anchor)
local wx, wy = window_center(window)
local ax, ay = window_center(anchor)
local dx = wx - ax
local dy = wy - ay
if math.abs(dx) >= math.abs(dy) then
return dx >= 0 and "left" or "right"
end
return dy >= 0 and "up" or "down"
end
local function grouping_directions(window, anchor)
local primary = grouping_direction(window, anchor)
local directions = { primary }
for _, direction in ipairs({ "left", "right", "up", "down" }) do
if direction ~= primary then
directions[#directions + 1] = direction
end
end
return directions
end
local function workspace_window_count(workspace_id)
local workspace = hl.get_workspace(tostring(workspace_id))
if not workspace then
return 0
end
return workspace.windows or tiled_window_count(workspace)
end
local function find_empty_workspace(target_monitor, exclude_id)
local unused_candidate = nil
local elsewhere_empty_candidate = nil
local target_monitor_name = target_monitor and target_monitor.name or nil
for i = 1, max_workspace do
if i ~= exclude_id then
local workspace = hl.get_workspace(tostring(i))
if not workspace then
unused_candidate = unused_candidate or i
elseif is_normal_workspace(workspace) and workspace_window_count(i) == 0 then
local monitor = workspace.monitor
if target_monitor_name and monitor and monitor.name == target_monitor_name then
return i
end
elsewhere_empty_candidate = elsewhere_empty_candidate or i
end
end
end
return unused_candidate or elsewhere_empty_candidate
end
ctx.command_line_contains = command_line_contains
ctx.bind = bind
ctx.exec = exec
ctx.dispatch = dispatch
ctx.shell_quote = shell_quote
ctx.overview_trace = overview_trace
ctx.window_selector = window_selector
ctx.hyprexpo = hyprexpo
ctx.hyprwinview = hyprwinview
ctx.workspacehistory = workspacehistory
ctx.apply_nstack_config = apply_nstack_config
ctx.apply_hyprexpo_config = apply_hyprexpo_config
ctx.apply_hyprwinview_config = apply_hyprwinview_config
ctx.active_workspace = active_workspace
ctx.active_workspace_id = active_workspace_id
ctx.workspace_key = workspace_key
ctx.current_workspace_layout = current_workspace_layout
ctx.write_layout_state = write_layout_state
ctx.is_normal_workspace = is_normal_workspace
ctx.same_workspace = same_workspace
ctx.is_minimized_workspace = is_minimized_workspace
ctx.is_minimized_window = is_minimized_window
ctx.is_normal_window = is_normal_window
ctx.tiled_windows = tiled_windows
ctx.tiled_window_count = tiled_window_count
ctx.sort_windows_by_focus_history = sort_windows_by_focus_history
ctx.window_address_set = window_address_set
ctx.window_address_list = window_address_list
ctx.window_address_in_set = window_address_in_set
ctx.windows_by_address = windows_by_address
ctx.numeric_component = numeric_component
ctx.window_center = window_center
ctx.tiled_window_geometry = tiled_window_geometry
ctx.window_distance_squared = window_distance_squared
ctx.sort_windows_by_visual_position = sort_windows_by_visual_position
ctx.grouping_direction = grouping_direction
ctx.grouping_directions = grouping_directions
ctx.workspace_window_count = workspace_window_count
ctx.find_empty_workspace = find_empty_workspace
end
return M

View File

@@ -1,96 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local fullscreen_states = {}
local function unset_fullscreen_state(window, state)
dispatch(hl.dsp.window.fullscreen_state({
internal = state.internal,
client = state.client,
action = "unset",
window = window_selector(window),
}))
end
local function reconcile_fullscreen_state(window)
if not window or not window.address then
return
end
local address = tostring(window.address)
local previous = fullscreen_states[address]
local current = {
internal = tonumber(window.fullscreen) or 0,
client = tonumber(window.fullscreen_client) or 0,
}
fullscreen_states[address] = current
if window.floating or current_layout == monocle_layout then
return
end
if current.internal == 1 or (previous and previous.internal >= 2 and current.internal > 0 and current.client == 0) then
unset_fullscreen_state(window, current)
fullscreen_states[address] = { internal = 0, client = 0 }
end
end
hl.on("hyprland.start", function()
apply_nstack_config()
apply_hyprexpo_config()
apply_hyprwinview_config()
apply_hyprwobbly_config()
apply_hyprglass_config()
apply_visual_performance_mode()
apply_rules()
if not dev_session then
hl.exec_cmd("sh -lc '/run/current-system/sw/bin/uwsm finalize HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE XAUTHORITY IMALISON_SESSION_TYPE=wayland IMALISON_WINDOW_MANAGER=hyprland || dbus-update-activation-environment --systemd XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE IMALISON_WINDOW_MANAGER; systemctl --user start hyprland-session.target'")
hl.exec_cmd("hypridle")
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
end
write_layout_state()
schedule_nstack_count_update()
refresh_monitor_reserved_cache(0.25)
refresh_monitor_reserved_cache(1.25)
end)
hl.on("config.reloaded", apply_nstack_config)
hl.on("config.reloaded", apply_hyprexpo_config)
hl.on("config.reloaded", apply_hyprwinview_config)
hl.on("config.reloaded", apply_hyprwobbly_config)
hl.on("config.reloaded", apply_hyprglass_config)
hl.on("config.reloaded", apply_visual_performance_mode)
hl.on("config.reloaded", apply_rules)
hl.on("config.reloaded", refresh_shell_workarea_and_scratchpads)
hl.on("layer.opened", refresh_shell_workarea_and_scratchpads)
hl.on("layer.closed", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.added", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.removed", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.layout_changed", refresh_shell_workarea_and_scratchpads)
hl.on("window.open", schedule_nstack_count_update)
hl.on("window.destroy", schedule_nstack_count_update)
hl.on("window.kill", schedule_nstack_count_update)
hl.on("window.move_to_workspace", schedule_nstack_count_update)
hl.on("workspace.active", sync_layout_for_active_workspace)
hl.on("monitor.focused", sync_layout_for_active_workspace)
hl.on("window.open", update_monocle_notice)
hl.on("window.destroy", update_monocle_notice)
hl.on("window.kill", update_monocle_notice)
hl.on("window.move_to_workspace", update_monocle_notice)
hl.on("window.fullscreen", reconcile_fullscreen_state)
hl.on("window.update_rules", reconcile_fullscreen_state)
hl.on("window.open", adopt_matching_scratchpad_window)
hl.on("window.class", adopt_matching_scratchpad_window)
hl.on("window.title", adopt_matching_scratchpad_window)
hl.on("window.open", raise_file_chooser_window_later)
hl.on("window.class", raise_file_chooser_window_later)
hl.on("window.title", raise_file_chooser_window_later)
end
return M

View File

@@ -1,596 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function is_nstack_layout(layout)
return layout == columns_layout or layout == grid_layout
end
local function hyprland_layout(layout)
if layout == grid_layout then
return columns_layout
end
return layout
end
local function update_nstack_count()
if not enable_nstack or not is_nstack_layout(current_layout) then
return
end
local workspace = hl.get_active_workspace()
local count = tiled_window_count(workspace)
if count == 0 then
return
end
local stack_count = count
if current_layout == grid_layout then
stack_count = math.ceil(math.sqrt(count))
end
stack_count = math.max(stack_count, 2)
dispatch(hl.dsp.layout("setstackcount " .. tostring(stack_count)))
end
local function schedule_nstack_count_update()
if stack_update_timer then
stack_update_timer:set_enabled(false)
end
stack_update_timer = hl.timer(update_nstack_count, { timeout = 25, type = "oneshot" })
end
local function dismiss_monocle_notice()
if monocle_notice and monocle_notice:is_alive() then
monocle_notice:dismiss()
end
monocle_notice = nil
end
local function update_monocle_notice()
if current_layout ~= monocle_layout then
dismiss_monocle_notice()
return
end
local workspace = hl.get_active_workspace()
local count = tiled_window_count(workspace)
if count <= 1 then
dismiss_monocle_notice()
return
end
local text = "Monocle: " .. tostring(count) .. " windows"
if monocle_notice and monocle_notice:is_alive() then
monocle_notice:set_text(text)
monocle_notice:set_timeout(60000)
monocle_notice:pause()
else
monocle_notice = hl.notification.create({
text = text,
duration = 60000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
monocle_notice:pause()
end
end
local function layout_name(layout)
return layout_names[layout] or tostring(layout)
end
local function notify_layout(layout)
hl.notification.create({
text = "Layout: " .. layout_name(layout),
duration = 1200,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function set_layout(layout)
workspace_layouts[workspace_key()] = layout
current_layout = layout
hl.config({ general = { layout = hyprland_layout(layout) } })
write_layout_state()
if is_nstack_layout(layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
update_monocle_notice()
end
end
_G.im_hyprland_set_layout = function(layout)
if not layout_names[layout] then
hl.notification.create({
text = "Unknown layout: " .. tostring(layout),
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
set_layout(layout)
notify_layout(layout)
end
local function sync_layout_for_active_workspace()
current_layout = current_workspace_layout()
hl.config({ general = { layout = hyprland_layout(current_layout) } })
write_layout_state()
if is_nstack_layout(current_layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
update_monocle_notice()
end
end
local function cycle_layout(delta)
local current_index = 1
for index, layout in ipairs(layout_cycle) do
if layout == current_layout then
current_index = index
break
end
end
local next_index = ((current_index - 1 + delta) % #layout_cycle) + 1
local next_layout = layout_cycle[next_index]
set_layout(next_layout)
notify_layout(next_layout)
end
local function toggle_columns_monocle()
if current_layout == columns_layout then
set_layout(monocle_layout)
else
set_layout(columns_layout)
end
end
local function active_group_size()
local window = hl.get_active_window()
return window and window.group and window.group.size or 0
end
local function monocle_next()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
dispatch(hl.dsp.group.next({ window = window_selector(window) }))
elseif current_layout == monocle_layout then
dispatch(hl.dsp.layout("cyclenext"))
update_monocle_notice()
else
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
end
end
local function monocle_prev()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
dispatch(hl.dsp.group.prev({ window = window_selector(window) }))
elseif current_layout == monocle_layout then
dispatch(hl.dsp.layout("cycleprev"))
update_monocle_notice()
else
dispatch(hl.dsp.window.cycle_next({ next = false, tiled = true, floating = false }))
end
end
local function focus_direction(direction)
overview_trace("focus_direction " .. direction)
if active_group_size() > 1 or current_layout == monocle_layout then
if direction == "up" or direction == "left" then
monocle_prev()
else
monocle_next()
end
return
end
dispatch(hl.dsp.focus({ direction = direction }))
end
local function swap_direction(direction)
if enable_nstack and is_nstack_layout(current_layout) and active_group_size() <= 1 then
dispatch(hl.dsp.layout("swapdirection " .. direction))
return
end
dispatch(hl.dsp.window.swap({ direction = direction }))
end
local function focus_workspace(workspace_id)
dispatch(hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true }))
end
local function move_window_to_workspace(workspace_id, follow, window)
local target_window = window or hl.get_active_window()
local target_selector = window_selector(target_window)
dispatch(hl.dsp.window.move({ workspace = tostring(workspace_id), follow = false, window = target_selector }))
if follow then
focus_workspace(workspace_id)
if target_selector then
dispatch(hl.dsp.focus({ window = target_selector }))
end
end
end
local function notify_tabbed_group(text)
hl.notification.create({
text = text,
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function active_workspace_tiled_group_candidates(workspace)
local candidates = tiled_windows(workspace)
sort_windows_by_focus_history(candidates)
return candidates
end
local function move_window_into_group(window, anchor)
local selector = window_selector(window)
if not selector then
return false
end
for _, direction in ipairs(grouping_directions(window, anchor)) do
dispatch(hl.dsp.focus({ window = selector }))
dispatch(hl.dsp.window.move({ into_group = direction, window = selector }))
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return true
end
end
return false
end
local function find_tabbed_group_anchor(state)
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return active
end
if not state then
return nil
end
for _, window in ipairs(hl.get_windows()) do
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
return window
end
end
return nil
end
local function ordered_windows_for_tabbed_group_restore(state, workspace_id)
local ordered = {}
local seen = {}
local live_windows = windows_by_address()
local workspace = workspace_id and hl.get_workspace(tostring(workspace_id)) or active_workspace()
if state and state.order then
for _, address in ipairs(state.order) do
local window = live_windows[address]
if window and not window.floating and not window.hidden and (not workspace or same_workspace(window.workspace, workspace)) then
ordered[#ordered + 1] = window
seen[address] = true
end
end
end
if workspace then
for _, window in ipairs(tiled_windows(workspace)) do
if window and window.address and not seen[window.address] then
ordered[#ordered + 1] = window
seen[window.address] = true
end
end
end
return ordered
end
local function restore_tabbed_group_window_order(state, workspace_id)
local ordered = ordered_windows_for_tabbed_group_restore(state, workspace_id)
if #ordered <= 1 or not workspace_id then
return
end
local restore_workspace = tabbed_group_restore_workspace_prefix .. tostring(workspace_id)
for _, window in ipairs(ordered) do
move_window_to_workspace(restore_workspace, false, window)
end
for _, window in ipairs(ordered) do
move_window_to_workspace(workspace_id, false, window)
end
end
local function restore_workspace_tabbed_group()
local key = workspace_key()
local state = tabbed_workspace_groups[key]
local anchor = find_tabbed_group_anchor(state)
local anchor_selector = window_selector(anchor)
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
if not anchor_selector then
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
notify_tabbed_group("No tabbed group to restore")
return
end
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
restore_tabbed_group_window_order(state, target_workspace_id)
dispatch(hl.dsp.focus({ window = anchor_selector }))
schedule_nstack_count_update()
end
local function gather_workspace_into_tabbed_group()
local workspace = active_workspace()
if not is_normal_workspace(workspace) then
return
end
local key = workspace_key(workspace)
if tabbed_workspace_groups[key] or active_group_size() > 1 then
restore_workspace_tabbed_group()
return
end
local original_windows = tiled_windows(workspace)
sort_windows_by_visual_position(original_windows)
local original_order = window_address_list(original_windows)
local candidates = active_workspace_tiled_group_candidates(workspace)
if #candidates <= 1 then
set_layout(columns_layout)
return
end
local candidate_addresses = window_address_set(candidates)
local focused = hl.get_active_window()
local anchor = nil
if focused and not focused.floating and not focused.group and window_address_in_set(focused, candidate_addresses) then
anchor = focused
end
if not anchor then
for _, window in ipairs(candidates) do
if not window.group then
anchor = window
break
end
end
end
local anchor_selector = window_selector(anchor)
if not anchor_selector then
notify_tabbed_group("Current tiled windows are already grouped")
return
end
set_layout(columns_layout)
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
local group_windows = {}
for _, window in ipairs(candidates) do
if window ~= anchor and not window.group then
group_windows[#group_windows + 1] = window
end
end
local anchor_x, anchor_y = window_center(anchor)
table.sort(group_windows, function(left, right)
return window_distance_squared(left, anchor_x, anchor_y) < window_distance_squared(right, anchor_x, anchor_y)
end)
local grouped_count = 1
for _, window in ipairs(group_windows) do
if move_window_into_group(window, anchor) then
grouped_count = grouped_count + 1
end
end
if grouped_count <= 1 then
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
notify_tabbed_group("Unable to group tiled windows")
return
elseif grouped_count < #candidates then
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
end
tabbed_workspace_groups[key] = {
anchor = anchor.address,
order = original_order,
windows = candidate_addresses,
}
dispatch(hl.dsp.focus({ window = anchor_selector }))
end
local function force_columns_layout()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
else
set_layout(columns_layout)
end
end
local function cycle_layout_or_restore_tabbed_group()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
return
end
cycle_layout(1)
end
local function copy_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if window and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function swap_current_workspace_with(target_id)
local current = active_workspace()
if not current or not current.id or current.id == target_id then
return
end
local target = hl.get_workspace(tostring(target_id))
local current_windows = copy_windows(current)
local target_windows = copy_windows(target)
for _, window in ipairs(current_windows) do
move_window_to_workspace(target_id, false, window)
end
for _, window in ipairs(target_windows) do
move_window_to_workspace(current.id, false, window)
end
focus_workspace(current.id)
end
local function enter_workspace_swap_mode()
hl.notification.create({
text = "Swap with workspace 1-9",
duration = 2200,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
dispatch(hl.dsp.submap("swap-workspace"))
end
local function focus_next_empty_workspace()
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
if workspace_id then
focus_workspace(workspace_id)
end
end
local function move_to_next_empty_workspace(follow)
local window = hl.get_active_window()
if not window then
return
end
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
if workspace_id then
move_window_to_workspace(workspace_id, follow, window)
end
end
local function cycle_workspace(delta)
local current = active_workspace_id()
local next_workspace = ((current - 1 + delta) % max_workspace) + 1
focus_workspace(next_workspace)
end
local function move_window_to_monitor(direction, follow)
local window = hl.get_active_window()
if not window then
return
end
local original_monitor = hl.get_active_monitor()
dispatch(hl.dsp.window.move({ monitor = direction, follow = follow, window = window_selector(window) }))
if not follow and original_monitor then
dispatch(hl.dsp.focus({ monitor = original_monitor }))
end
end
local function move_window_to_empty_workspace_on_monitor(direction)
local window = hl.get_active_window()
local original_monitor = hl.get_active_monitor()
local target_monitor = hl.get_monitor(direction)
if not window or not original_monitor or not target_monitor or target_monitor == original_monitor then
return
end
local workspace_id = find_empty_workspace(target_monitor, active_workspace_id())
if not workspace_id then
return
end
dispatch(hl.dsp.focus({ monitor = target_monitor }))
focus_workspace(workspace_id)
dispatch(hl.dsp.focus({ monitor = original_monitor }))
move_window_to_workspace(workspace_id, false, window)
end
ctx.is_nstack_layout = is_nstack_layout
ctx.hyprland_layout = hyprland_layout
ctx.update_nstack_count = update_nstack_count
ctx.schedule_nstack_count_update = schedule_nstack_count_update
ctx.dismiss_monocle_notice = dismiss_monocle_notice
ctx.update_monocle_notice = update_monocle_notice
ctx.layout_name = layout_name
ctx.notify_layout = notify_layout
ctx.set_layout = set_layout
ctx.sync_layout_for_active_workspace = sync_layout_for_active_workspace
ctx.cycle_layout = cycle_layout
ctx.toggle_columns_monocle = toggle_columns_monocle
ctx.active_group_size = active_group_size
ctx.monocle_next = monocle_next
ctx.monocle_prev = monocle_prev
ctx.focus_direction = focus_direction
ctx.swap_direction = swap_direction
ctx.focus_workspace = focus_workspace
ctx.move_window_to_workspace = move_window_to_workspace
ctx.notify_tabbed_group = notify_tabbed_group
ctx.active_workspace_tiled_group_candidates = active_workspace_tiled_group_candidates
ctx.move_window_into_group = move_window_into_group
ctx.find_tabbed_group_anchor = find_tabbed_group_anchor
ctx.ordered_windows_for_tabbed_group_restore = ordered_windows_for_tabbed_group_restore
ctx.restore_tabbed_group_window_order = restore_tabbed_group_window_order
ctx.restore_workspace_tabbed_group = restore_workspace_tabbed_group
ctx.gather_workspace_into_tabbed_group = gather_workspace_into_tabbed_group
ctx.force_columns_layout = force_columns_layout
ctx.cycle_layout_or_restore_tabbed_group = cycle_layout_or_restore_tabbed_group
ctx.copy_windows = copy_windows
ctx.swap_current_workspace_with = swap_current_workspace_with
ctx.enter_workspace_swap_mode = enter_workspace_swap_mode
ctx.focus_next_empty_workspace = focus_next_empty_workspace
ctx.move_to_next_empty_workspace = move_to_next_empty_workspace
ctx.cycle_workspace = cycle_workspace
ctx.move_window_to_monitor = move_window_to_monitor
ctx.move_window_to_empty_workspace_on_monitor = move_window_to_empty_workspace_on_monitor
end
return M

View File

@@ -1,490 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
scratchpad_size_ratio = 0.95
dropdown_height_ratio = 0.5
dropdown_animation_frames = 18
dropdown_animation_frame_ms = 16
scratchpad_pending = {}
monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv"
scratchpad_fallback_reserved_top = 60
scratchpads = {
codex = {
command = "codex_desktop_scratchpad",
class = "codex-desktop",
},
htop = {
command = "alacritty --class htop-scratch --title htop -e htop",
class = "htop-scratch",
},
volume = {
command = "pavucontrol",
class = "org.pulseaudio.pavucontrol",
},
spotify = {
command = "spotify",
class = "spotify",
},
element = {
command = "element-desktop",
classes = { "Element", "electron" },
title = "Element",
},
slack = {
command = "slack",
class = "Slack",
},
messages = {
command = "google-chrome-stable --profile-directory=Default --app=https://messages.google.com/web/conversations",
class = "chrome-messages.google.com",
},
transmission = {
command = "transmission-gtk",
class = "transmission-gtk",
},
dropdown = {
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown",
class = "com.mitchellh.ghostty.dropdown",
dropdown = true,
},
}
local function lower_contains(value, needle)
if not needle or needle == "" then
return true
end
value = string.lower(tostring(value or ""))
needle = string.lower(tostring(needle))
return value:find(needle, 1, true) ~= nil
end
local function lower_contains_any(value, needles)
if type(needles) ~= "table" then
return lower_contains(value, needles)
end
for _, needle in ipairs(needles) do
if lower_contains(value, needle) then
return true
end
end
return false
end
local function scratchpad_window_matches(window, def)
return window
and not (type(is_file_chooser_window) == "function" and is_file_chooser_window(window))
and lower_contains_any(window.class, def.classes or def.class)
and lower_contains(window.title, def.title)
end
local function is_scratchpad_window(window)
for _, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
return true
end
end
return false
end
local function matching_scratchpad_name(window)
for name, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
return name
end
end
return nil
end
local function scratchpad_workspace(name)
return "special:scratch-" .. name
end
local function as_number(value, default)
local number = tonumber(value)
if number == nil then
return default
end
return number
end
local function logical_monitor_dimension(value, scale)
value = as_number(value, 0)
scale = as_number(scale, 1)
if scale <= 0 then
scale = 1
end
return math.floor((value / scale) + 0.5)
end
local function split_tsv(line)
local fields = {}
for field in (line .. "\t"):gmatch("([^\t]*)\t") do
fields[#fields + 1] = field
end
return fields
end
local function monitor_from_reserved_fields(monitor, fields)
if not monitor or not monitor.name or fields[1] ~= monitor.name or #fields < 10 then
return nil
end
return {
name = monitor.name,
x = tonumber(fields[2]),
y = tonumber(fields[3]),
width = tonumber(fields[4]),
height = tonumber(fields[5]),
scale = tonumber(fields[6]),
reserved = {
tonumber(fields[7]),
tonumber(fields[8]),
tonumber(fields[9]),
tonumber(fields[10]),
},
}
end
local function monitor_from_reserved_lines(monitor, lines)
if not monitor or not monitor.name then
return nil
end
for line in lines do
local cached = monitor_from_reserved_fields(monitor, split_tsv(line))
if cached then
return cached
end
end
return nil
end
local function monitor_from_reserved_cache(monitor)
if verify_config or not monitor or not monitor.name then
return nil
end
local file = io.open(monitor_reserved_cache_path, "r")
if not file then
return nil
end
local cached = monitor_from_reserved_lines(monitor, file:lines())
file:close()
return cached
end
local function refresh_monitor_reserved_cache(delay)
if verify_config then
return
end
local command = string.format(
[=[sleep %.2f; cache="${XDG_RUNTIME_DIR:-/tmp}/hyprland-monitor-reserved.tsv"; tmp="$cache.tmp"; /run/current-system/sw/bin/hyprctl -j monitors 2>/dev/null | /run/current-system/sw/bin/jq -r '.[] | [.name, .x, .y, .width, .height, .scale, .reserved[0], .reserved[1], .reserved[2], .reserved[3]] | @tsv' > "$tmp" && mv "$tmp" "$cache"]=],
as_number(delay, 0)
)
hl.exec_cmd("sh -lc " .. shell_quote(command))
end
local function monitor_workarea(monitor)
monitor = monitor_from_reserved_cache(monitor) or monitor
local width = logical_monitor_dimension(monitor.width, monitor.scale)
local height = logical_monitor_dimension(monitor.height, monitor.scale)
local reserved = monitor.reserved or { 0, scratchpad_fallback_reserved_top, 0, 0 }
local left = math.floor(as_number(reserved[1], 0))
local top = math.floor(as_number(reserved[2], 0))
local right = math.floor(as_number(reserved[3], 0))
local bottom = math.floor(as_number(reserved[4], 0))
local work_width = width - left - right
local work_height = height - top - bottom
if work_width <= 0 then
left = 0
right = 0
work_width = width
end
if work_height <= 0 then
top = 0
bottom = 0
work_height = height
end
return {
x = math.floor(as_number(monitor.x, 0)) + left,
y = math.floor(as_number(monitor.y, 0)) + top,
width = work_width,
height = work_height,
}
end
local function matching_scratchpad_windows(name)
local def = scratchpads[name]
local windows = {}
if not def then
return windows
end
for _, window in ipairs(hl.get_windows()) do
if scratchpad_window_matches(window, def) then
windows[#windows + 1] = window
end
end
return windows
end
local function scratchpad_geometry(name, target_monitor, position)
local def = scratchpads[name]
local monitor = target_monitor or hl.get_active_monitor()
if not def or not monitor then
return
end
local workarea = monitor_workarea(monitor)
local width
local height
local x
local y
if def.dropdown then
width = workarea.width
height = math.floor(workarea.height * dropdown_height_ratio)
x = workarea.x
y = workarea.y
if position == "above" then
y = workarea.y - height
elseif type(position) == "number" then
y = position
end
else
width = math.floor(workarea.width * scratchpad_size_ratio)
height = math.floor(workarea.height * scratchpad_size_ratio)
x = workarea.x + math.floor((workarea.width - width) / 2)
y = workarea.y + math.floor((workarea.height - height) / 2)
end
return {
width = width,
height = height,
x = x,
y = y,
}
end
local function apply_scratchpad_geometry(name, window, target_monitor, position)
local def = scratchpads[name]
if not def or not window then
return
end
local geometry = scratchpad_geometry(name, target_monitor, position)
if not geometry then
return
end
local selector = window_selector(window)
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+scratchpad", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector }))
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = selector }))
if def.dropdown then
dispatch(hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = selector }))
dispatch(hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = selector }))
end
end
local function schedule_scratchpad_geometry(name, window, target_monitor, position, timeout)
hl.timer(function()
apply_scratchpad_geometry(name, window, target_monitor, position)
end, { timeout = timeout or 50, type = "oneshot" })
end
local function dropdown_spring_progress(progress)
if progress >= 1 then
return 1
end
return 1 - (math.exp(-5.0 * progress) * math.cos(7.0 * progress))
end
local function animate_dropdown_scratchpad_down(name, window, target_monitor)
local from = scratchpad_geometry(name, target_monitor, "above")
local to = scratchpad_geometry(name, target_monitor)
if not from or not to then
schedule_scratchpad_geometry(name, window, target_monitor, nil, 35)
return
end
for frame = 1, dropdown_animation_frames do
local progress = frame / dropdown_animation_frames
local eased = dropdown_spring_progress(progress)
local y = math.floor(from.y + ((to.y - from.y) * eased) + 0.5)
schedule_scratchpad_geometry(name, window, target_monitor, y, frame * dropdown_animation_frame_ms)
end
end
local function hide_scratchpad_window(name, window)
remove_minimized_window(window)
move_window_to_workspace(scratchpad_workspace(name), false, window)
end
local function show_scratchpad_window(name, window, workspace, target_monitor)
workspace = workspace or active_workspace()
if not workspace then
return
end
remove_minimized_window(window)
if scratchpads[name] and scratchpads[name].dropdown then
apply_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), "above")
end
move_window_to_workspace(workspace.id, false, window)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
if scratchpads[name] and scratchpads[name].dropdown then
animate_dropdown_scratchpad_down(name, window, target_monitor or hl.get_active_monitor())
else
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
end
end
local function scratchpad_is_visible(window)
local workspace = active_workspace()
return workspace and window and same_workspace(window.workspace, workspace)
end
-- Active scratchpads are scratchpad windows visible on the active workspace.
-- Invoking a different scratchpad replaces that active set.
local function active_scratchpad_windows(except_name)
local windows = {}
for _, window in ipairs(hl.get_windows()) do
local name = matching_scratchpad_name(window)
if name and name ~= except_name and scratchpad_is_visible(window) then
windows[#windows + 1] = {
name = name,
window = window,
}
end
end
return windows
end
local function hide_active_scratchpads(except_name)
for _, active in ipairs(active_scratchpad_windows(except_name)) do
hide_scratchpad_window(active.name, active.window)
end
end
local function refresh_active_scratchpad_geometries()
local monitor = hl.get_active_monitor()
for _, active in ipairs(active_scratchpad_windows()) do
schedule_scratchpad_geometry(active.name, active.window, monitor)
end
end
local function refresh_active_scratchpad_geometries_later(timeout)
hl.timer(refresh_active_scratchpad_geometries, { timeout = timeout or 300, type = "oneshot" })
end
local function refresh_shell_workarea_and_scratchpads()
refresh_monitor_reserved_cache(0.15)
refresh_active_scratchpad_geometries_later(400)
end
local function adopt_matching_scratchpad_window(window)
if not window then
return
end
for name, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
if scratchpad_pending[name] then
local pending = scratchpad_pending[name]
scratchpad_pending[name] = nil
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor())
elseif scratchpad_is_visible(window) then
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
end
end
end
end
local function toggle_scratchpad(name)
local def = scratchpads[name]
if not def then
return
end
if current_layout == monocle_layout then
set_layout(columns_layout)
end
local windows = matching_scratchpad_windows(name)
if #windows == 0 then
hide_active_scratchpads(name)
scratchpad_pending[name] = {
monitor = hl.get_active_monitor(),
workspace = active_workspace(),
}
hl.exec_cmd(def.command)
return
end
local any_visible = false
for _, window in ipairs(windows) do
if scratchpad_is_visible(window) then
any_visible = true
break
end
end
if any_visible then
for _, window in ipairs(windows) do
hide_scratchpad_window(name, window)
end
else
hide_active_scratchpads(name)
local workspace = active_workspace()
local target_monitor = hl.get_active_monitor()
for _, window in ipairs(windows) do
show_scratchpad_window(name, window, workspace, target_monitor)
end
end
end
ctx.lower_contains = lower_contains
ctx.lower_contains_any = lower_contains_any
ctx.scratchpad_window_matches = scratchpad_window_matches
ctx.is_scratchpad_window = is_scratchpad_window
ctx.matching_scratchpad_name = matching_scratchpad_name
ctx.scratchpad_workspace = scratchpad_workspace
ctx.as_number = as_number
ctx.logical_monitor_dimension = logical_monitor_dimension
ctx.split_tsv = split_tsv
ctx.monitor_from_reserved_fields = monitor_from_reserved_fields
ctx.monitor_from_reserved_lines = monitor_from_reserved_lines
ctx.monitor_from_reserved_cache = monitor_from_reserved_cache
ctx.refresh_monitor_reserved_cache = refresh_monitor_reserved_cache
ctx.monitor_workarea = monitor_workarea
ctx.scratchpad_geometry = scratchpad_geometry
ctx.matching_scratchpad_windows = matching_scratchpad_windows
ctx.apply_scratchpad_geometry = apply_scratchpad_geometry
ctx.schedule_scratchpad_geometry = schedule_scratchpad_geometry
ctx.dropdown_spring_progress = dropdown_spring_progress
ctx.animate_dropdown_scratchpad_down = animate_dropdown_scratchpad_down
ctx.hide_scratchpad_window = hide_scratchpad_window
ctx.show_scratchpad_window = show_scratchpad_window
ctx.scratchpad_is_visible = scratchpad_is_visible
ctx.active_scratchpad_windows = active_scratchpad_windows
ctx.hide_active_scratchpads = hide_active_scratchpads
ctx.refresh_active_scratchpad_geometries = refresh_active_scratchpad_geometries
ctx.refresh_active_scratchpad_geometries_later = refresh_active_scratchpad_geometries_later
ctx.refresh_shell_workarea_and_scratchpads = refresh_shell_workarea_and_scratchpads
ctx.adopt_matching_scratchpad_window = adopt_matching_scratchpad_window
ctx.toggle_scratchpad = toggle_scratchpad
end
return M

View File

@@ -1,422 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local file_chooser_title_rule = "^(Open File|Open Files|Save File|Save Files|Save As|Select File|Select Files|Choose File|Choose Files|File Upload|Upload File|Upload Files|Select Folder|Choose Folder|Open Folder|Save Folder)$"
local function lower_string(value)
return string.lower(tostring(value or ""))
end
local function title_indicates_file_chooser(title)
title = lower_string(title)
if title == "" then
return false
end
for _, exact in ipairs({
"open file",
"open files",
"save file",
"save files",
"save as",
"select file",
"select files",
"choose file",
"choose files",
"file upload",
"upload file",
"upload files",
"select folder",
"choose folder",
"open folder",
"save folder",
}) do
if title == exact then
return true
end
end
return title:find("file chooser", 1, true) ~= nil
or title:find("file picker", 1, true) ~= nil
end
local function is_file_chooser_window(window)
return window
and (title_indicates_file_chooser(window.title) or title_indicates_file_chooser(window.initial_title))
end
local function raise_file_chooser_window(window)
if verify_config or not is_file_chooser_window(window) then
return
end
local selector = window_selector(window)
if not selector then
return
end
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.center({ window = selector }))
dispatch(hl.dsp.focus({ window = selector }))
dispatch(hl.dsp.window.bring_to_top({ window = selector }))
end
local function raise_file_chooser_window_later(window, timeout)
hl.timer(function()
local refreshed = window and window.address and hl.get_window(window_selector(window)) or window
raise_file_chooser_window(refreshed)
end, { timeout = timeout or 50, type = "oneshot" })
end
if enable_nstack and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so")
end
if enable_hyprexpo and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
end
if enable_hyprwinview and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwinview.so")
end
if enable_workspace_history and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhypr-workspace-history.so")
end
if enable_hyprwobbly and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwobbly.so")
end
if enable_hyprglass and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/hyprglass.so")
end
hl.env("XCURSOR_SIZE", "24")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("HYPR_MAX_WORKSPACE", "9")
hl.config({
input = {
kb_layout = "us",
kb_variant = "",
kb_model = "",
kb_options = "",
kb_rules = "",
follow_mouse = 1,
sensitivity = 0,
touchpad = {
natural_scroll = false,
},
},
cursor = {
persistent_warps = true,
},
general = {
gaps_in = 5,
gaps_out = 10,
border_size = 2,
col = {
active_border = { colors = { "rgba(3b82f6ee)", "rgba(33ccffee)" }, angle = 45 },
inactive_border = "rgba(00000000)",
},
layout = columns_layout,
allow_tearing = false,
},
decoration = {
rounding = 5,
blur = {
enabled = true,
size = 7,
passes = 3,
},
active_opacity = 1.0,
inactive_opacity = 0.65,
},
animations = {
enabled = true,
},
binds = {
allow_workspace_cycles = true,
workspace_back_and_forth = true,
},
group = {
group_on_movetoworkspace = false,
col = {
border_active = "rgba(edb443ff)",
border_inactive = "rgba(091f2eff)",
},
groupbar = {
enabled = true,
blur = true,
font_size = 13,
gradients = true,
height = 26,
indicator_gap = 0,
indicator_height = 1,
rounding = 5,
gradient_rounding = 5,
text_padding = 8,
col = {
active = "rgba(edb443ff)",
inactive = "rgba(101820f2)",
},
text_color = "rgba(091018ff)",
text_color_inactive = "rgba(f2f5f7ff)",
},
},
misc = {
force_default_wallpaper = 0,
disable_hyprland_logo = true,
exit_window_retains_fullscreen = true,
},
})
hl.curve("overshoot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
hl.curve("smoothOut", { type = "bezier", points = { { 0.36, 1 }, { 0.3, 1 } } })
hl.curve("smoothInOut", { type = "bezier", points = { { 0.42, 0 }, { 0.58, 1 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
local spring_time_scale = 5
local function spring_curve(mass, stiffness, dampening)
return {
type = "spring",
mass = mass,
stiffness = stiffness * spring_time_scale * spring_time_scale,
dampening = dampening * spring_time_scale,
}
end
hl.curve("workspaceSpring", spring_curve(2.4, 38, 8))
hl.curve("windowSpring", spring_curve(2.5, 40, 10))
local animations = {
{ leaf = "global", enabled = true, speed = 8, bezier = "default" },
{ leaf = "windows", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
{ leaf = "windowsIn", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
{ leaf = "windowsOut", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
{ leaf = "windowsMove", enabled = true, speed = 8, spring = "windowSpring" },
{ leaf = "border", enabled = false },
{ leaf = "borderangle", enabled = false },
{ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeSwitch", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeShadow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeGlow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayers", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopups", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDpms", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "layers", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersIn", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersOut", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "workspaces", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
{ leaf = "workspacesIn", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
{ leaf = "workspacesOut", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
{ leaf = "specialWorkspace", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
{ leaf = "specialWorkspaceIn", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
{ leaf = "specialWorkspaceOut", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
{ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "smoothOut" },
-- Disabled for now: Hyprland 0.54.0 can crash while damaging a monitor
-- from this startup animation's update callback during output discovery.
-- { leaf = "monitorAdded", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "monitorAdded", enabled = false, speed = 5, bezier = "smoothOut" },
}
for _, animation in ipairs(animations) do
hl.animation(animation)
end
local function apply_hyprglass_config()
if verify_config or not enable_hyprglass then
return
end
hl.config({
plugin = {
hyprglass = {
enabled = 0,
default_theme = "dark",
default_preset = "default",
},
},
})
end
local function apply_hyprwobbly_config()
if verify_config or not enable_hyprwobbly then
return
end
hl.config({
plugin = {
hyprwobbly = {
enabled = hypr_visual_performance_mode and 0 or 1,
mode = "always",
grid_width = 4,
grid_height = 4,
tiles_x = 12,
tiles_y = 12,
spring_k = 18.0,
friction = 8.0,
mass = 12.0,
move_factor = 0.65,
resize_factor = 0.45,
max_warp = 140.0,
},
},
})
end
local function apply_visual_performance_mode()
if verify_config then
return
end
local visual_effects_enabled = not hypr_visual_performance_mode
hl.config({
decoration = {
blur = {
enabled = visual_effects_enabled,
},
},
animations = {
enabled = visual_effects_enabled,
},
})
if enable_hyprwobbly then
hl.config({
plugin = {
hyprwobbly = {
enabled = visual_effects_enabled and 1 or 0,
},
},
})
end
end
local function toggle_visual_performance_mode()
hypr_visual_performance_mode = not hypr_visual_performance_mode
apply_visual_performance_mode()
hl.notification.create({
text = "Hyprland performance mode: " .. (hypr_visual_performance_mode and "on" or "off"),
duration = 1800,
icon = hypr_visual_performance_mode and notification_icons.warning or notification_icons.ok,
color = hypr_visual_performance_mode and "rgba(edb443ff)" or "rgba(33ccffee)",
font_size = 13,
})
end
local function apply_rules()
if verify_config then
return
end
hl.workspace_rule({ workspace = "w[tv1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.workspace_rule({ workspace = "f[1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.window_rule({ match = { class = "^()$", title = "^()$" }, float = true })
hl.window_rule({ match = { title = "^(Picture-in-Picture)$" }, float = true })
hl.window_rule({
name = "rofi-glass-window",
match = { class = "^(rofi)$" },
float = true,
center = true,
decorate = false,
no_shadow = true,
xray = false,
})
hl.layer_rule({
name = "rofi-glass-layer",
match = { namespace = "^(rofi)$" },
blur = true,
ignore_alpha = 0.05,
xray = false,
})
hl.window_rule({
name = "file-chooser-dialogs",
match = { title = file_chooser_title_rule },
float = true,
center = true,
focus_on_activate = true,
stay_focused = true,
})
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
for index, match in ipairs({
{ class = "^(flameshot)$" },
{ title = "^(flameshot)$" },
}) do
hl.window_rule({
name = "flameshot-overlay-" .. tostring(index),
match = match,
float = true,
no_anim = true,
suppress_event = "fullscreen",
})
end
hl.layer_rule({
name = "flameshot-layer-overlay",
match = { namespace = "^(flameshot)$" },
no_anim = true,
})
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
no_anim = true,
})
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
tag = "+hyprglass_enabled",
})
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
tag = "+hyprglass_theme_light",
})
hl.window_rule({
match = { class = "^(.*[Rr]umno.*)$" },
float = true,
pin = true,
center = true,
decorate = false,
no_shadow = true,
})
hl.window_rule({
match = { title = "^(.*[Rr]umno.*)$" },
float = true,
pin = true,
center = true,
decorate = false,
no_shadow = true,
})
hl.window_rule({
name = "subtle-pinned-window-border",
match = { pin = true },
border_size = 2,
border_color = "rgba(edb443ff) rgba(ff4d5dcc)",
})
hl.window_rule({
match = { tag = inactive_opacity_override_tag },
opacity = "1.0 override 1.0 override 1.0 override",
})
end
ctx.apply_rules = apply_rules
ctx.apply_hyprglass_config = apply_hyprglass_config
ctx.apply_hyprwobbly_config = apply_hyprwobbly_config
ctx.apply_visual_performance_mode = apply_visual_performance_mode
ctx.is_file_chooser_window = is_file_chooser_window
ctx.raise_file_chooser_window = raise_file_chooser_window
ctx.raise_file_chooser_window_later = raise_file_chooser_window_later
ctx.toggle_visual_performance_mode = toggle_visual_performance_mode
end
return M

View File

@@ -1,63 +0,0 @@
local shell_ui_command = "hypr_shell_ui"
local columns_layout = "nStack"
local large_main_layout = "master"
local grid_layout = "grid"
local monocle_layout = "monocle"
return {
main_mod = "SUPER",
mod_alt = "SUPER + ALT",
hyper = "SUPER + CTRL + ALT",
terminal = "ghostty --gtk-single-instance=false",
shell_ui_command = shell_ui_command,
launcher_command = shell_ui_command .. " launcher",
run_menu = shell_ui_command .. " run",
-- Hyprland shadows ordinary keybinds after one fires; without transparent,
-- the first overview chord after a focus-moving bind can be skipped.
overview_bind_opts = { dont_inhibit = true, transparent = true },
overview_trace_enabled_path = "/tmp/hypr-overview-bind.enable",
overview_trace_path = "/tmp/hypr-overview-bind.log",
notification_icons = {
warning = 0,
info = 1,
hint = 2,
error = 3,
confused = 4,
ok = 5,
none = 6,
},
max_workspace = 9,
columns_layout = columns_layout,
large_main_layout = large_main_layout,
grid_layout = grid_layout,
monocle_layout = monocle_layout,
layout_cycle = { columns_layout, large_main_layout, grid_layout },
layout_names = {
[columns_layout] = "Columns",
[large_main_layout] = "Large main",
[grid_layout] = "Grid",
[monocle_layout] = "Monocle",
},
minimized_workspace = "special:minimized",
inactive_opacity_override_tag = "no-inactive-opacity",
tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-",
current_layout = columns_layout,
enable_nstack = true,
enable_hyprexpo = true,
enable_hyprwinview = true,
enable_workspace_history = true,
enable_hyprwobbly = true,
enable_hyprglass = false,
hypr_visual_performance_mode = false,
configure_nstack_plugin_from_lua = false,
workspace_layouts = {},
minimized_windows = {},
tabbed_workspace_groups = {},
window_picker_mode = nil,
window_picker_candidates = {},
stack_update_timer = nil,
monocle_notice = nil,
}

View File

@@ -1,502 +0,0 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function same_class_windows(class_name)
local windows = {}
if not class_name or class_name == "" then
return windows
end
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class == class_name then
windows[#windows + 1] = window
end
end
return windows
end
local function short_text(value, limit)
value = tostring(value or "")
value = value:gsub("[%c\t\r\n]", " ")
if #value <= limit then
return value
end
return value:sub(1, limit - 3) .. "..."
end
local function normal_windows()
local windows = {}
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) then
windows[#windows + 1] = window
end
end
table.sort(windows, function(left, right)
local left_workspace = left.workspace and left.workspace.id or max_workspace + 1
local right_workspace = right.workspace and right.workspace.id or max_workspace + 1
if left_workspace ~= right_workspace then
return left_workspace < right_workspace
end
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
return windows
end
local function window_picker_entry(index, window)
local workspace = window.workspace and window.workspace.id or "?"
local class = short_text(window.class, 18)
local title = short_text(window.title, 48)
return tostring(index) .. " [" .. tostring(workspace) .. "] " .. class .. " " .. title
end
local function remove_minimized_window(target)
local remaining = {}
local target_address = target and target.address
for _, window in ipairs(minimized_windows) do
if window and window.address ~= target_address then
remaining[#remaining + 1] = window
end
end
minimized_windows = remaining
end
local function add_minimized_window(window)
if not window or not window.address then
return
end
remove_minimized_window(window)
minimized_windows[#minimized_windows + 1] = window
end
local function hydrate_minimized_windows()
local by_address = {}
local current_by_address = {}
local hydrated = {}
for _, window in ipairs(hl.get_windows()) do
if window and window.address then
current_by_address[window.address] = window
end
end
for _, window in ipairs(minimized_windows) do
local current = window and window.address and current_by_address[window.address]
if current and is_minimized_window(current) and not by_address[current.address] then
by_address[current.address] = true
hydrated[#hydrated + 1] = current
end
end
for _, window in pairs(current_by_address) do
if window and window.address and is_minimized_window(window) and not by_address[window.address] then
by_address[window.address] = true
hydrated[#hydrated + 1] = window
end
end
minimized_windows = hydrated
end
local function float_active_window_preserving_tiled_geometry()
local geometry = tiled_window_geometry(hl.get_active_window())
dispatch(hl.dsp.window.float({ action = "enable", window = geometry and geometry.selector or nil }))
if geometry then
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = geometry.selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = geometry.selector }))
end
return geometry
end
local function float_and_drag_active_window()
float_active_window_preserving_tiled_geometry()
dispatch(hl.dsp.window.drag())
end
local function float_and_resize_active_window()
float_active_window_preserving_tiled_geometry()
dispatch(hl.dsp.window.resize())
end
local function toggle_pinned_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
if not window or not selector then
return
end
if window.pinned then
dispatch(hl.dsp.window.pin({ action = "disable", window = selector }))
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
return
end
if not window.floating then
float_active_window_preserving_tiled_geometry()
end
dispatch(hl.dsp.window.pin({ action = "enable", window = selector }))
end
local function current_minimized_windows()
hydrate_minimized_windows()
local windows = {}
for _, window in ipairs(minimized_windows) do
if window and window.address and is_minimized_window(window) then
windows[#windows + 1] = window
end
end
minimized_windows = windows
return windows
end
local function restore_minimized_window(window, workspace)
if not window or not workspace then
return false
end
move_window_to_workspace(workspace.id, false, window)
return true
end
local function window_picker_candidates_for(mode)
if mode == "minimized" then
return current_minimized_windows()
end
local focused = hl.get_active_window()
local workspace = active_workspace()
local candidates = {}
for _, window in ipairs(normal_windows()) do
local include = true
if mode == "bring" and workspace and window.workspace == workspace then
include = false
elseif mode == "replace" and focused and window == focused then
include = false
end
if include then
candidates[#candidates + 1] = window
end
end
return candidates
end
local function activate_window_picker_candidate(index)
local window = window_picker_candidates[index]
local mode = window_picker_mode
window_picker_mode = nil
window_picker_candidates = {}
dispatch(hl.dsp.submap("reset"))
if not window then
return
end
if mode == "go" then
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
local workspace = active_workspace()
if mode == "bring" and workspace then
move_window_to_workspace(workspace.id, false, window)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
if mode == "minimized" and workspace then
remove_minimized_window(window)
restore_minimized_window(window, workspace)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
if mode == "replace" then
local focused = hl.get_active_window()
if focused and focused ~= window then
dispatch(hl.dsp.window.swap({ target = window_selector(window), window = window_selector(focused) }))
dispatch(hl.dsp.focus({ window = window_selector(window) }))
end
end
end
local function enter_window_picker(mode)
window_picker_mode = mode
window_picker_candidates = window_picker_candidates_for(mode)
if #window_picker_candidates == 0 then
local empty_text = "No windows available"
if mode == "minimized" then
empty_text = "No minimized windows"
end
hl.notification.create({
text = empty_text,
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
local lines = {}
local count = math.min(#window_picker_candidates, 9)
for i = 1, count do
lines[#lines + 1] = window_picker_entry(i, window_picker_candidates[i])
end
hl.notification.create({
text = table.concat(lines, "\n"),
duration = 5000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 11,
})
dispatch(hl.dsp.submap("window-picker"))
end
local function gather_focused_class()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace or not focused.class or focused.class == "" then
return
end
local count = 0
for _, window in ipairs(same_class_windows(focused.class)) do
if window ~= focused and window.workspace ~= workspace then
move_window_to_workspace(workspace.id, false, window)
count = count + 1
end
end
hl.notification.create({
text = "Gathered " .. tostring(count) .. " " .. focused.class .. " windows",
duration = 1600,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function focus_next_class()
local focused = hl.get_active_window()
if not focused or not focused.class or focused.class == "" then
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
return
end
local classes = {}
local first_by_class = {}
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and window.class ~= "" and not first_by_class[window.class] then
first_by_class[window.class] = window
classes[#classes + 1] = window.class
end
end
table.sort(classes)
if #classes <= 1 then
return
end
local current_index = 1
for index, class_name in ipairs(classes) do
if class_name == focused.class then
current_index = index
break
end
end
local next_class = classes[(current_index % #classes) + 1]
local target = first_by_class[next_class]
if target then
dispatch(hl.dsp.focus({ window = window_selector(target) }))
end
end
local function show_active_window_info()
local window = hl.get_active_window()
if not window then
hl.notification.create({
text = "No active window",
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
local workspace = window.workspace and (window.workspace.name or window.workspace.id) or "?"
local lines = {
"Class: " .. tostring(window.class or ""),
"Title: " .. tostring(window.title or ""),
"Workspace: " .. tostring(workspace),
"Pinned: " .. tostring(window.pinned or false),
"Address: " .. tostring(window.address or ""),
"PID: " .. tostring(window.pid or ""),
}
hl.notification.create({
text = table.concat(lines, "\n"),
duration = 5000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 11,
})
end
local function window_has_tag(window, tag)
for _, value in ipairs((window and window.tags) or {}) do
if tostring(value):gsub("%*$", "") == tag then
return true
end
end
return false
end
local function toggle_inactive_opacity_for_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
if not selector then
return
end
local disabling_reduction = not window_has_tag(window, inactive_opacity_override_tag)
dispatch(hl.dsp.window.tag({ tag = inactive_opacity_override_tag, window = selector }))
hl.notification.create({
text = "Inactive opacity reduction: " .. (disabling_reduction and "off for window" or "on for window"),
duration = 1600,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function raise_or_spawn(class_fragment, command)
local fragment = string.lower(class_fragment)
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and string.find(string.lower(window.class), fragment, 1, true) then
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
end
hl.exec_cmd(command)
end
local function minimize_active_window()
local window = hl.get_active_window()
if not window then
return
end
add_minimized_window(window)
move_window_to_workspace(minimized_workspace, false, window)
end
local function restore_last_minimized()
local workspace = active_workspace()
if not workspace then
return
end
hydrate_minimized_windows()
while #minimized_windows > 0 do
local window = table.remove(minimized_windows)
if window and window.address and is_minimized_window(window) then
restore_minimized_window(window, workspace)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
end
end
local function restore_all_minimized()
local workspace = active_workspace()
if not workspace then
return
end
hydrate_minimized_windows()
while #minimized_windows > 0 do
restore_minimized_window(table.remove(minimized_windows), workspace)
end
end
local function minimize_other_classes()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace then
return
end
for _, window in ipairs(tiled_windows(workspace)) do
if window ~= focused and window.class ~= focused.class then
add_minimized_window(window)
move_window_to_workspace(minimized_workspace, false, window)
end
end
end
local function restore_focused_class()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace or not focused.class then
return
end
hydrate_minimized_windows()
local remaining = {}
for _, window in ipairs(minimized_windows) do
if window and window.class == focused.class and is_minimized_window(window) then
restore_minimized_window(window, workspace)
else
remaining[#remaining + 1] = window
end
end
minimized_windows = remaining
end
ctx.same_class_windows = same_class_windows
ctx.short_text = short_text
ctx.normal_windows = normal_windows
ctx.window_picker_entry = window_picker_entry
ctx.remove_minimized_window = remove_minimized_window
ctx.add_minimized_window = add_minimized_window
ctx.hydrate_minimized_windows = hydrate_minimized_windows
ctx.float_active_window_preserving_tiled_geometry = float_active_window_preserving_tiled_geometry
ctx.float_and_drag_active_window = float_and_drag_active_window
ctx.float_and_resize_active_window = float_and_resize_active_window
ctx.toggle_pinned_active_window = toggle_pinned_active_window
ctx.current_minimized_windows = current_minimized_windows
ctx.restore_minimized_window = restore_minimized_window
ctx.window_picker_candidates_for = window_picker_candidates_for
ctx.activate_window_picker_candidate = activate_window_picker_candidate
ctx.enter_window_picker = enter_window_picker
ctx.gather_focused_class = gather_focused_class
ctx.focus_next_class = focus_next_class
ctx.show_active_window_info = show_active_window_info
ctx.toggle_inactive_opacity_for_active_window = toggle_inactive_opacity_for_active_window
ctx.raise_or_spawn = raise_or_spawn
ctx.minimize_active_window = minimize_active_window
ctx.restore_last_minimized = restore_last_minimized
ctx.restore_all_minimized = restore_all_minimized
ctx.minimize_other_classes = minimize_other_classes
ctx.restore_focused_class = restore_focused_class
end
return M

View File

@@ -1,39 +0,0 @@
background {
monitor =
path = screenshot
blur_passes = 3
blur_size = 8
noise = 0.0117
contrast = 0.8916
brightness = 0.8172
vibrancy = 0.1696
}
input-field {
monitor =
size = 280, 56
outline_thickness = 3
dots_size = 0.2
dots_spacing = 0.2
outer_color = rgb(edb443)
inner_color = rgb(1e1e2e)
font_color = rgb(cdd6f4)
fade_on_empty = false
rounding = 12
placeholder_text = <i>Password...</i>
hide_input = false
position = 0, -80
halign = center
valign = center
}
label {
monitor =
text = cmd[update:1000] echo "$(date +'%a %b %-d %I:%M %p')"
color = rgb(cdd6f4)
font_size = 40
font_family = Noto Sans
position = 0, 80
halign = center
valign = center
}

View File

@@ -0,0 +1,106 @@
{
"global": {
"check_for_updates_on_startup": true,
"show_in_menu_bar": true,
"show_profile_name_in_menu_bar": false
},
"profiles": [
{
"complex_modifications": {
"parameters": {
"basic.to_if_alone_timeout_milliseconds": 1000
},
"rules": [
{
"manipulators": [
{
"description": "Change right command to command+control+option+shift.",
"from": {
"key_code": "right_command",
"modifiers": {
"optional": [
"any"
]
}
},
"to": [
{
"key_code": "left_shift",
"modifiers": [
"left_command",
"left_control",
"left_option"
]
}
],
"to_if_alone": [
{
"key_code": "escape",
"modifiers": {
"optional": [
"any"
]
}
}
],
"type": "basic"
}
]
}
]
},
"devices": [
{
"disable_built_in_keyboard_if_exists": false,
"fn_function_keys": {},
"identifiers": {
"is_keyboard": true,
"is_pointing_device": false,
"product_id": 610,
"vendor_id": 1452
},
"ignore": false,
"simple_modifications": {}
},
{
"disable_built_in_keyboard_if_exists": false,
"fn_function_keys": {},
"identifiers": {
"is_keyboard": true,
"is_pointing_device": false,
"product_id": 597,
"vendor_id": 1452
},
"ignore": false,
"simple_modifications": {}
}
],
"fn_function_keys": {
"f1": "vk_consumer_brightness_down",
"f10": "mute",
"f11": "volume_down",
"f12": "volume_up",
"f2": "vk_consumer_brightness_up",
"f3": "vk_mission_control",
"f4": "vk_launchpad",
"f5": "vk_consumer_illumination_down",
"f6": "vk_consumer_illumination_up",
"f7": "vk_consumer_previous",
"f8": "vk_consumer_play",
"f9": "vk_consumer_next"
},
"name": "Default profile",
"one_to_many_mappings": {},
"selected": true,
"simple_modifications": {
"caps_lock": "left_control"
},
"standalone_keys": {},
"virtual_hid_keyboard": {
"caps_lock_delay_milliseconds": 0,
"keyboard_type": "ansi",
"standalone_keys_delay_milliseconds": 200
}
}
]
}

View File

@@ -0,0 +1,21 @@
[Added Associations]
video/x-matroska=vlc.desktop;
audio/flac=vlc.desktop;
image/jpeg=feh.desktop;
video/x-msvideo=vlc.desktop;
text/vnd.trolltech.linguist=vlc.desktop;
audio/mpeg=vlc.desktop;
application/pdf=okularApplication_pdf.desktop;
image/png=okularApplication_kimgio.desktop;
video/mp4=vlc.desktop;org.gnome.Totem.desktop;
x-scheme-handler/magnet=userapp-transmission-gtk-24GQLZ.desktop;
element=element-desktop.desktop
[Default Applications]
text/html=google-chrome.desktop
x-scheme-handler/http=google-chrome.desktop
x-scheme-handler/https=google-chrome.desktop
x-scheme-handler/about=google-chrome.desktop
x-scheme-handler/unknown=google-chrome.desktop
x-scheme-handler/magnet=userapp-transmission-gtk-24GQLZ.desktop
x-scheme-handler/element=element-desktop.desktop

View File

@@ -1,7 +0,0 @@
default {
shader /run/current-system/sw/share/neowall/shaders/train_journey_optimized.glsl
shader_speed 0.7
shader_fps 30
mode fill
duration 0
}

View File

@@ -1,7 +0,0 @@
default {
shader /run/current-system/sw/share/neowall/shaders/matrix_rain.glsl
shader_speed 0.85
shader_fps 30
mode fill
duration 0
}

View File

@@ -0,0 +1,128 @@
[remmina_pref]
secret=SEkwV+ilNl+x9eTDKU6tLKFTKdJv2OK2ROlV3Z4K0uY=
uid=Linux+4.7.4-1-ARCH+x86_64+en_US+52817413
bdate=736234
save_view_mode=true
save_when_connect=true
survey=false
invisible_toolbar=false
floating_toolbar_placement=0
toolbar_placement=3
always_show_tab=true
hide_connection_toolbar=false
default_action=0
scale_quality=3
ssh_loglevel=1
screenshot_path=/home/imalison/Pictures
ssh_parseconfig=true
hide_toolbar=false
hide_statusbar=false
small_toolbutton=false
view_file_mode=0
resolutions=640x480,800x600,1024x768,1152x864,1280x960,1400x1050
keystrokes=Send hello world§hello world\\n
main_width=668
main_height=1321
main_maximize=false
main_sort_column_id=1
main_sort_order=0
expanded_group=
toolbar_pin_down=false
sshtunnel_port=4732
applet_new_ontop=false
applet_hide_count=false
applet_enable_avahi=false
disable_tray_icon=false
dark_tray_icon=false
recent_maximum=10
default_mode=0
tab_mode=0
show_buttons_icons=0
show_menu_icons=0
auto_scroll_step=10
hostkey=65508
shortcutkey_fullscreen=102
shortcutkey_autofit=49
shortcutkey_nexttab=65363
shortcutkey_prevtab=65361
shortcutkey_scale=115
shortcutkey_grab=65508
shortcutkey_screenshot=65481
shortcutkey_minimize=65478
shortcutkey_disconnect=65473
shortcutkey_toolbar=116
vte_font=
vte_allow_bold_text=true
vte_lines=512
vte_system_colors=false
vte_foreground_color=rgb(192,192,192)
vte_background_color=rgb(0,0,0)
rdp_use_client_keymap=0
rdp_quality_0=6F
rdp_quality_1=7
rdp_quality_2=1
rdp_quality_9=80
datadir_path=
remmina_file_name=%G_%P_%N_%h
screenshot_name=remmina_%p_%h_%Y%m%d-%H%M%S
deny_screenshot_clipboard=true
confirm_close=true
use_primary_password=false
unlock_timeout=300
unlock_password=
lock_connect=false
lock_edit=false
lock_view_passwords=false
enc_mode=1
audit=false
trust_all=false
prevent_snap_welcome_message=false
last_quickconnect_protocol=
fullscreen_on_auto=true
always_show_notes=false
hide_searchbar=false
ssh_tcp_keepidle=20
ssh_tcp_keepintvl=10
ssh_tcp_keepcnt=3
ssh_tcp_usrtimeout=60000
dark_theme=false
fullscreen_toolbar_visibility=0
shortcutkey_multimon=65365
shortcutkey_viewonly=109
vte_shortcutkey_copy=99
vte_shortcutkey_paste=118
vte_shortcutkey_select_all=97
vte_shortcutkey_increase_font=65365
vte_shortcutkey_decrease_font=65366
vte_shortcutkey_search_text=103
grab_color=#00ff00
grab_color_switch=false
[ssh_colors]
background=#d5ccba
cursor=#45373c
cursor_foreground=#d5ccba
highlight=#45373c
highlight_foreground=#d5ccba
colorBD=#45373c
foreground=#45373c
color0=#20111b
color1=#be100e
color2=#858162
color3=#eaa549
color4=#426a79
color5=#97522c
color6=#989a9c
color7=#968c83
color8=#5e5252
color9=#be100e
color10=#858162
color11=#eaa549
color12=#426a79
color13=#97522c
color14=#989a9c
color15=#d5ccba
[remmina]
name=
ignore-tls-errors=1

View File

@@ -1,53 +0,0 @@
[remmina_pref]
secret=
uid=
bdate=
save_view_mode=true
save_when_connect=true
survey=false
invisible_toolbar=false
floating_toolbar_placement=0
toolbar_placement=3
always_show_tab=true
hide_connection_toolbar=false
default_action=0
scale_quality=3
ssh_loglevel=1
screenshot_path=
ssh_parseconfig=true
hide_toolbar=false
hide_statusbar=false
small_toolbutton=false
view_file_mode=0
resolutions=640x480,800x600,1024x768,1152x864,1280x960,1400x1050
main_width=0
main_height=0
main_maximize=false
main_sort_column_id=1
main_sort_order=0
expanded_group=
toolbar_pin_down=false
sshtunnel_port=4732
applet_new_ontop=false
applet_hide_count=false
applet_enable_avahi=false
disable_tray_icon=false
dark_tray_icon=false
recent_maximum=10
default_mode=0
tab_mode=0
show_buttons_icons=0
show_menu_icons=0
auto_scroll_step=10
confirm_close=true
use_primary_password=false
unlock_timeout=300
unlock_password=
lock_connect=false
lock_edit=false
lock_view_passwords=false
enc_mode=1
audit=false
trust_all=false
prevent_snap_welcome_message=false
last_quickconnect_protocol=

View File

@@ -1,780 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import Control.Concurrent (forkIO)
import Data.Bits ((.&.), complement)
import Data.Char (toLower)
import Data.Function (on)
import Data.List (find, foldl', isInfixOf, isPrefixOf, minimumBy)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Typeable (Typeable)
import Data.Word (Word32)
import Graphics.X11.ExtraTypes.XF86
import System.Exit (ExitCode(..))
import System.IO (hFlush, stdout)
import System.Process (readCreateProcessWithExitCode, shell, spawnCommand, waitForProcess)
import XMonad
import qualified XMonad.Layout.Renamed as RN
import XMonad.River.WindowManager
import XMonad.River.WindowManager.Wayland
import qualified XMonad.StackSet as W
data Direction = DirectionUp | DirectionDown | DirectionLeft | DirectionRight
deriving (Eq, Show)
data EqualColumns a = EqualColumns
deriving (Read, Show, Typeable)
instance LayoutClass EqualColumns a where
description _ = "Columns"
pureLayout _ rect stack =
zip windows (equalColumnRects rect (length windows))
where
windows = W.integrate stack
main :: IO ()
main = do
let bindings = keyBindings
configLog $ "starting imalison-river-xmonad with keybindings=" ++ show (length bindings)
initialState <- initialRiverWMState riverConfig
runRiverWMWaylandConfig
RiverWMWaylandConfig
{ riverWMWaylandInitialState = initialState
, riverWMWaylandKeyBindings = bindings
}
riverLayouts =
renamed "Columns" EqualColumns
||| Full
where
renamed name = RN.renamed [RN.Replace name]
riverConfig =
(defaultRiverWMConfig riverLayouts)
{ riverWMWorkspaces = ordinaryWorkspaces ++ specialWorkspaces
, riverWMMouseFollowsFocus = True
, riverWMBorderWidth = 2
, riverWMFocusedBorderColor = rgba8 0xed 0xb4 0x43 0xee
, riverWMUnfocusedBorderColor = rgba8 0x59 0x59 0x59 0xaa
}
rgba8 :: Word32 -> Word32 -> Word32 -> Word32 -> RiverWMColor
rgba8 red green blue alpha =
RiverWMColor (wide red) (wide green) (wide blue) (wide alpha)
where
wide component = component * 0x01010101
keyBindings
:: (LayoutClass l Window, Read (l Window))
=> [RiverWMWaylandKeyBinding l]
keyBindings =
addHyperChordBindings hyper hyperChord $
concat
[ directionalBindings super directionalFocus
, directionalBindings (super .|. shift) directionalSwap
, directionalBindings (super .|. ctrl) (shiftFocusedToDirectionalScreen False)
, directionalBindings (super .|. ctrl .|. shift) shiftFocusedToEmptyWorkspaceOnDirectionalScreen
, directionalBindings hyper focusDirectionalScreen
, directionalBindings (hyper .|. shift) (shiftFocusedToDirectionalScreen True)
, workspaceBindings
, layoutBindings
, spawnBindings
, mediaBindings
]
directionalBindings
:: RiverWMWaylandModifiers
-> (Direction -> RiverWMWaylandAction l)
-> [RiverWMWaylandKeyBinding l]
directionalBindings mods command =
[ key mods xK_w (command DirectionUp)
, key mods xK_s (command DirectionDown)
, key mods xK_a (command DirectionLeft)
, key mods xK_d (command DirectionRight)
]
workspaceBindings
:: [RiverWMWaylandKeyBinding l]
workspaceBindings =
[ key (mods .|. super) keysym (action $ command workspace)
| (workspace, keysym) <- zip (map show [(1 :: Int) .. 9]) [xK_1 .. xK_9]
, (command, mods, action) <-
[ (W.greedyView, noMods, stackAction)
, (W.shift, shift, stackAction)
, (\workspaceId stackSet -> W.greedyView workspaceId (W.shift workspaceId stackSet), ctrl, stackActionWarpPointer)
]
]
layoutBindings
:: (LayoutClass l Window, Read (l Window))
=> [RiverWMWaylandKeyBinding l]
layoutBindings =
[ key super xK_space (layoutAction NextLayout)
, key (super .|. shift) xK_space (layoutAction (JumpToLayout "Columns"))
, key (super .|. ctrl) xK_space (layoutAction (JumpToLayout "Full"))
, key super xK_bracketleft (layoutAction Shrink)
, key super xK_bracketright (layoutAction Expand)
, key super xK_comma (layoutAction (IncMasterN 1))
, key super xK_period (layoutAction (IncMasterN (-1)))
]
spawnBindings
:: [RiverWMWaylandKeyBinding l]
spawnBindings =
[ key super xK_Return (spawnAction "ghostty --gtk-single-instance=false")
, key (super .|. shift) xK_Return (spawnAction "ghostty --gtk-single-instance=false")
, key super xK_p (spawnAction "rofi -show drun -show-icons")
, key (super .|. shift) xK_p (spawnAction "rofi -show run")
, key super xK_Tab (selectWindowAction "windows" focusSelectedWindow)
, key super xK_g (selectWindowAction "go to window" focusSelectedWindow)
, key super xK_b (selectWindowAction "bring window" bringSelectedWindow)
, key (super .|. shift) xK_b (selectWindowAction "replace window" replaceSelectedWindow)
, key super xK_m minimizeFocusedWindow
, key (super .|. shift) xK_m restoreLastMinimizedWindow
, key super xK_q (spawnAction "river-xmonad-restart")
, key (super .|. shift) xK_c closeFocusedWindow
, key (super .|. shift) xK_q (spawnAction "riverctl exit")
, key (super .|. alt) xK_e (toggleScratchpad "element")
, key (super .|. alt) xK_h (toggleScratchpad "htop")
, key (super .|. alt) xK_k (toggleScratchpad "slack")
, key (super .|. alt) xK_s (toggleScratchpad "spotify")
, key (super .|. alt) xK_t (toggleScratchpad "transmission")
, key (super .|. alt) xK_v (toggleScratchpad "volume")
, key (super .|. alt) xK_c (spawnAction "google-chrome-stable")
, key super xK_e (spawnAction "emacsclient --eval '(emacs-everywhere)'")
, key (super .|. ctrl) xK_e (shiftFocusedToNextEmptyWorkspace False)
, key (super .|. shift) xK_e (shiftFocusedToNextEmptyWorkspace True)
, key super xK_v (spawnAction "wl-paste | wtype -")
, key super xK_x (spawnAction "rofi_command.sh")
, key hyper xK_e viewNextEmptyWorkspace
, key hyper xK_v (spawnAction "rofi -modi 'clipboard:greenclip print' -show clipboard")
, key hyper xK_p (spawnAction "rofi-pass")
, key noMods xK_Print (spawnAction "flameshot gui")
, key hyper xK_h (spawnAction "flameshot gui")
, key hyper xK_c (spawnAction "shell_command.sh")
, key hyper xK_g gatherFocusedAppId
, key (hyper .|. shift) xK_l (spawnAction "loginctl lock-session")
, key hyper xK_k (spawnAction "rofi_kill_process.sh")
, key (hyper .|. shift) xK_k (spawnAction "rofi_kill_all.sh")
, key hyper xK_r (spawnAction "rofi_systemd_mono")
, key hyper xK_9 (spawnAction "start_synergy.sh")
, key hyper xK_backslash (spawnAction "$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")
, key hyper xK_i (spawnAction "rofi_select_input.hs")
, key hyper xK_o (spawnAction "rofi_paswitch")
, key hyper xK_comma (spawnAction "rofi_wallpaper.sh")
, key hyper xK_slash (spawnAction "toggle_taffybar")
, key hyper xK_y (spawnAction "rofi_agentic_skill")
]
mediaBindings
:: [RiverWMWaylandKeyBinding l]
mediaBindings =
[ key super xK_semicolon (spawnAction "playerctl play-pause")
, key noMods xF86XK_AudioPause (spawnAction "playerctl play-pause")
, key noMods xF86XK_AudioPlay (spawnAction "playerctl play-pause")
, key super xK_l (spawnAction "playerctl next")
, key noMods xF86XK_AudioNext (spawnAction "playerctl next")
, key super xK_j (spawnAction "playerctl previous")
, key noMods xF86XK_AudioPrev (spawnAction "playerctl previous")
, key noMods xF86XK_AudioRaiseVolume (spawnAction "set_volume --unmute --change-volume +5")
, key noMods xF86XK_AudioLowerVolume (spawnAction "set_volume --unmute --change-volume -5")
, key noMods xF86XK_AudioMute (spawnAction "set_volume --toggle-mute")
, key super xK_i (spawnAction "set_volume --unmute --change-volume +5")
, key super xK_k (spawnAction "set_volume --unmute --change-volume -5")
, key super xK_u (spawnAction "set_volume --toggle-mute")
, key (hyper .|. shift) xK_q (spawnAction "toggle_mute_current_window.sh")
, key (hyper .|. ctrl) xK_q (spawnAction "toggle_mute_current_window.sh only")
, key noMods xF86XK_MonBrightnessUp (spawnAction "brightness.sh up")
, key noMods xF86XK_MonBrightnessDown (spawnAction "brightness.sh down")
]
key
:: RiverWMWaylandModifiers
-> KeySym
-> RiverWMWaylandAction l
-> RiverWMWaylandKeyBinding l
key modifiers keysym action =
RiverWMWaylandKeyBinding
{ riverWMWaylandKeyModifiers = modifiers
, riverWMWaylandKeyKeysym = fromIntegral keysym
, riverWMWaylandKeyAction = action
}
spawnAction :: String -> RiverWMWaylandAction l
spawnAction command state = do
configLog $ "spawn start: " ++ command
process <- spawnCommand (riverSpawnPrelude ++ command)
_ <- forkIO $ do
exitCode <- waitForProcess process
configLog $ "spawn exit: " ++ command ++ " -> " ++ show exitCode
pure ()
pure ([], state)
riverSpawnPrelude :: String
riverSpawnPrelude =
"XDG_RUNTIME_DIR=\"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}\"; "
++ "export XDG_RUNTIME_DIR; "
++ "if [ -z \"${WAYLAND_DISPLAY:-}\" ]; then "
++ "for socket in \"$XDG_RUNTIME_DIR\"/wayland-*; do "
++ "[ -S \"$socket\" ] || continue; "
++ "WAYLAND_DISPLAY=\"$(basename \"$socket\")\"; "
++ "break; "
++ "done; "
++ "fi; "
++ "export WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:-wayland-1}\"; "
++ "export XDG_CURRENT_DESKTOP=river; "
++ "export XDG_SESSION_DESKTOP=river-xmonad; "
++ "export XDG_SESSION_TYPE=wayland; "
++ "export IMALISON_SESSION_TYPE=wayland; "
++ "export IMALISON_WINDOW_MANAGER=river-xmonad; "
configLog :: String -> IO ()
configLog message = do
putStrLn $ "imalison-river-xmonad: " ++ message
hFlush stdout
layoutAction
:: (LayoutClass l Window, Read (l Window), Message message)
=> message
-> RiverWMWaylandAction l
layoutAction = handleRiverWMLayoutMessage
stackAction
:: (W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail
-> W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail)
-> RiverWMWaylandAction l
stackAction f state =
pure $ modifyRiverWMStackSet f state
stackActionWarpPointer
:: (W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail
-> W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail)
-> RiverWMWaylandAction l
stackActionWarpPointer f state =
pure $ modifyRiverWMStackSetAndWarpPointer f state
data ScratchpadDefinition = ScratchpadDefinition
{ scratchpadName :: !String
, scratchpadCommand :: !String
, scratchpadMatches :: !(RiverWMWindowState -> Bool)
}
ordinaryWorkspaces :: [WorkspaceId]
ordinaryWorkspaces = map show [(1 :: Int) .. 9]
minimizedWorkspace :: WorkspaceId
minimizedWorkspace = "__minimized"
specialWorkspaces :: [WorkspaceId]
specialWorkspaces =
minimizedWorkspace : map (scratchpadWorkspace . scratchpadName) scratchpadDefinitions
scratchpadWorkspace :: String -> WorkspaceId
scratchpadWorkspace name = "__scratchpad:" ++ name
isSpecialWorkspace :: WorkspaceId -> Bool
isSpecialWorkspace workspace =
workspace == minimizedWorkspace || "__scratchpad:" `isPrefixOf` workspace
scratchpadDefinitions :: [ScratchpadDefinition]
scratchpadDefinitions =
[ ScratchpadDefinition "element" "element-desktop" $
anyMatcher [appIdMatches "Element", appIdMatches "element"]
, ScratchpadDefinition "htop" "ghostty --title=htop -e htop" $
titleContains "htop"
, ScratchpadDefinition "slack" "slack" $
anyMatcher [appIdMatches "Slack", appIdMatches "slack"]
, ScratchpadDefinition "spotify" "spotify" $
anyMatcher [appIdMatches "Spotify", appIdMatches "spotify"]
, ScratchpadDefinition "transmission" "transmission-gtk" $
anyMatcher [titleContains "Transmission", appIdContains "transmission"]
, ScratchpadDefinition "volume" "pavucontrol" $
anyMatcher [appIdMatches "Pavucontrol", appIdContains "pavucontrol"]
]
anyMatcher :: [RiverWMWindowState -> Bool] -> RiverWMWindowState -> Bool
anyMatcher matchers windowState =
any ($ windowState) matchers
appIdMatches :: String -> RiverWMWindowState -> Bool
appIdMatches expected windowState =
lower expected == maybe "" lower (riverWMWindowAppId windowState)
appIdContains :: String -> RiverWMWindowState -> Bool
appIdContains needle windowState =
lower needle `isInfixOf` maybe "" lower (riverWMWindowAppId windowState)
titleContains :: String -> RiverWMWindowState -> Bool
titleContains needle windowState =
lower needle `isInfixOf` maybe "" lower (riverWMWindowTitle windowState)
lower :: String -> String
lower = map toLower
closeFocusedWindow :: RiverWMWaylandAction l
closeFocusedWindow state@RiverWMState{riverWMStackSet, riverWMWindowIds} =
pure
( maybe [] ((: []) . RiverWMCloseWindow) $
W.peek riverWMStackSet >>= (`M.lookup` riverWMWindowIds)
, state
)
minimizeFocusedWindow :: RiverWMWaylandAction l
minimizeFocusedWindow =
stackAction $ W.shift minimizedWorkspace
restoreLastMinimizedWindow :: RiverWMWaylandAction l
restoreLastMinimizedWindow =
stackActionWarpPointer $ \stackSet ->
case workspaceFocusedWindow minimizedWorkspace stackSet of
Nothing -> stackSet
Just window ->
let currentTag = W.currentTag stackSet
in W.focusWindow window (W.shiftWin currentTag window stackSet)
toggleScratchpad :: String -> RiverWMWaylandAction l
toggleScratchpad name state@RiverWMState{riverWMStackSet} =
case find ((== name) . scratchpadName) scratchpadDefinitions of
Nothing ->
pure ([], state)
Just scratchpad ->
case W.peek riverWMStackSet of
Just focused | focused `elem` matchingWindows ->
pure $ modifyRiverWMStackSet (W.shift $ scratchpadWorkspace name) state
_ ->
case matchingWindows of
window : _ ->
pure $ modifyRiverWMStackSetAndWarpPointer (showScratchpadWindow window) state
[] ->
spawnAction (scratchpadCommand scratchpad) state
where
matchingWindows = scratchpadWindows scratchpad state
showScratchpadWindow window stackSet =
let currentTag = W.currentTag stackSet
in W.float window nearFullScratchpadRect $
W.focusWindow window (W.shiftWin currentTag window stackSet)
nearFullScratchpadRect :: W.RationalRect
nearFullScratchpadRect =
W.RationalRect left top width height
where
width = 0.9
height = 0.9
left = 0.95 - width
top = 0.95 - height
scratchpadWindows :: ScratchpadDefinition -> RiverWMState l -> [Window]
scratchpadWindows ScratchpadDefinition{scratchpadMatches} RiverWMState{riverWMWindows} =
[ riverWMWindowXWindow windowState
| windowState <- M.elems riverWMWindows
, scratchpadMatches windowState
]
selectWindowAction
:: String
-> (Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l))
-> RiverWMWaylandAction l
selectWindowAction prompt action state = do
selected <- rofiSelectWindow prompt state
pure $ maybe ([], state) (`action` state) selected
focusSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
focusSelectedWindow window state =
modifyRiverWMStackSetAndWarpPointer (focusWindowEverywhere window) state
bringSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
bringSelectedWindow window state =
modifyRiverWMStackSetAndWarpPointer (bringWindowToCurrentWorkspace window) state
replaceSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
replaceSelectedWindow selected state =
modifyRiverWMStackSetAndWarpPointer replaceWindow state
where
replaceWindow stackSet =
case (W.peek stackSet, W.findTag selected stackSet) of
(Just focused, Just selectedWorkspace)
| focused /= selected ->
W.focusWindow selected $
W.shiftWin selectedWorkspace focused $
W.shiftWin (W.currentTag stackSet) selected stackSet
_ -> stackSet
gatherFocusedAppId :: RiverWMWaylandAction l
gatherFocusedAppId state@RiverWMState{riverWMStackSet, riverWMWindowIds, riverWMWindows} =
pure $ modifyRiverWMStackSet gatherMatching state
where
focusedAppId = do
focused <- W.peek riverWMStackSet
windowId <- M.lookup focused riverWMWindowIds
riverWMWindowAppId =<< M.lookup windowId riverWMWindows
matchingWindows =
[ riverWMWindowXWindow windowState
| windowState <- M.elems riverWMWindows
, riverWMWindowAppId windowState == focusedAppId
]
gatherMatching stackSet =
case focusedAppId of
Nothing -> stackSet
Just _ ->
foldl' (\acc window -> W.shiftWin (W.currentTag acc) window acc) stackSet matchingWindows
rofiSelectWindow :: String -> RiverWMState l -> IO (Maybe Window)
rofiSelectWindow prompt state =
case windowEntries state of
[] ->
pure Nothing
entries -> do
(exitCode, selected, _stderr) <-
readCreateProcessWithExitCode
(shell $ "rofi -dmenu -i -show-icons -p " ++ shellQuote prompt)
(concatMap formatWindowEntry entries)
pure $ case exitCode of
ExitSuccess -> parseSelectedWindow selected
_ -> Nothing
data WindowEntry = WindowEntry
{ windowEntryWindow :: !Window
, windowEntryWorkspace :: !WorkspaceId
, windowEntryAppId :: !String
, windowEntryTitle :: !String
}
windowEntries :: RiverWMState l -> [WindowEntry]
windowEntries RiverWMState{riverWMStackSet, riverWMWindowIds, riverWMWindows} =
[ WindowEntry window (W.tag workspace) appId title
| workspace <- W.workspaces riverWMStackSet
, not (isSpecialWorkspace $ W.tag workspace)
, window <- W.integrate' (W.stack workspace)
, let windowId = M.lookup window riverWMWindowIds
, Just windowState <- [windowId >>= (`M.lookup` riverWMWindows)]
, let appId = fromMaybe "window" (riverWMWindowAppId windowState)
title = fromMaybe "" (riverWMWindowTitle windowState)
]
formatWindowEntry :: WindowEntry -> String
formatWindowEntry WindowEntry{..} =
visibleLabel ++ "\0icon\x1f" ++ iconName ++ "\n"
where
visibleLabel =
show windowEntryWindow
++ "\t["
++ windowEntryWorkspace
++ "] "
++ if null windowEntryTitle
then windowEntryAppId
else windowEntryAppId ++ " - " ++ windowEntryTitle
iconName = if null windowEntryAppId then "application-x-executable" else windowEntryAppId
parseSelectedWindow :: String -> Maybe Window
parseSelectedWindow selected =
case reads (takeWhile (/= '\t') $ takeWhile (/= '\0') selected) of
(window, _) : _ -> Just window
[] -> Nothing
focusWindowEverywhere
:: Eq sid
=> Window
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
focusWindowEverywhere window stackSet =
maybe stackSet (\workspace -> W.focusWindow window (W.greedyView workspace stackSet)) $
W.findTag window stackSet
bringWindowToCurrentWorkspace
:: Eq sid
=> Window
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
bringWindowToCurrentWorkspace window stackSet =
W.focusWindow window (W.shiftWin (W.currentTag stackSet) window stackSet)
workspaceFocusedWindow :: WorkspaceId -> W.StackSet WorkspaceId l Window sid sd -> Maybe Window
workspaceFocusedWindow workspace stackSet =
W.focus <$> (W.stack =<< find ((== workspace) . W.tag) (W.workspaces stackSet))
shellQuote :: String -> String
shellQuote value =
"'" ++ concatMap quoteChar value ++ "'"
where
quoteChar '\'' = "'\\''"
quoteChar char = [char]
viewNextEmptyWorkspace :: RiverWMWaylandAction l
viewNextEmptyWorkspace =
stackAction $ \stackSet ->
maybe stackSet (`W.greedyView` stackSet) (nextEmptyWorkspace stackSet)
shiftFocusedToNextEmptyWorkspace :: Bool -> RiverWMWaylandAction l
shiftFocusedToNextEmptyWorkspace follow =
(if follow then stackActionWarpPointer else stackAction) $ \stackSet ->
maybe stackSet (`shiftFocusedToWorkspace` stackSet) (nextEmptyWorkspace stackSet)
where
shiftFocusedToWorkspace workspace stackSet =
let shifted = W.shift workspace stackSet
in if follow then W.greedyView workspace shifted else shifted
nextEmptyWorkspace
:: W.StackSet WorkspaceId l Window sid sd
-> Maybe WorkspaceId
nextEmptyWorkspace stackSet =
find (`workspaceIsEmpty` stackSet) candidates
where
currentTag = W.currentTag stackSet
candidates =
case break (== currentTag) ordinaryWorkspaces of
(_before, []) -> ordinaryWorkspaces
(before, _current : after) -> after ++ before
workspaceIsEmpty
:: WorkspaceId
-> W.StackSet WorkspaceId l Window sid sd
-> Bool
workspaceIsEmpty workspace stackSet =
maybe False (null . W.integrate' . W.stack) $
find ((== workspace) . W.tag) (W.workspaces stackSet)
directionalSwap :: Direction -> RiverWMWaylandAction l
directionalSwap direction state@RiverWMState{riverWMStackSet} =
pure $ modifyRiverWMStackSet swapTarget state
where
target = directionalTargetAmong (W.index riverWMStackSet) direction state
swapTarget stackSet =
maybe (fallbackDirectionalSwap direction stackSet) (`swapFocusedWithWindow` stackSet) target
fallbackDirectionalSwap
:: Direction
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
fallbackDirectionalSwap DirectionUp = W.swapUp
fallbackDirectionalSwap DirectionLeft = W.swapUp
fallbackDirectionalSwap DirectionDown = W.swapDown
fallbackDirectionalSwap DirectionRight = W.swapDown
swapFocusedWithWindow
:: Window
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
swapFocusedWithWindow target stackSet =
case W.peek stackSet of
Just focused | focused /= target ->
W.modify' (swapStackOrder focused target) stackSet
_ -> stackSet
swapStackOrder :: Eq a => a -> a -> W.Stack a -> W.Stack a
swapStackOrder focused target stack =
stackFromListFocused stack focused $
map swapWindow (W.integrate stack)
where
swapWindow window
| window == focused = target
| window == target = focused
| otherwise = window
stackFromListFocused :: Eq a => W.Stack a -> a -> [a] -> W.Stack a
stackFromListFocused fallback focused windows =
case break (== focused) windows of
(before, _focused : after) -> W.Stack focused (reverse before) after
_ -> fallback
focusDirectionalScreen :: Direction -> RiverWMWaylandAction l
focusDirectionalScreen direction =
stackAction $ \stackSet ->
maybe stackSet ((`W.view` stackSet) . W.tag . W.workspace) $
directionalScreenTarget direction stackSet
shiftFocusedToDirectionalScreen :: Bool -> Direction -> RiverWMWaylandAction l
shiftFocusedToDirectionalScreen follow direction =
(if follow then stackActionWarpPointer else stackAction) $ \stackSet ->
maybe stackSet (shiftToScreen stackSet) $
directionalScreenTarget direction stackSet
where
shiftToScreen stackSet screen =
let workspace = W.tag (W.workspace screen)
shifted = W.shift workspace stackSet
in if follow then W.view workspace shifted else shifted
shiftFocusedToEmptyWorkspaceOnDirectionalScreen :: Direction -> RiverWMWaylandAction l
shiftFocusedToEmptyWorkspaceOnDirectionalScreen direction =
stackActionWarpPointer $ \stackSet ->
maybe stackSet (shiftToEmptyWorkspaceOnScreen stackSet) $
directionalScreenTarget direction stackSet
where
shiftToEmptyWorkspaceOnScreen stackSet screen =
let workspace = W.tag (W.workspace screen)
onDestination = W.view workspace (W.shift workspace stackSet)
in maybe onDestination
(\emptyWorkspace -> W.greedyView emptyWorkspace (W.shift emptyWorkspace onDestination))
(nextEmptyWorkspace onDestination)
directionalFocus :: Direction -> RiverWMWaylandAction l
directionalFocus direction state =
pure $ modifyRiverWMStackSet focusDirectionalWindow state
where
focusDirectionalWindow stackSet =
maybe (fallbackDirectionalFocus direction stackSet) (`W.focusWindow` stackSet) $
directionalTarget direction state
fallbackDirectionalFocus
:: Direction
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
fallbackDirectionalFocus DirectionUp = W.focusUp
fallbackDirectionalFocus DirectionLeft = W.focusUp
fallbackDirectionalFocus DirectionDown = W.focusDown
fallbackDirectionalFocus DirectionRight = W.focusDown
directionalTarget :: Direction -> RiverWMState l -> Maybe Window
directionalTarget direction state@RiverWMState{riverWMStackSet} =
directionalTargetAmong (W.index riverWMStackSet) direction state
directionalTargetAmong :: [Window] -> Direction -> RiverWMState l -> Maybe Window
directionalTargetAmong allowed direction RiverWMState{riverWMStackSet, riverWMWindows, riverWMWindowIds} = do
focused <- W.peek riverWMStackSet
focusedId <- M.lookup focused riverWMWindowIds
focusedRect <- riverWMWindowDesired =<< M.lookup focusedId riverWMWindows
let focusedCenter = rectCenter focusedRect
candidates =
[ (window, directionScore direction focusedCenter (rectCenter rect))
| (windowId, RiverWMWindowState{riverWMWindowXWindow = window, riverWMWindowDesired = Just rect}) <-
M.toList riverWMWindows
, windowId /= focusedId
, window `elem` allowed
]
viable = mapMaybe sequenceCandidate candidates
fst <$> minimumMaybeBy (compare `on` snd) viable
directionalScreenTarget
:: Direction
-> W.StackSet WorkspaceId l Window sid ScreenDetail
-> Maybe (W.Screen WorkspaceId l Window sid ScreenDetail)
directionalScreenTarget direction stackSet =
fst <$> minimumMaybeBy (compare `on` snd) viable
where
focusedCenter = screenCenter (W.current stackSet)
candidates =
[ (screen, directionScore direction focusedCenter (screenCenter screen))
| screen <- W.visible stackSet
]
viable = mapMaybe sequenceCandidate candidates
screenCenter :: W.Screen WorkspaceId l Window sid ScreenDetail -> (Double, Double)
screenCenter = rectCenter . screenRect . W.screenDetail
equalColumnRects :: Rectangle -> Int -> [Rectangle]
equalColumnRects _ count | count <= 0 = []
equalColumnRects rect 1 = [rect]
equalColumnRects (Rectangle x y width height) count =
[ Rectangle
(x + fromIntegral riverOuterGap + fromIntegral (columnOffset index))
(y + fromIntegral riverOuterGap)
(fromIntegral (columnWidth index))
contentHeight
| index <- [0 .. count - 1]
]
where
totalWidth = max 0 (fromIntegral width - 2 * riverOuterGap - riverInnerGap * (count - 1))
contentHeight = fromIntegral (max 1 (fromIntegral height - 2 * riverOuterGap :: Int))
baseWidth = totalWidth `div` count
extraPixels = totalWidth `mod` count
columnWidth index = baseWidth + if index < extraPixels then 1 else 0
columnOffset index = index * baseWidth + min index extraPixels + index * riverInnerGap
riverOuterGap :: Int
riverOuterGap = 10
riverInnerGap :: Int
riverInnerGap = 5
sequenceCandidate :: (a, Maybe b) -> Maybe (a, b)
sequenceCandidate (value, Just score) = Just (value, score)
sequenceCandidate (_, Nothing) = Nothing
rectCenter :: Rectangle -> (Double, Double)
rectCenter (Rectangle x y width height) =
( fromIntegral x + fromIntegral width / 2
, fromIntegral y + fromIntegral height / 2
)
directionScore :: Direction -> (Double, Double) -> (Double, Double) -> Maybe (Double, Double)
directionScore direction (fx, fy) (cx, cy) =
case direction of
DirectionUp | cy < fy -> Just (fy - cy, abs (cx - fx))
DirectionDown | cy > fy -> Just (cy - fy, abs (cx - fx))
DirectionLeft | cx < fx -> Just (fx - cx, abs (cy - fy))
DirectionRight | cx > fx -> Just (cx - fx, abs (cy - fy))
_ -> Nothing
minimumMaybeBy :: (a -> a -> Ordering) -> [a] -> Maybe a
minimumMaybeBy _ [] = Nothing
minimumMaybeBy compareFn xs = Just (minimumBy compareFn xs)
addHyperChordBindings
:: RiverWMWaylandModifiers
-> RiverWMWaylandModifiers
-> [RiverWMWaylandKeyBinding l]
-> [RiverWMWaylandKeyBinding l]
addHyperChordBindings hyperMask chordMask bindings =
bindings ++ M.elems chosen
where
existingKeys =
M.fromList
[ ((riverWMWaylandKeyModifiers binding, riverWMWaylandKeyKeysym binding), ())
| binding <- bindings
]
chordBinding binding@RiverWMWaylandKeyBinding{riverWMWaylandKeyModifiers} =
binding
{ riverWMWaylandKeyModifiers =
(riverWMWaylandKeyModifiers .&. complement hyperMask) .|. chordMask
}
candidates =
[ ( (riverWMWaylandKeyModifiers chorded, riverWMWaylandKeyKeysym chorded)
, (score (riverWMWaylandKeyModifiers binding), chorded)
)
| binding <- bindings
, riverWMWaylandKeyModifiers binding .&. hyperMask /= 0
, let chorded = chordBinding binding
, M.notMember (riverWMWaylandKeyModifiers chorded, riverWMWaylandKeyKeysym chorded) existingKeys
]
chosen =
fmap snd $
foldl' keepBest M.empty candidates
keepBest selected (bindingKey, candidate@(candidateScore, _binding)) =
case M.lookup bindingKey selected of
Nothing -> M.insert bindingKey candidate selected
Just (bestScore, _) ->
if candidateScore < bestScore
then M.insert bindingKey candidate selected
else selected
score modifiers =
length $
filter (/= 0)
[ modifiers .&. shift
, modifiers .&. ctrl
, modifiers .&. alt
, modifiers .&. hyper
, modifiers .&. super
, modifiers .&. riverWMWaylandModifierMod5
]
noMods, shift, ctrl, alt, hyper, super, hyperChord :: RiverWMWaylandModifiers
noMods = riverWMWaylandModifierNone
shift = riverWMWaylandModifierShift
ctrl = riverWMWaylandModifierCtrl
alt = riverWMWaylandModifierAlt
hyper = riverWMWaylandModifierHyper
super = riverWMWaylandModifierSuper
hyperChord = ctrl .|. alt .|. super

View File

@@ -1,18 +0,0 @@
cabal-version: 2.4
name: imalison-river-xmonad
version: 0.1.0.0
license: BSD-3-Clause
author: Ivan Malison
maintainer: IvanMalison@gmail.com
build-type: Simple
executable imalison-river-xmonad
main-is: Main.hs
build-depends: base >= 4.12 && < 5
, containers
, process
, X11
, xmonad
, xmonad-contrib
ghc-options: -threaded -Wall -Wno-unused-do-bind -Wno-deprecations -Wno-missing-signatures -Wno-name-shadowing
default-language: Haskell2010

View File

@@ -1,13 +0,0 @@
_: pkgs: {
haskellPackages = pkgs.haskellPackages.override (old: {
overrides = pkgs.lib.composeExtensions (old.overrides or (_: _: {})) (self: _super: {
imalison-river-xmonad = self.callCabal2nix "imalison-river-xmonad" (
pkgs.lib.sourceByRegex ./.
[
"Main.hs"
"imalison-river-xmonad.cabal"
]
) { };
});
});
}

View File

@@ -1,138 +0,0 @@
configuration {
font: "Roboto 12";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Search";
display-run: "Run";
display-window: "Windows";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
* {
bg: #00000000;
backdrop: #0b102026;
panel: #00000000;
control: #ffffffe0;
candidate: #18203372;
candidate-active:#2430489c;
text: #111827ff;
text-muted: #667085ff;
text-on-dark: #f8fafcff;
text-dark-muted: #d0d6e0ff;
accent: #007affff;
accent-soft: #d8eaffcc;
border: #ffffff96;
hairline: #cfd6df70;
}
window {
transparency: "real";
location: center;
anchor: center;
width: 72%;
height: 78%;
background-color: @backdrop;
text-color: @text;
border: 1px;
border-color: @border;
border-radius: 18px;
}
mainbox {
background-color: @panel;
children: [ inputbar, listview ];
spacing: 10px;
padding: 88px 136px;
margin: 0px;
border: 0px;
border-radius: 0px;
}
inputbar {
background-color: @control;
text-color: @text;
children: [ prompt, entry ];
border: 1px;
border-color: @hairline;
border-radius: 18px;
padding: 13px 15px;
spacing: 8px;
}
prompt {
enabled: true;
background-color: @bg;
text-color: @accent;
font: "Roboto 12";
}
entry {
background-color: @bg;
text-color: @text;
placeholder-color: @text-muted;
placeholder: "";
cursor: text;
expand: true;
}
listview {
background-color: @bg;
columns: 1;
lines: 10;
spacing: 0px;
border: 1px;
border-color: @border;
border-radius: 14px;
cycle: false;
dynamic: true;
layout: vertical;
scrollbar: false;
}
element {
background-color: @bg;
text-color: @text-on-dark;
orientation: horizontal;
border: 0px 0px 1px 0px;
border-color: @hairline;
border-radius: 0px;
padding: 11px 11px;
spacing: 10px;
}
element-icon {
background-color: @bg;
text-color: inherit;
size: 24px;
vertical-align: 0.5;
}
element-text {
background-color: @bg;
text-color: inherit;
vertical-align: 0.5;
horizontal-align: 0;
}
element selected {
background-color: @candidate;
text-color: @text-on-dark;
border-color: @border;
}
element selected element-text {
text-color: @text-on-dark;
}
message {
background-color: @candidate;
border-radius: 14px;
padding: 10px;
}
textbox {
background-color: @bg;
text-color: @text-dark-muted;
}

View File

@@ -0,0 +1,9 @@
/* colors */
* {
al: #00000000;
bg: #000000ff;
se: #101010ff;
fg: #FFFFFFff;
ac: #EC7875ff;
}

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
## Author : Aditya Shakya
## Mail : adi1090x@gmail.com
## Github : @adi1090x
## Twitter : @adi1090x
# Available Styles
# >> Created and tested on : rofi 1.6.0-1
#
# style_1 style_2 style_3 style_4 style_5 style_6
# style_7 style_8 style_9 style_10 style_11 style_12
theme="style_1"
dir="$HOME/.config/rofi/launchers/colorful"
# dark
ALPHA="#00000000"
BG="#000000ff"
FG="#FFFFFFff"
SELECT="#101010ff"
# light
#ALPHA="#00000000"
#BG="#FFFFFFff"
#FG="#000000ff"
#SELECT="#f3f3f3ff"
# accent colors
COLORS=('#EC7875' '#61C766' '#FDD835' '#42A5F5' '#BA68C8' '#4DD0E1' '#00B19F' \
'#FBC02D' '#E57C46' '#AC8476' '#6D8895' '#EC407A' '#B9C244' '#6C77BB')
ACCENT="${COLORS[$(( $RANDOM % 14 ))]}ff"
# overwrite colors file
cat > $dir/colors.rasi <<- EOF
/* colors */
* {
al: $ALPHA;
bg: $BG;
se: $SELECT;
fg: $FG;
ac: $ACCENT;
}
EOF
# comment these lines to disable random style
themes=($(ls -p --hide="launcher.sh" --hide="colors.rasi" $dir))
theme="${themes[$(( $RANDOM % 12 ))]}"
rofi -no-lazy-grab -show drun -modi drun -theme $dir/"$theme"

View File

@@ -0,0 +1,119 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 12px;
width: 35%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px;
columns: 5;
lines: 3;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 2% 0% 2% 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 64px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.5% -0.5% 0.5%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 12px;
border-color: @bg;
}

View File

@@ -0,0 +1,123 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 80%;
height: 80%;
}
prompt {
enabled: true;
padding: 1% 0.75% 1% 0.75%;
background-color: @ac;
text-color: @fg;
border-radius: 100%;
font: "Iosevka Nerd Font 12";
}
textbox-prompt-colon {
padding: 1% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search...";
padding: 1.15% 0.5% 1% 0.5%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0% 0.2% 0.3% 0%;
border-radius: 100%;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 3;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 20% 15% 20% 15%;
}
element {
background-color: @se;
text-color: @fg;
orientation: horizontal;
border-radius: 100%;
padding: 1% 0.5% 1% 0.75%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @ac;
border: 0% 0% 0.3% 0.2%;
border-radius: 100%;
border-color: @ac;
}

View File

@@ -0,0 +1,129 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 25px;
width: 50%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 1.25% 0.75% 1.25% 0.75%;
background-color: @ac;
text-color: @fg;
font: "Iosevka Nerd Font 12";
border-radius: 100%;
}
textbox-prompt-colon {
padding: 1.40% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 1.5% 0.5% 1% 0%;
blink: true;
}
inputbar {
children: [ prompt, textbox-prompt-colon, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 100px;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 3;
lines: 8;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 4% 2% 4% 2%;
}
element {
background-color: @bg;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 1%;
border-color: @ac;
border-radius: 15px;
background-color: @ac;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @ac;
border: 0% 0% 0% 0%;
border-radius: 15px;
border-color: @ac;
}

View File

@@ -0,0 +1,132 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: " Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 50px;
width: 50%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 1.25% 0.75% 1.25% 0.75%;
background-color: @ac;
text-color: @fg;
font: "Iosevka Nerd Font 12";
border-radius: 100%;
}
textbox-prompt-colon {
padding: 1.40% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 1.5% 0.5% 1% 0%;
blink: true;
}
inputbar {
children: [ prompt, textbox-prompt-colon, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0%;
border-radius: 100%;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 6;
lines: 3;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 10px 0px 10px 0px;
border-radius: 50px;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 4% 2% 2% 2%;
}
element {
background-color: @bg;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 64px;
border: 1%;
border-color: @se;
border-radius: 15px;
background-color: @se;
padding: 2% 1% 2% 1%;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.25% 0.5% 0.25%;
padding: 1% 0.5% 1% 0.5%;
}
element-text selected {
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
background-color: @ac;
text-color: @bg;
border-radius: 100%;
}

View File

@@ -0,0 +1,119 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 12px;
width: 18%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 0px;
columns: 1;
lines: 5;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 32px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -0,0 +1,120 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
height: 100%;
width: 18%;
location: west;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 0px;
columns: 1;
lines: 5;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 32px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -0,0 +1,119 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
height: 100%;
width: 19%;
location: east;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px 10px 0px 10px;
columns: 3;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 2% 0% 2% 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 48px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.5% -0.5% 0.5%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -0,0 +1,119 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 35%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @fg;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px;
columns: 2;
lines: 10;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @ac;
text-color: @bg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -0,0 +1,116 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 100%;
height: 100%;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 100%;
border-color: @ac;
margin: 0% 54.5% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 0px;
columns: 10;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2.5%;
padding: 20% 5% 20% 5%;
}
element {
background-color: @al;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 4% 0% 4% 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 80px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.5% -0.5% 0.5%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0.5% 0%;
border-radius: 25px;
border-color: @ac;
}

View File

@@ -0,0 +1,118 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Noto Sans 10";
show-icons: true;
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 12px;
width: 30%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @fg;
font: "Fira Code 12";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @bg;
text-color: @fg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px;
columns: 2;
lines: 7;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 0.5% 0.5% 0.5% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @ac;
text-color: @bg;
border: 0% 0% 0% 0%;
border-radius: 12px;
border-color: @bg;
}

Some files were not shown because too many files have changed in this diff Show More