From 9a9cfc13f68520cd2e9fc2687b019dc3e77068e8 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Wed, 1 May 2024 15:03:42 -0400 Subject: [PATCH] refactor(ags): abstract appL and clip with SortedList --- modules/ags/config/global-types.d.ts | 13 -- modules/ags/config/ts/applauncher/app-item.ts | 6 - modules/ags/config/ts/applauncher/main.ts | 162 +++++---------- modules/ags/config/ts/clipboard/main.ts | 196 ++++++------------ modules/ags/config/ts/misc/popup.ts | 26 ++- modules/ags/config/ts/misc/sorted-list.ts | 188 +++++++++++++++++ modules/ags/config/tsconfig.json | 4 +- 7 files changed, 317 insertions(+), 278 deletions(-) create mode 100644 modules/ags/config/ts/misc/sorted-list.ts diff --git a/modules/ags/config/global-types.d.ts b/modules/ags/config/global-types.d.ts index 6f7d47b..3f04e34 100644 --- a/modules/ags/config/global-types.d.ts +++ b/modules/ags/config/global-types.d.ts @@ -115,22 +115,9 @@ export type CursorBox = CursorBox; export type CursorBoxProps = CursorBoxProps; // For PopupWindow -export type PopupChild = Binding< -Var, -'is_listening' | 'is_polling' | 'value', -Widget[] ->; export type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' | 'slide right' | 'popin' | 'fade'; export type CloseType = 'none' | 'stay' | 'released' | 'clicked'; -export type PopupWindowProps = -WindowProps & { - transition?: HyprTransition; - on_open?(self: PopupWindow): void - on_close?(self: PopupWindow): void - close_on_unfocus?: CloseType - anchor?: Array<'top' | 'bottom' | 'right' | 'left'>; -}; import { PopupWindow } from 'ts/misc/popup'; export type PopupWindow = PopupWindow; diff --git a/modules/ags/config/ts/applauncher/app-item.ts b/modules/ags/config/ts/applauncher/app-item.ts index 99b6336..639b322 100644 --- a/modules/ags/config/ts/applauncher/app-item.ts +++ b/modules/ags/config/ts/applauncher/app-item.ts @@ -2,7 +2,6 @@ const { Box, Icon, Label } = Widget; const { lookUpIcon } = Utils; import CursorBox from '../misc/cursorbox.ts'; -import { launchApp } from './launch.ts'; /* Types */ import { Application } from 'types/service/applications.ts'; @@ -54,11 +53,6 @@ export default (app: Application) => { attribute: { app }, - on_primary_click_release: () => { - App.closeWindow('win-applauncher'); - launchApp(app); - }, - child: Box({ children: [ icon, diff --git a/modules/ags/config/ts/applauncher/main.ts b/modules/ags/config/ts/applauncher/main.ts index 19f20b1..59173f7 100644 --- a/modules/ags/config/ts/applauncher/main.ts +++ b/modules/ags/config/ts/applauncher/main.ts @@ -1,87 +1,67 @@ const Applications = await Service.import('applications'); -const { Box, Entry, Icon, Label, ListBox, Revealer, Scrollable } = Widget; import { Fzf, FzfResultItem } from 'fzf'; -import PopupWindow from '../misc/popup.ts'; +import SortedList from '../misc/sorted-list.ts'; import AppItem from './app-item.ts'; + import { launchApp } from './launch.ts'; -// Types +/* Types */ import { ListBoxRow } from 'types/@girs/gtk-3.0/gtk-3.0.cjs'; import { Application } from 'types/service/applications.ts'; import { AgsAppItem } from 'global-types'; -const Applauncher = (window_name = 'applauncher') => { +export default () => { let fzfResults: FzfResultItem[]; - const list = ListBox(); - const setSort = (text: string) => { - const fzf = new Fzf(Applications.list, { - selector: (app) => app.name + app.executable, + return SortedList({ + name: 'applauncher', + class_name: 'applauncher', + transition: 'slide top', - tiebreakers: [ - (a, b) => b.item.frequency - a.item.frequency, - ], - }); - - fzfResults = fzf.find(text); - list.set_sort_func((a, b) => { - const row1 = (a.get_children()[0] as AgsAppItem).attribute.app.name; - const row2 = (b.get_children()[0] as AgsAppItem).attribute.app.name; - - const s1 = fzfResults.find((r) => r.item.name === row1)?.score ?? 0; - const s2 = fzfResults.find((r) => r.item.name === row2)?.score ?? 0; - - return s2 - s1; - }); - }; - - const makeNewChildren = () => { - const rows = list.get_children() as ListBoxRow[]; - - rows.forEach((ch) => { - ch.destroy(); - }); - - const children = Applications.query('') - .flatMap((app) => AppItem(app)); - - children.forEach((ch) => { - list.add(ch); - }); - list.show_all(); - }; - - makeNewChildren(); - - const placeholder = Revealer({ - child: Label({ - label: " Couldn't find a match", - class_name: 'placeholder', - }), - }); - - const entry = Entry({ - // Set some text so on-change works the first time - text: '-', - hexpand: true, - - on_accept: ({ text }) => { - const appList = Applications.query(text || ''); - - if (appList[0]) { - App.closeWindow(`win-${window_name}`); - launchApp(appList[0]); - } + on_select: (r) => { + App.closeWindow('win-applauncher'); + launchApp((r.get_child() as AgsAppItem).attribute.app); }, - on_change: ({ text }) => { - if (text === null) { - return; - } - setSort(text); + init_rows: (list) => { + const rows = list.get_children() as ListBoxRow[]; + + rows.forEach((ch) => { + ch.destroy(); + }); + + const children = Applications.query('') + .flatMap((app) => AppItem(app)); + + children.forEach((ch) => { + list.add(ch); + }); + list.show_all(); + }, + + set_sort: (text, list, placeholder) => { + const fzf = new Fzf(Applications.list, { + selector: (app) => app.name + app.executable, + + tiebreakers: [ + (a, b) => b.item.frequency - a.item.frequency, + ], + }); + + fzfResults = fzf.find(text); + list.set_sort_func((a, b) => { + const row1 = (a.get_children()[0] as AgsAppItem).attribute.app.name; + const row2 = (b.get_children()[0] as AgsAppItem).attribute.app.name; + + const s1 = fzfResults.find((r) => r.item.name === row1)?.score ?? 0; + const s2 = fzfResults.find((r) => r.item.name === row2)?.score ?? 0; + + return s2 - s1; + }); + let visibleApps = 0; const rows = list.get_children() as ListBoxRow[]; @@ -106,52 +86,4 @@ const Applauncher = (window_name = 'applauncher') => { placeholder.reveal_child = visibleApps <= 0; }, }); - - return Box({ - class_name: 'applauncher', - vertical: true, - - setup: (self) => { - self.hook(App, (_, name, visible) => { - if (name !== `win-${window_name}`) { - return; - } - - entry.text = ''; - - if (visible) { - entry.grab_focus(); - } - else { - makeNewChildren(); - } - }); - }, - - children: [ - Box({ - class_name: 'header', - children: [ - Icon('preferences-system-search-symbolic'), - entry, - ], - }), - - Scrollable({ - hscroll: 'never', - vscroll: 'automatic', - child: Box({ - vertical: true, - children: [list, placeholder], - }), - }), - ], - }); }; - -export default () => PopupWindow({ - name: 'applauncher', - transition: 'slide top', - keymode: 'on-demand', - child: Applauncher(), -}); diff --git a/modules/ags/config/ts/clipboard/main.ts b/modules/ags/config/ts/clipboard/main.ts index bfd66fc..db1141a 100644 --- a/modules/ags/config/ts/clipboard/main.ts +++ b/modules/ags/config/ts/clipboard/main.ts @@ -1,14 +1,11 @@ -const { Box, Entry, Icon, Label, ListBox, Scrollable } = Widget; +const { Box, Icon, Label } = Widget; const { execAsync } = Utils; -const Hyprland = await Service.import('hyprland'); - import { Fzf, FzfResultItem } from 'fzf'; import Gtk from 'gi://Gtk?version=3.0'; import CursorBox from '../misc/cursorbox.ts'; -import PopupWindow from '../misc/popup.ts'; -import { Monitor } from 'types/service/hyprland'; +import SortedList from '../misc/sorted-list.ts'; const N_ITEMS = 30; @@ -27,39 +24,7 @@ export default () => { const getKey = (r: Gtk.ListBoxRow) => parseInt(r.get_child()?.name ?? ''); - const list = ListBox().on('row-activated', (_, row) => { - copyOldItem(getKey(row)); - }); - - const updateItems = () => { - (list.get_children() as Gtk.ListBoxRow[]).forEach((r) => { - r.changed(); - }); - }; - - const setSort = (text: string) => { - if (text === '' || text === '-') { - list.set_sort_func((row1, row2) => getKey(row2) - getKey(row1)); - } - else { - const fzf = new Fzf(CopiedItems, { - selector: (item) => item[0], - - tiebreakers: [(a, b) => b[1] - a[1]], - }); - - fzfResults = fzf.find(text); - list.set_sort_func((a, b) => { - const row1 = fzfResults.find((f) => f.item[1] === getKey(a))?.score ?? 0; - const row2 = fzfResults.find((f) => f.item[1] === getKey(b))?.score ?? 0; - - return row2 - row1; - }); - } - updateItems(); - }; - - const makeItem = (key: string, val: string) => { + const makeItem = (list: Gtk.ListBox, key: string, val: string) => { CopiedItems.push([val, parseInt(key)]); const widget = CursorBox({ @@ -87,110 +52,67 @@ export default () => { list.add(widget); widget.show_all(); - updateItems(); - }; - - // Decode old item: - const decodeItem = (index: string) => { - execAsync([ - 'bash', '-c', `cliphist list | grep ${index} | cliphist decode`, - ]).then((out) => { - makeItem(index, out); + (list.get_children() as Gtk.ListBoxRow[]).forEach((r) => { + r.changed(); }); }; - const entry = Entry({ - // Set some text so on-change works the first time - text: '-', - hexpand: true, + // Decode old item: + const decodeItem = (list: Gtk.ListBox, index: string) => { + execAsync([ + 'bash', '-c', `cliphist list | grep ${index} | cliphist decode`, + ]).then((out) => { + makeItem(list, index, out); + }); + }; - on_change: ({ text }) => { - if (text !== null) { - setSort(text); + return SortedList({ + name: 'clipboard', + class_name: 'clipboard', + transition: 'slide top', + + on_select: (r) => copyOldItem(getKey(r)), + + init_rows: (list) => { + CopiedItems = []; + + execAsync('clipboard-manager').then((out) => { + list.get_children()?.forEach((ch) => { + ch.destroy(); + }); + + const items = out.split('\n'); + + for (let i = 0; i < N_ITEMS; ++i) { + if (items[i].includes('img')) { + makeItem(list, (items[i].match('[0-9]+') ?? [''])[0], items[i]); + } + else { + decodeItem(list, items[i]); + } + } + }).catch(console.error); + }, + + set_sort: (text, list) => { + if (text === '' || text === '-') { + list.set_sort_func((row1, row2) => getKey(row2) - getKey(row1)); + } + else { + const fzf = new Fzf(CopiedItems, { + selector: (item) => item[0], + + tiebreakers: [(a, b) => b[1] - a[1]], + }); + + fzfResults = fzf.find(text); + list.set_sort_func((a, b) => { + const row1 = fzfResults.find((f) => f.item[1] === getKey(a))?.score ?? 0; + const row2 = fzfResults.find((f) => f.item[1] === getKey(b))?.score ?? 0; + + return row2 - row1; + }); } }, }); - - const on_open = () => { - CopiedItems = []; - entry.text = ''; - - execAsync('clipboard-manager').then(async(out) => { - list.get_children()?.forEach((ch) => { - ch.destroy(); - }); - - const items = out.split('\n'); - - for (let i = 0; i < N_ITEMS; ++i) { - if (items[i].includes('img')) { - makeItem((items[i].match('[0-9]+') ?? [''])[0], items[i]); - } - else { - decodeItem(items[i]); - } - } - - let x: number; - let y: number; - const monitor = (JSON.parse(await Hyprland.messageAsync('j/monitors')) as Monitor[]) - .find((m) => m.focused) as Monitor; - - switch (monitor.transform) { - case 1: - x = monitor.x - (monitor.height / 2); - y = monitor.y - (monitor.width / 2); - break; - - case 2: - x = monitor.x - (monitor.width / 2); - y = monitor.y - (monitor.height / 2); - break; - - case 3: - x = monitor.x + (monitor.height / 2); - y = monitor.y + (monitor.width / 2); - break; - - default: - x = monitor.x + (monitor.width / 2); - y = monitor.y + (monitor.height / 2); - break; - } - - await Hyprland.messageAsync(`dispatch movecursor ${x} ${y}`); - entry.grab_focus(); - }).catch(console.log); - }; - - on_open(); - - return PopupWindow({ - name: 'clipboard', - keymode: 'on-demand', - on_open, - - child: Box({ - class_name: 'clipboard', - vertical: true, - children: [ - Box({ - class_name: 'header', - children: [ - Icon('preferences-system-search-symbolic'), - entry, - ], - }), - - Scrollable({ - hscroll: 'never', - vscroll: 'automatic', - child: Box({ - vertical: true, - children: [list], - }), - }), - ], - }), - }); }; diff --git a/modules/ags/config/ts/misc/popup.ts b/modules/ags/config/ts/misc/popup.ts index a4769fb..adce313 100644 --- a/modules/ags/config/ts/misc/popup.ts +++ b/modules/ags/config/ts/misc/popup.ts @@ -3,13 +3,29 @@ const Hyprland = await Service.import('hyprland'); /* Types */ import { Window } from 'resource:///com/github/Aylur/ags/widgets/window.js'; +import type { WindowProps } from 'types/widgets/window'; +import type { Widget as AgsWidget } from 'types/widgets/widget'; import { CloseType, HyprTransition, - PopupWindowProps, } from 'global-types'; +export type PopupWindowProps< + Child extends Gtk.Widget, + Attr = unknown, + Self = PopupWindow, +> = WindowProps & { + transition?: HyprTransition; + on_open?(self: PopupWindow): void; + on_close?(self: PopupWindow): void; + close_on_unfocus?: CloseType; + anchor?: Array<'top' | 'bottom' | 'right' | 'left'>; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface PopupWindow extends AgsWidget { } + export class PopupWindow< Child extends Gtk.Widget, @@ -50,7 +66,7 @@ export class PopupWindow< } - private _on_open: (self: PopupWindow) => void; + protected _on_open: (self: PopupWindow) => void; get on_open() { return this._on_open; @@ -114,10 +130,10 @@ export class PopupWindow< this.transition = transition; if (isOpen) { - this.on_open(this); + this._on_open(this); } else { - this.on_close(this); + this._on_close(this); } } }); @@ -147,6 +163,6 @@ export class PopupWindow< } } -export default ( +export default ( props: PopupWindowProps, ) => new PopupWindow(props); diff --git a/modules/ags/config/ts/misc/sorted-list.ts b/modules/ags/config/ts/misc/sorted-list.ts new file mode 100644 index 0000000..7826258 --- /dev/null +++ b/modules/ags/config/ts/misc/sorted-list.ts @@ -0,0 +1,188 @@ +const Hyprland = await Service.import('hyprland'); + +const { Box, Entry, Icon, Label, ListBox, Revealer, Scrollable } = Widget; + +/* Types */ +import { PopupWindow, PopupWindowProps } from '../misc/popup.ts'; +import type { Widget as AgsWidget } from 'types/widgets/widget'; +import { ListBoxRow } from 'types/@girs/gtk-3.0/gtk-3.0.cjs'; +import { Monitor } from 'types/service/hyprland'; +import { Binding } from 'types/service'; + +type MakeChild = ReturnType; + +type SortedListProps> = + PopupWindowProps & { + on_select: (row: ListBoxRow) => void; + init_rows: (list: MakeChild['list']) => void; + set_sort: ( + text: string, + list: MakeChild['list'], + placeholder: MakeChild['placeholder'], + ) => void; + }; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface SortedList extends AgsWidget { } + + +const centerCursor = async(): Promise => { + let x: number; + let y: number; + const monitor = (JSON.parse(await Hyprland.messageAsync('j/monitors')) as Monitor[]) + .find((m) => m.focused) as Monitor; + + switch (monitor.transform) { + case 1: + x = monitor.x - (monitor.height / 2); + y = monitor.y - (monitor.width / 2); + break; + + case 2: + x = monitor.x - (monitor.width / 2); + y = monitor.y - (monitor.height / 2); + break; + + case 3: + x = monitor.x + (monitor.height / 2); + y = monitor.y + (monitor.width / 2); + break; + + default: + x = monitor.x + (monitor.width / 2); + y = monitor.y + (monitor.height / 2); + break; + } + + await Hyprland.messageAsync(`dispatch movecursor ${x} ${y}`); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const makeChild = (class_name: string | Binding) => { + const list = ListBox(); + + const placeholder = Revealer({ + child: Label({ + label: " Couldn't find a match", + class_name: 'placeholder', + }), + }); + + const entry = Entry({ + // Set some text so on-change works the first time + text: '-', + hexpand: true, + }); + + return { + list, + entry, + placeholder, + child: Box({ + class_name, + vertical: true, + children: [ + Box({ + class_name: 'header', + children: [ + Icon('preferences-system-search-symbolic'), + entry, + ], + }), + + Scrollable({ + hscroll: 'never', + vscroll: 'automatic', + child: Box({ + vertical: true, + children: [list, placeholder], + }), + }), + ], + }), + }; +}; + +export class SortedList< + Attr, +> extends PopupWindow { + static { + Widget.register(this, { + properties: { + }, + }); + } + + private _list: MakeChild['list']; + private _entry: MakeChild['entry']; + private _placeholder: MakeChild['placeholder']; + private _on_select: (row: ListBoxRow) => void; + private _init_rows: (list: MakeChild['list']) => void; + private _set_sort: ( + text: string, + list: MakeChild['list'], + placeholder: MakeChild['placeholder'], + ) => void; + + set on_open(fun: (self: PopupWindow) => void) { + this._on_open = () => { + this._entry.text = ''; + fun(this); + this._init_rows(this._list); + centerCursor(); + this._entry.grab_focus(); + }; + } + + constructor({ + on_select, + init_rows, + set_sort, + on_open = () => {/**/}, + class_name = '', + keymode = 'on-demand', + ...rest + }: SortedListProps>) { + const makeChildResult = makeChild(class_name); + + // PopupWindow + super({ + child: makeChildResult.child, + keymode, + ...rest, + }); + + this.on_open = on_open; + + // SortedList + this._on_select = on_select; + this._init_rows = init_rows; + this._set_sort = set_sort; + + this._placeholder = makeChildResult.placeholder; + + this._list = makeChildResult.list; + this._list.on('row-activated', (_, row) => { + this._on_select(row); + }); + + this._entry = makeChildResult.entry; + + this._entry.on_change = ({ text }) => { + if (text !== null) { + this._set_sort(text, this._list, this._placeholder); + (this._list.get_children() as ListBoxRow[]).forEach((r) => { + r.changed(); + }); + } + }; + // TODO: add on_accept where it just selects the first visible one + + this._init_rows(this._list); + this._set_sort('', this._list, this._placeholder); + } +} + +export default ( + props: SortedListProps>, +) => new SortedList(props); diff --git a/modules/ags/config/tsconfig.json b/modules/ags/config/tsconfig.json index 73b6a4b..33c6fa3 100644 --- a/modules/ags/config/tsconfig.json +++ b/modules/ags/config/tsconfig.json @@ -11,12 +11,12 @@ "noImplicitAny": false, "baseUrl": ".", "paths": { - "fzf": ["./node_modules/fzf/dist/types"], + "fzf": ["./node_modules/fzf/dist/types"] }, "typeRoots": [ "./types", "./global-types.d.ts", - "./node_modules", + "./node_modules" ], "skipLibCheck": true, "forceConsistentCasingInFileNames": true