Skip to content
Draft
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
41 changes: 41 additions & 0 deletions cmd-runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Command Runner

Save and execute CLI commands silently in the background with a single click and secure sudo password cache.

## Plugin

| Field | Value |
| --- | --- |
| ID | `nocode-96/cmd-runner` |
| Entries | Bar widget: `cmd-runner`; panel: `panel`; service: `cmd-service` |

## Requirements

No special system requirements. Requires Noctalia v5.

## Usage

Add the `cmd-runner` widget to a bar. Left-click it to open the command panel, where you can:
- **Run**: Execute custom CLI commands silently in the background.
- **Add**: Create a new command with a custom name, CLI command line, and optional sudo password.
- **Edit**: Edit an existing command. The sudo password is kept hidden and is not pre-populated in plain text.
- **Delete**: Remove a saved command.
- **Log**: Click the logs button on a command to view stdout/stderr output.

Open the panel directly with:

```sh
noctalia msg panel-toggle nocode-96/cmd-runner:panel
```

## Settings

| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `show_toast` | `bool` | `true` | Shows a toast notification when a command completes or fails. |
| `show_label` | `bool` | `true` | Displays 'Commands' next to the icon in the top bar. |

## Notes

Command Runner runs custom CLI command lines in the background. If a command requires sudo (root) privileges, the password is encrypted locally in `commands.json` (inside the plugin's data folder) using base64 and character-shifting. The password is never sent in the public UI state, preventing other plugins from harvesting it.

1 change: 1 addition & 0 deletions cmd-runner/commands.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"command":"echo 'Checking system updates...' && sleep 1 && echo 'System is up to date'","icon":"refresh","id":"cmd_demo_1","isSudo":false,"name":"System Update Check","sudoPassword":""},{"command":"sudo hda-verb /dev/snd/hwC1D0 0x1d SET_PIN_WIDGET_CONTROL 0x0","icon":"terminal","id":"cmd_304120","isSudo":true,"name":"hda","sudoPassword":"enc:oKOYrQ=="}]
341 changes: 341 additions & 0 deletions cmd-runner/panel.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
--!nonstrict
-- Panel entry for Command Runner plugin

local state = noctalia.state.get("cmd_runner_state") or {
commands = {},
runningMap = {},
outputMap = {},
exitCodeMap = {}
}

-- UI state
local isFormOpen = false
local formGen = 1
local editId = ""
local editName = ""
local editCommand = ""
local editIcon = "terminal"
local editIsSudo = false
local editSudoPassword = ""
local hasPassword = false
local activeLogId = ""

local render

local function tr(key, values)
return noctalia.tr(key, values)
end

local function sendAction(actData)
noctalia.state.set("cmd_runner_action", actData)
end

local function openAddForm()
editId = ""
editName = ""
editCommand = ""
editIcon = "terminal"
editIsSudo = false
editSudoPassword = ""
hasPassword = false
formGen = formGen + 1
isFormOpen = true
render()
end

local function openEditForm(cmd)
if not cmd then return end
editId = cmd.id or ""
editName = cmd.name or ""
editCommand = cmd.command or ""
editIcon = cmd.icon or "terminal"
editIsSudo = cmd.isSudo == true
editSudoPassword = "" -- Never pre-fill the password for security
hasPassword = cmd.hasPassword == true
formGen = formGen + 1
isFormOpen = true
render()
end

local function closeForm()
isFormOpen = false
render()
end

local function saveForm()
local trimmedName = noctalia.string.trim(editName)
local trimmedCmd = noctalia.string.trim(editCommand)
if trimmedName == "" or trimmedCmd == "" then return end

local item = {
id = editId ~= "" and editId or ("cmd_" .. tostring(math.random(100000, 999999))),
name = trimmedName,
command = trimmedCmd,
icon = editIcon ~= "" and editIcon or "terminal",
isSudo = editIsSudo,
sudoPassword = editSudoPassword
}

if editId ~= "" then
sendAction({ action = "update", item = item })
else
sendAction({ action = "add", item = item })
end
closeForm()
end

-- Build the edit/add form UI
local function renderForm()
local isEditing = editId ~= ""

local formItems = {
ui.row({ align = "center", justify = "space_between" }, {
ui.label({
text = isEditing and tr("panel.edit") or tr("panel.add_button"),
fontWeight = "bold",
fontSize = 14,
color = "primary"
}),
ui.button({
glyph = "close",
variant = "ghost",
onClick = function() closeForm() end
})
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("panel.name_label"), fontSize = 12, color = "on_surface_variant" }),
ui.input({
key = "edit_name_" .. tostring(formGen),
value = editName,
placeholder = "z.B. System Update",
onChange = function(val) editName = val or "" end
})
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("panel.cmd_label"), fontSize = 12, color = "on_surface_variant" }),
ui.input({
key = "edit_cmd_" .. tostring(formGen),
value = editCommand,
placeholder = "z.B. apt update && apt upgrade -y",
onChange = function(val) editCommand = val or "" end
})
}),
ui.button({
text = editIsSudo and "[✓] Sudo (root) erforderlich" or "[ ] Sudo (root) erforderlich",
variant = editIsSudo and "primary" or "secondary",
onClick = function()
editIsSudo = not editIsSudo
render()
end
})
}

if editIsSudo then
local placeholderText = "Sudo-Passwort..."
if isEditing and hasPassword then
placeholderText = "Passwort gespeichert (leer lassen zum Behalten)"
end

table.insert(formItems, ui.column({ gap = 4 }, {
ui.label({ text = tr("panel.sudo_pass_label"), fontSize = 12, color = "primary", fontWeight = "bold" }),
ui.input({
key = "edit_pass_" .. tostring(formGen),
value = editSudoPassword,
placeholder = placeholderText,
secret = true,
onChange = function(val) editSudoPassword = val or "" end
})
}))
end

table.insert(formItems, ui.row({ justify = "end", gap = 8 }, {
ui.button({
text = tr("panel.cancel_button"),
variant = "ghost",
onClick = function() closeForm() end
}),
ui.button({
text = tr("panel.save_button"),
variant = "primary",
onClick = function() saveForm() end
})
}))

return ui.column({ fill = "surface_variant/0.3", radius = 10, padding = 12, gap = 12 }, formItems)
end

-- Render a single command card
local function renderCommandCard(cmd)
local runningMap = (type(state) == "table" and type(state.runningMap) == "table") and state.runningMap or {}
local outputMap = (type(state) == "table" and type(state.outputMap) == "table") and state.outputMap or {}
local exitCodeMap = (type(state) == "table" and type(state.exitCodeMap) == "table") and state.exitCodeMap or {}

local isRunning = runningMap[cmd.id] == true
local hasOutput = outputMap[cmd.id] ~= nil
local exitCode = exitCodeMap[cmd.id]
local isLogOpen = activeLogId == cmd.id

-- Status badge
local statusGlyph = nil
if isRunning then
statusGlyph = ui.glyph({ name = "loader-2", size = 14, color = "primary" })
elseif exitCode == 0 then
statusGlyph = ui.glyph({ name = "check", size = 14, color = "success" })
elseif exitCode ~= nil then
statusGlyph = ui.glyph({ name = "x", size = 14, color = "error" })
end

-- Header line inside card
local cardHeader = {
ui.glyph({ name = cmd.icon or "terminal", size = 16, color = "primary" }),
ui.label({ text = cmd.name or "", fontWeight = "bold", fontSize = 13, color = "on_surface", flexGrow = 1, maxLines = 1 })
}
if cmd.isSudo then
table.insert(cardHeader, ui.label({ text = "[sudo]", fontSize = 10, color = "primary", fontWeight = "bold" }))
end
if statusGlyph then
table.insert(cardHeader, statusGlyph)
end

-- Action buttons row
local actionButtons = {
ui.button({
text = isRunning and tr("panel.running") or tr("panel.run_button"),
glyph = isRunning and "loader-2" or "player-play",
variant = "primary",
enabled = not isRunning,
onClick = function()
sendAction({ action = "run", id = cmd.id })
end
}),
ui.button({
glyph = "edit",
text = tr("panel.edit"),
variant = "secondary",
onClick = function()
openEditForm(cmd)
end
}),
ui.button({
glyph = "trash",
variant = "destructive",
onClick = function()
sendAction({ action = "delete", id = cmd.id })
if activeLogId == cmd.id then activeLogId = "" end
render()
end
})
}

if hasOutput then
table.insert(actionButtons, ui.button({
glyph = isLogOpen and "chevron-up" or "code",
text = tr("panel.logs"),
variant = "ghost",
onClick = function()
activeLogId = isLogOpen and "" or cmd.id
render()
end
}))
end

local cardItems = {
ui.row({ align = "center", gap = 8 }, cardHeader),
ui.label({ text = cmd.command or "", fontSize = 11, color = "on_surface_variant", maxLines = 2 }),
ui.row({ align = "center", gap = 6, justify = "end" }, actionButtons)
}

-- Log output preview if open
if isLogOpen and hasOutput then
local outText = outputMap[cmd.id] or ""
local logHeader = {}
if exitCode == 0 then
table.insert(logHeader, ui.label({ text = "✓ Status: Success (Code 0)", fontSize = 10, color = "success", fontWeight = "bold" }))
elseif exitCode ~= nil then
table.insert(logHeader, ui.label({ text = "✗ Status: Failed (Code " .. tostring(exitCode) .. ")", fontSize = 10, color = "error", fontWeight = "bold" }))
end

table.insert(logHeader, ui.label({ text = outText, fontSize = 10, color = "on_surface_variant", maxLines = 12 }))

table.insert(cardItems, ui.column({
fill = "surface_variant/0.5", radius = 6, padding = 8, gap = 4
}, logHeader))
end

return ui.column({
fill = "surface_variant/0.25", radius = 8, padding = 10, gap = 8
}, cardItems)
end

render = function()
local commands = (type(state) == "table" and type(state.commands) == "table") and state.commands or {}
local children = {}

-- Header Row 1: Icon + Title + Close Button
table.insert(children, ui.row({ align = "center", gap = 8 }, {
ui.glyph({ name = "terminal", size = 18, color = "primary" }),
ui.label({ text = tr("panel.title"), fontSize = 15, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({
glyph = "close",
variant = "ghost",
onClick = function() panel.close() end
})
}))

-- Header Row 2: Subtitle + Neuer Befehl Button (No collision!)
if not isFormOpen then
table.insert(children, ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.column({ flexGrow = 1, overflow = "hidden" }, {
ui.label({ text = tr("panel.subtitle"), fontSize = 10, color = "on_surface_variant", maxLines = 1 })
}),
ui.button({
glyph = "plus",
text = tr("panel.add_button"),
variant = "primary",
onClick = function() openAddForm() end
})
}))
end

-- Form or Command List
if isFormOpen then
table.insert(children, renderForm())
else
if #commands == 0 then
-- Empty state
table.insert(children, ui.column({ align = "center", gap = 12, padding = 32 }, {
ui.glyph({ name = "terminal", size = 42, color = "on_surface_variant" }),
ui.label({ text = tr("panel.no_commands"), color = "on_surface_variant", fontSize = 13 }),
ui.button({
glyph = "plus",
text = tr("panel.add_button"),
variant = "primary",
onClick = function() openAddForm() end
})
}))
else
-- List of command cards
local cardList = {}
for _, cmd in ipairs(commands) do
table.insert(cardList, renderCommandCard(cmd))
end

table.insert(children, ui.scroll({ flexGrow = 1, gap = 8 }, cardList))
end
end

panel.render(ui.column({ flexGrow = 1, gap = 8, padding = 4 }, children))
end

-- State watchers
noctalia.state.watch("cmd_runner_state", function(val)
if type(val) == "table" then
state = val
render()
end
end)

-- Lifecycle
function onOpen()
render()
end
Loading
Loading