From 02af447cc1f22a2906961550affd3b3365d63e89 Mon Sep 17 00:00:00 2001 From: n3ptune-plan3t Date: Sun, 30 Aug 2026 11:25:14 +0530 Subject: [PATCH 1/2] Create README.md for Scroll Tools plugin Add README for Scroll Tools plugin detailing features, requirements, and usage. --- scroll-tools/README.md | 146 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 scroll-tools/README.md diff --git a/scroll-tools/README.md b/scroll-tools/README.md new file mode 100644 index 00000000..02c9b903 --- /dev/null +++ b/scroll-tools/README.md @@ -0,0 +1,146 @@ +# AI GENERATED CONTENT + +# Scroll Tools for Noctalia + +ScrollWC-specific bar widgets for [Noctalia Shell v5](https://github.com/noctalia-dev/noctalia-shell) running under the [Scroll](https://github.com/dawsers/scroll) compositor. Mirrors the `ScrollScroller` / `ScrollTrails` / `ScrollSubmap` integration from `dawsers/gtkshell` for Noctalia. + +> Noctalia core already treats Scroll as Sway-IPC-compatible (Scroll exports `SWAYSOCK`), so generic workspaces, window-title, and keyboard-layout widgets work out of the box. This plugin only adds the **Scroll-specific extras** that have no sway/i3 equivalent: scroller modifiers, trails, and binding mode. + +## Features + +| Surface | ID | Type | Description | +|---------|----|------|-------------| +| **Scroller HUD** | `scroller` | Bar widget (`scroller_widget.luau:1`) | Compact one-line HUD of the focused workspace's scroller state: direction, insert position, focus, reorder, fit, centering, overview, and scale. Click to open the control panel. | +| **Trails Counter** | `trails` | Bar widget (`trails_widget.luau:1`) | Displays `active/length (marks)` for Scroll trails. | +| **Binding Mode Indicator** | `submap` | Bar widget (`submap_widget.luau:1`) | Shows the current Scroll binding mode / submap name. Auto-hides when in `default` mode. | +| **Scroller Controls** | `scroller-controls` | Panel (`scroller_panel.luau:1`) | Interactive control panel for all scroller modifiers on the focused workspace. | + +## Requirements + +- **Noctalia Shell v5** (plugin API `8`) +- **Scroll compositor** running +- **`scrollmsg`** on `PATH` — the Scroll IPC CLI (ships with Scroll) +- A valid IPC socket — resolved as `SCROLLSOCK` → `SWAYSOCK` (or the `socket_path` setting override) + +When `scrollmsg` is missing or no socket is found, all widgets hide themselves gracefully and set `scroll.available = false` in `noctalia.state`. + +## Widgets + +### 1. Scroller (`scroller`) — `scroller_widget.luau:26` + +Event-driven via `scrollmsg -t subscribe -m -r '["scroller"]'` (`scroll_ipc.luau:39`). No polling — the widget stretches Noctalia's update interval to 1 hour (`scroller_widget.luau:12`) and updates only on IPC events. + +**Bar text format** (`scroller_widget.luau:26`): + +``` + [ scale] +``` + +| Field | Icon mode (`show_icons = true`) | Text mode | Source | +|-------|----------------------------------|-----------|--------| +| Mode | `-` (horizontal) / `|` (vertical) | `H` / `V` | `scroller.mode` | +| Insert | `→` after / `←` before / `⇥` end / `⇤` beginning | `>` / `<` / `E` / `B` | `scroller.insert` | +| Focus | `◉` focus / `◎` nofocus | `f` / `-` | `scroller.focus` | +| Reorder | `A` auto / `M` manual | `a` / `-` | `scroller.reorder` | +| Fit | `·` nofit / `S` fitsplit / `F` fitfraction | `-` / `S` / `F` | `scroller.fit` | +| Center H | `⇔` when on | `h` / `` | `scroller.center_horizontal` | +| Center V | `⇕` when on | `v` / `` | `scroller.center_vertical` | +| Overview | `󰆾` when on | ` *` | `scroller.overview` | +| Scale | ` 1.25x` when `scaled == true` | same | `scroller.scale` | + +Hover shows a tooltip with all fields expanded (`scroller_widget.luau:69`). Clicking the widget toggles the `scroller-controls` panel (`scroller_widget.luau:133`). + +The widget also publishes the latest scroller object to `noctalia.state["scroll.scroller"]` for the control panel, deduplicated by fingerprint (`scroll_state.luau:15`). + +### 2. Trails (`trails`) — `trails_widget.luau:11` + +Subscribes to `["trails"]` events (`trails_widget.luau:52`). + +**Format:** `active/length (marks)` — e.g. `2/5 (3)` (`trails_widget.luau:18`) + +- `active` → `trails.active` +- `length` → `trails.length` +- `marks` → `trails.trail_length` + +Tooltip: `Scroll trails: position / length (marks)`. + +### 3. Submap / Binding Mode (`submap`) — `submap_widget.luau:16` + +Subscribes to `["mode"]` events (`submap_widget.luau:60`). + +- Hidden when mode is `nil`, empty, or `"default"` — only appears when a non-default binding mode is active. +- Icon mode: `⌨ ` · Text mode: `MODE: ` (`submap_widget.luau:16`). +- Handles both live events (`{change: ""}`) and the initial `get_binding_state` fetch (`{name: ""}`) (`submap_widget.luau:41`). + +## Panel: Scroller Controls (`scroller-controls`) — `scroller_panel.luau:1` + +Attached panel (`320×380`, auto-placed, dismiss on outside click — `plugin.toml:42`) that mirrors the popover in `ScrollScroller` (`gtkshell/scroll.cpp`). + +| Control | Type | Command sent via `scrollmsg -- ` | +|---------|------|----------------------------------------| +| Direction | Select: Horizontal / Vertical | `set_mode h` / `set_mode v` (`scroller_panel.luau:143`) | +| Insert position | Select: After / Before / Beginning / End | `set_mode after` / `before` / `beginning` / `end` (`scroller_panel.luau:146`) | +| Focus new window | Toggle | `set_mode focus` / `nofocus` (`scroller_panel.luau:153`) | +| Auto reorder | Toggle | `set_mode reorder_auto` / `noreorder_auto` (`scroller_panel.luau:157`) | +| Fit | Select: No fit / Split / Fraction | `set_mode nofit` / `fitsplit` / `fitfraction` (`scroller_panel.luau:161`) | +| Center column | Toggle | `set_mode center_horiz` / `nocenter_horiz` (`scroller_panel.luau:168`) | +| Center window | Toggle | `set_mode center_vert` / `nocenter_vert` (`scroller_panel.luau:172`) | +| Refresh | Button | Re-fetches `get_scroller` via IPC (`scroller_panel.luau:176`) | + +The panel watches `scroll.scroller` state and re-renders only when its semantic fingerprint changes (`scroller_panel.luau:130`, `scroll_state.luau:50`), avoiding churn from irrelevant fields like scale precision. + +If Scroll is not detected, the panel shows a placeholder message (`scroller_panel.luau:47`). + +## Settings — `plugin.toml:15` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `socket_path` | string (advanced) | `""` | Override IPC socket path. When empty, resolves `SCROLLSOCK` → `SWAYSOCK` via `scroll_ipc.luau:9`. | +| `show_icons` | bool | `true` | Use compact glyphs in `scroller` and `submap` widgets instead of plain-text labels (`translations/en.json:7`). | + +Configure via Noctalia Settings → Plugins → Scroll Tools, or directly in your Noctalia config. + +## How It Works + +``` +scroll_state.luau Semantic fingerprints + publishIfChanged + ▲ + │ +scroller_widget ──┬──► scroll_ipc.luau ──► scrollmsg -t get_scroller / subscribe + │ ├─ socketPath(): SCROLLSOCK → SWAYSOCK → socket_path override +trails_widget ────┤ ├─ scrollmsgBase(): builds `scrollmsg -s ''` + │ ├─ subscribe(eventsJson, onLine): runStream +submap_widget ────┤ └─ fetch(ipcType, onEnvelope): runAsync + │ +scroller_panel ───┘──► noctalia.state["scroll.scroller"] (watch) + scrollmsg -- set_mode … +``` + +- Each widget owns its own `scrollmsg subscribe` long-lived stream (`scroll_ipc.luau:39`), matching `ScrollIpc::working_thread` in `gtkshell`. +- One-shot state is fetched via `scroll_ipc.fetch()` (`scroll_ipc.luau:44`) on startup (`get_scroller`, `get_trails`, `get_binding_state`). +- `scroll_state.luau:15` provides three fingerprints — full scroller (bar display, scale rounded to `%.2f`), trails, and panel scroller (excludes scale/overview) — so watchers only fire on visible changes. +- Commands from the panel are sent as `scrollmsg -- ` via `runAsync` (`scroller_panel.luau:9`). + +## Project Structure + +``` +scroll-tools/ +├── plugin.toml # Plugin manifest, settings, widget/panel declarations +├── scroll_ipc.luau # Shared scrollmsg helpers (socket resolution, subscribe, fetch) +├── scroll_state.luau # Fingerprinting + deduplicated state publishing +├── scroller_widget.luau # Bar widget: scroller HUD +├── trails_widget.luau # Bar widget: trails counter +├── submap_widget.luau # Bar widget: binding-mode indicator +├── scroller_panel.luau # Panel: interactive scroller controls +└── translations/ + └── en.json # English strings for settings +``` + +## Compatibility + +- Tested with Noctalia plugin API `8`. +- Scroll is supported natively by Noctalia's Sway-IPC compatibility layer; no extra Noctalia configuration is needed beyond this plugin. +- The plugin is a no-op (widgets hidden) on non-Scroll compositors (Sway, Hyprland, etc.). + +## License + +MIT — see `plugin.toml:6`. Author: n3ptune-plan3t (n3ptune-plan3t). From 01a42d40471c0f49c77fb58997a1386d9b5fa4c9 Mon Sep 17 00:00:00 2001 From: n3ptune-plan3t Date: Sun, 30 Aug 2026 11:25:53 +0530 Subject: [PATCH 2/2] Add files via upload --- scroll-tools/plugin.toml | 49 ++++++++ scroll-tools/scroll_ipc.luau | 53 +++++++++ scroll-tools/scroll_state.luau | 75 ++++++++++++ scroll-tools/scroller_panel.luau | 186 ++++++++++++++++++++++++++++++ scroll-tools/scroller_widget.luau | 135 ++++++++++++++++++++++ scroll-tools/submap_widget.luau | 64 ++++++++++ scroll-tools/thumbnail.webp | Bin 0 -> 46702 bytes scroll-tools/trails_widget.luau | 56 +++++++++ scroll-tools/translations/en.json | 12 ++ 9 files changed, 630 insertions(+) create mode 100644 scroll-tools/plugin.toml create mode 100644 scroll-tools/scroll_ipc.luau create mode 100644 scroll-tools/scroll_state.luau create mode 100644 scroll-tools/scroller_panel.luau create mode 100644 scroll-tools/scroller_widget.luau create mode 100644 scroll-tools/submap_widget.luau create mode 100644 scroll-tools/thumbnail.webp create mode 100644 scroll-tools/trails_widget.luau create mode 100644 scroll-tools/translations/en.json diff --git a/scroll-tools/plugin.toml b/scroll-tools/plugin.toml new file mode 100644 index 00000000..ffeb77a4 --- /dev/null +++ b/scroll-tools/plugin.toml @@ -0,0 +1,49 @@ +id = "n3ptune-plan3t/scroll-tools" +name = "Scroll Tools" +version = "1.0.0" +plugin_api = 8 +author = "n3ptune-plan3t (n3ptune-plan3t)" +license = "MIT" +description = "Scroll-specific bar widgets: scroller mode/position/fit HUD with a control panel, a trails counter, and a binding-mode indicator. Mirrors the scroll integration in dawsers/gtkshell (scroll.cpp/scroll.h) for Noctalia Shell." +tags = ["Bar", "Panel", "Compositor", "Utility"] + +# Noctalia's core already treats Scroll as Sway-IPC-compatible (Scroll exports +# SWAYSOCK), so generic workspaces/window-title/keyboard-layout widgets work +# out of the box. This plugin only adds the scroll-specific extras that have +# no sway/i3 equivalent: scroller modifiers, trails, and binding mode. + +[[setting]] +key = "socket_path" +label_key = "settings.socket_path.label" +description_key = "settings.socket_path.description" +type = "string" +default = "" +advanced = true + +[[setting]] +key = "show_icons" +label_key = "settings.show_icons.label" +description_key = "settings.show_icons.description" +type = "bool" +default = true + +[[widget]] +id = "scroller" +entry = "scroller_widget.luau" + +[[widget]] +id = "trails" +entry = "trails_widget.luau" + +[[widget]] +id = "submap" +entry = "submap_widget.luau" + +[[panel]] +id = "scroller-controls" +entry = "scroller_panel.luau" +width = 320 +height = 380 +placement = "attached" +position = "auto" +dismiss_on_outside_click = true diff --git a/scroll-tools/scroll_ipc.luau b/scroll-tools/scroll_ipc.luau new file mode 100644 index 00000000..16c7b300 --- /dev/null +++ b/scroll-tools/scroll_ipc.luau @@ -0,0 +1,53 @@ +-- scroll_ipc.luau — shared scrollmsg helpers (mirrors ScrollIpc in gtkshell/scroll.cpp). + +local M = {} + +function M.shellQuote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +function M.socketPath() + local override = noctalia.getConfig("socket_path") + if override ~= nil and override ~= "" then + return override + end + local env = noctalia.getenv("SCROLLSOCK") + if env == nil or env == "" then + env = noctalia.getenv("SWAYSOCK") + end + return env +end + +function M.scrollmsgBase() + local sock = M.socketPath() + if sock ~= nil and sock ~= "" then + return "scrollmsg -s " .. M.shellQuote(sock) + end + return "scrollmsg" +end + +function M.isAvailable() + if not noctalia.commandExists("scrollmsg") then + return false + end + local sock = M.socketPath() + return sock ~= nil and sock ~= "" +end + +-- Long-lived subscribe stream; blocks in scrollmsg until events arrive (gtkshell +-- ScrollIpc::working_thread + recv). Returns false if the process failed to start. +function M.subscribe(eventsJson, onLine) + local cmd = M.scrollmsgBase() .. " -t subscribe -m -r " .. M.shellQuote(eventsJson) + return noctalia.runStream(cmd, onLine) +end + +function M.fetch(ipcType, onEnvelope) + noctalia.runAsync(M.scrollmsgBase() .. " -t " .. ipcType .. " -r", function(result) + if result.exitCode ~= 0 or result.stdout == nil or result.stdout == "" then + return + end + onEnvelope(noctalia.json.decode(result.stdout)) + end) +end + +return M diff --git a/scroll-tools/scroll_state.luau b/scroll-tools/scroll_state.luau new file mode 100644 index 00000000..6b723267 --- /dev/null +++ b/scroll-tools/scroll_state.luau @@ -0,0 +1,75 @@ +-- scroll_state.luau — semantic fingerprints and publish-if-changed helpers. +-- +-- Fingerprints match what each UI surface actually displays, so irrelevant +-- JSON churn (e.g. scale precision the bar rounds away) does not wake watchers. + +local M = {} + +local lastFingerprints = {} + +local function boolStr(value) + return value and "1" or "0" +end + +-- Fields used by scroller_widget composeText (scale rounded to 2 decimals). +function M.scrollerFingerprint(scroller) + if scroller == nil then + return "" + end + local scalePart = "" + if scroller.scaled then + scalePart = string.format("%.2f", scroller.scale or 1.0) + end + return table.concat({ + tostring(scroller.mode or ""), + tostring(scroller.insert or ""), + boolStr(scroller.focus == true), + tostring(scroller.reorder or ""), + tostring(scroller.fit or ""), + boolStr(scroller.center_horizontal == true), + boolStr(scroller.center_vertical == true), + boolStr(scroller.overview == true), + boolStr(scroller.scaled == true), + scalePart, + }, "|") +end + +-- Fields used by trails_widget composeText. +function M.trailsFingerprint(trails) + if trails == nil then + return "" + end + return table.concat({ + tostring(trails.active or 0), + tostring(trails.length or 0), + tostring(trails.trail_length or 0), + }, "|") +end + +-- Fields used by scroller_panel buildTree (excludes scale/overview/scaled). +function M.panelScrollerFingerprint(scroller) + if scroller == nil then + return "" + end + return table.concat({ + tostring(scroller.mode or ""), + tostring(scroller.insert or ""), + boolStr(scroller.focus == true), + tostring(scroller.reorder or ""), + tostring(scroller.fit or ""), + boolStr(scroller.center_horizontal == true), + boolStr(scroller.center_vertical == true), + }, "|") +end + +-- Publish value to noctalia.state only when the fingerprint changed. +function M.publishIfChanged(key, value, fingerprint) + if fingerprint == lastFingerprints[key] then + return false + end + lastFingerprints[key] = fingerprint + noctalia.state.set(key, value) + return true +end + +return M diff --git a/scroll-tools/scroller_panel.luau b/scroll-tools/scroller_panel.luau new file mode 100644 index 00000000..bf378436 --- /dev/null +++ b/scroll-tools/scroller_panel.luau @@ -0,0 +1,186 @@ +-- scroller_panel.luau — [[panel]] "scroller-controls" +-- +-- Interactive control panel for the focused workspace's scroller modifiers. +-- Mirrors the popover in ScrollScroller (gtkshell/scroll.cpp). + +local scrollIpc = require("./scroll_ipc.luau") +local scrollState = require("./scroll_state.luau") + +local function send(command) + noctalia.runAsync(scrollIpc.scrollmsgBase() .. " -- " .. command, function(result) + if result.exitCode ~= 0 then + noctalia.notifyError("Scroll", "Command failed: " .. command) + end + end) +end + +local MODE_OPTIONS = { "Horizontal", "Vertical" } +local INSERT_OPTIONS = { "After", "Before", "Beginning", "End" } +local INSERT_COMMANDS = { "after", "before", "beginning", "end" } +local FIT_OPTIONS = { "No fit", "Split", "Fraction" } +local FIT_COMMANDS = { "nofit", "fitsplit", "fitfraction" } + +local panelOpen = false +local lastRenderedFingerprint = nil + +local function indexOf(list, value) + for i, v in ipairs(list) do + if v == value then + return i - 1 + end + end + return 0 +end + +local function labeledRow(labelText, control) + return { + type = "row", + props = { gap = 8, align = "center", justify = "space_between" }, + children = { + { type = "label", props = { text = labelText } }, + control, + }, + } +end + +local function buildTree() + if not scrollIpc.isAvailable() then + return { + type = "column", + props = { gap = 8, padding = 12 }, + children = { + { type = "label", props = { text = "Scroll not detected." } }, + { type = "label", props = { text = "This panel only applies under the Scroll compositor." } }, + }, + } + end + + local scroller = noctalia.state.get("scroll.scroller") or {} + + return { + type = "column", + props = { gap = 10, padding = 12 }, + children = { + labeledRow("Direction", { + type = "select", + props = { + width = 140, + options = MODE_OPTIONS, + selectedIndex = scroller.mode == "vertical" and 1 or 0, + onChange = "onModeChange", + }, + }), + labeledRow("Insert position", { + type = "select", + props = { + width = 140, + options = INSERT_OPTIONS, + selectedIndex = indexOf(INSERT_COMMANDS, scroller.insert), + onChange = "onInsertChange", + }, + }), + labeledRow("Focus new window", { + type = "toggle", + props = { checked = scroller.focus == true, onChange = "onFocusChange" }, + }), + labeledRow("Auto reorder", { + type = "toggle", + props = { checked = scroller.reorder == "auto", onChange = "onReorderChange" }, + }), + labeledRow("Fit", { + type = "select", + props = { + width = 140, + options = FIT_OPTIONS, + selectedIndex = indexOf(FIT_COMMANDS, scroller.fit), + onChange = "onFitChange", + }, + }), + labeledRow("Center column", { + type = "toggle", + props = { checked = scroller.center_horizontal == true, onChange = "onCenterHChange" }, + }), + labeledRow("Center window", { + type = "toggle", + props = { checked = scroller.center_vertical == true, onChange = "onCenterVChange" }, + }), + { type = "separator", props = {} }, + { type = "button", props = { text = "Refresh", onClick = "onRefresh" } }, + }, + } +end + +local function refreshFingerprint() + local scroller = noctalia.state.get("scroll.scroller") + return scrollState.panelScrollerFingerprint(scroller) +end + +local function refresh() + if not panelOpen then + return + end + local fingerprint = refreshFingerprint() + if fingerprint == lastRenderedFingerprint then + return + end + lastRenderedFingerprint = fingerprint + panel.render(buildTree()) +end + +noctalia.state.watch("scroll.scroller", refresh) + +function onOpen() + panelOpen = true + lastRenderedFingerprint = nil + refresh() +end + +function onClose() + panelOpen = false +end + +function onModeChange(idxStr) + send(tonumber(idxStr) == 1 and "set_mode v" or "set_mode h") +end + +function onInsertChange(idxStr) + local command = INSERT_COMMANDS[(tonumber(idxStr) or 0) + 1] + if command ~= nil then + send("set_mode " .. command) + end +end + +function onFocusChange(checkedStr) + send(checkedStr == "true" and "set_mode focus" or "set_mode nofocus") +end + +function onReorderChange(checkedStr) + send(checkedStr == "true" and "set_mode reorder_auto" or "set_mode noreorder_auto") +end + +function onFitChange(idxStr) + local command = FIT_COMMANDS[(tonumber(idxStr) or 0) + 1] + if command ~= nil then + send("set_mode " .. command) + end +end + +function onCenterHChange(checkedStr) + send(checkedStr == "true" and "set_mode center_horiz" or "set_mode nocenter_horiz") +end + +function onCenterVChange(checkedStr) + send(checkedStr == "true" and "set_mode center_vert" or "set_mode nocenter_vert") +end + +function onRefresh() + scrollIpc.fetch("get_scroller", function(payload) + if type(payload) == "table" and payload.scroller ~= nil then + scrollState.publishIfChanged( + "scroll.scroller", + payload.scroller, + scrollState.scrollerFingerprint(payload.scroller) + ) + end + end) +end diff --git a/scroll-tools/scroller_widget.luau b/scroll-tools/scroller_widget.luau new file mode 100644 index 00000000..90c28a9a --- /dev/null +++ b/scroll-tools/scroller_widget.luau @@ -0,0 +1,135 @@ +-- scroller_widget.luau — [[widget]] "scroller" +-- +-- Mirrors ScrollScroller in gtkshell/scroll.cpp: owns its own scrollmsg +-- subscribe(["scroller"]) stream and updates the bar label directly on each +-- event — no timers, no central bridge. Also publishes scroll.scroller for the +-- control panel. + +local scrollIpc = require("./scroll_ipc.luau") +local scrollState = require("./scroll_state.luau") + +-- Event-driven via runStream; stretch Noctalia's default 250ms noop timer. +noctalia.setUpdateInterval(3600000) + +local showIcons = noctalia.getConfig("show_icons") +if showIcons == nil then + showIcons = true +end + +local function symbol(icon, plain) + if showIcons then + return icon + end + return plain +end + +local function composeText(scroller) + if scroller == nil then + return "" + end + + local mode = scroller.mode == "horizontal" and symbol("-", "H") or symbol("|", "V") + + local pos = "" + local insert = scroller.insert + if insert == "after" then + pos = symbol("→", ">") + elseif insert == "before" then + pos = symbol("←", "<") + elseif insert == "end" then + pos = symbol("⇥", "E") + elseif insert == "beginning" then + pos = symbol("⇤", "B") + end + + local focus = scroller.focus and symbol("◉", "f") or symbol("◎", "-") + local reorder = scroller.reorder == "auto" and symbol("A", "a") or symbol("M", "-") + + local fit = "" + if scroller.fit == "nofit" then + fit = symbol("·", "-") + elseif scroller.fit == "fitsplit" then + fit = symbol("S", "S") + elseif scroller.fit == "fitfraction" then + fit = symbol("F", "F") + end + + local centerH = scroller.center_horizontal and symbol("⇔", "h") or "" + local centerV = scroller.center_vertical and symbol("⇕", "v") or "" + + local overview = scroller.overview and symbol(" 󰆾", " *") or "" + local scale = "" + if scroller.scaled then + scale = string.format(" %.2fx", scroller.scale or 1.0) + end + + return string.format("%s %s %s %s %s%s%s%s", mode, pos, focus, reorder, fit, centerH, centerV, overview) .. scale +end + +local function tooltipRows(scroller) + if scroller == nil then + return "Waiting for Scroll…" + end + local rows = {} + table.insert(rows, { key = "Mode", value = scroller.mode }) + table.insert(rows, { key = "Insert", value = scroller.insert }) + table.insert(rows, { key = "Focus", value = scroller.focus and "focus" or "nofocus" }) + table.insert(rows, { key = "Reorder", value = scroller.reorder }) + table.insert(rows, { key = "Fit", value = scroller.fit }) + table.insert(rows, { key = "Center H", value = scroller.center_horizontal and "on" or "off" }) + table.insert(rows, { key = "Center V", value = scroller.center_vertical and "on" or "off" }) + return rows +end + +local lastText = nil + +local function render(scroller) + local text = composeText(scroller) + if text == lastText then + return + end + lastText = text + barWidget.setText(text) + barWidget.setTooltip(tooltipRows(scroller)) +end + +local function applyScroller(scroller) + scrollState.publishIfChanged( + "scroll.scroller", + scroller, + scrollState.scrollerFingerprint(scroller) + ) + render(scroller) +end + +local function handleEnvelope(payload) + if type(payload) ~= "table" or payload.scroller == nil then + return + end + applyScroller(payload.scroller) +end + +local function handleLine(line) + if line == nil or line == "" then + return + end + handleEnvelope(noctalia.json.decode(line)) +end + +if not scrollIpc.isAvailable() then + barWidget.setVisible(false) + noctalia.state.set("scroll.available", false) +else + barWidget.setVisible(true) + noctalia.state.set("scroll.available", true) + scrollIpc.fetch("get_scroller", handleEnvelope) + if not scrollIpc.subscribe('["scroller"]', handleLine) then + noctalia.log("scroll-tools: scroller widget failed to start scrollmsg subscribe") + barWidget.setVisible(false) + noctalia.state.set("scroll.available", false) + end +end + +function onClick() + noctalia.togglePanel("n3ptune-plan3t/scroll-tools:scroller-controls") +end diff --git a/scroll-tools/submap_widget.luau b/scroll-tools/submap_widget.luau new file mode 100644 index 00000000..87f9a099 --- /dev/null +++ b/scroll-tools/submap_widget.luau @@ -0,0 +1,64 @@ +-- submap_widget.luau — [[widget]] "submap" +-- +-- Mirrors ScrollSubmap in gtkshell/scroll.cpp: own subscribe(["mode"]) stream, +-- direct label update on each event. + +local scrollIpc = require("./scroll_ipc.luau") + +-- Event-driven via runStream; stretch Noctalia's default 250ms noop timer. +noctalia.setUpdateInterval(3600000) + +local showIcons = noctalia.getConfig("show_icons") +if showIcons == nil then + showIcons = true +end + +local function composeText(mode) + if mode == nil or mode == "" or mode == "default" then + return "" + end + local icon = showIcons and "⌨" or "MODE:" + return icon .. " " .. mode +end + +local lastText = nil + +local function render(mode) + local text = composeText(mode) + if text == lastText then + return + end + lastText = text + barWidget.setVisible(text ~= "") + barWidget.setText(text) + barWidget.setTooltip("Scroll binding mode") +end + +local function handleEnvelope(payload) + if type(payload) ~= "table" then + return + end + -- Live subscribe events use {"change": ""}; get_binding_state uses {"name": "..."}. + local mode = payload.change or payload.name + if mode == nil then + return + end + render(mode) +end + +local function handleLine(line) + if line == nil or line == "" then + return + end + handleEnvelope(noctalia.json.decode(line)) +end + +if not scrollIpc.isAvailable() then + barWidget.setVisible(false) +else + scrollIpc.fetch("get_binding_state", handleEnvelope) + if not scrollIpc.subscribe('["mode"]', handleLine) then + noctalia.log("scroll-tools: submap widget failed to start scrollmsg subscribe") + barWidget.setVisible(false) + end +end diff --git a/scroll-tools/thumbnail.webp b/scroll-tools/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..18e3e29901dcb1630072f5eb0d1214d9b30eaffd GIT binary patch literal 46702 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

frlb3@7NA2-=}*F>&|sx!BSu~pg? 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 he|%FTTNAYEyxIsK>_0mK)oKZ_5&R z7^2XjEF&78pMcDg6_-Q2dM4dXsQ?%gT;5UDwbosXfv~#zoEpTr{!a-Qp`=HJ*6h3< zh(TW=5_|!OedeNWY9L0PCqMaU4Z4^~4yM$#Y!(W7UadT%6N&qsN3@d%+B!<@=N`25 zwR^0($py>yDc=+>c8ZOh3>cBc*6u1YVwK)}{hg*3`e)<1Q*DZ~FA`r&65XfXQHcBt z@Sm4r)g$AS2gY1nz$J&aEjQW_cu(wOjL|>12hg;ckiFO`Zb$FjbdbE!uib-x8p!km z*e|6fK9Xn{Q#X@XS;MmeVtld;jjc*SX40X5z>emBo84)Vq>#47Z&Eu zH)ewzo3R2}Q0PyDmoaGkis-i|OX{&0xbGjVr>A?l09X_T+D+1nhfP&hdi7!snS2U( z%PRlDGc_b?5UOS23!soa{}Kn$kR#ptDi|udV@i$D>8>1ds(@vz-+2J4^X|&kLUznJ@$yw`W#pKt~szT#pA@ zJxSa?p58BypNw;>-lA3V&am{kYv?C~Z@8fD zZ6A*WSxBFD#?91^gN|Z~k~Ns7iZevy~m?eW5eS`Fc&tG>RhTy_HIgB!UlbBJ;g zB?m>WM^q?bPsgN9@Tp%oPW@oudNG$eaP3>PUImwM5&hvzzgN z{E#NJ7YfmphI-#Q$ed>SvDCJE;#AWmykrGZu2m=tT5lYb1EO+Ozf^wx90v8pAon5pmGb`SR@<0Cy$sTNBI#j!x2sIWeNCMOcFZ|AF~XMQcWih6M|ziZRK$@-p{?{zfDVAXyUE=#c9+5sGa z$%w=mB1e9P=O(HzE{EY3xk0Is3 z;GGR!`+WvMf^e(0!l9>lFSpKi2lJIR`3HVqDMX%?$^LpO#dXFa?jaKi+EYF(*OM?~xQjQ4ww#}lpNIbYZ~i}TY~mZ*@)ppfTm`mq0ZzUTC8zyi! z{U=AP_VVe&&%|iZ+n^!u9Z+;L*nLu_wf|Y`K~k8=0#$z)0^v~V!fW`1{*j_EUmg?q zpXT7#QiWfI1^1eYPuwH-X-yi+2q8KxlJ5mMG8xnq-UKymy#ZUN#W~Y6bw; zhZNKY4LThm!~icQFg#%2Jv~Vc@3=9^nOgF<3V%^0)jUo#t=)Hh8d7Kt%v4~=EZMp| zBH|Wg)YR##1hR#)%6Mpp&eCuJ1E%w9j{Z7it}ocp2i`$?lr4+sZ50H3frY_KuzO6{)x{R{@U)ezdaDI2RJ8%Fe8Jx6z~-At;sE!jB4h(5e>U z-be`q#bzcIGg`C4lX!{5=6kSg7yHl#KIFa!_wBZe6!oYDox8&N{vK*v&gC2BLe6#UG{@^Zeoi)_Tg^();9ZJxs7E~*?ji{T zbPk_%2yEn6{J5{-cXEex9ITt>TXXIN@8Q)f|JOL?$=rAW2{^INA_!SPLkd&3R@AGG z!II~NzmBCTAVE@nFupF1%b~9!Vmnlrt03|6hJS#N3Or#})e*6ltK8>;p5&n4tNR7J za+S+KLaejDQh=n9hHZnL-;@1chK;?xG}rv$QFAOVKwS4QkAu8#2J~iwpDwaJ5?^os zPatQU!O7>3gfX^8=llYEa-z9FWwi<{5wWsx`2rxrnZ59N3M9O~Cq5<<3MNC&b8^;0xpY+3v7Q9n3VKO# z9&w3V=ef}leLleXFnSE?D$qb#?=@cPY|P9Zi%5)fG7VLGLn#k0A?)5Jh*YF>$1`AmKi%ffW@Lm`BOouGN=Xm7@!X_#oNteeVYIStJVy_-6O(aYt zfF=JyCjFh_->qrww5jD}n5ZI}#|3(ga5?I8QR8@x$cmd0kG}X!9CL1-Q@38=%pM-i z&pvKtYv_~(afE9YC&7U^>Kpo6vh;9q)Q=iq5V#~?dwesi05veywEQG=({}DMtH)D0 z@5-nbdQ5gd8tjQca8lGe(d(L8{KEtB;2LiT_flK20GqT_Cb#f&Nd-in{g@()|#-QedoQaqsND2sgK+Y#vsKlH` ztEB77?-E6X8k;F?K85hlSBrBz#;(JCjN1rMPpJnDa-zi@xC{mtlo&%ua5F3W9kV)# zKz&F2ltRHGZ;w-zL~0)GjEJohizMVVZZxBC z!IR0EYXP^%)pukDHwJ#T>nKbwKFSX(Bw|rZ=&?YZ(<^jF$b_1a+ere!66cK86Z;kA z$`EPLSvWlLx@3ifbPgp@V%#_uAZ*217cBgvofEak)oNK9miPro&GvH8jaWVTv$gDm-fv`kmFxK}ylX=znR3k%EzC8`yg@4(hjHb%=# zumO#cT$ThuxW&ju64OX=s z=g{Ea4KSoz&A|Q%Ogx#nS$0V<=soRTtr||BI=_GfVQ{5or;GH9Fo_p=>#o4TSbOO! z^#?}v(`;5Qiz2QkK=~H z4)7dWWN^FVp|Y&BOzPVyVx1!)&a|9atf?}G3U)>AHxs?99G$2dBE^Y$cYXNW$Apgd z1=yBQCo{oIvT~}R=1@f>cem7T@7G0U%l-h)=dgNtj^BJ+1r-wG{v(_E`9$!Vy}-&Y z72NMEt3LwPf2FJjxyg==w-ptQ?Y%Q16*f!t0TBssKX!_-f_dB7x8r#E?lm1gGHf7X zxE!=JDv>q+Z;w$rwt&tmuoV93-q4(VGM?&-yAW!cW+~y;+@|%4rJL4Kv>4dECpI?o zi53+w{nE!_TD5rgth8k4$y~2sM3e+c6#~1AU(8%ML`d;clRcm<1o$mV*aI72CazM; zj1<_japs#Otj|#NwsszsJOc|;SQUH(z=?R!XEL0D&V`j}7Zg~4^IyMV2n`=?-@GU7 z$n+fG0{m!G=aw>Trc>CMBuoZgR@mn$(rua?@w!nhk;0$b>Vx69YCbbd^b+?uikspz zIO9fL2C1SxJSc`Mokw2d&tZy?s-dG^VO6DY8h(kI?g-)Eve=szi=sTQ>9z*&37~RQ zbgjLO1XVb(*;XxY$P_^WUr#d0NJaLEt%rq2RCtZ?Uok7Uq;N=oJrk6kfQD%ih?gkN<_-Eq}Q2R z-~@6>T3`yU(ZI}uT4_KF)#PlL=7e zSifE4oqUwmRqa_6%5nk0+)AJgt8Rt9Lt+K?9n=HIo0DQ!^|= zH-CS~5=kz4eTr(f&?hC7QR*mpE}XwSY7*7q@uJ z%xl{$c6^J@67nCD!tx}9;su@d_0eOJ59o%+YN(|=z!OlRuNPRqkNM(HKoB5gj;|2@ zxW1Vzl`mJl8wP~sBiLhJykkdpq~h4+_^ucUe$)j+7xO0u*_@>J6r`X1WSd#a{nJOG zx<5MmWUbZxFx4`GC*!=&MXGxwqn}zH$f6jusy&X3gNF`j!Ndrm{s)4Agwpoz*rP2lg#9j7it5mD>yRJzTBR(pa_2YYAk-_q z7<&4dD}rL}GyK4Xv@L*;qv}v3t+dF^66#UdUNff)j&F}tf>G3trwfko|MnsB{ugp$ zJ14z$d0V~gi9@~_oWD7%4!kQqa%$F7FZIo+NCYFgPTAvLZ3^xuEg;xax6aOPD9h8( z<1AOVjK>A7RdosN$$Vh2UY#C`{y((knMhcw_KnQU_uU%gsoU#iJUuTgckTXIe-c~c zTGjl1KWc**L=kIB8IhQu+s>=IjUT!&p24DnHieKRyqRiAfM1uyA{u>PJ_k_<&B*PL zT2UAaFiQLRH@VDmmE>a1{0r(GKQASxh(C3Ch$${hLB$z6h-wDRQV^?&sSU7JQXRNr z9MtKE9#iB`Ei^}l4>bo&AjLsZQGQz*yzjt6i3h8j4AY$Aoi_f8+^biYMlKIU)z4wvYw_5L_6G@uU&FdgW>;4{%$P)+ubAA#6! zkWbPnw-1bBhc*G5yP#%tNnGS*U?5ksor`|e_o!+)AtWa4eab*8Ftf0v~mmU#XG6&PI&d>yyezEYpHN&4VEhLUZ#9 z)r~47UMqgvn?noX_jdKVpKVPK6X9ruD~7Qr0lFFOHN_)dd;rTRxR+HTvbp*Lle~0Q zV62r&57x>QcA%@Oxdvx%3q)}8p{mA2-?d9u`KfWr#ls^Zwgj9FIu|I4CiPAn7rOQ` zzh|6Gfc;8G9dx~|n)~TI)J6G`aE6aTpL#S97=-eX|(3?RGmWKVgkrpqy`VysBPfeOw@NP7-*wZxRnOED8Vd+{&UEr!6! zAlilsf1*9Hgt~IrF*GujK~>GK^aO))Sr{kvn;umRxLs<|-ag0k`oODW0!D=*4f)Zq z0bOWEP|mOcakQi^2Aks^*A62GSpHBuhMI_t)a7gfikInZ;P-pU$BcBx{!G32WmXR% zR^*MbZ!r|ZMkcD%S6wY8uRtSxi6U3vQl$Fgnd3ER`IH6oGs}c3DZzE)k19EJ#4vW8 zK=2)DPk-%jyQ|1J7yS3{&f6{)!O`WPSnriZjvp`4q7Tvxh3kmFFefuqJE-EJ!jz$D a={Ku1i>IFDFaXKtcql`x9b@)@0001{QY=XT literal 0 HcmV?d00001 diff --git a/scroll-tools/trails_widget.luau b/scroll-tools/trails_widget.luau new file mode 100644 index 00000000..77a10671 --- /dev/null +++ b/scroll-tools/trails_widget.luau @@ -0,0 +1,56 @@ +-- trails_widget.luau — [[widget]] "trails" +-- +-- Mirrors ScrollTrails in gtkshell/scroll.cpp: own subscribe(["trails"]) stream, +-- direct label update on each event. + +local scrollIpc = require("./scroll_ipc.luau") + +-- Event-driven via runStream; stretch Noctalia's default 250ms noop timer. +noctalia.setUpdateInterval(3600000) + +local function composeText(trails) + if trails == nil then + return "" + end + local active = trails.active or 0 + local length = trails.length or 0 + local marks = trails.trail_length or 0 + return string.format("%d/%d (%d)", active, length, marks) +end + +local lastText = nil + +local function render(trails) + local text = composeText(trails) + if text == lastText then + return + end + lastText = text + barWidget.setText(text) + barWidget.setTooltip("Scroll trails: position / length (marks)") +end + +local function handleEnvelope(payload) + if type(payload) ~= "table" or payload.trails == nil then + return + end + render(payload.trails) +end + +local function handleLine(line) + if line == nil or line == "" then + return + end + handleEnvelope(noctalia.json.decode(line)) +end + +if not scrollIpc.isAvailable() then + barWidget.setVisible(false) +else + barWidget.setVisible(true) + scrollIpc.fetch("get_trails", handleEnvelope) + if not scrollIpc.subscribe('["trails"]', handleLine) then + noctalia.log("scroll-tools: trails widget failed to start scrollmsg subscribe") + barWidget.setVisible(false) + end +end diff --git a/scroll-tools/translations/en.json b/scroll-tools/translations/en.json new file mode 100644 index 00000000..249de96d --- /dev/null +++ b/scroll-tools/translations/en.json @@ -0,0 +1,12 @@ +{ + "settings": { + "socket_path": { + "label": "Scroll IPC socket path", + "description": "Override the socket used to talk to Scroll. Leave empty to use $SCROLLSOCK or $SWAYSOCK." + }, + "show_icons": { + "label": "Use icons", + "description": "Show compact glyphs in the scroller and binding-mode widgets instead of plain-text labels." + } + } +}