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.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
*_prehook 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*_prehook.It cannot reload the config:
ion_utils.config.reload()andion_utils.config.reset_defaults()raiseRuntimeError, and thecompositor_reloadoperator’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.argvis, so an argument that isn’t valid UTF-8 holdssurrogateescapesurrogates, andos.fsencodereturns its bytes.- 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.Decoded as
os.fsdecodedecodes a path, so one that isn’t valid UTF-8 holdssurrogateescapesurrogates, andos.fsencodereturns 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.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.See Hook callbacks in
ion.appfor what a callback cannot do.- 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.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 aKeymapHandlerin 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 keymapstests membership,keymaps["name"]returns theKeymap, andkeys()/values()/items()expose those views. Keymaps other than these, and thoseion_utilscreates for its operators, are created withnew()orensure().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: