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.wmonly exists once the compositor is running, so window, frame, and workspace manipulation belongs in callbacks (keybindings, hooks, operatorexec) that fire later, not at config load.- Reloading
The
compositor_reloadoperator re-runs the config; one that callsion_utils.config.reset_defaults()under itsreloadflag converges to exactly what it declares. Preference writes apply as the current callback returns; onlystartup.use_xwayland, and theXCURSORenvironment child processes inherit, still require a restart.A failed reload is not rolled back: the config keeps whatever it declared before raising.
Super-RandSuper-Qare 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 theglobalkeymap, so a config that rebound those keys to something else before raising shadows them - unusual, andionwl-cmdis the way back when it happens.is_initis 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.Reloading needs the registries mutable, which every direct dispatch site grants: a key or pointer binding, a menu item, a generator, and
ionwl-cmd. What remains ruled out is indirect re-entry - an operator run from inside another operator’sexec, and the hooks that fire from inside the windowing code, which is half of them: the desktop, workspace, and window focus, move, floating, fullscreen and urgent lifecycle. There the reload is refused rather than half-applied - aReload failednotification, the detail in the log, and the session unchanged.
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.
- Type:
list[str]
Submodules
Functions¶
- ion.app.config_dir()¶
Config directory: the
--config-dirargument if it was given, otherwiseXDG_CONFIG_HOME/ionwl,~/.config/ionwlor/etc/ionwl, whichever resolves first.- 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.version()¶
Get IonWL version.
- Return type:
str
Data¶
- ion.app.decor¶
- class ion.app.Decor¶
Decoration configuration (geometry and visual properties).
- Type:
- 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.Hookviaion.types.Hook.append(); they do not add or remove hooks themselves.- Type:
Output Hotplug
Use the
output_add_prehook to configure monitors as they are connected. The callback receives theion.types.Outputobject 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_posthook 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 keymapstests membership,keymaps["name"]returns theKeymap, andkeys()/values()/items()expose the three views.The views read the live compositor registry, so they require compositor state (a config, operator, hook, generator or
ionwl-cmd) and raiseRuntimeErroroutside those contexts.Basic Keybindings
Set up keyboard and pointer bindings using
ion.types.MatchKeyandion.types.MatchPointer. Modifiers such aslogo,shift,ctrlandaltcan 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())
Registry of named menus with builder callbacks:
ion.app.menus.new('tab', 'Tab', callback).Callbacks receive a
ion.types.MenuBuilderand 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 menustests membership,menus["name"]returns theion.types.Menu, andkeys()/values()/items()expose the three views.- Type:
Context Menus
Register context menus with
ion.types.MenuRegistry.new(). The callback receives aion.types.MenuBuilderand populates itsion.types.UILayout(menu.layout) with operators, separators, and sub-menus. Bind the menu to a pointer event with the built-inmenu_showoperator.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_xwaylandis the exception, consulted only at startup; so is theXCURSORenvironment child processes read, though the compositor’s own cursor does followion.types.CursorPreferences.- Type: