nix-flakes/hosts/machine/ena/home/config/hyprland.nix
2026-09-14 12:09:52 +09:00

885 lines
39 KiB
Nix

{ 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<var> availableTools: Object.keys(root.tools[models[currentModelId]?.api_format])' \
' property list<var> 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<var> extraModels: \[/,/ \]/c\
property list<var> 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;
};
};
}