Skip to content

Commit 905b33a

Browse files
committed
feat: implement OSC52 remote clipboard functionality
Feature Description: Implement OSC52 (Operating System Command 52) protocol to support remote servers/containers accessing local clipboard through terminal escape sequences, enabling cross-system clipboard sharing. Technical Implementation: 1. Parse OSC52 escape sequences (\033]52;<target>;<base64>\a) 2. Unified OSC sequence handling (Xte, OSC52 start, normal OSC) 3. Large data chunking mechanism (>256 bytes auto-chunking) 4. Base64 encoding/decoding + UTF-8 conversion 5. Clipboard target selection (c=CLIPBOARD, p=PRIMARY, s=SECONDARY, 0=ALL) Core Changes: - Vt102Emulation: OSC52 sequence parsing, chunking, data accumulation - termwidget: Clipboard writing, security validation, user settings check - Add state flags: _osc52InProgress, _osc52IsFirstChunk - Add allowOSC52 configuration (enabled by default) Security Limits: - Max data size: 64MB (after Base64 encoding) - Configurable via settings (allowOSC52) - Auto-reject invalid Base64/UTF-8 data Testing: - Small data transfer (single chunk, <150 bytes) ✓ - Large data transfer (multi-chunk, 10KB data) ✓ - Clipboard content integrity ✓ - Terminal responsiveness, no lag ✓ Configuration: - settings.json: "allowOSC52": true
1 parent 8184e2d commit 905b33a

File tree

13 files changed

+374
-2
lines changed

13 files changed

+374
-2
lines changed

3rdparty/terminalwidget/lib/Emulation.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,21 @@ public slots:
475475
*/
476476
void cursorChanged(KeyboardCursorShape cursorShape, bool blinkingCursorEnabled);
477477

478+
/**
479+
* @brief OSC52 clipboard operation request
480+
*
481+
* Emitted when a terminal program sends OSC52 escape sequence.
482+
* Format: ESC ] 52 ; target ; base64-data BEL
483+
*
484+
* @param target Clipboard target:
485+
* - 'c' = CLIPBOARD (system clipboard, Ctrl+C/V)
486+
* - 'p' = PRIMARY (X11 primary selection, middle-click paste)
487+
* - 's' = SECONDARY (X11 secondary selection)
488+
* - '0' = All clipboards
489+
* @param base64Data Base64-encoded clipboard data
490+
*/
491+
void osc52ClipboardRequest(char target, const QString &base64Data);
492+
478493
protected:
479494
virtual void setMode(int mode) = 0;
480495
virtual void resetMode(int mode) = 0;

3rdparty/terminalwidget/lib/Session.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Session::Session(QObject* parent) :
113113
//connect teletype to emulation backend
114114
_shellProcess->setUtf8Mode(_emulation->utf8());
115115

116+
// OSC52
117+
connect(_emulation, SIGNAL(osc52ClipboardRequest(char,QString)),
118+
this, SIGNAL(osc52ClipboardRequest(char,QString)));
119+
116120
connect( _shellProcess,SIGNAL(receivedData(const char *,int,bool)),this,
117121
SLOT(onReceiveBlock(const char *,int,bool)) );
118122
connect( _emulation,SIGNAL(sendData(const char *,int,const QTextCodec *)),_shellProcess,

3rdparty/terminalwidget/lib/Session.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,9 @@ public slots:
557557
// warning提示信息 currentShell当前使用的shell, 启用shell是否成功 true 替换了shell false 替换shell但启动失败
558558
void shellWarningMessage(QString currentShell, bool isSuccess);
559559

560+
// OSC52 clipboard operation request
561+
void osc52ClipboardRequest(char target, const QString &base64Data);
562+
560563
private slots:
561564
void done(int);
562565

3rdparty/terminalwidget/lib/Vt102Emulation.cpp

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@ void Vt102Emulation::addToCurrentToken(wchar_t cc)
205205
{
206206
tokenBuffer[tokenBufferPos] = cc;
207207
tokenBufferPos = qMin(tokenBufferPos+1,MAX_TOKEN_LENGTH-1);
208+
209+
// ========== OSC52: Check accumulated buffer size ==========
210+
if (_osc52InProgress && tokenBufferPos >= MAX_TOKEN_LENGTH - 100) {
211+
handleOSC52Chunk();
212+
}
213+
// ==========================================================
208214
}
209215

210216
// Character Class flags used while decoding
@@ -278,13 +284,30 @@ void Vt102Emulation::initTokenizer()
278284
#define CNTL(c) ((c)-'@')
279285
#define ESC 27
280286
#define DEL 127
287+
#define BEL 7
281288

282289
// process an incoming unicode character
283290
void Vt102Emulation::receiveChar(wchar_t cc)
284291
{
285292
if (cc == DEL)
286293
return; //VT100: ignore.
287294

295+
// ========== OSC52: Direct path for subsequent chunks ==========
296+
// If OSC52 is in progress and past first chunk, directly accumulate
297+
if (_osc52InProgress && cc != BEL) {
298+
addToCurrentToken(cc);
299+
// Chunk boundary check is handled inside addToCurrentToken()
300+
return;
301+
}
302+
// ========== OSC52: Check for BEL end ==========
303+
// CRITICAL: Remove Xpe check - BEL alone should end OSC52 sequence
304+
if (cc == BEL && _osc52InProgress) {
305+
handleOSC52End(cc);
306+
resetTokenizer();
307+
return;
308+
}
309+
// =======================================================
310+
288311
if (ces(CTL))
289312
{
290313
// ignore control characters in the text part of Xpe (aka OSC) "ESC]"
@@ -317,8 +340,26 @@ void Vt102Emulation::receiveChar(wchar_t cc)
317340
if (lec(1,0,ESC)) { return; }
318341
if (lec(1,0,ESC+128)) { s[0] = ESC; receiveChar('['); return; }
319342
if (les(2,1,GRP)) { return; }
320-
if (Xte ) { processWindowAttributeChange(); resetTokenizer(); return; }
321-
if (Xpe ) { prevCC = cc; return; }
343+
// ========== OSC: Unified handling ==========
344+
if (Xpe && !_osc52InProgress) {
345+
// Check 1: OSC52 start detection
346+
if (isOSC52Start()) {
347+
startOSC52();
348+
// Don't return - continue to accumulate
349+
}
350+
// Check 2: OSC end (BEL or ST)
351+
else if (cc == BEL || (prevCC == ESC && cc == 0x5C)) {
352+
processWindowAttributeChange();
353+
resetTokenizer();
354+
return;
355+
}
356+
// Check 3: Normal OSC - accumulate
357+
else {
358+
prevCC = cc;
359+
return;
360+
}
361+
}
362+
// ============================================
322363
if (lec(3,2,'?')) { return; }
323364
if (lec(3,2,'>')) { return; }
324365
if (lec(3,2,'!')) { return; }
@@ -917,6 +958,127 @@ void Vt102Emulation::reportTerminalParms(int p)
917958
sendString(tmp);
918959
}
919960

961+
// ========== OSC52 Clipboard Implementation ==========
962+
963+
bool Vt102Emulation::isOSC52Start() const
964+
{
965+
// Check if tokenBuffer starts with "ESC ] 5 2 ;"
966+
// tokenBuffer layout: [0]=ESC, [1]=']', [2]='5', [3]='2', [4]=';'
967+
return (tokenBufferPos >= 5 &&
968+
tokenBuffer[2] == L'5' &&
969+
tokenBuffer[3] == L'2' &&
970+
tokenBuffer[4] == L';');
971+
}
972+
973+
char Vt102Emulation::extractOSC52Target() const
974+
{
975+
// tokenBuffer layout: ESC ] 5 2 ; target ; base64...
976+
//
977+
// position 5
978+
if (tokenBufferPos > 5) {
979+
return static_cast<char>(tokenBuffer[5]);
980+
}
981+
return 'c'; // Default to CLIPBOARD
982+
}
983+
984+
QString Vt102Emulation::extractOSC52Data() const
985+
{
986+
// tokenBuffer layout: ESC ] 5 2 ; target ; base64...
987+
//
988+
// position 7 (data start for first chunk)
989+
// For subsequent chunks, data starts at position 0
990+
int dataStart = _osc52IsFirstChunk ? 7 : 0;
991+
if (tokenBufferPos > dataStart) {
992+
return QString::fromWCharArray(tokenBuffer + dataStart, tokenBufferPos - dataStart);
993+
}
994+
return QString();
995+
}
996+
997+
void Vt102Emulation::startOSC52()
998+
{
999+
_osc52InProgress = true;
1000+
_osc52Target = extractOSC52Target();
1001+
_osc52DataBuffer.clear();
1002+
_osc52IsFirstChunk = true; // Mark as first chunk
1003+
1004+
qInfo() << "OSC52: Start, target=" << _osc52Target;
1005+
}
1006+
1007+
void Vt102Emulation::handleOSC52End(wchar_t cc)
1008+
{
1009+
// cc should be BEL (7)
1010+
Q_ASSERT(cc == BEL);
1011+
Q_UNUSED(cc);
1012+
1013+
// Extract base64 data from current tokenBuffer
1014+
// For the final chunk, skip BEL character at the end
1015+
QString base64Data;
1016+
if (_osc52IsFirstChunk) {
1017+
// Single chunk OSC52: skip header (7 chars) and BEL (1 char)
1018+
if (tokenBufferPos > 8) {
1019+
base64Data = QString::fromWCharArray(tokenBuffer + 7, tokenBufferPos - 8);
1020+
}
1021+
} else {
1022+
// Multi-chunk OSC52 final chunk: tokenBuffer contains ONLY base64 data
1023+
// tokenBufferPos includes the BEL character, so subtract 1
1024+
if (tokenBufferPos > 1) {
1025+
base64Data = QString::fromWCharArray(tokenBuffer, tokenBufferPos - 1);
1026+
}
1027+
}
1028+
1029+
// Accumulate to buffer
1030+
_osc52DataBuffer += base64Data;
1031+
1032+
// ========== OSC52: Check final buffer size ==========
1033+
if (_osc52DataBuffer.size() >= MAX_OSC52_BUFFER) {
1034+
qWarning() << "OSC52: Buffer overflow (>=" << MAX_OSC52_BUFFER
1035+
<< "bytes), rejecting";
1036+
_osc52DataBuffer.clear();
1037+
_osc52InProgress = false;
1038+
_osc52IsFirstChunk = false;
1039+
return;
1040+
}
1041+
// ====================================================
1042+
1043+
// Emit signal with complete data
1044+
emit osc52ClipboardRequest(_osc52Target, _osc52DataBuffer);
1045+
1046+
qInfo() << "OSC52: Complete, sent" << _osc52DataBuffer.size() << "bytes";
1047+
1048+
// Clear state
1049+
_osc52DataBuffer.clear();
1050+
_osc52InProgress = false;
1051+
_osc52IsFirstChunk = false;
1052+
}
1053+
1054+
void Vt102Emulation::handleOSC52Chunk()
1055+
{
1056+
// ========== OSC52: Check buffer size limit ==========
1057+
if (_osc52DataBuffer.size() >= MAX_OSC52_BUFFER) {
1058+
qWarning() << "OSC52: Buffer overflow (>=" << MAX_OSC52_BUFFER
1059+
<< "bytes), rejecting";
1060+
_osc52DataBuffer.clear();
1061+
_osc52InProgress = false;
1062+
_osc52IsFirstChunk = false;
1063+
return;
1064+
}
1065+
// ====================================================
1066+
1067+
// Extract current chunk from tokenBuffer
1068+
QString base64Data = extractOSC52Data();
1069+
_osc52DataBuffer += base64Data;
1070+
1071+
// Mark as not first chunk for subsequent chunks
1072+
_osc52IsFirstChunk = false;
1073+
1074+
// ========== Add resetTokenizer ==========
1075+
resetTokenizer(); // 清空 tokenBuffer,准备接收新数据
1076+
// ========================================
1077+
// Don't clear state, continue receiving
1078+
}
1079+
1080+
// ====================================================
1081+
9201082
void Vt102Emulation::reportStatus()
9211083
{
9221084
sendString("\033[0n"); //VT100. Device status report. 0 = Ready.

3rdparty/terminalwidget/lib/Vt102Emulation.h

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
#define MODE_BracketedPaste (MODES_SCREEN+13) // Xterm-style bracketed paste mode
5252
#define MODE_total (MODES_SCREEN+14)
5353

54+
static const size_t MAX_OSC52_BUFFER = 64 * 1024 * 1024; // 64MB
55+
5456
namespace Konsole
5557
{
5658

@@ -166,6 +168,42 @@ private slots:
166168
void onScrollLock();
167169
void scrollLock(const bool lock);
168170

171+
// ========== OSC52 Clipboard Methods ==========
172+
/**
173+
* @brief Check if current OSC sequence is OSC52 clipboard
174+
* @return true if OSC52 start detected
175+
*/
176+
bool isOSC52Start() const;
177+
178+
/**
179+
* @brief Extract target clipboard character from tokenBuffer
180+
* @return target character (c/p/s/0)
181+
*/
182+
char extractOSC52Target() const;
183+
184+
/**
185+
* @brief Extract base64 data from tokenBuffer
186+
* @return base64 encoded data
187+
*/
188+
QString extractOSC52Data() const;
189+
190+
/**
191+
* @brief Start OSC52 clipboard sequence processing
192+
*/
193+
void startOSC52();
194+
195+
/**
196+
* @brief Handle OSC52 end (BEL character received)
197+
* @param cc The BEL character (should be 7)
198+
*/
199+
void handleOSC52End(wchar_t cc);
200+
201+
/**
202+
* @brief Handle OSC52 chunk when tokenBuffer is full
203+
*/
204+
void handleOSC52Chunk();
205+
// ===============================================
206+
169207
// clears the screen and resizes it to the specified
170208
// number of columns
171209
void clearScreenAndSetColumns(int columnCount);
@@ -194,6 +232,28 @@ private slots:
194232
QTimer* _titleUpdateTimer;
195233

196234
bool _reportFocusEvents;
235+
236+
// ========== OSC52 Clipboard Support ==========
237+
/**
238+
* @brief Flag indicating if OSC52 clipboard sequence is in progress
239+
*/
240+
bool _osc52InProgress = false;
241+
242+
/**
243+
* @brief Buffer for accumulating OSC52 base64 data
244+
*/
245+
QString _osc52DataBuffer;
246+
247+
/**
248+
* @brief Target clipboard for OSC52 (c=CLIPBOARD, p=PRIMARY, s=SECONDARY, 0=ALL)
249+
*/
250+
char _osc52Target = 'c';
251+
252+
/**
253+
* @brief Flag indicating if current OSC52 chunk is the first chunk
254+
*/
255+
bool _osc52IsFirstChunk = false;
256+
// ==============================================
197257
};
198258

199259
}

3rdparty/terminalwidget/lib/qtermwidget.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,10 @@ void QTermWidget::init(int startnow)
514514

515515
//将终端活动状态传给SessionManager单例
516516
connect(this, SIGNAL(isTermIdle(bool)), SessionManager::instance(), SIGNAL(sessionIdle(bool)));
517+
518+
// Connect OSC52 signal from emulation
519+
connect(m_impl->m_session, SIGNAL(osc52ClipboardRequest(char,QString)),
520+
this, SIGNAL(osc52ClipboardRequest(char,QString)));
517521
}
518522

519523
QTermWidget::~QTermWidget()

3rdparty/terminalwidget/lib/qtermwidget.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@ class TERMINALWIDGET_EXPORT QTermWidget : public QWidget
346346
// 标签标题参数改变 dzw 2020-12-2
347347
void titleArgsChange(QString key, QString value);
348348

349+
/**
350+
* @brief Forward OSC52 clipboard request from Emulation
351+
*/
352+
void osc52ClipboardRequest(char target, const QString &base64Data);
353+
349354
public slots:
350355
// Copy selection to clipboard
351356
void copyClipboard();

src/assets/other/default-config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,12 @@
336336
"text": "Copy on select",
337337
"default": true
338338
},
339+
{
340+
"key": "allow_osc52",
341+
"type": "checkbox",
342+
"text": "Allow OSC52 clipboard access",
343+
"default": true
344+
},
339345
{
340346
"key": "set_cursor_position",
341347
"type": "checkbox",

src/settings/settings.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,11 @@ QString Settings::debuginfodUrls()
729729
return settings->option("advanced.debuginfod.debuginfod_urls")->value().toString();
730730
}
731731

732+
bool Settings::allowOSC52() const
733+
{
734+
return settings->option("advanced.cursor.allow_osc52")->value().toBool();
735+
}
736+
732737
/******** Add by ut001000 renfeixiang 2020-06-15:增加 每次显示设置界面时,更新设置的等宽字体 End***************/
733738

734739
/*******************************************************************************

src/settings/settings.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ class Settings : public QObject
216216
bool enableDebuginfod();
217217
// deepin-terminal设置的DEBUGINFOD_URLS环境变量值
218218
QString debuginfodUrls();
219+
/**
220+
* @brief 获取当前配置粘贴是否为选择内容
221+
* @author dzw1995
222+
* @return
223+
*/
224+
bool allowOSC52() const;
219225

220226
/**
221227
* @brief 历史记录行数

0 commit comments

Comments
 (0)