Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions ip-monitor/README.md
Original file line number Diff line number Diff line change
@@ -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.
157 changes: 157 additions & 0 deletions ip-monitor/desktop.luau
Original file line number Diff line number Diff line change
@@ -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()
Loading