Configuration¶
IonWL is configured entirely through Python. The config file is ~/.config/ionwl/ion_init.py.
It runs at startup, and again on each reload (see ion.app).
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__ = (
"main",
)
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
# True when this module is being reloaded.
reload = "main" in locals()
# -----------------------------------------------------------------------------
# Settings
TERMINAL = ('foot',)
LAUNCHER = ('tofi-drun', '--drun-launch=true')
def main() -> None:
'''Apply preferences and declare keymaps and menus.'''
if reload:
ion_utils.config.reset_defaults(desktops=False)
# -------------------------------------------------------------------------
# Preferences
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())
main()
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__ = (
"main",
)
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
# True when this module is being reloaded.
reload = "main" in locals()
# -----------------------------------------------------------------------------
# Settings
TERMINAL = ('foot',)
LAUNCHER = ('tofi-drun', '--drun-launch=true')
SCRATCHPAD = '*scratch*' # Workspace used as a scratchpad.
def main() -> None:
'''Apply preferences and declare keymaps, menus and hooks.'''
if reload:
ion_utils.config.reset_defaults(desktops=False)
# -------------------------------------------------------------------------
# Preferences
# 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.system.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 fullscreen spanning all outputs.
km_window.items.new(MatchKey(kbd.F, logo=True, shift=True), ops.window_fullscreen_all_set(name='*fullscreen*'))
# 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')
main()
Type Checking¶
Validate your configuration with mypy using the installed stubs:
MYPYPATH=/opt/ionwl/share/ionwl/python mypy --strict ~/.config/ionwl/ion_init.py