diff --git a/nix-monitor/README.md b/nix-monitor/README.md new file mode 100644 index 0000000..7a0db7b --- /dev/null +++ b/nix-monitor/README.md @@ -0,0 +1,21 @@ +# Nix Monitor + +![Nix Monitor thumbnail](thumbnail.webp) + +**Nix Monitor** checks Nixpkgs update by comparing local nix hash and remote Nixpkgs's hash + +## Features + - Shows local and remote nixpkgs hash + - Shows NixOS and optionally Home Manager generations + - Shows Nix Store size and closure size + - Customizable clean and update command + + ## Requirements + - Git + - NixOS commands (`nix`, `nixos-rebuild`, `nixos-version`) + - Linux tools (`du`, `cat`, `awk`, `grep`, `tail`, `wc`, `kill`, `pkill`) + - Optionally `home-manager` + +## Notes + - You need to add the update command in the setting first + - By default, clean command executes `nix-collect-garbage -d`, you can change it too! diff --git a/nix-monitor/nix_monitor.luau b/nix-monitor/nix_monitor.luau new file mode 100644 index 0000000..2d5a27e --- /dev/null +++ b/nix-monitor/nix_monitor.luau @@ -0,0 +1,55 @@ +function tr(key) + return noctalia.tr(key) +end + +function cfg(key) + return noctalia.getConfig(key) +end + +function getState(key) + return noctalia.state.get(key) +end + +function displayWidget(glyph: string, text: string, color: string?) + if color == nil then + color = "on_surface" + end + + if cfg("colorize_text") then + barWidget.setColor(color) + end + + if cfg("colorize_glyph") then + barWidget.setGlyphColor(color) + end + + if cfg("show_glyph") then + barWidget.setGlyph(glyph) + end + + if cfg("show_text") then + barWidget.setText(text) + end +end + +-- Hooks +function update() + if getState("isRemoteCheckRunning") then + displayWidget(cfg("checking_glyph"), tr("bar.checking"), cfg("checking_color")) + else + if getState("isUpdateAvailable") then + displayWidget(cfg("update_available_glyph"), tr("bar.update_available"), cfg("update_available_color")) + else + if getState("localHash") == "n/a" or getState("remoteHash") == "n/a" then + displayWidget(cfg("unknown_glyph"), tr("bar.unknown"), cfg("unknown_color")) + else + displayWidget(cfg("up_to_date_glyph"), tr("bar.up_to_date"), cfg("up_to_date_color")) + end + end + end + +end + +function onClick() + noctalia.togglePanel("avivbintangaringga/nix-monitor:panel") +end diff --git a/nix-monitor/nix_monitor_service.luau b/nix-monitor/nix_monitor_service.luau new file mode 100644 index 0000000..334249d --- /dev/null +++ b/nix-monitor/nix_monitor_service.luau @@ -0,0 +1,292 @@ +noctalia.setUpdateInterval(5000) + +-- Helper functions +function tr(key) + return noctalia.tr(key) +end + +function cfg(key) + return noctalia.getConfig(key) +end + +function getState(key) + return noctalia.state.get(key) +end + +function setState(key, val) + noctalia.state.set(key, val) +end + +function watchState(key, cb) + noctalia.state.watch(key, cb) +end + +function killWithPidFile(file) + if noctalia.fileExists(file) then + local pid = noctalia.string.trim(noctalia.readFile(file)) + + if type(pid) == "string" and pid ~= "" then + noctalia.runAsync(`pkill -9 -P {pid} && kill -9 {pid}`) + end + noctalia.removeFile(file) + end +end + +-- Vars +local branch = cfg("branch") +local updateCheckInterval = cfg("update_check_interval") +local updateCheckDurationThreshold = cfg("update_check_duration_threshold") +local systemStatsCheckInterval = cfg("system_stats_check_interval") +local systemStatsCheckDurationThreshold = cfg("system_stats_check_duration_threshold") +local stateDir = "/tmp/nix-monitor" +local remoteHashFile = stateDir .. "/remoteHash" +local storeSizeFile = stateDir .. "/storeSize" +local closureSizeFile = stateDir .. "/closureSize" +local remotePidFile = stateDir .. "/remoteHash.pid" +local storePidFile = stateDir .. "/storeSize.pid" +local closurePidFile = stateDir .. "/closureSize.pid" +local showUpdateAvailableNotification = true + +local localHashCmd = "nixos-version --hash 2> /dev/null | cut -c -7" +local remoteHashCmd = "git ls-remote https://github.com/NixOS/nixpkgs " .. branch .. " 2>/dev/null | cut -c 1-7" +local nixosGenerationsCmd = "nixos-rebuild list-generations | tail -n +2 | wc -l" +local nixosCurrentGenCmd = "nixos-rebuild list-generations | grep True | awk '{print $1}'" +local hmGenerationsCmd = "home-manager generations | wc -l" +local hmCurrentGenCmd = "home-manager generations | grep '(current)' | awk '{print $5}'" +local storeSizeCmd = "du -s --block-size=1GiB /nix/store | awk '\{print $1 \" GiB\"\}'" +local closureSizeCmd = "nix path-info --closure-size --human-readable /run/current-system | awk '\{print $2, $3\}'" + +-- State inits +setState("lastLocalCheckTime", os.time()) +setState("lastRemoteCheckRunTime", os.time()) +setState("lastRemoteCheckTime", os.time()) +setState("lastRemoteCheckTimeStr", "n/a") +setState("localHash", "n/a") +setState("remoteHash", "n/a") +setState("isRemoteCheckRunning", false) +setState("isUpdateAvailable", false) +setState("lastGenerationsCheckTime", 0) +setState("isSystemStatsCheckRunning", false) +setState("lastSystemStatsCheckTime", os.time()) +setState("lastSystemStatsCheckRunTime", os.time()) +setState("hasHomeManager", noctalia.commandExists("home-manager")) +setState("nixosGenerations", "•••") +setState("nixosCurrentGen", "•••") +setState("hmGenerations", "•••") +setState("hmCurrentGen", "•••") +setState("storeSize", "•••") +setState("closureSize", "•••") + +noctalia.mkdirAll(stateDir) +noctalia.writeFile(remoteHashFile, "") + +-- Local check +function checkLocalHash() + setState("lastLocalCheckTime", os.time()) + noctalia.runAsync(localHashCmd, function(res) + if res.exitCode == 0 then + setState("localHash", noctalia.string.trim(res.stdout)) + end + end) +end + +-- Remote check +-- +-- Currently it is writing to a file because when I use the runAsync with +-- the callback, it is timed out +function checkRemoteHash(delay: number?) + local delay = delay or 0 + + if getState("isRemoteCheckRunning") then + return + end + + setState("isRemoteCheckRunning", true) + setState("lastRemoteCheckRunTime", os.time()) + + if cfg("show_update_check_notification") then + noctalia.notify(tr("notification.checking_update")) + end + + noctalia.writeFile(remoteHashFile, "") + noctalia.runAsync(`sleep {delay} && {remoteHashCmd} > {remoteHashFile} & echo $! > {remotePidFile}`) +end + +-- Check if the remoteHash check command is finished by reading the file +function checkRemoteStatus() + local remoteHash = noctalia.string.trim(noctalia.readFile(remoteHashFile)) + + if type(remoteHash) == "string" and remoteHash ~= "" then + setState("isRemoteCheckRunning", false) + setState("remoteHash", remoteHash) + onRemoteHashCheckCompleted() + end +end + +function onRemoteHashCheckCompleted() + if cfg("show_update_check_notification") then + noctalia.notify(tr("notification.update_check_completed")) + end + + setState("lastRemoteCheckTime", os.time()) + + local date = noctalia.formatTime("%Y-%m-%d %H:%M:%S") + setState("lastRemoteCheckTimeStr", date) + + noctalia.removeFile(remotePidFile) +end + +function cancelRemoteCheck() + setState("isRemoteCheckRunning", false) + killWithPidFile(remotePidFile) +end + +-- Generation check +function checkGenerations() + setState("lastGenerationsCheckTime", os.time()) + + noctalia.runAsync(nixosGenerationsCmd, function(result) + setState("nixosGenerations", result.stdout) + end) + + noctalia.runAsync(nixosCurrentGenCmd, function(result) + setState("nixosCurrentGen", result.stdout) + end) + + if getState("hasHomeManager") then + noctalia.runAsync(hmGenerationsCmd, function(result) + setState("hmGenerations", result.stdout) + end) + + noctalia.runAsync(hmCurrentGenCmd, function(result) + setState("hmCurrentGen", result.stdout) + end) + end +end + +-- System stats check +function checkSystemStats() + if getState("isSystemStatsCheckRunning") then + return + end + + setState("isSystemStatsCheckRunning", true) + setState("lastSystemStatsCheckRunTime", os.time()) + + -- Sleep 0 to trigger noctalia calling /bin/sh + noctalia.runAsync(`sleep 0 && {storeSizeCmd} > {storeSizeFile} & echo $! > {storePidFile}`) + noctalia.runAsync(`sleep 0 && {closureSizeCmd} > {closureSizeFile} & echo $! > {closurePidFile}`) +end + +function checkSystemStatsFile() + local storeSize = noctalia.string.trim(noctalia.readFile(storeSizeFile)) + local closureSize = noctalia.string.trim(noctalia.readFile(closureSizeFile)) + + if typeof(storeSize) == "string" and storeSize ~= "" then + setState("storeSize", storeSize) + end + + if typeof(closureSize) == "string" and closureSize ~= "" then + setState("closureSize", closureSize) + end + + local success = typeof(storeSize) == "string" and storeSize ~= "" and + typeof(closureSize) == "string" and closureSize ~= "" + + if success then + setState("isSystemStatsCheckRunning", false) + setState("lastSystemStatsCheckTime", os.time()) + noctalia.removeFile(storePidFile) + noctalia.removeFile(closurePidFile) + end +end + +function cancelSystemCheck() + setState("isSystemStatsCheckRunning", false) + killWithPidFile(storePidFile) + killWithPidFile(closurePidFile) +end + +-- Init +checkLocalHash() +checkRemoteHash(10) +checkSystemStats() + +-- Command watcher +watchState("command", function(command) + if command == "check" then + checkLocalHash() + cancelRemoteCheck() + checkRemoteHash() + checkGenerations() + checkSystemStats() + end + + if command == "cancel" then + cancelRemoteCheck() + cancelSystemCheck() + end +end) + +-- +-- Hooks +-- + +function update() + -- Check local hash every 10 seconds + if (os.time() - getState("lastLocalCheckTime") >= 10) then + checkLocalHash() + end + + -- Remote check + if (os.time() - getState("lastRemoteCheckTime")) >= 60 * updateCheckInterval then + checkRemoteHash() + end + + if getState("isRemoteCheckRunning") then + checkRemoteStatus() + + -- Remote check timeout + if (os.time() - getState("lastRemoteCheckRunTime")) >= 60 * updateCheckDurationThreshold then + cancelRemoteCheck() + end + end + + if getState("localHash") ~= getState("remoteHash") and + getState("localHash") ~= "n/a" and getState("remoteHash") ~= "n/a" + then + setState("isUpdateAvailable", true) + + if cfg("show_update_available_notification") and showUpdateAvailableNotification then + noctalia.notify(tr("notification.update_is_available")) + showUpdateAvailableNotification = false + end + else + setState("isUpdateAvailable", false) + showUpdateAvailableNotification = true + end + + -- Generations check + if (os.time() - getState("lastGenerationsCheckTime")) >= 20 then + checkGenerations() + end + + -- System stats check + if (os.time() - getState("lastSystemStatsCheckTime")) >= 60 * systemStatsCheckInterval then + checkSystemStats() + end + + if getState("isSystemStatsCheckRunning") then + checkSystemStatsFile() + + -- System stats check timeout + if (os.time() - getState("lastSystemStatsCheckRunTime")) >= 60 * systemStatsCheckDurationThreshold then + cancelSystemCheck() + end + end +end + +function onExit(signal) + cancelRemoteCheck() + cancelSystemCheck() +end diff --git a/nix-monitor/panel.luau b/nix-monitor/panel.luau new file mode 100644 index 0000000..3d82af1 --- /dev/null +++ b/nix-monitor/panel.luau @@ -0,0 +1,253 @@ +-- Helper functions + +function tr(key) + return noctalia.tr(key) +end + +function cfg(key) + return noctalia.getConfig(key) +end + +function getState(key) + return noctalia.state.get(key) +end + +function setState(key, val) + noctalia.state.set(key, val) +end + +function watchState(key, cb) + noctalia.state.watch(key, cb) +end + +local function labelBox(subtext, text, glyph) + return ui.column({ + fill = "surface/0.7", + flexGrow = 1, + height = 100, + radius = 8, + align = "center", + padding = 12, + justify = "space_between" + }, { + ui.glyph({ + name = glyph, + size = 30 + }), + ui.label({ + text = text, + fontWeight = "bold" + }), + ui.label({ + text = subtext, + fontSize = 12, + fontWeight = "medium" + }), + }) +end + +local function statLabel(text, glyph: string?) + local items = {} + if glyph ~= nil then + table.insert(items, ui.glyph({ + name = glyph, + size = 20 + })) + end + + table.insert(items, ui.label({ + text = text, + fontSize = 12, + fontWeight = "semibold" + })) + + return ui.row({ + gap = 8, + align = "center" + }, items) +end + +local function render() + local firstBtn + + if getState("isRemoteCheckRunning") then + firstBtn = ui.button({ + text = tr("panel.cancel"), + glyph = "cancel", + flexGrow = 1, + variant = "destructive", + onClick = "onCancelClicked", + }) + else + if getState("isUpdateAvailable") then + firstBtn = ui.button({ + text = tr("panel.update"), + glyph = "progress-down", + flexGrow = 1, + variant = "primary", + onClick = "onUpdateClicked", + }) + else + firstBtn = ui.button({ + text = tr("panel.check"), + glyph = "search", + flexGrow = 1, + variant = "outline", + onClick = "onCheckClicked", + }) + end + end + + local lastCheckTimeStr = getState("lastRemoteCheckTimeStr") + local generationItems = { + statLabel(`SYS: {getState("nixosGenerations")}`, "device-desktop"), + statLabel(`{tr("panel.current")}: {getState("nixosCurrentGen")}`, "device-desktop-check") + } + + if getState("hasHomeManager") then + table.insert(generationItems, + statLabel(`HM: {getState("hmGenerations")}`, "home") + ) + + table.insert(generationItems, + statLabel(`{tr("panel.current")}: {getState("hmCurrentGen")}`, "home-check") + ) + end + + panel.render( + ui.column({ + gap = 8, + }, { + ui.row({ + justify = "space_between", + align = "center" + }, { + ui.label({ + text = tr("panel.title"), + fontSize = 20, + }), + + ui.label({ + text = cfg("branch"), + fontSize = 14, + }), + }), + ui.separator({ + thickness = 2, + orientation = "horizontal", + spacing = 4 + }), + ui.row({ + gap = 8, + align = "center", + justify = "space_between" + }, { + labelBox(tr("panel.local"), getState("localHash"), "devices-pc"), + labelBox(tr("panel.remote"), getState("remoteHash"), "world"), + }), + ui.separator({ + thickness = 2, + orientation = "horizontal", + spacing = 4 + }), + ui.row({ + fill = "surface/0.7", + align = "center", + justify = "space_between", + flexGrow = 1, + paddingV = 4, + paddingH = 8, + radius = 8, + }, generationItems), + ui.row({ + fill = "surface/0.7", + align = "center", + justify = "center", + flexGrow = 1, + paddingV = 4, + paddingH = 8, + radius = 8, + }, { + statLabel(`{tr("panel.store_size")}: {getState("storeSize")}`, "database"), + ui.spacer({width = 16}), + statLabel(`{tr("panel.closure_size")}: {getState("closureSize")}`, "server"), + }), + ui.separator({ + thickness = 2, + orientation = "horizontal", + spacing = 4 + }), + ui.label({ + text = `{tr("panel.last_checked")}: {lastCheckTimeStr}` + }), + ui.row({ + gap = 8, + align = "center", + justify = "space_between" + }, { + firstBtn, + ui.button({ + text = tr("panel.clean"), + glyph = "trash", + flexGrow = 1, + variant = "destructive", + onClick = "onCleanClicked", + }), + }) + }) + ) +end + +-- State watcher +watchState("isRemoteCheckRunning", render) +watchState("lastRemoteCheckTimeStr", render) +watchState("localHash", render) +watchState("remoteHash", render) + +-- Hooks +function onOpen(_ctx) + render() +end + +function update() + render() +end + +-- Click handler +function onCheckClicked() + setState("command", "check") +end + +function onCancelClicked() + setState("command", "cancel") +end + +function onUpdateClicked() + command = cfg("update_command") + if command == "" then + noctalia.notifyError(tr("notification.update_command_is_empty")) + return + end + + if cfg("close_on_enter") then + command = `{command} && echo && read -p "{tr("terminal.press_enter_to_exit")}"` + end + + panel.close() + noctalia.runInTerminal(command) +end + +function onCleanClicked() + command = cfg("clean_command") + if command == "" then + noctalia.notifyError(tr("notification.clean_command_is_empty")) + return + end + + if cfg("close_on_enter") then + command = `{command} && echo && read -p "{tr("terminal.press_enter_to_exit")}"` + end + + panel.close() + noctalia.runInTerminal(command) +end diff --git a/nix-monitor/plugin.toml b/nix-monitor/plugin.toml new file mode 100644 index 0000000..4d089f1 --- /dev/null +++ b/nix-monitor/plugin.toml @@ -0,0 +1,192 @@ +id = "avivbintangaringga/nix-monitor" +name = "Nix Monitor" +version = "1.0.0" +min_noctalia = "5.0.0" +icon = "package" +author = "avivbintangaringga" +description = "Nixpkgs update monitor" +tags = [ "nixos", "utility" ] +dependencies = [ "nix", "nixos-version", "nixos-rebuild", "git", "du", "cat", "awk", "grep", "tail", "wc", "kill", "pkill" ] +license = "MIT" + +[[widget]] +id = "nix-monitor" +entry = "nix_monitor.luau" + +[[widget.setting]] +key = "show_text" +type = "bool" +label_key = "setting.widget.show_text.label" +default = true + +[[widget.setting]] +key = "colorize_text" +type = "bool" +label_key = "setting.widget.colorize_text.label" +default = false + +[[widget.setting]] +key = "show_glyph" +type = "bool" +label_key = "setting.widget.show_glyph.label" +default = true + +[[widget.setting]] +key = "colorize_glyph" +type = "bool" +label_key = "setting.widget.colorize_glyph.label" +default = true + +[[widget.setting]] +key = "up_to_date_glyph" +type = "glyph" +label_key = "setting.widget.up_to_date_glyph.label" +default = "rosette-discount-check" + +[[widget.setting]] +key = "up_to_date_color" +type = "color" +label_key = "setting.widget.up_to_date_color.label" +default = "#57ff57" + +[[widget.setting]] +key = "checking_glyph" +type = "glyph" +label_key = "setting.widget.checking_glyph.label" +default = "loader-3" + +[[widget.setting]] +key = "checking_color" +type = "color" +label_key = "setting.widget.checking_color.label" +default = "#ffeb57" + +[[widget.setting]] +key = "update_available_glyph" +type = "glyph" +label_key = "setting.widget.update_available_glyph.label" +default = "cloud-download" + +[[widget.setting]] +key = "update_available_color" +type = "color" +label_key = "setting.widget.update_available_color.label" +default = "#ff5757" + +[[widget.setting]] +key = "unknown_glyph" +type = "glyph" +label_key = "setting.widget.unknown_glyph.label" +default = "cloud-question" + +[[widget.setting]] +key = "unknown_color" +type = "color" +label_key = "setting.widget.unknown_color.label" +default = "on_surface" + +[[setting]] +key = "update_check_interval" +type = "int" +label_key = "setting.update_check_interval.label" +description_key = "setting.update_check_interval.description" +default = 60 +min = 10 + +[[setting]] +key = "update_check_duration_threshold" +type = "int" +label_key = "setting.update_check_duration_threshold.label" +description_key = "setting.update_check_duration_threshold.description" +default = 5 +min = 1 +max = 30 + +[[setting]] +key = "system_stats_check_interval" +type = "int" +label_key = "setting.system_stats_check_interval.label" +description_key = "setting.system_stats_check_interval.description" +default = 10 +min = 5 + +[[setting]] +key = "system_stats_check_duration_threshold" +type = "int" +label_key = "setting.system_stats_check_duration_threshold.label" +description_key = "setting.system_stats_check_duration_threshold.description" +default = 5 +min = 1 +max = 30 + +[[setting]] +key = "show_update_check_notification" +type = "bool" +label_key = "setting.show_update_check_notification.label" +description_key = "setting.show_update_check_notification.description" +default = false + +[[setting]] +key = "show_update_available_notification" +type = "bool" +label_key = "setting.show_update_available_notification.label" +description_key = "setting.show_update_available_notification.description" +default = true + +[[setting]] +key = "branch" +type = "select" +label_key = "setting.branch.label" +description_key = "setting.branch.description" +options = [ + { value = "master", label_key = "setting.option.branch.master" }, + { value = "nixos-unstable", label_key = "setting.option.branch.nixos_unstable" }, + { value = "nixos-unstable-small", label_key = "setting.option.branch.nixos_unstable_small" }, + { value = "nixos-25.11", label_key = "setting.option.branch.nixos_25_11" }, + { value = "nixos-25.05", label_key = "setting.option.branch.nixos_25_05" }, + { value = "nixos-24.11", label_key = "setting.option.branch.nixos_24_11" }, + { value = "nixos-24.05", label_key = "setting.option.branch.nixos_24_05" }, + { value = "nixos-23.11", label_key = "setting.option.branch.nixos_23_11" }, + { value = "nixos-23.05", label_key = "setting.option.branch.nixos_23_05" }, + { value = "nixos-25.11-small", label_key = "setting.option.branch.nixos_25_11_small" }, + { value = "nixos-25.05-small", label_key = "setting.option.branch.nixos_25_05_small" }, + { value = "nixos-24.11-small", label_key = "setting.option.branch.nixos_24_11_small" }, + { value = "nixos-24.05-small", label_key = "setting.option.branch.nixos_24_05_small" }, + { value = "nixos-23.11-small", label_key = "setting.option.branch.nixos_23_11_small" }, + { value = "nixos-23.05-small", label_key = "setting.option.branch.nixos_23_05_small" }, +] +default = "nixos-unstable" + +[[setting]] +key = "update_command" +type = "string" +label_key = "setting.update_command.label" +description_key = "setting.update_command.description" +default = "" + +[[setting]] +key = "clean_command" +type = "string" +label_key = "setting.clean_command.label" +description_key = "setting.clean_command.description" +default = "nix-collect-garbage -d" + +[[setting]] +key = "close_on_enter" +type = "bool" +label_key = "setting.close_on_enter.label" +description_key = "setting.close_on_enter.description" +default = true + +[[panel]] +id = "panel" +entry = "panel.luau" +placement = "attached" +position = "auto" +open_near_click = true +width = 420 +height = 370 + +[[service]] +id = "service" +entry = "nix_monitor_service.luau" diff --git a/nix-monitor/thumbnail.webp b/nix-monitor/thumbnail.webp new file mode 100644 index 0000000..12b26c2 Binary files /dev/null and b/nix-monitor/thumbnail.webp differ diff --git a/nix-monitor/translations/en.json b/nix-monitor/translations/en.json new file mode 100644 index 0000000..9e2f6a7 --- /dev/null +++ b/nix-monitor/translations/en.json @@ -0,0 +1,70 @@ +{ + "bar.checking": "Checking...", + "bar.update_available": "Update available!", + "bar.up_to_date": "Up to date", + "bar.unknown": "Unknown", + "notification.checking_update": "Checking update...", + "notification.update_is_available": "Update is available!", + "notification.update_check_completed": "Update check completed!", + "notification.update_command_is_empty": "Update command is empty!", + "notification.clean_command_is_empty": "Clean command is empty!", + "panel.title": "Nix Monitor", + "panel.cancel": "Cancel", + "panel.update": "Update", + "panel.check": "Check", + "panel.clean": "Clean", + "panel.local": "Local", + "panel.remote": "Remote", + "panel.last_checked": "Last checked", + "panel.current": "Current", + "panel.store_size": "Store size", + "panel.closure_size": "Closure size", + "terminal.press_enter_to_exit": "Press enter to exit...", + "setting.widget.show_text.label": "Show text", + "setting.widget.colorize_text.label": "Colorize text", + "setting.widget.show_glyph.label": "Show glyph", + "setting.widget.colorize_glyph.label": "Colorize glyph", + "setting.widget.up_to_date_glyph.label": "Up to date glyph", + "setting.widget.up_to_date_color.label": "Up to date color", + "setting.widget.checking_glyph.label": "Checking glyph", + "setting.widget.checking_color.label": "Checking color", + "setting.widget.update_available_glyph.label": "Update available glyph", + "setting.widget.update_available_color.label": "Update available color", + "setting.widget.unknown_glyph.label": "Unknown status glyph", + "setting.widget.unknown_color.label": "Unknown status color", + "setting.update_check_interval.label": "Update check interval", + "setting.update_check_interval.description": "How often to check for updates (in minutes)", + "setting.update_check_duration_threshold.label": "Update check duration threshold", + "setting.update_check_duration_threshold.description": "Cancel update check if it took too long (in minutes)", + "setting.system_stats_check_interval.label": "System stats check interval", + "setting.system_stats_check_interval.description": "How often to check for system stats (in minutes)", + "setting.system_stats_check_duration_threshold.label": "System stats check duration threshold", + "setting.system_stats_check_duration_threshold.description": "Cancel system stats check if it took too long (in minutes)", + "setting.show_update_check_notification.label": "Show update check notification", + "setting.show_update_check_notification.description": "Whether to show a notification whenever update check is in progress and completed", + "setting.show_update_available_notification.label": "Show update available notification", + "setting.show_update_available_notification.description": "Whether to show a notification whenever an update is available", + "setting.branch.label": "Branch", + "setting.branch.description": "Which nixpkgs branch to use", + "setting.update_command.label": "Update command", + "setting.update_command.description": "Command to run when clicking Update button", + "setting.clean_command.label": "Clean command", + "setting.clean_command.description": "Command to run when clicking Clean button", + "setting.close_on_enter.label": "Close on press Enter", + "setting.close_on_enter.description": "Add 'Press enter to exit' on the terminal window", + "setting.option.branch.master": "Master", + "setting.option.branch.nixos_unstable": "NixOS Unstable", + "setting.option.branch.nixos_unstable_small": "NixOS Unstable Small", + "setting.option.branch.nixos_25_11": "NixOS 25.11", + "setting.option.branch.nixos_25_05": "NixOS 25.05", + "setting.option.branch.nixos_24_11": "NixOS 24.11", + "setting.option.branch.nixos_24_05": "NixOS 24.05", + "setting.option.branch.nixos_23_11": "NixOS 23.11", + "setting.option.branch.nixos_23_05": "NixOS 23.05", + "setting.option.branch.nixos_25_11_small": "NixOS 25.11 Small", + "setting.option.branch.nixos_25_05_small": "NixOS 25.05 Small", + "setting.option.branch.nixos_24_11_small": "NixOS 24.11 Small", + "setting.option.branch.nixos_24_05_small": "NixOS 24.05 Small", + "setting.option.branch.nixos_23_11_small": "NixOS 23.11 Small", + "setting.option.branch.nixos_23_05_small": "NixOS 23.05 Small" +}