dots/ags/widget/launcher/clipboard.tsx
2025-04-27 15:35:41 +02:00

116 lines
3.7 KiB
TypeScript

import { Variable, bind, execAsync } from "astal";
import { App, Astal, Gtk } from "astal/gtk4";
import { hideWindow, limit, skip } from "@lib/utils";
import { SHELL } from "@/settings.json";
import Hyprland from "gi://AstalHyprland";
const hide = () => App.get_window("clipboard")?.hide();
type Entry = { id: string, content: string };
function ClipboardEntry({ id, content }: Entry) {
return <button
cssClasses={["entry"]}
onClicked={() => {
hide();
execAsync([SHELL, "-c", `cliphist decode ${id} | wl-copy`]);
}}
>
<label
cssClasses={["content"]}
xalign={0}
label={content}
/>
</button>
}
async function getClipboardHistory(history: Variable<Entry[]>) {
try {
const ids = await execAsync([SHELL, "-c", "cliphist list | awk '{print $1}'"])
.then(it => it.split("\n"));
const contents = await execAsync([SHELL, "-c", "cliphist list | awk '{$1=\"\"; print}'"])
.then(it => it.split("\n"));
history.set(ids
.map((id, index) => ({ id, content: contents[index] }))
.filter(({ content }) => content && content.length > 0));
} catch {
history.set([]);
}
}
export default async function Clipboard(_monitor_id: number) {
const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor;
const hypr = Hyprland.get_default();
const history: Variable<Entry[]> = Variable([]);
const toSkip = Variable(0);
const list = Variable.derive([
bind(history),
bind(toSkip)
], (history, count) => limit(
skip(history, count),
10
));
const isEmpty = bind(list).as(list => list.length === 0);
const onScroll = (dy: number) => {
const value = toSkip.get();
if (dy < 0) {
if ((value - 10) < 0) return;
toSkip.set(value - 10)
} else {
if ((value + 10) > history.get().length) return;
toSkip.set(value + 10)
}
};
const setup = (self: Gtk.Window) => self.connect('notify::visible', async () => {
await getClipboardHistory(history)
})
return <window
visible={false}
monitor={bind(hypr, "focusedMonitor").as(monitor => monitor.id)}
name={"clipboard"}
cssClasses={["clipboard"]}
keymode={Astal.Keymode.EXCLUSIVE}
anchor={TOP | BOTTOM | LEFT | RIGHT}
onKeyPressed={hideWindow}
application={App}
setup={setup}>
<box hexpand={false} cssClasses={["clipboard"]} vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
<box spacing={6} vertical onScroll={(_, __, dy) => onScroll(dy)}>
{bind(list).as(list => list.map(({ id, content }) => (
<ClipboardEntry id={id} content={content} />
)))}
<button
cssClasses={["entry", "primary"]}
visible={isEmpty.as(empty => !empty)}
onClicked={() => {
execAsync([SHELL, "-c", "cliphist wipe"])
history.set([])
}}
>
<label
cssClasses={["content"]}
xalign={0}
label="Wipe clipboard history"
/>
</button>
</box>
<box
halign={Gtk.Align.CENTER}
cssClasses={["not-found"]}
vertical
visible={isEmpty}>
<image iconName="system-search-symbolic" />
<label label="No match found" />
</box>
</box>
</window>
}