Troubleshooters.Com Presents

The VimOutliner
GPL Project

An Outline Processor for Linux

Copyright (C) 2001-2005 by Steve Litt

Stop the presses! VimOutliner now has its own website!
This page contains the older versions of VimOutliner, as well as a few extra-distro programs written by Steve Litt.
Versions 3 and above are downoadable from our new OFFICIAL website, VimOutliner.Org.

There is no warranty for anything contained in the VimOutliner distribution or documentation or its web pages, to the extent permitted by applicable law.  Except when otherwise stated in writing the copyright holders and/or other parties provide the program, documentation and web pages "as is" without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose.  The entire risk as to the quality and performance of the program is with you.  Should the program, documentation or web pages prove defective, you assume the cost of all necessary servicing, repair or  correction.

VimOutliner is a program to facilitate quick and productive outline processing using the Vim editor (version 6 or better). It also facilitates the "hyperlinking" of different outlines using Vim's tagging facilities.

Project Charter

VimOutliner's purpose is to give Linux users a fast and productive outliner in a reasonable timeframe. There are other Open Source outliner projects, but they are bogged down in the specification stage. Outliner users who have chosen the Linux operating system need outliners right now. I know I do.

The design manifesto of VimOutliner is easy and fast outlining. It has been designed so that you can outline as fast as you can think. Most of the credit for accomplishing that design goal goes to the Vim editor itself, which is so keyboarder friendly as to double the productivity of average "GUI apps", at least for those comfortable with its keystrokes.

VimOutliner does one thing and does it well -- outlining. There is no graphic representation other than the GUI hosted by vim itself (gvim). Although VimOutliner now has many of the same features as the "full featured" "golden age" outliners of late 80's and early 90's had, none of these features has in any way compromised authoring speed. VimOutliner is faster to use than other outliners. We will add features only if it doesn't reduce speed.

VimOutliner has been designed as a Vim plugin, with a perlscript to compile interoutline links. That means that at least in theory, you should be able to use VimOutliner in a Windows environment as well as in its native Linux/Unix/BSD environment. Only interoutline linking will not work. In time even this interoutline link compilation perlscript will be performed by vim scripts, and VimOutliner will become as portable as Vim itself.

The intended audience of VimOutliner are those who possess all of the following traits:

  1. Use outlines as part of their daily thinking, planning and design activities.
  2. Use Linux or UNIX.
  3. Are comfortable with VI as an editor
Those who prefer Emacs should use Emacs' built in outlining capability. Those who prefer Windows or Mac should use an outliner designed for those platforms, although if you're fast with VI you may wish to port VimOutliner to Windows (it would be almost trivial).

Project Specifications

VimOutliner consists of scripts and configuration files that put Vim in a state conducive to fast and productive outlining.


As of version 0.3.0, the old ol script is no longer necessary, nor shipped, nor supported. Vimoutliner is started simply by running vim on a file with a .otl extension.

The scripts expect that all VimOutliner configurations be placed in the $HOME/.vim directory, and for the interoutline linking tag file (vo_tags.tag) to be placed in the $HOME/.vimoutlinerdirectory. All scripts are expected to be in the $HOME/bin directory. If you want to add your own functionality to VimOutliner, use the $HOME/.vimoutlinerrc file.

As of version 0.3.0, the only script shipping with the distribution is, the script to recompile interoutline links. Scripts from older versions, such as, are available outside this distribution.


Older versions:

Extra-Distro downloads

Many features did not make it into the distribution (yet). If you want them, get them in this section.

Executable Lines (Steve Litt style)

Executable lines enable you to launch any command from a specially constructed headline within VimOutliner. The line must be constructed like this:
	Description _exe_ command

Here's an example to pull up Troubleshooters.Com:
	Troubleshooters.Com _exe_ mozilla
Executable lines offer the huge benefit of a single-source knowledge tree, where all your knowledge, no matter what its format, exists within a single tree of outlines connected with interoutline links and executable lines.

To enable this behavior, insert the following code into your $HOME/.vimoutlinerrc file:

if !exists("loaded_steveoutliner_functions")
let loaded_steveoutliner_functions=1
function Spawn()
let theline=getline(line("."))
let idx=matchend(theline, "_exe_\\s*")
if idx == -1
echo "Not an executable line"
let command=strpart(theline, idx)
let command="!".command
exec command


map ,,e :let junk=Spawn()<cr>
let use_space_colon=0

Noel Henson has a different syntax for executable lines. By version 0.4.0 the VimOutliner project team probably will have decided on an approved syntax and place scripts for that syntax within the VimOutliner distribution.

Here is the code code as of 1/4/2005:

#!/usr/bin/perl -w -I/data/perl/Node

# Copyright (C) 2004 by Steve Litt
# Licensed with the GNU General Public License, Version 2
# See

use strict;

use Node;

package Callbacks; # {{{1
sub new() #{{{2
my($type) = $_[0];
my($self) = {};
bless($self, $type);
$self->{'bt_break_var'} = 'no';
$self->{'bt_indicator_var'} = '^\t*: ';
$self->{'output_data'} = undef;
$self->{'level_mapping'} = undef; # shaky
$self->{'in_body_text'} = 0;
$self->{'this_body_text_node'} = undef;

sub getOutputData($) { return $_[0]->{'output_data'};} #{{{2

sub hasOutputData($) { return defined $_[0]->{'output_data'};} #{{{2

sub setOutputData($) { $_[0]->{'output_data'} = $_[1];} #{{{2

sub getLevelMapping($) { return $_[0]->{'level_mapping'};} #{{{2

sub hasLevelMapping($) { return defined $_[0]->{'level_mapping'};} #{{{2

sub setLevelMapping($) { $_[0]->{'level_mapping'} = $_[1];} #{{{2

sub getBtIndicatorVar($) { return $_[0]->{'bt_indicator_var'};} #{{{2

sub hasBtIndicatorVar($) { return defined $_[0]->{'bt_indicator_var'};} #{{{2

sub setBtIndicatorVar($$) { $_[0]->{'bt_indicator_var'} = $_[1];} #{{{2

sub getBtBreakVar($) { return $_[0]->{'bt_break_var'};} #{{{2

sub hasBtBreakVar($) { return defined $_[0]->{'bt_break_var'};} #{{{2

sub setBtBreakVar($$) #{{{2
my $state = lc($_[1]);
if($state eq 'no')
$_[0]->{'bt_break_var'} = 'no';
elsif($state eq 'yes')
$_[0]->{'bt_break_var'} = 'yes';
elsif($state eq 'new')
$_[0]->{'bt_break_var'} = 'new';
die "Invalid value (" . $state . ") given to setBtBreakVar(), aborting...\n";

sub cbCountChildren() #{{{2
my($self, $checker, $level) = @_;
unless (defined($checker)) {return;}

my $childCount=0;
my $checker2 = $checker->getFirstChild();
$checker2 = $checker2->getNextSibling();
$checker->setAttribute("children", $childCount);

sub cbPrintNode() #{{{2
my($self, $checker, $level) = @_;
unless (defined($checker)) {return;}

for(my $n=0; $n < $level; $n++) {print "\t";}
print "* ";
print $checker->getValue();
print "\n";

for(my $n=0; $n <= $level; $n++) {print "\t";}
print "(";

my %attribs = ();
%attribs = $checker->getAttributes() if $checker->hasAttributes();

my @keys = keys(%attribs);
foreach my $key (sort @keys)
print $key, "=", $attribs{$key}, "; ";

print ")\n";

sub cbLevel2paragraphType($) #{{{2
my($self, $checker, $level) = @_;
unless (defined($checker)) {return;}
$checker->setAttribute('paragraphType', $level);

sub cbCollectBodyTextParagraphs() #{{{2
my($self, $checker, $level) = @_;
unless (defined($checker)) {return;}

my $lineType = 'nobt';
my $lineValue = '';
my $lookfor = $self->getBtIndicatorVar();

if($checker->getValue() =~ m/($lookfor)\s*(\S)(.*)\s*/)
$lineType = 'bt';
$lineValue = $2 . $3;
$lineType = 'nobt';
if($self->{'in_body_text'} == 0)
if($lineType eq 'bt')
my $newnode = Node->new( '', 'bodytext', $lineValue);
$newnode->setAttribute('paragraphType', 'bodytext');
$self->{'this_body_text_node'} = $checker->getPrevSibling();
if($lineType eq 'bt')
$checker->getValue() =~ m/($lookfor)\s*(.*)(\S)/;
my $newtext = $2 . $3;

my $currtext = $self->{'this_body_text_node'}->getValue();
$newtext = $currtext . ' ' . $newtext;
$self->{'in_body_text'} = 1 if $lineType eq 'bt';
$self->{'in_body_text'} = 0 if $lineType eq 'nobt';

sub cbOutputLyx($) #{{{2
my($self, $checker, $level) = @_;
unless (defined($checker)) {return;}
return if $level < 1;

my $key = $checker->getAttribute('paragraphType');
my $startTag = $self->{'level_mapping'}->getStartTag($key);

package OutputData; #{{{1

sub new() #{{{2
my($type) = $_[0];
my($self) = {};
bless($self, $type);
$self->{'output_start_tag'} = '';
$self->{'output_value'} = '';
$self->{'output_end_tag'} = '';
$self->{'output_line_length'} = 78;


sub setOutputLineLength($$) { $_[0]->{'output_line_length'} = $_[1]; } #{{{2

sub getOutputLineLength($$) { return $_[0]->{'output_line_length'}; } #{{{2

sub hasOutputLineLength($$) { return defined $_[0]->{'output_line_length'}; } #{{{2

sub setOutputStartTag($$) { $_[0]->{'output_start_tag'} = $_[1]; } #{{{2

sub getOutputStartTag($) { return $_[0]->{'output_start_tag'}; } #{{{2

sub hasOutputStartTag($) { return defined($_[0]->{'output_start_tag'}); } #{{{2

sub setOutputValue($$) { $_[0]->{'output_value'} = $_[1]; } #{{{2

sub getOutputValue($) { return $_[0]->{'output_value'}; } #{{{2

sub hasOutputValue($) { return defined($_[0]->{'output_value'}); } #{{{2

sub setOutputEndTag($$) { $_[0]->{'output_end_tag'} = $_[1]; } #{{{2

sub getOutputEndTag($) { return $_[0]->{'output_end_tag'}; } #{{{2

sub hasOutputEndTag($) { return defined($_[0]->{'output_end_tag'}); } #{{{2

sub clear() #{{{2

sub write() #{{{2
my $self = shift;
print $self->getOutputStartTag();
my @words = split(/ /, $self->getOutputValue());
my $lineLength=0;
for my $word (@words)
my $wordLength = length($word);
if($lineLength + 1 + $wordLength > $self->getOutputLineLength())
print "\n";
if($lineLength > 0)
print ' ';
print $word;
$lineLength += $wordLength;
print $self->getOutputEndTag();
print "\n";

package LevelMapping; #{{{1
sub new($) #{{{2
my($type) = $_[0];
my($self) = {};
bless($self, $type);
$self->{'map_filename'} = 'o2l.conf';
my %starttags = ();
$self->{'starttags'} = \%starttags;
my %environments = ();
$self->{'environments'} = \%environments;
$self->{'output_end_tag'} = '';

sub getEndTag($$){return $_[0]->{'endtags'}->[$_[1]];} #{{{2

sub hasEndTag($$){return defined $_[0]->{'endtags'}->[$_[1]];} #{{{2

sub setEndTag($$$){;} ### NO END TAG FOR CURRENT LYX #{{{2

sub getStartTag($$){return $_[0]->{'starttags'}->{$_[1]};} #{{{2

sub hasStartTag($$){return defined $_[0]->{'starttags'}->{$_[1]};} #{{{2

sub setStartTag($$$){$_[0]->{'starttags'}->{$_[1]} = $_[2];} #{{{2

sub getEnvironment($$){return $_[0]->{'environments'}->{$_[1]};} #{{{2

sub hasEnvironment($$){return defined $_[0]->{'environments'}->{$_[1]};} #{{{2

sub setEnvironment($$$){$_[0]->{'environments'}->{$_[1]} = $_[2];} #{{{2

sub getEnvironmentChecked($$) #{{{2
my $self = shift;
my $key = shift;
return('ERROR_' . $key . '_ERROR');

sub setMapFilename($$) { $_[0]->{'map_filename'} = $_[1]; } #{{{2

sub getMapFilename($) { return $_[0]->{'map_filename'}; } #{{{2

sub hasMapFilename($) { return defined($_[0]->{'map_filename'}); } #{{{2

sub lrtrim($$) #{{{2
my $string = shift;
return '' if $string =~ m/^\s*$/;
$string =~ m/^\s*(.*)(\S)\s*/;
$string = $1 . $2;
return $string;

sub readMapping($) #{{{2
my $self = shift;
my $inf; #input file descriptor
open($inf, "<" . $self->getMapFilename()) or
die "Could not read map file (" . $self->getMapFilename()
. "), aborting ...\n";
my(@lines) = <$inf>;
foreach my $line (@lines)
my($key, $value) = split(/:/, $line, 2);
$key = $self->lrtrim($key);
$value = $self->lrtrim($value);
$self->setEnvironment($key, $value);

sub adjustMapping($) #{{{2
my $self = shift;
my @keys = keys %{$self->{'environments'}};
foreach my $key (@keys)
my $value = $self->getEnvironmentChecked($key);
$value = "\\layout " . $value . "\n\n";
$self->setStartTag($key, $value);

package Main; #{{{1
sub main() #{{{2
my $levelMap = LevelMapping->new();
$levelMap->setMapFilename($ARGV[1]) if defined $ARGV[1];

my $outputObject = OutputData->new();

#### DIAGNOSTIC: PRINT level/bodytext to environment to StartTag map
# my %st;
# %st = %{$levelMap->{'starttags'}};
# %st = %{$levelMap->{'environments'}};
# my @keys = sort keys %st;
# for my $key (@keys)
# {
# print ']' . $key . '[ ';
# print ">";
# print $levelMap->getStartTag($key);
# print "<\n";
# }
# exit(1);

my $parser = OutlineParser->new();
my $topNode=$parser->parse();

my $callbacks = Callbacks->new();

#### SET paragraphType attribute
my $walker;
$walker = Walker->new
[\&Callbacks::cbLevel2paragraphType, $callbacks]

$walker = Walker->new
[\&Callbacks::cbCollectBodyTextParagraphs, $callbacks]

$walker = Walker->new
[\&Callbacks::cbOutputLyx, $callbacks]

# $walker = Walker->new
# (
# $topNode,
# [\&Callbacks::cbPrintNode, $callbacks]
# );
# $walker->walk();

# }}}1

# vim600: set foldmethod=marker foldlevel=1:

Please note that for this to run, you must install Note also that in the preceding code, you must change the -I parameter in the first line to point to the directory where you installed Note also that you must have a file called o2l.conf, in the current directory. That file contains the mapping from outline levels to LyX environments. It also maps the outline's body text to a LyX environment (typically Standard). The outline must be a tab indented outline such as those produced by VimOutliner. The following is an example of an o2l.conf for an outline whose top level is Chapter:

0: Part           
1: Chapter
2: Section
3: Subsection
4: Subsubsection
5: Paragraph
6: Subparagraph
7: Garbage7
bodytext: Standard

Note that you can use a different name for the mapping file by having it as arg1 of the command, although as of 1/5/2005 this has not yet been tested.


Maintainers List

Todo List -- Needed Programming and Documentation Tasks

Basic outlining features
command execution (mozilla mydoc.html, etc)
Almost complete
Menu interface
Probably next version
VimOutliner to htmlslides converter
Probably next version
body text
Possibly next version
Help system
Under discussion
Under discussion
Port to Windows
Awaiting willing developer
Code snippets and other sorts of content
On hold
Headline/tree moveup and movedown commands
Under discussion
hoisting and dehoisting
Under discussion
level aware sorting
Under discussion
VimOutliner Groupware
Under discussion
multipage otl2html
(whole website instead of single page)
On hold
Install easers
.deb file
.rpm/redhat file
.rpm/mandrake file
Source version control
rcs or cvs system

Provisions for code snippets, and other sorts of content

As of 0.3.0, VimOutliner now supports freeform, wordwrapping body text. A headline is declared body text by starting it with a colon and then a space. Such a headline will wordwrap (so it's not really a headline at all), and will "do the right thing" when you use a Vim gq command on it.

Other types of content, such as code snippets, may be incorporated in the future. If you can think of a way to incorporate code snippets, please contact our mailing list.

Port to Windows

We have reason to believe that most features of VimOutliner 0.3.0 will work under Windows, but this hasn't been tested. We need people to try to run VimOutliner under Windows and report back on what worked and what didn't (interoutline links won't work as of 0.3.0).

Various scripts

We need scripts to convert VimOutliner to various other formats, and other formats to VimOutliner. We already have rudimentary VimOutliner to html, VimOutliner to LyX (not tested or expected to work with body text). Other conversions will be most welcome.

otl2html that follows Vim tags to create a whole site instead of  a single page

For my own purposes, I created an that converts a tab-indented outline to a web page. I've had a request that the program create an entire site that changes inter-outline hyperlinks to html hyperlinks.

Multilevel otl2htmlslides converter

This has been completed by Noel Henson and awaits incorporation in the VimOutliner distribution.

How to Participate

Get on our mailing list. We'll work with you to get your pet VimOutliner features incorporated within the scope of the VimOutliner manifesto.

Mailing List

You can sign up for the VimOutliner mailing list at Follow the instructions. It's very simple.

FAQ (Frequently Asked Questions) list

None exists. The project is too new to really know what to put in it. See the README. Once you have VimOutliner running, type :help vimoutliner from within Vim.

HTMLized versions of the project documentation

None. You might want to look at the README. Once you have VimOutliner running, type :help vimoutliner from within Vim.See also, which discusses outlining in general, and, which discusses how to use Vim 6 as an outliner.

Links to related projects.

Dedication: We Stand On Their Shoulders


On 6/1/2001, the first distribution of VimOutliner was released as version 0.1.3. This is alpha code. It seems to work perfectly on my box, and I tested it hard.

On 12/3/2002, we released version 0.2.0, which has expand/collapse that resembles real outline processors, rather than the default Vim folding. Version 0.2.0 also adds level coloration, as well as many additional commands.

On 6/22/2003 we released version 0.3.0, which incorporates body text and many other wonderful features.

Top of Page