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; } }