1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

Advanced Workspace Switcher Widget + Lockscreen Virtual Keyboard (#149)

* Virtual keyboard on lockscreen

Almost whole code was taken from https://github.com/LucasCodingM/customVirtualkeyboard

* AdvancedWorkspaceSwitcher + BottomBar

- AdvancedWorkspaceSwitcher shows opened apps and allows to move to
them
- focusWindow function for niri
- Bottom bar with AdvancedWorkspaceSwitcher

* Cleanup + Styling fixes

* Changed visibility defaults back to true

For advanced workspace switcher

* Formatting + resolved commets
This commit is contained in:
Aleksandr Lebedev
2025-09-03 00:26:52 +02:00
committed by GitHub
parent 96db0581d3
commit 5bffb1ba10
11 changed files with 786 additions and 4 deletions

View File

@@ -25,6 +25,7 @@ Singleton {
property bool useAutoLocation: false
property bool showLauncherButton: true
property bool showWorkspaceSwitcher: true
property bool showAdvancedWorkspaceSwitcher: true
property bool showFocusedWindow: true
property bool showWeather: true
property bool showMusic: true
@@ -198,6 +199,8 @@ Singleton {
!== undefined ? settings.showLauncherButton : true
showWorkspaceSwitcher = settings.showWorkspaceSwitcher
!== undefined ? settings.showWorkspaceSwitcher : true
showAdvancedWorkspaceSwitcher = settings.showAdvancedWorkspaceSwitcher
!== undefined ? settings.showAdvancedWorkspaceSwitcher : true
showFocusedWindow = settings.showFocusedWindow
!== undefined ? settings.showFocusedWindow : true
showWeather = settings.showWeather !== undefined ? settings.showWeather : true
@@ -571,6 +574,11 @@ Singleton {
saveSettings()
}
function setShowAdvancedWorkspaceSwitcher(enabled) {
showAdvancedWorkspaceSwitcher = enabled
saveSettings()
}
function setShowFocusedWindow(enabled) {
showFocusedWindow = enabled
saveSettings()
@@ -722,6 +730,7 @@ Singleton {
updateListModel(rightWidgetsModel, defaultRight)
showLauncherButton = true
showWorkspaceSwitcher = true
showAdvancedWorkspaceSwitcher = true
showFocusedWindow = true
showWeather = true
showMusic = true

View File

@@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
DankActionButton {
id: customButtonKeyboard
circular: false
property string text: ""
width: 40
height: 40
property bool isShift: false
color: Theme.surface
hoverColor: Theme.surfacePressed
StyledText {
id: contentItem
anchors.centerIn: parent
text: parent.text
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeXLarge
font.weight: Font.Normal
}
}

361
Modules/Lock/Keyboard.qml Normal file
View File

@@ -0,0 +1,361 @@
import QtQuick
import qs.Common
Rectangle {
id: root
property Item target
height: 60 * 5
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: Theme.widgetBackground()
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
property bool shift: false //Boolean for the shift state
property bool symbols: false //Boolean for the symbol state
property double columns: 10 // Number of column
property double rows: 4 // Number of row
property string strShift: '\u2191' // UPWARDS ARROW unicode
property string strBackspace: "Backspace"
property var modelKeyboard: {
"row_1": [
{
text: 'q',
symbol: '1',
width: 1
},
{
text: 'w',
symbol: '2',
width: 1
},
{
text: 'e',
symbol: '3',
width: 1
},
{
text: 'r',
symbol: '4',
width: 1
},
{
text: 't',
symbol: '5',
width: 1
},
{
text: 'y',
symbol: '6',
width: 1
},
{
text: 'u',
symbol: '7',
width: 1
},
{
text: 'i',
symbol: '8',
width: 1
},
{
text: 'o',
symbol: '9',
width: 1
},
{
text: 'p',
symbol: '0',
width: 1
},
],
"row_2": [
{
text: 'a',
symbol: '-',
width: 1
},
{
text: 's',
symbol: '/',
width: 1
},
{
text: 'd',
symbol: ':',
width: 1
},
{
text: 'f',
symbol: ';',
width: 1
},
{
text: 'g',
symbol: '(',
width: 1
},
{
text: 'h',
symbol: ')',
width: 1
},
{
text: 'j',
symbol: '€',
width: 1
},
{
text: 'k',
symbol: '&',
width: 1
},
{
text: 'l',
symbol: '@',
width: 1
}
],
"row_3": [
{
text: strShift,
symbol: strShift,
width: 1.5
},
{
text: 'z',
symbol: '.',
width: 1
},
{
text: 'x',
symbol: ',',
width: 1
},
{
text: 'c',
symbol: '?',
width: 1
},
{
text: 'v',
symbol: '!',
width: 1
},
{
text: 'b',
symbol: "'",
width: 1
},
{
text: 'n',
symbol: "%",
width: 1
},
{
text: 'm',
symbol: '"',
width: 1
},
{
text: "'",
symbol: "*",
width: 1.5
}
],
"row_4": [
{
text: '123',
symbol: 'ABC',
width: 1.5
},
{
text: ' ',
symbol: ' ',
width: 6
},
{
text: '.',
symbol: '.',
width: 1
},
, {
text: strBackspace,
symbol: strBackspace,
width: 1.5
}
]
}
//Here is the corresponding table between the ascii and the key event
property var tableKeyEvent: {
"_0": Qt.Key_0,
"_1": Qt.Key_1,
"_2": Qt.Key_2,
"_3": Qt.Key_3,
"_4": Qt.Key_4,
"_5": Qt.Key_5,
"_6": Qt.Key_6,
"_7": Qt.Key_7,
"_8": Qt.Key_8,
"_9": Qt.Key_9,
"_a": Qt.Key_A,
"_b": Qt.Key_B,
"_c": Qt.Key_C,
"_d": Qt.Key_D,
"_e": Qt.Key_E,
"_f": Qt.Key_F,
"_g": Qt.Key_G,
"_h": Qt.Key_H,
"_i": Qt.Key_I,
"_j": Qt.Key_J,
"_k": Qt.Key_K,
"_l": Qt.Key_L,
"_m": Qt.Key_M,
"_n": Qt.Key_N,
"_o": Qt.Key_O,
"_p": Qt.Key_P,
"_q": Qt.Key_Q,
"_r": Qt.Key_R,
"_s": Qt.Key_S,
"_t": Qt.Key_T,
"_u": Qt.Key_U,
"_v": Qt.Key_V,
"_w": Qt.Key_W,
"_x": Qt.Key_X,
"_y": Qt.Key_Y,
"_z": Qt.Key_Z,
"_\u2190": Qt.Key_Backspace,
"_return": Qt.Key_Return,
"_ ": Qt.Key_Space,
"_-": Qt.Key_Minus,
"_/": Qt.Key_Slash,
"_:": Qt.Key_Colon,
"_;": Qt.Key_Semicolon,
"_(": Qt.Key_BracketLeft,
"_)": Qt.Key_BracketRight,
"_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format
,
"_&": Qt.Key_Ampersand,
"_@": Qt.Key_At,
'_"': Qt.Key_QuoteDbl,
"_.": Qt.Key_Period,
"_,": Qt.Key_Comma,
"_?": Qt.Key_Question,
"_!": Qt.Key_Exclam,
"_'": Qt.Key_Apostrophe,
"_%": Qt.Key_Percent,
"_*": Qt.Key_Asterisk
}
Item {
id: keyboard_container
anchors.left: parent.left
anchors.leftMargin: 5
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 5
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
//One column which contains 5 rows
Column {
spacing: columnSpacing
Row {
id: row_1
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_1"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row {
id: row_2
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_2"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row {
id: row_3
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_3"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
isShift: shift && text === strShift
onClicked: root.clicked(text)
}
}
}
Row {
id: row_4
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_4"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
}
}
signal clicked(string text)
Connections {
target: root
function onClicked(text) {
if (!keyboard_controller.target)
return;
if (text === strShift) {
root.shift = !root.shift; // toggle shift
} else if (text === '123') {
root.symbols = true;
} else if (text === 'ABC') {
root.symbols = false;
} else {
// insert text into target
if (text === strBackspace) {
var current = keyboard_controller.target.text;
keyboard_controller.target.text = current.slice(0, current.length - 1);
} else {
// normal character
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text);
var current = keyboard_controller.target.text;
var cursorPos = keyboard_controller.target.cursorPosition;
keyboard_controller.target.text = current.slice(0, cursorPos) + charToInsert + current.slice(cursorPos);
keyboard_controller.target.cursorPosition = cursorPos + 1;
}
// shift is momentary
if (root.shift && text !== strShift)
root.shift = false;
}
}
}
}

View File

@@ -0,0 +1,36 @@
import QtQuick
Item {
id: keyboard_controller
// reference on the TextInput
property Item target
//Booléan on the state of the keyboard
property bool isKeyboardActive: false
property var rootObject
function show() {
if (!isKeyboardActive && keyboard === null) {
keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
keyboard.target = keyboard_controller.target;
isKeyboardActive = true;
} else
console.info("The keyboard is already shown");
}
function hide() {
if (isKeyboardActive && keyboard !== null) {
keyboard.destroy();
isKeyboardActive = false;
} else
console.info("The keyboard is already hidden");
}
// private
property Item keyboard: null
Component {
id: keyboardComponent
Keyboard {}
}
}

View File

@@ -258,7 +258,7 @@ Item {
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.rightMargin: (revealButton.visible ? revealButton.width + Theme.spacingM : 0) + (enterButton.visible ? enterButton.width + Theme.spacingM : 0) + (loadingSpinner.visible ? loadingSpinner.width + Theme.spacingM : Theme.spacingM)
anchors.rightMargin: (revealButton.visible ? revealButton.width + Theme.spacingM : 0) + (enterButton.visible ? enterButton.width + Theme.spacingM : 0) + (virtualKeyboardButton.visible ? virtualKeyboardButton.width + Theme.spacingM : 0) + (loadingSpinner.visible ? loadingSpinner.width + Theme.spacingM : Theme.spacingM)
opacity: 0
focus: !demoMode
enabled: !demoMode
@@ -295,6 +295,12 @@ Item {
}
}
KeyboardController {
id: keyboardController
target: passwordField
rootObject: root
}
StyledText {
id: placeholder
@@ -302,7 +308,7 @@ Item {
anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))
anchors.right: (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: {
@@ -340,7 +346,7 @@ Item {
StyledText {
anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))
anchors.right: (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: {
@@ -380,6 +386,27 @@ Item {
enabled: visible
onClicked: parent.showPassword = !parent.showPassword
}
DankActionButton {
id: virtualKeyboardButton
anchors.right: revealButton.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: 32
visible: !demoMode && !pam.active && !LockScreenService.unlocking
enabled: visible
onClicked:
{
if(keyboardController.isKeyboardActive)
{
keyboardController.hide()
} else
{
keyboardController.show()
}
}
}
Rectangle {
id: loadingSpinner

View File

@@ -20,6 +20,12 @@ Item {
"description": "Shows current workspace and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "advancedWorkspaceSwitcher",
"text": "Advanced Workspace Switcher",
"description": "Shows workspaced with opened apps and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "focusedWindow",
"text": "Focused Window",

View File

@@ -19,6 +19,12 @@ Item {
"description": "Shows current workspace and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "advancedWorkspaceSwitcher",
"text": "Advanced Workspace Switcher",
"description": "Shows workspaced with opened apps and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "focusedWindow",
"text": "Focused Window",

View File

@@ -0,0 +1,288 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string screenName: ""
property real widgetHeight: 30
property int currentWorkspace: {
if (CompositorService.isNiri) {
return getNiriActiveWorkspace();
} else if (CompositorService.isHyprland) {
return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : 1;
}
return 1;
}
property var workspaceList: {
if (CompositorService.isNiri) {
var baseList = getNiriWorkspaces();
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
} else if (CompositorService.isHyprland) {
var workspaces = Hyprland.workspaces ? Hyprland.workspaces.values : [];
if (workspaces.length === 0) {
return [
{
id: 1,
name: "1"
}
];
}
var sorted = workspaces.slice().sort((a, b) => a.id - b.id);
return SettingsData.showWorkspacePadding ? padWorkspaces(sorted) : sorted;
}
return [1];
}
function getWorkspaceIcons(ws) {
var chunks = [];
if (!ws)
return chunks;
var wsCandidates = [ws.id, ws.idx].filter(x => typeof x !== "undefined");
var wins = [];
if (CompositorService.isNiri) {
wins = NiriService.windows || [];
} else if (CompositorService.isHyprland) {
wins = Hyprland.clients ? Hyprland.clients.values : [];
}
var byApp = {}; // key = app_id/class, value =
var isActiveWs = ws.is_active;
for (var i = 0; i < wins.length; i++) {
var w = wins[i];
if (!w)
continue;
var winWs = w.workspace_id || w.workspaceId || (w.workspace && w.workspace.id) || w.idx || null;
if (winWs === null)
continue;
if (wsCandidates.indexOf(winWs) === -1)
continue;
// --- normalize app id
var keyBase = (w.app_id || w.appId || w.class || w.windowClass || w.exe || "unknown").toLowerCase();
// For active workspace every key should be unique. For inactive we just count the duplicates
var key = isActiveWs ? keyBase + "_" + i : keyBase;
if (!byApp[key]) {
var icon = Quickshell.iconPath(DesktopEntries.heuristicLookup(Paths.moddedAppId(keyBase))?.icon, true);
byApp[key] = {
type: "icon",
icon: icon,
active: !!w.is_focused,
count: 1,
windowId: w.id,
fallbackText: w.app_id || w.class || w.title || ""
};
} else {
byApp[key].count++;
if (w.is_focused)
byApp[key].active = true;
}
}
for (var k in byApp)
chunks.push(byApp[k]);
return chunks;
}
function padWorkspaces(list) {
var padded = list.slice();
while (padded.length < 3) {
if (CompositorService.isHyprland) {
padded.push({
id: -1,
name: ""
});
} else {
padded.push(-1);
}
}
return padded;
}
function getNiriWorkspaces() {
if (NiriService.allWorkspaces.length === 0)
return [1, 2];
if (!root.screenName)
return NiriService.getCurrentOutputWorkspaceNumbers();
var displayWorkspaces = [];
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.output === root.screenName)
displayWorkspaces.push(ws.idx + 1);
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
}
function getNiriActiveWorkspace() {
if (NiriService.allWorkspaces.length === 0)
return 1;
if (!root.screenName)
return NiriService.getCurrentWorkspaceNumber();
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.output === root.screenName && ws.is_active)
return ws.idx + 1;
}
return 1;
}
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Math.max(Theme.spacingS, SettingsData.topBarInnerPadding)
width: SettingsData.showWorkspacePadding ? Math.max(120, workspaceRow.implicitWidth + horizontalPadding * 2) : workspaceRow.implicitWidth + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground)
return "transparent";
const baseColor = Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
visible: CompositorService.isNiri || CompositorService.isHyprland
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: root.workspaceList
Rectangle {
id: wsBox
property bool isActive: {
if (CompositorService.isHyprland)
return modelData && modelData.id === root.currentWorkspace;
return modelData === root.currentWorkspace;
}
property var wsData: {
if (CompositorService.isHyprland)
return modelData;
if (CompositorService.isNiri) {
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.idx + 1 === modelData)
return ws;
}
}
return null;
}
property var icons: wsData ? root.getWorkspaceIcons(wsData) : []
property bool isHovered: mouseArea.containsMouse
width: isActive ? widgetHeight * 1.2 + Theme.spacingXS + contentRow.implicitWidth : widgetHeight * 0.8 + contentRow.implicitWidth
height: widgetHeight * 0.8
radius: height / 2
color: isActive ? Theme.primary : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
enabled: wsData !== null
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!wsData)
return;
if (CompositorService.isHyprland) {
Hyprland.dispatch(`workspace ${wsData.id}`);
} else if (CompositorService.isNiri) {
NiriService.switchToWorkspace(wsData.idx);
}
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: 4
Repeater {
model: root.getWorkspaceIcons(wsData)
delegate: Item {
width: wsBox.height * 0.9
height: wsBox.height * 0.9
IconImage {
id: appIcon
property var windowId: modelData.windowId
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
MouseArea {
id: appMouseArea
hoverEnabled: true
anchors.fill: parent
enabled: wsBox.isActive
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CompositorService.isHyprland) {
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`);
} else if (CompositorService.isNiri) {
NiriService.focusWindow(appIcon.windowId);
} else {
console.log("ERROR: Can't focus window with ", appIcon.windowId);
}
}
}
}
// Counter Badge
Rectangle {
visible: modelData.count > 1 && !wsBox.isActive
width: 12
height: 12
radius: 6
color: "black"
border.color: "white"
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 2
Text {
anchors.centerIn: parent
text: modelData.count
font.pixelSize: 9
color: "white"
}
}
}
}
// fallback: if there're no apps - we show workspace number/name
Rectangle {
visible: root.getWorkspaceIcons(wsData).length === 0
anchors.centerIn: parent
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
Text {
anchors.centerIn: parent
text: wsData ? (wsData.name || (wsData.idx ? wsData.idx : (wsData.id ? wsData.id : ""))) : ""
font.pixelSize: 12
color: modelData && modelData.active ? Theme.surfaceContainer : Theme.surfaceTextMedium
}
}
}
}
}
}
}

View File

@@ -340,6 +340,8 @@ PanelWindow {
return true
case "workspaceSwitcher":
return true
case "advancedWorkspaceSwitcher":
return true
case "focusedWindow":
return true
case "runningApps":
@@ -395,6 +397,8 @@ PanelWindow {
return launcherButtonComponent
case "workspaceSwitcher":
return workspaceSwitcherComponent
case "advancedWorkspaceSwitcher":
return advancedWorkspaceSwitcherComponent
case "focusedWindow":
return focusedWindowComponent
case "runningApps":
@@ -748,7 +752,15 @@ PanelWindow {
widgetHeight: root.widgetHeight
}
}
Component {
id: advancedWorkspaceSwitcherComponent
AdvancedWorkspaceSwitcher {
screenName: root.screenName
widgetHeight: root.widgetHeight
}
}
Component {
id: focusedWindowComponent

View File

@@ -72,6 +72,7 @@ https://github.com/user-attachments/assets/5ad934bb-e7aa-4c04-8d40-149181bd2d29
- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged.
- **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps.
- **Workspace Switcher** Dynamically resizing niri workspace switcher.
- **Advanced Workspace Switcher** Dynamically resizing niri workspace switcher, also showing opened apps on the workspace.
- **Focused Window** Displays the currently focused window app name and title.
- **Running Apps** A view of all running apps, sorted by monitor, workspace, then position on workspace.
- **Media Player** Short form media player with equalizer, song title, and controls.
@@ -90,7 +91,7 @@ https://github.com/user-attachments/assets/5ad934bb-e7aa-4c04-8d40-149181bd2d29
- **Notification Center** A center for notifications that has support for grouping.
- **Dock** A dock with pinned apps support, recent apps support, and currently running application support.
- **Control Center** A full control center with user profile information, network, bluetooth, audio input/output, display controls, and night mode automation.
- **Lock Screen** Using quickshell's WlSessionLock
- **Lock Screen** Using quickshell's WlSessionLock with embedded virtual keyboard for Niri (Niri doesn't support placing virtual keyboard above lockscreen natively: [issue](https://github.com/YaLTeR/niri/issues/2201))
- **Notepad** A simple text notepad/scratchpad with auto-save to session data and file export/import functionality.
**Features:**

View File

@@ -444,6 +444,17 @@ Singleton {
}
})
}
function focusWindow(windowId) {
return send({
"Action": {
"FocusWindow": {
"id": windowId
}
}
})
}
function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map(