This chapter tells how to customize Leo.
We first briefly discuss the three main ways of customizing Leo. Later sections provide all the details.
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:
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.
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:
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.
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:
The following sections discuss important security considerations. You should be familiar with these if you plan to use customizeLeo.py.
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()
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.
You must protect your copy of customizeLeo.py. Please be aware of the following security concerns:
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.
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".
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.
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.
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).
- name is the command name
- shortcut is the shortcut, or None to indicate no shortcut.
- command is the routine to execute when the menu item is selected.
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:
- menuName is the name of the menu to be created.
- parentName is the name of the parent menu,
or "top" if the menu is to created in the menu bar.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).
- name is the command name
- shortcut is the shortcut, or None to indicate no shortcut.
- data is a tuple of the form (command,arg,ext)
- command is one of "os.system", "os.startfile", "os.spawnl", "os.spawnv" or "exec".
- arg is an argument to be passed to the given command.
- path is the full path name of the temporary file to be passed to the external editor.
- ext is a file extension or None.
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".
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)
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
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.