Konsole 里 Session Profile 命令的解析过程

Grissiom | 2011/09/01

如果没有强大的动力,我基本是不会主动去读 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()-&gt;emulation()-&gt;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-&gt;toUnicode(text,length);

    //send characters to terminal emulator
    for (int i=0;i&lt;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&lt;length;i++)
    {
        if (text[i] == '30')
        {
            if ((length-i-1 &gt; 3) &amp;&amp; (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 &lt; tokenBufferPos-i-2; j++)
    newValue[j] = tokenBuffer[i+1+j];

  _pendingTitleUpdates[attributeToChange] = newValue;
  _titleUpdateTimer-&gt;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::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&lt;Session*&gt;(sender());
    Q_ASSERT( session );

    ProfileCommandParser parser;
    QHash&lt;Profile::Property,QVariant&gt; changes = parser.parse(text);

    Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));

    QHashIterator&lt;Profile::Property,QVariant&gt; iter(changes);
    while ( iter.hasNext() )
    {
        iter.next();
        newProfile-&gt;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 +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&lt;Session*&gt;(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

Tags: ,

10 FEEDBACKS

  1. 建议,将patch扔到 reviewboard上面或者bugs.kde.org 上面,如果你觉得fix ok的话……

  2. 这么快就审核通过了?…… 不会是我自己手抖点了发布了吧,嘿……

    嗯,我会往 konsole-dev 的 mail list 里也发的~

    对了,貌似现在 Konsole 的活跃开发者是个叫 Jekyll Wu 的人,很可能是中国人啊……

  3. 心之所在
  4. 心之所在

    今晚搞个zanshin todo软件又被akonadi给折磨,鬼晓得什么资源是什么资源

  5. @心之所在 ……你也玩那个了啊。老实说本来今天早上看见想写个,结果完全不会用……我虽然创建了几个东西……可没觉得有啥用……

  6. 心之所在

    @csslayer 4.6时代用arch遗留了几个akonadi的配置,为了用这个zanshin又特意装了akonadi,日历资源选择有老邮箱的google日历,添加是能添加东西,但是不知道有没同步到网页上的日历。然后删掉,想新建个新邮箱的google日历玩玩,然后,然后发现压根没添加google日历的选项,装了google-akonadidata点遍了选项还是没(我就纳闷,那刚才老邮箱的google日历资源怎么有的),后台akonadi依赖的mysqld那跑的相当奔放,怒气值飙升,懒得在上面浪费时间,连带akonadi mysql sorpxxx全干掉了。

  7. @心之所在 话说这个东西好像不能选google的日历,我资源里面有一个google的日历,但是选中的话没有用。我自己建立一个基于ical的倒是可以用。可能是google日历不支持任务什么的……估计这个日历资源也有一些特性支持什么的,比如支持事件或者不支持事件这种。

  8. adaptee

    感谢提供patch,欢迎更多的patch!

    其实我也是最近才加入konsole的开发,边熟悉代码边改bug,显得活跃是因为整体不活跃。。。

  9. 033 就是 ASCII 的 ESC,a 是 x7…… 这一般人大概不容易意识到……

Leave a Reply

Your email address will not be published. Required fields are marked *

Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).