diff --git a/nix-monitor/README.md b/nix-monitor/README.md new file mode 100644 index 0000000..c251f59 --- /dev/null +++ b/nix-monitor/README.md @@ -0,0 +1,7 @@ +# Nix Monitor + +![thumbnail](thumbnail.webp) + +*Nix Monitor* checks Nixpkgs update by comparing local nix hash and remote nixpkgs's hash. It also can run update and clean command. + +No AI is used here but this is my first time writing Luau and plugin in general, so expect some bug. PRs are welcomed! diff --git a/nix-monitor/nix_monitor.luau b/nix-monitor/nix_monitor.luau new file mode 100644 index 0000000..b65d110 --- /dev/null +++ b/nix-monitor/nix_monitor.luau @@ -0,0 +1,55 @@ + +function tr(key) + return noctalia.tr(key) +end + +function barCfg(key) + return barWidget.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 barCfg("colorize_text") then + barWidget.setColor(color) + end + + if barCfg("colorize_glyph") then + barWidget.setGlyphColor(color) + end + + if barCfg("show_glyph") then + barWidget.setGlyph(glyph) + end + + if barCfg("show_text") then + barWidget.setText(text) + end +end + +function update() + if getState("isRunning") then + displayWidget(barCfg("checking_glyph"), tr("bar.checking"), barCfg("checking_color")) + else + if getState("isUpdateAvailable") then + displayWidget(barCfg("update_available_glyph"), tr("bar.update_available"), barCfg("update_available_color")) + else + if getState("localHash") == "n/a" or getState("remoteHash") == "n/a" then + displayWidget(barCfg("unknown_glyph"), tr("bar.unknown"), barCfg("unknown_color")) + else + displayWidget(barCfg("up_to_date_glyph"), tr("bar.up_to_date"), barCfg("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..a12fc66 --- /dev/null +++ b/nix-monitor/nix_monitor_service.luau @@ -0,0 +1,111 @@ +noctalia.setUpdateInterval(100) + +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 branch = cfg("branch") +local updateCheckInterval = cfg("check_interval") +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 completedCommands = 0 + +setState("lastCheckTime", 0) +setState("lastCheckTimeStr", "n/a") +setState("localHash", "n/a") +setState("remoteHash", "n/a") +setState("isRunning", false) +setState("isUpdateAvailable", false) + +function update() + if (os.time() - getState("lastCheckTime")) >= 60 * updateCheckInterval then + checkUpdate() + end +end + +function checkUpdate() + if getState("isRunning") then + return + end + + setState("lastCheckTime", os.time()) + noctalia.runAsync("date", function(date) setState("lastCheckTimeStr", date.stdout) end) + + if cfg("show_checking_notification") then + noctalia.notify(tr("notification.checking_update")) + end + + setState("isRunning", true) + + completedCommands = 0 + + noctalia.runAsync(localHashCmd, getLocalHash) + noctalia.runStream(remoteHashCmd, getRemoteHash) +end + +function getLocalHash(result) + if getState("isRunning") ~= true then + return + end + + setState("localHash", result.stdout) + completedCommands += 1 + onHashCheckCompleted() +end + +function getRemoteHash(line) + if getState("isRunning") ~= true then + return + end + + setState("remoteHash", line) + completedCommands += 1 + onHashCheckCompleted() +end + +function onHashCheckCompleted() + if completedCommands < 2 then + return + end + + if getState("localHash") ~= getState("remoteHash") then + setState("isUpdateAvailable", true) + if cfg("show_update_notification") then + noctalia.notify(tr("notification.update_is_available")) + end + end + + if cfg("show_checking_notification") then + noctalia.notify(tr("notification.update_check_completed")) + end + + setState("isRunning", false) +end + +watchState("command", function(command) + if command == "check" then + checkUpdate() + end + + if command == "cancel" then + noctalia.runAsync(`kill $(ps -ef | grep "git ls-remote https://github.com/NixOS/nixpkgs" | grep -v grep | awk '\{print $2\}')`) + setState("isRunning", false) + completedCommands = 0 + end +end) diff --git a/nix-monitor/panel.luau b/nix-monitor/panel.luau new file mode 100644 index 0000000..2704d31 --- /dev/null +++ b/nix-monitor/panel.luau @@ -0,0 +1,186 @@ +function tr(key) + return noctalia.tr(key) +end + +function barCfg(key) + return barWidget.getConfig(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 render() + local firstBtn = btnCheck + + if getState("isRunning") 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("lastCheckTimeStr") + + 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.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 + +watchState("isRunning", render) +watchState("lastCheckTimeStr", render) +watchState("localHash", render) +watchState("remoteHash", render) + +function onOpen(_ctx) + render() +end + +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..4e34514 --- /dev/null +++ b/nix-monitor/plugin.toml @@ -0,0 +1,164 @@ +id = "avivbintangaringga/nix-monitor" +name = "Nix Monitor" +version = "1.0.0" +min_noctalia = "5.0.0" +author = "avivbintangaringga" +description = "Nixpkgs update monitor" +tags = [ "nix", "utility" ] +dependencies = [ "nixos-version", "git", "pkill" ] + +[[widget]] +id = "Nix Monitor" +entry = "nix_monitor.luau" + +[[widget.setting]] +key = "show_text" +type = "boolean" +label_key = "setting.widget.show_text.label" +default = true + +[[widget.setting]] +key = "colorize_text" +type = "boolean" +label_key = "setting.widget.colorize_text.label" +default = false + +[[widget.setting]] +key = "show_glyph" +type = "boolean" +label_key = "setting.widget.show_glyph.label" +default = true + +[[widget.setting]] +key = "colorize_glyph" +type = "boolean" +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 = "check_interval" +type = "int" +label_key = "setting.check_interval.label" +description_key = "setting.check_interval.description" +default = 60 +min = 10 + +[[setting]] +key = "show_checking_notification" +type = "boolean" +label_key = "setting.checking_notification.label" +description_key = "setting.checking_notification.description" +default = false + +[[setting]] +key = "show_update_notification" +type = "boolean" +label_key = "setting.show_update_notification.label" +description_key = "setting.show_update_notification.description" +default = true + +[[setting]] +key = "branch" +type = "select" +label_key = "setting.branch.label" +description_key = "setting.branch.description" +options = [ + { value = "master", label = "Master" }, + { value = "nixos-unstable", label = "NixOS Unstable" }, + { value = "nixos-unstable-small", label = "NixOS Unstable Small" }, + { value = "nixos-25.11", label = "NixOS 25.11" }, + { value = "nixos-25.05", label = "NixOS 25.05" }, + { value = "nixos-24.11", label = "NixOS 24.11" }, + { value = "nixos-24.05", label = "NixOS 24.05" }, + { value = "nixos-23.11", label = "NixOS 23.11" }, + { value = "nixos-23.05", label = "NixOS 23.05" }, + { value = "nixos-25.11-small", label = "NixOS 25.11 Small" }, + { value = "nixos-25.05-small", label = "NixOS 25.05 Small" }, + { value = "nixos-24.11-small", label = "NixOS 24.11 Small" }, + { value = "nixos-24.05-small", label = "NixOS 24.05 Small" }, + { value = "nixos-23.11-small", label = "NixOS 23.11 Small" }, + { value = "nixos-23.05-small", label = "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 = "nh clean all" + +[[setting]] +key = "close_on_enter" +type = "boolean" +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 = 290 + +[[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..a3137bd 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..4f91e91 --- /dev/null +++ b/nix-monitor/translations/en.json @@ -0,0 +1,46 @@ +{ + "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", + "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.check_interval.label": "Update check interval", + "setting.check_interval.description": "How often to check for updates (in minutes)", + "setting.checking_notification.label": "Show checking update notification", + "setting.checking_notification.description": "Wether to show a notification whenever checking is in progress", + "setting.show_update_notification.label": "Show update notification", + "setting.show_update_notification.description": "Wether to show a notification whenever 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" +}