2025-02-09 23:09:13 +01:00
|
|
|
/**
|
2025-02-09 23:13:53 +01:00
|
|
|
* V-Shell (Vertical Workspaces)
|
2025-02-09 23:09:13 +01:00
|
|
|
* windowSearchProvider.js
|
|
|
|
*
|
|
|
|
* @author GdH <G-dH@github.com>
|
|
|
|
* @copyright 2022 -2023
|
|
|
|
* @license GPL-3.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
const Gio = imports.gi.Gio;
|
|
|
|
const GLib = imports.gi.GLib;
|
|
|
|
const Meta = imports.gi.Meta;
|
|
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const St = imports.gi.St;
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
const Main = imports.ui.main;
|
2025-02-09 23:13:53 +01:00
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
let Me;
|
|
|
|
let opt;
|
2025-02-09 23:13:53 +01:00
|
|
|
// gettext
|
2025-02-09 23:16:18 +01:00
|
|
|
let _;
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
// prefix helps to eliminate results from other search providers
|
|
|
|
// so it needs to be something less common
|
|
|
|
// needs to be accessible from vw module
|
2025-02-09 23:16:18 +01:00
|
|
|
const PREFIX = 'wq//';
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
const Action = {
|
|
|
|
NONE: 0,
|
|
|
|
CLOSE: 1,
|
|
|
|
CLOSE_ALL: 2,
|
|
|
|
MOVE_TO_WS: 3,
|
2025-02-09 23:13:53 +01:00
|
|
|
MOVE_ALL_TO_WS: 4,
|
|
|
|
};
|
2025-02-09 23:09:13 +01:00
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
var WindowSearchProviderModule = class {
|
|
|
|
// export for other modules
|
|
|
|
static _PREFIX = PREFIX;
|
|
|
|
constructor(me) {
|
|
|
|
Me = me;
|
|
|
|
opt = Me.opt;
|
|
|
|
_ = Me.gettext;
|
|
|
|
|
|
|
|
this._firstActivation = true;
|
|
|
|
this.moduleEnabled = false;
|
|
|
|
// export for other modules
|
2025-02-09 23:09:13 +01:00
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
this._windowSearchProvider = null;
|
|
|
|
this._enableTimeoutId = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanGlobals() {
|
|
|
|
Me = null;
|
2025-02-09 23:09:13 +01:00
|
|
|
opt = null;
|
2025-02-09 23:16:18 +01:00
|
|
|
_ = null;
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
2025-02-09 23:16:18 +01:00
|
|
|
|
|
|
|
update(reset) {
|
|
|
|
this.moduleEnabled = opt.get('windowSearchProviderModule');
|
|
|
|
|
|
|
|
reset = reset || !this.moduleEnabled;
|
|
|
|
|
|
|
|
if (reset && !this._firstActivation) {
|
|
|
|
this._disableModule();
|
|
|
|
} else if (!reset) {
|
|
|
|
this._firstActivation = false;
|
|
|
|
this._activateModule();
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
2025-02-09 23:16:18 +01:00
|
|
|
if (reset && this._firstActivation)
|
|
|
|
console.debug(' WindowSearchProviderModule - Keeping untouched');
|
|
|
|
}
|
2025-02-09 23:09:13 +01:00
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
_activateModule() {
|
|
|
|
// delay because Fedora had problem to register a new provider soon after Shell restarts
|
|
|
|
this._enableTimeoutId = GLib.timeout_add(
|
|
|
|
GLib.PRIORITY_DEFAULT,
|
|
|
|
2000,
|
|
|
|
() => {
|
|
|
|
if (!this._windowSearchProvider) {
|
|
|
|
this._windowSearchProvider = new WindowSearchProvider(opt);
|
|
|
|
this._getOverviewSearchResult()._registerProvider(this._windowSearchProvider);
|
|
|
|
}
|
|
|
|
this._enableTimeoutId = 0;
|
|
|
|
return GLib.SOURCE_REMOVE;
|
|
|
|
}
|
2025-02-09 23:09:13 +01:00
|
|
|
);
|
2025-02-09 23:16:18 +01:00
|
|
|
console.debug(' WindowSearchProviderModule - Activated');
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
2025-02-09 23:16:18 +01:00
|
|
|
|
|
|
|
_disableModule() {
|
|
|
|
if (this._windowSearchProvider) {
|
|
|
|
this._getOverviewSearchResult()._unregisterProvider(this._windowSearchProvider);
|
|
|
|
this._windowSearchProvider = null;
|
|
|
|
}
|
|
|
|
if (this._enableTimeoutId) {
|
|
|
|
GLib.source_remove(this._enableTimeoutId);
|
|
|
|
this._enableTimeoutId = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.debug(' WindowSearchProviderModule - Disabled');
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
2025-02-09 23:16:18 +01:00
|
|
|
|
|
|
|
_getOverviewSearchResult() {
|
|
|
|
return Main.overview._overview.controls._searchController._searchResults;
|
|
|
|
}
|
|
|
|
};
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
const closeSelectedRegex = /^\/x!$/;
|
|
|
|
const closeAllResultsRegex = /^\/xa!$/;
|
|
|
|
const moveToWsRegex = /^\/m[0-9]+$/;
|
|
|
|
const moveAllToWsRegex = /^\/ma[0-9]+$/;
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
const WindowSearchProvider = class WindowSearchProvider {
|
|
|
|
constructor() {
|
2025-02-09 23:16:18 +01:00
|
|
|
this.id = 'open-windows';
|
|
|
|
const appSystem = Shell.AppSystem.get_default();
|
|
|
|
// use arbitrary app to get complete appInfo object
|
|
|
|
let appInfo = appSystem.lookup_app('com.matjakeman.ExtensionManager.desktop')?.get_app_info();
|
|
|
|
if (!appInfo)
|
|
|
|
appInfo = appSystem.lookup_app('org.gnome.Extensions.desktop')?.get_app_info();
|
|
|
|
if (!appInfo)
|
|
|
|
appInfo = Gio.AppInfo.create_from_commandline('true', _('Open Windows'), null);
|
|
|
|
appInfo.get_description = () => _('Search open windows');
|
|
|
|
appInfo.get_name = () => _('Open Windows');
|
|
|
|
appInfo.get_id = () => this.id;
|
|
|
|
appInfo.get_icon = () => Gio.icon_new_for_string('focus-windows-symbolic');
|
|
|
|
appInfo.should_show = () => true;
|
|
|
|
|
|
|
|
this.appInfo = appInfo;
|
|
|
|
this.canLaunchSearch = false;
|
2025-02-09 23:09:13 +01:00
|
|
|
this.isRemoteProvider = false;
|
|
|
|
|
|
|
|
this.action = 0;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
_getResultSet(terms) {
|
2025-02-09 23:09:13 +01:00
|
|
|
// do not modify original terms
|
|
|
|
let termsCopy = [...terms];
|
|
|
|
// search for terms without prefix
|
2025-02-09 23:16:18 +01:00
|
|
|
termsCopy[0] = termsCopy[0].replace(PREFIX, '');
|
2025-02-09 23:09:13 +01:00
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
/* if (gOptions.get('searchWindowsCommands')) {
|
2025-02-09 23:09:13 +01:00
|
|
|
this.action = 0;
|
|
|
|
this.targetWs = 0;
|
|
|
|
|
|
|
|
const lastTerm = terms[terms.length - 1];
|
|
|
|
if (lastTerm.match(closeSelectedRegex)) {
|
|
|
|
this.action = Action.CLOSE;
|
|
|
|
} else if (lastTerm.match(closeAllResultsRegex)) {
|
|
|
|
this.action = Action.CLOSE_ALL;
|
|
|
|
} else if (lastTerm.match(moveToWsRegex)) {
|
|
|
|
this.action = Action.MOVE_TO_WS;
|
|
|
|
} else if (lastTerm.match(moveAllToWsRegex)) {
|
|
|
|
this.action = Action.MOVE_ALL_TO_WS;
|
|
|
|
}
|
|
|
|
if (this.action) {
|
|
|
|
terms.pop();
|
|
|
|
if (this.action === Action.MOVE_TO_WS || this.action === Action.MOVE_ALL_TO_WS) {
|
|
|
|
this.targetWs = parseInt(lastTerm.replace(/^[^0-9]+/, '')) - 1;
|
|
|
|
}
|
|
|
|
} else if (lastTerm.startsWith('/')) {
|
|
|
|
terms.pop();
|
|
|
|
}
|
|
|
|
}*/
|
|
|
|
|
|
|
|
const candidates = this.windows;
|
|
|
|
const _terms = [].concat(termsCopy);
|
2025-02-09 23:13:53 +01:00
|
|
|
// let match;
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
const term = _terms.join(' ');
|
2025-02-09 23:13:53 +01:00
|
|
|
/* match = s => {
|
2025-02-09 23:09:13 +01:00
|
|
|
return fuzzyMatch(term, s);
|
2025-02-09 23:13:53 +01:00
|
|
|
}; */
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
const results = [];
|
|
|
|
let m;
|
|
|
|
for (let key in candidates) {
|
2025-02-09 23:13:53 +01:00
|
|
|
if (opt.SEARCH_FUZZY)
|
2025-02-09 23:16:18 +01:00
|
|
|
m = Me.Util.fuzzyMatch(term, candidates[key].name);
|
2025-02-09 23:13:53 +01:00
|
|
|
else
|
2025-02-09 23:16:18 +01:00
|
|
|
m = Me.Util.strictMatch(term, candidates[key].name);
|
2025-02-09 23:13:53 +01:00
|
|
|
|
|
|
|
if (m !== -1)
|
2025-02-09 23:09:13 +01:00
|
|
|
results.push({ weight: m, id: key });
|
|
|
|
}
|
|
|
|
|
|
|
|
results.sort((a, b) => a.weight > b.weight);
|
|
|
|
const currentWs = global.workspace_manager.get_active_workspace_index();
|
|
|
|
// prefer current workspace
|
2025-02-09 23:16:18 +01:00
|
|
|
switch (opt.WINDOW_SEARCH_ORDER) {
|
|
|
|
case 1: // MRU - current ws first
|
|
|
|
results.sort((a, b) => (this.windows[a.id].window.get_workspace().index() !== currentWs) && (this.windows[b.id].window.get_workspace().index() === currentWs));
|
|
|
|
break;
|
|
|
|
case 2: // MRU - by workspace
|
|
|
|
results.sort((a, b) => this.windows[a.id].window.get_workspace().index() > this.windows[b.id].window.get_workspace().index());
|
|
|
|
break;
|
|
|
|
case 3: // Stable sequence - by workspace
|
|
|
|
results.sort((a, b) => this.windows[a.id].window.get_stable_sequence() > this.windows[b.id].window.get_stable_sequence());
|
|
|
|
results.sort((a, b) => this.windows[a.id].window.get_workspace().index() > this.windows[b.id].window.get_workspace().index());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
results.sort((a, b) => (_terms !== ' ') && (a.weight > 0 && b.weight === 0));
|
2025-02-09 23:09:13 +01:00
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
this.resultIds = results.map(item => item.id);
|
2025-02-09 23:09:13 +01:00
|
|
|
return this.resultIds;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
getResultMetas(resultIds, callback = null) {
|
|
|
|
const metas = resultIds.map(id => this.getResultMeta(id));
|
2025-02-09 23:16:18 +01:00
|
|
|
if (Me.shellVersion >= 43)
|
2025-02-09 23:09:13 +01:00
|
|
|
return new Promise(resolve => resolve(metas));
|
2025-02-09 23:13:53 +01:00
|
|
|
else
|
2025-02-09 23:09:13 +01:00
|
|
|
callback(metas);
|
2025-02-09 23:13:53 +01:00
|
|
|
return null;
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
getResultMeta(resultId) {
|
2025-02-09 23:09:13 +01:00
|
|
|
const result = this.windows[resultId];
|
|
|
|
const wsIndex = result.window.get_workspace().index();
|
|
|
|
const app = Shell.WindowTracker.get_default().get_window_app(result.window);
|
|
|
|
return {
|
|
|
|
'id': resultId,
|
|
|
|
'name': `${wsIndex + 1}: ${result.windowTitle}`,
|
|
|
|
'description': result.appName,
|
2025-02-09 23:13:53 +01:00
|
|
|
'createIcon': size => {
|
2025-02-09 23:09:13 +01:00
|
|
|
return app
|
|
|
|
? app.create_icon_texture(size)
|
|
|
|
: new St.Icon({ icon_name: 'icon-missing', icon_size: size });
|
2025-02-09 23:13:53 +01:00
|
|
|
},
|
|
|
|
};
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
makeResult(window, i) {
|
|
|
|
const app = Shell.WindowTracker.get_default().get_window_app(window);
|
|
|
|
const appName = app ? app.get_name() : 'Unknown';
|
|
|
|
const windowTitle = window.get_title();
|
|
|
|
const wsIndex = window.get_workspace().index();
|
|
|
|
|
|
|
|
return {
|
|
|
|
'id': i,
|
|
|
|
// convert all accented chars to their basic form and lower case for search
|
|
|
|
'name': `${wsIndex + 1}: ${windowTitle} ${appName}`.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(),
|
|
|
|
appName,
|
|
|
|
windowTitle,
|
|
|
|
window,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
launchSearch(/* terms, timeStamp*/) {
|
2025-02-09 23:16:18 +01:00
|
|
|
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
activateResult(resultId/* , terms, timeStamp*/) {
|
2025-02-09 23:16:18 +01:00
|
|
|
const isCtrlPressed = Me.Util.isCtrlPressed();
|
|
|
|
const isShiftPressed = Me.Util.isShiftPressed();
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
this.action = 0;
|
|
|
|
this.targetWs = 0;
|
|
|
|
|
|
|
|
this.targetWs = global.workspaceManager.get_active_workspace().index() + 1;
|
2025-02-09 23:13:53 +01:00
|
|
|
if (isShiftPressed && !isCtrlPressed)
|
2025-02-09 23:09:13 +01:00
|
|
|
this.action = Action.MOVE_TO_WS;
|
2025-02-09 23:13:53 +01:00
|
|
|
else if (isShiftPressed && isCtrlPressed)
|
2025-02-09 23:09:13 +01:00
|
|
|
this.action = Action.MOVE_ALL_TO_WS;
|
2025-02-09 23:13:53 +01:00
|
|
|
|
2025-02-09 23:09:13 +01:00
|
|
|
|
|
|
|
if (!this.action) {
|
|
|
|
const result = this.windows[resultId];
|
|
|
|
Main.activateWindow(result.window);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (this.action) {
|
|
|
|
case Action.CLOSE:
|
|
|
|
this._closeWindows([resultId]);
|
|
|
|
break;
|
|
|
|
case Action.CLOSE_ALL:
|
|
|
|
this._closeWindows(this.resultIds);
|
|
|
|
break;
|
|
|
|
case Action.MOVE_TO_WS:
|
|
|
|
this._moveWindowsToWs(resultId, [resultId]);
|
|
|
|
break;
|
|
|
|
case Action.MOVE_ALL_TO_WS:
|
|
|
|
this._moveWindowsToWs(resultId, this.resultIds);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_closeWindows(ids) {
|
|
|
|
let time = global.get_current_time();
|
2025-02-09 23:13:53 +01:00
|
|
|
for (let i = 0; i < ids.length; i++)
|
2025-02-09 23:09:13 +01:00
|
|
|
this.windows[ids[i]].window.delete(time + i);
|
2025-02-09 23:13:53 +01:00
|
|
|
|
2025-02-09 23:09:13 +01:00
|
|
|
Main.notify('Window Search Provider', `Closed ${ids.length} windows.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
_moveWindowsToWs(selectedId, resultIds) {
|
|
|
|
const workspace = global.workspaceManager.get_active_workspace();
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
for (let i = 0; i < resultIds.length; i++)
|
2025-02-09 23:09:13 +01:00
|
|
|
this.windows[resultIds[i]].window.change_workspace(workspace);
|
2025-02-09 23:13:53 +01:00
|
|
|
|
2025-02-09 23:09:13 +01:00
|
|
|
const selectedWin = this.windows[selectedId].window;
|
|
|
|
selectedWin.activate_with_workspace(global.get_current_time(), workspace);
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
getInitialResultSet(terms, callback/* , cancellable = null*/) {
|
|
|
|
// In GS 43 callback arg has been removed
|
|
|
|
/* if (shellVersion >= 43)
|
|
|
|
cancellable = callback;*/
|
|
|
|
|
2025-02-09 23:09:13 +01:00
|
|
|
let windows;
|
|
|
|
this.windows = windows = {};
|
|
|
|
global.display.get_tab_list(Meta.TabList.NORMAL, null).filter(w => w.get_workspace() !== null).map(
|
2025-02-09 23:13:53 +01:00
|
|
|
(v, i) => {
|
2025-02-09 23:16:18 +01:00
|
|
|
windows[`${i}-${v.get_id()}`] = this.makeResult(v, `${i}-${v.get_id()}`);
|
2025-02-09 23:13:53 +01:00
|
|
|
return windows[`${i}-${v.get_id()}`];
|
|
|
|
}
|
2025-02-09 23:09:13 +01:00
|
|
|
);
|
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
if (Me.shellVersion >= 43)
|
2025-02-09 23:09:13 +01:00
|
|
|
return new Promise(resolve => resolve(this._getResultSet(terms)));
|
2025-02-09 23:13:53 +01:00
|
|
|
else
|
2025-02-09 23:09:13 +01:00
|
|
|
callback(this._getResultSet(terms));
|
2025-02-09 23:16:18 +01:00
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
return null;
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
|
|
|
|
2025-02-09 23:13:53 +01:00
|
|
|
filterResults(results /* , maxResults*/) {
|
|
|
|
// return results.slice(0, maxResults);
|
2025-02-09 23:09:13 +01:00
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
getSubsearchResultSet(previousResults, terms, callback) {
|
|
|
|
if (Me.shellVersion < 43) {
|
|
|
|
this.getSubsearchResultSet42(terms, callback);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return this.getInitialResultSet(terms);
|
2025-02-09 23:09:13 +01:00
|
|
|
}
|
|
|
|
|
2025-02-09 23:16:18 +01:00
|
|
|
getSubsearchResultSet42(terms, callback) {
|
|
|
|
callback(this._getResultSet(terms));
|
|
|
|
}
|
2025-02-09 23:13:53 +01:00
|
|
|
};
|