import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import St from 'gi://St'; import {Button as PanelButton} from 'resource:///org/gnome/shell/ui/panelMenu.js'; import Clutter from 'gi://Clutter'; import {gnomeExecutables, autocomplete, clear} from './autocomplete.js'; import {launchApp} from './utils.js'; export class TopBarSearchEntry { _searchButton; _searchContainer; _searchEntry; _searchSuggestion; _alive; constructor(settings) { this._alive = true; this._searchButton = new PanelButton(0.0, 'searchEntry', false); this._searchContainer = new St.Bin({ x_expand: true, }); this._searchEntry = new St.Entry({ style_class: 'custom-search-entry', can_focus: true, hint_text: 'Type to search…', track_hover: true, x_expand: true, }); this._searchSuggestion = new St.Label({ style_class: 'suggestion', text: '', x_expand: true, }); this._searchEntry.clutter_text.connect('notify::mapped', actor => { if (actor.mapped) actor.grab_key_focus(); }); this._searchContainer.add_child(this._searchSuggestion); this._searchContainer.add_child(this._searchEntry); this._searchEntry.clutter_text.connect('text-changed', actor => { let current = actor.get_text(); if (current.length > 1 && this._searchSuggestion) { let matches = autocomplete(current); if (matches.length > 0 && this._searchEntry) { let match = matches[0]; const ct = this._searchEntry.get_clutter_text(); const layout = ct.get_layout(); const [textW] = layout.get_pixel_size(); const themeNode = this._searchEntry.get_theme_node(); const leftPad = themeNode.get_padding(St.Side.LEFT); const x = leftPad + textW; this._searchSuggestion.set_style(`color: rgba(255,255,255,0.35); margin-left: ${x + 4}px;`); this._searchSuggestion.set_text(match.slice(current.length)); } else { this._searchSuggestion.set_text(''); } } else if (this._searchSuggestion) { this._searchSuggestion.set_text(''); } }); let completeText = () => { const typed = this._searchEntry?.get_text(); const ct = this._searchEntry?.get_clutter_text(); if (!typed || !ct) return; let full = typed + this._searchSuggestion?.get_text(); this._searchEntry?.set_text(full); ct.set_cursor_position(full.length); this._searchSuggestion?.set_text(''); }; this._searchEntry.clutter_text.connect('key-press-event', (ct, event) => { const key = event.get_key_symbol(); if (key === Clutter.KEY_KP_Right || key === Clutter.KEY_Right) { // Only accept if cursor is at end and a suggestion exists const typed = this._searchEntry?.get_text(); if (typed) { completeText(); return Clutter.EVENT_STOP; } } return Clutter.EVENT_PROPAGATE; }); this._searchEntry.connect('captured-event', (actor, event) => { if (event.type() !== Clutter.EventType.KEY_PRESS) return Clutter.EVENT_PROPAGATE; const sym = event.get_key_symbol(); if (sym === Clutter.KEY_Tab || sym === Clutter.KEY_ISO_Left_Tab) { completeText(); return Clutter.EVENT_STOP; } return Clutter.EVENT_PROPAGATE; }); this._searchEntry.clutter_text.connect('activate', actor => { let query = actor.get_text().trim().toLowerCase(); if (query === '') { this.destroy(); } else if (gnomeExecutables?.get(query)) { gnomeExecutables.get(query)?.launch([], null); this.destroy(); } else if (launchApp([query])) { this.destroy(); } else { actor.set_text(''); this._searchEntry?.set_style('border: 2px solid red;'); } }); this._searchButton.add_child(this._searchContainer); let positionInt = settings.get_int('search-entry-position'); let position; if (positionInt === 0) position = 'left'; else if (positionInt === 1) position = 'center'; else position = 'right'; Main.panel.addToStatusArea('SearchEntry', this._searchButton, 0, position); } isAlive() { return this._alive; } destroy() { this._alive = false; if (this._searchButton) { this._searchButton?.destroy(); this._searchContainer = undefined; this._searchEntry = undefined; this._searchButton = undefined; this._searchSuggestion = undefined; } clear(); } }