Scripts and ipelets

Most of the Ipe program is written in the scripting language Lua, and the same Lua library that Ipe uses to read, write, and modify documents is available for your own programs. Interesting classes like vectors, matrices, Ipe objects, and Ipe documents are made available through this library. There are two ways to use it:

  • An Ipe script is a Lua program that you run from the command line using ipescript. A script reads an Ipe document, does something with it, and typically writes a new document or prints some information. Scripts are good for batch processing and for tasks that you want to run on many files.

  • An ipelet is an extension of the Ipe editor. It is a Lua file that adds one or more entries to the Ipelets menu, and works on the document you are editing, usually on the objects you have selected. Ipelets are good for functions that you want to use interactively.

A quick aside: Sometimes you will already have code that provides some geometric computation in another programming language such as C++. Ipe provides an easy mechanism, sufficient for many applictions, to implement your ipelet entirely in C++ . It is described in the Ipelib documentation.

But back to Lua: scripts and ipelets share most of what they need to know. They use the same classes for documents, pages, objects, and geometry, and the same conventions. The differences are in how they are started and how they treat the document:

Script

Ipelet

Started by

ipescript on the command line

the Ipelets menu (or a keyboard shortcut) in the Ipe editor

Document

loads and saves it itself

the document being edited, accessed through the model

Changing the document

directly

never directly: it registers an undo item with the model

User interface

command-line arguments, print, files

dialogs, selection, mouse position, status bar

Placed in

Ipe’s scripts folder

Ipe’s ipelets folder

The rest of this chapter first introduces the common Lua library, then describes scripts and ipelets in turn. The complete reference for the Lua library is the Ipelib documentation (look for the pages Lua bindings for …, and Ipelets). The best documentation of what is possible are the existing scripts and ipelets that come with Ipe: the scripts are in the scripts folder, and the ipelets in the ipelets folder of the Ipe installation (and in the Ipe source distribution).

The Lua library in a nutshell

All of Ipe’s Lua functions are in a table called ipe. Here are the most important things you need to know.

local doc = assert(ipe.Document("figure.ipe"))  -- load a document
print(#doc)                    -- number of pages
local page = doc[1]            -- pages are numbered from 1
for i, obj, selected, layer in page:objects() do
  print(i, obj:type(), layer)  -- "path", "text", "group", ...
end
local v = ipe.Vector(100, 200)
local m = ipe.Translation(v) * ipe.Rotation(math.pi / 2)
doc:save("result.ipe")
  • Pages, views, objects, and layers are numbered from 1, as usual in Lua. Layers are identified by their name, not by a number.

  • The coordinate system is the one of the Ipe file: units are PostScript points (1/72 inch), the origin is at the lower left of the paper, and the y-axis points up.

  • The geometric classes ipe.Vector, ipe.Matrix, ipe.Line, ipe.Segment, ipe.Bezier, and ipe.Arc are immutable values: operations create new values. Only ipe.Rect can be changed (r:add(v) extends the rectangle). Vectors and matrices have operators: v + w, 2 * v, m * v, m1 * m2.

  • The objects you get from a page (page[i] or page:objects()) are the page’s own objects, not copies. Changing such an object changes the page. Use obj:clone() to make a copy, and page:clone() to make a copy of the entire page.

  • Attributes of objects are set and read using obj:set(name, value) and obj:get(name). The attribute names are those of the Ipe editor’s property boxes, for instance stroke, fill, pen, and dashstyle. The value can be an absolute value (a number, or a color given as {r=0.5, g=0.5, b=0.5}), or a symbolic name from the stylesheet such as "red" or "normal".

  • Functions that read files return nil and an error message when they fail. Calling a function with illegal arguments (such as a page number that does not exist) raises a Lua error.

  • New objects are created with ipe.Path, ipe.Text, ipe.Reference (a symbol), ipe.Group, and ipe.Image. The first three take a table with the attributes of the object (in an ipelet, model.attributes is such a table) and the data of the object. A path’s geometry is described by a shape, a Lua table. For instance, a circle with radius r and center c is { { type="ellipse"; ipe.Matrix(r, 0, 0, r, c.x, c.y) } }. The reference documentation explains shapes in detail.

Writing scripts

A script is a Lua file in one of Ipe’s script folders. The script count-objects.lua is run by saying

ipescript count-objects figure.ipe

(Do not write the extension .lua.) The command-line arguments are available in the global table argv. Ipe looks for scripts in the current directory, in the user’s script folder (~/.local/share/ipe/scripts on Linux, ~/Library/Ipe/Scripts on MacOS), and in the system-wide script folder. The environment variable IPESCRIPTS changes this, see Environment variables.

Here is a script that prints how many objects of each type there are on every page of a document:

-- Count the objects in an Ipe document, page by page.
--
-- Usage: ipescript count-objects <document>

if #argv ~= 1 then
  io.stderr:write("Usage: ipescript count-objects <document>\n")
  return
end

local doc = assert(ipe.Document(argv[1]))

for pno, page in doc:pages() do
  -- count the objects of every type (group, text, path, image, reference)
  local counts = {}
  for _, obj, _, layer in page:objects() do
    local t = obj:type()
    counts[t] = (counts[t] or 0) + 1
  end
  local parts = {}
  for t, n in pairs(counts) do
    parts[#parts + 1] = n .. " " .. t
  end
  table.sort(parts)
  print(string.format("Page %d: %d objects (%s)", pno, #page, table.concat(parts, ", ")))
end

count-objects.lua

A script can do anything that Lua can do, and has the entire document at its disposal, so it can modify it directly. Here is a script that makes all paths of a document red, and saves the result in a new file:

-- Make all paths in an Ipe document red.
--
-- Usage: ipescript red-paths <input> <output>
--
-- A script owns the document, so it can modify it directly.
-- Compare with ipelet-red-paths.lua, which does the same inside Ipe.

if #argv ~= 2 then
  io.stderr:write("Usage: ipescript red-paths <input> <output>\n")
  return
end

local doc = assert(ipe.Document(argv[1]))

for _, page in doc:pages() do
  for i, obj in page:objects() do
    if obj:type() == "path" then
      -- change the page (and not only the object), so that the page
      -- knows that the object's bounding box may have changed
      page:setAttribute(i, "stroke", "red")
    end
  end
end

assert(doc:save(argv[2]))

red-paths.lua

Here is a Lua script that reads an Ipe document and then recenters each page on the paper.

-- recenter.lua
-- center objects on each page of the document

if #argv ~= 2 then
  io.stderr:write("Usage: ipescript recenter <inputfile> <outputfile>\n")
  return
end

inname = argv[1]
outname = argv[2]

doc = assert(ipe.Document(inname))

-- make sure we have size information for text
assert(doc:runLatex())

layout = doc:sheets():find("layout")
fs = layout.framesize

for i,p in doc:pages() do
  box = ipe.Rect()
  for j = 1,#p do
    box:add(p:bbox(j))
  end
  nx = (fs.x - box:width()) / 2
  ny = (fs.y - box:height()) / 2
  trans = ipe.Vector(nx, ny) - box:bottomLeft()
  m = ipe.Translation(trans)
  for j = 1,#p do
    p:transform(j, m)
  end
end
doc:save(outname)

recenter.lua

The following script prints out all the gradients defined in a document:

-- -*- lua -*-

function printTable(t, indent)
  for k in pairs(t) do
    local v = t[k]
    if type(v) == "table" then 
      print(indent .. k .. ":")
      printTable(v, "  " .. indent)
    else
      print(indent .. k .. ": " .. tostring(v))
    end
  end
end

if #argv ~= 1 then
  io.stderr:write("Usage: ipescript show-gradients <ipe-document>\n")
  return
end

local figname = argv[1]
local doc = assert(ipe.Document(figname))
local s = doc:sheets()

for _,w in ipairs(s:allNames("gradient")) do
  print("Gradient: ", w)
  local g = s:find("gradient", w)
  printTable(g, "  ")
end

show-gradients.lua

Here is a script that shows how to add a gradient to a document (the same technique works for tilings and effects). The script simply adds to the top style sheet - in general this is not a good idea, as you shouldn’t modify the distributed style sheets (updating style sheets from Ipe would lose the modifications), and if the document has only the built-in style sheet, the change would not be preserved at all.

-- -*- lua -*-

if #argv ~= 1 then
  io.stderr:write("Usage: ipescript add-gradient <ipe-document>\n")
  return
end

local figname = argv[1]
local doc = assert(ipe.Document(figname))

local sheet = [[<ipestyle>
<gradient name="ball" type="radial" coords="-4 10 2 0 0 18">
 <stop offset="0" color="1 1 0"/>
 <stop offset="1" color="0 1 0"/>
</gradient>
</ipestyle>]]

local ts = ipe.Sheet(nil, sheet)

local s = doc:sheets()
for i = 1,s:count() do
  print(s:sheet(i):name())
end

-- add the gradient to the sheet on top of the stack
-- in general it is not a good idea to change the "basic" sheet,
-- better verify if a custom sheet for gradients exists already
-- and create it if not.
s:sheet(1):addFrom(ts, "gradient", "ball")
-- assert(doc:runLatex())
doc:save(figname .. ".new.ipe")

add-gradient.lua

Ipe comes with several scripts that are good starting points for your own scripts. For instance, onepage collapses all pages of a document into the layers of a single page, scratchpad prepares a presentation for annotating during the talk, and update-styles updates the stylesheets in many documents.

Writing ipelets

An ipelet is a Lua file in one of Ipe’s ipelet folders. Ipe loads all ipelets when it starts. The ipelet folders are the system-wide folder that comes with Ipe, and the user’s own folder (~/.local/share/ipe/ipelets on Linux, ~/Library/Ipe/Ipelets on MacOS; you can find the exact location under Help ‣ Show configuration, and change it using the environment variable IPELETPATH, see Environment variables).

An ipelet defines some global variables that tell Ipe about it:

label

the name of the ipelet in the Ipelets menu. A file that does not define a label is not shown in the menu (this is how you can write customizations of Ipe).

about

a string shown in Help ‣ Ipelet information.

run

the function to call when the ipelet is invoked.

methods

for an ipelet with several functions, a table with one entry for each function. Each entry is a table with a label (the text of its menu entry) and optionally a run function (if it has none, the global run is called).

category

optional: the submenu of the Ipelets menu in which the ipelet appears. (The menu is set up by prefs.ipelet_menu.)

When the user invokes the ipelet, Ipe calls the run function with two arguments: the model, which represents the document window, and the number of the method (always 1 for an ipelet with a single function).

The simplest possible ipelet draws a circle at the mouse position:

----------------------------------------------------------------------
-- Draw a circle at the mouse position
----------------------------------------------------------------------
--
-- Creating a single new object is the simplest case: model:creation
-- registers the undo item for you.

label = "Circle"

about = [[
Draw a circle with radius 32 at the current mouse position.
]]

function run(model)
  local center = model.ui:pos()   -- mouse position, after snapping
  local radius = 32
  -- an ellipse is given by the matrix that maps the unit circle to it
  local shape = { { type = "ellipse";
		    ipe.Matrix(radius, 0, 0, radius, center.x, center.y) } }
  local obj = ipe.Path(model.attributes, shape)
  model:creation("create circle", obj)
end

ipelet-circle.lua

Save this in your ipelet folder as circle.lua, restart Ipe, and you will find Circle in the Ipelets menu. If you set prefs.developer = true in your customization.lua (see Customizing Ipe), the Help menu has a Developer submenu with an entry Help ‣ Developer ‣ Reload ipelets, so you do not need to restart Ipe after every change.

Keyboard shortcuts can be assigned in customization.lua using the name of the ipelet’s file (without the extension) and the number of the method, for instance shortcuts.ipelet_1_circle = "Ctrl+Alt+C".

The ipelet’s environment

Each ipelet runs in a private environment, so you do not have to worry about polluting the global namespace. The following names are available inside an ipelet: ipe, ipeui, math, string, table, assert, ipairs, pairs, print, tonumber, tostring, and the Ipe tables prefs, config, shortcuts, and mouse. Everything else, such as type, pcall, io, os, or the Lua code of Ipe, must be accessed through _G, for instance _G.type(x) or _G.revertOriginal. (Writing type(x) gives the error “attempt to call a nil value (global ‘type’)”.)

Text printed with print appears on the terminal from which you started Ipe.

The model, and why you must not touch the document

The model is a Lua object with these members:

model.doc

the document being edited.

model.pno, model.vno

the number of the current page and view.

model.file_name

the name of the document’s file, or nil if the document has not been saved yet.

model:page()

the current page.

model:selection()

a table with the numbers of the selected objects of the current page.

model.attributes

the attributes currently set in the property boxes of the user interface (a table you can pass to ipe.Path and friends).

model.snap

the current snapping settings, see The snap settings.

model.ui

the user interface, with for instance model.ui:pos() (the mouse position after snapping), model.ui:win() (the window, to be used as the parent of dialogs), model.ui:zoom() (the zoom of the canvas, in pixels per point), and model.ui:explain(text) (shows text in the status bar). All methods are listed in the Ipelib documentation.

model:register(t)

registers an undo item and performs its change, see below.

model:creation(label, obj)

inserts a new object into the active layer of the current page, and makes it the only selected object (registers the undo item).

model:transformation(label, matrix, deselect)

transforms the selected objects by a matrix, and deselects them if deselect is true (registers the undo item).

model:setAttribute(name, value)

sets an attribute of the selected objects (registers the undo item).

model:warning(text, details)

shows a warning box.

model:getString(prompt, caption, initial)

asks the user for a string (returns nil if the user cancels); caption and initial are optional.

model:getDouble(caption, label, value, minimum, maximum)

asks the user for a number between minimum and maximum, with value as the initial value (returns nil if the user cancels or enters something illegal).

An ipelet has access to the live document, but it must not modify it directly. The Ipe editor keeps an undo stack, and it does not notice if the document changes behind its back. An ipelet that changes the page directly breaks undo and redo, may corrupt the document, and does not mark the document as modified. Instead, the ipelet makes its changes through an undo item. This is a table with at least these fields:

label

a text for the status bar and the undo history (Undo ‘label’).

pno, vno

the page and view to show after an undo or redo.

undo, redo

functions that are called with the undo item and the document. redo makes the change, undo reverts it.

The ipelet creates the item and passes it to model:register(t), which calls t.redo(t, model.doc) to perform the change, remembers the selection, and puts the item on the undo stack. The ipelet should not change the document itself, and neither should it keep any reference to the objects of the document: the item’s redo and undo functions are the only code that may touch the document after the item has been registered.

There are three ways to create an undo item, from the simplest to the most general one.

1. Use one of the model’s helper functions. model:creation(label, obj) inserts a new object on the current page (as in the circle ipelet above), model:transformation(label, matrix) transforms the selected objects, and model:setAttribute(name, value) sets an attribute of the selected objects. These functions create and register the undo item for you. The next ipelet uses model:transformation, and also shows how to ask the user for a value in a dialog:

----------------------------------------------------------------------
-- Scale the selection by a factor entered by the user
----------------------------------------------------------------------
--
-- This ipelet shows a dialog to ask for the scale factor, and then uses
-- model:transformation, which takes care of undo and redo when all you
-- want to do is to transform the selected objects by a matrix.

label = "Scale selection"

about = [[
Scale the selected objects around the center of their bounding box.
]]

function run(model)
  local selection = model:selection()  -- table with object numbers
  if #selection == 0 then
    model:warning("Nothing is selected")
    return
  end

  local d = ipeui.Dialog(model.ui:win(), "Scale selection")
  d:add("label", "label", { label = "Scale factor" }, 1, 1)
  d:add("factor", "input", { select_all = true }, 1, 2)
  d:set("factor", "2")
  d:addButton("ok", "&Ok", "accept")
  d:addButton("cancel", "&Cancel", "reject")
  if not d:execute() then return end

  local factor = tonumber(d:get("factor"))
  if not factor or factor <= 0 then
    model:warning("The scale factor must be a positive number")
    return
  end

  local page = model:page()
  local box = ipe.Rect()
  for _, i in ipairs(selection) do box:add(page:bbox(i)) end
  local center = (box:bottomLeft() + box:topRight()) * 0.5

  local m = ipe.Translation(center) * ipe.Matrix(factor, 0, 0, factor)
            * ipe.Translation(-center)
  model:transformation("scale selection", m)
end

ipelet-scale.lua

2. Work on a copy of the page. When the ipelet changes several objects, the easiest approach is to make two copies of the page: the original, and the final page in which the ipelet makes all its changes. The undo item then simply swaps the page in the document between these two copies, using the functions _G.revertOriginal and _G.revertFinal. This is the pattern used by most of the ipelets that come with Ipe:

----------------------------------------------------------------------
-- Make the selected paths red
----------------------------------------------------------------------
--
-- An ipelet must not modify the document directly.  Instead, it prepares
-- the new version of the page, and registers an undo item that switches
-- between the original and the final page.  This is the simplest way to
-- write an ipelet that changes existing objects.

label = "Red paths"

about = [[
Make the stroke color of all selected paths red.
]]

function run(model)
  local t = { label = "make paths red",
	      pno = model.pno,
	      vno = model.vno,
	      original = model:page():clone(),
	      final = model:page():clone(),
	      undo = _G.revertOriginal,
	      redo = _G.revertFinal,
	    }
  local changed = false
  for i, obj, sel, layer in t.final:objects() do
    if sel and obj:type() == "path" then
      if t.final:setAttribute(i, "stroke", "red") then changed = true end
    end
  end
  if changed then
    model:register(t)
  else
    model.ui:explain("No selected path to make red")
  end
end

ipelet-red-paths.lua

The same pattern also works to create several objects. Here the ipelet collects what it needs to know about the selected objects first, and only then inserts new objects, since inserting objects while iterating over the page would also visit the new objects:

----------------------------------------------------------------------
-- Mark the center of the bounding box of every selected object
----------------------------------------------------------------------
--
-- This ipelet creates several new objects at once.  It uses the same
-- undo item as ipelet-red-paths.lua: the original page and the final page.

label = "Mark centers"

about = [[
Create a mark at the center of the bounding box of every selected object.
]]

function run(model)
  local page = model:page()
  local layer = page:active(model.vno)
  local t = { label = "mark centers",
	      pno = model.pno,
	      vno = model.vno,
	      original = page:clone(),
	      final = page:clone(),
	      undo = _G.revertOriginal,
	      redo = _G.revertFinal,
	    }

  -- Collect the centers first: inserting objects while iterating over
  -- the page would visit the new objects as well.
  local centers = {}
  for i, obj, sel in t.final:objects() do
    if sel then
      local box = t.final:bbox(i)
      centers[#centers + 1] = (box:bottomLeft() + box:topRight()) * 0.5
    end
  end

  for _, center in ipairs(centers) do
    -- model.attributes are the attributes currently set in the user interface
    local mark = ipe.Reference(model.attributes, "mark/disk(sx)", center)
    t.final:insert(nil, mark, nil, layer)
  end

  if #centers > 0 then
    model:register(t)
  end
end

ipelet-mark-centers.lua

3. Write your own undo and redo functions. Copying a page is easy, but wasteful if the ipelet only changes a small thing, such as the properties of a layer. Then the undo item can remember what it needs to revert its action. The next example also shows an ipelet with several methods:

----------------------------------------------------------------------
-- An ipelet with several methods, and an undo item with explicit
-- undo and redo functions
----------------------------------------------------------------------
--
-- Cloning the whole page is simple, but wasteful when only a small
-- thing changes.  Here the undo item remembers just what is needed to
-- go back: which layers were locked before.

label = "Lock layers"

about = [[
Lock or unlock all layers of the current page.
]]

-- The method is called with the model and the number of the method.
local function lock_layers(model, num)
  local locked = (num == 1)
  local page = model:page()
  local was_locked = {}
  for _, layer in ipairs(page:layers()) do
    was_locked[layer] = page:isLocked(layer)
  end

  local t = { label = locked and "lock all layers" or "unlock all layers",
	      pno = model.pno,
	      vno = model.vno,
	      locked = locked,
	      was_locked = was_locked,
	    }
  -- redo is also called to perform the action the first time
  t.redo = function (t, doc)
    for layer in pairs(t.was_locked) do
      doc[t.pno]:setLocked(layer, t.locked)
    end
  end
  t.undo = function (t, doc)
    for layer, was in pairs(t.was_locked) do
      doc[t.pno]:setLocked(layer, was)
    end
  end
  model:register(t)
end

methods = {
  { label = "Lock all layers", run = lock_layers },
  { label = "Unlock all layers", run = lock_layers },
}

ipelet-lock-layers.lua

If your ipelet changes the stylesheets of the document, set t.style_sheets_changed = true in the undo item, so that Ipe updates the lists of symbolic names in the user interface after an undo or redo.

The snap settings

The table model.snap contains the snapping settings of the editor (see Snapping). The same table is passed to C++ ipelets and to ui:setSnap. Its fields are:

snapvtx, snapctl, snapbd, snapint, snapgrid, snapangle, snapcustom, snapauto

booleans that say whether snapping to vertices, control points, boundaries, intersections, the grid, angles, the custom grid, and automatic angular snapping is switched on.

grid_visible

boolean: is the grid shown?

gridsize

the grid size in points (an integer).

anglesize

the angle for angular snapping, in degrees.

snap_distance

the distance in pixels within which snapping takes effect.

with_axes

boolean: are the axes of the axis system shown?

origin, orientation

the origin of the axis system (a vector) and the direction of its x-axis (an angle in radians).

Dialogs

Ipelets can show dialogs using ipeui.Dialog, as in the scale example above. Dialogs can contain labels, input fields, text boxes, lists, combo boxes, check boxes, and image previews, arranged in a grid. See the Ipelib documentation (Lua bindings for dialogs and menus) for all the details, and the ipelets search-replace and selectby that come with Ipe for more examples.

Testing an ipelet without the editor

It is tedious to test an ipelet by starting the Ipe editor, loading a document, selecting objects, and invoking the ipelet. The script run-ipelet that comes with Ipe runs an ipelet on a document from the command line, with a model that behaves like the one in the editor (it uses the real model of the Ipe editor, including its undo stack):

ipescript run-ipelet circle.lua figure.ipe result.ipe

The ipelet is loaded in the same restricted environment as in the editor. It is run with all objects of the first page selected (use the options --select none or --select 1,3 to change this, --page, --view, and --method to change the page, the view, and the number of the method to run). The result is saved as result.ipe.

The script then undoes and redoes the changes made by the ipelet, using Ipe’s own undo and redo, and checks that the page is restored exactly. This finds the most common mistake in ipelets, namely not registering an undo item or not reverting everything in the undo function:

$ ipescript run-ipelet scale.lua figure.ipe result.ipe factor=3
Running scale on page 1, view 1 with 3 selected objects
[dialog] Scale selection
[dialog]   factor = 3
[explain] scale selection
1 undo item(s) registered: scale selection
[explain] Undo 'scale selection'
Undo restores the original page: OK
[explain] Redo 'scale selection'
Redo restores the result: OK
Saved result.ipe

Dialogs cannot be shown by this script. Instead, a dialog behaves as if the user had accepted it (or canceled it, if you pass --cancel), and with every element having the value set by the ipelet. You can give a different value by adding arguments of the form name=value, as in the example above, where factor=3 sets the value of the dialog element factor. The exit status of run-ipelet is not zero if the ipelet fails or the undo check finds a problem.

run-ipelet takes the Lua code of the Ipe editor from your Ipe installation. If you want to test with a different version (for instance a source tree), set the environment variable IPELUAPATH to a Lua path such as /path/to/ipe/src/ipe/lua/?.lua.

run-ipelet has no canvas and does not run LaTeX, so it cannot test ipelets that depend on them. It is very useful for everything else, and also makes it possible to write automatic tests for your ipelets.

C++ ipelets

Ipelets can also be written in C++, which is useful if you already have code for some geometric computation, or want to use a library such as CGAL. Such an ipelet consists of a shared library, and a small Lua file that declares its label and its methods, and calls the C++ code. The details are described in the Ipelib documentation (see the page Ipelets).

Writing scripts and ipelets with an AI assistant

AI assistants (large language models) can write simple scripts and ipelets for you. The result will be much better if the assistant has the right information. For this purpose, the Ipe manual is also published as plain text, following the llms.txt convention: the file llms.txt is an index for assistants, and llms-full.txt contains this chapter and the complete reference of the Lua library in a single file, which you can give to your assistant (or paste into its instructions). If you work with a coding assistant in your ipelet folder, copy the file AGENTS-ipelets.md there under the name AGENTS.md: many assistants read such a file automatically. It tells the assistant where to find the documentation, summarizes the most important rules, and asks it to test its ipelets with run-ipelet.

If you prefer to select the material yourself, we suggest to give it:

  • this chapter of the manual, and the reference of the Lua library (the page Lua bindings of the Ipelib documentation, which lists all functions, and the page about dialogs if your ipelet has a dialog),

  • one or two existing scripts or ipelets that are similar to what you need (the examples in this chapter, or the ones that come with Ipe).

Tell the assistant that it must write a script or an ipelet (it matters, see above), what the objects it works on are, and what should happen. Then check the result carefully before using it on a precious document.

If you use an assistant that can run commands, tell it about run-ipelet: it can then test the ipelet on a sample document, see the error messages and the result of the undo check, and fix its code without your help. Ipelets that modify the document without registering an undo item are the most common problem of ipelets written without testing. Here is the output of run-ipelet for the example ipelet-wrong.lua, which changes the page directly:

----------------------------------------------------------------------
-- WRONG: an ipelet that modifies the document without an undo item
----------------------------------------------------------------------
--
-- This ipelet is an example of what *not* to do.  The page is changed
-- directly, so Ipe does not know about the change: the document is not
-- marked as modified, and undo and redo no longer work correctly.
-- Run it with ipescript run-ipelet to see the warning.

label = "Wrong"

about = "Do not write ipelets like this."

function run(model)
  local page = model:page()
  for i, obj, sel in page:objects() do
    if sel and obj:type() == "path" then
      page:setAttribute(i, "stroke", "red")   -- no undo item!
    end
  end
end
$ ipescript run-ipelet ipelet-wrong.lua figure.ipe result.ipe
Running ipelet-wrong on page 1, view 1 with 3 selected objects
The ipelet did not register an undo item.
WARNING: the page has been modified without registering an undo item!
Saved result.ipe