Chapter 8: Customizing Leo

This chapter tells how to customize Leo.

Overview
Using leoConfig.txt
Using Python "hooks" in customizeLeo.py
Security warnings concerning customizeLeo.py
Using convenience routines called from hooks
Using leoConfig.leo and leoConfig.txt
Security considerations involving customizeLeo.py
The danger of trusting code in shared .leo files
Don't use rexec!
Protect your copy of customizeLeo.py!
Using customizeLeo.py
About hooks
Convenience routines for customizeLeo.py
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 Python "hooks" in customizeLeo.py

Warning: Naively using customizeLeo.py can expose you and your .leo files to malicious attacks.  See below.

You may customize how Leo works by placing your own Python code in the file customizeLeo.py. When executing any command or handling any event, Leo calls the customizeLeo() routine in customizeLeo.py if it exists. The arguments to customizeLeo() are a "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 executed in customizeLeo() corresponding to a particular tag is called the "hook" code (or simply hook) for that tag.

This is a simple, powerful and general mechanism for customizing Leo as you see fit. There are over 20 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 customizeLeo() returns any value except None Leo will assume that customizeLeo() has completely handled the command or event and will take no further action. 

Leo catches all exceptions raised in hook code, so syntax errors and other exceptions do not affect Leo.

Security warnings concerning customizeLeo.py

Naively using customizeLeo.py 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 called from hooks

Hook routines 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. Your code in customizeLeo() 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 involving customizeLeo.py

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

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 code in customizeLeo.py 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.

Protect your copy of customizeLeo.py!

You must protect your copy of customizeLeo.py. Please be aware of the following security concerns:

  1. You should never accept a copy of customizeLeo.py from someone else. Such people may be hostile. Please report any suspicious incidents to me.
  2. Leo will call customizeLeo() in customizeLeo.py only if leoConfig.txt contains:
    use_customizeLeo_dot_py = 1
    If you plan to use the customizeLeo() routine, it should print a message unique to you.
    Be worried if that message appears or changes unexpectedly.
  3. Leo now warns you when creating or modifying customizeLeo.py.
    Don't do so unless you made the modifications.

Using customizeLeo.py

Beginning with version 3.9, it is much easier to customize Leo:  The customizeLeo() routine in customizeLeo.py is called before and after all commands and many important events. You can use customizeLeo() to make Leo to:

You could easily create a Scripts menu based on the name of the .leo file. The code in customizeLeo.py has 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.

Your custom code in customizeLeo.py is permanent; it will not go away when Leo is updated. You can take advantage of the latest CVS updates without having to throw away your modifications.

About hooks

Leo calls customizeLeo(tag,keywords) at various times during execution. Leo catches exceptions, including syntax errors in this code, so it is safe to hack away on this code.

The code in customizeLeo() corresponding to each tag is known as the "hook" routine for that tag. The keywords argument is a Python dictionary containing information unique to each hook. For example, keywords["label"] indicates the kind of command for "command1" and "command2" hooks.

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 returning anything other than None overrides Leo's normal command or event processing.

hook name

 overrides 

when called

keys in keywords argument

"bodykey1"

yes

before body keystrokes v,ch,oldSel,undoType

"bodykey2"

after body keystrokes v,ch,oldSel,undoType

"command1"

yes

before each command label

"command2"

 after each command label
"end1" start of app.quit()
"headkey1"

no

before body keystrokes c,v,ch
"headkey2" after body keystrokes c,v,ch
"idle" periodically (at idle time)
"menu1"

yes

before creating menus
"menu2"

yes

before updating menus
"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
"openwith2"  after Open With command c,v,openType,arg,ext
"recentfiles1" yes before Recent Files command c,fileName,closeFlag
"recentfiles2" after Recent Files command c,fileName,closeFlag
"select1" yes before selecting a vnode c,v,new_v
"select2" after selecting a vnode c,v,old_v
"start1" no after app.finishCreate()
"start2" after opening first Leo window  fileName
"@url1" yes before  @url event c,v
"@url2" after @url event c,v

 Notes:

Both "open1" and "open2" 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.

Neither customizeLeo("open1") nor customizeLeo("open2") is called if the file is already open when frame.OpenWithFileName was called.

Leo calls frame.OpenWithFileName, and thus possibly customizeLeo("open1") and customizeLeo("open2"), when opening a file using either the Open command or the Recent Files menu.

Setting app().realMenuNameDict when customizeLeo("menu1") is called is an easy way of translating menu names to other languages. Please note that 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". When customizeLeo("command1") is called, app().commandName will be "open", not "ouvre".

Convenience routines for customizeLeo.py

The code in customizeLeo.py has 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.  The file customizeLeo.py shows 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 code in customizeLeo.py to translate menus into another language. It need only create entries in the app().realMenuNameDict dictionary. 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"))
d = app().realMenuNameDict
for untrans,trans in table:
	# Keys are untranslated names.
	# Values are translated names.
	d[untrans]=trans

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.