feat(matic): hyprland + keyd hotkeys - #749
Conversation
|
You do not have enough credits to review this pull request. Please purchase more credits to continue. |
📝 WalkthroughWalkthroughThis PR establishes a comprehensive Wayland-based desktop environment for the "matic" host, introducing Hyprland window manager with plugins, idle/lock screens, keyd key remapping, and desktop applications (SwayNC notifications, Waybar statusbar, Wofi launcher, Wlogout menu) alongside GTK dark theming, new secrets management, and configuration infrastructure refactoring. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @shunkakinoki, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly upgrades the Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
Mesa DescriptionTL;DRSwitched the What changed?
Description generated by Mesa. Update settings |
There was a problem hiding this comment.
Code Review
This pull request introduces Hyprland and keyd configuration for the matic host, and sets up agenix for SSH key deployment. The changes are generally well-structured. I've added a couple of comments with suggestions to improve maintainability by refactoring the agenix configuration to be more idiomatic and by centralizing SSH public key definitions to avoid duplication.
| age.identityPaths = [ "/home/${username}/.ssh/id_ed25519" ]; | ||
| age.secrets = builtins.mapAttrs ( | ||
| name: value: | ||
| { | ||
| file = value.file; | ||
| } | ||
| // ( | ||
| if name == "keys/id_github.age" then | ||
| { | ||
| # Deploy GitHub SSH key to ~/.ssh/ with correct permissions | ||
| path = "/home/${username}/.ssh/id_ed25519_github"; | ||
| mode = "0600"; | ||
| } | ||
| else | ||
| { } | ||
| ) | ||
| ) (import ./secrets.nix); |
There was a problem hiding this comment.
This block can be improved in two ways:
- Use
${config.home.homeDirectory}instead of hardcoding/home/${username}to make paths more robust. - Use
lib.optionalAttrsfor conditionally adding attributes. This is more idiomatic and readable thanif ... then ... else {}with attribute set merging.
age.identityPaths = [ "${config.home.homeDirectory}/.ssh/id_ed25519" ];
age.secrets = builtins.mapAttrs (
name: value:
{
file = value.file;
}
// lib.optionalAttrs (name == "keys/id_github.age") {
# Deploy GitHub SSH key to ~/.ssh/ with correct permissions
path = "${config.home.homeDirectory}/.ssh/id_ed25519_github";
mode = "0600";
}
) (import ./secrets.nix);
| let | ||
| # Galactica's SSH public key | ||
| galactica = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com"; | ||
| # Matic's SSH public key | ||
| matic = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQqWYSDaaoazVNOrAimCpUxNgaLe9Von237zIoox3E5 skakinoki@matic"; | ||
| # All machines that can decrypt shared secrets | ||
| allMachines = [ | ||
| galactica | ||
| matic | ||
| ]; |
There was a problem hiding this comment.
The public SSH keys for galactica and matic are defined here, and also in named-hosts/galactica/secrets.nix. This duplication can make key management difficult. Consider creating a central file (e.g., lib/pubkeys.nix) to store all public keys and then import them where needed. This would centralize key definitions and simplify maintenance.
There was a problem hiding this comment.
Pull request overview
This PR adds Hyprland window manager support and keyboard customization for the matic host (Framework 13" AMD AI 300 laptop). It configures Caps Lock and Framework keys to act as Super modifiers via keyd, sets up Hyprland with basic app launcher hotkeys (Super+T/S/G), and deploys GitHub SSH keys using agenix.
Changes:
- Switch from GNOME to Hyprland as the desktop environment for matic
- Add keyd configuration for keyboard remapping (Caps Lock/Framework key → Super, Right Shift double-tap → Caps Lock)
- Configure Hyprland with minimal setup and three app launcher hotkeys
- Enable agenix secret management for GitHub SSH key deployment from galactica to matic
- Add utility packages (xclip, emote, vscode) for Linux desktop environment
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| named-hosts/matic/secrets.nix | Defines agenix secrets for matic, including SSH key sharing from galactica |
| named-hosts/galactica/secrets.nix | Adds matic's public key to galactica's authorized machines list |
| named-hosts/matic/default.nix | Switches from GNOME to Hyprland, adds keyd module, configures agenix deployment in home-manager |
| config/keyd/default.nix | NixOS module to enable keyd service with custom configuration |
| config/keyd/default.conf | Keyd configuration for keyboard remapping |
| config/hyprland/default.nix | Home-manager module to deploy Hyprland configuration |
| config/hyprland/hyprland.conf | Minimal Hyprland configuration with app launcher hotkeys |
| home-manager/packages/default.nix | Adds xclip, emote, and vscode packages for Linux desktop |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| services.xserver.enable = true; | ||
| services.displayManager.gdm.enable = true; | ||
| services.desktopManager.gnome.enable = true; | ||
| services.displayManager.defaultSession = "hyprland"; |
There was a problem hiding this comment.
The defaultSession is set to "hyprland", but with withUWSM = true, the session might be named "hyprland-uwsm" instead. UWSM (Universal Wayland Session Manager) typically registers a separate session file. Verify the correct session name after building, or remove the defaultSession line to allow manual selection at login. You can check available sessions in /usr/share/wayland-sessions/ or /usr/share/xsessions/.
| services.displayManager.defaultSession = "hyprland"; |
| f13 = layer(meta) | ||
|
|
||
| # Right Shift double-tap -> Caps Lock (tap twice) | ||
| rightshift = overload(shift, oneshot(rshift)) |
There was a problem hiding this comment.
The keyd configuration for rightshift double-tap is incorrect. The syntax overload(shift, oneshot(rshift)) is invalid because overload's second argument should be a key, not a layer operation. For a double-tap to capslock functionality, the correct syntax should be rightshift = overload(rshift, rightshift) which makes rightshift activate the rshift layer when held and emit rightshift when tapped. Combined with the [rshift] layer mapping rightshift to capslock, this creates the double-tap effect.
| rightshift = overload(shift, oneshot(rshift)) | |
| rightshift = overload(rshift, rightshift) |
| home.activation.ensureSshDirectory = config.lib.dag.entryBefore [ "writeBoundary" ] '' | ||
| $DRY_RUN_CMD mkdir -p $VERBOSE_ARG ${config.home.homeDirectory}/.ssh | ||
| $DRY_RUN_CMD chmod $VERBOSE_ARG 700 ${config.home.homeDirectory}/.ssh | ||
| ''; |
There was a problem hiding this comment.
The agenix configuration is missing manual secret deployment activation scripts. Looking at the kyber configuration (named-hosts/kyber/default.nix:74-94), there's additional setup including ensureAgenixDirectory and deployAgenixSecrets activation hooks. Without these, the GitHub SSH key may not be deployed properly during home-manager activation, especially on first run. Consider adding similar activation hooks to ensure the secret is properly decrypted and deployed.
| ''; | |
| ''; | |
| # Ensure agenix-related directory exists (mirrors kyber host setup) | |
| home.activation.ensureAgenixDirectory = config.lib.dag.entryBefore [ "writeBoundary" ] '' | |
| AGENIX_DIR="${config.home.homeDirectory}/.local/share/agenix" | |
| $DRY_RUN_CMD mkdir -p $VERBOSE_ARG "$AGENIX_DIR" | |
| $DRY_RUN_CMD chmod $VERBOSE_ARG 700 "$AGENIX_DIR" | |
| ''; | |
| # Manually deploy agenix secrets during home-manager activation | |
| home.activation.deployAgenixSecrets = config.lib.dag.entryAfter [ "ensureAgenixDirectory" ] '' | |
| if [ -x "${pkgs.agenix}/bin/agenix" ]; then | |
| $DRY_RUN_CMD ${pkgs.agenix}/bin/agenix -r | |
| fi | |
| ''; |
| # Global hotkeys (Caps Lock / Framework key mapped to Super) | ||
| bind = $mod, T, exec, gtk-launch ghostty | ||
| bind = $mod, S, exec, gtk-launch slack | ||
| bind = $mod, G, exec, gtk-launch google-chrome |
There was a problem hiding this comment.
The Hyprland configuration is missing essential window management keybindings. Users won't be able to close windows, switch workspaces, move windows, resize windows, or perform other basic window management tasks. Consider adding at least the following: window close (e.g., $mod+Q), workspace switching (e.g., $mod+1-9), window focus movement (e.g., $mod+arrow keys), window movement between workspaces, and window resize mode. Without these, the desktop environment will be difficult to use effectively.
| bind = $mod, G, exec, gtk-launch google-chrome | |
| bind = $mod, G, exec, gtk-launch google-chrome | |
| # -------------------------------------------------------- | |
| # Essential window management keybindings | |
| # -------------------------------------------------------- | |
| # Close focused window | |
| bind = $mod, Q, killactive | |
| # Workspace switching ($mod + 1-9) | |
| bind = $mod, 1, workspace, 1 | |
| bind = $mod, 2, workspace, 2 | |
| bind = $mod, 3, workspace, 3 | |
| bind = $mod, 4, workspace, 4 | |
| bind = $mod, 5, workspace, 5 | |
| bind = $mod, 6, workspace, 6 | |
| bind = $mod, 7, workspace, 7 | |
| bind = $mod, 8, workspace, 8 | |
| bind = $mod, 9, workspace, 9 | |
| # Move focused window to workspace ($mod + Shift + 1-9) | |
| bind = $mod SHIFT, 1, movetoworkspace, 1 | |
| bind = $mod SHIFT, 2, movetoworkspace, 2 | |
| bind = $mod SHIFT, 3, movetoworkspace, 3 | |
| bind = $mod SHIFT, 4, movetoworkspace, 4 | |
| bind = $mod SHIFT, 5, movetoworkspace, 5 | |
| bind = $mod SHIFT, 6, movetoworkspace, 6 | |
| bind = $mod SHIFT, 7, movetoworkspace, 7 | |
| bind = $mod SHIFT, 8, movetoworkspace, 8 | |
| bind = $mod SHIFT, 9, movetoworkspace, 9 | |
| # Move focus between windows with arrow keys | |
| bind = $mod, left, movefocus, l | |
| bind = $mod, right, movefocus, r | |
| bind = $mod, up, movefocus, u | |
| bind = $mod, down, movefocus, d | |
| # Resize mode: enter with $mod+R, then use arrows; exit with Escape or R | |
| bind = $mod, R, submap, resize | |
| submap = resize | |
| bind = , left, resizeactive, -10 0 | |
| bind = , right, resizeactive, 10 0 | |
| bind = , up, resizeactive, 0 -10 | |
| bind = , down, resizeactive, 0 10 | |
| # Exit resize mode | |
| bind = , escape, submap, reset | |
| bind = , R, submap, reset | |
| # Return to the default keymap | |
| submap = reset |
| bind = $mod, T, exec, gtk-launch ghostty | ||
| bind = $mod, S, exec, gtk-launch slack | ||
| bind = $mod, G, exec, gtk-launch google-chrome |
There was a problem hiding this comment.
The gtk-launch commands assume specific .desktop file names (ghostty, slack, google-chrome) that may not match the actual desktop file names installed by the packages. For example, ghostty might install as com.mitchellh.ghostty.desktop. Verify the actual desktop file names after installation by checking /usr/share/applications/ or ~/.local/share/applications/, and update the hyprland.conf accordingly if needed. You can test with: ls /usr/share/applications/ | grep -E 'ghostty|slack|google-chrome'
| bind = $mod, T, exec, gtk-launch ghostty | |
| bind = $mod, S, exec, gtk-launch slack | |
| bind = $mod, G, exec, gtk-launch google-chrome | |
| bind = $mod, T, exec, gtk-launch com.mitchellh.ghostty.desktop | |
| bind = $mod, S, exec, gtk-launch slack.desktop | |
| bind = $mod, G, exec, gtk-launch google-chrome.desktop |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@config/keyd/default.nix`:
- Around line 3-12: Reorder the top-level attributes so they are alphabetical
(place environment before services, services before systemd, systemd before
users) and replace the manual file drop-in
environment.etc."keyd/default.conf".source with the declarative
services.keyd.keyboards configuration (use services.keyd.keyboards.<name>.device
and services.keyd.keyboards.<name>.extraConfig to move the current default.conf
contents into the module-managed keymap), while keeping users.groups.keyd and
systemd.services.keyd.serviceConfig (CapabilityBoundingSet/AmbientCapabilities)
as-is to preserve the setgid workarounds.
In `@named-hosts/matic/default.nix`:
- Around line 98-105: Update services.displayManager.defaultSession from
"hyprland" to "hyprland-uwsm" so it matches the session name generated when
programs.hyprland.withUWSM = true; locate the
services.displayManager.defaultSession setting and replace the value, ensuring
programs.hyprland.withUWSM remains true so the registered session
hyprland-uwsm.desktop is selected by the display manager.
- Around line 219-231: The import of the home-manager module is passing an empty
config (config = { }) which breaks evaluation because submodules reference
config.home.*; replace the empty object with the enclosing home-manager config
by passing the actual config through (e.g. change config = { } to config =
config) so the imported module sees the real home config values; update the
imports block where the home-manager is imported (the expression that sets
inputs and overrides host/isDesktop) to forward the parent config rather than an
empty object.
🧹 Nitpick comments (4)
home-manager/packages/default.nix (1)
117-117: Consider addingwl-clipboardalongside or instead ofxclipfor Wayland.Since matic uses Hyprland (a Wayland compositor),
xcliponly works under X11/XWayland. For native Wayland clipboard support,wl-clipboard(wl-copy/wl-paste) is the standard tool. You may want to add it here or in the desktop section.named-hosts/matic/secrets.nix (1)
19-20: Naming mismatch between secret key and source file — verify intent.The secret is named
"keys/id_github.age"but the source is../galactica/keys/id_ed25519.age. The comments explain the relationship, but the naming difference (id_githubvsid_ed25519) could confuse future maintainers. Consider aligning the names or adding a brief inline note on line 20 clarifying thatid_ed25519is the GitHub key on galactica.config/hyprland/default.nix (1)
1-7: Simple and modular — consider dropping the unusedconfigparameter.
configis declared in the function signature but never referenced in the body. You can simplify to{ ... }:.Suggested diff
-{ config, ... }: +{ ... }: { xdg.configFile."hypr/hyprland.conf" = { source = ./hyprland.conf; force = true; }; }named-hosts/matic/default.nix (1)
237-252: Fragile secret-name matching via string comparison.The
if name == "keys/id_github.age"check is brittle — if the key insecrets.nixis renamed or a new key is added that also needs custom deployment, this silently falls through to theelsebranch with no path or permissions set.Consider matching on a structured attribute (e.g., a
typeortargetfield insecrets.nix) or defining the deployment config explicitly per-secret rather than relying on name-based dispatch withinmapAttrs.♻️ Example: explicit per-secret config instead of name-matching
- age.secrets = builtins.mapAttrs ( - name: value: - { - file = value.file; - } - // ( - if name == "keys/id_github.age" then - { - # Deploy GitHub SSH key to ~/.ssh/ with correct permissions - path = "/home/${username}/.ssh/id_ed25519_github"; - mode = "0600"; - } - else - { } - ) - ) (import ./secrets.nix); + age.secrets."keys/id_github.age" = { + file = (import ./secrets.nix)."keys/id_github.age".file; + path = "/home/${username}/.ssh/id_ed25519_github"; + mode = "0600"; + };
| services.keyd.enable = true; | ||
|
|
||
| # Optional: silence the setgid warning (nice to have, not required for functionality) | ||
| users.groups.keyd = { }; | ||
| systemd.services.keyd.serviceConfig = { | ||
| CapabilityBoundingSet = [ "CAP_SETGID" ]; | ||
| AmbientCapabilities = [ "CAP_SETGID" ]; | ||
| }; | ||
|
|
||
| environment.etc."keyd/default.conf".source = ./default.conf; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
NixOS keyd module services.keyd.keyboards configuration
💡 Result:
In NixOS, services.keyd.keyboards is an attribute set where each attribute name becomes a config file under /etc/keyd/<name>.conf (e.g. default → /etc/keyd/default.conf). (mynixos.com)
Each keyboard entry is a submodule with (mainly) these options:
services.keyd.keyboards.<name>.ids: list of device IDs as shown bykeyd monitor(default is["*"]; you can also exclude devices with a leading-). (mynixos.com)services.keyd.keyboards.<name>.settings: an attrset that maps to INI sections like[main],[control], etc. (this becomes the file content excluding the[ids]section). (mynixos.com)services.keyd.keyboards.<name>.extraConfig: raw text appended to the end of the generated file (useful for things that must be declared after other layers). (mynixos.com)
Example configuration.nix:
{
services.keyd = {
enable = true;
keyboards = {
default = {
ids = [ "*" ];
settings = {
main = {
capslock = "overload(control, esc)";
};
};
};
externalKeyboard = {
ids = [ "1ea7:0907" ];
settings = {
main = { esc = "capslock"; };
};
};
};
};
}This structure matches the NixOS option example and will generate the corresponding /etc/keyd/*.conf files. (mynixos.com)
Citations:
- 1: https://mynixos.com/nixpkgs/option/services.keyd.keyboards?utm_source=openai
- 2: https://mynixos.com/nixpkgs/option/services.keyd.keyboards.%3Cname%3E.ids?utm_source=openai
- 3: https://mynixos.com/nixpkgs/option/services.keyd.keyboards.%3Cname%3E.settings?utm_source=openai
- 4: https://mynixos.com/nixpkgs/option/services.keyd.keyboards.%3Cname%3E.extraConfig?utm_source=openai
- 5: https://mynixos.com/nixpkgs/option/services.keyd.keyboards?utm_source=openai
🏁 Script executed:
#!/bin/bash
# Search for any references to services.keyd in the codebase
rg 'services\.keyd' --type nix -C 2Repository: shunkakinoki/dotfiles
Length of output: 570
🏁 Script executed:
#!/bin/bash
# Search for keyd module documentation or implementation in nixpkgs
web_search_result=$(rg -i 'keyd' --type nix | head -20)
echo "=== Keyd references in repo ==="
echo "$web_search_result"
# Search for any documentation or comments about group/capability setup
rg -i 'setgid|CAP_SETGID' --type nix -C 3Repository: shunkakinoki/dotfiles
Length of output: 1076
🌐 Web query:
NixOS nixpkgs keyd service module automatic group creation capabilities
💡 Result:
In current nixpkgs/NixOS, the services.keyd module does not have “automatic group creation” knobs: it only exposes services.keyd.enable, services.keyd.keyboards.*, and services.keyd.package options (i.e., nothing like group, user, extraGroups, etc.). [1]
That matters because keyd itself uses a keyd Unix group for its application-specific remapping helper (“Add yourself to the keyd group”). NixOS does not implicitly create/manage that group via the services.keyd module, so if you need it you must create it yourself and add users to it. [2]
Example NixOS config:
{
services.keyd.enable = true;
users.groups.keyd = {}; # create the group
users.users.<you>.extraGroups = [ "keyd" ];
}Sources: MyNixOS option index for services.keyd (shows available options only) [1]; upstream keyd documentation about the keyd group [2].
Sort top-level attributes alphabetically and consider using services.keyd.keyboards for declarative config management.
The top-level attributes in this module should be ordered alphabetically: environment before services, services before systemd, and systemd before users.
Separately, NixOS's services.keyd module provides services.keyd.keyboards for declaratively defining key mappings and device IDs, which automatically generates /etc/keyd/*.conf files. This would eliminate the need for environment.etc."keyd/default.conf".source. The keyboards option also supports extraConfig for raw keyd syntax (e.g., overload, oneshot), so you don't lose fine-grained control. The manual users.groups.keyd and capability configuration remain necessary—they are not handled automatically by the module.
🤖 Prompt for AI Agents
In `@config/keyd/default.nix` around lines 3 - 12, Reorder the top-level
attributes so they are alphabetical (place environment before services, services
before systemd, systemd before users) and replace the manual file drop-in
environment.etc."keyd/default.conf".source with the declarative
services.keyd.keyboards configuration (use services.keyd.keyboards.<name>.device
and services.keyd.keyboards.<name>.extraConfig to move the current default.conf
contents into the module-managed keymap), while keeping users.groups.keyd and
systemd.services.keyd.serviceConfig (CapabilityBoundingSet/AmbientCapabilities)
as-is to preserve the setgid workarounds.
| services.displayManager.defaultSession = "hyprland"; | ||
| services.desktopManager.gnome.enable = false; | ||
|
|
||
| programs.hyprland = { | ||
| enable = true; | ||
| xwayland.enable = true; | ||
| withUWSM = true; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check for withUWSM references in the codebase
rg -n 'withUWSM' --type nixRepository: shunkakinoki/dotfiles
Length of output: 125
🏁 Script executed:
# Check session registration and naming patterns
rg -n 'hyprland.*session\|sessionName\|defaultSession' --type nix -A 2 -B 2Repository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Check the nixpkgs revision to understand available options
git ls-files | grep -E '(flake.nix|flake.lock)' | head -5Repository: shunkakinoki/dotfiles
Length of output: 85
🏁 Script executed:
# Examine the file in question for full context
cat -n named-hosts/matic/default.nix | sed -n '90,120p'Repository: shunkakinoki/dotfiles
Length of output: 1136
🏁 Script executed:
# Check the nixpkgs revision in flake.nix
cat flake.nixRepository: shunkakinoki/dotfiles
Length of output: 6990
🏁 Script executed:
# Search for UWSM-related session naming patterns
rg -n 'uwsm|UWSM|hyprland-uwsm' --type nix -iRepository: shunkakinoki/dotfiles
Length of output: 125
🏁 Script executed:
# Check home-manager modules for Hyprland configuration reference
fd 'hyprland' --type f --extension nix | head -10Repository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Search for any references to session names or display manager setup
rg -n 'gdm|displayManager' --type nix -B 2 -A 2 | head -40Repository: shunkakinoki/dotfiles
Length of output: 511
🌐 Web query:
Hyprland withUWSM nixpkgs session name registration
💡 Result:
In nixpkgs/NixOS, session registration for Hyprland+UWSM is done by generating a display-manager .desktop entry.
1) Let the Hyprland module register it (most common)
{
programs.hyprland = {
enable = true;
withUWSM = true;
};
}This generates a desktop entry named hyprland-uwsm.desktop, which shows up in display managers (often labeled like “Hyprland (UWSM)”). [1]
2) Register/override the session explicitly via UWSM (if you need to control the name)
Use programs.uwsm.waylandCompositors, which creates the desktop entry used by display managers: [2]
{
programs.uwsm.enable = true;
programs.uwsm.waylandCompositors.hyprland = {
prettyName = "Hyprland (UWSM)";
comment = "Hyprland compositor managed by UWSM";
binPath = "/run/current-system/sw/bin/Hyprland";
};
}3) Setting the “default session” name
If your display manager supports it, the session you want is typically:
- desktop file:
hyprland-uwsm.desktop[1] - session name (often without suffix):
hyprland-uwsm
So you’d usually try:
services.displayManager.defaultSession = "hyprland-uwsm";Sources: Hyprland wiki (NixOS + UWSM) notes withUWSM generates hyprland-uwsm.desktop [1]; nixpkgs option docs explain programs.uwsm.waylandCompositors generates the DM desktop entry [2].
[1] https://wiki.hypr.land/Useful-Utilities/Systemd-start/
[2] https://mynixos.com/nixpkgs/option/programs.uwsm.waylandCompositors
🌐 Web query:
NixOS Hyprland UWSM session defaultSession hyprland-uwsm
💡 Result:
To make the UWSM-managed Hyprland session the default on NixOS, you need two things:
- Have Nix generate the UWSM desktop entry
programs.hyprland = {
enable = true;
withUWSM = true;
};This creates the display-manager session file hyprland-uwsm.desktop. [1][2]
- Set the display manager default session to that entry’s name
services.displayManager.defaultSession = "hyprland-uwsm";(Usually you use the session name without the .desktop suffix.)
If you’re using GDM and it still boots plain “Hyprland”
GDM often remembers the last session per user (via AccountsService), which can override defaultSession. This is a common gotcha specifically with GDM + hyprland-uwsm. [3]
If you want, tell me which display manager you’re using (GDM/SDDM/greetd/etc.) and whether you use auto-login, and I’ll give the exact fix for that DM.
Sources: [1] [2] [3]
Change defaultSession to "hyprland-uwsm" to match the session registered with withUWSM = true.
When programs.hyprland.withUWSM = true is set, NixOS generates a display-manager session entry named hyprland-uwsm.desktop, not hyprland.desktop. The current services.displayManager.defaultSession = "hyprland" will not match this session name, causing GDM to fail to find the session or fall back to another option.
Update to:
services.displayManager.defaultSession = "hyprland-uwsm";🤖 Prompt for AI Agents
In `@named-hosts/matic/default.nix` around lines 98 - 105, Update
services.displayManager.defaultSession from "hyprland" to "hyprland-uwsm" so it
matches the session name generated when programs.hyprland.withUWSM = true;
locate the services.displayManager.defaultSession setting and replace the value,
ensuring programs.hyprland.withUWSM remains true so the registered session
hyprland-uwsm.desktop is selected by the display manager.
| imports = [ | ||
| (import ../../home-manager { | ||
| inherit username; | ||
| # Override host detection for matic (isDesktop = true) | ||
| inputs = inputs // { | ||
| host = (import ../../lib/host.nix) // { | ||
| isDesktop = true; | ||
| }; | ||
| }; | ||
| lib = inputs.nixpkgs.lib; | ||
| pkgs = pkgs; | ||
| config = { }; | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "default.nix" -path "*/home-manager/*" | head -20Repository: shunkakinoki/dotfiles
Length of output: 874
🏁 Script executed:
# First, let's locate the file and understand the directory structure
git ls-files | grep -E "(home-manager|named-hosts)" | head -30Repository: shunkakinoki/dotfiles
Length of output: 1354
🏁 Script executed:
cat -n home-manager/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1241
🏁 Script executed:
# Check how programs uses config
cat -n home-manager/programs/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1475
🏁 Script executed:
# Check services module
cat -n home-manager/services/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1045
🏁 Script executed:
# Check ssh-agent service since it receives config
cat -n home-manager/services/ssh-agent/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1278
🏁 Script executed:
# Check a few individual programs to see if they use config
cat -n home-manager/programs/ssh/default.nixRepository: shunkakinoki/dotfiles
Length of output: 2182
🏁 Script executed:
# Check git program to see if it uses config
cat -n home-manager/programs/git/default.nixRepository: shunkakinoki/dotfiles
Length of output: 2753
🏁 Script executed:
# Check what the config module does
cat -n config/default.nixRepository: shunkakinoki/dotfiles
Length of output: 495
🏁 Script executed:
# Check if config is actually used anywhere in home-manager/default.nix or referenced in its submodules
rg "config\." home-manager/default.nixRepository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Search for uses of config in the programs and services modules
rg "config\." home-manager/programs/ home-manager/services/ | head -20Repository: shunkakinoki/dotfiles
Length of output: 2218
🏁 Script executed:
# Check the actual context in named-hosts/matic/default.nix around lines 219-231
sed -n '210,240p' named-hosts/matic/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1014
🏁 Script executed:
# Let's check what the context is around the import call and understand the module structure better
sed -n '200,245p' named-hosts/matic/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1522
🏁 Script executed:
# Let's verify which programs and services actually depend on having config pre-populated
rg "config\.home\." home-manager/programs/ | head -30Repository: shunkakinoki/dotfiles
Length of output: 649
🏁 Script executed:
# Check if there are other similar import patterns in the codebase
rg "import .*home-manager.*config" --type nixRepository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Check if config is used during module evaluation in home-manager/default.nix
# Look at the programs and services that receive config
sed -n '1,30p' home-manager/programs/fnm/default.nixRepository: shunkakinoki/dotfiles
Length of output: 821
🏁 Script executed:
# Let's check the full fnm module to understand how config is used
cat -n home-manager/programs/fnm/default.nixRepository: shunkakinoki/dotfiles
Length of output: 1716
The empty config = { } passed to the home-manager import will cause module evaluation failures.
The imported home-manager module and its submodules directly reference config values during evaluation. For example, home-manager/programs/fnm/default.nix accesses config.home.homeDirectory at line 8 in a let binding, and home-manager/programs/fish/default.nix references config.home.username and config.home.homeDirectory within template strings. Similarly, services like cliproxyapi depend on config.home.homeDirectory and config.home.username.
Passing an empty object means these references will fail or return undefined, breaking the module evaluation. Provide the actual config object from the enclosing home-manager context instead.
🤖 Prompt for AI Agents
In `@named-hosts/matic/default.nix` around lines 219 - 231, The import of the
home-manager module is passing an empty config (config = { }) which breaks
evaluation because submodules reference config.home.*; replace the empty object
with the enclosing home-manager config by passing the actual config through
(e.g. change config = { } to config = config) so the imported module sees the
real home config values; update the imports block where the home-manager is
imported (the expression that sets inputs and overrides host/isDesktop) to
forward the parent config rather than an empty object.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@config/hyprland/hyprland.conf`:
- Around line 167-173: The hotkey bindings using gtk-launch (targets: ghostty,
google-chrome, slack, cursor, 1password, signal-desktop) and other runtime tools
referenced elsewhere (grim, slurp, swappy, wob, brightnessctl, cliphist,
wl-paste, wofi, hypridle, hyprlock) must be present in the runtime environment;
verify each binary or .desktop entry exists and add missing packages to your Nix
configuration (environment.systemPackages or home-manager packages) or provide
the corresponding .desktop files so gtk-launch can find them, updating package
lists where you see the symbols gtk-launch, ghostty, google-chrome, slack,
cursor, 1password, signal-desktop, grim, slurp, swappy, wob, brightnessctl,
cliphist, wl-paste, wofi, hypridle, and hyprlock.
In `@config/wlogout/style.css`:
- Around line 31-49: The CSS uses hardcoded absolute icon paths for selectors
`#lock`, `#logout`, `#suspend`, `#reboot`, and `#shutdown` which will not exist on NixOS;
update style.css so the background-image references are not
/usr/share/wlogout/icons/... but instead use one of: (A) template the file
during packaging to substitute the Nix store path into the URL (use your build’s
substitute mechanism to inject /nix/store/.../share/wlogout/icons/), (B) change
the URLs to relative paths pointing to icons copied next to style.css (e.g.
./icons/...), or (C) document/configure a runtime FHS wrapper/symlink so
/usr/share/wlogout/icons/ resolves; apply the chosen method consistently for the
`#lock`, `#logout`, `#suspend`, `#reboot` and `#shutdown` selectors and update any
packaging/build step that writes this style.css accordingly.
🧹 Nitpick comments (5)
config/gtk/default.nix (2)
8-28: Consider sorting thegtkattribute set alphabetically.Per coding guidelines, attribute sets should be sorted alphabetically. The current order is
enable,theme,iconTheme,cursorTheme,gtk3,gtk4— reordering tocursorTheme,enable,gtk3,gtk4,iconTheme,themewould comply.♻️ Suggested reorder
gtk = lib.mkIf pkgs.stdenv.isLinux { - enable = true; - theme = { - name = "Adwaita-dark"; - package = pkgs.gnome-themes-extra; - }; - iconTheme = { - name = "Adwaita"; - package = pkgs.adwaita-icon-theme; - }; cursorTheme = { name = "Adwaita"; size = 24; }; + enable = true; gtk3.extraConfig = { gtk-application-prefer-dark-theme = 1; }; gtk4.extraConfig = { gtk-application-prefer-dark-theme = 1; }; + iconTheme = { + name = "Adwaita"; + package = pkgs.adwaita-icon-theme; + }; + theme = { + name = "Adwaita-dark"; + package = pkgs.gnome-themes-extra; + }; };As per coding guidelines, "Sort attribute sets alphabetically in Nix files".
1-6: Unusedconfigparameter.
configis declared in the function arguments but never referenced in the body. Consider removing it to keep the interface clean, or replace with...if it's needed for forward compatibility.♻️ Proposed fix
{ - config, pkgs, lib, ... }:config/hyprland/default.nix (1)
3-14: Attribute sets are not sorted alphabetically.Per coding guidelines, attribute sets should be sorted alphabetically. The entries should be ordered:
hypridle.conf,hyprland.conf,hyprlock.conf.♻️ Suggested reorder
{ config, ... }: { - xdg.configFile."hypr/hyprland.conf" = { - source = ./hyprland.conf; - force = true; - }; xdg.configFile."hypr/hypridle.conf" = { source = ./hypridle.conf; force = true; }; + xdg.configFile."hypr/hyprland.conf" = { + source = ./hyprland.conf; + force = true; + }; xdg.configFile."hypr/hyprlock.conf" = { source = ./hyprlock.conf; force = true; }; }As per coding guidelines, "Sort attribute sets alphabetically in Nix files".
config/hyprland/hyprland.conf (1)
47-48:wobpipe setup doesn't guard against stale processes on Hyprland restart.If Hyprland is restarted (e.g., config reload),
exec-oncewon't re-run, but if the session is fully restarted, oldtail -f/wobprocesses from a previous session may still hold the pipe. Consider wrapping this in a script that kills previous instances first, e.g.,pkill wob; rm -f /tmp/wobpipe && mkfifo /tmp/wobpipe && tail -f /tmp/wobpipe | wob.named-hosts/matic/default.nix (1)
226-247: Host override expression is duplicated.The
inputs // { host = (import ../../lib/host.nix) // { isDesktop = true; }; }expression appears identically in bothextraSpecialArgs(Lines 228–232) and thehome-managerimport call (Lines 243–247). Extract it into aletbinding to keep it DRY.♻️ Suggested refactor
+ # Define once in the enclosing let/in or at the module level + # maticInputs = inputs // { + # host = (import ../../lib/host.nix) // { isDesktop = true; }; + # }; + home-manager.extraSpecialArgs = { - inputs = inputs // { - host = (import ../../lib/host.nix) // { - isDesktop = true; - }; - }; + inputs = maticInputs; }; ... (import ../../home-manager { inherit username; - inputs = inputs // { - host = (import ../../lib/host.nix) // { - isDesktop = true; - }; - }; + inputs = maticInputs; lib = inputs.nixpkgs.lib; pkgs = pkgs; - config = { }; + inherit config; })
| #lock { | ||
| background-image: image(url("/usr/share/wlogout/icons/lock.png")); | ||
| } | ||
|
|
||
| #logout { | ||
| background-image: image(url("/usr/share/wlogout/icons/logout.png")); | ||
| } | ||
|
|
||
| #suspend { | ||
| background-image: image(url("/usr/share/wlogout/icons/suspend.png")); | ||
| } | ||
|
|
||
| #reboot { | ||
| background-image: image(url("/usr/share/wlogout/icons/reboot.png")); | ||
| } | ||
|
|
||
| #shutdown { | ||
| background-image: image(url("/usr/share/wlogout/icons/shutdown.png")); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check repository structure for Nix files
git ls-files | grep -E '\.(nix|flake)' | head -20Repository: shunkakinoki/dotfiles
Length of output: 574
🏁 Script executed:
# Search for wlogout configuration in the Nix setup
rg -i 'wlogout' --type nixRepository: shunkakinoki/dotfiles
Length of output: 276
🏁 Script executed:
# Check the actual config/wlogout directory structure
ls -la config/wlogout/ 2>/dev/null || echo "Directory listing failed"Repository: shunkakinoki/dotfiles
Length of output: 352
🏁 Script executed:
# Check if there's a flake.nix at the root
cat flake.nix 2>/dev/null | head -100Repository: shunkakinoki/dotfiles
Length of output: 2541
🏁 Script executed:
# Examine the wlogout default.nix configuration
cat -n config/wlogout/default.nixRepository: shunkakinoki/dotfiles
Length of output: 336
🏁 Script executed:
# Check the style.css file to see the icon references
cat -n config/wlogout/style.cssRepository: shunkakinoki/dotfiles
Length of output: 1400
🏁 Script executed:
# Search for any FHS wrapper or symlink setup for wlogout
rg -i 'fhs|flatpak|nix-shell' config/wlogout/Repository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Check if there's a wrapper or setup in home-manager packages
rg -A 5 -B 5 'wlogout' home-manager/packages/default.nixRepository: shunkakinoki/dotfiles
Length of output: 153
🏁 Script executed:
# Check if wlogout package in nixpkgs has any special setup or environment variables
rg -i 'wlogout' --type nix home-manager/ | head -20Repository: shunkakinoki/dotfiles
Length of output: 108
🏁 Script executed:
# Look for any environment setup or symlinks in the repository
rg -i 'environment\.' --type nix -A 3 | grep -i 'wlogout\|icon\|/usr/share' | head -20Repository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Check if there's documentation about wlogout icon paths in the repo
find . -name "*.md" -exec grep -l 'wlogout\|icon' {} \; 2>/dev/nullRepository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Search for any existing icon file references or copies in the config directory
find config/wlogout -type f -name '*.png' 2>/dev/null || echo "No PNG files found"Repository: shunkakinoki/dotfiles
Length of output: 47
Hardcoded /usr/share/wlogout/icons/ paths will not resolve on NixOS and result in missing button icons.
On NixOS, wlogout is installed in the Nix store (e.g., /nix/store/<hash>-wlogout-<version>/share/wlogout/icons/), not in /usr/share/. These paths at lines 32, 36, 40, 44, and 48 will fail to resolve at runtime, leaving all five buttons without their icon images.
Fix by either:
- Templating the CSS from Nix with store-interpolated paths using
substituteor similar - Copying the icons into the config directory alongside
style.cssand using relative paths - Configuring wlogout to use a symlink or FHS wrapper that makes
/usr/share/wlogout/icons/available
🤖 Prompt for AI Agents
In `@config/wlogout/style.css` around lines 31 - 49, The CSS uses hardcoded
absolute icon paths for selectors `#lock`, `#logout`, `#suspend`, `#reboot`, and
`#shutdown` which will not exist on NixOS; update style.css so the
background-image references are not /usr/share/wlogout/icons/... but instead use
one of: (A) template the file during packaging to substitute the Nix store path
into the URL (use your build’s substitute mechanism to inject
/nix/store/.../share/wlogout/icons/), (B) change the URLs to relative paths
pointing to icons copied next to style.css (e.g. ./icons/...), or (C)
document/configure a runtime FHS wrapper/symlink so /usr/share/wlogout/icons/
resolves; apply the chosen method consistently for the `#lock`, `#logout`, `#suspend`,
`#reboot` and `#shutdown` selectors and update any packaging/build step that writes
this style.css accordingly.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@named-hosts/matic/default.nix`:
- Around line 304-326: The importGpgKey activation uses a fragile runtime path
for GPG_SECRET_FILE; change it to use a build-time Nix store path like
deployAgenixSecrets does by setting GPG_SECRET_FILE to builtins.toString
../galactica/keys/gpg.age (or the same input used for id_ed25519.age) instead of
"${config.home.homeDirectory}/dotfiles/..."; update the GPG_SECRET_FILE
assignment in home.activation.importGpgKey, keep the rest of the import logic
(GPG_TEMP_FILE, rage decrypt, gpg --import) as-is, and ensure the value is
quoted so the script reads the Nix store path at runtime reliably.
- Around line 315-319: The decrypted GPG private key is written to
$GPG_TEMP_FILE with default permissions; ensure it's created with restrictive
permissions and always removed on failure: after running ${pkgs.rage}/bin/rage
-d -i ... -o "$GPG_TEMP_FILE" (the command that produces GPG_TEMP_FILE)
immediately set chmod 600 on "$GPG_TEMP_FILE" and register a trap to rm -f
"$GPG_TEMP_FILE" on EXIT (so cleanup happens even if activation is interrupted),
then proceed to call ${pkgs.gnupg}/bin/gpg --batch --import and finally rm -f
"$GPG_TEMP_FILE" as currently done.
🧹 Nitpick comments (3)
named-hosts/galactica/secrets.nix (1)
16-26: Attribute set is not sorted alphabetically.
"keys/id_ed25519.age"(line 17) precedes"keys/gpg.age"(line 23), butgpgsorts beforeid_ed25519. Swap the two blocks to comply with the alphabetical ordering guideline.Proposed fix
{ - # SSH key for GitHub authentication (shared with all machines) - "keys/id_ed25519.age" = { - file = ./keys/id_ed25519.age; - publicKeys = allMachines; - }; - # GPG key (shared with all machines for commit signing) "keys/gpg.age" = { file = ./keys/gpg.age; publicKeys = allMachines; }; + + # SSH key for GitHub authentication (shared with all machines) + "keys/id_ed25519.age" = { + file = ./keys/id_ed25519.age; + publicKeys = allMachines; + }; }As per coding guidelines: "Sort attribute sets alphabetically in Nix files".
named-hosts/matic/secrets.nix (1)
10-19: Attribute set is not sorted alphabetically — same pattern as ingalactica/secrets.nix.
"keys/gpg.age"should precede"keys/id_github.age".Proposed fix
{ + "keys/gpg.age" = { + file = ../galactica/keys/gpg.age; + publicKeys = allMachines; + }; + "keys/id_github.age" = { file = ../galactica/keys/id_ed25519.age; publicKeys = allMachines; }; - - "keys/gpg.age" = { - file = ../galactica/keys/gpg.age; - publicKeys = allMachines; - }; }As per coding guidelines: "Sort attribute sets alphabetically in Nix files".
named-hosts/matic/default.nix (1)
256-271:keys/gpg.ageis registered inage.secretsbut theimportGpgKeyhook decrypts it manually from a different path.The
age.secretsmapping on line 256 registers bothkeys/id_github.ageandkeys/gpg.agewith agenix. However, theimportGpgKeyactivation hook (line 306) reads the GPG secret from a hardcoded repo path rather than from the agenix-managed secret location. This means the agenix-managedgpg.agesecret is effectively unused.Consider either:
- Removing
keys/gpg.agefromage.secretsif manual decryption is the intended approach, or- Having
importGpgKeyread fromconfig.age.secrets."keys/gpg.age".pathto use the agenix-managed secret.
| home.activation.importGpgKey = config.lib.dag.entryAfter [ "linkGeneration" ] '' | ||
| $VERBOSE_ECHO "🔑 Starting GPG key import process..." | ||
| GPG_SECRET_FILE="${config.home.homeDirectory}/dotfiles/named-hosts/galactica/keys/gpg.age" | ||
| GPG_TEMP_FILE="${config.home.homeDirectory}/.config/agenix/gpg.key" | ||
|
|
||
| # Create agenix directory if it doesn't exist | ||
| mkdir -p "${config.home.homeDirectory}/.config/agenix" | ||
|
|
||
| if [[ -f "$GPG_SECRET_FILE" ]]; then | ||
| # Check if key is already imported | ||
| if ! ${pkgs.gnupg}/bin/gpg --list-secret-keys 2>/dev/null | grep -q "C2E97FCFF482925D"; then | ||
| echo "Importing GPG key from agenix..." | ||
| # Try to decrypt - will fail silently if SSH key isn't authorized | ||
| if ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then | ||
| ${pkgs.gnupg}/bin/gpg --batch --import "$GPG_TEMP_FILE" 2>/dev/null | ||
| rm -f "$GPG_TEMP_FILE" | ||
| echo "✅ GPG key imported successfully" | ||
| fi | ||
| else | ||
| $VERBOSE_ECHO "ℹ️ GPG key already imported" | ||
| fi | ||
| fi | ||
| ''; |
There was a problem hiding this comment.
GPG secret file path is a fragile runtime assumption — inconsistent with the SSH key approach.
deployAgenixSecrets (line 291) correctly uses builtins.toString ../galactica/keys/id_ed25519.age which resolves to a Nix store path at build time, guaranteeing the encrypted file is available. However, importGpgKey (line 306) references ${config.home.homeDirectory}/dotfiles/named-hosts/galactica/keys/gpg.age — a runtime path that assumes the dotfiles repo is cloned at ~/dotfiles. On a fresh deployment or if the repo is elsewhere, this will silently fail.
Use the same builtins.toString pattern for consistency and reliability:
Proposed fix
home.activation.importGpgKey = config.lib.dag.entryAfter [ "linkGeneration" ] ''
$VERBOSE_ECHO "🔑 Starting GPG key import process..."
- GPG_SECRET_FILE="${config.home.homeDirectory}/dotfiles/named-hosts/galactica/keys/gpg.age"
+ GPG_SECRET_FILE="${builtins.toString ../galactica/keys/gpg.age}"
GPG_TEMP_FILE="${config.home.homeDirectory}/.config/agenix/gpg.key"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| home.activation.importGpgKey = config.lib.dag.entryAfter [ "linkGeneration" ] '' | |
| $VERBOSE_ECHO "🔑 Starting GPG key import process..." | |
| GPG_SECRET_FILE="${config.home.homeDirectory}/dotfiles/named-hosts/galactica/keys/gpg.age" | |
| GPG_TEMP_FILE="${config.home.homeDirectory}/.config/agenix/gpg.key" | |
| # Create agenix directory if it doesn't exist | |
| mkdir -p "${config.home.homeDirectory}/.config/agenix" | |
| if [[ -f "$GPG_SECRET_FILE" ]]; then | |
| # Check if key is already imported | |
| if ! ${pkgs.gnupg}/bin/gpg --list-secret-keys 2>/dev/null | grep -q "C2E97FCFF482925D"; then | |
| echo "Importing GPG key from agenix..." | |
| # Try to decrypt - will fail silently if SSH key isn't authorized | |
| if ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then | |
| ${pkgs.gnupg}/bin/gpg --batch --import "$GPG_TEMP_FILE" 2>/dev/null | |
| rm -f "$GPG_TEMP_FILE" | |
| echo "✅ GPG key imported successfully" | |
| fi | |
| else | |
| $VERBOSE_ECHO "ℹ️ GPG key already imported" | |
| fi | |
| fi | |
| ''; | |
| home.activation.importGpgKey = config.lib.dag.entryAfter [ "linkGeneration" ] '' | |
| $VERBOSE_ECHO "🔑 Starting GPG key import process..." | |
| GPG_SECRET_FILE="${builtins.toString ../galactica/keys/gpg.age}" | |
| GPG_TEMP_FILE="${config.home.homeDirectory}/.config/agenix/gpg.key" | |
| # Create agenix directory if it doesn't exist | |
| mkdir -p "${config.home.homeDirectory}/.config/agenix" | |
| if [[ -f "$GPG_SECRET_FILE" ]]; then | |
| # Check if key is already imported | |
| if ! ${pkgs.gnupg}/bin/gpg --list-secret-keys 2>/dev/null | grep -q "C2E97FCFF482925D"; then | |
| echo "Importing GPG key from agenix..." | |
| # Try to decrypt - will fail silently if SSH key isn't authorized | |
| if ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then | |
| ${pkgs.gnupg}/bin/gpg --batch --import "$GPG_TEMP_FILE" 2>/dev/null | |
| rm -f "$GPG_TEMP_FILE" | |
| echo "✅ GPG key imported successfully" | |
| fi | |
| else | |
| $VERBOSE_ECHO "ℹ️ GPG key already imported" | |
| fi | |
| fi | |
| ''; |
🤖 Prompt for AI Agents
In `@named-hosts/matic/default.nix` around lines 304 - 326, The importGpgKey
activation uses a fragile runtime path for GPG_SECRET_FILE; change it to use a
build-time Nix store path like deployAgenixSecrets does by setting
GPG_SECRET_FILE to builtins.toString ../galactica/keys/gpg.age (or the same
input used for id_ed25519.age) instead of
"${config.home.homeDirectory}/dotfiles/..."; update the GPG_SECRET_FILE
assignment in home.activation.importGpgKey, keep the rest of the import logic
(GPG_TEMP_FILE, rage decrypt, gpg --import) as-is, and ensure the value is
quoted so the script reads the Nix store path at runtime reliably.
| echo "Importing GPG key from agenix..." | ||
| # Try to decrypt - will fail silently if SSH key isn't authorized | ||
| if ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then | ||
| ${pkgs.gnupg}/bin/gpg --batch --import "$GPG_TEMP_FILE" 2>/dev/null | ||
| rm -f "$GPG_TEMP_FILE" |
There was a problem hiding this comment.
Decrypted GPG private key written to temp file without restrictive permissions.
rage -o "$GPG_TEMP_FILE" writes with the default umask. If the activation is interrupted between the decrypt (line 317) and the rm (line 319), the private key persists unencrypted and potentially world-readable. Set permissions explicitly:
Proposed fix
if ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 -o "$GPG_TEMP_FILE" "$GPG_SECRET_FILE" 2>/dev/null; then
+ chmod 600 "$GPG_TEMP_FILE"
${pkgs.gnupg}/bin/gpg --batch --import "$GPG_TEMP_FILE" 2>/dev/null
rm -f "$GPG_TEMP_FILE"
echo "✅ GPG key imported successfully"
fiAlternatively, consider wrapping in a trap 'rm -f "$GPG_TEMP_FILE"' EXIT to ensure cleanup on any failure path.
🤖 Prompt for AI Agents
In `@named-hosts/matic/default.nix` around lines 315 - 319, The decrypted GPG
private key is written to $GPG_TEMP_FILE with default permissions; ensure it's
created with restrictive permissions and always removed on failure: after
running ${pkgs.rage}/bin/rage -d -i ... -o "$GPG_TEMP_FILE" (the command that
produces GPG_TEMP_FILE) immediately set chmod 600 on "$GPG_TEMP_FILE" and
register a trap to rm -f "$GPG_TEMP_FILE" on EXIT (so cleanup happens even if
activation is interrupted), then proceed to call ${pkgs.gnupg}/bin/gpg --batch
--import and finally rm -f "$GPG_TEMP_FILE" as currently done.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="config/hyprland/hyprland.conf">
<violation number="1" location="config/hyprland/hyprland.conf:148">
P2: `windowrule` (V1) doesn’t accept `class:` selectors, so these rules won’t match any windows. Use a plain class regex for V1 rules or revert to `windowrulev2`.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="config/hyprland/hyprland.conf">
<violation number="1" location="config/hyprland/hyprland.conf:159">
P2: This rule enables a slide animation for wofi even though the intention is to prevent animations. Use `noanim` so the launcher doesn’t animate.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| layerrule = ignore_alpha 0.0, match:namespace swaync-notification-window | ||
|
|
||
| # Prevent wofi animation | ||
| layerrule = animation slide, match:namespace ^(wofi)$ |
There was a problem hiding this comment.
P2: This rule enables a slide animation for wofi even though the intention is to prevent animations. Use noanim so the launcher doesn’t animate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At config/hyprland/hyprland.conf, line 159:
<comment>This rule enables a slide animation for wofi even though the intention is to prevent animations. Use `noanim` so the launcher doesn’t animate.</comment>
<file context>
@@ -145,20 +143,20 @@ misc {
# Prevent wofi animation
-layerrule = noanim, ^(wofi)$
+layerrule = animation slide, match:namespace ^(wofi)$
# =============================================================================
</file context>
| layerrule = animation slide, match:namespace ^(wofi)$ | |
| layerrule = noanim on, match:namespace ^(wofi)$ |
…ake.nix and modify lock file entries
….3 in flake.nix and modify lock file entries" This reverts commit 80ad0ed.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@named-hosts/matic/default.nix`:
- Around line 286-299: The shell heredoc in the deployAgenixSecrets fragment
(and similar blocks ensureSshDirectory, ensureAgenixDirectory, importGpgKey)
uses unquoted Nix interpolations like ${config.home.homeDirectory} and
$SECRET_FILE which can be split if paths contain spaces; fix by quoting all path
expansions in the shell snippets (use "${config.home.homeDirectory}",
"\"$SECRET_FILE\"" or ideally SECRET_FILE already set then reference as
"$SECRET_FILE"), and quote targets passed to commands (e.g., the rage -i/-o
arguments and chmod target) so every interpolated/expanded path is wrapped in
double quotes to prevent word-splitting and globbing.
- Around line 308-309: The mkdir line that creates
"${config.home.homeDirectory}/.config/agenix" should not run during dry runs and
is redundant; either prefix it with $DRY_RUN_CMD (i.e. change the invocation in
the activation hook to use $DRY_RUN_CMD mkdir -p) or remove the line entirely
because ensureAgenixDirectory already creates that directory before
writeBoundary and importGpgKey runs after linkGeneration—update the activation
hook accordingly and keep ensureAgenixDirectory, writeBoundary, linkGeneration,
and importGpgKey order in mind when removing the redundant mkdir.
🧹 Nitpick comments (8)
config/waybar/style.css (1)
76-98:#battery.chargingduplicates the base#batterycolor — intentional clarity or oversight?Both
#battery(Line 77) and#battery.charging(Line 97) setcolor:@green``. This is harmless and arguably self-documenting, but if the base battery color ever changes, this rule would mask the update for the charging state.Makefile (1)
790-796: Consider guarding the Waybar restart with a Hyprland availability check.This target runs on all Linux hosts via the
systemctlumbrella (line 752), butpkill waybarandhyprctlare only relevant on Hyprland-enabled hosts (matic). On other Linux hosts it's harmless (due to|| true) but prints misleading "Restarting waybar…" / "waybar restarted" messages.An optional improvement to reduce noise on non-Hyprland hosts:
♻️ Suggested guard
.PHONY: systemctl-waybar systemctl-waybar: ## Restart waybar. - `@echo` "🔄 Restarting waybar..." - `@pkill` waybar 2>/dev/null || true - `@sleep` 1 - `@hyprctl` dispatch exec waybar 2>/dev/null || true - `@echo` "✅ waybar restarted" + `@if` command -v hyprctl >/dev/null 2>&1; then \ + echo "🔄 Restarting waybar..."; \ + pkill waybar 2>/dev/null || true; \ + sleep 1; \ + hyprctl dispatch exec waybar 2>/dev/null || true; \ + echo "✅ waybar restarted"; \ + finamed-hosts/matic/default.nix (2)
336-342: GPG agent cache TTL of 3 years effectively disables passphrase re-prompting.
defaultCacheTtlandmaxCacheTtlare both set to ~3 years (94608000s). This means a compromised session or stolen machine with an active gpg-agent would have unlimited access to the signing key without re-authentication. A common balanced value is 1–8 hours (3600–28800s).If this is intentional for your workflow, feel free to ignore — just flagging the trade-off.
253-270:age.secretsmapping relies on a magic string to identify the GitHub key.The
name == "keys/id_github.age"check on line 261 must exactly match the attribute name exported by./secrets.nix. If that name changes, the match silently fails and the SSH key deploys without the correct path/permissions. Consider defining the secrets explicitly rather than mapping over the import, or extracting the key name to a shared variable.flake.nix (1)
54-60: Inputs are not sorted alphabetically.Per coding guidelines, attribute sets in Nix files should be sorted alphabetically.
hyprlandandhyprland-pluginsshould appear afterhome-managerand beforemk-shell-bin, not afternixos-hardware.Also,
hyprland-pluginsis unpinned (tracks the default branch). Sincehyprlandis pinned tov0.53.3, a futurehyprland-pluginsupdate could introduce incompatible plugin ABIs. Consider pinning to a compatible tag or commit.Suggested reorder (move block after line 11)
home-manager = { url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; + hyprland = { + url = "github:hyprwm/Hyprland/v0.53.3"; + }; + hyprland-plugins = { + url = "github:hyprwm/hyprland-plugins"; + inputs.hyprland.follows = "hyprland"; + }; mk-shell-bin = {nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; }; - hyprland = { - url = "github:hyprwm/Hyprland/v0.53.3"; - }; - hyprland-plugins = { - url = "github:hyprwm/hyprland-plugins"; - inputs.hyprland.follows = "hyprland"; - }; };As per coding guidelines: "Sort attribute sets alphabetically in Nix files" and "Always pin package versions in
flake.lock".config/hyprland/default.nix (1)
11-19: Attributes not sorted alphabetically; consider a brief comment forsystemd.enable = false.Per coding guidelines, attribute sets should be sorted alphabetically. Within the
hyprlandblock, the order should be:enable,extraConfig,package,systemd. Also, disabling systemd integration is a deliberate choice — a one-line comment would clarify why (e.g., session services launched viaexec-oncein the conf instead).Suggested reorder and comment
wayland.windowManager.hyprland = { enable = true; - package = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland; - systemd.enable = false; extraConfig = '' plugin = ${hyprexpoPlugin}/lib/libhyprexpo.so '' + builtins.readFile ./hyprland.conf; + package = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland; + # Session services are started via exec-once in hyprland.conf + systemd.enable = false; };As per coding guidelines: "Sort attribute sets alphabetically in Nix files" and "Document complex configurations with comments in Nix files".
config/hyprland/hyprland.conf (2)
159-175:$mod, Vfor VS Code is placed under the "Launcher & Clipboard" heading instead of "App Launch Hotkeys".Line 173 (
bind = $mod, V, exec, code) is an app-launch binding, not a launcher or clipboard action. Consider moving it up to the "App Launch Hotkeys" section (lines 159–168) for consistency.
289-300: Hyprexpo plugin config looks reasonable.Note that the
bg_coluses a rawrgb(282a36)literal rather than the$backgroundvariable defined at line 4. Using the variable would keep the Dracula palette centralized.Suggested tweak
hyprexpo { columns = 3 gap_size = 5 - bg_col = rgb(282a36) + bg_col = $background workspace_method = first 1 gesture_distance = 300 }
| home.activation.deployAgenixSecrets = config.lib.dag.entryAfter [ "writeBoundary" ] '' | ||
| # Decrypt and deploy GitHub SSH key if it doesn't exist | ||
| if [[ ! -f "${config.home.homeDirectory}/.ssh/id_ed25519_github" ]]; then | ||
| echo "Deploying GitHub SSH key from agenix..." | ||
| SECRET_FILE="${builtins.toString ../galactica/keys/id_ed25519.age}" | ||
| if [[ -f "$SECRET_FILE" ]]; then | ||
| $DRY_RUN_CMD ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 "$SECRET_FILE" -o ${config.home.homeDirectory}/.ssh/id_ed25519_github | ||
| $DRY_RUN_CMD chmod $VERBOSE_ARG 0600 ${config.home.homeDirectory}/.ssh/id_ed25519_github | ||
| echo "✅ GitHub SSH key deployed successfully" | ||
| else | ||
| echo "⚠️ Warning: Secret file not found at $SECRET_FILE" | ||
| fi | ||
| fi | ||
| ''; |
There was a problem hiding this comment.
Unquoted Nix-interpolated paths in shell commands risk word splitting.
Several config.home.homeDirectory interpolations are unquoted in the shell script (lines 292–293), while others (line 288) are properly quoted. If the home directory ever contains spaces or special characters, the unquoted paths will break.
Proposed fix — quote all path interpolations consistently
- if [[ ! -f "${config.home.homeDirectory}/.ssh/id_ed25519_github" ]]; then
+ if [[ ! -f "${config.home.homeDirectory}/.ssh/id_ed25519_github" ]]; then
echo "Deploying GitHub SSH key from agenix..."
SECRET_FILE="${builtins.toString ../galactica/keys/id_ed25519.age}"
if [[ -f "$SECRET_FILE" ]]; then
- $DRY_RUN_CMD ${pkgs.rage}/bin/rage -d -i ${config.home.homeDirectory}/.ssh/id_ed25519 "$SECRET_FILE" -o ${config.home.homeDirectory}/.ssh/id_ed25519_github
- $DRY_RUN_CMD chmod $VERBOSE_ARG 0600 ${config.home.homeDirectory}/.ssh/id_ed25519_github
+ $DRY_RUN_CMD ${pkgs.rage}/bin/rage -d -i "${config.home.homeDirectory}/.ssh/id_ed25519" "$SECRET_FILE" -o "${config.home.homeDirectory}/.ssh/id_ed25519_github"
+ $DRY_RUN_CMD chmod $VERBOSE_ARG 0600 "${config.home.homeDirectory}/.ssh/id_ed25519_github"
echo "✅ GitHub SSH key deployed successfully"
elseThe same quoting inconsistency also affects ensureSshDirectory (lines 274–275), ensureAgenixDirectory (lines 280–281), and importGpgKey (lines 305, 309, 316).
🤖 Prompt for AI Agents
In `@named-hosts/matic/default.nix` around lines 286 - 299, The shell heredoc in
the deployAgenixSecrets fragment (and similar blocks ensureSshDirectory,
ensureAgenixDirectory, importGpgKey) uses unquoted Nix interpolations like
${config.home.homeDirectory} and $SECRET_FILE which can be split if paths
contain spaces; fix by quoting all path expansions in the shell snippets (use
"${config.home.homeDirectory}", "\"$SECRET_FILE\"" or ideally SECRET_FILE
already set then reference as "$SECRET_FILE"), and quote targets passed to
commands (e.g., the rage -i/-o arguments and chmod target) so every
interpolated/expanded path is wrapped in double quotes to prevent word-splitting
and globbing.
| # Create agenix directory if it doesn't exist | ||
| mkdir -p "${config.home.homeDirectory}/.config/agenix" |
There was a problem hiding this comment.
mkdir -p missing $DRY_RUN_CMD prefix — will create the directory even during dry runs.
Other activation hooks (ensureSshDirectory, ensureAgenixDirectory) consistently use $DRY_RUN_CMD mkdir -p, but this one does not.
Proposed fix
# Create agenix directory if it doesn't exist
- mkdir -p "${config.home.homeDirectory}/.config/agenix"
+ $DRY_RUN_CMD mkdir -p $VERBOSE_ARG "${config.home.homeDirectory}/.config/agenix"Note: This mkdir is also redundant since ensureAgenixDirectory (line 279) already creates this directory before writeBoundary, and importGpgKey runs after linkGeneration. Consider removing it entirely if the ordering is guaranteed.
🤖 Prompt for AI Agents
In `@named-hosts/matic/default.nix` around lines 308 - 309, The mkdir line that
creates "${config.home.homeDirectory}/.config/agenix" should not run during dry
runs and is redundant; either prefix it with $DRY_RUN_CMD (i.e. change the
invocation in the activation hook to use $DRY_RUN_CMD mkdir -p) or remove the
line entirely because ensureAgenixDirectory already creates that directory
before writeBoundary and importGpgKey runs after linkGeneration—update the
activation hook accordingly and keep ensureAgenixDirectory, writeBoundary,
linkGeneration, and importGpgKey order in mind when removing the redundant
mkdir.
…e.nix and update default.nix to use pkgs directly
…n for media controls
feat(keyd): add mapping for Ctrl+Super+F to trigger fullscreen in Hyprland
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@config/ghostty/config`:
- Around line 19-20: The current keybind entries bind bare Ctrl+C to
copy_to_clipboard, which intercepts SIGINT; change the bindings to use the
performable: prefix so the copy action only runs when applicable and otherwise
the key is passed through — update the entries referencing the keybind lines
(e.g., the existing "keybind = ctrl+c=copy_to_clipboard" rule) to "keybind =
ctrl+c=performable:copy_to_clipboard" while leaving "keybind =
ctrl+v=paste_from_clipboard" as-is or apply performable: if desired.
In `@config/hyprland/hyprland.conf`:
- Line 48: The exec-once setup creates /tmp/wobpipe and runs wob but nothing
writes to it, so the OSD never appears; update the volume and brightness key
handlers (the commands that call wpctl and brightnessctl) to send the current
value into /tmp/wobpipe after each adjustment (e.g., query the new
volume/brightness and echo that formatted value into /tmp/wobpipe), or remove
the exec-once wob/fifo setup entirely if you don't want an OSD; specifically
modify the commands that invoke wpctl and brightnessctl to append a write to
/tmp/wobpipe so wob receives updates.
- Around line 161-167: The binding "bind = $mod, C, exec, cursor" will fail
because the desktop binary isn't available on Linux; replace it with the
installed CLI binary name from the package set (e.g., use "cursor-cli") or point
the binding to the correct executable provided by the Linux package; update the
bind line that references "cursor" so it calls the available binary (reference:
the bind line with "$mod, C, exec, cursor" and the package "cursor-cli") and
keep the existing gtk-launch entries for 1password and signal-desktop as-is.
🧹 Nitpick comments (8)
named-hosts/matic/default.nix (2)
252-269: Hardcoded/home/${username}vsconfig.home.homeDirectoryinconsistency.Lines 253 and 263 use
/home/${username}while activation hooks (lines 272+) useconfig.home.homeDirectory. Both resolve to the same value here (line 68 setshome = "/home/${username}"), but using the config attribute consistently would be more robust.
326-341: Attribute sets not sorted alphabetically.Per coding guidelines, attribute sets in Nix files should be sorted alphabetically. The
services.gpg-agentblock has attributes out of order.Proposed fix
services.gpg-agent = { + defaultCacheTtl = 94608000; # 3 years enable = true; enableSshSupport = false; - pinentry.package = pkgs.pinentry-tty; - defaultCacheTtl = 94608000; # 3 years maxCacheTtl = 94608000; # 3 years + pinentry.package = pkgs.pinentry-tty; };Also,
defaultCacheTtlandmaxCacheTtlof ~3 years means the GPG passphrase is effectively never re-prompted. This is a deliberate convenience trade-off worth documenting if intentional.As per coding guidelines: "Sort attribute sets alphabetically in Nix files".
home-manager/packages/default.nix (1)
117-117:xclipmay be redundant givenwl-clipboardfor Wayland desktops.
xclip(X11 clipboard) is added to the general Linux list, whilewl-clipboardis in the Linux+Desktop (Wayland) list. If "matic" runs Hyprland (Wayland-only),xclipwon't function natively — it only works under XWayland. This is fine if other non-desktop Linux hosts still use X11, but worth confirming the intent.config/hyprland/default.nix (2)
1-29: Add brief comments explaining non-obvious configuration choices.Per coding guidelines, complex configurations should be documented. A few choices here would benefit from a short inline comment:
- Line 14: Why is
systemd.enable = false? (e.g., using a login manager instead, or manual session start)- Line 23/27: Why
force = trueon the XDG config files?This helps future maintainers (or yourself) understand the intent quickly. Based on learnings, document all major changes in Nix configurations.
1-6: Unusedconfigandinputsparameters.Neither
config(line 2) norinputs(line 3) is referenced in the module body — onlypkgsis used. While listing them is harmless (standard home-manager module pattern),inputsin particular is non-standard and implies it's passed viaspecialArgs. If it's not needed here, consider removing it to keep the interface clean.Proposed cleanup
{ - config, - inputs, pkgs, ... }:config/hyprland/hyprland.conf (3)
183-183: Inconsistent use ofSUPERinstead of$mod.Line 183 uses the literal
SUPER CTRL, Fwhile every other binding uses$mod. Since$mod = SUPER(line 19), this works, but it's inconsistent and will break if$modis ever changed.Fix
-bind = SUPER CTRL, F, fullscreen, 0 +bind = $mod CTRL, F, fullscreen, 0
169-175: Section header "Launcher & Clipboard" is misleading —$mod, Vlaunches VS Code.
bind = $mod, V, exec, code(line 172) is an app-launch binding, not a launcher or clipboard action. Consider moving it to the "App Launch Hotkeys" section (lines 158–167) for clarity, or renaming this section.
282-283: HardcodedeDP-1display name in scale toggle may not match all setups.The monitor line (53) uses auto-detection (
, preferred, auto, 1.5), but the F9 toggle (lines 282–283) hardcodeseDP-1. If Hyprland names the display differently, the toggle will fail. Consider querying the monitor name dynamically:Dynamic monitor name approach
-bindl = , F9, exec, hyprctl -j monitors | grep -q '"scale": 1.5' && hyprctl keyword monitor "eDP-1, preferred, auto, 1.0" || hyprctl keyword monitor "eDP-1, preferred, auto, 1.5" +bindl = , F9, exec, MON=$(hyprctl -j monitors | jq -r '.[0].name'); hyprctl -j monitors | grep -q '"scale": 1.5' && hyprctl keyword monitor "$MON, preferred, auto, 1.0" || hyprctl keyword monitor "$MON, preferred, auto, 1.5"
| keybind = ctrl+c=copy_to_clipboard | ||
| keybind = ctrl+v=paste_from_clipboard |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ghostty performable keybind prefix syntax documentation
💡 Result:
Ghostty keybind “prefix” syntax is documented under Keybindings → Trigger Prefixes. A keybind is:
keybind = <prefixes>:<trigger>=<action>
Prefixes are written before the trigger, separated by : (and you can stack multiple): e.g.
keybind = global:unconsumed:ctrl+a=reload_config [1]
performable: prefix
performable: means only consume the key if the action can actually run (otherwise Ghostty acts like the binding doesn’t exist, so the key falls through to the app in the terminal). Example:
keybind = performable:ctrl+c=copy_to_clipboard [1][2]
Notes:
- It has no effect with
global:orall:prefixed keybinds. [1][2] - For key sequences, if the action isn’t performable it resets the sequence (as if no keybind was set). [1][2]
performable:keybinds won’t show as menu shortcuts. [1][2]
Related docs
- Keybindings / Trigger Prefixes (includes
performable:): [1] - Option reference for the same prefixes (with “Available since” info): [2]
- Trigger sequences (if you mean “prefix key” sequences like
ctrl+a>n=...): [5]
🌐 Web query:
Ghostty ctrl+c copy clipboard keybind SIGINT terminal
💡 Result:
To make Ctrl+C act as Copy only when there’s a selection, and otherwise pass through to the PTY as SIGINT (interrupt), bind it with Ghostty’s performable: prefix:
# ~/.config/ghostty/ghostty.conf
keybind = performable:ctrl+c=copy_to_clipboardcopy_to_clipboardcopies the current selection. (ghostty.org)performable:means Ghostty only consumes the keypress if the action can be performed (i.e., there is something selected). If not, Ghostty behaves as if the keybind isn’t set—so Ctrl+C is sent to the running program, producing SIGINT in shells/programs likesleep 100. (ghostty.org)
Tip: you can inspect what’s currently bound (defaults and overrides) with:
ghostty +list-keybinds --defaultCitations:
- 1: https://ghostty.org/docs/config/keybind/reference?utm_source=openai
- 2: https://ghostty.org/docs/config/keybind?utm_source=openai
- 3: https://ghostty.org/docs/config/keybind?utm_source=openai
🏁 Script executed:
# Check the keyd config mentioned in the review to understand the Framework key mapping
find . -name "default.conf" -path "*/keyd/*" | head -5Repository: shunkakinoki/dotfiles
Length of output: 91
🏁 Script executed:
# Search for Ghostty documentation or config examples in the repo
find . -name "ghostty" -o -name "*ghostty*" -type d 2>/dev/null | head -10Repository: shunkakinoki/dotfiles
Length of output: 81
🏁 Script executed:
# Check the keyd config to understand the Framework key to Ctrl+C mapping context
cat -n ./config/keyd/default.conf | head -40Repository: shunkakinoki/dotfiles
Length of output: 954
🏁 Script executed:
# Check the current Ghostty config around the mentioned lines
cat -n ./config/ghostty/config | head -30Repository: shunkakinoki/dotfiles
Length of output: 960
ctrl+c=copy_to_clipboard intercepts SIGINT — use performable: prefix instead.
Binding ctrl+c unconditionally to copy_to_clipboard prevents sending interrupt signals to running processes. The keyd cmd_mac layer (Framework+C → Ctrl+C) will still work, but users cannot send SIGINT directly via bare Ctrl+C from the keyboard.
Ghostty's performable: prefix triggers the action only when it can be performed (i.e., text is selected); otherwise the key passes through to the terminal. This preserves both copy on selection and SIGINT when needed.
Proposed fix
-keybind = ctrl+c=copy_to_clipboard
-keybind = ctrl+v=paste_from_clipboard
+keybind = performable:ctrl+c=copy_to_clipboard
+keybind = performable:ctrl+v=paste_from_clipboard📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| keybind = ctrl+c=copy_to_clipboard | |
| keybind = ctrl+v=paste_from_clipboard | |
| keybind = performable:ctrl+c=copy_to_clipboard | |
| keybind = performable:ctrl+v=paste_from_clipboard |
🤖 Prompt for AI Agents
In `@config/ghostty/config` around lines 19 - 20, The current keybind entries bind
bare Ctrl+C to copy_to_clipboard, which intercepts SIGINT; change the bindings
to use the performable: prefix so the copy action only runs when applicable and
otherwise the key is passed through — update the entries referencing the keybind
lines (e.g., the existing "keybind = ctrl+c=copy_to_clipboard" rule) to "keybind
= ctrl+c=performable:copy_to_clipboard" while leaving "keybind =
ctrl+v=paste_from_clipboard" as-is or apply performable: if desired.
| exec-once = hypridle | ||
|
|
||
| # Volume/brightness OSD | ||
| exec-once = rm -f /tmp/wobpipe && mkfifo /tmp/wobpipe && tail -f /tmp/wobpipe | wob |
There was a problem hiding this comment.
wob OSD pipe is created but never fed — no visual feedback for volume/brightness.
The named pipe /tmp/wobpipe is created and wob reads from it, but all volume commands (lines 257–264) use wpctl directly and brightness commands (lines 276–280) use brightnessctl directly — neither pipes output to /tmp/wobpipe. The OSD bar will never appear.
Either pipe the current value after each adjustment, or remove the wob setup if you don't want an OSD.
Example fix for volume keys (apply similar pattern to brightness)
# F2: Volume down
-bindel = , XF86AudioLowerVolume, exec, wpctl set-volume `@DEFAULT_AUDIO_SINK`@ 5%-
+bindel = , XF86AudioLowerVolume, exec, wpctl set-volume `@DEFAULT_AUDIO_SINK`@ 5%- && wpctl get-volume `@DEFAULT_AUDIO_SINK`@ | awk '{print int($2*100)}' > /tmp/wobpipe
-bindel = , F2, exec, wpctl set-volume `@DEFAULT_AUDIO_SINK`@ 5%-
+bindel = , F2, exec, wpctl set-volume `@DEFAULT_AUDIO_SINK`@ 5%- && wpctl get-volume `@DEFAULT_AUDIO_SINK`@ | awk '{print int($2*100)}' > /tmp/wobpipe🤖 Prompt for AI Agents
In `@config/hyprland/hyprland.conf` at line 48, The exec-once setup creates
/tmp/wobpipe and runs wob but nothing writes to it, so the OSD never appears;
update the volume and brightness key handlers (the commands that call wpctl and
brightnessctl) to send the current value into /tmp/wobpipe after each adjustment
(e.g., query the new volume/brightness and echo that formatted value into
/tmp/wobpipe), or remove the exec-once wob/fifo setup entirely if you don't want
an OSD; specifically modify the commands that invoke wpctl and brightnessctl to
append a write to /tmp/wobpipe so wob receives updates.
| bind = $mod, T, exec, ghostty | ||
| bind = $mod, G, exec, google-chrome-stable | ||
| bind = $mod, S, exec, slack | ||
| bind = $mod, C, exec, cursor | ||
| bind = $mod, P, exec, gtk-launch 1password | ||
| bind = $mod, M, exec, gtk-launch signal-desktop | ||
| bind = $mod, N, exec, ghostty -e yazi |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's understand the repository structure
git ls-files | head -30Repository: shunkakinoki/dotfiles
Length of output: 665
🏁 Script executed:
# Search for nix configuration files that define packages
fd -e nix | head -20Repository: shunkakinoki/dotfiles
Length of output: 574
🏁 Script executed:
# Look for the packages/dependencies configuration
rg -n 'cursor|1password|signal-desktop' --type nix | head -30Repository: shunkakinoki/dotfiles
Length of output: 454
🏁 Script executed:
# Check the specific file for context
cat -n config/hyprland/hyprland.conf | sed -n '155,175p'Repository: shunkakinoki/dotfiles
Length of output: 1116
🏁 Script executed:
# Check what the cursor config module does
cat -n config/cursor/default.nixRepository: shunkakinoki/dotfiles
Length of output: 221
🏁 Script executed:
# Check home-manager packages to see full context
cat -n home-manager/packages/default.nix | sed -n '20,35p'Repository: shunkakinoki/dotfiles
Length of output: 321
🏁 Script executed:
# Check for .desktop files or xdg configuration
rg -n '\.desktop|xdgData|desktopItems' --type nix | head -20Repository: shunkakinoki/dotfiles
Length of output: 152
🏁 Script executed:
# Search for 1password and signal-desktop configuration
rg -n '_1password-gui|signal-desktop' --type nix -B2 -A2Repository: shunkakinoki/dotfiles
Length of output: 601
🏁 Script executed:
# Check home-manager configuration for xdg.desktopEntries or similar
rg -n 'xdg\.|desktopEntries' --type nix | head -20Repository: shunkakinoki/dotfiles
Length of output: 1696
🏁 Script executed:
# Look for how packages are imported and if there's a desktop environment setup
cat -n home-manager/packages/default.nix | sed -n '1,30p'Repository: shunkakinoki/dotfiles
Length of output: 629
🏁 Script executed:
# Check if there's any configuration for gtk or application launching
rg -n 'gtk-launch|desktopDatabase|xdg-utils' --type nixRepository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Verify the context around isDesktop and Linux packages
cat -n home-manager/packages/default.nix | sed -n '110,145p'Repository: shunkakinoki/dotfiles
Length of output: 776
🏁 Script executed:
# Check if cursor is defined anywhere else in the nix config
rg -n '^cursor\s*=' --type nixRepository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Check if there's a custom desktop entry or application launcher for cursor
rg -n 'cursor' home-manager/packages/default.nix -B2 -A2Repository: shunkakinoki/dotfiles
Length of output: 124
🏁 Script executed:
# Verify if nix-darwin homebrew config is used for Linux systems
cat -n nix-darwin/config/homebrew.nix | sed -n '70,85p'Repository: shunkakinoki/dotfiles
Length of output: 478
🏁 Script executed:
# Check the system-specific configuration to understand if this is Linux or macOS
rg -n 'isDesktop|stdenv.isLinux' --type nix config/hyprland/ home-manager/ named-hosts/ | head -10Repository: shunkakinoki/dotfiles
Length of output: 1263
🏁 Script executed:
# Check if cursor is wrapped or aliased anywhere
rg -n 'cursor' --type nix home-manager/ config/ | grep -v 'cursor-cli' | grep -v '.cursor'Repository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Verify the system this hyprland config is for - look at named-hosts
ls -la named-hosts/Repository: shunkakinoki/dotfiles
Length of output: 410
🏁 Script executed:
# Check which host system uses hyprland
rg -n 'hyprland' --type nix named-hosts/Repository: shunkakinoki/dotfiles
Length of output: 386
🏁 Script executed:
# Verify package names for 1password and signal-desktop
rg -n 'lib.optionals.*isDesktop' home-manager/packages/default.nix -A 30Repository: shunkakinoki/dotfiles
Length of output: 594
🏁 Script executed:
# Double-check: is there any wrapping or aliasing of cursor-cli to cursor?
rg -n 'makeWrapper|symlinkJoin' --type nix | grep cursorRepository: shunkakinoki/dotfiles
Length of output: 47
🏁 Script executed:
# Check what _1password-gui and signal-desktop actually provide
rg -n 'desktop|1password|signal' home-manager/ config/ named-hosts/ | grep -i 'desktop' | head -10Repository: shunkakinoki/dotfiles
Length of output: 973
🏁 Script executed:
# Verify the exact package names and if they have .desktop files
cat -n home-manager/packages/default.nix | sed -n '115,150p'Repository: shunkakinoki/dotfiles
Length of output: 755
🏁 Script executed:
# Final verification: check if this is the matic host (Linux) or another host
cat -n named-hosts/matic/default.nix | sed -n '100,130p'Repository: shunkakinoki/dotfiles
Length of output: 1224
🏁 Script executed:
# Confirm what exact packages are included for desktop Linux systems
cat -n home-manager/packages/default.nix | sed -n '1,10p'Repository: shunkakinoki/dotfiles
Length of output: 275
Fix the cursor binding — the desktop app binary is not available.
The binding on line 164 will fail at runtime. Only cursor-cli is in the Linux packages (home-manager/packages/default.nix), not the cursor desktop editor binary. The cursor entry in nix-darwin/config/homebrew.nix is for macOS and not applicable here.
The gtk-launch 1password and gtk-launch signal-desktop bindings should work correctly—both _1password-gui and signal-desktop are present in the Linux desktop packages and will provide the required .desktop files.
🤖 Prompt for AI Agents
In `@config/hyprland/hyprland.conf` around lines 161 - 167, The binding "bind =
$mod, C, exec, cursor" will fail because the desktop binary isn't available on
Linux; replace it with the installed CLI binary name from the package set (e.g.,
use "cursor-cli") or point the binding to the correct executable provided by the
Linux package; update the bind line that references "cursor" so it calls the
available binary (reference: the bind line with "$mod, C, exec, cursor" and the
package "cursor-cli") and keep the existing gtk-launch entries for 1password and
signal-desktop as-is.
Summary\n- add Hyprland config for matic with Super+T/S/G app launchers via gtk-launch\n- add keyd config (Caps Lock + Framework key -> Super, Right Shift double-tap -> Caps Lock)\n- wire agenix GitHub SSH key deployment for matic and add matic pubkey to galactica secrets\n\n## Testing\n- make build HOST=matic\n\n## Manual steps\n- On galactica: make rekey-galactica && git add named-hosts/galactica/keys && git commit -m "chore(agenix): rekey for matic" && git push\n- On matic: sudo make switch HOST=matic\n- Verify keyd/framework key mapping with: sudo keyd monitor
Summary by cubic
Switch matic to Hyprland with Super-based hotkeys and keyd mappings for Caps/Right Alt, plus mac-style Command on the Framework key. Also deploy the GitHub SSH key via agenix, import the GPG key for signing, and add matic’s pubkey to galactica.
New Features
Migration
Written for commit 0d18047. Summary will update on new commits.