172 lines
4.9 KiB
Luau
172 lines
4.9 KiB
Luau
--!nonstrict
|
|
-- valw toolbar panel: the four modes, the timer and the options, drawn
|
|
-- with Noctalia's own components. The choices live in valw's toolbar.toml:
|
|
-- read with `valw toolbar --state`, changed with `valw toolbar --set`.
|
|
-- A mode click closes the panel, waits for it to go, then captures.
|
|
|
|
local MODES = {
|
|
{ id = "screen", glyph = "device-desktop" },
|
|
{ id = "region", glyph = "crop" },
|
|
{ id = "window", glyph = "app-window" },
|
|
{ id = "zoom", glyph = "zoom-in" },
|
|
}
|
|
local OPTIONS = { "cursor", "preview", "sound" }
|
|
local KEYS = { "mode", "timer", "cursor", "preview", "sound" }
|
|
|
|
-- The decoded `--state` JSON; nil until it arrives.
|
|
local state = nil
|
|
local unavailable = false
|
|
|
|
-- valw's stderr on an error starts with "error: <message>", then cause,
|
|
-- hint and log lines.
|
|
local function errorMessage(lines)
|
|
for _, line in ipairs(lines) do
|
|
local message = string.match(line, "^error: (.+)$")
|
|
if message then
|
|
return message
|
|
end
|
|
end
|
|
return noctalia.tr("error.failed")
|
|
end
|
|
|
|
local function linesOf(text)
|
|
local lines = {}
|
|
for line in string.gmatch(text or "", "[^\n]+") do
|
|
table.insert(lines, line)
|
|
end
|
|
return lines
|
|
end
|
|
|
|
-- Exit 3 is a cancel (Esc): not an error.
|
|
local function report(result)
|
|
if result.exitCode ~= 0 and result.exitCode ~= 3 then
|
|
noctalia.notifyError("valw", errorMessage(linesOf(result.stderr)))
|
|
end
|
|
end
|
|
|
|
-- A capture lasts as long as the user takes (a region, a zoom, the timer),
|
|
-- so it runs as a stream: runAsync with a callback kills its command after
|
|
-- 5 s. stderr joins the stream and the exit status comes last.
|
|
local function capture(command)
|
|
local lines = {}
|
|
local started = noctalia.runStream("(" .. command .. ") 2>&1; echo valw-exit:$?", function(line)
|
|
local code = string.match(line, "^valw%-exit:(%d+)$")
|
|
if code == nil then
|
|
table.insert(lines, line)
|
|
elseif code ~= "0" and code ~= "3" then
|
|
noctalia.notifyError("valw", errorMessage(lines))
|
|
end
|
|
end)
|
|
if not started then
|
|
noctalia.notifyError("valw", noctalia.tr("error.failed"))
|
|
end
|
|
end
|
|
|
|
local render
|
|
|
|
-- Every choice goes with every --set, not just the changed one: each
|
|
-- --set loads, changes and saves the file, so two quick changes sending
|
|
-- one key each could undo each other. The values are valw's own tokens
|
|
-- (from --state or our lists), never user text.
|
|
local function remember(key, value)
|
|
state[key] = value
|
|
render()
|
|
local sets = {}
|
|
for _, k in ipairs(KEYS) do
|
|
table.insert(sets, "--set " .. k .. "=" .. tostring(state[k]))
|
|
end
|
|
noctalia.runAsync("valw toolbar " .. table.concat(sets, " "), report)
|
|
end
|
|
|
|
local function modeButton(m)
|
|
return ui.button({
|
|
text = noctalia.tr("mode." .. m.id),
|
|
glyph = m.glyph,
|
|
variant = if state.mode == m.id then "primary" else "outline",
|
|
onClick = function()
|
|
panel.close()
|
|
capture("sleep 0.3 && valw toolbar --run " .. m.id)
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function optionRow(key)
|
|
return ui.row({ gap = 8, align = "center", justify = "space_between" }, {
|
|
ui.label({ text = noctalia.tr("option." .. key), color = "on_surface" }),
|
|
ui.toggle({
|
|
checked = state[key],
|
|
onChange = function(value)
|
|
remember(key, value == "true")
|
|
end,
|
|
}),
|
|
})
|
|
end
|
|
|
|
render = function()
|
|
if unavailable then
|
|
panel.render(ui.column({ padding = 16 }, {
|
|
ui.label({ text = noctalia.tr("panel.unavailable"), color = "error" }),
|
|
}))
|
|
return
|
|
end
|
|
if state == nil then
|
|
return
|
|
end
|
|
local modes = {}
|
|
for _, m in ipairs(MODES) do
|
|
table.insert(modes, modeButton(m))
|
|
end
|
|
local timers, selected = {}, 0
|
|
for i, t in ipairs(state.timers) do
|
|
table.insert(timers, if t == 0 then noctalia.tr("timer.off") else tostring(t) .. " s")
|
|
if t == state.timer then
|
|
selected = i - 1
|
|
end
|
|
end
|
|
local rows = {
|
|
ui.row({ gap = 8, justify = "space_between" }, modes),
|
|
ui.separator({}),
|
|
ui.row({ gap = 8, align = "center", justify = "space_between" }, {
|
|
ui.label({ text = noctalia.tr("timer.label"), color = "on_surface" }),
|
|
ui.select({
|
|
options = timers,
|
|
selectedIndex = selected,
|
|
onChange = function(index)
|
|
remember("timer", state.timers[tonumber(index) + 1])
|
|
end,
|
|
}),
|
|
}),
|
|
}
|
|
for _, key in ipairs(OPTIONS) do
|
|
table.insert(rows, optionRow(key))
|
|
end
|
|
panel.render(ui.column({ gap = 12, padding = 16 }, rows))
|
|
end
|
|
|
|
local function fail(message)
|
|
unavailable = true
|
|
noctalia.notifyError("valw", message)
|
|
render()
|
|
end
|
|
|
|
function onOpen(_context)
|
|
unavailable = false
|
|
if not noctalia.commandExists("valw") then
|
|
fail(noctalia.tr("panel.unavailable"))
|
|
return
|
|
end
|
|
noctalia.runAsync("valw toolbar --state", function(result)
|
|
if result.exitCode ~= 0 then
|
|
fail(errorMessage(linesOf(result.stderr)))
|
|
return
|
|
end
|
|
local decoded = noctalia.json.decode(result.stdout)
|
|
if type(decoded) ~= "table" then
|
|
fail(noctalia.tr("panel.unavailable"))
|
|
return
|
|
end
|
|
state = decoded
|
|
render()
|
|
end)
|
|
end
|