Chapter 8: Customizing Leo

This chapter tells how to customize Leo.

Overview
Using leoConfig.txt
Using plugins and hooks
Security warnings
Using convenience routines
Using leoConfig.leo and leoConfig.txt
Security considerations
The danger of trusting code in shared .leo files
Don't use rexec!
Using plugins and hooks
About plugins
About hooks
Convenience routines for hooks
Convenience functions for periodic actions
Convenience methods for menus
Putting the Leo icon in Leo windows

Overview

We first briefly discuss the three main ways of customizing Leo.  Later sections provide all the details.

Using leoConfig.txt

When Leo first starts, Leo looks for a file called leoConfig.txt, derived from leoConfig.leo, which contains extensive documentation for each setting. The settings in leoConfig.txt include:

Using plugins and hooks

Warning: Naively using plugins and hooks can expose you and your .leo files to malicious attacks.  See the section called "Security warnings" below.

You may customize how Leo works by placing your own Python code in the plugins folder. When executing commands or handling events Leo calls hook routines defined in mod_*.py files in the plugins folder.  The arguments to hook routines are "tag", a string telling the kind of command or event about to be executed, and "keywords", a Python dictionary of information whose contents depend on the specific command or event. The code Leo calls when a command or event happens is called the "hook" for that tag.

This is a simple, powerful and general mechanism for customizing Leo. There are dozens of kinds of hooks, including the "command1" and "command2" hooks, that are called before and after each of Leo's menu commands. Leo will allow you to override most commands and event handling. In many cases, if a hook returns any value except None Leo will assume that the hook has completely handled the command or event and will take no further action. 

Leo catches all exceptions raised in hooks, so syntax errors and other exceptions are not serious concerns.

Security warnings

Naively using hooks can expose you and your .leo files to malicious attacks. You will be safe as long as you follow these basic principles:

Using convenience routines

Hooks can import any file in Leo's source code and execute routines in that file. Leo's contains a number of convenience routines designed to make common customization tasks easier. Hooks can use these routines to create your own menus, to translate menus into other languages, and to create entries in the Open With menu. These convenience routines are discussed in detail below.

Using leoConfig.txt

Leo will override settings in .leo files if it finds a file called leoConfig.txt. You should generate leoConfig.txt from leoConfig.leo.  Leo works just as before if it does not find a leoConfig.txt file. The next section contains an example of leoConfig.txt showing all the options that may be set.

Leo looks for leoConfig.txt first in the directory specified by the Python variable sys.leo_config_directory. You would typically set this variable in Python's sitecustomize.py file. If this variable does not exist, Leo looks in the directory from which Leo was loaded.

Settings in leoConfig.txt overrides preferences in .leo files, but only for those items actually in leoConfig.txt, so you can choose which settings you want to override. Also, a Leo ignores any setting in leoConfig.txt whose value is "ignore" (without the quotes). For example:

[prefs panel options]
tab_width = ignore

If a setting is overridden, it is _not_ written to the .leo file when the outline is saved. Note that this does not change the file format: all previous versions of Leo will be able to read such .leo files.

The preceding is probably all you need to know to use leoConfig.txt. The following discuss some minor details:

  1. When reading a .leo file, if a setting is found neither in leoConfig.txt nor in the .leo file, Leo uses a default, hard-coded value. In leo.py 3.0 and later these default settings are found in tables that appear in the section called <<define default tables for settings>> in the file leoConfig.py. So it is now convenient to change settings in leo.py itself as well as in leoConfig.txt.
  2. Leo will update leoConfig.txt unless the read_only option is on in leoConfig.txt.  Warning: there are problems when Leo does write leoConfig.txt: all comments are lost and options and sections are written in a random order. This is due to problems in Python's ConfigParser module and will not be changed any time soon.
  3. Provided the read_only option is off, Leo updates leoConfig.txt whenever it saves a .leo file or whenever the Preferences panel is closed without being cancelled. When updating leoConfig.txt, Leo will write only existing settings whose value is not "ignore".
  4. When Leo saves a .leo file, Leo will write a Preferences setting to the .leo file only if the setting will not be written when updating leoConfig.txt. In particular, changes made in the Preferences Panel will become permanent immediately if Leo the read_only option is off. Otherwise the change will become permanent when any .leo file is saved.

Security considerations

The following sections discuss important security considerations.  You should be familiar with these if you plan to use hooks:

The danger of trusting code in shared .leo files

I'd like to thank Stephen Schaefer for gently insisting that we guard against malicious code in shared .leo files. To quote Stephen directly:

"I foresee a future in which the majority of leo projects come from marginally trusted sources... I see a world of leo documents sent hither and yon--resumes, project proposals, textbooks, magazines, contracts-- and as a race of Pandora's, we cannot resist wanting to see 'What's in the box?' Are we going to fire up a text editor to make a detailed examination of the file? Never! We're going to double click on the cute leo file icon, and leo will fire up in all its raging glory. Just like Word (and its macros) or Excel (and its macros)."

In short, when we share "our" .leo files we can not assume that we know what is our "own" documents. So hooks that naively searches through .leo files looking for scripts to execute is looking for big trouble.

Never use this kind of code in a hook:

@ WARNING Using the following routine exposes you to malicious code in .leo files!
Do not EVER use code that blindly executes code in .leo files!
Someone could send you malicious code embedded in the .leo file.

WARNING 1: Changing "@onloadpythonscript" to something else will NOT protect
you if you EVER share either your files with anyone else.

WARNING 2: Replacing exec by rexec below provides NO additional protection!
A malicious rexec script could trash your .leo file in subtle ways.
@c
# WRONG: This blindly execute scripts found in an .leo file!
def onLoadFile():
	v = top().rootVnode()
	while v:
		h = v.headString().lower()
		if match_word(h,0,"@onloadpythonscript"):
			s = v.bodyString()
			if s and len(s) > 0:
				try: # SECURITY BREACH: s may be malicious!
					exec(s+'\n',__builtins__,__builtins__)
				except:
					es_exception()
		v = v.threadNext()

Don't use rexec!

Do not expect rexec to protect you against malicious code contained in .leo files. Remember that Leo is a repository of source code, so any text operation is potentially malicious.

For example, consider the following script--a script is valid in rexec mode:

c = top()
thisNode = c.currentVnode()
v = c.rootVnode()
while v:
	<< change all instances of rexec to exec in v's body >>
	v = v.threadNext()
<< delete thisNode >>
<< clear the undo stack >>

This script will introduce a security hole the .leo file without doing anything prohibited by rexec, and without leaving any traces of the perpetrating script behind. The damage will become permanent outside this script when the user saves the .leo file.  Many other kinds of mischief could be done by similar scripts.

Using plugins and hooks

New in 3.11. Leo now looks in the plugins folder for files whose name matches mod_*.py. Such plugin files define routines called hooks.  Leo calls these hooks before and after all commands and many important events. You can use hooks to:

Hooks have full access to all of Leo's source code. Several convenience methods have been added to make customizing menus and commands easier. These convenience methods are described in details below.

Plugins will not change when Leo is updated. You can take advantage of the latest CVS updates without having to throw away your modifications.

About plugins

Plugin files should define hook routines and should register those routines when the plugin is imported.  See LeoPy.leo for the code for many examples of defining plugins.

For example, the file plugins\mod_open_with.py contains  the plugin that handles the Open With menu.  The @file node for plugins\mod_open_with.py is:

"""Open With handler"""
from customizeLeo import *
from leoGlobals import *
@others

if 1: # Register the handlers...
	registerHandler("idle",on_idle)
	registerHandler(("start2","open2","command2"),
		create_open_with_menu)
	es("...open with")

The actual hooks (the on_idle and open_with_menu routines) are defined in child nodes.  See LeoPy.leo for the code for these routines.  At startup time Leo will import mod_open_with and the code shown will be executed.

The registerHandler function registers a function as a hook for one or more tags.  Plugins may call registerHandler with a hook name like "idle" or a list of hook names like ("start2","open2","command2"). Using "all" as the hook name registers the function for all hooks. This is useful for tracing hooks.

The registerExclusiveHandler function registers a function for hooks that should not be redefined in other plugins.

About hooks

The code in each plugin registered to each tag is known as the "hook" routine for that tag. Leo calls hook routines at various times during execution. Leo passes two arguments to each hook:

tag, a string identifying the time at which the hook has been called

keywords, a Python dictionary containing information unique to each hook. For example, keywords["label"] indicates the kind of command for "command1" and "command2" hooks.

Leo catches exceptions, including syntax errors in this code, so it is safe to hack away on this code.

For some hooks, returning anything other than None "overrides" Leo's default action. Hooks have full access to all of Leo's source code. Just import the relevant file. For example, top() returns the commander for the topmost Leo window.

The following table summarizes the arguments passed to customizeLeo().

Overrides is "yes" if the hook will override Leo's normal processing by returning anything other than None.

hook name 

 overrides 

when called

keys in keywords

"bodyclick1" yes before normal click in body c,v,event (new in 3.11)
"bodyclick2" after normal click in body c,v,event (new in 3.11)
"bodydclick1" yes before double click in body c,v,event (new in 3.11)
"bodydclick2" after double click in body c,v,event (new in 3.11)

"bodykey1"

yes

before body keystrokes c,v,ch,oldSel,undoType

"bodykey2"

after body keystrokes c,v,ch,oldSel,undoType
"bodyrclick1" yes before right click in body c,v,event (new in 3.11)
"bodyrclick2" after right click in body c,v,event (new in 3.11)
"boxclick1" yes before click in +- box  c,v,event (new in 3.11)
"boxclick2" after click in +- box c,v,event (new in 3.11)

"command1"

yes

before each command c,v,label (note 2)

"command2"

after each command c,v,label (note 2)
"drag1" yes before start of drag c,v,event (new in 3.11)
"drag2" after start of drag c,v,event (new in 3.11)
"dragging1" yes before continuing to drag c,v,event (new in 3.11)
"dragging2" after continuing to drag c,v,event (new in 3.11)
"end1" start of app.quit()
"enddrag1" yes before end of drag c,v,event (new in 3.11)
"enddrag2" after end of drag c,v,event (new in 3.11)
"headclick1" yes before normal click in headline c,v,event (new in 3.11)
"headclick2" after normal click in headline c,v,event (new in 3.11)
"headrclick1" yes before right click in headline c,v,event (new in 3.11)
"headrclick2" after right click in headline c,v,event (new in 3.11)
"headkey1"

no

before headline keystrokes c,v,ch
"headkey2" after headline keystrokes c,v,ch
"hypercclick1" yes before control click in hyperlink c,v,event (new in 3.11)
"hypercclick2" after control click in hyperlink c,v,event (new in 3.11)
"hyperenter1" yes before entering hyperlink  c,v,event (new in 3.11)
"hyperenter2" after entering hyperlink  c,v,event (new in 3.11)
"hyperleave1" yes before leaving hyperlink c,v,event (new in 3.11)
"hyperleave2" after leaving hyperlink c,v,event (new in 3.11)
"iconclick1" yes before single click in icon box c,v,event (new in 3.11)
"iconclick2" after single click in icon box c,v,event (new in 3.11)
"iconrclick1" yes before right click in icon box c,v,event (new in 3.11)
"iconrclick2" after right click in icon box c,v,event (new in 3.11)
"icondclick1" yes before double click in icon box c,v,event (new in 3.11)
"icondclick2" after double click in icon box c,v,event (new in 3.11)
"idle" periodically (at idle time) c,v
"menu1"

yes

before creating menus c,v (note 3)
"menu2"

yes

before updating menus c,v
"open1" yes before opening any file old_c,new_c,fileName
"open2" after opening any file old_c,new_c,fileName
"openwith1" yes before Open With command c,v,openType,arg,ext (note 4)
"openwith2" after Open With command c,v,openType,arg,ext (note 4)
"recentfiles1" yes before Recent Files command c,v,fileName,closeFlag
"recentfiles2" after Recent Files command c,v,fileName,closeFlag
"select1" yes before selecting a vnode c,new_v,old_v
"select2" after selecting a vnode c,new_v,old_v
"start1" no after app.finishCreate()
"start2" after opening first Leo window fileName
"unselect1" yes before unselecting a vnode c,new_v,old_v (new in 3.11)
"unselect2" after unselecting a vnode c,new_v,old_v (new in 3.11)
"@url1" yes before  @url event c,v (note 5)
"@url2" after @url event c,v (note 5)

Notes:

(1) "activate" and "deactivate" hooks: These have been removed because they do not work as expected.

(2) "commands" hooks: The label entry in the keywords dict contains the "canonicalized" form of the command, that is, the lowercase name of the command with all non-alphabetic characters removed. New for 3.11: The label for all variants of the Undo and Redo commands are "undo" and "redo".

(3) "menu1" hook: Setting app().realMenuNameDict in this hook is an easy way of translating menu names to other languages. Note: the "new" names created this way affect only the actual spelling of the menu items, they do _not_ affect how you specify shortcuts in leoConfig.txt, nor do they affect the "official" command names passed in app().commandName. For example, suppose you set app().realMenuNameDict["Open..."] = "Ouvre".

(4) "open1" and "open2" hooks: These are called with a keywords dict containing the following entries:

old_c: The commander of the previously open window.
new_c: The commander of the newly opened window.
fileName: The name of the file being opened.

You can use old_c.currentVnode() and new_c.currentVnode() to get the current vnode in the old and new windows.

Leo calls the "open1" and "open2" hooks only if the file is already open. Leo will also call the "open1" and "open2" hooks if a) a file is opened using the Recent Files menu and b) the file is not already open.

(5) Leo calls the "@url1" and "@url2" hooks only if "icondclick1" hook returns None.

Convenience routines for hooks

Hooks have full access to all of Leo's source code simply by importing it. Moreover, several convenience methods have been added to make customizing menus and commands easier. The following paragraphs discuss these routines and how to use them.

Convenience functions for periodic actions

The following routines enable and disable "idle" hooks. They are defined in leoGlobals.py.  Idle hooks are good places to check for changed temporary files created by the Open With command.

enableIdleTimeHook(idleTimeDelay=100)

Enables the "idle" hook. After this routine is called Leo will call customizeLeo("idle") approximately every idleTimeDelay milliseconds. Leo will continue to call customizeLeo("idle") periodically until disableIdleTimeHook() is called.

disableIdleTimeHook()

Disables the "idle" hook.

Convenience methods for menus

The following convenience routines make creating menus easier. These are methods of the leoFrame class. Use top().frame to get the frame object for the presently active Leo window. These convenience methods all do complete error checking and write messages to the log pane and to the console if errors are encountered.  LeoPy.leo gives examples of how to use these routines to create custom menus and to add items to the Open With menu.

createMenuItemsFromTable (self,menuName,table,openWith=0)

This method adds items to the menu whose name is menuName. The table argument describes the entries to be created. This table is a sequence of items of the form (name,shortcut,command).

An entry of the form ("-",None,None) indicates a separator line between menu items. For example:

table =
	("Toggle Active Pane","Ctrl-T",self.OnToggleActivePane),
	("-",None,None),
	("Toggle Split Direction",None,self.OnToggleSplitDirection))

top().frame.createMenuItemsFromTable("Window",table)

If the openWith keyword argument is 1 the items are added to a submenu of the Open With menu. However, it will be more convenient to use the createOpenWithMenuFromTable method to create the Open With menu.

createNewMenu (self,menuName,parentName="top")

This method creates a new menu:

This method returns the menu object that was created, or None if there was a problem. Your code need not remember the value returned by this method. Instead, your code will refer to menus by name.

createOpenWithMenuFromTable(self,table)

This method adds items to submenu of the Open With menu item in the File menu.

The table argument describes the entries to be created; table is a sequence of items of the form (name,shortcut,data).

When the user selects the Open With item corresponding to the table item Leo executes command(arg+path) where path is the full path to the temp file. If ext is not None, the temp file has the given extension. Otherwise, Leo computes an extension based on what @language directive is in effect. For example:

table = (
	("Idle", "Alt+Shift+I",("os.system",idle_arg,".py")),
	("Word", "Alt+Shift+W",("os.startfile",None,".doc")),
	("WordPad","Alt+Shift+T",("os.startfile",None,".txt")))

top().frame.createOpenWithMenuFromTable(table)
deleteMenu (self,menuName)

Deletes the menu whose name is given, including all entries in the menu.

deleteMenuItem (self,itemName,menuName="top")

Deletes the item whose name is itemName from the menu whose name is menuName. To delete a menu in the menubar, specify menuName="top".

Example: creating a menu

The leoFrame class creates the Window menu as follows:

windowMenu = self.createNewMenu("Window")
table = (
	("Equal Sized Panes","Ctrl-E",self.OnEqualSizedPanes),
	("Toggle Active Pane","Ctrl-T",self.OnToggleActivePane),
	("Toggle Split Direction",None,self.OnToggleSplitDirection),
	("-",None,None),
	("Cascade",None,self.OnCascade),
	("Minimize All",None,self.OnMinimizeAll),
	("-",None,None),
	("Open Compare Window",None,self.OnOpenCompareWindow),
	("Open Python Window","Alt+P",self.OnOpenPythonWindow))
self.createMenuEntries(windowMenu,table)

Translating menus into other languages

It is easy for a hook to translate menus into another language. For example, code similar to the following code would typically be found in the "start2" hook:

table = (
	("Open...","Ouvre"),
	("Open With...","Ouvre Avec..."),
	("Close","Ferme"))
# Call the convenience routine to do the work.
app().setRealMenuNamesFromTable(table)

Putting the Leo icon in Leo windows

Leo will now draw the LeoDoc.ico from the Icons directory in Leo windows, provided you have installed Fredrik Lundh's PIL and tkIcon packages.

Download PIL from http://www.pythonware.com/downloads/index.htm#pil
Download tkIcon from http://www.effbot.org/downloads/#tkIcon

Many thanks to Jonathan M. Gilligan for suggesting this code. At present, the icon is not drawn very well.  This may be corrected in version 3.10.