Architecture¶
aws-tui follows a five-layer architecture with enforced forbidden edges:
View (Textual) → ViewModel (VMx) → Service plugins → Domain ops → Infrastructure
scripts/check-layers.sh parses imports with ast, resolves relative
imports, and checks the banned edges in the script. app.py and
composition.py are trusted composition roots and are not scanned.
services/ is a service-composition boundary: it can import concrete
VMs to build service pages, but it cannot import Textual widgets.
1. Layers¶
- View — Textual widgets and
.tcssthemes (src/aws_tui/ui/). Never touchesboto3,aioboto3, orbotocore. Talks to VMs via property reads + relay-commandexecute(...); subscribes toMessageHubfor change notifications. The S3 root is the code-backedDualPanefromsrc/aws_tui/ui/widgets/dual_pane.py, mounted throughsrc/aws_tui/ui/widgets/service_view_factory.py; there is noS3Pageclass.ContextPickerprovides bordered keyboard-focusable context selection, and EMR's specializedApplicationPickerpreserves its Rich application-state rendering. Both compose the sharedOverlayOptionList: the trigger keeps its compact footprint while the choices use Textual's screen overlay, so opening or closing a picker does not resize adjacent regions. Overlay geometry and dismissal remain view state rather than entering VMx.ServiceTabStriprenders a persistent segmented frame while providing one predictable focus stop for service-local views. - ViewModel — VMx-based viewmodels with reactive commands and
property-changed messages (
src/aws_tui/vm/). Never imports Textual; tests run headless.ServiceSelectionStoreis a VM-layer type insrc/aws_tui/vm/service_source_vm.py, shared by the single-context service VMs rather than owned by Infrastructure. Subtrees: vm/chrome/— persistent shell state (hint legend, toasts, and overlays like command palette / confirm / quick look / crash). Transfer journals are diagnostic-only; no startup recovery VM or UI ships.HintLegendVMowns service-scoped action membership, configured shortcut labels, complete effect/prerequisite tooltips, availability, and fitting priority. TheHintLegendview performs terminal-width measurement and centers a fitted one-line command row; lower-priority hints still yield to[:] moreand[q] quitrather than wrapping, without changing shortcut bindings.vm/file_manager/—DualPaneVM, twoPaneVMchildren, entry VMs, and transfer state.DualPaneVMowns cross-provider copy/move orchestration; eachPaneVMowns one provider-backed projection and cursor.vm/table_clipboard_vm.py—TableClipboardVM, an app-lifetime VMx component that retains one typed, replaceable table reference.vm/emr_serverless/—EmrServerlessPageVMplus itsApplicationsVM/JobRunsVM/JobRunDetailVM/JobRunLogsVMchildren (the read-mostly EMR Serverless browser with logs streaming and focused clone submission). Its immutableServiceSourceContextcarries the active connection name, optional distinct AWS profile, and region to the service view; the sharedServiceSourceHeaderrenders that identity above the EMR application picker.ServiceSelectionStorescopes remembered service selections by(service_id, connection_name, region).JobRunCloneVMbacks the clone-job-run modal — a sibling VM undervm/emr_serverless/clone_vm.py, instantiated per modal-mount with the focused run as the source.vm/glue/—GluePageVMwith independent Catalog, Jobs, and Crawlers child VMs.GlueCatalogVMowns aGlueIcebergVMchild that appears only for tables classified as Iceberg and loads Snapshots, History, Manifests, Files, Partitions, and References independently. The page sharesServiceSourceContextand connection/region-scoped selection memory with the other single-context AWS services.GluePageVMowns page-scoped table and snapshot handoff capability checks.GlueCatalogVMpublishes immutable, service-neutralOpenS3LocationRequestand tableOpenAthenaTableRequestmessages from the selected table;GlueIcebergVMpublishes snapshotOpenAthenaTableRequestmessages from the visible snapshot. They never mount a Textual view or construct a destination service themselves.vm/athena/—AthenaPageVMwith Query, History, Results, and Saved child VMs. The view composes Source, Workgroup, Catalog, and Database as individually framed selectors in an unframed row, followed by the tabs and active view. Its context and remembered selections are scoped by connection name and region; changing workgroup, catalog, or database invalidates the query context. Query work stays in the VM layer, whiledomain/athena.pyowns boto mapping anddomain/sql_policy.pyfails closed before dispatch.AthenaPageVM.open_table(...)sets exact catalog/database context and prefills a quoted, context-relativeLIMIT 5starter without executing it.open_table_in_glue()publishesOpenGlueTableRequestonly when the current SQL resolves to one visible table.vm/settings/—SettingsVM(built per-mount when the user selects the Settings nav peer) andS3ConnectionsVM(singleton onAppContext, drives the in-app Connections CRUD).- Top-level
vm/nav_menu_vm.py—NavMenuVM(renamed fromServicesMenuVM;RootVM.services_menuis a legacy alias) keeps the active service selected while focus moves into its content. The same layer includesvm/content_host_vm.pyandvm/root_vm.py. - Service plugins — One folder per top-level service
(
src/aws_tui/services/). The current tree shipss3,emr-serverless(read-only browser + clone-job-run plus job-run logs — applications listing, job-runs master-detail, state-filter chips, clone-and-edit modal viac; cancel / vanilla submit are still deferred),glue(read-only Catalog, Jobs, and Crawlers), andathena(read-only query, history, results, and saved-query views).GlueServicecomposes bothGlueClientand an Athena-backedIcebergInspector;AthenaServicecomposes the query page. Each service implements theServiceprotocol (declared invm/services_protocol.py, re-exported fromservices/__init__.py). - Domain —
FileSystemProviderprotocol withLocalFSandS3FSimplementations + the cross-FS copy/move engine + the transfer journal (src/aws_tui/domain/). The Norton-Commander unifier; the pane VMs treat both sides as the same protocol. Domain adapters perform the runtime AWS and filesystem I/O:LocalFSandTransferJournalaccess host storage, whileS3FS,EmrServerlessClient,GlueClient, andAthenaClientissue service operations and map external responses/errors into domain values.TableRefandQueryContextcarry immutable table and execution identity. User-driven Glue and EMR lists use VMx token paging through the app's bounded composition specialization. Athena uses an app-owned bounded snapshot pager because VMx does not expose public snapshot hydration, while retaining VMx commands for execution and enablement. Both stop at explicit cumulative collection ceilings, and the UI distinguishes a safety-limit stop from an ordinary continuation.IcebergInspectorusesAthenaQueryRunnerto read bounded Iceberg metadata tables; Athena history hydrates each bounded page with one batch request.ReadOnlySqlPolicyvalidates both user and generated SQL. Raw AWS responses remain below VMs. - Infrastructure — Infrastructure owns sessions, credentials,
configuration, SDK client construction, and OS-backed stores.
AwsSessionandConnectionResolverprovide configured AWS identities and client contexts to the domain adapters. Resolver order drivesShift+S; the VM-layerServiceSelectionStorescopes workgroup and resource selections by service, connection name, and region.ConfigStore,ThemeStore,KeymapStore,LogSink,CrashDump, andKeychainBackendpersist application and platform state.ConfigStoreand Settings share endpoint and credential-source validation for S3-compatible connections. Infrastructure prepares those boundaries; domain adapters perform the provider operations.
demo/ is a composition-only provider substitution outside the production
layers. Demo mode selects in-memory service adapters at the composition root;
production modules do not import the demo package.
2. Composition root¶
The two top-level files src/aws_tui/composition.py and
src/aws_tui/app.py are the only modules permitted to import from
every layer. composition.py builds the dependency graph; app.py
is the Textual App subclass that mounts widgets and wires action
handlers.
composition.py also constructs the app-lifetime TableClipboardVM and the
infra clipboard port. app.py owns the single clipboard writer that every
copy goes through, including the one that follows the typed request: it writes
OSC 52 and asks the port, then reports the port's result — the OS clipboard
took it, a helper was there and failed, or there was no helper and only the
terminal was written. The VM remains the authoritative in-app clipboard.
ActionRegistry is also rooted in app.py: Shift+Q, Shift+V,
command-palette entries, and the Iceberg arrow button dispatch the same
registered Glue actions before the Glue VMs publish a single typed Athena
request path.
AwsTuiApp remains a large composition-root adapter that coordinates navigation,
cross-service rollback, action routing, and shutdown. Extracting those transaction
coordinators into dedicated mediators is deferred to a focused architecture change:
the move spans cancellation ownership and complete rollback state across all four
services, so treating it as a mechanical maintenance refactor would carry more risk
than the line-count reduction justifies.
At startup, automatic connection attempts consume one shared 90-second budget; untried sources remain available for explicit selection after the local fallback mounts.
3. Lifecycle¶
Those four enums are independent. A transfer cancelled mid-flight says nothing about the query that produced its source, and the app never drives an EMR job run's state — it only observes what the poller reports.
Disposal order is the part that cannot be read off the code: a widget's workers must drain before its view model is disposed, or a late callback resumes against a disposed VM.
VMx components implement construct → destruct → dispose. Hosted service VMs
may additionally expose app-owned asynchronous setup and shutdown hooks;
those hooks are not a VMx lifecycle phase.
The RootVM constructs the chrome and content-host children
depth-first; ContentHostVM.set_content(new) disposes the previous
content via the same cascade. When outgoing content exposes shutdown,
ContentHostVM.set_content(...) awaits it before calling dispose; hosted VM
shutdown is awaited before disposal, and the host owns disposal of a candidate
that is never adopted. Every hosted service's owned operations drain before
teardown. App shutdown is task-owned and idempotent. Explicit quit and Textual
unmount (including fatal teardown) await the same sequence: stop navigation
intake, drain transfers, setup, queries, and preview workers, close every
aioboto3 client, dispose subscriptions and the VM tree, then flush and close
logs last so teardown diagnostics remain available
(spec §6.4).
4. Messaging¶
The sequence above carries what the flow boxes cannot: the ordering, the await points, and the fact that a generated statement waits for an explicit run. A failure at any step restores the snapshot rather than leaving a half-switched service.
Cross-service and shell-wide event communication goes through the session's
single MessageHub. Parent VMs orchestrate their owned children directly when
the interaction stays inside one subtree. Custom envelopes (defined in
src/aws_tui/vm/messages.py):
ConnectionChangedMessage,ThemeChangedMessage,TransferProgressMessage,FocusChangedMessage,PaletteActionFailedMessage,TransferCancelRequestedMessage,ConnectionListChangedMessage,OpenS3LocationRequest,OpenAthenaTableRequest,OpenGlueTableRequest,CopyTableReferenceRequest, andServiceOperationFailedMessage. Recovered service exceptions use the last envelope to reachRootVM's redacted durable-log boundary without coupling service VMs to logging infrastructure.
Cross-service navigation stays service-neutral. OpenAthenaTableRequest and
OpenGlueTableRequest carry a TableRef containing catalog, database, table,
connection name, and region; the Athena request may add a validated snapshot
ID. app.py serializes table handoffs, resolves the exact connection, rejects
region drift, snapshots the outgoing Glue/Athena state for rollback, and
mounts the destination through RootVM. For an Athena destination, the
AthenaPageVM publishes the exact quoted
SELECT * FROM "database"."table" LIMIT 5 immediately after mount
and before awaiting remote Athena setup; a snapshot request adds
FOR VERSION AS OF <snapshot-id> before the limit. The query VM keeps its VMx
Run command disabled while the exact workgroup, catalog, and database context
is resolving. AthenaClient sends that selected catalog and database through
QueryExecutionContext, and categorized start-query validation failures cross
the runner as fixed redacted domain errors. Athena receives the generated SQL
in the editor but does not execute it automatically. For S3,
OpenS3LocationRequest carries the
exact connection, region, URI, pane, and reveal-object intent. The Results VM
reloads an execution and publishes only when it succeeded, belongs to the
active context, and has a valid s3:// output location. Missing, malformed,
ambiguous, or stale identities stop at an advisory; VMs never import UI code.
CopyTableReferenceRequest carries one exact TableRef to the composition
root, which updates TableClipboardVM and hands the identifier to the app's
clipboard writer, whose toast states which channel actually accepted it.
Palette eligibility is VM-owned state: the palette projects global commands
and only commands whose declared service IDs include the active service.
Cross-service and shared-chrome messages use the MessageHub: VMs subscribe
directly where they own the reaction, while widgets use HubSubscriberMixin
when a view must consume hub traffic. Local VM-to-view updates use each VM's
on_property_changed observable instead of the global hub. Widget-owned
subscriptions are disposed on unmount, and VM-owned subscriptions are disposed
with the VM lifecycle.
5. Testing pyramid¶
| Tier | Source | What it proves |
|---|---|---|
| Unit | tests/unit |
VM, domain, infra behavior; isolated local I/O only, with no external services |
| Snapshot | tests/snapshot |
View rendering against golden SVGs per theme × screen-state combination, plus paired content-presence guards that reject blank-but-valid output |
| Integration (in-process) | tests/integration |
Full-app smoke + regression flows (app pilot, modal forwarding, multi-select, source swap, settings nav-page toggle, expired-SSO probe, etc.) |
| E2E | tests/e2e |
Pilot-driven user journeys |
| Integration (S3-compatible) | -m integration |
Adobe S3Mock via testcontainers (opt-in) |
Counts change as coverage grows, so they are not restated here. Recount any tier by collecting it:
uv run pytest tests/unit --collect-only -q | tail -1
uv run pytest --collect-only -q | tail -1 # default tiers together
uv run pytest -m integration --collect-only -q | tail -1
find tests/snapshot/__snapshots__ -name '*.raw' | wc -l
Run the default tiers (unit + snapshot + e2e + in-process integration)
with uv run pytest. Opt into the S3-compatible tier with
uv run pytest -m integration — it spins up a container, which the
default addopts filter excludes (-m 'not integration').
6. Layer-rule check¶
scripts/check-layers.sh parses Python imports with ast across the
five layer subtrees, resolves relative imports to absolute module names,
and matches them against the banned-import rules inlined in the script.
The composition root and app.py are deliberately excluded — they live
at src/aws_tui/ top-level so the check never inspects them.
7. Where to start reading the code¶
src/aws_tui/composition.py— see how everything wires.src/aws_tui/vm/root_vm.py— top of the VM tree.src/aws_tui/vm/file_manager/dual_pane_vm.py— the first concrete page VM (S3 service hosts it).src/aws_tui/vm/emr_serverless/page_vm.py::EmrServerlessPageVM— another concrete page VM; a richer pattern that orchestrates four child VMs (ApplicationsVM,JobRunsVM,JobRunDetailVM,JobRunLogsVM) and runs three independent pollers.src/aws_tui/services/s3/service.py— the first concrete service in v0.7.0; pattern for future ones.src/aws_tui/services/emr_serverless/service.pyuses the richer per-service subtree pattern (dedicated domain client + VM subtree + UI widget tree).src/aws_tui/services/glue/service.pyfollows the same factory lifecycle for an AWS-only, read-only three-view page and retains validated selection identifiers per connection name and region. S3 owns independent sources for each pane, while single-context AWS services useRootVM's active connection and are rebuilt as a whole when their source changes.src/aws_tui/services/athena/service.pyis the corresponding query-service reference: it composes an Athena client, read-only SQL policy, and a freshAthenaPageVMper AWS connection. Glue composes its own contextual Athena client forIcebergInspector; it does not reuse the mounted Athena VM.src/aws_tui/domain/cross_fs.py— the engine that moves bytes between any pair ofFileSystemProviders.src/aws_tui/ui/widgets/— pure Textual widgets; per-VM smoke tests intests/unit/ui/.src/aws_tui/vm/nav_menu_vm.py+src/aws_tui/ui/widgets/nav_menu.py— the left-rail nav: services list on top, Settings docked at the bottom (split into twoOptionLists in the widget).src/aws_tui/vm/settings/settings_vm.py+src/aws_tui/ui/widgets/settings_view.py— the in-app Settings page (built per-mount, not as anAppContextsingleton — see the lifecycle amendment in the Settings-as-nav-page design spec).