693 lines
23 KiB
Bash
693 lines
23 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# Putting the flag where the application will read it, and taking it back out.
|
|
#
|
|
# There are two ways in, and the order matters:
|
|
#
|
|
# 1. A flag file. Some distributions wrap Electron and Chromium in a
|
|
# launcher script that reads extra arguments from
|
|
# $XDG_CONFIG_HOME/<name>-flags.conf. Arch and its derivatives do, and
|
|
# their packages follow that convention. Where it exists, this is the good
|
|
# way in: it is the supported way to pass arguments, it survives package upgrades
|
|
# untouched, and it applies however the program is started, including from
|
|
# a terminal.
|
|
#
|
|
# Nothing is assumed about which distribution this is. The launcher itself
|
|
# is read, and the route is taken only for a launcher that really does
|
|
# name such a file. On Debian, Ubuntu, Fedora and openSUSE the
|
|
# equivalent file lives under /etc and is the system's to write. There is
|
|
# no flag file to use there, and route 2 answers for everything.
|
|
#
|
|
# 2. A desktop entry. For applications that ship their own binary with no
|
|
# wrapper, and for everything a Flatpak or a snap contains, there is
|
|
# nowhere else to put an argument, so a copy of the entry with the flag
|
|
# appended goes into ~/.local/share/applications, where it shadows the
|
|
# system one.
|
|
#
|
|
# Nothing outside $HOME is ever written to.
|
|
|
|
MCA_MARK_BEGIN='# >>> middleclick-autoscroll'
|
|
MCA_MARK_END='# <<< middleclick-autoscroll'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Feature lists
|
|
# ---------------------------------------------------------------------------
|
|
# Both --enable-features and --enable-blink-features take a comma separated
|
|
# list, and Chromium keeps the last one it is given and drops the rest. A
|
|
# second one of the same kind would therefore silently switch off whatever the
|
|
# first one turned on, so an option that is already there is extended instead.
|
|
|
|
# _mca_list_merge <list> <wanted>
|
|
# The two lists as one, in the order they came and without repeating anything.
|
|
_mca_list_merge() {
|
|
local out="$1" item
|
|
for item in ${2//,/ }; do
|
|
[[ ",$out," == *",$item,"* ]] && continue
|
|
out="${out:+$out,}$item"
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# _mca_list_strip <list> <unwanted>
|
|
# The list without any of the named entries.
|
|
_mca_list_strip() {
|
|
local out='' item
|
|
for item in ${1//,/ }; do
|
|
[[ ",$2," == *",$item,"* ]] && continue
|
|
out="${out:+$out,}$item"
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# The two options, as a regular expression branch, for the places that have to
|
|
# recognise either of them.
|
|
MCA_FEATURE_OPTS='--enable-blink-features|--enable-features'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Command lines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# _mca_exec_insert_at <exec line>
|
|
# Where an argument has to go: after the program and its own arguments, but
|
|
# before the first field code (%U and friends) or Flatpak's @@ markers, which
|
|
# are placeholders the launcher expands rather than arguments.
|
|
#
|
|
# A bare "--" stops it too. That is where Chromium stops reading switches and
|
|
# starts treating everything as an address, so a flag put after it is read as a
|
|
# page to open and the feature never comes on. Discord's entry is exactly that
|
|
# shape: `discord --url -- %u`.
|
|
_mca_exec_insert_at() {
|
|
local rest="$1" tok idx=0
|
|
|
|
while [[ -n $rest ]]; do
|
|
while [[ $rest == ' '* ]]; do rest="${rest# }"; idx=$(( idx + 1 )); done
|
|
[[ -n $rest ]] || break
|
|
tok="${rest%% *}"
|
|
case "$tok" in
|
|
%[a-zA-Z]|@@|@@u|--) printf '%s\n' "$idx"; return 0 ;;
|
|
esac
|
|
rest="${rest:${#tok}}"
|
|
idx=$(( idx + ${#tok} ))
|
|
done
|
|
|
|
printf '%s\n' "$(( ${#1} ))"
|
|
}
|
|
|
|
# _mca_exec_after_program <exec line>
|
|
# The position just after the program name, for arguments that a program only
|
|
# accepts before its positional ones.
|
|
_mca_exec_after_program() {
|
|
local rest="$1" idx=0 tok
|
|
|
|
while [[ $rest == ' '* ]]; do rest="${rest# }"; idx=$(( idx + 1 )); done
|
|
tok="${rest%% *}"
|
|
printf '%s\n' "$(( idx + ${#tok} ))"
|
|
}
|
|
|
|
# mca_exec_inject <exec line> [flags] [position: before-fields|after-program]
|
|
# The same line with the given flags added, defaulting to the configured ones.
|
|
# Prints nothing and returns 1 when there was nothing to change, so callers can
|
|
# tell a real edit from a no-op without diffing.
|
|
#
|
|
# A feature list that is already on the line is extended rather than repeated.
|
|
# See _mca_list_merge.
|
|
mca_exec_inject() {
|
|
local line="$1" flagstr="${2:-}" where="${3:-before-fields}"
|
|
local flag opt value existing merged re at changed=0
|
|
local -a flags
|
|
|
|
[[ -n $flagstr ]] || flagstr="$(mca_flags)"
|
|
read -r -a flags <<< "$flagstr"
|
|
|
|
for flag in "${flags[@]}"; do
|
|
case "$flag" in
|
|
--enable-blink-features=*|--enable-features=*)
|
|
opt="${flag%%=*}"
|
|
value="${flag#*=}"
|
|
re="(^|[[:space:]])$opt=([^[:space:]]*)"
|
|
if [[ $line =~ $re ]]; then
|
|
existing="${BASH_REMATCH[2]}"
|
|
merged="$(_mca_list_merge "$existing" "$value")"
|
|
[[ $merged == "$existing" ]] && continue
|
|
line="${line/$opt=$existing/$opt=$merged}"
|
|
changed=1
|
|
continue
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
[[ " $line " == *" $flag "* ]] && continue
|
|
|
|
if [[ $where == after-program ]]; then
|
|
at="$(_mca_exec_after_program "$line")"
|
|
line="${line:0:$at} $flag${line:$at}"
|
|
else
|
|
at="$(_mca_exec_insert_at "$line")"
|
|
if (( at >= ${#line} )); then
|
|
line="${line% } $flag"
|
|
else
|
|
line="${line:0:$at}$flag ${line:$at}"
|
|
fi
|
|
fi
|
|
changed=1
|
|
done
|
|
|
|
(( changed )) || return 1
|
|
printf '%s\n' "$line"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Flag files
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# mca_flags_apply <program> [kind]
|
|
# Writes the flags into the file the program's launcher reads. Returns 0 when
|
|
# the program has such a file (whether or not anything needed changing), 1 when
|
|
# it has none and the desktop entry route has to be used instead.
|
|
mca_flags_apply() {
|
|
local prog="$1" kind="${2:-app}" name target='' existing_target=''
|
|
local -a candidates=()
|
|
|
|
mapfile -t candidates < <(mca_flags_candidates "$prog" | awk '!seen[$0]++')
|
|
(( ${#candidates[@]} )) || return 1
|
|
|
|
# A wrapper that offers several files reads the first one that exists and
|
|
# ignores the rest, so an existing file is always the right one to edit.
|
|
for name in "${candidates[@]}"; do
|
|
if [[ -f "$MCA_XDG_CONFIG/$name" ]]; then
|
|
existing_target="$name"
|
|
_mca_flags_write "$MCA_XDG_CONFIG/$name" "$kind"
|
|
fi
|
|
done
|
|
[[ -n $existing_target ]] && return 0
|
|
|
|
# None exists yet. The last candidate is the wrapper's own fallback (the
|
|
# generic electron-flags.conf rather than electron43-flags.conf). So
|
|
# creating that one also covers every other application using the same
|
|
# shared Electron.
|
|
target="${candidates[-1]}"
|
|
_mca_flags_write "$MCA_XDG_CONFIG/$target" "$kind"
|
|
return 0
|
|
}
|
|
|
|
# _mca_flags_write <file> [kind]
|
|
# Adds the flags as a marked block, or merges into a feature list that is
|
|
# already in the file.
|
|
_mca_flags_write() {
|
|
local file="$1" kind="${2:-app}"
|
|
local content='' line flag opt want existing merged=0 block=''
|
|
local -a flags
|
|
local -A merged_opts=()
|
|
local created=0
|
|
|
|
read -r -a flags <<< "$(mca_flags "$kind")"
|
|
|
|
[[ -f $file ]] || created=1
|
|
if (( ! created )); then
|
|
content="$(< "$file")"
|
|
# Already ours: the block is rewritten below, so drop the old one first.
|
|
if [[ $content == *"$MCA_MARK_BEGIN"* ]]; then
|
|
content="$(_mca_flags_drop_block "$file")"
|
|
fi
|
|
fi
|
|
|
|
# Merge into a feature list the user (or a distribution) put there, rather
|
|
# than write a second line of the same option further down.
|
|
if [[ $content == *--enable-* ]]; then
|
|
local out=''
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ ^[[:space:]]*($MCA_FEATURE_OPTS)=([^[:space:]]*)[[:space:]]*$ ]]; then
|
|
opt="${BASH_REMATCH[1]}"
|
|
existing="${BASH_REMATCH[2]}"
|
|
want=''
|
|
for flag in "${flags[@]}"; do
|
|
[[ $flag == "$opt="* ]] && want="${flag#*=}"
|
|
done
|
|
if [[ -n $want ]]; then
|
|
line="$opt=$(_mca_list_merge "$existing" "$want")"
|
|
merged_opts["$opt"]=1
|
|
merged=1
|
|
fi
|
|
fi
|
|
out+="$line"$'\n'
|
|
done <<< "$content"
|
|
content="${out%$'\n'}"
|
|
fi
|
|
|
|
for flag in "${flags[@]}"; do
|
|
[[ -n ${merged_opts["${flag%%=*}"]:-} ]] && continue
|
|
block+="$flag"$'\n'
|
|
done
|
|
|
|
if [[ -n $block ]]; then
|
|
content="${content:+$content$'\n'}$MCA_MARK_BEGIN"$'\n'"$block$MCA_MARK_END"
|
|
fi
|
|
|
|
if mca_write_if_changed "$file" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
|
|
if (( created )); then
|
|
mca_ledger_add flags "$file" created
|
|
elif (( merged )) && [[ -z $block ]]; then
|
|
mca_ledger_add flags "$file" merged
|
|
elif (( merged )); then
|
|
mca_ledger_add flags "$file" merged+block
|
|
else
|
|
mca_ledger_add flags "$file" block
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# _mca_flags_drop_block <file> [begin marker] [end marker]
|
|
_mca_flags_drop_block() {
|
|
awk -v b="${2:-$MCA_MARK_BEGIN}" -v e="${3:-$MCA_MARK_END}" '
|
|
$0 == b { skip = 1; next }
|
|
$0 == e { skip = 0; next }
|
|
!skip
|
|
' "$1" 2>/dev/null
|
|
}
|
|
|
|
# mca_flags_revert <file> <detail>
|
|
mca_flags_revert() {
|
|
local file="$1" detail="$2" content line out='' opt existing new
|
|
|
|
[[ -f $file ]] || return 0
|
|
|
|
case "$detail" in
|
|
created)
|
|
# Only remove the file if nothing but our block ended up in it;
|
|
# anything else means the user started using it in the meantime.
|
|
content="$(_mca_flags_drop_block "$file")"
|
|
if [[ -z ${content//[[:space:]]/} ]]; then
|
|
rm -f -- "$file"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
content="$(_mca_flags_drop_block "$file")"
|
|
|
|
if [[ $detail == merged* ]]; then
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ ^[[:space:]]*($MCA_FEATURE_OPTS)=([^[:space:]]*)[[:space:]]*$ ]]; then
|
|
opt="${BASH_REMATCH[1]}"
|
|
existing="${BASH_REMATCH[2]}"
|
|
# Either name, whichever version of this program put it there.
|
|
new="$(_mca_list_strip "$existing" "$MCA_BROWSER_FEATURES")"
|
|
[[ -z $new ]] && continue
|
|
line="$opt=$new"
|
|
fi
|
|
out+="$line"$'\n'
|
|
done <<< "$content"
|
|
content="${out%$'\n'}"
|
|
fi
|
|
|
|
if [[ -z ${content//[[:space:]]/} ]]; then
|
|
rm -f -- "$file"
|
|
else
|
|
printf '%s\n' "$content" > "$file"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Desktop entries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# A generated entry and an entry that was edited in place look similar and must
|
|
# never be confused: the first one is ours to delete, the second one is the
|
|
# user's file with one line changed and has to be restored from its backup. They
|
|
# carry different markers so that the scan, the "already done" checks and the
|
|
# undo can all tell them apart.
|
|
MCA_MARK_SHADOW='X-MCA-Generated'
|
|
MCA_MARK_INPLACE='X-MCA-Patched'
|
|
|
|
# _mca_desktop_transform <file> <marker> [flags] [position]
|
|
# The whole file with every Exec line rewritten: the main one and the one in
|
|
# each Desktop Action, because those are the right-click menu entries and a
|
|
# user who starts Steam from "Library" expects the same behaviour there.
|
|
_mca_desktop_transform() {
|
|
local file="$1" marker="$2" flags="${3:-}" where="${4:-before-fields}"
|
|
local line rest new marked=0
|
|
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
case "$line" in
|
|
Exec=*)
|
|
rest="${line#Exec=}"
|
|
new="$(mca_exec_inject "$rest" "$flags" "$where")" && line="Exec=$new"
|
|
;;
|
|
# A D-Bus activated entry is started through its service file and
|
|
# the Exec line is never used, so the flag would go nowhere.
|
|
DBusActivatable=true)
|
|
line='DBusActivatable=false'
|
|
;;
|
|
# A marker from an older version; the current one is written below.
|
|
X-MCA-*)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
printf '%s\n' "$line"
|
|
|
|
if (( ! marked )) && [[ $line =~ ^[[:space:]]*\[Desktop\ Entry\][[:space:]]*$ ]]; then
|
|
printf '%s=%s\n' "$marker" "$MCA_VERSION"
|
|
marked=1
|
|
fi
|
|
done < "$file"
|
|
}
|
|
|
|
# mca_desktop_apply <id> <source entry> [flags] [position]
|
|
# Puts a patched copy in ~/.local/share/applications, or patches the entry in
|
|
# place when it already lives there.
|
|
mca_desktop_apply() {
|
|
local id="$1" src="$2" flags="${3:-}" where="${4:-before-fields}"
|
|
local target="$MCA_APPDIR/$id.desktop" content backup
|
|
|
|
if [[ "$src" == "$target" ]]; then
|
|
# The user's own entry, like an AppImage, a web app shortcut or
|
|
# something installed by hand. There is nowhere to shadow it from, so it is
|
|
# edited directly and the original is kept.
|
|
grep -q "^$MCA_MARK_INPLACE=" "$src" 2>/dev/null && return 0
|
|
|
|
content="$(_mca_desktop_transform "$src" "$MCA_MARK_INPLACE" "$flags" "$where")"
|
|
[[ -n $content ]] || return 1
|
|
[[ "$content" == "$(< "$src")" ]] && return 0
|
|
|
|
backup="$(mca_backup "$src")" || return 1
|
|
if _mca_entry_write "$src" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add inplace "$src" "$backup"
|
|
return 0
|
|
fi
|
|
|
|
content="$(_mca_desktop_transform "$src" "$MCA_MARK_SHADOW" "$flags" "$where")"
|
|
[[ -n $content ]] || return 1
|
|
|
|
mkdir -p "$MCA_APPDIR" 2>/dev/null || return 1
|
|
|
|
# Something is already shadowing this entry. Unless it is a shadow of ours,
|
|
# it is the user's own file (possibly one we edited in place earlier), and
|
|
# overwriting it here would lose it.
|
|
if [[ -e $target ]] && ! grep -q "^$MCA_MARK_SHADOW=" "$target" 2>/dev/null; then
|
|
return 1
|
|
fi
|
|
|
|
if mca_write_if_changed "$target" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add shadow "$target" "$src"
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entries outside the search path
|
|
# ---------------------------------------------------------------------------
|
|
# Two directories hold desktop entries that no part of the XDG search path
|
|
# looks at, so nothing the scan does can reach them:
|
|
#
|
|
# ~/.config/autostart, where applications that start themselves at login
|
|
# write their own entry, pointing straight at their binary. Those bypass the
|
|
# entry in the menu completely, which is why Discord launched at login used
|
|
# to behave differently from Discord launched from the menu.
|
|
#
|
|
# The desktop folder, where a shortcut somebody dragged out of the menu
|
|
# (or asked Steam for) lives and nowhere else.
|
|
#
|
|
# Neither can be shadowed from anywhere, so both are edited where they stand,
|
|
# with the original kept.
|
|
|
|
# _mca_entry_write <file> <content>
|
|
# Writes an entry edited in place, like mca_write_if_changed. A symlink is
|
|
# replaced by the edited file instead of written through: it usually points at
|
|
# a package's entry in /usr, which is not ours to change. Undo puts the link
|
|
# back from the backup.
|
|
_mca_entry_write() {
|
|
local file="$1" content="$2" tmp
|
|
|
|
[[ -L $file ]] || { mca_write_if_changed "$file" "$content"; return; }
|
|
|
|
tmp="$(mktemp "$file.XXXXXX")" || return 2
|
|
chmod --reference="$file" -- "$tmp" 2>/dev/null || chmod 644 -- "$tmp"
|
|
|
|
# A link into /usr/share/applications is trusted for where it points. A
|
|
# copy on the desktop is only trusted by KDE when it can be run.
|
|
chmod u+x -- "$tmp" 2>/dev/null || true
|
|
|
|
if printf '%s' "$content" > "$tmp" && mv -f -- "$tmp" "$file"; then
|
|
return 0
|
|
fi
|
|
rm -f -- "$tmp"
|
|
return 2
|
|
}
|
|
|
|
# _mca_entry_patch_inplace <file>
|
|
# One desktop entry that lives outside the XDG search path, edited where it is
|
|
# because there is nowhere to shadow it from. It follows the applications list
|
|
# under its own file name, which is the name of the application's own entry.
|
|
_mca_entry_patch_inplace() {
|
|
local file="$1"
|
|
local id prog packaging=native content backup kind copy
|
|
|
|
# Already ours, and normally that is the end of it: what is in there is what
|
|
# this version put there. Not so for a file the ledger has no record of -
|
|
# nothing would ever look at that one again, neither the undo a change of
|
|
# flags goes through nor this, which leaves a marked file alone, and it would
|
|
# keep an answer from an older version for good. The copy taken before the
|
|
# edit is named after the path and is still there, so the file goes back to
|
|
# what it was and is written again below.
|
|
if grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null; then
|
|
mca_ledger_has "$file" && return 0
|
|
copy="$MCA_BACKUPDIR/$(mca_backup_name "$file")"
|
|
[[ -f $copy ]] || return 0
|
|
cp -p -- "$copy" "$file" 2>/dev/null || return 0
|
|
fi
|
|
|
|
_mca_desktop_read "$file"
|
|
[[ -n $DE_EXEC ]] || return 0
|
|
[[ $DE_HIDDEN == true ]] && return 0
|
|
|
|
mca_exec_program "$DE_EXEC" || return 0
|
|
prog="$MCA_PROG"
|
|
|
|
# A Flatpak or a snap entry names the wrapper rather than the application.
|
|
# The scan resolves those into an id; outside the scan the same has to
|
|
# happen here, or every Flatpak looks like the flatpak program itself.
|
|
if [[ ${prog##*/} == flatpak ]]; then
|
|
mca_exec_flatpak_id "$DE_EXEC" || return 0
|
|
prog="flatpak:$MCA_PROG"
|
|
packaging=flatpak
|
|
elif mca_snap_name "$prog"; then
|
|
prog="snap:$MCA_PROG"
|
|
packaging=snap
|
|
fi
|
|
|
|
# An entry that starts Steam is left where it is. Steam takes nothing
|
|
# from a command line that would help, and what it does take belongs to
|
|
# the web helper script in its own installation.
|
|
mca_prog_is_steam "$prog" && return 0
|
|
|
|
case "$packaging" in
|
|
flatpak) mca_flatpak_is_chromium "${prog#flatpak:}" || return 0 ;;
|
|
snap) mca_snap_is_chromium "${prog#snap:}" || return 0 ;;
|
|
*) mca_is_chromium "$prog" || return 0 ;;
|
|
esac
|
|
|
|
mca_desktop_is_browser && kind=browser || kind=app
|
|
|
|
id="${file##*/}"; id="${id%.desktop}"
|
|
mca_kind_wanted "$kind" "$id" || return 0
|
|
|
|
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
|
|
"$(mca_flags "$kind")")"
|
|
|
|
[[ -n $content ]] || return 0
|
|
[[ "$content" == "$(< "$file")" ]] && return 0
|
|
|
|
backup="$(mca_backup "$file")" || return 0
|
|
if _mca_entry_write "$file" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add inplace "$file" "$backup"
|
|
}
|
|
|
|
mca_autostart_apply() {
|
|
local dir="$MCA_XDG_CONFIG/autostart" file
|
|
|
|
[[ -d $dir ]] || return 0
|
|
|
|
for file in "$dir"/*.desktop; do
|
|
[[ -f $file ]] || continue
|
|
_mca_entry_patch_inplace "$file"
|
|
done
|
|
}
|
|
|
|
mca_shortcuts_apply() {
|
|
local dir file
|
|
dir="$(mca_desktop_folder)"
|
|
|
|
[[ -d $dir ]] || return 0
|
|
|
|
for file in "$dir"/*.desktop; do
|
|
[[ -f $file ]] || continue
|
|
_mca_entry_patch_inplace "$file"
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Spotify
|
|
# ---------------------------------------------------------------------------
|
|
# The official client is CEF rather than Electron and, when installed through
|
|
# spotify-launcher, is started by a program that builds its own command line.
|
|
# That launcher has a configuration file with a slot for exactly this.
|
|
|
|
mca_spotify_apply() {
|
|
local file="$MCA_XDG_CONFIG/spotify-launcher.conf" content line out='' backup
|
|
local found_section=0 found_args=0
|
|
|
|
mca_have spotify-launcher || return 0
|
|
|
|
if [[ -f $file ]]; then
|
|
content="$(< "$file")"
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ ^[[:space:]]*\[spotify\][[:space:]]*$ ]]; then
|
|
found_section=1
|
|
elif [[ $line =~ ^[[:space:]]*extra_arguments[[:space:]]*=[[:space:]]*\[(.*)\][[:space:]]*$ ]]; then
|
|
local args="${BASH_REMATCH[1]}"
|
|
found_args=1
|
|
if [[ $args != *"$MCA_FEATURE"* ]]; then
|
|
line="extra_arguments = [${args:+$args, }\"$MCA_FLAG\"]"
|
|
fi
|
|
fi
|
|
out+="$line"$'\n'
|
|
done <<< "$content"
|
|
content="${out%$'\n'}"
|
|
fi
|
|
|
|
if (( ! found_args )); then
|
|
(( found_section )) || content="${content:+$content$'\n'}[spotify]"
|
|
content="${content:+$content$'\n'}extra_arguments = [\"$MCA_FLAG\"]"
|
|
fi
|
|
|
|
[[ -f $file ]] && backup="$(mca_backup "$file")"
|
|
|
|
if mca_write_if_changed "$file" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add inplace "$file" "${backup:-}"
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Leftovers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# mca_prune_orphans
|
|
# Forgets changes there is no longer anything to undo.
|
|
#
|
|
# Uninstalling something deletes its entry from /usr/share/applications, but the
|
|
# copy shadowing it is in the user's home and pacman knows nothing about it. It
|
|
# would sit in the menu forever, offering to start a program that is no longer
|
|
# installed. And the watcher would not notice, because a plain apply only ever
|
|
# looks at what is there now. A file that was edited where it stood and has
|
|
# since been deleted is the same problem from the other side: its record would
|
|
# have `disable` put the file back.
|
|
mca_prune_orphans() {
|
|
local kind path source line
|
|
local -a lines=()
|
|
|
|
[[ -f $MCA_LEDGER ]] || return 0
|
|
mapfile -t lines < "$MCA_LEDGER"
|
|
|
|
for line in "${lines[@]}"; do
|
|
IFS=$'\t' read -r kind path source <<< "$line"
|
|
|
|
# An entry that was edited where it stood and has since been deleted -
|
|
# a game shortcut dragged to the wastebasket, an autostart entry the
|
|
# application removed. There is nothing left to put back, and keeping
|
|
# the record would have `disable` recreate a file the user threw away.
|
|
if [[ $kind == inplace && ! -e $path ]]; then
|
|
[[ -n $source ]] && rm -f -- "$MCA_BACKUPDIR/$source"
|
|
mca_ledger_forget "$path"
|
|
continue
|
|
fi
|
|
|
|
[[ $kind == shadow && -n $source ]] || continue
|
|
[[ -e $source ]] && continue
|
|
|
|
# Only ever remove something still recognisably ours.
|
|
if [[ -f $path ]] && grep -q "^$MCA_MARK_SHADOW=" "$path" 2>/dev/null; then
|
|
rm -f -- "$path"
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_forget "$path"
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Undo
|
|
# ---------------------------------------------------------------------------
|
|
|
|
mca_revert_all() {
|
|
local kind path detail backup
|
|
local -a keep=()
|
|
|
|
[[ -f $MCA_LEDGER ]] || return 0
|
|
|
|
# Backwards, so a file that was recorded twice ends up at its oldest state.
|
|
while IFS=$'\t' read -r kind path detail; do
|
|
[[ -n $path ]] || continue
|
|
case "$kind" in
|
|
shadow)
|
|
# Only remove what is still recognisably ours.
|
|
if [[ -f $path ]] && grep -q "^$MCA_MARK_SHADOW=" "$path" 2>/dev/null; then
|
|
rm -f -- "$path"
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
;;
|
|
inplace)
|
|
backup="$MCA_BACKUPDIR/$detail"
|
|
if [[ -n $detail ]] && [[ -f $backup || -L $backup ]]; then
|
|
if [[ -e $path ]] || [[ -d "$(dirname -- "$path")" ]]; then
|
|
# A link goes back as a link, not written through.
|
|
[[ -L $backup ]] && rm -f -- "$path"
|
|
cp -P -p -- "$backup" "$path" 2>/dev/null \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
rm -f -- "$backup"
|
|
elif [[ -f $path ]]; then
|
|
# No backup: the file did not exist before we wrote it.
|
|
rm -f -- "$path"
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
;;
|
|
flags)
|
|
mca_flags_revert "$path" "$detail" \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
;;
|
|
steam)
|
|
mca_steam_revert "$path" "$detail" \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
;;
|
|
kwin)
|
|
mca_kde_paste_revert "$path" "$detail" \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
;;
|
|
gecko)
|
|
mca_gecko_revert "$path" "$detail" \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
# Still open, so not done yet. See gecko.sh.
|
|
(( MCA_GECKO_PENDING )) && keep+=("$kind"$'\t'"$path"$'\t'"$detail")
|
|
;;
|
|
esac
|
|
done < <(tac "$MCA_LEDGER" 2>/dev/null)
|
|
|
|
rm -f -- "$MCA_LEDGER"
|
|
(( ${#keep[@]} )) && printf '%s\n' "${keep[@]}" > "$MCA_LEDGER"
|
|
rmdir "$MCA_BACKUPDIR" 2>/dev/null || true
|
|
return 0
|
|
}
|