aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/user/userctl
blob: 5ad31d1278be3165a260cf04765d88a11f6cd593 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/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,
# and ~/.user-icon.jpg, the avatar hyprlock, the SDDM theme and the rofi
# power menu read (udt-accent points the greeter at it).
# 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

AVATAR="$HOME/.user-icon.jpg"
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.
# Fails with the first line of stderr as its output, so get can report it.
prop() {
    local out
    out="$(busctl --system --json=short get-property "$ACC" "$OBJ" "$IFACE" "$1" 2>&1)" \
        || { head -n1 <<< "${out:-failed}"; return 1; }
    jq -r .data <<< "$out"
}

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)
        out=
        # Read everything first: one error line and exit 1, never blank fields.
        for p in RealName:name Email:email IconFile:avatar Language:language Location:location; do
            val="$(prop "${p%%:*}")" || { printf 'error=%s\n' "$val"; exit 1; }
            # The desktop's avatar wins; AccountsService's is the fallback.
            [[ "${p#*:}" == avatar && -e "$AVATAR" ]] && val="$AVATAR"
            out+="${p#*:}=$val"$'\n'
        done
        printf '%s' "$out"
        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 = ($2 !~ /^[rei]$/ && $12 ~ /S/) ? 1 : 0; next }
            $1 == "fpr" && want == 1 { fpr = $10; want = 2; next }
            $1 == "uid" && want == 2 && $2 !~ /^[re]$/ { 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)
                # Converted, so the .jpg name is true whatever was picked;
                # renamed into place, so no reader sees half a file.
                run magick "$v" -auto-orient -strip "$AVATAR.tmp.jpg"
                run mv -f "$AVATAR.tmp.jpg" "$AVATAR"
                acc SetIconFile "$AVATAR"
                ;;
            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-all user.signingkey
                fi
                ;;
            *) usage ;;
        esac
        ;;
    *) usage ;;
esac