From c100f3e975a3f4e6cadb07ab021e4a7cf6519490 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 3 Jun 2026 17:32:37 +0200 Subject: [PATCH] feat: add application blacklist to suppress shortcut when focused Adds a configurable list of applications that suppress the search shortcut when they have focus. Instead of swallowing the key event, the accelerator is dynamically ungrabbed so the application receives it natively (e.g. Ctrl+Space in IntelliJ). Settings: new 'blacklist-apps' gsettings key (string array of desktop IDs). Preferences: new 'Application Blacklist' section in the General page with a searchable app chooser dialog. Running applications are listed first with icons, followed by all other installed apps. Apps can be removed from the blacklist individually. Ref: icedman/search-light#160 --- extension.js | 52 +++- preferences/keys.js | 4 + prefs.js | 234 +++++++++++++++++- schemas/gschemas.compiled | Bin 1988 -> 2004 bytes ....shell.extensions.search-light.gschema.xml | 5 + ui/general.ui | 19 ++ 6 files changed, 311 insertions(+), 3 deletions(-) diff --git a/extension.js b/extension.js index d25c83a..3350170 100644 --- a/extension.js +++ b/extension.js @@ -96,10 +96,10 @@ export default class SearchLightExt extends Extension { case 'border-radius': break; case 'shortcut-search': - this._updateShortcut(); + if (this._shortcutsGrabbed) this._updateShortcut(); break; case 'secondary-shortcut-search': - this._updateShortcut2(); + if (this._shortcutsGrabbed) this._updateShortcut2(); break; case 'window-effect': { this._updateWindowEffect(); @@ -163,11 +163,17 @@ export default class SearchLightExt extends Extension { this._updateShortcut(); this._updateShortcut2(); + this._shortcutsGrabbed = true; this._updateCss(); this._useAnimations = this._settings.get_boolean('use-animations'); this._animationSpeed = this._settings.get_double('animation-speed'); + this._focusWindowId = global.display.connect( + 'notify::focus-window', + this._onFocusWindowChanged.bind(this), + ); + Main.overview.connectObject( 'overview-showing', this._onOverviewShowing.bind(this), @@ -222,6 +228,11 @@ export default class SearchLightExt extends Extension { } disable() { + if (this._focusWindowId) { + global.display.disconnect(this._focusWindowId); + this._focusWindowId = null; + } + this._hiTimer?.shutdown(); this._loTimer?.shutdown(); this._hiTimer = null; @@ -894,6 +905,43 @@ export default class SearchLightExt extends Extension { this._style.build('custom-search-light', styles); } + _isFocusedAppBlacklisted() { + let dominated = this.blacklist_apps || []; + if (dominated.length === 0) return false; + + let focusWindow = global.display.focus_window; + if (!focusWindow) return false; + + let tracker = Shell.WindowTracker.get_default(); + let app = tracker.get_window_app(focusWindow); + if (!app) return false; + + let appId = app.get_id(); + return dominated.includes(appId); + } + + _onFocusWindowChanged() { + if (this._isFocusedAppBlacklisted()) { + this._ungrabShortcuts(); + } else { + this._regrabShortcuts(); + } + } + + _ungrabShortcuts() { + if (this._shortcutsGrabbed === false) return; + this._shortcutsGrabbed = false; + this.accel.unlisten(); + this.accel2.unlisten(); + } + + _regrabShortcuts() { + if (this._shortcutsGrabbed === true) return; + this._shortcutsGrabbed = true; + this._updateShortcut(); + this._updateShortcut2(); + } + _toggle_search_light() { if (this._inOverview) return; if (!this._visible) { diff --git a/preferences/keys.js b/preferences/keys.js index d814065..1e2547f 100644 --- a/preferences/keys.js +++ b/preferences/keys.js @@ -115,6 +115,10 @@ export const SettingsKeys = () => { default_value: 100, widget_type: 'scale', }, + 'blacklist-apps': { + default_value: [], + widget_type: 'shortcut', + }, }); return settingsKeys; diff --git a/prefs.js b/prefs.js index 1b4b496..a6c59f3 100644 --- a/prefs.js +++ b/prefs.js @@ -3,6 +3,8 @@ import Gdk from 'gi://Gdk'; import Gtk from 'gi://Gtk'; import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Adw from 'gi://Adw'; import { ShortcutSettingWidget } from './shortcuts.js'; @@ -103,7 +105,7 @@ export default class Preferences extends ExtensionPreferences { } addButtonEvents(window, builder, settings) { - // + this._setupBlacklist(window, builder, settings); } fillPreferencesWindow(window) { @@ -163,6 +165,236 @@ export default class Preferences extends ExtensionPreferences { this.updateMonitors(); } + _setupBlacklist(window, builder, settings) { + let group = builder.get_object('blacklist-group'); + let addBtn = builder.get_object('blacklist-add-btn'); + + this._blacklistGroup = group; + this._blacklistSettings = settings; + this._blacklistWindow = window; + this._blacklistRows = []; + this._refreshBlacklistRows(); + + addBtn.connect('clicked', () => { + this._openAppChooser(); + }); + } + + _refreshBlacklistRows() { + this._blacklistRows.forEach((row) => { + this._blacklistGroup.remove(row); + }); + this._blacklistRows = []; + + let apps = this._blacklistSettings.get_strv('blacklist-apps'); + apps.forEach((appId) => { + let appInfo = Gio.DesktopAppInfo.new(appId); + let label = appInfo ? appInfo.get_display_name() : appId; + + let row = new Adw.ActionRow({ + title: label, + subtitle: appId, + }); + + if (appInfo && appInfo.get_icon()) { + let icon = new Gtk.Image({ + gicon: appInfo.get_icon(), + pixel_size: 32, + margin_end: 8, + }); + row.add_prefix(icon); + } + + let removeBtn = new Gtk.Button({ + icon_name: 'list-remove-symbolic', + valign: Gtk.Align.CENTER, + css_classes: ['flat'], + }); + removeBtn.connect('clicked', () => { + let current = this._blacklistSettings.get_strv('blacklist-apps'); + current = current.filter((id) => id !== appId); + this._blacklistSettings.set_strv('blacklist-apps', current); + this._refreshBlacklistRows(); + }); + + row.add_suffix(removeBtn); + this._blacklistGroup.add(row); + this._blacklistRows.push(row); + }); + } + + _getRunningAppIds() { + let runningCmds = new Set(); + try { + let procDir = Gio.File.new_for_path('/proc'); + let enumerator = procDir.enumerate_children( + 'standard::name,standard::type', + Gio.FileQueryInfoFlags.NONE, + null, + ); + let info; + while ((info = enumerator.next_file(null)) !== null) { + let name = info.get_name(); + if (!/^\d+$/.test(name)) continue; + try { + let cmdlineFile = Gio.File.new_for_path(`/proc/${name}/cmdline`); + let [ok, contents] = cmdlineFile.load_contents(null); + if (ok) { + let cmdline = new TextDecoder().decode(contents).split('\0')[0]; + let basename = cmdline.split('/').pop(); + if (basename) runningCmds.add(basename); + } + } catch (e) { + continue; + } + } + } catch (e) { + return []; + } + + let runningIds = []; + let allApps = Gio.AppInfo.get_all(); + for (let app of allApps) { + if (!app.should_show()) continue; + let exe = app.get_executable(); + if (exe && runningCmds.has(exe.split('/').pop())) { + runningIds.push(app.get_id()); + } + } + return runningIds; + } + + _openAppChooser() { + let dialog = new Adw.Window({ + modal: true, + transient_for: this._blacklistWindow, + title: 'Select Application', + default_width: 400, + default_height: 500, + }); + + let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); + let headerBar = new Adw.HeaderBar(); + box.append(headerBar); + + let searchEntry = new Gtk.SearchEntry({ + placeholder_text: 'Filter applications\u2026', + margin_start: 12, + margin_end: 12, + margin_top: 6, + margin_bottom: 6, + }); + box.append(searchEntry); + + let loadingLabel = new Gtk.Label({ + label: 'Loading application list\u2026', + vexpand: true, + valign: Gtk.Align.CENTER, + css_classes: ['dim-label'], + }); + box.append(loadingLabel); + + let scrolled = new Gtk.ScrolledWindow({ + vexpand: true, + visible: false, + hscrollbar_policy: Gtk.PolicyType.NEVER, + }); + let listBox = new Gtk.ListBox({ + selection_mode: Gtk.SelectionMode.NONE, + }); + listBox.set_filter_func((row) => { + let text = searchEntry.get_text().toLowerCase(); + if (!text) return true; + if (!row._appName) return false; + return row._appName.toLowerCase().includes(text); + }); + scrolled.set_child(listBox); + box.append(scrolled); + + searchEntry.connect('search-changed', () => { + listBox.invalidate_filter(); + }); + + dialog.set_content(box); + dialog.present(); + + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 80, () => { + this._populateAppChooser(dialog, listBox); + loadingLabel.visible = false; + scrolled.visible = true; + return GLib.SOURCE_REMOVE; + }); + } + + _populateAppChooser(dialog, listBox) { + let allApps = Gio.AppInfo.get_all() + .filter((app) => app.should_show()) + .sort((a, b) => a.get_display_name().localeCompare(b.get_display_name())); + + let current = this._blacklistSettings.get_strv('blacklist-apps'); + let runningIds = this._getRunningAppIds(); + + let runningApps = allApps.filter( + (app) => runningIds.includes(app.get_id()) && !current.includes(app.get_id()), + ); + let otherApps = allApps.filter( + (app) => !runningIds.includes(app.get_id()) && !current.includes(app.get_id()), + ); + + let appendAppRow = (appInfo) => { + let appId = appInfo.get_id(); + let row = new Adw.ActionRow({ + title: appInfo.get_display_name(), + subtitle: appId, + }); + row._appName = appInfo.get_display_name(); + + let gicon = appInfo.get_icon(); + if (gicon) { + row.add_prefix(new Gtk.Image({ gicon, pixel_size: 32, margin_end: 8 })); + } + + let addBtn = new Gtk.Button({ + icon_name: 'list-add-symbolic', + valign: Gtk.Align.CENTER, + css_classes: ['flat'], + }); + addBtn.connect('clicked', () => { + let cur = this._blacklistSettings.get_strv('blacklist-apps'); + if (!cur.includes(appId)) { + cur.push(appId); + this._blacklistSettings.set_strv('blacklist-apps', cur); + this._refreshBlacklistRows(); + } + dialog.close(); + }); + + row.add_suffix(addBtn); + row.set_activatable_widget(addBtn); + listBox.append(row); + }; + + runningApps.forEach(appendAppRow); + + if (runningApps.length > 0 && otherApps.length > 0) { + let separatorRow = new Gtk.ListBoxRow({ + selectable: false, + activatable: false, + }); + separatorRow.set_child( + new Gtk.Separator({ + orientation: Gtk.Orientation.HORIZONTAL, + margin_top: 4, + margin_bottom: 4, + }), + ); + separatorRow._appName = ''; + listBox.append(separatorRow); + } + + otherApps.forEach(appendAppRow); + } + updateMonitors() { let monitors = this._monitorsConfig.monitors; let count = monitors.length; diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 51cff3e3187fc3a246e6bf0e71db85376fee800f..1c617a91d4bbea03536d742c23b130274567d485 100644 GIT binary patch literal 2004 zcmZuyU1%It7`-v3P1>gU>CZN;#)mvO^Dw)D2!h16AXK43L8{H0vokllLuO}&duN(P zQ1ehQsGtu%q(ZO{ib4^r*fxrS5~%S(jHQ%TuoUW(1Wgc1ODTHJ-I>iM@xs}2_RPI^ zzVCi_zdiD@s{%JlMLza{Pc*glQ0{erhsEbx$b9y=cwS6{mpX;$+9Slj7)x7Wh@*mh z6^K=>T#Mp{EbF=q!?HY;%18%sq{~{m%BwqJP^+8QTEkq5!Tk$74BeGu3z*vz-M}_r z2ha=j<>&_w0E57;|3L{Ja~lF4ZNu#9`33IBTzbVxaSOZ*+`Dpbm_GFYe1U+vf%Bv1 zCh1f6!`}x!4BYr}_XK_FKKRGL$ARx(e`7n(hq?>?tKfHmZ=Nds&H2>Z;Ln2RfRA2C z=jl`Lg#QKj0&#BOE&9}h@E5^91Bd2r-lk8@_uc|81K)o9<`MeTjFX4i4HWgFX1&O# zfQ7LO_c@=M`^^v{An0k+Oe|3aUd{yg|Q;KsFI-=N~H{ zr)Iv#z*E3KpN_srpLz%U8Sn={-{HST=u^`_3;qch`SP-{ z74Q%)+BQIS#L7$rExd`TOuNo^~Do43~kZC+`4a>I^CC8;w(svqh6r`~# z9$JB2b-jt2inGXfyf|EYR7_dVO9zEz`53mX>+z(MxRDH6axtg2m3+_c_YQ9&c9 zm_|@Bji9pD2rBK3pdy5B=$ezp4rXwzrR{2nsKP*}j+-QTHkPYxFGCiQH-%86lTxLU znc%(EFjG!dv9Ux_n*~praU1dac78T#J@6)q=O#+zzZZi24rMicg?;7`0PW3;9(>|> zQOjn3*$-wP!L>C#hrNfrYd63ivlC#CLi@n|<((Ab0Kk65J|v@5O*!LnlsYSb_Emqc2~x0zR~LRgN0e6_>@_%1fuw9KrV3?d)Zk>lgiYjt=o4eSgTVBPU? zza&nw#BuQ;Q#w}buL+*#1>!xzCfaT$k)mK5F3rk86QbD2e0OngJU5h1UFQD+AD-d( zy1hPUb>AFd+j=vx_}&)Zy0hDm-ln{Y_242N zZStE8{N^vAJ-@Z8H={K5(9&t8fScRVy3abOHQapSvnHSX*NdRC)*mK^#Ng8Okue|$Rns#y04s4#A{eeC;$IpR31aAENt8i~RX+wL7L#CnEXE(d6i8a$-XI?P^nw zoDO?K*!BBZ^Sd2ahlrYbv23`JpTyW`&8ehb78a|iq7YS0A*z}}RDD#4s%r~TRS2zp zuOZz~@&GIDX^~`UFe8UOO;7;Ls-{D5@)WC z-jepDFwW1FlCeM2Io1TJImJbHeASNsZ?}(({{Q<&XvRQk6MF&H+Y-RK$y&+UxfR$3 zu(8f=0iFa{Us*p{Tb}?}k6BAuSMijP(|oT*y+w&Kb-bzTZkD*Ik`r*)%G#+VDv&(4 zj^st(f2|*W`@ZeBSs3|~t66FSn{($x5=R=i_I7NSA)L4UTzNs17a89EHgIw+-PPw| zJG+=q=3(fl-t_F?7svP>ka{Zfb6lzUk5=@RJ?*4Pny0RZ1jv;}f^;mU9j~_fn(T)) ZM@wW(;Nfv^49&RqFpAnimation speed Change the speed of the appear and disappear window effect. + + + Blacklisted applications + List of application IDs where the shortcut is disabled when they have focus. + diff --git a/ui/general.ui b/ui/general.ui index 111bd38..e679b7c 100644 --- a/ui/general.ui +++ b/ui/general.ui @@ -120,6 +120,25 @@ + + + Application Blacklist + Applications where the shortcut is disabled when focused. + + + Blacklisted Apps + Add applications to suppress the search shortcut (e.g. games). + + + list-add-symbolic + center + Add application + + + + + +