diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0037e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +.vscode/settings.json diff --git a/README.bbcode b/README.bbcode index 09ed4fe..962bd19 100644 --- a/README.bbcode +++ b/README.bbcode @@ -1,6 +1,6 @@ Automatically raises all other visible windows of the same application together when activating one of them, effectively creating application groups to task-switch between. -[b]Please make sure to install the most recent version (v1.6) and to not use Discover for installation.[/b] For more information on installation, setup and usage as well as any requests, please visit [url=https://github.com/nclarius/kwin-application-switcher]the GitHub page[/url]. +[b]Please make sure to install the most recent version (v1.7) and to not use Discover for installation.[/b] For more information on installation, setup and usage as well as any requests, please visit [url=https://github.com/nclarius/kwin-application-switcher]the GitHub page[/url]. This extension gives rise to an application-centric task switching workflow as known from environments such as GNOME or MacOS, where an application’s windows are treated as a group, and task switching can take place at two levels: one mode for switching applications and one mode for switching between windows of an application. diff --git a/README.md b/README.md index 25a07f6..3dd317d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Seen in the screencast: Switching from Konsole back to Dolphin also brings the o ### Installation via graphical interface -**Please make sure to select the most recent version (v1.6)** in the installation process. +**Please make sure to select the most recent version (v1.7)** in the installation process. A [bug](https://bugs.kde.org/show_bug.cgi?id=453521) in Discover causes a wrong version to be installed, so using the installation module in System Settings instead is recommended. diff --git a/application-switcher_v1.6.kwinscript b/application-switcher_v1.7.kwinscript similarity index 55% rename from application-switcher_v1.6.kwinscript rename to application-switcher_v1.7.kwinscript index 0b77014..957b87f 100644 Binary files a/application-switcher_v1.6.kwinscript and b/application-switcher_v1.7.kwinscript differ diff --git a/contents/code/main.js b/contents/code/main.js index 5245f68..76118b0 100644 --- a/contents/code/main.js +++ b/contents/code/main.js @@ -8,12 +8,49 @@ GNU General Public License v3.0 // initialization /////////////////////// -const debugMode = readConfig("debugMode", true); +// const debugMode = readConfig("debugMode", true); +const debugMode = readConfig("debugMode", false); + function debug(...args) { if (debugMode) { console.debug("applicationswitcher:", ...args); } } debug("initializing"); +// Detect KDE version +const isKDE6 = typeof workspace.windowList === 'function'; + +function isAppOnCurrentDesktopKDE6(window) { + return window && + (window.desktops && window.desktops.includes(workspace.currentDesktop)) || + (window.desktops && window.desktops.length === 0); +} + +function isAppOnCurrentDesktopKDE5(window) { + return window && + (window.x11DesktopIds && window.x11DesktopIds.includes(workspace.currentDesktop)) || + (window.x11DesktopIds && window.x11DesktopIds.length === 0); +} + +let activeWindow; +let windowList; +let connectWindowActivated; +let setActiveWindow; +let isAppOnCurrentDesktop; + +// Set up aliases to abstract away the API differences between KDE 5 and KDE 6 +if (isKDE6) { + activeWindow = () => workspace.activeWindow; + windowList = () => workspace.windowList(); + connectWindowActivated = (handler) => workspace.windowActivated.connect(handler); + setActiveWindow = (window) => { workspace.activeWindow = window; }; + isAppOnCurrentDesktop = isAppOnCurrentDesktopKDE6 +} else { + activeWindow = () => workspace.activeClient; + windowList = () => workspace.clientList(); + connectWindowActivated = (handler) => workspace.clientActivated.connect(handler); + setActiveWindow = (window) => { workspace.activeClient = window; }; + isAppOnCurrentDesktop = isAppOnCurrentDesktopKDE5 +} /////////////////////// // special applications to ignore @@ -33,7 +70,7 @@ const ignoredApps = ["plasmashell", "org.kde.plasmashell", // desktop shell // "dolphin" function getApp(current) { - if (!current) return ""; + if (!current || typeof current.resourceClass !== 'string') return ""; return String(current.resourceClass); } @@ -46,7 +83,7 @@ function getApp(current) { var prevActiveApp = "" // set previously active application for initially active window -setPrevActiveApp(workspace.activeClient); +setPrevActiveApp(activeWindow()) // set previously active application for recently activated window function setPrevActiveApp(current) { @@ -68,7 +105,7 @@ function getPrevActiveApp() { var appGroups = {}; // compute app groups for initially present windows -workspace.clientList().forEach(window => updateAppGroups(window)); +windowList().forEach(window => updateAppGroups(window)); // update app groups with given window function updateAppGroups(current) { @@ -78,17 +115,35 @@ function updateAppGroups(current) { appGroups[app] = appGroups[app].filter(window => window && window != current); appGroups[app].push(current); - debug("updating app group", appGroups[app].map(window => window.caption)); + debug("updating app group", appGroups[app].map(window => + window && window.caption ? window.caption : "undefined window" + )); +} + +function isAppOnCurrentActivity(window) { + return (window.activities && window.activities.includes(workspace.currentActivity)) || + (window.activities && window.activities.length === 0); +} + +function getFilterConditions(window) { + return window && !window.minimized && + isAppOnCurrentDesktop(window) && isAppOnCurrentActivity(window); } // return other visible windows of same application as given window function getAppGroup(current) { if (!current) return; - let appGroup = appGroups[getApp(current)].filter(window => window && - !window.minimized && - (window.x11DesktopIds.includes(workspace.currentDesktop) || window.x11DesktopIds.length == 0) && - (window.activities.includes(workspace.currentActivity) || window.activities.length == 0)); - debug("getting app group", appGroup.map(window => window.caption)); + + unfilteredAppGroup = appGroups[getApp(current)]; + debug("unfiltered app group", unfilteredAppGroup.map(window => + window && window.caption ? window.caption : "undefined window")); + + // let appGroup = appGroups[getApp(current)].filter(getFilterConditions); + let appGroup = unfilteredAppGroup.filter(getFilterConditions); + + debug("filtered app group", appGroup.map(window => + window && window.caption ? window.caption : "undefined window" + )); return appGroup; } @@ -97,8 +152,8 @@ function getAppGroup(current) { // main /////////////////////// -// when client is activated, auto-raise other windows of the same applicaiton -workspace.clientActivated.connect(active => { +// when client is activated, auto-raise other windows of the same application +function onWindowActivated(active) { if (!active) return; debug("---------"); debug("activated", active.caption); @@ -117,8 +172,12 @@ workspace.clientActivated.connect(active => { setPrevActiveApp(active); // auto-raise other windows of same application for (let window of getAppGroup(active)) { - debug("auto-raising", window.caption); - workspace.activeClient = window; + if (window) { + debug("auto-raising", window.caption); + setActiveWindow(window); + } } } -}); +} + +connectWindowActivated(onWindowActivated); diff --git a/install.sh b/install.sh index 460763c..07fa655 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,128 @@ -#!/bin/bash -name=$(grep -oP '"Id":\s*"[^"]*' ./metadata.json | grep -oP '[^"]*$') -kpackagetool5 --type=KWin/Script --install . || kpackagetool5 --type=KWin/Script --upgrade . -kwriteconfig5 --file kwinrc --group Plugins --key "$name"Enabled true -qdbus org.kde.KWin /KWin reconfigure +#!/usr/bin/env bash + + +# This script will install a KWin script structure found in the path designated in '$script_path' + +exit_w_error() { + local msg="$1" + echo -e "\nERROR: ${msg} \nExiting...\n" + exit 1 +} + +install_w_kpackagetool6() { + if ! command -v kpackagetool6 &> /dev/null; then + exit_w_error "The 'kpackagetool6' command was not found. Cannot install KWin script." + fi + echo "Installing KWin script: '${script_name}'" + if ! kpackagetool6 --type="${script_type}" --install "${script_path}" &> /dev/null; then + kpackagetool6 --type="${script_type}" --upgrade "${script_path}" + fi +} + +install_w_kpackagetool5() { + if ! command -v kpackagetool5 &> /dev/null; then + exit_w_error "The 'kpackagetool5' command was not found. Cannot install KWin script." + fi + echo "Installing KWin script: '${script_name}'" + if ! kpackagetool5 --type="${script_type}" --install "${script_path}" &> /dev/null; then + kpackagetool5 --type="${script_type}" --upgrade "${script_path}" + fi +} + +KDE_ver=${KDE_SESSION_VERSION:-0} # Default to zero value if environment variable not set +script_type="KWin/Script" +script_path="." +script_name="" + + +if [ -f "./metadata.json" ]; then + # The "P" option for grep causes a problem on Chimera Linux, which uses BSD utils + # script_name=$(grep -oP '"Id":\s*"[^"]*' ./metadata.json | grep -oP '[^"]*$') + script_name=$(grep '"Id":' ./metadata.json | sed 's/.*"Id":[[:space:]]*"\([^"]*\).*/\1/') +elif [ -f "./metadata.desktop" ]; then + script_name=$(grep '^X-KDE-PluginInfo-Name=' ./metadata.desktop | cut -d '=' -f2) + echo "FYI: 'metadata.desktop' files are deprecated. Use 'metadata.json' format." +else + exit_w_error "No suitable metadata file found. Unable to get script name." +fi + +if [ "$script_name" == "" ]; then + exit_w_error "Failed to parse KWin script name from metadata file." +fi + +if [[ ${KDE_ver} -eq 0 ]]; then + echo "KDE_SESSION_VERSION environment variable was not set." + exit_w_error "Cannot install '${script_name}' KWin script." +elif [[ ${KDE_ver} -eq 6 ]]; then + if ! install_w_kpackagetool6; then + exit_w_error "Problem installing '${script_name}' with kpackagetool6." + fi + if ! command -v kwriteconfig6 &> /dev/null; then + exit_w_error "The 'kwriteconfig6' command was not found. Cannot enable KWin script." + fi + kwriteconfig6 --file kwinrc --group Plugins --key "$script_name"Enabled true +elif [[ ${KDE_ver} -eq 5 ]]; then + if ! install_w_kpackagetool5; then + exit_w_error "Problem installing '${script_name}' with kpackagetool5." + fi + if ! command -v kwriteconfig5 &> /dev/null; then + exit_w_error "The 'kwriteconfig5' command was not found. Cannot enable KWin script." + fi + kwriteconfig5 --file kwinrc --group Plugins --key "$script_name"Enabled true +else + echo "KDE_SESSION_VERSION had a value, but that value was unrecognized: '${KDE_ver}'" + exit_w_error "This script is meant to run only on KDE 5 or 6." +fi + + +sleep 0.5 + +# We need to gracefully cascade through common D-Bus utils to +# find one that is available to use for the KWin reconfigure +# command. Sometimes 'qdbus' is not available. Start with 'gdbus'. + +# Extended array of D-Bus command names with prioritized qdbus variants +dbus_commands=("gdbus" "qdbus6" "qdbus-qt6" "qdbus-qt5" "qdbus" "dbus-send") + +# Functions to handle reconfiguration with different dbus utilities +reconfigure() { + case "$1" in + gdbus) + gdbus call --session --dest org.kde.KWin --object-path /KWin --method org.kde.KWin.reconfigure + ;; + qdbus6 | qdbus-qt6 | qdbus-qt5 | qdbus) + "$1" org.kde.KWin /KWin reconfigure + ;; + dbus-send) + dbus-send --session --type=method_call --dest=org.kde.KWin /KWin org.kde.KWin.reconfigure + ;; + *) + echo "Unsupported DBus utility: $1" >&2 + return 1 + ;; + esac +} + +# Unquoted 'true' and 'false' values are built-in commands in bash, +# returning 0 or 1 exit status. +# So they can sort of be treated like Python's 'True' or 'False' in 'if' conditions. +dbus_cmd_found=false + +# Iterate through the dbus_commands array +for cmd in "${dbus_commands[@]}"; do + if command -v "${cmd}" &> /dev/null; then + dbus_cmd_found=true + echo "Refreshing KWin configuration using $cmd." + reconfigure "${cmd}" &> /dev/null + sleep 0.5 + # Break out of the loop once a command is found and executed + break + fi +done + +if ! $dbus_cmd_found; then + echo "No suitable DBus utility found. KWin configuration may need manual reloading." +fi + + +echo "Finished installing KWin script: '${script_name}'" diff --git a/metadata.json b/metadata.json index e72013d..fe6fad5 100644 --- a/metadata.json +++ b/metadata.json @@ -1,4 +1,5 @@ { + "KPackageStructure": "KWin/Script", "KPlugin": { "Authors": [ { @@ -15,7 +16,7 @@ "KWin/Script", "KCModule" ], - "Version": "1.6", + "Version": "1.7", "Website": "" }, "X-Plasma-API": "javascript", diff --git a/uninstall.sh b/uninstall.sh index 97db65b..6da94eb 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,3 +1,171 @@ -#!/bin/bash -kpackagetool5 --type=KWin/Script --remove . -qdbus org.kde.KWin /KWin reconfigure +#!/usr/bin/env bash + + +# This is a script to remove an installed KWin script matching a script found in the current folder + +exit_w_error() { + local msg="$1" + echo -e "\nERROR: ${msg} \nExiting...\n" + exit 1 +} + +unload_kwin_script() { + echo "Attempting to unload KWin script '${script_name}' prior to removal." + + local output="" + local success=0 + local not_loaded=0 + + if command -v gdbus &> /dev/null; then + output=$(gdbus call --session --dest org.kde.KWin --object-path /Scripting \ + --method org.kde.kwin.Scripting.unloadScript "${script_name}") + [[ "$output" == "(true,)" ]] && success=1 + [[ "$output" == "(false,)" ]] && not_loaded=1 + elif command -v qdbus &> /dev/null; then + output=$(qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript "${script_name}") + [[ "$output" == "true" ]] && success=1 + [[ "$output" == "false" ]] && not_loaded=1 + elif command -v dbus-send &> /dev/null; then + output=$(dbus-send --session --print-reply --type=method_call --dest=org.kde.KWin \ + /Scripting org.kde.kwin.Scripting.unloadScript string:"${script_name}") + echo "$output" | grep -q "boolean true" && success=1 + echo "$output" | grep -q "boolean false" && not_loaded=1 + else + echo "No available D-Bus utility to unload the KWin script." + echo "You may need to log out to remove the KWin script from memory." + success=0 # Indicates failure to unload due to lack of tools + fi + + if [[ $success -eq 1 ]]; then + echo "Successfully unloaded the KWin script." + elif [[ $not_loaded -eq 1 ]]; then + echo "The KWin script was already unloaded or does not exist." + else + echo "ERROR: Failed to unload the KWin script. Here is the output:" + echo "" + echo "$output" + echo "" + echo "Uninstalling the script now may leave it active in memory until you log out." + read -r -p "Continue with uninstalling the script files anyway? [y/N]: " response + case $response in + [Yy]* ) + echo "Proceeding with removal. The KWin script might still be active in memory." + ;; + * ) + echo "Try to unload the script manually from GUI KWin Scripts settings panel." + exit_w_error "Run this script again to uninstall, or click trash icon in GUI and Apply." + ;; + esac + fi +} + +remove_w_kpackagetool6() { + if ! command -v kpackagetool6 &> /dev/null; then + exit_w_error "The 'kpackagetool6' command is missing. Cannot remove KWin script." + else + echo "Removing '${script_name}' KWin script." + kpackagetool6 --type=${script_type} --remove "${script_name}" + fi +} + +remove_w_kpackagetool5() { + if ! command -v kpackagetool5 &> /dev/null; then + exit_w_error "The 'kpackagetool5' command is missing. Cannot remove KWin script." + else + echo "Removing '${script_name}' KWin script." + kpackagetool5 --type=${script_type} --remove "${script_name}" + fi +} + +KDE_ver=${KDE_SESSION_VERSION:-0} # Default to zero value if environment variable not set +script_type="KWin/Script" +script_name="" + + +if [ -f "./metadata.json" ]; then + # The "P" option for grep causes a problem on Chimera Linux, which uses BSD utils + # script_name=$(grep -oP '"Id":\s*"[^"]*' ./metadata.json | grep -oP '[^"]*$') + script_name=$(grep '"Id":' ./metadata.json | sed 's/.*"Id":[[:space:]]*"\([^"]*\).*/\1/') +elif [ -f "./metadata.desktop" ]; then + script_name=$(grep '^X-KDE-PluginInfo-Name=' ./metadata.desktop | cut -d '=' -f2) + echo "FYI: 'metadata.desktop' files are deprecated. Use 'metadata.json' format." +else + exit_w_error "No suitable metadata file found. Unable to get script name." +fi + +if [ "$script_name" == "" ]; then + exit_w_error "Failed to parse KWin script name from metadata file." +fi + +if [[ $KDE_ver -eq 0 ]]; then + echo "KDE_SESSION_VERSION environment variable was not set." + exit_w_error "Cannot remove '${script_name}' KWin script." +elif [[ $KDE_ver -eq 6 ]]; then + unload_kwin_script + if ! remove_w_kpackagetool6; then + exit_w_error "Problem while removing '${script_name}' KWin script." + fi + echo "KWin script '${script_name}' was removed." +elif [[ ${KDE_ver} -eq 5 ]]; then + unload_kwin_script + if ! remove_w_kpackagetool5; then + exit_w_error "Problem while removing '${script_name}' KWin script." + fi + echo "KWin script '${script_name}' was removed." +else + echo "KDE_SESSION_VERSION had a value, but that value was unrecognized: '${KDE_ver}'" + exit_w_error "This script is meant to run only on KDE 5 or 6." +fi + + +sleep 0.5 + +# We need to gracefully cascade through common D-Bus utils to +# find one that is available to use for the KWin reconfigure +# command. Sometimes 'qdbus' is not available. Start with 'gdbus'. + +# Extended array of D-Bus command names with prioritized qdbus variants +dbus_commands=("gdbus" "qdbus6" "qdbus-qt6" "qdbus-qt5" "qdbus" "dbus-send") + +# Functions to handle reconfiguration with different dbus utilities +reconfigure() { + case "$1" in + gdbus) + gdbus call --session --dest org.kde.KWin --object-path /KWin --method org.kde.KWin.reconfigure + ;; + qdbus6 | qdbus-qt6 | qdbus-qt5 | qdbus) + "$1" org.kde.KWin /KWin reconfigure + ;; + dbus-send) + dbus-send --session --type=method_call --dest=org.kde.KWin /KWin org.kde.KWin.reconfigure + ;; + *) + echo "Unsupported DBus utility: $1" >&2 + return 1 + ;; + esac +} + +# Unquoted 'true' and 'false' values are built-in commands in bash, +# returning 0 or 1 exit status. +# So they can sort of be treated like Python's 'True' or 'False' in 'if' conditions. +dbus_cmd_found=false + +# Iterate through the dbus_commands array +for cmd in "${dbus_commands[@]}"; do + if command -v "${cmd}" &> /dev/null; then + dbus_cmd_found=true + echo "Refreshing KWin configuration using $cmd." + reconfigure "${cmd}" &> /dev/null + sleep 0.5 + # Break out of the loop once a command is found and executed + break + fi +done + +if ! $dbus_cmd_found; then + echo "No suitable DBus utility found. KWin configuration may need manual reloading." +fi + + +echo "Finished removing KWin script: '${script_name}'"