Configuration¶
IonWL is configured entirely through Python. The config file is ~/.config/ionwl/ion_init.py.
It runs once at startup or when reloading (ion.app.reload()).
A typical configuration sets up:
Preferences (
ion.app.preferences).Key and pointer bindings (
ion.app.keymaps,ion.ops).Hooks (
ion.app.hooks) - may create desktops or launch services on startup.
Sample configurations¶
Copy either to ~/.config/ionwl/ion_init.py and customize.
Minimal¶
A minimal starting point: window management, launchers, and compositor controls, without tiling-navigation keys. Other actions remain reachable through the right-click context menus.
# SPDX-License-Identifier: GPL-3.0-or-later
# IonWL sample configuration.
#
# Copy this file to ~/.config/ionwl/ion_init.py and customize as needed.
# Only needed for Python < 3.14.
from __future__ import annotations
__all__ = ()
import ion
from ion.app import preferences as prefs
from ion.types import MatchKey
import ion.constants.keys as kbd
import ion.ops as ops
import ion_utils.config
# -----------------------------------------------------------------------------
# Settings
_TERMINAL = ('foot',)
_LAUNCHER = ('tofi-drun', '--drun-launch=true')
prefs.fonts.titlebar_font.face = 'DejaVu Sans:size=11:weight=bold'
# -----------------------------------------------------------------------------
# Keymaps
_km_global = ion.app.keymaps['global']
_km_tiling = ion.app.keymaps['tiling']
_km_floating = ion.app.keymaps['floating']
_km_window = ion.app.keymaps['window']
_km_fullscreen = ion.app.keymaps['fullscreen']
# -----------------------------------------------------------------------------
# Context menus and pointer bindings
ion_utils.config.setup_defaults()
# -----------------------------------------------------------------------------
# Window management
# Close window, or join frame if no window focused.
for km in (_km_window, _km_tiling):
km.items.new(MatchKey(kbd.Q, logo=True), ops.window_close(delete_empty_frame=True, delete_empty_workspace=True))
# Toggle fullscreen.
_km_window.items.new(MatchKey(kbd.F, logo=True), ops.window_fullscreen_set())
_km_fullscreen.items.new(MatchKey(kbd.F, logo=True), ops.window_fullscreen_set())
# Toggle floating (detach/reattach).
_km_window.items.new(MatchKey(kbd.X, logo=True), ops.window_floating_set())
# Toggle tag on current window.
_km_window.items.new(MatchKey(kbd.T, logo=True), ops.tag_set())
# Attach tagged windows to current frame.
for km in (_km_tiling, _km_floating):
km.items.new(MatchKey(kbd.A, logo=True), ops.tag_attach())
# -----------------------------------------------------------------------------
# Launchers
# Terminal.
_km_global.items.new(MatchKey(kbd.RETURN, logo=True), ops.system_exec(command=_TERMINAL))
# App launcher.
_km_global.items.new(MatchKey(kbd.P, logo=True), ops.system_exec(command=_LAUNCHER))
# -----------------------------------------------------------------------------
# Compositor control
# Logout (graceful): close all windows, exit when none remain.
_km_global.items.new(MatchKey(kbd.Q, logo=True, shift=True), ops.compositor_logout())
# Exit (force): quit immediately without closing windows.
_km_global.items.new(MatchKey(kbd.Q, logo=True, shift=True, ctrl=True), ops.compositor_quit())
# Reload config.
_km_global.items.new(MatchKey(kbd.R, logo=True, shift=True), ops.compositor_reload())
Full¶
The bundled sample configuration wires up Notion-style keybindings with VIM-style (HJKL) navigation.
# SPDX-License-Identifier: GPL-3.0-or-later
# IonWL sample configuration.
#
# Copy this file to ~/.config/ionwl/ion_init.py and customize as needed.
#
# This config uses VIM-style navigation.
# Only needed for Python < 3.14.
from __future__ import annotations
__all__ = ()
import ion
from ion.app import preferences as prefs
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
import ion_utils.config
# -----------------------------------------------------------------------------
# Settings
_TERMINAL = ('foot',)
_LAUNCHER = ('tofi-drun', '--drun-launch=true')
_SCRATCHPAD = '*scratch*' # Workspace used as a scratchpad.
# Enable numlock on startup.
prefs.startup.use_numlock = True
# Enable sloppy focus (focus follows mouse).
prefs.focus.pointer_follow = True
# Warp pointer to frame on directional focus.
prefs.focus.pointer_warp = True
prefs.focus.pointer_warp_factor = (0.5, 0.5) # Centered.
prefs.focus.pointer_warp_margin = 5
# Key repeat: 40 Hz, 200ms delay.
prefs.input.keyboard_defaults.repeat_rate = 40
prefs.input.keyboard_defaults.repeat_delay = 200
# Show desktop notification on Python errors (requires notify-send).
prefs.notify_on_error = True
# Cursor size.
prefs.cursor.size = 48
# Font settings.
prefs.fonts.titlebar_font.face = 'DejaVu Sans:size=11:weight=bold'
# -----------------------------------------------------------------------------
# Keymaps
_km_global = ion.app.keymaps['global']
_km_tiling = ion.app.keymaps['tiling']
_km_floating = ion.app.keymaps['floating']
_km_window = ion.app.keymaps['window']
_km_fullscreen = ion.app.keymaps['fullscreen']
# -----------------------------------------------------------------------------
# Startup
@ion.app.hooks.compositor_startup_post.append
def _on_startup() -> None:
'''Create 4 desktops with a horizontal split, start background services.'''
# Desktop 0 already exists, create 3 more.
desktops = [ion.wm.desktops[0]]
for i in range(3):
desktops.append(ion.wm.desktops.new(str(i + 1)))
# Give each desktop a workspace with a horizontal split.
for i, desktop in enumerate(desktops):
desktop.name = str(i + 1)
ws = ion.wm.workspaces.new(ion.types.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_utils.config.setup_session(dbus=True, systemd=True)
ion.utils.exec(('mako',))
# -----------------------------------------------------------------------------
# Context menus and pointer bindings
ion_utils.config.setup_defaults()
# -----------------------------------------------------------------------------
# Window management
# Close window, or join frame if no window focused.
for km in (_km_window, _km_tiling):
km.items.new(MatchKey(kbd.Q, logo=True), ops.window_close(delete_empty_frame=True, delete_empty_workspace=True))
# Toggle fullscreen.
_km_window.items.new(MatchKey(kbd.F, logo=True), ops.window_fullscreen_set())
# Toggle floating (detach/reattach).
_km_window.items.new(MatchKey(kbd.X, logo=True), ops.window_floating_set())
# Toggle tag on current window.
_km_window.items.new(MatchKey(kbd.T, logo=True), ops.tag_set())
# Attach tagged windows to current frame.
for km in (_km_tiling, _km_floating):
km.items.new(MatchKey(kbd.A, logo=True), ops.tag_attach())
# -----------------------------------------------------------------------------
# Fullscreen overrides (block operations that don't apply in fullscreen)
# Exit fullscreen.
_km_fullscreen.items.new(MatchKey(kbd.F, logo=True), ops.window_fullscreen_set())
# Block tiling move (vim-style).
_km_fullscreen.items.new(MatchKey(kbd.H, logo=True, shift=True), ops.system_noop())
_km_fullscreen.items.new(MatchKey(kbd.J, logo=True, shift=True), ops.system_noop())
_km_fullscreen.items.new(MatchKey(kbd.K, logo=True, shift=True), ops.system_noop())
_km_fullscreen.items.new(MatchKey(kbd.L, logo=True, shift=True), ops.system_noop())
# -----------------------------------------------------------------------------
# Tab cycling within frame
# Alt+Tab - next tab.
_km_window.items.new(MatchKey(kbd.TAB, alt=True), ops.frame_tab_cycle(direction=1))
# Alt+Shift+Tab - previous tab.
_km_window.items.new(MatchKey(kbd.TAB, alt=True, shift=True), ops.frame_tab_cycle(direction=-1))
# -----------------------------------------------------------------------------
# Tiling navigation (vim-style)
_km_global.items.new(MatchKey(kbd.H, logo=True), ops.tiling_focus_directional(direction='LEFT'))
_km_global.items.new(MatchKey(kbd.J, logo=True), ops.tiling_focus_directional(direction='DOWN'))
_km_global.items.new(MatchKey(kbd.K, logo=True), ops.tiling_focus_directional(direction='UP'))
_km_global.items.new(MatchKey(kbd.L, logo=True), ops.tiling_focus_directional(direction='RIGHT'))
# -----------------------------------------------------------------------------
# Moving windows between frames (vim-style)
_km_tiling.items.new(MatchKey(kbd.H, logo=True, shift=True), ops.window_tiling_move_directional(direction='LEFT'))
_km_tiling.items.new(MatchKey(kbd.J, logo=True, shift=True), ops.window_tiling_move_directional(direction='DOWN'))
_km_tiling.items.new(MatchKey(kbd.K, logo=True, shift=True), ops.window_tiling_move_directional(direction='UP'))
_km_tiling.items.new(MatchKey(kbd.L, logo=True, shift=True), ops.window_tiling_move_directional(direction='RIGHT'))
# -----------------------------------------------------------------------------
# Splitting frames
# Split horizontal (new frame to the right) - backslash key.
_km_tiling.items.new(MatchKey(kbd.BACK_SLASH, logo=True), ops.tiling_split(direction='RIGHT'))
# Split vertical (new frame below).
_km_tiling.items.new(MatchKey(kbd.BACK_SLASH, logo=True, shift=True), ops.tiling_split(direction='DOWN'))
# Join frame with sibling, moving windows.
_km_tiling.items.new(MatchKey(kbd.X, logo=True, shift=True), ops.frame_join())
# -----------------------------------------------------------------------------
# Layout manipulation
# Move focused item to the next output (wraps back to the left).
_km_global.items.new(MatchKey(kbd.Z, logo=True), ops.output_move_directional(direction='RIGHT'))
# Toggle split axis at current frame.
_km_tiling.items.new(MatchKey(kbd.Z, logo=True, shift=True), ops.tiling_split_axis_set())
# Toggle split axis at workspace root.
_km_tiling.items.new(MatchKey(kbd.Z, logo=True, ctrl=True), ops.tiling_split_axis_set(parent_depth=-1))
# -----------------------------------------------------------------------------
# Launchers
# Terminal.
_km_global.items.new(MatchKey(kbd.RETURN, logo=True), ops.system_exec(command=_TERMINAL))
# App launcher.
_km_global.items.new(MatchKey(kbd.P, logo=True), ops.system_exec(command=_LAUNCHER))
# -----------------------------------------------------------------------------
# Floating workspace (scratch)
_km_global.items.new(MatchKey(kbd.SPACE, logo=True), ops.workspace_floating_set(name=_SCRATCHPAD))
_km_window.items.new(MatchKey(kbd.W, logo=True), ops.window_workspace_push(name=_SCRATCHPAD))
_km_global.items.new(MatchKey(kbd.W, logo=True, shift=True), ops.window_workspace_pop(name=_SCRATCHPAD))
# -----------------------------------------------------------------------------
# Compositor control
# Logout (graceful): close all windows, exit when none remain.
_km_global.items.new(MatchKey(kbd.Q, logo=True, shift=True), ops.compositor_logout())
# Exit (force): quit immediately without closing windows.
_km_global.items.new(MatchKey(kbd.Q, logo=True, shift=True, ctrl=True), ops.compositor_quit())
# Reload config.
_km_global.items.new(MatchKey(kbd.R, logo=True, shift=True), ops.compositor_reload())
# -----------------------------------------------------------------------------
# Workspace cycling
# Cycle to next/previous occupied desktop.
_km_global.items.new(MatchKey(kbd.M, logo=True), ops.desktop_focus_occupied_cycle(direction=1))
_km_global.items.new(MatchKey(kbd.COMMA, logo=True), ops.desktop_focus_occupied_cycle(direction=-1))
# Cycle to next empty desktop.
_km_global.items.new(MatchKey(kbd.RETURN, logo=True, ctrl=True), ops.desktop_focus_empty_cycle(direction=1))
# Window carousel.
_km_window_carousel = ion.app.keymaps['window_carousel']
_km_window_carousel.items.new(MatchKey(kbd.RETURN, action=act.CLICK), ops.window_carousel_confirm())
_km_window_carousel.items.new(MatchKey(kbd.ESCAPE, action=act.CLICK), ops.window_carousel_cancel())
_km_window_carousel.items.new(MatchPointer(btn.LEFT, action=act.CLICK), ops.window_carousel_cancel())
_km_global.items.new(MatchKey(kbd.TAB, logo=True, ctrl=True), ops.window_cycle_carousel(keymap='window_carousel'))
# Desktop overview on logo key click.
_km_desktop_overview = ion.app.keymaps['desktop_overview']
_km_desktop_overview.items.new(MatchKey(kbd.H), ops.desktop_overview_navigate(direction='LEFT'))
_km_desktop_overview.items.new(MatchKey(kbd.J), ops.desktop_overview_navigate(direction='DOWN'))
_km_desktop_overview.items.new(MatchKey(kbd.K), ops.desktop_overview_navigate(direction='UP'))
_km_desktop_overview.items.new(MatchKey(kbd.L), ops.desktop_overview_navigate(direction='RIGHT'))
_km_desktop_overview.items.new(MatchKey(kbd.RETURN, action=act.CLICK), ops.desktop_overview_confirm())
_km_desktop_overview.items.new(MatchKey(kbd.ESCAPE, action=act.CLICK), ops.desktop_overview_cancel())
_km_desktop_overview.items.new(MatchPointer(btn.LEFT, action=act.CLICK), ops.desktop_overview_select())
for key in (kbd.LOGO_L, kbd.LOGO_R):
_km_global.items.new(
MatchKey(key, action=act.CLICK),
ops.desktop_overview(keymap='desktop_overview', trim_empty=True),
)
# Window overview.
_km_window_overview = ion.app.keymaps['window_overview']
_km_window_overview.items.new(MatchKey(kbd.H), ops.window_overview_navigate(direction='LEFT'))
_km_window_overview.items.new(MatchKey(kbd.J), ops.window_overview_navigate(direction='DOWN'))
_km_window_overview.items.new(MatchKey(kbd.K), ops.window_overview_navigate(direction='UP'))
_km_window_overview.items.new(MatchKey(kbd.L), ops.window_overview_navigate(direction='RIGHT'))
_km_window_overview.items.new(MatchKey(kbd.RETURN, action=act.CLICK), ops.window_overview_confirm())
_km_window_overview.items.new(MatchKey(kbd.ESCAPE, action=act.CLICK), ops.window_overview_cancel())
_km_window_overview.items.new(MatchPointer(btn.LEFT, action=act.CLICK), ops.window_overview_select())
_km_global.items.new(MatchKey(kbd.Y, logo=True), ops.window_overview(keymap='window_overview'))
# -----------------------------------------------------------------------------
# Media keys
_km_global.items.new(
MatchKey(kbd.XF86_AUDIO_RAISE_VOLUME),
ops.system_exec(command=('wpctl', 'set-volume', '@DEFAULT_AUDIO_SINK@', '5%+')),
)
_km_global.items.new(
MatchKey(kbd.XF86_AUDIO_LOWER_VOLUME),
ops.system_exec(command=('wpctl', 'set-volume', '@DEFAULT_AUDIO_SINK@', '5%-')),
)
_km_global.items.new(MatchKey(kbd.XF86_AUDIO_PLAY), ops.system_exec(command=('cmus-remote', '--pause')))
_km_global.items.new(MatchKey(kbd.XF86_AUDIO_PREV), ops.system_exec(command=('cmus-remote', '--prev')))
_km_global.items.new(MatchKey(kbd.XF86_AUDIO_NEXT), ops.system_exec(command=('cmus-remote', '--next')))
_km_global.items.new(MatchKey(kbd.XF86_AUDIO_REWIND), ops.system_exec(command=('cmus-remote', '--seek', '-10')))
_km_global.items.new(MatchKey(kbd.XF86_AUDIO_FORWARD), ops.system_exec(command=('cmus-remote', '--seek', '+10')))
ion.utils.log('Config loaded')
Type Checking¶
Validate your configuration with mypy using the installed stubs:
MYPYPATH=/opt/ionwl/share/ionwl/python mypy --strict ~/.config/ionwl/ion_init.py