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

Tags: ,

10 FEEDBACKS

Leave a Reply

Your email address will not be published.

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.