From 3dae1d26ef46b3415daf3822a0a4002d7eaf362f Mon Sep 17 00:00:00 2001 From: moons Date: Fri, 31 Jul 2026 22:02:52 +0900 Subject: [PATCH] noctalia --- hosts/x1g13/home.nix | 23 ++- modules/applications/labwc/home.nix | 4 + modules/applications/noctalia/codexbar.nix | 44 ++++++ modules/applications/noctalia/home.nix | 44 +++++- .../plugins/codexbar-usage/plugin.toml | 13 ++ .../plugins/codexbar-usage/usage.luau | 145 ++++++++++++++++++ 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 modules/applications/noctalia/codexbar.nix create mode 100644 modules/applications/noctalia/plugins/codexbar-usage/plugin.toml create mode 100644 modules/applications/noctalia/plugins/codexbar-usage/usage.luau diff --git a/hosts/x1g13/home.nix b/hosts/x1g13/home.nix index e884ec8..b401f13 100644 --- a/hosts/x1g13/home.nix +++ b/hosts/x1g13/home.nix @@ -1,6 +1,27 @@ { + + services.kanshi = { + enable = true; + systemdTarget = "labwc-session.target"; + settings = [ + { + profile = { + name = "x1g13"; + outputs = [ + { + criteria = "eDP-1"; + status = "enable"; + position = "0,0"; + scale = 1.5; + } + ]; + }; + } + ]; + }; + programs.niri.settings.outputs."eDP-1" = { - scale = 1.2; + scale = 1.5; position = { x = 0; y = 0; diff --git a/modules/applications/labwc/home.nix b/modules/applications/labwc/home.nix index 0ab647c..6365903 100644 --- a/modules/applications/labwc/home.nix +++ b/modules/applications/labwc/home.nix @@ -151,6 +151,10 @@ in # Pull in graphical-session.target so Home Manager services such as # portals, Vicinae, and the idle manager follow the labwc session. systemd.extraCommands = [ + # Clear the previous session's target first. Otherwise services such as + # kanshi that lose their Wayland connection on logout remain in a + # failed/start-limit-hit state and are not started for the next login. + "${lib.getExe' pkgs.systemd "systemctl"} --user stop labwc-session.target" "${lib.getExe' pkgs.systemd "systemctl"} --user --no-block start labwc-session.target" ]; }; diff --git a/modules/applications/noctalia/codexbar.nix b/modules/applications/noctalia/codexbar.nix new file mode 100644 index 0000000..be207d3 --- /dev/null +++ b/modules/applications/noctalia/codexbar.nix @@ -0,0 +1,44 @@ +{ lib, pkgs }: +let + version = "0.46.0"; + architecture = + if pkgs.stdenv.hostPlatform.isx86_64 then + "x86_64" + else if pkgs.stdenv.hostPlatform.isAarch64 then + "aarch64" + else + throw "CodexBar CLI is only packaged for x86_64-linux and aarch64-linux"; + hash = + { + x86_64 = "sha256-yMrOpu1WIv2sGVgvY9qf2XCoX2AXMSqR2b8bQDRU6os="; + aarch64 = "sha256-pCp3NOlxFN6zeH67W04WGSPbUae+ktzR5vEunWqu00g="; + } + .${architecture}; +in +pkgs.stdenvNoCC.mkDerivation { + pname = "codexbar"; + inherit version; + + src = pkgs.fetchurl { + url = "https://github.com/steipete/CodexBar/releases/download/v${version}/CodexBarCLI-v${version}-linux-musl-${architecture}.tar.gz"; + inherit hash; + }; + + dontUnpack = true; + + installPhase = '' + tar -xzf "$src" CodexBarCLI + install -Dm755 CodexBarCLI "$out/bin/codexbar" + ''; + + meta = { + description = "CLI for CodexBar usage data"; + homepage = "https://github.com/steipete/CodexBar"; + license = lib.licenses.mit; + mainProgram = "codexbar"; + platforms = [ + "aarch64-linux" + "x86_64-linux" + ]; + }; +} diff --git a/modules/applications/noctalia/home.nix b/modules/applications/noctalia/home.nix index 186b640..5fd9ebd 100644 --- a/modules/applications/noctalia/home.nix +++ b/modules/applications/noctalia/home.nix @@ -5,6 +5,14 @@ ... }: let + codexbar = import ./codexbar.nix { inherit lib pkgs; }; + codexbarUsagePlugin = pkgs.runCommand "noctalia-codexbar-usage-plugin" { } '' + mkdir -p "$out" + cp ${./plugins/codexbar-usage/plugin.toml} "$out/plugin.toml" + substitute ${./plugins/codexbar-usage/usage.luau} "$out/usage.luau" \ + --replace-fail "@codexbarPath@" "${lib.getExe codexbar}" + ''; + wallpapers = pkgs.fetchFromGitHub { owner = "moons-14"; repo = "wallpapers"; @@ -21,6 +29,14 @@ let "cpu-graph" "ram" ]; + labwcSystemWidgets = [ + "network" + "bluetooth" + "network-connection" + "spacer" + "cpu" + "ram" + ]; centerWidgets = [ "app-launcher" "workspaces" @@ -45,7 +61,13 @@ let margin_ends = 0; start = [ "taskbar" ]; center = centerWidgets; - end = systemWidgets ++ [ "spacer" ] ++ endWidgets; + end = + labwcSystemWidgets + ++ [ + "moons/codexbar-usage:usage" + "spacer" + ] + ++ endWidgets; }; dock = { @@ -56,6 +78,7 @@ let network.show_label = false; taskbar = { type = "taskbar"; + scale = 1.5; pinned = [ "org.gnome.Nautilus" "org.gnome.TextEditor" @@ -64,10 +87,14 @@ let "code" "dev.zed.Zed" "codex" + "vesktop" ]; group_by_workspace = false; show_window_title = false; + pinned_opacity = 0.60; + inactive_opacity = 1.0; }; + input-volume.show_label = false; }; }; @@ -97,7 +124,10 @@ let }; in { - home.packages = [ pkgs.networkmanagerapplet ]; + home.packages = [ + codexbar + pkgs.networkmanagerapplet + ]; home.file = { ".face".source = ../../../images/avatar.jpg; @@ -107,6 +137,11 @@ in }; }; + xdg.dataFile."noctalia/plugins/codexbar-usage" = { + source = codexbarUsagePlugin; + recursive = true; + }; + programs.noctalia = { enable = true; @@ -141,11 +176,16 @@ in center = centerWidgets; end = [ "media" + "moons/codexbar-usage:usage" "spacer" ] ++ endWidgets; }; + plugins = { + enabled = [ "moons/codexbar-usage" ]; + }; + widget = { app-launcher = { type = "custom_button"; diff --git a/modules/applications/noctalia/plugins/codexbar-usage/plugin.toml b/modules/applications/noctalia/plugins/codexbar-usage/plugin.toml new file mode 100644 index 0000000..93dd818 --- /dev/null +++ b/modules/applications/noctalia/plugins/codexbar-usage/plugin.toml @@ -0,0 +1,13 @@ +id = "moons/codexbar-usage" +name = "CodexBar Usage" +version = "1.0.0" +plugin_api = 3 +min_noctalia = "5.0.0" +author = "moons" +license = "MIT" +icon = "ai" +description = "Codex usage limits from the CodexBar CLI." + +[[widget]] +id = "usage" +entry = "usage.luau" diff --git a/modules/applications/noctalia/plugins/codexbar-usage/usage.luau b/modules/applications/noctalia/plugins/codexbar-usage/usage.luau new file mode 100644 index 0000000..3eab7d7 --- /dev/null +++ b/modules/applications/noctalia/plugins/codexbar-usage/usage.luau @@ -0,0 +1,145 @@ +local codexbar = "@codexbarPath@" +local refreshIntervalMs = 60000 +local usagePage = "https://chatgpt.com/codex/settings/usage" + +local refreshing = false + +local function percent(value) + if type(value) ~= "number" then + return "—" + end + return tostring(math.floor(value + 0.5)) .. "%" +end + +local function oneDecimal(value) + local tenths = math.floor(value * 10 + 0.5) + return tostring(math.floor(tenths / 10)) .. "." .. tostring(tenths % 10) +end + +local function unixSeconds(isoTime) + if type(isoTime) ~= "string" then + return nil + end + + local year, month, day, hour, minute, second = string.match( + isoTime, + "^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)" + ) + if year == nil then + return nil + end + + year = tonumber(year) + month = tonumber(month) + day = tonumber(day) + hour = tonumber(hour) + minute = tonumber(minute) + second = tonumber(second) + + if month <= 2 then + year = year - 1 + month = month + 12 + end + + local era = math.floor(year / 400) + local yearOfEra = year - era * 400 + local dayOfYear = math.floor((153 * (month - 3) + 2) / 5) + day - 1 + local dayOfEra = yearOfEra * 365 + math.floor(yearOfEra / 4) - math.floor(yearOfEra / 100) + dayOfYear + local daysSinceEpoch = era * 146097 + dayOfEra - 719468 + + return daysSinceEpoch * 86400 + hour * 3600 + minute * 60 + second +end + +local function resetRemaining(resetsAt, resetDescription) + local resetSeconds = unixSeconds(resetsAt) + if resetSeconds == nil then + return resetDescription or "—" + end + + local remaining = math.max(0, resetSeconds - math.floor(noctalia.nowMs() / 1000)) + local days = math.floor(remaining / 86400) + if days > 0 then + return tostring(days) .. "d" .. oneDecimal((remaining - days * 86400) / 3600) .. "h" + end + + return oneDecimal(remaining / 3600) .. "h" +end + +local function showError(message) + barWidget.setGlyph("alert-circle") + barWidget.setText("Codex: unavailable") + barWidget.setTooltip("CodexBar: " .. message) +end + +local function showUsage(item) + local usage = item.usage or {} + local primary = usage.primary or {} + local secondary = usage.secondary or {} + local primaryReset = resetRemaining(primary.resetsAt, primary.resetDescription) + local secondaryReset = resetRemaining(secondary.resetsAt, secondary.resetDescription) + + barWidget.setGlyph("ai") + barWidget.setText( + "5h:" + .. percent(primary.usedPercent) + .. "(" + .. primaryReset + .. ") - 7d:" + .. percent(secondary.usedPercent) + .. "(" + .. secondaryReset + .. ")" + ) + barWidget.setTooltip({ + { "5-hour usage", percent(primary.usedPercent) .. " · resets in " .. primaryReset }, + { "Weekly usage", percent(secondary.usedPercent) .. " · resets in " .. secondaryReset }, + { "Click", "Open Codex usage settings" }, + }) +end + +local function refresh() + if refreshing then + return + end + + refreshing = true + noctalia.runAsync( + codexbar .. " usage --provider codex --source cli --format json", + function(result) + refreshing = false + + if result.exitCode ~= 0 then + showError(result.stderr ~= "" and result.stderr or "codexbar exited " .. tostring(result.exitCode)) + return + end + + local data, error = noctalia.json.decode(result.stdout) + if data == nil then + showError("invalid CodexBar JSON: " .. (error or "unknown error")) + return + end + + local item = data[1] or data + if item.error ~= nil then + showError(item.error.message or "CodexBar returned an error") + return + end + + showUsage(item) + end, + 30000 + ) +end + +function update() + noctalia.setUpdateInterval(refreshIntervalMs) + refresh() +end + +function onClick() + noctalia.runAsync("xdg-open " .. usagePage) +end + +function onRightClick() + refresh() +end