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:
KeymapDatacontains multipleLayerinstancesEach
Layerrepresents 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:
objectRepresents 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'
- __post_init__()[source]¶
Validate layer data structure after initialization.
- Raises:
ValueError – If any structural constraints are violated.
- Return type:
- 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': [...]}
- class skim.domain.models.KeymapData(layers)[source]¶
Bases:
objectContainer 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.
- 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'
- to_dict()[source]¶
Convert keymap to dictionary format for JSON serialization.
- Return type:
- 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:
- 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
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:
SkimConfig(root) contains:LayerConfigListofLayerConfiginstancesAppearanceConfigwithBorderConfigandColorConfigOptional keycode overrides and layer mappings
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:
objectConfiguration for key border appearance.
- 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)
- class skim.domain.config.ColorConfig(text='#000000', background='#FFFFFF', neutral='#70768B', named_colors=None)[source]¶
Bases:
objectConfiguration 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.
- 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"}, ... )
- class skim.domain.config.AppearanceConfig(border=<factory>, colors=<factory>)[source]¶
Bases:
objectCombined 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 (BorderConfig)
colors (ColorConfig)
- 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¶
- __init__(border=<factory>, colors=<factory>)¶
- Parameters:
border (BorderConfig)
colors (ColorConfig)
- Return type:
None
- class skim.domain.config.LayerConfig(base_color, id=None, name=None, label=None, index=-1)[source]¶
Bases:
objectConfiguration for a single keyboard layer.
Defines the visual properties and identification for a layer, including its base color, display name, and optional identifiers.
- 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
- 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:
- Returns:
True if layer has a valid index (>= 0), False otherwise.
- 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 layerString 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:
- 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:
- class skim.domain.config.SkimConfig(layers, appearance=None, keycodes=None, layer_keycode=None, reversed_alias=None)[source]¶
Bases:
objectRoot 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¶
- 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:
- 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:
- 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)¶
- 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:
- 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:
- 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:
- Return type:
- 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:
- Return type:
- 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:
- Return type:
- 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'