2023-12-18 20:20:26 +01:00
import Gio from 'gi://Gio' ;
import GLib from 'gi://GLib' ;
import GObject from 'gi://GObject' ;
import {
ExtensionState , ExtensionType , deserializeExtension
} from './misc/extensionUtils.js' ;
const GnomeShellIface = loadInterfaceXML ( 'org.gnome.Shell.Extensions' ) ;
const GnomeShellProxy = Gio . DBusProxy . makeProxyWrapper ( GnomeShellIface ) ;
let shellVersion ;
function loadInterfaceXML ( iface ) {
const uri = ` resource:///org/gnome/Extensions/dbus-interfaces/ ${ iface } .xml ` ;
const f = Gio . File . new _for _uri ( uri ) ;
try {
let [ ok _ , bytes ] = f . load _contents ( null ) ;
return new TextDecoder ( ) . decode ( bytes ) ;
} catch ( e ) {
console . error ( ` Failed to load D-Bus interface ${ iface } ` ) ;
}
return null ;
}
2023-12-19 18:45:04 +01:00
const Extension = GObject . registerClass ( {
GTypeName : 'Extension' ,
Properties : {
'uuid' : GObject . ParamSpec . string (
'uuid' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'name' : GObject . ParamSpec . string (
'name' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'description' : GObject . ParamSpec . string (
'description' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'state' : GObject . ParamSpec . int (
'state' , null , null ,
GObject . ParamFlags . READABLE ,
1 , 99 , ExtensionState . INITIALIZED ) ,
2023-09-12 14:25:03 +02:00
'enabled' : GObject . ParamSpec . boolean (
'enabled' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
2023-12-19 18:45:04 +01:00
'creator' : GObject . ParamSpec . string (
'creator' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'url' : GObject . ParamSpec . string (
'url' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'version' : GObject . ParamSpec . string (
'version' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'error' : GObject . ParamSpec . string (
'error' , null , null ,
GObject . ParamFlags . READABLE ,
'' ) ,
'has-error' : GObject . ParamSpec . boolean (
'has-error' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
'has-prefs' : GObject . ParamSpec . boolean (
'has-prefs' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
'has-update' : GObject . ParamSpec . boolean (
'has-update' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
'has-version' : GObject . ParamSpec . boolean (
'has-version' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
'can-change' : GObject . ParamSpec . boolean (
'can-change' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
'is-user' : GObject . ParamSpec . boolean (
'is-user' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
} ,
} , class Extension extends GObject . Object {
2023-12-18 20:20:26 +01:00
constructor ( variant ) {
2023-12-19 18:45:04 +01:00
super ( ) ;
2023-12-18 20:20:26 +01:00
this . update ( variant ) ;
}
update ( variant ) {
const deserialized = deserializeExtension ( variant ) ;
const {
2023-09-12 14:25:03 +02:00
uuid , type , state , enabled , error , hasPrefs , hasUpdate , canChange , metadata ,
2023-12-18 20:20:26 +01:00
} = deserialized ;
if ( ! this . _uuid )
this . _uuid = uuid ;
if ( this . _uuid !== uuid )
throw new Error ( ` Invalid update of extension ${ this . _uuid } with data from ${ uuid } ` ) ;
2023-12-19 18:45:04 +01:00
this . freeze _notify ( ) ;
2023-12-18 20:20:26 +01:00
const { name } = metadata ;
2023-12-19 18:45:04 +01:00
if ( this . _name !== name ) {
this . _name = name ;
this . notify ( 'name' ) ;
}
2023-12-18 20:20:26 +01:00
const [ desc ] = metadata . description . split ( '\n' ) ;
2023-12-19 18:45:04 +01:00
if ( this . _description !== desc ) {
this . _description = desc ;
this . notify ( 'description' ) ;
}
2023-12-18 20:20:26 +01:00
2023-12-19 18:45:04 +01:00
if ( this . _type !== type ) {
this . _type = type ;
this . notify ( 'is-user' ) ;
}
if ( this . _errorDetail !== error ) {
this . _errorDetail = error ;
this . notify ( 'error' ) ;
}
2023-09-12 14:25:03 +02:00
if ( this . _enabled !== enabled ) {
this . _enabled = enabled ;
this . notify ( 'enabled' ) ;
}
2023-12-19 18:45:04 +01:00
if ( this . _state !== state ) {
const hadError = this . hasError ;
this . _state = state ;
this . notify ( 'state' ) ;
2023-12-20 00:17:29 +01:00
// Compat with older shell versions
if ( this . _enabled === undefined )
this . notify ( 'enabled' ) ;
2023-12-19 18:45:04 +01:00
if ( this . hasError !== hadError ) {
this . notify ( 'has-error' ) ;
this . notify ( 'error' ) ;
}
}
2023-12-18 20:20:26 +01:00
const creator = metadata . creator ? ? '' ;
2023-12-19 18:45:04 +01:00
if ( this . _creator !== creator ) {
this . _creator = creator ;
this . notify ( 'creator' ) ;
}
2023-12-18 20:20:26 +01:00
const url = metadata . url ? ? '' ;
2023-12-19 18:45:04 +01:00
if ( this . _url !== url ) {
this . _url = url ;
this . notify ( 'url' ) ;
}
2023-12-18 20:20:26 +01:00
const version = String (
metadata [ 'version-name' ] || metadata [ 'version' ] || '' ) ;
2023-12-19 18:45:04 +01:00
if ( this . _version !== version ) {
this . _version = version ;
this . notify ( 'version' ) ;
this . notify ( 'has-version' ) ;
}
if ( this . _hasPrefs !== hasPrefs ) {
this . _hasPrefs = hasPrefs ;
this . notify ( 'has-prefs' ) ;
}
2023-12-18 20:20:26 +01:00
2023-12-19 18:45:04 +01:00
if ( this . _hasUpdate !== hasUpdate ) {
this . _hasUpdate = hasUpdate ;
this . notify ( 'has-update' ) ;
}
if ( this . _canChange !== canChange ) {
this . _canChange = canChange ;
this . notify ( 'can-change' ) ;
}
this . thaw _notify ( ) ;
2023-12-18 20:20:26 +01:00
}
get uuid ( ) {
return this . _uuid ;
}
get name ( ) {
return this . _name ;
}
get description ( ) {
return this . _description ;
}
get state ( ) {
return this . _state ;
}
2023-09-12 14:25:03 +02:00
get enabled ( ) {
2023-12-20 00:17:29 +01:00
// Compat with older shell versions
if ( this . _enabled === undefined ) {
return this . state === ExtensionState . ACTIVE ||
this . state === ExtensionState . ACTIVATING ;
}
2023-09-12 14:25:03 +02:00
return this . _enabled ;
}
2023-12-18 20:20:26 +01:00
get creator ( ) {
return this . _creator ;
}
get url ( ) {
return this . _url ;
}
get version ( ) {
return this . _version ;
}
get error ( ) {
if ( ! this . hasError )
return '' ;
if ( this . state === ExtensionState . OUT _OF _DATE ) {
return this . version !== ''
? _ ( 'The installed version of this extension (%s) is incompatible with the current version of GNOME (%s). The extension has been disabled.' ) . format ( this . version , shellVersion )
: _ ( 'The installed version of this extension is incompatible with the current version of GNOME (%s). The extension has been disabled.' ) . format ( shellVersion ) ;
}
const message = [
_ ( 'An error has occurred in this extension. This could cause issues elsewhere in the system. It is recommended to turn the extension off until the error is resolved.' ) ,
] ;
if ( this . _errorDetail ) {
message . push (
// translators: Details for an extension error
_ ( 'Error details:' ) , this . _errorDetail ) ;
}
return message . join ( '\n\n' ) ;
}
get hasError ( ) {
return this . state === ExtensionState . OUT _OF _DATE ||
this . state === ExtensionState . ERROR ;
}
get hasPrefs ( ) {
return this . _hasPrefs ;
}
get hasUpdate ( ) {
return this . _hasUpdate ;
}
get hasVersion ( ) {
return this . _version !== '' ;
}
get canChange ( ) {
return this . _canChange ;
}
get isUser ( ) {
return this . _type === ExtensionType . PER _USER ;
}
2023-12-19 18:45:04 +01:00
} ) ;
const { $gtype : TYPE _EXTENSION } = Extension ;
export { TYPE _EXTENSION as Extension } ;
2023-12-18 20:20:26 +01:00
export const ExtensionManager = GObject . registerClass ( {
Properties : {
'user-extensions-enabled' : GObject . ParamSpec . boolean (
'user-extensions-enabled' , null , null ,
GObject . ParamFlags . READWRITE ,
true ) ,
2023-12-19 18:57:37 +01:00
'extensions' : GObject . ParamSpec . object (
'extensions' , null , null ,
GObject . ParamFlags . READABLE ,
Gio . ListModel ) ,
2023-12-18 20:20:26 +01:00
'n-updates' : GObject . ParamSpec . int (
'n-updates' , null , null ,
GObject . ParamFlags . READABLE ,
0 , 999 , 0 ) ,
'failed' : GObject . ParamSpec . boolean (
'failed' , null , null ,
GObject . ParamFlags . READABLE ,
false ) ,
} ,
Signals : {
'extensions-loaded' : { } ,
} ,
} , class ExtensionManager extends GObject . Object {
constructor ( ) {
super ( ) ;
2023-12-19 18:57:37 +01:00
this . _extensions = new Gio . ListStore ( { itemType : Extension } ) ;
2023-12-18 20:20:26 +01:00
this . _proxyReady = false ;
this . _shellProxy = new GnomeShellProxy ( Gio . DBus . session ,
'org.gnome.Shell.Extensions' , '/org/gnome/Shell/Extensions' ,
( ) => {
this . _proxyReady = true ;
shellVersion = this . _shellProxy . ShellVersion ;
this . _shellProxy . connect ( 'notify::g-name-owner' ,
( ) => this . notify ( 'failed' ) ) ;
this . notify ( 'failed' ) ;
} ) ;
this . _shellProxy . connect ( 'g-properties-changed' , ( proxy , properties ) => {
const enabledChanged = ! ! properties . lookup _value ( 'UserExtensionsEnabled' , null ) ;
if ( enabledChanged )
this . notify ( 'user-extensions-enabled' ) ;
} ) ;
this . _shellProxy . connectSignal (
'ExtensionStateChanged' , this . _onExtensionStateChanged . bind ( this ) ) ;
this . _loadExtensions ( ) . catch ( console . error ) ;
}
2023-12-19 18:57:37 +01:00
get extensions ( ) {
return this . _extensions ;
}
2023-12-18 20:20:26 +01:00
get userExtensionsEnabled ( ) {
return this . _shellProxy . UserExtensionsEnabled ? ? false ;
}
set userExtensionsEnabled ( enabled ) {
this . _shellProxy . UserExtensionsEnabled = enabled ;
}
get nUpdates ( ) {
let nUpdates = 0 ;
2023-12-19 18:57:37 +01:00
for ( const ext of this . _extensions ) {
2023-12-18 20:20:26 +01:00
if ( ext . isUser && ext . hasUpdate )
nUpdates ++ ;
}
return nUpdates ;
}
get failed ( ) {
return this . _proxyReady && this . _shellProxy . gNameOwner === null ;
}
enableExtension ( uuid ) {
this . _shellProxy . EnableExtensionAsync ( uuid ) . catch ( console . error ) ;
}
disableExtension ( uuid ) {
this . _shellProxy . DisableExtensionAsync ( uuid ) . catch ( console . error ) ;
}
uninstallExtension ( uuid ) {
this . _shellProxy . UninstallExtensionAsync ( uuid ) . catch ( console . error ) ;
}
openExtensionPrefs ( uuid , parentHandle ) {
this . _shellProxy . OpenExtensionPrefsAsync ( uuid ,
parentHandle ,
{ modal : new GLib . Variant ( 'b' , true ) } ) . catch ( console . error ) ;
}
checkForUpdates ( ) {
this . _shellProxy . CheckForUpdatesAsync ( ) . catch ( console . error ) ;
}
async _loadExtensions ( ) {
const [ extensionsMap ] = await this . _shellProxy . ListExtensionsAsync ( ) ;
for ( let uuid in extensionsMap ) {
const extension = new Extension ( extensionsMap [ uuid ] ) ;
2023-12-19 18:57:37 +01:00
this . _extensions . append ( extension ) ;
2023-12-18 20:20:26 +01:00
}
this . emit ( 'extensions-loaded' ) ;
}
2023-12-19 18:57:37 +01:00
_findExtension ( uuid ) {
const len = this . _extensions . get _n _items ( ) ;
for ( let pos = 0 ; pos < len ; pos ++ ) {
const extension = this . _extensions . get _item ( pos ) ;
if ( extension . uuid === uuid )
return [ extension , pos ] ;
}
return [ null , - 1 ] ;
}
2023-12-18 20:20:26 +01:00
_onExtensionStateChanged ( p , sender , [ uuid , newState ] ) {
2023-12-19 18:57:37 +01:00
const [ extension , pos ] = this . _findExtension ( uuid ) ;
2023-12-18 20:20:26 +01:00
if ( extension )
extension . update ( newState ) ;
if ( ! extension )
2023-12-19 18:57:37 +01:00
this . _extensions . append ( new Extension ( newState ) ) ;
2023-12-18 20:20:26 +01:00
else if ( extension . state === ExtensionState . UNINSTALLED )
2023-12-19 18:57:37 +01:00
this . _extensions . remove ( pos ) ;
2023-12-18 20:20:26 +01:00
if ( this . _updatesCheckId )
return ;
this . _updatesCheckId = GLib . timeout _add _seconds (
GLib . PRIORITY _DEFAULT , 1 , ( ) => {
this . notify ( 'n-updates' ) ;
delete this . _updatesCheckId ;
return GLib . SOURCE _REMOVE ;
} ) ;
}
} ) ;