├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── extension.js ├── metadata.json ├── prefs.json ├── window-search-provider-fuzzy.png └── window-search-provider-regex.png /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .pytest_cache 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Kay-Uwe (Kiwi) Lorenz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | extension-package: 2 | rm -rf dist 3 | mkdir -p dist 4 | zip dist/window-search-provider.zip extension.js LICENSE.md metadata.json README.md 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE** - I stopped maintaining this project. I do not have the time to do the fixes from gnome version to gnome version. Last time the changes got so big, that I did not manage to do it. I am using now @G-dH 's [windows-search-provider](https://github.com/G-dH/windows-search-provider), which is amazing! 2 | 3 | 4 | Window Search Provider 5 | ====================== 6 | 7 | Find your current open windows with gnome shell search :) 8 | 9 | This is something I ever wanted to have again since I used compiz' windows 10 | matching in expose mode. 11 | 12 | So here it is: You can search current open windows using gnome search. Type 13 | windows key, then start typing to find your window. It will fuzzy-match 14 | application name and window title like you might know from 15 | [Sublime Text](http://www.sublimetext.com/) or [Atom](http://atom.io). 16 | Current algorithm does not yet order the matches by best score. 17 | 18 | If you start your search with a "/", it will interprete all search terms 19 | (separated by whitespace) as regular expressions, which have to match all. 20 | 21 | This extension was inspired by https://github.com/daniellandau/switcher/ 22 | 23 | 24 | Features 25 | -------- 26 | 27 | - Fuzzy match current open windows and activate selected 28 | 29 | - Match current open windows with a regex starting search term with "/" 30 | (spaces are substituted with ".*?") and activate selected 31 | 32 | - Start a search with !x and whitespace, then either fuzzy match or match 33 | regex to close *all* selected windows. 34 | 35 | Example: "!x specsu" to close all windows labelled "Spec Suite". I 36 | tend to leave many atom package test windows open. So this is a killer 37 | feature ;) 38 | 39 | - End a search with "!x" to close all selected windows. (using feature 40 | above showed me, that I intuitively first selected windows to close, 41 | then had to edit the search text) 42 | 43 | Example: "specsu!x" to close all windows labelled "Spec Suite". 44 | 45 | - A "!" or "/" at end of a search string is ignored, if you have a "!" or "/" 46 | in your search term (other than describe above), it is part of search term 47 | as expected. 48 | 49 | - A "!" (or "/") + number like "!2" or "/2" at end of your search term will 50 | select the second matched window from list. 51 | 52 | Example: "code/2" will select the second window matching "code". This makes 53 | the search also more unique, such that you can simply hit return for 54 | activating the window. 55 | 56 | - I found "/" at end of windows faster to type than "!", so you can use both 57 | chars at end of string for options. 58 | 59 | Preferences 60 | ----------- 61 | 62 | Preferences file is read, whenever you enable the extension. 63 | 64 | There is some rudimentary preference support now. For simplicity, this reads 65 | prefs from a JSON file `prefs.json` in following order. The first existing is 66 | taken: 67 | 68 | - `~/.config/gnome-shell-window-search-provider/prefs.json` 69 | - `/prefs.json` 70 | 71 | What you can configure: 72 | 73 | ```json 74 | { 75 | "searchPrefix": ["kw", "p"], 76 | "useAppInfo": true, 77 | "debug": false 78 | } 79 | ``` 80 | 81 | - `searchPrefix` is either a string or a list of strings, specifying optional 82 | prefixes to boost your window search over application results. The prefix 83 | will not be considered in searches. 84 | 85 | - `useAppInfo` - (this did not work in Gnome 40) if off, results displayed like 86 | application search results, if on, results are displayed like other search results 87 | (with more info) 88 | 89 | - `debug` - Display very verbose logging in `journalctl /usr/bin/gnome-shell` 90 | 91 | Installation 92 | ------------ 93 | 94 | You can install this extension from https://extensions.gnome.org or simply 95 | clone this repository into your extension folder: 96 | ``` 97 | $ cd ~/.local/share/gnome-shell/extensions 98 | $ git clone https://github.com/klorenz/gnome-shell-window-search-provider.git window-search-provider@quelltexter.org 99 | $ gnome-shell-extension-tool -e "window-search-provider" 100 | ``` 101 | 102 | You can also download a zip of this repo and install using [Gnome Tweak Tool](https://wiki.gnome.org/Apps/GnomeTweakTool). 103 | 104 | 105 | Screenshots 106 | ----------- 107 | 108 | Match all windows containing (case insensitive) characters "e" "x" "t" "a" and "t" in that order: 109 | 110 | ![Screenshot fuzzy search](https://github.com/klorenz/gnome-shell-window-search-provider/blob/master/window-search-provider-fuzzy.png) 111 | 112 | Match all windows matching "ext" and "at" regular expressions: 113 | 114 | ![Screenshot regex search](https://github.com/klorenz/gnome-shell-window-search-provider/blob/master/window-search-provider-regex.png) 115 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | // https://github.com/daniellandau/switcher/blob/master/extension.js 2 | // https://extensions.gnome.org/extension/783/tracker-search-provider/ 3 | // https://github.com/hamiller/tracker-search-provider 4 | // https://git.gnome.org/browse/gnome-weather/tree/src/service/searchProvider.js 5 | // https://gjs.guide/extensions/ 6 | // https://gjs-docs.gnome.org/gio20~2.0/ 7 | // gnome shell extension ts getInitialResultSet 2023 8 | // view log: journalctl /usr/bin/gnome-session -f -o cat 9 | 10 | 11 | const Meta = imports.gi.Meta; 12 | const St = imports.gi.St; 13 | const Lang = imports.lang; 14 | const Main = imports.ui.main; 15 | const Shell = imports.gi.Shell; 16 | const ExtensionUtils = imports.misc.extensionUtils; 17 | 18 | const Me = ExtensionUtils.getCurrentExtension(); 19 | 20 | var windowSearchProvider = null; 21 | 22 | var windowSearchProviderDebug = false; 23 | 24 | const {Gio, GLib} = imports.gi; 25 | 26 | 27 | function logDebug(a,b,c,d,e) { 28 | if (windowSearchProviderDebug) { 29 | global.log.apply(this, [ 'WINDOW SEARCH PROVIDER', 'DEBUG' ].concat([].slice.call(arguments))) 30 | } 31 | } 32 | 33 | function logError(a,b,c,d,e) { 34 | global.log.apply(this, [ 'WINDOW SEARCH PROVIDER', 'ERROR' ].concat([].slice.call(arguments))) 35 | } 36 | 37 | function logWarning(a,b,c,d,e) { 38 | global.log.apply(this, [ 'WINDOW SEARCH PROVIDER', 'WARNING' ].concat([].slice.call(arguments))) 39 | } 40 | 41 | function logInfo(a,b,c,d,e) { 42 | global.log.apply(this, [ 'WINDOW SEARCH PROVIDER', 'INFO' ].concat([].slice.call(arguments))) 43 | } 44 | 45 | 46 | const myPrefs = { 47 | useAppInfo: true, 48 | 49 | // optional search prefix to make search unique (applicantion matcher is quite aggressive) 50 | searchPrefix: "w" 51 | } 52 | 53 | function fuzzyMatch(term, text) { 54 | var pos = -1; 55 | var matches = []; 56 | var _text = text.toLowerCase(); 57 | var _term = term.toLowerCase(); 58 | logDebug("fuzzyTerm: " + _term); 59 | logDebug("fuzzyText: " + _text); 60 | 61 | for (var i = 0; i < _term.length; i++) { 62 | var c = _term[i]; 63 | while (true) { 64 | pos += 1; 65 | if (pos >= _text.length) { 66 | return false; 67 | } 68 | if (_text[pos] == c) { 69 | matches.push(pos); 70 | break; 71 | } 72 | } 73 | } 74 | logDebug("matches: " + matches.join(" ")) 75 | 76 | return matches; 77 | } 78 | 79 | 80 | function makeResult(window, i) { 81 | const app = Shell.WindowTracker.get_default().get_window_app(window); 82 | // for (var k in window) { 83 | // logDebug("window."+k); 84 | // } 85 | 86 | try { 87 | const appName = app.get_name(); 88 | const windowTitle = window.get_title(); 89 | return { 90 | 'id': i, 91 | 'name': appName + ": " + windowTitle, 92 | 'appName': appName, 93 | 'windowTitle': windowTitle, 94 | 'window': window 95 | } 96 | } 97 | catch (e) { 98 | logWarning("Cannot create result (" + i + ") for window " + window + ": " + e); 99 | 100 | return { 101 | 'id': i, 102 | 'window': window, 103 | 'name': "Not Available" 104 | } 105 | } 106 | } 107 | 108 | 109 | const WindowSearchProvider = new Lang.Class({ 110 | Name: 'WindowSearchProvider', 111 | // appInfo: true, 112 | 113 | canLaunchSearch: true, 114 | isRemoteProvider: false, 115 | 116 | _init: function (title, categoryType) { 117 | logDebug(`title: ${title}, cat: ${categoryType}`) 118 | let prefs_file = "unkown" 119 | 120 | try { 121 | 122 | const default_prefs_file = Me.dir.get_path() + "/prefs.json" 123 | const config_prefs_file = GLib.get_home_dir() + "/.config/gnome-shell-window-search-provider/prefs.json" 124 | 125 | const cpf = Gio.File.new_for_path(config_prefs_file) 126 | 127 | if (cpf.query_exists(null)) { 128 | prefs_file = config_prefs_file 129 | } else { 130 | prefs_file = default_prefs_file 131 | } 132 | 133 | logInfo("Read Prefs "+prefs_file) 134 | let byte_array = GLib.file_get_contents(prefs_file)[1] 135 | let decoder = new TextDecoder("utf-8") 136 | 137 | logDebug("fileContents "+byte_array) 138 | this.prefs = JSON.parse(decoder.decode(byte_array)) 139 | 140 | } catch(e) { 141 | logError("Error reading prefs file "+prefs_file+": "+e) 142 | this.prefs = myPrefs 143 | } 144 | 145 | if (this.prefs.debug) { 146 | windowSearchProviderDebug = true 147 | } else { 148 | windowSearchProviderDebug = false 149 | } 150 | 151 | if (this.prefs.useAppInfo) { 152 | this.appInfo = Gio.AppInfo.get_all().filter(function (appInfo) { 153 | try { 154 | let id = appInfo.get_id() 155 | return id.match(/gnome-session-properties/) 156 | } 157 | catch (e) { 158 | return null 159 | } 160 | })[0] 161 | 162 | this.appInfo.get_name = function () { 163 | return 'Windows'; 164 | }; 165 | } 166 | 167 | 168 | this.windows = null; 169 | 170 | logDebug("_init"); 171 | }, 172 | 173 | _getResultSet: function (terms) { 174 | logDebug("getResultSet"); 175 | var resultIds = []; 176 | var candidates = this.windows; 177 | var _terms = [].concat(terms); 178 | var match = null; 179 | var m; 180 | this.action = "activate"; 181 | var selection = null; 182 | 183 | logDebug("Getting results for terms "+terms) 184 | 185 | try { 186 | var searchPrefixes = this.prefs.searchPrefix; 187 | 188 | if (!(searchPrefixes instanceof Array)) { 189 | searchPrefixes = [ searchPrefixes ] 190 | } 191 | 192 | for (var i = 0; i < searchPrefixes.length; i++) { 193 | let searchPrefix = searchPrefixes[i] 194 | 195 | if (_terms[0].length >= searchPrefix.length && _terms[0].substring(0, searchPrefix.length) == searchPrefix) { 196 | logDebug("removing Prefix "+searchPrefix) 197 | _terms[0] = _terms[0].substring(searchPrefix.length) 198 | } 199 | } 200 | } catch(e) { 201 | logWarning("Cannot find/remove search prefix: "+e) 202 | } 203 | 204 | // action may be at start 205 | if (_terms.length > 1 && _terms[0][0] === '!') { 206 | if (_terms[0].toLowerCase === "!x") { 207 | this.action = 'close'; 208 | } 209 | _terms = _terms.slice(1) 210 | 211 | } else if (_terms[_terms.length - 1].match(/(.*)[!\/]x$/)) { 212 | m = _terms[_terms.length - 1].match(/(.*)[!\/]x$/); 213 | // or at end 214 | this.action = 'close' 215 | if (m[1] !== '') { 216 | _terms[_terms.length - 1] = m[1] 217 | } else { 218 | _terms.pop() 219 | } 220 | } else if (_terms[_terms.length - 1].match(/(.*)[!\/](\d+)$/)) { 221 | m = _terms[_terms.length - 1].match(/(.*)[!\/](\d+)$/); 222 | selection = parseInt(m[2]) 223 | if (m[1] !== '') { 224 | _terms[_terms.length - 1] = m[1] 225 | } else { 226 | _terms.pop() 227 | } 228 | } else if (_terms[_terms.length - 1].match(/(.*)[!\/]$/)) { 229 | m = _terms[_terms.length - 1].match(/(.*)[!\/]$/); 230 | if (m[1] !== '') { 231 | _terms[_terms.length - 1] = m[1] 232 | } else { 233 | _terms.pop() 234 | } 235 | } 236 | 237 | if (_terms[0][0] == "/") { 238 | var regex = new RegExp(_terms.join('.*?').substring(1), 'i'); 239 | match = function (s) { 240 | var m = s.match(regex); return m ? m[0].length+1 : false; } 241 | } 242 | else { 243 | var term = _terms.join(''); 244 | match = function (s) { var m = fuzzyMatch(term, s); return m ? m[m.length - 1] - m[0] : false; } 245 | } 246 | var results = []; 247 | 248 | for (var key in candidates) { 249 | logDebug("match candidate: "+candidates[key].name); 250 | var m = match(candidates[key].name); 251 | 252 | if (m !== false) { 253 | results.push({ weight: m, id: key }); 254 | } 255 | } 256 | results.sort(function (a, b) { if (a.weight < b.weight) return -1; if (a.weight > b.weight) return 1; return 0 }); 257 | 258 | this.resultIds = results.map(function (item) { return item.id }); 259 | 260 | // let the user select number of match 261 | if (selection !== null) { 262 | if (selection > results.length) { 263 | return []; 264 | } 265 | return [ results[selection-1].id ] 266 | } 267 | 268 | logDebug("resultSet: ", this.resultIds); 269 | 270 | return this.resultIds; 271 | }, 272 | 273 | getResultMetas: function (resultIds, callback) { 274 | logDebug("result metas for name: " + resultIds.join(" ")); 275 | let _this = this; 276 | let metas = resultIds.map(function (id) { return _this._getResultMeta(id); }); 277 | logDebug("metas: " + metas.join(" ")); 278 | logDebug("callback: " + callback) 279 | 280 | if (typeof callback === "function") { 281 | logDebug("metas called with callback") 282 | callback(metas); 283 | } else { 284 | logDebug("metas NOT called with callback: " + callback) 285 | return metas; 286 | } 287 | }, 288 | 289 | _getResultMeta: function (resultId) { 290 | logDebug("getResultMeta: " + resultId); 291 | var result = this.windows[resultId]; 292 | const app = Shell.WindowTracker.get_default().get_window_app(result.window); 293 | logDebug("result meta for name: " + result.name); 294 | logDebug("result meta: " + resultId); 295 | return { 296 | 'id': resultId, 297 | 'name': result.windowTitle, 298 | // TODO: do highlighting of search term (i.e. for fuzzy matching) 299 | // 'description': "hello "+result.windowTitle, 300 | 'description': result.appName, 301 | 'createIcon': function (size) { 302 | logDebug('createIcon size='+size); 303 | return app.create_icon_texture(size); 304 | } 305 | } 306 | }, 307 | 308 | activateResult: function (resultId, terms) { 309 | logDebug("action: " + this.action) 310 | if (this.action === "activate") { 311 | var result = this.windows[resultId] 312 | logDebug("activateResult: " + result) 313 | Main.activateWindow(result.window) 314 | } else if (this.action === "close") { 315 | for (var i = 0; i < this.resultIds.length; i++) { 316 | const win = this.windows[this.resultIds[i]] 317 | const actor = win.window.get_compositor_private() 318 | try { 319 | const meta = actor.get_meta_window() 320 | meta.delete(global.get_current_time()) 321 | } 322 | catch (e) { 323 | logDebug("my error") 324 | logDebug(e) 325 | } 326 | } 327 | Main.overview.hide(); 328 | } 329 | }, 330 | 331 | launchSearch: function (result) { 332 | logDebug("launchSearch: " + result); 333 | // Main.activateWindow(result.window); 334 | }, 335 | 336 | getInitialResultSet: function (terms, callback, cancellable) { 337 | logDebug("getInitialResultSet: " + terms.join(" ") + " callback: " + callback + " cancellable: " + cancellable); 338 | var windows = null 339 | this.windows = windows = {}; 340 | global.display.get_tab_list(Meta.TabList.NORMAL, null).map(function (v, i) { windows['w' + i] = makeResult(v, 'w' + i) }); 341 | //global.get_window_actors().map(function(v,i) { windows['w'+i] = makeResult(v,'w'+i) }); 342 | logDebug("getInitialResultSet: " + this.windows); 343 | //logDebug("window id", windows[0].get_id()); 344 | var resultSet = this._getResultSet(terms); 345 | logDebug("getInitialResultSet resultSet: " + resultSet); 346 | 347 | // in the past this was a function, since Gnome 43 it is a cancellable 348 | if (typeof callback === "function") { 349 | logDebug("callback " + callback); 350 | callback(resultSet); 351 | } else { 352 | logDebug("return resultset") 353 | return resultSet || [] 354 | } 355 | }, 356 | 357 | filterResults: function (results, maxResults) { 358 | logDebug("filterResults", results, maxResults); 359 | //return results.slice(0, maxResults); 360 | return results; 361 | }, 362 | 363 | getSubsearchResultSet: function (previousResults, terms, callback, cancellable) { 364 | logDebug("getSubSearchResultSet: " + terms.join(" ")); 365 | this.getInitialResultSet(terms, callback, cancellable); 366 | }, 367 | 368 | // createIcon: function (size, meta) { 369 | // logDebug("createIcon"); 370 | // // TODO: implement meta icon? 371 | // }, 372 | 373 | // createResultOjbect: function (resultMeta) { 374 | // const app = Shell.WindowTracker.get_default().get_window_app(resultMeta.id); 375 | // return new AppIcon(app); 376 | // } 377 | 378 | }); 379 | 380 | function init() { 381 | } 382 | 383 | function getOverviewSearchResult() { 384 | if (Main.overview.viewSelector !== undefined) { 385 | return Main.overview.viewSelector._searchResults; 386 | } else { 387 | return Main.overview._overview.controls._searchController._searchResults; 388 | } 389 | } 390 | 391 | function enable() { 392 | global.log("*** enable window search provider"); 393 | global.log("windowSearchProvider", windowSearchProvider) 394 | 395 | if (windowSearchProvider == null) { 396 | logDebug("enable window search provider"); 397 | windowSearchProvider = new WindowSearchProvider(); 398 | 399 | //Main.overview.addSearchProvider(windowSearchProvider); 400 | //log("main.overview", moan) 401 | getOverviewSearchResult()._registerProvider( 402 | windowSearchProvider 403 | ); 404 | } 405 | } 406 | 407 | function disable() { 408 | if (windowSearchProvider) { 409 | global.log("*** disable window search provider"); 410 | // Main.overview.removeSearchProvider(windowSearchProvider) 411 | getOverviewSearchResult()._unregisterProvider( 412 | windowSearchProvider 413 | ); 414 | windowSearchProvider = null; 415 | } 416 | } 417 | 418 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "window-search-provider@quelltexter.org", 3 | "name": "Window Search Provider", 4 | "description": "Provide active windows as search results in overview", 5 | "shell-version": [ 6 | "3.16", 7 | "3.18", 8 | "3.20", 9 | "3.22", 10 | "3.24", 11 | "3.26", 12 | "3.28", 13 | "3.30", 14 | "3.32", 15 | "3.34", 16 | "3.36", 17 | "3.38", 18 | "3.40", 19 | "40", 20 | "42", 21 | "43", 22 | "44", 23 | "45" 24 | ], 25 | "url": "https://github.com/klorenz/gnome-shell-window-search-provider" 26 | } 27 | -------------------------------------------------------------------------------- /prefs.json: -------------------------------------------------------------------------------- 1 | { 2 | "searchPrefix": ["kw", "p"], 3 | "useAppInfo": true, 4 | "debug": false 5 | } 6 | -------------------------------------------------------------------------------- /window-search-provider-fuzzy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klorenz/gnome-shell-window-search-provider/f86272cea8573539fba5760e196e940369c002b6/window-search-provider-fuzzy.png -------------------------------------------------------------------------------- /window-search-provider-regex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klorenz/gnome-shell-window-search-provider/f86272cea8573539fba5760e196e940369c002b6/window-search-provider-regex.png --------------------------------------------------------------------------------