From fb5dd0506e31ecf7bd6c360d99a0952be9301fa9 Mon Sep 17 00:00:00 2001 From: 3ri4nG0ld Date: Mon, 3 Aug 2026 02:25:35 +0000 Subject: [PATCH] feat: add ip-monitor plugin (#207) * Add ip-monitor plugin * fix: removed typo * feat: increase default refresh interval to 60 seconds * fix: prevent shell injection in IP monitor gateway lookup. * docs: fix author casing in README * feat: add configurable click actions * feat: add desktop widget support and refactor core logic - Extracted IP fetching and data parsing into a shared `utils.luau` module to prevent code duplication. - Created `desktop.luau` to display network details explicitly in a tabulated UI. - Added specific configuration options for font sizes and colors for the new desktop widget * feat: add hidden_fields configuration to allow filtering tooltip and desktop widget details * fix: remove raw HTML from README * refactor(utils): use bit32 and fix glob pattern escaping & gateway parsing * refactor(utils): centralize shared UI builder and display name resolution * refactor(widget): use shared utils helpers and clean up duplicate logic * refactor(desktop): use shared utils helpers and add IPC click action parity * docs: update README with desktop widget settings * feat: remove unused IPC copy actions from widget logic --- ip-monitor/README.md | 120 +++++++++++++ ip-monitor/desktop.luau | 157 +++++++++++++++++ ip-monitor/plugin.toml | 298 ++++++++++++++++++++++++++++++++ ip-monitor/thumbnail.webp | Bin 0 -> 55574 bytes ip-monitor/translations/en.json | 110 ++++++++++++ ip-monitor/utils.luau | 262 ++++++++++++++++++++++++++++ ip-monitor/widget.luau | 176 +++++++++++++++++++ 7 files changed, 1123 insertions(+) create mode 100644 ip-monitor/README.md create mode 100644 ip-monitor/desktop.luau create mode 100644 ip-monitor/plugin.toml create mode 100644 ip-monitor/thumbnail.webp create mode 100644 ip-monitor/translations/en.json create mode 100644 ip-monitor/utils.luau create mode 100644 ip-monitor/widget.luau diff --git a/ip-monitor/README.md b/ip-monitor/README.md new file mode 100644 index 0000000..9e53ee8 --- /dev/null +++ b/ip-monitor/README.md @@ -0,0 +1,120 @@ +# IP Monitor + +A Noctalia bar and desktop widget to monitor network IPs. +It supports fetching IPs from network interfaces, custom commands, or via IPC. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `3ri4ng0ld/ip-monitor` | +| Entries | Bar widget: `widget`, Desktop widget: `desktop` | + +## Requirements + +- `ip` (part of `iproute2`): Required to resolve IP and gateway in Interface mode. +- `curl`: Required if using the default custom command to fetch public IPs. + +## Settings + +### Bar Widget Settings (`widget`) + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `glyph` | `glyph` | `network` | The icon glyph displayed on the widget | +| `glyph_color` | `color` | `on_surface` | Color of the icon | +| `mode` | `select` | `interface` | Operating mode: Interface, Custom Command, or IPC | +| `iface` | `string` | `wlan*` | Network interface wildcard to fetch IP from | +| `custom_command` | `string` | `curl -s ifconfig.me` | Shell command to execute in custom command mode | +| `text_color` | `color` | `on_surface` | Color of the IP text | +| `name` | `string` | `""` | A custom name to display alongside the IP | +| `ipc_id` | `string` | `default` | Identifier used to target this specific widget via IPC | +| `name_color` | `color` | `on_surface` | Color of the name text | +| `separator` | `string` | `-` | Separator symbol between IP and Name | +| `separator_color` | `color` | `on_surface` | Color of the separator | +| `hide_on_empty` | `boolean` | `true` | Hide the widget completely if no IP is found | +| `refresh_interval` | `int` | `60` | Refresh interval in seconds (max 3600) | +| `hidden_fields` | `string` | `""` | Comma-separated fields to hide in tooltip (e.g. `network,mask`). Options: `iface`, `ip`, `network`, `gateway`, `mask`, `broadcast` | +| `left_click_action` | `select` | `copy_ip` | Action to perform on left click (Copy IP, Copy Name, None) | +| `right_click_action` | `select` | `copy_name` | Action to perform on right click (Copy IP, Copy Name, None) | + +### Desktop Widget Settings (`desktop`) + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `glyph` | `glyph` | `network` | The icon glyph displayed on the widget | +| `glyph_color` | `color` | `on_surface` | Color of the icon | +| `mode` | `select` | `interface` | Operating mode: Interface, Custom Command, or IPC | +| `iface` | `string` | `wlan*` | Network interface wildcard to fetch IP from | +| `custom_command` | `string` | `curl -s ifconfig.me` | Shell command to execute in custom command mode | +| `text_color` | `color` | `on_surface` | Color of the IP text | +| `name` | `string` | `""` | A custom name to display alongside the IP | +| `ipc_id` | `string` | `default` | Identifier used to target this specific widget via IPC | +| `name_color` | `color` | `on_surface` | Color of the name text | +| `separator` | `string` | `-` | Separator symbol between IP and Name | +| `separator_color` | `color` | `on_surface` | Color of the separator | +| `hide_on_empty` | `boolean` | `true` | Hide the widget completely if no IP is found | +| `refresh_interval` | `int` | `60` | Refresh interval in seconds (max 3600) | +| `ip_font_size` | `int` | `32` | Font size for the IP and name text | +| `details_font_size` | `int` | `16` | Font size for the network details text | +| `details_key_color` | `color` | `primary` | Color for detail label keys (e.g. Network, Mask) | +| `details_value_color` | `color` | `on_surface` | Color for detail values | +| `hidden_fields` | `string` | `""` | Comma-separated fields to hide in details table (e.g. `network,mask`). Options: `iface`, `ip`, `network`, `gateway`, `mask`, `broadcast` | + +## Usage + +Add the widget to a bar from *Settings → Bar*. Plugin options live in *Settings → Plugins*. + +### Modes + +1. **Interface**: Fetches the IP from a local network interface. The `Interface` setting supports wildcards, for example `wlan*` or `eth*`, and will select the first matching interface that has a valid IP address. +2. **Custom Command**: Runs a shell command to fetch the IP. For example, `curl -s ifconfig.me` for the public IP. +3. **IPC**: Listens to external events to set the IP and name. + +### IPC + +The widget listens to the `set` event. By default, the widget is assigned the IPC ID `default`. To set the IP and name for a default widget, use the following command (you don't need to specify an ID in the JSON, it defaults to `default`): + +```sh +noctalia msg plugin 3ri4ng0ld/ip-monitor:widget all set '{"ip":"192.168.1.5","name":"MyIP"}' +``` + +If you have multiple `ip-monitor` widgets in IPC mode and want to update them independently, you can change the **IPC ID** setting for each widget. Then, target them by passing their ID string in the JSON payload: + +```sh +noctalia msg plugin 3ri4ng0ld/ip-monitor:widget all set '{"id":"my_vpn","ip":"100.64.0.1","name":"VPN"}' +noctalia msg plugin 3ri4ng0ld/ip-monitor:widget all set '{"id":"my_local_iface","ip":"192.168.1.5","name":"Ethernet"}' +``` + +### Tooltips + +Hovering over the widget displays detailed network information if available: +- **Interface**: Network interface name (e.g. `eth0`) +- **IP**: IPv4 address +- **Network**: Network range (e.g. `192.168.1.0/24`) +- **Default route**: Gateway address (e.g. `192.168.1.1`) +- **Mask**: Subnet mask (e.g. `255.255.255.0`) +- **Broadcast**: Broadcast address (e.g. `192.168.1.255`) + +> [!TIP] +> You can selectively hide any of these fields by listing their internal keys (`iface, ip, network, gateway, mask, broadcast`) separated by commas in the **Hide Tooltip Options** (for the bar widget) or **Hide Details Options** (for the desktop widget) configuration. + +In **IPC Mode**, tooltip fields can be passed optionally in the JSON payload (either as top-level keys or nested inside a `tooltip` object): + +```sh +noctalia msg plugin 3ri4ng0ld/ip-monitor:widget all set '{ + "id": "my_vpn", + "ip": "100.64.0.2", + "name": "Tailscale", + "iface": "tailscale0", + "network": "100.64.0.0/10", + "gateway": "100.64.0.1", + "mask": "255.192.0.0" +}' +``` + +### Click Actions + +By default, left-clicking the widget copies the IP address to the clipboard and right-clicking copies the display name. + +These bindings can be changed or disabled from the **Settings** section in the widget's bar settings. diff --git a/ip-monitor/desktop.luau b/ip-monitor/desktop.luau new file mode 100644 index 0000000..3e156a4 --- /dev/null +++ b/ip-monitor/desktop.luau @@ -0,0 +1,157 @@ +------------------------------------------------------------------------------- +-- IP Monitor Desktop Widget for Noctalia +-- +-- Monitors network IP addresses and displays details on the desktop. +------------------------------------------------------------------------------- + +local utils = require("./utils.luau") + +-- Current runtime state +local currentIp = "" +local currentIpcName = "" +local currentTooltip = nil + +------------------------------------------------------------------------------- +-- HELPER FUNCTIONS +------------------------------------------------------------------------------- + +--- Resolves the active display name according to active mode and IPC state. +local function getDisplayName() + return utils.getDisplayName(noctalia.getConfig("mode"), currentIpcName, noctalia.getConfig("name")) +end + +local function copyNotifyAndLog(text, optionKey, logMessage) + if text and text ~= "" then + noctalia.copyToClipboard(text, "text/plain") + noctalia.notify(noctalia.tr("name"), noctalia.tr(optionKey) .. ": " .. text) + noctalia.log("ip-monitor: " .. logMessage .. ": " .. text) + end +end + +local function applyAction(action) + if action == "copy_ip" then + copyNotifyAndLog(currentIp, "settings.click.options.copy_ip", "copied IP to clipboard") + elseif action == "copy_name" then + copyNotifyAndLog(getDisplayName(), "settings.click.options.copy_name", "copied name to clipboard") + end +end + +------------------------------------------------------------------------------- +-- UI RENDERING +------------------------------------------------------------------------------- + +--- Renders the desktop widget UI based on current state and user settings. +local function render() + local hideOnEmpty = noctalia.getConfig("hide_on_empty") + if hideOnEmpty == nil then hideOnEmpty = true end + + -- Omit rendering completely if no IP is present and hide_on_empty is active + if hideOnEmpty and (not currentIp or currentIp == "") then + desktopWidget.render(ui.row({}, {})) + return + end + + local ipFontSize = noctalia.getConfig("ip_font_size") or 32 + local detailsFontSize = noctalia.getConfig("details_font_size") or 16 + + local displayName = getDisplayName() + local topRowContent = utils.buildTopRow(currentIp, displayName, ipFontSize) + + local content = {} + table.insert(content, ui.row({ gap = 8, align = "center", justify = "center" }, topRowContent)) + + -- Add Details + if currentTooltip then + local hiddenConfig = noctalia.getConfig("hidden_fields") + local hiddenFields = utils.parseHiddenFields(hiddenConfig) + + local colKeys = {} + local colValues = {} + local fields = { "iface", "ip", "network", "gateway", "mask", "broadcast" } + for _, field in ipairs(fields) do + if not hiddenFields[field] then + local val = currentTooltip[field] + if val and val ~= "" then + table.insert(colKeys, ui.label({ + text = noctalia.tr("tooltip." .. field), + fontSize = detailsFontSize, + color = noctalia.getConfig("details_key_color") or "primary" + })) + table.insert(colValues, ui.label({ + text = val, + fontSize = detailsFontSize, + color = noctalia.getConfig("details_value_color") or "on_surface" + })) + end + end + end + + if #colKeys > 0 then + table.insert(content, ui.row({ gap = 12, align = "center", justify = "center" }, { + ui.column({ gap = 4, align = "start" }, colKeys), + ui.column({ gap = 4, align = "start" }, colValues) + })) + end + end + + desktopWidget.render(ui.column({ gap = 12, align = "center" }, content)) +end + +------------------------------------------------------------------------------- +-- LIFECYCLE HOOKS +------------------------------------------------------------------------------- + +--- Handler for IPC messages dispatched to this widget. +function onIpc(event, payload) + if noctalia.getConfig("mode") ~= "ipc" then return end + + if event == "set" then + local myId = noctalia.getConfig("ipc_id") or "default" + local data = utils.handleIpcPayload(payload, myId) + if data then + currentIp = data.ip + currentIpcName = data.name + currentTooltip = data.tooltip + render() + end + end +end + +--- Primary update entry point called periodically by Noctalia script runtime. +function update() + local mode = noctalia.getConfig("mode") + if mode == "interface" then + utils.fetchInterfaceIp(noctalia.getConfig("iface"), function(ip, tooltip) + currentIp = ip + currentTooltip = tooltip + render() + end) + elseif mode == "custom_command" then + utils.fetchCustomCommandIp(noctalia.getConfig("custom_command"), function(ip, tooltip) + currentIp = ip + currentTooltip = tooltip + render() + end) + end +end + +--- Config change handler called automatically when user alters widget settings. +function onConfigChanged() + local mode = noctalia.getConfig("mode") + currentIp = "" + currentIpcName = "" + currentTooltip = nil + + if mode == "interface" or mode == "custom_command" then + local interval = noctalia.getConfig("refresh_interval") or 60 + noctalia.setUpdateInterval(interval * 1000) + update() + else + -- IPC mode doesn't need periodic updates + noctalia.setUpdateInterval(0) + render() + end +end + +-- Initial setup upon loading widget +onConfigChanged() diff --git a/ip-monitor/plugin.toml b/ip-monitor/plugin.toml new file mode 100644 index 0000000..2e106e6 --- /dev/null +++ b/ip-monitor/plugin.toml @@ -0,0 +1,298 @@ +# ip-monitor — monitor network IPs + +id = "3ri4ng0ld/ip-monitor" +name = "IP Monitor" +version = "0.0.1" +plugin_api = 3 +author = "3ri4nG0ld" +license = "MIT" +deprecated = false +icon = "network" +description = "Bar widget to monitor IPs from interfaces, custom commands, or IPC." +tags = ["network"] +dependencies = ["ip", "curl"] + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "mode" + type = "select" + label_key = "settings.mode.label" + description_key = "settings.mode.description" + default = "interface" + options = [ + { value = "interface", label_key = "settings.mode.options.interface" }, + { value = "ipc", label_key = "settings.mode.options.ipc" }, + { value = "custom_command", label_key = "settings.mode.options.custom_command" }, + ] + + [[widget.setting]] + key = "iface" + type = "string" + label_key = "settings.iface.label" + description_key = "settings.iface.description" + default = "wlan*" + visible_when = { key = "mode", values = ["interface"] } + + [[widget.setting]] + key = "custom_command" + type = "string" + label_key = "settings.custom_command.label" + description_key = "settings.custom_command.description" + default = "curl -s ifconfig.me" + visible_when = { key = "mode", values = ["custom_command"] } + + [[widget.setting]] + key = "ipc_id" + type = "string" + label_key = "settings.ipc_id.label" + description_key = "settings.ipc_id.description" + default = "default" + visible_when = { key = "mode", values = ["ipc"] } + + [[widget.setting]] + key = "refresh_interval" + type = "int" + label_key = "settings.refresh_interval.label" + description_key = "settings.refresh_interval.description" + default = 60 + min = 10 + max = 3600 + visible_when = { key = "mode", values = ["interface", "custom_command"] } + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + description_key = "settings.glyph.description" + default = "network" + + [[widget.setting]] + key = "glyph_color" + type = "color" + label_key = "settings.glyph_color.label" + description_key = "settings.glyph_color.description" + default = "on_surface" + + [[widget.setting]] + key = "text_color" + type = "color" + label_key = "settings.text_color.label" + description_key = "settings.text_color.description" + default = "on_surface" + + [[widget.setting]] + key = "name" + type = "string" + label_key = "settings.name.label" + description_key = "settings.name.description" + default = "" + visible_when = { key = "mode", values = ["interface", "custom_command"] } + + [[widget.setting]] + key = "name_color" + type = "color" + label_key = "settings.name_color.label" + description_key = "settings.name_color.description" + default = "on_surface" + + [[widget.setting]] + key = "separator" + type = "string" + label_key = "settings.separator.label" + description_key = "settings.separator.description" + default = "-" + + [[widget.setting]] + key = "separator_color" + type = "color" + label_key = "settings.separator_color.label" + description_key = "settings.separator_color.description" + default = "on_surface" + + [[widget.setting]] + key = "hide_on_empty" + type = "bool" + label_key = "settings.hide_on_empty.label" + description_key = "settings.hide_on_empty.description" + default = true + + [[widget.setting]] + key = "left_click_action" + type = "select" + label_key = "settings.left_click_action.label" + description_key = "settings.left_click_action.description" + default = "copy_ip" + options = [ + { value = "copy_ip", label_key = "settings.click.options.copy_ip" }, + { value = "copy_name", label_key = "settings.click.options.copy_name" }, + { value = "none", label_key = "settings.click.options.none" }, + ] + + [[widget.setting]] + key = "right_click_action" + type = "select" + label_key = "settings.right_click_action.label" + description_key = "settings.right_click_action.description" + default = "copy_name" + options = [ + { value = "copy_ip", label_key = "settings.click.options.copy_ip" }, + { value = "copy_name", label_key = "settings.click.options.copy_name" }, + { value = "none", label_key = "settings.click.options.none" }, + ] + + [[widget.setting]] + key = "hidden_fields" + type = "string" + label_key = "settings.hide_tooltip_options.label" + description_key = "settings.hide_tooltip_options.description" + default = "" + + +[[desktop_widget]] +id = "desktop" +entry = "desktop.luau" + + [[desktop_widget.setting]] + key = "mode" + type = "select" + label_key = "settings.mode.label" + description_key = "settings.mode.description" + default = "interface" + options = [ + { value = "interface", label_key = "settings.mode.options.interface" }, + { value = "ipc", label_key = "settings.mode.options.ipc" }, + { value = "custom_command", label_key = "settings.mode.options.custom_command" }, + ] + + [[desktop_widget.setting]] + key = "iface" + type = "string" + label_key = "settings.iface.label" + description_key = "settings.iface.description" + default = "wlan*" + visible_when = { key = "mode", values = ["interface"] } + + [[desktop_widget.setting]] + key = "custom_command" + type = "string" + label_key = "settings.custom_command.label" + description_key = "settings.custom_command.description" + default = "curl -s ifconfig.me" + visible_when = { key = "mode", values = ["custom_command"] } + + [[desktop_widget.setting]] + key = "ipc_id" + type = "string" + label_key = "settings.ipc_id.label" + description_key = "settings.ipc_id.description" + default = "default" + visible_when = { key = "mode", values = ["ipc"] } + + [[desktop_widget.setting]] + key = "refresh_interval" + type = "int" + label_key = "settings.refresh_interval.label" + description_key = "settings.refresh_interval.description" + default = 60 + min = 10 + max = 3600 + visible_when = { key = "mode", values = ["interface", "custom_command"] } + + [[desktop_widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + description_key = "settings.glyph.description" + default = "network" + + [[desktop_widget.setting]] + key = "glyph_color" + type = "color" + label_key = "settings.glyph_color.label" + description_key = "settings.glyph_color.description" + default = "on_surface" + + [[desktop_widget.setting]] + key = "text_color" + type = "color" + label_key = "settings.text_color.label" + description_key = "settings.text_color.description" + default = "on_surface" + + [[desktop_widget.setting]] + key = "name" + type = "string" + label_key = "settings.name.label" + description_key = "settings.name.description" + default = "" + visible_when = { key = "mode", values = ["interface", "custom_command"] } + + [[desktop_widget.setting]] + key = "name_color" + type = "color" + label_key = "settings.name_color.label" + description_key = "settings.name_color.description" + default = "on_surface" + + [[desktop_widget.setting]] + key = "separator" + type = "string" + label_key = "settings.separator.label" + description_key = "settings.separator.description" + default = "-" + + [[desktop_widget.setting]] + key = "separator_color" + type = "color" + label_key = "settings.separator_color.label" + description_key = "settings.separator_color.description" + default = "on_surface" + + [[desktop_widget.setting]] + key = "hide_on_empty" + type = "bool" + label_key = "settings.hide_on_empty.label" + description_key = "settings.hide_on_empty.description" + default = true + + [[desktop_widget.setting]] + key = "ip_font_size" + type = "int" + label_key = "settings.ip_font_size.label" + description_key = "settings.ip_font_size.description" + default = 32 + min = 10 + max = 100 + + [[desktop_widget.setting]] + key = "details_font_size" + type = "int" + label_key = "settings.details_font_size.label" + description_key = "settings.details_font_size.description" + default = 16 + min = 10 + max = 100 + + [[desktop_widget.setting]] + key = "details_key_color" + type = "color" + label_key = "settings.details_key_color.label" + description_key = "settings.details_key_color.description" + default = "primary" + + [[desktop_widget.setting]] + key = "details_value_color" + type = "color" + label_key = "settings.details_value_color.label" + description_key = "settings.details_value_color.description" + default = "on_surface" + + [[desktop_widget.setting]] + key = "hidden_fields" + type = "string" + label_key = "settings.hide_details_options.label" + description_key = "settings.hide_details_options.description" + default = "" diff --git a/ip-monitor/thumbnail.webp b/ip-monitor/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..d6b84bd7130b703133f750c525288a7e0fdc6972 GIT binary patch literal 55574 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 w$k}l(Z0(zHwv|It#;8U_Xi0Dtd4iU0rr literal 0 HcmV?d00001 diff --git a/ip-monitor/translations/en.json b/ip-monitor/translations/en.json new file mode 100644 index 0000000..5e0bd25 --- /dev/null +++ b/ip-monitor/translations/en.json @@ -0,0 +1,110 @@ +{ + "name": "IP Monitor", + "description": "Bar widget to monitor IPs from interfaces, custom commands, or IPC.", + "tooltip": { + "iface": "Interface", + "ip": "IP", + "network": "Network", + "gateway": "Default route", + "mask": "Mask", + "broadcast": "Broadcast" + }, + "settings": { + "left_click_action": { + "label": "Left Click Action", + "description": "Action to perform on left click." + }, + "right_click_action": { + "label": "Right Click Action", + "description": "Action to perform on right click." + }, + "glyph": { + "label": "Icon", + "description": "Icon to show before the IP." + }, + "glyph_color": { + "label": "Icon Color", + "description": "Color of the icon." + }, + "mode": { + "label": "Mode", + "description": "How to fetch the IP.", + "options": { + "interface": "Network Interface", + "ipc": "IPC", + "custom_command": "Custom Command" + } + }, + "iface": { + "label": "Interface", + "description": "Pattern to match network interfaces (e.g. wlan*, eth0)." + }, + "custom_command": { + "label": "Custom Command", + "description": "Command to run to fetch the IP." + }, + "text_color": { + "label": "IP Text Color", + "description": "Color of the IP text." + }, + "name": { + "label": "Name", + "description": "Optional name to show next to the IP." + }, + "name_color": { + "label": "Name Text Color", + "description": "Color of the name text." + }, + "separator": { + "label": "Separator", + "description": "Character to separate the IP and the name (e.g. -)." + }, + "ipc_id": { + "label": "IPC ID", + "description": "String ID used to target this specific widget via IPC JSON payload." + }, + "separator_color": { + "label": "Separator Color", + "description": "Color of the separator." + }, + "hide_on_empty": { + "label": "Hide on Empty", + "description": "Hide the widget entirely when no IP is found." + }, + "refresh_interval": { + "label": "Refresh Interval", + "description": "How often to refresh the IP (in seconds)." + }, + "click": { + "options": { + "copy_ip": "Copy IP", + "copy_name": "Copy Name", + "none": "None" + } + }, + "ip_font_size": { + "label": "IP Font Size", + "description": "Font size for the IP and name text." + }, + "details_font_size": { + "label": "Details Font Size", + "description": "Font size for the network details text." + }, + "details_key_color": { + "label": "Details Key Color", + "description": "Color for the detail labels (e.g. Network, Mask)." + }, + "details_value_color": { + "label": "Details Value Color", + "description": "Color for the detail values." + }, + "hide_tooltip_options": { + "label": "Hide Tooltip Options", + "description": "Comma-separated list of fields to hide in the tooltip (e.g. network,mask,iface)." + }, + "hide_details_options": { + "label": "Hide Details Options", + "description": "Comma-separated list of fields to hide in the details table (e.g. network,mask,iface)." + } + } +} diff --git a/ip-monitor/utils.luau b/ip-monitor/utils.luau new file mode 100644 index 0000000..6485eb7 --- /dev/null +++ b/ip-monitor/utils.luau @@ -0,0 +1,262 @@ +------------------------------------------------------------------------------- +-- IP Monitor Shared Utilities +------------------------------------------------------------------------------- +local utils = {} + +--- Calculates IPv4 subnet mask string (e.g. "255.255.255.0") from a CIDR prefix length (0-32). +function utils.cidrToSubnetMask(prefix) + local n = tonumber(prefix) + if not n or n < 0 or n > 32 then return "" end + + local mask32 = (n == 0) and 0 or bit32.lshift(0xFFFFFFFF, 32 - n) + local o1 = bit32.rshift(mask32, 24) + local o2 = bit32.band(bit32.rshift(mask32, 16), 0xFF) + local o3 = bit32.band(bit32.rshift(mask32, 8), 0xFF) + local o4 = bit32.band(mask32, 0xFF) + + return string.format("%d.%d.%d.%d", o1, o2, o3, o4) +end + +--- Converts a simple glob pattern (e.g. "wlan*", "eth0") into a standard Lua pattern. +function utils.globToPattern(glob) + local escaped = glob:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1") + local pattern = escaped:gsub("%%%*", ".*") + return "^" .. pattern .. "$" +end + +--- Calculates network address (e.g. "192.168.1.0/24") from IP, Subnet Mask, and CIDR prefix. +function utils.calculateNetwork(ipStr, maskStr, cidrStr) + local a, b, c, d = ipStr:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") + local m1, m2, m3, m4 = maskStr:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") + if not (a and b and c and d and m1 and m2 and m3 and m4) then return "" end + + local net1 = bit32.band(tonumber(a), tonumber(m1)) + local net2 = bit32.band(tonumber(b), tonumber(m2)) + local net3 = bit32.band(tonumber(c), tonumber(m3)) + local net4 = bit32.band(tonumber(d), tonumber(m4)) + + return string.format("%d.%d.%d.%d/%s", net1, net2, net3, net4, cidrStr) +end + +--- Fetches IPv4 address and detailed network interface metadata. +function utils.fetchInterfaceIp(ifaceSetting, callback) + if not ifaceSetting or ifaceSetting == "" then + callback("", nil) + return + end + + local pattern = utils.globToPattern(ifaceSetting) + + noctalia.runAsync("ip -4 -o addr show", function(result) + if result.exitCode ~= 0 then + noctalia.log("ip-monitor: failed to get ip addr: " .. result.stderr) + callback("", nil) + return + end + + local matchedIface = nil + local foundIp = "" + local foundCidr = "" + local foundBrd = "" + + for line in result.stdout:gmatch("[^\r\n]+") do + local ifaceName, inetInfo = line:match("%d+:%s+([^%s]+)%s+inet%s+([^%s]+)") + if ifaceName and inetInfo and ifaceName:match(pattern) then + local ipOnly, cidr = inetInfo:match("([^/]+)/(%d+)") + if ipOnly then + matchedIface = ifaceName + foundIp = ipOnly + foundCidr = cidr or "" + foundBrd = line:match("%s+brd%s+([^%s]+)") or "" + break + end + end + end + + if not matchedIface or foundIp == "" then + callback("", nil) + return + end + + -- Calculate Mask and Network Range + local mask = utils.cidrToSubnetMask(foundCidr) + local network = "" + if foundIp ~= "" and mask ~= "" and foundCidr ~= "" then + network = utils.calculateNetwork(foundIp, mask, foundCidr) + end + + -- Fetch gateway + noctalia.runAsync("ip route show", function(routeResult) + local gateway = "" + if routeResult.exitCode == 0 then + -- 1. Try finding default route for matched interface + for line in routeResult.stdout:gmatch("[^\r\n]+") do + local lineDev = line:match("%sdev%s+([^%s]+)") + if lineDev == matchedIface then + local g = line:match("default via ([^%s]+)") + if g then + gateway = g + break + end + end + end + -- 2. Fallback: find any route with via for matched interface + if gateway == "" then + for line in routeResult.stdout:gmatch("[^\r\n]+") do + local lineDev = line:match("%sdev%s+([^%s]+)") + if lineDev == matchedIface then + local g = line:match("via ([^%s]+)") + if g then + gateway = g + break + end + end + end + end + end + + local tooltip = { + iface = matchedIface, + ip = foundIp, + network = network, + gateway = gateway, + mask = mask, + broadcast = foundBrd + } + callback(foundIp, tooltip) + end) + end) +end + +--- Fetches IP address by executing a custom shell command. +function utils.fetchCustomCommandIp(cmd, callback) + if not cmd or cmd == "" then + callback("", nil) + return + end + + noctalia.runAsync(cmd, function(result) + if result.exitCode ~= 0 then + noctalia.log("ip-monitor: custom command failed: " .. result.stderr) + callback("", nil) + return + end + + local newIp = noctalia.string.trim(result.stdout) + local tooltip = { ip = newIp } + callback(newIp, tooltip) + end) +end + +--- Parses an IPC payload and extracts data if it matches the configured ID. +function utils.handleIpcPayload(payload, myId) + local ok, data = pcall(noctalia.json.decode, payload) + if not ok or type(data) ~= "table" then return nil end + + local targetId = data.id or "default" + if targetId ~= myId then return nil end + + local tt = (type(data.tooltip) == "table") and data.tooltip or {} + local tooltip = { + iface = tt.iface or data.iface or data.interface or nil, + ip = tt.ip or data.ip or nil, + network = tt.network or data.network or nil, + gateway = tt.gateway or data.gateway or data.default_route or nil, + mask = tt.mask or data.mask or nil, + broadcast = tt.broadcast or data.broadcast or nil + } + + return { + ip = data.ip or "", + name = data.name or "", + tooltip = tooltip + } +end + +--- Parses a comma-separated string of hidden fields into a Set for O(1) lookups. +-- Also maps some common aliases (e.g., "net" to "network"). +function utils.parseHiddenFields(configStr) + local hidden = {} + if type(configStr) ~= "string" or configStr == "" then + return hidden + end + + local aliases = { + ["net"] = "network", + ["gw"] = "gateway", + ["def"] = "gateway", + ["route"] = "gateway" + } + + for field in configStr:gmatch("[^,]+") do + local cleanField = field:match("^%s*(.-)%s*$"):lower() + if cleanField ~= "" then + local actualField = aliases[cleanField] or cleanField + hidden[actualField] = true + end + end + return hidden +end + +--- Resolves the active display name according to the active mode and IPC state. +function utils.getDisplayName(mode, currentIpcName, staticName) + if mode == "ipc" and currentIpcName and currentIpcName ~= "" then + return currentIpcName + end + return staticName or "" +end + +--- Builds the top row UI components (Glyph, IP Label, Separator, Name Label). +-- @param currentIp string The IP address string +-- @param displayName string The resolved display name +-- @param fontSize integer|nil Optional font size for desktop widget +-- @return table List of UI component elements +function utils.buildTopRow(currentIp, displayName, fontSize) + local content = {} + + local glyph = noctalia.getConfig("glyph") + if glyph and glyph ~= "" then + local glyphOpts = { + name = glyph, + color = noctalia.getConfig("glyph_color") + } + if fontSize then + glyphOpts.size = fontSize + end + table.insert(content, ui.glyph(glyphOpts)) + end + + local hasIp = currentIp and currentIp ~= "" + local hasName = displayName and displayName ~= "" + + local function addLabel(text, colorKey) + if text and text ~= "" then + local labelOpts = { + text = text, + color = noctalia.getConfig(colorKey) + } + if fontSize then + labelOpts.fontSize = fontSize + end + table.insert(content, ui.label(labelOpts)) + end + end + + if hasIp then + addLabel(currentIp, "text_color") + end + + if hasIp and hasName then + local sep = noctalia.getConfig("separator") + if sep == nil then sep = "-" end + addLabel(sep, "separator_color") + end + + if hasName then + addLabel(displayName, "name_color") + end + + return content +end + +return utils diff --git a/ip-monitor/widget.luau b/ip-monitor/widget.luau new file mode 100644 index 0000000..7c34e7e --- /dev/null +++ b/ip-monitor/widget.luau @@ -0,0 +1,176 @@ +------------------------------------------------------------------------------- +-- IP Monitor Widget for Noctalia +-- +-- Monitors network IP addresses from network interfaces (with glob matching), +-- custom shell commands, or external IPC events. +------------------------------------------------------------------------------- + +local utils = require("./utils.luau") + +-- Current runtime state +local currentIp = "" +local currentIpcName = "" +local currentTooltip = nil + +------------------------------------------------------------------------------- +-- HELPER FUNCTIONS +------------------------------------------------------------------------------- + +--- Resolves the active display name according to active mode and IPC state. +local function getDisplayName() + return utils.getDisplayName(noctalia.getConfig("mode"), currentIpcName, noctalia.getConfig("name")) +end + +--- Updates the widget hover tooltip from the currentTooltip table. +local function updateTooltip() + if not currentTooltip then + barWidget.setTooltip(nil) + return + end + + local hiddenConfig = noctalia.getConfig("hidden_fields") + local hiddenFields = utils.parseHiddenFields(hiddenConfig) + + local rows = {} + local fields = { "iface", "ip", "network", "gateway", "mask", "broadcast" } + for _, field in ipairs(fields) do + if not hiddenFields[field] then + local val = currentTooltip[field] + if val and val ~= "" then + table.insert(rows, { key = noctalia.tr("tooltip." .. field), value = val }) + end + end + end + + if #rows > 0 then + barWidget.setTooltip(rows) + else + barWidget.setTooltip(nil) + end +end + +------------------------------------------------------------------------------- +-- UI RENDERING +------------------------------------------------------------------------------- + +--- Renders the bar widget UI based on current state and user settings. +local function render() + local hideOnEmpty = noctalia.getConfig("hide_on_empty") + if hideOnEmpty == nil then hideOnEmpty = true end + + -- Omit widget rendering completely if no IP is present and hide_on_empty is active + if hideOnEmpty and (not currentIp or currentIp == "") then + barWidget.setTooltip(nil) + barWidget.render(ui.row({}, {})) + return + end + + -- Update widget hover tooltip + updateTooltip() + + local displayName = getDisplayName() + local content = utils.buildTopRow(currentIp, displayName) + + -- Layout container according to bar orientation (Vertical -> column, Horizontal -> row) + local container = barWidget.isVertical() and ui.column or ui.row + barWidget.render(container({ gap = 6, align = "center" }, content)) +end + +------------------------------------------------------------------------------- +-- ACTIONS & HANDLERS +------------------------------------------------------------------------------- + +local function copyNotifyAndLog(text, optionKey, logMessage) + if text and text ~= "" then + noctalia.copyToClipboard(text, "text/plain") + noctalia.notify(noctalia.tr("name"), noctalia.tr(optionKey) .. ": " .. text) + noctalia.log("ip-monitor: " .. logMessage .. ": " .. text) + end +end + +--- Executes click / IPC actions (e.g. copy IP or copy Name to clipboard). +-- @param action string The action identifier ("copy_ip" or "copy_name") +local function applyClickAction(action) + if action == "copy_ip" then + copyNotifyAndLog(currentIp, "settings.click.options.copy_ip", "copied IP to clipboard") + elseif action == "copy_name" then + copyNotifyAndLog(getDisplayName(), "settings.click.options.copy_name", "copied name to clipboard") + end +end + +--- Handler for left click action on the bar widget. +function onClick() + local action = noctalia.getConfig("left_click_action") or "copy_ip" + if action ~= "none" then + applyClickAction(action) + end +end + +--- Handler for right click action on the bar widget. +function onRightClick() + local action = noctalia.getConfig("right_click_action") or "copy_name" + if action ~= "none" then + applyClickAction(action) + end +end + +--- Handler for IPC messages dispatched to this widget. +-- @param event string The IPC event name +-- @param payload string The JSON payload string +function onIpc(event, payload) + if noctalia.getConfig("mode") ~= "ipc" then return end + + if event == "set" then + local myId = noctalia.getConfig("ipc_id") or "default" + local data = utils.handleIpcPayload(payload, myId) + if data then + currentIp = data.ip + currentIpcName = data.name + currentTooltip = data.tooltip + render() + end + end +end + +------------------------------------------------------------------------------- +-- LIFECYCLE HOOKS +------------------------------------------------------------------------------- + +--- Primary update entry point called periodically by Noctalia script runtime. +function update() + local mode = noctalia.getConfig("mode") + if mode == "interface" then + utils.fetchInterfaceIp(noctalia.getConfig("iface"), function(ip, tooltip) + currentIp = ip + currentTooltip = tooltip + render() + end) + elseif mode == "custom_command" then + utils.fetchCustomCommandIp(noctalia.getConfig("custom_command"), function(ip, tooltip) + currentIp = ip + currentTooltip = tooltip + render() + end) + end +end + +--- Config change handler called automatically when user alters widget settings. +function onConfigChanged() + local mode = noctalia.getConfig("mode") + currentIp = "" + currentIpcName = "" + currentTooltip = nil + + if mode == "interface" or mode == "custom_command" then + local interval = noctalia.getConfig("refresh_interval") or 60 + noctalia.setUpdateInterval(interval * 1000) + update() + else + -- IPC mode doesn't need periodic updates + noctalia.setUpdateInterval(0) + render() + end +end + +-- Initial setup upon loading widget +onConfigChanged()