ion.app

This module is the configuration entry point. It hosts both persistent application services - lifecycle and environment functions plus dynamic data - and the declarative registries that the config file populates and the compositor consults at runtime.

When code runs

Top-level config code runs at load time - that is where the declarative registries and preferences are populated. Live state in ion.wm only exists once the compositor is running, so window, frame, and workspace manipulation belongs in callbacks (keybindings, hooks, operator exec) that fire later, not at config load.

Reloading

The compositor_reload operator re-runs the config; one that calls ion_utils.config.reset_defaults() under its reload flag converges to exactly what it declares. Preference writes apply as the current callback returns; only startup.use_xwayland, and the XCURSOR environment child processes inherit, still require a restart.

A failed reload is not rolled back: the config keeps whatever it declared before raising. Super-R and Super-Q are then added unconditionally, without inspecting what survived, so a config that raised before declaring either still has a reload and a quit key. They go on the end of the global keymap, so a config that rebound those keys to something else before raising shadows them - unusual, and ionwl-cmd is the way back when it happens.

is_init is False while a reload re-runs the config, so one-shot setup (launching a bar, exporting the session environment) gated on it happens at startup only.

A reload runs from a key or pointer binding, a menu item, a generator or ionwl-cmd, and never from a hook callback (see below).

Hook callbacks

A hook fires in the middle of the operation that raised it, and the operation carries on once its callbacks return. That shapes what a callback can do:

  • It cannot cancel or alter the operation. Return values are ignored, so a *_pre hook is a notification, not a veto.

  • An exception is reported like any other callback error, and the remaining callbacks and the operation still run.

  • Causing the event it handles fires the same hook again, from inside the callback. Nothing suppresses the recursion.

  • The argument of a hook fired after a removal (window_close_post, workspace_delete_post, desktop_remove_post, output_remove_post) names something already gone. Read what the callback needs in the matching *_pre hook.

  • It cannot reload the config: ion_utils.config.reload() and ion_utils.config.reset_defaults() raise RuntimeError, and the compositor_reload operator’s poll refuses it. A reset replaces the desktops the operation still holds, and fires hooks the reloaded config registers again, so a reload from one of those would recurse.

Application settings.

ion.app.is_init

True while the initial config load is running, False otherwise - including while a reload re-runs the config. Gate one-shot setup on this so a reload does not repeat it.

Type:

bool

ion.app.ipc_args

Arguments passed to the current IPC command. Empty list outside of IPC execution. Decoded as sys.argv is, so an argument that isn’t valid UTF-8 holds surrogateescape surrogates, and os.fsencode returns its bytes.

Type:

list[str]

Submodules

Functions

ion.app.config_dir()

Config directory: the --config-dir argument if it was given, otherwise XDG_CONFIG_HOME/ionwl, ~/.config/ionwl or /etc/ionwl, whichever resolves first.

Decoded as os.fsdecode decodes a path, so one that isn’t valid UTF-8 holds surrogateescape surrogates, and os.fsencode returns its bytes.

Return type:

str

ion.app.data_dir()

Data directory containing resources and bundled Python packages. Resolved once at startup from the XDG data directory search path.

Decoded as config_dir() is.

Return type:

str

ion.app.logout()

Graceful logout: request all windows to close, then exit when none remain. Windows that need confirmation (unsaved files etc.) will stay open until the user handles them. The logout is abandoned when no window closes within ion.types.SystemPreferences.logout_timeout.

Return type:

None

ion.app.quit()

Quit the compositor.

Return type:

None

ion.app.version()

Get IonWL version.

Return type:

str

Data

ion.app.decor
class ion.app.Decor

Decoration configuration (geometry and visual properties).

Type:

ion.types.Decor

ion.app.hooks

Namespace exposing all hooks as attributes, e.g. ion.app.hooks.window_new_post.

The set of hooks is fixed at compile time. Users attach callbacks to a ion.types.Hook via ion.types.Hook.append(); they do not add or remove hooks themselves.

See Hook callbacks in ion.app for what a callback cannot do.

Type:

ion.types.HooksRegistry

Output Hotplug

Use the output_add_pre hook to configure monitors as they are connected. The callback receives the ion.types.Output object before it is used, so properties like position and transform take effect immediately.


import ion


@ion.app.hooks.output_add_pre.append
def configure_output(output: ion.types.Output) -> None:
    if output.name == "DP-2":
        output.configure(position=(0, 0), transform=90)
    elif output.name == "HDMI-A-1":
        output.configure(position=(2560, 0))

Startup Layout

Use the compositor_startup_post hook to create desktops and workspaces when the compositor starts. Desktop 0 always exists, so only additional desktops need to be created explicitly.


import ion
from ion.types import Rect


@ion.app.hooks.compositor_startup_post.append
def on_startup() -> None:
    # Desktop 0 already exists; create three more.
    desktops = [ion.wm.desktops[0]]
    for i in range(3):
        desktops.append(ion.wm.desktops.new(str(i + 2)))

    # Give each desktop a named workspace with a horizontal split.
    for i, desktop in enumerate(desktops):
        desktop.name = str(i + 1)
        ws = ion.wm.workspaces.new(Rect((0, 0), (800, 600)))
        desktop.items_tiling.add(ws)
        node = ws.frames[0].split_node
        assert node is not None
        node.split(0, 1)
ion.app.keymaps

Registry for keymaps, accessed as ion.app.keymaps["global"], ion.app.keymaps["window"], etc.

A key press consults the standard keymaps in this order:

  • "tiling", "floating" or "fullscreen": whichever holds focus, a tiled frame, a floating frame or a fullscreen window.

  • "window": when the focused frame holds a window.

  • "global": always.

A pointer button consults the same keymaps first, then the keymaps of every region under the pointer, in this order:

  • "tab": over a tab.

  • "title": over a frame title bar.

  • "root": over the empty background.

  • "divider": over a split divider.

  • "border": over a floating window border.

  • "decoration": over a window decoration.

The regions are passed over when the focus keymap, "window" or "global" claims the button: on a press, when a binding runs for it or a click, drag or double-click binding waits on it; on a release, when a release binding runs for it. A fullscreen window under the pointer passes them over too, as does an overlay or top layer surface (a panel or launcher above the windows) on a press.

Any other keymap is consulted only where it is named: by prompt() for one key press, or by a KeymapHandler in a generator.

Within a keymap, bindings on one keysym are tried with those that ignore fewer modifiers first, then in the order they were declared. A key press tries the keysym it types, with Shift and the like applied, before its unmodified keysym.

The registry follows the dict protocol: iteration yields keymap names, "name" in keymaps tests membership, keymaps["name"] returns the Keymap, and keys() / values() / items() expose those views. Keymaps other than these, and those ion_utils creates for its operators, are created with new() or ensure().

The views read the live compositor registry, so they require compositor state (a config, operator, hook, generator or ionwl-cmd) and raise RuntimeError outside those contexts.

Type:

ion.types.KeymapsRegistry

Basic Keybindings

Set up keyboard and pointer bindings using ion.types.MatchKey and ion.types.MatchPointer. Modifiers such as logo, shift, ctrl and alt can be combined freely.


import ion
from ion.types import MatchKey, MatchPointer
import ion.constants.keys as kbd
import ion.constants.pointer_buttons as btn
import ion.constants.event_actions as act
import ion.ops as ops

km = ion.app.keymaps['global']

# Launch a terminal.
km.items.new(MatchKey(kbd.RETURN, logo=True),
             operator=ops.system_exec(command=("foot",)))

# Close the focused window (or remove frame if empty).
km.items.new(MatchKey(kbd.Q, logo=True), operator=ops.window_close())

# Toggle fullscreen.
km.items.new(MatchKey(kbd.F, logo=True), operator=ops.window_fullscreen_set())

# Vim-style focus navigation.
km.items.new(MatchKey(kbd.H, logo=True), operator=ops.tiling_focus_directional(direction="LEFT"))
km.items.new(MatchKey(kbd.J, logo=True), operator=ops.tiling_focus_directional(direction="DOWN"))
km.items.new(MatchKey(kbd.K, logo=True), operator=ops.tiling_focus_directional(direction="UP"))
km.items.new(MatchKey(kbd.L, logo=True), operator=ops.tiling_focus_directional(direction="RIGHT"))

# Move windows with Logo+Shift.
km.items.new(MatchKey(kbd.H, logo=True, shift=True), operator=ops.window_tiling_move_directional(direction="LEFT"))
km.items.new(MatchKey(kbd.L, logo=True, shift=True), operator=ops.window_tiling_move_directional(direction="RIGHT"))

# Tab cycling with Alt+Tab / Alt+Shift+Tab.
km.items.new(MatchKey(kbd.TAB, alt=True),
             operator=ops.frame_tab_cycle(direction=1))
km.items.new(MatchKey(kbd.TAB, alt=True, shift=True),
             operator=ops.frame_tab_cycle(direction=-1))

# Pointer: Logo+Right-click drag to move a floating window.
km.items.new(MatchPointer(btn.RIGHT, logo=True, action=act.PRESS),
             operator=ops.floating_move())

# Pointer: Logo+Left-click drag to resize a floating window.
km.items.new(MatchPointer(btn.LEFT, logo=True, action=act.PRESS),
             operator=ops.floating_resize())
ion.app.menus

Registry of named menus with builder callbacks: ion.app.menus.new('tab', 'Tab', callback).

Callbacks receive a ion.types.MenuBuilder and are called each time the menu is shown, enabling dynamic items.

import ion
import ion.ops as ops

def my_menu(menu):
    menu.layout.operator(ops.window_close())
    menu.layout.separator()
    menu.layout.operator(ops.frame_delete())

ion.app.menus.new("context", "Context Menu", my_menu)

The registry follows the dict protocol: iteration yields menu idnames, "name" in menus tests membership, menus["name"] returns the ion.types.Menu, and keys() / values() / items() expose the three views.

Type:

ion.types.MenuRegistry

Context Menus

Register context menus with ion.types.MenuRegistry.new(). The callback receives a ion.types.MenuBuilder and populates its ion.types.UILayout (menu.layout) with operators, separators, and sub-menus. Bind the menu to a pointer event with the built-in menu_show operator.


import ion
from ion.types import MatchPointer, MenuBuilder
import ion.constants.pointer_buttons as btn
import ion.ops as ops


# Tab context menu - shown on right-click over a tab.
def _tab_menu(menu: MenuBuilder) -> None:
    layout = menu.layout
    layout.operator(ops.window_close())
    layout.operator(ops.frame_delete())
    layout.separator()
    layout.operator(ops.tag_set())
    layout.operator(ops.window_floating_set())


ion.app.menus.new('tab', "Tab", _tab_menu)


# Frame context menu with a nested sub-menu.
def _frame_menu(menu: MenuBuilder) -> None:
    layout = menu.layout
    layout.operator(ops.tiling_split(direction='RIGHT'))
    layout.operator(ops.tiling_split(direction='DOWN'))
    layout.operator(ops.tiling_split_axis_set())
    layout.separator()
    layout.menu(idname="root")


ion.app.menus.new('frame', "Frame", _frame_menu)


# Root sub-menu referenced by the frame menu above.
def _root_menu(menu: MenuBuilder) -> None:
    layout = menu.layout
    layout.operator(ops.workspace_new_tiling_on_desktop())
    layout.separator()
    layout.operator(ops.desktop_new())
    layout.operator(ops.desktop_delete())


ion.app.menus.new('root', "Root", _root_menu)


# Bind right-click on a tab to show the "tab" menu.
ion.app.keymaps['tab'].items.new(
    MatchPointer(btn.RIGHT),
    operator=ops.menu_show(idname="tab"),
)
ion.app.preferences

Main preferences object accessed via ion.app.preferences.

Writes apply after the current operator, hook or IPC call returns. startup.use_xwayland is the exception, consulted only at startup; so is the XCURSOR environment child processes read, though the compositor’s own cursor does follow ion.types.CursorPreferences.

Type:

ion.types.Preferences