diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 4d38252575..bf6fe18c5e 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,17 +1,27 @@ +AAAANSUh ABCDEFGHIX ABCXDEFGHI ABCXYDEFGH ABCXZEFGHI BBBBB +bcimage +bctext +bdimage +BINARYPASTE BLval BRval CCCCC +cimage circleval ctrled dcx DDDDD +DECRST +DECSET +dimage ellips emtpy +EUg FFFFF GGGGG gitbranch @@ -19,9 +29,12 @@ gitgraph HHHHH HTP isakbm +KGgo +mday pseudoconsole rbong ULval unscroll URval +VBORw xad diff --git a/docs/vt-extensions/binary-paste.md b/docs/vt-extensions/binary-paste.md new file mode 100644 index 0000000000..0aba867726 --- /dev/null +++ b/docs/vt-extensions/binary-paste.md @@ -0,0 +1,234 @@ +# Binary Paste Mode + +Applications running inside a terminal currently have no way to receive binary clipboard data +(such as images) from the user's clipboard. The traditional paste mechanism — including +[Bracketed Paste Mode](https://en.wikipedia.org/wiki/Bracketed-paste) (DEC mode 2004) — +only supports plain text. + +Binary Paste Mode is a terminal protocol extension that allows the terminal emulator +to deliver binary clipboard data to applications that opt in, complete with MIME type +metadata and base64 encoding. This enables use cases such as pasting images into +terminal-based editors, chat clients, or file managers. + +## Feature Detection + +An application can detect support by sending a **DECRQM** query for mode 2033: + +``` +CSI ? 2033 $ p Query Binary Paste Mode +``` + +Response: `CSI ? 2033 ; Ps $ y` where `Ps` indicates: + +| `Ps` | Meaning | +|------|------------------------------------------------------------------| +| `1` | Mode is set (enabled) | +| `2` | Mode is reset (disabled) | +| `0` | Mode not recognized (terminal does not support Binary Paste) | + +If the terminal does not recognize mode 2033, it will respond with `Ps = 0`, +allowing the application to distinguish between "supported but disabled" and +"not supported at all". + +## Mode Control + +Applications opt in via standard DECSET/DECRST sequences using **DEC private mode 2033**: + +``` +CSI ? 2033 h Enable Binary Paste Mode +CSI ? 2033 l Disable Binary Paste Mode (default) +``` + +Binary Paste Mode is disabled by default and resets to disabled on hard reset (RIS) +and soft reset (DECSTR). + +## Sub-Command Architecture + +All binary paste operations use a single DCS sequence format with a **sub-command +character** as the first byte of the data string: + +``` +DCS 2033 [; params] b ST +``` + +Where: + +| Component | Description | +|-----------|-------------------------------------------------------------------------| +| `2033` | First parameter, matching the mode number | +| `params` | Optional additional parameters (e.g., byte count for data delivery) | +| `b` | DCS final character (mnemonic: *binary*) | +| `sub-cmd` | First byte of data string: sub-command identifier | +| `payload` | Sub-command-specific data | +| `ST` | String Terminator (`ESC \`) | + +### Sub-command Summary + +| Sub-cmd | Direction | Purpose | +|---------|----------------|---------------------------------| +| `d` | Terminal → App | Data delivery (paste event) | +| `c` | App → Terminal | Configure MIME type preferences | + +## Data Delivery (`d`) + +When Binary Paste Mode is enabled and the user initiates a paste action while the +system clipboard contains a matching MIME type, the terminal sends the following DCS +sequence to the application via the PTY: + +``` +DCS 2033 ; Ps b d ; ST +``` + +Where: + +| Component | Description | +|-------------------------|-------------------------------------------------------------------------| +| `Ps` | Second parameter: byte count of the original (pre-encoding) binary data | +| `d` | Sub-command: data delivery | +| `` | MIME type of the delivered content (e.g., `image/png`) | +| `` | Base64-encoded binary data | + +### Example + +A 1234-byte PNG image on the clipboard produces: + +``` +ESC P 2033 ; 1234 b dimage/png ; iVBORw0KGgoAAAANSUhEUgAA... ESC \ +``` + +The `Ps` (size) parameter allows the application to pre-allocate a buffer before +decoding the base64 payload. If absent or zero, the size is unknown. + +### Size Validation + +When `Ps` is present and non-zero, the application **must** verify that the decoded +payload size matches the declared `Ps` value. If the sizes do not match, the +application **should** discard the entire paste and treat it as a protocol error. + +| Condition | Action | +|----------------------|--------------------------------------| +| `Ps` absent or zero | Accept (size unknown, no validation) | +| Decoded size == `Ps` | Accept | +| Decoded size != `Ps` | Discard and report error | + +## MIME Preference Configuration (`c`) + +Applications can configure which MIME types they accept and their priority order +by sending: + +``` +DCS 2033 b c ST +``` + +Where `` is a **comma-separated** list of MIME types in priority order +(highest priority first): + +``` +ESC P 2033 b cimage/png,image/svg+xml,text/html ESC \ +``` + +### Behavior + +- **Any MIME type is valid**: not limited to `image/*` types. Applications may + request `text/html`, `application/json`, `text/plain`, etc. +- **Priority order**: the terminal checks the system clipboard for each listed + type in order and delivers the first match via the `d` sub-command. +- **Default fallback**: if no preferences are configured, the terminal uses its + built-in default list (see Terminal Defaults below). +- **Empty payload**: sending `DCS 2033 b c ST` (no MIME types) resets preferences + to terminal defaults. +- **Replacement**: sending a new configure sequence replaces all previous preferences. +- **Mode gating**: the configure sequence is silently ignored if mode 2033 is not + enabled. +- **Cleared on reset**: preferences are cleared when mode 2033 is disabled + (DECRST 2033), on soft reset (DECSTR), and on hard reset (RIS). + +### Terminal Defaults + +When no application preferences are configured, the terminal uses the following +built-in priority list: + +| Priority | MIME Type | +|----------|-----------------| +| 1 | `image/png` | +| 2 | `image/jpeg` | +| 3 | `image/gif` | +| 4 | `image/bmp` | +| 5 | `image/svg+xml` | + +### Separator Choice + +Commas (`,`) are used to separate MIME types instead of semicolons because: +- Semicolons (`;`) are already used in the data delivery sub-command to separate + the MIME type from the base64 payload. +- Commas align with the HTTP `Accept` header convention for MIME type lists. + +## Interaction with Bracketed Paste Mode + +Binary Paste Mode is independent of Bracketed Paste Mode (2004). Both can be +active simultaneously. When both are active and the clipboard contains a matching +MIME type, the DCS binary paste sequence takes precedence: + +| Mode 2004 | Mode 2033 | Has Match | Behavior | +|-----------|-----------|-----------|----------------------------| +| off | off | any | Normal text paste | +| on | off | any | Bracketed text paste | +| off | on | yes | DCS binary paste | +| on | on | yes | DCS binary paste | +| any | on | no | Fall through to text paste | + +Only one representation is sent per paste action — the terminal never sends both +a DCS binary paste and a bracketed text paste for the same clipboard content. + +**Note:** if `text/plain` appears in the application's MIME preference list, it +will be delivered via the DCS binary paste format (the application explicitly opted +into receiving it through the binary paste protocol). + +## Size Limits + +Terminals implementing this extension may enforce size limits on binary paste data: + +- **Hard limit**: Payloads exceeding the limit are silently dropped (no DCS sent). + Recommended: 10 MB pre-encoding. +- **Soft limit**: Payloads between the soft and hard limit may trigger a user + confirmation prompt. Recommended: 5 MB pre-encoding. + +The base64 encoding inflates the wire size by approximately 33%. + +## Future Extensions + +The following sub-commands are reserved for future use: + +| Sub-cmd | Direction | Purpose | +|---------|----------------|---------------------------------------------| +| `r` | App → Terminal | Request clipboard contents (clipboard read) | +| `?` | Terminal → App | Report available MIME types | + +Clipboard read would require a permission model to prevent unauthorized clipboard +access by applications. + +Multi-MIME delivery (sending all matching types in a single paste event with an +end-of-batch marker `DCS 2033 ; 0 b d ST`) is also under consideration. + +## Adoption State + +| Support | Terminal/Toolkit/App | Notes | +|----------|----------------------|------------------------------------| +| ✅ | Contour | since `0.6.3` (initial prototype) | +| not yet | Kitty | | +| not yet | WezTerm | | +| not yet | Ghostty | | +| not yet | foot | | +| not yet | tmux | | +| ... | ... | | + +If your project adds support for this feature, please +[open an issue](https://github.com/contour-terminal/contour/issues) or submit a PR +so we can update this table. + +## Reference + +- [Bracketed Paste Mode](https://en.wikipedia.org/wiki/Bracketed-paste) — DEC mode 2004, text-only predecessor +- [OSC 52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) — clipboard read/write via escape sequences (text/base64) +- [Kitty Clipboard Protocol](https://sw.kovidgoyal.net/kitty/clipboard/) — full bidirectional clipboard protocol (OSC 5522) +- [DECRQM (Request Mode)](https://vt100.net/docs/vt510-rm/DECRQM.html) — mode support query diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 265b4e0462..500176e041 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,6 +3,9 @@ if(LIBTERMINAL_TESTING) add_executable(watch-mouse-events watch-mouse-events.cpp) target_link_libraries(watch-mouse-events vtbackend) + add_executable(watch-clipboard-paste watch-clipboard-paste.cpp) + target_link_libraries(watch-clipboard-paste vtbackend) + add_executable(detect-dark-light-mode detect-dark-light-mode.cpp) endif() endif() diff --git a/examples/watch-clipboard-paste.cpp b/examples/watch-clipboard-paste.cpp new file mode 100644 index 0000000000..bbdb57d2e7 --- /dev/null +++ b/examples/watch-clipboard-paste.cpp @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: Apache-2.0 + +/// @file watch-clipboard-paste.cpp +/// +/// Example application demonstrating the Binary Paste Mode (DEC mode 2033). +/// +/// This program enables both bracketed text paste (mode 2004) and binary paste (mode 2033), +/// then listens on stdin for paste events. +/// +/// - Text paste: prints the pasted text to stdout. +/// - Binary paste: saves the binary data to ~/Downloads/binary-paste-. +/// +/// Terminate with Ctrl+D. + +#include +#include +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) + #include +#else + #include +#endif + +#include +#include + +using namespace std; +using namespace vtbackend; + +namespace +{ + +/// Returns a file extension for a given MIME type, or "bin" for unknown types. +constexpr string_view extensionForMimeType(string_view mimeType) noexcept +{ + if (mimeType == "image/png") + return "png"; + if (mimeType == "image/jpeg") + return "jpg"; + if (mimeType == "image/gif") + return "gif"; + if (mimeType == "image/bmp") + return "bmp"; + if (mimeType == "image/svg+xml") + return "svg"; + return "bin"; +} + +/// Generates a timestamp string suitable for filenames (YYYYMMDD-HHMMSS). +string makeTimestamp() +{ + auto const now = chrono::system_clock::now(); + auto const time = chrono::system_clock::to_time_t(now); + auto const tm = *localtime(&time); + return format("{:04}{:02}{:02}-{:02}{:02}{:02}", + tm.tm_year + 1900, + tm.tm_mon + 1, + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec); +} + +struct PasteWatcher final: public vtparser::NullParserEvents +{ + static bool _running; + + termios savedTermios; + vtparser::Parser vtInputParser; + + // State for bracketed paste collection + bool inBracketedPaste = false; + string bracketedPasteBuffer; + + // State for DCS binary paste collection + bool inDcsBinaryPaste = false; + unsigned dcsBinarySize = 0; + string dcsDataString; + + // State for tracking CSI sequences + Sequence _sequence {}; + SequenceParameterBuilder _parameterBuilder; + + PasteWatcher() noexcept: + savedTermios { vtpty::util::getTerminalSettings(STDIN_FILENO) }, + vtInputParser { *this }, + _parameterBuilder(_sequence.parameters()) + { + // Set raw mode so we receive all bytes directly + auto tio = savedTermios; + tio.c_lflag &= static_cast(~(ECHO | ICANON)); + tio.c_cc[VMIN] = 1; + tio.c_cc[VTIME] = 0; + vtpty::util::applyTerminalSettings(STDIN_FILENO, tio); + + // Enable bracketed paste (mode 2004) and binary paste (mode 2033) + writeToTTY("\033[?2004h"); // bracketed paste + writeToTTY("\033[?2033h"); // binary paste + + // Configure MIME preferences: DCS 2033 b c ST + // Accept images and HTML, in priority order. + writeToTTY("\033P2033bcimage/png,image/jpeg,image/gif,image/bmp,image/svg+xml,text/html\033\\"); + + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + writeToTTY("Binary Paste Watcher\r\n"); + writeToTTY("====================\r\n"); + writeToTTY("Modes enabled: bracketed paste (2004), binary paste (2033)\r\n"); + writeToTTY( + "MIME preferences: image/png, image/jpeg, image/gif, image/bmp, image/svg+xml, text/html\r\n"); + writeToTTY("Paste text or images from your clipboard.\r\n"); + writeToTTY("Press Ctrl+D to exit.\r\n\r\n"); + } + + ~PasteWatcher() override + { + writeToTTY("\033[?2033l"); // disable binary paste + writeToTTY("\033[?2004l"); // disable bracketed paste + vtpty::util::applyTerminalSettings(STDIN_FILENO, savedTermios); + writeToTTY("\r\nTerminating.\r\n"); + } + + static void signalHandler(int signo) + { + _running = false; + signal(signo, SIG_DFL); + } + + void writeToTTY(string_view s) noexcept { ::write(STDOUT_FILENO, s.data(), s.size()); } + + int run() + { + while (_running) + processInput(); + return EXIT_SUCCESS; + } + + void processInput() + { + char buf[4096]; + auto const n = ::read(STDIN_FILENO, buf, sizeof(buf)); + if (n > 0) + vtInputParser.parseFragment(string_view(buf, static_cast(n))); + else if (n == 0) + _running = false; // EOF + } + + // --- Parser event handlers --- + + void execute(char controlCode) override + { + if (controlCode == 0x04) // Ctrl+D (EOT) + _running = false; + } + + void print(char32_t ch) override + { + if (inBracketedPaste) + { + // Collect pasted text between CSI 200~ and CSI 201~ delimiters. + // Encode the codepoint as UTF-8 into the buffer. + char buf[4]; + if (ch < 0x80) + { + buf[0] = static_cast(ch); + bracketedPasteBuffer.append(buf, 1); + } + else if (ch < 0x800) + { + buf[0] = static_cast(0xC0 | (ch >> 6)); + buf[1] = static_cast(0x80 | (ch & 0x3F)); + bracketedPasteBuffer.append(buf, 2); + } + else if (ch < 0x10000) + { + buf[0] = static_cast(0xE0 | (ch >> 12)); + buf[1] = static_cast(0x80 | ((ch >> 6) & 0x3F)); + buf[2] = static_cast(0x80 | (ch & 0x3F)); + bracketedPasteBuffer.append(buf, 3); + } + else + { + buf[0] = static_cast(0xF0 | (ch >> 18)); + buf[1] = static_cast(0x80 | ((ch >> 12) & 0x3F)); + buf[2] = static_cast(0x80 | ((ch >> 6) & 0x3F)); + buf[3] = static_cast(0x80 | (ch & 0x3F)); + bracketedPasteBuffer.append(buf, 4); + } + } + } + + size_t print(std::string_view text, size_t /*columnsUsed*/) override + { + if (inBracketedPaste) + bracketedPasteBuffer.append(text); + return 0; + } + + // CSI sequence handling for bracketed paste delimiters + void clear() noexcept override + { + _sequence.clearExceptParameters(); + _parameterBuilder.reset(); + } + + void collectLeader(char leader) noexcept override { _sequence.setLeader(leader); } + + void collect(char ch) override { _sequence.intermediateCharacters().push_back(ch); } + + void paramDigit(char ch) noexcept override + { + _parameterBuilder.multiplyBy10AndAdd(static_cast(ch - '0')); + } + + void param(char ch) noexcept override + { + switch (ch) + { + case ';': _parameterBuilder.nextParameter(); break; + case ':': _parameterBuilder.nextSubParameter(); break; + default: + if (ch >= '0' && ch <= '9') + _parameterBuilder.multiplyBy10AndAdd(static_cast(ch - '0')); + break; + } + } + + void dispatchCSI(char finalChar) override + { + _sequence.setCategory(FunctionCategory::CSI); + _sequence.setFinalChar(finalChar); + _parameterBuilder.fixiate(); + + // Check for bracketed paste start: CSI 200 ~ + if (finalChar == '~' && _sequence.parameterCount() >= 1) + { + auto const param0 = _sequence.param(0); + if (param0 == 200) // Start of bracketed paste + { + inBracketedPaste = true; + bracketedPasteBuffer.clear(); + return; + } + if (param0 == 201) // End of bracketed paste + { + inBracketedPaste = false; + handleTextPaste(bracketedPasteBuffer); + bracketedPasteBuffer.clear(); + return; + } + } + + _sequence.clear(); + } + + // DCS handling for binary paste: DCS 2033 ; b d; ST + void hook(char finalChar) override + { + _parameterBuilder.fixiate(); + + if (finalChar == 'b' && _sequence.parameterCount() >= 1 && _sequence.param(0) == 2033) + { + inDcsBinaryPaste = true; + dcsBinarySize = _sequence.parameterCount() >= 2 ? static_cast(_sequence.param(1)) : 0; + dcsDataString.clear(); + if (dcsBinarySize > 0) + dcsDataString.reserve(dcsBinarySize * 4 / 3 + 100); // base64 size estimate + mime header + } + } + + void put(char ch) override + { + if (inDcsBinaryPaste) + dcsDataString += ch; + else if (inBracketedPaste) + bracketedPasteBuffer += ch; + } + + void unhook() override + { + if (inDcsBinaryPaste) + { + inDcsBinaryPaste = false; + + if (!dcsDataString.empty()) + { + auto const subCommand = dcsDataString.front(); + auto const payload = string_view(dcsDataString).substr(1); + + switch (subCommand) + { + case 'd': // Data delivery sub-command + handleBinaryPaste(payload); + break; + default: + writeToTTY(format("[Binary Paste] Unknown sub-command: '{}'\r\n", subCommand)); + break; + } + } + + dcsDataString.clear(); + } + } + + // --- Paste handlers --- + + void handleTextPaste(string_view text) + { + writeToTTY(format("[Text Paste] {} bytes:\r\n", text.size())); + // Write line by line, adding CR before each LF for terminal display + for (auto const ch: text) + { + if (ch == '\n') + writeToTTY("\r\n"sv); + else + { + auto const c = static_cast(ch); + ::write(STDOUT_FILENO, &c, 1); + } + } + writeToTTY("\r\n---\r\n"sv); + } + + void handleBinaryPaste(string_view dataString) + { + // Parse data delivery payload: ; + auto const semicolonPos = dataString.find(';'); + if (semicolonPos == string_view::npos) + { + writeToTTY("[Binary Paste] Error: malformed data string (no semicolon separator)\r\n"sv); + return; + } + + auto const mimeType = dataString.substr(0, semicolonPos); + auto const base64Data = dataString.substr(semicolonPos + 1); + + // Decode base64 + auto const decoded = crispy::base64::decode(base64Data); + + // Validate size: if Ps was provided and non-zero, the decoded size must match. + if (dcsBinarySize > 0 && decoded.size() != dcsBinarySize) + { + writeToTTY(format("[Binary Paste] Error: size mismatch (declared {} bytes, decoded {} bytes). " + "Discarding.\r\n", + dcsBinarySize, + decoded.size())); + return; + } + + auto const ext = extensionForMimeType(mimeType); + auto const timestamp = makeTimestamp(); + + // Build output path: ~/Downloads/binary-paste-. + auto const homeDir = string(getenv("HOME") ? getenv("HOME") : "/tmp"); + auto const downloadsDir = filesystem::path(homeDir) / "Downloads"; + filesystem::create_directories(downloadsDir); + + auto const filename = format("binary-paste-{}.{}", timestamp, ext); + auto const outputPath = downloadsDir / filename; + + // Write to file + ofstream file(outputPath, ios::binary); + if (file.is_open()) + { + file.write(decoded.data(), static_cast(decoded.size())); + file.close(); + writeToTTY(format("[Binary Paste] MIME: {}, {} bytes -> {}\r\n", + mimeType, + decoded.size(), + outputPath.string())); + } + else + { + writeToTTY(format("[Binary Paste] Error: could not write to {}\r\n", outputPath.string())); + } + } +}; + +bool PasteWatcher::_running = true; + +} // namespace + +int main() +{ + auto watcher = PasteWatcher {}; + return watcher.run(); +} diff --git a/metainfo.xml b/metainfo.xml index a2fa696f0d..78b234a515 100644 --- a/metainfo.xml +++ b/metainfo.xml @@ -147,6 +147,7 @@
  • Adds complete DECCIR (Cursor Information Report) response including character set designations, GL/GR mappings, and wrap-pending state (#97)
  • Adds environment variable expansion (${VAR_NAME} syntax) in configuration file paths for cross-platform config reuse (#1278)
  • Adds configurable text outline rendering to improve glyph readability on transparent or low-opacity backgrounds (#1895)
  • +
  • Adds Binary Paste Mode (DEC mode 2033) for receiving binary clipboard data (e.g., images) with MIME type metadata via DCS sequences, with DECRQM feature detection and size validation up to 10 MB
  • diff --git a/mkdocs.yml b/mkdocs.yml index 892e9a0c7b..eb1d58b9e2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -124,6 +124,7 @@ nav: - vt-extensions/line-reflow-mode.md - vt-extensions/save-and-restore-sgr-attributes.md - vt-extensions/osc-133-shell-integration.md + - vt-extensions/binary-paste.md - Internals: - internals/index.md - internals/CODING_STYLE.md diff --git a/src/contour/TerminalSession.cpp b/src/contour/TerminalSession.cpp index 1319e85796..63c87a143d 100644 --- a/src/contour/TerminalSession.cpp +++ b/src/contour/TerminalSession.cpp @@ -818,15 +818,68 @@ void TerminalSession::pasteFromClipboard(unsigned count, bool strip) for (int i = 0; i < md->formats().size(); ++i) sessionLog()("pasteFromClipboard[{}]: {}\n", i, md->formats().at(i).toStdString()); + // Binary paste path: when mode 2033 is enabled, check for MIME types. + // Use the app's configured preferences if available, otherwise fall back to defaults. + if (terminal().isBinaryPasteModeEnabled()) + { + auto const appPrefs = terminal().binaryPasteMimePreferences(); + + // Build a flat list of MIME types to try, in priority order. + auto mimeTypesToTry = std::vector(); + if (!appPrefs.empty()) + for (auto const& mime: appPrefs) + mimeTypesToTry.emplace_back(mime); + else + for (auto const& mime: vtbackend::Terminal::DefaultBinaryPasteMimeTypes) + mimeTypesToTry.emplace_back(mime); + + for (auto const& mimeType: mimeTypesToTry) + { + auto const qMime = QString::fromUtf8(mimeType.data(), static_cast(mimeType.size())); + if (!md->hasFormat(qMime)) + continue; + + auto const data = md->data(qMime); + + // 10 MB hard limit for binary paste + if (data.size() > 10 * 1024 * 1024) + { + sessionLog()("Binary clipboard data too large ({} bytes). Ignoring.", data.size()); + _display->post([this]() { + emit showNotification("Paste", QString::fromStdString("Binary paste is too large")); + }); + return; + } + + // 5 MB soft limit: request user permission + if (data.size() > 5 * 1024 * 1024) + { + _pendingBinaryPaste = PendingBinaryPaste { + .mimeType = std::string(mimeType), + .data = data, + }; + emit requestPermissionForPasteLargeFile(); + sessionLog()("Binary clipboard data large ({} bytes). Requesting permission.", + data.size()); + return; + } + + auto const dataSpan = std::span( + reinterpret_cast(data.constData()), static_cast(data.size())); + terminal().sendBinaryPaste(mimeType, dataSpan); + return; + } + // No matching MIME type found — fall through to text paste below. + } + auto const text = clipboard->text(QClipboard::Clipboard); // 1 MB hard limit if (text.size() > 1024 * 1024) { sessionLog()("Clipboard contains huge text. Ignoring."); - _display->post([this]() { - emit showNotification("Screenshot", QString::fromStdString("Paste is too big")); - }); + _display->post( + [this]() { emit showNotification("Paste", QString::fromStdString("Paste is too big")); }); return; } // 512 KB soft limit to ask user for permission @@ -876,6 +929,28 @@ void TerminalSession::applyPendingPaste(bool allow, bool remember) terminal().sendPaste(string_view { text.toStdString() }); } +void TerminalSession::applyPendingBinaryPaste(bool allow, bool remember) +{ + sessionLog()("applyPendingBinaryPaste: allow={}, remember={}", allow, remember); + if (remember) + _rememberedPermissions[GuardedRole::BigPaste] = allow; + + if (!_pendingBinaryPaste) + return; + + if (!allow) + { + _pendingBinaryPaste = std::nullopt; + return; + } + + auto const& pending = _pendingBinaryPaste.value(); + auto const dataSpan = std::span(reinterpret_cast(pending.data.constData()), + static_cast(pending.data.size())); + terminal().sendBinaryPaste(pending.mimeType, dataSpan); + _pendingBinaryPaste = std::nullopt; +} + void TerminalSession::onSelectionCompleted() { switch (_config.onMouseSelection.value()) @@ -1213,7 +1288,7 @@ void TerminalSession::playSound(vtbackend::Sequence::Parameters const& params) auto range = params.range(); _musicalNotesBuffer.clear(); _musicalNotesBuffer.insert(_musicalNotesBuffer.begin(), range.begin() + 2, range.end()); - emit _audio->play(params.at(0), params.at(1), _musicalNotesBuffer); + emit _audio->play(static_cast(params.at(0)), static_cast(params.at(1)), _musicalNotesBuffer); } void TerminalSession::cursorPositionChanged() diff --git a/src/contour/TerminalSession.h b/src/contour/TerminalSession.h index 9e430a44a3..168eae8988 100644 --- a/src/contour/TerminalSession.h +++ b/src/contour/TerminalSession.h @@ -252,6 +252,7 @@ class TerminalSession: public QAbstractItemModel, public vtbackend::Terminal::Ev Q_INVOKABLE void applyPendingFontChange(bool allow, bool remember); Q_INVOKABLE void applyPendingPaste(bool allow, bool remember); + Q_INVOKABLE void applyPendingBinaryPaste(bool allow, bool remember); Q_INVOKABLE void executePendingBufferCapture(bool allow, bool remember); Q_INVOKABLE void executeShowHostWritableStatusLine(bool allow, bool remember); Q_INVOKABLE void resizeTerminalToDisplaySize(); @@ -502,6 +503,15 @@ class TerminalSession: public QAbstractItemModel, public vtbackend::Terminal::Ev std::optional _pendingBufferCapture; std::optional _pendingFontChange; std::optional _pendingBigPaste; + + /// Holds binary paste data pending user permission (for large payloads). + struct PendingBinaryPaste + { + std::string mimeType; + QByteArray data; + }; + std::optional _pendingBinaryPaste; + PermissionCache _rememberedPermissions; std::unique_ptr _exitWatcherThread; diff --git a/src/vtbackend/BinaryPaste_test.cpp b/src/vtbackend/BinaryPaste_test.cpp new file mode 100644 index 0000000000..c225f9e40b --- /dev/null +++ b/src/vtbackend/BinaryPaste_test.cpp @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using crispy::escape; +using namespace vtbackend; +using namespace vtbackend::test; +using namespace std; +using namespace std::string_view_literals; + +// NOLINTBEGIN(misc-const-correctness,readability-function-cognitive-complexity) + +// {{{ Mode enable/disable and feature detection + +TEST_CASE("BinaryPaste.mode_enable_disable", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Initially disabled + REQUIRE_FALSE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); + REQUIRE_FALSE(mock.terminal.isBinaryPasteModeEnabled()); + + // Enable via DECSM + mock.writeToScreen(DECSM(2033)); + REQUIRE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); + REQUIRE(mock.terminal.isBinaryPasteModeEnabled()); + + // Disable via DECRM + mock.writeToScreen(DECRM(2033)); + REQUIRE_FALSE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); + REQUIRE_FALSE(mock.terminal.isBinaryPasteModeEnabled()); +} + +TEST_CASE("BinaryPaste.DECRQM_query", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Query when disabled — expect mode value 2 (reset) + mock.writeToScreen(DECRQM(2033)); + CHECK(e(mock.terminal.peekInput()) == e("\033[?2033;2$y"sv)); + mock.terminal.flushInput(); + + // Enable the mode + mock.writeToScreen(DECSM(2033)); + mock.terminal.flushInput(); + + // Query when enabled — expect mode value 1 (set) + mock.writeToScreen(DECRQM(2033)); + CHECK(e(mock.terminal.peekInput()) == e("\033[?2033;1$y"sv)); +} + +// }}} Mode enable/disable and feature detection + +// {{{ Data delivery (DCS 2033 ; b d; ST) + +TEST_CASE("BinaryPaste.generateBinaryPaste_produces_DCS", "[binary_paste]") +{ + auto input = InputGenerator {}; + auto constexpr TestBytes = std::array { 't', 'e', 's', 't', ' ', 'b', 'i', 'n', + 'a', 'r', 'y', ' ', 'd', 'a', 't', 'a' }; + auto const testData = std::span(TestBytes); + input.generateBinaryPaste("image/png", testData); + + auto const output = std::string(input.peek()); + // Verify DCS structure: ESC P 2033 ; b d ; ESC backslash + auto const prefix = std::format("\033P2033;{}bdimage/png;", testData.size()); + CHECK(output.starts_with(prefix)); + CHECK(output.ends_with("\033\\")); + + // Extract and verify base64 payload decodes back to original + auto const mimeEnd = output.find(';', prefix.size() - 1); + auto const base64Start = mimeEnd + 1; + auto const base64End = output.size() - 2; // before ESC backslash + auto const base64Data = output.substr(base64Start, base64End - base64Start); + auto const decoded = crispy::base64::decode(base64Data); + CHECK(decoded == std::string_view(reinterpret_cast(testData.data()), testData.size())); +} + +TEST_CASE("BinaryPaste.generateBinaryPaste_empty_data", "[binary_paste]") +{ + auto input = InputGenerator {}; + input.generateBinaryPaste("image/png", std::span {}); + CHECK(input.peek().empty()); +} + +TEST_CASE("BinaryPaste.sendBinaryPaste_via_terminal", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable binary paste mode + mock.writeToScreen(DECSM(2033)); + mock.resetReplyData(); + + // Send binary paste (this calls flushInput(), writing to PTY stdin buffer) + auto constexpr TestBytes = std::array { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' }; + auto const testData = std::span(TestBytes); + mock.terminal.sendBinaryPaste("image/png", testData); + + auto const& output = mock.replyData(); + auto const prefix = std::format("\033P2033;{}bdimage/png;", testData.size()); + CHECK(output.starts_with(prefix)); + CHECK(output.ends_with("\033\\")); +} + +// }}} Data delivery + +// {{{ Reset behavior + +TEST_CASE("BinaryPaste.hard_reset_clears_mode", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable the mode + mock.writeToScreen(DECSM(2033)); + REQUIRE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); + + // Hard reset (RIS) + mock.writeToScreen("\033c"); + CHECK_FALSE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); +} + +TEST_CASE("BinaryPaste.soft_reset_clears_mode_and_preferences", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable the mode and configure MIME preferences + mock.writeToScreen(DECSM(2033)); + mock.writeToScreen("\033P2033bcimage/png,image/jpeg\033\\"); + REQUIRE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); + REQUIRE(mock.terminal.binaryPasteMimePreferences().size() == 2); + + // Soft reset (DECSTR) + mock.writeToScreen("\033[!p"); + CHECK_FALSE(mock.terminal.isModeEnabled(DECMode::BinaryPaste)); + CHECK(mock.terminal.binaryPasteMimePreferences().empty()); +} + +TEST_CASE("BinaryPaste.reset_clears_input_generator_flag", "[binary_paste]") +{ + auto input = InputGenerator {}; + + // Enable binary paste + input.setBinaryPaste(true); + REQUIRE(input.binaryPaste()); + + // Reset + input.reset(); + CHECK_FALSE(input.binaryPaste()); +} + +// }}} Reset behavior + +// {{{ MIME Preference Configuration (DCS 2033 b c ST) + +TEST_CASE("BinaryPaste.configure_mime_preferences", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable binary paste mode + mock.writeToScreen(DECSM(2033)); + + // Send MIME preference configuration: DCS 2033 b c ST + mock.writeToScreen("\033P2033bcimage/png,image/jpeg,text/html\033\\"); + + auto const prefs = mock.terminal.binaryPasteMimePreferences(); + REQUIRE(prefs.size() == 3); + CHECK(prefs[0] == "image/png"); + CHECK(prefs[1] == "image/jpeg"); + CHECK(prefs[2] == "text/html"); +} + +TEST_CASE("BinaryPaste.configure_reset_to_defaults", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable and configure + mock.writeToScreen(DECSM(2033)); + mock.writeToScreen("\033P2033bcimage/png,text/html\033\\"); + REQUIRE(mock.terminal.binaryPasteMimePreferences().size() == 2); + + // Send empty configure to reset to defaults + mock.writeToScreen("\033P2033bc\033\\"); + CHECK(mock.terminal.binaryPasteMimePreferences().empty()); +} + +TEST_CASE("BinaryPaste.configure_clears_on_mode_disable", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable and configure + mock.writeToScreen(DECSM(2033)); + mock.writeToScreen("\033P2033bcimage/png,image/jpeg\033\\"); + REQUIRE(mock.terminal.binaryPasteMimePreferences().size() == 2); + + // Disable mode — preferences should be cleared + mock.writeToScreen(DECRM(2033)); + CHECK(mock.terminal.binaryPasteMimePreferences().empty()); +} + +TEST_CASE("BinaryPaste.configure_clears_on_hard_reset", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable and configure + mock.writeToScreen(DECSM(2033)); + mock.writeToScreen("\033P2033bcimage/png\033\\"); + REQUIRE(mock.terminal.binaryPasteMimePreferences().size() == 1); + + // Hard reset (RIS) + mock.writeToScreen("\033c"); + CHECK(mock.terminal.binaryPasteMimePreferences().empty()); +} + +TEST_CASE("BinaryPaste.configure_ignored_when_mode_disabled", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Mode is not enabled — send configure sequence anyway + mock.writeToScreen("\033P2033bcimage/png\033\\"); + + // Preferences should remain empty (configure was silently ignored) + CHECK(mock.terminal.binaryPasteMimePreferences().empty()); +} + +TEST_CASE("BinaryPaste.configure_arbitrary_mime_types", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable mode + mock.writeToScreen(DECSM(2033)); + + // Configure with non-image MIME types + mock.writeToScreen("\033P2033bctext/html,application/json,text/plain\033\\"); + + auto const prefs = mock.terminal.binaryPasteMimePreferences(); + REQUIRE(prefs.size() == 3); + CHECK(prefs[0] == "text/html"); + CHECK(prefs[1] == "application/json"); + CHECK(prefs[2] == "text/plain"); +} + +TEST_CASE("BinaryPaste.configure_updates_replace_previous", "[binary_paste]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } }; + + // Enable and configure + mock.writeToScreen(DECSM(2033)); + mock.writeToScreen("\033P2033bcimage/png,image/jpeg\033\\"); + REQUIRE(mock.terminal.binaryPasteMimePreferences().size() == 2); + + // Send another configure — replaces previous preferences + mock.writeToScreen("\033P2033bctext/html\033\\"); + auto const prefs = mock.terminal.binaryPasteMimePreferences(); + REQUIRE(prefs.size() == 1); + CHECK(prefs[0] == "text/html"); +} + +// }}} MIME Preference Configuration + +// NOLINTEND(misc-const-correctness,readability-function-cognitive-complexity) diff --git a/src/vtbackend/CMakeLists.txt b/src/vtbackend/CMakeLists.txt index 1444e35f3f..d1bc140a55 100644 --- a/src/vtbackend/CMakeLists.txt +++ b/src/vtbackend/CMakeLists.txt @@ -125,6 +125,7 @@ if(LIBTERMINAL_TESTING) enable_testing() add_executable(vtbackend_test Animation_test.cpp + BinaryPaste_test.cpp Capabilities_test.cpp DesktopNotification_test.cpp Color_test.cpp diff --git a/src/vtbackend/Functions.h b/src/vtbackend/Functions.h index d82a2722ec..adc2785ea0 100644 --- a/src/vtbackend/Functions.h +++ b/src/vtbackend/Functions.h @@ -134,6 +134,7 @@ constexpr inline auto DECRQSS = FunctionDocumentation { .mnemonic = "DECRQSS", . constexpr inline auto DECSIXEL = FunctionDocumentation { .mnemonic = "DECSIXEL", .comment = "Sixel Graphics Image" }; constexpr inline auto STP = FunctionDocumentation { .mnemonic = "STP", .comment = "Set Terminal Profile" }; constexpr inline auto XTGETTCAP = FunctionDocumentation { .mnemonic = "XTGETTCAP", .comment = "Request Termcap/Terminfo String" }; +constexpr inline auto BINARYPASTE = FunctionDocumentation { .mnemonic = "BINARYPASTE", .comment = "Binary Paste Mode (DEC 2033) sub-commands" }; // OSC constexpr inline auto CLIPBOARD = FunctionDocumentation { .mnemonic = "CLIPBOARD", .comment = "Clipboard management." }; @@ -623,6 +624,7 @@ constexpr inline auto DECRQSS = detail::DCS(std::nullopt, 0, 0, '$', 'q', VT constexpr inline auto DECSIXEL = detail::DCS(std::nullopt, 0, 3, std::nullopt, 'q', VTType::VT330, documentation::DECSIXEL); constexpr inline auto STP = detail::DCS(std::nullopt, 0, 0, '$', 'p', VTExtension::Contour, documentation::STP); constexpr inline auto XTGETTCAP = detail::DCS(std::nullopt, 0, 0, '+', 'q', VTExtension::XTerm, documentation::XTGETTCAP); +constexpr inline auto BINARYPASTE = detail::DCS(std::nullopt, 1, 2, std::nullopt, 'b', VTExtension::Contour, documentation::BINARYPASTE); // OSC constexpr inline auto CLIPBOARD = detail::OSC(52, VTExtension::XTerm, documentation::CLIPBOARD); @@ -795,6 +797,7 @@ constexpr static auto allFunctionsArray() noexcept DECRQSS, DECSIXEL, XTGETTCAP, + BINARYPASTE, // OSC SETICON, diff --git a/src/vtbackend/InputGenerator.cpp b/src/vtbackend/InputGenerator.cpp index 523053936d..6b5f5c7309 100644 --- a/src/vtbackend/InputGenerator.cpp +++ b/src/vtbackend/InputGenerator.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -594,6 +595,7 @@ void InputGenerator::reset() { _keyboardInputGenerator.reset(); _bracketedPaste = false; + _binaryPaste = false; _generateFocusEvents = false; _mouseProtocol = std::nullopt; _mouseTransport = MouseTransport::Default; @@ -693,6 +695,19 @@ void InputGenerator::generatePaste(std::string_view const& text) append("\033[201~"sv); } +void InputGenerator::generateBinaryPaste(std::string_view mimeType, std::span binaryData) +{ + inputLog()("Sending binary paste: {} bytes, MIME: {}.", binaryData.size(), mimeType); + + if (binaryData.empty()) + return; + + auto const encoded = crispy::base64::encode(binaryData.begin(), binaryData.end()); + // DCS 2033 ; b d ; ST + // Sub-command 'd' = data delivery + append(std::format("\033P2033;{}bd{};{}\033\\", binaryData.size(), mimeType, encoded)); +} + inline bool InputGenerator::append(std::string_view sequence) { _pendingSequence.insert(end(_pendingSequence), begin(sequence), end(sequence)); diff --git a/src/vtbackend/InputGenerator.h b/src/vtbackend/InputGenerator.h index dab0c32531..c8f8a716f7 100644 --- a/src/vtbackend/InputGenerator.h +++ b/src/vtbackend/InputGenerator.h @@ -9,9 +9,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -458,6 +460,9 @@ class InputGenerator [[nodiscard]] bool bracketedPaste() const noexcept { return _bracketedPaste; } void setBracketedPaste(bool enable) { _bracketedPaste = enable; } + [[nodiscard]] bool binaryPaste() const noexcept { return _binaryPaste; } + void setBinaryPaste(bool enable) { _binaryPaste = enable; } + void setMouseProtocol(MouseProtocol mouseProtocol, bool enabled); [[nodiscard]] std::optional mouseProtocol() const noexcept { return _mouseProtocol; } @@ -502,6 +507,15 @@ class InputGenerator } bool generate(Key key, Modifiers modifier, KeyboardEventType eventType); void generatePaste(std::string_view const& text); + + /// Generates a binary paste DCS data delivery sequence with MIME type and base64-encoded data. + /// + /// Format: DCS 2033 ; b d ; ST + /// Sub-command 'd' indicates data delivery. + /// + /// @param mimeType The MIME type of the binary data (e.g., "image/png"). + /// @param binaryData The raw binary data to encode and send. + void generateBinaryPaste(std::string_view mimeType, std::span binaryData); bool generateMousePress(Modifiers modifier, MouseButton button, CellLocation pos, @@ -593,6 +607,7 @@ class InputGenerator // private fields // bool _bracketedPaste = false; + bool _binaryPaste = false; bool _generateFocusEvents = false; std::optional _mouseProtocol = std::nullopt; bool _passiveMouseTracking = false; diff --git a/src/vtbackend/Screen.cpp b/src/vtbackend/Screen.cpp index b1b9a20a69..9d76885d90 100644 --- a/src/vtbackend/Screen.cpp +++ b/src/vtbackend/Screen.cpp @@ -4099,6 +4099,7 @@ ApplyResult Screen::apply(Function const& function, Sequence const& seq) case STP: _terminal->hookParser(hookSTP(seq)); break; case DECRQSS: _terminal->hookParser(hookDECRQSS(seq)); break; case XTGETTCAP: _terminal->hookParser(hookXTGETTCAP(seq)); break; + case BINARYPASTE: _terminal->hookParser(hookBinaryPaste(seq)); break; default: return ApplyResult::Unsupported; } @@ -4253,6 +4254,50 @@ unique_ptr Screen::hookDECRQSS(Sequence const& /*seq*/) }); } +template +unique_ptr Screen::hookBinaryPaste(Sequence const& seq) +{ + // DCS 2033 b ST + // Verify the first parameter is 2033 (our binary paste mode number). + if (seq.parameterCount() < 1 || seq.param(0) != 2033) + return {}; + + return make_unique([this](string_view data) { + if (data.empty()) + return; + + // Silently ignore if mode 2033 is not enabled. + if (!_terminal->isBinaryPasteModeEnabled()) + return; + + auto const subCommand = data.front(); + auto const payload = data.substr(1); + + switch (subCommand) + { + case 'c': { + // Configure MIME preferences: comma-separated MIME types in priority order. + auto preferences = std::vector(); + if (!payload.empty()) + { + auto const parts = crispy::split(payload, ','); + for (auto const& part: parts) + { + auto const mimeType = std::string(part); + if (!mimeType.empty()) + preferences.emplace_back(mimeType); + } + } + _terminal->setBinaryPasteMimePreferences(std::move(preferences)); + break; + } + default: + // Unknown sub-command — silently ignore for forward compatibility. + break; + } + }); +} + template optional Screen::search(std::u32string_view searchText, CellLocation startPosition) { diff --git a/src/vtbackend/Screen.h b/src/vtbackend/Screen.h index 69eef1a693..b66a331328 100644 --- a/src/vtbackend/Screen.h +++ b/src/vtbackend/Screen.h @@ -657,6 +657,7 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase [[nodiscard]] std::unique_ptr hookSixel(Sequence const& seq); [[nodiscard]] std::unique_ptr hookDECRQSS(Sequence const& seq); [[nodiscard]] std::unique_ptr hookXTGETTCAP(Sequence const& seq); + [[nodiscard]] std::unique_ptr hookBinaryPaste(Sequence const& seq); void processShellIntegration(Sequence const& seq); diff --git a/src/vtbackend/Screen_test.cpp b/src/vtbackend/Screen_test.cpp index 3663e8295b..a7f07b45fc 100644 --- a/src/vtbackend/Screen_test.cpp +++ b/src/vtbackend/Screen_test.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include diff --git a/src/vtbackend/Sequence.h b/src/vtbackend/Sequence.h index 09d91b0482..a64bd600ef 100644 --- a/src/vtbackend/Sequence.h +++ b/src/vtbackend/Sequence.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -29,9 +30,9 @@ class SequenceParameterBuilder; class SequenceParameters { public: - using Storage = std::array; + using Storage = std::array; - [[nodiscard]] constexpr uint16_t at(size_t index) const noexcept { return _values[index]; } + [[nodiscard]] constexpr uint32_t at(size_t index) const noexcept { return _values[index]; } [[nodiscard]] constexpr bool isSubParameter(size_t index) const noexcept { @@ -67,12 +68,12 @@ class SequenceParameters return std::format("{:016b}: ", _subParameterTest); } - [[nodiscard]] constexpr gsl::span range() noexcept + [[nodiscard]] constexpr gsl::span range() noexcept { return gsl::span { _values.data(), _count }; } - [[nodiscard]] constexpr gsl::span range() const noexcept + [[nodiscard]] constexpr gsl::span range() const noexcept { return gsl::span { _values.data(), _count }; } @@ -148,21 +149,21 @@ class SequenceParameterBuilder constexpr void multiplyBy10AndAdd(uint8_t value) noexcept { - unsigned const newValue = (*_currentParameter * 10) + value; - if (newValue > 0xFFFF) - *_currentParameter = 0xFFFF; + uint64_t const newValue = (static_cast(*_currentParameter) * 10) + value; + if (newValue > std::numeric_limits::max()) + *_currentParameter = std::numeric_limits::max(); else - *_currentParameter = static_cast(newValue); + *_currentParameter = static_cast(newValue); } - constexpr void apply(uint16_t value) noexcept + constexpr void apply(uint32_t value) noexcept { if (value >= 10) multiplyBy10AndAdd(static_cast(value / 10)); multiplyBy10AndAdd(static_cast(value % 10)); } - constexpr void set(uint16_t value) noexcept { *_currentParameter = value; } + constexpr void set(uint32_t value) noexcept { *_currentParameter = value; } [[nodiscard]] constexpr bool isSubParameter(size_t index) const noexcept { @@ -202,7 +203,7 @@ class Sequence // and the clipboard can contain large amounts of text. size_t constexpr static MaxOscLength = 1024 * 50; // NOLINT(readability-identifier-naming) - using Parameter = uint16_t; + using Parameter = uint32_t; using Intermediaries = std::string; using DataString = std::string; using Parameters = SequenceParameters; diff --git a/src/vtbackend/Terminal.cpp b/src/vtbackend/Terminal.cpp index 029e0112a4..61a9099c32 100644 --- a/src/vtbackend/Terminal.cpp +++ b/src/vtbackend/Terminal.cpp @@ -2152,6 +2152,37 @@ void Terminal::setBracketedPaste(bool enabled) _inputGenerator.setBracketedPaste(enabled); } +void Terminal::setBinaryPaste(bool enabled) +{ + _inputGenerator.setBinaryPaste(enabled); + if (!enabled) + _binaryPasteMimePreferences.clear(); +} + +bool Terminal::isBinaryPasteModeEnabled() const noexcept +{ + return isModeEnabled(DECMode::BinaryPaste); +} + +void Terminal::setBinaryPasteMimePreferences(std::vector preferences) +{ + _binaryPasteMimePreferences = std::move(preferences); +} + +std::span Terminal::binaryPasteMimePreferences() const noexcept +{ + return _binaryPasteMimePreferences; +} + +void Terminal::sendBinaryPaste(std::string_view mimeType, std::span binaryData) +{ + if (!allowInput()) + return; + + _inputGenerator.generateBinaryPaste(mimeType, binaryData); + flushInput(); +} + void Terminal::setModifyOtherKeys(int mode) { _inputGenerator.setModifyOtherKeys(mode); @@ -2365,6 +2396,7 @@ void Terminal::setMode(DECMode mode, bool enable) } break; case DECMode::BracketedPaste: setBracketedPaste(enable); break; + case DECMode::BinaryPaste: setBinaryPaste(enable); break; case DECMode::MouseSGR: if (enable) setMouseTransport(MouseTransport::SGR); @@ -2507,6 +2539,9 @@ void Terminal::softReset() // TODO: DECSASD (Select active status display) // TODO: DECKPM (Keyboard position mode) // TODO: DECPCTERM (PCTerm mode) + + setMode(DECMode::BinaryPaste, false); + _binaryPasteMimePreferences.clear(); } void Terminal::setGraphicsRendition(GraphicsRendition rendition) @@ -2559,6 +2594,7 @@ void Terminal::hardReset() _imagePool.clear(); _tabs.clear(); + _binaryPasteMimePreferences.clear(); resetColorPalette(); @@ -3267,6 +3303,7 @@ std::string to_string(DECMode mode) case DECMode::TextReflow: return "TextReflow"; case DECMode::SixelCursorNextToGraphic: return "SixelCursorNextToGraphic"; case DECMode::ReportColorPaletteUpdated: return "ReportColorPaletteUpdated"; + case DECMode::BinaryPaste: return "BinaryPaste"; } return std::format("({})", static_cast(mode)); } diff --git a/src/vtbackend/Terminal.h b/src/vtbackend/Terminal.h index 92ee639222..d5f9b0eb7d 100644 --- a/src/vtbackend/Terminal.h +++ b/src/vtbackend/Terminal.h @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -475,6 +476,26 @@ class Terminal bool sendFocusInEvent(); bool sendFocusOutEvent(); void sendPaste(std::string_view text); // Sends verbatim text in bracketed mode to application. + + /// Sends binary data with a given MIME type via DCS binary paste if mode is enabled. + void sendBinaryPaste(std::string_view mimeType, std::span binaryData); + + /// Returns true if binary paste mode (DECMode 2033) is currently active. + [[nodiscard]] bool isBinaryPasteModeEnabled() const noexcept; + + /// Sets the application's MIME type preferences for binary paste mode. + /// Types are listed in priority order. On paste, the terminal delivers the + /// highest-priority match from the system clipboard. + /// An empty list resets to terminal defaults. + void setBinaryPasteMimePreferences(std::vector preferences); + + /// Returns the application's MIME type preferences, or an empty span if using defaults. + [[nodiscard]] std::span binaryPasteMimePreferences() const noexcept; + + /// Default MIME types used when no application preferences are configured. + static constexpr std::array DefaultBinaryPasteMimeTypes = { + "image/png", "image/jpeg", "image/gif", "image/bmp", "image/svg+xml" + }; void sendPasteFromClipboard(unsigned count, bool strip) { _eventListener.pasteFromClipboard(count, strip); @@ -928,6 +949,7 @@ class Terminal void requestWindowResize(ImageSize); void setApplicationkeypadMode(bool enabled); void setBracketedPaste(bool enabled); + void setBinaryPaste(bool enabled); void setCursorStyle(CursorDisplay display, CursorShape shape); void setCursorVisibility(bool visible); void setGenerateFocusEvents(bool enabled); @@ -1479,6 +1501,7 @@ class Terminal uint64_t _instructionCounter = 0; InputGenerator _inputGenerator {}; + std::vector _binaryPasteMimePreferences; ViCommands _viCommands; ViInputHandler _inputHandler; diff --git a/src/vtbackend/primitives.h b/src/vtbackend/primitives.h index b03b9be5fa..429a9897d5 100644 --- a/src/vtbackend/primitives.h +++ b/src/vtbackend/primitives.h @@ -732,6 +732,11 @@ enum class DECMode : std::uint16_t // if modified by the user or operating system (e.g. dark/light mode adaption). ReportColorPaletteUpdated = 2031, + // If enabled, the terminal will deliver binary clipboard data (e.g. images) to the + // application via DCS sequences with MIME type metadata and base64 encoding. + // Applications opt in to receive binary paste data instead of text-only paste. + BinaryPaste = 2033, + // If enabled (default, as per spec), then the cursor is left next to the graphic, // that is, the text cursor is placed at the position of the sixel cursor. // If disabled otherwise, the cursor is placed below the image, as if CR LF was sent, @@ -844,6 +849,7 @@ constexpr unsigned toDECModeNum(DECMode m) noexcept case DECMode::MousePassiveTracking: return 2029; case DECMode::ReportGridCellSelection: return 2030; case DECMode::ReportColorPaletteUpdated: return 2031; + case DECMode::BinaryPaste: return 2033; case DECMode::BatchedRendering: return 2026; case DECMode::Unicode: return 2027; case DECMode::TextReflow: return 2028; @@ -903,6 +909,7 @@ constexpr std::optional fromDECModeNum(unsigned int modeNum) noexcept case 2029: return DECMode::MousePassiveTracking; case 2030: return DECMode::ReportGridCellSelection; case 2031: return DECMode::ReportColorPaletteUpdated; + case 2033: return DECMode::BinaryPaste; case 8452: return DECMode::SixelCursorNextToGraphic; default: return std::nullopt; }