Skip to content

Package Layers

lazy-cuh should keep the current primitive packages and add higher-level packages above them. The goal is not more folders for their own sake. The goal is to make the abstraction layers visible in code:

  1. primitives for correctness and escape hatches
  2. builders for typed declarations and derived wiring
  3. presets for strong defaults
  4. composition helpers for complete lazygit-style screens
  5. widgets as the Textual adapter boundary

Keep the existing focused primitive packages:

lazy_cuh/
core/
inputs/
keybar/
layout/
modals/
styles/
viewport/
view/
widgets/

Add these packages when the corresponding abstractions are extracted from the examples:

lazy_cuh/
builders/
options.py
actions.py
shell.py
presets/
keys.py
shell.py
themes.py
composition/
shell.py

Facade modules are the public entry points for a domain. They may re-export focused implementation modules so users get a stable import path while the internals stay small:

lazy_cuh/
builders/
options.py # public facade: lc.builders.options.*
option_fields.py # field constructors
option_spec.py # OptionsSpec and view/edit derivation
composition/
shell.py # public facade: lc.composition.shell.*
shell_panels.py # panel/tab declarations
shell_navigation.py # focus graph declarations
shell_action_models.py # shell action behavior
shell_bindings.py # keybinding declarations and presets
shell_spec.py # ShellSpec orchestration
shell_runtime.py # runtime/Textual wiring

Only facade modules should be treated as the stable user-facing import path unless another implementation module is explicitly documented as public.

The package responsibilities should be:

  • core: plain data, commands, events, selection, and semantic items
  • inputs: key sequences, binding registries, input resolution, and key profiles
  • layout: row composition, wrapping, and line numbers
  • viewport: visual-line mapping and scroll policy
  • view: list, tree, text, panel, tab, and shell view models
  • keybar: keybar models, hint groups, and renderers
  • modals: modal specs and pure modal state transitions
  • styles: palettes, roles, and reusable default CSS
  • widgets: Textual adapters only
  • builders: typed declarations that derive primitive models and wiring
  • presets: reusable defaults for keys, themes, shell behavior, and common lazygit-style choices
  • composition: app-shell helpers that assemble panels, tabs, focus, keybars, runtime wiring, and widgets for common layouts

Layered packages should be imported in a way that keeps the layer and domain visible at the call site.

Prefer:

import lazy_cuh as lc
registry = lc.builders.actions.registry(actions, aliases=aliases)
panel = lc.composition.shell.panel("main", index=1)

Avoid flattening this into a long root-level helper:

from lazy_cuh import registry_from_action_map

Also avoid hiding the layer behind app-local aliases in examples:

from lazy_cuh.builders import actions as action_builders

Use lc.inputs for key and action primitives. The plural package name avoids shadowing Python’s built-in input() in normal examples.

Short function names are acceptable when the module path carries the meaning. lc.builders.actions.registry(...) is clearer than a long function name because it reads as:

framework namespace -> builder layer -> action domain -> registry output

The same pattern should apply as the higher-level packages grow:

import lazy_cuh as lc
options = lc.builders.options.declare(...)
keys = lc.presets.keys.lazy()
shell = lc.composition.shell.declare(...)

The exact helper names may change while the project is pre-alpha, but the shape should remain: short names inside explicit layer/domain modules.

Tiny documentation snippets may still use direct imports when that makes the example shorter:

from lazy_cuh.view import ListViewModel

Full examples should prefer the lc namespace so readers learn where each concept belongs.

The current primitive packages are already useful and testable. Replacing them with one large component tree would create churn without solving the main problem: app authors still need too much glue for normal screens.

The missing layer is above the primitives. For example, an option field declaration should be able to derive:

  • rendered rows
  • parser and formatter behavior
  • validation
  • input modal specs
  • immutable update handling
  • keybar/help metadata where relevant

That belongs in builders, not in view, widgets, or the example app.

Presets belong in a separate package because they are opinionated defaults, not core behavior. A user should be able to use lazy-cuh without the default lazygit-style key profile, theme, or shell choices.

The first preset domains are lc.presets.keys, lc.presets.navigation, and lc.presets.shell. keys owns reusable key defaults such as leader-key aliases and movement-direction maps. navigation owns item-navigation defaults that need to stay aligned across keymaps, keybars, docs, and examples. shell owns opinionated shell binding groups for panel focus, spatial focus, help, and quit. lc.presets.shell.lazygit(...) can use bare panel numbers, while lc.presets.shell.lazyvim(...) keeps panel numbers behind a leader so bare digits can remain count prefixes. Movement presets should include vim keys and accessible alternatives where practical, for example both h/j/k/l and arrow keys for directional focus.

Composition helpers belong above builders and presets. They should remove boring app-shell wiring, but they should not own app domain state. The shell declaration API belongs there because it coordinates panels, focus graph edges, shell commands, tab commands, keybar contexts, and runtime composition as one app-shell concept.

lazy_cuh/
primitives/
builders/
adapters/
presets/

This is conceptually clean, but it would make imports noisier and force large churn before the higher-level API is proven.

lazy_cuh/
components/
list/
tree/
options/
shell/

This is friendly at first, but it risks mixing pure models, rendering, navigation, and Textual widgets in the same package. Lazy-cuh should keep those concerns separate.

lazy_cuh/
options.py
shell.py
actions.py

This makes imports short, but it hides the architecture and tends to create large modules over time. Top-level exports are fine as convenience imports, but they should not be the main structure.

When extracting from examples:

  • repeated primitive construction moves to builders
  • opinionated defaults move to presets
  • complete shell wiring moves to composition
  • Textual lifecycle code stays in widgets
  • app-specific state stays in the downstream app

If a package starts importing app-specific concepts, it is in the wrong layer.