661 lines
22 KiB
JavaScript
661 lines
22 KiB
JavaScript
/* ADW-DING: Desktop Icons New Generation for GNOME Shell
|
|
*
|
|
* Adw/Gtk4 Port Copyright (C) 2025 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, version 3 of the License.
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {IconCreator} from '../dependencies/localFiles.js';
|
|
import {DesktopFolderUtils} from '../dependencies/localFiles.js';
|
|
|
|
import {Gio, GLib} from '../dependencies/gi.js';
|
|
import {_} from '../dependencies/gettext.js';
|
|
|
|
export {DesktopMonitor};
|
|
|
|
const DesktopMonitor = class extends DesktopFolderUtils {
|
|
constructor(desktopManager) {
|
|
super();
|
|
this.desktopManager = desktopManager;
|
|
this.mainApp = desktopManager.mainApp;
|
|
this.DesktopIconsUtil = desktopManager.DesktopIconsUtil;
|
|
this.desktopActions = desktopManager.desktopActions;
|
|
this.dbusManager = desktopManager.dbusManager;
|
|
this.windowManager = desktopManager.windowManager;
|
|
this.Prefs = desktopManager.Prefs;
|
|
this.FileUtils = desktopManager.FileUtils;
|
|
this.Enums = desktopManager.Enums;
|
|
|
|
this._desktopFilesChanged = false;
|
|
this._readingDesktopFiles = false;
|
|
this._fileList = [];
|
|
this._forcedExit = false;
|
|
this._writableByOthers = false;
|
|
|
|
this._updateWritableByOthers().catch(e => console.error(e));
|
|
this._createDesktopChangeActions();
|
|
this._monitorDesktopDirChanges();
|
|
this._monitorDesktopChanges();
|
|
this._monitorVolumes();
|
|
this.DBusUtils = this.desktopManager.DBusUtils;
|
|
|
|
this.DBusUtils.GtkVfsMetadata.connectSignalToProxy(
|
|
'AttributeChanged',
|
|
this._metadataChanged.bind(this)
|
|
);
|
|
|
|
this._updateFileList().catch(e => console.error(e));
|
|
}
|
|
|
|
_createDesktopChangeActions() {
|
|
const changeDesktop = Gio.SimpleAction.new('changeDesktop', null);
|
|
|
|
changeDesktop.connect('activate', () => {
|
|
this.changeDesktop();
|
|
});
|
|
|
|
this.mainApp.add_action(changeDesktop);
|
|
|
|
this.restoreDefaultDesktopAction =
|
|
Gio.SimpleAction.new('restoreDefaultDesktop', null);
|
|
|
|
this.restoreDefaultDesktopAction.connect('activate', () => {
|
|
this.restoreDefaultDesktop();
|
|
});
|
|
|
|
this.mainApp.add_action(this.restoreDefaultDesktopAction);
|
|
|
|
this.restoreDefaultDesktopAction
|
|
.set_enabled(!this.isDefaultDesktop);
|
|
}
|
|
|
|
stopMonitoring() {
|
|
if (this._monitorDesktopCancellable)
|
|
this._monitorDesktopCancellable.cancel();
|
|
|
|
this._forcedExit = true;
|
|
if (this._desktopEnumerateCancellable)
|
|
this._desktopEnumerateCancellable.cancel();
|
|
|
|
super._stopMonitoring();
|
|
}
|
|
|
|
onDesktopFolderChanged(newDesktopDir) {
|
|
const fileType =
|
|
newDesktopDir.query_file_type(
|
|
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
|
null
|
|
);
|
|
|
|
if (fileType === Gio.FileType.SYMBOLIC_LINK) {
|
|
const header = _('Desktop Folder Change Failed');
|
|
const text = _('The new Desktop Folder is a Symbolic Link');
|
|
|
|
this.dbusManager.doNotify(header, text);
|
|
return;
|
|
} else if (fileType !== Gio.FileType.DIRECTORY) {
|
|
const header = _('Desktop Folder Change Failed');
|
|
const text =
|
|
_('The new Desktop Folder does not exist or is not a Folder!');
|
|
|
|
this.dbusManager.doNotify(header, text);
|
|
return;
|
|
}
|
|
|
|
if (newDesktopDir.get_path() ===
|
|
this._desktopDir.get_path()
|
|
)
|
|
return;
|
|
|
|
const header = _('Desktop Folder Changed');
|
|
const text = _('Switching to new Desktop...');
|
|
this.dbusManager.doNotify(header, text);
|
|
|
|
this._desktopDir = newDesktopDir;
|
|
|
|
this.restoreDefaultDesktopAction
|
|
.set_enabled(!this.isDefaultDesktop);
|
|
|
|
this._updateWritableByOthers()
|
|
.catch(e => console.error(e));
|
|
|
|
this._desktops.forEach(d => d.unsetErrorState());
|
|
|
|
this._updateFileList().catch(e => console.error(e));
|
|
|
|
this._monitorDesktopChanges();
|
|
}
|
|
|
|
async _updateWritableByOthers() {
|
|
try {
|
|
const info =
|
|
await this._desktopDir.query_info_async(
|
|
Gio.FILE_ATTRIBUTE_UNIX_MODE,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_LOW,
|
|
null
|
|
);
|
|
|
|
this.unixMode =
|
|
info.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE);
|
|
|
|
let writableByOthers =
|
|
(this.unixMode & this.Enums.UnixPermissions.S_IWOTH) !== 0;
|
|
|
|
if (writableByOthers !== this._writableByOthers) {
|
|
this._writableByOthers = writableByOthers;
|
|
|
|
if (this._writableByOthers) {
|
|
console.log('desktop-icons: The desktop is writable by' +
|
|
' others. Not allowing launching any desktop files.'
|
|
);
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
|
|
this._writableByOthers = true;
|
|
|
|
return true;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
_monitorDesktopChanges() {
|
|
const cancellable = new Gio.Cancellable();
|
|
|
|
if (this._monitorDesktopCancellable)
|
|
this._monitorDesktopCancellable.cancel();
|
|
|
|
this._monitorDesktopCancellable = cancellable;
|
|
|
|
this._monitorDesktopDir =
|
|
this._desktopDir.monitor_directory(
|
|
Gio.FileMonitorFlags.WATCH_MOVES,
|
|
cancellable
|
|
);
|
|
|
|
this._monitorDesktopDir.set_rate_limit(1000);
|
|
|
|
const monitorID = this._monitorDesktopDir.connect(
|
|
'changed',
|
|
(obj, file, otherFile, eventType) => {
|
|
this._updateFileListIfChanged(file, otherFile, eventType)
|
|
.catch(e => console.error(e));
|
|
}
|
|
);
|
|
|
|
cancellable.connect(
|
|
() => {
|
|
this._monitorDesktopDir.disconnect(monitorID);
|
|
this._monitorDesktopDir.cancel();
|
|
this._monitorDesktopDir = null;
|
|
this.monitorDesktopCancellable = null;
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
_monitorVolumes() {
|
|
this._volumeMonitor = Gio.VolumeMonitor.get();
|
|
|
|
this._volumeMonitor.connect(
|
|
'mount-added',
|
|
() => {
|
|
this.onMountAdded();
|
|
}
|
|
);
|
|
|
|
this._volumeMonitor.connect(
|
|
'mount-removed',
|
|
() => {
|
|
GLib.timeout_add(
|
|
GLib.PRIORITY_DEFAULT,
|
|
500,
|
|
() => {
|
|
this.onMountRemoved();
|
|
return GLib.SOURCE_REMOVE;
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
async _updateFileListIfChanged(file, otherFile, eventType) {
|
|
if (eventType === Gio.FileMonitorEvent.CHANGED) {
|
|
// use only CHANGES_DONE_HINT
|
|
return;
|
|
}
|
|
|
|
if (!this.Prefs.showHidden && (file.get_basename()[0] === '.')) {
|
|
// If the file is not visible, we don't need to refresh the desktop
|
|
// Unless it is a hidden file being renamed to visible
|
|
if (!otherFile || (otherFile.get_basename()[0] === '.'))
|
|
return;
|
|
}
|
|
|
|
switch (eventType) {
|
|
case Gio.FileMonitorEvent.MOVED_IN:
|
|
case Gio.FileMonitorEvent.MOVED_CREATED:
|
|
/* Remove the coordinates that could exist to avoid conflicts
|
|
between files that are already in the desktop and the new one
|
|
*/
|
|
try {
|
|
const info = new Gio.FileInfo();
|
|
|
|
info.set_attribute_string(
|
|
'metadata::desktop-icon-position', ''
|
|
);
|
|
|
|
file.set_attributes_async(
|
|
info,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_LOW,
|
|
null
|
|
);
|
|
} catch (e) {
|
|
// can happen if a file is created and deleted very fast
|
|
}
|
|
break;
|
|
case Gio.FileMonitorEvent.ATTRIBUTE_CHANGED:
|
|
/* The desktop is what changed, and not a file inside it */
|
|
if (file.get_uri() === this._desktopDir.get_uri()) {
|
|
if (await this._updateWritableByOthers()) {
|
|
try {
|
|
await this._updateFileList();
|
|
} catch (e) {
|
|
console.error(
|
|
e,
|
|
'Exception while updating desktop from' +
|
|
` Directory Monitor attribute change: ${e.message}`
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
try {
|
|
await this._updateFileList();
|
|
} catch (e) {
|
|
console.error(
|
|
e,
|
|
'Exception while updating desktop from Directory Monitor: ' +
|
|
`${e.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async _updateFileList() {
|
|
if (this._readingDesktopFiles) {
|
|
// just notify that the files changed while being read from disk.
|
|
this._desktopFilesChanged = true;
|
|
|
|
if (this._desktopEnumerateCancellable && !this._forceDraw) {
|
|
this._desktopEnumerateCancellable.cancel();
|
|
this._desktopEnumerateCancellable = null;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
this._readingDesktopFiles = true;
|
|
this._forceDraw = false;
|
|
this._lastDesktopUpdateRequest = GLib.get_monotonic_time();
|
|
let fileList;
|
|
|
|
while (true) {
|
|
this._desktopFilesChanged = false;
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
fileList = await this._doReadAsync();
|
|
} catch (e) {
|
|
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
|
|
fileList = [];
|
|
break;
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
|
|
if (this._forcedExit)
|
|
return;
|
|
|
|
if (fileList !== null) {
|
|
if (!this._desktopFilesChanged)
|
|
break;
|
|
|
|
if (this._forceDraw) {
|
|
this._fileList = fileList;
|
|
this.desktopManager.refreshDesktop();
|
|
this._lastDesktopUpdateRequest = GLib.get_monotonic_time();
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.DesktopIconsUtil.waitDelayMs(500);
|
|
|
|
if (
|
|
(GLib.get_monotonic_time() - this._lastDesktopUpdateRequest) >
|
|
1000000
|
|
)
|
|
this._forceDraw = true;
|
|
else
|
|
this._forceDraw = false;
|
|
}
|
|
|
|
this._readingDesktopFiles = false;
|
|
this._forceDraw = false;
|
|
this._fileList = fileList;
|
|
this.desktopManager.refreshDesktop();
|
|
}
|
|
|
|
async _doReadAsync() {
|
|
if (this._desktopEnumerateCancellable)
|
|
this._desktopEnumerateCancellable.cancel();
|
|
|
|
|
|
const cancellable = new Gio.Cancellable();
|
|
this._desktopEnumerateCancellable = cancellable;
|
|
|
|
try {
|
|
const fileList = [];
|
|
|
|
const extraFoldersItems =
|
|
this.DesktopIconsUtil.getExtraFolders().map(
|
|
async ([newFolder, fileTypeEnum]) => {
|
|
try {
|
|
if (imports.system.version < 17200) {
|
|
Gio._promisify(
|
|
newFolder.constructor.prototype,
|
|
'query_info_async'
|
|
);
|
|
}
|
|
|
|
const newFolderInfo =
|
|
await newFolder.query_info_async(
|
|
this.Enums.DEFAULT_ATTRIBUTES,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable
|
|
);
|
|
|
|
fileList.push(
|
|
new IconCreator(
|
|
this.desktopManager,
|
|
newFolder,
|
|
newFolderInfo,
|
|
fileTypeEnum,
|
|
null
|
|
)
|
|
);
|
|
} catch (e) {
|
|
if (e.matches(
|
|
Gio.IOErrorEnum,
|
|
Gio.IOErrorEnum.CANCELLED)
|
|
)
|
|
throw e;
|
|
|
|
console.error(e,
|
|
`Failed with ${e.message} while adding` +
|
|
` extra folder ${newFolder.get_uri()}`
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
const getLocalFilesInfos =
|
|
async () => {
|
|
const childrenInfo =
|
|
await this.FileUtils.enumerateDir(
|
|
this._desktopDir,
|
|
cancellable,
|
|
GLib.PRIORITY_DEFAULT,
|
|
this.Enums.DEFAULT_ATTRIBUTES
|
|
);
|
|
|
|
childrenInfo?.forEach(info => {
|
|
const fileItem =
|
|
new IconCreator(
|
|
this.desktopManager,
|
|
this._desktopDir.get_child(info.get_name()),
|
|
info,
|
|
this.Enums.FileType.NONE,
|
|
null
|
|
);
|
|
|
|
if (fileItem.isHidden && !this.Prefs.showHidden) {
|
|
/* if there are hidden files in the desktop and the
|
|
user doesn't want to show them, remove the
|
|
coordinates. This ensures that if the user enables
|
|
showing them, they won't fight with other icons
|
|
for the same place
|
|
*/
|
|
if (fileItem.savedCoordinates) {
|
|
// only overwrite them if needed
|
|
fileItem.savedCoordinates = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
fileItem.savedCoordinates =
|
|
fileItem.savedCoordinates ?? null;
|
|
|
|
fileItem.dropCoordinates =
|
|
fileItem.dropCoordinates ?? null;
|
|
|
|
if (fileItem.savedCoordinates === null ||
|
|
fileItem.dropCoordinates === null
|
|
) {
|
|
const basename = fileItem.file.get_basename();
|
|
this._checkBasenameInPending(fileItem, basename);
|
|
}
|
|
|
|
fileList.push(fileItem);
|
|
});
|
|
};
|
|
|
|
const mountsItems =
|
|
this.DesktopIconsUtil.getMounts(this._volumeMonitor).map(
|
|
async ([newFolder, fileTypeEnum, gioMount]) => {
|
|
try {
|
|
if (imports.system.version < 17200) {
|
|
Gio._promisify(
|
|
newFolder.constructor.prototype,
|
|
'query_info_async'
|
|
);
|
|
}
|
|
|
|
const newFolderInfo =
|
|
await newFolder.query_info_async(
|
|
this.Enums.DEFAULT_ATTRIBUTES,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable
|
|
);
|
|
|
|
fileList.push(
|
|
new IconCreator(
|
|
this.desktopManager,
|
|
newFolder,
|
|
newFolderInfo,
|
|
fileTypeEnum,
|
|
gioMount
|
|
)
|
|
);
|
|
} catch (e) {
|
|
if (e.matches(
|
|
Gio.IOErrorEnum,
|
|
Gio.IOErrorEnum.CANCELLED)
|
|
)
|
|
throw e;
|
|
|
|
console.error(e,
|
|
`Failed with ${e.message} while ` +
|
|
`adding volume ${newFolder}`
|
|
);
|
|
}
|
|
});
|
|
|
|
await Promise.all(
|
|
[
|
|
getLocalFilesInfos(),
|
|
...extraFoldersItems,
|
|
...mountsItems,
|
|
]
|
|
);
|
|
|
|
if (this._desktopFilesChanged && !this._forceDraw)
|
|
return null;
|
|
|
|
return fileList;
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
|
console.error(e,
|
|
'Failed to read contents of' +
|
|
`${this._desktopDir.get_path()}`
|
|
);
|
|
}
|
|
|
|
return null;
|
|
} finally {
|
|
if (cancellable === this._desktopEnumerateCancellable)
|
|
this._desktopEnumerateCancellable = null;
|
|
}
|
|
}
|
|
|
|
_checkBasenameInPending(fileItem, basename) {
|
|
if (basename in this._pendingSelfCopyFiles) {
|
|
if (fileItem.savedCoordinates === null) {
|
|
fileItem.savedCoordinates =
|
|
this._pendingSelfCopyFiles[basename];
|
|
}
|
|
|
|
delete this._pendingSelfCopyFiles[basename];
|
|
return;
|
|
}
|
|
|
|
if (basename in this._pendingDropFiles) {
|
|
fileItem.dropCoordinates = this._pendingDropFiles[basename];
|
|
delete this._pendingDropFiles[basename];
|
|
return;
|
|
}
|
|
|
|
const regex = /\(.*\)[^()]*$/;
|
|
let basenameStart;
|
|
const lastParenthesisPosition = basename.search(regex);
|
|
|
|
if (lastParenthesisPosition > 1) {
|
|
basenameStart = basename.slice(0, lastParenthesisPosition - 1);
|
|
if (basenameStart) {
|
|
for (let fileName of Object.keys(this._pendingDropFiles)) {
|
|
if (fileName.startsWith(basenameStart)) {
|
|
fileItem.dropCoordinates =
|
|
this._pendingDropFiles[fileName];
|
|
|
|
delete this._pendingDropFiles[fileName];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_metadataChanged(proxy, nameOwner, args) {
|
|
const filepath = GLib.build_filenamev([GLib.get_home_dir(), args[1]]);
|
|
|
|
if (this._desktopDir.get_path() === GLib.path_get_dirname(filepath)) {
|
|
for (let fileItem of this._fileList) {
|
|
if (fileItem.path === filepath) {
|
|
fileItem.updatedMetadata();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onMountAdded() {
|
|
this._updateFileList().catch(e => {
|
|
console.log(
|
|
'Exception while updating Desktop after a' +
|
|
` mount was added: ${e.message}\n${e.stack}`
|
|
);
|
|
});
|
|
}
|
|
|
|
onMountRemoved() {
|
|
this._updateFileList().catch(e => {
|
|
console.log(
|
|
'Exception while updating Desktop after a ' +
|
|
`mount was removed: ${e.message}\n${e.stack}`
|
|
);
|
|
});
|
|
}
|
|
|
|
fileExistsOnDesktop(searchName) {
|
|
const listOfFileNamesOnDesktop = this._fileList.map(f => f.fileName);
|
|
|
|
if (listOfFileNamesOnDesktop.includes(searchName))
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
getDesktopUniqueFileName(fileName) {
|
|
let fileParts = this.DesktopIconsUtil.getFileExtensionOffset(fileName);
|
|
let i = 0;
|
|
let newName = fileName;
|
|
|
|
while (this.fileExistsOnDesktop(newName)) {
|
|
i += 1;
|
|
newName = `${fileParts.basename} ${i}${fileParts.extension}`;
|
|
}
|
|
return newName;
|
|
}
|
|
|
|
async getFileList() {
|
|
const fileList = await this._doReadAsync();
|
|
this._fileList = fileList;
|
|
return fileList;
|
|
}
|
|
|
|
async reLoadFileList() {
|
|
await this._updateFileList().catch(e => logError(e));
|
|
}
|
|
|
|
get fileList() {
|
|
return this._fileList;
|
|
}
|
|
|
|
get _desktops() {
|
|
return this.windowManager.desktops;
|
|
}
|
|
|
|
get _pendingDropFiles() {
|
|
return this.desktopManager.pendingDropFiles;
|
|
}
|
|
|
|
get _pendingSelfCopyFiles() {
|
|
return this.desktopManager.pendingSelfCopyFiles;
|
|
}
|
|
|
|
get desktopDir() {
|
|
return this._desktopDir;
|
|
}
|
|
};
|
|
|