sync current state
This commit is contained in:
6
README.md
Normal file
6
README.md
Normal file
@@ -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`.
|
||||
10
global-extension-installer/Makefile
Normal file
10
global-extension-installer/Makefile
Normal file
@@ -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
|
||||
47
global-extension-installer/README.md
Normal file
47
global-extension-installer/README.md
Normal file
@@ -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
|
||||
<uuid> <source>
|
||||
```
|
||||
|
||||
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`).
|
||||
461
global-extension-installer/scripts/apply-extension-defaults.sh
Executable file
461
global-extension-installer/scripts/apply-extension-defaults.sh
Executable file
@@ -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 <<USAGE
|
||||
Usage: sudo bash scripts/apply-extension-defaults.sh [options]
|
||||
|
||||
Options:
|
||||
--extensions-file PATH Extensions list file (default: scripts/extensions.txt)
|
||||
--users user1,user2 Override target users (default: kari,owen,romy)
|
||||
--lock-default Lock dconf defaults
|
||||
--refresh-existing Reinstall even if extension dir already exists
|
||||
--dry-run Print actions without writing changes
|
||||
-h, --help Show help
|
||||
|
||||
extensions.txt format:
|
||||
<uuid> <source>
|
||||
|
||||
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 '<uuid> <source>'" >&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."
|
||||
10
global-extension-installer/scripts/extensions.txt
Normal file
10
global-extension-installer/scripts/extensions.txt
Normal file
@@ -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
|
||||
24
global-extension-installer/tests/extensions-defaults-smoke.sh
Executable file
24
global-extension-installer/tests/extensions-defaults-smoke.sh
Executable file
@@ -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"
|
||||
38
gnome-user-switcher-extension/Makefile
Normal file
38
gnome-user-switcher-extension/Makefile
Normal file
@@ -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)"
|
||||
49
gnome-user-switcher-extension/README.md
Normal file
49
gnome-user-switcher-extension/README.md
Normal file
@@ -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.
|
||||
BIN
gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip
vendored
Normal file
BIN
gnome-user-switcher-extension/dist/user-switcher@felixfoertsch.de.shell-extension.zip
vendored
Normal file
Binary file not shown.
286
gnome-user-switcher-extension/extension.js
Normal file
286
gnome-user-switcher-extension/extension.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
11
gnome-user-switcher-extension/metadata.json
Normal file
11
gnome-user-switcher-extension/metadata.json
Normal file
@@ -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"
|
||||
}
|
||||
46
gnome-user-switcher-extension/scripts/build-release.sh
Executable file
46
gnome-user-switcher-extension/scripts/build-release.sh
Executable file
@@ -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"
|
||||
22
gnome-user-switcher-extension/tests/release-smoke.sh
Executable file
22
gnome-user-switcher-extension/tests/release-smoke.sh
Executable file
@@ -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"
|
||||
27
gnome-user-switcher-extension/tests/smoke.sh
Executable file
27
gnome-user-switcher-extension/tests/smoke.sh
Executable file
@@ -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"
|
||||
Reference in New Issue
Block a user