refactor(ags): abstract appL and clip with SortedList
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2024-05-01 15:03:42 -04:00
parent 384e0c24b3
commit 9a9cfc13f6
7 changed files with 317 additions and 278 deletions

View file

@ -115,22 +115,9 @@ export type CursorBox = CursorBox;
export type CursorBoxProps = CursorBoxProps;
// For PopupWindow
export type PopupChild = Binding<
Var<Widget>,
'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<Child extends Widget, Attr = unknown> =
WindowProps<Child> & {
transition?: HyprTransition;
on_open?(self: PopupWindow<Child, Attr>): void
on_close?(self: PopupWindow<Child, Attr>): void
close_on_unfocus?: CloseType
anchor?: Array<'top' | 'bottom' | 'right' | 'left'>;
};
import { PopupWindow } from 'ts/misc/popup';
export type PopupWindow = PopupWindow;

View file

@ -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,

View file

@ -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<Application>[];
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(),
});

View file

@ -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],
}),
}),
],
}),
});
};

View file

@ -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<Child, Attr>,
> = WindowProps<Child, Attr, Self> & {
transition?: HyprTransition;
on_open?(self: PopupWindow<Child, Attr>): void;
on_close?(self: PopupWindow<Child, Attr>): void;
close_on_unfocus?: CloseType;
anchor?: Array<'top' | 'bottom' | 'right' | 'left'>;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface PopupWindow<Child, Attr> extends AgsWidget<Attr> { }
export class PopupWindow<
Child extends Gtk.Widget,
@ -50,7 +66,7 @@ export class PopupWindow<
}
private _on_open: (self: PopupWindow<Child, Attr>) => void;
protected _on_open: (self: PopupWindow<Child, Attr>) => 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 <Child extends Gtk.Widget, Attr>(
export default <Child extends Gtk.Widget, Attr> (
props: PopupWindowProps<Child, Attr>,
) => new PopupWindow(props);

View file

@ -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<typeof makeChild>;
type SortedListProps<Attr = unknown, Self = SortedList<Attr>> =
PopupWindowProps<MakeChild['child'], Attr, Self> & {
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<Attr> extends AgsWidget<Attr> { }
const centerCursor = async(): Promise<void> => {
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<any, any, string>) => {
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<MakeChild['child'], Attr> {
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<MakeChild['child'], Attr>) => 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<Attr, PopupWindow<MakeChild['child'], Attr>>) {
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 <Attr>(
props: SortedListProps<Attr, PopupWindow<MakeChild['child'], Attr>>,
) => new SortedList(props);

View file

@ -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