mirror of
https://github.com/zephrynis/nix-flake.git
synced 2026-02-18 12:11:54 +00:00
- Add complete colorshell v2.0.3 configuration to home/ags-config/ - Disable runner plugin and NightLight tile (incompatible with NixOS) - Customize SCSS with full opacity (no transparency) - Add dark pale blue color scheme in home/pywal-colors/ - Configure Papirus-Dark icon theme via home-manager - Make ~/.config/ags/ immutable and managed by Nix store - Auto-deploy pywal colors to ~/.cache/wal/colors.json All AGS configuration is now reproducible and version controlled.
261 lines
7.9 KiB
TypeScript
261 lines
7.9 KiB
TypeScript
import { timeout } from "ags/time";
|
|
import { monitorFile, readFile } from "ags/file";
|
|
import { execAsync } from "ags/process";
|
|
import GObject, { getter, register, signal } from "ags/gobject";
|
|
|
|
import AstalIO from "gi://AstalIO";
|
|
import GLib from "gi://GLib?version=2.0";
|
|
import Gio from "gi://Gio?version=2.0";
|
|
|
|
|
|
export enum ClipboardItemType {
|
|
TEXT = 0,
|
|
IMAGE = 1
|
|
}
|
|
|
|
export class ClipboardItem {
|
|
id: number;
|
|
type: ClipboardItemType;
|
|
preview: string;
|
|
|
|
constructor(id: number, type: ClipboardItemType, preview: string) {
|
|
this.id = id;
|
|
this.type = type;
|
|
this.preview = preview;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
declare $signals: GObject.Object.SignalSignatures & {
|
|
"copied": Clipboard["copied"];
|
|
"wiped": Clipboard["wiped"];
|
|
};
|
|
|
|
#dbFile: Gio.File;
|
|
#dbMonitor: Gio.FileMonitor;
|
|
#updateDone: boolean = false;
|
|
#history = new Array<ClipboardItem>;
|
|
#changesTimeout: (AstalIO.Time|undefined);
|
|
#ignoreChanges: boolean = false;
|
|
|
|
@signal(GObject.TYPE_JSOBJECT) copied(_item: object) {}
|
|
@signal() wiped() {};
|
|
|
|
@getter(Array)
|
|
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<boolean> {
|
|
const proc = Gio.Subprocess.new(
|
|
["wl-copy", content],
|
|
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
|
|
);
|
|
|
|
const stderr = Gio.DataInputStream.new(proc.get_stderr_pipe()!);
|
|
|
|
if(!proc.wait_check(null)) {
|
|
try {
|
|
const [err, ] = stderr.read_upto('\x00', -1);
|
|
console.error(`Clipboard: An error occurred while copying text. Stderr: ${err}`);
|
|
} catch(_) {
|
|
console.error(`Clipboard: An error occurred while copying text and shell couldn't read \
|
|
stderr for more info.`);
|
|
}
|
|
}
|
|
|
|
return proc.get_exit_status() === 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|