Sometimes I Wish I’m a Dog

A dog that eats well and sleeps well, of course.

And with a good owner hopefully, who’ll walk me around on sunny weekends and buy me expensive toys.

And don’t forget nice fur and strong muscles - these are a must for successful relationships.

Oh, and don’t ever envy humans - they are smart, interesting and can do whatever they want, but, there surely is no reason to want to become a human.

…or is there?

It’s unfortunate that every time I pay such thoughts a visit it ends up in a circle, where the thing I wish to become would somehow take a shape that wishes back to become what I am right now.

It seems that the frustration is not in trying to be something, but really in trying to be more than something.

That means a good dog should never try to be more than a dog.

And a good human should never wish to be a dog.

Aug 15th, 2015
en, rant

Coming Back

Haven’t updated my website for ages.

A lot has happened - graduated from university, got a job, found a home in one of the busiest cities in the world, laptop broken, laptop fixed, laptop broken again, laptop fixed again.

Fortunately I’m having more and more time to do things I like - by that I mean developing programs, as a developer - and paid for that as well.

The hustle-bustle meant most of my projects, just-starting or on-going, had to be put on shelf. Now over two months later I can barely recognize any of them under the thick dust that’s been covering up.

Well, nothing is too late - or at least I hope so. I’ve been itching to give my newborn freedom a try and any of these projects seems to be a perfect starting point. So stay tuned and expect great things.

Oct 31st, 2014
en, life

VIMonad- Transforming XMonad to Vim With Motion Keys, Register, Visual Mode, Macros, …

I’ve finally got the time to clean up my XMonad configurations, split them into modules, and formally made a fork at VIMonad.

You can find all the description and documentation on its project page.

Jun 11th, 2014

现代家居系统

今天心血来潮,自己yy了一套现代家居系统。

系统的第一步是把整个house做成一台电脑,所谓的live inside a computer的感觉。要实现这个相当简单(?),把家里所有的墙都改成OLED的可弯曲屏幕(并且支持触控),连接到家里的一台central server。Plug的插口上不仅有用来充电的插座,还会有usb等端口- 这样的话比如需要查看usbstick里面的图片直接把它插到墙上,墙上就会显示出内容。触控的需要也相当明显-家里的任何一个角落都可以对这台“电脑”进行控制-比如直接触摸墙壁就会显示出常用菜单。这个concept不仅仅可以用在墙面上,茶几台上也可以,餐桌上也可以,以此类推。

有了全方位的OLED屏幕可以说照明系统也就不需要了-直接通过墙壁来照明。并且由于屏幕连接到server,所以这套照明系统完全可以智能化。server通过家里安装的camera或其他sensor收集用户及周围环境的情报,然后根据需要智能调节屏幕的明暗,颜色等等。这些就不赘述了。

接下去是比较有趣的地方。正比如大多数电脑都需要一个filesystem,家这台“电脑”也需要,只有有了一套完备的filesystem才可以完善地储备家里的万千事物。正常人家里的东西都放在一个一个的抽屉,柜子,橱门里,但不可避免的就是一个容易凌乱而又整理困难的结果。而我今天思考的结果就是一个自动而又高效的系统,我们暂且把这个叫做“保险柜列阵系统”。首先每个房间都会有任意个数的“柜子”,这些柜子看起来跟平常的柜子没什么不同,除了就是顶部有一个LED显示这个柜子的名字。但他们的实际作用相当于一个个传送点-柜子内部是可替换的,直接通到地下的一个保险柜列阵,通过传送带可以随时替换当前使用的那个保险柜。而家庭成员可以根据当前的需要在手机上告诉server需要哪一个柜子,然后系统会直接把它传送到最近的传送点。平常待机状态下每个传送点所使用的保险柜就是server通过使用数据估测出的在当前位置最有可能被用到的保险柜。这个系统的优势首先是你不再需要去记住什么东西在家里的什么位置了-所有东西都在分好类的保险柜里面,并且根据需要可以在任意传送点access这些保险柜。事实上这里提到的每个保险柜就相当于电脑上的folder的概念,而传送点就是一个搜索窗口。如果愿意的话还可以加入更加高级的功能-比如不仅仅是搜索保险柜的名字,系统也可以安装一部字典然后根据单词的相近程度推荐最有可能的保险柜:比如我输入screwdriver它就列出名为toolbox的保险柜。当然保险柜也不一定是真正的保险柜-比如你可以把有些保险柜做成保鲜柜或是冰柜,然后取名beers或者ice creams,这样你在客厅不用走路就可以让系统直接把想要吃的东西传送到你面前(这种意义上冰箱就不再需要了)。

Mar 31st, 2014
cn

More Prompt Stuff: Reverse Completion, Colored Prompt, Dynamic Prompt/preview Widget

It’s been a long time since I last wrote something on XMonad. It’s not that I don’t have anything to share in the last couple of weeks, but rather, I felt it a little bit troublesome to explain some of my most recent tweaks. But enough with my laziness, I finally decided to spend some quality time to write on these nifty new things.

Reverse completion

I’ve always wondered why the default prompt does not support Shift+Tab to move the focus up the completion menu. So I basically added another branch in the main loop of the prompt system such that whenever a Shift+Tab is pressed it passes a reversed list of the completion items to the completionFunction - achieving what I want in most cases.

Colored prompt

Ever thought about having a colorful prompt system within XMonad? Now it is possible. I first got this idea when I was working on the taskwarrior prompt system - the taskwarrior program supports outputting ANSI colors, but apparently the stock XMonad.Prompt wouldn’t do anything for that extra information.

The functions behind these color renderings aren’t terribly complicated - it just looks for terminal color sequences and transform them into hex color codes that can be printed easily using standard X library.

However, here’s the catch: if you are going with the color, make sure you know something about color encoding and have the time and effort to tweak it to make it look nice. Unfortunately I have neither of those and that’s why I ended up not using any color after all.

More on Dynamic Prompt

The concept of dynamic prompt was first introduced in one of my earlier post.

One major gripe I’ve always had regarding standard unix tools is that it’s not that straightforward to perform certain tasks chained together in a visual and direct way.

For example, say you want to remove a file. You know it’s located in a folder with Prompt in its name. You also know that the file has the keyword shell inside.

What you’d normally do is probably like this:

Step 1

1
2
3
4
5
$ pwd
/home/lingnan/DB/indie/XMonad
$ find * -name '*Prompt*'
XMonadContrib/dist/build/XMonad/Prompt
XMonadContrib/XMonad/Prompt

Now you remember that the folder you are looking for is XMonadContrib/XMonad/Prompt.

Step 2

1
2
3
$ grep -R 'shell' XMonadContrib/XMonad/Prompt
XMonadContrib/XMonad/Prompt/Shell.hs:A shell prompt for XMonad
XMonadContrib/XMonad/Prompt/Shell.hs:    , shellPrompt

Now you’ve found the file! It’s XMonadContrib/XMonad/Prompt/Shell.hs.

Step 3

1
$ rm XMonadContrib/XMonad/Prompt/Shell.hs

It’s not terribly complicated, but it’s certainly nowhere near convenient.

That’s where my dynamic prompt widget comes in. Basically each of these widgets defines a keyword and as long as one such keyword is detected on the prompt line, anything after that keyword is passed to the relevant widget, which will then display the appropriate autocompletions for the user to complete against.

Search Widget

Currently there are 8 search widgets

  • f: search for recent files using fasd
  • a: search for recent files or directories using fasd
  • d: search for recent directories using fasd
  • z: search for recent directoties using fasd; on completion substitute the prompt line command such that it’s suitable for changing directory from the prompt
  • l (locate): search for files recursively in a given directory (or the current one, if not specified) using find
  • t (tag): search for a directory in my tag database using find
  • g (grep): list all files containing the given words
  • gp (pdfgrep): list all pdf files containing the given words

So for the same problem we discussed before, it can be done with my dynamic prompt system in the following way:

Step 1

Invoke the prompt. Since you want to remove a file, just type in rm.

Step 2

Now you realize that you don’t know the exact location of the file. You remember that the file contains the word shell. That means we should use g widget to grep for shell. So you type in another g. The prompt line is now rm g.

Step 3

You want to narrow down to a directory having Prompt in its name instead of greping blindly in the current directory which might contain 1000 files. That means we need to use l widget to locate that directory and pass it back to the g widget as its argument. So type in l Prompt. Now you’ll get some screen like this:

Now press tab to autocomplete. The completion algorithm is smart enough to remove the preceding widget keyword automatically - l in this case.

Step 4

Now you’ve got the directory to grep in. Finish typing by adding the word to grep against.

Like before, tab through to the right file to make the final prompt line look like this:

Step 5

Press Return to execute the command.

After this lengthy explanation, you might jump up and say: gosh, that’s complicated! The fact that there seem to be more steps with my system than with the standard unix tools is because I’m trying to explain all the details as clearly as possible. In practice my system is designed to be as close to your thinking process as possible - type the action you want to achieve directly, and when you want to get hold to some file as the argument to the action you are trying to complete, just use one of the widgets to help you out.

Preview widget

If you are familiar with ranger, you’d know that it uses a preview script to render information about the file you are working on. The same works for my dyanmic prompt - if you’ve given a file argument on the prompt, press another Space would trigger the previewing of that file argument.

previewing a source file

previewing a pdf file

previewing a zip file

Other widgets

top

git

Feb 24th, 2014

The Tree

最近眼前总是浮现出一棵树。

树是普通的行道树,算不上枝繁叶茂,也没有纵横交错的蟠根。只是一棵年轻的生命,无比宁静地伫立在一片与世隔绝的白色光晕中。

也许某天一觉醒来它就在自己面前。我会轻轻地依靠在它瘦弱的树干上,声情并茂地向它诉说我这短暂的一生;或者侧过脑袋,听它喃喃低语这个世界我所不知道的秘密。

也或许某天我真的在现实生活中找到了它,像个孩子一样欢快地向它飞奔而去,来到跟前却发现只是个梦。

无论如何,还是尽量sketch出了脑海中它的模样。

Feb 9th, 2014

Supercharge Your XMonad! Colored Tabs, Dynamic Prompt, Window/Workspace Insert Position

Here’s another post to show some of my latest hacks on XMonad.

Colored Tabs

One of the things that annoyed me for the Tabbed layout is that I couldn’t make out which program is in which tab from the look of the tabs alone easily. Having a different theme for different task groups thus becomes a natural choice for me.

To apply this mod the key is to modify the updateDeco method in XMonad.Layout.Decoration.

A little thanks goes to OODavo from #xmonad.

Dynamic Prompt

The stock XMonad.Prompt.Shell is fine but I can’t think of any other things it can do aside from launching programs.

My dynamic prompt, in contrast, serves to

  • launch apps with shell completion (of course)
  • render output directly in the autocompletion window for some commands e.g. man head
  • opens the file or directory on prompt directly using rifle

Now I don’t have to launch a ranger instance in each and every workspace anymore.

Window/Workspace Insert Position

This one is not totally necessary (I think), but I consider it useful at times.

Window Insert Position

Using XMonad.Hooks.InsertPosition you can easily decide whether the focus should stay with the old window or transfer to the new window when it is created; the module also claims to support changing the insert position of the new window but I haven’t been able to make that work (probably it’s a compatibility problem with my complicated custom layout). I’ve further extended this module to allow dynamic toggling of the focus-stay-with-old-or-new-window feature (let’s call it window insert position feature) via a keyboard shortcut.

Why would this be useful? Imagine you are using a browser application like vimb; it does not manage windows itself so new web pages opened by it will always take the focus in XMonad. Now with this mod you can decide that behavior. In fact I’ve defined g;t in my vimb (the shortcut responsible for entering the hint mode and continue opening the activated links until the mode is quit via Esc) such that it automatically opens the tab in the background and when it finishes recovers the old window insert position configuration.

Workspace Insert Position

Continuing from my Dynamic Workspace, the following keybinding are added:

  • M-a {a,M-a}: launches the workspace creation prompt and on completion insert the workspace just after the current one
  • M-a {i,M-i}: same as before but on completion insert the workspace just before the current one
  • M-a {I,M-I}: you’ve guessed it; insert at the beginning
  • M-a {A,M-A}: insert at the end

There is another set of key bindings beginning with M-S-a which creates the new workspace in the same way but also at the same time move the current window to it.

Jan 6th, 2014

Change Cursor Shape for Zsh Vi-mode

I’ve been using zsh and its wonderful vi-mode line editing keybindings for a long time, but one thing that has always troubled me is the lack of visual clues to the current mode.

With oh-my-zsh you can optionally have an indicator to the right of the prompt telling you this, but I feel it’s not native enough - we need something simpler and more direct, just as in vim I’ve set my cursor to be an underscore in the insert mode and a solid block in the command mode.

Luckily you can do just that with the powerful zsh shell.

First off the relevant escape sequences for changing the cursor shape are:

  • "\e[4 q": solid underscore
  • "\e[2 q": solid block

The hook called when your vi mode changes is zle-keymap-select. If you want comprehensiveness, also include zle-line-init and zle-line-finish.

Now just append this to the end of your zshrc.

1
2
3
4
5
6
7
8
9
10
11
zle-keymap-select () {
    if [ "$TERM" = "xterm-256color" ]; then
        if [ $KEYMAP = vicmd ]; then
            # the command mode for vi
            echo -ne "\e[2 q"
        else
            # the insert mode for vi
            echo -ne "\e[4 q"
        fi
    fi
}

Boom, you’ve just configured your cursor to change shape on the fly according to your editing mode!

Jan 5th, 2014
en, zsh

More XMonad Goodies: 3-dimensional Workspace, Window Sorting and Shelving

Following up my previous entry on some advanced vim-like feature addition to XMonad, I’ve played around with my configuration a bit more and apparently made it more than 2000 lines, marking a new milestone where my config file has even more lines than the core xmonad source.

Well, that can be a good or bad thing, depending on how you look at it.

But the good news is that most of the new additions I consider quite useful, and therefore would take some time here to share them with those interested.

3-dimensional Workspace

Inception

If you’ve followed my previous blog on XMonad then hopefully you’ve grasped my idea at building a vim-splitter-like layout for workspaces (if you’ve forgotten by now, here’s the refreshment).

After I wrote that article, I’ve taken some more serious moments at pondering about this and have gotten a few more insights:

  1. For the inner-most layout, Tabbed is still by far the most efficient available. The functional equivalent in vim is a split view. The rationale here is that you want to divide the whole screen into some non-overlapping rectangles, but at the same time you want to allow some windows to be viewed from the same rectangle. For example, imagine you have two rectangles - one for research, and the other for taking notes - you might want to open multiple browser windows in the research rectangle and switch between them while keeping the same notes rectangle on the other side.
  2. What’s the best layout for managing these rectangles, you ask? For me, I used the stock Tall layout for a long, long time but I had to admit it’s not very efficient. For one thing, although you have all the rectangles tiled, their position and width are not really controllable - the only thing you can do is shrinking and expanding the master view. On the other hand, the rectangles are essentially tiled in a one-dimensional manner - you navigate through these rectangles using up’s and down’s and at each point of time you only have access to this one axis.
  3. Again we turn to vim for inspiration - in vim, you can split each view, or rectangle, vertically or horizontally. You can navigate between these views vertically or horizontally. You can also shrink or expand each view vertically or horizontally. More importantly, when you perform these actions, other views aren’t affected in major ways, and that’s a really nice advantage which many people might overlook - a serious drawback of dynamic tiling (which XMonad is based on) is that each new window can disrupt the positions and sizes of existing windows, and that can be very frustrating for the person sitting before the screen trying to remember all of them - YOU.

In light of these observation and thoughts, an obvious conclusion is that we should use a layout that allows the same sort of power, as well as the flexibility, afforded by the similar in vim. The problem is that there isn’t such a layout available. If you think carefully about this, you’d realize that this wouldn’t be an easy one-time pull-off even for those Haskell gurus.

Problem-solving

What should we do then?

Luckily, during my mulling over this, I turned to XMonad.Layout.Groups and XMonad.Layout.Groups.Examples again. The Rows of columns, or as it is called in the examples, seems to be the exact answer to my need. Essentially a combination of ZoomRows, it allows one to divide a workspace into multiple columns, which each then holds several rows. The size of each rectangle within can be adjusted in both dimensions. However, by using this layout we have to forgo Tabbed for each rectangle - the Rows of columns is already taking advantage of the Groups combinator and we have no way to insert another level of group nesting below it.

Or do we?

As I played around with the Groups source code, it turns out that further nesting of groups is indeed possible, with only minor tweaks and limitations. Simply open the source file for XMonad.Layout.Groups and add this function

1
2
3
group3 l l2 l3 = Groups g l3 start (U 2 0)
    where g = group l l2
          start = fromJust $ singletonZ $ G (ID (U 2 1) g) emptyZ

Then in your xmonad.hs you can use a 3-dimensional layout like this

1
rectTabs = G.group3 (addTabs shrinkText myTabsTheme Simplest) (Mirror (zoomRowWith GroupEQ) ||| Full) (zoomRowWith GroupEQ ||| Full)

An additional benefit with using a combinator like Groups is that you can switch between different layouts for each level of nesting, as you can see from my code above, which allows me to switch between rows/columns and Full layout.

In more practical terms, this allows us to

  • group windows into work contexts at the workspace level e.g. xmonad, school project, etc.
  • group windows in each work context into functional sub-groups in the form of columns e.g. inside xmonad, you can have a column devoted to research (many browser and document windows) and another one devoted to code (vim windows)

    Note how similar this concept is to the taskGroup concept that I spent a lot of words explaining in my previous blog. Later in this blog you’ll see how I combine these two concepts together and make window sorting possible.

    Also, just a slight digression: I saw a lot of tutorials or videos online teaching people how to put all browser windows into one workspace and all text-editors into another - in my opinion such approaches miss the entire point of window tiling: if you can’t compare an editor and a browser window side by side then there’s no difference from just using tabbed editor and tabbed browser in a normal window manager - if you don’t need to compare two windows, why bother tiling them in the first place? In that case a tabbed layout would be the most efficient one that makes full use of all screen estate.

    My previous setup - that is, Tabbed groups embedded in a Tall layout - does not allow much functional separation among windows, assuming I’m using a topical assortment of workspaces. This problem is now fully solved: the column dimension, as I explained at the beginning of this bullet point, serves to separate windows by functions. If I switch the layout at the column level to Full, then I essentially have research and code workspaces inside a bigger workspace called xmonad (if you’d like to visualize it in that way), and I can switch back to columns and compare these functional groups if I need to.

  • group windows in each functional group, or column, into rows (visually, it’s probably more straightforward to call them rectangles or cells) e.g. inside the column code, you can have a editor rectangle holding vim instances and another debugger rectangle positioned below it holding xterms
  • lastly for windows that do not need to be compared against each other, group them into tabs and fit them into each rectangle e.g. in the debugger rectangle you can have xterm 1, xterm 2, etc.

This is getting a little confusing, so we’d better hurry to the next section which explains how to output these complicated group nestings into human-readable forms.

Piecing it together

Here’s what my dzen log bar typically looks like

Some of the terms might require explanation:

  • columnLayout: the layout used by the outermost group, or the columns in a workspace
  • rectLayout: the layout used by the intermediate group (the one below columns but above the tabs)
  • auto sort: M means manual while A means auto; will be explained in a later section
  • history: just like the history indicator in vim; + and - mean there are newer and older windows to navigate to respectively; refer to window history if you are curious about how this is implemented
  • ref key: the highlighted letter is the filterKey of the respective taskGroup (refer to task group if you don’t understand), which reminds me what filter key to press to perform actions on that group

The main part relevant to our 3-dimensional layout is the part labelled columns and rows - each column is also referenced by a number (just like workspaces); and each row is contained in a pair of square brackets for clarity.

It took me quite a while to hack the original Groups layout to enable proper logging of the full window structure. Those interested can scroll down to the bottom and go to my github link directly to see the source.

Of course, a 3-dimensional layout would be less interesting if you don’t have a full set of operations to perform on it. Here I’ll just list a few operations that I normally use

  • M-{k,j,h,l}: navigate up/down/left/right to the rectangle on the corresponding direction
  • M-{p,n}: navigate to previous/next tab
  • M-S-{p,n}: swap the current tab with the previous/next tab
  • M-C-{h,l}: split a window out to the left/right to form a new column
  • M-C-{k,j}: split a window out to the up/down to form a new row/rectangle within the current column
  • M-S-{k,j,h,l}: move a window up/down/left/right to the rectangle on the corresponding direction
  • M1-<num>: go to the num‘th tab within the current rectangle
  • M1-S-<num>: swap the current tab with the num‘th tab
  • C-<num>: go to the num‘th column
  • C-S-<num>: move the current window to the num‘th column
  • M-S-q: close all tabs in the current rectangle
  • M-C-q: close all windows in the current column
  • M-C-S-{k,j}: swap the current rectangle up/down
  • M-C-S-{h,l}: swap the current column left/right
  • M-{-,+}: shrink/expand current rectangle vertically
  • M-{<,>}: shrink/expand current rectangle horizontally
  • M-{=,\}: reset the focus

There are many, many more. In fact, since there are three levels of nesting within a single workspace, there is such an abundance of operations to invent that I eventually ran out of keyboard shortcuts to assign.

Window Sorting

Like I mentioned in the last section, the new dimension - column - allows me to group windows by their functions inside each workspace; and since I’ve already defined many task groups in my config file, why not combine the two together? The results are what I call Window Sorting and Auto Window Sorting.

Note: you’d need to first understand task group before you can possibly understand what I’m doing here

Manual Window Sorting

I’ve assigned M-s for this useful function. As soon as I press down this key combination, my XMonad loops through all the windows inside the current workspace and put them into columns, according to my task group definition. For example, if I have two vimb windows, one vim window and another zathura window all jumbled together in a single rectangle inside a workspace, pressing M-s gives me back three columns - one holding the two vimb windows, another holding the vim, and the last one holding the zathura. My function also tries to be as smart as possible: it will keep the focus on the same window after sorting; and it also tries to maintain the position of the current rectangle in the final assortment.

On the implementation side, I don’t feel there’s too much magic to talk about. Sorting is just like filtering, which also makes use of Query Bools. If you are familiar with them then that’s good - you can do a hell lot of stuff with these predicate functions.

Automatic Window Sorting

This is more interesting as what I’m trying to achieve is essentially Manage Hook within workspace. Each workspace starts off in the manual mode, as indicated by the M in the sample image of my dzen bar. But if I press down M-S-s, automatic mode is turned on and the M changes to A. In the automatic mode, each new window is inserted into the column which has the highest percentage of windows of the same task group; if such column is not found, a new column is created before the current column for that new window to fit in. An example will better illustrate the use case:

  1. imagine you are in a workspace and you’ve spawned a vim window to write some code in
  2. now you want to search for something, so you press M-[ b - this triggers the cycling protocol of the vimb task group, and since there are no vimb windows to cycle about, the construction function of the group is invoked and a new vimb window is created
  3. since this is the first vimb window and there are no columns found in the workspace to contain a window with the matching task group, the vimb window is put into a new column and the focus automatically transfers to it
  4. all subsequent new vimb windows shall be inserted in that new column just created, given that you don’t do any manual arrangement

As you can see, this automates some common sorting tasks while at the same time preserves the manual arrangement made so far by the user. I found this useful especially for simple work contexts (workspaces) containing less than 10 windows.

Shelf

Although we can now manage windows with 3 dimensions, maximise a column or even maximise a rectangle, sometimes we still wish that a window can be stashed somewhere temporarily. In my previous post I talked about how we can use perworkspace scratchpad to achieve this aim, but what I realized later is that it still has some major limitation, the biggest of which is that all the scratchpads have to be pre-defined in the config file.

The alternative is to use XMonad.Layout.Minimize. This allows any window in a workspace to be toggled, but it also has problems:

  1. the user cannot see directly how many and which windows are currently minimized
  2. the user can only un-minimize windows one by one using the un-minimize shortcut

Fortunately they are both easy to solve:

  1. using hacks similar to what I’ve done to Groups for group structure displaying, we can show the minimized windows on our bar, just as in my sample image
  2. since I’ve already invented many ways to navigate to a particular window (by filterKey cycling, by history, by title prefix, etc.) we can easily retrieve a minimized window by one of these means

What’s more?

This is by no means the end of all possible things one can do with XMonad. In particular, I haven’t played around with UrgencyHooks and a couple of other seemingly useful modules. The more I customize XMonad, the more I begin to appreciate how powerful and flexible it is. For those interested in the customizations I’ve talked about in my blogs, I’ve managed to put all of them into a single repo. Here.

Dec 30th, 2013

Making Vim Play Nice With Your Window Manager

Motivation

For people who’ve used Vim extensively and exclusively, you’ve probably seen this dreaded message a dozen of times

For most cases this glaring warning is there just because you’ve forgotten having opened the same file in another Vim instance - and it happens for me a lot. Therefore for a long time I’ve been thinking of an extension to Vim that:

  1. whenever a file with an existing swap is asked to be opened, instead of showing the message, jump to that editing session of Vim and switch to the file in question
  2. when a file is asked to be opened check whether there is already some Vim instance lying around in the current workspace; if yes, then forward that request to the existing instance

Collecting the pieces

One can argue that such features should come shipped with any modern editor - but well, this is our good old Vim running in terminals, so we probably shouldn’t complain too much. But the good news is, I’ve finally come to a solution for this based on wmctrl (and I don’t know why I didn’t bump into this little gem before).

To start with, wmctrl is a little program that interacts with your window manager on the command line (yes that is possible). The most useful features include listing the workspaces, checking for window information, jumping to a specific window based on title or window ID.

The other piece of the puzzle is Vim’s own server-client feature, which I’ve somehow looked over in the past. It’s actually very simple:

  • To start a server-enabled Vim

      vim --servername <name> ARG1 ARG2 ARG3
    
  • To send command to a server-enabled Vim

      vim --servername <name> --remote FILE1 FILE2
    

    This will connect to the vim by the server name and make it edit the files given in the rest of the arguments.

  • To query information regarding the remote vim, you can use

      vim --servername <name> --remote-expr {expr}
    

    This will connect to the vim server, evalute {expr} in it and print the result on stdout.

Another interesting discovery of mine is that Vim actually includes a plugin called editexisting.vim for the default installation. This will be the script we build upon.

Taken from editexisting.vim:

  1. On startup, if we were invoked with one file name argument and the file is not modified then try to find another Vim instance that is editing this file. If there is one then bring it to the foreground and exit.
  2. When a file is edited and a swap file exists for it, try finding that other Vim and bring it to the foreground. Requires Vim 7, because it uses the SwapExists autocommand event.

Most of the script works fine, except the part which concerns itself with bringing (the remote Vim session) to the foreground and exit. From my testing it doesn’t work with XMonad (and I guess it wouldn’t work with other lightweight window managers as well under Linux). So our primary aim would be to fix this problem.

Solution

Since we’ve had wmctrl, what we need to do is really

  1. get the process id of the remote vim that is editing the same file
  2. get the process id of the window that actually holds that vim; this is achieved by repeatedly getting the parent pid and checking against the process ids given in wmctrl -lp
  3. from the table of wmctrl -lp, get the title for the window and use wmctrl -a TITLE to jump to the window

This is best shown in code

sh (wtitle) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /bin/sh

# this script aims to find the window id of a given process 
# it does this by repeatedly getting the (parent) process id until a match is found in the window list
output="`wmctrl -lp`"
pid="${1:-$$}"
while true; do
    #echo "pid is $pid"
    wid="`awk '$3=='"$pid"'{for(i=5; i<NF; i++){printf "%s ", $i}; print $NF}' <<< "$output"`"
    #echo "wid found is $wid"
    if [ -n "$wid" ]; then
        echo "$wid"
        break
    else
        pid="`ps -o ppid= -p $pid`"
        ! [ "$?" == 0 ] && exit 1
    fi
done

The above code will print the title of the window given a process id. To use it in the vimscript:

1
2
3
let pid = remote_expr(servername, "getpid()")
" execute the wmctrl command
call system("wmctrl -a \"`wtitle " . pid . "`\"")

Now the second task I mentioned is to forward all file opening requests to the same vim instance within the workspace. This is a little more complicated than the previous task, but there’s nothing especially tricky

  1. get the list of vim servers by vim --serverlist; for each server get its pid
  2. for each pid get its window title using the wtitle script shown above
  3. check whether this window title corresponds to the same workspace as the current one
  4. if yes, we’ve obtained the vim instance in the current workspace
  5. else if there’s no vim instance in the current workspace, we should then start a new server-enabled vim

All this can be wrapped up in this tiny script

sh (xvim) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/bin/sh
dname="`wmctrl -d | awk '$2=="*"{print $1}'`"
# get the pid of all available vim instances
output="`wmctrl -lp`"
vim --serverlist | (while read line; do
    pid="`vim --servername "$line" --remote-expr 'getpid()'`"
    # try to get the pid that works
    while true; do
        wtitle="`awk '$3=='"$pid"'{
            if ($2=="'"$dname"'") {
                for(i=5; i<NF; i++)
                    printf "%s ", $i 
                print $NF
            } else
                exit 1
        }' <<< "$output"`"
        #echo "wid found is $wid"
        if [ "$?" == 0 ]; then
            if [ -n "$wtitle" ]; then
                # we should try to navigate to this window and ask the window to open the required files
                wmctrl -a "$wtitle"
                vim --servername "$line" --remote "$@"
                exit
            else
                pid="`ps -o ppid= -p $pid`"
                ! [ "$?" == 0 ] && break
            fi
        else
            break
        fi
    done
    srvnames+=(":$line:")
done

#echo "all servernames found: ${srvnames[@]}"

c=0
while true; do
    srvname="VIM @$dname.$c"
    ! [[ "${srvnames[@]}" = *:"$srvname":* ]] && break
    c=$((c+1))
done

# start a new vim instance with a unique servername
xterm -name vim -e loader vim --servername "$srvname" "$@" &)
Nov 15th, 2013