From 7b529149206b383757e3362b7bc63c63315b73d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Sun, 1 Mar 2026 11:44:02 +0100 Subject: [PATCH] sync current state --- README.md | 6 + global-extension-installer/Makefile | 10 + global-extension-installer/README.md | 47 ++ .../scripts/apply-extension-defaults.sh | 461 ++++++++++++++++++ .../scripts/extensions.txt | 10 + .../tests/extensions-defaults-smoke.sh | 24 + gnome-user-switcher-extension/Makefile | 38 ++ gnome-user-switcher-extension/README.md | 49 ++ ...tcher@felixfoertsch.de.shell-extension.zip | Bin 0 -> 6560 bytes gnome-user-switcher-extension/extension.js | 286 +++++++++++ .../locale/de/LC_MESSAGES/user-switcher.po | 25 + .../locale/en/LC_MESSAGES/user-switcher.po | 25 + .../locale/es/LC_MESSAGES/user-switcher.po | 25 + .../locale/fr/LC_MESSAGES/user-switcher.po | 25 + gnome-user-switcher-extension/metadata.json | 11 + .../scripts/build-release.sh | 46 ++ .../tests/release-smoke.sh | 22 + gnome-user-switcher-extension/tests/smoke.sh | 27 + 18 files changed, 1137 insertions(+) create mode 100644 README.md create mode 100644 global-extension-installer/Makefile create mode 100644 global-extension-installer/README.md create mode 100755 global-extension-installer/scripts/apply-extension-defaults.sh create mode 100644 global-extension-installer/scripts/extensions.txt create mode 100755 global-extension-installer/tests/extensions-defaults-smoke.sh create mode 100644 gnome-user-switcher-extension/Makefile create mode 100644 gnome-user-switcher-extension/README.md create mode 100644 gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip create mode 100644 gnome-user-switcher-extension/extension.js create mode 100644 gnome-user-switcher-extension/locale/de/LC_MESSAGES/user-switcher.po create mode 100644 gnome-user-switcher-extension/locale/en/LC_MESSAGES/user-switcher.po create mode 100644 gnome-user-switcher-extension/locale/es/LC_MESSAGES/user-switcher.po create mode 100644 gnome-user-switcher-extension/locale/fr/LC_MESSAGES/user-switcher.po create mode 100644 gnome-user-switcher-extension/metadata.json create mode 100755 gnome-user-switcher-extension/scripts/build-release.sh create mode 100755 gnome-user-switcher-extension/tests/release-smoke.sh create mode 100755 gnome-user-switcher-extension/tests/smoke.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e5eec0 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Workspace Layout + +This repository now contains two separate projects: + +- `gnome-user-switcher-extension/`: GNOME Shell extension project (source, packaging, release zip). +- `global-extension-installer/`: System-wide installer project driven by `extensions.txt`. diff --git a/global-extension-installer/Makefile b/global-extension-installer/Makefile new file mode 100644 index 0000000..651f27f --- /dev/null +++ b/global-extension-installer/Makefile @@ -0,0 +1,10 @@ +.PHONY: test apply dry-run + +test: + bash tests/extensions-defaults-smoke.sh + +apply: + sudo bash scripts/apply-extension-defaults.sh + +dry-run: + bash scripts/apply-extension-defaults.sh --dry-run diff --git a/global-extension-installer/README.md b/global-extension-installer/README.md new file mode 100644 index 0000000..542aefd --- /dev/null +++ b/global-extension-installer/README.md @@ -0,0 +1,47 @@ +# Global Extension Installer + +Installs GNOME Shell extensions system-wide, sets default enabled extensions for new users, and applies the same list to existing users. + +## Source List + +Edit `scripts/extensions.txt`: + +```text + +``` + +Supported source values: +- local zip path +- zip URL +- git URL +- git URL with subdir (`...git#path/to/extension`) + +Example (`scripts/extensions.txt`): + +```text +user-switcher@felixfoertsch.de ../gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip +dash-to-panel@jderose9.github.com https://github.com/home-sweet-gnome/dash-to-panel.git +``` + +## Run + +```bash +sudo bash scripts/apply-extension-defaults.sh +``` + +Dry run: + +```bash +bash scripts/apply-extension-defaults.sh --dry-run +``` + +Override users: + +```bash +sudo bash scripts/apply-extension-defaults.sh --users kari,owen,romy +``` + +## Notes + +- System install target: `/usr/share/gnome-shell/extensions/` +- The script normalizes permissions after install (`root:root`, dirs `755`, files `644`). diff --git a/global-extension-installer/scripts/apply-extension-defaults.sh b/global-extension-installer/scripts/apply-extension-defaults.sh new file mode 100755 index 0000000..b688dc1 --- /dev/null +++ b/global-extension-installer/scripts/apply-extension-defaults.sh @@ -0,0 +1,461 @@ +#!/usr/bin/env bash +set -euo pipefail + +DEFAULT_USERS=(kari owen romy) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SYSTEM_EXT_DIR="/usr/share/gnome-shell/extensions" +DEFAULT_EXTENSIONS_FILE="${SCRIPT_DIR}/extensions.txt" + +DCONF_PROFILE_FILE="/etc/dconf/profile/user" +DCONF_DEFAULTS_FILE="/etc/dconf/db/local.d/00-gnome-extensions" +DCONF_LOCKS_FILE="/etc/dconf/db/local.d/locks/gnome-extensions" + +DRY_RUN=0 +LOCK_DEFAULT=0 +REFRESH_EXISTING=0 +USERS=("${DEFAULT_USERS[@]}") +EXTENSIONS_FILE="${DEFAULT_EXTENSIONS_FILE}" + +EXTENSION_UUIDS=() +EXTENSION_SOURCES=() +EXTENSIONS_BASE_DIR="" + +print_usage() { + cat < + +Supported source values: + - local zip path (relative or absolute) + - zip URL (https://...zip) + - git URL (https://...git) + - git URL with subdir (https://...git#path/to/extension) +USAGE +} + +run_cmd() { + if [[ $DRY_RUN -eq 1 ]]; then + printf '[dry-run] ' + printf '%q ' "$@" + echo + return 0 + fi + "$@" +} + +run_cmd_may_fail() { + if [[ $DRY_RUN -eq 1 ]]; then + printf '[dry-run] ' + printf '%q ' "$@" + echo + return 0 + fi + "$@" +} + +write_file() { + local path="$1" + local content="$2" + + if [[ $DRY_RUN -eq 1 ]]; then + echo "[dry-run] write ${path}" + echo "$content" + return 0 + fi + + mkdir -p "$(dirname "$path")" + printf '%s\n' "$content" > "$path" +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --extensions-file) + EXTENSIONS_FILE="$2" + shift 2 + ;; + --users) + IFS=',' read -r -a USERS <<< "$2" + shift 2 + ;; + --lock-default) + LOCK_DEFAULT=1 + shift + ;; + --refresh-existing) + REFRESH_EXISTING=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + print_usage + exit 1 + ;; + esac + done +} + +ensure_root() { + if [[ $DRY_RUN -eq 1 ]]; then + return 0 + fi + + if [[ ${EUID:-$(id -u)} -ne 0 ]]; then + echo "Run as root (sudo) unless using --dry-run." >&2 + exit 1 + fi +} + +extract_uuid() { + local metadata_file="$1" + sed -n 's/.*"uuid"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file" | head -n1 +} + +resolve_source_path() { + local source="$1" + if [[ "$source" = /* ]]; then + printf '%s' "$source" + else + printf '%s/%s' "$EXTENSIONS_BASE_DIR" "$source" + fi +} + +is_zip_source() { + local source="$1" + [[ "$source" == *.zip ]] || [[ "$source" == *.zip\?* ]] +} + +is_url_source() { + local source="$1" + [[ "$source" =~ ^https?:// ]] +} + +normalize_extension_permissions() { + local target_dir="$1" + run_cmd chown -R root:root "$target_dir" + run_cmd find "$target_dir" -type d -exec chmod 755 {} + + run_cmd find "$target_dir" -type f -exec chmod 644 {} + +} + +copy_dir_into_system_extensions() { + local source_dir="$1" + local uuid="$2" + local target_dir="${SYSTEM_EXT_DIR}/${uuid}" + + if [[ ! -f "$source_dir/metadata.json" ]]; then + echo "Invalid extension source for $uuid: metadata.json missing in $source_dir" >&2 + return 1 + fi + + if [[ "$(extract_uuid "$source_dir/metadata.json")" != "$uuid" ]]; then + echo "UUID mismatch for $uuid in $source_dir/metadata.json" >&2 + return 1 + fi + + run_cmd mkdir -p "$target_dir" + run_cmd cp -a "$source_dir/." "$target_dir/" + normalize_extension_permissions "$target_dir" + echo "System-wide extension ensured: $uuid" +} + +resolve_extension_dir_from_tree() { + local tree_root="$1" + local uuid="$2" + local subdir="$3" + local candidate + local metadata + + if [[ -n "$subdir" ]]; then + candidate="$tree_root/$subdir" + if [[ -f "$candidate/metadata.json" ]] && [[ "$(extract_uuid "$candidate/metadata.json")" == "$uuid" ]]; then + printf '%s' "$candidate" + return 0 + fi + echo "Subdir '$subdir' does not contain extension $uuid" >&2 + return 1 + fi + + if [[ -f "$tree_root/metadata.json" ]] && [[ "$(extract_uuid "$tree_root/metadata.json")" == "$uuid" ]]; then + printf '%s' "$tree_root" + return 0 + fi + + while IFS= read -r metadata; do + if [[ "$(extract_uuid "$metadata")" == "$uuid" ]]; then + printf '%s' "$(dirname "$metadata")" + return 0 + fi + done < <(find "$tree_root" -maxdepth 6 -type f -name metadata.json) + + echo "Could not locate extension $uuid in source tree" >&2 + return 1 +} + +install_from_zip_source() { + local uuid="$1" + local source="$2" + local work_dir + local zip_path + local extract_dir + local ext_dir + + if [[ $DRY_RUN -eq 1 ]]; then + if is_url_source "$source"; then + run_cmd curl -fsSL "$source" -o /tmp/extension.zip + run_cmd unzip -q /tmp/extension.zip -d /tmp/extension-unzip + else + zip_path="$(resolve_source_path "$source")" + run_cmd unzip -q "$zip_path" -d /tmp/extension-unzip + fi + echo "[dry-run] resolve extension directory for $uuid from zip" + run_cmd mkdir -p "${SYSTEM_EXT_DIR}/${uuid}" + run_cmd cp -a /tmp/extension-unzip/. "${SYSTEM_EXT_DIR}/${uuid}/" + normalize_extension_permissions "${SYSTEM_EXT_DIR}/${uuid}" + echo "System-wide extension ensured: $uuid" + return 0 + fi + + work_dir="$(mktemp -d)" + extract_dir="$work_dir/unzip" + mkdir -p "$extract_dir" + + if is_url_source "$source"; then + zip_path="$work_dir/source.zip" + curl -fsSL "$source" -o "$zip_path" + else + zip_path="$(resolve_source_path "$source")" + if [[ ! -f "$zip_path" ]]; then + echo "Missing zip source for $uuid: $zip_path" >&2 + rm -rf "$work_dir" + return 1 + fi + fi + + unzip -q "$zip_path" -d "$extract_dir" + ext_dir="$(resolve_extension_dir_from_tree "$extract_dir" "$uuid" "")" + copy_dir_into_system_extensions "$ext_dir" "$uuid" + rm -rf "$work_dir" +} + +install_from_git_source() { + local uuid="$1" + local source="$2" + local repo_url + local subdir="" + local work_dir + local repo_dir + local ext_dir + + repo_url="${source%%#*}" + if [[ "$source" == *"#"* ]]; then + subdir="${source#*#}" + fi + + if [[ $DRY_RUN -eq 1 ]]; then + run_cmd git clone --depth 1 "$repo_url" /tmp/extension-repo + echo "[dry-run] resolve extension directory for $uuid (subdir: ${subdir:-auto})" + run_cmd mkdir -p "${SYSTEM_EXT_DIR}/${uuid}" + run_cmd cp -a /tmp/extension-repo/. "${SYSTEM_EXT_DIR}/${uuid}/" + normalize_extension_permissions "${SYSTEM_EXT_DIR}/${uuid}" + echo "System-wide extension ensured: $uuid" + return 0 + fi + + work_dir="$(mktemp -d)" + repo_dir="$work_dir/repo" + git clone --depth 1 "$repo_url" "$repo_dir" >/dev/null 2>&1 + ext_dir="$(resolve_extension_dir_from_tree "$repo_dir" "$uuid" "$subdir")" + copy_dir_into_system_extensions "$ext_dir" "$uuid" + rm -rf "$work_dir" +} + +install_extension() { + local uuid="$1" + local source="$2" + local target_dir="${SYSTEM_EXT_DIR}/${uuid}" + + if [[ -f "$target_dir/metadata.json" && $REFRESH_EXISTING -eq 0 ]]; then + normalize_extension_permissions "$target_dir" + echo "System-wide extension already present: $uuid" + return 0 + fi + + if is_zip_source "$source"; then + install_from_zip_source "$uuid" "$source" + return + fi + + if is_url_source "$source"; then + install_from_git_source "$uuid" "$source" + return + fi + + echo "Unsupported source for $uuid: $source" >&2 + return 1 +} + +load_extensions_file() { + local file="$1" + local line + local lineno=0 + local uuid + local source + + if [[ ! -f "$file" ]]; then + echo "Missing extensions file: $file" >&2 + exit 1 + fi + + EXTENSIONS_BASE_DIR="$(cd "$(dirname "$file")" && pwd)" + + while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) + line="${line%%$'\r'}" + [[ -z "$line" ]] && continue + [[ "${line:0:1}" == "#" ]] && continue + + uuid="${line%%[[:space:]]*}" + source="${line#"$uuid"}" + source="$(echo "$source" | sed 's/^[[:space:]]*//')" + + if [[ -z "$uuid" || -z "$source" || "$uuid" == "$line" ]]; then + echo "Invalid line ${lineno} in ${file}: expected ' '" >&2 + exit 1 + fi + + EXTENSION_UUIDS+=("$uuid") + EXTENSION_SOURCES+=("$source") + done < "$file" + + if [[ ${#EXTENSION_UUIDS[@]} -eq 0 ]]; then + echo "No extensions loaded from ${file}" >&2 + exit 1 + fi + + echo "Loaded ${#EXTENSION_UUIDS[@]} extension entries from ${file}" +} + +install_system_wide_extensions() { + local i + local failures=0 + + for i in "${!EXTENSION_UUIDS[@]}"; do + if ! install_extension "${EXTENSION_UUIDS[$i]}" "${EXTENSION_SOURCES[$i]}"; then + echo "Failed to install ${EXTENSION_UUIDS[$i]} from ${EXTENSION_SOURCES[$i]}" >&2 + failures=$((failures + 1)) + fi + done + + if [[ $failures -gt 0 ]]; then + echo "System-wide install failed for ${failures} extension(s)." >&2 + return 1 + fi +} + +json_array_from_uuids() { + local out="[" + local first=1 + local uuid + + for uuid in "${EXTENSION_UUIDS[@]}"; do + if [[ $first -eq 0 ]]; then + out+=", " + fi + out+="'${uuid}'" + first=0 + done + + out+="]" + printf '%s' "$out" +} + +apply_dconf_defaults() { + local extensions_variant + extensions_variant="$(json_array_from_uuids)" + + if [[ -f "$DCONF_PROFILE_FILE" ]]; then + if [[ $DRY_RUN -eq 1 ]]; then + echo "[dry-run] ensure ${DCONF_PROFILE_FILE} contains system-db:local" + elif ! rg -q '^system-db:local$' "$DCONF_PROFILE_FILE"; then + printf '%s\n' 'system-db:local' >> "$DCONF_PROFILE_FILE" + fi + else + write_file "$DCONF_PROFILE_FILE" $'user-db:user\nsystem-db:local' + fi + + write_file "$DCONF_DEFAULTS_FILE" "[org/gnome/shell] +enabled-extensions=${extensions_variant} +disable-user-extensions=false" + + if [[ $LOCK_DEFAULT -eq 1 ]]; then + write_file "$DCONF_LOCKS_FILE" $'/org/gnome/shell/enabled-extensions\n/org/gnome/shell/disable-user-extensions' + fi + + run_cmd dconf update +} + +apply_user_settings() { + local extensions_variant + extensions_variant="$(json_array_from_uuids)" + local failures=0 + local user + local uuid + + for user in "${USERS[@]}"; do + if ! id "$user" >/dev/null 2>&1; then + echo "Skipping unknown user: $user" + continue + fi + + if run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- gsettings set org.gnome.shell enabled-extensions "$extensions_variant" \ + && run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- gsettings set org.gnome.shell disable-user-extensions false; then + echo "Applied extension list for user: $user" + else + echo "Primary gsettings path failed for user: $user, trying dconf fallback" + if run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- dconf write /org/gnome/shell/enabled-extensions "$extensions_variant" \ + && run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- dconf write /org/gnome/shell/disable-user-extensions false; then + echo "Applied extension list for user via dconf fallback: $user" + else + echo "Failed to apply extension list for user: $user" >&2 + failures=$((failures + 1)) + fi + fi + + for uuid in "${EXTENSION_UUIDS[@]}"; do + run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- gnome-extensions enable "$uuid" >/dev/null 2>&1 || true + done + done + + if [[ $failures -gt 0 ]]; then + echo "One or more user updates failed (${failures})." >&2 + return 1 + fi +} + +parse_args "$@" +ensure_root +load_extensions_file "$EXTENSIONS_FILE" +install_system_wide_extensions +apply_dconf_defaults +apply_user_settings + +echo "System-wide extension install complete, defaults configured for new users, settings applied to selected existing users." +echo "Users should log out and back in once to ensure GNOME Shell loads the new defaults cleanly." diff --git a/global-extension-installer/scripts/extensions.txt b/global-extension-installer/scripts/extensions.txt new file mode 100644 index 0000000..a94d974 --- /dev/null +++ b/global-extension-installer/scripts/extensions.txt @@ -0,0 +1,10 @@ +# uuid source +# source supports local zip paths, zip URLs, and git URLs (+ optional #subdir) + +user-switcher@felixfoertsch.de ../gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip +dash-to-panel@jderose9.github.com https://github.com/home-sweet-gnome/dash-to-panel.git +just-perfection-desktop@just-perfection https://gitlab.gnome.org/jrahmatzadeh/just-perfection.git +logomenu@aryan_k https://github.com/Aryan20/Logomenu.git +drive-menu@gnome-shell-extensions.gcampax.github.com https://gitlab.gnome.org/GNOME/gnome-shell-extensions.git#extensions/drive-menu +appindicatorsupport@rgcjonas.gmail.com https://github.com/ubuntu/gnome-shell-extension-appindicator.git +caffeine@patapon.info https://github.com/eonpatapon/gnome-shell-extension-caffeine.git diff --git a/global-extension-installer/tests/extensions-defaults-smoke.sh b/global-extension-installer/tests/extensions-defaults-smoke.sh new file mode 100755 index 0000000..317c127 --- /dev/null +++ b/global-extension-installer/tests/extensions-defaults-smoke.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +OUTPUT="$(bash scripts/apply-extension-defaults.sh --dry-run 2>&1)" + +echo "$OUTPUT" | rg -q "scripts/extensions.txt" +echo "$OUTPUT" | rg -q "user-switcher@felixfoertsch.de" +echo "$OUTPUT" | rg -q "/etc/dconf/db/local.d/00-gnome-extensions" +echo "$OUTPUT" | rg -q "user-switcher@felixfoertsch.de" +echo "$OUTPUT" | rg -q "dash-to-panel@jderose9.github.com" +echo "$OUTPUT" | rg -q "just-perfection-desktop@just-perfection" +echo "$OUTPUT" | rg -q "logomenu@aryan_k" +echo "$OUTPUT" | rg -q "drive-menu@gnome-shell-extensions.gcampax.github.com" +echo "$OUTPUT" | rg -q "appindicatorsupport@rgcjonas.gmail.com" +echo "$OUTPUT" | rg -q "caffeine@patapon.info" + +echo "$OUTPUT" | rg -q "Applied extension list for user: kari" +echo "$OUTPUT" | rg -q "Applied extension list for user: owen" +echo "$OUTPUT" | rg -q "Applied extension list for user: romy" + +echo "extension defaults smoke test passed" diff --git a/gnome-user-switcher-extension/Makefile b/gnome-user-switcher-extension/Makefile new file mode 100644 index 0000000..f378458 --- /dev/null +++ b/gnome-user-switcher-extension/Makefile @@ -0,0 +1,38 @@ +UUID := user-switcher@felixfoertsch.de +EXT_DIR := $(HOME)/.local/share/gnome-shell/extensions/$(UUID) + +.PHONY: install install-shell uninstall test package package-release check-release enable disable + +install: + mkdir -p "$(EXT_DIR)" + cp metadata.json extension.js "$(EXT_DIR)/" + if [ -d locale ]; then cp -r locale "$(EXT_DIR)/"; fi + @echo "Installed to $(EXT_DIR)" + +install-shell: package + gnome-extensions install --force "dist/$(UUID).shell-extension.zip" + @echo "Installed via gnome-extensions CLI" + +uninstall: + rm -rf "$(EXT_DIR)" + @echo "Removed $(EXT_DIR)" + +test: + bash tests/smoke.sh + bash tests/release-smoke.sh + +package: + mkdir -p dist + gnome-extensions pack --force --out-dir dist . + +package-release: + bash scripts/build-release.sh + +check-release: + bash tests/release-smoke.sh + +enable: + gnome-extensions enable "$(UUID)" + +disable: + gnome-extensions disable "$(UUID)" diff --git a/gnome-user-switcher-extension/README.md b/gnome-user-switcher-extension/README.md new file mode 100644 index 0000000..08395d4 --- /dev/null +++ b/gnome-user-switcher-extension/README.md @@ -0,0 +1,49 @@ +# GNOME User Switcher + +A GNOME Shell extension that adds a user switcher to the top bar. + +## Features + +- Shows the current username in the top bar. +- Opens a dropdown with all regular local users from `/etc/passwd`. +- Switches directly to the selected user if that user already has an active session (`loginctl activate`). +- Uses `dm-tool switch-to-user` when available. +- Falls back to the GNOME switch-user login screen. + +## Install (recommended) + +```bash +make install-shell +make enable +``` + +This path uses GNOME's extension installer and registers the extension in the running shell session. + +## Install (manual copy) + +```bash +make install +make enable +``` + +## Build For extensions.gnome.org + +Create a release bundle with compiled translations: + +```bash +make package-release +``` + +Validate release bundle contents: + +```bash +make check-release +``` + +Upload file: + +`dist/user-switcher@felixfoertsch.de.shell-extension.zip` + +## Notes + +- Direct switching to a user that is not logged in is display-manager dependent. On GDM, the reliable fallback is opening the login screen. diff --git a/gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip b/gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip new file mode 100644 index 0000000000000000000000000000000000000000..06e68bd9b3b6a3de1236fbf59474672f7ab5ccf6 GIT binary patch literal 6560 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 Z*?&J5Jp3j{0kDr-k|^h literal 0 HcmV?d00001 diff --git a/gnome-user-switcher-extension/extension.js b/gnome-user-switcher-extension/extension.js new file mode 100644 index 0000000..fab0f87 --- /dev/null +++ b/gnome-user-switcher-extension/extension.js @@ -0,0 +1,286 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; +import Clutter from 'gi://Clutter'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; + +const MIN_NORMAL_UID = 1000; +const MAX_NORMAL_UID = 60000; + +const UserSwitcherIndicator = GObject.registerClass( +class UserSwitcherIndicator extends PanelMenu.Button { + _init(extension) { + super._init(0.0, 'User Switcher'); + + this._extension = extension; + this._ = extension.gettext.bind(extension); + this._currentUser = GLib.get_user_name(); + + const panelBox = new St.BoxLayout({ + style_class: 'panel-status-menu-box', + y_align: Clutter.ActorAlign.CENTER, + }); + + const icon = new St.Icon({ + icon_name: 'avatar-default-symbolic', + style_class: 'system-status-icon', + }); + panelBox.add_child(icon); + + const label = new St.Label({ + text: this._currentUser, + y_align: Clutter.ActorAlign.CENTER, + }); + panelBox.add_child(label); + + this.add_child(panelBox); + this._buildMenu(); + } + + _buildMenu() { + this.menu.removeAll(); + + const title = new PopupMenu.PopupMenuItem( + this._('Switch User'), + {reactive: false, can_focus: false} + ); + title.label.add_style_class_name('popup-menu-item-title'); + this.menu.addMenuItem(title); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + for (const user of this._getLocalUsers()) { + const item = new PopupMenu.PopupMenuItem(user.displayName); + if (user.username === this._currentUser) + item.setOrnament(PopupMenu.Ornament.DOT); + + item.connect('activate', () => { + void this._switchToUser(user.username); + }); + this.menu.addMenuItem(item); + } + + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + const loginScreenItem = new PopupMenu.PopupMenuItem(this._('Open Login Screen')); + loginScreenItem.connect('activate', () => this._openLoginScreen()); + this.menu.addMenuItem(loginScreenItem); + + const logoutItem = new PopupMenu.PopupMenuItem(this._('Log Out')); + logoutItem.connect('activate', () => this._logOutCurrentUser()); + this.menu.addMenuItem(logoutItem); + } + + _getLocalUsers() { + const users = []; + let contents; + + try { + [, contents] = GLib.file_get_contents('/etc/passwd'); + } catch (error) { + log(`User Switcher: failed to read /etc/passwd: ${error}`); + return users; + } + + const lines = new TextDecoder().decode(contents).split('\n'); + for (const line of lines) { + if (!line || line.startsWith('#')) + continue; + + const fields = line.split(':'); + if (fields.length < 7) + continue; + + const username = fields[0]; + const uid = Number.parseInt(fields[2], 10); + const gecos = fields[4] ?? ''; + const shell = fields[6] ?? ''; + + if (Number.isNaN(uid)) + continue; + if (uid < MIN_NORMAL_UID || uid > MAX_NORMAL_UID) + continue; + if (shell.endsWith('/nologin') || shell.endsWith('/false')) + continue; + + const fullName = gecos.split(',')[0].trim(); + const displayName = fullName ? `${fullName} (${username})` : username; + users.push({username, displayName}); + } + + users.sort((a, b) => a.username.localeCompare(b.username)); + return users; + } + + async _switchToUser(username) { + if (username === this._currentUser) + return; + + if (await this._activateUserSession(username)) + return; + + if (await this._runSuccessfulCommand(['dm-tool', 'switch-to-user', username])) + return; + + Main.notify( + this._('User Switcher'), + this._('Direct switch is unavailable. Opening login screen instead.') + ); + this._openLoginScreen(); + } + + _openLoginScreen() { + const commands = [ + ['gdmflexiserver'], + ['gnome-session-quit', '--switch-user', '--no-prompt'], + ]; + + for (const argv of commands) { + if (this._spawnIfAvailable(argv)) + return; + } + + Main.notify( + this._('User Switcher'), + this._('No supported switch command was found on this system.') + ); + } + + _logOutCurrentUser() { + if (this._spawnIfAvailable(['gnome-session-quit', '--logout', '--no-prompt'])) + return; + + Main.notify( + this._('User Switcher'), + this._('No supported logout command was found on this system.') + ); + } + + async _runSuccessfulCommand(argv) { + if (!argv.length) + return false; + + if (!GLib.find_program_in_path(argv[0])) + return false; + + try { + const process = Gio.Subprocess.new( + argv, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + ); + const result = await this._communicateUtf8(process); + return result.ok && process.get_successful(); + } catch (error) { + log(`User Switcher: failed to start ${argv[0]}: ${error}`); + return false; + } + } + + async _activateUserSession(username) { + const sessionId = await this._getUserSessionId(username); + if (!sessionId) + return false; + + return this._runSuccessfulCommand(['loginctl', 'activate', sessionId]); + } + + async _getUserSessionId(username) { + const sessionsOutput = await this._runCommandGetStdout([ + 'loginctl', + 'list-sessions', + '--no-legend', + ]); + if (!sessionsOutput) + return null; + + for (const line of sessionsOutput.split('\n')) { + const fields = line.trim().split(/\s+/); + if (fields.length < 3) + continue; + + const sessionId = fields[0]; + const sessionUser = fields[2]; + if (sessionUser === username) + return sessionId; + } + + return null; + } + + async _runCommandGetStdout(argv) { + if (!argv.length) + return null; + + if (!GLib.find_program_in_path(argv[0])) + return null; + + try { + const process = Gio.Subprocess.new( + argv, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + ); + const result = await this._communicateUtf8(process); + if (!result.ok || !process.get_successful()) + return null; + + const value = result.stdout.trim(); + return value.length > 0 ? value : null; + } catch (error) { + log(`User Switcher: failed to run ${argv[0]}: ${error}`); + return null; + } + } + + _communicateUtf8(process) { + return new Promise(resolve => { + process.communicate_utf8_async(null, null, (proc, res) => { + try { + const [ok, stdout] = proc.communicate_utf8_finish(res); + resolve({ + ok, + stdout: stdout ?? '', + }); + } catch (error) { + log(`User Switcher: command communication failed: ${error}`); + resolve({ + ok: false, + stdout: '', + }); + } + }); + }); + } + + _spawnIfAvailable(argv) { + if (!argv.length) + return false; + + if (!GLib.find_program_in_path(argv[0])) + return false; + + try { + Gio.Subprocess.new(argv, Gio.SubprocessFlags.NONE); + return true; + } catch (error) { + log(`User Switcher: failed to spawn ${argv[0]}: ${error}`); + return false; + } + } +}); + +export default class UserSwitcherExtension extends Extension { + enable() { + this.initTranslations(); + this._indicator = new UserSwitcherIndicator(this); + Main.panel.addToStatusArea(this.uuid, this._indicator, 1, 'right'); + } + + disable() { + this._indicator?.destroy(); + this._indicator = null; + } +} diff --git a/gnome-user-switcher-extension/locale/de/LC_MESSAGES/user-switcher.po b/gnome-user-switcher-extension/locale/de/LC_MESSAGES/user-switcher.po new file mode 100644 index 0000000..dbbe41f --- /dev/null +++ b/gnome-user-switcher-extension/locale/de/LC_MESSAGES/user-switcher.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Language: de\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "Switch User" +msgstr "Benutzer wechseln" + +msgid "Open Login Screen" +msgstr "Anmeldebildschirm öffnen" + +msgid "User Switcher" +msgstr "Benutzerwechsler" + +msgid "Direct switch is unavailable. Opening login screen instead." +msgstr "Direktes Wechseln ist nicht verfügbar. Stattdessen wird der Anmeldebildschirm geöffnet." + +msgid "No supported switch command was found on this system." +msgstr "Auf diesem System wurde kein unterstützter Befehl zum Benutzerwechsel gefunden." + +msgid "Log Out" +msgstr "Abmelden" + +msgid "No supported logout command was found on this system." +msgstr "Auf diesem System wurde kein unterstützter Befehl zum Abmelden gefunden." diff --git a/gnome-user-switcher-extension/locale/en/LC_MESSAGES/user-switcher.po b/gnome-user-switcher-extension/locale/en/LC_MESSAGES/user-switcher.po new file mode 100644 index 0000000..abb3ed9 --- /dev/null +++ b/gnome-user-switcher-extension/locale/en/LC_MESSAGES/user-switcher.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Language: en\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "Switch User" +msgstr "Switch User" + +msgid "Open Login Screen" +msgstr "Open Login Screen" + +msgid "User Switcher" +msgstr "User Switcher" + +msgid "Direct switch is unavailable. Opening login screen instead." +msgstr "Direct switch is unavailable. Opening login screen instead." + +msgid "No supported switch command was found on this system." +msgstr "No supported switch command was found on this system." + +msgid "Log Out" +msgstr "Log Out" + +msgid "No supported logout command was found on this system." +msgstr "No supported logout command was found on this system." diff --git a/gnome-user-switcher-extension/locale/es/LC_MESSAGES/user-switcher.po b/gnome-user-switcher-extension/locale/es/LC_MESSAGES/user-switcher.po new file mode 100644 index 0000000..2b05c5b --- /dev/null +++ b/gnome-user-switcher-extension/locale/es/LC_MESSAGES/user-switcher.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Language: es\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "Switch User" +msgstr "Cambiar usuario" + +msgid "Open Login Screen" +msgstr "Abrir pantalla de inicio de sesión" + +msgid "User Switcher" +msgstr "Selector de usuario" + +msgid "Direct switch is unavailable. Opening login screen instead." +msgstr "El cambio directo no está disponible. Se abrirá la pantalla de inicio de sesión." + +msgid "No supported switch command was found on this system." +msgstr "No se encontró un comando compatible para cambiar de usuario en este sistema." + +msgid "Log Out" +msgstr "Cerrar sesión" + +msgid "No supported logout command was found on this system." +msgstr "No se encontró un comando compatible para cerrar sesión en este sistema." diff --git a/gnome-user-switcher-extension/locale/fr/LC_MESSAGES/user-switcher.po b/gnome-user-switcher-extension/locale/fr/LC_MESSAGES/user-switcher.po new file mode 100644 index 0000000..14293e3 --- /dev/null +++ b/gnome-user-switcher-extension/locale/fr/LC_MESSAGES/user-switcher.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Language: fr\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "Switch User" +msgstr "Changer d'utilisateur" + +msgid "Open Login Screen" +msgstr "Ouvrir l'écran de connexion" + +msgid "User Switcher" +msgstr "Sélecteur d'utilisateur" + +msgid "Direct switch is unavailable. Opening login screen instead." +msgstr "Le changement direct n'est pas disponible. L'écran de connexion va s'ouvrir." + +msgid "No supported switch command was found on this system." +msgstr "Aucune commande de changement d'utilisateur prise en charge n'a été trouvée sur ce système." + +msgid "Log Out" +msgstr "Se déconnecter" + +msgid "No supported logout command was found on this system." +msgstr "Aucune commande de déconnexion prise en charge n'a été trouvée sur ce système." diff --git a/gnome-user-switcher-extension/metadata.json b/gnome-user-switcher-extension/metadata.json new file mode 100644 index 0000000..0b80cc4 --- /dev/null +++ b/gnome-user-switcher-extension/metadata.json @@ -0,0 +1,11 @@ +{ + "uuid": "user-switcher@felixfoertsch.de", + "name": "User Switcher", + "description": "Adds a top bar user menu for fast user switching.", + "version": 1, + "shell-version": [ + "49" + ], + "gettext-domain": "user-switcher", + "url": "https://github.com/felixfoertsch/gnome-user-switcher" +} diff --git a/gnome-user-switcher-extension/scripts/build-release.sh b/gnome-user-switcher-extension/scripts/build-release.sh new file mode 100755 index 0000000..01a4eb9 --- /dev/null +++ b/gnome-user-switcher-extension/scripts/build-release.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UUID="user-switcher@felixfoertsch.de" +STAGE_DIR="${ROOT_DIR}/.build/release/${UUID}" +DIST_DIR="${ROOT_DIR}/dist" +ZIP_PATH="${DIST_DIR}/${UUID}.shell-extension.zip" + +if ! command -v msgfmt >/dev/null 2>&1; then + echo "msgfmt is required to build release translations (install gettext)." >&2 + exit 1 +fi + +if ! command -v zip >/dev/null 2>&1; then + echo "zip is required to create the release archive." >&2 + exit 1 +fi + +rm -rf "$STAGE_DIR" +mkdir -p "$STAGE_DIR" +mkdir -p "$DIST_DIR" + +cp "$ROOT_DIR/metadata.json" "$ROOT_DIR/extension.js" "$STAGE_DIR/" + +for lang in en de es fr; do + po_path="${ROOT_DIR}/locale/${lang}/LC_MESSAGES/user-switcher.po" + mo_dir="${STAGE_DIR}/locale/${lang}/LC_MESSAGES" + mo_path="${mo_dir}/user-switcher.mo" + + if [[ ! -f "$po_path" ]]; then + echo "missing translation source: $po_path" >&2 + exit 1 + fi + + mkdir -p "$mo_dir" + msgfmt "$po_path" -o "$mo_path" +done + +rm -f "$ZIP_PATH" +( + cd "$STAGE_DIR" + zip -qr "$ZIP_PATH" metadata.json extension.js locale +) + +echo "Release package created: $ZIP_PATH" diff --git a/gnome-user-switcher-extension/tests/release-smoke.sh b/gnome-user-switcher-extension/tests/release-smoke.sh new file mode 100755 index 0000000..6eed345 --- /dev/null +++ b/gnome-user-switcher-extension/tests/release-smoke.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UUID="user-switcher@felixfoertsch.de" +ZIP_PATH="${ROOT_DIR}/dist/${UUID}.shell-extension.zip" + +cd "$ROOT_DIR" + +make package-release >/dev/null + +if [[ ! -f "$ZIP_PATH" ]]; then + echo "missing release zip: $ZIP_PATH" >&2 + exit 1 +fi + +ZIP_LIST="$(unzip -l "$ZIP_PATH")" +echo "$ZIP_LIST" | rg -q "metadata.json" +echo "$ZIP_LIST" | rg -q "extension.js" +echo "$ZIP_LIST" | rg -q "locale/.*/LC_MESSAGES/user-switcher.mo" + +echo "release smoke test passed" diff --git a/gnome-user-switcher-extension/tests/smoke.sh b/gnome-user-switcher-extension/tests/smoke.sh new file mode 100755 index 0000000..9a362a3 --- /dev/null +++ b/gnome-user-switcher-extension/tests/smoke.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +cd "$ROOT_DIR" + +for required in metadata.json extension.js; do + if [[ ! -f "$required" ]]; then + echo "missing required file: $required" >&2 + exit 1 + fi +done + +if ! command -v gnome-extensions >/dev/null 2>&1; then + echo "gnome-extensions CLI not found; skipped shell validation" + exit 0 +fi + +gnome-extensions pack --force --out-dir /tmp . >/dev/null + +if rg -n "communicate_utf8\\(null, null\\)" extension.js >/dev/null; then + echo "found blocking subprocess call in extension.js" >&2 + exit 1 +fi + +echo "smoke test passed"