Subsections


5. Scripting

This chapter documents some additional features of the Notion configuration and scripting interface that can be used for more advanced scripting than the basic configuration explained in chapter 3.

5.1 Protected mode and (un-)safe functions

Some of Notion's functions call other functions in so-called ``protected mode''. In particular this is true for all the iterator functions mentioned in 6. If a function fn is called in protected mode, the call will only succeed if fn is considered ``safe'', and will otherwise fail with the message ``Ignoring call to unsafe function fn in restricted mode.''.

Let us try to explain the rationale behind this behavior and the heuristic for deciding when a function can be considered safe through an example. The function WMPlex.managed_i iterates over all regions managed by an mplex and calls iterfn on each of these regions. If iterfn was able to alter the list of regions managed by mplex, say by adding additional regions, this iteration could easily turn into an infinite loop or lead to inconsistent behavior.

To avoid situations like this, in order to be considered safe, a function must not alter any of the internal data structures of Notion.

If you ever need to use an unsafe function in combination with an iterator, there is a simple workaround. In the example above, instead of writing WMPlex.managed_i(mplex, iterfn), which fails if iterfn is unsafe, you can use the following code snippet to achieve the same result:

managed_regions={}
mplex:managed_i(function(reg)
    table.insert(managed_regions, reg) return true
end)
for _,reg in ipairs(managed_regions) do
    iterfn(reg)
end


5.2 Hooks

Hooks are lists of functions to be called when a certain event occurs. There are two types of them; normal and ``alternative'' hooks. Normal hooks do not return anything, but alt-hooks should return a boolean indicating whether it handled its assigned task successfully. In the case that true is returned, remaining handlers are not called.

Hook handlers are registered by first finding the hook with ioncore.get_hook and then calling WHook.add on the (successful) result with the handler as parameter. Similarly handlers are unregistered with WHook.remove. For example:

ioncore.get_hook("ioncore_snapshot_hook"):add(
    function() print("Snapshot hook called.") end
)

In this example the hook handler has no parameters, but many hook handlers do. The types of parameters for each hook are listed in the hook reference, section 6.9.

Note that many of the hooks are called in ``protected mode'' and can not use any functions that modify Notion's internal state.

5.3 Referring to regions

5.3.1 Direct object references

All Notion objects are passed to Lua scripts as 'userdatas', and you may safely store such object references for future use. The C-side object may be destroyed while Lua still refers to the object. All exported functions gracefully fail in such a case, but if you need to explicitly test that the C-side object still exists, use obj_exists.

As an example, the following short piece of code implements bookmarking:

local bookmarks={}

-- Set bookmark bm point to the region reg
function set_bookmark(bm, reg)
    bookmarks[bm]=reg
end

-- Go to bookmark bm
function goto_bookmark(bm)
    if bookmarks[bm] then
        -- We could check that bookmarks[bm] still exists, if we
        -- wanted to avoid an error message.
        bookmarks[bm]:goto()
    end
end

5.3.2 Name-based lookups

If you want to a single non-WClientWin region with an exact known name, use ioncore.lookup_region. If you want a list of all regions, use ioncore.region_list. Both functions accept an optional argument that can be used to specify that the returned region(s) must be of a more specific type. Client windows live in a different namespace and for them you should use the equivalent functions ioncore.lookup_clientwin and ioncore.clientwin_list.

To get the name of an object, use WRegion.name. Please be aware, that the names of client windows reflect their titles and are subject to changes. To change the name of a non-client window region, use WRegion.set_name.

5.4 Alternative winprop selection criteria

It is possible to write more complex winprop selection routines than those described in section 3.5. To match a particular winprop using whatever way you want to, just set the match field of the winprop to a function that receives the as its parameters the triple (prop, cwin, id), where prop is the table for the winprop itself, cwin is the client window object, and id is the WClientWin.get_ident result. The function should return true if the winprop matches, and false otherwise. Note that the match function is only called after matching against class/role/instance.

The title of a client window can be obtained with WRegion.name. If you want to match against (almost) arbitrary window properties, have a look at the documentation for the following functions, and their standard Xlib counterparts: ioncore.x_intern_atom (XInternAtom), ioncore.x_get_window_property (XGetWindowProperty), and ioncore.x_get_text_property (XGetTextProperty).


5.5 Writing ion-statusd monitors

All statusbar meters that do not monitor the internal state of Notion should go in the separate ion-statusd program.

Whenever the user requests a meter `%foo' or `%foo_bar' to be inserted in a statusbar, mod_statusbar asks ion-statusd to load statusd_foo.lua on its search path (same as that for Notion-side scripts). This script should then supply all meters with the initial part `foo'.

To provide this value, the script should simply call statusd.inform with the name of the meter and the value as a string. Additionally the script should provide a 'template' for the meter to facilitate expected width calculation by mod_statusbar, and may provide a 'hint' for colour-coding the value. The interpretation of hints depends on the graphical style in use, and currently the stock styles support the `normal', `important' and `critical' hints.

In our example of the 'foo monitor', at script initialisation we might broadcast the template as follows:

statusd.inform("foo_template", "000")

To inform mod_statusbar of the actual value of the meter and indicate that the value is critical if above 100, we might write the following function:

local function inform_foo(foo)
    statusd.inform("foo", tostring(foo))
    if foo>100 then
        statusd.inform("foo_hint", "critical")
    else
        statusd.inform("foo_hint", "normal")
    end
end

To periodically update the value of the meter, we must use timers. First we must create one:

local foo_timer=statusd.create_timer()

Then we write a function to be called whenever the timer expires. This function must also restart the timer.

local function update_foo()
    local foo= ... measure foo somehow ...
    inform_foo(foo)
    foo_timer:set(settings.update_interval, update_foo)
end

Finally, at the end of our script we want to do the initial measurement, and set up timer for further measurements:

update_foo()

If our scripts supports configurable parameters, the following code (at the beginning of the script) will allow them to be configured in cfg_statusbar.lua and passed to the status daemon and our script:

local defaults={
    update_interval=10*1000, -- 10 seconds
}
                
local settings=table.join(statusd.get_config("foo"), defaults)