diff --git a/screen-toolkit/README.md b/screen-toolkit/README.md index 6e5b483..012a000 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` (tools), `result` (result view); service: `service` | +| Entries | Bar widget: `widget`; control-center shortcut: `toggle`; panels: `panel` (compact tools), `panel-full` (full tools), `result` (result view); service: `service` | ## Requirements @@ -60,12 +60,45 @@ 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. -Open the main tools panel: +The main panel has two 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). + +The widget and shortcut open whichever entry matches the setting; the panel +footnote under Settings → Plugins shows both panels' placement/position +options. + +Toggle the tools panel (opens the entry matching the `panel-mode` setting): + +```sh +noctalia msg plugin alexander/screen-toolkit:service all toggle +``` + +For example, bind it to `SUPER+P` in Hyprland: + +```ini +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. + +Open the compact tools panel: ```sh noctalia msg panel-toggle alexander/screen-toolkit:panel ``` +Open the full tools panel (the original spacious grid): + +```sh +noctalia msg panel-toggle alexander/screen-toolkit:panel-full +``` + Open the result panel (shows the last capture/recording output): ```sh @@ -131,6 +164,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). | ## IPC @@ -153,17 +187,70 @@ noctalia msg plugin alexander/screen-toolkit:service all recordFullscreen noctalia msg plugin alexander/screen-toolkit:service all recordFullscreenMp4 noctalia msg plugin alexander/screen-toolkit:service all recordStop noctalia msg plugin alexander/screen-toolkit:service all recordSave +noctalia msg plugin alexander/screen-toolkit:service all recordCopy noctalia msg plugin alexander/screen-toolkit:service all recordDiscard +noctalia msg plugin alexander/screen-toolkit:service all ocrSearch +noctalia msg plugin alexander/screen-toolkit:service all clearResult +noctalia msg plugin alexander/screen-toolkit:service all clearHistory ``` -`ocrTranslate` takes a language code payload: +`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 +same mode-aware toggle. + +Commands that take a payload: ```sh +# Translate the current OCR text into a language (language code payload) noctalia msg plugin alexander/screen-toolkit:service all ocrTranslate en +# Search the current OCR text (or a payload.text) with the configured engine +noctalia msg plugin alexander/screen-toolkit:service all ocrSearch +# Share a file on disk (absolute path payload) +noctalia msg plugin alexander/screen-toolkit:service all share /path/to/image.png +# Save the finished recording (payload format: "gif" or "mp4") +noctalia msg plugin alexander/screen-toolkit:service all recordSave mp4 +# Show or hide the cursor in captures (payload: "true" or "false") +noctalia msg plugin alexander/screen-toolkit:service all setCursorHidden true ``` +Summary of every service command: + +| Command | Payload | Action | +| --- | --- | --- | +| `toggle` | — | Open/close the tools panel that matches `panel-mode` | +| `colorPicker` | — | Pick a color from the screen (region crosshair) | +| `ocr` | — | Extract text from a region | +| `qr` | — | Decode a QR / barcode from a region | +| `palette` | — | Extract hex colors from a region | +| `lens` | — | Google Lens search on a region | +| `measure` | — | Report a region's pixel size | +| `annotate` | — | Open a region in the annotation editor | +| `annotateFullscreen` | — | Annotate the full screen | +| `annotateWindow` | — | Annotate the focused window (Hyprland only) | +| `record` | — | Record a region as GIF | +| `recordMp4` | — | Record a region as MP4 | +| `recordFullscreen` | — | Record the full screen as GIF | +| `recordFullscreenMp4` | — | Record the full screen as MP4 | +| `recordStop` | — | Stop the active recording | +| `recordSave` | `"gif"` / `"mp4"` | Save the finished recording | +| `recordCopy` | — | Finalize to MP4 and copy the file URI | +| `recordDiscard` | — | Discard the finished recording | +| `ocrSearch` | optional `{text}` | Search OCR text with the configured engine | +| `ocrTranslate` | language code | Translate OCR text | +| `share` | file path | Upload a file and copy the link | +| `clearResult` | — | Clear the current result panel state | +| `clearHistory` | — | Clear the color history | +| `setCursorHidden` | `"true"` / `"false"` | Include/exclude the cursor in captures | + ## Notes +- **Two 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). + 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. - This is a port of the legacy v4 [screen-toolkit](https://github.com/noctalia-dev/legacy-v4-plugins/tree/main/screen-toolkit) plugin. Tools that relied on freeform v4 QML overlays are adapted: region diff --git a/screen-toolkit/panel.luau b/screen-toolkit/panel.luau index 824ef18..5697d83 100644 --- a/screen-toolkit/panel.luau +++ b/screen-toolkit/panel.luau @@ -1,13 +1,22 @@ --!nonstrict -- Screen Toolkit — main [[panel]]. -- --- Thin client for the toolkit: a header plus the tool grid grouped into --- Capture / Annotate / Record sections. It only dispatches actions to the --- [[service]] through the "command" state channel. Capture tools close the --- panel first so the capture never includes it. +-- Thin client for the toolkit. Two layouts are provided, selected by the +-- "panel-mode" plugin setting: +-- * compact — a dense grid grouped into tinted sections (default) +-- * full — the original spacious layout with section titles +-- The panel entry to open (and thus its host-owned size) is chosen by the +-- bar [[widget]] / control-center [[shortcut]] from the same setting. +-- It only dispatches actions to the [[service]] through the "command" +-- state channel. Capture tools close the panel first so the capture never +-- includes it. local panelOpen = false local recording = noctalia.state.get("recording") == true +local hoveredTool = nil +local hoveredClose = false +local hoveredStop = false +local render -- ── Helpers ──────────────────────────────────────────────────────────────── @@ -20,6 +29,10 @@ local function send(action, payload) noctalia.state.set("command", { action = action, payload = payload }) end +local function getMode() + return noctalia.getConfig("panel-mode") == "full" and "full" or "compact" +end + -- Capture tools must not see the panel in the frame, so close it first. local CAPTURE_ACTIONS = { colorPicker = true, @@ -44,28 +57,7 @@ local function runTool(action) send(action) end --- ── Header ───────────────────────────────────────────────────────────────── - -local function header() - 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 = 8 }, { - ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 16, fontWeight = "bold", color = "on_surface" }), - ui.row({ gap = 4 }, actions), - }) -end - --- ── Tool grid ────────────────────────────────────────────────────────────── +-- ── Tool data ────────────────────────────────────────────────────────────── local TOOLS = { { action = "colorPicker", label_key = "tools.colorpicker", glyph = "palette" }, @@ -89,30 +81,155 @@ local RECORD_TOOLS = { { action = "recordFullscreenMp4", label_key = "tools.record_fullscreen_mp4", glyph = "video" }, } -local function toolTile(t) +-- ── 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), + }) +end + +local function compactTile(t) + local color = hoveredTool == t.action and "primary" or "on_surface" + return ui.column({ + key = "tile-" .. t.action, + flexGrow = 1, + gap = 2, + align = "center", + }, { + 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 }), + }), + }) +end + +local function compactToolsRow(tools) + local row = {} + 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), + }) +end + +local function compactGrid() + 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), + }) +end + +local function fullTile(t) + local color = hoveredTool == t.action and "primary" or "on_surface" return ui.column({ key = "tile-" .. t.action, flexGrow = 1, gap = 6, align = "center", paddingV = 6, - radius = 10, - fill = "surface_variant/0.28", + radius = 12, + fill = "surface_variant", borderWidth = 1, - border = "outline", + 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, }, { - ui.column({ width = 30, height = 30, radius = 9, fill = "primary/0.12", align = "center", justify = "center" }, { - ui.glyph({ name = t.glyph, size = 16, color = "primary" }), + 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 = "on_surface_variant" }), + ui.label({ text = tr(t.label_key), maxWidth = 100, maxLines = 1, textAlign = "center", fontSize = 11, color = color }), }) end -local function toolsRow(tools) +local function fullToolsRow(tools) local row = {} for _, t in ipairs(tools) do - row[#row + 1] = toolTile(t) + row[#row + 1] = fullTile(t) end return ui.row({ flexGrow = 1, gap = 6 }, row) end @@ -121,30 +238,40 @@ local function sectionLabel(text) return ui.label({ text = text, fontSize = 11, fontWeight = "bold", color = "on_surface_variant" }) end -local function toolGrid() +local function fullGrid() return ui.column({ flexGrow = 1, gap = 4, justify = "space_between" }, { sectionLabel(tr("panel.section_capture")), - toolsRow(TOOLS), + fullToolsRow(TOOLS), sectionLabel(tr("panel.section_annotate")), - toolsRow(ANNOTATE_TOOLS), + fullToolsRow(ANNOTATE_TOOLS), sectionLabel(tr("panel.section_record")), - toolsRow(RECORD_TOOLS), + fullToolsRow(RECORD_TOOLS), }) end -- ── Main render ──────────────────────────────────────────────────────────── -local function render() - panel.render(ui.column({ flexGrow = 1, gap = 10 }, { - header(), - toolGrid(), - })) +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 + panel.render(body) end -- ── Lifecycle ────────────────────────────────────────────────────────────── function onOpen(_context) panelOpen = true + hoveredTool = nil render() end diff --git a/screen-toolkit/plugin.toml b/screen-toolkit/plugin.toml index 5c9b00c..847e4f2 100644 --- a/screen-toolkit/plugin.toml +++ b/screen-toolkit/plugin.toml @@ -8,7 +8,7 @@ id = "alexander/screen-toolkit" name = "Screen Toolkit" -version = "1.0.0" +version = "1.1.0" plugin_api = 9 author = "alexander" license = "MIT" @@ -40,6 +40,38 @@ dependencies = [ "wf-recorder", ] +# ── Panel entries (host-injected placement/size settings appear here, first in the settings editor) ── + +# Main toolkit panel: tool grid + per-tool result views + recording controls. +# Open with: noctalia msg panel-toggle alexander/screen-toolkit:panel +# Compact layout (default). Full mode uses the "panel-full" entry below; the +# bar widget / shortcut open whichever entry matches the "panel-mode" setting. +[[panel]] +id = "panel" +entry = "panel.luau" +width = 380 +height = 260 +placement = "floating" +position = "center" + +# Full-mode layout: the original spacious grid (660x340). +[[panel]] +id = "panel-full" +entry = "panel.luau" +width = 660 +height = 340 +placement = "floating" +position = "center" + +# Result panel: opened by the service when a capture tool finishes. +[[panel]] +id = "result" +entry = "result.luau" +width = 560 +height = 520 +placement = "floating" +position = "center" + # ── Plugin-level settings (shared by every entry, edited in Settings → Plugins) ── [[setting]] @@ -170,6 +202,17 @@ default = 30 min = 1 max = 600 +[[setting]] +key = "panel-mode" +type = "select" +label_key = "settings.panel_mode.label" +description_key = "settings.panel_mode.description" +default = "full" +options = [ + { value = "compact", label_key = "settings.panel_mode.options.compact" }, + { value = "full", label_key = "settings.panel_mode.options.full" }, +] + # ── Entries ───────────────────────────────────────────────────────────────── # Bar widget: crosshair glyph; shows a pulsing red dot while recording. @@ -183,25 +226,6 @@ entry = "widget.luau" id = "toggle" entry = "shortcut.luau" -# Main toolkit panel: tool grid + per-tool result views + recording controls. -# Open with: noctalia msg panel-toggle alexander/screen-toolkit:panel -[[panel]] -id = "panel" -entry = "panel.luau" -width = 660 -height = 340 -placement = "floating" -position = "center" - -# Result panel: opened by the service when a capture tool finishes. -[[panel]] -id = "result" -entry = "result.luau" -width = 560 -height = 520 -placement = "floating" -position = "center" - # Headless engine: owns captures, tool pipelines, recording, and persistence. [[service]] id = "service" diff --git a/screen-toolkit/service.luau b/screen-toolkit/service.luau index 8923a05..f08fa1f 100644 --- a/screen-toolkit/service.luau +++ b/screen-toolkit/service.luau @@ -16,6 +16,7 @@ -- lands in /tmp as MP4; GIF is produced on save via record.sh convert-gif. local PANEL_ID = "alexander/screen-toolkit:panel" +local FULL_PANEL_ID = "alexander/screen-toolkit:panel-full" local RESULT_PANEL_ID = "alexander/screen-toolkit:result" local SCRIPT_CAPTURE = noctalia.pluginDir() .. "/scripts/capture.sh" @@ -919,7 +920,10 @@ local HANDLERS = { clearResult = clearResult, clearHistory = clearHistory, setCursorHidden = setCursorHidden, - toggle = function() noctalia.togglePanel(PANEL_ID) end, + toggle = function() + local id = cfg("panel-mode") == "full" and FULL_PANEL_ID or PANEL_ID + noctalia.togglePanel(id) + end, } -- Tools that begin a brand-new capture. Starting one discards any recording diff --git a/screen-toolkit/shortcut.luau b/screen-toolkit/shortcut.luau index 75fe022..9062607 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. - noctalia.togglePanel("alexander/screen-toolkit:panel") + local panelId = noctalia.getConfig("panel-mode") == "full" and "alexander/screen-toolkit:panel-full" or "alexander/screen-toolkit:panel" + noctalia.togglePanel(panelId) end apply() diff --git a/screen-toolkit/thumbnail.webp b/screen-toolkit/thumbnail.webp index 7bab42b..b96141e 100644 Binary files a/screen-toolkit/thumbnail.webp and b/screen-toolkit/thumbnail.webp differ diff --git a/screen-toolkit/translations/en.json b/screen-toolkit/translations/en.json index fbd1eb9..0cab860 100644 --- a/screen-toolkit/translations/en.json +++ b/screen-toolkit/translations/en.json @@ -199,6 +199,14 @@ "gif_max_seconds": { "label": "Max GIF Duration", "description": "Maximum duration in seconds for GIF recordings." + }, + "panel_mode": { + "label": "Panel Mode", + "description": "Layout of the main panel. Compact is the dense grid; Full is the original spacious grid.", + "options": { + "compact": "Compact", + "full": "Full" + } } } } diff --git a/screen-toolkit/widget.luau b/screen-toolkit/widget.luau index 759e8cd..d81df60 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. - noctalia.togglePanel("alexander/screen-toolkit:panel") + local panelId = noctalia.getConfig("panel-mode") == "full" and "alexander/screen-toolkit:panel-full" or "alexander/screen-toolkit:panel" + noctalia.togglePanel(panelId) end function onRightClick()