如果没有强大的动力,我基本是不会主动去读 C++ 的源代码的。事情的起因其实很简单, 就是 Jesse 在 #beihang-osc@freenode.net上吼,想在 gnome-terminal 里不用 gconf 实现 http://vim.wikia.com/wiki/Change_cursor_shape_in_different_modes 里的效果。他想 看看 Konsole 是怎么实现的。作为伪 KDE 控的我当然不能放过这个机会啦……
第一步是从 anongit.kde.org 上 clone konsole 的源代码。无他,主要是为了打 patch 方便和那个无比好用的 git grep ;P 写这篇文章的时候 git HEAD 是 d6ca64fe4d。
然后这逛那逛的也一直没有头绪。忽然想,那个指令不是 "\<Esc>]50;CursorShape=1\x7" 和 "\<Esc>]50;CursorShape=0\x7" 么,干脆 git grep -n ']50' 试试。嘿,还真找到了:
src/Part.cpp:276: buffer.append("33]50;").append(text.toUtf8()).append('\a');
src/konsoleprofile:3:/bin/echo -e "33]50;$1\a"
lines 1-2/2 (END)
火速去 src/Part.cpp 第 276 行看:
void Part::changeSessionSettings(const QString& text)
{
// send a profile change command, the escape code format
// is the same as the normal X-Term commands used to change the window title or icon,
// but with a magic value of '50' for the parameter which specifies what to change
Q_ASSERT( activeSession() );
QByteArray buffer;
buffer.append("33]50;").append(text.toUtf8()).append('\a');
activeSession()->emulation()->receiveData(buffer.constData(),buffer.length());
}
注释说的挺清楚了,而且 033 就是 ASCII 的 ESC,a 是 x7。一切都对应上了。最后它 把指令送给了 activeSession()->emulation()->receiveData 。省略中间痛苦的寻找 过程直接说了,konsole 里每一个窗口/Tab都会有一个 Session,activeSession() 获取的是当前用户正在使用的 Session。每个 Session 里都会有一个 emulation 来模拟 terminal,处理用户的输入输出(src/Session.h 的 136 行)。 Session->emulation() 获取的就是它。具体的东西是 src/Session.cpp 129 行的_emulation = new Vt102Emulation(); 嗯,跳去 src/Vt102Emulation.cpp 找 receiveData ,木有找 到…… .h 里也没有…… 想起来有可能在鸡肋里, Vt102Emulation 是继承 Emulation 的,于是再跳到 src/Emulation.cpp 看 receiveData
/*
We are doing code conversion from locale to unicode first.
TODO: Character composition from the old code. See #96536
*/
void Emulation::receiveData(const char* text, int length)
{
emit stateSet(NOTIFYACTIVITY);
bufferedUpdate();
QString unicodeText = _decoder->toUnicode(text,length);
//send characters to terminal emulator
for (int i=0;i<unicodeText.length();i++)
receiveChar(unicodeText[i].unicode());
//look for z-modem indicator
//-- someone who understands more about z-modems that I do may be able to move
//this check into the above for loop?
for (int i=0;i<length;i++)
{
if (text[i] == '30')
{
if ((length-i-1 > 3) && (strncmp(text+i+1, "B00", 3) == 0))
emit zmodemDetected();
}
}
}
略掉编码转换和 z-modem 的过程,这个 receiveData 就是把东西送给了 receiveChar 。再找 receiveChar
// process application unicode input to terminal
// this is a trivial scanner
void Emulation::receiveChar(int c)
{
c &= 0xff;
switch (c)
{
case '\b' : _currentScreen->backspace(); break;
case '\t' : _currentScreen->tab(); break;
case '\n' : _currentScreen->newLine(); break;
case '\r' : _currentScreen->toStartOfLine(); break;
case 0x07 : emit stateSet(NOTIFYBELL); break;
default : _currentScreen->displayCharacter(c); break;
};
}
不会这么简单吧!又忽然想起来, Emulation::receiveData 可能会调用 Vt102Emulation::receiveChar 的吧…… 在 src/Emulation.h 的 427 行,这货果然是 virtual 的。于是再返回 src/Vt102Emulation.cpp 找 receiveChar 。这回终于 找到处理字符序列的地方了。不过因为整个函数有101行,还要加上前面18行注释和15行宏 ,就不贴在这里了。那个函数主要是把用户输入 tokenize ,并且对 token 进行处理。这 整个过程我还不是完全理解,但是大概的内容可以猜的出来。对于本文起作用的主要是第 316 行
if (Xte ) { processWindowAttributeChange(); resetTokenizer(); return; }
Xte 是个判断 33] 的宏。(好吧,它其实只判断了 token 的位置和 ']') processWindowAttributeChange 就在receiveChar 的下面
void Vt102Emulation::processWindowAttributeChange()
{
// Describes the window or terminal session attribute to change
// See Session::UserTitleChange for possible values
int attributeToChange = 0;
int i;
for (i = 2; i < tokenBufferPos &&
tokenBuffer[i] >= '0' &&
tokenBuffer[i] <= '9'; i++)
{
attributeToChange = 10 * attributeToChange + (tokenBuffer[i]-'0');
}
if (tokenBuffer[i] != ';')
{
reportDecodingError();
return;
}
QString newValue;
newValue.reserve(tokenBufferPos-i-2);
for (int j = 0; j < tokenBufferPos-i-2; j++)
newValue[j] = tokenBuffer[i+1+j];
_pendingTitleUpdates[attributeToChange] = newValue;
_titleUpdateTimer->start(20);
}
前半部分是提取 33 和 ';' 中间的数字,然后把剩下的字串放到 _pendingTitleUpdates 里给别人处理。这里作者启动了一个 20ms 的计时器,计时器到时 间之后才会更新。这可以压缩更新的次数,避免频繁更新吧。计时器的 callback 就在下 面
void Vt102Emulation::updateTitle()
{
QListIterator iter( _pendingTitleUpdates.keys() );
while (iter.hasNext()) {
int arg = iter.next();
emit titleChanged( arg , _pendingTitleUpdates[arg] );
}
_pendingTitleUpdates.clear();
}
简单的函数,它又发出了 titleChanged 这个信号。这个信号是在哪处理的呢?(中间 省略N多 git grep 之类的过程)是在 src/Session.cpp 的 Session::setUserTitle
void Session::setUserTitle( int what, const QString &caption )
{
....
if (what == ProfileChange)
{
emit profileChangeCommandReceived(caption);
return;
}
....
}
这个 ProfileChange 就等于我们所要的 50(src/Session.h, 341 行) ……再追踪 profileChangeCommandReceived 这个信号。(别急,快完啦)处理它的是 src/SessionManager.cpp 里的 SessionManager::sessionProfileCommandReceived
void SessionManager::sessionProfileCommandReceived(const QString& text)
{
// FIXME: This is inefficient, it creates a new profile instance for
// each set of changes applied. Instead a new profile should be created
// only the first time changes are applied to a session
Session* session = qobject_cast<Session*>(sender());
Q_ASSERT( session );
ProfileCommandParser parser;
QHash<Profile::Property,QVariant> changes = parser.parse(text);
Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));
QHashIterator<Profile::Property,QVariant> iter(changes);
while ( iter.hasNext() )
{
iter.next();
newProfile->setProperty(iter.key(),iter.value());
}
_sessionProfiles[session] = newProfile;
applyProfile(newProfile,true);
emit sessionUpdated(session);
}
还是一个挂着 FIXME 的函数呢…… 不过逻辑还是比较简单的,基本上就是把当前的Profile 作为父 Profile 新建一个 Profile。然后根据命令的内容修改 Profile 的属性。也就是说 ,理论上讲,只要是 Profile 里可以改的,就可以通过 \<ESC>50;x1=y1;x2=y2\x7 来 修改。后来我又把 vim 里的 t_{S,E}I 修改成
if $TERM =~ 'xterm'
let &t_SI = "\<Esc>]50;CursorShape=1;BlinkingCursorEnabled=true\x7"
let &t_EI = "\<Esc>]50;CursorShape=0;BlinkingCursorEnabled=false\x7"
endif
然后在插入模式下,光标果然编程一闪一闪的竖线了。哈哈。不过需要小注意的是,用这个 方式修改的 Profile 是临时的,不会保存,新建的标签也不会继承这个 Profile。
现在就拨云见日,回顾一下整个调用过程吧
Emulation::receiveData
||
\/
Vt102Emulation::receiveChar
|| tokenize/process token
\/
Vt102Emulation::processWindowAttributeChange
|| 提取 \<ESC>] 后面的 code 和 cmd
\/ 20ms 延迟,聚集变更
Vt102Emulation::updateTitle
|| emit titleChanged(code, cmd)
\/
Session::setUserTitle(int, const QString &)
|| emit profileChangeCommandReceived(cmd)
\/
SessionManager::sessionProfileCommandReceived(const QString)
{
...
Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));
...
newProfile->setProperty
...
_sessionProfiles[session] = newProfile;
applyProfile(newProfile,true);
emit sessionUpdated(session);
}
回头来看,一步一步的到还挺清晰的。
多谢各位能够读到最后。作为奖励,贴一个解决上面 FIXME 的补丁吧,哈哈
commit 5fb452e51ac1a9d18952fdd26f8bfa55438aedf3
Author: Grissiom <chaos.proton@gmail.com>
Date: Thu Sep 1 01:17:24 2011 +0800
use a static _sessionRuntimeProfiles to store runtime profiles
diff --git a/src/SessionManager.cpp b/src/SessionManager.cpp
index 028b76f..697589c 100644
--- a/src/SessionManager.cpp
+++ b/src/SessionManager.cpp
@@ -758,9 +758,7 @@ Profile::Ptr SessionManager::findByShortcut(const QKeySequence& shortcut)
void SessionManager::sessionProfileCommandReceived(const QString& text)
{
- // FIXME: This is inefficient, it creates a new profile instance for
- // each set of changes applied. Instead a new profile should be created
- // only the first time changes are applied to a session
+ static QHash<Session*,Profile::Ptr> _sessionRuntimeProfiles;
Session* session = qobject_cast<Session*>(sender());
Q_ASSERT( session );
@@ -768,14 +766,23 @@ void SessionManager::sessionProfileCommandReceived(const QString& text)
ProfileCommandParser parser;
QHash<Profile::Property,QVariant> changes = parser.parse(text);
- Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));
-
+ Profile::Ptr newProfile;
+ if (!_sessionRuntimeProfiles.contains(session))
+ {
+ newProfile = new Profile(_sessionProfiles[session]);
+ _sessionRuntimeProfiles.insert(session,newProfile);
+ }
+ else
+ {
+ newProfile = _sessionRuntimeProfiles[session];
+ }
+
QHashIterator<Profile::Property,QVariant> iter(changes);
while ( iter.hasNext() )
{
iter.next();
newProfile->setProperty(iter.key(),iter.value());
- }
+ }
_sessionProfiles[session] = newProfile;
applyProfile(newProfile,true);
C++ 代码看的比较少,Konsole 的代码也是刚看。有什么不对的地方还请指教~;P
10 FEEDBACKS