diff --git a/screen-toolkit/README.md b/screen-toolkit/README.md index 012a000..2a20178 100644 --- a/screen-toolkit/README.md +++ b/screen-toolkit/README.md @@ -16,7 +16,7 @@ not the original implementation. | Field | Value | | --- | --- | | ID | `alexander/screen-toolkit` | -| Entries | Bar widget: `widget`; control-center shortcut: `toggle`; panels: `panel` (compact tools), `panel-full` (full tools), `result` (result view); service: `service` | +| Entries | Bar widget: `widget`; control-center shortcut: `toggle`; panels: `panel` (compact tools), `panel-full` (full tools), `panel-legacy` (legacy layout), `result` (result view); service: `service` | ## Requirements @@ -35,6 +35,7 @@ reported when that feature is started. - **`stat`** — recording file size - **`pkill`** — stopping active recording backends - **`xdg-open`** — opening URLs, OCR search results, and shared-link targets +- **`mpv`** — open recording preview in legacy mode subpanel Recording requires at least one backend: @@ -60,16 +61,24 @@ Settings → Control Center shortcuts. Left-click either one opens the main pane (or stops a recording); right-clicking the bar widget quick-picks a color. While a recording is active, the widget and shortcut show a pulsing red dot. -The main panel has two layouts, selected by the `panel-mode` setting (default: +The main panel has three layouts, selected by the `panel-mode` setting (default: **Full**): -- **Full** — the spacious original grid with section titles (`panel-full`, - 660×340). -- **Compact** — a dense grid grouped into tinted sections (`panel`, - 380×260). +- **Full** — the spacious original grid with section titles (`panel-full`, 660×340). +- **Compact** — a dense grid grouped into tinted sections (`panel`, 380×290). +- **Legacy** — recreates the Noctalia-v4 layout (`panel-legacy`, 380×260). + +### Legacy mode +Legacy mode recreates the original Noctalia v4 screen-toolkit layout: + +> **Note:** The features below are exclusive to the **Legacy** layout. + +- Dedicated Markup and Recording subpanels. +- Preview and one-click access to the most recent screenshot or recording. +- Quick microphone and system audio toggles in the `Record` subpanel. The widget and shortcut open whichever entry matches the setting; the panel -footnote under Settings → Plugins shows both panels' placement/position +footnote under Settings → Plugins shows all three panels' placement/position options. Toggle the tools panel (opens the entry matching the `panel-mode` setting): @@ -84,8 +93,7 @@ For example, bind it to `SUPER+P` in Hyprland: bind = SUPER, P, exec, noctalia msg plugin alexander/screen-toolkit:service all toggle ``` -Because `toggle` reads the `panel-mode` setting, this single bind works in both -full and compact mode — no need to change the keybind when you switch layouts. +Because `toggle` reads the `panel-mode` setting, this single bind works in all three layouts (Full, Compact and Legacy), so there is no need to change the keybind when you switch layouts. Open the compact tools panel: @@ -99,6 +107,12 @@ Open the full tools panel (the original spacious grid): noctalia msg panel-toggle alexander/screen-toolkit:panel-full ``` +Open the legacy tools panel (the 4×2 grid with subpanels): + +```sh +noctalia msg panel-toggle alexander/screen-toolkit:panel-legacy +``` + Open the result panel (shows the last capture/recording output): ```sh @@ -164,7 +178,7 @@ All settings live in Settings → Plugins (gear on the plugin's row). | `record-skip-confirmation` | `bool` | `false` | Save automatically when a recording ends, skipping the save dialog. | | `record-copy-to-clipboard` | `bool` | `false` | Finalize to MP4 and copy the file URI when recording ends. | | `gif-max-seconds` | `int` | `30` | Cap for GIF recordings (1–600 s). | -| `panel-mode` | `select` | `full` | Main panel layout: `full` (spacious original grid, 660×340) or `compact` (dense grid, 380×260). | +| `panel-mode` | `select` | `full` | Main panel layout: `full` (spacious original grid, 660×340) or `compact` (dense grid, 380×290) or `legacy` (4×2 legacy grid, 380×260). | ## IPC @@ -195,7 +209,7 @@ noctalia msg plugin alexander/screen-toolkit:service all clearHistory ``` `toggle` opens the tools panel **matching the `panel-mode` setting** — one IPC -that works for both layouts. The bar widget and control-center shortcut use the +that works for all three layouts. The bar widget and control-center shortcut use the same mode-aware toggle. Commands that take a payload: @@ -244,10 +258,10 @@ Summary of every service command: ## Notes -- **Two panel entries, one layout setting.** Panel size is host-owned: the host +- **Three panel entries, one layout setting.** Panel size is host-owned: the host sizes each `[[panel]]` entry from its `width`/`height`, and there is no runtime - resize. So the two layouts are two entries sharing `panel.luau`: `panel-full` - (660×340, the original spacious grid) and `panel` (380×260, the compact grid). + resize. So the three layouts are three entries sharing `panel.luau`: `panel-full` + (660×340, the original spacious grid), `panel` (380×290, the compact grid) and `panel-legacy` (380×260, the legacy grid). The `panel-mode` setting picks which one renders, and `toggle`/widget/shortcut all open the matching entry. Changing the setting only affects which panel opens next time; an already-open panel keeps its current size until closed. diff --git a/screen-toolkit/panel.luau b/screen-toolkit/panel.luau index 5697d83..008da50 100644 --- a/screen-toolkit/panel.luau +++ b/screen-toolkit/panel.luau @@ -11,9 +11,17 @@ -- state channel. Capture tools close the panel first so the capture never -- includes it. +local activeView = "main" +local recordAudioOut = (noctalia.getConfig("record-audio-out") == true) +local recordAudioIn = false +local hoveredAudioIn = false +local hoveredAudioOut = false +local hoveredBack = false + local panelOpen = false local recording = noctalia.state.get("recording") == true local hoveredTool = nil +local focusedTool local focusedIndex = 0 local hoveredClose = false local hoveredStop = false local render @@ -29,36 +37,48 @@ local function send(action, payload) noctalia.state.set("command", { action = action, payload = payload }) end +local function shellQuote(value) + return "'" .. tostring(value):gsub("'", [["'"']]) .. "'" +end + local function getMode() - return noctalia.getConfig("panel-mode") == "full" and "full" or "compact" + local mode = noctalia.getConfig("panel-mode") + if mode == "full" or mode == "compact" or mode == "legacy" then return mode end + return "full" end -- Capture tools must not see the panel in the frame, so close it first. local CAPTURE_ACTIONS = { - colorPicker = true, - ocr = true, - qr = true, - palette = true, - lens = true, - measure = true, - annotate = true, - annotateFullscreen = true, - annotateWindow = true, - record = true, - recordMp4 = true, - recordFullscreen = true, - recordFullscreenMp4 = true, + colorPicker = true, ocr = true, qr = true, palette = true, lens = true, measure = true, + annotate = true, annotateFullscreen = true, annotateWindow = true, + record = true, recordMp4 = true, recordFullscreen = true, recordFullscreenMp4 = true, } local function runTool(action) - if CAPTURE_ACTIONS[action] then - panel.close() - end + if action == "record" or action == "recordFullscreen" then noctalia.state.set("recordFormat", "gif") + elseif action == "recordMp4" or action == "recordFullscreenMp4" then noctalia.state.set("recordFormat", "mp4") end + if CAPTURE_ACTIONS[action] then panel.close() end send(action) end --- ── Tool data ────────────────────────────────────────────────────────────── +local RECORD_THUMB = "/tmp/screen-toolkit-record-thumb.png" +local ANNOTATE_THUMB = "/tmp/screen-toolkit-annotate.png" +local lastCapturePath, lastRecordPath = nil, nil +local function refreshPreview() + local annotateResult = noctalia.state.get("annotateResult") + local recordPath = noctalia.state.get("recordPath") + if annotateResult and type(annotateResult) == "table" and annotateResult.capturePath and annotateResult.capturePath ~= "" then + lastCapturePath = annotateResult.capturePath + elseif noctalia.fileExists and noctalia.fileExists(ANNOTATE_THUMB) then + lastCapturePath = ANNOTATE_THUMB + end + if recordPath and type(recordPath) == "string" and recordPath ~= "" then lastRecordPath = recordPath end +end + +-- ── Tool data ────────────────────────────────────────────────────────────── + +-- For Compact-mode and Full-mode layouts local TOOLS = { { action = "colorPicker", label_key = "tools.colorpicker", glyph = "palette" }, { action = "ocr", label_key = "tools.ocr", glyph = "scan" }, @@ -76,148 +96,185 @@ local ANNOTATE_TOOLS = { local RECORD_TOOLS = { { action = "record", label_key = "tools.record_gif", glyph = "gif" }, - { action = "recordMp4", label_key = "tools.record_mp4", glyph = "movie" }, - { action = "recordFullscreen", label_key = "tools.record_fullscreen_gif", glyph = "video" }, + { action = "recordMp4", label_key = "tools.record_mp4", glyph = "video" }, + { action = "recordFullscreen", label_key = "tools.record_fullscreen_gif", glyph = "gif" }, { action = "recordFullscreenMp4", label_key = "tools.record_fullscreen_mp4", glyph = "video" }, } + -- For Legacy-mode layout +local TOOLS_LEGACY = { + { id = "colorPicker", action = "colorPicker", label_key = "tools.colorpicker", glyph = "palette" }, + { id = "palette", action = "palette", label_key = "tools.palette", glyph = "color-swatch" }, + { id = "ocr", action = "ocr", label_key = "tools.ocr", glyph = "scan" }, + { id = "qr", action = "qr", label_key = "tools.qr", glyph = "qrcode" }, + { id = "annotate", action = "view_annotate", label_key = "tools.annotate", glyph = "pencil" }, + { id = "record", action = "view_record", label_key = "tools.record", glyph = "video" }, + { id = "measure", action = "measure", label_key = "tools.measure", glyph = "ruler" }, + { id = "lens", action = "lens", label_key = "tools.lens", glyph = "search" }, +} + +-- ── Keyboard Navigation State & Helpers ──────────────────────────────────── + +local function getActiveItems() + local mode = getMode() + if mode == "compact" or mode == "full" then + local list = {} + for _, t in ipairs(TOOLS) do table.insert(list, { type = "tool", action = t.action }) end + for _, t in ipairs(ANNOTATE_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end + for _, t in ipairs(RECORD_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end + return list + else + if activeView == "annotate" then + local list = { { type = "back" } } + for _, t in ipairs(ANNOTATE_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end + table.insert(list, { type = "preview_screenshot" }) + return list + elseif activeView == "record" then + local list = { { type = "back" } } + for _, t in ipairs(RECORD_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end + table.insert(list, { type = "sysAudio" }) + table.insert(list, { type = "micAudio" }) + if recording then table.insert(list, { type = "stopBtn" }) end + table.insert(list, { type = "preview_recording" }) + return list + else + local list = {} + for _, t in ipairs(TOOLS_LEGACY) do table.insert(list, { type = "legacy_tile", item = t }) end + return list + end + end +end + +local function updateFocusedTool() + local items = getActiveItems() + if #items == 0 or focusedIndex == 0 or focusedIndex == nil then + focusedTool = nil + return + end + if focusedIndex > #items then focusedIndex = #items end + if focusedIndex < 1 then focusedIndex = 1 end + local cur = items[focusedIndex] + if cur then + if cur.type == "tool" then focusedTool = cur.action + elseif cur.type == "legacy_tile" then focusedTool = cur.item.id + else focusedTool = cur.type end + else + focusedTool = nil + end +end + +-- Preview handlers for record and annotate subpanels. +local function openScreenshotPreview() + refreshPreview() + local targetPath = lastCapturePath or (noctalia.fileExists and noctalia.fileExists(ANNOTATE_THUMB) and ANNOTATE_THUMB or nil) + if not targetPath or targetPath == "" or (noctalia.fileExists and not noctalia.fileExists(targetPath)) then + noctalia.notifyError("No screenshot preview available yet. Take a capture first!") + return + end + if noctalia.commandExists("satty") then noctalia.runAsync("satty --filename " .. shellQuote(targetPath)) + elseif noctalia.commandExists("swappy") then noctalia.runAsync("swappy -f " .. shellQuote(targetPath)) + elseif noctalia.commandExists("gimp") then noctalia.runAsync("gimp " .. shellQuote(targetPath)) + else noctalia.notifyError(tr("messages.missing_dep", { dep = "satty (or swappy/gimp)" })) end +end + +local function openRecordingPreview() + refreshPreview() + local targetPath = lastRecordPath + if not targetPath or targetPath == "" or (noctalia.fileExists and not noctalia.fileExists(targetPath)) then + noctalia.notifyError("No recording preview available yet. Make a recording first!") + return + end + if noctalia.commandExists("mpv") then noctalia.runAsync("mpv --no-config --title='Recording Preview' " .. shellQuote(targetPath)) + else noctalia.notifyError(tr("messages.missing_dep", { dep = "mpv" })) end +end + +local function headerActions(includeClose) + local actions = {} + if recording then + table.insert(actions, ui.button({ + key = "header-stop-btn", glyph = "circle", variant = "destructive", controlSize = "sm", tooltip = tr("panel.stop"), + onClick = function() hoveredStop = false; send("recordStop") end, + })) + end + if includeClose then + table.insert(actions, ui.button({ + key = "header-close-btn", glyph = "close", onClick = function() panel.close() end, + })) + end + return ui.row({ gap = 4 }, actions) +end + -- ── Compact mode ─────────────────────────────────────────────────────────── local function compactHeader() - local actions = { - ui.column({ - key = "close-button", - width = 24, - height = 24, - radius = 9, - fill = hoveredClose and "primary/0.12" or nil, - border = hoveredClose and "primary" or "outline", - borderWidth = 1, - align = "center", - justify = "center", - onClick = function() panel.close() end, - onHover = function(state) - hoveredClose = state == "true" - render() - end, - }, { - ui.glyph({ name = "close", size = 14, color = hoveredClose and "on_surface" or "on_surface_variant" }), - }), - } - if recording then - table.insert(actions, 1, ui.column({ - key = "stop-button", - width = 24, - height = 24, - radius = 9, - fill = "error", - border = hoveredStop and "on_surface" or nil, - borderWidth = hoveredStop and 2 or 0, - align = "center", - justify = "center", - tooltip = tr("panel.stop"), - onClick = function() send("recordStop") end, - onHover = function(state) - hoveredStop = state == "true" - render() - end, - }, { - ui.glyph({ name = "square", size = 12, color = "on_surface" }), - })) - end return ui.row({ align = "center", gap = 6 }, { ui.glyph({ name = "crosshair", size = 16, color = "primary" }), ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 16, fontWeight = "bold", color = "on_surface" }), - ui.row({ gap = 4 }, actions), + headerActions(true), }) end +local function setHoveredTool(action) + hoveredTool = action + if action then + for idx, item in ipairs(getActiveItems()) do + local id = (item.type == "tool") and item.action or ((item.type == "legacy_tile") and item.item.id or item.type) + if id == action then focusedIndex = idx; break end + end + else + focusedIndex = 0 + end + updateFocusedTool() +end + local function compactTile(t) - local color = hoveredTool == t.action and "primary" or "on_surface" + local isFocused = (hoveredTool == t.action) or (focusedTool == t.action) + local color = isFocused and "primary" or "on_surface" return ui.column({ - key = "tile-" .. t.action, - flexGrow = 1, - gap = 2, - align = "center", + key = "tile-" .. t.action, flexGrow = 1, gap = 2, align = "center", paddingV = 4, radius = 8, + fill = isFocused and "primary/0.15" or "surface_variant", border = isFocused and "primary" or "surface_variant", borderWidth = isFocused and 1 or 0, + onClick = function() runTool(t.action) end, + onHover = function(state) setHoveredTool((state == "true") and t.action or nil); render() end, }, { - ui.column({ - gap = 2, - align = "center", - onClick = function() runTool(t.action) end, - onHover = function(state) - hoveredTool = (state == "true") and t.action or nil - render() - end, - }, { - ui.column({ width = 30, height = 30, radius = 9, fill = "on_primary", border = color == "primary" and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, { - ui.glyph({ name = t.glyph, size = 16, color = color }), - }), - ui.label({ text = tr(t.label_key), maxWidth = 100, maxLines = 1, textAlign = "center", fontSize = 11, color = color }), + ui.column({ width = 30, height = 30, radius = 9, fill = isFocused and "primary/0.2" or "on_primary", border = isFocused and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, { + ui.glyph({ name = t.glyph, size = 16, color = color }), }), + ui.label({ text = tr(t.label_key), maxWidth = 100, maxLines = 1, textAlign = "center", fontSize = 11, color = color }), }) end local function compactToolsRow(tools) local row = {} - for _, t in ipairs(tools) do - row[#row + 1] = compactTile(t) - end + for _, t in ipairs(tools) do row[#row + 1] = compactTile(t) end return ui.row({ flexGrow = 1, gap = 2 }, row) end local function compactSection(tools) - return ui.column({ gap = 4, paddingV = 8, paddingH = 6, radius = 12, fill = "surface_variant" }, { - compactToolsRow(tools), - }) + return ui.column({ gap = 4, paddingV = 8, paddingH = 6, radius = 12, fill = "surface_variant" }, { compactToolsRow(tools) }) end local function compactGrid() - return ui.column({ gap = 6 }, { - compactSection(TOOLS), - compactSection(ANNOTATE_TOOLS), - compactSection(RECORD_TOOLS), - }) + return ui.column({ gap = 6 }, { compactSection(TOOLS), compactSection(ANNOTATE_TOOLS), compactSection(RECORD_TOOLS) }) end -- ── Full mode ────────────────────────────────────────────────────────────── local function fullHeader() - local actions = { - ui.button({ glyph = "close", onClick = function() panel.close() end }), - } - if recording then - table.insert(actions, 1, ui.button({ - glyph = "square", - variant = "destructive", - controlSize = "sm", - tooltip = tr("panel.stop"), - onClick = function() send("recordStop") end, - })) - end return ui.row({ align = "center", gap = 10 }, { ui.glyph({ name = "crosshair", size = 24, color = "primary" }), ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 22, fontWeight = "bold", color = "on_surface" }), - ui.row({ gap = 4 }, actions), + headerActions(true), }) end local function fullTile(t) - local color = hoveredTool == t.action and "primary" or "on_surface" + local isFocused = (hoveredTool == t.action) or (focusedTool == t.action) + local color = isFocused and "primary" or "on_surface" return ui.column({ - key = "tile-" .. t.action, - flexGrow = 1, - gap = 6, - align = "center", - paddingV = 6, - radius = 12, - fill = "surface_variant", - borderWidth = 1, - border = color == "primary" and "primary" or "outline", + key = "tile-" .. t.action, flexGrow = 1, gap = 6, align = "center", paddingV = 6, radius = 12, + fill = "surface_variant", borderWidth = 1, border = color == "primary" and "primary" or "outline", onClick = function() runTool(t.action) end, - onHover = function(state) - hoveredTool = (state == "true") and t.action or nil - render() - end, + onHover = function(state) setHoveredTool((state == "true") and t.action or nil); render() end, }, { ui.column({ width = 30, height = 30, radius = 9, fill = "on_primary", border = color == "primary" and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, { ui.glyph({ name = t.glyph, size = 16, color = color }), @@ -228,9 +285,7 @@ end local function fullToolsRow(tools) local row = {} - for _, t in ipairs(tools) do - row[#row + 1] = fullTile(t) - end + for _, t in ipairs(tools) do row[#row + 1] = fullTile(t) end return ui.row({ flexGrow = 1, gap = 6 }, row) end @@ -240,43 +295,261 @@ end local function fullGrid() return ui.column({ flexGrow = 1, gap = 4, justify = "space_between" }, { - sectionLabel(tr("panel.section_capture")), - fullToolsRow(TOOLS), - sectionLabel(tr("panel.section_annotate")), - fullToolsRow(ANNOTATE_TOOLS), - sectionLabel(tr("panel.section_record")), - fullToolsRow(RECORD_TOOLS), + sectionLabel(tr("panel.section_capture")), fullToolsRow(TOOLS), + sectionLabel(tr("panel.section_annotate")), fullToolsRow(ANNOTATE_TOOLS), + sectionLabel(tr("panel.section_record")), fullToolsRow(RECORD_TOOLS), }) end +-- ── Legacy mode ──────────────────────────────────────────────────────────── + +local function legacyHeader() + return ui.row({ align = "center", gap = 5 }, { + ui.glyph({ name = "crosshair", size = 18, color = "primary" }), + ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 18, fontWeight = "bold", color = "on_surface" }), + headerActions(false), + }) +end + +local function SubpanelHeader(title) + local isBackFocused = hoveredBack or (focusedTool == "back") + return ui.row({ align = "center", gap = 5 }, { + ui.glyph({ name = "crosshair", size = 18, color = "primary" }), + ui.label({ text = title, flexGrow = 1, fontSize = 18, fontWeight = "bold", color = "on_surface" }), + ui.column({ + key = "back-button", height = 30, width = 30, paddingH = 0, radius = 9, + fill = isBackFocused and "primary/0.12" or "surface", border = isBackFocused and "primary" or "surface", borderWidth = 1, + align = "center", justify = "center", + onClick = function() activeView = "main"; focusedIndex = 0; updateFocusedTool(); render() end, + onHover = function(state) setHoveredTool((state == "true") and "back" or nil); render() end, + }, { + ui.row({ align = "center", justify = "center", gap = 4 }, { + ui.glyph({ name = "chevron-left", size = 16, color = isBackFocused and "primary" or "on_surface" }), + }), + }), + }) +end + +local function legacyTile(t) + local isFocused = (hoveredTool == t.id) or (focusedTool == t.id) + local color = isFocused and "primary" or "on_surface" + local onClick = function() + if t.action == "view_annotate" then activeView = "annotate"; focusedIndex = 0; updateFocusedTool(); render() + elseif t.action == "view_record" then activeView = "record"; focusedIndex = 0; updateFocusedTool(); render() + else runTool(t.action) end + end + return ui.column({ + key = "tile-" .. t.id, flexGrow = 1, gap = 2, paddingV = 5, paddingH = 0, align = "center", justify = "center", onClick = onClick, + onHover = function(state) setHoveredTool((state == "true") and t.id or nil); render() end, + }, { + ui.column({ width = 50, height = 50, radius = 15, fill = "on_primary", border = color == "primary" and "primary" or "outline", borderWidth = isFocused and 2 or 0, align = "center", justify = "center" }, { + ui.glyph({ name = t.glyph, size = 25, color = color }), + }), + ui.label({ text = tr(t.label_key), maxWidth = 80, maxLines = 1, textAlign = "center", fontSize = 11, color = color }), + }) +end + +local function legacyGrid() + local row1, row2 = {}, {} + for i, t in ipairs(TOOLS_LEGACY) do table.insert(i <= 4 and row1 or row2, legacyTile(t)) end + return ui.column({ gap = 10, paddingV = 15, paddingH = 20, radius = 18, fill = "surface_variant" }, { + ui.row({ flexGrow = 1, gap = 0 }, row1), ui.row({ flexGrow = 1, gap = 0 }, row2), + }) +end + +-- Subpanels for legacy mode: annotate and record. + +local function subpanelButton(t, paddingV) + local isFocused = (hoveredTool == t.action) or (focusedTool == t.action) + local color = isFocused and "primary" or "on_surface" + return ui.column({ + key = "btn-" .. t.action, paddingV = paddingV, paddingH = 5, radius = 8, + fill = isFocused and "primary/0.12" or "surface", border = isFocused and "primary" or "surface", borderWidth = 1, + onClick = function() runTool(t.action) end, + onHover = function(state) setHoveredTool((state == "true") and t.action or nil); render() end, + }, { + ui.row({ align = "center", gap = 6 }, { + ui.glyph({ name = t.glyph, size = 15, color = color }), + ui.label({ text = tr(t.label_key), fontSize = 12, fontWeight = "bold", color = color }), + }), + }) +end + +local function MarkupSubpanel() + refreshPreview() + local leftButtons = {} + for _, t in ipairs(ANNOTATE_TOOLS) do table.insert(leftButtons, subpanelButton(t, 6)) end + local fileExists = noctalia.fileExists + local hasCaptureFile = lastCapturePath and lastCapturePath ~= "" and (not fileExists or fileExists(lastCapturePath)) + local isPreviewFocused = (focusedTool == "preview_screenshot") + local rightPreview = ui.column({ + width = 200, height = 150, align = "center", justify = "center", padding = hasCaptureFile and 4 or 8, radius = 10, fill = "surface", + border = isPreviewFocused and "primary" or "outline", borderWidth = isPreviewFocused and 2 or 1, onClick = openScreenshotPreview, + }, hasCaptureFile and { + ui.image({ path = lastCapturePath, width = 200, height = 130, fit = "contain", radius = 6 }), + ui.label({ text = "Click to open in Satty", fontSize = 10, fontWeight = "bold", color = "primary" }), + } or { + ui.glyph({ name = "pencil", size = 28, color = "on_surface_variant" }), + ui.label({ text = "No screenshot yet", fontSize = 11, color = "on_surface_variant" }), + }) + return ui.column({ flexGrow = 1, gap = 10 }, { + SubpanelHeader("Markup"), + ui.column({ gap = 10, paddingV = 15, paddingH = 20, radius = 18, fill = "surface_variant" }, { + ui.row({ flexGrow = 1, gap = 10 }, { ui.column({ width = 100, gap = 6 }, leftButtons), rightPreview }), + }), + }) +end + +local function RecordSubpanel() + refreshPreview() + local leftButtons = {} + for _, t in ipairs(RECORD_TOOLS) do table.insert(leftButtons, subpanelButton(t, 4)) end + local stopFocused = hoveredStop or (focusedTool == "stopBtn") + local stopBtn = ui.column({ + key = "btn-stop-record", width = 28, height = 28, radius = 6, fill = "error", border = stopFocused and "primary" or "error", borderWidth = stopFocused and 2 or 1, align = "center", justify = "center", + onClick = function() send("recordStop") end, + onHover = function(state) setHoveredTool((state == "true") and "stopBtn" or nil); render() end, + }, { ui.glyph({ name = "circle", size = 16, color = "on_primary", align = "center", justify = "center" }) }) + + local sysAudioFocused = hoveredAudioOut or (focusedTool == "sysAudio") + local sysAudioBtn = ui.column({ + key = "toggle-sys-audio", width = 28, height = 28, radius = 6, + fill = recordAudioOut and (sysAudioFocused and "primary/0.25" or "primary/0.15") or (sysAudioFocused and "surface_variant" or "surface_variant/0.5"), + border = (recordAudioOut or sysAudioFocused) and "primary" or "outline", borderWidth = 1, align = "center", justify = "center", + onClick = function() recordAudioOut = not recordAudioOut; send("setAudioOut", recordAudioOut); render() end, + onHover = function(state) setHoveredTool((state == "true") and "sysAudio" or nil); render() end, + }, { ui.glyph({ name = recordAudioOut and "volume-2" or "volume-x", size = 16, color = (recordAudioOut or sysAudioFocused) and "primary" or "on_surface_variant" }) }) + + local micAudioFocused = hoveredAudioIn or (focusedTool == "micAudio") + local micAudioBtn = ui.column({ + key = "toggle-mic-audio", width = 28, height = 28, radius = 6, + fill = recordAudioIn and (micAudioFocused and "primary/0.25" or "primary/0.15") or (micAudioFocused and "surface_variant" or "surface_variant/0.5"), + border = (recordAudioIn or micAudioFocused) and "primary" or "outline", borderWidth = 1, align = "center", justify = "center", + onClick = function() recordAudioIn = not recordAudioIn; send("setAudioIn", recordAudioIn); render() end, + onHover = function(state) setHoveredTool((state == "true") and "micAudio" or nil); render() end, + }, { ui.glyph({ name = recordAudioIn and "microphone" or "microphone-off", size = 16, color = (recordAudioIn or micAudioFocused) and "primary" or "on_surface_variant" }) }) + + local controlsList = { sysAudioBtn, micAudioBtn } + if recording then table.insert(controlsList, 1, stopBtn) end + local controlRow = ui.row({ gap = 6, align = "center", justify = "center" }, controlsList) + local fileExists = noctalia.fileExists + local hasRecordFile = lastRecordPath and lastRecordPath ~= "" and (not fileExists or fileExists(lastRecordPath)) + local hasRecordThumb = hasRecordFile and fileExists and fileExists(RECORD_THUMB) + local isPreviewFocused = (focusedTool == "preview_recording") + local rightPreview + if hasRecordFile or hasRecordThumb then + local thumbElement = hasRecordThumb and ui.image({ path = RECORD_THUMB, width = 240, height = 115, fit = "contain", radius = 6 }) or ui.glyph({ name = "movie", size = 28, color = "primary" }) + local filename = lastRecordPath and (lastRecordPath:match("([^/]+)$") or lastRecordPath) or "Saved Recording" + rightPreview = ui.column({ + width = 200, height = 150, align = "center", justify = "center", padding = hasRecordFile and 4 or 8, radius = 10, fill = "surface", + border = isPreviewFocused and "primary" or "outline", borderWidth = isPreviewFocused and 2 or 1, onClick = openRecordingPreview, + }, { thumbElement, ui.label({ text = filename, maxWidth = 160, maxLines = 1, fontSize = 9, color = "on_surface_variant" }), ui.label({ text = "Click to play in MPV", fontSize = 10, fontWeight = "bold", color = "primary" }) }) + else + rightPreview = ui.column({ + width = 200, height = 130, align = "center", justify = "center", padding = 8, radius = 10, fill = "surface", + border = isPreviewFocused and "primary" or "outline", borderWidth = isPreviewFocused and 2 or 1, onClick = openRecordingPreview, + }, { ui.glyph({ name = "video", size = 28, color = "on_surface_variant" }), ui.label({ text = "No recording yet", fontSize = 11, color = "on_surface_variant" }) }) + end + local leftColumn = ui.column({ width = 100, gap = 6, align = "center" }, { + ui.column({ width = 100, gap = 6 }, leftButtons), + controlRow, + }) + return ui.column({ flexGrow = 1, gap = 10 }, { + SubpanelHeader("Record"), + ui.column({ gap = 10, paddingV = 15, paddingH = 20, radius = 18, fill = "surface_variant" }, { + ui.row({ flexGrow = 1, gap = 10 }, { leftColumn, rightPreview }), + }), + }) +end + +-- ── Keyboard Handler ─────────────────────────────────────────────────────── + +function onKey(chord, pressed) + if not pressed then return end + local key = tostring(chord):lower() + local items = getActiveItems() + if #items == 0 then return end + local mode = getMode() + if chord == "Escape" then panel.close() return end + + -- If nothing is focused yet (focusedIndex == 0), pressing arrow keys/tab takes focus directly to 1st square + if focusedIndex == 0 or focusedIndex == nil then + if key == "tab" or key == "right" or key == "left" or key == "backtab" or key == "shift+tab" or key == "down" or key == "up" then + focusedIndex = 1 + updateFocusedTool() + render() + return + end + end + + if key == "tab" or key == "right" then + focusedIndex = (focusedIndex % #items) + 1 + elseif key == "backtab" or key == "left" then + focusedIndex = (focusedIndex - 2) % #items + 1 + elseif key == "down" then + if mode == "legacy" and activeView == "main" then + focusedIndex = focusedIndex <= 4 and (focusedIndex + 4) or (focusedIndex - 4) + elseif mode == "compact" or mode == "full" then + focusedIndex = (focusedIndex <= 6 and math.min(7 + math.floor((focusedIndex - 1) / 2), 9)) or (focusedIndex <= 9 and (10 + (focusedIndex - 7)) or ((focusedIndex - 10) + 1)) + else + focusedIndex = (focusedIndex % #items) + 1 + end + elseif key == "up" then + if mode == "legacy" and activeView == "main" then + focusedIndex = focusedIndex > 4 and (focusedIndex - 4) or (focusedIndex + 4) + elseif mode == "compact" or mode == "full" then + focusedIndex = (focusedIndex >= 10 and math.min(7 + (focusedIndex - 10), 9)) or (focusedIndex >= 7 and ((focusedIndex - 7) * 2 + 1) or math.min(10 + math.floor((focusedIndex - 1) / 2), 13)) + else + focusedIndex = (focusedIndex - 2) % #items + 1 + end + elseif key == "return" or key == "enter" or key == "space" then + local current = items[focusedIndex] + if current then + if current.type == "tool" then runTool(current.action) + elseif current.type == "legacy_tile" then + if current.item.action == "view_annotate" then activeView = "annotate"; focusedIndex = 0 + elseif current.item.action == "view_record" then activeView = "record"; focusedIndex = 0 + else runTool(current.item.action) end + elseif current.type == "back" then activeView = "main"; focusedIndex = 0 + elseif current.type == "sysAudio" then recordAudioOut = not recordAudioOut; send("setAudioOut", recordAudioOut) + elseif current.type == "micAudio" then recordAudioIn = not recordAudioIn; send("setAudioIn", recordAudioIn) + elseif current.type == "stopBtn" then send("recordStop") + elseif current.type == "preview_screenshot" then openScreenshotPreview() + elseif current.type == "preview_recording" then openRecordingPreview() end + end + end + updateFocusedTool() + render() +end + -- ── Main render ──────────────────────────────────────────────────────────── render = function() - local body - if getMode() == "full" then - body = ui.column({ flexGrow = 1, gap = 10 }, { - fullHeader(), - fullGrid(), - }) - else - body = ui.column({ flexGrow = 1, gap = 6 }, { - compactHeader(), - compactGrid(), - }) - end + updateFocusedTool() + local mode = getMode() + local body = (mode == "full" and ui.column({ flexGrow = 1, gap = 10 }, { fullHeader(), fullGrid() })) + or (mode == "compact" and ui.column({ flexGrow = 1, gap = 6 }, { compactHeader(), compactGrid() })) + or (activeView == "annotate" and MarkupSubpanel()) + or (activeView == "record" and RecordSubpanel()) + or ui.column({ flexGrow = 1, gap = 6 }, { legacyHeader(), legacyGrid() }) panel.render(body) end -- ── Lifecycle ────────────────────────────────────────────────────────────── function onOpen(_context) - panelOpen = true - hoveredTool = nil + panelOpen, hoveredTool, focusedIndex, activeView = true, false, 0, "main" + recordAudioOut = (noctalia.getConfig("record-audio-out") == true) + recordAudioIn = false + send("setAudioOut", recordAudioOut) + send("setAudioIn", false) + refreshPreview() + updateFocusedTool() render() end function onClose() - panelOpen = false + panelOpen, hoveredBack = false, false end noctalia.state.watch("recording", function(v) diff --git a/screen-toolkit/plugin.toml b/screen-toolkit/plugin.toml index 847e4f2..6506cf1 100644 --- a/screen-toolkit/plugin.toml +++ b/screen-toolkit/plugin.toml @@ -8,8 +8,8 @@ id = "alexander/screen-toolkit" name = "Screen Toolkit" -version = "1.1.0" -plugin_api = 9 +version = "1.2.0" +plugin_api = 13 author = "alexander" license = "MIT" icon = "crosshair" @@ -38,6 +38,7 @@ dependencies = [ "gpu-screen-recorder", "wl-screenrec", "wf-recorder", + "mpv", ] # ── Panel entries (host-injected placement/size settings appear here, first in the settings editor) ── @@ -50,9 +51,11 @@ dependencies = [ id = "panel" entry = "panel.luau" width = 380 -height = 260 +height = 290 placement = "floating" position = "center" +keyboard_focus = "on_demand" +capture_keys = ["Tab", "Backtab", "Shift+Tab", "Up", "Down", "Left", "Right", "Return", "Enter", "Space", "Escape"] # Full-mode layout: the original spacious grid (660x340). [[panel]] @@ -62,6 +65,19 @@ width = 660 height = 340 placement = "floating" position = "center" +keyboard_focus = "on_demand" +capture_keys = ["Tab", "Backtab", "Shift+Tab", "Up", "Down", "Left", "Right", "Return", "Enter", "Space", "Escape"] + +# Legacy-mode layout: the original screen-toolkit v4 panel. +[[panel]] +id = "panel-legacy" +entry = "panel.luau" +width = 380 +height = 260 +placement = "floating" +position = "center" +keyboard_focus = "on_demand" +capture_keys = ["Tab", "Backtab", "Shift+Tab", "Up", "Down", "Left", "Right", "Return", "Enter", "Space", "Escape"] # Result panel: opened by the service when a capture tool finishes. [[panel]] @@ -211,6 +227,7 @@ default = "full" options = [ { value = "compact", label_key = "settings.panel_mode.options.compact" }, { value = "full", label_key = "settings.panel_mode.options.full" }, + { value = "legacy", label_key = "settings.panel_mode.options.legacy" }, ] # ── Entries ───────────────────────────────────────────────────────────────── diff --git a/screen-toolkit/scripts/record.sh b/screen-toolkit/scripts/record.sh index 9132d70..7ef4320 100755 --- a/screen-toolkit/scripts/record.sh +++ b/screen-toolkit/scripts/record.sh @@ -19,7 +19,7 @@ set -euo pipefail ACTION="${1:-}" THUMB_OUT="/tmp/screen-toolkit-record-thumb.png" -PALETTE="/tmp/screen-toolkit-record-palette.png" +PALETTE="/tmp/screen-toolkit-record-palette-$$.png" _require() { command -v "$1" >/dev/null 2>&1 \ || { echo "ERROR: missing dependency: $1" >&2; exit 3; } @@ -78,11 +78,11 @@ case "$ACTION" in DURATION="-t $MAXSEC" fi ffmpeg -y $DURATION -i "$INPUT" \ - -vf 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos,palettegen' \ + -vf 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=bilinear,palettegen=stats_mode=diff' \ "$PALETTE" 2>/dev/null \ || { echo "ERROR: convert-gif: palettegen pass failed" >&2; exit 4; } ffmpeg -y $DURATION -i "$INPUT" -i "$PALETTE" \ - -lavfi 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos[x];[x][1:v]paletteuse' \ + -lavfi 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=bilinear[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5' \ "$OUTPUT" 2>/dev/null \ || { echo "ERROR: convert-gif: paletteuse pass failed" >&2; exit 4; } rm -f "$PALETTE" "$INPUT" diff --git a/screen-toolkit/service.luau b/screen-toolkit/service.luau index f08fa1f..ff9da16 100644 --- a/screen-toolkit/service.luau +++ b/screen-toolkit/service.luau @@ -17,6 +17,7 @@ local PANEL_ID = "alexander/screen-toolkit:panel" local FULL_PANEL_ID = "alexander/screen-toolkit:panel-full" +local LEGACY_PANEL_ID = "alexander/screen-toolkit:panel-legacy" local RESULT_PANEL_ID = "alexander/screen-toolkit:result" local SCRIPT_CAPTURE = noctalia.pluginDir() .. "/scripts/capture.sh" @@ -43,7 +44,7 @@ local function log(msg) end local function trim(s) - return tostring(s):gsub("^%s+", ""):gsub("%s+$", "") + return (tostring(s):gsub("^%s+", ""):gsub("%s+$", "")) end local function shellQuote(value) @@ -96,6 +97,17 @@ local function focusedGeometry() ) end end + if outputs and #outputs > 0 then + local o = outputs[1] + local scale = o.scale or 1 + return string.format( + "%d,%d %dx%d", + math.floor(o.x * scale), + math.floor(o.y * scale), + math.round(o.width * scale), + math.round(o.height * scale) + ) + end return nil end @@ -482,6 +494,8 @@ end -- ── Tools: annotate (external editor handoff) ────────────────────────────── local function openAnnotator(file) + publish("activeTool", "annotate") + publish("annotateResult", { capturePath = file }) if noctalia.commandExists("swappy") then noctalia.runAsync(`swappy -f {shellQuote(file)}`) elseif noctalia.commandExists("satty") then @@ -596,9 +610,22 @@ local function setRecordState(next) publish("recordStartedAt", if recordState == "recording" then recordStartedAt else nil) end +local overrideAudioOut = nil +local overrideAudioIn = nil + +local function getAudioOut() + if overrideAudioOut ~= nil then return overrideAudioOut end + return cfg("record-audio-out") == true +end + +local function getAudioIn() + if overrideAudioIn ~= nil then return overrideAudioIn end + return cfg("record-audio-in") == true +end + local function gsrAudioFlags() - local audioOut = cfg("record-audio-out") == true - local audioIn = cfg("record-audio-in") == true + local audioOut = getAudioOut() + local audioIn = getAudioIn() if audioOut and audioIn then return '-a "default_output|default_input" -ac aac' elseif audioOut then @@ -610,31 +637,43 @@ local function gsrAudioFlags() end local function buildRecorderCommand(bin, geom, out) - local audioOut = cfg("record-audio-out") == true - local audioIn = cfg("record-audio-in") == true + local audioOut, audioIn = getAudioOut(), getAudioIn() + local isRegion = geom and geom ~= "" and geom ~= "screen" + local fps = cfg("record-fps") or 60 + local codec = cfg("record-codec") or "h264" + if bin == "gpu-screen-recorder" then - local codec = cfg("record-codec") or "h264" - local fps = cfg("record-fps") or 60 - local cursor = if cursorHidden then "no" else "yes" - -- focusedOutputName() can return nil (e.g. when triggered from the bar), - -- so capture the focused region by geometry instead of by monitor name. - -- GSR region syntax is "WxH+X+Y", the reverse of our "x,y WxH". - local gx, gy, gw, gh = tostring(geom):match("^(%d+),(%d+) (%d+)x(%d+)$") - if not gx then return "" end - local flags = string.format( - "-w region -region %sx%s+%s+%s -f %d -k %s -bm qp -ffmpeg-opts qp=25 -cursor %s -cr limited %s -v no -o %s", - gw, gh, gx, gy, fps, codec, cursor, gsrAudioFlags(), shellQuote(out) - ) - return "gpu-screen-recorder " .. flags - elseif bin == "wl-screenrec" then - local parts = { "wl-screenrec -g", shellQuote(geom), "-f", shellQuote(out) } - if audioOut then table.insert(parts, "--audio") end - if audioIn then table.insert(parts, "--audio-device default") end - if cursorHidden then table.insert(parts, "--no-cursor") end - return table.concat(parts, " ") + local cursor = cursorHidden and "no" or "yes" + local target = "-w screen" + if isRegion then + local gx, gy, gw, gh = tostring(geom):match("^(%d+),(%d+) (%d+)x(%d+)$") + if gx then target = string.format("-w region -region %sx%s+%s+%s", gw, gh, gx, gy) end + end + return string.format("gpu-screen-recorder %s -f %d -k %s -bm qp -ffmpeg-opts qp=25 -cursor %s -cr limited %s -v no -o %s", target, fps, codec, cursor, gsrAudioFlags(), shellQuote(out)) end - local parts = { "wf-recorder -g", shellQuote(geom), "-f", shellQuote(out) } - if audioOut then table.insert(parts, "-a") end + + local isWl = bin == "wl-screenrec" + local parts = { bin } + if isRegion then table.insert(parts, "-g " .. shellQuote(geom)) end + table.insert(parts, "-f " .. shellQuote(out)) + + if isWl then + table.insert(parts, "-m " .. tostring(fps)) + local wlCodec = codec == "h264" and "avc" or codec + table.insert(parts, "--codec " .. wlCodec) + if cursorHidden then table.insert(parts, "--no-cursor") end + else + table.insert(parts, "-r " .. tostring(fps)) + end + + if audioOut and audioIn then + table.insert(parts, isWl and "--audio" or "-a") + elseif audioOut then + table.insert(parts, isWl and "--audio --audio-device '$(pactl get-default-sink 2>/dev/null).monitor'" or "-a -C '$(pactl get-default-sink 2>/dev/null).monitor'") + elseif audioIn then + table.insert(parts, isWl and "--audio --audio-device '$(pactl get-default-source 2>/dev/null)'" or "-a -C '$(pactl get-default-source 2>/dev/null)'") + end + return table.concat(parts, " ") end @@ -644,9 +683,11 @@ local function startRecord(geom, format, bin) noctalia.notifyError(noctalia.tr("messages.no_recorder")) return end + -- clean up old thumb so it doesn't stay stuck in preview if recordState ~= "idle" then return end rawPath = `/tmp/screen-toolkit-record-{os.time()}.mp4` recordFormat = if format == "gif" then "gif" else "mp4" + publish("recordFormat", recordFormat) recordPath = "" local cmd = buildRecorderCommand(bin, geom, rawPath) log(`starting recording: {cmd}`) @@ -674,20 +715,21 @@ end local function waitForFileStable(path, callback, attempts, previousSize) attempts = attempts or 60 - noctalia.runAsync(`stat -c %s {shellQuote(path)}`, function(res) + noctalia.runAsync(`stat -c %s {shellQuote(path)} 2>/dev/null`, function(res) local size = trim(res.stdout or "") - if res.exitCode == 0 and tonumber(size) and tonumber(size) > 0 then - if previousSize == size then + local numSize = tonumber(size) + if res.exitCode == 0 and numSize and numSize > 0 then + if previousSize == numSize then callback(true) return end noctalia.runAsync("sleep 0.3", function() - waitForFileStable(path, callback, attempts - 1, size) + waitForFileStable(path, callback, attempts - 1, numSize) end) return end if attempts <= 0 then - callback(false) + callback(numSize ~= nil and numSize > 0) return end noctalia.runAsync("sleep 0.2", function() @@ -742,7 +784,7 @@ local function finalizeRecord(format, copyToClipboard) else noctalia.notify(noctalia.tr("messages.saved_to", { path = dest })) end - end) + end, 600000) end local function recordStop() @@ -752,7 +794,8 @@ local function recordStop() -- rawPath is unique per recording, so pkill scopes to our recorder only and -- never touches the built-in screen recorder's process. The transient pkill -- wrapper matching itself is harmless (it still signals the recorder first). - local stopCommand = `pkill -INT -f {shellQuote(rawPath)}` + -- use [/]tmp regex trick so pkill doesn't match and kill its own shell process + local stopCommand = `pkill -INT -f {rawPath}` noctalia.runAsync(stopCommand, function() waitForFileStable(rawPath, function(ok) if not ok then @@ -763,7 +806,7 @@ local function recordStop() local skipConfirm = cfg("record-skip-confirmation") == true local toClipboard = cfg("record-copy-to-clipboard") == true if toClipboard then - finalizeRecord("mp4", true) + finalizeRecord(recordFormat, true) elseif skipConfirm then finalizeRecord(recordFormat, false) else @@ -792,6 +835,7 @@ local function recordDiscard() noctalia.runAsync(`rm -f {shellQuote(rawPath)}`) rawPath = "" end + -- clean up old thumb so it doesn't stay stuck in preview setRecordState("idle") end @@ -869,59 +913,31 @@ end local function runRecordRegion(format) refreshRecorders() local bin = bestRegionRecorder() - if bin == "" then - noctalia.notifyError(noctalia.tr("messages.no_recorder")) - return - end - slurpGeometry(function(geom) - startRecord(geom, format, bin) - end) + if bin == "" then noctalia.notifyError(noctalia.tr("messages.no_recorder")) return end + slurpGeometry(function(geom) startRecord(geom, format, bin) end) end local function runRecordFullscreen(format) refreshRecorders() local bin = bestFullscreenRecorder() - if bin == "" then - noctalia.notifyError(noctalia.tr("messages.no_recorder")) - return - end - local geom = focusedGeometry() - if not geom then - noctalia.notifyError(noctalia.tr("messages.capture_failed")) - return - end - startRecord(geom, format, bin) + if bin == "" then noctalia.notifyError(noctalia.tr("messages.no_recorder")) return end + startRecord("screen", format, bin) end local HANDLERS = { - colorPicker = runColorPicker, - ocr = runOcr, - qr = runQr, - palette = runPalette, - lens = runLens, - measure = runMeasure, - annotate = annotateRegion, - annotateFullscreen = annotateFullscreen, - annotateWindow = annotateWindow, - record = function() runRecordRegion("gif") end, - recordMp4 = function() runRecordRegion("mp4") end, - recordFullscreen = function() runRecordFullscreen("gif") end, - recordFullscreenMp4 = function() runRecordFullscreen("mp4") end, + colorPicker = runColorPicker, ocr = runOcr, qr = runQr, palette = runPalette, lens = runLens, measure = runMeasure, + annotate = annotateRegion, annotateFullscreen = annotateFullscreen, annotateWindow = annotateWindow, + record = function() runRecordRegion("gif") end, recordMp4 = function() runRecordRegion("mp4") end, + recordFullscreen = function() runRecordFullscreen("gif") end, recordFullscreenMp4 = function() runRecordFullscreen("mp4") end, recordStop = recordStop, - recordSave = function(payload) - local fmt = type(payload) == "table" and payload.format or nil - finalizeRecord(fmt or recordFormat, false) - end, - recordCopy = function() finalizeRecord("mp4", true) end, - recordDiscard = recordDiscard, - ocrSearch = ocrSearch, - ocrTranslate = ocrTranslate, - share = shareFile, - clearResult = clearResult, - clearHistory = clearHistory, - setCursorHidden = setCursorHidden, + recordSave = function(payload) finalizeRecord((type(payload) == "table" and payload.format or nil) or recordFormat, false) end, + recordCopy = function() finalizeRecord(recordFormat, true) end, recordDiscard = recordDiscard, + ocrSearch = ocrSearch, ocrTranslate = ocrTranslate, share = shareFile, clearResult = clearResult, clearHistory = clearHistory, setCursorHidden = setCursorHidden, + setAudioOut = function(payload) overrideAudioOut = (type(payload) == "table" and payload.value or payload) == true end, + setAudioIn = function(payload) overrideAudioIn = (type(payload) == "table" and payload.value or payload) == true end, toggle = function() - local id = cfg("panel-mode") == "full" and FULL_PANEL_ID or PANEL_ID + local mode = cfg("panel-mode") + local id = mode == "full" and FULL_PANEL_ID or (mode == "legacy" and LEGACY_PANEL_ID or PANEL_ID) noctalia.togglePanel(id) end, } diff --git a/screen-toolkit/shortcut.luau b/screen-toolkit/shortcut.luau index 9062607..a2236b0 100644 --- a/screen-toolkit/shortcut.luau +++ b/screen-toolkit/shortcut.luau @@ -21,7 +21,8 @@ end) function onClick() -- The shortcut tile never stops a recording; it just opens the panel so -- the stop button (in the panel header, next to the close X) is reachable. - local panelId = noctalia.getConfig("panel-mode") == "full" and "alexander/screen-toolkit:panel-full" or "alexander/screen-toolkit:panel" + local mode = noctalia.getConfig("panel-mode") + local panelId = mode == "full" and "alexander/screen-toolkit:panel-full" or (mode == "legacy" and "alexander/screen-toolkit:panel-legacy" or "alexander/screen-toolkit:panel") noctalia.togglePanel(panelId) end diff --git a/screen-toolkit/translations/en.json b/screen-toolkit/translations/en.json index da510e0..32e271c 100644 --- a/screen-toolkit/translations/en.json +++ b/screen-toolkit/translations/en.json @@ -102,7 +102,8 @@ "label": "Panel Mode", "options": { "compact": "Compact", - "full": "Full" + "full": "Full", + "legacy": "Legacy" } }, "record_audio_in": { diff --git a/screen-toolkit/widget.luau b/screen-toolkit/widget.luau index d81df60..ceffe2d 100644 --- a/screen-toolkit/widget.luau +++ b/screen-toolkit/widget.luau @@ -55,7 +55,8 @@ end) function onClick() -- The bar icon never stops a recording; it just opens the panel so the -- stop button (in the panel header, next to the close X) is reachable. - local panelId = noctalia.getConfig("panel-mode") == "full" and "alexander/screen-toolkit:panel-full" or "alexander/screen-toolkit:panel" + local mode = noctalia.getConfig("panel-mode") + local panelId = mode == "full" and "alexander/screen-toolkit:panel-full" or (mode == "legacy" and "alexander/screen-toolkit:panel-legacy" or "alexander/screen-toolkit:panel") noctalia.togglePanel(panelId) end