diff --git a/README.md b/README.md index c38a480..2bfccf1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Just run `middleclick-autoscroll`. This will open the configuration TUI that loo Not identified 1 - see the applications list Steam ON New applications ON + Middle-click paste off Last applied 2 minutes ago Applications pick this up the next time they are started. diff --git a/doc/middleclick-autoscroll.1.scd b/doc/middleclick-autoscroll.1.scd index d937eef..50dccc4 100644 --- a/doc/middleclick-autoscroll.1.scd +++ b/doc/middleclick-autoscroll.1.scd @@ -172,6 +172,35 @@ and has a configuration file with a slot for extra arguments; that slot is where the flag goes. Installed as a plain package, a Flatpak or a snap, it is an ordinary desktop entry and needs nothing special. +# MIDDLE-CLICK PASTE + +Inside a Chromium application the argument settles this on its own: with +autoscroll on, Blink stops pasting the primary selection on middle click, +because the button is doing something else now. Everywhere else on the desktop +middle click goes on pasting, so the desktop's own switch for it is turned off +too. + +KDE has such a switch, and only for a Wayland session, where the paste is a +protocol the compositor either offers or does not: + + ~/.config/kwinrc, [Wayland], EnablePrimarySelection=false + +It is written with *kwriteconfig6*, the same tool System Settings uses, because +kwinrc has a cascade behind it and entries a distribution can mark immutable; +an edit that reaches past all that writes something KDE goes on to ignore. KWin +reads the key once, while it starts, so the change is true from the next login +on rather than straight away. + +What the key said before is recorded, and *disable* puts that back. A key that +already said *false* before any of this is left alone and not recorded either, +so a paste the user switched off themselves is never switched back on. + +On X11 there is nothing to switch. The primary selection is part of X itself +and every toolkit reaches for it on its own; no one place can say no. The +status screen leaves the row out where there is nothing to report. + +Turn it off under *Settings* to keep the desktop's middle-click paste. + # WHAT CANNOT BE DETECTED An AppImage keeps its payload in a compressed filesystem, so there is no way to @@ -190,6 +219,11 @@ _~/.local/state/middleclick-autoscroll/ledger_ _~/.local/state/middleclick-autoscroll/backup/_ Copies of the files that are edited in place rather than shadowed. +_~/.config/kwinrc_ + KDE's own configuration. One key in it, *EnablePrimarySelection*, is + written when the desktop's middle-click paste is turned off, and put + back by *disable*. + _~/.cache/middleclick-autoscroll/detect_ Which programs were found to be Chromium, keyed by size and modification time. Safe to delete. @@ -207,7 +241,7 @@ up at the next *apply* rather than on their own. # SEE ALSO -*systemctl*(1), *flatpak*(1), *snap*(8) +*systemctl*(1), *flatpak*(1), *snap*(8), *kwriteconfig6*(1) # AUTHORS diff --git a/po/de.po b/po/de.po index 3574900..aa788b6 100644 --- a/po/de.po +++ b/po/de.po @@ -63,6 +63,9 @@ msgstr "%d – siehe Anwendungsliste" msgid "Steam" msgstr "Steam" +msgid "Middle-click paste" +msgstr "Mittelklick-Einfügen" + msgid "New applications" msgstr "Neue Anwendungen" @@ -112,6 +115,9 @@ msgstr "Programme, die sich beim Anmelden selbst starten" msgid "Spotify" msgstr "Spotify" +msgid "Turn off KDE's middle-click paste" +msgstr "KDEs Mittelklick-Einfügen abschalten" + msgid "Apply to newly installed applications" msgstr "Auf neu installierte Anwendungen anwenden" @@ -179,6 +185,9 @@ msgstr "Steam startet seine Oberfläche auf eine Art, die diese Version nicht ä msgid "Steam's interface cannot be given the flag without leaving a file Steam would reinstall; the change has been taken back." msgstr "Steams Oberfläche lässt sich das Flag nicht geben, ohne eine Datei zu hinterlassen, die Steam neu installieren würde; die Änderung wurde zurückgenommen." +msgid "Middle click stops pasting at the next login." +msgstr "Mittelklick fügt ab der nächsten Anmeldung nichts mehr ein." + #. Help msgid "Usage: middleclick-autoscroll [command]" diff --git a/po/middleclick-autoscroll.pot b/po/middleclick-autoscroll.pot index 924a4f4..afae7b0 100644 --- a/po/middleclick-autoscroll.pot +++ b/po/middleclick-autoscroll.pot @@ -63,6 +63,9 @@ msgstr "" msgid "Steam" msgstr "" +msgid "Middle-click paste" +msgstr "" + msgid "New applications" msgstr "" @@ -112,6 +115,9 @@ msgstr "" msgid "Spotify" msgstr "" +msgid "Turn off KDE's middle-click paste" +msgstr "" + msgid "Apply to newly installed applications" msgstr "" @@ -179,6 +185,9 @@ msgstr "" msgid "Steam's interface cannot be given the flag without leaving a file Steam would reinstall; the change has been taken back." msgstr "" +msgid "Middle click stops pasting at the next login." +msgstr "" + #. Help msgid "Usage: middleclick-autoscroll [command]" diff --git a/src/lib/apply.sh b/src/lib/apply.sh index 85df233..28cc6ba 100644 --- a/src/lib/apply.sh +++ b/src/lib/apply.sh @@ -134,6 +134,11 @@ mca_apply() { [[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply + # Not about any one application: middle click goes on pasting everywhere + # else on the desktop, and KDE is the one desktop that can be told not to. + # See kde.sh. + [[ $CFG_PASTE == yes ]] && mca_kde_paste_apply + mca_prune_orphans if (( MCA_CHANGES )) && mca_have update-desktop-database; then diff --git a/src/lib/common.sh b/src/lib/common.sh index b8f21e3..9bc3805 100644 --- a/src/lib/common.sh +++ b/src/lib/common.sh @@ -278,9 +278,12 @@ mca_state_write() { # shadow # inplace # flags created | appended | merged +# kwin what EnablePrimarySelection said before # -# `inplace` and `flags` differ in how they are undone: a backup is restored -# wholesale, a flag file only loses the one line that was added to it. +# The kinds differ in how they are undone: a backup is restored wholesale, a +# flag file only loses the one line that was added to it, and kwinrc gets one +# key written back - it is a file KDE writes to itself, and putting a whole +# copy of it back would take everything else settled since with it. mca_ledger_add() { local kind="$1" path="$2" detail="${3:-}" @@ -303,6 +306,15 @@ mca_ledger_has() { awk -F'\t' -v p="$1" '$2 == p { found = 1 } END { exit !found }' "$MCA_LEDGER" } +# mca_ledger_detail +# The third column of that file's line, empty when the ledger has none. What it +# means is the kind's business; for `kwin` it is the value to put back. +mca_ledger_detail() { + [[ -f $MCA_LEDGER ]] || return 1 + awk -F'\t' -v p="$1" '$2 == p { d = $3 } END { if (!length(d)) exit 1; print d }' \ + "$MCA_LEDGER" +} + mca_ledger_forget() { local path="$1" tmp [[ -f $MCA_LEDGER ]] || return 0 diff --git a/src/lib/config.sh b/src/lib/config.sh index 9d1213b..91b4dec 100644 --- a/src/lib/config.sh +++ b/src/lib/config.sh @@ -144,6 +144,7 @@ mca_config_load() { CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes CFG_WATCH=no; mca_config_bool WatchNewApps yes && CFG_WATCH=yes + CFG_PASTE=no; mca_config_bool DisablePaste yes && CFG_PASTE=yes CFG_EXTRA_FLAGS="$(mca_config_get ExtraFlags '')" diff --git a/src/lib/kde.sh b/src/lib/kde.sh new file mode 100644 index 0000000..0f0d708 --- /dev/null +++ b/src/lib/kde.sh @@ -0,0 +1,167 @@ +# shellcheck shell=bash +# +# KDE's middle-click paste. +# +# Inside a Chromium application the flag settles this on its own: with +# autoscroll on, Blink stops pasting the primary selection on middle click, +# because the button is doing something else now. Everywhere else on the +# desktop middle click goes on pasting, which is the behaviour this whole +# program exists to get away from - so the desktop's own switch for it is +# turned off too. +# +# KWin has such a switch, and only for a Wayland session, where the paste is a +# protocol the compositor either offers or does not: +# +# ~/.config/kwinrc, [Wayland], EnablePrimarySelection=false +# +# On X11 there is nothing to switch. The primary selection is part of X itself +# and every toolkit reaches for it on its own; no one place can say no. +# +# KWin reads the key once, while it starts, so what is written here is true +# from the next login on rather than straight away. + +MCA_KWINRC="${MCA_XDG_CONFIG}/kwinrc" +MCA_KWIN_GROUP="Wayland" +MCA_KWIN_KEY="EnablePrimarySelection" + +# The kconfig command line tools, whichever generation this session has. They +# are what System Settings writes kwinrc with, and worth the fork: the file has +# a cascade behind it - /etc/xdg, the kdedefaults directory - and entries a +# distribution can mark immutable, and an edit that reaches past all that with +# sed writes something KDE goes on to ignore. +MCA_KDE_TOOL='' +MCA_KDE_TOOL_LOOKED=0 + +_mca_kde_tool() { + local gen + + if (( ! MCA_KDE_TOOL_LOOKED )); then + MCA_KDE_TOOL_LOOKED=1 + for gen in 6 5; do + if mca_have "kreadconfig$gen" && mca_have "kwriteconfig$gen"; then + MCA_KDE_TOOL="$gen" + break + fi + done + fi + + [[ -n $MCA_KDE_TOOL ]] || return 1 + printf '%s\n' "$MCA_KDE_TOOL" +} + +# mca_kde_session +# Whether this is a KWin Wayland session. +mca_kde_session() { + local desktop="${XDG_CURRENT_DESKTOP:-}" + + # The watcher is a systemd service, and one started before the session + # imported its environment sees none of these variables. A running + # kwin_wayland answers both halves of the question at once, and is what + # such a run ends up going by. + if [[ -z $desktop && -z ${KDE_FULL_SESSION:-} ]]; then + mca_have pgrep || return 1 + pgrep -u "$(id -u)" -x kwin_wayland > /dev/null 2>&1 + return + fi + + # XDG_CURRENT_DESKTOP is the one every desktop sets, and the one to go by + # wherever it is set; KDE_FULL_SESSION is what answers when it is not. + [[ -z $desktop || ${desktop^^} == *KDE* ]] || return 1 + + [[ ${XDG_SESSION_TYPE:-} == wayland || -n ${WAYLAND_DISPLAY:-} ]] +} + +mca_kde_available() { + mca_kde_session && _mca_kde_tool > /dev/null +} + +# mca_kde_read [file] +# What the key says now, lowercased, empty when it is not set at all. +mca_kde_read() { + local file="${1:-$MCA_KWINRC}" gen val + gen="$(_mca_kde_tool)" || return 1 + + # kreadconfig creates the file it is pointed at, and the status screen - + # which asks on every redraw - has no business writing anything. A file + # that is not there has no value in it to read anyway. + [[ -f $file ]] || return 0 + + val="$("kreadconfig$gen" --file "$file" --group "$MCA_KWIN_GROUP" \ + --key "$MCA_KWIN_KEY" 2>/dev/null)" + printf '%s\n' "${val,,}" +} + +# mca_kde_write [file] +# An empty value takes the key back out rather than writing nothing into it - +# a key that is missing and a key that says nothing are not the same thing to +# KDE, and only the first one is what an untouched kwinrc looks like. +mca_kde_write() { + local file="$1" value="$2" gen + gen="$(_mca_kde_tool)" || return 1 + + if [[ -z $value ]]; then + "kwriteconfig$gen" --file "$file" --group "$MCA_KWIN_GROUP" \ + --key "$MCA_KWIN_KEY" --delete 2>/dev/null + else + "kwriteconfig$gen" --file "$file" --group "$MCA_KWIN_GROUP" \ + --key "$MCA_KWIN_KEY" "$value" 2>/dev/null + fi +} + +# mca_kde_paste_apply +# Turns the paste off, and remembers what the key said before so that undoing +# puts that back rather than a guess. +# +# A key that already says false without the ledger knowing about it was set by +# the user or by the distribution. That one is left alone and nothing is +# recorded, so `disable` cannot switch a paste back on that this program never +# switched off. +mca_kde_paste_apply() { + local current previous changed=0 + + mca_kde_available || return 1 + current="$(mca_kde_read)" || return 1 + + if mca_ledger_has "$MCA_KWINRC"; then + # Ours already. What goes back is the value recorded the first time + # round, not what reading the file now would say - that is false, and + # false is what is being undone. + previous="$(mca_ledger_detail "$MCA_KWINRC")" || previous=unset + elif [[ $current == false ]]; then + return 0 + else + previous="${current:-unset}" + fi + + if [[ $current != false ]]; then + mca_kde_write "$MCA_KWINRC" false || return 1 + MCA_CHANGES=$(( MCA_CHANGES + 1 )) + changed=1 + fi + + mca_ledger_add kwin "$MCA_KWINRC" "$previous" + + (( changed )) && mca_note "$(mca_msg "Middle click stops pasting at the next login.")" + return 0 +} + +# mca_kde_paste_revert +mca_kde_paste_revert() { + local file="$1" previous="$2" + + _mca_kde_tool > /dev/null || return 1 + [[ -f $file ]] || return 1 + + # Only what is still recognisably ours. A key somebody has since put a + # value of their own into is theirs now. + [[ "$(mca_kde_read "$file")" == false ]] || return 1 + + [[ $previous == unset ]] && previous='' + mca_kde_write "$file" "$previous" || return 1 + + # Nothing was in the file but the key, which means there was no file before + # this program wrote one. An empty kwinrc says nothing that its absence + # does not say better. + [[ -s $file ]] || rm -f -- "$file" + return 0 +} diff --git a/src/lib/menu.sh b/src/lib/menu.sh index 7d3d8cc..fca6946 100644 --- a/src/lib/menu.sh +++ b/src/lib/menu.sh @@ -194,6 +194,18 @@ mca_ui_status() { fi fi + # Not an ON/OFF like the rows around it: what is being reported is the state + # of the paste, not of a switch, and off is the state this program is after. + if mca_kde_available; then + if [[ "$(mca_kde_read)" == false ]]; then + _mca_row "$(mca_msg "Middle-click paste")" \ + "${MCA_C_GREEN}$(mca_msg "off")${MCA_C_RESET}" + else + _mca_row "$(mca_msg "Middle-click paste")" \ + "${MCA_C_DIM}$(mca_msg "on")${MCA_C_RESET}" + fi + fi + _mca_row "$(mca_msg "Last applied")" "$(mca_time_ago "$last")" if [[ $CFG_ENABLED == yes ]]; then @@ -216,6 +228,7 @@ MCA_SETTINGS=( "PatchAutostart|bool|yes|Programs that start themselves at login" "PatchSteam|bool|yes|Steam" "PatchSpotify|bool|yes|Spotify" + "DisablePaste|bool|yes|Turn off KDE's middle-click paste" "WatchNewApps|bool|yes|Apply to newly installed applications" "ExtraFlags|text||Additional Chromium arguments" ) diff --git a/src/lib/patch.sh b/src/lib/patch.sh index 497d090..35cfd1b 100644 --- a/src/lib/patch.sh +++ b/src/lib/patch.sh @@ -646,6 +646,10 @@ mca_revert_all() { mca_steam_revert "$path" "$detail" \ && MCA_CHANGES=$(( MCA_CHANGES + 1 )) ;; + kwin) + mca_kde_paste_revert "$path" "$detail" \ + && MCA_CHANGES=$(( MCA_CHANGES + 1 )) + ;; esac done < <(tac "$MCA_LEDGER" 2>/dev/null) diff --git a/src/middleclick-autoscroll b/src/middleclick-autoscroll index 9449f2b..4412ddb 100644 --- a/src/middleclick-autoscroll +++ b/src/middleclick-autoscroll @@ -17,7 +17,7 @@ set -uo pipefail MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}" -for _mod in common config detect patch steam apply menu; do +for _mod in common config detect patch steam kde apply menu; do # shellcheck source=/dev/null if ! source "$MCA_LIBDIR/$_mod.sh"; then printf 'middleclick-autoscroll: cannot load %s/%s.sh\n' "$MCA_LIBDIR" "$_mod" >&2