The .leo files in Leo’s distribution contain many @button nodes (many disabled), that do repetitive chores. Here is one, @button promote-child-bodies, from LeoDocs.leo:
"""Copy the body text of all children to the parent's body text."""# Great for creating what's new nodes.result=[p.b]b=c.undoer.beforeChangeNodeContents(p)forchildinp.children():ifchild.b:result.append('\n- %s\n\n%s\n'%(child.h,child.b))else:result.append('\n- %s\n\n'%(child.h))p.b=''.join(result)c.undoer.afterChangeNodeContents(p,'promote-child-bodies',b)
This creates a fully undoable promote-child-bodies command.
This script will compare the trees whose roots are p1 and p2 and show the results like “Recovered nodes”. That is, the script creates a node called “compare vr1 & vr2”. This top-level node contains one child node for every node that is different. Each child node contains a diff of the node. The grand children are one or two clones of the changed or inserted node.
Title case means that all words start with capital letters. The
following script converts the selected body text to title case. If
nothing has been selected, the entire current node is converted. The
conversion is undoable:
"""Undoably convert selection or body to title case."""w=c.frame.body.wrapperp=c.ps=p.bu=c.undoerstart,end=w.getSelectionRange()use_entire=start==end# no selection, convert entire bodyundoType='title-case-body-selection'undoData=u.beforeChangeNodeContents(p)ifuse_entire:p.b=s.title()else:sel=s[start:end]head,tail=s[:start],s[end:]p.b=head+sel.title()+tailc.setChanged()p.setDirty()u.afterChangeNodeContents(p,undoType,undoData)c.redraw()
The following script will create a minimal Leo outline:
if1:# Create a visible frame.c2=g.app.newCommander(fileName=None)else:# Create an invisible frame.c2=g.app.newCommander(fileName=None,gui=g.app.nullGui)c2.frame.createFirstTreeNode()c2.redraw()# Test that the script works.forpinc2.all_positions():g.es(p.h)
The following puts up a test window when run as a Leo script:
fromPyQt5importQtGuiw=QtGui.QWidget()w.resize(250,150)w.move(300,300)w.setWindowTitle('Simple test')w.show()c.my_test=w# <-- Keep a reference to the window!
Important: Something like the last line is essential. Without it, the window would immediately disappear after being created. The assignment:
c.my_test=w
creates a permanent reference to the window so the window won’t be garbage collected after the Leo script exits.
The values returned are in (‘ok’,’yes’,’no’,’cancel’), as indicated by the method names. Some dialogs also return strings or numbers, again as indicated by their names.
Scripts can run File Open and Save dialogs with these methods:
k.get1Arg handles the next character the user types when accumulating a user argument from the minibuffer. k.get1Arg handles details such as tab completion, backspacing, Ctrl-G etc.
Commands should use k.get1Arg to get the first minibuffer argument and k.getNextArg to get all other arguments.
k.get1Arg is a state machine. It has to be because it’s almost always waiting for the user to type the next character. The handle keyword arg specifies the next state in the machine.
The following examples will work in any class having a ‘c’ ivar bound to a commander.
Example 1: get one argument from the user:
@cmd('my-command')defmyCommand(self,event):"""State 0"""k=self.c.kk.setLabelBlue('prompt: ')k.get1Arg(event,handler=self.myCommand1)defmyCommand1(self,event):"""State 1"""k=self.c.k# ----> k.arg contains the argument.# Finish the command....# Reset the minibuffer.k.clearState()k.resetLabel()k.showStateAndMode()
Example 2: get two arguments from the user:
@cmd('my-command')defmyCommand(self,event):"""State 0"""k=self.c.kk.setLabelBlue('first prompt: ')k.get1Arg(event,handler=self.myCommand1)defmyCommand1(self,event):"""State 1"""k=self.c.kself.arg1=k.argk.setLabelBlue('second prompt: ')k.getNextArg(handler=self.myCommand2)defmyCommand2(self,event):"""State 2"""k=self.c.k# -----> k.arg contains second argument.# Finish the command, using self.arg1 and k.arg....# Reset the minibuffer.k.clearState()k.resetLabel()k.showStateAndMode()
Summary
The handler keyword argument to k.get1Arg and k.getNextArg specifies the next state in the state machine.
k.get1Arg contains many optional keyword arguments. See its docstring for details.
You can add an icon to the presently selected node with c.editCommands.insertIconFromFile(path). path is an absolute path or a path relative to the leo/Icons folder. A relative path is recommended if you plan to use the icons on machines with different directory structures.
Plugins and scripts should call u.beforeX and u.afterX methods to describe the operation that is being performed. Note: u is shorthand for c.undoer. Most u.beforeX methods return undoData that the client code merely passes to the corresponding u.afterX method. This data contains the ‘before’ snapshot. The u.afterX methods then create a bead containing both the ‘before’ and ‘after’ snapshots.
u.beforeChangeGroup and u.afterChangeGroup allow multiple calls to u.beforeX and u.afterX methods to be treated as a single undoable entry. See the code for the Replace All, Sort, Promote and Demote commands for examples. The u.beforeChangeGroup and u.afterChangeGroup methods substantially reduce the number of u.beforeX and afterX methods needed.
Plugins and scripts may define their own u.beforeX and u.afterX methods. Indeed, u.afterX merely needs to set the bunch.undoHelper and bunch.redoHelper ivars to the methods used to undo and redo the operation. See the code for the various u.beforeX and u.afterX methods for guidance.
See the section << How Leo implements unlimited undo >> in leoUndo.py for more details. In general, the best way to see how to implement undo is to see how Leo’s core calls the u.beforeX and afterX methods.
The mod_scripting plugin runs @scripts before plugin initiation is complete. Thus, such scripts can not directly modify plugins. Instead, a script can create an event handler for the after-create-leo-frame that will modify the plugin.
For example, the following modifies the cleo.py plugin after Leo has completed loading it:
Attempting to modify c.cleo.prikey immediately in the @script gives an AttributeError as c has no .cleo when the @script is executed. Deferring it by using registerHandler() avoids the problem.
Scripts can get or change the context of the body as follows:
w.appendText(s)# Append s to end of body text.w.delete(i,j=None)# Delete characters from i to j.w.deleteTextSelection()# Delete the selected text, if any.s=w.get(i,j=None)# Return the text from i to j.s=w.getAllText# Return the entire body text.i=w.getInsertPoint()# Return the location of the cursor.s=w.getSelectedText()# Return the selected text, if any.i,j=w.getSelectionRange(sort=True)# Return the range of selected text.w.setAllText(s)# Set the entire body text to s.w.setSelectionRange(i,j,insert=None)# Select the text.
Notes:
These are only the most commonly-used methods. For more information, consult Leo’s source code.
i and j are zero-based indices into the the text. When j is not specified, it defaults to i. When the sort parameter is in effect, getSelectionRange ensures i <= j.
color is a Tk color name, even when using the Gt gui.
The following script imports files from a given directory and all subdirectories:
c.recursiveImport(dir_='path to file or directory',kind='@clean',# or '@file' or '@auto'one_file=False,# True: import only one file.safe_at_file=False,# True: generate @@clean nodes.theTypes=None,# Same as ['.py'])
from PyQt5 import QtGui
w = QtGui.QWidget()
w.resize(250, 150)
w.move(300, 300)
w.setWindowTitle(‘Simple test’)
w.show()
When the script exits the sole reference to the window, w, ceases to exist, so the window is destroyed (garbage collected). To keep the window open, add the following code as the last line to keep the reference alive:
g.app.scriptsDict['my-script_w']=w
Note that this reference will persist until the next time you run the execute-script. If you want something even more permanent, you can do something like:
Scripts and plugins can call g.app.idleTimeManager.add_callback(callback) to cause
the callback to be called at idle time forever. This should suffice for most purposes:
For greater control, g.IdleTime is a thin wrapper for the Leo’s IdleTime class. The IdleTime class executes a handler with a given delay at idle time. The handler takes a single argument, the IdleTime instance:
defhandler(it):"""IdleTime handler. it is an IdleTime instance."""delta_t=it.time-it.starting_timeg.trace(f"{it.count}{c.shortFileName()}{delta_t:2.2}")ifit.count>=5:g.trace('done')it.stop()# Execute handler every 500 msec. at idle time.it=g.IdleTime(handler,delay=500)ifit:it.start()
The code creates an instance of the IdleTime class that calls the given handler at idle time, and no more than once every 500 msec. Here is the output:
It is dead easy for scripts, including @button scripts, plugins, etc., to drive any external processes, including compilers and interpreters, from within Leo.
The first section discusses three ways of calling subprocess.popen directly or via Leo helper functions.
The second section discusses the BackgroundProcessManager class. Leo’s pylint command uses this class to run pylint commands sequentially without blocking Leo. Running processes sequentially prevents unwanted interleaving of output.
The last two sections discuss using g.execute_shell_commands and g.execute_shell_commands_with_options.
Calling subprocess.popen is often simple and good. For example, the following executes the ‘npm run dev’ command in a given directory. Leo continues, without waiting for the command to return:
os.chdir(base_dir)subprocess.Popen('npm run dev',shell=True)
The following hangs Leo until the command completes:
g.execute_shell_commands_with_options is more flexible. It allows scripts to get both the starting directory and the commands themselves from Leo’s settings. Its signature is:
defexecute_shell_commands_with_options(base_dir=None,c=None,command_setting=None,commands=None,path_setting=None,warning=None,):''' A helper for prototype commands or any other code that runs programs in a separate process. base_dir: Base directory to use if no path_setting is given. commands: A list of commands, for g.execute_shell_commands. commands_setting: Name of @data setting for commands. path_setting: Name of @string setting for the base directory. warning: A warning to be printed before executing the commands. '''
g.app.backgroundProcessManager is the singleton instance of the BackgroundProcessManager (BPM) class. This class runs background processes, without blocking Leo. The BPM manages a queue of processes, and runs them one at a time so that their output remains separate.
BPM.start_process(c, command, kind, fn=None, shell=False) adds a process to the queue that will run the given command:
g.execute_shell_command takes a single argument, which may be either a string or a list of strings. Each string represents one command.
g.execute_shell_command executes each command in order, waiting for each command to complete, except those commands starting with ‘&’.
Examples:
# Run the qml app in a separate process:g.execute_shell_commands('qmlscene /test/qml_test.qml')# List the contents of a directory:g.execute_shell_commands(['cd ~/test','ls -a',])# Run a python test in a separate process.g.execute_shell_commands('python /test/qt_test.py')
g.execute_shell_commands_with_options inits an environment and then calls g.execute_shell_commands. See Leo’s source code for details.
On startup, Leo looks for two arguments of the form:
--scriptscriptFile
If found, Leo enters batch mode. In batch mode Leo does not show any windows. Leo assumes the scriptFile contains a Python script and executes the contents of that file using Leo’s Execute Script command. By default, Leo sends all output to the console window. Scripts in the scriptFile may disable or enable this output by calling app.log.disable or app.log.enable
Scripts in the scriptFile may execute any of Leo’s commands except the Edit Body and Edit Headline commands. Those commands require interaction with the user. For example, the following batch script reads a Leo file and prints all the headlines in that file:
path=g.os_path_finalize_join(g.app.loadDir,'..','test','test.leo')assertg.os_path_exists(path),pathg.app.log.disable()# disable reading messages while opening the filec2=g.openWithFileName(path)g.app.log.enable()# re-enable the log.forpinc2.all_positions():g.es(g.toEncodedString(p.h,"utf-8"))
Leo’s @pyplot nodes support
matplotlib.
@pyplot nodes start with @pyplot in the headline. The rest of the headline is comments. These nodes should contain matplotlib scripts that create figures or animations. Like this:
fig2=plt.figure()x=np.arange(-9,10)y=np.arange(-9,10).reshape(-1,1)base=np.hypot(x,y)images=[]foraddinnp.arange(15):images.append((plt.pcolor(x,y,base+add,norm=plt.Normalize(0,30)),))animation=animation.ArtistAnimation(fig2,images,interval=50,repeat_delay=3000,blit=True)g.app.permanentScriptDict['animations']=animation# Keep a python reference to the animation, so it will complete.
Notes
If the viewrendered (VR) pane is open, Leo will display the animation in the VR pane whenever the user selects the @pyplot node. This has been tested only with the viewrendered.py, not the viewrendered2.py plugin.
In addition to c, g, and p, the VR code predefines several other vars. The VR code does the following imports:
To display images and animations in an external window, don’t put the script in an @pyplot node. Instead, put the script in a regular node, with the following modifications:
plt.ion()# sets interactive mode. Prevents this message:# QCoreApplication::exec: The event loop is already runningplt.show()
Bugs
Once you use the VR pane to display an image, you can’t (at present) display an image externally.
The VR plugin will refuse to close the VR pane if it ever displays an @pyplot image or animation. This prevents Leo from hard crashing in the pyplot code. As a workaround, you can resize the VR pane so it is effectively hidden.
Scripts can easily determine what directives are in effect at a particular position in an outline. c.scanAllDirectives(p) returns a Python dictionary whose keys are directive names and whose values are the value in effect at position p. For example:
d=c.scanAllDirectives(p)g.es(g.dictToString(d))
In particular, d.get(‘path’) returns the full, absolute path created by all @path directives that are in ancestors of node p. If p is any kind of @file node (including @file, @auto, @clean, etc.), the following script will print the full path to the created file: