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).

This site uses Akismet to reduce spam. Learn how your comment data is processed.