Last active
August 15, 2026 15:31
-
-
Save Kr328/947d88bedc50c77e31fd9cece2c8b8a2 to your computer and use it in GitHub Desktop.
A desktop entry hider for Linux desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/gjs -m | |
| import Gio from 'gi://Gio'; | |
| import GioUnix from 'gi://GioUnix?version=2.0'; | |
| import GLib from 'gi://GLib'; | |
| import Gtk from 'gi://Gtk?version=4.0'; | |
| import Pango from 'gi://Pango'; | |
| const APPLICATION_ID = 'com.github.kr328.DesktopEntryHider'; | |
| const GNOME_SHELL_SCHEMA = 'org.gnome.shell'; | |
| const APP_PICKER_LAYOUT_KEY = 'app-picker-layout'; | |
| const SYSTEM_APPLICATIONS_DIR = '/usr/share/applications'; | |
| const USER_APPLICATIONS_DIR = GLib.build_filenamev([ | |
| GLib.get_home_dir(), | |
| '.local', | |
| 'share', | |
| 'applications', | |
| ]); | |
| const GENERATED_MARKER = '# Generated by desktop-entry-hider; do not edit.'; | |
| const textDecoder = new TextDecoder('utf-8'); | |
| const textEncoder = new TextEncoder(); | |
| function pathFor(directory, name) { | |
| return GLib.build_filenamev([directory, name]); | |
| } | |
| function isNotFound(error) { | |
| return error instanceof GLib.Error && | |
| error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND); | |
| } | |
| function queryFileType(file) { | |
| try { | |
| const info = file.query_info( | |
| Gio.FILE_ATTRIBUTE_STANDARD_TYPE, | |
| Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, | |
| null | |
| ); | |
| return info.get_file_type(); | |
| } catch (error) { | |
| if (isNotFound(error)) | |
| return null; | |
| throw error; | |
| } | |
| } | |
| function readText(file) { | |
| const [, contents] = file.load_contents(null); | |
| return textDecoder.decode(contents); | |
| } | |
| function firstLine(text) { | |
| const newline = text.indexOf('\n'); | |
| const line = newline < 0 ? text : text.slice(0, newline); | |
| return line.endsWith('\r') ? line.slice(0, -1) : line; | |
| } | |
| function inspectUserFile(name) { | |
| const file = Gio.File.new_for_path(pathFor(USER_APPLICATIONS_DIR, name)); | |
| const type = queryFileType(file); | |
| if (type === null) | |
| return { state: 'missing', file }; | |
| if (type !== Gio.FileType.REGULAR) | |
| return { state: 'conflict', file }; | |
| try { | |
| return { | |
| state: firstLine(readText(file)) === GENERATED_MARKER | |
| ? 'managed' | |
| : 'conflict', | |
| file, | |
| }; | |
| } catch (_error) { | |
| return { state: 'conflict', file }; | |
| } | |
| } | |
| function transformDesktopFile(sourceText) { | |
| let text = sourceText; | |
| const sourceNewline = text.includes('\r\n') ? '\r\n' : '\n'; | |
| if (firstLine(text) === GENERATED_MARKER) { | |
| const markerEnd = text.indexOf('\n'); | |
| text = markerEnd < 0 ? '' : text.slice(markerEnd + 1); | |
| } | |
| const hadFinalNewline = text.endsWith('\n'); | |
| const lines = text.split(/\r?\n/); | |
| if (hadFinalNewline) | |
| lines.pop(); | |
| const groupStart = lines.findIndex(line => line.trim() === '[Desktop Entry]'); | |
| if (groupStart < 0) | |
| throw new Error('The file is missing a [Desktop Entry] group'); | |
| let groupEnd = lines.length; | |
| for (let index = groupStart + 1; index < lines.length; index++) { | |
| if (/^\s*\[.*\]\s*$/.test(lines[index])) { | |
| groupEnd = index; | |
| break; | |
| } | |
| } | |
| const hiddenKeys = new Map([ | |
| ['Hidden', false], | |
| ['NoDisplay', false], | |
| ]); | |
| for (let index = groupStart + 1; index < groupEnd;) { | |
| const match = lines[index].match(/^\s*(Hidden|NoDisplay)\s*=/); | |
| if (match !== null) { | |
| const key = match[1]; | |
| if (!hiddenKeys.get(key)) { | |
| lines[index] = `${key}=true`; | |
| hiddenKeys.set(key, true); | |
| index++; | |
| } else { | |
| lines.splice(index, 1); | |
| groupEnd--; | |
| } | |
| } else { | |
| index++; | |
| } | |
| } | |
| const missingKeys = [...hiddenKeys] | |
| .filter(([, found]) => !found) | |
| .map(([key]) => `${key}=true`); | |
| lines.splice(groupStart + 1, 0, ...missingKeys); | |
| let result = `${GENERATED_MARKER}${sourceNewline}${lines.join(sourceNewline)}`; | |
| if (hadFinalNewline) | |
| result += sourceNewline; | |
| return result; | |
| } | |
| function ensureUserApplicationsDirectory() { | |
| if (GLib.mkdir_with_parents(USER_APPLICATIONS_DIR, 0o700) !== 0) | |
| throw new Error(`Unable to create directory: ${USER_APPLICATIONS_DIR}`); | |
| } | |
| function replaceManagedFile(file, text, expectedState) { | |
| const name = file.get_basename(); | |
| const current = inspectUserFile(name).state; | |
| if (current !== expectedState && !(expectedState === 'missing' && current === 'managed')) | |
| throw new Error(`Refusing to overwrite a file not generated by this application: ${name}`); | |
| file.replace_contents( | |
| textEncoder.encode(text), | |
| null, | |
| false, | |
| Gio.FileCreateFlags.REPLACE_DESTINATION, | |
| null | |
| ); | |
| } | |
| function hideEntry(name) { | |
| const source = Gio.File.new_for_path(pathFor(SYSTEM_APPLICATIONS_DIR, name)); | |
| if (queryFileType(source) !== Gio.FileType.REGULAR) | |
| throw new Error(`The system desktop entry is missing or is not a regular file: ${name}`); | |
| const targetInfo = inspectUserFile(name); | |
| if (targetInfo.state === 'conflict') | |
| throw new Error(`A user file with the same name exists and was not generated by this application: ${name}`); | |
| const transformed = transformDesktopFile(readText(source)); | |
| ensureUserApplicationsDirectory(); | |
| replaceManagedFile(targetInfo.file, transformed, targetInfo.state); | |
| } | |
| function unhideEntry(name) { | |
| const targetInfo = inspectUserFile(name); | |
| if (targetInfo.state === 'missing') | |
| return; | |
| if (targetInfo.state !== 'managed') | |
| throw new Error(`Refusing to delete a file not generated by this application: ${name}`); | |
| targetInfo.file.delete(null); | |
| } | |
| function enumerateDesktopFiles(directory) { | |
| const root = Gio.File.new_for_path(directory); | |
| let enumerator; | |
| try { | |
| enumerator = root.enumerate_children( | |
| `${Gio.FILE_ATTRIBUTE_STANDARD_NAME},${Gio.FILE_ATTRIBUTE_STANDARD_TYPE}`, | |
| Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, | |
| null | |
| ); | |
| } catch (error) { | |
| if (isNotFound(error)) | |
| return []; | |
| throw error; | |
| } | |
| const names = []; | |
| try { | |
| for (; ;) { | |
| const info = enumerator.next_file(null); | |
| if (info === null) | |
| break; | |
| const name = info.get_name(); | |
| if (info.get_file_type() === Gio.FileType.REGULAR && name.endsWith('.desktop')) | |
| names.push(name); | |
| } | |
| } finally { | |
| enumerator.close(null); | |
| } | |
| return names; | |
| } | |
| function desktopInfoFor(path) { | |
| try { | |
| const info = GioUnix.DesktopAppInfo.new_from_filename(path); | |
| if (info === null || info.get_string('Type') !== 'Application') | |
| return null; | |
| return info; | |
| } catch (_error) { | |
| return null; | |
| } | |
| } | |
| function entryFromInfo(name, info, state, sourceExists, listedWhenShown) { | |
| const displayName = info?.get_display_name() || | |
| info?.get_name() || name.replace(/\.desktop$/, ''); | |
| return { | |
| name, | |
| displayName, | |
| description: info?.get_description() || '', | |
| icon: info?.get_icon() || null, | |
| state, | |
| sourceExists, | |
| listedWhenShown, | |
| }; | |
| } | |
| function normalizeSearchText(text) { | |
| return text.trim().toLocaleLowerCase(); | |
| } | |
| function entryMatchesSearch(entry, normalizedQuery) { | |
| if (normalizedQuery === '') | |
| return true; | |
| const searchableText = normalizeSearchText( | |
| `${entry.displayName}\n${entry.description}` | |
| ); | |
| return searchableText.includes(normalizedQuery); | |
| } | |
| function desktopSessionIsGnome(value) { | |
| if (value === null || value === '') | |
| return false; | |
| return value.split(':').some(desktop => { | |
| const normalized = desktop.trim().toLocaleLowerCase(); | |
| return normalized === 'gnome' || normalized.startsWith('gnome-'); | |
| }); | |
| } | |
| function isGnomeSession() { | |
| const currentDesktop = GLib.getenv('XDG_CURRENT_DESKTOP'); | |
| if (currentDesktop !== null && currentDesktop !== '') | |
| return desktopSessionIsGnome(currentDesktop); | |
| return desktopSessionIsGnome(GLib.getenv('XDG_SESSION_DESKTOP')) || | |
| desktopSessionIsGnome(GLib.getenv('DESKTOP_SESSION')); | |
| } | |
| function createAppPickerSettings() { | |
| if (!isGnomeSession()) | |
| return null; | |
| try { | |
| const source = Gio.SettingsSchemaSource.get_default(); | |
| const schema = source?.lookup(GNOME_SHELL_SCHEMA, true) || null; | |
| if (schema === null || !schema.has_key(APP_PICKER_LAYOUT_KEY)) | |
| return null; | |
| return new Gio.Settings({ settings_schema: schema }); | |
| } catch (_error) { | |
| return null; | |
| } | |
| } | |
| function loadEntries() { | |
| const entries = new Map(); | |
| for (const name of enumerateDesktopFiles(SYSTEM_APPLICATIONS_DIR)) { | |
| const systemPath = pathFor(SYSTEM_APPLICATIONS_DIR, name); | |
| const info = desktopInfoFor(systemPath); | |
| if (info === null) | |
| continue; | |
| const targetState = inspectUserFile(name).state; | |
| const listedWhenShown = info.should_show(); | |
| if (listedWhenShown || targetState === 'managed') { | |
| entries.set(name, entryFromInfo( | |
| name, | |
| info, | |
| targetState, | |
| true, | |
| listedWhenShown | |
| )); | |
| } | |
| } | |
| for (const name of enumerateDesktopFiles(USER_APPLICATIONS_DIR)) { | |
| const targetState = inspectUserFile(name).state; | |
| if (targetState !== 'managed' || entries.has(name)) | |
| continue; | |
| const systemPath = pathFor(SYSTEM_APPLICATIONS_DIR, name); | |
| const sourceExists = queryFileType(Gio.File.new_for_path(systemPath)) === | |
| Gio.FileType.REGULAR; | |
| const info = (sourceExists && desktopInfoFor(systemPath)) || | |
| desktopInfoFor(pathFor(USER_APPLICATIONS_DIR, name)); | |
| const listedWhenShown = sourceExists && info !== null && info.should_show(); | |
| entries.set(name, entryFromInfo( | |
| name, | |
| info, | |
| targetState, | |
| sourceExists, | |
| listedWhenShown | |
| )); | |
| } | |
| return [...entries.values()].sort((left, right) => { | |
| const byDisplayName = GLib.utf8_collate(left.displayName, right.displayName); | |
| return byDisplayName || left.name.localeCompare(right.name); | |
| }); | |
| } | |
| function managedUserFiles() { | |
| return enumerateDesktopFiles(USER_APPLICATIONS_DIR) | |
| .filter(name => inspectUserFile(name).state === 'managed'); | |
| } | |
| function synchronizeManagedFiles() { | |
| const result = { updated: 0, deleted: 0, failed: [] }; | |
| for (const name of managedUserFiles()) { | |
| const source = Gio.File.new_for_path(pathFor(SYSTEM_APPLICATIONS_DIR, name)); | |
| const target = Gio.File.new_for_path(pathFor(USER_APPLICATIONS_DIR, name)); | |
| try { | |
| const sourceType = queryFileType(source); | |
| if (sourceType === null) { | |
| if (inspectUserFile(name).state === 'managed') { | |
| target.delete(null); | |
| result.deleted++; | |
| } | |
| continue; | |
| } | |
| if (sourceType !== Gio.FileType.REGULAR) | |
| throw new Error('The system source is not a regular file'); | |
| const transformed = transformDesktopFile(readText(source)); | |
| replaceManagedFile(target, transformed, 'managed'); | |
| result.updated++; | |
| } catch (error) { | |
| result.failed.push(`${name}: ${error.message}`); | |
| } | |
| } | |
| return result; | |
| } | |
| function clearManagedFiles() { | |
| const result = { deleted: 0, failed: [] }; | |
| for (const name of managedUserFiles()) { | |
| try { | |
| const targetInfo = inspectUserFile(name); | |
| if (targetInfo.state !== 'managed') | |
| throw new Error('The file state has changed'); | |
| targetInfo.file.delete(null); | |
| result.deleted++; | |
| } catch (error) { | |
| result.failed.push(`${name}: ${error.message}`); | |
| } | |
| } | |
| return result; | |
| } | |
| class IconHiderWindow { | |
| constructor(application) { | |
| this._busy = false; | |
| this._hasBeenActive = false; | |
| this._scrollRestoreSourceId = 0; | |
| this._entries = []; | |
| this._entryForChild = new Map(); | |
| this._normalizedQuery = ''; | |
| this._appPickerSettings = createAppPickerSettings(); | |
| this._window = new Gtk.ApplicationWindow({ | |
| application, | |
| title: 'Desktop Entry Hider', | |
| default_width: 720, | |
| default_height: 640, | |
| }); | |
| const headerBar = new Gtk.HeaderBar(); | |
| const menu = new Gio.Menu(); | |
| menu.append('Refresh', 'app.refresh'); | |
| menu.append('Sync with System', 'app.sync'); | |
| menu.append('Clear All Hidden Entries', 'app.clear'); | |
| menu.append('Reset AppPicker Layout', 'app.reset-app-picker-layout'); | |
| headerBar.pack_end(new Gtk.MenuButton({ | |
| icon_name: 'open-menu-symbolic', | |
| menu_model: menu, | |
| tooltip_text: 'Menu', | |
| })); | |
| this._window.set_titlebar(headerBar); | |
| const root = new Gtk.Box({ | |
| orientation: Gtk.Orientation.VERTICAL, | |
| spacing: 0, | |
| }); | |
| this._window.set_child(root); | |
| this._searchEntry = new Gtk.SearchEntry({ | |
| placeholder_text: 'Search applications…', | |
| hexpand: true, | |
| margin_start: 12, | |
| margin_end: 12, | |
| margin_top: 12, | |
| margin_bottom: 8, | |
| }); | |
| root.append(this._searchEntry); | |
| this._flowBox = new Gtk.FlowBox({ | |
| selection_mode: Gtk.SelectionMode.NONE, | |
| orientation: Gtk.Orientation.HORIZONTAL, | |
| min_children_per_line: 1, | |
| max_children_per_line: 4, | |
| row_spacing: 4, | |
| margin_start: 12, | |
| margin_end: 12, | |
| margin_top: 4, | |
| margin_bottom: 4, | |
| valign: Gtk.Align.START, | |
| }); | |
| this._flowBox.set_filter_func(child => { | |
| const entry = this._entryForChild.get(child); | |
| return entry !== undefined && | |
| entryMatchesSearch(entry, this._normalizedQuery); | |
| }); | |
| this._scroller = new Gtk.ScrolledWindow({ | |
| hscrollbar_policy: Gtk.PolicyType.NEVER, | |
| vscrollbar_policy: Gtk.PolicyType.AUTOMATIC, | |
| vexpand: true, | |
| child: this._flowBox, | |
| }); | |
| this._emptyState = new Gtk.Label({ | |
| label: 'No desktop entries to display', | |
| margin_start: 24, | |
| margin_end: 24, | |
| margin_top: 32, | |
| margin_bottom: 32, | |
| valign: Gtk.Align.CENTER, | |
| }); | |
| this._emptyState.add_css_class('dim-label'); | |
| this._contentStack = new Gtk.Stack({ vexpand: true }); | |
| this._contentStack.add_named(this._scroller, 'content'); | |
| this._contentStack.add_named(this._emptyState, 'empty'); | |
| root.append(this._contentStack); | |
| this._status = new Gtk.Label({ | |
| xalign: 0, | |
| ellipsize: Pango.EllipsizeMode.END, | |
| margin_start: 12, | |
| margin_end: 12, | |
| margin_top: 8, | |
| margin_bottom: 8, | |
| label: '', | |
| }); | |
| this._status.add_css_class('dim-label'); | |
| root.append(this._status); | |
| this._installSearch(); | |
| this._installActions(application); | |
| this._installFocusRefresh(); | |
| this.reload(); | |
| } | |
| present() { | |
| this._window.present(); | |
| } | |
| _installSearch() { | |
| this._searchEntry.connect('search-changed', () => { | |
| this._normalizedQuery = normalizeSearchText(this._searchEntry.text); | |
| this._flowBox.invalidate_filter(); | |
| this._updateContentState(); | |
| this._cancelScrollRestore(); | |
| this._restoreScrollPosition(this._scroller.vadjustment.lower); | |
| }); | |
| this._searchEntry.connect('stop-search', () => { | |
| if (this._searchEntry.text !== '') | |
| this._searchEntry.text = ''; | |
| }); | |
| } | |
| _installActions(application) { | |
| const refreshAction = new Gio.SimpleAction({ name: 'refresh' }); | |
| refreshAction.connect('activate', () => { | |
| if (!this._busy) | |
| this.reload({ preserveScroll: true }); | |
| }); | |
| application.add_action(refreshAction); | |
| const syncAction = new Gio.SimpleAction({ name: 'sync' }); | |
| syncAction.connect('activate', () => this._runBatch('sync')); | |
| application.add_action(syncAction); | |
| const clearAction = new Gio.SimpleAction({ name: 'clear' }); | |
| clearAction.connect('activate', () => this._runBatch('clear')); | |
| application.add_action(clearAction); | |
| const resetAppPickerAction = new Gio.SimpleAction({ | |
| name: 'reset-app-picker-layout', | |
| enabled: this._appPickerSettings !== null, | |
| }); | |
| resetAppPickerAction.connect('activate', () => | |
| this._confirmResetAppPickerLayout() | |
| ); | |
| application.add_action(resetAppPickerAction); | |
| } | |
| _confirmResetAppPickerLayout() { | |
| const dialog = new Gtk.AlertDialog({ | |
| message: 'Reset AppPicker Layout?', | |
| detail: 'This will clear the custom application grid and folder layout, and restore the GNOME default layout.', | |
| buttons: ['Cancel', 'Reset'], | |
| cancel_button: 0, | |
| default_button: 0, | |
| }); | |
| dialog.choose(this._window, null, (source, result) => { | |
| try { | |
| if (source.choose_finish(result) === 1) | |
| this._resetAppPickerLayout(); | |
| } catch (error) { | |
| this._showError('Unable to Reset AppPicker Layout', error.message); | |
| } | |
| }); | |
| } | |
| _resetAppPickerLayout() { | |
| try { | |
| if (this._appPickerSettings === null) | |
| throw new Error('GNOME AppPicker settings are unavailable'); | |
| this._appPickerSettings.reset(APP_PICKER_LAYOUT_KEY); | |
| this._showMessage( | |
| 'AppPicker Layout Reset', | |
| 'The default GNOME application grid and folder layout has been restored. The change will take effect after you log in again.' | |
| ); | |
| } catch (error) { | |
| this._showError('Unable to Reset AppPicker Layout', error.message); | |
| } | |
| } | |
| _installFocusRefresh() { | |
| this._window.connect('notify::is-active', () => { | |
| if (!this._window.is_active) | |
| return; | |
| if (this._hasBeenActive && !this._busy) | |
| this.reload({ preserveScroll: true }); | |
| this._hasBeenActive = true; | |
| }); | |
| } | |
| _clearTiles() { | |
| for (; ;) { | |
| const child = this._flowBox.get_first_child(); | |
| if (child === null) | |
| break; | |
| this._flowBox.remove(child); | |
| } | |
| this._entryForChild.clear(); | |
| } | |
| _updateContentState() { | |
| const total = this._entries.length; | |
| const matches = this._entries.filter(entry => | |
| entryMatchesSearch(entry, this._normalizedQuery) | |
| ).length; | |
| if (total === 0) { | |
| this._emptyState.label = 'No desktop entries to display'; | |
| this._contentStack.visible_child_name = 'empty'; | |
| } else if (matches === 0) { | |
| this._emptyState.label = 'No matching desktop entries'; | |
| this._contentStack.visible_child_name = 'empty'; | |
| } else { | |
| this._contentStack.visible_child_name = 'content'; | |
| } | |
| this._status.label = this._normalizedQuery === '' | |
| ? `${total} entries` | |
| : `${matches} of ${total} entries`; | |
| } | |
| _cancelScrollRestore() { | |
| if (this._scrollRestoreSourceId === 0) | |
| return; | |
| GLib.Source.remove(this._scrollRestoreSourceId); | |
| this._scrollRestoreSourceId = 0; | |
| } | |
| _restoreScrollPosition(value) { | |
| this._scrollRestoreSourceId = GLib.idle_add( | |
| GLib.PRIORITY_DEFAULT_IDLE, | |
| () => { | |
| this._scrollRestoreSourceId = 0; | |
| const adjustment = this._scroller.vadjustment; | |
| const maximum = Math.max( | |
| adjustment.lower, | |
| adjustment.upper - adjustment.page_size | |
| ); | |
| adjustment.value = Math.min( | |
| Math.max(value, adjustment.lower), | |
| maximum | |
| ); | |
| return GLib.SOURCE_REMOVE; | |
| } | |
| ); | |
| } | |
| reload({ preserveScroll = false } = {}) { | |
| const scrollPosition = preserveScroll | |
| ? this._scroller.vadjustment.value | |
| : null; | |
| this._cancelScrollRestore(); | |
| this._clearTiles(); | |
| try { | |
| const entries = loadEntries(); | |
| this._entries = entries; | |
| for (const entry of entries) | |
| this._flowBox.append(this._createTile(entry)); | |
| this._flowBox.invalidate_filter(); | |
| this._updateContentState(); | |
| } catch (error) { | |
| this._entries = []; | |
| this._contentStack.visible_child_name = 'empty'; | |
| this._emptyState.label = 'Unable to load desktop entries'; | |
| this._status.label = `Failed to load desktop entries: ${error.message}`; | |
| this._showError('Load Failed', error.message); | |
| } | |
| if (scrollPosition !== null) | |
| this._restoreScrollPosition(scrollPosition); | |
| } | |
| _createTile(entry) { | |
| const content = new Gtk.Box({ | |
| orientation: Gtk.Orientation.HORIZONTAL, | |
| spacing: 12, | |
| margin_start: 12, | |
| margin_end: 12, | |
| margin_top: 10, | |
| margin_bottom: 10, | |
| }); | |
| const image = new Gtk.Image({ pixel_size: 40 }); | |
| if (entry.icon !== null) | |
| image.set_from_gicon(entry.icon); | |
| else | |
| image.set_from_icon_name('application-x-executable-symbolic'); | |
| content.append(image); | |
| const labels = new Gtk.Box({ | |
| orientation: Gtk.Orientation.VERTICAL, | |
| spacing: 3, | |
| hexpand: true, | |
| valign: Gtk.Align.CENTER, | |
| }); | |
| content.append(labels); | |
| const nameLabel = new Gtk.Label({ | |
| label: entry.displayName, | |
| xalign: 0, | |
| ellipsize: Pango.EllipsizeMode.END, | |
| max_width_chars: 28, | |
| }); | |
| labels.append(nameLabel); | |
| if (entry.description) { | |
| const descriptionLabel = new Gtk.Label({ | |
| label: entry.description, | |
| xalign: 0, | |
| wrap: true, | |
| wrap_mode: Pango.WrapMode.WORD_CHAR, | |
| lines: 2, | |
| max_width_chars: 32, | |
| }); | |
| descriptionLabel.add_css_class('dim-label'); | |
| labels.append(descriptionLabel); | |
| } | |
| if (!entry.sourceExists) { | |
| const missingLabel = new Gtk.Label({ | |
| label: 'The system source no longer exists; this entry will be removed during synchronization', | |
| xalign: 0, | |
| wrap: true, | |
| wrap_mode: Pango.WrapMode.WORD_CHAR, | |
| lines: 2, | |
| max_width_chars: 32, | |
| }); | |
| missingLabel.add_css_class('warning'); | |
| labels.append(missingLabel); | |
| } | |
| if (entry.state === 'conflict') { | |
| const warning = new Gtk.Image({ | |
| icon_name: 'dialog-warning-symbolic', | |
| tooltip_text: 'A user file with the same name was not generated by this application and cannot be managed', | |
| }); | |
| content.append(warning); | |
| } | |
| const toggle = new Gtk.Switch({ | |
| active: entry.state !== 'managed', | |
| sensitive: entry.state !== 'conflict' && | |
| (entry.sourceExists || entry.state === 'managed'), | |
| valign: Gtk.Align.CENTER, | |
| tooltip_text: entry.state === 'conflict' | |
| ? 'A user file with the same name was not generated by this application and will not be overwritten' | |
| : 'Show or hide this desktop entry', | |
| }); | |
| content.append(toggle); | |
| let changingProgrammatically = false; | |
| toggle.connect('notify::active', () => { | |
| if (changingProgrammatically || this._busy) | |
| return; | |
| const desired = toggle.active; | |
| toggle.sensitive = false; | |
| try { | |
| if (desired) | |
| unhideEntry(entry.name); | |
| else | |
| hideEntry(entry.name); | |
| this._status.label = desired | |
| ? `Shown: ${entry.displayName}` | |
| : `Hidden: ${entry.displayName}`; | |
| entry.state = desired ? 'missing' : 'managed'; | |
| if (desired && !entry.listedWhenShown) { | |
| this.reload({ preserveScroll: true }); | |
| } else { | |
| toggle.sensitive = entry.state !== 'conflict' && | |
| (entry.sourceExists || entry.state === 'managed'); | |
| if (this._normalizedQuery !== '') | |
| this._updateContentState(); | |
| } | |
| } catch (error) { | |
| changingProgrammatically = true; | |
| toggle.active = !desired; | |
| changingProgrammatically = false; | |
| toggle.sensitive = entry.state !== 'conflict' && | |
| (entry.sourceExists || entry.state === 'managed'); | |
| this._showError('Operation Failed', error.message); | |
| } | |
| }); | |
| const child = new Gtk.FlowBoxChild({ child: content }); | |
| this._entryForChild.set(child, entry); | |
| return child; | |
| } | |
| _runBatch(kind) { | |
| if (this._busy) | |
| return; | |
| this._busy = true; | |
| this._flowBox.sensitive = false; | |
| try { | |
| if (kind === 'sync') { | |
| const result = synchronizeManagedFiles(); | |
| const details = [ | |
| `Updated ${result.updated} entries and removed ${result.deleted} obsolete entries.`, | |
| ]; | |
| if (result.failed.length > 0) | |
| details.push(`${result.failed.length} failed:\n${result.failed.join('\n')}`); | |
| this._showMessage('Synchronization Complete', details.join('\n\n')); | |
| } else { | |
| const result = clearManagedFiles(); | |
| const details = [`Cleared ${result.deleted} hidden entries.`]; | |
| if (result.failed.length > 0) | |
| details.push(`${result.failed.length} failed:\n${result.failed.join('\n')}`); | |
| this._showMessage('Cleanup Complete', details.join('\n\n')); | |
| } | |
| } catch (error) { | |
| this._showError(kind === 'sync' ? 'Synchronization Failed' : 'Cleanup Failed', error.message); | |
| } finally { | |
| this._busy = false; | |
| this._flowBox.sensitive = true; | |
| this.reload({ preserveScroll: true }); | |
| } | |
| } | |
| _showMessage(title, detail) { | |
| const dialog = new Gtk.AlertDialog({ message: title, detail }); | |
| dialog.show(this._window); | |
| } | |
| _showError(title, detail) { | |
| const dialog = new Gtk.AlertDialog({ message: title, detail }); | |
| dialog.show(this._window); | |
| } | |
| } | |
| const application = new Gtk.Application({ | |
| application_id: APPLICATION_ID, | |
| flags: Gio.ApplicationFlags.DEFAULT_FLAGS, | |
| }); | |
| let mainWindow = null; | |
| application.connect('activate', app => { | |
| if (mainWindow === null) | |
| mainWindow = new IconHiderWindow(app); | |
| mainWindow.present(); | |
| }); | |
| application.run(ARGV); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment