💥 fix(clipboard): issues with special characters on Clipboard.copyAsync()

This commit is contained in:
retrozinndev
2025-08-18 18:44:46 -03:00
parent db73023ac9
commit 49ded11c51
5 changed files with 54 additions and 38 deletions
+28 -17
View File
@@ -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 AstalIO from "gi://AstalIO";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
import Gio from "gi://Gio?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 { export enum ClipboardItemType {
TEXT = 0, TEXT = 0,
@@ -38,7 +33,10 @@ export { Clipboard };
class Clipboard extends GObject.Object { class Clipboard extends GObject.Object {
private static instance: Clipboard; private static instance: Clipboard;
declare $signals: ClipboardSignals; declare $signals: GObject.Object.SignalSignatures & {
"copied": Clipboard["copied"];
"wiped": Clipboard["wiped"];
};
#dbFile: Gio.File; #dbFile: Gio.File;
#dbMonitor: Gio.FileMonitor; #dbMonitor: Gio.FileMonitor;
@@ -50,7 +48,6 @@ class Clipboard extends GObject.Object {
@signal(GObject.TYPE_JSOBJECT) copied(_item: object) {} @signal(GObject.TYPE_JSOBJECT) copied(_item: object) {}
@signal() wiped() {}; @signal() wiped() {};
@getter(Array) @getter(Array)
public get history() { return this.#history; } public get history() { return this.#history; }
@@ -97,11 +94,25 @@ class Clipboard extends GObject.Object {
); );
} }
public async copyAsync(content: string): Promise<void> { public async copyAsync(content: string): Promise<boolean> {
await execAsync(`wl-copy "${content}"`).catch((err: Gio.IOErrorEnum) => { const proc = Gio.Subprocess.new(
console.error(`Clipboard: Couldn't copy text using wl-copy. Stderr:\n\t${err.message ["wl-copy", content],
} | Stack:\n\t\t${err.stack}`); 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<boolean> { public async selectItem(itemToSelect: number|ClipboardItem): Promise<boolean> {
+14 -1
View File
@@ -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 GObject from "ags/gobject";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
@@ -54,6 +55,18 @@ export function initPlayer(): void {
}); });
} }
export function accessMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> {
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 { export function disposePlayer(): void {
if(disposeFun) { if(disposeFun) {
disposeFun(); disposeFun();
+4
View File
@@ -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<Gtk.Widget> { export function getChildren(widget: Gtk.Widget): Array<Gtk.Widget> {
const firstChild = widget.get_first_child(), const firstChild = widget.get_first_child(),
children: Array<Gtk.Widget> = []; children: Array<Gtk.Widget> = [];
+5 -17
View File
@@ -1,10 +1,10 @@
import { Accessor, createBinding, createConnection, onCleanup, With } from "ags"; import { createBinding, onCleanup, With } from "ags";
import { Gtk } from "ags/gtk4"; import { Gtk } from "ags/gtk4";
import { Separator } from "../Separator"; import { Separator } from "../Separator";
import { Windows } from "../../windows"; import { Windows } from "../../windows";
import { Clipboard } from "../../modules/clipboard"; import { Clipboard } from "../../modules/clipboard";
import { decoder, getPlayerIconFromBusName, variableToBoolean } from "../../modules/utils"; import { getPlayerIconFromBusName, variableToBoolean } from "../../modules/utils";
import { player, setPlayer } from "../../modules/media"; import { accessMediaUrl, player, setPlayer } from "../../modules/media";
import GObject from "ags/gobject"; import GObject from "ags/gobject";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
@@ -105,9 +105,9 @@ export const Media = () => {
<With value={player(pl => pl.available)}> <With value={player(pl => pl.available)}>
{(available: boolean) => available && <Gtk.Box class={"media-controls button-row"}> {(available: boolean) => available && <Gtk.Box class={"media-controls button-row"}>
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"} <Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
visible={variableToBoolean(getMediaUrl(player.get()))} visible={variableToBoolean(accessMediaUrl(player.get()))}
tooltipText={"Copy link to Clipboard"} onClicked={() => { tooltipText={"Copy link to Clipboard"} onClicked={() => {
const url = getMediaUrl(player.get()).get(); const url = accessMediaUrl(player.get()).get();
url && Clipboard.getDefault().copyAsync(url); url && Clipboard.getDefault().copyAsync(url);
}} }}
/> />
@@ -133,15 +133,3 @@ export const Media = () => {
</Gtk.Revealer> </Gtk.Revealer>
</Gtk.Box> </Gtk.Box>
} }
export function getMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> {
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;
})
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { timeout } from "ags/time"; import { timeout } from "ags/time";
import { Astal, Gtk } from "ags/gtk4"; import { Astal, Gtk } from "ags/gtk4";
import { Clipboard } from "../../modules/clipboard"; import { Clipboard } from "../../modules/clipboard";
import { getMediaUrl } from "../bar/Media"; import { accessMediaUrl } from "../../modules/media";
import { player, setPlayer } from "../../modules/media"; import { player, setPlayer } from "../../modules/media";
import { createBinding, For } from "ags"; import { createBinding, For } from "ags";
import { pathToURI, variableToBoolean } from "../../modules/utils"; import { pathToURI, variableToBoolean } from "../../modules/utils";
@@ -126,9 +126,9 @@ class PlayerWidget extends Gtk.Box {
<Gtk.Box class={"controls button-row"} $type="center"> <Gtk.Box class={"controls button-row"} $type="center">
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"} <Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
tooltipText={"Copy link to clipboard"} tooltipText={"Copy link to clipboard"}
visible={variableToBoolean(getMediaUrl(player))} visible={variableToBoolean(accessMediaUrl(player))}
onClicked={() => { onClicked={() => {
const url = getMediaUrl(player).get(); const url = accessMediaUrl(player).get();
url && Clipboard.getDefault().copyAsync(url); url && Clipboard.getDefault().copyAsync(url);
}} }}
/> />