/* Desktop Icons GNOME Shell extension * * Copyright (C) 2023 Sundeep Mediratta (smedius@gmail.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import {Gtk, Gdk, Gio, GLib, GObject, Adw} from '../dependencies/gi.js'; import {DesktopWidgetCapability} from '../dependencies/gi.js'; import {_} from '../dependencies/gettext.js'; import {DesktopFolderUtils} from '../dependencies/localFiles.js'; export {AdwPreferencesWindow}; const appID = 'com.desktop.ding'; const appPath = GLib.build_filenamev(['/', ...appID.split('.')]); const ListObject = GObject.registerClass({ GTypeName: 'peferences-list', Properties: { 'indexkey': GObject.ParamSpec.string( 'indexkey', 'Indexkey', 'A read-write string property', GObject.ParamFlags.READWRITE, '' ), 'description': GObject.ParamSpec.string( 'description', 'Description', 'A read-write string property', GObject.ParamFlags.READWRITE, '' ), }, }, class listObject extends GObject.Object { constructor(constructProperties = {}) { super(constructProperties); } get indexkey() { if (this._indexkey === undefined) this._indexkey = ''; return this._indexkey; } set indexkey(value) { if (this.indexkey === value) return; this._indexkey = value; this.notify('indexkey'); } get description() { if (this._description === undefined) this._description = ''; return this._description; } set description(value) { if (this.description === value) return; this._description = value; this.notify('description'); } }); const ComboRowWithKey = GObject.registerClass({ GTypeName: 'ComboRowWithKey', Properties: { 'indexkey': GObject.ParamSpec.string( 'indexkey', 'Indexkey', 'A read-write string property', GObject.ParamFlags.READWRITE, '' ), }, }, class ComboRowWithKey extends Adw.ComboRow { constructor(constructProperties = {}) { super(constructProperties); this._indexKey = ''; this.connect('notify::selected-item', () => { let item = this.get_selected_item(); this.indexkey = item.indexkey; }); } makeEnumn(enumexpression) { const listStore = new Gio.ListStore(ListObject._$gtype); this.enumExpression = {}; let i = 0; for (let key in enumexpression) { this.enumExpression[key] = parseInt(i); let listObject = new ListObject(); listObject.indexkey = key; listObject.description = enumexpression[key]; listStore.append(listObject); i += 1; } this.set_model(listStore); const listFactory = new Gtk.SignalListItemFactory(); listFactory.connect('setup', (_actor, listitem) => { let label = new Gtk.Label(); listitem.set_child(label); }); listFactory.connect('bind', (_actor, listitem) => { let label = listitem.get_child(); let item = listitem.get_item(); label.set_text(item.description); }); this.set_factory(listFactory); const expression = new Gtk.PropertyExpression(ListObject, null, 'description' ); this.set_expression(expression); } get indexkey() { if (this._indexkey === undefined) this._indexkey = ''; return this._indexkey; } set indexkey(value) { if (this.indexkey === value) return; this._indexkey = value; if (this.get_selected !== this.enumExpression[value]) this.set_selected(this.enumExpression[value]); this.notify('indexkey'); } }); const CssOverrideGroup = GObject.registerClass( class CssOverrideGroup extends Adw.PreferencesGroup { constructor(params = {}) { super({}); this.set_title(_('CSS Override')); this.set_description(_('Customise the appearance of desktop icons with CSS')); const warningLabel = new Gtk.Label(); warningLabel.set_markup( `${ _('Warning: This can break the extension if done incorrectly') }` ); this.add(warningLabel); const icon = Gtk.Image.new_from_icon_name('window-pop-out-symbolic'); this.cssOverrideButton = new Adw.ActionRow({ title: _('Edit CSS Override File...'), }); this.cssOverrideButton.add_suffix(icon); this.cssOverrideButton.set_activatable_widget(icon); this.cssOverrideButton.connect('activated', this.openUserCssOverrideFile.bind(this)); this.add(this.cssOverrideButton); this.reloadButtonRow = new Adw.ActionRow({ title: _('Apply CSS Changes Now'), subtitle: _('Reload the CSS to apply changes immediately'), }); const button = Gtk.Button.new_with_label('Reload'); button.set_size_request(120, -1); button.set_halign(Gtk.Align.END); button.set_valign(Gtk.Align.CENTER); button.set_hexpand(true); button.set_vexpand(false); button.connect('clicked', () => { this.reloadCSS(); }); this.reloadButtonRow.add_suffix(button); this.reloadButtonRow.set_activatable_widget(button); this.add(this.reloadButtonRow); this.update(params.remoteActions); } reloadCSS() { try { this.remoteActions.activate_action('reloadCSS', null); console.info('CSS reload requested'); } catch (e) { console.error(`Failed to reload CSS: ${e}`); } } update(remoteActions) { this.remoteActions = remoteActions; if (this.remoteActions?.list_actions()) this.reloadButtonRow.set_sensitive(true); else this.reloadButtonRow.set_sensitive(false); } openUserCssOverrideFile() { const configDir = GLib.get_user_config_dir(); const cssFile = Gio.File.new_for_path( GLib.build_filenamev([configDir, appID, 'stylesheet-override.css']) ); // Create directory if it doesn't exist const cssDir = cssFile.get_parent(); try { cssDir.make_directory_with_parents(null); } catch (e) { // Directory already exists } // Create file if it doesn't exist if (!cssFile.query_exists(null)) cssFile.create(Gio.FileCreateFlags.NONE, null); // Open with default text editor const context = Gdk.Display.get_default().get_app_launch_context(); context.set_timestamp(Gdk.CURRENT_TIME); Gio.AppInfo.launch_default_for_uri(cssFile.get_uri(), context); } }); const ShortcutGroup = GObject.registerClass( class ShortcutGroup extends Adw.PreferencesGroup { constructor(params = {}) { super({}); this.set_title(_('Shortcuts')); this.shortcutButton = new Adw.ActionRow({ title: _('Edit Shortcuts...'), }); const icon = Gtk.Image.new_from_icon_name('window-pop-out-symbolic'); this.shortcutButton.add_suffix(icon); this.shortcutButton.set_activatable_widget(icon); this.shortcutButton.connect('activated', this.showShortcuts.bind(this)); this.add(this.shortcutButton); this.update(params.remoteActions); } update(remoteActions) { this.remoteActions = remoteActions; if (this.remoteActions?.list_actions()) { this.shortcutButton.set_sensitive(true); this.set_description(_('Edit Application Shortcuts')); } else { this.shortcutButton.set_sensitive(false); this.set_description( _('Shortcuts Editable only when Extension Enabled...') ); } } showShortcuts() { this.remoteActions.activate_action('showShortcutViewer', null); } }); const aboutApp = class AboutDialog { constructor(params = {}) { this.version = params.version; this.appID = appID; const aboutDialog = Adw.AboutDialog.new(); this.init(aboutDialog); return aboutDialog; } init(aboutDialog) { aboutDialog.modal = true; aboutDialog.set_application_icon(this.appID); aboutDialog.set_application_name('Adw. Desktop Icons'); aboutDialog.set_comments( 'An application to show Icons on the Gnome Desktop' ); aboutDialog.set_copyright('© 2025 Sundeep Mediratta'); aboutDialog.set_developer_name('Sundeep Mediratta'); aboutDialog.set_comments( 'Adw. Desktop Icons is an extension and a program together for ' + 'the GNOME Shell that renders icons on the desktop. It is a fork ' + 'from Desktop Icons NG (DING), by Sergio Costas, which itself ' + 'is a fork/rewrite of the official "Desktop Icons" extension, ' + 'originally by Carlos Soriano.' + '\n\n' + 'All these came into existence when Nautilus and Gnome decided ' + 'to drop showing a "Desktop" with Icons!' + '\n\n' + 'Many thanks to the original developers of Desktop Icons NG, ' + 'specially Sergio Costas for his work on ' + 'Meta.WaylandClient that makes this privileged window possible in' + 'the first place and to Florian Müllner for implementing ' + 'Meta.Windotype.DESKTOP through Meta.WaylandClient, which makes ' + 'this so much easier!' ); aboutDialog.add_credit_section( 'Originally developed by', [ 'Sergio Costas', 'Carlos Soriano', ] ); aboutDialog.add_acknowledgement_section( 'For coding Meta.WaylandClient in mutter', ['Sergio Costas'] ); aboutDialog.add_acknowledgement_section( 'Enabling Meta.Windowtype.DESKTOP\nthrough Meta.Waylandclient', ['Florian Müllner'] ); aboutDialog.add_acknowledgement_section( 'Async code contribution', ['Marco Trevisan'] ); aboutDialog.add_acknowledgement_section( 'Gnome Extensions Matrix Channel support', [ 'Andy Holmes', 'Just Perfection', 'And Others..', ] ); aboutDialog.add_acknowledgement_section( 'GJS Maintainers for GJS\n@ptomato for answering', ['@ptomato'] ); aboutDialog.set_license_type(Gtk.License.GPL_3_0); aboutDialog.set_issue_url( 'https://gitlab.com/smedius/desktop-icons-ng/-/issues' ); aboutDialog.set_support_url( 'https://gitlab.com/smedius/desktop-icons-ng/-/blob/main/ISSUES.md?ref_type=heads' ); aboutDialog.set_translator_credits( `Weblate Translators, See History.MD on website. Translated using machine translation with LibreTranslate. Unverified strings, translation may contain errors. Corrections, verification and additional translation can be done on Weblate. Translations available in- ar,az,be,bg,bn,ca,cs,da,de,el,eo,es,et,eu,fa,fi,fr,fur,ga,gl,he,hi,hr,hu,id,it,ja,ka,ko,ky,lv,lt,ms,nb,nb_NO,nl,oc,pl,pt_BR,pt,ro,ru,sk,sl,sq,sv,ta,tl,tr,th,uk,ur,zh-Hans,zh-Hant,zh_CN,zh_TW.` ); aboutDialog.set_version(this.version); aboutDialog.set_website('https://gitlab.com/smedius/desktop-icons-ng'); aboutDialog.add_link( _('Help translate in your web browser'), 'https://hosted.weblate.org/engage/gtk4-desktop-icons-ng' ); aboutDialog.set_release_notes( `

* Adw version 100.17 for Gnome 45, 46, 47, 48, 49

* Adw version 100.16 for Gnome 45, 46, 47, 48, 49

* Adw version 100.15 for Gnome 45, 46, 47, 48, 49

* Adw version 100.14 for Gnome 45, 46, 47, 48, 49

* Adw version 100.13 for Gnome 45, 46, 47, 48, 49

* Adw version 100.11 for Gnome 45, 46, 47, 48, 49

* Adw version 100.9 for Gnome 45, 46, 47, 48, 49

* Adw version 100.8-2 for Gnome 45, 46, 47, 48 49

* Adw version 100.8 for Gnome 45, 46, 47, 48, 49

* Adw version 100.7 for Gnome 45, 46, 47, 48, 49

* Adw version 100.6 for Gnome 45, 46, 47, 48, 49

* Adw version 100.5 for Gnome 45, 46, 47, 48

* Adw version 100.3 for Gnome 45, 46, 47, 48

* Adw version 100.2 for Gnome 45, 46, 47, 48

* Adw version 100.1 for Gnome 45, 46, 47, 48

Minor bug fixes to run on older Adw 1.5, errors on connecting second monitor, fix open terminal shortcut

Yay! Version 100! Actually version 1.0, started with 0.01, but got tired of writing a 0 before every version. I believe mostly feature complete, except DBus Activation and packaging as a GJS app. Change Name to Adw. Desktop Icons :)

` ); } }; const DingPreferencesWindow = class extends DesktopFolderUtils { constructor(params) { super(params); this.iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); this.iconTheme.add_resource_path(`${appPath}/icons`); } addActionRowSwitch(settings, key, labelText, bindFlags = null) { const actionRow = Adw.ActionRow.new(); const switcher = new Gtk.Switch({active: settings.get_boolean(key)}); switcher.set_halign(Gtk.Align.END); switcher.set_valign(Gtk.Align.CENTER); switcher.set_hexpand(false); switcher.set_vexpand(false); actionRow.set_title(labelText); actionRow.add_suffix(switcher); if (!bindFlags) bindFlags = Gio.SettingsBindFlags.DEFAULT; settings.bind(key, switcher, 'active', bindFlags); actionRow.set_activatable_widget(switcher); return actionRow; } addActionRowSelector(settings, key, labelText, elements) { const actionRow = new ComboRowWithKey(); actionRow.set_title(labelText); actionRow.set_use_subtitle(false); actionRow.makeEnumn(elements); actionRow.set_selected(settings.get_enum(key)); settings.bind(key, actionRow, 'indexkey', Gio.SettingsBindFlags.DEFAULT); return actionRow; } addActionRowButton(title, subtitle, buttonLabel, action, key = null) { const actionRow = Adw.ActionRow.new(); actionRow.set_title(title); if (subtitle) { actionRow.use_markup = false; actionRow.set_subtitle(subtitle); if (Adw.get_minor_version() > 2) actionRow.set_subtitle_selectable(true); } if (buttonLabel && action) { const button = Gtk.Button.new_with_label(buttonLabel); button.set_size_request(120, -1); button.set_halign(Gtk.Align.END); button.set_valign(Gtk.Align.CENTER); button.set_hexpand(true); button.set_vexpand(false); button.connect('clicked', action.bind(this, actionRow, button, key)); actionRow.add_suffix(button); actionRow.set_activatable_widget(button); } return actionRow; } launchUri(uri) { const context = Gdk.Display.get_default().get_app_launch_context(); context.set_timestamp(Gdk.CURRENT_TIME); Gio.AppInfo.launch_default_for_uri(uri, context); } }; const AdwPreferencesWindow = class extends DingPreferencesWindow { constructor( desktopSettings, nautilusSettings, gtkSettings, version, actiongroup = null ) { super(); this.desktopSettings = desktopSettings; this.nautilusSettings = nautilusSettings; this.gtkSettings = gtkSettings; if (!actiongroup) this.getRemoteActions(); else this.remoteActions = actiongroup; this.version = version; } destroy() { if (this.watchNameID) Gio.DBus.unwatch_name(this.watchNameID); this.watchNameID = 0; } getRemoteActions() { this.watchNameID = Gio.DBus.watch_name( Gio.BusType.SESSION, appID, Gio.BusNameWatcherFlags.NONE, (_conn, _name, _nameOwner) => { try { this.remoteActions = Gio.DBusActionGroup.get( Gio.DBus.session, appID, appPath ); this.shortcutGroup?.update(this.remoteActions); this.cssOverrideGroup?.update(this.remoteActions); } catch (e) { logError(e, 'Error getting action group'); } }, (_conn, _name) => { this.remoteActions = null; this.shortcutGroup?.update(this.remoteActions); this.cssOverrideGroup?.update(this.remoteActions); } ); } getAdwPreferencesWindow(window = null) { var prefsWindow; if (window) { prefsWindow = window; } else { prefsWindow = new Adw.PreferencesWindow(); const app = Gtk.Application.get_default(); if (app) prefsWindow.set_application(app); } prefsWindow.set_can_navigate_back(true); prefsWindow.set_search_enabled(true); this.prefsWindow = prefsWindow; this.activeWindow = prefsWindow; const prefsFrame = new Adw.PreferencesPage(); prefsFrame.set_name(_('Desktop')); prefsFrame.set_title(_('Desktop')); prefsFrame.set_icon_name('prefs-desktop-symbolic'); const filesPrefsFrame = new Adw.PreferencesPage(); filesPrefsFrame.set_name(_('Files')); filesPrefsFrame.set_title(_('Files')); filesPrefsFrame.set_icon_name('prefs-files-symbolic'); const tweaksFrame = new Adw.PreferencesPage(); tweaksFrame.set_name(_('Tweaks')); tweaksFrame.set_title(_('Tweaks')); tweaksFrame.set_icon_name('prefs-tweaks-symbolic'); const aboutFrame = new Adw.PreferencesPage(); aboutFrame.set_name(_('More')); aboutFrame.set_title(_('More')); aboutFrame.set_icon_name('prefs-more-symbolic'); prefsWindow.add(prefsFrame); prefsWindow.add(filesPrefsFrame); prefsWindow.add(tweaksFrame); prefsWindow.add(aboutFrame); prefsWindow.set_visible(prefsFrame); const desktopGroup = new Adw.PreferencesGroup(); desktopGroup.set_title(_('Desktop Settings')); desktopGroup.set_description(_('Settings for the Desktop Program')); prefsFrame.add(desktopGroup); this.desktopFolderGroup = new Adw.PreferencesGroup(); this.desktopFolderGroup.set_title(_('Desktop Folder')); this.FolderGroupDescription = _('Current Desktop: '); const desktoPath = this.getDesktopDir().get_path(); this.desktopFolderGroup.set_description( `${this.FolderGroupDescription} ${desktoPath}` ); prefsFrame.add(this.desktopFolderGroup); const filesGroup = new Adw.PreferencesGroup(); filesGroup.set_title(_('Files Settings')); filesGroup.set_description(_('Settings shared with Gnome Files')); filesPrefsFrame.add(filesGroup); const tweaksGroup = new Adw.PreferencesGroup(); tweaksGroup.set_title(_('Tweaks')); tweaksGroup.set_description(_('Miscellaneous Tweaks')); tweaksFrame.add(tweaksGroup); this.shortcutGroup = new ShortcutGroup({remoteActions: this.remoteActions}); aboutFrame.add(this.shortcutGroup); this.cssOverrideGroup = new CssOverrideGroup({ remoteActions: this.remoteActions, }); this.cssOverrideGroup.set_visible(false); // Initially hidden aboutFrame.add(this.cssOverrideGroup); const aboutGroup = new Adw.PreferencesGroup(); aboutGroup.set_title('About Adw. Desktop Icons'); let versiontitle = _(`Version ${this.version}`); aboutGroup.set_description(versiontitle); aboutFrame.add(aboutGroup); desktopGroup.add(this.addActionRowSelector(this.desktopSettings, 'icon-size', _('Size for the desktop icons'), { 'tiny': _('Tiny'), 'small': _('Small'), 'standard': _('Standard'), 'large': _('Large'), } )); desktopGroup.add(this.addActionRowSelector(this.desktopSettings, 'start-corner', _('New icons alignment'), { 'top-left': _('Top left corner'), 'top-right': _('Top right corner'), 'bottom-left': _('Bottom left corner'), 'bottom-right': _('Bottom right corner'), } )); desktopGroup.add(this.addActionRowSwitch(this.desktopSettings, 'show-second-monitor', _('Add new icons to Secondary Monitors first, if available'))); desktopGroup.add(this.addActionRowSwitch(this.desktopSettings, 'free-position-icons', _('Snap icons to grid'), Gio.SettingsBindFlags.INVERT_BOOLEAN )); const showWidgets = this.addActionRowSwitch(this.desktopSettings, 'show-desktop-widgets', _('Show desktop widgets')); showWidgets.set_sensitive(DesktopWidgetCapability); desktopGroup.add(showWidgets); this.desktopFolderGroup .add(this.addActionRowButton(_('New Desktop Folder'), _('Set a new folder for the desktop'), _('Choose'), this.changeDesktop.bind(this) )); const defaultDesktopPath = this.getSystemLocalizedDesktopDir(); const secondarytext = _('Set Desktop back to ~/'); this.defaultDesktopRow = this.addActionRowButton(_('Restore Default Desktop Folder'), `${secondarytext}${defaultDesktopPath}`, _('Restore'), this.restoreDefaultDesktop.bind(this) ); this.desktopFolderGroup.add(this.defaultDesktopRow); this.defaultDesktopRow.set_sensitive(!this.isDefaultDesktop); const dropPlaceRow = this.addActionRowSwitch(this.desktopSettings, 'show-drop-place', _('Highlight the drop grid')); this.desktopSettings.bind('free-position-icons', dropPlaceRow, 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN); tweaksGroup.add(dropPlaceRow); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'show-link-emblem', _('Add information emblems for links, encryption'))); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'dark-text-in-labels', _('Use dark text in icon labels') )); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'show-home', _('Show the personal folder on the desktop') )); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'show-trash', _('Show the trash icon on the desktop') )); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'show-volumes', _('Show external drives on the desktop') )); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'show-network-volumes', _('Show network drives on the desktop') )); tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings, 'add-volumes-opposite', _('Add new drives to the opposite side of the desktop') )); filesGroup.add(this.addActionRowSelector(this.nautilusSettings, 'click-policy', _('Action to Open Items'), { 'single': _('Single click'), 'double': _('Double click'), })); filesGroup.add(this.addActionRowSelector(this.nautilusSettings, 'show-image-thumbnails', _('Show image thumbnails'), { 'always': _('Always'), 'local-only': _('On this computer only'), 'never': _('Never'), })); filesGroup.add(this.addActionRowSwitch(this.nautilusSettings, 'show-delete-permanently', _('Show a context menu item to delete permanently') )); filesGroup.add(this.addActionRowSwitch(this.gtkSettings, 'show-hidden', _('Show hidden files') )); filesGroup.add(this.addActionRowSwitch(this.nautilusSettings, 'open-folder-on-dnd-hover', _('Open folders on drag hover') )); const aboutButton = new Adw.ActionRow(); aboutButton.set_title(_('About...')); const icon = Gtk.Image.new_from_icon_name('window-pop-out-symbolic'); aboutButton.add_suffix(icon); aboutButton.set_activatable_widget(icon); aboutButton.connect('activated', () => { const aboutDialog = new aboutApp({version: this.version}); aboutDialog.present(prefsWindow); }); aboutGroup.add(aboutButton); const tranlationGroup = new Adw.PreferencesGroup({ title: _('Translation'), description: _('Machine translated using LibreTranslate. User verified and edited on Weblate.'), }); aboutFrame.add(tranlationGroup); tranlationGroup.add(this.addActionRowButton(_('Edit Translations'), _('Verify, add or correct translation in your web browser'), _('Translate'), this.launchWebTranslation.bind(this) )); // Track Alt key state using event controllers this._altKeyPressed = false; // Add key event controller to track Alt key const keyController = new Gtk.EventControllerKey(); keyController.connect('key-pressed', (controller, keyval, _keycode, _state) => { if (keyval === Gdk.KEY_Alt_L || keyval === Gdk.KEY_Alt_R) this._altKeyPressed = true; return false; }); keyController.connect('key-released', (controller, keyval, _keycode, _state) => { if (keyval === Gdk.KEY_Alt_L || keyval === Gdk.KEY_Alt_R) this._altKeyPressed = false; }); prefsWindow.add_controller(keyController); // Add click gesture controller to detect Alt+Click on tab const clickController = new Gtk.GestureClick(); clickController.connect('pressed', (gesture, _nPress, _x, _y) => { const state = gesture.get_current_event().get_modifier_state(); this._altKeyPressed = (state & Gdk.ModifierType.ALT_MASK) !== 0; }); prefsWindow.add_controller(clickController); // Show CSS Override group only when navigating to More tab with Alt held prefsWindow.connect('notify::visible-page', () => { if (prefsWindow.get_visible_page() === aboutFrame) this.cssOverrideGroup.set_visible(this._altKeyPressed); }); prefsWindow.set_default_size(600, 650); this._monitorDesktopDirChanges(); prefsWindow.connect( 'close-request', () => { this._stopMonitoring(); this.activeWindow = null; } ); if (!window) return prefsWindow; else return true; } onDesktopFolderChanged(newDesktopDir) { super.onDesktopFolderChanged(newDesktopDir); const desktopPath = this._desktopDir.get_path(); this.desktopFolderGroup.set_description( `${this.FolderGroupDescription} ${desktopPath}` ); this.defaultDesktopRow.set_sensitive(!this.isDefaultDesktop); } launchWebTranslation() { const translationUri = 'https://hosted.weblate.org/engage/gtk4-desktop-icons-ng'; this.launchUri(translationUri); } };