const {Gdk, Gtk} = imports.gi import {App, Service, Utils, Widget} from "../imports.js" import Applications from "resource:///com/github/Aylur/ags/service/applications.js" import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js" const {execAsync, exec} = Utils import {setupCursorHover, setupCursorHoverAim} from "./lib/cursorhover.js" import {MaterialIcon} from "./lib/materialicon.js" import {searchItem} from "./lib/searchitem.js" import {ContextMenuItem} from "./lib/contextmenuitem.js" import Todo from "../scripts/todo.js" var searching = false // 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 TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)] const searchPromptTexts = [ 'Try "Kolourpaint"', 'Try "6*cos(pi)"', 'Try "sudo pacman -Syu"', 'Try "How to basic"', "Drag n' drop to move windows", "Type to search", ] function execAndClose(command, terminal) { App.closeWindow("overview") if (terminal) { execAsync([`bash`, `-c`, `foot fish -C "${command}"`, `&`]).catch(print) } else execAsync(command).catch(print) } function startsWithNumber(str) { var pattern = /^\d/ return pattern.test(str) } function substitute(str) { const subs = [ {from: "code-url-handler", to: "visual-studio-code"}, {from: "Code", to: "visual-studio-code"}, {from: "GitHub Desktop", to: "github-desktop"}, {from: "wpsoffice", to: "wps-office2019-kprometheus"}, {from: "gnome-tweaks", to: "org.gnome.tweaks"}, {from: "Minecraft* 1.20.1", to: "minecraft"}, {from: "", to: "image-missing"}, ] for (const {from, to} of subs) { if (from === str) return to } return str } function destroyContextMenu(menu) { if (menu !== null) { menu.remove_all() menu.destroy() menu = null } } const CalculationResultButton = ({result, text}) => searchItem({ materialIconName: "calculate", name: `Math result`, actionName: "Copy", content: `${result}`, onActivate: () => { App.closeWindow("overview") console.log(result) execAsync(["bash", "-c", `wl-copy '${result}'`, `&`]).catch(print) }, }) const CustomCommandButton = ({text = ""}) => searchItem({ materialIconName: "settings_suggest", name: "Action", actionName: "Run", content: `${text}`, onActivate: () => { App.closeWindow("overview") launchCustomCommand(text) }, }) const SearchButton = ({text = ""}) => searchItem({ materialIconName: "travel_explore", name: "Search Google", actionName: "Go", content: `${text}`, onActivate: () => { App.closeWindow("overview") execAsync(["xdg-open", `https://www.google.com/search?q=${text}`]).catch(print) }, }) const ContextWorkspaceArray = ({label, onClickBinary, thisWorkspace}) => Widget({ type: Gtk.MenuItem, label: `${label}`, setup: menuItem => { let submenu = new Gtk.Menu() submenu.className = "menu" for (let i = 1; i <= 10; i++) { let button = new Gtk.MenuItem({label: `${i}`}) button.connect("activate", () => { execAsync([`${onClickBinary}`, `${thisWorkspace}`, `${i}`]).catch(print) }) submenu.append(button) } menuItem.set_reserve_indicator(true) menuItem.set_submenu(submenu) }, }) const client = ({address, size: [w, h], workspace: {id, name}, class: c, title}) => Widget.Button({ className: "overview-tasks-window", halign: "center", valign: "center", onClicked: () => { execAsync([`bash`, `-c`, `hyprctl dispatch focuswindow address:${address}`, `&`]).catch(print) App.closeWindow("overview") }, onMiddleClick: () => execAsync([`bash`, `-c`, `hyprctl dispatch closewindow address:${address}`, `&`]).catch(print), onSecondaryClick: button => { button.toggleClassName("overview-tasks-window-selected", true) const menu = Widget({ type: Gtk.Menu, className: "menu", setup: menu => { menu.append( ContextMenuItem({ label: "Close (Middle-click)", onClick: () => { execAsync([`bash`, `-c`, `hyprctl dispatch closewindow address:${address}`, `&`]).catch(print) destroyContextMenu(menu) }, }), ) menu.append( ContextWorkspaceArray({ label: "Dump windows to workspace", onClickBinary: `${App.configDir}/scripts/dumptows`, thisWorkspace: Number(id), }), ) menu.append( ContextWorkspaceArray({ label: "Swap windows with workspace", onClickBinary: `${App.configDir}/scripts/dumptows`, thisWorkspace: Number(id), }), ) menu.show_all() }, }) menu.connect("deactivate", () => { button.toggleClassName("overview-tasks-window-selected", false) }) menu.connect("selection-done", () => { button.toggleClassName("overview-tasks-window-selected", false) }) menu.popup_at_pointer(null) // Show the menu at the pointer's position }, child: Widget.Box({ vertical: true, children: [ Widget.Icon({ style: ` min-width: ${w * OVERVIEW_SCALE - 4}px; min-height: ${h * OVERVIEW_SCALE - 4}px; `, size: (Math.min(w, h) * OVERVIEW_SCALE) / 2.5, icon: substitute(c), }), Widget.Scrollable({ hexpand: true, vexpand: true, child: Widget.Label({ style: ` font-size: ${(Math.min(w, h) * OVERVIEW_SCALE) / 20}px; `, label: title, }), }), ], }), tooltipText: `${c}: ${title}`, setup: button => { setupCursorHoverAim(button) button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.MOVE) button.drag_source_set_icon_name(substitute(c)) // button.drag_source_set_icon_gicon(icon); button.connect("drag-begin", button => { // On drag start, add the dragging class button.toggleClassName("overview-tasks-window-dragging", true) }) button.connect("drag-data-get", (_w, _c, data) => { // On drag finish, give address data.set_text(address, address.length) button.toggleClassName("overview-tasks-window-dragging", false) }) // button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.COPY); // button.connect('drag-data-get', (_w, _c, data) => data.set_text(address, address.length)); // button.connect('drag-begin', (_, context) => { // Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(button)); // button.toggleClassName('hidden', true); // }); // button.connect('drag-end', () => button.toggleClassName('hidden', false)); }, }) const workspace = index => { const fixed = Gtk.Fixed.new() const widget = Widget.Box({ className: "overview-tasks-workspace", valign: "center", style: ` min-width: ${SCREEN_WIDTH * OVERVIEW_SCALE}px; min-height: ${SCREEN_HEIGHT * OVERVIEW_SCALE}px; `, connections: [ [ Hyprland, box => { box.toggleClassName("active", Hyprland.active.workspace.id === index) }, ], ], children: [ Widget.EventBox({ hexpand: true, vexpand: true, onPrimaryClickRelease: () => { execAsync([`bash`, `-c`, `hyprctl dispatch workspace ${index}`, `&`]).catch(print) App.closeWindow("overview") }, // onSecondaryClick: (eventbox) => { // const menu = Widget({ // type: Gtk.Menu, // setup: menu => { // menu.append(ContextWorkspaceArray({ label: "Dump windows to workspace", onClickBinary: `${App.configDir}/scripts/dumptows`, thisWorkspace: Number(index) })); // menu.append(ContextWorkspaceArray({ label: "Swap windows with workspace", onClickBinary: `${App.configDir}/scripts/dumptows`, thisWorkspace: Number(index) })); // menu.show_all(); // } // }); // menu.popup_at_pointer(null); // Show the menu at the pointer's position // }, setup: eventbox => { eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY) eventbox.connect("drag-data-received", (_w, _c, _x, _y, data) => { execAsync([ `bash`, `-c`, `hyprctl dispatch movetoworkspacesilent ${index},address:${data.get_text()}`, `&`, ]).catch(print) }) }, child: fixed, }), ], }) widget.update = clients => { clients = clients.filter(({workspace: {id}}) => id === index) // this is for my monitor layout // shifts clients back by SCREEN_WIDTHpx if necessary clients = clients.map(client => { // console.log(client); const [x, y] = client.at if (x > SCREEN_WIDTH) client.at = [x - SCREEN_WIDTH, y] return client }) fixed.get_children().forEach(ch => ch.destroy()) clients.forEach(c => c.mapped && fixed.put(client(c), c.at[0] * OVERVIEW_SCALE, c.at[1] * OVERVIEW_SCALE)) fixed.show_all() } return widget } const arr = (s, n) => { const array = [] for (let i = 0; i < n; i++) array.push(s + i) return array } const OverviewRow = ({startWorkspace = 1, workspaces = 5, windowName = "overview"}) => Widget.Box({ children: arr(startWorkspace, workspaces).map(workspace), properties: [ [ "update", box => { execAsync("hyprctl -j clients") .then(clients => { const json = JSON.parse(clients) box.get_children().forEach(ch => ch.update(json)) }) .catch(print) }, ], ], setup: box => box._update(box), connections: [ [ Hyprland, box => { if (!App.getWindow(windowName).visible) return box._update(box) }, ], ], }) export const SearchAndWindows = () => { var _appSearchResults = [] const clickOutsideToClose = Widget.EventBox({ onPrimaryClick: () => App.closeWindow("overview"), onSecondaryClick: () => App.closeWindow("overview"), onMiddleClick: () => App.closeWindow("overview"), }) const resultsBox = Widget.Box({ className: "spacing-v-15 overview-search-results", vertical: true, vexpand: true, }) const resultsRevealer = Widget.Revealer({ transitionDuration: 200, revealChild: false, transition: "slide_down", // duration: 200, halign: "center", child: resultsBox, }) const overviewRevealer = Widget.Revealer({ revealChild: true, transition: "slide_down", transitionDuration: 200, child: Widget.Box({ vertical: true, className: "overview-tasks", children: [ OverviewRow({startWorkspace: 1, workspaces: 5}), OverviewRow({startWorkspace: 6, workspaces: 5}), ], }), }) const entryPromptRevealer = Widget.Revealer({ transition: "crossfade", transitionDuration: 150, revealChild: true, halign: "center", child: Widget.Label({ className: "overview-search-prompt txt-small txt", label: searchPromptTexts[Math.floor(Math.random() * searchPromptTexts.length)], }), }) const entryIconRevealer = Widget.Revealer({ transition: "crossfade", transitionDuration: 150, revealChild: false, halign: "end", child: Widget.Label({ className: "txt txt-large icon-material overview-search-icon", label: "search", }), }) 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", halign: "center", onAccept: ({text}) => { // This is when you press Enter const isAction = text.startsWith(">") if (startsWithNumber(text)) { // Eval on typing is dangerous, this is a workaround try { const fullResult = eval(text) // copy execAsync(["bash", "-c", `wl-copy '${fullResult}'`, `&`]).catch(print) App.closeWindow("overview") return } catch (e) { // console.log(e); } } if (_appSearchResults.length > 0) { App.closeWindow("overview") _appSearchResults[0].launch() return } else if (text[0] == ">") { // Custom commands 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(["xdg-open", `https://www.google.com/search?q=${text}`]).catch(print) } }, // Actually onChange but this is ta workaround for a bug connections: [ [ "notify::text", entry => { // This is when you type const isAction = entry.text.startsWith(">") resultsBox.get_children().forEach(ch => ch.destroy()) //check empty if so then dont do stuff if (entry.text == "") { resultsRevealer.set_reveal_child(false) overviewRevealer.set_reveal_child(true) entryPromptRevealer.set_reveal_child(true) entryIconRevealer.set_reveal_child(false) entry.toggleClassName("overview-search-box-extended", false) searching = false } else { const text = entry.text resultsRevealer.set_reveal_child(true) overviewRevealer.set_reveal_child(false) entryPromptRevealer.set_reveal_child(false) entryIconRevealer.set_reveal_child(true) entry.toggleClassName("overview-search-box-extended", true) _appSearchResults = Applications.query(text) // Calculate if (startsWithNumber(text)) { // Eval on typing is dangerous, this is a workaround. try { const fullResult = eval(text) resultsBox.add(CalculationResultButton({result: fullResult, text: text})) } catch (e) { // console.log(e); } } 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 && 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() searching = true } }, ], ], }) return Widget.Box({ vertical: true, children: [ clickOutsideToClose, Widget.Box({ halign: "center", children: [ entry, Widget.Box({ className: "overview-search-icon-box", setup: box => box.pack_start(entryPromptRevealer, true, true, 0), }), entryIcon, ], }), overviewRevealer, resultsRevealer, ], connections: [ [ App, (_b, name, visible) => { if (name == "overview" && !visible) { entryPromptRevealer.child.label = searchPromptTexts[Math.floor(Math.random() * searchPromptTexts.length)] resultsBox.children = [] entry.set_text("") } }, ], ], }) }