diff --git a/dot_config/hypr/.luarc.json b/dot_config/hypr/.luarc.json new file mode 100644 index 0000000..babfd90 --- /dev/null +++ b/dot_config/hypr/.luarc.json @@ -0,0 +1,8 @@ +{ + "workspace": { + "library": ["/usr/share/hypr/stubs"] + }, + "diagnostics": { + "globals": ["hl"] + } +} diff --git a/dot_config/hypr/AGENTS.md b/dot_config/hypr/AGENTS.md index 4ebafe2..2c00497 100644 --- a/dot_config/hypr/AGENTS.md +++ b/dot_config/hypr/AGENTS.md @@ -71,3 +71,16 @@ No established Git history. Use concise, imperative commit subjects with optiona - Screenshots for visible UI changes. ## Agent-Specific Notes + +### Lua config (`hyprland.lua.tmpl` + `hyprland.d.lua/`) + +The Lua config entry point is `hyprland.lua.tmpl`. It is rendered by chezmoi to `~/.config/hypr/hyprland.lua`, which Hyprland v0.55+ loads in preference to `hyprland.conf`. + +`theme.lua.tmpl` reads `.chezmoi.config.data.theme` to select the color palette. This key is **not** in the tracked AGENTS.md reference but must be present in `~/.config/chezmoi/chezmoi.toml`: + +```toml +[data] +theme = "apex-neon" # or "apex-aeon" +``` + +Valid values: `"apex-neon"` (dark), `"apex-aeon"` (light). If the key is absent, the theme table will be empty and all border/groupbar colors will be unset. diff --git a/dot_config/hypr/hypridle.conf b/dot_config/hypr/hypridle.conf index 875ae48..7cf710d 100644 --- a/dot_config/hypr/hypridle.conf +++ b/dot_config/hypr/hypridle.conf @@ -1,7 +1,7 @@ ### based on the example config from hyprland.org general { - lock_cmd = pgrep -x swaylock >/dev/null || swaylock -f # avoid starting multiple swaylock instances. + lock_cmd = pgrep -x hyprlock >/dev/null || hyprlock -f # avoid starting multiple swaylock instances. before_sleep_cmd = loginctl lock-session # lock before suspend. after_sleep_cmd = hyprctl dispatch dpms on # to avoid having to press a key twice to turn on the display. ignore_dbus_inhibit = false # whether to ignore dbus-sent idle-inhibit requests (used by e.g. firefox or steam) diff --git a/dot_config/hypr/hyprland.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.conf similarity index 100% rename from dot_config/hypr/hyprland.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.conf diff --git a/dot_config/hypr/hyprland.d/00-theme.conf.tmpl b/dot_config/hypr/hyprland.conf.bak/hyprland.d/00-theme.conf.tmpl similarity index 100% rename from dot_config/hypr/hyprland.d/00-theme.conf.tmpl rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/00-theme.conf.tmpl diff --git a/dot_config/hypr/hyprland.d/10-monitors.conf.tmpl b/dot_config/hypr/hyprland.conf.bak/hyprland.d/10-monitors.conf.tmpl similarity index 100% rename from dot_config/hypr/hyprland.d/10-monitors.conf.tmpl rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/10-monitors.conf.tmpl diff --git a/dot_config/hypr/hyprland.d/20-workspaces.conf.tmpl b/dot_config/hypr/hyprland.conf.bak/hyprland.d/20-workspaces.conf.tmpl similarity index 100% rename from dot_config/hypr/hyprland.d/20-workspaces.conf.tmpl rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/20-workspaces.conf.tmpl diff --git a/dot_config/hypr/hyprland.d/30-general.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/30-general.conf similarity index 100% rename from dot_config/hypr/hyprland.d/30-general.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/30-general.conf diff --git a/dot_config/hypr/hyprland.d/40-input.conf.tmpl b/dot_config/hypr/hyprland.conf.bak/hyprland.d/40-input.conf.tmpl similarity index 100% rename from dot_config/hypr/hyprland.d/40-input.conf.tmpl rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/40-input.conf.tmpl diff --git a/dot_config/hypr/hyprland.d/50-layout.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/50-layout.conf similarity index 100% rename from dot_config/hypr/hyprland.d/50-layout.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/50-layout.conf diff --git a/dot_config/hypr/hyprland.d/60-rules.conf.tmpl b/dot_config/hypr/hyprland.conf.bak/hyprland.d/60-rules.conf.tmpl similarity index 100% rename from dot_config/hypr/hyprland.d/60-rules.conf.tmpl rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/60-rules.conf.tmpl diff --git a/dot_config/hypr/hyprland.d/70-keybinds.conf.tmpl b/dot_config/hypr/hyprland.conf.bak/hyprland.d/70-keybinds.conf.tmpl similarity index 100% rename from dot_config/hypr/hyprland.d/70-keybinds.conf.tmpl rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/70-keybinds.conf.tmpl diff --git a/dot_config/hypr/hyprland.d/80-autostart.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/80-autostart.conf similarity index 100% rename from dot_config/hypr/hyprland.d/80-autostart.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/80-autostart.conf diff --git a/dot_config/hypr/hyprland.d/catpuccinmocha.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/catpuccinmocha.conf similarity index 100% rename from dot_config/hypr/hyprland.d/catpuccinmocha.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/catpuccinmocha.conf diff --git a/dot_config/hypr/hyprland.d/default_theme.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/default_theme.conf similarity index 100% rename from dot_config/hypr/hyprland.d/default_theme.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/default_theme.conf diff --git a/dot_config/hypr/hyprland.d/rose-pine.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/rose-pine.conf similarity index 100% rename from dot_config/hypr/hyprland.d/rose-pine.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/rose-pine.conf diff --git a/dot_config/hypr/hyprland.d/theme-apex-aeon.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/theme-apex-aeon.conf similarity index 100% rename from dot_config/hypr/hyprland.d/theme-apex-aeon.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/theme-apex-aeon.conf diff --git a/dot_config/hypr/hyprland.d/theme-apex-neon.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/theme-apex-neon.conf similarity index 100% rename from dot_config/hypr/hyprland.d/theme-apex-neon.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/theme-apex-neon.conf diff --git a/dot_config/hypr/hyprland.d/theme-owl.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/theme-owl.conf similarity index 100% rename from dot_config/hypr/hyprland.d/theme-owl.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/theme-owl.conf diff --git a/dot_config/hypr/hyprland.d/tokyonight.conf b/dot_config/hypr/hyprland.conf.bak/hyprland.d/tokyonight.conf similarity index 100% rename from dot_config/hypr/hyprland.d/tokyonight.conf rename to dot_config/hypr/hyprland.conf.bak/hyprland.d/tokyonight.conf diff --git a/dot_config/hypr/hyprland.d.lua/general.lua b/dot_config/hypr/hyprland.d.lua/general.lua new file mode 100644 index 0000000..361827f --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/general.lua @@ -0,0 +1,85 @@ +hl.config({ + general = { + gaps_in = 5, + gaps_out = { top = 5, left = 5, right = 5, bottom = 5 }, + border_size = 2, + layout = "master", + allow_tearing = false + }, + render = { + new_render_scheduling = true + }, + cursor = { + hide_on_key_press = true, + persistent_warps = true, + warp_on_change_workspace = true, + enable_hyprcursor = true, + sync_gsettings_theme = true, + no_hardware_cursors = true + }, + decoration = { + rounding = 5, + active_opacity = 1.0, + inactive_opacity = 1.0, + dim_modal = true, + dim_inactive = true, + dim_strength = 0.1, + blur = { + enabled = true, + size = 3, + passes = 1, + vibrancy = 0.1696, + popups = true -- NEW: Blur for menus and tooltips + }, + shadow = { + enabled = false, + range = 4, + render_power = 3, + -- color integrated via theme + }, + glow = { -- NEW in v0.55 + enabled = true, + range = 10, + -- color integrated via theme + } + }, + misc = { + force_default_wallpaper = -1, + disable_hyprland_logo = false, + vrr = 2, + mouse_move_enables_dpms = true, + key_press_enables_dpms = true, + layers_hog_keyboard_focus = true, + mouse_move_focuses_monitor = true + }, + ecosystem = { + enforce_permissions = true -- NEW: Enable Android-style permissions + } +}) + +-- Curves +hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } }) +hl.curve("easeInOutCubic", { type = "bezier", points = { {0.65, 0.05}, {0.36, 1} } }) +hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1} } }) +hl.curve("almostLinear", { type = "bezier", points = { {0.5, 0.5}, {0.75, 1.0} } }) +hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } }) +hl.curve("bouncy", { type = "spring", mass = 1, stiffness = 100, dampening = 15 }) -- NEW: Spring curve + +-- Animations +hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) +hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" }) +hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) +hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) +hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }) +hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }) +hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" }) +hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" }) +hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" }) +hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 1.5, spring = "bouncy", style = "slidevert" }) diff --git a/dot_config/hypr/hyprland.d.lua/input.lua.tmpl b/dot_config/hypr/hyprland.d.lua/input.lua.tmpl new file mode 100644 index 0000000..9195c65 --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/input.lua.tmpl @@ -0,0 +1,30 @@ +{{- $tags := .chezmoi.config.data.tags -}} + +hl.config({ + input = { + kb_layout = "ultimatekeys", + kb_options = "caps:escape_shifted_capslock", + numlock_by_default = true, + repeat_rate = 25, + repeat_delay = 600, + follow_mouse = 1, + focus_on_close = 2, + + mouse_refocus = true, + float_switch_override_focus = 2, + special_fallthrough = true, + touchpad = { + disable_while_typing = true, + scroll_factor = 1.0, + tap_to_click = true + } + } +}) + +{{- if (index $tags "desktop") }} +hl.device({ + name = "Logitech Gaming Mouse G502", + sensitivity = 0.0, + accel_profile = "flat" +}) +{{- end }} diff --git a/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl b/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl new file mode 100644 index 0000000..d8ec16c --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl @@ -0,0 +1,156 @@ +local mainMod = "SUPER" + +-- Apps +local terminal = "uwsm app -- kitty" +local term_tmux = "uwsm app -- kitty -e tmux" +local term_tmux_append = "uwsm app -- kitty -e tmux a" +local editor = "uwsm app -- kitty -e nvim" +local alteditor = "uwsm app -- zeditor" +local filemanager = "uwsm app -- nautilus" +local launcher = "uwsm app -- owlry -p app,cmd,system,ssh" +local clipman = "uwsm app -- owlry -m clipboard" +local browser = "uwsm app -- firefox" +local taskman = "uwsm app -- owlry -m uuctl" +local pwdmgr = "uwsm app -- bitwarden-desktop" +local soundctl = "uwsm app -- pwvucontrol" + +local notcenter = "uwsm app -- swaync-client -t -sw" +local notdnd = "uwsm app -- swaync-client -d" +local nothide = "uwsm app -- swaync-client --hide-latest" + +-- First-class launchers +hl.bind(mainMod .. " + Return", hl.dsp.exec_cmd(terminal)) +hl.bind(mainMod .. " + SHIFT + Return", hl.dsp.exec_cmd(term_tmux)) +hl.bind(mainMod .. " + CTRL + Return", hl.dsp.exec_cmd(term_tmux_append)) +hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("hypr-show-binds")) +hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(filemanager)) +hl.bind(mainMod .. " + W", hl.dsp.exec_cmd(browser)) +hl.bind(mainMod .. " + Space", hl.dsp.exec_cmd(launcher)) + +-- Secondary launchers +hl.bind(mainMod .. " + SHIFT + E", hl.dsp.exec_cmd(editor)) +hl.bind(mainMod .. " + CTRL + E", hl.dsp.exec_cmd(alteditor)) +hl.bind(mainMod .. " + X", hl.dsp.exec_cmd(taskman)) +hl.bind(mainMod .. " + C", hl.dsp.exec_cmd(clipman)) +hl.bind(mainMod .. " + F4", hl.dsp.exec_cmd(soundctl)) + +-- Quick Workspaces submap +hl.bind(mainMod .. " + A", hl.dsp.submap("quickws")) +hl.define_submap("quickws", function() + hl.bind("z", function() hl.dispatch(hl.dsp.focus({ workspace = 1 })); hl.dispatch(hl.dsp.submap("reset")) end) + hl.bind("d", function() hl.dispatch(hl.dsp.focus({ workspace = 2 })); hl.dispatch(hl.dsp.submap("reset")) end) + hl.bind("a", function() hl.dispatch(hl.dsp.focus({ workspace = 3 })); hl.dispatch(hl.dsp.submap("reset")) end) + hl.bind("x", function() hl.dispatch(hl.dsp.focus({ workspace = 4 })); hl.dispatch(hl.dsp.submap("reset")) end) + hl.bind("s", function() hl.dispatch(hl.dsp.focus({ workspace = 5 })); hl.dispatch(hl.dsp.submap("reset")) end) + hl.bind("c", function() hl.dispatch(hl.dsp.focus({ workspace = 6 })); hl.dispatch(hl.dsp.submap("reset")) end) + hl.bind("Escape", hl.dsp.submap("reset")) + hl.bind("Return", hl.dsp.submap("reset")) +end) + +-- Notifications +hl.bind(mainMod .. " + grave", hl.dsp.exec_cmd(notcenter)) +hl.bind(mainMod .. " + SHIFT + grave", hl.dsp.exec_cmd(notdnd)) +hl.bind(mainMod .. " + CTRL + grave", hl.dsp.exec_cmd(nothide)) + +-- Session +hl.bind(mainMod .. " + Pause", hl.dsp.exec_cmd("hyprlock")) +hl.bind(mainMod .. " + SHIFT + Pause", hl.dsp.exec_cmd("owlry-power-menu")) +hl.bind(mainMod .. " + End", hl.dsp.exec_cmd("hyprlock")) +hl.bind(mainMod .. " + SHIFT + End", hl.dsp.exec_cmd("owlry-power-menu")) + +-- Window management +hl.bind(mainMod .. " + Q", hl.dsp.window.kill()) +hl.bind(mainMod .. " + SHIFT + Q", hl.dsp.window.kill()) +hl.bind(mainMod .. " + F", hl.dsp.window.float()) +hl.bind(mainMod .. " + SHIFT + F", hl.dsp.window.fullscreen({ action = "toggle" })) +hl.bind(mainMod .. " + P", hl.dsp.window.pin()) +hl.bind(mainMod .. " + U", hl.dsp.focus({ urgent_or_last = true })) +hl.bind(mainMod .. " + V", hl.dsp.window.center()) + +-- Special workspaces +hl.bind(mainMod .. " + SHIFT + Space", hl.dsp.workspace.toggle_special()) +hl.bind(mainMod .. " + CTRL + Space", hl.dsp.window.move({ workspace = "special" })) +hl.bind(mainMod .. " + N", hl.dsp.workspace.toggle_special("passwordmgr")) + +-- Monitor focus +hl.bind(mainMod .. " + I", hl.dsp.focus({ monitor = "l" })) +hl.bind(mainMod .. " + O", hl.dsp.focus({ monitor = "r" })) +hl.bind(mainMod .. " + SHIFT + I", hl.dsp.workspace.move({ monitor = "l" })) +hl.bind(mainMod .. " + SHIFT + O", hl.dsp.workspace.move({ monitor = "r" })) + +-- Layout +hl.bind(mainMod .. " + comma", hl.dsp.exec_cmd("hypr-workspace-layout toggle-ms")) +hl.bind(mainMod .. " + period", hl.dsp.exec_cmd("hypr-workspace-layout cycle")) + +-- Smart Gaps Toggle +hl.bind(mainMod .. " + SHIFT + G", function() + local gapsIn = hl.get_config("general.gaps_in") + local current = type(gapsIn) == "table" and (gapsIn.top or 0) or (gapsIn or 0) + if current ~= 0 then + hl.config({ general = { gaps_in = 0, gaps_out = 0 } }) + hl.notification.create({ text = "Gaps: OFF", timeout = 2000, icon = "info" }) + else + hl.config({ general = { gaps_in = 5, gaps_out = 5 } }) + hl.notification.create({ text = "Gaps: ON", timeout = 2000, icon = "ok" }) + end +end) + +-- Global Focus Notification (Lua Event) +hl.on("window.active", function(w) + if w ~= nil and w.title ~= nil then + print("Focused: " .. w.title) + end +end) + +-- Vim-like navigation +hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "l" })) +hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "r" })) +hl.bind(mainMod .. " + K", hl.dsp.focus({ direction = "u" })) +hl.bind(mainMod .. " + J", hl.dsp.focus({ direction = "d" })) + +-- Move window +hl.bind(mainMod .. " + SHIFT + H", hl.dsp.exec_cmd("hypr-workspace-layout move-left")) +hl.bind(mainMod .. " + SHIFT + L", hl.dsp.exec_cmd("hypr-workspace-layout move-right")) +hl.bind(mainMod .. " + SHIFT + K", hl.dsp.exec_cmd("hypr-workspace-layout move-up")) +hl.bind(mainMod .. " + SHIFT + J", hl.dsp.exec_cmd("hypr-workspace-layout move-down")) + +-- Resize submap +hl.bind(mainMod .. " + R", hl.dsp.submap("resize")) +hl.define_submap("resize", function() + hl.bind("h", hl.dsp.window.resize({ x = -25, y = 0, relative = true }), { repeating = true }) + hl.bind("l", hl.dsp.window.resize({ x = 25, y = 0, relative = true }), { repeating = true }) + hl.bind("k", hl.dsp.window.resize({ x = 0, y = -25, relative = true }), { repeating = true }) + hl.bind("j", hl.dsp.window.resize({ x = 0, y = 25, relative = true }), { repeating = true }) + hl.bind("Escape", hl.dsp.submap("reset")) +end) + +-- Workspace cycling +hl.bind(mainMod .. " + Tab", hl.dsp.focus({ workspace = "m+1" })) +hl.bind(mainMod .. " + SHIFT + Tab", hl.dsp.focus({ workspace = "m-1" })) + +-- Switch Workspaces 1-10 (IDs 21-30 in setup) +for i = 1, 9 do + hl.bind(mainMod .. " + " .. i, hl.dsp.focus({ workspace = 20 + i })) + hl.bind(mainMod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = 20 + i })) +end +hl.bind(mainMod .. " + 0", hl.dsp.focus({ workspace = 30 })) +hl.bind(mainMod .. " + SHIFT + 0", hl.dsp.window.move({ workspace = 30 })) + +-- Groups +hl.bind(mainMod .. " + Z", hl.dsp.group.next()) +hl.bind(mainMod .. " + SHIFT + Z", hl.dsp.group.prev()) + +-- Mouse binds +hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) +hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) + +-- master-scroll slave scrolling (mouse wheel + bracket fallback) +hl.bind(mainMod .. " + mouse_down", hl.dsp.layout("scrollup"), { mouse = true }) +hl.bind(mainMod .. " + mouse_up", hl.dsp.layout("scrolldown"), { mouse = true }) +hl.bind(mainMod .. " + bracketright", hl.dsp.layout("scrolldown")) +hl.bind(mainMod .. " + bracketleft", hl.dsp.layout("scrollup")) + +-- Multimedia +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { repeating = true }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { repeating = true }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true }) diff --git a/dot_config/hypr/hyprland.d.lua/layout.lua b/dot_config/hypr/hyprland.d.lua/layout.lua new file mode 100644 index 0000000..17ec57f --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/layout.lua @@ -0,0 +1,239 @@ +hl.config({ + master = { + orientation = "left", + mfact = 0.60, + new_status = "slave", + new_on_top = true, + new_on_active = "after", + allow_small_split = true, + special_scale_factor = 0.8, + drop_at_cursor = true + }, + scrolling = { + fullscreen_on_one_column = true, + focus_fit_method = 0, + follow_focus = true, + follow_min_visible = 0.1, + column_width = 0.7 + } +}) + +-- ─── Scrolling slave column helper ─────────────────────────────────────────── +-- Shared by all master-scroll variants. +-- state = { visible, peek, offset, peek_top_addr, peek_bottom_addr } +-- slave_area = HL.Box for this column +-- targets = ctx.targets +-- slave_indices = ordered list of indices into `targets` for this column +local function place_scroll_col(state, slave_area, targets, slave_indices) + local n = #slave_indices + state.peek_top_addr = nil + state.peek_bottom_addr = nil + if n == 0 then return end + + local max_off = math.max(0, n - state.visible) + state.offset = math.max(0, math.min(state.offset, max_off)) + + if n <= state.visible then + local h = slave_area.h / n + for j = 1, n do + targets[slave_indices[j]]:place({ + x = slave_area.x, y = slave_area.y + (j - 1) * h, + w = slave_area.w, h = h, + }) + end + return + end + + local has_top = state.offset > 0 + local has_bot = state.offset < max_off + local top_f = has_top and state.peek or 0 + local bot_f = has_bot and state.peek or 0 + -- h chosen so top_peek + visible*h + bot_peek == slave_area.h exactly + local h = slave_area.h / (state.visible + top_f + bot_f) + + if has_top then + local w = targets[slave_indices[state.offset]].window + if w then state.peek_top_addr = w.address end + end + if has_bot then + local ti = slave_indices[state.offset + state.visible + 1] + if ti then + local w = targets[ti].window + if w then state.peek_bottom_addr = w.address end + end + end + + for j = 1, n do + local t = targets[slave_indices[j]] + if has_top and j == state.offset then + -- Peek at top: extends above slave_area; safe because master is in a different x-range + t:place({ x=slave_area.x, y=slave_area.y - h*(1-state.peek), w=slave_area.w, h=h }) + elseif has_bot and j == state.offset + state.visible + 1 then + -- Peek at bottom: extends below the last visible slot + t:place({ x=slave_area.x, y=slave_area.y + (top_f + state.visible)*h, w=slave_area.w, h=h }) + elseif j >= state.offset + 1 and j <= state.offset + state.visible then + local k = j - state.offset - 1 + t:place({ x=slave_area.x, y=slave_area.y + (top_f + k)*h, w=slave_area.w, h=h }) + else + -- Fully off-screen: park below work area + t:set_box({ x=slave_area.x, y=slave_area.y + slave_area.h + h, w=slave_area.w, h=h }) + end + end +end + +-- ─── Layout states ──────────────────────────────────────────────────────────── +local mfact = 0.60 -- master width fraction (shared across single-master variants) + +local ms = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil } +local sm = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil } + +local cm = { side_w=0.20, visible=2, peek=0.10 } +local cm_left = { visible=cm.visible, peek=cm.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil } +local cm_right = { visible=cm.visible, peek=cm.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil } +local cm_left_addrs = {} +local cm_right_addrs = {} + +-- ─── master-scroll: master left, slaves right ───────────────────────────────── +hl.layout.register("master-scroll", { + recalculate = function(ctx) + local targets = ctx.targets + local n = #targets + if n == 0 then return end + if n == 1 then targets[1]:place(ctx.area); return end + + local slave_area = ctx:split(ctx.area, "right", 1.0 - mfact) + local master_area = { x=ctx.area.x, y=ctx.area.y, w=slave_area.x-ctx.area.x, h=ctx.area.h } + targets[1]:place(master_area) + + local idx = {} + for i = 2, n do idx[#idx+1] = i end + place_scroll_col(ms, slave_area, targets, idx) + end, + + layout_msg = function(ctx, msg) + local max_off = math.max(0, #ctx.targets - 1 - ms.visible) + if msg == "scrolldown" then ms.offset = math.min(ms.offset + 1, max_off); return true + elseif msg == "scrollup" then ms.offset = math.max(ms.offset - 1, 0); return true + elseif msg == "reset" then ms.offset = 0; return true + end + end, +}) + +-- ─── slave-master-scroll: slaves left, master right ────────────────────────── +hl.layout.register("slave-master-scroll", { + recalculate = function(ctx) + local targets = ctx.targets + local n = #targets + if n == 0 then return end + if n == 1 then targets[1]:place(ctx.area); return end + + local slave_area = ctx:split(ctx.area, "left", 1.0 - mfact) + local master_area = { + x = slave_area.x + slave_area.w, y = ctx.area.y, + w = ctx.area.w - slave_area.w, h = ctx.area.h, + } + targets[1]:place(master_area) + + local idx = {} + for i = 2, n do idx[#idx+1] = i end + place_scroll_col(sm, slave_area, targets, idx) + end, + + layout_msg = function(ctx, msg) + local max_off = math.max(0, #ctx.targets - 1 - sm.visible) + if msg == "scrolldown" then sm.offset = math.min(sm.offset + 1, max_off); return true + elseif msg == "scrollup" then sm.offset = math.max(sm.offset - 1, 0); return true + elseif msg == "reset" then sm.offset = 0; return true + end + end, +}) + +-- ─── center-master-scroll: center master, both columns scroll ───────────────── +-- Slaves alternate: odd (1,3,5…) → left column, even (2,4,6…) → right column. +-- scrolldown/scrollup applies to whichever column the active window is in. +hl.layout.register("center-master-scroll", { + recalculate = function(ctx) + local targets = ctx.targets + local n = #targets + cm_left_addrs = {} + cm_right_addrs = {} + if n == 0 then return end + if n == 1 then targets[1]:place(ctx.area); return end + + local left_area = ctx:split(ctx.area, "left", cm.side_w) + local right_area = ctx:split(ctx.area, "right", cm.side_w) + local master_area = { + x = left_area.x + left_area.w, y = ctx.area.y, + w = right_area.x - (left_area.x + left_area.w), h = ctx.area.h, + } + targets[1]:place(master_area) + + local left_idx, right_idx = {}, {} + for i = 2, n do + local slave_pos = i - 1 -- 1-indexed slave number + if slave_pos % 2 == 1 then + left_idx[#left_idx+1] = i + else + right_idx[#right_idx+1] = i + end + local w = targets[i].window + if w then + if slave_pos % 2 == 1 then cm_left_addrs[w.address] = true + else cm_right_addrs[w.address] = true + end + end + end + + place_scroll_col(cm_left, left_area, targets, left_idx) + place_scroll_col(cm_right, right_area, targets, right_idx) + end, + + layout_msg = function(ctx, msg) + local aw = hl.get_active_window() + local addr = aw and aw.address + + local function scroll_col(state, delta) + local max_off = math.max(0, #state - state.visible) -- recalculated below + -- Count windows in this column from targets + local col_n = 0 + for i = 2, #ctx.targets do + local w = ctx.targets[i].window + if w and ((state == cm_left and cm_left_addrs[w.address]) + or (state == cm_right and cm_right_addrs[w.address])) then + col_n = col_n + 1 + end + end + local col_max = math.max(0, col_n - state.visible) + if delta > 0 then state.offset = math.min(state.offset + 1, col_max) + else state.offset = math.max(state.offset - 1, 0) + end + return true + end + + if msg == "scrolldown" then + if addr and cm_left_addrs[addr] then return scroll_col(cm_left, 1) end + if addr and cm_right_addrs[addr] then return scroll_col(cm_right, 1) end + elseif msg == "scrollup" then + if addr and cm_left_addrs[addr] then return scroll_col(cm_left, -1) end + if addr and cm_right_addrs[addr] then return scroll_col(cm_right, -1) end + elseif msg == "reset" then + cm_left.offset = 0; cm_right.offset = 0; return true + end + end, +}) + +-- ─── Auto-scroll on focus ───────────────────────────────────────────────────── +-- When a peek-slot window gets focus (Super+J/K or mouse click on the strip), +-- dispatch the appropriate scroll so it slides into full view. +local all_col_states = { ms, sm, cm_left, cm_right } + +hl.on("window.active", function(w) + if w == nil or w.address == nil then return end + for _, state in ipairs(all_col_states) do + if w.address == state.peek_bottom_addr then + hl.dispatch(hl.dsp.layout("scrolldown")); return + elseif w.address == state.peek_top_addr then + hl.dispatch(hl.dsp.layout("scrollup")); return + end + end +end) diff --git a/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl b/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl new file mode 100644 index 0000000..3c49866 --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl @@ -0,0 +1,35 @@ +{{- range .monitors }} +hl.monitor({ + output = "{{ .name }}", + mode = "{{ .width }}x{{ .height }}@{{ .refresh_rate }}", + position = "{{ .position }}", + scale = {{ .scale }} + {{- if hasKey . "vrr" }}, + vrr = {{ .vrr }} + {{- end }} + {{- if hasKey . "transform" }}, + transform = {{ .transform }} + {{- end }} + {{- if hasKey . "bitdepth" }}, + bitdepth = {{ .bitdepth }} + {{- end }} + {{- if hasKey . "cm" }}, + cm = "{{ .cm }}" + {{- end }} + {{- if hasKey . "supports_hdr" }}, + supports_hdr = {{ .supports_hdr }} + {{- end }} + {{- if hasKey . "sdrbrightness" }}, + sdrbrightness = {{ .sdrbrightness }} + {{- end }} + {{- if hasKey . "sdrsaturation" }}, + sdrsaturation = {{ .sdrsaturation }} + {{- end }} + {{- if hasKey . "max_luminance" }}, + max_luminance = {{ .max_luminance }} + {{- end }} +}) +{{- end }} +{{- if not .monitors }} +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 }) +{{- end }} diff --git a/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl b/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl new file mode 100644 index 0000000..155b33e --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl @@ -0,0 +1,82 @@ +{{- $tags := .chezmoi.config.data.tags -}} + +hl.config({ + group = { + auto_group = true, + insert_after_current = true, + focus_removed_window = true, + drag_into_group = 1, + merge_groups_on_drag = true, + merge_groups_on_groupbar = true, + merge_floated_into_tiled_on_groupbar = true, + group_on_movetoworkspace = false, + -- colors to be integrated via theme table + groupbar = { + enabled = true, + height = 12, + font_family = "GeistMono Nerd Font", + font_size = 8, + font_weight_active = "semibold", + font_weight_inactive = "normal", + stacked = false, + gradients = true, + gradient_rounding = 5, + indicator_height = 0, + rounding = 0, + gradient_round_only_edges = true, + render_titles = true, + scrolling = true, + priority = 3 + } + } +}) + +-- Permissions (NEW in v0.55) +hl.permission({ binary = "/usr/bin/grim", type = "screencopy", mode = "allow" }) + +-- Window Rules +hl.window_rule({ match = { class = ".*" }, suppress_event = "maximize" }) + +-- XWayland fixes +hl.window_rule({ + match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false }, + no_focus = true +}) +hl.window_rule({ match = { xwayland = true }, no_initial_focus = true }) + +-- Communication +hl.window_rule({ match = { class = "^(info\\.mumble\\.Mumble|discord|vesktop|teamspeak-client|TeamSpeak|TeamSpeak 3|teamspeak3)$" }, workspace = "2" }) +hl.window_rule({ match = { class = "^(Element)$" }, workspace = "3" }) + +-- Mail +hl.window_rule({ match = { class = "^(org\\.mozilla\\.Thunderbird)$" }, workspace = "1" }) + +-- Notes +hl.window_rule({ match = { class = "^(@joplin/app-desktop)$" }, workspace = "4" }) + +{{- if index $tags "entertainment" }} +-- Multimedia +hl.window_rule({ match = { class = "Spotify" }, workspace = "6" }) +{{- end }} + +{{- if index $tags "cs2" }} +-- Gaming +hl.window_rule({ match = { class = "^(steam)$" }, workspace = "5" }) +{{- end }} + +-- System +hl.window_rule({ match = { class = "com.saivert.pwvucontrol" }, float = true }) + +hl.window_rule({ + match = { class = "scrrec" }, + float = true, pin = true, idle_inhibit = "always", + rounding = 10, opacity = 0.6, border_size = 0, + size = { 300, 100 }, move = { "1%", "1%" }, monitor = 0, + no_initial_focus = true +}) + +hl.window_rule({ match = { class = "com.gabm.satty" }, float = true, min_size = { 700, 400 } }) + +-- Layer Rules +hl.layer_rule({ match = { namespace = "quickshell:.*" }, blur = true, ignore_alpha = 0.79 }) +hl.layer_rule({ match = { namespace = "quickshell:popout" }, animation = "bouncy" }) diff --git a/dot_config/hypr/hyprland.d.lua/theme.lua.tmpl b/dot_config/hypr/hyprland.d.lua/theme.lua.tmpl new file mode 100644 index 0000000..7b6386c --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/theme.lua.tmpl @@ -0,0 +1,110 @@ +{{- $themeName := .chezmoi.config.data.theme -}} +-- Theme Bridge: {{ $themeName }} + +local theme = {} + +{{- if eq $themeName "apex-neon" }} +theme.base = "rgb(050505)" +theme.surface = "rgb(141414)" +theme.overlay = "rgb(262626)" +theme.muted = "rgb(404040)" +theme.text = "rgb(ededed)" +theme.love = "rgb(ff0044)" +theme.gold = "rgb(ffb700)" +theme.pine = "rgb(00ff99)" +theme.foam = "rgb(00eaff)" +theme.iris = "rgb(9d00ff)" +theme.rose = "rgb(ff8899)" + +theme.splash_text = "rgba(edededee)" +theme.dec_shadow = "rgba(000000ee)" +theme.glow = "rgba(ff0044ee)" + +theme.border_active = { colors = { "rgba(ff0044ff)", "rgba(9d00ffff)" }, angle = 45 } +theme.border_inactive = { colors = { "rgba(40404044)", "rgba(14141444)" }, angle = 45 } + +theme.border_nogroup_active = { colors = { "rgba(00eaffff)", "rgba(00ff99ff)" }, angle = 45 } +theme.border_nogroup_inactive = { colors = { "rgba(40404044)", "rgba(14141444)" }, angle = 45 } + +theme.border_group_active = { colors = { "rgba(ffb700ff)", "rgba(ff0044ff)" }, angle = 45 } +theme.border_group_inactive = { colors = { "rgba(40404066)", "rgba(14141466)" }, angle = 45 } + +theme.border_grouplocked_active = { colors = { "rgba(00eaffff)", "rgba(9d00ffff)" }, angle = 45 } +theme.border_grouplocked_inactive = { colors = { "rgba(00eaff66)", "rgba(9d00ff66)" }, angle = 45 } + +theme.groupbar_text = "rgba(050505ff)" +theme.groupbar_active = { colors = { "rgba(ff0044ff)", "rgba(9d00ffaa)" } } +theme.groupbar_inactive = { colors = { "rgba(141414ee)", "rgba(404040aa)" } } +theme.groupbar_grouplocked_active = { colors = { "rgba(00eaffff)", "rgba(9d00ffff)" } } +theme.groupbar_grouplocked_inactive = { colors = { "rgba(00eaffaa)", "rgba(9d00ffaa)" } } +{{- else if eq $themeName "apex-aeon" }} +theme.base = "rgb(f5f5f5)" +theme.surface = "rgb(e8e8e8)" +theme.overlay = "rgb(ffffff)" +theme.muted = "rgb(737373)" +theme.text = "rgb(0a0a0a)" +theme.love = "rgb(ff0044)" +theme.gold = "rgb(d18f00)" +theme.pine = "rgb(00b377)" +theme.foam = "rgb(007a88)" +theme.iris = "rgb(7a3cff)" +theme.rose = "rgb(ff4d6d)" + +theme.splash_text = "rgba(0a0a0aee)" +theme.dec_shadow = "rgba(73737388)" +theme.glow = "rgba(ff0044ee)" + +theme.border_active = { colors = { "rgba(ff0044ff)", "rgba(7a3cffff)" }, angle = 45 } +theme.border_inactive = { colors = { "rgba(73737344)", "rgba(e8e8e844)" }, angle = 45 } + +theme.border_nogroup_active = { colors = { "rgba(007a88ff)", "rgba(00b377ff)" }, angle = 45 } +theme.border_nogroup_inactive = { colors = { "rgba(73737344)", "rgba(e8e8e844)" }, angle = 45 } + +theme.border_group_active = { colors = { "rgba(d18f00ff)", "rgba(ff0044ff)" }, angle = 45 } +theme.border_group_inactive = { colors = { "rgba(73737366)", "rgba(e8e8e866)" }, angle = 45 } + +theme.border_grouplocked_active = { colors = { "rgba(007a88ff)", "rgba(7a3cffff)" }, angle = 45 } +theme.border_grouplocked_inactive = { colors = { "rgba(007a8866)", "rgba(7a3cff66)" }, angle = 45 } + +theme.groupbar_text = "rgba(0a0a0aff)" +theme.groupbar_active = { colors = { "rgba(ff0044ff)", "rgba(7a3cffaa)" } } +theme.groupbar_inactive = { colors = { "rgba(e8e8e8ee)", "rgba(737373aa)" } } +theme.groupbar_grouplocked_active = { colors = { "rgba(007a88ff)", "rgba(7a3cffff)" } } +theme.groupbar_grouplocked_inactive = { colors = { "rgba(007a88aa)", "rgba(7a3cffaa)" } } +{{- end }} + +-- Apply colors to config +hl.config({ + general = { + ["col.active_border"] = theme.border_active, + ["col.inactive_border"] = theme.border_inactive, + ["col.nogroup_border"] = theme.border_nogroup_inactive, + ["col.nogroup_border_active"] = theme.border_nogroup_active + }, + decoration = { + shadow = { + color = theme.dec_shadow + }, + glow = { + color = theme.glow + } + }, + group = { + ["col.border_active"] = theme.border_group_active, + ["col.border_inactive"] = theme.border_group_inactive, + ["col.border_locked_active"] = theme.border_grouplocked_active, + ["col.border_locked_inactive"] = theme.border_grouplocked_inactive, + groupbar = { + text_color = theme.groupbar_text, + ["col.active"] = theme.groupbar_active, + ["col.inactive"] = theme.groupbar_inactive, + ["col.locked_active"] = theme.groupbar_grouplocked_active, + ["col.locked_inactive"] = theme.groupbar_grouplocked_inactive + } + }, + misc = { + ["col.splash"] = theme.splash_text + } +}) + +return theme diff --git a/dot_config/hypr/hyprland.d.lua/workspaces.lua.tmpl b/dot_config/hypr/hyprland.d.lua/workspaces.lua.tmpl new file mode 100644 index 0000000..c42d6db --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/workspaces.lua.tmpl @@ -0,0 +1,41 @@ +{{- $tags := .chezmoi.config.data.tags -}} + +-- Special Workspaces +hl.workspace_rule({ workspace = "special:passwordmgr", on_created_empty = "uwsm app -- bitwarden-desktop" }) + +-- Named Workspaces (IDs 1-6, sorted before numbered) +hl.workspace_rule({ workspace = "1", default_name = "mail" }) +hl.workspace_rule({ workspace = "2", default_name = "comms", monitor = "DP-2", layout = "scrolling", layout_opts = { direction = "down" } }) +hl.workspace_rule({ workspace = "3", default_name = "element", monitor = "DP-2", layout = "scrolling", layout_opts = { direction = "down" } }) +hl.workspace_rule({ workspace = "4", default_name = "joplin" }) + +{{- if index $tags "cs2" }} +hl.workspace_rule({ workspace = "5", default_name = "steam", layout = "scrolling" }) +{{- end }} + +{{- if index $tags "entertainment" }} +hl.workspace_rule({ workspace = "6", default_name = "spotify", monitor = "DP-2", layout = "monocle", on_created_empty = "uwsm app -- spotify-launcher" }) +{{- end }} + +-- Monitor Workspaces +{{- range $monitor := .monitors }} + {{- range $index, $ws := $monitor.workspaces }} + {{- if kindIs "map" $ws }} +hl.workspace_rule({ + workspace = "{{ $ws.id }}", + monitor = "{{ $monitor.name }}", + default = {{ if eq $index 0 }}true{{ else }}false{{ end }} + {{- if index $ws "name" }}, default_name = "{{ $ws.name }}"{{ end }} + {{- if index $ws "layout" }}, layout = "{{ $ws.layout }}"{{ end }} + {{- if index $ws "layoutopts" }}, layout_opts = { + {{- range $i, $opt := index $ws "layoutopts" }} + {{- $parts := splitList ":" $opt }} + {{ if $i }}, {{ end }}{{ index $parts 0 }} = "{{ index $parts 1 }}" + {{- end }} + }{{ end }} +}) + {{- else }} +hl.workspace_rule({ workspace = "{{ $ws }}", monitor = "{{ $monitor.name }}", default = {{ if eq $index 0 }}true{{ else }}false{{ end }} }) + {{- end }} + {{- end }} +{{- end }} diff --git a/dot_config/hypr/hyprland.lua.tmpl b/dot_config/hypr/hyprland.lua.tmpl new file mode 100644 index 0000000..15c1a6d --- /dev/null +++ b/dot_config/hypr/hyprland.lua.tmpl @@ -0,0 +1,14 @@ +{{- $tags := .chezmoi.config.data.tags -}} +-- Main Hyprland Lua Config Entry Point (v0.55+) + +local cfgdir = os.getenv("HOME") .. "/.config/hypr" +package.path = package.path .. ";" .. cfgdir .. "/hyprland.d.lua/?.lua" + +require("theme") +require("general") +require("monitors") +require("workspaces") +require("input") +require("layout") +require("rules") +require("keybinds")