Skip to content

Commit

Permalink
🔀 Merge pull request #74 from kando-menu/feature/kde-wayland
Browse files Browse the repository at this point in the history
  • Loading branch information
Schneegans authored Jun 14, 2023
2 parents 57992e6 + 12d046a commit 5a4f187
Show file tree
Hide file tree
Showing 16 changed files with 669 additions and 151 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install Dependencies
run: |
sudo apt install libx11-dev
npm ci
npm install
- name: Create Release
run: |
npm run make
Expand All @@ -39,7 +39,7 @@ jobs:
node-version: 18
- name: Install Dependencies
run: |
npm ci
npm install
- name: Create Release
run: |
npm run make
44 changes: 28 additions & 16 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: MIT

import os from 'os';

import { screen, BrowserWindow, ipcMain } from 'electron';
import { Backend, getBackend } from './backends';

Expand All @@ -33,8 +35,13 @@ export class KandoApp {
}

await this.backend.init();
await this.backend.bindShortcut('CommandOrControl+Space', () => {
this.showMenu();
await this.backend.bindShortcut({
id: 'prototype_trigger',
description: 'Trigger the Kando prototype',
accelerator: 'CommandOrControl+Space',
action: () => {
this.showMenu();
},
});

this.window = await this.initWindow();
Expand Down Expand Up @@ -62,8 +69,8 @@ export class KandoApp {
},
transparent: true,
resizable: false,
skipTaskbar: true,
frame: false,
alwaysOnTop: true,
x: display.workArea.x,
y: display.workArea.y,
width: display.workArea.width + 1,
Expand All @@ -87,7 +94,6 @@ export class KandoApp {
// we wait for the fade-out animation to finish. We also make the window click-through
// by ignoring any input events during the fade-out animation.
ipcMain.on('hide-window', (event, delay) => {
this.window.setFocusable(false);
this.window.setIgnoreMouseEvents(true);
this.hideTimeout = setTimeout(() => {
this.window.hide();
Expand All @@ -106,46 +112,52 @@ export class KandoApp {
ipcMain.on('simulate-shortcut', () => {
this.window.hide();

this.backend.simulateShortcut('Super+A');
if (os.platform() === 'win32') {
this.backend.simulateShortcut('Ctrl+Alt+Tab');
} else {
this.backend.simulateShortcut('Super+A');
}
});

ipcMain.on('move-pointer', (event, pos) => {
const windowPos = this.window.getPosition();
this.backend.movePointer(pos.x + windowPos[0], pos.y + windowPos[1]);
ipcMain.on('move-pointer', (event, dist) => {
this.backend.movePointer(Math.floor(dist.x), Math.floor(dist.y));
});
}

// This is called when the user presses the shortcut. It will get the current
// window and pointer position and send them to the renderer process.
private showMenu() {
Promise.all([this.backend.getFocusedWindow(), this.backend.getPointer()])
.then(([window, pointer]) => {
this.backend
.getWMInfo()
.then((info) => {
// Abort any ongoing hide animation.
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
}

// Move the window to the monitor which contains the pointer.
const workarea = screen.getDisplayNearestPoint(pointer).workArea;
const workarea = screen.getDisplayNearestPoint({
x: info.pointerX,
y: info.pointerY,
}).workArea;
this.window.setBounds({
x: workarea.x,
y: workarea.y,
width: workarea.width + 1,
height: workarea.height + 1,
});

if (window) {
console.log('Currently focused window: ' + window.wmClass);
if (info.windowClass) {
console.log('Currently focused window: ' + info.windowClass);
} else {
console.log('Currently no window is focused.');
}

this.window.webContents.send('show-menu', {
x: pointer.x - workarea.x,
y: pointer.y - workarea.y,
x: info.pointerX - workarea.x,
y: info.pointerY - workarea.y,
});

this.window.setFocusable(true);
this.window.setIgnoreMouseEvents(false);
this.window.show();
})
Expand Down
66 changes: 41 additions & 25 deletions src/backends/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: MIT

/**
* This interface is used to transfer information required from the window manager when
* opening the pie menu. It contains the name and class of the currently focused window as
* well as the current pointer position.
*
* How to get the name and class of the currently focused window depends on the operating
* system and the window manager.
*/
export interface WMInfo {
windowName: string;
windowClass: string;
pointerX: number;
pointerY: number;
}

/**
* This interface is used to describe a keyboard shortcut. It contains a unique id, a
* description and the actual shortcut.
*
* @todo: Add information about the string format of the shortcut.
*/
export interface Shortcut {
id: string;
description: string;
accelerator: string;
action: () => void;
}

/**
* This interface must be implemented by all backends. A backend is responsible for
* communicating with the operating system. It provides methods to move the mouse pointer,
Expand Down Expand Up @@ -37,31 +65,22 @@ export interface Backend {
getWindowType: () => string;

/**
* Each backend must provide the current pointer position via this method.
* Each backend must provide a way to get the name and class of the currently focused
* application window as well as the current pointer position.
*
* @returns A promise which resolves to the current pointer position and the currently
* pressed modifier keys.
* @todo: Add information about the modifier keys.
* @returns A promise which resolves to the name and class of the currently focused
* window as well as to the current pointer position.
*/
getPointer: () => Promise<{ x: number; y: number; mods: number }>;
getWMInfo: () => Promise<WMInfo>;

/**
* Each backend must provide a way to move the pointer to a given position.
* Each backend must provide a way to move the pointer.
*
* @param x The x coordinate to move the pointer to.
* @param y The y coordinate to move the pointer to.
* @param dx The amount of horizontal movement.
* @param dy The amount of vertical movement.
* @returns A promise which resolves when the pointer has been moved.
*/
movePointer: (x: number, y: number) => Promise<void>;

/**
* Each backend must provide a way to get the name and class of the currently focused
* application window.
*
* @returns A promise which resolves to the name and class of the currently focused
* window.
*/
getFocusedWindow: () => Promise<{ name: string; wmClass: string }>;
movePointer: (dx: number, dy: number) => Promise<void>;

/**
* Each backend must provide a way to simulate a keyboard shortcut. This is used to
Expand All @@ -74,23 +93,20 @@ export interface Backend {
simulateShortcut: (shortcut: string) => Promise<void>;

/**
* Each backend must provide a way to bind a callback to a keyboard shortcut. The
* callback should be called whenever the shortcut is pressed.
* Each backend must provide a way to bind an action to a keyboard shortcut.
*
* @param shortcut The shortcut to bind.
* @param callback The method to call when the shortcut is pressed.
* @returns A promise which resolves when the shortcut has been bound.
* @todo: Add information about the string format of the shortcut.
*/
bindShortcut: (shortcut: string, callback: () => void) => Promise<void>;
bindShortcut: (shortcut: Shortcut) => Promise<void>;

/**
* Each backend must provide a way to unbind a previously bound keyboard shortcut.
*
* @param shortcut The shortcut to unbind.
* @param shortcut The ID of the shortcut to unbind.
* @returns A promise which resolves when the shortcut has been unbound.
*/
unbindShortcut: (shortcut: string) => Promise<void>;
unbindShortcut: (shortcut: Shortcut) => Promise<void>;

/**
* Each backend must provide a way to unbind all previously bound keyboard shortcuts.
Expand Down
5 changes: 5 additions & 0 deletions src/backends/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export function getBackend(): Backend | null {
return new KDEX11Backend();
}

if (desktop === 'KDE' && session === 'wayland') {
const { KDEWaylandBackend } = require('./linux/kde/wayland/backend');
return new KDEWaylandBackend();
}

if (session === 'x11') {
const { X11Backend } = require('./linux/x11/backend');
return new X11Backend();
Expand Down
64 changes: 32 additions & 32 deletions src/backends/linux/gnome/wayland/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// SPDX-License-Identifier: MIT

import DBus from 'dbus-final';
import { Backend } from '../../../backend';
import { Backend, Shortcut } from '../../../backend';

/**
* This backend uses the DBus interface of the Kando GNOME Shell integration extension to
Expand Down Expand Up @@ -52,38 +52,37 @@ export class GnomeBackend implements Backend {

this.interface = obj.getInterface('org.gnome.Shell.Extensions.KandoIntegration');

this.interface.on('ShortcutPressed', (shortcut: string) => {
this.callbacks[shortcut]();
this.interface.on('ShortcutPressed', (accelerator: string) => {
this.callbacks[accelerator]();
});
}

/**
* Returns the current pointer position and the currently pressed modifier keys.
*/
public async getPointer() {
const [x, y, mods] = await this.interface.GetPointer();
return { x: x, y: y, mods: mods };
}

/**
* Moves the pointer to the given position.
* This uses the DBus interface of the Kando GNOME Shell integration extension to get
* the name and class of the currently focused window as well as the current pointer
* position.
*
* @param x The x coordinate to move the pointer to.
* @param y The y coordinate to move the pointer to.
* @returns The name and class of the currently focused window as well as the current
* pointer position.
*/
public async movePointer(x: number, y: number) {
await this.interface.MovePointer(x, y);
public async getWMInfo() {
const info = await this.interface.GetWMInfo();
return {
windowName: info[0],
windowClass: info[1],
pointerX: info[2],
pointerY: info[3],
};
}

/**
* This uses the DBus interface of the Kando GNOME Shell integration extension to
* retrieve the name and class of the currently focused window.
* Moves the pointer by the given amount.
*
* @returns The name and class of the currently focused window.
* @param dx The amount of horizontal movement.
* @param dy The amount of vertical movement.
*/
public async getFocusedWindow() {
const [name, wmClass] = await this.interface.GetFocusedWindow();
return { name: name, wmClass: wmClass };
public async movePointer(dx: number, dy: number) {
await this.interface.MovePointer(dx, dy);
}

/**
Expand All @@ -103,34 +102,35 @@ export class GnomeBackend implements Backend {
}

/**
* Binds a callback to a keyboard shortcut. The callback is called whenever the shortcut
* is pressed.
* This binds a shortcut. The action callback is called when the shortcut is pressed. On
* GNOME Wayland, this uses the DBus interface of the Kando GNOME Shell integration.
*
* @param shortcut The shortcut to simulate.
* @returns A promise which resolves when the shortcut has been simulated.
* @todo: Add information about the string format of the shortcut.
*/
public async bindShortcut(shortcut: string, callback: () => void) {
shortcut = this.toGdkAccelerator(shortcut);
public async bindShortcut(shortcut: Shortcut) {
const accelerator = this.toGdkAccelerator(shortcut.accelerator);

const success = await this.interface.BindShortcut(shortcut);
const success = await this.interface.BindShortcut(accelerator);

if (!success) {
throw new Error('Shortcut is already in use.');
}

this.callbacks[shortcut] = callback;
this.callbacks[accelerator] = shortcut.action;
}

/**
* This unbinds a previously bound shortcut.
*
* @param shortcut The shortcut to unbind.
*/
public async unbindShortcut(shortcut: string) {
shortcut = this.toGdkAccelerator(shortcut);
public async unbindShortcut(shortcut: Shortcut) {
const accelerator = this.toGdkAccelerator(shortcut.accelerator);

const success = await this.interface.UnbindShortcut(accelerator);

const success = await this.interface.UnbindShortcut(shortcut);
delete this.callbacks[accelerator];

if (!success) {
throw new Error('Shortcut was not bound.');
Expand Down
Loading

0 comments on commit 5a4f187

Please sign in to comment.