如果没有强大的动力,我基本是不会主动去读 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()); }</pre>
注释说的挺清楚了,而且 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(); } } }</pre>
略掉编码转换和 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); }</pre>
前半部分是提取 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::setUserTitlevoid 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::sessionProfileCommandReceivedvoid 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 sessionSession* 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); }</pre>
还是一个挂着 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 +0800use 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
建议,将patch扔到 reviewboard上面或者bugs.kde.org 上面,如果你觉得fix ok的话……
这么快就审核通过了?…… 不会是我自己手抖点了发布了吧,嘿……
嗯,我会往 konsole-dev 的 mail list 里也发的~
对了,貌似现在 Konsole 的活跃开发者是个叫 Jekyll Wu 的人,很可能是中国人啊……
@Grissiom 搞不好是他 http://www.ikde.org/app/kdepim-calendar/
@Grissiom ……你点发布也发不了的。 @心之所在 不用搞不好……就是。 http://osdir.com/ml/konsole-devel/2011-08/msg00339.html
今晚搞个zanshin todo软件又被akonadi给折磨,鬼晓得什么资源是什么资源
@心之所在 ……你也玩那个了啊。老实说本来今天早上看见想写个,结果完全不会用……我虽然创建了几个东西……可没觉得有啥用……
@csslayer 4.6时代用arch遗留了几个akonadi的配置,为了用这个zanshin又特意装了akonadi,日历资源选择有老邮箱的google日历,添加是能添加东西,但是不知道有没同步到网页上的日历。然后删掉,想新建个新邮箱的google日历玩玩,然后,然后发现压根没添加google日历的选项,装了google-akonadidata点遍了选项还是没(我就纳闷,那刚才老邮箱的google日历资源怎么有的),后台akonadi依赖的mysqld那跑的相当奔放,怒气值飙升,懒得在上面浪费时间,连带akonadi mysql sorpxxx全干掉了。
@心之所在 话说这个东西好像不能选google的日历,我资源里面有一个google的日历,但是选中的话没有用。我自己建立一个基于ical的倒是可以用。可能是google日历不支持任务什么的……估计这个日历资源也有一些特性支持什么的,比如支持事件或者不支持事件这种。
感谢提供patch,欢迎更多的patch!
其实我也是最近才加入konsole的开发,边熟悉代码边改bug,显得活跃是因为整体不活跃。。。
033 就是 ASCII 的 ESC,a 是 x7…… 这一般人大概不容易意识到……