Domain Layer

Core domain models and configuration classes.

Models

Keymap data models for keyboard layout visualization.

This module defines the core domain models that represent keyboard layers and keymap data structures used throughout the skim application.

The data models follow a hierarchical structure:
  • KeymapData contains multiple Layer instances

  • Each Layer represents a single keyboard layer with labels, colors, and layer toggle information

Example

Creating a simple layer:

layer = Layer(
    name="Base",
    labels=[["Q", "W", "E", "R", "T", "Y"] for _ in range(10)],
    colors=[
        "#FF0000",
        "#FF3333",
        "#FF6666",
        "#FF9999",
        "#FFCCCC",
        "#FFFFFF",
        "#808080",
    ],
    primary_color=2,
    secondary_color=6,
    layer_toggles=[[None] * 6 for _ in range(10)],
)

Creating keymap data from layers:

keymap = KeymapData(layers=[layer])
layer_count = keymap.layer_count()  # Returns 1
class skim.domain.models.Layer(name, labels, colors, primary_color, secondary_color, layer_toggles)[source]

Bases: object

Represents a single keyboard layer with visual and functional data.

A Layer contains all information needed to render a keyboard layer image, including key labels, color scheme, and layer toggle mappings.

The Svalboard layout uses 10 clusters (8 finger + 2 thumb), each with 6 keys, totaling 60 keys per layer.

name

Display name for this layer (e.g., “Base”, “Navigation”, “Symbols”).

labels

2D list of key labels organized as 10 rows x 6 columns. Each row represents a key cluster on the keyboard.

colors

List of exactly 7 hex color strings forming the layer’s gradient. Colors 0-5 are the gradient, color 6 is the neutral/secondary color.

primary_color

Index (0-5) into colors list for primary key highlighting.

secondary_color

Index (0-6) into colors list for secondary elements.

layer_toggles

2D list matching labels dimensions, containing target layer indices for layer-switching keys, or None for regular keys.

Raises:
  • ValueError – If colors list doesn’t have exactly 7 elements.

  • ValueError – If labels or layer_toggles don’t have exactly 10 rows.

  • ValueError – If any row in labels or layer_toggles doesn’t have 6 keys.

Parameters:

Example

>>> layer = Layer(
...     name="Symbols",
...     labels=[["!", "@", "#", "$", "%", "^"] for _ in range(10)],
...     colors=["#3471FF"] * 6 + ["#808080"],
...     primary_color=2,
...     secondary_color=6,
...     layer_toggles=[[None, None, None, None, None, 0] for _ in range(10)],
... )
>>> layer.to_dict()["name"]
'Symbols'
name: str
labels: list[list[str]]
colors: list[str]
primary_color: int
secondary_color: int
layer_toggles: list[list[int | None]]
__post_init__()[source]

Validate layer data structure after initialization.

Raises:

ValueError – If any structural constraints are violated.

Return type:

None

to_dict()[source]

Convert layer to dictionary format for JSON serialization.

The output dictionary uses camelCase keys to match the Typst template’s expected input format.

Returns:

name, labels, colors, primaryColor, secondaryColor, layerToggles.

Return type:

Dictionary with keys

Example

>>> layer.to_dict()
{'name': 'Base', 'labels': [...], 'colors': [...],
 'primaryColor': 2, 'secondaryColor': 6, 'layerToggles': [...]}
__init__(name, labels, colors, primary_color, secondary_color, layer_toggles)
Parameters:
Return type:

None

class skim.domain.models.KeymapData(layers)[source]

Bases: object

Container for all keyboard layers in a keymap.

KeymapData serves as the top-level data structure holding all layers that make up a complete keyboard layout configuration.

Parameters:

layers (list[Layer])

layers

List of Layer objects representing each keyboard layer.

Example

>>> base = Layer(name="Base", ...)
>>> nav = Layer(name="Nav", ...)
>>> keymap = KeymapData(layers=[base, nav])
>>> keymap.layer_count()
2
>>> keymap.get_layer(0).name
'Base'
layers: list[Layer]
to_dict()[source]

Convert keymap to dictionary format for JSON serialization.

Return type:

dict[str, Any]

Returns:

Dictionary with ‘layers’ key containing list of layer dictionaries.

Example

>>> keymap.to_dict()
{'layers': [{'name': 'Base', ...}, {'name': 'Nav', ...}]}
get_layer(index)[source]

Retrieve a layer by its index.

Parameters:

index (int) – Zero-based index of the layer to retrieve.

Return type:

Layer | None

Returns:

The Layer at the specified index, or None if index is out of bounds.

Example

>>> keymap = KeymapData(layers=[base_layer, nav_layer])
>>> keymap.get_layer(0).name
'Base'
>>> keymap.get_layer(99) is None
True
layer_count()[source]

Return the total number of layers in this keymap.

Return type:

int

Returns:

Integer count of layers.

Example

>>> keymap = KeymapData(layers=[layer1, layer2, layer3])
>>> keymap.layer_count()
3
__init__(layers)
Parameters:

layers (list[Layer])

Return type:

None

Configuration

Configuration models for Skim keyboard layout image generator.

This module provides dataclass-based configuration models for customizing the appearance and behavior of generated keymap images. Configuration can be loaded from YAML files or constructed programmatically.

The configuration hierarchy is:

Example

Loading configuration from a YAML file:

from pathlib import Path
import yaml

with open("config.yaml") as f:
    data = yaml.safe_load(f)
config = SkimConfig.from_dict(data)

Using default configuration with overrides:

user_config = SkimConfig.from_dict({"layers": [...]})
full_config = user_config.merge_with_defaults()
class skim.domain.config.BorderConfig(color='#000000', radius=20)[source]

Bases: object

Configuration for key border appearance.

Parameters:
color

Hex color string for key borders (e.g., “#000000”).

radius

Corner radius in pixels for rounded key corners.

Example

>>> border = BorderConfig(color="#333333", radius=15)
color: str = '#000000'
radius: int = 20
__init__(color='#000000', radius=20)
Parameters:
Return type:

None

class skim.domain.config.ColorConfig(text='#000000', background='#FFFFFF', neutral='#70768B', named_colors=None)[source]

Bases: object

Configuration for the color scheme used in keymap images.

Defines the base colors used throughout the generated images, including text, background, and optional named color palette.

Parameters:
text

Hex color for key label text.

background

Hex color for the image background.

neutral

Hex color for neutral/inactive elements and layer-toggle keys.

named_colors

Optional dictionary mapping color names to hex values. Used to reference colors by name in layer configurations.

Example

>>> colors = ColorConfig(
...     text="#000000",
...     background="#FFFFFF",
...     neutral="#70768B",
...     named_colors={"primary": "#3471FF", "accent": "#FF5733"},
... )
text: str = '#000000'
background: str = '#FFFFFF'
neutral: str = '#70768B'
named_colors: dict[str, str] | None = None
__init__(text='#000000', background='#FFFFFF', neutral='#70768B', named_colors=None)
Parameters:
Return type:

None

class skim.domain.config.AppearanceConfig(border=<factory>, colors=<factory>)[source]

Bases: object

Combined appearance configuration for keymap images.

Groups border and color configurations into a single structure that can be serialized and passed to the Typst rendering engine.

Parameters:
border

Border styling configuration.

colors

Color scheme configuration.

Example

>>> appearance = AppearanceConfig(
...     border=BorderConfig(radius=10),
...     colors=ColorConfig(background="#F5F5F5"),
... )
>>> appearance.to_dict()
{'border': {'color': '#000000', 'radius': 10},
 'colors': {'text': '#000000', 'background': '#F5F5F5', ...}}
border: BorderConfig
colors: ColorConfig
to_dict()[source]

Convert appearance config to dictionary for JSON serialization.

Return type:

dict[str, Any]

Returns:

Dictionary with ‘border’ and ‘colors’ keys containing the respective configuration values.

__init__(border=<factory>, colors=<factory>)
Parameters:
Return type:

None

class skim.domain.config.LayerConfig(base_color, id=None, name=None, label=None, index=-1)[source]

Bases: object

Configuration for a single keyboard layer.

Defines the visual properties and identification for a layer, including its base color, display name, and optional identifiers.

Parameters:
  • base_color (str)

  • id (str | None)

  • name (str | None)

  • label (str | None)

  • index (int)

base_color

Hex color string or named color for this layer’s theme. A gradient will be generated from this base color.

id

Optional unique identifier for referencing this layer in keycodes (e.g., “_NAV”, “_SYM”). Used for layer toggle resolution.

name

Display name shown in the generated image (e.g., “Navigation”).

label

Short label (typically 2-4 chars) for compact display.

index

Zero-based position in the layer list. Set automatically when added to a LayerConfigList.

Example

>>> layer = LayerConfig(
...     base_color="#3471FF",
...     id="_NAV",
...     name="Navigation",
...     label="NAV",
... )
>>> layer.is_valid()
False  # index not yet assigned
base_color: str
id: str | None = None
name: str | None = None
label: str | None = None
index: int = -1
is_valid()[source]

Check if this layer configuration has been properly initialized.

A layer is valid if it has been assigned a non-negative index, which happens when it’s added to a LayerConfigList.

Return type:

bool

Returns:

True if layer has a valid index (>= 0), False otherwise.

__init__(base_color, id=None, name=None, label=None, index=-1)
Parameters:
  • base_color (str)

  • id (str | None)

  • name (str | None)

  • label (str | None)

  • index (int)

Return type:

None

skim.domain.config.NoneConfigLayer = LayerConfig(base_color='#000000', id='', name='', label='', index=-1)

Sentinel value representing a missing or unresolved layer configuration.

Used as a return value when layer lookup fails, avoiding None checks. The is_valid() method will return False for this sentinel.

class skim.domain.config.LayerConfigList(initlist=None)[source]

Bases: UserList[LayerConfig]

A list of layer configurations with indexed access by ID or position.

Extends UserList to provide flexible layer lookup by either numeric index or string identifier. Automatically assigns indices to layers when added.

The list supports three access patterns:
  • Numeric index: layers[0] returns first layer

  • String ID: layers["_NAV"] returns layer with id=”_NAV”

  • String numeric: layers["2"] returns layer at index 2

Example

>>> layers = LayerConfigList(
...     [
...         LayerConfig(base_color="#FF0000", id="_BASE", name="Base"),
...         LayerConfig(base_color="#00FF00", id="_NAV", name="Nav"),
...     ]
... )
>>> layers[0].name
'Base'
>>> layers["_NAV"].name
'Nav'
>>> layers["nonexistent"].is_valid()
False  # Returns NoneConfigLayer sentinel
Parameters:

initlist (list[LayerConfig] | None)

__init__(initlist=None)[source]

Initialize the layer list with optional initial layers.

Parameters:

initlist (Optional[list[LayerConfig]]) – Optional list of LayerConfig objects to initialize with. Each layer will have its index automatically assigned.

Return type:

None

__getitem__(i)[source]

Retrieve a layer by index or ID.

Parameters:

i (str | int) – Either an integer index or string identifier. String identifiers are matched against layer IDs first, then attempted as numeric strings.

Return type:

LayerConfig

Returns:

The matching LayerConfig, or NoneConfigLayer sentinel if not found.

Example

>>> layers[0]  # By index
LayerConfig(...)
>>> layers["_SYM"]  # By ID
LayerConfig(...)
append(item)[source]

Add a layer to the list, automatically assigning its index.

Parameters:

item (LayerConfig) – LayerConfig to append. Its index will be set to the current list length, and its ID (if present) will be registered for lookup.

Return type:

None

class skim.domain.config.SkimConfig(layers, appearance=None, keycodes=None, layer_keycode=None, reversed_alias=None)[source]

Bases: object

Root configuration for the Skim keymap image generator.

Contains all settings needed to generate keymap images, including layer definitions, appearance settings, and keycode customizations.

Parameters:
layers

List of layer configurations defining colors and names.

appearance

Visual styling for borders and colors.

keycodes

Optional dict mapping QMK keycodes to custom display labels.

layer_keycode

Optional dict for custom layer-switching key behavior.

reversed_alias

Optional dict mapping keycode functions to aliases (e.g., “LSFT(KC_1)” -> “KC_EXLM”).

Example

Loading and merging with defaults:

user_config = SkimConfig.from_dict(
    {
        "layers": [
            {"base_color": "#FF0000", "name": "Base"},
            {"base_color": "#00FF00", "name": "Nav"},
        ],
    }
)
config = user_config.merge_with_defaults()
layers: LayerConfigList
appearance: AppearanceConfig | None = None
keycodes: dict[str, str] | None = None
layer_keycode: dict[str, Any] | None = None
reversed_alias: dict[str, str] | None = None
classmethod from_dict(data)[source]

Create a SkimConfig from a dictionary (typically from YAML).

Parses nested configuration structures and constructs the appropriate dataclass hierarchy.

Parameters:

data (dict[str, Any]) – Dictionary with configuration data. Expected keys: - layers: List of layer config dicts - appearance: Optional appearance settings dict - keycodes: Optional keycode override dict - layer_keycode: Optional layer keycode mappings - reversed_alias: Optional alias mappings

Return type:

SkimConfig

Returns:

Populated SkimConfig instance.

Example

>>> data = yaml.safe_load(open("config.yaml"))
>>> config = SkimConfig.from_dict(data)
classmethod load_default()[source]

Load the bundled default configuration.

Return type:

SkimConfig

Returns:

SkimConfig with default settings from the bundled default-config.yaml asset file.

Raises:

FileNotFoundError – If the default config file is missing.

__init__(layers, appearance=None, keycodes=None, layer_keycode=None, reversed_alias=None)
Parameters:
Return type:

None

merge_with_defaults()[source]

Merge this configuration with default values.

Creates a new SkimConfig where any unset (None) values are filled in from the default configuration. User-provided values take precedence over defaults.

Return type:

SkimConfig

Returns:

New SkimConfig with defaults applied to missing values.

Example

>>> partial = SkimConfig.from_dict({"layers": [...]})
>>> full = partial.merge_with_defaults()
>>> full.appearance is not None
True

Colors

Color utilities for converting and manipulating colors.

This module provides functions for color format conversion (hex to RGB, RGB to hex), color adjustment (lightness and saturation), and gradient generation for keymap layer colors.

skim.domain.colors.str_to_rgb(hex_color)[source]

Convert a hex color string to RGB tuple with values 0.0-1.0.

Parameters:

hex_color (str) – Hexadecimal color string with or without ‘#’ prefix. Examples: ‘#FF0000’, ‘FF0000’, ‘#00ff00’

Return type:

tuple[float, float, float]

Returns:

Tuple of (red, green, blue) with values in range 0.0-1.0.

Examples

>>> str_to_rgb("#FF0000")
(1.0, 0.0, 0.0)
>>> str_to_rgb("00FF00")
(0.0, 1.0, 0.0)
skim.domain.colors.hex_str(red, green, blue)[source]

Convert RGB floats to hexadecimal color string.

Parameters:
  • red (float) – Red component in range 0.0-1.0.

  • green (float) – Green component in range 0.0-1.0.

  • blue (float) – Blue component in range 0.0-1.0.

Return type:

str

Returns:

Hexadecimal color string with ‘#’ prefix in uppercase. Format: ‘#RRGGBB’

Examples

>>> hex_str(1.0, 0.0, 0.0)
'#FF0000'
>>> hex_str(0.5, 0.5, 0.5)
'#808080'
skim.domain.colors.adjust_color(hex_color, target_lightness=0.31, target_saturation=0.5)[source]

Adjust the lightness and saturation of a color.

Converts the color to HLS space, adjusts saturation (capped at target) and lightness (set to target), then converts back to RGB hex format. If a target is None, the original value is preserved.

Parameters:
  • hex_color (str) – Input color in hexadecimal format.

  • target_lightness (float | None) – Desired lightness value (0.0-1.0). Default: 0.31

  • target_saturation (float | None) – Maximum saturation value (0.0-1.0). Original saturation is capped at this value. Default: 0.50

Return type:

str

Returns:

Adjusted color as hexadecimal string with ‘#’ prefix.

Examples

>>> adjust_color("#FF0000", 0.31, 0.50)
'#7F0000'  # Darker, less saturated red
skim.domain.colors.generate_gradient(base_color, base_index=2)[source]

Generate a 6-color gradient with base color at specified index.

Creates a gradient that interpolates from dark to light colors, with the base color appearing at the specified index position.

Parameters:
  • base_color (str) – The base color in hexadecimal format.

  • base_index (int) – Position (0-5) where base color should appear. Colors before this index will be darker, colors after will be lighter. Default: 2

Return type:

list[str]

Returns:

List of 6 hexadecimal color strings forming a gradient.

Examples

>>> gradient = generate_gradient("#347156", base_index=2)
>>> len(gradient)
6
>>> gradient[2]  # Base color at index 2
'#347156'