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). 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 00000000..18e3e299 Binary files /dev/null and b/scroll-tools/thumbnail.webp differ 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." + } + } +}