Skip to content

Data Layer

Data structures and configuration models.

Configuration Models

config

Configuration models for Svalboard Keymap Image Maker tool.

This module defines the Pydantic configuration models used to customize the appearance and behavior of generated keymap images. The configuration is hierarchical, with the root :class:SkimConfig containing nested models for keyboard settings, keycode mappings, and output styling.

Configuration can be loaded from YAML files or constructed programmatically. All models use Pydantic's BaseModel for validation and serialization.

Example

Loading configuration from a YAML file:

import yaml
from skim.data.config import SkimConfig

with open("skim-config.yaml") as f:
    data = yaml.safe_load(f)
config = SkimConfig(**data)

Creating configuration programmatically:

from skim.data.config import SkimConfig, LayerColor, Palette

config = SkimConfig()
new_layers = config.output.style.palette.layers + (LayerColor(base_color="#FF0000"),)
new_palette = config.output.style.palette.model_copy(update={"layers": new_layers})
new_style = config.output.style.model_copy(update={"palette": new_palette})
new_output = config.output.model_copy(update={"style": new_style})
config = config.model_copy(update={"output": new_output})

Attributes:

Name Type Description
SplitSidePositionStr

Annotated type alias for SplitSidePosition that accepts string values and converts them to enum members.

SpacingValue module-attribute

SpacingValue = Annotated[
    float | None, BeforeValidator(_parse_spacing)
]

A spacing field accepting None, float, int, or a 'N%' string.

After validation the value is always float | None; the renderer interprets the float's magnitude (< 1.0 proportion, >= 1.0 absolute units).

SplitSidePositionStr module-attribute

SplitSidePositionStr = Annotated[
    SplitSidePosition,
    BeforeValidator(lambda v: SplitSidePosition(v)),
]

Annotated type for SplitSidePosition that accepts string inputs.

This type alias allows configuration files to specify hold symbol positions as plain strings (e.g., "inward", "outward") which are automatically converted to SplitSidePosition enum members during validation.

Example

In a YAML configuration file:

style:
  hold_symbol_position: outward

When parsed, "outward" is converted to SplitSidePosition.OUTWARD.

SymbolLegendFlowStr module-attribute

SymbolLegendFlowStr = Annotated[
    SymbolLegendFlow,
    BeforeValidator(lambda v: SymbolLegendFlow(v)),
]

LayerOrderStr module-attribute

LayerOrderStr = Annotated[
    LayerOrder, BeforeValidator(lambda v: LayerOrder(v))
]

Annotated type for LayerOrder that accepts string inputs at YAML load time.

ThumbPositionStr module-attribute

ThumbPositionStr = Annotated[
    ThumbPosition,
    BeforeValidator(lambda v: ThumbPosition(v)),
]

Annotated type for ThumbPosition that accepts string inputs at YAML load time.

KeyboardLayer

Bases: BaseModel

Configuration for a single keyboard layer.

Defines the metadata associated with a layer in the keymap, including its internal name. This is used to customize how layers are named in generated images.

Attributes:

Name Type Description
id str | None

Optional unique identifier for the layer. Used for internal reference when processing a keymap from c2json that uses C define-macros instead of layer numbers in with the layer switch functions. It may be None if not specified.

name str

Full descriptive name of the layer (e.g., "Base Layer", "Symbols", "Navigation"). Used as the "image title" on the generated images.

variant str | None

Optional secondary label shown below the layer name (e.g., "COLEMAK"). Used to display additional layer metadata in the overview image. Defaults to None if not specified.

Example
>>> layer = KeyboardLayer(index=0, name="Base Layer")
>>> layer.name
'Base Layer'
>>> layer.variant is None
True

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

index instance-attribute

index: int

id class-attribute instance-attribute

id: str | None = None

name instance-attribute

name: str

variant class-attribute instance-attribute

variant: str | None = None

KeyboardFeatures

Bases: BaseModel

Configuration for optional keyboard hardware features.

Controls which optional hardware features are enabled when generating keymap images. These settings affect which keys are rendered.

Attributes:

Name Type Description
double_south bool

Whether to render the DS (double-south) keys on finger clusters. When False, these positions are hidden. Defaults to False as not all Svalboard configurations have these keys.

Example
>>> features = KeyboardFeatures(double_south=True)
>>> features.double_south
True
Note

Currently the Svalboard keyboard only have a single feature modifier that can impact a keymap image, but this configuration object is here to future-proof this tool on eventual changes.

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

double_south class-attribute instance-attribute

double_south: bool = False

Keyboard

Bases: BaseModel

Keyboard-specific configuration settings.

Contains settings that describe the physical keyboard configuration and layer definitions. This is used to customize the rendering based on the specific keyboard setup.

Attributes:

Name Type Description
features KeyboardFeatures

Hardware feature flags controlling the keymap rendering. Defaults to a KeyboardFeatures instance with all features disabled.

layers Annotated[tuple[KeyboardLayer, ...], BeforeValidator(_coerce_to_tuple)]

Tuple of layer configurations defining the metadata for each layer in the keymap. The order corresponds to layer indices (0, 1, 2, etc.).

Example
>>> keyboard = Keyboard(
...     features=KeyboardFeatures(double_south=True),
...     layers=(
...         KeyboardLayer(index=0, name="Base"),
...         KeyboardLayer(index=1, name="Symbols"),
...     ),
... )
>>> len(keyboard.layers)
2

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

features class-attribute instance-attribute

features: KeyboardFeatures = Field(
    default_factory=KeyboardFeatures
)

layers class-attribute instance-attribute

layers: Annotated[
    tuple[KeyboardLayer, ...],
    BeforeValidator(_coerce_to_tuple),
] = ()

model_post_init

model_post_init(context: object) -> None

Initialize the layer ID lookup map after model construction.

Builds an internal mapping from layer identifiers to their QMK firmware indices for efficient layer lookup. If a layer has an explicit id, that ID is used as the key. Otherwise, the string representation of the layer's position index is used.

This method is called automatically by Pydantic after the model is constructed.

Parameters:

Name Type Description Default
context object

Pydantic validation context (unused but required by the Pydantic post-init signature).

required
Example
>>> keyboard = Keyboard(
...     layers=[
...         KeyboardLayer(index=0, id="base", name="Base"),
...         KeyboardLayer(index=15, name="Symbols"),
...     ]
... )
>>> keyboard.layer_index("base")
0
>>> keyboard.layer_index("1")  # Second layer has no id, uses QMK index
15
Source code in src/skim/data/config.py
def model_post_init(self, context: object) -> None:
    """Initialize the layer ID lookup map after model construction.

    Builds an internal mapping from layer identifiers to their QMK
    firmware indices for efficient layer lookup. If a layer has an
    explicit ``id``, that ID is used as the key. Otherwise, the string
    representation of the layer's position index is used.

    This method is called automatically by Pydantic after the model is
    constructed.

    Args:
        context: Pydantic validation context (unused but required by
            the Pydantic post-init signature).

    Example:
        ```pycon
        >>> keyboard = Keyboard(
        ...     layers=[
        ...         KeyboardLayer(index=0, id="base", name="Base"),
        ...         KeyboardLayer(index=15, name="Symbols"),
        ...     ]
        ... )
        >>> keyboard.layer_index("base")
        0
        >>> keyboard.layer_index("1")  # Second layer has no id, uses QMK index
        15

        ```
    """
    for idx, layer in enumerate(self.layers):
        if layer.id is not None:
            self._layer_id_map[layer.id] = layer.index
        else:
            self._layer_id_map[str(idx)] = layer.index
        self._qmk_index_map[layer.index] = idx

layer_index

layer_index(key: str | None) -> int | None

Look up a layer's QMK firmware index by its identifier.

Returns the QMK firmware index of the layer matching the given key. The key can be either a layer's explicit id, the string representation of its position index (for layers without an id), or an integer index which is converted to string for lookup.

Parameters:

Name Type Description Default
key str | None

The layer identifier to look up. This can be a layer's id attribute, a string index like "0", "1", or an integer index like 0, 1. If None, returns None.

required

Returns:

Type Description
int | None

The QMK firmware index of the matching layer, or None if no

int | None

layer matches the given key or if key is None.

Example
>>> keyboard = Keyboard(
...     layers=[
...         KeyboardLayer(index=0, id="nav", name="Navigation"),
...         KeyboardLayer(index=15, name="Symbols"),
...     ]
... )
>>> keyboard.layer_index("nav")
0
>>> keyboard.layer_index("1")
15
>>> keyboard.layer_index("unknown") is None
True
Source code in src/skim/data/config.py
def layer_index(self, key: str | None) -> int | None:
    """Look up a layer's QMK firmware index by its identifier.

    Returns the QMK firmware index of the layer matching the given key.
    The key can be either a layer's explicit ``id``, the string
    representation of its position index (for layers without an id),
    or an integer index which is converted to string for lookup.

    Args:
        key: The layer identifier to look up. This can be a layer's
            ``id`` attribute, a string index like ``"0"``, ``"1"``,
            or an integer index like ``0``, ``1``. If ``None``, returns
            ``None``.

    Returns:
        The QMK firmware index of the matching layer, or ``None`` if no
        layer matches the given key or if key is ``None``.

    Example:
        ```pycon
        >>> keyboard = Keyboard(
        ...     layers=[
        ...         KeyboardLayer(index=0, id="nav", name="Navigation"),
        ...         KeyboardLayer(index=15, name="Symbols"),
        ...     ]
        ... )
        >>> keyboard.layer_index("nav")
        0
        >>> keyboard.layer_index("1")
        15
        >>> keyboard.layer_index("unknown") is None
        True

        ```
    """
    if key is None:
        return None
    return self._layer_id_map.get(key)

qmk_index_to_position

qmk_index_to_position(qmk_idx: int) -> int | None

Look up a layer's position by its QMK firmware index.

Returns the zero-based position of the layer in the layers tuple that has the given QMK firmware index. This is useful when layer indices in the firmware are non-sequential (e.g., 0, 1, 2, 15).

Parameters:

Name Type Description Default
qmk_idx int

The QMK firmware layer index to look up.

required

Returns:

Type Description
int | None

The position of the matching layer in the layers tuple, or

int | None

None if no layer has the given QMK index.

Example
>>> keyboard = Keyboard(
...     layers=[
...         KeyboardLayer(index=0, name="Base"),
...         KeyboardLayer(index=15, name="Mouse"),
...     ]
... )
>>> keyboard.qmk_index_to_position(15)
1
>>> keyboard.qmk_index_to_position(5) is None
True
Source code in src/skim/data/config.py
def qmk_index_to_position(self, qmk_idx: int) -> int | None:
    """Look up a layer's position by its QMK firmware index.

    Returns the zero-based position of the layer in the layers tuple
    that has the given QMK firmware index. This is useful when layer
    indices in the firmware are non-sequential (e.g., 0, 1, 2, 15).

    Args:
        qmk_idx: The QMK firmware layer index to look up.

    Returns:
        The position of the matching layer in the layers tuple, or
        ``None`` if no layer has the given QMK index.

    Example:
        ```pycon
        >>> keyboard = Keyboard(
        ...     layers=[
        ...         KeyboardLayer(index=0, name="Base"),
        ...         KeyboardLayer(index=15, name="Mouse"),
        ...     ]
        ... )
        >>> keyboard.qmk_index_to_position(15)
        1
        >>> keyboard.qmk_index_to_position(5) is None
        True

        ```
    """
    return self._qmk_index_map.get(qmk_idx)

layer_qmk_index

layer_qmk_index(position: int) -> int

Get the QMK firmware index for a layer at a given position.

Returns the QMK firmware layer index for the layer at the given position in the layers tuple.

Parameters:

Name Type Description Default
position int

The zero-based position of the layer in the layers tuple.

required

Returns:

Type Description
int

The QMK firmware layer index.

Example
>>> keyboard = Keyboard(
...     layers=[
...         KeyboardLayer(index=0, name="Base"),
...         KeyboardLayer(index=15, name="Mouse"),
...     ]
... )
>>> keyboard.layer_qmk_index(1)
15
Source code in src/skim/data/config.py
def layer_qmk_index(self, position: int) -> int:
    """Get the QMK firmware index for a layer at a given position.

    Returns the QMK firmware layer index for the layer at the given
    position in the layers tuple.

    Args:
        position: The zero-based position of the layer in the layers tuple.

    Returns:
        The QMK firmware layer index.

    Example:
        ```pycon
        >>> keyboard = Keyboard(
        ...     layers=[
        ...         KeyboardLayer(index=0, name="Base"),
        ...         KeyboardLayer(index=15, name="Mouse"),
        ...     ]
        ... )
        >>> keyboard.layer_qmk_index(1)
        15

        ```
    """
    return self.layers[position].index

Keycode

Bases: BaseModel

A keycode-to-label mapping override.

Defines a custom mapping from a QMK keycode string to a display label. This is used to customize how specific keycodes are rendered, either by preprocessing them before the standard mapping or by overriding the default label entirely.

Attributes:

Name Type Description
keycode str

The QMK keycode string to match (e.g., "KC_A", "KC_ESC"). Must match exactly, including case.

target str

The replacement label or transformed keycode. For pre-processing, this is typically another keycode. For overrides, this is the display label.

Example
>>> # Override KC_SPC to display as "Space"
>>> override = Keycode(keycode="KC_SPC", target="Space")
>>> override.keycode
'KC_SPC'

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

keycode instance-attribute

keycode: str

target instance-attribute

target: str

__hash__

__hash__() -> int
Source code in src/skim/data/config.py
def __hash__(self) -> int:
    return hash((self.keycode, self.target))

Macro

Bases: BaseModel

A macro reference with an optional human-readable name and preview.

Attributes:

Name Type Description
id str

String id matching how the keycode references this macro ("0" for MACRO_0, "MY_MACRO" for MACRO_MY_MACRO).

name str | None

Optional human-readable name surfaced by the renderer.

preview str

Single-line display summary, generated at bootstrap time or set to "Undefined" for manually-added entries. Read-only in the TUI.

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

id instance-attribute

id: str

name class-attribute instance-attribute

name: str | None = None

preview class-attribute instance-attribute

preview: str = ''

__hash__

__hash__() -> int
Source code in src/skim/data/config.py
def __hash__(self) -> int:
    return hash((self.id, self.name, self.preview))

TapDance

Bases: BaseModel

A tap-dance reference with an optional human-readable name and preview.

Attributes:

Name Type Description
id str

String id matching how the keycode references this tap dance ("0" for TD(0), "MY_TD" for TD(MY_TD)).

name str | None

Optional human-readable name surfaced by the renderer.

preview str

Single-line display summary, generated at bootstrap time or set to "Undefined" for manually-added entries. Read-only in the TUI.

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

id instance-attribute

id: str

name class-attribute instance-attribute

name: str | None = None

preview class-attribute instance-attribute

preview: str = ''

__hash__

__hash__() -> int
Source code in src/skim/data/config.py
def __hash__(self) -> int:
    return hash((self.id, self.name, self.preview))

Keycodes

Bases: BaseModel

Configuration for keycode transformation and display.

Contains tuples of keycode mappings that customize how QMK keycodes are transformed and displayed. The pre-processing transformation happens without any processing by the tool. This mapping should not use the alias and other replacements features from key resolution. Then, the standard label mapping happens with the overrides defined in this configuration having higher priority than the default transformations.

Attributes:

Name Type Description
pre_process Annotated[tuple[Keycode, ...], BeforeValidator(_coerce_to_tuple)]

Keycode transformations applied before standard mapping. Useful for normalizing keycodes or representing custom keycodes that act like othes QMK keycodes including functions. Defaults to an empty tuple.

overrides Annotated[tuple[Keycode, ...], BeforeValidator(_coerce_to_tuple)]

Keycode-to-label mappings that override the standard mapping results. Applied after all other transformations. Defaults to an empty tuple.

macros Annotated[tuple[Macro, ...], BeforeValidator(_coerce_to_tuple)]

Macro references with optional names and previews. Defaults to an empty tuple.

tap_dances Annotated[tuple[TapDance, ...], BeforeValidator(_coerce_to_tuple)]

Tap-dance references with optional names and previews. Defaults to an empty tuple.

symbol_descriptions dict[str, dict[str, str]]

User overrides for the bundled symbol description table, structured as {category: {keycode: description}}. User keys in an existing bundled category take precedence; new categories are appended after the bundled ones. Defaults to an empty dict (no overrides).

Example:

symbol_descriptions:
  Modifiers:
    KC_LEFT_CTRL: "Control (my label)"
  "My Section":
    MY_KEY: "Does the thing"
function_descriptions dict[str, dict[str, str]]

User overrides for the bundled function description table, same shape as symbol_descriptions. Defaults to an empty dict (no overrides).

Example:

function_descriptions:
  Layers:
    MO: "Custom MO description with @0;"
symbol_legend_aliases dict[str, str]

Shallow-merge overrides for the bundled symbol_legend_aliases map. Each entry maps a keycode to the canonical keycode whose legend entry it should share. Defaults to an empty dict (no overrides).

Example:

symbol_legend_aliases:
  KC_RIGHT_GUI: KC_LEFT_GUI
Example
>>> keycodes = Keycodes(
...     pre_process=(Keycode(keycode="LCTL_T(KC_A)", target="MT(MOD_LCTL,KC_A)"),),
...     overrides=(Keycode(keycode="KC_SPC", target="Space"),),
... )
>>> len(keycodes.overrides)
1

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

pre_process class-attribute instance-attribute

pre_process: Annotated[
    tuple[Keycode, ...], BeforeValidator(_coerce_to_tuple)
] = ()

overrides class-attribute instance-attribute

overrides: Annotated[
    tuple[Keycode, ...], BeforeValidator(_coerce_to_tuple)
] = ()

macros class-attribute instance-attribute

macros: Annotated[
    tuple[Macro, ...], BeforeValidator(_coerce_to_tuple)
] = ()

tap_dances class-attribute instance-attribute

tap_dances: Annotated[
    tuple[TapDance, ...], BeforeValidator(_coerce_to_tuple)
] = ()

symbol_descriptions class-attribute instance-attribute

symbol_descriptions: dict[str, dict[str, str]] = Field(
    default_factory=dict
)

function_descriptions class-attribute instance-attribute

function_descriptions: dict[str, dict[str, str]] = Field(
    default_factory=dict
)

symbol_legend_aliases class-attribute instance-attribute

symbol_legend_aliases: dict[str, str] = Field(
    default_factory=dict
)

__hash__

__hash__() -> int
Source code in src/skim/data/config.py
def __hash__(self) -> int:
    return hash(
        (
            self.pre_process,
            self.overrides,
            self.macros,
            self.tap_dances,
            tuple(
                (cat, tuple(sorted(items.items())))
                for cat, items in sorted(self.symbol_descriptions.items())
            ),
            tuple(
                (cat, tuple(sorted(items.items())))
                for cat, items in sorted(self.function_descriptions.items())
            ),
            tuple(sorted(self.symbol_legend_aliases.items())),
        )
    )

Spacing

Bases: BaseModel

Configurable spacing values for the document layout.

Every gap, padding, and inset the renderer applies has a built-in proportional default. Override any of them here. Each field accepts:

  • Float < 1.0 — proportion of the field's base.
  • Float ≥ 1.0 — absolute SVG units (independent of doc width).
  • String "N%" — shorthand for N / 100.0 (proportion form).
  • None (default) — the field's built-in default proportion.

Each field documents its base. Most scale to the document width (output.layout.width); the two cluster geometry fields scale to the cluster's own width so they stay proportional regardless of how the keyboard is positioned in the canvas.

Example
>>> spacing = Spacing(margin=20, inset="3%", chip_padding=12)
>>> spacing.margin
20.0
>>> spacing.inset
0.03
>>> spacing.chip_padding
12.0

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

margin class-attribute instance-attribute

margin: SpacingValue = Field(default=None)

Canvas edge → outer border line. Default: 0 (flush).

inset class-attribute instance-attribute

inset: SpacingValue = Field(default=None)

Border line → content. Also the inter-element gap inside the document Column. Default: 40/1600 (2.5%) of doc width.

column_gap class-attribute instance-attribute

column_gap: SpacingValue = Field(default=None)

Horizontal gap between half-keyboards (and between side-by-side sections). Default: 40/1600 (2.5%) of doc width.

section_spacing class-attribute instance-attribute

section_spacing: SpacingValue = Field(default=None)

Section title stripe → section body. Default: 24/1600 (1.5%) of doc width.

section_title_rule_gap class-attribute instance-attribute

section_title_rule_gap: SpacingValue = Field(default=None)

Section title bottom → rule line below it. Default: 9/1600 (~0.56%) of doc width.

table_header_spacing class-attribute instance-attribute

table_header_spacing: SpacingValue = Field(default=None)

Table header → first row (also: chip → cells, named-macro header → pill row). Default: 12/1600 (0.75%) of doc width.

table_col_spacing class-attribute instance-attribute

table_col_spacing: SpacingValue = Field(default=None)

Between adjacent table columns (pills, cells). Default: 6/1600 (0.375%) of doc width.

table_row_spacing class-attribute instance-attribute

table_row_spacing: SpacingValue = Field(default=None)

Between adjacent table rows. Default: 9/1600 (~0.56%) of doc width.

finger_key_gap class-attribute instance-attribute

finger_key_gap: SpacingValue = Field(default=None)

Center key → outer keys inside a finger cluster. Base: finger cluster width. Default: 1.8%.

thumb_key_gap class-attribute instance-attribute

thumb_key_gap: SpacingValue = Field(default=None)

Vertical gap above each outer thumb key (pad / nail / up / knuckle). Base: thumb cluster width. Default: 3.8%.

layer_indicator_spacing class-attribute instance-attribute

layer_indicator_spacing: SpacingValue = Field(default=None)

Outer key → its layer indicator circle. Default chosen so finger and thumb indicators share the same gap regardless of cluster size.

chip_padding class-attribute instance-attribute

chip_padding: SpacingValue = Field(default=None)

Symmetric horizontal inset inside any chip. Vertical inset is derived as chip_padding * 0.25. Default: 20/1600 (1.25%) of doc width.

tap_dance_pill_padding class-attribute instance-attribute

tap_dance_pill_padding: SpacingValue = Field(default=None)

Symmetric horizontal inset inside a tap-dance pill (cell). Vertical inset is derived as tap_dance_pill_padding * 0.25. Default: 20/1600 (1.25%) of doc width.

macro_action_inset class-attribute instance-attribute

macro_action_inset: SpacingValue = Field(default=None)

Uniform inset for all three positions inside a macro pill — pill edge → icon centre, icon centre → text start, text end → pill edge. Default: 10/1600 (0.625%) of doc width.

layer_badge_inset class-attribute instance-attribute

layer_badge_inset: SpacingValue = Field(default=None)

Leading horizontal inset inside a layer badge (badge edge → label text). Trailing inset is derived as layer_badge_inset * 2. Default: 15/1600 (~0.94%) of doc width.

Layout

Bases: BaseModel

Layout dimensions and spacing configuration.

Controls the overall dimensions and spacing of generated keymap images. The height is calculated automatically based on the width to maintain the correct aspect ratio for the Svalboard layout.

Attributes:

Name Type Description
width float

Total width of the generated image in SVG units (typically pixels at default scale). Defaults to 1600.

spacing Spacing

Spacing configuration for margins and padding. Defaults to a Spacing instance with default values.

Example
>>> layout = Layout(width=1200, spacing=Spacing(margin=20))
>>> layout.width
1200.0

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

width class-attribute instance-attribute

width: float = 1600

spacing class-attribute instance-attribute

spacing: Spacing = Field(default_factory=Spacing)

Border

Bases: BaseModel

Border styling configuration.

Controls the appearance of borders drawn around the keyboard layout and optionally around individual key groups.

Attributes:

Name Type Description
width float

Line width of the border in SVG units. Defaults to 2.

radius float

Corner radius for rounded borders in SVG units. Set to 0 for square corners. Defaults to 10.

Example
>>> border = Border(width=3, radius=15)
>>> border.radius
15.0

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

width class-attribute instance-attribute

width: float = 2

radius class-attribute instance-attribute

radius: float = 10

LayerConnector

Bases: BaseModel

Styling for the dotted connector paths in the keymap overview.

The overview links each layer indicator circle to its key on the miniature keymap with a dotted path. This block configures visibility, stroke, and dotted cadence. The two stroke / spacing fields follow the :data:SpacingValue magnitude rule (< 1.0 proportion of doc width, >= 1.0 absolute SVG units, "N%" string shorthand).

Attributes:

Name Type Description
show bool

Whether to draw connector paths in the overview at all. Defaults to True.

width SpacingValue

Stroke width of the connector path. Default: 4.375 / 1600 (~0.27%) of doc width — the legacy magic number tuned to read clearly on the canonical 1600-unit overview.

dot_spacing SpacingValue

Gap between adjacent dots along the path — controls the visible cadence of the dotted line. Default: 12.25 / 1600 (~0.77%) of doc width.

Example
>>> connector = LayerConnector(show=False, width=5, dot_spacing="1%")
>>> connector.show
False
>>> connector.width
5.0
>>> connector.dot_spacing
0.01

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

show class-attribute instance-attribute

show: bool = True

width class-attribute instance-attribute

width: SpacingValue = Field(default=None)

dot_spacing class-attribute instance-attribute

dot_spacing: SpacingValue = Field(default=None)

LayerIndicator

Bases: BaseModel

Styling for the layer-indicator badges painted next to layer-switch keys in each cluster (and the matching badges in the overview's LAYERS column).

Mirrors :class:LayerConnector — visibility flag plus a stroke width that follows the :data:SpacingValue magnitude rule (< 1.0 proportion of doc width, >= 1.0 absolute SVG units, "N%" string shorthand).

The gap between an outer key's edge and its indicator circle lives elsewhere, on output.layout.spacing.layer_indicator_spacing, since it's a spacing value that applies between two elements rather than a property of the indicator itself.

Attributes:

Name Type Description
show bool

Whether to draw layer-indicator badges. Defaults to True.

width SpacingValue

Stroke width of the indicator circle. Default: 2.0 / 1600 (~0.125%) of doc width.

Example
>>> indicator = LayerIndicator(show=True, width=3)
>>> indicator.show
True
>>> indicator.width
3.0

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

show class-attribute instance-attribute

show: bool = True

width class-attribute instance-attribute

width: SpacingValue = Field(default=None)

Strokes

Bases: BaseModel

Configurable stroke widths for the rendered chrome.

Two stroke widths live here; both follow the :data:SpacingValue magnitude rule (< 1.0 proportion of doc width, >= 1.0 absolute SVG units, "N%" string shorthand).

Three stroke values live elsewhere because they're tied to a visibility flag or other styling on the same conceptual element:

  • output.style.border.width — paired with Border.radius.
  • output.style.overview.layer_connector.width — paired with layer_connector.show and dot_spacing.
  • output.style.layer_indicator.width — paired with layer_indicator.show.

Attributes:

Name Type Description
chip_outline SpacingValue

Stroke around macro and tap-dance chips. Default: 1.2 / 1600 (~0.075%) of doc width.

header_rule SpacingValue

Stroke of every header rule — the section title stripe rule and the named-macro hairline below the chip. Default: 1.2 / 1600 (~0.075%) of doc width.

Example
>>> strokes = Strokes(chip_outline=2, header_rule="0.1%")
>>> strokes.chip_outline
2.0
>>> strokes.header_rule
0.001

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

chip_outline class-attribute instance-attribute

chip_outline: SpacingValue = Field(default=None)

header_rule class-attribute instance-attribute

header_rule: SpacingValue = Field(default=None)

LayerColor

Bases: BaseModel

Color configuration for a keyboard layer.

Defines the colour scheme used for keys on a specific layer. Each cluster position (centre, north, east, south, west, double-south) pulls its fill from a 6-stop gradient — adjacent keys land on adjacent stops, so a cluster reads with visual depth.

Two ways to populate the gradient:

  • Provide base_color (and optionally color_index) and leave gradient at None. :func:skim.application.keymap_generator.draw_keymap auto-derives a 6-stop gradient via :func:skim.application.render.styling.make_gradient, anchoring base_color at color_index and stepping darker / lighter to fill the surrounding stops.
  • Set gradient explicitly to a 6-tuple of CSS colour strings. In that case draw_keymap keeps the user-supplied tuple verbatim. color_index still matters: it picks which stop the rest of the render path treats as the layer's "primary" colour (indicator circles, layer badges, the layer-trigger highlight on a source key).

Attributes:

Name Type Description
base_color str

The primary CSS colour for the layer (e.g. "#FF0000", "red", "rgb(255,0,0)"). Used both as the gradient anchor when gradient is None and as the layer's "primary" colour everywhere a single colour is needed (e.g. the auto-mouse-layer accent).

color_index int

Position (0–5) where base_color lands in the gradient. Defaults to 2 (the third stop). Used by :func:make_gradient when auto-deriving the gradient and by the renderer to pick the layer's "primary" stop when gradient is set explicitly.

gradient Annotated[tuple[str, str, str, str, str, str], BeforeValidator(_coerce_to_tuple)] | None

Optional 6-tuple of CSS colour strings — one per cluster position. When None the gradient is auto-derived from base_color and color_index at keymap-generation time, so the rendered output always sees a fully-populated gradient regardless of which form the user wrote.

Example
>>> # Auto-derived gradient — base_color anchors index 2.
>>> layer = LayerColor(base_color="#FF0000")
>>> layer.gradient is None
True

>>> # Explicit 6-stop gradient.
>>> layer = LayerColor(
...     base_color="#FF0000",
...     gradient=("#FF0000", "#CC0000", "#990000", "#660000", "#330000", "#000000"),
... )
>>> layer[1]
'#CC0000'

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

base_color instance-attribute

base_color: str

color_index class-attribute instance-attribute

color_index: int = 2

gradient class-attribute instance-attribute

gradient: (
    Annotated[
        tuple[str, str, str, str, str, str],
        BeforeValidator(_coerce_to_tuple),
    ]
    | None
) = None

dark_accent_color property

dark_accent_color: str

Get the darker accent color for this layer.

Returns the second color in the gradient (index 1) if a gradient is defined, otherwise returns the base_color. This is typically used for key borders, shadows, or accent elements.

Returns:

Type Description
str

A CSS color string for the accent color.

Example
>>> layer = LayerColor(
...     base_color="#FF0000",
...     gradient=("#FF0000", "#AA0000", "#880000", "#660000", "#440000", "#220000"),
... )
>>> layer.dark_accent_color
'#AA0000'

__getitem__

__getitem__(index: int) -> str

Get the color for a specific cluster position.

Parameters:

Name Type Description Default
index int

Position index from 0-5 corresponding to cluster key positions.

required

Returns:

Type Description
str

The CSS colour string for the gradient stop at index.

str

When gradient is None (the user-facing schema's

str

"let Skim derive it" case), falls back to base_color

str

for every position so the lookup never fails. The

str

keymap-generation pipeline replaces None gradients

str

with auto-derived 6-stop tuples before rendering, so the

str

fallback only kicks in for callers that bypass that

str

pipeline (tests, introspection, the TUI's pre-fill path).

Raises:

Type Description
IndexError

If index is outside the valid range (0-5).

Example
>>> layer = LayerColor(base_color="#FFF")
>>> layer[0]
'#FFF'
Source code in src/skim/data/config.py
def __getitem__(self, index: int) -> str:
    """Get the color for a specific cluster position.

    Args:
        index: Position index from 0-5 corresponding to cluster
            key positions.

    Returns:
        The CSS colour string for the gradient stop at ``index``.
        When ``gradient`` is ``None`` (the user-facing schema's
        "let Skim derive it" case), falls back to ``base_color``
        for every position so the lookup never fails. The
        keymap-generation pipeline replaces ``None`` gradients
        with auto-derived 6-stop tuples before rendering, so the
        fallback only kicks in for callers that bypass that
        pipeline (tests, introspection, the TUI's pre-fill path).

    Raises:
        IndexError: If index is outside the valid range (0-5).

    Example:
        ```pycon
        >>> layer = LayerColor(base_color="#FFF")
        >>> layer[0]
        '#FFF'

        ```
    """
    if not 0 <= index < 6:
        raise IndexError(f"Gradient index {index} out of range (0-5)")
    if not self.gradient:
        return self.base_color
    return self.gradient[index]

__str__

__str__() -> str

Return a string representation of the color configuration.

Returns:

Type Description
str

A JSON-like array string of the colors. For single-color

str

mode, returns a single-element array. For gradient mode,

str

returns all 6 colors.

Example
>>> str(LayerColor(base_color="#FFF"))
'["#FFF"]'
Source code in src/skim/data/config.py
def __str__(self) -> str:
    """Return a string representation of the color configuration.

    Returns:
        A JSON-like array string of the colors. For single-color
        mode, returns a single-element array. For gradient mode,
        returns all 6 colors.

    Example:
        ```pycon
        >>> str(LayerColor(base_color="#FFF"))
        '["#FFF"]'

        ```
    """
    str_colors = (
        (f'"{self.base_color}"',) if not self.gradient else (f'"{x}"' for x in self.gradient)
    )
    return f"[{', '.join(str_colors)}]"

Palette

Bases: BaseModel

Color palette configuration for the entire keyboard.

Defines the color scheme used throughout the generated keymap images, including background colors, text colors, and per-layer key colors.

Attributes:

Name Type Description
neutral_color str

Color for keys that don't have layer-specific coloring (e.g., some thumb cluster keys). Defaults to "#6F768B" (gray).

text_color str

Default text color for non key labels. Defaults to "black".

key_label_color str

Text color for key labels. Defaults to "white" for contrast against typically dark key backgrounds.

background_color str

Background color for the entire image. Defaults to "white".

border_color str

Color for keyboard and cluster borders. Defaults to "black".

macro_color str

Background color for macro badges and macro-table titles in the rendered keymap. Defaults to "#89511C".

tap_dance_color str

Background color for tap-dance badges and tap-dance-table titles in the rendered keymap. Defaults to "#41687F".

layers Annotated[tuple[LayerColor, ...], BeforeValidator(_coerce_to_tuple)]

Tuple of LayerColor configurations, one per layer. Layer indices correspond to positions in this tuple. Defaults to an empty tuple.

Example
>>> palette = Palette(
...     background_color="#F0F0F0",
...     layers=(
...         LayerColor(base_color="#3366CC"),
...         LayerColor(base_color="#CC6633"),
...     ),
... )
>>> palette.background_color
'#F0F0F0'

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

neutral_color class-attribute instance-attribute

neutral_color: str = '#6F768B'

text_color class-attribute instance-attribute

text_color: str = 'black'

key_label_color class-attribute instance-attribute

key_label_color: str = 'white'

background_color class-attribute instance-attribute

background_color: str = 'white'

border_color class-attribute instance-attribute

border_color: str = 'black'

macro_color class-attribute instance-attribute

macro_color: str = '#89511C'

tap_dance_color class-attribute instance-attribute

tap_dance_color: str = '#41687F'

layers class-attribute instance-attribute

layers: Annotated[
    tuple[LayerColor, ...],
    BeforeValidator(_coerce_to_tuple),
] = ()

SplitSidePosition

Bases: str, Enum

Position options for hold-tap key symbol placement.

Controls where the "hold" portion of a hold-tap key is displayed relative to the "tap" portion. This affects keys like LT(1, KC_A) where tapping produces "A" but holding activates layer 1.

Attributes:

Name Type Description
QMK_DEFINED

Use the position defined in QMK firmware settings. This respects the argument order of the macro-functions defined by QMK, which is always the hold part as the first argument, and the tap part as the second.

INWARD

Place the hold symbol toward the center of the keyboard cluster (right on left side, left on right side).

OUTWARD

Place the hold symbol toward the outside of the keyboard cluster (left on left side, right on right side). This is the default.

QMK_DEFINED class-attribute instance-attribute

QMK_DEFINED = 'qmk'

INWARD class-attribute instance-attribute

INWARD = 'inward'

OUTWARD class-attribute instance-attribute

OUTWARD = 'outward'

SymbolLegendFlow

Bases: str, Enum

Flow direction for the symbol legend's multi-column layout.

Attributes:

Name Type Description
ROW_MAJOR

Entries fill left-to-right in the top row first, then drop to the next row.

COLUMN_MAJOR

Entries fill top-to-bottom in the leftmost column first, then move to the next column. This is the default.

ROW_MAJOR class-attribute instance-attribute

ROW_MAJOR = 'row'

COLUMN_MAJOR class-attribute instance-attribute

COLUMN_MAJOR = 'column'

MacrosLegend

Bases: BaseModel

Configuration for the macros legend table.

Controls visibility (whether the macros legend renders inside per-layer / overview images) and the body-scale multiplier applied when the macros are rendered as a standalone image.

Attributes:

Name Type Description
show bool

Whether to embed the macros legend in per-layer and overview images. Defaults to True.

scale float

Body-scale multiplier for the standalone macros image (the artifact skim generate -l macros produces). Body chips and pills scale by this factor; chrome (title, footer, outer padding) stays at the unscaled per-image size. Defaults to 1.5.

Example
>>> macros = MacrosLegend(show=True, scale=2.0)
>>> macros.scale
2.0

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

show class-attribute instance-attribute

show: bool = True

scale class-attribute instance-attribute

scale: float = Field(default=1.5, gt=0)

TapDancesLegend

Bases: BaseModel

Configuration for the tap-dances legend table.

Mirrors :class:MacrosLegend — visibility plus a body-scale multiplier for the standalone tap-dances image.

Attributes:

Name Type Description
show bool

Whether to embed the tap-dances legend in per-layer and overview images. Defaults to True.

scale float

Body-scale multiplier for the standalone tap-dances image. Defaults to 1.5.

Example
>>> td = TapDancesLegend(show=False)
>>> td.show
False
>>> td.scale
1.5

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

show class-attribute instance-attribute

show: bool = True

scale class-attribute instance-attribute

scale: float = Field(default=1.5, gt=0)

SymbolsLegend

Bases: BaseModel

Configuration for the symbols legend table.

Carries the same visibility / scale fields as the macros and tap-dances legends, plus two layout knobs unique to the symbol table (the multi-column layout's flow direction and column count).

Attributes:

Name Type Description
show bool

Whether to embed the symbol legend in per-layer and overview images. Defaults to True.

scale float

Body-scale multiplier for the standalone symbols image. Defaults to 1.5.

flow SymbolLegendFlowStr

Flow direction for the multi-column layout. COLUMN_MAJOR (default) fills each column top-to-bottom before moving to the next. ROW_MAJOR fills each row left-to-right before dropping to the next row.

columns int | None

When set, force the standalone symbols image to lay out at exactly this many columns and shrink the canvas to fit the resulting natural width. None (default) lets the table pick the largest column count that fits the canvas budget — current per-layer / overview behaviour.

Example
>>> symbols = SymbolsLegend(columns=3, flow="row")
>>> symbols.columns
3
>>> symbols.flow
<SymbolLegendFlow.ROW_MAJOR: 'row'>

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

show class-attribute instance-attribute

show: bool = True

scale class-attribute instance-attribute

scale: float = Field(default=1.5, gt=0)

flow class-attribute instance-attribute

flow: SymbolLegendFlowStr = Field(default=COLUMN_MAJOR)

columns class-attribute instance-attribute

columns: int | None = Field(default=None, gt=0)

LegendTables

Bases: BaseModel

Container for the three legend tables Skim renders alongside the keymap: macros, tap-dances, and symbols.

Each sub-block carries its own visibility flag and standalone-image body-scale multiplier (plus, for symbols, a flow direction and column count).

Attributes:

Name Type Description
macros MacrosLegend

Configuration for the macros legend.

tap_dances TapDancesLegend

Configuration for the tap-dances legend.

symbols SymbolsLegend

Configuration for the symbol legend.

Example
>>> legends = LegendTables(
...     macros=MacrosLegend(show=False),
...     symbols=SymbolsLegend(scale=2.0, columns=4),
... )
>>> legends.macros.show
False
>>> legends.symbols.columns
4

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

macros class-attribute instance-attribute

macros: MacrosLegend = Field(default_factory=MacrosLegend)

tap_dances class-attribute instance-attribute

tap_dances: TapDancesLegend = Field(
    default_factory=TapDancesLegend
)

symbols class-attribute instance-attribute

symbols: SymbolsLegend = Field(
    default_factory=SymbolsLegend
)

LayerOrder

Bases: str, Enum

Direction in which the overview stacks its per-layer rows.

ASCENDING class-attribute instance-attribute

ASCENDING = 'ascending'

DESCENDING class-attribute instance-attribute

DESCENDING = 'descending'

ThumbPosition

Bases: str, Enum

Where the thumb-cluster row sits in the overview stack.

TOP class-attribute instance-attribute

TOP = 'top'

BOTTOM class-attribute instance-attribute

BOTTOM = 'bottom'

FOLLOW class-attribute instance-attribute

FOLLOW = 'follow'

Overview

Bases: BaseModel

Overview-specific layout configuration.

Controls how the multi-layer overview image stacks its per-layer rows, where the (single) thumb cluster row sits among them, which layer the thumb cluster is sourced from, and the dotted layer- connector paths that link each layer-indicator badge to its key on the miniature keymap.

Attributes:

Name Type Description
layer_order LayerOrderStr

"ascending" to render layers top-to-bottom by ascending QMK index (Layer 0 at top, highest index at the bottom), "descending" to keep the legacy ordering (highest index at the top, Layer 0 at the bottom). Defaults to "descending".

thumb_position ThumbPositionStr

Where to place the thumb-cluster row in the stack. "top" puts it before every layer; "bottom" puts it after every layer (legacy behaviour); "follow" (default) keeps it directly below the layer it represents (the layer named by thumb_layer), regardless of where that layer sits in the stack.

thumb_layer int | None

QMK layer index whose thumb cluster is rendered in the overview's THUMBS row. null (the default) and absent values resolve at render time to the last rendered layer when layer_order is "descending" and to the first rendered layer when layer_order is "ascending" — the layer that visually anchors the stack regardless of whether QMK index 0 is even present in the keymap. If an explicit value is provided but the layer isn't in the keymap, the renderer falls back to the same contextual default.

layer_connector LayerConnector

Configuration for the dotted connector paths in the keymap overview (visibility + stroke + dot cadence).

Example

Layer 0 at the top with its thumb cluster directly below it, then layers 1, 2, … going down::

output:
  style:
    overview:
      layer_order: ascending
      thumb_position: follow

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

layer_order class-attribute instance-attribute

layer_order: LayerOrderStr = Field(default=DESCENDING)

thumb_position class-attribute instance-attribute

thumb_position: ThumbPositionStr = Field(default=FOLLOW)

thumb_layer class-attribute instance-attribute

thumb_layer: int | None = None

layer_connector class-attribute instance-attribute

layer_connector: LayerConnector = Field(
    default_factory=LayerConnector
)

Style

Bases: BaseModel

Visual styling configuration for keymap images.

Controls the overall visual appearance of generated images, including colors, borders, and key labeling options.

Attributes:

Name Type Description
use_layer_colors_on_keys bool

Whether to color keys backgrounds based on the layer it activates. When True, keys use colors from the palette's layer list. When False, all keys use their standard colors. Defaults to True.

hold_symbol_position SplitSidePositionStr

Where to place the "hold" portion of hold-tap keys relative to the "tap" portion. See :class:SplitSidePosition for options. Defaults to OUTWARD.

use_system_fonts bool

When True, the SVG references system fonts by name instead of embedding the bundled font subsets. Smaller file size; viewers without those fonts installed will see a fallback. Defaults to False.

show_transparent_fallthrough bool

When True (default), transparent keycodes (KC_TRNS / _) on layers above 0 render the same label as layer 0 in a faded "ghost" color. Set False to leave transparent keys blank.

border Border | None

Document border configuration, or None to disable. Defaults to a :class:Border with the canonical 2-unit stroke and 10-unit corner radius.

overview Overview

Overview-image-specific layout configuration — layer-row ordering, thumb-row position, and the dotted layer-connector paths.

layer_indicator LayerIndicator

Configuration for the layer-indicator badges painted next to layer-switch keys (visibility + stroke).

legend_tables LegendTables

Container for the macros / tap-dances / symbols legend tables (visibility + scale per legend, plus the symbol-specific flow and column count).

strokes Strokes

Stroke widths for chrome lines that don't have their own dedicated block (chip outlines, header rules).

palette Palette

Color palette configuration for the entire keyboard.

Example
>>> style = Style(
...     use_layer_colors_on_keys=True,
...     hold_symbol_position=SplitSidePosition.INWARD,
... )
>>> style.hold_symbol_position
<SplitSidePosition.INWARD: 'inward'>

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

use_layer_colors_on_keys class-attribute instance-attribute

use_layer_colors_on_keys: bool = True

hold_symbol_position class-attribute instance-attribute

hold_symbol_position: SplitSidePositionStr = Field(
    default=OUTWARD
)

use_system_fonts class-attribute instance-attribute

use_system_fonts: bool = False

show_transparent_fallthrough class-attribute instance-attribute

show_transparent_fallthrough: bool = True

border class-attribute instance-attribute

border: Border | None = Field(default_factory=Border)

overview class-attribute instance-attribute

overview: Overview = Field(default_factory=Overview)

layer_indicator class-attribute instance-attribute

layer_indicator: LayerIndicator = Field(
    default_factory=LayerIndicator
)

legend_tables class-attribute instance-attribute

legend_tables: LegendTables = Field(
    default_factory=LegendTables
)

strokes class-attribute instance-attribute

strokes: Strokes = Field(default_factory=Strokes)

palette class-attribute instance-attribute

palette: Palette = Field(default_factory=Palette)

Output

Bases: BaseModel

Output configuration for generated images.

Groups together layout dimensions and visual styling settings that control the final appearance of generated keymap images.

Attributes:

Name Type Description
layout Layout

Layout dimensions and spacing configuration. Defaults to a Layout instance with default values.

style Style

Visual styling configuration. Defaults to a Style instance with default values.

keymap_title str | None

Optional title for the overview keymap image. When set, overrides the auto-generated title. Defaults to None.

copyright str | None

Optional copyright notice displayed in the overview image. Defaults to None.

Example
>>> output = Output(
...     layout=Layout(width=1000),
...     style=Style(use_layer_colors_on_keys=False),
... )
>>> output.layout.width
1000.0

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

layout class-attribute instance-attribute

layout: Layout = Field(default_factory=Layout)

style class-attribute instance-attribute

style: Style = Field(default_factory=Style)

keymap_title class-attribute instance-attribute

keymap_title: str | None = None

copyright class-attribute instance-attribute

copyright: str | None = None

SkimConfig

Bases: BaseModel

Root configuration model for skim keymap image generation.

This is the top-level configuration class that contains all settings for generating Svalboard keymap images. It can be loaded from YAML files or constructed programmatically.

The configuration is organized into three main sections: - keyboard: Hardware and layer settings - keycodes: Keycode transformation and display rules - output: Layout dimensions and visual styling

Attributes:

Name Type Description
keyboard Keyboard

Keyboard-specific configuration including hardware features and layer definitions. Defaults to a Keyboard instance with default values.

keycodes Keycodes

Keycode transformation rules including pre-processing and overrides. Defaults to a Keycodes instance with empty rule tuples.

output Output

Output configuration including layout dimensions and visual styling. Defaults to an Output instance with default values.

Example

Creating a basic configuration:

config = SkimConfig()
new_layout = config.output.layout.model_copy(update={"width": 1200})
new_output = config.output.model_copy(update={"layout": new_layout})
config = config.model_copy(update={"output": new_output})

Loading from a dictionary (e.g., parsed YAML):

data = {
    "keyboard": {
        "features": {"double_south": True},
        "layers": [{"index": 0, "id": "1", "name": "Base"}],
    },
    "output": {"layout": {"width": 1000}},
}
config = SkimConfig(**data)

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

keyboard class-attribute instance-attribute

keyboard: Keyboard = Field(default_factory=Keyboard)

keycodes class-attribute instance-attribute

keycodes: Keycodes = Field(default_factory=Keycodes)

output class-attribute instance-attribute

output: Output = Field(default_factory=Output)

resolve_spacing

resolve_spacing(
    value: float | None,
    *,
    base: float,
    default_proportion: float,
) -> float

Resolve a :data:SpacingValue to absolute SVG units.

Implements the magnitude rule:

  • None → base * default_proportion (renderer's built-in).
  • value < 1.0 → base * value (proportion of base).
  • value >= 1.0 → value (absolute SVG units).

The 1.0 boundary is conventional: proportions live in [0, 1), and any meaningful spacing in this codebase is at least 1 SVG unit (sub-pixel gaps are invisible anyway).

Example
>>> resolve_spacing(None, base=1600.0, default_proportion=0.025)
40.0
>>> resolve_spacing(0.05, base=1600.0, default_proportion=0.025)
80.0
>>> resolve_spacing(20, base=1600.0, default_proportion=0.025)
20.0
Source code in src/skim/data/config.py
def resolve_spacing(value: float | None, *, base: float, default_proportion: float) -> float:
    """Resolve a :data:`SpacingValue` to absolute SVG units.

    Implements the magnitude rule:

    - ``None`` → ``base * default_proportion`` (renderer's built-in).
    - ``value < 1.0`` → ``base * value`` (proportion of base).
    - ``value >= 1.0`` → ``value`` (absolute SVG units).

    The ``1.0`` boundary is conventional: proportions live in
    ``[0, 1)``, and any meaningful spacing in this codebase is at
    least 1 SVG unit (sub-pixel gaps are invisible anyway).

    Example:
        ```pycon
        >>> resolve_spacing(None, base=1600.0, default_proportion=0.025)
        40.0
        >>> resolve_spacing(0.05, base=1600.0, default_proportion=0.025)
        80.0
        >>> resolve_spacing(20, base=1600.0, default_proportion=0.025)
        20.0

        ```
    """
    if value is None:
        return base * default_proportion
    if value < 1.0:
        return max(0.0, base * value)
    return value

Keyboard Structures

keyboard

Keyboard cluster data structures for Svalboard key layouts.

This module provides generic container classes for representing key clusters on the Svalboard keyboard. The Svalboard has a unique 3D layout with two types of clusters:

  • Finger clusters: 5-directional keys (center, north, east, south, west) plus an optional double-south key. Each hand has 4 finger clusters (pinky, ring, middle, index).
  • Thumb clusters: 6 keys (down, pad, up, nail, knuckle, double-down). Each hand has 1 thumb cluster.

Both cluster types are generic containers that can hold any type of value, making them suitable for storing keycodes, labels, colors, or any other per-key data.

Example

Creating clusters with a default value and overrides::

from skim.data.keyboard import FingerCluster, ThumbCluster

# All keys set to empty string, except south_key
finger = FingerCluster("", south_key="A")

# Clusters are iterable and unpackable
center, north, east, south, west, dsouth = finger

Zipping multiple clusters together::

from skim.data.keyboard import FingerCluster, zip_clusters

keycodes = FingerCluster("KC_NO", center_key="KC_A")
labels = FingerCluster("", center_key="A")

# Create a cluster where each position contains both values
combined = zip_clusters(FingerCluster, "KeyData", codes=keycodes, labels=labels)
# combined.center_key.codes == "KC_A"
# combined.center_key.labels == "A"

T module-attribute

T = TypeVar('T')

TypeVar for the value type stored in cluster positions.

U module-attribute

U = TypeVar('U')

TypeVar for the mapped value type in map operations.

__all__ module-attribute

__all__ = [
    "ClusterT",
    "FingerCluster",
    "ThumbCluster",
    "SplitSide",
    "SvalboardLayout",
    "SvalboardKeymap",
    "zip_clusters",
    "zip_layouts",
]

SVALBOARD_KEY_COUNT module-attribute

SVALBOARD_KEY_COUNT = 60

Constant value representing how many keys a Svalboard keyboard has.

SVALBOARD_SIDE_KEY_COUNT module-attribute

SVALBOARD_SIDE_KEY_COUNT = SVALBOARD_KEY_COUNT / 2

Constant representing how many keys a single sides of the Svalboard has.

SVALBOARD_CLUSTER_KEY_COUNT module-attribute

SVALBOARD_CLUSTER_KEY_COUNT = 6

Constant value representing how many keys a single key cluster has.

ClusterT module-attribute

ClusterT = TypeVar('ClusterT', bound=_ClusterBase[Any])

TypeVar bound to cluster types for writing generic cluster functions.

This TypeVar is constrained to FingerCluster or ThumbCluster (or any subclass of _ClusterBase), enabling type-safe generic functions that work with any cluster type.

Example

Writing a generic function that works with any cluster::

from skim.data.keyboard import ClusterT, FingerCluster, ThumbCluster

def count_non_empty(cluster: ClusterT) -> int:
    return sum(1 for value in cluster if value)

finger = FingerCluster("", center_key="A")
thumb = ThumbCluster("X")

count_non_empty(finger)  # Works with FingerCluster
count_non_empty(thumb)   # Works with ThumbCluster

FingerCluster dataclass

FingerCluster(
    *,
    center_key: T,
    north_key: T,
    east_key: T,
    south_key: T,
    west_key: T,
    double_south_key: T,
)
FingerCluster(
    default: T,
    *,
    center_key: T = ...,
    north_key: T = ...,
    east_key: T = ...,
    south_key: T = ...,
    west_key: T = ...,
    double_south_key: T = ...,
)
FingerCluster(default: T | _Unset = _UNSET, **kwargs: Any)

Bases: _ClusterBase[T]

A container for values associated with a Svalboard finger cluster.

Each finger on the Svalboard has a 5-directional key cluster arranged in a cross pattern, plus an optional double-south key for chording. The physical layout corresponds to pushing the finger in different directions:

  • center_key: The home/rest position (pressing straight down)
  • north_key: Pushing the finger forward (away from palm)
  • east_key: Pushing the finger toward the thumb
  • south_key: Pulling the finger backward (toward palm)
  • west_key: Pushing the finger away from the thumb
  • double_south_key: A secondary south key on certain Svalboard boards

This class is generic and can store any type of value at each position, making it suitable for keycodes, labels, styling information, or any other per-key data.

The class supports two initialization modes: 1. Explicit: All six fields must be provided as keyword arguments 2. Default with overrides: A default value fills all positions, with optional keyword arguments to override specific positions

Attributes:

Name Type Description
center_key T

Value for the center (home) position.

north_key T

Value for the north (forward) position.

east_key T

Value for the east (thumb-ward) position.

south_key T

Value for the south (backward) position.

west_key T

Value for the west (away from thumb) position.

double_south_key T

Value for the double-south position.

Example

Creating with all explicit values::

cluster = FingerCluster(
    center_key="A",
    north_key="B",
    east_key="C",
    south_key="D",
    west_key="E",
    double_south_key="F",
)

Creating with a default and overrides::

cluster = FingerCluster("", south_key="Space")
# center="", north="", east="", south="Space", west="", dsouth=""

Unpacking values::

center, north, east, south, west, dsouth = cluster

Initialize the finger cluster.

Parameters:

Name Type Description Default
default T | _Unset

Optional default value for all positions. If provided, any position not explicitly set via kwargs will use this value. If not provided, all positions must be set via kwargs.

_UNSET
**kwargs Any

Key position values. Valid keys are: center_key, north_key, east_key, south_key, west_key, double_south_key.

{}

Raises:

Type Description
TypeError

If default is not provided and any required key position is missing from kwargs.

Source code in src/skim/data/keyboard.py
def __init__(self, default: T | _Unset = _UNSET, **kwargs: Any) -> None:
    """Initialize the finger cluster.

    Args:
        default: Optional default value for all positions. If provided,
            any position not explicitly set via kwargs will use this value.
            If not provided, all positions must be set via kwargs.
        **kwargs: Key position values. Valid keys are: center_key,
            north_key, east_key, south_key, west_key, double_south_key.

    Raises:
        TypeError: If default is not provided and any required key
            position is missing from kwargs.
    """
    self._setup_cluster(default, kwargs)

center_key instance-attribute

center_key: T

north_key instance-attribute

north_key: T

east_key instance-attribute

east_key: T

south_key instance-attribute

south_key: T

west_key instance-attribute

west_key: T

double_south_key instance-attribute

double_south_key: T

from_sequence classmethod

from_sequence(values: Sequence[T]) -> FingerCluster[T]

Create a FingerCluster from a sequence of 6 values.

See :meth:_ClusterBase.from_sequence for full documentation.

Source code in src/skim/data/keyboard.py
@classmethod
def from_sequence(cls, values: Sequence[T]) -> "FingerCluster[T]":
    """Create a FingerCluster from a sequence of 6 values.

    See :meth:`_ClusterBase.from_sequence` for full documentation.
    """
    if len(values) != 6:
        raise ValueError(f"Expected exactly 6 values, got {len(values)}")
    field_names = [f.name for f in cls._get_fields()]
    return cls(**dict(zip(field_names, values, strict=True)))

map

map(fn: Callable[[T], U]) -> FingerCluster[U]

Create a new FingerCluster by applying a function to each value.

See :meth:_ClusterBase.map for full documentation.

Source code in src/skim/data/keyboard.py
def map(self, fn: Callable[[T], U]) -> "FingerCluster[U]":
    """Create a new FingerCluster by applying a function to each value.

    See :meth:`_ClusterBase.map` for full documentation.
    """
    return FingerCluster.from_sequence([fn(v) for v in self])

ThumbCluster dataclass

ThumbCluster(
    *,
    down_key: T,
    pad_key: T,
    up_key: T,
    nail_key: T,
    knuckle_key: T,
    double_down_key: T,
)
ThumbCluster(
    default: T,
    *,
    down_key: T = ...,
    pad_key: T = ...,
    up_key: T = ...,
    nail_key: T = ...,
    knuckle_key: T = ...,
    double_down_key: T = ...,
)
ThumbCluster(default: T | _Unset = _UNSET, **kwargs: Any)

Bases: _ClusterBase[T]

A container for values associated with a Svalboard thumb cluster.

Each thumb on the Svalboard has a cluster of keys operated by different parts of the thumb and in different directions. The physical layout corresponds to:

  • down_key: Pressing the thumb straight down
  • pad_key: Using the thumb pad (fleshy part)
  • up_key: Pressing upward with the thumb
  • nail_key: Using the thumbnail area
  • knuckle_key: Using the thumb knuckle
  • double_down_key: A secondary down position activated by exercing extra force to the down key

This class is generic and can store any type of value at each position, making it suitable for keycodes, labels, styling information, or any other per-key data.

The class supports two initialization modes: 1. Explicit: All six fields must be provided as keyword arguments 2. Default with overrides: A default value fills all positions, with optional keyword arguments to override specific positions

Attributes:

Name Type Description
down_key T

Value for the down (pressing) position.

pad_key T

Value for the thumb pad position.

up_key T

Value for the up position.

nail_key T

Value for the thumbnail position.

knuckle_key T

Value for the thumb knuckle position.

double_down_key T

Value for the double-down position.

Example

Creating with all explicit values::

cluster = ThumbCluster(
    down_key="Space",
    pad_key="Enter",
    up_key="Tab",
    nail_key="Esc",
    knuckle_key="Ctrl",
    double_down_key="",
)

Creating with a default and overrides::

cluster = ThumbCluster("", down_key="Space")
# down="Space", pad="", up="", nail="", knuckle="", ddown=""

Unpacking values::

down, pad, up, nail, knuckle, ddown = cluster

Initialize the thumb cluster.

Parameters:

Name Type Description Default
default T | _Unset

Optional default value for all positions. If provided, any position not explicitly set via kwargs will use this value. If not provided, all positions must be set via kwargs.

_UNSET
**kwargs Any

Key position values. Valid keys are: down_key, pad_key, up_key, nail_key, knuckle_key, double_down_key.

{}

Raises:

Type Description
TypeError

If default is not provided and any required key position is missing from kwargs.

Source code in src/skim/data/keyboard.py
def __init__(self, default: T | _Unset = _UNSET, **kwargs: Any) -> None:
    """Initialize the thumb cluster.

    Args:
        default: Optional default value for all positions. If provided,
            any position not explicitly set via kwargs will use this value.
            If not provided, all positions must be set via kwargs.
        **kwargs: Key position values. Valid keys are: down_key,
            pad_key, up_key, nail_key, knuckle_key, double_down_key.

    Raises:
        TypeError: If default is not provided and any required key
            position is missing from kwargs.
    """
    self._setup_cluster(default, kwargs)

down_key instance-attribute

down_key: T

pad_key instance-attribute

pad_key: T

up_key instance-attribute

up_key: T

nail_key instance-attribute

nail_key: T

knuckle_key instance-attribute

knuckle_key: T

double_down_key instance-attribute

double_down_key: T

from_sequence classmethod

from_sequence(values: Sequence[T]) -> ThumbCluster[T]

Create a ThumbCluster from a sequence of 6 values.

See :meth:_ClusterBase.from_sequence for full documentation.

Source code in src/skim/data/keyboard.py
@classmethod
def from_sequence(cls, values: Sequence[T]) -> "ThumbCluster[T]":
    """Create a ThumbCluster from a sequence of 6 values.

    See :meth:`_ClusterBase.from_sequence` for full documentation.
    """
    if len(values) != 6:
        raise ValueError(f"Expected exactly 6 values, got {len(values)}")
    field_names = [f.name for f in cls._get_fields()]
    return cls(**dict(zip(field_names, values, strict=True)))

map

map(fn: Callable[[T], U]) -> ThumbCluster[U]

Create a new ThumbCluster by applying a function to each value.

See :meth:_ClusterBase.map for full documentation.

Source code in src/skim/data/keyboard.py
def map(self, fn: Callable[[T], U]) -> "ThumbCluster[U]":
    """Create a new ThumbCluster by applying a function to each value.

    See :meth:`_ClusterBase.map` for full documentation.
    """
    return ThumbCluster.from_sequence([fn(v) for v in self])

SplitSide dataclass

SplitSide(
    index: FingerCluster[T],
    middle: FingerCluster[T],
    ring: FingerCluster[T],
    pinky: FingerCluster[T],
    thumb: ThumbCluster[T],
)

Bases: Generic[T]

A container representing one side (left or right) of a Svalboard keyboard.

Each side of the Svalboard consists of four finger clusters (index, middle, ring, pinky) and one thumb cluster. This class provides a generic container that can hold any type of per-key data for an entire side of the keyboard.

The class is generic over type T, which represents the type of value stored at each key position, allowing it to be used for keycodes, labels, colors, or any other per-key data.

Attributes:

Name Type Description
index FingerCluster[T]

The index finger cluster (6 keys).

middle FingerCluster[T]

The middle finger cluster (6 keys).

ring FingerCluster[T]

The ring finger cluster (6 keys).

pinky FingerCluster[T]

The pinky finger cluster (6 keys).

thumb ThumbCluster[T]

The thumb cluster (6 keys).

Example

Creating a side with string values::

from skim.data.keyboard import SplitSide, FingerCluster, ThumbCluster

side = SplitSide(
    index=FingerCluster(""),
    middle=FingerCluster(""),
    ring=FingerCluster(""),
    pinky=FingerCluster(""),
    thumb=ThumbCluster(""),
)

Accessing keys by linear index::

key = side[0]  # First key of index finger (center)
key = side[24]  # First key of thumb (down)

Iterating over all clusters::

for cluster in side:
    print(cluster)

index instance-attribute

index: FingerCluster[T]

middle instance-attribute

middle: FingerCluster[T]

ring instance-attribute

pinky instance-attribute

pinky: FingerCluster[T]

thumb instance-attribute

thumb: ThumbCluster[T]

fingers property

Return all finger clusters as a tuple.

Returns:

Type Description
FingerCluster[T]

A tuple of the four finger clusters in order:

FingerCluster[T]

(index, middle, ring, pinky).

__iter__

__iter__() -> Iterator[
    FingerCluster[T] | ThumbCluster[T]
]

Iterate over all clusters in the side.

Yields clusters in order: index, middle, ring, pinky, thumb.

Yields:

Type Description
FingerCluster[T] | ThumbCluster[T]

Each cluster on this side of the keyboard, starting with finger

FingerCluster[T] | ThumbCluster[T]

clusters and ending with the thumb cluster.

Source code in src/skim/data/keyboard.py
def __iter__(self) -> Iterator[FingerCluster[T] | ThumbCluster[T]]:
    """Iterate over all clusters in the side.

    Yields clusters in order: index, middle, ring, pinky, thumb.

    Yields:
        Each cluster on this side of the keyboard, starting with finger
        clusters and ending with the thumb cluster.
    """
    yield self.index
    yield self.middle
    yield self.ring
    yield self.pinky
    yield self.thumb

__getitem__

__getitem__(idx: int) -> T

Access a key value by linear index.

Provides flat indexing across all 30 keys on this side. Keys 0-23 are the finger clusters (6 keys each × 4 fingers), and keys 24-29 are the thumb cluster.

Parameters:

Name Type Description Default
idx int

Linear index from 0-29. Indices 0-5 are index finger, 6-11 are middle finger, 12-17 are ring finger, 18-23 are pinky finger, and 24-29 are thumb.

required

Returns:

Type Description
T

The value stored at the specified key position.

Raises:

Type Description
IndexError

If idx is outside the valid range (0-29).

Example

Accessing individual keys::

side[0]  # Index finger center key
side[6]  # Middle finger center key
side[24]  # Thumb down key
Source code in src/skim/data/keyboard.py
def __getitem__(self, idx: int) -> T:
    """Access a key value by linear index.

    Provides flat indexing across all 30 keys on this side. Keys 0-23 are
    the finger clusters (6 keys each × 4 fingers), and keys 24-29 are the
    thumb cluster.

    Args:
        idx: Linear index from 0-29. Indices 0-5 are index finger,
            6-11 are middle finger, 12-17 are ring finger, 18-23 are
            pinky finger, and 24-29 are thumb.

    Returns:
        The value stored at the specified key position.

    Raises:
        IndexError: If idx is outside the valid range (0-29).

    Example:
        Accessing individual keys::

            side[0]  # Index finger center key
            side[6]  # Middle finger center key
            side[24]  # Thumb down key
    """
    if 0 <= idx < 24:
        cluster_idx, key_idx = divmod(idx, SVALBOARD_CLUSTER_KEY_COUNT)
        cluster = getattr(self, self._FINGER_ORDER[cluster_idx])
        # noinspection PyProtectedMember
        return getattr(cluster, cluster._get_fields()[key_idx].name)
    if 24 <= idx < 30:
        return getattr(self.thumb, self._THUMB_ORDER[idx - SplitSide._THUMB_FIRST_INDEX])
    raise IndexError(f"Index {idx} out of range for SplitSide (0-29)")

SvalboardLayout dataclass

SvalboardLayout(left: SplitSide[T], right: SplitSide[T])

Bases: Generic[T]

A container representing the complete Svalboard keyboard layout.

The Svalboard is a split ergonomic keyboard with a unique 3D key arrangement. This class provides a container for storing per-key data across the entire keyboard, with support for both hierarchical access (side → cluster → key) and flat linear indexing.

The keyboard has 60 total keys: - Left side: 24 finger keys (4 clusters × 6 keys) + 6 thumb keys = 30 keys - Right side: 24 finger keys (4 clusters × 6 keys) + 6 thumb keys = 30 keys

The class is generic over type T, which represents the type of value stored at each key position.

Attributes:

Name Type Description
left SplitSide[T]

The left side of the keyboard (30 keys).

right SplitSide[T]

The right side of the keyboard (30 keys).

Example

Creating a layout with string values::

from skim.data.keyboard import (
    SvalboardLayout,
    SplitSide,
    FingerCluster,
    ThumbCluster,
)


def make_side():
    return SplitSide(
        index=FingerCluster(""),
        middle=FingerCluster(""),
        ring=FingerCluster(""),
        pinky=FingerCluster(""),
        thumb=ThumbCluster(""),
    )


layout = SvalboardLayout(left=make_side(), right=make_side())

Accessing keys by linear index::

key = layout[0]  # Right index finger center
key = layout[24]  # Left index finger center
key = layout[48]  # Right thumb down
key = layout[54]  # Left thumb down

Iterating over all keys::

for key in layout:
    print(key)  # Prints all 60 keys

left instance-attribute

left: SplitSide[T]

right instance-attribute

right: SplitSide[T]

__iter__

__iter__() -> Iterator[T]

Iterate over all key values in the layout.

Yields keys in the standard Svalboard order: 1. Right hand finger clusters (index → pinky, 24 keys) 2. Left hand finger clusters (index → pinky, 24 keys) 3. Right thumb cluster (6 keys) 4. Left thumb cluster (6 keys)

This order matches the typical QMK keymap array layout for the Svalboard.

Yields:

Type Description
T

Each key value in the layout, totaling 60 values.

Source code in src/skim/data/keyboard.py
def __iter__(self) -> Iterator[T]:
    """Iterate over all key values in the layout.

    Yields keys in the standard Svalboard order:
    1. Right hand finger clusters (index → pinky, 24 keys)
    2. Left hand finger clusters (index → pinky, 24 keys)
    3. Right thumb cluster (6 keys)
    4. Left thumb cluster (6 keys)

    This order matches the typical QMK keymap array layout for the Svalboard.

    Yields:
        Each key value in the layout, totaling 60 values.
    """
    for finger in self.right.fingers:
        yield from finger
    for finger in self.left.fingers:
        yield from finger
    yield from self.right.thumb
    yield from self.left.thumb

__getitem__

__getitem__(idx: int) -> T

Access a key value by linear index.

Provides flat indexing across all 60 keys in the layout using the standard Svalboard key ordering: - 0-23: Right hand finger keys - 24-47: Left hand finger keys - 48-53: Right thumb keys - 54-59: Left thumb keys

Parameters:

Name Type Description Default
idx int

Linear index from 0-59.

required

Returns:

Type Description
T

The value stored at the specified key position.

Raises:

Type Description
IndexError

If idx is outside the valid range (0-59).

Example

Accessing individual keys::

layout[0]  # Right index finger center
layout[24]  # Left index finger center
layout[48]  # Right thumb down
layout[54]  # Left thumb down
Source code in src/skim/data/keyboard.py
def __getitem__(self, idx: int) -> T:
    """Access a key value by linear index.

    Provides flat indexing across all 60 keys in the layout using the
    standard Svalboard key ordering:
    - 0-23: Right hand finger keys
    - 24-47: Left hand finger keys
    - 48-53: Right thumb keys
    - 54-59: Left thumb keys

    Args:
        idx: Linear index from 0-59.

    Returns:
        The value stored at the specified key position.

    Raises:
        IndexError: If idx is outside the valid range (0-59).

    Example:
        Accessing individual keys::

            layout[0]  # Right index finger center
            layout[24]  # Left index finger center
            layout[48]  # Right thumb down
            layout[54]  # Left thumb down
    """
    if idx in SvalboardLayout._RIGHT_FINGER_CLUSTER_KEYS:
        return self.right[idx]
    if idx in SvalboardLayout._LEFT_FINGER_CLUSTER_KEYS:
        return self.left[idx - SvalboardLayout._LEFT_FINGER_CLUSTER_KEYS.start]
    if idx in SvalboardLayout._RIGHT_THUMB_CLUSTER_KEYS:
        # noinspection PyProtectedMember
        return getattr(
            self.right.thumb,
            SplitSide._THUMB_ORDER[idx - SvalboardLayout._RIGHT_THUMB_CLUSTER_KEYS.start],
        )
    if idx in SvalboardLayout._LEFT_THUMB_CLUSTER_KEYS:
        # noinspection PyProtectedMember
        return getattr(
            self.left.thumb,
            SplitSide._THUMB_ORDER[idx - SvalboardLayout._LEFT_THUMB_CLUSTER_KEYS.start],
        )
    raise IndexError(f"Index {idx} out of range for SvalboardLayout (0-59)")

from_sequence classmethod

from_sequence(values: Sequence[T]) -> SvalboardLayout[T]

Create a SvalboardLayout from a flat sequence of 60 values.

Constructs a complete keyboard layout from a linear sequence of values, mapping each index to its corresponding key position using the standard Svalboard ordering (same as __getitem__ and __iter__).

The mapping is: - 0-23: Right hand finger keys (index, middle, ring, pinky clusters) - 24-47: Left hand finger keys (index, middle, ring, pinky clusters) - 48-53: Right thumb keys - 54-59: Left thumb keys

Within each finger cluster, the 6 keys are ordered: center, north, east, south, west, double_south.

Within each thumb cluster, the 6 keys are ordered: down, pad, up, nail, knuckle, double_down.

Parameters:

Name Type Description Default
values Sequence[T]

A sequence of exactly 60 values to populate the layout. Can be a list, tuple, or any object supporting len() and index access.

required

Returns:

Type Description
SvalboardLayout[T]

A new SvalboardLayout with values distributed across all key

SvalboardLayout[T]

positions.

Raises:

Type Description
ValueError

If the sequence does not contain exactly 60 values.

Example

Creating a layout from a list::

keys = ["KC_A"] * 60  # 60 identical values
layout = SvalboardLayout.from_sequence(keys)

# Or with distinct values
keys = [f"KEY_{i}" for i in range(60)]
layout = SvalboardLayout.from_sequence(keys)
layout[0]  # "KEY_0" (right index center)
layout[59]  # "KEY_59" (left thumb double_down)
Source code in src/skim/data/keyboard.py
@classmethod
def from_sequence(cls, values: Sequence[T]) -> "SvalboardLayout[T]":
    """Create a SvalboardLayout from a flat sequence of 60 values.

    Constructs a complete keyboard layout from a linear sequence of values,
    mapping each index to its corresponding key position using the standard
    Svalboard ordering (same as ``__getitem__`` and ``__iter__``).

    The mapping is:
    - 0-23: Right hand finger keys (index, middle, ring, pinky clusters)
    - 24-47: Left hand finger keys (index, middle, ring, pinky clusters)
    - 48-53: Right thumb keys
    - 54-59: Left thumb keys

    Within each finger cluster, the 6 keys are ordered:
    center, north, east, south, west, double_south.

    Within each thumb cluster, the 6 keys are ordered:
    down, pad, up, nail, knuckle, double_down.

    Args:
        values: A sequence of exactly 60 values to populate the layout.
            Can be a list, tuple, or any object supporting ``len()`` and
            index access.

    Returns:
        A new SvalboardLayout with values distributed across all key
        positions.

    Raises:
        ValueError: If the sequence does not contain exactly 60 values.

    Example:
        Creating a layout from a list::

            keys = ["KC_A"] * 60  # 60 identical values
            layout = SvalboardLayout.from_sequence(keys)

            # Or with distinct values
            keys = [f"KEY_{i}" for i in range(60)]
            layout = SvalboardLayout.from_sequence(keys)
            layout[0]  # "KEY_0" (right index center)
            layout[59]  # "KEY_59" (left thumb double_down)
    """
    if len(values) != 60:
        raise ValueError(f"Expected exactly 60 values, got {len(values)}")

    def make_side(finger_start: int, thumb_start: int) -> SplitSide[T]:
        return SplitSide(
            index=FingerCluster.from_sequence(values[finger_start : finger_start + 6]),
            middle=FingerCluster.from_sequence(values[finger_start + 6 : finger_start + 12]),
            ring=FingerCluster.from_sequence(values[finger_start + 12 : finger_start + 18]),
            pinky=FingerCluster.from_sequence(values[finger_start + 18 : finger_start + 24]),
            thumb=ThumbCluster.from_sequence(values[thumb_start : thumb_start + 6]),
        )

    return cls(
        right=make_side(finger_start=0, thumb_start=48),
        left=make_side(finger_start=24, thumb_start=54),
    )

from_zipped classmethod

from_zipped(
    *,
    bundle: str | type = "KeyValues",
    **layouts: SvalboardLayout[Any],
) -> SvalboardLayout[Any]

Create a new layout by zipping values from multiple source layouts.

This class method combines multiple SvalboardLayout instances into a single layout where each key position contains a bundle of values from all source layouts. The bundle can be either a dynamically created frozen dataclass or a user-provided dataclass type.

This is useful when you need to associate multiple pieces of data with each key position, such as pairing keycodes with their display labels, or combining styling information from multiple sources.

Parameters:

Name Type Description Default
bundle str | type

Either a string name for a dynamically created bundle dataclass, or an existing dataclass type to use. Defaults to "KeyValues".

'KeyValues'
**layouts SvalboardLayout[Any]

Keyword arguments mapping names to source layouts. Each name becomes an attribute on the bundle objects.

{}

Returns:

Type Description
SvalboardLayout[Any]

A new SvalboardLayout where each key position contains a frozen

SvalboardLayout[Any]

dataclass instance with attributes from each source layout.

Raises:

Type Description
ValueError

If no layouts are provided.

TypeError

If a provided bundle class is missing required attributes.

Example

Using a dynamic bundle class::

codes = SvalboardLayout.from_sequence(["KC_A"] * 60)
labels = SvalboardLayout.from_sequence(["A"] * 60)

combined = SvalboardLayout.from_zipped(
    bundle="KeyData",
    code=codes,
    label=labels,
)

combined[0].code  # "KC_A"
combined[0].label  # "A"

Using a custom dataclass::

@dataclass(frozen=True)
class KeyData:
    code: str
    label: str


combined = SvalboardLayout.from_zipped(bundle=KeyData, code=codes, label=labels)
Source code in src/skim/data/keyboard.py
@classmethod
def from_zipped(
    cls,
    *,
    bundle: str | type = "KeyValues",
    **layouts: "SvalboardLayout[Any]",
) -> "SvalboardLayout[Any]":
    """Create a new layout by zipping values from multiple source layouts.

    This class method combines multiple SvalboardLayout instances into a
    single layout where each key position contains a bundle of values from
    all source layouts. The bundle can be either a dynamically created
    frozen dataclass or a user-provided dataclass type.

    This is useful when you need to associate multiple pieces of data with
    each key position, such as pairing keycodes with their display labels,
    or combining styling information from multiple sources.

    Args:
        bundle: Either a string name for a dynamically created bundle
            dataclass, or an existing dataclass type to use. Defaults
            to "KeyValues".
        **layouts: Keyword arguments mapping names to source layouts.
            Each name becomes an attribute on the bundle objects.

    Returns:
        A new SvalboardLayout where each key position contains a frozen
        dataclass instance with attributes from each source layout.

    Raises:
        ValueError: If no layouts are provided.
        TypeError: If a provided bundle class is missing required attributes.

    Example:
        Using a dynamic bundle class::

            codes = SvalboardLayout.from_sequence(["KC_A"] * 60)
            labels = SvalboardLayout.from_sequence(["A"] * 60)

            combined = SvalboardLayout.from_zipped(
                bundle="KeyData",
                code=codes,
                label=labels,
            )

            combined[0].code  # "KC_A"
            combined[0].label  # "A"

        Using a custom dataclass::

            @dataclass(frozen=True)
            class KeyData:
                code: str
                label: str


            combined = SvalboardLayout.from_zipped(bundle=KeyData, code=codes, label=labels)
    """
    if not layouts:
        raise ValueError("At least one layout is required")

    sorted_keys = tuple(sorted(layouts.keys()))
    bundle_class = _resolve_bundle_class(bundle, sorted_keys)

    # Zip all 60 positions
    zipped_values = [
        bundle_class(**{name: layout[i] for name, layout in layouts.items()}) for i in range(60)
    ]

    return cls.from_sequence(zipped_values)

map

map(fn: Callable[[T], U]) -> SvalboardLayout[U]

Create a new layout by applying a function to each key value.

This method transforms each of the 60 key values in the layout using the provided function, returning a new SvalboardLayout with the transformed values.

Parameters:

Name Type Description Default
fn Callable[[T], U]

A callable that takes a value of type T and returns a value of type U. Applied to each key position in the layout.

required

Returns:

Type Description
SvalboardLayout[U]

A new SvalboardLayout with transformed values.

Example

Transforming layout values::

codes = SvalboardLayout.from_sequence(["KC_A"] * 60)
labels = codes.map(keycode_to_label)

# Chain multiple transformations
result = layout.map(fn1).map(fn2)

# Use with lambdas
upper = labels.map(str.upper)
Source code in src/skim/data/keyboard.py
def map(self, fn: Callable[[T], U]) -> "SvalboardLayout[U]":
    """Create a new layout by applying a function to each key value.

    This method transforms each of the 60 key values in the layout using
    the provided function, returning a new SvalboardLayout with the
    transformed values.

    Args:
        fn: A callable that takes a value of type T and returns a value
            of type U. Applied to each key position in the layout.

    Returns:
        A new SvalboardLayout with transformed values.

    Example:
        Transforming layout values::

            codes = SvalboardLayout.from_sequence(["KC_A"] * 60)
            labels = codes.map(keycode_to_label)

            # Chain multiple transformations
            result = layout.map(fn1).map(fn2)

            # Use with lambdas
            upper = labels.map(str.upper)
    """
    return SvalboardLayout.from_sequence([fn(v) for v in self])

SvalboardKeymap dataclass

SvalboardKeymap(
    layers: dict[int, SvalboardLayout[T]],
    tap_dances: tuple[SvalboardTapDance[T], ...] = (),
    macros: tuple[SvalboardMacro[T], ...] = (),
)

Bases: Generic[T]

A complete Svalboard keymap containing multiple layers.

A keymap represents the full key configuration for a Svalboard keyboard, organized into multiple layers. Each layer is a complete SvalboardLayout containing all 60 keys. Users typically switch between layers to access different key bindings (e.g., base layer, symbols layer, navigation layer).

The class is generic over type T, which represents the type of value stored at each key position, allowing it to be used for keycodes, labels, colors, or any other per-key data.

Attributes:

Name Type Description
layers dict[int, SvalboardLayout[T]]

A dict mapping layer indices to SvalboardLayout objects. Keys are QMK layer indices (which may be non-sequential, e.g. 0, 1, 2, 15). Layer 0 is typically the base/default layer.

tap_dances tuple[SvalboardTapDance[T], ...]

Tuple of SvalboardTapDance definitions referenced by keys on the layers. Defaults to an empty tuple. Source formats that don't carry tap-dance definitions (e.g. QMK c2json) yield an empty tuple.

macros tuple[SvalboardMacro[T], ...]

Tuple of SvalboardMacro definitions referenced by keys on the layers. Defaults to an empty tuple. Source formats that don't carry macro definitions yield an empty tuple.

Example

Creating a keymap with multiple layers::

from skim.data.keyboard import SvalboardKeymap, SvalboardLayout

# Create layouts for each layer
base_layer = SvalboardLayout.from_sequence(["KC_A"] * 60)
symbol_layer = SvalboardLayout.from_sequence(["KC_1"] * 60)
nav_layer = SvalboardLayout.from_sequence(["KC_LEFT"] * 60)

keymap = SvalboardKeymap(layers={0: base_layer, 1: symbol_layer, 2: nav_layer})

Accessing layers::

keymap.layers[0]  # Base layer
keymap.layers[1][0]  # First key of symbol layer
len(keymap.layers)  # Number of layers

layers instance-attribute

layers: dict[int, SvalboardLayout[T]]

tap_dances class-attribute instance-attribute

tap_dances: tuple[SvalboardTapDance[T], ...] = ()

macros class-attribute instance-attribute

macros: tuple[SvalboardMacro[T], ...] = ()

zip_clusters

zip_clusters(
    cluster_type: type[ClusterT],
    bundle: str = "KeyValues",
    /,
    **clusters: _ClusterBase[Any],
) -> ClusterT
zip_clusters(
    cluster_type: type[ClusterT],
    bundle: type,
    /,
    **clusters: _ClusterBase[Any],
) -> ClusterT
zip_clusters(
    cluster_type: type[ClusterT],
    bundle: str | type = "KeyValues",
    /,
    **clusters: _ClusterBase[Any],
) -> ClusterT

Zip multiple clusters into a single cluster of bundled values.

This function combines multiple clusters of the same category (all FingerCluster or all ThumbCluster) into a new cluster where each position contains a bundle object holding values from all source clusters.

This is useful when you need to associate multiple pieces of data with each key position, such as pairing keycodes with their display labels, or combining styling information from multiple sources.

Parameters:

Name Type Description Default
cluster_type type[ClusterT]

The type of cluster to create (FingerCluster or ThumbCluster). This also determines the expected field structure for all source clusters.

required
bundle str | type

Either a string name for a dynamically created bundle dataclass, or an existing dataclass type to use. Defaults to "KeyValues".

'KeyValues'
**clusters _ClusterBase[Any]

Keyword arguments mapping names to source clusters. Each name becomes an attribute on the bundle objects. All clusters must have the same field structure as cluster_type.

{}

Returns:

Type Description
ClusterT

A new cluster of the specified type where each field contains a

ClusterT

frozen dataclass instance with attributes from each source cluster.

Raises:

Type Description
ValueError

If no clusters are provided.

TypeError

If any source cluster has fields that don't match the target cluster type, or if a provided bundle class is missing required attributes.

Example

Using a dynamic bundle class::

from skim.data.keyboard import FingerCluster, zip_clusters

keycodes = FingerCluster("KC_NO", center_key="KC_A", south_key="KC_SPC")
labels = FingerCluster("", center_key="A", south_key="Space")
colors = FingerCluster("#888", center_key="#F00", south_key="#0F0")

combined = zip_clusters(
    FingerCluster, "KeyData", code=keycodes, label=labels, color=colors
)

# Access bundled values
combined.center_key.code  # "KC_A"
combined.center_key.label  # "A"
combined.center_key.color  # "#F00"

Using a custom dataclass::

@dataclass(frozen=True)
class KeyData:
    code: str
    label: str
    color: str


combined = zip_clusters(
    FingerCluster, KeyData, code=keycodes, label=labels, color=colors
)
Source code in src/skim/data/keyboard.py
def zip_clusters(
    cluster_type: type[ClusterT],
    bundle: str | type = "KeyValues",
    /,
    **clusters: _ClusterBase[Any],
) -> ClusterT:
    """Zip multiple clusters into a single cluster of bundled values.

    This function combines multiple clusters of the same category (all
    FingerCluster or all ThumbCluster) into a new cluster where each
    position contains a bundle object holding values from all source
    clusters.

    This is useful when you need to associate multiple pieces of data
    with each key position, such as pairing keycodes with their display
    labels, or combining styling information from multiple sources.

    Args:
        cluster_type: The type of cluster to create (FingerCluster or
            ThumbCluster). This also determines the expected field
            structure for all source clusters.
        bundle: Either a string name for a dynamically created bundle
            dataclass, or an existing dataclass type to use. Defaults
            to "KeyValues".
        **clusters: Keyword arguments mapping names to source clusters.
            Each name becomes an attribute on the bundle objects. All
            clusters must have the same field structure as cluster_type.

    Returns:
        A new cluster of the specified type where each field contains a
        frozen dataclass instance with attributes from each source cluster.

    Raises:
        ValueError: If no clusters are provided.
        TypeError: If any source cluster has fields that don't match the
            target cluster type, or if a provided bundle class is missing
            required attributes.

    Example:
        Using a dynamic bundle class::

            from skim.data.keyboard import FingerCluster, zip_clusters

            keycodes = FingerCluster("KC_NO", center_key="KC_A", south_key="KC_SPC")
            labels = FingerCluster("", center_key="A", south_key="Space")
            colors = FingerCluster("#888", center_key="#F00", south_key="#0F0")

            combined = zip_clusters(
                FingerCluster, "KeyData", code=keycodes, label=labels, color=colors
            )

            # Access bundled values
            combined.center_key.code  # "KC_A"
            combined.center_key.label  # "A"
            combined.center_key.color  # "#F00"

        Using a custom dataclass::

            @dataclass(frozen=True)
            class KeyData:
                code: str
                label: str
                color: str


            combined = zip_clusters(
                FingerCluster, KeyData, code=keycodes, label=labels, color=colors
            )
    """
    if not clusters:
        raise ValueError("At least one cluster required")
    return cluster_type.from_zipped(bundle=bundle, **clusters)

zip_layouts

zip_layouts(
    bundle: str = "KeyValues",
    /,
    **layouts: SvalboardLayout[Any],
) -> SvalboardLayout[Any]
zip_layouts(
    bundle: type, /, **layouts: SvalboardLayout[Any]
) -> SvalboardLayout[Any]
zip_layouts(
    bundle: str | type = "KeyValues",
    /,
    **layouts: SvalboardLayout[Any],
) -> SvalboardLayout[Any]

Zip multiple layouts into a single layout of bundled values.

This function combines multiple SvalboardLayout instances into a new layout where each key position contains a bundle object holding values from all source layouts.

This is useful when you need to associate multiple pieces of data with each key position, such as pairing keycodes with their display labels, or combining styling information from multiple sources.

Parameters:

Name Type Description Default
bundle str | type

Either a string name for a dynamically created bundle dataclass, or an existing dataclass type to use. Defaults to "KeyValues".

'KeyValues'
**layouts SvalboardLayout[Any]

Keyword arguments mapping names to source layouts. Each name becomes an attribute on the bundle objects.

{}

Returns:

Type Description
SvalboardLayout[Any]

A new SvalboardLayout where each key position contains a frozen

SvalboardLayout[Any]

dataclass instance with attributes from each source layout.

Raises:

Type Description
ValueError

If no layouts are provided.

TypeError

If a provided bundle class is missing required attributes.

Example

Using a dynamic bundle class::

from skim.data.keyboard import SvalboardLayout, zip_layouts

codes = SvalboardLayout.from_sequence(["KC_A"] * 60)
labels = SvalboardLayout.from_sequence(["A"] * 60)
colors = SvalboardLayout.from_sequence(["#F00"] * 60)

combined = zip_layouts("KeyData", code=codes, label=labels, color=colors)

# Access bundled values
combined[0].code  # "KC_A"
combined[0].label  # "A"
combined[0].color  # "#F00"

Using a custom dataclass::

@dataclass(frozen=True)
class KeyData:
    code: str
    label: str
    color: str


combined = zip_layouts(KeyData, code=codes, label=labels, color=colors)
Source code in src/skim/data/keyboard.py
def zip_layouts(
    bundle: str | type = "KeyValues",
    /,
    **layouts: "SvalboardLayout[Any]",
) -> "SvalboardLayout[Any]":
    """Zip multiple layouts into a single layout of bundled values.

    This function combines multiple SvalboardLayout instances into a new layout
    where each key position contains a bundle object holding values from all
    source layouts.

    This is useful when you need to associate multiple pieces of data with
    each key position, such as pairing keycodes with their display labels,
    or combining styling information from multiple sources.

    Args:
        bundle: Either a string name for a dynamically created bundle
            dataclass, or an existing dataclass type to use. Defaults
            to "KeyValues".
        **layouts: Keyword arguments mapping names to source layouts.
            Each name becomes an attribute on the bundle objects.

    Returns:
        A new SvalboardLayout where each key position contains a frozen
        dataclass instance with attributes from each source layout.

    Raises:
        ValueError: If no layouts are provided.
        TypeError: If a provided bundle class is missing required attributes.

    Example:
        Using a dynamic bundle class::

            from skim.data.keyboard import SvalboardLayout, zip_layouts

            codes = SvalboardLayout.from_sequence(["KC_A"] * 60)
            labels = SvalboardLayout.from_sequence(["A"] * 60)
            colors = SvalboardLayout.from_sequence(["#F00"] * 60)

            combined = zip_layouts("KeyData", code=codes, label=labels, color=colors)

            # Access bundled values
            combined[0].code  # "KC_A"
            combined[0].label  # "A"
            combined[0].color  # "#F00"

        Using a custom dataclass::

            @dataclass(frozen=True)
            class KeyData:
                code: str
                label: str
                color: str


            combined = zip_layouts(KeyData, code=codes, label=labels, color=colors)
    """
    if not layouts:
        raise ValueError("At least one layout required")
    return SvalboardLayout.from_zipped(bundle=bundle, **layouts)

CLI Data Transfer Objects

cli

Data transfer objects for CLI argument handling.

This module defines frozen dataclasses used to pass parsed CLI arguments between the command-line interface and the application layer. These DTOs provide type-safe, immutable containers for input/output file specifications and layer selection options.

Example
>>> from skim.data import InputFiles, OutputFiles, KeymapGeneratorTargets
>>> from pathlib import Path

>>> inputs = InputFiles(config=Path("config.yaml"), keymap=Path("keymap.kbi"))
>>> outputs = OutputFiles(output_dir=Path("./images"), output_format="png")
>>> targets = KeymapGeneratorTargets.from_args(("1", "3-5", "overview"))

RenderEngine

Bases: Enum

Available render engines for non-vector image generation.

Attributes:

Name Type Description
CHROMIUM

Use Playwright with Chromium browser for rendering.

CAIRO

Use Cairo graphics library for rendering.

CHROMIUM class-attribute instance-attribute

CHROMIUM = 'chromium'

CAIRO class-attribute instance-attribute

CAIRO = 'cairo'

OutputFiles dataclass

OutputFiles(
    output_dir: Path = Path(),
    output_format: str = "svg",
    force_overwrite: bool = False,
    use_system_fonts: bool = False,
    render_engine: RenderEngine | None = None,
)

Configuration for output file generation.

Specifies where and how to write generated keymap images. This is a frozen dataclass, meaning instances are immutable after creation.

Attributes:

Name Type Description
output_dir Path

Directory path where generated images will be written. The directory will be created if it doesn't exist. Defaults to the current working directory.

output_format str

Image format for output files. Supported values are "svg", "png", "jpeg", "webp", and "avif". Defaults to "svg".

force_overwrite bool

Whether to overwrite existing files without prompting for confirmation. Defaults to False.

use_system_fonts bool

Whether to use system fonts instead of embedding fonts in SVG. Defaults to False.

render_engine RenderEngine | None

Which render engine to use for non-vector formats. Options are CHROMIUM (Playwright) or CAIRO. If None, uses the first available engine. Defaults to None.

Example
>>> output = OutputFiles(
...     output_dir=Path("./images"),
...     output_format="png",
...     force_overwrite=True,
... )
>>> output.output_format
'png'

output_dir class-attribute instance-attribute

output_dir: Path = field(default_factory=Path)

output_format class-attribute instance-attribute

output_format: str = 'svg'

force_overwrite class-attribute instance-attribute

force_overwrite: bool = False

use_system_fonts class-attribute instance-attribute

use_system_fonts: bool = False

render_engine class-attribute instance-attribute

render_engine: RenderEngine | None = None

InputFiles dataclass

InputFiles(
    config: Path | None = None,
    keymap: Path | None = None,
    force_stdin_keymap: bool = False,
)

Configuration for input file sources.

Specifies the source files for keymap data and optional configuration. This is a frozen dataclass, meaning instances are immutable after creation.

Attributes:

Name Type Description
config Path | None

Optional path to a YAML configuration file. When provided, settings from this file override the default configuration. Defaults to None (use default configuration).

keymap Path | None

Optional path to the keymap file (.kbi, .vil, or .json). When None, keymap data is read from stdin. Defaults to None.

force_stdin_keymap bool

Whether to read keymap data from stdin instead of a file. When True, the keymap is ignored and stdin is used. Defaults to False.

Example
>>> # Read keymap from file with custom config
>>> inputs = InputFiles(
...     config=Path("my-config.yaml"),
...     keymap=Path("my-keymap.kbi"),
... )

>>> # Read keymap from stdin
>>> inputs = InputFiles(force_stdin_keymap=True)

config class-attribute instance-attribute

config: Path | None = None

keymap class-attribute instance-attribute

keymap: Path | None = None

force_stdin_keymap class-attribute instance-attribute

force_stdin_keymap: bool = False

KeymapGeneratorTargets dataclass

KeymapGeneratorTargets(
    all_layers: bool = False,
    overview: bool = False,
    macros: bool = False,
    tap_dances: bool = False,
    special_keys: bool = False,
    symbols: bool = False,
    selected_layers: list[int] = list(),
)

Specification of which layers and views to generate.

Defines the target outputs for keymap generation, including which individual layers to render and whether to generate an overview image. This is a frozen dataclass, meaning instances are immutable after creation.

Attributes:

Name Type Description
all_layers bool

Whether to generate images for all layers in the keymap. When True, selected_layers is ignored. Defaults to False.

overview bool

Whether to generate an overview image showing all layers in a grid layout. Defaults to False.

selected_layers list[int]

List of specific layer indices to generate. Only used when all_layers is False. Layer indices are 1-based in CLI input but stored as 0-based internally. Defaults to an empty list.

Example
>>> # Generate specific layers and overview
>>> targets = KeymapGeneratorTargets(
...     overview=True,
...     selected_layers=[0, 2, 4],
... )
>>> targets.overview
True

>>> # Generate all layers
>>> targets = KeymapGeneratorTargets(all_layers=True, overview=True)

all_layers class-attribute instance-attribute

all_layers: bool = False

overview class-attribute instance-attribute

overview: bool = False

macros class-attribute instance-attribute

macros: bool = False

tap_dances class-attribute instance-attribute

tap_dances: bool = False

special_keys class-attribute instance-attribute

special_keys: bool = False

symbols class-attribute instance-attribute

symbols: bool = False

selected_layers class-attribute instance-attribute

selected_layers: list[int] = field(default_factory=list)

from_args classmethod

from_args(
    layer: tuple[str, ...],
    logger: Callable[[str], None] = print,
) -> KeymapGeneratorTargets

Parse CLI layer arguments into a KeymapGeneratorTargets instance.

Interprets various layer selection formats from command-line arguments and constructs the appropriate targets configuration. Supports ranges, comma-separated values, and special keywords.

Parameters:

Name Type Description Default
layer tuple[str, ...]

Tuple of layer specification strings from the CLI. Each string can be:

  • A single number: "1", "3" (generates that layer)
  • A range: "1-3" (generates layers 1, 2, and 3)
  • Comma-separated: "1,3,5" (generates layers 1, 3, and 5)
  • "overview": Generates only the overview image
  • "all-layers": Generates all individual layers
  • "all": Generates all layers plus overview (default behavior)
required
logger Callable[[str], None]

Callable for warning messages about invalid input. Defaults to print. Called with a string message when invalid layer specifications are encountered.

print

Returns:

Type Description
KeymapGeneratorTargets

A KeymapGeneratorTargets instance configured according to the

KeymapGeneratorTargets

parsed arguments.

Example
>>> # No arguments = all layers + overview
>>> targets = KeymapGeneratorTargets.from_args(())
>>> targets.all_layers
True
>>> targets.overview
True

>>> # Specific layers
>>> targets = KeymapGeneratorTargets.from_args(("1", "3-5"))
>>> targets.selected_layers
[1, 3, 4, 5]

>>> # Keywords
>>> targets = KeymapGeneratorTargets.from_args(("overview",))
>>> targets.overview
True
>>> targets.all_layers
False
Source code in src/skim/data/cli.py
@classmethod
def from_args(
    cls, layer: tuple[str, ...], logger: Callable[[str], None] = print
) -> "KeymapGeneratorTargets":
    """Parse CLI layer arguments into a KeymapGeneratorTargets instance.

    Interprets various layer selection formats from command-line arguments
    and constructs the appropriate targets configuration. Supports ranges,
    comma-separated values, and special keywords.

    Args:
        layer: Tuple of layer specification strings from the CLI. Each
            string can be:

            - A single number: "1", "3" (generates that layer)
            - A range: "1-3" (generates layers 1, 2, and 3)
            - Comma-separated: "1,3,5" (generates layers 1, 3, and 5)
            - "overview": Generates only the overview image
            - "all-layers": Generates all individual layers
            - "all": Generates all layers plus overview (default behavior)

        logger: Callable for warning messages about invalid input.
            Defaults to print. Called with a string message when
            invalid layer specifications are encountered.

    Returns:
        A KeymapGeneratorTargets instance configured according to the
        parsed arguments.

    Example:
        ```pycon
        >>> # No arguments = all layers + overview
        >>> targets = KeymapGeneratorTargets.from_args(())
        >>> targets.all_layers
        True
        >>> targets.overview
        True

        >>> # Specific layers
        >>> targets = KeymapGeneratorTargets.from_args(("1", "3-5"))
        >>> targets.selected_layers
        [1, 3, 4, 5]

        >>> # Keywords
        >>> targets = KeymapGeneratorTargets.from_args(("overview",))
        >>> targets.overview
        True
        >>> targets.all_layers
        False

        ```
    """
    if not layer:
        return KeymapGeneratorTargets(all_layers=True, overview=True, selected_layers=[])

    tokens = [token.strip() for item in layer for token in item.split(",")]

    all_layers = False
    overview = False
    macros = False
    tap_dances = False
    special_keys = False
    symbols = False
    selected_layers: list[int] = []

    for token in tokens:
        if not token:
            continue

        match token:
            case "all":
                # Skip the combined ``special-keys`` image: the
                # macros and tap-dances images already show the
                # same content individually, so generating both
                # would be redundant. Users who want the combined
                # layout can still opt in via ``-l special-keys``.
                return KeymapGeneratorTargets(
                    all_layers=True,
                    overview=True,
                    macros=True,
                    tap_dances=True,
                    special_keys=False,
                    symbols=True,
                    selected_layers=[],
                )
            case "all-layers":
                all_layers = True
                selected_layers.clear()
            case "overview":
                overview = True
            case "macros":
                macros = True
            case "tap-dances":
                tap_dances = True
            case "special-keys":
                special_keys = True
            case "symbols":
                symbols = True
            case _ if "-" in token:
                try:
                    start_str, end_str = token.split("-")
                    start, end = int(start_str), int(end_str) + 1
                    selected_layers.extend(range(start, end))
                except ValueError:
                    logger(f"Skipping invalid layer range: {token} ...")
            case _:
                if all_layers:
                    continue

                try:
                    selected_layers.append(int(token))
                except ValueError:
                    logger(f"Skipping invalid layer selection: {token} ...")

    return cls(
        all_layers=all_layers,
        overview=overview,
        macros=macros,
        tap_dances=tap_dances,
        special_keys=special_keys,
        symbols=symbols,
        selected_layers=selected_layers,
    )

Trie

trie

Trie (prefix tree) data structure for efficient prefix matching.

This module provides a Trie implementation optimized for checking whether a string starts with any of a predefined set of prefixes. It is used internally by the keycode label adapter to efficiently match QMK macro function names like "LT", "MO", "TG", etc.

Example
>>> from skim.data.trie import Trie
>>> trie = Trie(["LT", "MO", "TG", "OSL"])
>>> trie.get_matching_prefix("LT0")
'LT'
>>> trie.get_matching_prefix("MO(1)")
'MO'
>>> trie.get_matching_prefix("KC_A") is None
True

Trie

Trie(words: Iterable[str])

A trie (prefix tree) for efficient prefix matching.

This data structure allows O(m) lookup time to check if a string starts with any word from a predefined set, where m is the length of the search string (or more precisely, the length of the matching prefix).

The trie is built once during initialization and then supports fast prefix queries. It is particularly useful for parsing QMK macro functions where we need to identify function names at the start of strings like "LT(1, KC_A)" or "MO(2)".

Attributes:

Name Type Description
root dict

The root node of the trie, represented as a nested dictionary. Each key is a character leading to a child node (another dict), except for None which marks the end of a word and stores the complete word string.

Example
>>> trie = Trie(["cat", "car", "card"])
>>> trie.get_matching_prefix("catalog")
'cat'
>>> trie.get_matching_prefix("car")
'car'
>>> trie.get_matching_prefix("dog") is None
True

Initialize the trie with a collection of words.

Builds the trie structure by inserting all provided words. Each word creates a path through the trie, with the complete word stored at the terminal node.

Parameters:

Name Type Description Default
words Iterable[str]

An iterable of strings to index in the trie. Can be a list, tuple, set, generator, or any other iterable of strings.

required
Example
>>> trie = Trie(["LT", "MO", "TG"])
>>> trie.get_matching_prefix("LT0")
'LT'

>>> # Can also use generators
>>> trie = Trie(w.upper() for w in ["lt", "mo", "tg"])
>>> trie.get_matching_prefix("MO(1)")
'MO'
Source code in src/skim/data/trie.py
def __init__(self, words: Iterable[str]) -> None:
    """Initialize the trie with a collection of words.

    Builds the trie structure by inserting all provided words. Each word
    creates a path through the trie, with the complete word stored at
    the terminal node.

    Args:
        words: An iterable of strings to index in the trie. Can be a list,
            tuple, set, generator, or any other iterable of strings.

    Example:
        ```pycon
        >>> trie = Trie(["LT", "MO", "TG"])
        >>> trie.get_matching_prefix("LT0")
        'LT'

        >>> # Can also use generators
        >>> trie = Trie(w.upper() for w in ["lt", "mo", "tg"])
        >>> trie.get_matching_prefix("MO(1)")
        'MO'

        ```
    """
    self.root: dict = {}
    for word in words:
        node = self.root
        for char in word:
            node = node.setdefault(char, {})
        node[None] = word

__slots__ class-attribute instance-attribute

__slots__ = ['root']

root instance-attribute

root: dict = {}

get_matching_prefix

get_matching_prefix(search_string: str) -> str | None

Find the longest indexed word that is a prefix of the search string.

Traverses the trie following characters from the search string. If a complete indexed word is found (marked by a None key), that word is returned. The search continues to find the longest possible match.

Parameters:

Name Type Description Default
search_string str

The string to check for a matching prefix.

required

Returns:

Type Description
str | None

The matching prefix word if found, or None if the search string

str | None

doesn't start with any indexed word.

Example
>>> trie = Trie(["LT", "LM", "OSL"])
>>> trie.get_matching_prefix("LT(1, KC_A)")
'LT'
>>> trie.get_matching_prefix("OSL(2)")
'OSL'
>>> trie.get_matching_prefix("KC_SPACE") is None
True
>>> trie.get_matching_prefix("LT")
'LT'
Source code in src/skim/data/trie.py
def get_matching_prefix(self, search_string: str) -> str | None:
    """Find the longest indexed word that is a prefix of the search string.

    Traverses the trie following characters from the search string. If a
    complete indexed word is found (marked by a None key), that word is
    returned. The search continues to find the longest possible match.

    Args:
        search_string: The string to check for a matching prefix.

    Returns:
        The matching prefix word if found, or None if the search string
        doesn't start with any indexed word.

    Example:
        ```pycon
        >>> trie = Trie(["LT", "LM", "OSL"])
        >>> trie.get_matching_prefix("LT(1, KC_A)")
        'LT'
        >>> trie.get_matching_prefix("OSL(2)")
        'OSL'
        >>> trie.get_matching_prefix("KC_SPACE") is None
        True
        >>> trie.get_matching_prefix("LT")
        'LT'

        ```
    """
    node = self.root
    for char in search_string:
        if None in node:
            return node[None]  # type: ignore[return-value]
        if char not in node:
            return None
        node = node[char]  # type: ignore[assignment]

    return node.get(None)  # type: ignore[return-value]