This commit is contained in:
2026-07-31 22:02:52 +09:00
parent 06cd9516b3
commit 3dae1d26ef
6 changed files with 270 additions and 3 deletions
+4
View File
@@ -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"
];
};
@@ -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"
];
};
}
+42 -2
View File
@@ -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";
@@ -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"
@@ -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