const { Gdk, Gtk } = imports.gi; import App from 'resource:///com/github/Aylur/ags/app.js'; import Widget from 'resource:///com/github/Aylur/ags/widget.js'; import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; import Applications from 'resource:///com/github/Aylur/ags/service/applications.js'; const { execAsync, exec } = Utils; import { execAndClose, expandTilde, hasUnterminatedBackslash, couldBeMath, launchCustomCommand, ls } from './miscfunctions.js'; import { CalculationResultButton, CustomCommandButton, DirectoryButton, DesktopEntryButton, ExecuteCommandButton, SearchButton } from './searchbuttons.js'; import { checkKeybind } from '../.widgetutils/keybind.js'; // Add math funcs const { abs, sin, cos, tan, cot, asin, acos, atan, acot } = Math; const pi = Math.PI; // trigonometric funcs for deg const sind = x => sin(x * pi / 180); const cosd = x => cos(x * pi / 180); const tand = x => tan(x * pi / 180); const cotd = x => cot(x * pi / 180); const asind = x => asin(x) * 180 / pi; const acosd = x => acos(x) * 180 / pi; const atand = x => atan(x) * 180 / pi; const acotd = x => acot(x) * 180 / pi; const MAX_RESULTS = 10; const OVERVIEW_SCALE = 0.18; // = overview workspace box / screen size const OVERVIEW_WS_NUM_SCALE = 0.0; const OVERVIEW_WS_NUM_MARGIN_SCALE = 0.07; const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)]; function iconExists(iconName) { let iconTheme = Gtk.IconTheme.get_default(); return iconTheme.has_icon(iconName); } const OptionalOverview = async () => { try { return (await import('./overview_hyprland.js')).default(); } catch { return Widget.Box({}); // return (await import('./overview_hyprland.js')).default(); } }; const overviewContent = await OptionalOverview(); export const SearchAndWindows = () => { var _appSearchResults = []; const ClickToClose = ({ ...props }) => Widget.EventBox({ ...props, onPrimaryClick: () => App.closeWindow('overview'), onSecondaryClick: () => App.closeWindow('overview'), onMiddleClick: () => App.closeWindow('overview'), }); const resultsBox = Widget.Box({ className: 'overview-search-results', vertical: true, vexpand: true, }); const resultsRevealer = Widget.Revealer({ transitionDuration: userOptions.animations.durationLarge, revealChild: false, transition: 'slide_down', // duration: 200, hpack: 'center', child: resultsBox, }); const entryPromptRevealer = Widget.Revealer({ transition: 'crossfade', transitionDuration: userOptions.animations.durationLarge, revealChild: true, hpack: 'center', child: Widget.Label({ className: 'overview-search-prompt txt-small txt', label: 'Type to search' }), }); const entryIconRevealer = Widget.Revealer({ transition: 'crossfade', transitionDuration: userOptions.animations.durationLarge, revealChild: false, hpack: 'end', child: Widget.Label({ className: 'txt txt-large icon-material overview-search-icon', label: ' ', }), }); const entryIcon = Widget.Box({ className: 'overview-search-prompt-box', setup: box => box.pack_start(entryIconRevealer, true, true, 0), }); const entry = Widget.Entry({ className: 'overview-search-box txt-small txt', hpack: 'center', onAccept: (self) => { // This is when you hit Enter const text = self.text; if (text.length == 0) return; const isAction = text.startsWith('>'); const isDir = (['/', '~'].includes(entry.text[0])); if (couldBeMath(text)) { // Eval on typing is dangerous, this is a workaround try { const fullResult = eval(text.replace(/\^/g, "**")); // copy execAsync(['wl-copy', `${fullResult}`]).catch(print); App.closeWindow('overview'); return; } catch (e) { // console.log(e); } } if (isDir) { App.closeWindow('overview'); execAsync(['bash', '-c', `xdg-open "${expandTilde(text)}"`, `&`]).catch(print); return; } if (_appSearchResults.length > 0) { App.closeWindow('overview'); _appSearchResults[0].launch(); return; } else if (text[0] == '>') { // Custom commands App.closeWindow('overview'); launchCustomCommand(text); return; } // Fallback: Execute command if (!isAction && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') { if (text.startsWith('sudo')) execAndClose(text, true); else execAndClose(text, false); } else { App.closeWindow('overview'); execAsync(['bash', '-c', `xdg-open '${userOptions.search.engineBaseUrl}${text} ${['', ...userOptions.search.excludedSites].join(' -site:')}' &`]).catch(print); } }, onChange: (entry) => { // this is when you type const isAction = entry.text[0] == '>'; const isDir = (['/', '~'].includes(entry.text[0])); resultsBox.get_children().forEach(ch => ch.destroy()); // check empty if so then dont do stuff if (entry.text == '') { resultsRevealer.revealChild = false; overviewContent.revealChild = true; entryPromptRevealer.revealChild = true; entryIconRevealer.revealChild = false; entry.toggleClassName('overview-search-box-extended', false); return; } const text = entry.text; resultsRevealer.revealChild = true; overviewContent.revealChild = false; entryPromptRevealer.revealChild = false; entryIconRevealer.revealChild = true; entry.toggleClassName('overview-search-box-extended', true); _appSearchResults = Applications.query(text); // Calculate if (couldBeMath(text)) { // Eval on typing is dangerous; this is a small workaround. try { const fullResult = eval(text.replace(/\^/g, "**")); resultsBox.add(CalculationResultButton({ result: fullResult, text: text })); } catch (e) { // console.log(e); } } if (isDir) { var contents = []; contents = ls({ path: text, silent: true }); contents.forEach((item) => { resultsBox.add(DirectoryButton(item)); }) } if (isAction) { // Eval on typing is dangerous, this is a workaround. resultsBox.add(CustomCommandButton({ text: entry.text })); } // Add application entries let appsToAdd = MAX_RESULTS; _appSearchResults.forEach(app => { if (appsToAdd == 0) return; resultsBox.add(DesktopEntryButton(app)); appsToAdd--; }); // Fallbacks // if the first word is an actual command if (!isAction && !hasUnterminatedBackslash(text) && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') { resultsBox.add(ExecuteCommandButton({ command: entry.text, terminal: entry.text.startsWith('sudo') })); } // Add fallback: search resultsBox.add(SearchButton({ text: entry.text })); resultsBox.show_all(); }, }); return Widget.Box({ vertical: true, children: [ ClickToClose({ // Top margin. Also works as a click-outside-to-close thing child: Widget.Box({ className: 'bar-height', }) }), Widget.Box({ hpack: 'center', children: [ entry, Widget.Box({ className: 'overview-search-icon-box', setup: (box) => { box.pack_start(entryPromptRevealer, true, true, 0) }, }), entryIcon, ] }), overviewContent, resultsRevealer, ], setup: (self) => self .hook(App, (_b, name, visible) => { if (name == 'overview' && !visible) { resultsBox.children = []; entry.set_text(''); } }) .on('key-press-event', (widget, event) => { // Typing const keyval = event.get_keyval()[1]; const modstate = event.get_state()[1]; if (checkKeybind(event, userOptions.keybinds.overview.altMoveLeft)) entry.set_position(Math.max(entry.get_position() - 1, 0)); else if (checkKeybind(event, userOptions.keybinds.overview.altMoveRight)) entry.set_position(Math.min(entry.get_position() + 1, entry.get_text().length)); else if (checkKeybind(event, userOptions.keybinds.overview.deleteToEnd)) { const text = entry.get_text(); const pos = entry.get_position(); const newText = text.slice(0, pos); entry.set_text(newText); entry.set_position(newText.length); } else if (!(modstate & Gdk.ModifierType.CONTROL_MASK)) { // Ctrl not held if (keyval >= 32 && keyval <= 126 && widget != entry) { Utils.timeout(1, () => entry.grab_focus()); entry.set_text(entry.text + String.fromCharCode(keyval)); entry.set_position(-1); } } }) , }); };