-- ─────────────────────────────────────────────────────────────────────────────
-- Look and feel -- Tokyo Night Moon
--
-- Colors here must stay in sync with quickshell/config/Theme.qml.
--   accent  user-selectable, see `accents` below -- blue (#82aaff) ships
--   bg      #222436   base
--
-- Performance note: every animation below is event-driven. Nothing uses the
-- "loop" style on borderangle/shadowangle/glowangle -- that style forces
-- Hyprland to render at full refresh rate forever, even when nothing is
-- visible, which is exactly the kind of idle GPU burn to avoid on a 4500x3000
-- display.
-- ─────────────────────────────────────────────────────────────────────────────

local prefs = require("prefs")

-- The eight accents, from config/palette.json -- the one place they are
-- written down for everything outside QML. They used to be restated here,
-- which made adding a ninth accent a five-file edit; the file that got
-- forgotten was always the one that fails silently.
--
-- Falls back to the shipped blue pair if the palette cannot be read, for the
-- same reason prefs never raises: a missing file costs the accent colour, not
-- the compositor config.
local palette = prefs.readJson(os.getenv("HOME") .. "/.config/quickshell/config/palette.json")
local accents = palette.accents or {
    blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
}

-- The accent pair a fresh session or `hyprctl reload` starts from.
-- services/ColorScheme.qml overwrites this live, from the same Theme.accents
-- data, once the shell settles (~1.2s after startup -- see its `settle`
-- Timer). This table exists only so the compositor is never observably blue
-- for a non-blue accent during the gap before that first live apply.
local accentScheme = prefs.get("colorScheme", "dark")
local accentPair = accents[prefs.get("accentName", "blue")] or accents.blue
local accentStart = accentScheme == "light" and accentPair.light or accentPair.dark
local accentEnd = accentScheme == "light" and accentPair.lightSecondary or accentPair.darkSecondary

hl.config({
    general = {
        gaps_in  = prefs.get("gapsIn", 5),
        gaps_out = prefs.get("gapsOut", 10),

        border_size = prefs.get("borderSize", 2),

        col = {
            -- The focused accent role, on a diagonal so the pair is visible
            -- on both a tall and a wide window. Driven by the chosen
            -- accentName (see the `accents` table above); services/
            -- ColorScheme.qml applies the same values live, and restates them
            -- from Theme.accent/Theme.accentSecondary on every scheme change
            -- too, since each accent carries a separate pair per scheme.
            active_border   = { colors = { "rgba(" .. accentStart .. "ee)", "rgba(" .. accentEnd .. "ee)" }, angle = 115 },
            -- The neutral inactive role follows the color scheme because a
            -- dark neutral disappears against a light desktop.
            -- services/ColorScheme.qml applies the same values live; this is
            -- the value a fresh session starts from.
            inactive_border = prefs.get("colorScheme", "dark") == "light"
                and "rgba(a8aecb99)" or "rgba(3b426199)",
        },

        resize_on_border         = prefs.get("resizeOnBorder", true),
        extend_border_grab_area  = prefs.getInt("borderGrabArea", 15),
        hover_icon_on_border     = prefs.get("hoverIconOnBorder", true),

        -- Enables the per-window "immediate" rule used for games in rules.lua.
        -- Harmless on its own; tearing only happens where a rule opts in.
        allow_tearing = true,

        layout = prefs.get("windowLayout", "dwindle"),

        snap = {
            enabled      = prefs.get("windowSnapping", true),
            window_gap   = prefs.getInt("snapWindowGap", 10),
            monitor_gap  = prefs.getInt("snapMonitorGap", 10),
            respect_gaps = prefs.get("snapRespectGaps", false),
        },
    },

    decoration = {
        -- 18 to match the shell's popover radius, so a window and a panel sitting
        -- next to each other read as the same object family.
        rounding       = prefs.get("windowRounding", 18),
        rounding_power = prefs.get("roundingPower", 2),

        active_opacity     = prefs.get("activeOpacity", 1.0),
        fullscreen_opacity = prefs.get("fullscreenOpacity", 1.0),
        inactive_opacity = prefs.get("inactiveOpacity", 1.0),

        -- Accessibility: dim every window but the focused one.
        dim_inactive = prefs.get("dimInactive", false),
        dim_strength = prefs.get("dimStrength", 0.5),

        blur = {
            enabled = prefs.get("blurEnabled", true),
            size    = prefs.get("blurSize", 8),
            passes  = prefs.get("blurPasses", 3),

            -- Required for blur to be affordable. Never turn this off.
            new_optimizations = true,
            -- Floating windows skip blurring what's behind tiled ones. Big win.
            xray              = true,

            ignore_opacity    = true,
            noise             = 0.0117,
            contrast          = 0.9,
            brightness        = 0.85,
            vibrancy          = 0.17,
            vibrancy_darkness = 0.05,

            popups             = true,
            popups_ignorealpha = 0.2,

            -- Blurring the special workspace is expensive and rarely seen.
            special = false,
        },

        shadow = {
            enabled      = prefs.get("shadowEnabled", true),
            range        = prefs.get("shadowRange", 20),
            render_power = prefs.getInt("shadowRenderPower", 3),
            sharp        = prefs.get("shadowSharp", false),
            color        = "rgba(15161eee)",
            -- Deliberately not a setting: a two-axis offset needs a control we
            -- do not have, and a slider bound to half a value is worse than
            -- leaving it alone. SystemSettings understands the vec2 shape
            -- already, so adding it later is only a matter of the widget.
            offset       = { 0, 4 },
            scale        = 1.0,
        },

        -- New in 0.56. Kept deliberately faint: in this direction the gradient
        -- border is the signature, and a strong halo would compete with it.
        -- This is just enough to lift the focused window off the wallpaper.
        -- Derived from the same accent as active_border above, not hardcoded,
        -- so the halo never disagrees with the border it surrounds.
        glow = {
            enabled        = prefs.get("glowEnabled", true),
            range          = prefs.get("glowRange", 8),
            render_power   = 2,
            color          = "rgba(" .. accentStart .. "33)",
            color_inactive = "rgba(00000000)",
        },

        -- Off: costs real frame time and reads as smeary on a 60Hz panel.
        motion_blur = { enabled = false },
    },

    animations = { enabled = prefs.get("animationsEnabled", true) },

    dwindle = {
        -- Keep the split orientation a window was created with. Closest match
        -- to how the Forge extension behaved on GNOME.
        preserve_split = prefs.get("preserveSplit", true),
        smart_resizing = true,
        force_split    = prefs.getInt("forceSplit", 0),
    },

    -- Only in effect when the tiling layout is "master". Panama ships dwindle,
    -- but Settings offers master as a choice, and a layout you can select and
    -- cannot configure is barely a choice at all.
    master = {
        mfact       = prefs.get("masterFactor", 0.55),
        orientation = prefs.get("masterOrientation", "left"),
        new_status  = prefs.get("masterNewStatus", "slave"),
        new_on_top  = prefs.get("masterNewOnTop", false),
    },

    misc = {
        force_default_wallpaper  = 0,
        -- Stored as "show the logo / show the splash" and written as Hyprland's
        -- `disable_*`, matching the `invert` flag on these entries in
        -- PreferenceSchema so both sides agree about which way round they are.
        disable_hyprland_logo    = not prefs.get("hyprlandLogo", false),
        disable_splash_rendering = not prefs.get("hyprlandSplash", false),

        -- Keep native Wayland selection paste and GTK's matching preference
        -- in lockstep. DesktopStyle applies the GTK half only after this value
        -- has been read back and stored by SystemSettings.
        middle_click_paste = prefs.get("middleClickPaste", true),

        -- Same setting as Theme.fontFamily in the shell. If only the QML side
        -- followed the preference, the compositor and the shell would disagree
        -- about the interface font and nobody would notice until a tooltip
        -- rendered in a different typeface.
        font_family = prefs.get("interfaceFont", "Adwaita Sans"),

        -- Variable refresh rate. 3 = enable only for fullscreen windows whose
        -- content type is "video" or "game" -- the tag rules.lua applies to
        -- games. Keeps VRR off the desktop, where it causes visible flicker.
        vrr = prefs.getInt("vrrPolicy", 3),

        -- Blur behind the lock screen.
        session_lock_blur = true,

        -- Let a crashed lock screen be recovered rather than stranding you.
        allow_session_lock_restore = true,

        -- Don't let apps steal focus by shouting; matches GNOME's behavior.
        focus_on_activate = prefs.get("focusOnActivate", false),

        -- Whether pointing at another monitor is enough to move focus there,
        -- or it takes a click. GNOME moves on pointer; both are offered.
        mouse_move_focuses_monitor = prefs.get("mouseMoveFocusesMonitor", true),

        -- Window swallowing: a terminal hides itself while a graphical
        -- application launched from it is open, and comes back when that
        -- application exits. Off by default -- it is a real change in how the
        -- desktop behaves, and one that is confusing rather than broken if you
        -- did not ask for it: your terminal appears to vanish.
        --
        -- The regex is narrow on purpose. Anything matching it can swallow, so
        -- a permissive pattern means windows disappearing in cases nobody
        -- intended. Only the two terminals this desktop actually ships.
        enable_swallow = prefs.get("windowSwallow", false),
        swallow_regex  = "^(kitty|com\\.mitchellh\\.ghostty)$",
    },

    render = {
        -- Color management is stable and on by default in 0.56.
        cm_enabled = true,

        -- 1 = automatically flip the monitor into HDR for fullscreen content
        -- that asks for it, and back out afterwards. This is how games get HDR
        -- without the desktop paying the screencopy cost. See monitors.lua.
        cm_auto_hdr = prefs.getInt("autoHdr", 1),

        -- 2 = direct scanout only for windows tagged content = "game"
        -- (set by the rules in rules.lua). Bypasses compositing for real
        -- fullscreen games.
        direct_scanout = prefs.getInt("directScanoutPolicy", 2),
    },

    cursor = {
        -- 2 = auto. Disables the hardware cursor when tearing, which is what
        -- keeps the cursor from ghosting in games.
        no_hardware_cursors = 2,

        -- Don't let the cursor break VRR on content tagged as a game.
        no_break_fs_vrr = 2,
        min_refresh_rate = 48,

        -- Pull the cursor theme from gsettings so GTK apps and Hyprland agree.
        sync_gsettings_theme = true,

        -- Fade the cursor out after 4s of no movement, like GNOME does.
        inactive_timeout = prefs.get("cursorInactiveTimeout", 4),

        -- The accessibility magnifier. 1.0 = off.
        zoom_factor = prefs.get("magnifierFactor", 1.0),
        zoom_rigid  = prefs.get("magnifierRigid", false),
    },

    ecosystem = {
        no_update_news   = not prefs.get("hyprlandUpdateNews", false),
        no_donation_nag  = not prefs.get("hyprlandDonationNag", false),
    },

    xwayland = {
        -- Render X11 apps at 1:1 device pixels so they're sharp rather than
        -- bilinearly upscaled. See the note in README.md about the unavoidable
        -- sizing tradeoff at 1.5x.
        force_zero_scaling = true,
    },

    quirks = {
        -- 2 = report HDR as preferred to gamescope only. Stops gamescope from
        -- white-screening when it expects HDR to already be live.
        prefer_hdr = 2,
    },
})

-- ── Motion ──────────────────────────────────────────────────────────────────
-- speed is in deciseconds: speed = 4 means 400ms. Higher = slower.

hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } })
hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } })
hl.curve("quick",        { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } })
hl.curve("easy",         { type = "spring", mass = 1, stiffness = 238.1191, dampening = 24.21279 })
-- A touch bouncier than "easy"; used for windows appearing.
hl.curve("snappy",       { type = "spring", mass = 1, stiffness = 320, dampening = 22 })

hl.animation({ leaf = "global",      enabled = true, speed = 10,   bezier = "default" })
hl.animation({ leaf = "border",      enabled = true, speed = 5.39, bezier = "easeOutQuint" })

-- The focus sweep: when a window takes focus, its gradient border rotates into
-- place once. Style "once" is load-bearing -- the alternative, "loop", makes
-- Hyprland render at full refresh rate forever, even when no border is visible.
-- That is exactly the idle GPU cost this config exists to avoid, so: never loop.
hl.animation({ leaf = "borderangle", enabled = true, speed = 6, bezier = "easeOutQuint", style = "once" })

hl.animation({ leaf = "windows",     enabled = true, speed = 4.79, spring = "easy" })
hl.animation({ leaf = "windowsIn",   enabled = true, speed = 4.1,  spring = "snappy",       style = "popin 90%" })
hl.animation({ leaf = "windowsOut",  enabled = true, speed = 1.49, bezier = "almostLinear", style = "popin 90%" })

hl.animation({ leaf = "fade",        enabled = true, speed = 3.03, bezier = "quick" })

-- Layer surfaces = the Quickshell bar, dock, popovers and overlays.
hl.animation({ leaf = "layers",      enabled = true, speed = 3.81, bezier = "easeOutQuint" })
hl.animation({ leaf = "layersIn",    enabled = true, speed = 3,    bezier = "easeOutQuint", style = "fade" })
hl.animation({ leaf = "layersOut",   enabled = true, speed = 2,    bezier = "almostLinear", style = "fade" })

-- Workspace switching: horizontal slide, matching Alt+H / Alt+L direction.
hl.animation({ leaf = "workspaces",  enabled = true, speed = 2.5,  bezier = "easeOutQuint", style = "slidefade 15%" })

hl.animation({ leaf = "zoomFactor",  enabled = true, speed = 7,    bezier = "quick" })

return true
