diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2025-08-17 13:04:40 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2025-08-18 00:44:22 +0800 |
| commit | 1078ef324f8e83a4390fcc5e565119dff9228c8c (patch) | |
| tree | 668ac8859dbeb7ea8ce2a4330a38bcaf5c228304 | |
| parent | f23ced82012a9f85739210aa333cd6f92815c50a (diff) | |
Reapply "refactor(clip): use Quickshell.execDetached when avaliable"
This reverts commit 7b4076be7e305bacf8c55d75589e841c29d250e1.
fix(Clip): sh escaping args from user data
chore: migrate mpris stringutils to actual stringutils
StringUtils is copied from end4-dots, GPL Licensed
This also fixes image not being copied properly
| -rw-r--r-- | bar/Mpris.qml | 16 | ||||
| -rw-r--r-- | services/Clip.qml | 28 | ||||
| -rw-r--r-- | utils/string_utils.js | 186 |
3 files changed, 191 insertions, 39 deletions
diff --git a/bar/Mpris.qml b/bar/Mpris.qml index 092e459..7d14ba4 100644 --- a/bar/Mpris.qml +++ b/bar/Mpris.qml @@ -10,24 +10,12 @@ import qs.services as Services import qs.config import qs.components +import "../utils/string_utils.js" as StringUtils Item { id: root readonly property MprisPlayer activePlayer: Services.Mpris.activePlayer - readonly property string cleanedTitle: cleanMusicTitle(activePlayer?.trackTitle) - - function cleanMusicTitle(title) { - if (!title) - return ""; - title = title.replace(/\s*\([^)]*\)/g, " "); - title = title.replace(/\s*\[[^\]]*\]/g, " "); - title = title.replace(/\s*\{[^\}]*\}/g, " "); - title = title.replace(/\s*【[^】]*】/g, " "); - title = title.replace(/\s*《[^》]*》/g, " "); - title = title.replace(/\s*「[^」]*」/g, " "); - title = title.replace(/\s*『[^』]*』/g, " "); - return title.trim(); - } + readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) implicitWidth: activePlayer ? rect.width - General.rectMargin / 1.2 : 0 height: Bar.height + General.rectMargin / 1.5 diff --git a/services/Clip.qml b/services/Clip.qml index 5feb1fb..622b293 100644 --- a/services/Clip.qml +++ b/services/Clip.qml @@ -7,6 +7,7 @@ import Quickshell.Wayland import qs.components import qs.utils import qs.services +import "../utils/string_utils.js" as StringUtils Searchable { id: root @@ -19,8 +20,6 @@ Searchable { property list<string> _clipImgIds: _clipImg.map(p => p.split("/").pop().split(".").shift()) property var _clipMetadata: ({}) - property var toDecode: "" - property string decodedAwaitingForCopy: "" list: { if (!_clipHist?.length) @@ -88,25 +87,16 @@ Searchable { clipMetaStore.adapter.metadata = _clipMetadata; } - onDecodedAwaitingForCopyChanged: { - if (!decodedAwaitingForCopy) - return; - - Quickshell.execDetached(["wl-copy", decodedAwaitingForCopy]); - } - function transformSearch(search) { return search.toLowerCase(); } function decodeAndCopy(text) { - toDecode = text; - decodeProc.running = true; + Quickshell.execDetached(["sh", "-c", `cliphist decode '${StringUtils.shellSingleQuoteEscape(text)}' | wl-copy`]); } function copy(text) { - decodedAwaitingForCopy = ""; - decodedAwaitingForCopy = text; + Quickshell.execDetached(["wl-copy", text]); } function _update() { @@ -148,18 +138,6 @@ Searchable { } Process { - id: decodeProc - running: false - command: ["cliphist", "decode", toDecode] - onExited: { - toDecode = ""; - } - stdout: StdioCollector { - onStreamFinished: decodedAwaitingForCopy = this.data - } - } - - Process { id: clipProc running: true command: ["cliphist", "-preview-width", 9 ** 9, "-max-items", 9 ** 9, "list"] diff --git a/utils/string_utils.js b/utils/string_utils.js new file mode 100644 index 0000000..e0bf8ef --- /dev/null +++ b/utils/string_utils.js @@ -0,0 +1,186 @@ +/** + * Formats a string according to the args that are passed in + * @param { string } str + * @param {...any} args + * @returns + */ +function format(str, ...args) { + return str.replace(/{(\d+)}/g, (match, index) => + typeof args[index] !== 'undefined' ? args[index] : match + ); +} + +/** + * Returns the domain of the passed in url or null + * @param { string } url + * @returns { string| null } + */ +function getDomain(url) { + const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/); + return match ? match[1] : null; +} + +/** + * Returns the base url of the passed in url or null + * @param { string } url + * @returns { string | null } + */ +function getBaseUrl(url) { + const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); + return match ? match[1] : null; +} + +/** + * Escapes single quotes in shell commands + * @param { string } str + * @returns { string } + */ +function shellSingleQuoteEscape(str) { + // escape single quotes + return String(str) + // .replace(/\\/g, '\\\\') + .replace(/'/g, "'\\''"); +} + +/** + * Splits markdown blocks into three different types: text, think, and code. + * @param { string } markdown + */ +function splitMarkdownBlocks(markdown) { + const regex = /```(\w+)?\n([\s\S]*?)```|<think>([\s\S]*?)<\/think>/g; + /** + * @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]} + */ + let result = []; + let lastIndex = 0; + let match; + while ((match = regex.exec(markdown)) !== null) { + if (match.index > lastIndex) { + const text = markdown.slice(lastIndex, match.index); + if (text.trim()) { + result.push({ type: "text", content: text }); + } + } + if (match[0].startsWith('```')) { + if (match[2] && match[2].trim()) { + result.push({ type: "code", lang: match[1] || "", content: match[2], completed: true }); + } + } else if (match[0].startsWith('<think>')) { + if (match[3] && match[3].trim()) { + result.push({ type: "think", content: match[3], completed: true }); + } + } + lastIndex = regex.lastIndex; + } + // Handle any remaining text after the last match + if (lastIndex < markdown.length) { + const text = markdown.slice(lastIndex); + // Check for unfinished <think> block + const thinkStart = text.indexOf('<think>'); + const codeStart = text.indexOf('```'); + if ( + thinkStart !== -1 && + (codeStart === -1 || thinkStart < codeStart) + ) { + const beforeThink = text.slice(0, thinkStart); + if (beforeThink.trim()) { + result.push({ type: "text", content: beforeThink }); + } + const thinkContent = text.slice(thinkStart + 7); + if (thinkContent.trim()) { + result.push({ type: "think", content: thinkContent, completed: false }); + } + } else if (codeStart !== -1) { + const beforeCode = text.slice(0, codeStart); + if (beforeCode.trim()) { + result.push({ type: "text", content: beforeCode }); + } + // Try to detect language after ``` + const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/); + let lang = ""; + let codeContentStart = codeStart + 3; + if (codeLangMatch) { + lang = codeLangMatch[1] || ""; + codeContentStart += codeLangMatch[0].length; + } else if (text[codeStart + 3] === '\n') { + codeContentStart += 1; + } + const codeContent = text.slice(codeContentStart); + if (codeContent.trim()) { + result.push({ type: "code", lang, content: codeContent, completed: false }); + } + } else if (text.trim()) { + result.push({ type: "text", content: text }); + } + } + // console.log(JSON.stringify(result, null, 2)); + return result; +} + +/** + * Returns the original string with backslashes escaped + * @param { string } str + * @returns { string } + */ +function escapeBackslashes(str) { + return str.replace(/\\/g, '\\\\'); +} + +/** + * Wraps words to supplied maximum length + * @param { string | null } str + * @param { number } maxLen + * @returns { string } + */ +function wordWrap(str, maxLen) { + if (!str) return ""; + let words = str.split(" "); + let lines = []; + let current = ""; + for (let i = 0; i < words.length; ++i) { + if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) { + if (current.length > 0) lines.push(current); + current = words[i]; + } else { + current += (current.length > 0 ? " " : "") + words[i]; + } + } + if (current.length > 0) lines.push(current); + return lines.join("\n"); +} + +function cleanMusicTitle(title) { + if (!title) + return ""; + title = title.replace(/\s*\([^)]*\)/g, " "); + title = title.replace(/\s*\[[^\]]*\]/g, " "); + title = title.replace(/\s*\{[^\}]*\}/g, " "); + title = title.replace(/\s*【[^】]*】/g, " "); + title = title.replace(/\s*《[^》]*》/g, " "); + title = title.replace(/\s*「[^」]*」/g, " "); + title = title.replace(/\s*『[^』]*』/g, " "); + return title.trim(); +} + +function friendlyTimeForSeconds(seconds) { + if (isNaN(seconds) || seconds < 0) return "0:00"; + seconds = Math.floor(seconds); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) { + return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; + } else { + return `${m}:${s.toString().padStart(2, '0')}`; + } +} + +function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} |
