Kde wayland (#264)

* First implementation of kde client

* load kwin-script via dbus interface on startup, if not already present

* remove temp file after use

* fix wrong object path

* fix wrong load_script return type

* log important information

* try to unload plugin if loading fails.

* change info to println, since it should always be printed

* remove unnecessary mut

* extend README.md

* extend build.yml to build, test, and publish the kde version

* improve README.md
event-log
N4tus 1 year ago committed by GitHub
parent c3ffcfc953
commit 84ea31f09e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -22,7 +22,7 @@ jobs:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
feature: [x11, gnome, sway, hypr]
feature: [x11, gnome, kde, sway, hypr]
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -101,10 +101,12 @@ jobs:
# Release binary
- { uses: actions/download-artifact@v3, with: { name: xremap-x86_64-x11, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-x86_64-gnome, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-x86_64-kde, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-x86_64-sway, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-x86_64-hypr, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-aarch64-x11, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-aarch64-gnome, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-aarch64-kde, path: package/ } }
- { uses: actions/download-artifact@v3, with: { name: xremap-aarch64-sway, path: package/ } }
- name: Release
run: |

@ -33,3 +33,4 @@ gnome = ["zbus"]
sway = ["swayipc"]
x11 = ["x11rb"]
hypr = ["hyprland"]
kde = ["zbus"]

@ -33,6 +33,7 @@ and run one of the following commands:
```bash
cargo install xremap --features x11 # X11
cargo install xremap --features gnome # GNOME Wayland
cargo install xremap --features kde # KDE-Plasma Wayland
cargo install xremap --features sway # Sway
cargo install xremap --features hypr # Hyprland
cargo install xremap # Others
@ -126,6 +127,10 @@ Update `/usr/share/dbus-1/session.conf` as follows, and reboot your machine.
</details>
### KDE-Plasma Wayland
Xremap cannot be run as root. Follow the instructions above to run xremap without sudo.
## Configuration
Your `config.yml` should look like this:
@ -279,6 +284,16 @@ or just the last segment after `.` (`Slack`, `Code`).
```
busctl --user call org.gnome.Shell /com/k0kubun/Xremap com.k0kubun.Xremap WMClass
```
#### KDE-Plasma Wayland
Xremap prints the active window to the console.
However, it will only start printing, once a mapping has been triggered that uses an application filter.
So you have to create a mapping with a filter using a dummy application name and trigger it.
Then each time you switch to a new window xremap will print its caption, class, and name in the following style:
`active window: caption: '<caption>', class: '<class>', name: '<name>'`
You want to use the class for the filter.
If you use a systemd-daemon to manage xremap, the prints will be visible in the system-logs (Can be opened with `journalctl -f`)
#### Sway

@ -0,0 +1,215 @@
use log::{debug, warn};
use std::env::temp_dir;
use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
use crate::client::Client;
use zbus::{dbus_interface, fdo, Connection};
const KWIN_SCRIPT: &str = include_str!("kwin-script.js");
const KWIN_SCRIPT_PLUGIN_NAME: &str = "xremap";
pub struct KdeClient {
active_window: Arc<Mutex<ActiveWindow>>,
}
struct KwinScriptTempFile(PathBuf);
impl KwinScriptTempFile {
fn new() -> Self {
Self(temp_dir().join("xremap-kwin-script.js"))
}
}
impl Drop for KwinScriptTempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
trait KWinScripting {
fn load_script(&self, path: &Path) -> Result<String, ConnectionError>;
fn unload_script(&self) -> Result<bool, ConnectionError>;
fn start_script(&self, script_obj_path: &str) -> Result<(), ConnectionError>;
fn is_script_loaded(&self) -> Result<bool, ConnectionError>;
}
impl KWinScripting for Connection {
fn load_script(&self, path: &Path) -> Result<String, ConnectionError> {
self.call_method(
Some("org.kde.KWin"),
"/Scripting",
Some("org.kde.kwin.Scripting"),
"loadScript",
// since OsStr does not implement zvariant::Type, the temp-path must be valid utf-8
&(path.to_str().ok_or(ConnectionError::TempPathNotValidUtf8)?, KWIN_SCRIPT_PLUGIN_NAME),
)
.map_err(|_| ConnectionError::LoadScriptCall)?
.body::<i32>()
.map_err(|_| ConnectionError::InvalidLoadScriptResult)
.map(|obj_path| format!("/{obj_path}"))
}
fn unload_script(&self) -> Result<bool, ConnectionError> {
self.call_method(
Some("org.kde.KWin"),
"/Scripting",
Some("org.kde.kwin.Scripting"),
"unloadScript",
// since OsStr does not implement zvariant::Type, the temp-path must be valid utf-8
&KWIN_SCRIPT_PLUGIN_NAME,
)
.map_err(|_| ConnectionError::UnloadScriptCall)?
.body::<bool>()
.map_err(|_| ConnectionError::InvalidUnloadScriptResult)
}
fn start_script(&self, script_obj_path: &str) -> Result<(), ConnectionError> {
self.call_method(Some("org.kde.KWin"), script_obj_path, Some("org.kde.kwin.Script"), "run", &())
.map_err(|_| ConnectionError::StartScriptCall)
.map(|_| ())
}
fn is_script_loaded(&self) -> Result<bool, ConnectionError> {
self.call_method(
Some("org.kde.KWin"),
"/Scripting",
Some("org.kde.kwin.Scripting"),
"isScriptLoaded",
&KWIN_SCRIPT_PLUGIN_NAME,
)
.map_err(|_| ConnectionError::IsScriptLoadedCall)?
.body::<bool>()
.map_err(|_| ConnectionError::InvalidIsScriptLoadedResult)
}
}
fn load_kwin_script() -> Result<(), ConnectionError> {
let dbus = Connection::new_session().map_err(|_| ConnectionError::ClientSession)?;
if !dbus.is_script_loaded()? {
let init_script = || {
let temp_file_path = KwinScriptTempFile::new();
std::fs::write(&temp_file_path.0, KWIN_SCRIPT).map_err(|_| ConnectionError::WriteScriptToTempFile)?;
let script_obj_path = dbus.load_script(&temp_file_path.0)?;
dbus.start_script(&script_obj_path)?;
Ok(())
};
if let Err(err) = init_script() {
debug!("Trying to unload kwin-script plugin ('{KWIN_SCRIPT_PLUGIN_NAME}').");
match dbus.unload_script() {
Err(err) => debug!("Error unloading plugin ('{err:?}'). It may still be loaded and could cause future runs of xremap to fail."),
Ok(unloaded) if unloaded => debug!("Successfully unloaded plugin."),
Ok(_) => debug!("Plugin was not loaded in the first place."),
}
return Err(err);
}
}
Ok(())
}
impl KdeClient {
pub fn new() -> KdeClient {
let active_window = Arc::new(Mutex::new(ActiveWindow {
title: String::new(),
res_name: String::new(),
res_class: String::new(),
}));
KdeClient { active_window }
}
fn connect(&mut self) -> Result<(), ConnectionError> {
load_kwin_script()?;
let active_window = Arc::clone(&self.active_window);
let (tx, rx) = channel();
std::thread::spawn(move || {
let connect = move || {
let connection = Connection::new_session().map_err(|_| ConnectionError::ServerSession)?;
fdo::DBusProxy::new(&connection)
.map_err(|_| ConnectionError::CreateDBusProxy)?
.request_name("com.k0kubun.Xremap", fdo::RequestNameFlags::ReplaceExisting.into())
.map_err(|_| ConnectionError::RequestName)?;
let mut object_server = zbus::ObjectServer::new(&connection);
let awi = ActiveWindowInterface { active_window };
object_server
.at(&"/com/k0kubun/Xremap".try_into().unwrap(), awi)
.map_err(|_| ConnectionError::ServeObjServer)?;
Ok(object_server)
};
let object_server: Result<zbus::ObjectServer, ConnectionError> = connect();
match object_server {
Ok(mut object_server) => {
let _ = tx.send(Ok(()));
loop {
if let Err(err) = object_server.try_handle_next() {
eprintln!("{}", err);
}
}
}
Err(err) => tx.send(Err(err)),
}
});
rx.recv().unwrap()
}
}
impl Client for KdeClient {
fn supported(&mut self) -> bool {
let conn_res = self.connect();
if let Err(err) = &conn_res {
warn!("Could not connect to kwin-script. Error: {err:?}");
}
conn_res.is_ok()
}
fn current_application(&mut self) -> Option<String> {
let aw = self.active_window.lock().unwrap();
Some(aw.res_class.clone())
}
}
#[derive(Debug)]
enum ConnectionError {
TempPathNotValidUtf8,
WriteScriptToTempFile,
ClientSession,
LoadScriptCall,
InvalidLoadScriptResult,
UnloadScriptCall,
InvalidUnloadScriptResult,
StartScriptCall,
IsScriptLoadedCall,
InvalidIsScriptLoadedResult,
ServerSession,
CreateDBusProxy,
RequestName,
ServeObjServer,
}
struct ActiveWindow {
res_class: String,
res_name: String,
title: String,
}
struct ActiveWindowInterface {
active_window: Arc<Mutex<ActiveWindow>>,
}
#[dbus_interface(name = "com.k0kubun.Xremap")]
impl ActiveWindowInterface {
fn notify_active_window(&mut self, caption: String, res_class: String, res_name: String) {
// I want to always print this, since it is the only way to know what the resource class of applications is.
println!("active window: caption: '{caption}', class: '{res_class}', name: '{res_name}'");
let mut aw = self.active_window.lock().unwrap();
aw.title = caption;
aw.res_class = res_class;
aw.res_name = res_name;
}
}

@ -0,0 +1,11 @@
workspace.clientActivated.connect(function(client){
callDBus(
"com.k0kubun.Xremap",
"/com/k0kubun/Xremap",
"com.k0kubun.Xremap",
"NotifyActiveWindow",
"caption" in client ? client.caption : "",
"resourceClass" in client ? client.resourceClass : "",
"resourceName" in client ? client.resourceName : ""
);
});

@ -48,6 +48,13 @@ pub fn build_client() -> WMClient {
WMClient::new("GNOME", Box::new(gnome_client::GnomeClient::new()))
}
#[cfg(feature = "kde")]
mod kde_client;
#[cfg(feature = "kde")]
pub fn build_client() -> WMClient {
WMClient::new("KDE", Box::new(kde_client::KdeClient::new()))
}
#[cfg(feature = "sway")]
mod sway_client;
#[cfg(feature = "sway")]
@ -69,9 +76,21 @@ pub fn build_client() -> WMClient {
WMClient::new("X11", Box::new(x11_client::X11Client::new()))
}
#[cfg(not(any(feature = "gnome", feature = "sway", feature = "x11", feature = "hypr")))]
#[cfg(not(any(
feature = "gnome",
feature = "sway",
feature = "x11",
feature = "hypr",
feature = "kde"
)))]
mod null_client;
#[cfg(not(any(feature = "gnome", feature = "sway", feature = "x11", feature = "hypr")))]
#[cfg(not(any(
feature = "gnome",
feature = "sway",
feature = "x11",
feature = "hypr",
feature = "kde"
)))]
pub fn build_client() -> WMClient {
WMClient::new("none", Box::new(null_client::NullClient))
}

Loading…
Cancel
Save