diff --git a/color_picker/README.md b/color_picker/README.md new file mode 100644 index 0000000..8e26623 --- /dev/null +++ b/color_picker/README.md @@ -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. diff --git a/color_picker/panel.luau b/color_picker/panel.luau new file mode 100644 index 0000000..0dec221 --- /dev/null +++ b/color_picker/panel.luau @@ -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 \ No newline at end of file diff --git a/color_picker/plugin.toml b/color_picker/plugin.toml new file mode 100644 index 0000000..0cabff5 --- /dev/null +++ b/color_picker/plugin.toml @@ -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" diff --git a/color_picker/service.luau b/color_picker/service.luau new file mode 100644 index 0000000..5aacd0e --- /dev/null +++ b/color_picker/service.luau @@ -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 = ":", 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 \ No newline at end of file diff --git a/color_picker/thumbnail.webp b/color_picker/thumbnail.webp new file mode 100644 index 0000000..2f2b2af Binary files /dev/null and b/color_picker/thumbnail.webp differ diff --git a/color_picker/translations/en.json b/color_picker/translations/en.json new file mode 100644 index 0000000..fd0eebe --- /dev/null +++ b/color_picker/translations/en.json @@ -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." +} \ No newline at end of file diff --git a/color_picker/translations/pt-BR.json b/color_picker/translations/pt-BR.json new file mode 100644 index 0000000..dc85550 --- /dev/null +++ b/color_picker/translations/pt-BR.json @@ -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." +} \ No newline at end of file diff --git a/color_picker/widget.luau b/color_picker/widget.luau new file mode 100644 index 0000000..ec4769c --- /dev/null +++ b/color_picker/widget.luau @@ -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() \ No newline at end of file