From 57e4b108c44a30ae2449b50feb81fc8f512a5cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ar=C3=ADstides=20P=C3=A9rez?= Date: Wed, 5 Aug 2026 17:58:15 -0400 Subject: [PATCH] feat(udiskie): add aristides/udiskie plugin for usb/media drives management (#256) * feat(udiskie): add Udiskie service for real-time device monitoring, notifications, and IPC * feat(udiskie): add main manager panel for drives and partitions * feat(udiskie): add plugin manifest, status widget, and english translation * feat(udiskie): add Makefile for testing and linting translations * feat(udiskie): add error handling for missing udiskie package * feat(udiskie): add right-click functionality to refresh Udiskie service * feat(udiskie): add option to hide widget when no devices are connected * feat(udiskie): add settings button to the panel for quick access to configuration * feat(udiskie): add button to copy mount path to clipboard in partition row * fix(udiskie): duplicated notifications when removing or unmounting devices * feat(udiskie): add disk usage information for mounted partitions in device state * feat(udiskie): update partition row status colors and enhance UI for status display * fix(udiskie): adjust timeout to give time for sudo tasks such as LUKS unlock * feat(udiskie): update partition row status color for LUKS devices and adjust panel width * fix(udiskie): scroll not working properly and items overflow container * feat(udiskie): add missing translations and remove hardcoded strings messages * feat(udiskie): add README and thumbnail * docs(udiskie): add performance section to README with resource usage comparison * fix(udiskie): update output parsing to use safe tab delimiters for device information * fix(udiskie): improve device hierarchy parsing to correctly identify partitions * feat(udiskie): centralize open with file manager code, and close panel on auto-open on mount (like manually open) * fix(udiskie): exclude LUKS containers from "Drive Connected" notifications * fix(udiskie): quote paths and device names in shell commands * fix(udiskie): improve error message cleaning regex * feat(udiskie): enhance README with device attributes and update service to suppress errors * fix(udiskie): use shell quoting for device paths in panel async commands * feat(udiskie): add spanish translations and update tests to check every translation --- udiskie/Makefile | 18 ++ udiskie/README.md | 98 +++++++++ udiskie/panel.luau | 307 ++++++++++++++++++++++++++++ udiskie/plugin.toml | 70 +++++++ udiskie/service.luau | 263 ++++++++++++++++++++++++ udiskie/status.luau | 79 +++++++ udiskie/tests/check_translations.py | 82 ++++++++ udiskie/thumbnail.webp | Bin 0 -> 56792 bytes udiskie/translations/en.json | 69 +++++++ udiskie/translations/es.json | 69 +++++++ 10 files changed, 1055 insertions(+) create mode 100644 udiskie/Makefile create mode 100644 udiskie/README.md create mode 100644 udiskie/panel.luau create mode 100644 udiskie/plugin.toml create mode 100644 udiskie/service.luau create mode 100644 udiskie/status.luau create mode 100755 udiskie/tests/check_translations.py create mode 100644 udiskie/thumbnail.webp create mode 100644 udiskie/translations/en.json create mode 100644 udiskie/translations/es.json diff --git a/udiskie/Makefile b/udiskie/Makefile new file mode 100644 index 0000000..54e8dc7 --- /dev/null +++ b/udiskie/Makefile @@ -0,0 +1,18 @@ +SHELL := /bin/sh + +.PHONY: test translations lint + +test: translations lint + +translations: + @if command -v jq >/dev/null 2>&1; then \ + jq empty translations/en.json && echo "✓ translations/en.json is valid JSON"; \ + fi + python3 tests/check_translations.py + +lint: + @if command -v noctalia >/dev/null 2>&1; then \ + noctalia plugins lint . && echo "✓ Noctalia plugin lint passed"; \ + fi + python3 ../.github/workflows/scripts/validate-plugins.py && echo "✓ Official repo manifest validation passed" + git diff --check diff --git a/udiskie/README.md b/udiskie/README.md new file mode 100644 index 0000000..7feb5e8 --- /dev/null +++ b/udiskie/README.md @@ -0,0 +1,98 @@ +# Udiskie Manager + +Manage USB drives and media with notifications in just one panel. + +## Features + +- Real-time device discovery and status monitoring via UDisks2 event streaming. +- Storage capacity and partition usage indicators with formatted bytes and percentage. +- Automatic detection and dedicated unlock action for LUKS encrypted volumes (`crypto_LUKS`). +- Interactive management panel with drive hierarchy, open, mount, unmount, eject, and power off actions. +- Copy mount path to clipboard action. +- Configurable bar widget with mounted device counter and auto-hide when empty option. +- Native desktop notifications for drive connections, disconnections, mounts, unmounts, and errors. +- Quick refresh and native settings integration. + +## Plugin + +| Field | Value | +| ------- | ---------------------------------------------------------- | +| ID | `aristides/udiskie` | +| Entries | Bar widget: `status`; panel: `manager`; service: `service` | + +## Requirements + +Install `udiskie`, `udisks2`, and `xdg-open` on `PATH`: + +- `udiskie`: Device mount operations and device info queries. +- `udisks2`: Provides `udisksctl` for real-time DBus event streaming. +- `xdg-open`: Launcher binary for opening mounted folders in the file manager. + +> **`udiskie-info -o` fields**: the service queries `udiskie-info -o` with the +> device attributes listed under `VALID_PARAMETERS` (e.g. `is_drive`, +> `is_partition`, `is_luks`, `mount_path`, `is_detachable`). If your `udiskie` is +> old enough to lack any of them, the device list will come back empty — upgrade +> `udiskie` in that case. A recent release (2.x) is recommended. + +## Usage + +- **Bar Widget (`status`)**: Add `status` to the bar configuration in Noctalia settings. Left-click to open the Udiskie Manager Panel. Right-click to trigger an immediate plugin refresh. +- **Panel (`manager`)**: Open via the bar widget or run: + +```sh +noctalia msg panel-toggle aristides/udiskie:manager +``` + +## Settings + +| Setting | Type | Default | Description | +| ----------------------- | -------- | ------------ | ---------------------------------------------------------------------- | +| `enable_notifications` | `bool` | `true` | Show desktop notifications on drive events and errors. | +| `auto_open_filemanager` | `bool` | `false` | Automatically open mounted drives in the file manager upon connection. | +| `file_manager_cmd` | `string` | `xdg-open` | File manager launcher command. | +| `glyph` | `glyph` | `device-usb` | Icon glyph shown for the widget on the bar. | +| `show_count` | `bool` | `true` | Display the count of mounted devices on the bar widget. | +| `hide_when_empty` | `bool` | `false` | Hide the bar widget when no USB drives are connected. | + +## IPC + +```sh +# Toggle panel +noctalia msg panel-toggle aristides/udiskie:manager + +# Service actions +noctalia msg plugin aristides/udiskie:service all mount /dev/sdX +noctalia msg plugin aristides/udiskie:service all unmount /dev/sdX +noctalia msg plugin aristides/udiskie:service all eject /dev/sdX +noctalia msg plugin aristides/udiskie:service all detach /dev/sdX +noctalia msg plugin aristides/udiskie:service all mount_all +noctalia msg plugin aristides/udiskie:service all unmount_all +noctalia msg plugin aristides/udiskie:service all refresh +``` + +## Performance + +The plugin eliminates the standalone Python `udiskie` daemon by using an event-driven `udisksctl monitor` subprocess managed by Noctalia. Both approaches activate `udisksd` via D-Bus on first use. + +| Component | RSS | VSZ | CPU | +| ------------------------------------ | ---------- | -------- | ---- | +| `udisksctl monitor` (this plugin) | ~12 MB | ~170 MB | 0.0% | +| `udiskie` Python daemon (standalone) | ~84–110 MB | ~1183 MB | 0.2% | +| `udisksd` (shared, D-Bus activated) | ~22 MB | ~688 MB | 0.2% | + +**Total footprint**: plugin ~34 MB vs standalone ~106–132 MB. **Saves ~72–98 MB RAM** and avoids a persistent Python process. + +> Measurements are local to a given system/kernel and udiskie version; actual +> RSS/VSZ/CPU values vary by hardware and environment. + +## Development + +Run plugin validation and tests using the workspace Makefile: + +```sh +make test +``` + +## Notes + +- Requires plugin API level 9 or newer. diff --git a/udiskie/panel.luau b/udiskie/panel.luau new file mode 100644 index 0000000..2aa798f --- /dev/null +++ b/udiskie/panel.luau @@ -0,0 +1,307 @@ +--!nonstrict +-- Udiskie Manager Panel: Hierarchical Declarative UI panel for managing drives & partitions. + +local function shellQuote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function mountDevice(dev) + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all mount " .. shellQuote(dev)) + -- Close the panel when mounting if the file manager will auto-open. + if noctalia.getConfig("auto_open_filemanager") == true then + panel.close() + end +end + +local function unmountDevice(dev) + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all unmount " .. shellQuote(dev)) +end + +local function ejectDevice(dev) + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all eject " .. shellQuote(dev)) +end + +local function detachDevice(dev) + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all detach " .. shellQuote(dev)) +end + +local function openPath(path) + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all open " .. shellQuote(path)) + panel.close() +end + +local function getData() + local data = noctalia.state.get("udiskie_devices") + if type(data) ~= "table" then + return { drives = {}, standalone = {}, raw = {} } + end + return data +end + +local function formatSize(bytes) + local b = tonumber(bytes) + if not b or b <= 0 then return "" end + local units = { "B", "KB", "MB", "GB", "TB" } + local i = 1 + while b >= 1000 and i < #units do + b = b / 1000 + i = i + 1 + end + return string.format("%.1f %s", b, units[i]) +end + +local function renderPartitionRow(part) + local statusText = part.mounted and noctalia.tr("status_mounted") or (part.isLuks and noctalia.tr("status_locked") or noctalia.tr("status_unmounted")) + local statusColor = part.mounted and "primary" or (part.isLuks and "error" or "on_surface_variant") + local statusGlyph = part.mounted and "circle-check" or (part.isLuks and "lock" or "circle") + + local usageText = part.device + if part.mounted and part.usedSize and part.usedSize > 0 then + local usedStr = formatSize(part.usedSize) + if usedStr ~= "" then + local pct = math.floor((part.usedSize / (part.size > 0 and part.size or 1)) * 100) + usageText = usageText .. " · " .. usedStr .. " used (" .. tostring(pct) .. "%)" + end + end + + local actions = {} + if part.mounted then + if part.mountPath ~= "" then + table.insert(actions, ui.button({ + glyph = "copy", + tooltip = noctalia.tr("copy_path_tooltip"), + variant = "ghost", + controlSize = "sm", + onClick = function() + noctalia.copyToClipboard(part.mountPath, "text/plain;charset=utf-8") + noctalia.notify(noctalia.tr("copy_path_tooltip"), noctalia.tr("notify_path_copied")) + end, + })) + table.insert(actions, ui.button({ + text = noctalia.tr("btn_open"), + glyph = "folder", + variant = "ghost", + controlSize = "sm", + onClick = function() openPath(part.mountPath) end, + })) + end + table.insert(actions, ui.button({ + glyph = "player-stop", + tooltip = noctalia.tr("btn_unmount"), + variant = "ghost", + controlSize = "sm", + onClick = function() unmountDevice(part.device) end, + })) + else + if part.isLuks then + table.insert(actions, ui.button({ + text = noctalia.tr("btn_unlock"), + glyph = "lock-open", + variant = "ghost", + controlSize = "sm", + onClick = function() mountDevice(part.device) end, + })) + else + table.insert(actions, ui.button({ + text = noctalia.tr("btn_mount"), + glyph = "player-play", + variant = "ghost", + controlSize = "sm", + onClick = function() mountDevice(part.device) end, + })) + end + end + + return ui.row({ + align = "center", + justify = "space_between", + fill = "surface_variant/0.2", + radius = 6, + paddingV = 6, + paddingH = 8, + gap = 12, + height = 52, + }, { + ui.column({ gap = 2, flexGrow = 1, justify = "center" }, { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = statusGlyph, size = 14, color = statusColor }), + ui.label({ text = part.label, fontWeight = "bold", fontSize = 13, maxLines = 1 }), + ui.row({ + align = "center", + fill = statusColor .. "/0.15", + radius = 4, + paddingV = 1, + paddingH = 6, + }, { + ui.label({ text = statusText, color = statusColor, fontSize = 10, fontWeight = "medium" }), + }), + }), + ui.label({ + text = usageText, + color = "on_surface_variant", + fontSize = 11, + maxLines = 1, + }), + }), + ui.row({ gap = 4, align = "center" }, actions), + }) +end + +local function renderDriveCard(drvItem) + local drv = drvItem.drive + local parts = drvItem.partitions or {} + local sizeStr = formatSize(drv.size) + + local driveHeaderActions = { + ui.button({ + text = noctalia.tr("btn_eject"), + glyph = "player-eject", + variant = "outline", + controlSize = "sm", + onClick = function() ejectDevice(drv.device) end, + }), + ui.button({ + glyph = "plug-off", + tooltip = noctalia.tr("btn_poweroff"), + variant = "destructive", + controlSize = "sm", + onClick = function() detachDevice(drv.device) end, + }), + } + + local partRows = {} + if #parts == 0 then + table.insert(partRows, ui.label({ text = noctalia.tr("no_partitions_found"), color = "on_surface_variant", fontSize = 12 })) + else + for _, p in ipairs(parts) do + table.insert(partRows, renderPartitionRow(p)) + end + end + + return ui.column({ + fill = "surface_variant/0.3", + radius = 8, + padding = 12, + gap = 10, + }, { + -- Drive Header + ui.row({ align = "center", justify = "space_between" }, { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = "device-usb", size = 18, color = "primary" }), + ui.column({ gap = 2 }, { + ui.label({ text = drv.label, fontWeight = "bold", fontSize = 14, maxLines = 1 }), + ui.label({ + text = drv.device .. (sizeStr ~= "" and (" · " .. sizeStr) or ""), + color = "on_surface_variant", + fontSize = 11, + }), + }), + }), + ui.row({ gap = 4, align = "center" }, driveHeaderActions), + }), + ui.separator({ thickness = 1, color = "surface_variant/0.4" }), + -- Partition List + ui.column({ gap = 6 }, partRows), + }) +end + +local function renderHeaderRow() + return ui.row({ align = "center", paddingV = 4 }, { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = "device-usb", size = 20, color = "primary" }), + ui.label({ text = noctalia.tr("panel_title"), fontSize = 16, fontWeight = "bold" }), + }), + ui.spacer(), + ui.row({ gap = 6 }, { + ui.button({ + glyph = "settings", + variant = "ghost", + controlSize = "sm", + onClick = function() + noctalia.openSettings() + end, + }), + ui.button({ + text = noctalia.tr("btn_mount_all"), + glyph = "link", + variant = "secondary", + controlSize = "sm", + onClick = function() noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all mount_all") end, + }), + ui.button({ + glyph = "unlink", + tooltip = noctalia.tr("btn_unmount_all"), + variant = "destructive", + controlSize = "sm", + onClick = function() noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all unmount_all") end, + }), + }), + }) +end + +local function render() + local data = getData() + local children = {} + + table.insert(children, renderHeaderRow()) + table.insert(children, ui.separator({ thickness = 1, color = "surface_variant" })) + + local drives = data.drives or {} + local standalone = data.standalone or {} + + if data.error then + table.insert(children, ui.column({ align = "center", justify = "center", paddingV = 24, gap = 12 }, { + ui.glyph({ name = "alert-triangle", size = 36, color = "warning" }), + ui.label({ text = noctalia.tr("error_missing_dep"), color = "on_surface" }), + ui.row({ gap = 8, align = "center" }, { + ui.button({ + text = noctalia.tr("btn_retry"), + glyph = "refresh", + variant = "outline", + controlSize = "sm", + onClick = function() + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all refresh") + end, + }), + ui.button({ + text = noctalia.tr("btn_docs"), + glyph = "external-link", + variant = "ghost", + controlSize = "sm", + onClick = function() + noctalia.runAsync("xdg-open 'https://github.com/coldfix/udiskie/wiki/Installation'") + end, + }), + }), + })) + elseif #drives == 0 and #standalone == 0 then + table.insert(children, ui.column({ align = "center", justify = "center", paddingV = 24, gap = 8 }, { + ui.glyph({ name = "device-floppy", size = 32, color = "on_surface_variant/0.5" }), + ui.label({ text = noctalia.tr("panel_empty"), color = "on_surface_variant" }), + })) + else + local cardItems = {} + + for _, drvItem in ipairs(drives) do + table.insert(cardItems, renderDriveCard(drvItem)) + end + + for _, p in ipairs(standalone) do + table.insert(cardItems, renderPartitionRow(p)) + end + + table.insert(children, ui.scroll({ flexGrow = 1, gap = 10 }, cardItems)) + end + + panel.render(ui.column({ flexGrow = 1, gap = 12, padding = 16, align = "stretch" }, children)) +end + +function onOpen() + render() +end + +noctalia.state.watch("udiskie_devices", function() + render() +end) + +render() diff --git a/udiskie/plugin.toml b/udiskie/plugin.toml new file mode 100644 index 0000000..98bd490 --- /dev/null +++ b/udiskie/plugin.toml @@ -0,0 +1,70 @@ +# Udiskie Manager: bar widget, service, and panel for managing USB drives and media. +# A service monitors udiskie events in real time and publishes to shared state. +# The bar widget renders mount count and the panel provides full device actions. + +id = "aristides/udiskie" +name = "Udiskie Manager" +version = "0.1.0" +plugin_api = 9 +author = "aristides" +license = "MIT" +dependencies = ["udiskie", "udisks2", "xdg-open"] +tags = ["bar", "hardware", "panel", "service", "system", "utility"] +icon = "device-usb" +description = "Manage USB drives and media with notifications in just one panel." + +[[setting]] +key = "enable_notifications" +type = "bool" +label_key = "settings.enable_notifications.label" +description_key = "settings.enable_notifications.description" +default = true + +[[setting]] +key = "auto_open_filemanager" +type = "bool" +label_key = "settings.auto_open_filemanager.label" +description_key = "settings.auto_open_filemanager.description" +default = false + +[[setting]] +key = "file_manager_cmd" +type = "string" +label_key = "settings.file_manager_cmd.label" +description_key = "settings.file_manager_cmd.description" +default = "xdg-open" + +[[service]] +id = "service" +entry = "service.luau" + +[[widget]] +id = "status" +entry = "status.luau" + +[[widget.setting]] +key = "glyph" +type = "glyph" +label_key = "settings.glyph.label" +description_key = "settings.glyph.description" +default = "device-usb" + +[[widget.setting]] +key = "show_count" +type = "bool" +label_key = "settings.show_count.label" +description_key = "settings.show_count.description" +default = true + +[[widget.setting]] +key = "hide_when_empty" +type = "bool" +label_key = "settings.hide_when_empty.label" +description_key = "settings.hide_when_empty.description" +default = false + +[[panel]] +id = "manager" +entry = "panel.luau" +width = 600 +height = 450 diff --git a/udiskie/service.luau b/udiskie/service.luau new file mode 100644 index 0000000..236bff6 --- /dev/null +++ b/udiskie/service.luau @@ -0,0 +1,263 @@ +--!nonstrict +-- Udiskie service: monitors `udisksctl monitor` in real time, publishes device. +-- state to shared state (`udiskie_devices`), handles notifications, and processes IPC. +-- +-- External commands: udiskie-info, udiskie-mount, udiskie-umount, udisksctl, xdg-open. + +local prevMountedDevices = {} +local prevAllDevices = {} +local isInitialFetch = true + +local function isTrue(val) + return type(val) == "string" and val:lower() == "true" +end + +local function shellQuote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function openWithFileManager(path) + local cmd = noctalia.getConfig("file_manager_cmd") or "xdg-open" + noctalia.runAsync(cmd .. " " .. shellQuote(path)) +end + +local function parseUdiskieOutput(stdout) + local rawDevices = {} + local currentMounted = {} + + if not stdout then + return rawDevices, currentMounted + end + + for line in stdout:gmatch("[^\r\n]+") do + local dev, label, mounted, mountPath, isLuks, isDrive, isPartition, isFilesystem, isDetachable, isEjectable, inUse, deviceSize = line:match("^([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t(.*)$") + if dev ~= nil and dev ~= "" then + local cleanLabel = (label ~= nil and label ~= "") and label or dev + cleanLabel = cleanLabel:gsub("^[^:]+:%s*", "") + + local isM = isTrue(mounted) + local devObj = { + device = dev, + label = cleanLabel, + mounted = isM, + mountPath = mountPath or "", + isLuks = isTrue(isLuks), + isDrive = isTrue(isDrive), + isPartition = isTrue(isPartition), + isFilesystem = isTrue(isFilesystem), + isDetachable = isTrue(isDetachable), + isEjectable = isTrue(isEjectable), + inUse = isTrue(inUse), + size = tonumber(deviceSize) or 0, + } + table.insert(rawDevices, devObj) + if isM then + currentMounted[dev] = devObj.label + end + end + end + + return rawDevices, currentMounted +end + +local function buildHierarchy(rawDevices) + local drives = {} + local standalonePartitions = {} + + for _, d in ipairs(rawDevices) do + if not d.isPartition and not d.isFilesystem then + table.insert(drives, { drive = d, partitions = {} }) + end + end + + for _, d in ipairs(rawDevices) do + if d.isPartition or d.isFilesystem or d.isLuks then + local parentFound = false + for _, drvItem in ipairs(drives) do + if d.device:find("^" .. drvItem.drive.device .. "[%dp]") then + table.insert(drvItem.partitions, d) + parentFound = true + break + end + end + if not parentFound then + table.insert(standalonePartitions, d) + end + end + end + + return { + drives = drives, + standalone = standalonePartitions, + raw = rawDevices, + } +end + +local function processNotifications(rawDevices, currentMounted) + local enableNotifs = noctalia.getConfig("enable_notifications") + if enableNotifs == false then + return + end + + local currentAllDevs = {} + for _, d in ipairs(rawDevices) do + -- Physical drives only: LUKS containers are excluded so the "Drive + -- Connected" notification fires once for the underlying physical disk, + -- not again for the encrypted volume. + if not d.isLuks and (d.isDrive or (not d.isPartition and not d.isFilesystem)) then + currentAllDevs[d.device] = d.label + end + end + + -- 1. Device Insertion (Physical USB plugged in) + if not isInitialFetch then + for dev, label in pairs(currentAllDevs) do + if not prevAllDevices[dev] then + noctalia.notify(noctalia.tr("notif_drive_connected_title"), noctalia.tr("notif_drive_connected_body", { label = label })) + end + end + -- 2. Device Removal (Physical USB unplugged / powered off) + for dev, label in pairs(prevAllDevices) do + if not currentAllDevs[dev] then + noctalia.notify(noctalia.tr("notif_drive_removed_title"), noctalia.tr("notif_drive_removed_body", { label = label })) + end + end + end + + -- 3. Device Mounted + for dev, label in pairs(currentMounted) do + if not prevMountedDevices[dev] then + noctalia.notify(noctalia.tr("notif_drive_mounted_title"), noctalia.tr("notif_drive_mounted_body", { label = label })) + if noctalia.getConfig("auto_open_filemanager") == true then + for _, d in ipairs(rawDevices) do + if d.device == dev and d.mountPath ~= "" then + openWithFileManager(d.mountPath) + break + end + end + end + end + end + + -- 4. Device Unmounted + for dev, label in pairs(prevMountedDevices) do + if not currentMounted[dev] and currentAllDevs[dev] then + noctalia.notify(noctalia.tr("notif_drive_unmounted_title"), noctalia.tr("notif_drive_unmounted_body", { label = label })) + end + end + + prevAllDevices = currentAllDevs + prevMountedDevices = currentMounted + isInitialFetch = false +end + +local function fetchDevices() + noctalia.runAsync("udiskie-info -a -o \"{device_file}\t{ui_label}\t{is_mounted}\t{mount_path}\t{is_luks}\t{is_drive}\t{is_partition}\t{is_filesystem}\t{is_detachable}\t{is_ejectable}\t{in_use}\t{device_size}\"", function(res) + if not res or res.exitCode ~= 0 then + noctalia.state.set("udiskie_devices", { drives = {}, standalone = {}, raw = {}, error = "missing_dep" }) + return + end + + local rawDevices, currentMounted = parseUdiskieOutput(res.stdout) + local structured = buildHierarchy(rawDevices) + structured.error = nil + + processNotifications(rawDevices, currentMounted) + + -- Fetch df disk usage for mounted partitions + noctalia.runAsync("df -B1 --output=target,used 2>/dev/null", function(dfRes) + if dfRes and dfRes.exitCode == 0 and dfRes.stdout then + local usageMap = {} + for line in dfRes.stdout:gmatch("[^\r\n]+") do + local target, used = line:match("^(%S+)%s+(%d+)$") + if target and used then + usageMap[target] = tonumber(used) + end + end + + for _, d in ipairs(rawDevices) do + if d.mounted and d.mountPath ~= "" and usageMap[d.mountPath] then + d.usedSize = usageMap[d.mountPath] + end + end + end + + noctalia.state.set("udiskie_devices", structured) + end) + end, 5000) +end + +-- Initial fetch on startup +noctalia.state.set("udiskie_devices", {}) +fetchDevices() + +-- Event-driven streaming using udisksctl monitor +noctalia.runStream("udisksctl monitor 2>/dev/null", function(line) + if line:find("Added") or line:find("PropertiesChanged") or line:find("Removed") then + fetchDevices() + end +end) + +local function cleanError(stderr, fallback) + if not stderr or stderr == "" then + return fallback + end + -- upstream formats errors as "failed to : "). + local clean = stderr:gsub("failed to %w+ [^:]+:%s*", "") + -- Strip GDBus error identifiers like "GDBus.Error:org.freedesktop.UDisks2.Error.DeviceBusy:" + clean = clean:gsub("GDBus%.Error:[%w%.]+:%s*", "") + -- Clean leading/trailing whitespace + clean = clean:gsub("^%s*", ""):gsub("%s*$", "") + return clean ~= "" and clean or fallback +end + +-- IPC Handlers for Panel and Shortcuts +function onIpc(event, payload) + if event == "mount" and payload then + noctalia.runAsync("udiskie-mount -r " .. shellQuote(payload), function(res) + if res and res.exitCode ~= 0 and noctalia.getConfig("enable_notifications") ~= false then + noctalia.notifyError(noctalia.tr("notif_mount_failed_title"), cleanError(res.stderr, noctalia.tr("notif_mount_failed_fallback", { device = payload }))) + end + fetchDevices() + end, 30000) + elseif event == "mount_all" then + noctalia.runAsync("udiskie-mount -a", function(res) + if res and res.exitCode ~= 0 and noctalia.getConfig("enable_notifications") ~= false then + noctalia.notifyError(noctalia.tr("notif_mount_all_failed_title"), cleanError(res and res.stderr, noctalia.tr("notif_mount_all_failed_fallback"))) + end + fetchDevices() + end, 10000) + elseif event == "unmount" and payload then + noctalia.runAsync("udiskie-umount " .. shellQuote(payload), function(res) + if res and res.exitCode ~= 0 and noctalia.getConfig("enable_notifications") ~= false then + noctalia.notifyError(noctalia.tr("notif_unmount_failed_title"), cleanError(res.stderr, noctalia.tr("notif_unmount_failed_fallback", { device = payload }))) + end + fetchDevices() + end, 10000) + elseif event == "unmount_all" then + noctalia.runAsync("udiskie-umount -a", function(res) + if res and res.exitCode ~= 0 and noctalia.getConfig("enable_notifications") ~= false then + noctalia.notifyError(noctalia.tr("notif_unmount_all_failed_title"), cleanError(res and res.stderr, noctalia.tr("notif_unmount_all_failed_fallback"))) + end + fetchDevices() + end, 10000) + elseif event == "eject" and payload then + noctalia.runAsync("udiskie-umount -e " .. shellQuote(payload), function(res) + if res and res.exitCode ~= 0 and noctalia.getConfig("enable_notifications") ~= false then + noctalia.notifyError(noctalia.tr("notif_eject_failed_title"), cleanError(res.stderr, noctalia.tr("notif_eject_failed_fallback", { device = payload }))) + end + fetchDevices() + end, 10000) + elseif event == "detach" and payload then + noctalia.runAsync("udiskie-umount -d " .. shellQuote(payload), function(res) + if res and res.exitCode ~= 0 and noctalia.getConfig("enable_notifications") ~= false then + noctalia.notifyError(noctalia.tr("notif_poweroff_failed_title"), cleanError(res.stderr, noctalia.tr("notif_poweroff_failed_fallback", { device = payload }))) + end + fetchDevices() + end) + elseif event == "open" and payload then + openWithFileManager(payload) + elseif event == "refresh" then + fetchDevices() + end +end diff --git a/udiskie/status.luau b/udiskie/status.luau new file mode 100644 index 0000000..92c5460 --- /dev/null +++ b/udiskie/status.luau @@ -0,0 +1,79 @@ +--!nonstrict +-- Udiskie bar widget: renders the mounted-device count and a tooltip listing. +-- the mounted devices. State comes from the udiskie service via shared state. + +local function mountedDevices(data) + if type(data) ~= "table" or type(data.raw) ~= "table" then + return {} + end + local out = {} + for _, d in ipairs(data.raw) do + if d.mounted then + table.insert(out, d) + end + end + return out +end + +local function render() + local data = noctalia.state.get("udiskie_devices") + local mounted = mountedDevices(data) + local glyph = noctalia.getConfig("glyph") or "device-usb" + local showCount = noctalia.getConfig("show_count") + + barWidget.setGlyph(glyph) + + if type(data) == "table" and data.error then + barWidget.setGlyphColor("#f59e0b") + barWidget.setText("") + barWidget.setTooltip(noctalia.tr("error_missing_dep")) + return + else + -- Reset to the theme default. The API does not document "" as a reset; + -- today it clears the role/color. Revisit if the host ever rejects it. + barWidget.setGlyphColor("") + barWidget.setColor("") + end + + local hideWhenEmpty = noctalia.getConfig("hide_when_empty") == true + local rawCount = (type(data) == "table" and type(data.raw) == "table") and #data.raw or 0 + if hideWhenEmpty and rawCount == 0 then + barWidget.setVisible(false) + return + else + barWidget.setVisible(true) + end + + if #mounted == 0 then + barWidget.setText("") + barWidget.setTooltip(noctalia.tr("tooltip_empty")) + return + end + + if showCount == nil or showCount == true then + barWidget.setText(tostring(#mounted)) + else + barWidget.setText("") + end + + local rows = {} + for _, d in ipairs(mounted) do + table.insert(rows, d.label .. (d.mountPath ~= "" and (" (" .. d.mountPath .. ")") or "")) + end + barWidget.setTooltip(noctalia.tr("tooltip_mounted") .. "\n" .. table.concat(rows, "\n")) +end + +noctalia.state.watch("udiskie_devices", function() + render() +end) + +function onClick() + noctalia.togglePanel("aristides/udiskie:manager") +end + +function onRightClick() + noctalia.runAsync("noctalia msg plugin aristides/udiskie:service all refresh") +end + +render() + diff --git a/udiskie/tests/check_translations.py b/udiskie/tests/check_translations.py new file mode 100755 index 0000000..7a6994c --- /dev/null +++ b/udiskie/tests/check_translations.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import json +import os +import sys + +def main(): + plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + trans_dir = os.path.join(plugin_dir, "translations") + en_file = os.path.join(trans_dir, "en.json") + + if not os.path.exists(en_file): + print(f"Error: Translation file not found at {en_file}") + sys.exit(1) + + def flatten_keys(d, prefix=""): + keys = [] + for k, v in d.items(): + full_key = f"{prefix}.{k}" if prefix else k + if isinstance(v, dict): + keys.extend(flatten_keys(v, full_key)) + else: + keys.append(full_key) + return keys + + with open(en_file, "r", encoding="utf-8") as f: + en_translations = json.load(f) + + en_keys = set(flatten_keys(en_translations)) + errors = [] + + # Code and manifest files to scan (checked against en.json, the reference). + scan_files = ["plugin.toml", "service.luau", "status.luau", "panel.luau"] + combined_content = "" + + for fname in scan_files: + fpath = os.path.join(plugin_dir, fname) + if os.path.exists(fpath): + with open(fpath, "r", encoding="utf-8") as f: + combined_content += f.read() + "\n" + + # 1. Every en.json key must be used somewhere in the codebase. + missing_keys = [] + for key in sorted(en_keys): + # Setting label_key and description_key append .label and .description automatically. + base_key = key.replace(".label", "").replace(".description", "") + if key not in combined_content and base_key not in combined_content: + missing_keys.append(key) + + if missing_keys: + errors.append("Unused translation key(s) found in translations/en.json:\n - " + "\n - ".join(missing_keys)) + + # 2. Every other translation file must have exactly the same keys as en.json. + for fname in sorted(os.listdir(trans_dir)): + if not fname.endswith(".json") or fname == "en.json": + continue + fpath = os.path.join(trans_dir, fname) + with open(fpath, "r", encoding="utf-8") as f: + try: + other = json.load(f) + except json.JSONDecodeError as e: + errors.append(f"Invalid JSON in translations/{fname}: {e}") + continue + other_keys = set(flatten_keys(other)) + missing = sorted(en_keys - other_keys) + extra = sorted(other_keys - en_keys) + if missing: + errors.append(f"translations/{fname} is missing key(s):\n - " + "\n - ".join(missing)) + if extra: + errors.append(f"translations/{fname} has extra key(s) not in en.json:\n - " + "\n - ".join(extra)) + + if errors: + for e in errors: + print(e) + sys.exit(1) + + other_count = len([f for f in os.listdir(trans_dir) if f.endswith(".json") and f != "en.json"]) + print(f"✓ All {len(en_keys)} translation keys in translations/en.json are active and used in codebase.") + if other_count: + print(f"✓ {other_count} other translation file(s) match the en.json key set exactly.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/udiskie/thumbnail.webp b/udiskie/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..b44e3348676acf9abd3741411bca7773b7b65c59 GIT binary patch literal 56792 zcmWIYbaT6Kmw_SN)hQq>z(V23T?U2^3e0mEwGJ@LFxhc3H99m}F)#>BD_i+n_Rx>& z3p1e1-`lJ&oqGGT-HH7R&6Vrj`5ul0`@Qv*`)~f^{Xcu7{__8)pZ`94|IPlr zb$9>Q{#^Y({-6I(|GV?w)W_Q||3Be>^*`tD(J%PFI{$V5e*f0~d-Lb%-{Eih|H%H? z|66}2{NVia|JVCJ`5*W1tT&nevi^1bwC}lp&;Mcn)BoQ7wVh-AlK)9x_y0fnrud)u zKkreeZ|L^(t@|Tl;i~r{TGyiRU^}e?MiT3yEZq*<8`~Nq? z|CO&8|Jz?--g5hY_*ePE?{CGgy08Ch`;WPQlmBl2@xSuF-S77I`@8qw`G2i`+V4}J z!%yWO`}_Wv=%4Lx&tIKdhxj}7AM02B`u?W<@%R1p z$Llx!SN!qzZ~4Fb5BNXIKeoS8zvX||zrTM^|4;vAzt6tVW?sGN|MY+PzwI03AN@bG ze`|f=fAxRPzvKVwfB8Rg{_FaA{|*1G{@eb8|6l#ve^viZ{wx3U|Ht~z_3!QX)_?n7 z`HTBc_)q_D=imH)Vt=W=+h7mCI3r)-h7w+tLk6co7va@|HFChz}mV4 z$^MIT1kd`Mn$LB5X80v8_Kl`~S)8u}s%L~MTV4~b`>%EL9)JAL<0m)q%P_01<_O$r zYM`XPLFC>A^GmhmKOVEIs~DY~Qr@tKRb;j6<~c9+@@TF)>h>+IpW zc_yUXv=NJ!@LvCEQ+nUSMzKGJzuzxnURYQ2@b=f`d^^~kJSx(ro{vvE$34gVd3GS% zs*L;Tao%yuuKqCGbG1=K{`NXGn}~N!WttB%3)QE;v(xxzaoE<;W#Qr-e@-lV+3(_& zDYQr9;)Iyz`*~9HUhS3=h?m-<{)T&j>b7%#RE_WNtb250|03mMH~vY!*Z)%eu_VDf zx|x5wszCf2$?TkEO8XNM6O3|X9DmKduJK4D;o$`n|K@i(xBiqrml2wA>Cp7FOabRW zHI_f3@ju+9ABwY9_U^pXyy5@H1DgBx$fvICuswImQu~479J?Y3=~EglFP2H%e>Ic; z^V}m({M{Hbn)m$Jcv8JUNF?CI#hlEk)p{qpeM5DMvShA!EQ|~YxO>&qDK0K=T~oUK zMhyv@9}8Blvwpw&iGySvbLz?XKQ&^nPcZI#?X+QDY;W_)?jyduXAbk#tWsZj-eL!v z)cI5vtNjPB^zX~O_@{YBU%v6}ywr##TFL%uGSb%3XA<7dbd9#{nx%R6qW0&B>pISt zMTa#i&T}$1(cjA~{%c=-g1T{JRr8J+nKN$w&04?sgJBr|ui5djiC@2ZneUt*Q@*{# za#5hGz$61s!|uY3sW*d7Zo4i%pI*MIf%{&>X6KCClXCT!7v_o|lkq>%_K3qF&bIL2 zn$GxnMzzl$xqUns_$X}OugbeU(xNXIB#M2x7I)V8^CrdbxX;OOcdbTHe^RoT=~Ln6 zA_;@guP<}_ZR)z>;~)Jzb5Ufy9@A6Jhm258C%zfh8@x<`nyQoef?s~KVL2- z+6k#E>h?0{Y;5rD$>4VBIrjLZ4uk2u^y1pzdY?CDHr)1YW>kn*H(Pvj-pSsemEQlQ zCK@bJi}alF`cvcYeYZuzyE)kyCf{r7vO3ih*qnZ@@a-wnztbk@wqM(q7g4fR`kj=m zx{vfa#V8rGoVQUIKV_)0>?*zd<Brj{MiFh65y<^v6jpr*;o^Vabn)K})8hDm}XrqpL54<=dVdu zzfF9t#oqNX=~v^)`ki0*cYpPXVv(2J;eAM_dVlNrD-0_+UO%&b-{^9nLuf^Tc6Fm* z`@huhH&V1J-@KCgy7IpMvIAFT1pcW#QEHF#%vfLTXvx3o_S`;`6s6^%CU>v81i0Ng zbVvTv#wlJVE0)_8H7q~BzzhxJ{y6Vbm^Tl419OtsSYc00h z)3D=&NYkX!ti@JLA9xjKJ(jDn?3vn^-@RsH{HEwe^C#%AYR z*S_5MJ?Pl3opEU!*E03@Oc(MQN?&?xJa>5G-Bqb-3qHGf)V&p}woTmrk~8~;#!9n~ zCU4pLSoSP)ym?qE;&;l%_4|LeMqV&Z-IBLQcxR@xJmZDa+^H!V)nDIn{V8Uj#>nS# zbH|RvBL}y*h-+~l-|=p5WlH3Pn?eEW80}|>c)h*#|4OeyjOFz{g-Zb@(@#sS@AiJ~ zw{DNs%ZYZoZmc@GVcJh8zMeBP{+gJ(sd?I7YOe6uwli#M#Z{~K{41YET$p>C@aLE>~{ve&~N@+HY1qcD@3!&vH*buVC&!V7%FN`a9#l z{+;{s=5KoXj4w-9Pop9=;PEeIwYox`--a0p8(!ZQN?30DYu{&)yzc77ub#|XYa^(_ zSn0f3m|;mlPSepBn?KxIq<+xYXHl|EuHoX^tS{Dif;WD%h2<_i;hXr`YfG zRd-ElVpALc)W-Z*`)~f6JVlS+xYc;Wl(w~1k>8l!XbMld8NGs0+_j; zyBHMhvXyPqmA^S=Q=$W={C5d{UR~w7KQ@T3#cQS9ow~C*T_^6yDwM9>Cz{`A@@}Ki zntPEEC#Hmyw#?BKiDfi;udB6|NAHZBb)oN@3-*f+`_w<{y0~g?Lb(~M|H^MUbEoa# zuldD4|KHz9-y-FnD~Se3tUUhz!J=6PA+wJ#qdso$5Ur^upTkXfu z+b7TaoV&N-obRVO`Q;&xa<$71ABaW>=q%lx)8TS3;a0`Nli^`gSDI(-W_b18j+b4p z-S}wLna7;JruYyiQz?mSY+moY-EuzW(0+TnzB2l^Ch7e?F8S7|a<;YY!K~XNv*s#KIDWijnYcqqfvL!}%K;Ogxb?lM zUOD$a)8^@u`WE$b&3d}sW!uN5IXQKFjjaqb?(f=tRr%nL^`cCrr{j!$ayzGV&QJTE zX}yYr<9p^84U)Z)FiP0Rn#cDnYt_8iM! zyv~7=JHFR_x~S{)g=05^WXKg(|>=93C?=9PxSZs zElLk{e;(CZ|3zx^ZhM^{8rGr#SJfY`iHZ1ZAvm#hD(}Ult$RE)jYM<>*@8}PTe9-U zq^kcq zl)Y{G_tyT~QzxAA^qXM4>fE&Th6^p+h1nnP=T$%UrT@8sg;?C*wQZLR)O#X6&EdX! zDkJ)u@)IgWj{@*J7f8sZ< z@6S$ru;p9sJ@E_we4BW_@A=ZsanUuk$NJ2heJLK>&P09XZ#$xtRMaT=aJCcoIf1kd zH$VG#&yfCE`D^-?BDKe=`#){A4tl}x{Rl7L63fU(elq)~-0v$@^}2mx((+!5nR)@A z5|8hyF24V#`lgWCw-<|k#p<5d))AThMR~`Ry<(HRrnfRGA3l6&rbln_xd)7xEHc47 z>U!$d=O1qi?3<-E=#(V!^)H)oi$*;07ukFq}yG-R3Ru;WkE2o)#XYrJCGoO-gB=h_I zmW`h~6_2f4Wb85{_J?`?By*!>NB1S%{-sn|&pi9Sx0*}Z&yV_lSKdOqH?WeKrPN?$jBHPUgJL=~0Uj6c^!2eam zy|{Y*uc1@8{-4@;dfBb;jxIjVuBAWLOR8=Pxv^yy(=5l}-_L{l)^+VT`=Rc~O^2SE zd5Wrk|GfAVzuer$!pSAN{nY7eK~b4IUiZkbST3^Ld`GJPQN+^OOce}`D%>H;m6IFf znt!i$_TTq7Fpbm6Y|7glfut*Xw^i3IY3dINle{gZTeTujJ)p78dg*S)UZ z^uOYA=h4R5+Y(Dx#qfOG?r&qeI&A(DcP+K`wkvK_9_O5=BHT2;gFSBhT&Y~HnE0^gg<=66`8829UVCSB%{pY4cfdk;)uSsvTuM8HRm8t^J%1F| zlEE&$^W68R4|YG2le}#4Yp2WXL<>)qCw*_9ukGEw>c#p zb%(S`bN{u)?n#By#DRbig}{%XmT^KNa6u zJ$x&xP6W-lD__R(4 zR-e^+uI&uLeWy=|@XtM-$YSRB!rA1`5`CACZ_f1Tn#zSGy`?m7$4 zoZK1zXz7Wg%3WX9o9}4l@cQr8s}_R`^3@!%Z;x}fxi0)0 zqHEUSCI9Ykb&-0YR^5@dwEs?fB(Es$%oh_VU}WV;S-`F@ef;GYq1&HVuz61m)qMGl z)jH44B5!56+L6@)Z)&3!&tVWPvMY+ellA?PZkw3xpEgAxa^|&{b|ak;ep%M|?eS=$?Q`cN z5t|xqqnQHKlAaZ42Ps~g6{l}!86DS~z3HE-XufQRTs6zBXs_ht&>*eI$1jxZ-M8}? ziQHqfQ@3Rc-OY4mOR$sfvbea;m8)mn?lwQM>*9=@R#m4Gfz8}o<|w%8S-t;Qde%zc z`-i+&pLFZ>ADiay-(L|YCi-F1zjVQeN&LlXULV#>&b~k8!dIrqIVn;4`@3rnFZvd^ z!*1Sr(e-w7-jyGCR<=R>@}%v%=IT7_&+@fiW&a>RPKRmY3(@SWhc7HLOXsq8x#}RG zC$Z(+i7oe1O;k)~R~YX4$Mr(h=w4;~A+NqaMOOtEYFrii{$%=j>&%PCGnej*m^IO4 z!&Upn%h?JH{(P4YJgD8U<=x>8AsBJa)d}_hufx zShsIdX~wY&w(VY6@8Q+fYH?=jA8WC_9Ive$)@%$pujD4zcr$?Glj8EJ$!iwOf1ta; zmFer8p5+S6)5OB%zOwA$n|^M=3@gSZe%0DDgELyT%Fnvdx44yU;Uf96A7@T(;1;fC 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

V*=pZyo;eeiN1eBbuyi|dY}f5A(+noB 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 L-CD@bz`y_iAfDOA literal 0 HcmV?d00001 diff --git a/udiskie/translations/en.json b/udiskie/translations/en.json new file mode 100644 index 0000000..3824347 --- /dev/null +++ b/udiskie/translations/en.json @@ -0,0 +1,69 @@ +{ + "settings": { + "enable_notifications": { + "label": "Desktop notifications", + "description": "Show native Noctalia desktop notifications on device events." + }, + "auto_open_filemanager": { + "label": "Auto-open file manager", + "description": "Automatically open mounted drives in file manager." + }, + "file_manager_cmd": { + "label": "File manager command", + "description": "Command used to open mounted folders." + }, + "glyph": { + "label": "Bar glyph", + "description": "The glyph shown for the Udiskie widget on the bar." + }, + "show_count": { + "label": "Show device count", + "description": "Display count of mounted devices on the bar widget." + }, + "hide_when_empty": { + "label": "Hide when empty", + "description": "Hide the bar widget when no USB drives are connected." + } + }, + "tooltip_empty": "No devices mounted", + "tooltip_mounted": "Mounted devices", + "panel_title": "Udiskie Manager", + "panel_empty": "No USB drives detected", + "btn_mount_all": "Mount All", + "btn_unmount_all": "Unmount All", + "btn_mount": "Mount", + "btn_unlock": "Unlock", + "btn_unmount": "Unmount", + "btn_eject": "Eject", + "btn_poweroff": "Power Off", + "btn_open": "Open", + "status_mounted": "Mounted", + "status_unmounted": "Unmounted", + "status_locked": "Locked (LUKS)", + "error_missing_dep": "udiskie binary is missing from PATH", + "btn_retry": "Retry Connection", + "btn_docs": "Check Documentation", + "copy_path_tooltip": "Copy mount path", + "notify_path_copied": "Path copied to clipboard", + "no_partitions_found": "No partitions found", + "notif_drive_connected_title": "Drive Connected", + "notif_drive_connected_body": "{label} connected", + "notif_drive_removed_title": "Drive Removed", + "notif_drive_removed_body": "{label} disconnected", + "notif_drive_mounted_title": "Drive Mounted", + "notif_drive_mounted_body": "{label} is ready to use", + "notif_drive_unmounted_title": "Drive Unmounted", + "notif_drive_unmounted_body": "{label} was safely unmounted", + "notif_mount_failed_title": "Mount Failed", + "notif_mount_failed_fallback": "Could not mount {device}", + "notif_mount_all_failed_title": "Mount All Failed", + "notif_mount_all_failed_fallback": "Could not mount all devices", + "notif_unmount_failed_title": "Unmount Failed", + "notif_unmount_failed_fallback": "Device {device} is busy or in use", + "notif_unmount_all_failed_title": "Unmount All Failed", + "notif_unmount_all_failed_fallback": "One or more devices are busy", + "notif_eject_failed_title": "Eject Failed", + "notif_eject_failed_fallback": "Could not eject {device}", + "notif_poweroff_failed_title": "Power Off Failed", + "notif_poweroff_failed_fallback": "Could not power off {device}" +} \ No newline at end of file diff --git a/udiskie/translations/es.json b/udiskie/translations/es.json new file mode 100644 index 0000000..96856bc --- /dev/null +++ b/udiskie/translations/es.json @@ -0,0 +1,69 @@ +{ + "settings": { + "enable_notifications": { + "label": "Notificaciones de escritorio", + "description": "Mostrar notificaciones nativas de escritorio de Noctalia en los eventos de dispositivos." + }, + "auto_open_filemanager": { + "label": "Abrir gestor de archivos automáticamente", + "description": "Abrir automáticamente los dispositivos montados en el gestor de archivos." + }, + "file_manager_cmd": { + "label": "Comando del gestor de archivos", + "description": "Comando usado para abrir las carpetas montadas." + }, + "glyph": { + "label": "Icono de la barra", + "description": "El icono que se muestra para el widget de Udiskie en la barra." + }, + "show_count": { + "label": "Mostrar número de dispositivos", + "description": "Mostrar el número de dispositivos montados en el widget de la barra." + }, + "hide_when_empty": { + "label": "Ocultar cuando esté vacío", + "description": "Ocultar el widget de la barra cuando no haya unidades USB conectadas." + } + }, + "tooltip_empty": "No hay dispositivos montados", + "tooltip_mounted": "Dispositivos montados", + "panel_title": "Udiskie Manager", + "panel_empty": "No se detectaron unidades USB", + "btn_mount_all": "Montar todo", + "btn_unmount_all": "Desmontar todo", + "btn_mount": "Montar", + "btn_unlock": "Desbloquear", + "btn_unmount": "Desmontar", + "btn_eject": "Expulsar", + "btn_poweroff": "Apagar", + "btn_open": "Abrir", + "status_mounted": "Montado", + "status_unmounted": "Desmontado", + "status_locked": "Bloqueado (LUKS)", + "error_missing_dep": "El binario udiskie no está en el PATH", + "btn_retry": "Reintentar conexión", + "btn_docs": "Consultar documentación", + "copy_path_tooltip": "Copiar ruta de montaje", + "notify_path_copied": "Ruta copiada al portapapeles", + "no_partitions_found": "No se encontraron particiones", + "notif_drive_connected_title": "Unidad conectada", + "notif_drive_connected_body": "{label} se ha conectado", + "notif_drive_removed_title": "Unidad extraída", + "notif_drive_removed_body": "{label} se ha desconectado", + "notif_drive_mounted_title": "Unidad montada", + "notif_drive_mounted_body": "{label} está listo para usar", + "notif_drive_unmounted_title": "Unidad desmontada", + "notif_drive_unmounted_body": "{label} se desmontó de forma segura", + "notif_mount_failed_title": "Error al montar", + "notif_mount_failed_fallback": "No se pudo montar {device}", + "notif_mount_all_failed_title": "Error al montar todo", + "notif_mount_all_failed_fallback": "No se pudieron montar todos los dispositivos", + "notif_unmount_failed_title": "Error al desmontar", + "notif_unmount_failed_fallback": "El dispositivo {device} está ocupado o en uso", + "notif_unmount_all_failed_title": "Error al desmontar todo", + "notif_unmount_all_failed_fallback": "Uno o más dispositivos están ocupados", + "notif_eject_failed_title": "Error al expulsar", + "notif_eject_failed_fallback": "No se pudo expulsar {device}", + "notif_poweroff_failed_title": "Error al apagar", + "notif_poweroff_failed_fallback": "No se pudo apagar {device}" +}