{ inputs, lib, pkgs, ... }: let assets = "${inputs.self}/assets"; upstream = "${inputs.arch-hyprland}/.config"; dots = "${inputs.dots-hyprland}/dots"; wallpaper = "${assets}/wallpaper/wallpaper.png"; iconDataDirs = lib.makeSearchPath "share" [ pkgs.kdePackages.breeze-icons pkgs.adwaita-icon-theme pkgs.hicolor-icon-theme ]; # end-4's shell is distributed as a Quickshell config, not a Waybar theme. # Keep the launcher wrapped so the extra QML modules used by the upstream # config are visible to Nixpkgs' Quickshell package. qsWrapper = pkgs.writeShellScriptBin "qs" '' export XDG_DATA_DIRS="${iconDataDirs}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export QML2_IMPORT_PATH="${lib.makeSearchPath "lib/qt-6/qml" [ pkgs.qt6Packages.qt5compat pkgs.qt6Packages.qtimageformats pkgs.qt6Packages.qtmultimedia pkgs.qt6Packages.qtpositioning pkgs.qt6Packages.qtquicktimeline pkgs.qt6Packages.qtsensors pkgs.qt6Packages.qtvirtualkeyboard # kirigami is a wrapped package in Nixpkgs; its wrapper has no QML # files. Quickshell needs the unwrapped runtime output here. pkgs.kdePackages.kirigami.unwrapped pkgs.kdePackages.qqc2-desktop-style pkgs.kdePackages.syntax-highlighting pkgs.quickshell ]}''${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" exec ${pkgs.quickshell}/bin/qs "$@" ''; quickshellPython = pkgs.python3.withPackages (pythonPackages: with pythonPackages; [ materialyoucolor pillow ]); quickshellConfig = pkgs.runCommand "ena-quickshell-config" {} '' mkdir -p "$out" cp -R ${dots}/.config/quickshell/ii/. "$out/" chmod -R u+w "$out" mkdir -p "$out/defaults/ai/prompts" ln -s ${assets}/prompt/mizuki.md "$out/defaults/ai/prompts/mizuki.md" cp ${./quickshell/OpenAiApiStrategy.qml} "$out/services/ai/OpenAiApiStrategy.qml" rm -rf "$out/modules/common/widgets/shapes" mkdir -p "$out/modules/common/widgets/shapes" cp -R ${inputs.rounded-polygon-qmljs}/. "$out/modules/common/widgets/shapes/" substituteInPlace "$out/modules/common/Config.qml" \ --replace-fail 'property string wallpaperPath: ""' 'property string wallpaperPath: "${wallpaper}"' substituteInPlace "$out/services/Wallpapers.qml" \ --replace-fail '`''${Directories.pictures}/Wallpapers`' '`''${Directories.pictures}/wallpapers`' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' root.addUserModels() // Config onReadyChanged above might not fire if config is loaded before this service' \ ' root.addUserModels() // Config onReadyChanged above might not fire if config is loaded before this service root.loadPrompt(`''${Directories.defaultAiPrompts}/mizuki.md`); KeyringStorage.fetchKeyringData();' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' root.addMessage(Translation.tr("Loaded the following system prompt\n\n---\n\n%1").arg(Config.options.ai.systemPrompt), root.interfaceRole);' \ ' // Keep the system prompt internal; do not render it in the chat.' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' if (requester.message.thinking) requester.message.thinking = false;' \ ' // Responses emits metadata events before visible text. Keep the loading indicator // until the parser has actually appended content or finished the response.' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' const result = requester.currentStrategy.parseResponseLine(data, requester.message);' \ ' const result = requester.currentStrategy.parseResponseLine(data, requester.message); if (requester.message.thinking && (requester.message.content.length > 0 || result.functionCall || result.finished)) requester.message.thinking = false;' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' // Fetch API keys if needed' \ ' if (model?.requires_key && !KeyringStorage.loaded) { KeyringStorage.fetchKeyringData(); root.addMessage(Translation.tr("API key is still loading. Please try again in a moment."), root.interfaceRole); return; } if (model?.requires_key && !root.currentModelHasApiKey) { root.addApiKeyAdvice(model); return; } // Fetch API keys if needed' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' const result = requester.currentStrategy.onRequestFinished(requester.message);' \ ' if (!requester.message.done && requester.message.content.length === 0) { const reason = exitCode === 0 ? Translation.tr("The API returned no visible text. Check the API key and endpoint.") : Translation.tr("The API request exited with code %1.").arg(exitCode); requester.message.rawContent = "**Error**: " + reason; requester.message.content = requester.message.rawContent; } const result = requester.currentStrategy.onRequestFinished(requester.message);' substituteInPlace "$out/services/Ai.qml" \ --replace-fail ' property list availableTools: Object.keys(root.tools[models[currentModelId]?.api_format])' \ ' property list availableTools: Object.keys(root.tools[models[currentModelId]?.api_format] ?? {})' substituteInPlace "$out/modules/ii/sidebarLeft/AiChat.qml" \ --replace-fail ' text: Ai.getModel().name' ' text: Ai.getModel()?.name ?? Translation.tr("Loading model")' \ --replace-fail ' tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL").arg(Ai.getModel().name).arg(root.commandPrefix)' ' tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL").arg(Ai.getModel()?.name ?? Translation.tr("Loading model")).arg(root.commandPrefix ?? "/")' substituteInPlace "$out/modules/ii/sidebarLeft/aiChat/AiMessage.qml" \ --replace-fail " visible: messageData?.role == 'assistant' && Ai.models[messageData?.model].icon" " visible: messageData?.role == 'assistant' && Ai.models[messageData?.model]?.icon" \ --replace-fail " source: messageData?.role == 'assistant' ? Ai.models[messageData?.model].icon :" " source: messageData?.role == 'assistant' ? (Ai.models[messageData?.model]?.icon ?? String()) :" \ --replace-fail " text: messageData?.role == 'assistant' ? Ai.models[messageData?.model].name :" " text: messageData?.role == 'assistant' ? (Ai.models[messageData?.model]?.name ?? messageData?.model ?? Translation.tr('Assistant')) :" substituteInPlace "$out/modules/common/Config.qml" \ --replace-fail 'property string terminal: "kitty -1"' 'property string terminal: "ghostty"' \ --replace-fail 'property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"`' 'property string volumeMixer: "pavucontrol"' sed -i '/ property list extraModels: \[/,/ \]/c\ property list extraModels: []' "$out/modules/common/Config.qml" sed -i '/ property var models: Config.options.policies.ai === 2 ? {} : {/,/ property var modelList: Object.keys(root.models)/c\ property var models: Config.options.policies.ai === 2 ? {} : {\ "gpt-5.6-luna": aiModelComponent.createObject(this, {\ "name": "GPT-5.6 Luna",\ "icon": "spark-symbolic",\ "description": Translation.tr("OpenAI model optimized for cost-sensitive workloads"),\ "homepage": "https://developers.openai.com/api/docs/models/gpt-5.6-luna",\ "endpoint": "https://api.openai.com/v1/responses",\ "model": "gpt-5.6-luna",\ "requires_key": true,\ "key_id": "openai",\ "key_get_link": "https://platform.openai.com/api-keys",\ "key_get_description": Translation.tr("Use an OpenAI API key. API usage is billed separately from ChatGPT."),\ "api_format": "openai",\ }),\ }\ property var modelList: Object.keys(root.models)' "$out/services/Ai.qml" sed -i '/ function addUserModels() {/,/^ }$/c\ function addUserModels() {\ }' "$out/services/Ai.qml" sed -i '/ id: getOllamaModels/{n;s/running: true/running: false/;}' "$out/services/Ai.qml" substituteInPlace "$out/scripts/colors/switchwall.sh" \ --replace-fail 'source "$(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate"' ':' \ --replace-fail 'deactivate' ':' ''; quickshellStart = pkgs.writeShellScript "ena-quickshell-start" '' set -eu export XDG_DATA_DIRS="${iconDataDirs}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" state_home="''${XDG_STATE_HOME:-$HOME/.local/state}" mkdir -p "$state_home/quickshell/user" generated_dir="$state_home/quickshell/user/generated" mkdir -p "$generated_dir" cat > "$generated_dir/colors.json" <<'EOF' ${quickshellColorsJson} EOF if [ ! -e "$state_home/quickshell/user/first_run.txt" ]; then printf '%s\n' 'Managed by NixOS' > "$state_home/quickshell/user/first_run.txt" fi export PATH="${quickshellPython}/bin:$PATH" exec ${qsWrapper}/bin/qs -n -c ii ''; quickshellToggleSearch = pkgs.writeShellScript "ena-quickshell-toggle-search" '' set -eu export PATH="${quickshellPython}/bin:$PATH" if ${qsWrapper}/bin/qs -c ii ipc call search toggle >/dev/null 2>&1; then exit 0 fi ${pkgs.systemd}/bin/systemctl --user start ena-quickshell.service for attempt in $(${pkgs.coreutils}/bin/seq 1 50); do if ${qsWrapper}/bin/qs -c ii ipc call search toggle >/dev/null 2>&1; then exit 0 fi ${pkgs.coreutils}/bin/sleep 0.1 done exit 1 ''; selectHangul = pkgs.writeShellScript "ena-select-hangul" '' for attempt in $(${pkgs.coreutils}/bin/seq 1 20); do if ${pkgs.fcitx5}/bin/fcitx5-remote --check; then ${pkgs.fcitx5}/bin/fcitx5-remote -s hangul exit 0 fi ${pkgs.coreutils}/bin/sleep 0.5 done exit 0 ''; screenshotArea = pkgs.writeShellScript "ena-screenshot-area" '' set -eu geometry="$(${pkgs.slurp}/bin/slurp -d)" || exit 0 [ -n "$geometry" ] || exit 0 ${pkgs.grim}/bin/grim -g "$geometry" - | ${pkgs.satty}/bin/satty --filename - ''; screenshotScreen = pkgs.writeShellScript "ena-screenshot-screen" '' set -eu ${pkgs.grim}/bin/grim - | ${pkgs.satty}/bin/satty --filename - ''; scratchpad = pkgs.writeShellScript "ena-scratchpad" '' hyprctl="${pkgs.hyprland}/bin/hyprctl" if "$hyprctl" clients -j | ${pkgs.jq}/bin/jq -e 'any(.[]; .class == "org.imnyang.scratchpad")' >/dev/null; then "$hyprctl" dispatch togglespecialworkspace scratchpad else "$hyprctl" dispatch exec "[workspace special:scratchpad;float;size 80% 70%;center] ${pkgs.ghostty}/bin/ghostty --gtk-single-instance=false --class=org.imnyang.scratchpad" fi ''; # Shared Mizuki Dark palette. The UI colors follow Zed's One Dark theme, # while the foreground and terminal colors follow Ghostty's dark theme. lightColors = { background = "191017"; foreground = "fafafa"; cursor = "fafafa"; selection = "2c1f2d"; primary = "f5c2e7"; primaryContainer = "4a354c"; primaryFixedDim = "f5c2e7"; secondary = "bac2de"; secondaryContainer = "2c1f2d"; secondaryFixedDim = "bac2de"; tertiary = "89b4fa"; tertiaryContainer = "4a354c"; outline = "a38f96"; outlineVariant = "2c1f2d"; surfaceBright = "4a354c"; surfaceContainer = "2c1f2d"; surfaceContainerHigh = "4a354c"; surfaceContainerHighest = "604562"; surfaceContainerLow = "191017"; surfaceDim = "191017"; error = "f38ba8"; errorContainer = "4a354c"; }; rgba = color: "rgba(${color}ff)"; # Qt/QML colors use #AARRGGBB, unlike Hyprland's rgba(RRGGBBAA). hex = color: "#ff${color}"; # MaterialThemeLoader reads this file at startup and applies its keys to # Appearance.m3colors. Keep it sourced from the same palette as Hyprland, # Zed, and Ghostty so the shell does not fall back to upstream defaults. quickshellColorsJson = builtins.toJSON { background = hex lightColors.background; on_background = hex lightColors.foreground; surface = hex lightColors.background; surface_dim = hex lightColors.surfaceDim; surface_bright = hex lightColors.surfaceBright; surface_container_lowest = hex lightColors.surfaceBright; surface_container_low = hex lightColors.surfaceContainerLow; surface_container = hex lightColors.surfaceContainer; surface_container_high = hex lightColors.surfaceContainerHigh; surface_container_highest = hex lightColors.surfaceContainerHighest; on_surface = hex lightColors.foreground; surface_variant = hex lightColors.outlineVariant; on_surface_variant = hex lightColors.outline; inverse_surface = hex lightColors.foreground; inverse_on_surface = hex lightColors.surfaceBright; outline = hex lightColors.outline; outline_variant = hex lightColors.outlineVariant; shadow = hex lightColors.foreground; scrim = "#ff000000"; surface_tint = hex lightColors.primary; primary = hex lightColors.primary; on_primary = hex lightColors.surfaceBright; primary_container = hex lightColors.primaryContainer; on_primary_container = hex lightColors.cursor; inverse_primary = hex lightColors.primaryFixedDim; secondary = hex lightColors.secondary; on_secondary = hex lightColors.surfaceBright; secondary_container = hex lightColors.secondaryContainer; on_secondary_container = hex lightColors.foreground; tertiary = hex lightColors.tertiary; on_tertiary = hex lightColors.surfaceBright; tertiary_container = hex lightColors.tertiaryContainer; on_tertiary_container = hex lightColors.foreground; error = hex lightColors.error; on_error = hex lightColors.surfaceBright; error_container = hex lightColors.errorContainer; on_error_container = hex lightColors.foreground; primary_fixed = hex lightColors.primaryContainer; primary_fixed_dim = hex lightColors.primaryFixedDim; on_primary_fixed = hex lightColors.cursor; on_primary_fixed_variant = hex lightColors.primary; secondary_fixed = hex lightColors.secondaryContainer; secondary_fixed_dim = hex lightColors.secondaryFixedDim; on_secondary_fixed = hex lightColors.foreground; on_secondary_fixed_variant = hex lightColors.secondary; tertiary_fixed = hex lightColors.tertiaryContainer; tertiary_fixed_dim = hex lightColors.tertiary; on_tertiary_fixed = hex lightColors.foreground; on_tertiary_fixed_variant = hex lightColors.tertiary; success = hex "a6e3a1"; on_success = hex lightColors.background; success_container = hex lightColors.secondaryContainer; on_success_container = hex "a6e3a1"; }; colorsConfig = '' # Mizuki Dark: Zed One Dark surfaces + Ghostty Mizuki Dark accents. $background = ${rgba lightColors.background} $on_background = ${rgba lightColors.foreground} $surface = ${rgba lightColors.background} $surface_bright = ${rgba lightColors.surfaceBright} $surface_container = ${rgba lightColors.surfaceContainer} $surface_container_high = ${rgba lightColors.surfaceContainerHigh} $surface_container_highest = ${rgba lightColors.surfaceContainerHighest} $surface_container_low = ${rgba lightColors.surfaceContainerLow} $surface_container_lowest = ${rgba lightColors.surfaceBright} $surface_dim = ${rgba lightColors.surfaceDim} $surface_tint = ${rgba lightColors.primary} $surface_variant = ${rgba lightColors.outlineVariant} $on_surface = ${rgba lightColors.foreground} $on_surface_variant = ${rgba lightColors.outline} $primary = ${rgba lightColors.primary} $primary_container = ${rgba lightColors.primaryContainer} $primary_fixed = ${rgba lightColors.primaryContainer} $primary_fixed_dim = ${rgba lightColors.primaryFixedDim} $on_primary = ${rgba lightColors.surfaceBright} $on_primary_container = ${rgba lightColors.cursor} $on_primary_fixed = ${rgba lightColors.cursor} $on_primary_fixed_variant = ${rgba lightColors.primary} $secondary = ${rgba lightColors.secondary} $secondary_container = ${rgba lightColors.secondaryContainer} $secondary_fixed = ${rgba lightColors.secondaryContainer} $secondary_fixed_dim = ${rgba lightColors.secondaryFixedDim} $on_secondary = ${rgba lightColors.surfaceBright} $on_secondary_container = ${rgba lightColors.foreground} $on_secondary_fixed = ${rgba lightColors.foreground} $on_secondary_fixed_variant = ${rgba lightColors.secondary} $tertiary = ${rgba lightColors.tertiary} $tertiary_container = ${rgba lightColors.tertiaryContainer} $tertiary_fixed = ${rgba lightColors.tertiaryContainer} $tertiary_fixed_dim = ${rgba lightColors.tertiary} $on_tertiary = ${rgba lightColors.surfaceBright} $on_tertiary_container = ${rgba lightColors.foreground} $on_tertiary_fixed = ${rgba lightColors.foreground} $on_tertiary_fixed_variant = ${rgba lightColors.tertiary} $outline = ${rgba lightColors.outline} $outline_variant = ${rgba lightColors.outlineVariant} $error = ${rgba lightColors.error} $error_container = ${rgba lightColors.errorContainer} $on_error = ${rgba lightColors.surfaceBright} $on_error_container = ${rgba lightColors.foreground} $inverse_surface = ${rgba lightColors.foreground} $inverse_on_surface = ${rgba lightColors.surfaceBright} $inverse_primary = ${rgba lightColors.primaryFixedDim} $shadow = ${rgba lightColors.foreground} $scrim = rgba(000000ff) $source_color = ${rgba lightColors.primary} # Ghostty Mizuki Dark ANSI palette, used by hyprlock and related widgets. $color0 = rgba(5c5f77ff) $color1 = rgba(f38ba8ff) $color2 = rgba(a6e3a1ff) $color3 = rgba(f9e2afff) $color4 = rgba(89b4faff) $color5 = rgba(f5c2e7ff) $color6 = rgba(94e2d5ff) $color7 = rgba(bac2deff) $color8 = rgba(6c7086ff) $color9 = rgba(f38ba8ff) $color10 = rgba(a6e3a1ff) $color11 = rgba(f9e2afff) $color12 = rgba(89b4faff) $color13 = rgba(f5c2e7ff) $color14 = rgba(94e2d5ff) $color15 = rgba(cdd6f4ff) ''; sattyConfig = '' [general] fullscreen = "current-screen" resize = { mode = "smart" } floating-hack = true corner-roundness = 12 initial-tool = "pointer" copy-command = "${pkgs.wl-clipboard}/bin/wl-copy" annotation-size-factor = 2 output-filename = "~/Pictures/Screenshots/%Y-%m-%d_%H-%M-%S.png" early-exit = ["all"] actions-on-enter = ["save-to-file", "save-to-clipboard"] actions-on-escape = ["exit"] no-window-decoration = true primary-highlighter = "block" notification-thumbnail = "screenshot" [font] family = "JetBrainsMono NFM" style = "Regular" fallback = ["Noto Sans CJK KR"] [color-palette] palette = [ "#f5c2e7ff", "#89b4faff", "#f38ba8ff", "#a6e3a1ff", "#f9e2afff", "#94e2d5ff", "#f5c2e7ff", "#fafafaff", ] ''; hyprlockConfig = builtins.replaceStrings [ "color = $on_secondary_container" "color = $secondary" "font_color = $secondary" "color = rgb(0,0,0) # color will be rendered initially until path is available" ] [ "color = ${rgba lightColors.cursor}" "color = ${rgba lightColors.primaryFixedDim}" "font_color = ${rgba lightColors.primaryFixedDim}" "color = ${rgba lightColors.background}" ] (builtins.readFile "${upstream}/hypr/hyprlock.conf"); keybindHelpText = '' APPLICATIONS SUPER + Enter Terminal SUPER + Shift + Enter Browser SUPER + Alt + Enter Floating terminal SUPER + Space Vicinae launcher SUPER + E File manager SUPER + Shift + F File manager SUPER + Shift + E Yazi file manager SUPER + V Clipboard history SUPER + Ctrl + V Clipboard history SUPER + Grave Scratchpad terminal SUPER + K Show this keybinding list WINDOWS SUPER + Q Close active window gracefully SUPER + W Close active window gracefully SUPER + T Toggle floating SUPER + F Toggle fullscreen SUPER + P Toggle pseudo-tile SUPER + J Toggle split direction SUPER + L Lock screen SUPER + Ctrl + L Lock screen (compatibility) SUPER + F1 Toggle Quickshell overview SUPER + H Toggle Quickshell bar SUPER + Alt + Space Hyprland settings NAVIGATION SUPER + Arrow Focus window in that direction SUPER + Ctrl + Arrow Move window in that direction SUPER + Shift + Arrow Swap window in that direction SUPER + Alt + Arrow Resize active window SUPER + 1..0 Switch to workspace 1..10 SUPER + Shift + 1..0 Move window to workspace 1..10 SUPER + Tab Next workspace SUPER + Shift + Tab Previous workspace SUPER + Mouse wheel Switch workspace SUPER + Left click Move window SUPER + Right click Resize window SCREENSHOTS AND INPUT SUPER + Shift + S Select an area and annotate screenshot Print Screenshot the whole screen Right Alt Toggle Korean/English input ZOOM SUPER + + / KP_ADD Zoom in around cursor SUPER + - / KP_SUBTRACT Zoom out around cursor HARDWARE Volume keys Change or mute audio Mic mute key Mute microphone Brightness keys Change screen brightness Media keys Previous / play-pause / next OTHER Ctrl + Alt + Delete Exit Hyprland ''; keybindHelp = pkgs.writeShellScript "ena-keybind-help" '' ${lib.getExe pkgs.rofi} -dmenu -i -no-sort -no-custom -p "Keybindings" <<'EOF' ${keybindHelpText} EOF ''; hyprlandLuaConfig = '' local terminal = "${pkgs.ghostty}/bin/ghostty" local browser = "${pkgs.firefox-devedition}/bin/firefox-devedition" local fileManager = "${pkgs.nautilus}/bin/nautilus" local menu = "${pkgs.vicinae}/bin/vicinae toggle" local mainMod = "SUPER" -- Hyprland 0.55+ Lua configuration. hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1.33, }) hl.env("XCURSOR_SIZE", "32") hl.env("HYPRCURSOR_SIZE", "32") hl.env("XCURSOR_THEME", "pjsk-cursor-n25-ena-ani") hl.config({ general = { gaps_in = 5, gaps_out = 10, border_size = 2, col = { active_border = "${rgba lightColors.outline}", inactive_border = "${rgba lightColors.outlineVariant}", }, resize_on_border = false, allow_tearing = false, layout = "dwindle", }, decoration = { rounding = 10, rounding_power = 2, active_opacity = 1.0, inactive_opacity = 0.8, shadow = { enabled = false, range = 4, render_power = 3, color = "rgba(1a1a1aee)", }, blur = { enabled = true, size = 5, passes = 3, ignore_opacity = true, new_optimizations = true, special = false, popups = true, xray = true, vibrancy = 0.1696, }, }, animations = { enabled = true }, dwindle = { preserve_split = true }, master = { new_status = "master" }, misc = { force_default_wallpaper = 0, disable_hyprland_logo = true, }, render = { new_render_scheduling = true }, input = { kb_layout = "us", kb_variant = "", kb_model = "", kb_options = "", kb_rules = "", follow_mouse = 1, sensitivity = 0, accel_profile = "flat", force_no_accel = true, touchpad = { natural_scroll = true, tap_to_click = true, tap_and_drag = true, drag_lock = true, disable_while_typing = true, clickfinger_behavior = true, middle_button_emulation = true, }, touchdevice = { enabled = true, transform = 0, }, }, gestures = { workspace_swipe_touch = true, workspace_swipe_touch_invert = false, workspace_swipe_distance = 300, }, }) hl.gesture({ fingers = 4, direction = "horizontal", action = "workspace" }) hl.device({ name = "epic-mouse-v1", sensitivity = -0.5 }) -- Animations from the existing Hyprland setup. hl.curve("myBezier", { type = "bezier", points = {{0.05, 0.9}, {0.1, 1.05}} }) hl.curve("been", { type = "bezier", points = {{0.24, 0.9}, {0.25, 0.91}} }) hl.curve("been2", { type = "bezier", points = {{0, 0.94}, {0.5, 0.99}} }) hl.curve("menu_decel", { type = "bezier", points = {{0.1, 1}, {0, 1}} }) hl.curve("linear", { type = "bezier", points = {{0, 0}, {1, 1}} }) hl.curve("wind", { type = "bezier", points = {{0.05, 0.9}, {0.1, 1.05}} }) hl.curve("winIn", { type = "bezier", points = {{0.1, 1.1}, {0.1, 1.1}} }) hl.curve("winOut", { type = "bezier", points = {{0.3, -0.3}, {0, 1}} }) hl.curve("slow", { type = "bezier", points = {{0, 0.85}, {0.3, 1}} }) hl.curve("overshot", { type = "bezier", points = {{0.7, 0.6}, {0.1, 1.1}} }) hl.curve("bounce", { type = "bezier", points = {{1.1, 1.6}, {0.1, 0.85}} }) hl.curve("sligshot", { type = "bezier", points = {{1, -1}, {0.15, 1.25}} }) hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "slow", style = "popin" }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 7, bezier = "been", style = "popin 70%" }) hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" }) hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "linear" }) hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "overshot" }) hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" }) hl.animation({ leaf = "windows", enabled = true, speed = 5, bezier = "bounce", style = "popin" }) -- Tags used by the blur and floating rules below. hl.window_rule({ match = { class = "^([Mm]pv|vlc)$" }, tag = "+multimedia_video" }) hl.window_rule({ match = { class = "^(nm-applet|nm-connection-editor|blueman-manager|org.gnome.FileRoller)$" }, tag = "+settings" }) hl.window_rule({ match = { class = "^(org.gnome.DiskUtility|wihotspot(-gui)?)$" }, tag = "+settings" }) hl.window_rule({ match = { class = "^(org.gnome.SystemMonitor)$" }, tag = "+viewer" }) hl.window_rule({ match = { class = "^(org.gnome.Evince)$" }, tag = "+viewer" }) hl.window_rule({ match = { class = "^(eog|org.gnome.Loupe)$" }, tag = "+viewer" }) -- Application window rules. hl.window_rule({ match = { tag = "multimedia_video*" }, no_blur = true, opacity = "1.0" }) hl.window_rule({ match = { tag = "settings*" }, opacity = "0.8", float = true }) hl.window_rule({ match = { tag = "viewer*" }, float = true }) hl.window_rule({ match = { class = "^(org.gnome.Nautilus)$" }, opacity = "0.8" }) hl.window_rule({ match = { class = "^(gedit|org.gnome.TextEditor|mousepad)$" }, opacity = "0.9" }) hl.window_rule({ match = { class = "^(org.pulseaudio.pavucontrol)$" }, opacity = "0.9", float = true, size = {"monitor_w*0.5", "monitor_h*0.6"} }) hl.window_rule({ match = { class = "^(kitty)$" }, opacity = "0.9" }) hl.window_rule({ match = { class = "^(discord|vesktop|org.telegram.desktop)$" }, opacity = "0.85 override 0.7 override 1 override" }) hl.window_rule({ match = { class = "^(Spotify)$" }, opacity = "0.8 override 0.6 override 1 override" }) hl.window_rule({ match = { class = "^(zen)$" }, opacity = "0.9 override 0.7 override 1 override" }) hl.window_rule({ match = { tag = "multimedia_video*" }, float = true, size = {900, 506} }) hl.window_rule({ match = { class = "^(org.imnyang.clipse)$" }, float = true, size = {"monitor_w*0.7", "monitor_h*0.75"}, center = true }) hl.window_rule({ match = { class = "^(org.imnyang.scratchpad)$" }, float = true, size = {"monitor_w*0.8", "monitor_h*0.7"}, center = true }) hl.window_rule({ match = { class = ".*" }, suppress_event = "maximize" }) hl.window_rule({ match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false }, no_focus = true, }) hl.window_rule({ match = { title = "^(Save As|Save a File|Pick Files)$" }, float = true, size = {"monitor_w*0.5", "monitor_h*0.6"}, center = true }) hl.window_rule({ match = { initial_title = "^(Open Files)$" }, float = true, size = {"monitor_w*0.7", "monitor_h*0.6"} }) -- Layer rules. hl.layer_rule({ match = { namespace = "quickshell:bar" }, blur = true, ignore_alpha = 0.5 }) hl.layer_rule({ match = { namespace = "logout_dialog" }, blur = true }) -- Startup processes replace the old exec-once entries. hl.on("hyprland.start", function() hl.exec_cmd("${pkgs.networkmanagerapplet}/bin/nm-applet --indicator") hl.exec_cmd("${pkgs.awww}/bin/awww-daemon") hl.exec_cmd("${pkgs.clipse}/bin/clipse -listen") hl.exec_cmd("${pkgs.blueman}/bin/blueman-applet") hl.exec_cmd("${pkgs.hyprpolkitagent}/bin/hyprpolkitagent") hl.exec_cmd("${selectHangul}") hl.exec_cmd("ln -sfn ${wallpaper} ~/.config/hypr/current_wallpaper") hl.exec_cmd("sleep 1 && ${pkgs.awww}/bin/awww img ${wallpaper} --transition-type any") end) -- Familiar Super-key layout, based on Omarchy's core bindings. hl.bind(mainMod .. " + Space", hl.dsp.exec_cmd(menu)) hl.bind(mainMod .. " + Return", hl.dsp.exec_cmd(terminal)) hl.bind(mainMod .. " + SHIFT + Return", hl.dsp.exec_cmd(browser)) hl.bind(mainMod .. " + ALT + Return", hl.dsp.exec_cmd(terminal, { float = true, size = {800, 550} })) hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager)) hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd(fileManager)) hl.bind(mainMod .. " + SHIFT + E", hl.dsp.exec_cmd(terminal .. " -e ${pkgs.yazi}/bin/yazi")) hl.bind(mainMod .. " + V", hl.dsp.exec_cmd(terminal .. " --gtk-single-instance=false --class=org.imnyang.clipse -e ${pkgs.clipse}/bin/clipse")) hl.bind(mainMod .. " + CTRL + V", hl.dsp.exec_cmd(terminal .. " --gtk-single-instance=false --class=org.imnyang.clipse -e ${pkgs.clipse}/bin/clipse")) hl.bind(mainMod .. " + Grave", hl.dsp.exec_cmd("${scratchpad}")) hl.bind(mainMod .. " + W", hl.dsp.window.close()) hl.bind(mainMod .. " + ALT + Space", hl.dsp.exec_cmd("${inputs.hyprmod.packages.${pkgs.system}.default}/bin/hyprmod")) hl.bind(mainMod .. " + K", hl.dsp.exec_cmd("${keybindHelp}")) hl.bind(mainMod .. " + Q", hl.dsp.window.close()) hl.bind("CTRL + ALT + Delete", hl.dsp.exit()) hl.bind(mainMod .. " + T", hl.dsp.window.float({ action = "toggle" })) hl.bind(mainMod .. " + F", hl.dsp.window.fullscreen()) hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) hl.bind(mainMod .. " + L", hl.dsp.exec_cmd("${pkgs.hyprlock}/bin/hyprlock")) hl.bind(mainMod .. " + CTRL + L", hl.dsp.exec_cmd("${pkgs.hyprlock}/bin/hyprlock")) hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("${quickshellToggleSearch}")) hl.bind(mainMod .. " + H", hl.dsp.exec_cmd("${qsWrapper}/bin/qs -c ii ipc call bar toggle")) hl.bind("Alt_R", hl.dsp.exec_cmd("${pkgs.fcitx5}/bin/fcitx5-remote -t")) hl.bind(mainMod .. " + SHIFT + S", hl.dsp.exec_cmd("${screenshotArea}")) hl.bind("Print", hl.dsp.exec_cmd("${screenshotScreen}")) local MIN_ZOOM = 1.0 local MAX_ZOOM = 3.0 local ZOOM_STEP = 0.25 local function setZoom(delta) local current = hl.get_config("cursor.zoom_factor") local next = math.max(MIN_ZOOM, math.min(MAX_ZOOM, current + delta)) hl.config({ cursor = { zoom_factor = next } }) end local zoomIn = function() setZoom(ZOOM_STEP) end local zoomOut = function() setZoom(-ZOOM_STEP) end hl.bind(mainMod .. " + equal", zoomIn) hl.bind(mainMod .. " + SHIFT + equal", zoomIn) hl.bind(mainMod .. " + KP_ADD", zoomIn) hl.bind(mainMod .. " + minus", zoomOut) hl.bind(mainMod .. " + KP_SUBTRACT", zoomOut) for i = 1, 10 do local key = tostring(i % 10) hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i })) hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) end hl.bind(mainMod .. " + Tab", hl.dsp.focus({ workspace = "e+1" })) hl.bind(mainMod .. " + SHIFT + Tab", hl.dsp.focus({ workspace = "e-1" })) hl.bind(mainMod .. " + CTRL + Tab", hl.dsp.focus({ workspace = "previous" })) hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) for _, direction in ipairs({ "left", "right", "up", "down" }) do hl.bind(mainMod .. " + " .. direction, hl.dsp.focus({ direction = direction })) hl.bind(mainMod .. " + CTRL + " .. direction, hl.dsp.window.move({ direction = direction })) hl.bind(mainMod .. " + SHIFT + " .. direction, hl.dsp.window.swap({ direction = direction })) end -- Resize with Super + Alt + arrows; repeating matches the old binde behavior. hl.bind(mainMod .. " + ALT + left", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), { repeating = true }) hl.bind(mainMod .. " + ALT + right", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), { repeating = true }) hl.bind(mainMod .. " + ALT + up", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), { repeating = true }) hl.bind(mainMod .. " + ALT + down", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), { repeating = true }) hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) hl.bind(mainMod .. " + Control_L", hl.dsp.window.drag(), { mouse = true }) hl.bind(mainMod .. " + Alt_L", hl.dsp.window.resize(), { mouse = true }) hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("${pkgs.wireplumber}/bin/wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true }) hl.bind("XF86AudioMute", hl.dsp.exec_cmd("${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true }) hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true }) hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("${pkgs.brightnessctl}/bin/brightnessctl set 10%+"), { locked = true, repeating = true }) hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("${pkgs.brightnessctl}/bin/brightnessctl set 10%-"), { locked = true, repeating = true }) hl.bind("XF86AudioNext", hl.dsp.exec_cmd("${pkgs.playerctl}/bin/playerctl next"), { locked = true }) hl.bind("XF86AudioPause", hl.dsp.exec_cmd("${pkgs.playerctl}/bin/playerctl play-pause"), { locked = true }) hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("${pkgs.playerctl}/bin/playerctl play-pause"), { locked = true }) hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("${pkgs.playerctl}/bin/playerctl previous"), { locked = true }) ''; in { # Run the shell under the user manager so it survives the launcher shell and # is restarted if a QML/runtime failure takes it down. systemd.user.services.ena-quickshell = { Unit = { Description = "ena Quickshell desktop shell"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; }; Service = { ExecStart = quickshellStart; Restart = "on-failure"; RestartSec = 2; }; Install = { WantedBy = [ "graphical-session.target" ]; }; }; systemd.user.services.ena-1password = { Unit = { Description = "1Password background agent"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; }; Service = { ExecStart = "${pkgs._1password-gui}/bin/1password --silent"; Restart = "on-failure"; RestartSec = 5; }; Install = { WantedBy = [ "graphical-session.target" ]; }; }; home.packages = with pkgs; [ awww adwaita-icon-theme bc blueman brightnessctl cava cliphist clipse ddcutil ffmpeg fuzzel grim hypridle hyprpicker jq hicolor-icon-theme hyprpolkitagent hyprsunset kdePackages.kirigami kdePackages.breeze-icons kdePackages.syntax-highlighting imagemagick libqalculate libnotify matugen material-symbols nautilus networkmanagerapplet pavucontrol playerctl qt6Packages.qtimageformats qt6Packages.qtmultimedia qt6Packages.qtpositioning qt6Packages.qtquicktimeline qt6Packages.qtsensors qt6Packages.qt5compat qt6Packages.qtvirtualkeyboard rofi slurp satty songrec swappy tesseract translate-shell upower wl-clipboard wf-recorder qsWrapper wireplumber wlogout wget wtype xdg-user-dirs yazi ydotool ]; home.file."Pictures/wallpapers" = { source = "${assets}/wallpaper"; recursive = true; }; xdg.configFile = { "hypr/hyprland.lua".text = hyprlandLuaConfig; "hypr/colors.conf".text = colorsConfig; "hypr/hypridle.conf".source = "${upstream}/hypr/hypridle.conf"; "hypr/hyprlock.conf".text = hyprlockConfig; "matugen".source = "${dots}/.config/matugen"; "quickshell/ii".source = quickshellConfig; "satty/config.toml".text = sattyConfig; "wlogout/layout".text = '' { "label" : "lock", "action" : "${pkgs.hyprlock}/bin/hyprlock", "text" : "Lock", "keybind" : "l" } { "label" : "reboot", "action" : "systemctl reboot", "text" : "Reboot", "keybind" : "r" } { "label" : "shutdown", "action" : "systemctl poweroff", "text" : "Shutdown", "keybind" : "s" } { "label" : "logout", "action" : "loginctl kill-session $XDG_SESSION_ID", "text" : "Logout", "keybind" : "e" } { "label" : "suspend", "action" : "systemctl suspend", "text" : "Suspend", "keybind" : "u" } ''; "wlogout/style.css".source = "${upstream}/wlogout/style.css"; "wlogout/icons" = { source = "${upstream}/wlogout/icons"; recursive = true; }; }; }