From 49ded11c512560b2d3bfa687defa2cc259e81080 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Mon, 18 Aug 2025 18:44:46 -0300 Subject: [PATCH] :boom: fix(clipboard): issues with special characters on `Clipboard.copyAsync()` --- src/modules/clipboard.ts | 45 +++++++++++++++++---------- src/modules/media.ts | 15 ++++++++- src/modules/utils.ts | 4 +++ src/widget/bar/Media.tsx | 22 +++---------- src/widget/center-window/BigMedia.tsx | 6 ++-- 5 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/modules/clipboard.ts b/src/modules/clipboard.ts index c7dab59..8c0fc52 100644 --- a/src/modules/clipboard.ts +++ b/src/modules/clipboard.ts @@ -1,17 +1,12 @@ +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"; -import GObject, { getter, register, signal } from "ags/gobject"; -import { timeout } from "ags/time"; -import { monitorFile, readFile } from "ags/file"; -import { execAsync } from "ags/process"; - - -interface ClipboardSignals extends GObject.Object.SignalSignatures { - copied: Clipboard["copied"]; - wiped: Clipboard["wiped"]; -}; export enum ClipboardItemType { TEXT = 0, @@ -38,7 +33,10 @@ export { Clipboard }; class Clipboard extends GObject.Object { private static instance: Clipboard; - declare $signals: ClipboardSignals; + declare $signals: GObject.Object.SignalSignatures & { + "copied": Clipboard["copied"]; + "wiped": Clipboard["wiped"]; + }; #dbFile: Gio.File; #dbMonitor: Gio.FileMonitor; @@ -50,7 +48,6 @@ class Clipboard extends GObject.Object { @signal(GObject.TYPE_JSOBJECT) copied(_item: object) {} @signal() wiped() {}; - @getter(Array) public get history() { return this.#history; } @@ -97,11 +94,25 @@ class Clipboard extends GObject.Object { ); } - public async copyAsync(content: string): Promise { - 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 copyAsync(content: string): Promise { + 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()) { + 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 { diff --git a/src/modules/media.ts b/src/modules/media.ts index 965138a..3e06174 100644 --- a/src/modules/media.ts +++ b/src/modules/media.ts @@ -1,4 +1,5 @@ -import { createRoot, createState, onCleanup } from "ags"; +import { Accessor, createConnection, createRoot, createState, onCleanup } from "ags"; +import { decoder } from "./utils"; import GObject from "ags/gobject"; import AstalMpris from "gi://AstalMpris"; @@ -54,6 +55,18 @@ export function initPlayer(): void { }); } +export function accessMediaUrl(player: AstalMpris.Player): Accessor { + return createConnection(player.get_meta("xesam:url"), + [player, "notify::metadata", () => player.get_meta("xesam:url")] + ).as(url => { + const byteString = url?.get_data_as_bytes(); + + return byteString ? + decoder.decode(byteString.toArray()) + : undefined; + }) +} + export function disposePlayer(): void { if(disposeFun) { disposeFun(); diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 281e01f..6b1aacd 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -51,6 +51,10 @@ export function escapeUnintendedMarkup(input: string): string { }); } +export function escapeSpecialCharacters(str: string): string { + return str.replace(/[\\^$.*?()[\]{}|]/g, "\\$&"); +} + export function getChildren(widget: Gtk.Widget): Array { const firstChild = widget.get_first_child(), children: Array = []; diff --git a/src/widget/bar/Media.tsx b/src/widget/bar/Media.tsx index 544ab1d..6cc4936 100644 --- a/src/widget/bar/Media.tsx +++ b/src/widget/bar/Media.tsx @@ -1,10 +1,10 @@ -import { Accessor, createBinding, createConnection, onCleanup, With } from "ags"; +import { createBinding, onCleanup, With } from "ags"; import { Gtk } from "ags/gtk4"; import { Separator } from "../Separator"; import { Windows } from "../../windows"; import { Clipboard } from "../../modules/clipboard"; -import { decoder, getPlayerIconFromBusName, variableToBoolean } from "../../modules/utils"; -import { player, setPlayer } from "../../modules/media"; +import { getPlayerIconFromBusName, variableToBoolean } from "../../modules/utils"; +import { accessMediaUrl, player, setPlayer } from "../../modules/media"; import GObject from "ags/gobject"; import AstalMpris from "gi://AstalMpris"; @@ -105,9 +105,9 @@ export const Media = () => { pl.available)}> {(available: boolean) => available && { - const url = getMediaUrl(player.get()).get(); + const url = accessMediaUrl(player.get()).get(); url && Clipboard.getDefault().copyAsync(url); }} /> @@ -133,15 +133,3 @@ export const Media = () => { } - -export function getMediaUrl(player: AstalMpris.Player): Accessor { - return createConnection(player.get_meta("xesam:url"), - [player, "notify::metadata", () => player.get_meta("xesam:url")] - ).as(url => { - const byteString = url?.get_data_as_bytes(); - - return byteString ? - decoder.decode(byteString.toArray()) - : undefined; - }) -} diff --git a/src/widget/center-window/BigMedia.tsx b/src/widget/center-window/BigMedia.tsx index 7c543d0..f87af6c 100644 --- a/src/widget/center-window/BigMedia.tsx +++ b/src/widget/center-window/BigMedia.tsx @@ -1,7 +1,7 @@ import { timeout } from "ags/time"; import { Astal, Gtk } from "ags/gtk4"; import { Clipboard } from "../../modules/clipboard"; -import { getMediaUrl } from "../bar/Media"; +import { accessMediaUrl } from "../../modules/media"; import { player, setPlayer } from "../../modules/media"; import { createBinding, For } from "ags"; import { pathToURI, variableToBoolean } from "../../modules/utils"; @@ -126,9 +126,9 @@ class PlayerWidget extends Gtk.Box { { - const url = getMediaUrl(player).get(); + const url = accessMediaUrl(player).get(); url && Clipboard.getDefault().copyAsync(url); }} />