233 lines
7.0 KiB
TypeScript
233 lines
7.0 KiB
TypeScript
import { AstalIO, execAsync, Gio, GLib, GObject, monitorFile, property, readFile, register, signal, timeout } from "astal";
|
|
|
|
|
|
export enum ClipboardItemType {
|
|
TEXT = 0,
|
|
IMAGE = 1
|
|
}
|
|
|
|
export type ClipboardItem = {
|
|
id: number;
|
|
type: ClipboardItemType;
|
|
preview: string;
|
|
}
|
|
|
|
export { Clipboard };
|
|
|
|
/** Cliphist Manager and event listener
|
|
* This only supports wipe and store events from cliphist */
|
|
@register({ GTypeName: "Clipboard" })
|
|
class Clipboard extends GObject.Object {
|
|
private static instance: Clipboard;
|
|
|
|
#dbFile: Gio.File;
|
|
#dbMonitor: Gio.FileMonitor;
|
|
#updateDone: boolean = false;
|
|
#history = new Array<ClipboardItem>;
|
|
#changesTimeout: (AstalIO.Time|undefined);
|
|
#ignoreChanges: boolean = false;
|
|
|
|
@signal(Object)
|
|
declare copied: () => ClipboardItem;
|
|
|
|
@signal()
|
|
declare wiped: () => void;
|
|
|
|
|
|
@property()
|
|
public get history() { return this.#history; }
|
|
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.#dbFile = this.getCliphistDatabase();
|
|
|
|
this.#dbMonitor = monitorFile(this.#dbFile.get_path()!, () => {
|
|
if(this.#ignoreChanges || this.#changesTimeout)
|
|
return;
|
|
|
|
this.#changesTimeout = timeout(300, () => this.#changesTimeout = undefined);
|
|
|
|
if(this.#updateDone) {
|
|
this.updateDatabase();
|
|
return;
|
|
}
|
|
|
|
this.init();
|
|
});
|
|
|
|
if(this.#dbFile.query_exists(null)) {
|
|
this.init();
|
|
return;
|
|
}
|
|
|
|
console.log("Clipboard: cliphist database not found. Try copying something first!");
|
|
}
|
|
|
|
vfunc_dispose(): void {
|
|
this.#dbMonitor.cancel();
|
|
this.#dbMonitor.unref();
|
|
}
|
|
|
|
private init() {
|
|
console.log("Clipboard: Starting to read cliphist history...");
|
|
|
|
this.updateDatabase().then(() => {
|
|
console.log("Clipboard: Done reading cliphist history!");
|
|
}).catch((err) =>
|
|
console.error(`Clipboard: An error occurred while reading cliphist history. Stderr: ${err}`)
|
|
);
|
|
}
|
|
|
|
public async copyAsync(content: string): Promise<void> {
|
|
await execAsync(`wl-copy "${content}"`).catch((err: Gio.IOErrorEnum) => {
|
|
console.error(`Clipboard: Couldn't copy text using wl-copy. Stderr:\n\t${err.message
|
|
} | Stack:\n\t\t${err.stack}`);
|
|
});
|
|
}
|
|
|
|
public async selectItem(itemToSelect: number|ClipboardItem): Promise<boolean> {
|
|
const item = await this.getItemContent(itemToSelect);
|
|
let res: boolean = true;
|
|
|
|
if(item)
|
|
await this.copyAsync(item).catch(() => res = false);
|
|
|
|
return res;
|
|
}
|
|
|
|
/** Gets history item's content by its ID.
|
|
* @returns the clipboard item's content */
|
|
public async getItemContent(item: number|ClipboardItem): Promise<string|undefined> {
|
|
const id = (typeof item === "number") ?
|
|
item : item.id;
|
|
|
|
const cmd = Gio.Subprocess.new([ "cliphist", "decode", id.toString() ],
|
|
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
|
|
|
const [ , stdout, stderr ] = cmd.communicate_utf8(null, null);
|
|
|
|
if(stderr) {
|
|
console.error(`Clipboard: An error occurred while getting item content. Stderr:\n${stderr}`);
|
|
return;
|
|
}
|
|
|
|
return stdout;
|
|
}
|
|
|
|
/** Searches for the cliphist database file
|
|
* Will not work if cliphist config file is not on default path */
|
|
private getCliphistDatabase(): Gio.File {
|
|
// Check if env variable is set
|
|
const path = GLib.getenv("CLIPHIST_DB_PATH");
|
|
if(path != null)
|
|
return Gio.File.new_for_path(path);
|
|
|
|
// Check config file
|
|
const confFile = Gio.File.new_for_path(`${GLib.get_user_config_dir()}/cliphist/config`);
|
|
if(confFile.query_exists(null)) {
|
|
const cliphistConf = readFile(confFile.get_path()!);
|
|
for(const line of cliphistConf.split('\n').map(l => l.trim())) {
|
|
if(line.startsWith('#'))
|
|
continue;
|
|
|
|
const [ key, value ] = line.split('\s', 1);
|
|
if(key === "db-path") {
|
|
return Gio.File.new_for_path(value.trimStart());
|
|
}
|
|
}
|
|
}
|
|
|
|
// return default path if none of the above matches
|
|
return Gio.File.new_for_path(`${GLib.get_user_cache_dir()}/cliphist/db`);
|
|
}
|
|
|
|
private getContentType(preview: string): ClipboardItemType {
|
|
return /^\[\[.*binary data.*x.*\]\]$/u.test(preview) ?
|
|
ClipboardItemType.IMAGE
|
|
: ClipboardItemType.TEXT;
|
|
}
|
|
|
|
public async wipeHistory(noExec?: boolean): Promise<void> {
|
|
if(noExec) {
|
|
this.#history = [];
|
|
this.emit("wiped");
|
|
return;
|
|
}
|
|
|
|
this.#ignoreChanges = true;
|
|
await execAsync("cliphist wipe").then(() => {
|
|
this.#history = [];
|
|
this.emit("wiped");
|
|
}).catch((err: Gio.IOErrorEnum) =>
|
|
console.error(`Clipboard: An error occurred on cliphist database wipe. Stderr: ${
|
|
err.message ? `${err.message}\n` : ""}${err.stack}`)
|
|
).finally(() => this.#ignoreChanges = false);
|
|
}
|
|
|
|
public async updateDatabase(): Promise<void> {
|
|
const proc = Gio.Subprocess.new([ "cliphist", "list" ],
|
|
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
|
|
|
proc.communicate_utf8_async(null, null, (_, asyncRes) => {
|
|
const [ success, stdout, stderr ] = proc.communicate_utf8_finish(asyncRes);
|
|
|
|
if(!success || stderr) {
|
|
console.error("Clipboard: Couldn't communicate with cliphist! Is it installed?");
|
|
return;
|
|
}
|
|
|
|
if(!stdout.trim()) {
|
|
this.wipeHistory(true);
|
|
this.notify("history");
|
|
return;
|
|
}
|
|
|
|
const items = stdout.split('\n');
|
|
|
|
if(this.#updateDone) {
|
|
const [ id, preview ] = items[0].split('\t');
|
|
const clipItem = {
|
|
id: Number.parseInt(id),
|
|
preview,
|
|
type: this.getContentType(preview)
|
|
} as ClipboardItem;
|
|
|
|
this.#history.unshift(clipItem);
|
|
|
|
this.emit("copied", clipItem);
|
|
this.notify("history");
|
|
return;
|
|
}
|
|
|
|
for(const item of items) {
|
|
if(!item) continue;
|
|
|
|
const [ id, preview ] = item.split('\t');
|
|
|
|
const clipItem = {
|
|
id: Number.parseInt(id),
|
|
preview,
|
|
type: this.getContentType(preview)
|
|
} as ClipboardItem;
|
|
|
|
this.#history.push(clipItem);
|
|
|
|
this.emit("copied", clipItem);
|
|
this.notify("history");
|
|
}
|
|
|
|
this.#updateDone = true;
|
|
|
|
});
|
|
}
|
|
|
|
public static getDefault(): Clipboard {
|
|
if(!this.instance)
|
|
this.instance = new Clipboard();
|
|
|
|
return this.instance;
|
|
}
|
|
}
|