From f4618c3271436a9ae3b72b0900a8a5b86b92ed14 Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Wed, 1 May 2024 10:21:28 -0400
Subject: [PATCH] feat(ags): use experimental clipboard manager

---
 home/wofi/default.nix                         |  17 ---
 home/wofi/style.css                           |  92 ---------------
 modules/ags/clipboard/default.nix             |  19 +++
 modules/ags/clipboard/script.sh               |  45 +++++++
 .../config/scss/wim-widgets/clipboard.scss    |  64 ++++++++++
 modules/ags/config/scss/wim.scss              |   1 +
 modules/ags/config/ts/clipboard/main.ts       | 110 ++++++++++++++++++
 modules/ags/config/wim.ts                     |   2 +
 modules/ags/default.nix                       |   1 +
 modules/hyprland/packages.nix                 |   4 +-
 10 files changed, 243 insertions(+), 112 deletions(-)
 delete mode 100644 home/wofi/default.nix
 delete mode 100644 home/wofi/style.css
 create mode 100644 modules/ags/clipboard/default.nix
 create mode 100755 modules/ags/clipboard/script.sh
 create mode 100644 modules/ags/config/scss/wim-widgets/clipboard.scss
 create mode 100644 modules/ags/config/ts/clipboard/main.ts

diff --git a/home/wofi/default.nix b/home/wofi/default.nix
deleted file mode 100644
index 03838d16..00000000
--- a/home/wofi/default.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-{...}: {
-  programs = {
-    wofi = {
-      enable = true;
-      settings = {
-        prompt = "";
-        allow_images = true;
-        normal_window = true;
-        image_size = "48";
-        matching = "fuzzy";
-        insensitive = true;
-        no_actions = true;
-      };
-      style = builtins.readFile ./style.css;
-    };
-  };
-}
diff --git a/home/wofi/style.css b/home/wofi/style.css
deleted file mode 100644
index 62a32b6a..00000000
--- a/home/wofi/style.css
+++ /dev/null
@@ -1,92 +0,0 @@
-/* https://github.com/dracula/wofi/blob/master/style.css */
-* {
-  font-size: 16px;
-}
-
-window,
-undershoot {
-  all: unset;
-}
-
-#input {
-  all: unset;
-  border-radius: 9px;
-  color: #f8f8f2;
-  background-color: rgba(#44475a, 0.6);
-  border: 1px solid #44475a;
-  padding: 8px;
-  margin: 16.2px;
-  margin-bottom: 0;
-}
-
-#outer-box {
-  all: unset;
-  box-shadow: 0 0 4.5px 0 rgba(0, 0, 0, 0.6);
-  border: 2px solid rgba(189, 147, 249, 0.8);
-  border-radius: 25px;
-  background-color: rgba(40, 42, 54, 0.8);
-  color: #f8f8f2;
-  padding: 16.2px;
-}
-
-#inner-box {
-  padding: 16.2px;
-  min-width: 500px;
-  min-height: 450px;
-}
-
-#scroll scrollbar, #scroll scrollbar * {
-  all: unset;
-}
-
-#scroll scrollbar {
-  transition: 200ms;
-  background-color: rgba(23, 23, 23, 0.3);
-
-  &:hover {
-    background-color: rgba(23, 23, 23, 0.7);
-  }
-}
-#scroll scrollbar.vertical:hover slider {
-  background-color: rgba(238, 238, 238, 0.7);
-  min-width: .6em;
-}
-#scroll scrollbar.horizontal:hover slider {
-  background-color: rgba(238, 238, 238, 0.7);
-  min-height: .6em;
-}
-#scroll .vertical slider {
-  background-color: rgba(238, 238, 238, 0.5);
-  border-radius: 9px;
-  min-width: .4em;
-  min-height: 2em;
-  transition: 200ms;
-}
-#scroll .horizontal slider {
-  background-color: rgba(238, 238, 238, 0.5);
-  border-radius: 9px;
-  min-height: .4em;
-  min-width: 2em;
-  transition: 200ms;
-}
-
-#entry {
-  all: unset;
-  padding: 9px;
-}
-
-#entry image, #entry label {
-  all: unset;
-}
- #entry image {
-  margin-right: 9px;
-}
-
-#entry:selected {
-  background-color: rgba(189, 147, 249, 0.5);
-  border-radius: 9px;
-  box-shadow: inset 0 0 0 3px rgba(238, 238, 238, 0.03);
-}
-#entry:selected image {
-  -gtk-icon-shadow: 3px 3px rgba(0, 0, 0, 0.8);
-}
diff --git a/modules/ags/clipboard/default.nix b/modules/ags/clipboard/default.nix
new file mode 100644
index 00000000..c9487b9a
--- /dev/null
+++ b/modules/ags/clipboard/default.nix
@@ -0,0 +1,19 @@
+{
+  cliphist,
+  gawk,
+  imagemagick,
+  ripgrep,
+  writeShellApplication,
+  ...
+}:
+writeShellApplication {
+  name = "clipboard-manager";
+  runtimeInputs = [
+    cliphist
+    gawk
+    imagemagick
+    ripgrep
+  ];
+
+  text = builtins.readFile ./script.sh;
+}
diff --git a/modules/ags/clipboard/script.sh b/modules/ags/clipboard/script.sh
new file mode 100755
index 00000000..01e7bde2
--- /dev/null
+++ b/modules/ags/clipboard/script.sh
@@ -0,0 +1,45 @@
+set +o errexit
+
+# Modified from https://github.com/sentriz/cliphist/blob/master/contrib/cliphist-wofi-img
+
+# set up thumbnail directory
+thumb_dir="/tmp/cliphist/thumbs"
+mkdir -p "$thumb_dir"
+
+cliphist_list="$(cliphist list)"
+
+# delete thumbnails in cache but not in cliphist
+for thumb in "$thumb_dir"/*; do
+    clip_id="${thumb##*/}"
+    clip_id="${clip_id%.*}"
+    check=$(rg <<< "$cliphist_list" "^$clip_id\s")
+    if [ -z "$check" ]; then
+        >&2 rm -v "$thumb"
+    fi
+done
+
+# create thumbnail if image not processed already
+read -r -d '' prog <<EOF
+/^[0-9]+\s<meta http-equiv=/ { next }
+
+match(\$0, /^([0-9]+)\s(\[\[\s)?binary.*(jpg|jpeg|png|bmp)/, grp) {
+    image = grp[1]"."grp[3]
+    system("[ -f $thumb_dir/"image" ] || echo " grp[1] "\\\\\t | cliphist decode | convert - -resize '256x256>' $thumb_dir/"image )
+    print "img:$thumb_dir/"image
+    next
+}
+
+1
+EOF
+
+output=$(gawk <<< "$cliphist_list" "$prog")
+
+# Use a while loop with read to iterate over each line
+echo "$output" | while IFS= read -r line; do
+    if [[ ! $line =~ ^img:/tmp/cliphist/thumbs ]]; then
+        [[ $line =~ ([0-9]+) ]]
+        line=${BASH_REMATCH[1]}
+    fi
+
+    echo "$line"
+done
diff --git a/modules/ags/config/scss/wim-widgets/clipboard.scss b/modules/ags/config/scss/wim-widgets/clipboard.scss
new file mode 100644
index 00000000..d08f16e8
--- /dev/null
+++ b/modules/ags/config/scss/wim-widgets/clipboard.scss
@@ -0,0 +1,64 @@
+.clipboard {
+  all: unset;
+  border: 2px solid $contrast-bg;
+  border-radius: 25px;
+  background-color: $bg;
+  color: #f8f8f2;
+  padding: 2px;
+
+  * {
+    font-size: 16px;
+  }
+
+  list, row {
+    all: unset;
+  }
+
+  scrolledwindow {
+    padding: 10px;
+    padding-bottom: 0;
+    min-width: 900px;
+    min-height: 750px;
+
+    scrollbar, scrollbar * {
+      all: unset;
+    }
+
+    scrollbar.vertical {
+      transition: 200ms;
+      background-color: rgba(23, 23, 23, 0.3);
+      margin: 20px 0;
+
+      &:hover {
+        background-color: rgba(23, 23, 23, 0.7);
+
+        slider {
+          background-color: rgba(238, 238, 238, 0.7);
+          min-width: .6em;
+        }
+      }
+
+      slider {
+        background-color: rgba(238, 238, 238, 0.5);
+        border-radius: 9px;
+        min-width: .4em;
+        min-height: 2em;
+        transition: 200ms;
+      }
+    }
+  }
+
+  .item {
+    all: unset;
+    transition: 200ms;
+    border-radius: 9px;
+
+    box {
+      padding: 10px;
+    }
+  }
+
+  *:selected, .item:hover, .item:focus {
+    background-color: #363449;
+  }
+}
diff --git a/modules/ags/config/scss/wim.scss b/modules/ags/config/scss/wim.scss
index fdc0460f..91f9b23e 100644
--- a/modules/ags/config/scss/wim.scss
+++ b/modules/ags/config/scss/wim.scss
@@ -21,3 +21,4 @@ undershoot {
 @import "./wim-widgets/applauncher";
 @import "./wim-widgets/osd";
 @import "./wim-widgets/osk";
+@import "./wim-widgets/clipboard";
diff --git a/modules/ags/config/ts/clipboard/main.ts b/modules/ags/config/ts/clipboard/main.ts
new file mode 100644
index 00000000..15e28bef
--- /dev/null
+++ b/modules/ags/config/ts/clipboard/main.ts
@@ -0,0 +1,110 @@
+const { Box, Icon, Label, ListBox, Scrollable } = Widget;
+const { execAsync } = Utils;
+
+import Gtk from 'gi://Gtk?version=3.0';
+
+import CursorBox from '../misc/cursorbox.ts';
+import PopupWindow from '../misc/popup.ts';
+
+
+const N_ITEMS = 30;
+
+export default () => {
+    const list = ListBox();
+
+    list.set_sort_func((row1, row2) => {
+        const getKey = (r: Gtk.ListBoxRow) => parseInt(r.get_child()?.name ?? '');
+
+        return getKey(row2) - getKey(row1);
+    });
+
+    const updateItems = () => {
+        (list.get_children() as Gtk.ListBoxRow[]).forEach((r) => {
+            r.changed();
+        });
+    };
+
+    const makeItem = (key: string, val: string) => {
+        const widget = CursorBox({
+            class_name: 'item',
+            name: key,
+
+            on_primary_click_release: () => {
+                execAsync([
+                    'bash', '-c', `cliphist list | grep ${key} | cliphist decode | wl-copy`,
+                ]).then(() => {
+                    App.closeWindow('win-clipboard');
+                });
+            },
+
+            child: Box({
+                children: [
+                    val.startsWith('img:') ?
+                        Icon({
+                            icon: val.replace('img:', ''),
+                            size: 100 * 2,
+                        }) :
+
+                        Label({
+                            label: val,
+                            truncate: 'end',
+                            max_width_chars: 100,
+                        }),
+                ],
+            }),
+        });
+
+        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);
+        });
+    };
+
+    const on_open = () => {
+        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((items[i].match('[0-9]+') ?? [''])[0], items[i]);
+                }
+                else {
+                    decodeItem(items[i]);
+                }
+            }
+        }).catch(console.log);
+    };
+
+    on_open();
+
+    return PopupWindow({
+        name: 'clipboard',
+        on_open,
+
+        child: Box({
+            class_name: 'clipboard',
+            children: [
+                Scrollable({
+                    hscroll: 'never',
+                    vscroll: 'automatic',
+                    child: Box({
+                        vertical: true,
+                        children: [list],
+                    }),
+                }),
+            ],
+        }),
+    });
+};
diff --git a/modules/ags/config/wim.ts b/modules/ags/config/wim.ts
index aafdd1b0..fce27139 100644
--- a/modules/ags/config/wim.ts
+++ b/modules/ags/config/wim.ts
@@ -3,6 +3,7 @@ import AppLauncher from './ts/applauncher/main.ts';
 import Bar from './ts/bar/wim.ts';
 import BgFade from './ts/misc/background-fade.ts';
 import Calendar from './ts/date.ts';
+import Clipboard from './ts/clipboard/main.ts';
 import Corners from './ts/corners/main.ts';
 import { NotifPopups, NotifCenter } from './ts/notifications/wim.ts';
 import OSD from './ts/osd/main.ts';
@@ -23,6 +24,7 @@ App.config({
 
         AppLauncher(),
         Calendar(),
+        Clipboard(),
         NotifCenter(),
         OSD(),
         OSK(),
diff --git a/modules/ags/default.nix b/modules/ags/default.nix
index c428f70f..d315a54c 100644
--- a/modules/ags/default.nix
+++ b/modules/ags/default.nix
@@ -107,6 +107,7 @@ in {
             dart-sass
             bun
             playerctl
+            (callPackage ./clipboard {})
 
             ## gui
             pavucontrol # TODO: replace with ags widget
diff --git a/modules/hyprland/packages.nix b/modules/hyprland/packages.nix
index 40a617a0..0363c266 100644
--- a/modules/hyprland/packages.nix
+++ b/modules/hyprland/packages.nix
@@ -17,7 +17,6 @@ in {
       ../../home/foot.nix
       ../../home/mpv
       ../../home/obs.nix
-      ../../home/wofi
 
       ({config, ...}: let
         symlink = config.lib.file.mkOutOfStoreSymlink;
@@ -115,7 +114,6 @@ in {
         ];
 
         windowrule = [
-          "noborder,^(wofi)$"
           "tile,^(libreoffice)$"
           "float,^(org.gnome.Calculator)$"
           "float,^(com.gabm.satty)$"
@@ -133,7 +131,7 @@ in {
           "$mainMod, Q, exec, foot"
 
           # Clipboard History
-          "$mainMod, V, exec, killall -r wofi || cliphist list | wofi --dmenu | cliphist decode | wl-copy"
+          "$mainMod, V, exec, ags -t win-clipboard"
 
           "        , Print, exec, screenshot"
           "$mainMod, Print, exec, grim -g \"$(slurp)\" - | satty -f - --output-filename \"screenshot-$(date --iso-8601=seconds)\""