Skip to content

Commit c5ac273

Browse files
authored
Merge pull request #643 from blackteaextract/osk2
add (osk-toggle): consolidates squeekboard-toggle, works now also with wvkbd
2 parents 565e9fe + eab7004 commit c5ac273

26 files changed

Lines changed: 1245 additions & 1 deletion

osk-toggle/BarWidget.qml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Quickshell
2+
import qs.Commons
3+
import qs.Services.UI
4+
import qs.Widgets
5+
6+
NIconButton {
7+
id: root
8+
9+
property var pluginApi: null
10+
property ShellScreen screen
11+
property string widgetId: ""
12+
property string section: ""
13+
property int sectionWidgetIndex: -1
14+
property int sectionWidgetsCount: 0
15+
16+
readonly property var mainInstance: pluginApi?.mainInstance
17+
property bool keyboardActive: mainInstance?.keyboardActive ?? false
18+
property bool available: mainInstance?.available ?? false
19+
20+
readonly property var pluginDefaults: pluginApi?.manifest?.metadata?.defaultSettings ?? ({})
21+
readonly property bool hideWhenUnavailable: pluginApi?.pluginSettings?.hideWhenUnavailable ?? pluginDefaults.hideWhenUnavailable ?? false
22+
readonly property bool disableHoverIcon: pluginApi?.pluginSettings?.disableHoverIcon ?? pluginDefaults.disableHoverIcon ?? false
23+
24+
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name ?? "")
25+
readonly property bool flipHoverIcons: barPosition === "bottom"
26+
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
27+
28+
visible: available || !hideWhenUnavailable
29+
30+
icon: !available ? "alert-circle"
31+
: (hovering && !isVerticalBar && !disableHoverIcon
32+
? (keyboardActive
33+
? (flipHoverIcons ? "keyboard-show" : "keyboard-hide")
34+
: (flipHoverIcons ? "keyboard-hide" : "keyboard-show"))
35+
: (keyboardActive ? "keyboard" : "keyboard-off"))
36+
tooltipText: !available
37+
? pluginApi?.tr(mainInstance?.unavailableTooltipKey ?? "tooltip.detecting")
38+
: (keyboardActive ? pluginApi?.tr("tooltip.active") : pluginApi?.tr("tooltip.hidden"))
39+
tooltipDirection: BarService.getTooltipDirection(screen?.name)
40+
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
41+
applyUiScale: false
42+
customRadius: Style.radiusL
43+
colorBg: Style.capsuleColor
44+
colorFg: !available ? Color.mError : (keyboardActive ? Color.mPrimary : Color.mOnSurfaceVariant)
45+
border.color: Style.capsuleBorderColor
46+
border.width: Style.capsuleBorderWidth
47+
48+
onClicked: { if (available) mainInstance?.toggleKeyboard() }
49+
50+
onRightClicked: {
51+
PanelService.showContextMenu(contextMenu, root, screen)
52+
}
53+
54+
NPopupContextMenu {
55+
id: contextMenu
56+
57+
model: [
58+
{
59+
"label": pluginApi?.tr("menu.settings"),
60+
"action": "widget-settings",
61+
"icon": "settings"
62+
}
63+
]
64+
65+
onTriggered: function(action) {
66+
contextMenu.close()
67+
PanelService.closeContextMenu(screen)
68+
if (action === "widget-settings") {
69+
BarService.openPluginSettings(root.screen, pluginApi.manifest)
70+
}
71+
}
72+
}
73+
}

osk-toggle/Main.qml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import QtQuick
2+
import Quickshell
3+
import Quickshell.Io
4+
5+
Item {
6+
id: root
7+
8+
property var pluginApi: null
9+
10+
// Reads the user's backend preference: "auto", "squeekboard", or "wvkbd".
11+
// "auto" checks whether Squeekboard's D-Bus name is owned at startup and
12+
// falls back to wvkbd if it is not.
13+
readonly property string backendSetting: pluginApi?.pluginSettings?.backend ?? "auto"
14+
15+
// Set once detection/selection is complete; drives the Loader below.
16+
property string resolvedBackend: ""
17+
18+
readonly property var _backend: backendLoader.item
19+
20+
// Unified API consumed by BarWidget and Settings
21+
property bool keyboardActive: _backend?.keyboardActive ?? false
22+
property bool available: _backend?.available ?? false
23+
readonly property string unavailableTooltipKey: _backend?.unavailableTooltipKey ?? "tooltip.detecting"
24+
25+
onBackendSettingChanged: _resolveBackend()
26+
Component.onCompleted: _resolveBackend()
27+
28+
// --- Auto-detection: check if Squeekboard's D-Bus name is owned ---
29+
Process {
30+
id: autoDetect
31+
command: ["busctl", "--user", "call",
32+
"org.freedesktop.DBus", "/org/freedesktop/DBus",
33+
"org.freedesktop.DBus", "GetNameOwner",
34+
"s", "sm.puri.OSK0"]
35+
onExited: (exitCode, exitStatus) => {
36+
root.resolvedBackend = exitCode === 0 ? "squeekboard" : "wvkbd"
37+
}
38+
}
39+
40+
Loader {
41+
id: backendLoader
42+
source: {
43+
if (root.resolvedBackend === "squeekboard") return "SqueekboardBackend.qml"
44+
if (root.resolvedBackend === "wvkbd") return "WvkbdBackend.qml"
45+
return ""
46+
}
47+
onLoaded: {
48+
if (item) item.pluginApi = root.pluginApi
49+
}
50+
}
51+
52+
// Keep pluginApi in sync with the backend (wvkbd reads wvkbdBin from it)
53+
onPluginApiChanged: {
54+
if (backendLoader.item) backendLoader.item.pluginApi = pluginApi
55+
}
56+
57+
function _resolveBackend() {
58+
if (backendSetting === "auto") {
59+
resolvedBackend = ""
60+
autoDetect.running = true
61+
} else {
62+
resolvedBackend = backendSetting
63+
}
64+
}
65+
66+
function recheckState() {
67+
_backend?.recheckState()
68+
}
69+
70+
function toggleKeyboard() {
71+
_backend?.toggleKeyboard()
72+
}
73+
}

osk-toggle/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# OSK Toggle
2+
3+
A [Noctalia](https://github.com/noctalia-dev/noctalia) bar widget for toggling an on-screen keyboard. Supports **Squeekboard** and **wvkbd**, with automatic backend detection.
4+
5+
## Features
6+
7+
- Auto-detects which OSK is available at startup, or use a fixed backend
8+
- Live availability detection — shows an alert icon if the keyboard is no longer available
9+
- Hover icons indicate the action you'll take (show/hide), not just the current state
10+
- Configurable per-widget settings
11+
12+
## Requirements
13+
14+
- Noctalia ≥ 4.4.3
15+
- One of:
16+
- **Squeekboard** — must be running and accessible via D-Bus (`sm.puri.OSK0`); requires `gsettings` and `dconf`
17+
- **wvkbd** — any binary variant (`wvkbd-mobintl`, `wvkbd-comp`, `wvkbd-abc`, etc.) on your `PATH`
18+
19+
## Settings
20+
21+
| Setting | Default | Description |
22+
|---|---|---|
23+
| `backend` | `auto` | `auto`, `squeekboard`, or `wvkbd` |
24+
| `hideWhenUnavailable` | `false` | Hide the widget entirely when the OSK is unavailable |
25+
| `disableHoverIcon` | `false` | Always show the state icon; never show the directional hover icon |
26+
| `wvkbdBin` | `wvkbd-mobintl` | wvkbd binary name or path (wvkbd backend only) |
27+
28+
### Backend selection
29+
30+
- **`auto`** — at startup, checks if Squeekboard's D-Bus name (`sm.puri.OSK0`) is owned. Uses Squeekboard if yes, wvkbd otherwise. Detection happens once; it does not switch automatically if availability changes mid-session.
31+
- **`squeekboard`** — always use Squeekboard.
32+
- **`wvkbd`** — always use wvkbd.
33+
34+
## How it works
35+
36+
### Squeekboard backend
37+
38+
Toggle state is controlled via `gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled`. State changes are tracked live with `dconf watch`, and Squeekboard's D-Bus presence is monitored continuously with `dbus-monitor` so the widget reacts if Squeekboard stops or starts.
39+
40+
#### Tablet Mode (2-in-1 Laptops)
41+
42+
This widget **complements** automated tablet-mode switching.
43+
For Niri configure `switch-events` in `~/.config/niri/config.kdl` to auto-toggle the keyboard:
44+
45+
```kdl
46+
switch-events {
47+
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
48+
tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; }
49+
}
50+
```
51+
52+
The widget will **reflect these changes in real-time** without conflicts. Manual toggles via the widget work independently of tablet-mode automation.
53+
54+
55+
### wvkbd backend
56+
57+
The plugin takes ownership of the wvkbd process: on load it kills any pre-existing instance and relaunches it with `--hidden`. Show/hide is then controlled by sending `SIGUSR1` / `SIGUSR2` directly to the owned process. When the plugin unloads, the process is stopped.
58+
59+
60+
## License
61+
62+
MIT — see [LICENSE](LICENSE).

osk-toggle/Settings.qml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import QtQuick
2+
import QtQuick.Layouts
3+
import qs.Commons
4+
import qs.Widgets
5+
6+
ColumnLayout {
7+
id: root
8+
9+
property var pluginApi: null
10+
11+
property var cfg: pluginApi?.pluginSettings || ({})
12+
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
13+
14+
property string editBackend: cfg.backend ?? defaults.backend ?? "auto"
15+
property bool editHideWhenUnavailable: cfg.hideWhenUnavailable ?? defaults.hideWhenUnavailable ?? false
16+
property bool editDisableHoverIcon: cfg.disableHoverIcon ?? defaults.disableHoverIcon ?? false
17+
property string editWvkbdBin: cfg.wvkbdBin ?? defaults.wvkbdBin ?? "wvkbd-mobintl"
18+
19+
readonly property bool showWvkbdBin: editBackend === "wvkbd" || editBackend === "auto"
20+
21+
spacing: Style.marginL
22+
23+
NComboBox {
24+
Layout.fillWidth: true
25+
label: pluginApi?.tr("settings.backend.label")
26+
description: pluginApi?.tr("settings.backend.desc")
27+
model: [
28+
{ key: "auto", name: pluginApi?.tr("settings.backend.option.auto") },
29+
{ key: "squeekboard", name: pluginApi?.tr("settings.backend.option.squeekboard") },
30+
{ key: "wvkbd", name: pluginApi?.tr("settings.backend.option.wvkbd") }
31+
]
32+
currentKey: root.editBackend
33+
onSelected: key => {
34+
root.editBackend = key
35+
root.saveSettings()
36+
}
37+
}
38+
39+
NToggle {
40+
Layout.fillWidth: true
41+
label: pluginApi?.tr("settings.hideWhenUnavailable.label")
42+
description: pluginApi?.tr("settings.hideWhenUnavailable.desc")
43+
checked: root.editHideWhenUnavailable
44+
onToggled: checked => {
45+
root.editHideWhenUnavailable = checked
46+
root.saveSettings()
47+
}
48+
}
49+
50+
NToggle {
51+
Layout.fillWidth: true
52+
label: pluginApi?.tr("settings.disableHoverIcon.label")
53+
description: pluginApi?.tr("settings.disableHoverIcon.desc")
54+
checked: root.editDisableHoverIcon
55+
onToggled: checked => {
56+
root.editDisableHoverIcon = checked
57+
root.saveSettings()
58+
}
59+
}
60+
61+
NTextInput {
62+
Layout.fillWidth: true
63+
visible: root.showWvkbdBin
64+
label: pluginApi?.tr("settings.wvkbdBin.label")
65+
description: pluginApi?.tr("settings.wvkbdBin.desc")
66+
text: root.editWvkbdBin
67+
onEditingFinished: {
68+
root.editWvkbdBin = text
69+
root.saveSettings()
70+
}
71+
}
72+
73+
RowLayout {
74+
Layout.fillWidth: true
75+
76+
NLabel {
77+
Layout.fillWidth: true
78+
label: pluginApi?.tr("settings.recheck.desc")
79+
}
80+
81+
NButton {
82+
text: recheckDone ? pluginApi?.tr("settings.recheck.done") : pluginApi?.tr("settings.recheck.label")
83+
property bool recheckDone: false
84+
onClicked: {
85+
pluginApi?.mainInstance?.recheckState()
86+
recheckDone = true
87+
recheckTimer.restart()
88+
}
89+
Timer {
90+
id: recheckTimer
91+
interval: 1500
92+
onTriggered: parent.recheckDone = false
93+
}
94+
}
95+
}
96+
97+
function saveSettings() {
98+
if (!pluginApi) return
99+
pluginApi.pluginSettings.backend = root.editBackend
100+
pluginApi.pluginSettings.hideWhenUnavailable = root.editHideWhenUnavailable
101+
pluginApi.pluginSettings.disableHoverIcon = root.editDisableHoverIcon
102+
pluginApi.pluginSettings.wvkbdBin = root.editWvkbdBin
103+
pluginApi.saveSettings()
104+
}
105+
}

0 commit comments

Comments
 (0)