Add QR Code Encoder plugin (#226)

* init

* feat: notify options; all, minimal and none

* update: plugin.toml

* update: add README

* feat: add button toggle

* feat: add configurable glyph

* fix: valid image radius

* fix: shell escape entry

* feat: add settings

* update: add thumbnail.webp

* feat: add titlebar and titlebar toggle

* feat: add custom image widget setting, reorder keys

* fix: auto-size image with titlebar

* update: README (new settings)

* fix: lowercase correction_level label keys

* fix: file type and move shellEscape to the cmd
This commit is contained in:
Yocraft-2000
2026-08-03 08:17:21 -04:00
committed by GitHub
parent 56edefe993
commit 8277d1bc6d
6 changed files with 433 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# QR Code encoder
Transform any text or URL into a QR code completely offline. Then scan or copy the code.
## Plugin
| Field | Value |
| --- | --- |
| ID | `yocraft/qrcode` |
| Entries | Bar widget: `widget`; panel: `panel` |
## Requirements
Install `qrencode` on `PATH`.
## Usage
Open the panel from the bar widget or with this command:
```sh
noctalia msg panel-toggle yocraft/qrcode:panel
```
Enter your text or URL and click on Generate or press enter to generate the QR code, then scan it from the panel or copy it by clicking on it.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `titlebar` | `bool` | `true` | Show titlebar, panel name and buttons like settings and close. |
| `generate_button` | `bool` | `true` | Show Generate button, disable will submit on enter. |
| `close_on_copy` | `bool` | `false` | Close the panel when copying the QR code. |
| `notify` | `select` | `minimal` | Controls the notifications, minimal only notifies when Close on Copy is used. |
| `size` | `int` | `8` | Specify module size in dots (pixels). |
| `correction_level` | `select` | `M` | Specify error correction level. |
| `glyph` | `glyph` | `qrcode` | Bar widget icon glyph name. |
| `custom_image` | `image` | `""` | Path to a custom image; leave empty to use the icon glyph. |
## Notes
The plugin runs entirely locally and does not require network access.
+226
View File
@@ -0,0 +1,226 @@
local currentText = ""
local genCount = 0
local errorMessage = nil
local imagePath = nil
local status = "idle"
local imageSize = 400
local notifyConf = noctalia.getConfig("notify")
local function notify(text, error)
local cmd = error and noctalia.notifyError or noctalia.notify
cmd("QR Code encoder", text)
end
local function statusText()
if status == "generating" then
return noctalia.tr("panel.status.generating")
elseif status == "error" then
return noctalia.tr("panel.status.error", { message = errorMessage })
elseif status == "ready" then
return noctalia.tr("panel.status.ready")
elseif status == "copied" then
return noctalia.tr("panel.status.copied")
else
return noctalia.tr("panel.status.idle")
end
end
local function render()
local rowContent = {
ui.input({
flexGrow = 1,
focus = true,
placeholder = noctalia.tr("panel.placeholder"),
onChange = "onTextChange",
onSubmit = "onTextSubmit",
})
}
if noctalia.getConfig("generate_button") then
table.insert(
rowContent,
ui.button({
text = "Generate",
variant = "primary",
enabled = status ~= "generating",
onClick = "onGenerateClick",
})
)
end
local content = {
ui.row({ gap = 8 }, rowContent),
ui.label({
flexGrow = 1,
textAlign = "center",
text = statusText(),
color = status == "error" and "error" or "on_surface/0.75",
})
}
if (status == "ready"
or status == "copied"
or (status == "error" and errorMessage == noctalia.tr("panel.error.copy_failed")))
and imagePath ~= nil then
table.insert(
content,
ui.row({ justify = "center" }, {
ui.image({
path = imagePath,
width = imageSize,
height = imageSize,
radius = 24,
onClick = "onImageClick",
})
})
)
end
if noctalia.getConfig("titlebar") then
imageSize = 350
table.insert(
content, 1,
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = "QR Code Encoder", fontSize = 16, fontWeight = "bold", flexGrow = 1 }),
ui.button({ glyph = "settings", onClick = noctalia.openSettings }),
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
})
)
end
panel.render(ui.column({ flexGrow = 1, gap = 16 }, content))
end
local function shellEscape(raw)
return "'" .. raw:gsub("'", "'\\''") .. "'"
end
local function generate(text)
if status == "generating" then
return
end
text = noctalia.string.trim(text or "")
if text == "" then
if imagePath ~= nil then
noctalia.removeFile(imagePath)
end
status = "idle"
errorMessage = nil
imagePath = nil
render()
return
end
if not noctalia.commandExists("qrencode") then
status = "error"
errorMessage = noctalia.tr("panel.error.missing_qrencode")
if notifyConf == "all" then
notify(statusText(), status == "error")
end
render()
return
end
status = "generating"
render()
local dir = noctalia.pluginDir()
local outputPath = string.format("%s/qr-%s.png", dir, genCount)
local previousPath = imagePath
genCount += 1
local size = noctalia.getConfig("size") or 8
local correctionLevel = noctalia.getConfig("correction_level") or "M"
local cmd = string.format(
'qrencode %s -o %s -s %s -l %s',
shellEscape(text),
outputPath,
size,
correctionLevel
)
noctalia.runAsync(
cmd,
function(result)
if result.exitCode == 0 and noctalia.fileExists(outputPath) then
imagePath = outputPath
status = "ready"
if previousPath ~= nil then
noctalia.removeFile(previousPath)
end
else
status = "error"
errorMessage = noctalia.tr("panel.error.generate_failed")
end
if notifyConf == "all" then
notify(statusText(), status == "error")
end
render()
end
)
end
function onOpen(_context)
status = "idle"
render()
end
function onTextChange(value)
currentText = value
end
function onTextSubmit(value)
currentText = value
generate(currentText)
end
function onGenerateClick()
generate(currentText)
end
function onImageClick()
local bytes, err = noctalia.readFile(imagePath)
errorMessage = noctalia.tr("panel.error.copy_failed")
if bytes == nil then
status = "error"
else
local ok = noctalia.copyToClipboard(bytes, "image/png")
status = ok and "copied" or "error"
end
if notifyConf == "all" then
notify(statusText(), status == "error")
end
if noctalia.getConfig("close_on_copy") then
if notifyConf == "minimal" then
notify(statusText(), status == "error")
end
panel.close()
return
end
render()
end
function onCloseClicked()
panel.close()
end
function onClose()
if imagePath ~= nil then
noctalia.removeFile(imagePath)
end
end
+93
View File
@@ -0,0 +1,93 @@
id = "yocraft/qrcode"
name = "QR Code Encoder"
version = "1.0.0"
plugin_api = 15
author = "yocraft"
license = "MIT"
deprecated = false
icon = "qrcode"
description = "Transform any text or URL in a QR code"
tags = [ "panel", "fun", "network", "privacy", "productivity", "utility" ]
dependencies = [ "qrencode" ]
[[widget]]
id = "widget"
entry = "widget.luau"
[[widget.setting]]
key = "glyph"
type = "glyph"
label_key = "widget.glyph.label"
description_key = "widget.glyph.description"
default = "qrcode"
[[widget.setting]]
key = "custom_image"
type = "file"
label_key = "widget.custom_image.label"
description_key = "widget.custom_image.description"
default = ""
extensions = [".png", ".jpg", ".jpeg", ".webp", ".svg"]
[[panel]]
id = "panel"
entry = "panel.luau"
width = 500
height = 550
placement = "floating"
position = "center"
[[setting]]
key = "titlebar"
type = "bool"
label_key = "settings.titlebar.label"
description_key = "settings.titlebar.description"
default = true
[[setting]]
key = "generate_button"
type = "bool"
label_key = "settings.generate_button.label"
description_key = "settings.generate_button.description"
default = true
[[setting]]
key = "close_on_copy"
type = "bool"
label_key = "settings.close_on_copy.label"
description_key = "settings.close_on_copy.description"
default = false
[[setting]]
key = "notify"
type = "select"
label_key = "settings.notify.label"
description_key = "settings.notify.description"
default = "minimal"
options = [
{ value = "all", label_key = "settings.notify.all" },
{ value = "minimal", label_key = "settings.notify.minimal" },
{ value = "none", label_key = "settings.notify.none" },
]
[[setting]]
key = "size"
type = "int"
label_key = "settings.size.label"
description_key = "settings.size.description"
default = 8
advanced = true
[[setting]]
key = "correction_level"
type = "select"
label_key = "settings.correction_level.label"
description_key = "settings.correction_level.description"
default = "M"
options = [
{ value = "L", label_key = "settings.correction_level.l" },
{ value = "M", label_key = "settings.correction_level.m" },
{ value = "Q", label_key = "settings.correction_level.q" },
{ value = "H", label_key = "settings.correction_level.h" },
]
advanced = true
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+61
View File
@@ -0,0 +1,61 @@
{
"settings": {
"titlebar": {
"label": "Show Titlebar",
"description": "Show panel name and buttons like settings and close."
},
"generate_button": {
"label": "Show Generate Button",
"description": "Disable will submit on enter."
},
"close_on_copy": {
"label": "Close on Copy",
"description": "Close the panel when copying the QR code."
},
"notify": {
"label": "Notify",
"description": "Controls the notifications, minimal only notifies when Close on Copy is used.",
"all": "All",
"minimal": "Minimal",
"none": "None"
},
"size": {
"label": "QR Code Size",
"description": "Specify module size in dots (pixels)."
},
"correction_level": {
"label": "Correction Level",
"description": "Specify error correction level.",
"l": "Lowest",
"m": "Medium",
"q": "High",
"h": "Highest"
}
},
"widget": {
"tooltip": "Generate a QR Code",
"glyph": {
"label": "Glyph",
"description": "Icon glyph name"
},
"custom_image": {
"label": "Custom image",
"description": "Path to a custom image; leave empty to use the icon glyph"
}
},
"panel": {
"placeholder": "Enter text or url...",
"status": {
"generating": "Generating...",
"error": "Error: {message}",
"ready": "QR code ready. Scan it here or click it to copy.",
"copied": "QR code copied to clipboard.",
"idle": "Enter some text, then press Generate."
},
"error": {
"generate_failed": "failed to generate the QR code.",
"missing_qrencode": "qrencode not installed.",
"copy_failed": "failed to copy the QR code."
}
}
}
+13
View File
@@ -0,0 +1,13 @@
local image = noctalia.getConfig("custom_image")
if image ~= "" and image ~= nil then
barWidget.setImage(image)
else
barWidget.setGlyph(noctalia.getConfig("glyph"))
end
barWidget.setTooltip(noctalia.tr("widget.tooltip"))
function onClick()
noctalia.togglePanel("yocraft/qrcode:panel")
end