|
1 | 1 | // Copyright 2025, Command Line Inc. |
2 | 2 | // SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
| 4 | +import * as TermTypes from "@xterm/xterm"; |
| 5 | + |
4 | 6 | export const DefaultTermTheme = "default-dark"; |
5 | 7 | export const DefaultLightTermTheme = "light-default"; |
6 | 8 | import { getResolvedTheme } from "@/app/hook/usetheme"; |
@@ -141,9 +143,8 @@ export async function extractClipboardData(item: ClipboardItem): Promise<GenClip |
141 | 143 | if (!html) { |
142 | 144 | return null; |
143 | 145 | } |
144 | | - const tempDiv = document.createElement("div"); |
145 | | - tempDiv.innerHTML = html; |
146 | | - const text = tempDiv.textContent || ""; |
| 146 | + const doc = new DOMParser().parseFromString(html, "text/html"); |
| 147 | + const text = doc.body.textContent || ""; |
147 | 148 | return text ? { text } : null; |
148 | 149 | } |
149 | 150 |
|
@@ -269,9 +270,8 @@ export async function extractDataTransferItems(items: DataTransferItemList): Pro |
269 | 270 | resolve([]); |
270 | 271 | return; |
271 | 272 | } |
272 | | - const tempDiv = document.createElement("div"); |
273 | | - tempDiv.innerHTML = html; |
274 | | - const text = tempDiv.textContent || ""; |
| 273 | + const doc = new DOMParser().parseFromString(html, "text/html"); |
| 274 | + const text = doc.body.textContent || ""; |
275 | 275 | resolve(text ? [{ text }] : []); |
276 | 276 | }); |
277 | 277 | }); |
@@ -327,3 +327,70 @@ export async function extractAllClipboardData(e?: ClipboardEvent): Promise<Array |
327 | 327 | return results; |
328 | 328 | } |
329 | 329 | } |
| 330 | + |
| 331 | + |
| 332 | +/** |
| 333 | + * Converts terminal buffer lines to text, properly handling wrapped lines. |
| 334 | + * Wrapped lines (long lines split across multiple buffer rows) are concatenated |
| 335 | + * without adding newlines between them, while preserving actual line breaks. |
| 336 | + * |
| 337 | + * @param buffer - The xterm.js buffer to extract lines from |
| 338 | + * @param startIndex - Starting buffer index (inclusive, 0-based) |
| 339 | + * @param endIndex - Ending buffer index (exclusive, 0-based) |
| 340 | + * @returns Array of logical lines (with wrapped lines concatenated) |
| 341 | + */ |
| 342 | +export function bufferLinesToText(buffer: TermTypes.IBuffer, startIndex: number, endIndex: number): string[] { |
| 343 | + const lines: string[] = []; |
| 344 | + let currentLine = ""; |
| 345 | + let isFirstLine = true; |
| 346 | + |
| 347 | + // Clamp indices to valid buffer range to avoid out-of-bounds access on the |
| 348 | + // underlying circular buffer, which could return stale/wrong data. |
| 349 | + const clampedStart = Math.max(0, Math.min(startIndex, buffer.length)); |
| 350 | + const clampedEnd = Math.max(0, Math.min(endIndex, buffer.length)); |
| 351 | + |
| 352 | + for (let i = clampedStart; i < clampedEnd; i++) { |
| 353 | + const line = buffer.getLine(i); |
| 354 | + if (line) { |
| 355 | + const lineText = line.translateToString(true); |
| 356 | + // If this line is wrapped (continuation of previous line), concatenate without newline |
| 357 | + if (line.isWrapped && !isFirstLine) { |
| 358 | + currentLine += lineText; |
| 359 | + } else { |
| 360 | + // This is a new logical line |
| 361 | + if (!isFirstLine) { |
| 362 | + lines.push(currentLine); |
| 363 | + } |
| 364 | + currentLine = lineText; |
| 365 | + isFirstLine = false; |
| 366 | + } |
| 367 | + } |
| 368 | + } |
| 369 | + |
| 370 | + // Don't forget the last line |
| 371 | + if (!isFirstLine) { |
| 372 | + lines.push(currentLine); |
| 373 | + } |
| 374 | + |
| 375 | + // Trim trailing blank lines only when the requested range extends to the |
| 376 | + // actual end of the buffer. A terminal allocates a fixed number of rows |
| 377 | + // (e.g. 80) but only the first few may contain real content; the rest are |
| 378 | + // empty placeholder rows. We strip those so callers don't receive a wall |
| 379 | + // of empty strings. |
| 380 | + // |
| 381 | + // Crucially, if the caller requested a specific sub-range (e.g. lines |
| 382 | + // 100-150) and lines 140-150 happen to be blank, those blanks are |
| 383 | + // intentional and must NOT be removed. We only trim when the range |
| 384 | + // reaches the very end of the buffer. |
| 385 | + if (clampedEnd >= buffer.length) { |
| 386 | + while (lines.length > 0 && lines[lines.length - 1] === "") { |
| 387 | + lines.pop(); |
| 388 | + } |
| 389 | + } |
| 390 | + |
| 391 | + return lines; |
| 392 | +} |
| 393 | + |
| 394 | +export function quoteForPosixShell(filePath: string): string { |
| 395 | + return "'" + filePath.replace(/'/g, "'\\''") + "'"; |
| 396 | +} |
0 commit comments