feat: Color Picker plugin (#12)

* feat: add color picker panel (WIP - render bug)

* docs: update README and add thumbnail

* fix: rgb and hex options label key

* fix: current swatch not loading on open

* Modify plugin.toml metadata for color picker

Updated plugin metadata fields including tags.

---------

Co-authored-by: Lemmy <studio@quadbyte.net>
This commit is contained in:
Ian Ribeiro
2026-07-14 23:42:37 -04:00
committed by GitHub
co-authored by Lemmy
parent a6ea5ec9ed
commit 273700dad3
8 changed files with 932 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
# Color Picker
A screen color picker for Noctalia v5, built on top of [hyprpicker](https://github.com/hyprwm/hyprpicker).
## Plugin
| Field | Value |
| ------- | ----- |
| ID | `oldirtty/color_picker` |
| Entries | Service: `service`; bar widget: `widget`; panel: `panel` |
## Requirements
Install `hyprpicker`
- [hyprpicker](https://github.com/hyprwm/hyprpicker)
## Installation
The headless `service` triggers `hyprpicker` with arguments set in the plugin's settings.
Widget controls:
| Action | Behavior |
| ----------- | -------- |
| Left click | Open plugin's panel. |
| Right click | Sample a color directly, without opening the panel. |
Panel controls:
| Action | Behavior |
| ----------- | -------- |
| Click bigger swatch | Open color picker dialog. |
## Usage
- **Left-click** the bar widget to open the panel.
- **Right-click** the bar widget to sample a color directly, without opening the panel.
- Inside the panel, click any recent color swatch to select it, or edit the HEX/RGB/HSL fields directly or click the bigger swatch to open color picker dialog.
- Colors picked during a panel session are added to history when the panel closes.
## Settings
| setting | type | default | description |
| ---------------------------- | -------- | ----------- | ----------- |
| `hyprpicker-format` | `select` | `"hex"` | Default color format to copy to clipboard (`hex` or `rgb`). |
| `hyprpicker-lowercase` | `bool` | `false` | Outputs the hexcode in lowercase. |
| `swatch-radius` | `int` | `8` | Corner radius of the history swatches and current-color preview. |
| `hyprpicker-no-zoom` | `bool` | `false` | Turns off the magnifying zoom lens while picking. |
| `hyprpicker-scale` | `int` | `10` | Zoom lens magnification, from `1` to `10`. |
| `hyprpicker-radius` | `int` | `100` | Zoom lens circle radius in pixels, from `1` to `1000`. |
| `hyprpicker-disable-preview` | `bool` | `false` | Turns off the live color preview while picking. |
| `hyprpicker-cursor` | `bool` | `false` | Includes the cursor in the frozen screen preview. |
| `glyph` | `glyph` | `"palette"` | Glyph |
## IPC
Runs `hyprpicker` with the arguments given in plugin's settings.
```bash
noctalia msg panel-toggle oldirtty/color_picker:panel
# Sample a color without opening the panel
noctalia msg plugin oldirtty/color_picker:service all pick
```
## Notes
- There is a known behavior where invoking `hyprpicker` through the plugin's background service imposes a timeout. The picker will close prematurely if a color is not selected within a certain timeframe, rather than staying open indefinitely waiting for a click.
+385
View File
@@ -0,0 +1,385 @@
-- [[panel]] entry:
-- shows the last/current picked color with a button to
-- pick a new one, a history of the last colors, and
-- color fields with real-time editing.
--
-- No hyprpicker, no disk, no history mutation here -- that all lives in
-- service.luau. This script only reads noctalia.state and dispatches IPC
-- events to the service for anything that touches picking or persistence.
--!nocheck
--!nolint UnknownGlobal
--==============================================
-- CONSTANTS
--==============================================
local SERVICE = "oldirtty/color_picker:service"
local swatch_radius = noctalia.getConfig("swatch-radius")
local tr_color_picker = noctalia.tr("color_picker")
--==============================================
-- TRANSLATION HELPER
--==============================================
local function tr(key: string, subst: table?): string
if subst then
return noctalia.tr(key, subst)
end
return noctalia.tr(key)
end
--==============================================
-- COLOR CONVERSION
--==============================================
local function hexToRgb(hex: string)
local r = tonumber(hex:sub(2, 3), 16)
local g = tonumber(hex:sub(4, 5), 16)
local b = tonumber(hex:sub(6, 7), 16)
return r, g, b
end
local function rgbToHsl(r: number, g: number, b: number)
r, g, b = r / 255, g / 255, b / 255
local max, min = math.max(r, g, b), math.min(r, g, b)
local h, s, l = 0, 0, (max + min) / 2
if max ~= min then
local d = max - min
s = if l > 0.5 then d / (2 - max - min) else d / (max + min)
if max == r then
h = (g - b) / d + (if g < b then 6 else 0)
elseif max == g then
h = (b - r) / d + 2
else
h = (r - g) / d + 4
end
h = h / 6
end
return math.floor(h * 360 + 0.5), math.floor(s * 100 + 0.5), math.floor(l * 100 + 0.5)
end
local function formatHex(hex: string, alpha: number): string
if alpha < 1 then
return hex .. string.format("%02x", math.floor(alpha * 255 + 0.5))
end
return hex
end
local function formatRgb(hex: string, alpha: number): string
local r, g, b = hexToRgb(hex)
if alpha < 1 then
return string.format("rgba(%d, %d, %d, %.2f)", r, g, b, alpha)
end
return string.format("rgb(%d, %d, %d)", r, g, b)
end
local function formatHsl(hex: string, alpha: number): string
local r, g, b = hexToRgb(hex)
local h, s, l = rgbToHsl(r, g, b)
if alpha < 1 then
return string.format("hsla(%d, %d%%, %d%%, %.2f)", h, s, l, alpha)
end
return string.format("hsl(%d, %d%%, %d%%)", h, s, l)
end
local function hexWithAlpha(hex: string, alpha: number): string
if alpha >= 1 then
return hex
end
return hex .. string.format("%02x", math.floor(alpha * 255 + 0.5))
end
--==============================================
-- COLOR TEXT PARSING (field input -> hex)
--==============================================
local function parseRgbText(text: string): string?
local r, g, b = text:match("^%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d+)")
if r == nil then
return nil
end
r, g, b = tonumber(r), tonumber(g), tonumber(b)
if r > 255 or g > 255 or b > 255 then
return nil
end
return string.format("#%02x%02x%02x", r, g, b)
end
local function parseHslText(text: string): string?
local h, s, l = text:match("^%s*(%d+)%s*,%s*(%d+)%%?%s*,%s*(%d+)%%?")
if h == nil then
return nil
end
h, s, l = tonumber(h), tonumber(s) / 100, tonumber(l) / 100
local function hueToRgb(p: number, q: number, t: number): number
if t < 0 then t = t + 1 end
if t > 1 then t = t - 1 end
if t < 1 / 6 then return p + (q - p) * 6 * t end
if t < 1 / 2 then return q end
if t < 2 / 3 then return p + (q - p) * (2 / 3 - t) * 6 end
return p
end
local r, g, b
if s == 0 then
r, g, b = l, l, l
else
local q = if l < 0.5 then l * (1 + s) else l + s - l * s
local p = 2 * l - q
local hn = h / 360
r = hueToRgb(p, q, hn + 1 / 3)
g = hueToRgb(p, q, hn)
b = hueToRgb(p, q, hn - 1 / 3)
end
return string.format(
"#%02x%02x%02x",
math.floor(r * 255 + 0.5),
math.floor(g * 255 + 0.5),
math.floor(b * 255 + 0.5)
)
end
local function parseHexText(text: string): string?
local clean = text:match("^#?(%x%x%x%x%x%x)$")
if clean == nil then
return nil
end
return "#" .. clean
end
--==============================================
-- STATE (read-only view + pure-UI selection)
--
-- selectedColor / selectedOpacity / colorHistory all live in noctalia.state,
-- owned and mutated by service.luau. This panel only reads them for
-- rendering, except for pure-UI interactions (history swatch click, field
-- edit, opacity drag) that change the *selection* without picking or
-- persisting -- those are cheap enough to set directly here.
--==============================================
local function getSelectedColor(): string?
return noctalia.state.get("selectedColor")
end
local function getSelectedOpacity(): number
return tonumber(noctalia.state.get("selectedOpacity")) or 1
end
local function setSelected(hex: string, opacity: number)
noctalia.state.set("selectedColor", hex)
noctalia.state.set("selectedOpacity", opacity)
end
--==============================================
-- SERVICE DISPATCH
--==============================================
local function sendToService(event: string, payload: string?)
local cmd = "noctalia msg plugin " .. SERVICE .. " all " .. event
if payload ~= nil then
cmd = cmd .. " " .. payload
end
noctalia.runAsync(cmd, function() end)
end
--==============================================
-- RENDERING
--==============================================
local function renderRecentItem(color: string, index: number)
return ui.box({
width = 32,
height = 32,
fill = color,
radius = swatch_radius / 2.5,
border = "outline",
onClick = "onHistoryColorClicked" .. index,
})
end
-- Recent-colors row: history positions 2..N. Position 1 is the selected
-- color, already shown by its own preview swatch.
local function renderRecent()
local recent = noctalia.state.get("colorHistory") or {}
if #recent <= 1 then
return ui.label({ text = tr("no_history"), fontSize = 12, color = "muted" })
end
local items = {}
for i = #recent, 2, -1 do
table.insert(items, renderRecentItem(recent[i].hex, i))
end
return ui.row({ gap = 8, align = "center" }, items)
end
local function renderTitlebar()
return ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("color_picker"), fontSize = 18, fontWeight = "bold", color = "primary", flexGrow = 1 }),
ui.button({ glyph = "color-picker", onClick = "onPickClickedFromPanel" }),
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
})
end
local function renderColorspaceColumn(name: string, value: string, onSubmit: string, onCopy: string, keySuffix: string)
local text_align = "left"
if name ~= "HEX" then
value = value:match("%((.-)%)")
text_align = "center"
end
return ui.column({ gap = 4, flexGrow = 1 }, {
ui.label({ text = name, fontSize = 14, color = "on_surface_variant" }),
ui.row({ gap = 8, align = "center" }, {
ui.input({
key = name .. "-input-" .. keySuffix,
value = value,
textAlign = text_align,
flexGrow = 1,
onSubmit = onSubmit,
}),
ui.button({ glyph = "copy", variant = "ghost", onClick = onCopy }),
}),
})
end
local function render()
local current = getSelectedColor()
local opacity = getSelectedOpacity()
if current == nil then
panel.render(ui.column({ gap = 12, padding = 16 }, {
renderTitlebar(),
ui.label({ text = tr("no_color_picked"), padding = { top = 8 } }),
renderRecent(),
}))
return
end
panel.render(ui.column({ gap = 12, padding = 16 }, {
renderTitlebar(),
ui.row({ gap = 12, align = "stretch" }, {
ui.column({ gap = 8, flexGrow = 2 }, {
ui.label({ text = tr("recent_colors"), fontSize = 14, color = "on_surface_variant" }),
renderRecent(),
}),
ui.column({ gap = 8, flexGrow = 2 }, {
ui.label({ text = tr("opacity"), fontSize = 14, color = "on_surface_variant" }),
ui.slider({ min = 0, max = 1, step = 0.01, value = opacity, onChange = "onOpacityChange", onDragEnd = "onOpacityChange" }),
}),
-- Render current swatch
ui.box({
width = 128,
height = 64,
fill = hexWithAlpha(current, opacity),
radius = swatch_radius,
border = "outline",
borderWidth = 2,
flexGrow = 1,
onClick = "onNativePickerClicked"
}),
}),
ui.row({ gap = 12 }, {
renderColorspaceColumn("HEX", formatHex(current, opacity), "onSubmitHex", "onCopyHex", current .. "-" .. opacity),
renderColorspaceColumn("RGB", formatRgb(current, opacity), "onSubmitRgb", "onCopyRgb", current .. "-" .. opacity),
renderColorspaceColumn("HSL", formatHsl(current, opacity), "onSubmitHsl", "onCopyHsl", current .. "-" .. opacity),
}),
}))
end
--==============================================
-- CALLBACKS: history interaction (pure UI, no service call)
--==============================================
function handleHistoryClick(index: number)
local history = noctalia.state.get("colorHistory") or {}
local entry = history[index]
if entry then
setSelected(entry.hex, entry.opacity or 1)
render()
end
end
function onHistoryColorClicked2() handleHistoryClick(2) end
function onHistoryColorClicked3() handleHistoryClick(3) end
function onHistoryColorClicked4() handleHistoryClick(4) end
function onHistoryColorClicked5() handleHistoryClick(5) end
function onHistoryColorClicked6() handleHistoryClick(6) end
function onHistoryColorClicked7() handleHistoryClick(7) end
--==============================================
-- CALLBACKS: colorspace fields (pure UI, no service call)
--==============================================
local function processSubmit(parseText: (string) -> string?, text: string)
local color_value = parseText(text)
if color_value == nil then return end
setSelected(color_value, getSelectedOpacity())
render()
end
function onSubmitHex(text: string) processSubmit(parseHexText, text) end
function onSubmitRgb(text: string) processSubmit(parseRgbText, text) end
function onSubmitHsl(text: string) processSubmit(parseHslText, text) end
local function processCopy(format: (string, number) -> string)
local current = getSelectedColor()
if current == nil then return end
local text = format(current, getSelectedOpacity())
noctalia.copyToClipboard(text, "text/plain")
noctalia.notify(tr_color_picker, tr("color_copied", { color = text }))
end
function onCopyHex() processCopy(formatHex) end
function onCopyRgb() processCopy(formatRgb) end
function onCopyHsl() processCopy(formatHsl) end
function onOpacityChange(value)
local parsed = tonumber(value)
if parsed == nil then return end
noctalia.state.set("selectedOpacity", parsed)
render()
end
--==============================================
-- CALLBACKS: picking (delegates to the service)
--==============================================
-- Panel's own pick button: asks the service to pick and update the
-- selection only. No history commit until the panel closes.
function onPickClickedFromPanel()
sendToService("pick-panel")
end
function onNativePickerClicked()
sendToService("picker-dialog")
end
function onCloseClicked()
panel.close()
end
-- Panel closed (any path: close button, Esc, click outside). Commits
-- whatever is currently selected to the service's history.
function onClose()
local hex = getSelectedColor()
if hex == nil then return end
local payload = hex .. ":" .. tostring(getSelectedOpacity())
sendToService("commit", payload)
end
function onOpen(context)
noctalia.state.watch("selectedColor", function() render() end)
noctalia.state.watch("colorHistory", function() render() end)
render()
end
+104
View File
@@ -0,0 +1,104 @@
author = "oldirtty"
id = "oldirtty/color_picker"
name = "Color Picker"
description = "Pick a color from your screen with hyprpicker."
license = "MIT"
version = "1.0.0"
min_noctalia = "5.0.0"
icon = "palette"
dependencies = ["hyprpicker"]
tags = ["theming", "utility", "bar", "panel"]
[[setting]]
key = "hyprpicker-format"
type = "select"
default = "hex"
label_key = "settings.hyprpicker_format.label"
description_key = "settings.hyprpicker_format.description"
options = [
{ value = "hex", label_key = "hex" },
{ value = "rgb", label_key = "rgb" }
]
[[setting]]
key = "hyprpicker-lowercase"
type = "bool"
label_key = "settings.hyprpicker_lowercase.label"
description_key = "settings.hyprpicker_lowercase.description"
visible_when = { key = "hyprpicker-format", values = ["hex"]}
default = false
[[setting]]
key = "swatch-radius"
type = "int"
label_key = "settings.swatch_radius.label"
description_key = "settings.swatch_radius.description"
default = 8
min = 0
max = 64
[[setting]]
key = "hyprpicker-no-zoom"
type = "bool"
label_key = "settings.hyprpicker_no_zoom.label"
description_key = "settings.hyprpicker_no_zoom.description"
default = false
[[setting]]
key = "hyprpicker-scale"
type = "int"
label_key = "settings.hyprpicker_scale.label"
description_key = "settings.hyprpicker_scale.description"
visible_when = { key = "hyprpicker-no-zoom", values = ["false"]}
default = 10
min = 1
max = 10
[[setting]]
key = "hyprpicker-radius"
type = "int"
label_key = "settings.hyprpicker_radius.label"
description_key = "settings.hyprpicker_radius.description"
visible_when = { key = "hyprpicker-no-zoom", values = ["false"]}
default = 100
min = 1
max = 1000
[[setting]]
key = "hyprpicker-disable-preview"
type = "bool"
label_key = "settings.hyprpicker_disable_preview.label"
description_key = "settings.hyprpicker_disable_preview.description"
visible_when = { key = "hyprpicker-no-zoom", values = ["false"]}
default = false
[[setting]]
key = "hyprpicker-cursor"
type = "bool"
label_key = "settings.hyprpicker_cursor.label"
description_key = "settings.hyprpicker_cursor.description"
default = false
[[widget]]
id = "widget"
entry = "widget.luau"
[[widget.setting]]
key = "glyph"
type = "glyph"
label_key = "glyph"
default = "palette"
[[panel]]
id = "panel"
entry = "panel.luau"
width = 720
height = 240
placement = "floating"
position = "center"
# Headless background service: owns the hyprpicker backend
[[service]]
id = "service"
entry = "service.luau"
+286
View File
@@ -0,0 +1,286 @@
-- [[service]] entry:
-- owns all hyprpicker interaction, color history, and disk persistence.
-- Headless -- no UI. Widget and panel talk to it via onIpc, never touch
-- hyprpicker or the history file directly.
--!nocheck
--!nolint UnknownGlobal
--==============================================
-- CONSTANTS
--==============================================
local MAX_HISTORY = 7 -- selected color (index 1) + 6 recent
local STATE_DIR = "~/.local/state/noctalia/plugin-cache/color_picker"
local HISTORY_FILE = STATE_DIR .. "/history.json"
local tr_color_picker = noctalia.tr("color_picker")
local function tr(key: string, subst: table?): string
if subst then
return noctalia.tr(key, subst)
end
return noctalia.tr(key)
end
--==============================================
-- HYPRPICKER COMMAND BUILDER
--==============================================
-- Builds the hyprpicker argv from plugin settings. Returns an array of
-- shell-safe tokens; join with spaces before passing to noctalia.runAsync,
-- which only accepts a single command string.
local function buildHyprpickerArgs(): { string }
local args = { "hyprpicker", "-a" }
if noctalia.getConfig("hyprpicker-format") == "rgb" then
table.insert(args, "-f")
table.insert(args, "rgb")
end
if noctalia.getConfig("hyprpicker-cursor") then
table.insert(args, "--cursor")
end
if noctalia.getConfig("hyprpicker-disable-preview") then
table.insert(args, "--disable-preview")
end
if noctalia.getConfig("hyprpicker-lowercase") then
table.insert(args, "--lowercase-hex")
end
if noctalia.getConfig("hyprpicker-no-zoom") then
table.insert(args, "--no-zoom")
return args -- returns earlier since next args apply only when zoom is enabled
end
local scale = noctalia.getConfig("hyprpicker-scale")
if scale ~= nil then
table.insert(args, "--scale=" .. tostring(scale))
end
local radius = noctalia.getConfig("hyprpicker-radius")
if radius ~= nil then
table.insert(args, "--radius=" .. tostring(radius))
end
return args
end
local function hyprpickerCommand(): string
return table.concat(buildHyprpickerArgs(), " ")
end
local function parsePickerOutput(output: string): string?
-- Tries Hex first
local hex = output:match("#%x%x%x%x%x%x")
if hex then
-- Garante que o retorno é maiúsculo
return hex:upper()
end
-- Then tries RGB
local r, g, b = output:match("(%d+)%s+(%d+)%s+(%d+)")
if r and g and b then
-- Usa %02X (X MAIÚSCULO) para retornar Hex maiúsculo
return string.format("#%02X%02X%02X", tonumber(r), tonumber(g), tonumber(b))
end
return nil
end
local function formatRgb(hex: string): string
local r = tonumber(hex:sub(2, 3), 16)
local g = tonumber(hex:sub(4, 5), 16)
local b = tonumber(hex:sub(6, 7), 16)
return string.format("rgb(%d, %d, %d)", r, g, b)
end
--==============================================
-- STATE MODEL
--
-- selectedColor / selectedOpacity: ephemeral, what the panel is currently
-- showing. Set here on every successful pick; also updated by the panel
-- itself for pure-UI interactions (history swatch click, field edit,
-- opacity slider) that don't touch hyprpicker or disk.
--
-- colorHistory: persisted list (index 1 = most recently committed color).
-- Only mutated by pushCurrentColor(), called on immediate-commit picks
-- (widget right-click) and on "commit" (panel close).
--==============================================
local function setSelected(hex: string, opacity: number)
noctalia.state.set("selectedColor", hex)
noctalia.state.set("selectedOpacity", opacity)
end
-- Moves (or inserts) a color to the front of colorHistory, capped at
-- MAX_HISTORY entries. Memory only -- callers decide when to persist.
local function pushCurrentColor(hex: string, opacity: number)
hex = hex:upper() -- Garante o padrão maiúsculo
local history = noctalia.state.get("colorHistory") or {}
for i, entry in ipairs(history) do
if entry.hex and entry.hex:upper() == hex then
table.remove(history, i)
break
end
end
table.insert(history, 1, { hex = hex, opacity = opacity })
while #history > MAX_HISTORY do
table.remove(history)
end
noctalia.state.set("colorHistory", history)
end
local function loadHistoryFromDisk()
local content = noctalia.readFile(HISTORY_FILE)
if content == nil or content == "" then return end
local decoded = noctalia.json.decode(content)
if type(decoded) ~= "table" then return end
noctalia.state.set("colorHistory", decoded)
end
local function saveHistoryToDisk()
local history = noctalia.state.get("colorHistory") or {}
local encoded = noctalia.json.encode(history)
if encoded ~= nil then
noctalia.writeFile(HISTORY_FILE, encoded)
end
end
--==============================================
-- PICKING
--==============================================
-- Runs hyprpicker and invokes onResult(hex) on success. onResult is nil-safe
-- to call with nil (caller decides what "no result" means for that flow).
local function runPicker(onResult: (string?, string?) -> ())
if not noctalia.commandExists("hyprpicker") then
noctalia.notifyError(tr_color_picker, tr("hyprpicker_not_installed"))
onResult(nil, nil)
return
end
noctalia.runAsync(hyprpickerCommand(), function(result)
if result.exitCode ~= 0 then
onResult(nil, nil)
return
end
local hex = parsePickerOutput(result.stdout or "")
if hex == nil then
noctalia.notifyError(tr_color_picker, tr("could_not_parse"))
onResult(nil, nil)
return
end
local copyText = hex
if noctalia.getConfig("hyprpicker-format") == "rgb" then
copyText = formatRgb(hex)
elseif noctalia.getConfig("hyprpicker-lowercase") then
copyText = hex:lower()
end
noctalia.copyToClipboard(copyText, "text/plain")
onResult(hex, copyText)
end)
end
--==============================================
-- IPC
--
-- "pick" - immediate commit (widget right-click). Picks, pushes to
-- history, saves to disk right away.
-- "pick-panel" - panel's own pick button. Picks and updates the selection
-- only; history/disk are untouched until "commit".
-- "commit" - panel is closing. payload = "<hex>:<opacity>", the
-- selection at close time. Pushes it to history and saves.
--==============================================
local function handlePick()
runPicker(function(hex, copyText)
if hex == nil then return end
setSelected(hex, 1)
pushCurrentColor(hex, 1)
saveHistoryToDisk()
noctalia.notify(tr_color_picker, tr("color_picked", { color = copyText }))
end)
end
local function handlePickFromPanel()
runPicker(function(hex, copyText)
if hex == nil then return end
setSelected(hex, 1)
noctalia.notify(tr_color_picker, tr("color_picked", { color = copyText }))
end)
end
local function handleCommit(payload: string?)
if payload == nil then return end
local hex, opacityStr = payload:match("^(#%x%x%x%x%x%x):([%d%.]+)$")
if hex == nil then return end
pushCurrentColor(hex, tonumber(opacityStr) or 1)
saveHistoryToDisk()
end
-- Opens Noctalia's native color picker dialog (canvas + hue slider).
-- Only updates the selection, same as the hyprpicker panel button --
-- history commit still happens on panel close, not here. The panel
-- re-renders on its own via noctalia.state.watch("selectedColor", ...).
local function handleNativePicker()
local current = noctalia.state.get("selectedColor") or "#FFFFFF"
local accepted = noctalia.openColorPicker(current, function(color)
if color == nil then
return
end
local copyText = color
if noctalia.getConfig("hyprpicker-format") == "rgb" then
copyText = formatRgb(color)
elseif noctalia.getConfig("hyprpicker-lowercase") then
copyText = color:lower()
end
noctalia.notify(tr_color_picker, tr("color_picked", { color = copyText }))
setSelected(color, noctalia.state.get("selectedOpacity") or 1)
pushCurrentColor(color, 1)
saveHistoryToDisk()
end)
if not accepted then
noctalia.notify(tr_color_picker, tr("picker_already_open"))
end
end
function onIpc(event: string, payload: string?)
if event == "pick" then
handlePick()
elseif event == "picker-dialog" then
handleNativePicker()
elseif event == "pick-panel" then
handlePickFromPanel()
elseif event == "commit" then
handleCommit(payload)
end
end
--==============================================
-- BOOT (runs once at load)
--==============================================
noctalia.mkdirAll(STATE_DIR)
loadHistoryFromDisk()
local bootHistory = noctalia.state.get("colorHistory") or {}
local bootTop = bootHistory[1]
if bootTop ~= nil then
setSelected(bootTop.hex, bootTop.opacity or 1)
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+31
View File
@@ -0,0 +1,31 @@
{
"color_picker": "Color Picker",
"no_color_picked": "No color picked yet.",
"recent_colors": "Recent Colors",
"opacity": "Opacity",
"no_history": "No colors in history yet.",
"color_copied": "Color {color} copied.",
"color_picked": "Color {color} picked.",
"hyprpicker_not_installed": "hyprpicker is not installed.",
"could_not_parse": "could not parse hyprpicker output.",
"glyph": "Glyph",
"hex": "HEX",
"rgb": "RGB",
"settings.hyprpicker_format.label": "Default Format",
"settings.hyprpicker_format.description": "Default color format to copy to clipboard (Hex or RGB).",
"settings.swatch_radius.label": "Swatches Corner Roundness",
"settings.swatch_radius.description": "Corner radius of the history swatches and current-color preview.",
"settings.hyprpicker_lowercase.label": "Lowercase Hex",
"settings.hyprpicker_lowercase.description": "Outputs the hexcode in lowercase.",
"settings.hyprpicker_scale.label": "Zoom Scale",
"settings.hyprpicker_scale.description": "Zoom lens magnification, from 1 to 10.",
"settings.hyprpicker_radius.label": "Zoom Radius",
"settings.hyprpicker_radius.description": "Zoom lens circle radius in pixels, from 1 to 1000.",
"settings.hyprpicker_no_zoom.label": "Disable Zoom Lens",
"settings.hyprpicker_no_zoom.description": "Turns off the magnifying zoom lens while picking.",
"settings.hyprpicker_disable_preview.label": "Disable Live Preview",
"settings.hyprpicker_disable_preview.description": "Turns off the live color preview while picking.",
"settings.hyprpicker_cursor.label": "Show cursor",
"settings.hyprpicker_cursor.description": "Includes the cursor in the frozen screen preview."
}
+31
View File
@@ -0,0 +1,31 @@
{
"color_picker": "Seletor de Cores",
"no_color_picked": "Nenhuma cor escolhida ainda.",
"recent_colors": "Cores Recentes",
"opacity": "Opacidade",
"no_history": "Ainda não há cores no histórico.",
"color_copied": "Cor {color} copiada.",
"color_picked": "Cor {color} selecionada.",
"hyprpicker_not_installed": "hyprpicker não está instalado.",
"could_not_parse": "não foi possível extrair a saída do hyprpicker.",
"glyph": "Símbolo",
"hex": "HEX",
"rgb": "RGB",
"settings.hyprpicker_format.label": "Formato Padrão",
"settings.hyprpicker_format.description": "Formato de cor padrão para copiar para a área de transferência (Hex ou RGB).",
"settings.swatch_radius.label": "Arredondamento de Cantos das Amostras",
"settings.swatch_radius.description": "Raio dos cantos das amostras do histórico e da prévia da cor atual.",
"settings.hyprpicker_lowercase.label": "Hexadecimal em Minúsculas",
"settings.hyprpicker_lowercase.description": "Exibe o código hexadecimal em letras minúsculas.",
"settings.hyprpicker_scale.label": "Escala do Zoom",
"settings.hyprpicker_scale.description": "Ampliação da lupa, de 1 a 10.",
"settings.hyprpicker_radius.label": "Raio do Zoom",
"settings.hyprpicker_radius.description": "Raio do círculo da lupa em pixels, de 1 a 1000.",
"settings.hyprpicker_no_zoom.label": "Desabilitar Lupa",
"settings.hyprpicker_no_zoom.description": "Dpesativa a lupa ao selecionar a cor.",
"settings.hyprpicker_disable_preview.label": "Desabilitar Prévia em Tempo Real",
"settings.hyprpicker_disable_preview.description": "Desativa a prévia da cor em tempo real ao selecionar.",
"settings.hyprpicker_cursor.label": "Mostrar cursor",
"settings.hyprpicker_cursor.description": "Inclui o cursor na prévia congelada da tela."
}
+26
View File
@@ -0,0 +1,26 @@
-- Bar [[widget]] entry:
-- Left Click: opens the panel,
-- Right Click: picks a color directly
-- and copies the hex to clipboard (no panel).
local glyph = barWidget.getConfig("glyph")
local tr_color_picker = noctalia.tr("color_picker");
local function render()
barWidget.setGlyph(glyph)
barWidget.setTooltip(tr_color_picker)
end
function onClick()
noctalia.togglePanel("oldirtty/color_picker:panel")
end
function onRightClick()
noctalia.runAsync("noctalia msg plugin oldirtty/color_picker:service all pick", function(result)
if result.exitCode ~= 0 then
noctalia.notifyError(tr_color_picker, noctalia.tr("could_not_parse"))
end
end)
end
render()