97 lines
3.0 KiB
TypeScript
97 lines
3.0 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`]);
|
|
}}
|
|
>
|
|
<box>
|
|
<label
|
|
cssClasses={["content"]}
|
|
xalign={0}
|
|
label={content}
|
|
/>
|
|
</box>
|
|
</button>
|
|
}
|
|
|
|
async function getClipboardHistory() {
|
|
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"));
|
|
|
|
return ids
|
|
.map((id, index) => ({ id, content: contents[index] }))
|
|
.filter(({ content }) => content && content.length > 0);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export default async function Clipboard(_monitor_id: number) {
|
|
const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor;
|
|
|
|
const history = await getClipboardHistory();
|
|
const hypr = Hyprland.get_default();
|
|
|
|
const toSkip = Variable(0);
|
|
const list = bind(toSkip).as(count => limit(
|
|
skip(history, count),
|
|
10
|
|
));
|
|
|
|
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.length) return;
|
|
toSkip.set(value + 10)
|
|
}
|
|
};
|
|
|
|
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}>
|
|
<box hexpand={false} vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
|
<box cssClasses={["clipboard"]} vertical>
|
|
<box spacing={6} vertical onScroll={(_, __, dy) => onScroll(dy)}>
|
|
{list.as(list => list.map(({ id, content }) => (
|
|
<ClipboardEntry id={id} content={content} />
|
|
)))}
|
|
</box>
|
|
<box
|
|
halign={Gtk.Align.CENTER}
|
|
cssClasses={["not-found"]}
|
|
vertical
|
|
visible={list.as(it => it.length === 0)}>
|
|
<image iconName="system-search-symbolic" />
|
|
<label label="No match found" />
|
|
</box>
|
|
</box>
|
|
</box>
|
|
</window>
|
|
}
|