如果没有强大的动力,我基本是不会主动去读 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