Thursday, December 4, 2008

Vim as typewriting tutor

Vim is an advanced text editor, used by many free software fanatics. It is highly configurable, beside the default features it has a way to be enhanced using the built-in interpreter.

Who is this article intended for?

This article is intended for all people familiar with Vim, but also those who don’t know this marvellous tool. The main goal is to show that this editor is an advanced, fully featured program. We will show this by modifying its default behavior into a keyboard training program. The way it is performed might also be useful for translators - it only needs a few configuration tweaks and enabling the Aspell support.

Vim jako program do ćwiczenia szybkiego czytania (1)
Pic 1. Typewriting tutor (1)

What do we want to do?

Our goal is to convert the standard editor into a keyboard trainer program. Let’s make it so that every time it opens a *.keymaster file the program will open two windows, where the upper one shows the text to be typed, and the lower one will serve the training purpose, accepting the type-in.

Vim jako program do ćwiczenia szybkiego czytania (2)
Pic 2. Typewriting tutor (2)

Additionally, we need speed meters - a simple status line like “Typing time : 12s” might be enough for starters. This information will be cleared every time we enter the insert mode. It will be nice to see the typing time while key pressing.

The programming possibilities

Vim has an embedded interpreter that has astonishing features, especially considering that the program is a text editor. Making use of the interpreter we can almost indefinitely enhance Vim’s features.
To define a local variable, we need to use the ‘let’ keyword. Vim lets you create global, local and function-local variables.
A global variable is visible in all files Vim loads. Affects macros, plug-ins, configuration files.
A local variable is visible in the file they were declared at.
A function-local variable is visible only within one function, e.g.:


function NewNumberOneFunction()
let a:function_variable = "value"
endfunction

A global variable declaration would look e.g. this way:


function NewNumberOneFunction()
let a:function_variable = "value"
let g:global_variable = "global value"
endfunction

And the local variables look like :


let s:local = 333

The editor allows you to add new commands and use the automatically executed event handlers with ‘autocmd’. The commands can be grouped together, for example:


augroup group_name
autocmd {event name} {file extension} [{ command(s) }]
augroup END

The Vim features quite a lot of events to use in one’s scripts. The full list is available in the manuals.

How it will all work

First of all, we need to establish, what do we need, that is which functions and variables will we need.

We will need a variable that stores the starting moment of typing. It should be initialized right after entering the insert mode, so we will need to use the InsertEnter event. When the user finishes typing, by pressing escape (ESC) the typing time information should show up. Here we will need the InsertLeave event, which is executed when we leave the ‘insert mode’.

Vim jako program do ćwiczenia szybkiego czytania (3)
Pic 3. Typewriting tutor (3)

So, here is some clarification on the above outline: when we enter the insert mode, we store the starting time (in seconds) and when we leave, we calculate the difference between the start and the finish moment.

The following function, fetching the number of seconds and setting the keymaster_session to 1, might come handy:

function SessionStart()
let g:start_writing_time = reltime()[0]
let g:keymaster_session = 1
endfunction

But this is not all we need. It might be also handy to have two windows open: one will display the ready text, the other will accept data - doesn’t have to be the same text ;)

function GuiStart()
execute ":wincmd n"
execute ":wincmd r"
execute ":windo :set scrollbind"
execute ":syncbind"
execute ":resize -1"
endfunction

The GuiStart function is responsible for opening another window, executing ’scrollbind’ on all of them, setting the scrolling synchronization and adjusting the text entering window.

‘Scrollbind’ lets us scroll many windows at a time. Our script uses two basic windows that are to be scrolled together. The ’syncbind’ command enables the synchronization.

A function returning the current number of seconds since insert mode was entered would help too:

function CurSessionTime()
if !exists("g:keymaster_session")
let g:keymaster_session = 0
endif
let a:rs = 0
if g:keymaster_session == 1
let a:rs = reltime()[0] - g:start_writing_time
endif
return “[Typing time : " . a:rs . "s]”
endfunction

The Vim’s ‘exists’ function checks if the given variable exists. The global ‘keymaster_session’ variable tells us if the session is active : that is, have we started typing already or not ?

All’s well that ends well, so we need the last function : one that stops the current session:

function SessionStop()
let a:sec = reltime()[0] - g:start_writing_time
let g:keymaster_session = 0
execute “:echo \”[Typing time: \" . (a:sec) . \"s]]\”"
endfunction

All we need has been shown. The functions are executed with “:call FunctionName()”. Unfortunately, putting all the code above into configuration file is not going to work, so we need to “bind” the functions to given events:

augroup session
autocmd!
autocmd VimEnter *.keymaster call GuiStart()
autocmd InsertEnter * call SessionStart()
autocmd InsertLeave * call SessionStop()
augroup END

Such a system of functions lets us enable our ‘magic’ interface only when we open a *.keymaster file. Say, we contain the training text there.

Additionally, the following line will show the typing time in the status line:

set statusline=%<%f%<%{CurSessionTime()}%<%h%m%r%=%-20.(%3l:%02c%V:%L%)\%h%m%r%=-10(\#%n%Y%)\%P

All this, we insert in the .vimrc file or another .vim type file, loaded with a “source” command within .vimrc.

While training, we would like to know how many errors we have committed and where. Vim is able to provide such a functionality, it is enough to execute a “:set diff” command on all windows, or quicker : “:windo :set diff”, and if we want to disable the differences showing, we use “:windo :set nodiff”. Of course, one can also create a new mapping, like this:


map :windo :set diff
map :windo :set nodiff

If you like, you can use key mapping like this, to have the F2 key turn the diffing on, and F3 to turn it off:


map :windo :set diff
map :windo :set nodiff

Now, when F2 key is pressed we turn the buffer comparison on all available windows on, and with F3 we turn it off. It is possible to work while the diff option is enabled. To compare the windows we’ve been working on (to show differences) we type “:diff”.

Closing all windows with one command or key-press is not a problem :

:windo :q!

or

:windo :q

The first case will ignore the text we typed in, the second will not. It is noticeable, that the two-window interface with bound scrolling, only shows when we open a file with ‘.keymaster’ extension - and the typing time will show every time we open Vim. This can of course be adjusted to your needs.

Vim jako program do ćwiczenia szybkiego czytania (4)
Pic 4. Typewriting tutor (4)

No comments: