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. The registries do not accumulate across reloads: they are cleared and re-declared each time the config runs.

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.

Reloads

reload() re-runs the config file. Declarative state is cleared first (see reload() for what is cleared), so the config is always the complete declaration of those settings rather than an addition to the previous load. Use is_init_startup to gate one-shot setup that should not re-run on each reload.

Application settings.

ion.app.is_init

True during config loading (both initial startup and reload), False otherwise.

Type:

bool

ion.app.is_init_startup

True only during the initial startup config load, False during reload and at runtime.

Type:

bool

ion.app.ipc_args

Arguments passed to the current IPC command. Empty list outside of IPC execution.

Type:

list[str]

Functions

ion.app.config_dir()

Config directory (XDG_CONFIG_HOME/ionwl or ~/.config/ionwl).

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.

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.

Return type:

None

ion.app.quit()

Quit the compositor.

ion.app.reload()

Reload the configuration file.

All keybindings, hooks, and menus are cleared before the config re-runs, so the config file is always the complete declaration of these settings. Check ion.app.is_init_startup to gate actions that should only run on initial startup.

Note

This action is deferred until the current callback returns.

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.

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.

Keymaps are checked in registration order (first registered = highest priority). Standard keymaps:

  • "global": Always active, lowest priority.

  • "window": Active when any window has keyboard focus.

  • "tiling": Active when focus is on a tiled frame.

  • "floating": Active when focus is on a floating frame.

  • "fullscreen": Active when a fullscreen window has focus.

  • "tab": Active when the pointer is over a tab.

  • "title": Active when the pointer is over a frame title bar.

  • "root": Active when the pointer is over the empty background.

  • "divider": Active when hovering a split divider.

  • "decoration": Active when the pointer is over a window decoration.

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 the three views.

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.

Type:

ion.types.Preferences