Files
community-plugins/color_picker/service.luau
T

297 lines
8.7 KiB
Luau

-- [[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 = 6
local PERSISTENT_DIR = noctalia.pluginDataDir()
local HISTORY_FILE = PERSISTENT_DIR .. "/history.json"
local SELECTED_FILE = PERSISTENT_DIR .. "/selected.json"
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
--==============================================
-- 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: persisted color, state is saved once
-- per panel session when closed.
-- colorHistory: persisted list.
-- 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 raw = noctalia.readFile(HISTORY_FILE)
if raw == nil or raw == "" then return end
local decoded = noctalia.json.decode(raw)
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
local function loadSelectedFromDisk()
local raw = noctalia.readFile(SELECTED_FILE)
if raw == nil or raw == "" then return end
local decoded = noctalia.json.decode(raw)
if type(decoded) ~= "table" or decoded.hex == nil then return end
setSelected(decoded.hex, decoded.opacity or 1)
end
local function saveSelectedToDisk()
local hex = noctalia.state.get("selectedColor")
if hex == nil then return end
local opacity = tonumber(noctalia.state.get("selectedOpacity")) or 1
local encoded = noctalia.json.encode({ hex = hex, opacity = opacity })
if encoded ~= nil then
noctalia.writeFile(SELECTED_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".
-- "push-to-history" - saves the initial panel color to history when it's first changed.
-- "save-selected" - panel is closing for real. Saves the current draft to SELECTED_FILE.
--==============================================
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 handlePushToHistory(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
local function handleNativePicker()
local current = noctalia.state.get("selectedColor") or "#FFFFFF"
local accepted = noctalia.openColorPicker(current, function(color)
if color == nil then
return
end
setSelected(color, noctalia.state.get("selectedOpacity") or 1)
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 == "push-to-history" then
handlePushToHistory(payload)
elseif event == "save-selected" then
saveSelectedToDisk()
end
end
--==============================================
-- BOOT (runs once at load)
--==============================================
function load()
noctalia.mkdirAll(PERSISTENT_DIR)
loadHistoryFromDisk()
loadSelectedFromDisk()
end
load()