aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/obs
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/obs')
-rw-r--r--desktop/modules/obs/ObsScenesModule.qml59
-rw-r--r--desktop/modules/obs/ObsScenesPage.qml138
-rw-r--r--desktop/modules/obs/README.md21
3 files changed, 218 insertions, 0 deletions
diff --git a/desktop/modules/obs/ObsScenesModule.qml b/desktop/modules/obs/ObsScenesModule.qml
new file mode 100644
index 0000000..a91149e
--- /dev/null
+++ b/desktop/modules/obs/ObsScenesModule.qml
@@ -0,0 +1,59 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+import "../.."
+import "../../island"
+
+// Which glyph each OBS scene shows in the island pill, from the same watched
+// map the pill reads. Set once per scene: paste an icon from the Nerd Fonts
+// cheat sheet. OBS is not required to edit the configured rows.
+Module {
+ id: mod
+ name: "obs"
+ label: "OBS scenes"
+ // nf-md-record_circle, present in Inconsolata Nerd Font.
+ icon: "\udb83\udec2"
+ settings: true
+ group: "Desktop"
+ dirty: SceneIcons.dirty
+ keywords: ["obs", "scene", "icon", "glyph", "recording", "webcam", "stream"]
+
+ // The saved keys plus whatever OBS currently reports, so a scene created
+ // in OBS appears without a restart and a configured scene survives OBS
+ // being closed.
+ function names() {
+ const out = [];
+ for (const n in SceneIcons.map.scenes) out.push(n);
+ for (const n of Obs.scenes) if (out.indexOf(n) < 0) out.push(n);
+ return out;
+ }
+
+ function refresh() {
+ if (!SceneIcons.dirty) SceneIcons.begin();
+ mod.searchItems = mod.names().map(n => ({ text: n, key: n }));
+ }
+
+ page: Component {
+ Page {
+ title: "OBS scenes"
+ actions: Component {
+ SaveBar {
+ dirty: SceneIcons.dirty
+ valid: SceneIcons.valid
+ onSave: SceneIcons.save(SceneIcons.draft)
+ onDiscard: SceneIcons.discard()
+ }
+ }
+ ObsScenesPage { width: parent.width }
+ }
+ }
+}
diff --git a/desktop/modules/obs/ObsScenesPage.qml b/desktop/modules/obs/ObsScenesPage.qml
new file mode 100644
index 0000000..0fb06a9
--- /dev/null
+++ b/desktop/modules/obs/ObsScenesPage.qml
@@ -0,0 +1,138 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+import "../.."
+import "../../island"
+
+// One row per scene: its name and a field holding its glyph. Paste an icon
+// from the Nerd Fonts cheat sheet; an empty field falls back to the generic.
+Column {
+ id: page
+
+ spacing: 6
+
+ // Bumped on discard/save so the rows are rebuilt. A TextInput breaks its
+ // text binding on user edit, so without this a Discard would leave the
+ // discarded glyph on screen.
+ property int rev: 0
+ Connections {
+ target: SceneIcons
+ function onReset() { page.rev++ }
+ }
+
+ readonly property var rows: {
+ const out = [];
+ const add = n => { if (!out.some(r => r.name === n)) out.push({ name: n, rev: page.rev }); };
+ for (const n in SceneIcons.map.scenes) add(n);
+ for (const n of Obs.scenes) add(n);
+ return out;
+ }
+
+ function glyphOf(name) {
+ const d = SceneIcons.draft;
+ if (d && d.scenes) return d.scenes[name] ?? "";
+ return SceneIcons.map.scenes[name] ?? "";
+ }
+
+ function genericGlyph() {
+ const d = SceneIcons.draft;
+ return d ? (d.generic ?? "") : SceneIcons.map.generic;
+ }
+
+ Text {
+ width: parent.width
+ wrapMode: Text.Wrap
+ text: "Paste an icon from the Nerd Fonts cheat sheet. An empty field falls back to the default for unmapped scenes."
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+
+ Repeater {
+ model: page.rows
+ delegate: Item {
+ id: row
+ required property var modelData
+ objectName: modelData.name
+ width: page.width
+ height: 40
+
+ Text {
+ anchors { left: parent.left; right: field.left; rightMargin: 10; verticalCenter: parent.verticalCenter }
+ elide: Text.ElideRight
+ text: row.modelData.name
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
+ }
+
+ Rectangle {
+ id: field
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ width: 64
+ height: 34
+ radius: 8
+ color: Theme.surface
+ border.width: 1
+ border.color: field.invalid ? Theme.red : Qt.alpha(Theme.text, 0.15)
+ readonly property string value: page.glyphOf(row.modelData.name)
+ readonly property bool invalid: value !== "" && !SceneIcons.validGlyph(value)
+
+ TextInput {
+ anchors.fill: parent
+ horizontalAlignment: TextInput.AlignHCenter
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: true
+ text: field.value
+ color: Theme.text
+ font { family: Theme.iconFamily; pixelSize: Theme.fontSize + 4 }
+ onTextEdited: SceneIcons.editScene(row.modelData.name, text)
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 40
+
+ Text {
+ anchors { left: parent.left; right: gfield.left; rightMargin: 10; verticalCenter: parent.verticalCenter }
+ elide: Text.ElideRight
+ text: "Default for unmapped scenes"
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
+ }
+
+ Rectangle {
+ id: gfield
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ width: 64
+ height: 34
+ radius: 8
+ color: Theme.surface
+ border.width: 1
+ border.color: gfield.invalid ? Theme.red : Qt.alpha(Theme.text, 0.15)
+ readonly property string value: page.genericGlyph()
+ readonly property bool invalid: value !== "" && !SceneIcons.validGlyph(value)
+
+ TextInput {
+ anchors.fill: parent
+ horizontalAlignment: TextInput.AlignHCenter
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: true
+ text: gfield.value
+ color: Theme.text
+ font { family: Theme.iconFamily; pixelSize: Theme.fontSize + 4 }
+ onTextEdited: SceneIcons.editGeneric(text)
+ }
+ }
+ }
+}
diff --git a/desktop/modules/obs/README.md b/desktop/modules/obs/README.md
new file mode 100644
index 0000000..c114192
--- /dev/null
+++ b/desktop/modules/obs/README.md
@@ -0,0 +1,21 @@
+# OBS scenes
+
+The settings page for the island's OBS pill. It edits
+`~/.config/quickshell/obs-scene-icons.json`, the same watched file the pill
+reads, so saving recolours the scene grid with no restart.
+
+One row per scene: the scene name and a field holding its glyph. Find an icon
+on the Nerd Fonts cheat sheet, copy the glyph, and paste it into the field. The
+glyph renders in Inconsolata Nerd Font; one the font lacks shows as tofu, which
+means pick another. A field holds exactly one code point (a supplementary-plane
+glyph is two UTF-16 units); anything longer turns the border red and blocks
+Save.
+
+An empty scene field means "use the generic glyph"; the "Default for unmapped
+scenes" row edits that, and an empty default falls back to `?` so no tile is
+ever blank.
+
+Rows are the configured keys plus whatever scenes OBS reports, so the page
+works with OBS closed for scenes it already knows. A scene created in OBS
+appears once the pill is connected. The JSON can also be hand-edited; the pill
+reloads it.