aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
Diffstat (limited to 'desktop')
-rwxr-xr-xdesktop/modules/user/test-userctl.sh143
-rwxr-xr-xdesktop/modules/user/userctl115
2 files changed, 258 insertions, 0 deletions
diff --git a/desktop/modules/user/test-userctl.sh b/desktop/modules/user/test-userctl.sh
new file mode 100755
index 0000000..cc90550
--- /dev/null
+++ b/desktop/modules/user/test-userctl.sh
@@ -0,0 +1,143 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# The one runnable check for userctl. busctl, kwriteconfig6, git and gpg are
+# stubs on PATH that log their arguments, so nothing here reaches the real
+# AccountsService, emaildefaults or git config. jq is the real one.
+#
+# Usage: ./test-userctl.sh (exit 0 = all passed)
+
+set -u
+
+here="$(cd "$(dirname "$0")" && pwd)"
+ctl="$here/userctl"
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+
+mkdir "$tmp/bin"
+cat > "$tmp/bin/stub" <<'EOF'
+#!/bin/bash
+n="${0##*/}"
+{ printf '%s' "$n"; printf ' [%s]' "$@"; echo; } >> "$LOG"
+[[ "${FAIL:-}" == "$n" ]] && { echo boom >&2; exit 1; }
+case "$n" in
+ busctl) [[ " $* " == *" get-property "* ]] && printf '{"type":"s","data":"%s"}\n' "${@: -1}" ;;
+ git) [[ " $* " == *" --get "* ]] && { [[ -n "${GIT_VALUE:-}" ]] && echo "$GIT_VALUE" || exit 1; } ;;
+ gpg) cat "$GPG_FIXTURE" ;;
+esac
+exit 0
+EOF
+chmod +x "$tmp/bin/stub"
+for n in busctl kwriteconfig6 git gpg; do ln -s stub "$tmp/bin/$n"; done
+export PATH="$tmp/bin:$PATH" HOME="$tmp" LOG="$tmp/log"
+
+obj="/org/freedesktop/Accounts/User$(id -u)"
+acc="busctl [--system] [call] [org.freedesktop.Accounts] [$obj] [org.freedesktop.Accounts.User]"
+
+pass=0
+fail=0
+check() {
+ local label="$1" want="$2" got="$3"
+ if [[ "$want" == "$got" ]]; then
+ printf 'ok %s\n' "$label"; pass=$((pass + 1))
+ else
+ printf 'FAIL %s:\n want %q\n got %q\n' "$label" "$want" "$got"; fail=$((fail + 1))
+ fi
+}
+reset() { : > "$LOG"; unset FAIL GIT_VALUE; }
+
+# get: one line per field, AccountsService through --json, the key from git.
+reset
+export GIT_VALUE=ABCDEF
+check "get" "name=RealName
+email=Email
+avatar=IconFile
+language=Language
+location=Location
+signingkey=ABCDEF" "$("$ctl" get)"
+
+# keys: the primary fingerprint and first uid of each secret key; subkey
+# fingerprints and later uids are skipped.
+reset
+cat > "$tmp/gpg.txt" <<'EOF'
+sec:u:255:22:AAAA1111:1700000000:::u:::scESC:::+:::23::0:
+fpr:::::::::1111222233334444555566667777AAAA1111:
+grp:::::::::0000:
+uid:u::::1700000000::HASH::Jane Doèr <jane@example.org>::::::::::0:
+uid:u::::1700000000::HASH::Jane D. <jd@example.org>::::::::::0:
+ssb:u:255:18:SUB1:1700000000::::::e:::+:::23:
+fpr:::::::::9999888877776666555544443333SUB1:
+sec:u:255:22:BBBB2222:1700000000:::u:::scESC:::#:::23::0:
+fpr:::::::::AAAABBBBCCCCDDDDEEEEFFFF0000BBBB2222:
+uid:u::::1700000000::HASH::Work Key <work@example.org>::::::::::0:
+EOF
+export GPG_FIXTURE="$tmp/gpg.txt"
+check "keys" "1111222233334444555566667777AAAA1111 Jane Doèr <jane@example.org>
+AAAABBBBCCCCDDDDEEEEFFFF0000BBBB2222 Work Key <work@example.org>" "$("$ctl" keys)"
+
+# set name: AccountsService, emaildefaults, git with the surname shortened.
+reset
+out="$("$ctl" set name "Jane Doèr")"
+check "set name prints nothing" "" "$out"
+check "set name writes" "$acc [SetRealName] [s] [Jane Doèr]
+kwriteconfig6 [--file] [emaildefaults] [--group] [PROFILE_Default] [--key] [FullName] [Jane Doèr]
+git [config] [--global] [user.name] [Jane D.]" "$(cat "$LOG")"
+
+gitname() { reset; "$ctl" set name "$1" >/dev/null; grep '^git' "$LOG"; }
+check "three words" "git [config] [--global] [user.name] [Anna Maria R.]" "$(gitname "Anna Maria Rossi")"
+check "one word" "git [config] [--global] [user.name] [Cher]" "$(gitname "Cher")"
+check "accented initial" "git [config] [--global] [user.name] [Ève À.]" "$(gitname "Ève Àlba")"
+check "extra spaces" "git [config] [--global] [user.name] [Jane D.]" "$(gitname " Jane Doèr ")"
+
+# set email: the same three stores.
+reset
+"$ctl" set email jane@example.org >/dev/null
+check "set email writes" "$acc [SetEmail] [s] [jane@example.org]
+kwriteconfig6 [--file] [emaildefaults] [--group] [PROFILE_Default] [--key] [EmailAddress] [jane@example.org]
+git [config] [--global] [user.email] [jane@example.org]" "$(cat "$LOG")"
+
+# AccountsService only.
+for pair in avatar:SetIconFile language:SetLanguage location:SetLocation; do
+ reset
+ "$ctl" set "${pair%%:*}" v >/dev/null
+ check "set ${pair%%:*} writes" "$acc [${pair#*:}] [s] [v]" "$(cat "$LOG")"
+done
+
+# signingkey: a fingerprint is written; empty unsets, but only if one is set.
+reset
+"$ctl" set signingkey ABCD >/dev/null
+check "set signingkey" "git [config] [--global] [user.signingkey] [ABCD]" "$(cat "$LOG")"
+reset
+export GIT_VALUE=OLD
+"$ctl" set signingkey "" >/dev/null
+check "unset signingkey" "git [config] [--global] [--get] [user.signingkey]
+git [config] [--global] [--unset] [user.signingkey]" "$(cat "$LOG")"
+reset
+"$ctl" set signingkey "" >/dev/null
+check "unset when none is set" "git [config] [--global] [--get] [user.signingkey]" "$(cat "$LOG")"
+
+# A failing write stops there, says why on stdout, and exits 1.
+reset
+export FAIL=busctl
+out="$("$ctl" set name "Jane Doèr")"; rc=$?
+check "failure exit" "1" "$rc"
+check "failure message" "busctl: boom" "$out"
+check "failure stops" "1" "$(wc -l < "$LOG" | tr -d ' ')"
+
+# An unknown field is refused.
+reset
+"$ctl" set shoesize 42 >/dev/null 2>&1; rc=$?
+check "unknown field" "2" "$rc"
+
+printf '\n%d passed, %d failed\n' "$pass" "$fail"
+[[ $fail -eq 0 ]]
diff --git a/desktop/modules/user/userctl b/desktop/modules/user/userctl
new file mode 100755
index 0000000..07d3a63
--- /dev/null
+++ b/desktop/modules/user/userctl
@@ -0,0 +1,115 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# The user's details, read and written where other programs look for them.
+#
+# userctl get key=value: name email avatar language location signingkey
+# userctl keys fingerprint<TAB>uid per gpg secret key
+# userctl set <key> <val> write one field to every store holding it
+#
+# Stores: AccountsService (and through SetRealName the passwd GECOS that
+# LibreOffice reads), KDE's ~/.config/emaildefaults, the global git config.
+# Polkit lets a user change their own AccountsService entry without a prompt.
+#
+# set prints nothing on success. On a failing write it prints the command and
+# its error on stdout, for the drawer page, and exits 1 without trying the
+# remaining stores. A new field is one case below and one entry in the
+# page's field list.
+
+set -u
+# Character-wise ${x:0:1}, so an accented initial is not cut in half.
+export LC_ALL=C.UTF-8
+
+ACC=org.freedesktop.Accounts
+OBJ="/org/freedesktop/Accounts/User$(id -u)"
+IFACE=org.freedesktop.Accounts.User
+
+usage() {
+ printf 'usage: %s get | keys | set <name|email|avatar|language|location|signingkey> <value>\n' \
+ "${0##*/}" >&2
+ exit 2
+}
+
+run() {
+ local err
+ if ! err="$("$@" 2>&1 >/dev/null)"; then
+ printf '%s: %s\n' "$1" "${err:-failed}"
+ exit 1
+ fi
+}
+
+# --json, because plain get-property prints non-ASCII as octal escapes.
+prop() {
+ busctl --system --json=short get-property "$ACC" "$OBJ" "$IFACE" "$1" | jq -r .data
+}
+
+acc() { run busctl --system call "$ACC" "$OBJ" "$IFACE" "$1" s "$2"; }
+kde() { run kwriteconfig6 --file emaildefaults --group PROFILE_Default --key "$1" "$2"; }
+
+# The surname as an initial, for commits: "Jane Doèr" -> "Jane D.".
+gitname() {
+ local -a w
+ read -ra w <<< "$1"
+ local n=${#w[@]}
+ if (( n < 2 )); then printf '%s' "${w[*]}"; return; fi
+ printf '%s %s.' "${w[*]:0:n-1}" "${w[n-1]:0:1}"
+}
+
+[[ $# -ge 1 ]] || usage
+
+case "$1" in
+ get)
+ printf 'name=%s\n' "$(prop RealName)"
+ printf 'email=%s\n' "$(prop Email)"
+ printf 'avatar=%s\n' "$(prop IconFile)"
+ printf 'language=%s\n' "$(prop Language)"
+ printf 'location=%s\n' "$(prop Location)"
+ printf 'signingkey=%s\n' "$(git config --global --get user.signingkey)"
+ ;;
+ keys)
+ # The first fpr after a sec is the primary key's; the first uid
+ # after that names it. ssb fingerprints come later and are skipped.
+ gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: '
+ $1 == "sec" { want = 1; next }
+ $1 == "fpr" && want == 1 { fpr = $10; want = 2; next }
+ $1 == "uid" && want == 2 { print fpr "\t" $10; want = 0 }'
+ ;;
+ set)
+ [[ $# -eq 3 ]] || usage
+ v="$3"
+ case "$2" in
+ name)
+ acc SetRealName "$v"
+ kde FullName "$v"
+ run git config --global user.name "$(gitname "$v")"
+ ;;
+ email)
+ acc SetEmail "$v"
+ kde EmailAddress "$v"
+ run git config --global user.email "$v"
+ ;;
+ avatar) acc SetIconFile "$v" ;;
+ language) acc SetLanguage "$v" ;;
+ location) acc SetLocation "$v" ;;
+ signingkey)
+ if [[ -n "$v" ]]; then
+ run git config --global user.signingkey "$v"
+ elif git config --global --get user.signingkey >/dev/null; then
+ run git config --global --unset user.signingkey
+ fi
+ ;;
+ *) usage ;;
+ esac
+ ;;
+ *) usage ;;
+esac