From ccdde8135603ba2d96b896a96975d52f94b75350 Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Mon, 28 Oct 2024 18:18:07 -0400
Subject: [PATCH] refactor(agsV2): abstract sorted-list code

---
 nixosModules/ags/v2/style.scss                |   1 +
 .../ags/v2/widgets/applauncher/main.tsx       | 139 +++-----------
 .../ags/v2/widgets/applauncher/style.scss     |  32 +---
 .../ags/v2/widgets/icon-browser/main.tsx      | 134 ++------------
 .../ags/v2/widgets/icon-browser/style.scss    |  34 +---
 .../ags/v2/widgets/misc/popup-window.tsx      |   4 +-
 .../ags/v2/widgets/misc/sorted-list.tsx       | 174 ++++++++++++++++++
 nixosModules/ags/v2/widgets/misc/style.scss   |  27 +++
 8 files changed, 250 insertions(+), 295 deletions(-)
 create mode 100644 nixosModules/ags/v2/widgets/misc/sorted-list.tsx
 create mode 100644 nixosModules/ags/v2/widgets/misc/style.scss

diff --git a/nixosModules/ags/v2/style.scss b/nixosModules/ags/v2/style.scss
index fbbbb6e0..7717419f 100644
--- a/nixosModules/ags/v2/style.scss
+++ b/nixosModules/ags/v2/style.scss
@@ -17,6 +17,7 @@ window, viewport {
 @import 'widgets/date/style.scss';
 @import 'widgets/icon-browser/style.scss';
 @import 'widgets/lockscreen/style.scss';
+@import 'widgets/misc/style.scss';
 @import 'widgets/notifs/style.scss';
 @import 'widgets/powermenu/style.scss';
 @import 'widgets/screenshot/style.scss';
diff --git a/nixosModules/ags/v2/widgets/applauncher/main.tsx b/nixosModules/ags/v2/widgets/applauncher/main.tsx
index 89e7c812..5a6bab62 100644
--- a/nixosModules/ags/v2/widgets/applauncher/main.tsx
+++ b/nixosModules/ags/v2/widgets/applauncher/main.tsx
@@ -1,66 +1,36 @@
-import { App, Astal, Gtk, Widget } from 'astal/gtk3';
-import { idle } from 'astal';
+import { App } from 'astal/gtk3';
 
 import AstalApps from 'gi://AstalApps';
 
-import { Fzf, FzfResultItem } from 'fzf';
+import SortedList from '../misc/sorted-list';
 
-import PopupWindow from '../misc/popup-window';
-import { centerCursor } from '../../lib';
-
-import AppItemWidget, { AppItem } from './app-item';
 import { launchApp } from './launch';
+import AppItemWidget, { AppItem } from './app-item';
 
 
-export default () => {
-    let Applications: AstalApps.Application[] = [];
-    let fzfResults = [] as FzfResultItem<AstalApps.Application>[];
+export default () => SortedList({
+    name: 'applauncher',
 
-    const list = new Gtk.ListBox({
-        selectionMode: Gtk.SelectionMode.SINGLE,
-    });
+    create_list: () => AstalApps.Apps.new().get_list(),
 
-    list.connect('row-activated', (_, row) => {
+    create_row: (app) => AppItemWidget({ app }),
+
+    fzf_options: {
+        selector: (app) => app.name + app.executable,
+
+        tiebreakers: [
+            (a, b) => b.item.frequency - a.item.frequency,
+        ],
+    },
+
+    on_row_activated: (row) => {
         const app = (row.get_children()[0] as AppItem).app;
 
         launchApp(app);
         App.get_window('win-applauncher')?.set_visible(false);
-    });
+    },
 
-    const placeholder = (
-        <revealer>
-            <label
-                label="   Couldn't find a match"
-                className="placeholder"
-            />
-        </revealer>
-    ) as Widget.Revealer;
-
-    const on_text_change = (text: string) => {
-        const fzf = new Fzf(Applications, {
-            selector: (app) => app.name + app.executable,
-
-            tiebreakers: [
-                (a, b) => b.item.frequency - a.item.frequency,
-            ],
-        });
-
-        fzfResults = fzf.find(text);
-        list.invalidate_sort();
-
-        const visibleApplications = list.get_children().filter((row) => row.visible).length;
-
-        placeholder.reveal_child = visibleApplications <= 0;
-    };
-
-    const entry = (
-        <entry
-            onChanged={(self) => on_text_change(self.text)}
-            hexpand
-        />
-    ) as Widget.Entry;
-
-    list.set_sort_func((a, b) => {
+    sort_func: (a, b, entry, fzfResults) => {
         const row1 = (a.get_children()[0] as AppItem).app;
         const row2 = (b.get_children()[0] as AppItem).app;
 
@@ -79,72 +49,5 @@ export default () => {
 
             return s2 - s1;
         }
-    });
-
-    const refreshApplications = () => idle(() => {
-        (list.get_children() as Gtk.ListBoxRow[])
-            .forEach((child) => {
-                child.destroy();
-            });
-
-        Applications = AstalApps.Apps.new().get_list();
-
-        Applications
-            .flatMap((app) => AppItemWidget({ app }))
-            .forEach((child) => {
-                list.add(child);
-            });
-
-        list.show_all();
-        on_text_change('');
-    });
-
-    refreshApplications();
-
-    return (
-        <PopupWindow
-            name="applauncher"
-            keymode={Astal.Keymode.ON_DEMAND}
-            on_open={() => {
-                entry.text = '';
-                centerCursor();
-            }}
-        >
-            <box
-                vertical
-                className="applauncher"
-            >
-                <box className="widget app-search">
-
-                    <icon icon="preferences-system-search-symbolic" />
-
-                    {entry}
-
-                    <button
-                        css="margin-left: 5px;"
-                        cursor="pointer"
-                        onButtonReleaseEvent={refreshApplications}
-                    >
-                        <icon icon="view-refresh-symbolic" css="font-size: 26px;" />
-                    </button>
-
-                </box>
-
-                <eventbox cursor="pointer">
-                    <scrollable
-                        className="widget app-list"
-
-                        css="min-height: 600px; min-width: 600px;"
-                        hscroll={Gtk.PolicyType.NEVER}
-                        vscroll={Gtk.PolicyType.AUTOMATIC}
-                    >
-                        <box vertical>
-                            {list}
-                            {placeholder}
-                        </box>
-                    </scrollable>
-                </eventbox>
-            </box>
-        </PopupWindow>
-    );
-};
+    },
+});
diff --git a/nixosModules/ags/v2/widgets/applauncher/style.scss b/nixosModules/ags/v2/widgets/applauncher/style.scss
index 2ee89180..cbba15fc 100644
--- a/nixosModules/ags/v2/widgets/applauncher/style.scss
+++ b/nixosModules/ags/v2/widgets/applauncher/style.scss
@@ -1,32 +1,6 @@
 .applauncher {
-    .app-search {
-        icon {
-            font-size: 20px;
-            min-width: 40px;
-            min-height: 40px
-        }
-
-        entry {}
-    }
-
-    .app-list {
-        row {
-            border-radius: 10px;
-
-            &:hover, &:selected {
-                icon {
-                    -gtk-icon-shadow: 2px 2px $accent_color;
-                }
-            }
-
-            .app {
-                margin: 20px;
-                font-size: 16px;
-            }
-        }
-
-        .placeholder {
-            font-size: 20px;
-        }
+    .app {
+        margin: 20px;
+        font-size: 16px;
     }
 }
diff --git a/nixosModules/ags/v2/widgets/icon-browser/main.tsx b/nixosModules/ags/v2/widgets/icon-browser/main.tsx
index 81dcf1c1..4ce12117 100644
--- a/nixosModules/ags/v2/widgets/icon-browser/main.tsx
+++ b/nixosModules/ags/v2/widgets/icon-browser/main.tsx
@@ -1,54 +1,29 @@
-import { Astal, Gtk, Widget } from 'astal/gtk3';
-import { idle } from 'astal';
+import { Gtk, Widget } from 'astal/gtk3';
 
-import { Fzf, FzfResultItem } from 'fzf';
-
-import PopupWindow from '../misc/popup-window';
-import { centerCursor } from '../../lib';
+import SortedList from '../misc/sorted-list';
 
 
-export default () => {
-    let Icons: string[] = [];
-    let fzfResults = [] as FzfResultItem<string>[];
+export default () => SortedList({
+    name: 'icon-browser',
 
-    const list = new Gtk.ListBox({
-        selectionMode: Gtk.SelectionMode.SINGLE,
-    });
+    create_list: () => Gtk.IconTheme.get_default().list_icons(null)
+        .filter((icon) => icon.endsWith('symbolic'))
+        .sort(),
 
-    list.connect('row-activated', (_, row) => {
+    create_row: (icon) => (
+        <box>
+            <icon css="font-size: 60px; margin-right: 25px;" icon={icon} />
+            <label label={icon} />
+        </box>
+    ),
+
+    on_row_activated: (row) => {
         const icon = ((row.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
 
         console.log(icon);
-    });
+    },
 
-    const placeholder = (
-        <revealer>
-            <label
-                label="   Couldn't find a match"
-                className="placeholder"
-            />
-        </revealer>
-    ) as Widget.Revealer;
-
-    const on_text_change = (text: string) => {
-        const fzf = new Fzf(Icons);
-
-        fzfResults = fzf.find(text);
-        list.invalidate_sort();
-
-        const visibleIcons = list.get_children().filter((row) => row.visible).length;
-
-        placeholder.reveal_child = visibleIcons <= 0;
-    };
-
-    const entry = (
-        <entry
-            onChanged={(self) => on_text_change(self.text)}
-            hexpand
-        />
-    ) as Widget.Entry;
-
-    list.set_sort_func((a, b) => {
+    sort_func: (a, b, entry, fzfResults) => {
         const row1 = ((a.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
         const row2 = ((b.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
 
@@ -67,76 +42,5 @@ export default () => {
 
             return s2 - s1;
         }
-    });
-
-    const refreshIcons = () => idle(() => {
-        (list.get_children() as Gtk.ListBoxRow[])
-            .forEach((child) => {
-                child.destroy();
-            });
-
-        Icons = Gtk.IconTheme.get_default().list_icons(null)
-            .filter((icon) => icon.endsWith('symbolic'))
-            .sort();
-
-        Icons
-            .flatMap((icon) => (
-                <box>
-                    <icon css="font-size: 60px; margin-right: 25px;" icon={icon} />
-                    <label label={icon} />
-                </box>
-            ))
-            .forEach((child) => {
-                list.add(child);
-            });
-
-        list.show_all();
-        on_text_change('');
-    });
-
-    refreshIcons();
-
-    return (
-        <PopupWindow
-            name="icon-browser"
-            keymode={Astal.Keymode.ON_DEMAND}
-            on_open={() => {
-                entry.text = '';
-                centerCursor();
-            }}
-        >
-            <box
-                vertical
-                className="icon-browser"
-            >
-                <box className="widget icon-search">
-
-                    <icon icon="preferences-system-search-symbolic" />
-
-                    {entry}
-
-                    <button
-                        css="margin-left: 5px;"
-                        onButtonReleaseEvent={refreshIcons}
-                    >
-                        <icon icon="view-refresh-symbolic" css="font-size: 26px;" />
-                    </button>
-
-                </box>
-
-                <scrollable
-                    className="widget icon-list"
-
-                    css="min-height: 600px; min-width: 600px;"
-                    hscroll={Gtk.PolicyType.NEVER}
-                    vscroll={Gtk.PolicyType.AUTOMATIC}
-                >
-                    <box vertical>
-                        {list}
-                        {placeholder}
-                    </box>
-                </scrollable>
-            </box>
-        </PopupWindow>
-    );
-};
+    },
+});
diff --git a/nixosModules/ags/v2/widgets/icon-browser/style.scss b/nixosModules/ags/v2/widgets/icon-browser/style.scss
index 167997c7..c5669131 100644
--- a/nixosModules/ags/v2/widgets/icon-browser/style.scss
+++ b/nixosModules/ags/v2/widgets/icon-browser/style.scss
@@ -1,32 +1,4 @@
-.icon-browser {
-    .icon-search {
-        icon {
-            font-size: 20px;
-            min-width: 40px;
-            min-height: 40px
-        }
-
-        entry {}
-    }
-
-    .icon-list {
-        row {
-            border-radius: 10px;
-
-            &:hover, &:selected {
-                icon {
-                    -gtk-icon-shadow: 2px 2px $accent_color;
-                }
-            }
-
-            box {
-                margin: 20px;
-                font-size: 16px;
-            }
-        }
-
-        .placeholder {
-            font-size: 20px;
-        }
-    }
+.icon-browser .icon-list row box {
+    margin: 20px;
+    font-size: 16px;
 }
diff --git a/nixosModules/ags/v2/widgets/misc/popup-window.tsx b/nixosModules/ags/v2/widgets/misc/popup-window.tsx
index dd1b4f7e..afceb7f4 100644
--- a/nixosModules/ags/v2/widgets/misc/popup-window.tsx
+++ b/nixosModules/ags/v2/widgets/misc/popup-window.tsx
@@ -8,9 +8,9 @@ import { get_hyprland_monitor, hyprMessage } from '../../lib';
 type CloseType = 'none' | 'stay' | 'released' | 'clicked';
 type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' |
     'slide right' | 'popin' | 'fade';
-type PopupCallback = (self: PopupWindow) => void;
+type PopupCallback = (self?: Widget.Window) => void;
 
-type PopupWindowProps = Widget.WindowProps & {
+export type PopupWindowProps = Widget.WindowProps & {
     transition?: HyprTransition | Binding<HyprTransition>
     close_on_unfocus?: CloseType | Binding<CloseType>
     on_open?: PopupCallback
diff --git a/nixosModules/ags/v2/widgets/misc/sorted-list.tsx b/nixosModules/ags/v2/widgets/misc/sorted-list.tsx
new file mode 100644
index 00000000..d46397fa
--- /dev/null
+++ b/nixosModules/ags/v2/widgets/misc/sorted-list.tsx
@@ -0,0 +1,174 @@
+// This is definitely not good practice but I couldn't figure out how to extend PopupWindow
+// so here we are with a cursed function that returns a prop of the class.
+
+import { Astal, Gtk, Widget } from 'astal/gtk3';
+import { idle } from 'astal';
+
+import { Fzf, FzfOptions, FzfResultItem } from 'fzf';
+
+import PopupWindow, { PopupWindow as PopupWindowClass } from '../misc/popup-window';
+import { centerCursor } from '../../lib';
+
+export interface SortedListProps<T> {
+    create_list: () => T[]
+    create_row: (item: T) => Gtk.Widget
+    fzf_options?: FzfOptions<T>
+    on_row_activated: (row: Gtk.ListBoxRow) => void
+    sort_func: (
+        a: Gtk.ListBoxRow,
+        b: Gtk.ListBoxRow,
+        entry: Widget.Entry,
+        fzf: FzfResultItem<T>[],
+    ) => number
+    name: string
+};
+
+
+export class SortedList<T> {
+    private item_list: T[] = [];
+    private fzf_results: FzfResultItem<T>[] = [];
+
+    readonly window: PopupWindowClass;
+
+    readonly create_list: () => T[];
+    readonly create_row: (item: T) => Gtk.Widget;
+    readonly fzf_options: FzfOptions<T> | undefined;
+
+    readonly on_row_activated: (row: Gtk.ListBoxRow) => void;
+
+    readonly sort_func: (
+        a: Gtk.ListBoxRow,
+        b: Gtk.ListBoxRow,
+        entry: Widget.Entry,
+        fzf: FzfResultItem<T>[],
+    ) => number;
+
+
+    constructor({
+        create_list,
+        create_row,
+        fzf_options,
+        on_row_activated,
+        sort_func,
+        name,
+    }: SortedListProps<T>) {
+        const list = new Gtk.ListBox({
+            selectionMode: Gtk.SelectionMode.SINGLE,
+        });
+
+        list.connect('row-activated', (_, row) => {
+            this.on_row_activated(row);
+        });
+
+        const placeholder = (
+            <revealer>
+                <label
+                    label="   Couldn't find a match"
+                    className="placeholder"
+                />
+            </revealer>
+        ) as Widget.Revealer;
+
+        const on_text_change = (text: string) => {
+            // @ts-expect-error this should be okay
+            this.fzf_results = (new Fzf(this.item_list, this.fzf_options)).find(text);
+            list.invalidate_sort();
+
+            const visibleApplications = list.get_children().filter((row) => row.visible).length;
+
+            placeholder.reveal_child = visibleApplications <= 0;
+        };
+
+        const entry = (
+            <entry
+                onChanged={(self) => on_text_change(self.text)}
+                hexpand
+            />
+        ) as Widget.Entry;
+
+        list.set_sort_func((a, b) => {
+            return this.sort_func(a, b, entry, this.fzf_results);
+        });
+
+        const refreshItems = () => idle(() => {
+            (list.get_children() as Gtk.ListBoxRow[])
+                .forEach((child) => {
+                    child.destroy();
+                });
+
+            this.item_list = this.create_list();
+
+            this.item_list
+                .flatMap((prop) => this.create_row(prop))
+                .forEach((child) => {
+                    list.add(child);
+                });
+
+            list.show_all();
+            on_text_change('');
+        });
+
+        this.window = (
+            <PopupWindow
+                name={name}
+                keymode={Astal.Keymode.ON_DEMAND}
+                on_open={() => {
+                    entry.text = '';
+                    centerCursor();
+                }}
+            >
+                <box
+                    vertical
+                    className={`${name} sorted-list`}
+                >
+                    <box className="widget search">
+
+                        <icon icon="preferences-system-search-symbolic" />
+
+                        {entry}
+
+                        <button
+                            css="margin-left: 5px;"
+                            cursor="pointer"
+                            onButtonReleaseEvent={refreshItems}
+                        >
+                            <icon icon="view-refresh-symbolic" css="font-size: 26px;" />
+                        </button>
+
+                    </box>
+
+                    <eventbox cursor="pointer">
+                        <scrollable
+                            className="widget list"
+
+                            css="min-height: 600px; min-width: 600px;"
+                            hscroll={Gtk.PolicyType.NEVER}
+                            vscroll={Gtk.PolicyType.AUTOMATIC}
+                        >
+                            <box vertical>
+                                {list}
+                                {placeholder}
+                            </box>
+                        </scrollable>
+                    </eventbox>
+                </box>
+            </PopupWindow>
+        ) as PopupWindowClass;
+
+        this.create_list = create_list;
+        this.create_row = create_row;
+        this.fzf_options = fzf_options;
+        this.on_row_activated = on_row_activated;
+        this.sort_func = sort_func;
+
+        refreshItems();
+    }
+};
+
+/**
+ * @param props props for a SortedList Widget
+ * @returns the widget
+ */
+export default function<Attr>(props: SortedListProps<Attr>) {
+    return (new SortedList(props)).window;
+}
diff --git a/nixosModules/ags/v2/widgets/misc/style.scss b/nixosModules/ags/v2/widgets/misc/style.scss
new file mode 100644
index 00000000..89fabde4
--- /dev/null
+++ b/nixosModules/ags/v2/widgets/misc/style.scss
@@ -0,0 +1,27 @@
+.sorted-list {
+    .search {
+        icon {
+            font-size: 20px;
+            min-width: 40px;
+            min-height: 40px
+        }
+
+        entry {}
+    }
+
+    .list {
+        row {
+            border-radius: 10px;
+
+            &:hover, &:selected {
+                icon {
+                    -gtk-icon-shadow: 2px 2px $accent_color;
+                }
+            }
+        }
+
+        .placeholder {
+            font-size: 20px;
+        }
+    }
+}