Seamless Glance is a single-binary Rust TUI application. Its control flow is straightforward:
- parse CLI flags
- initialize terminal state and AWS clients
- load cached cost data
- enter the event loop
- refresh overview and active-view data on demand
- render the current screen with ratatui
The codebase is intentionally organized around one central App state object and a set of service-specific fetch modules.
Today that architecture is better at inventory and summaries than triage. A likely next evolution is to add a dedicated findings layer that consumes the existing AWS fetchers and turns raw resource data into prioritized operator actions.
That findings layer now exists in an initial form and is intentionally lightweight: it derives a small set of triage findings from already-fetched overview and security-group data, then routes operators into the relevant service views.
Owns:
- CLI entrypoint
- help and version output
- crossterm alternate-screen and mouse-capture lifecycle (mouse capture is released while a CLI command runs and restored after, in
ui::terminal) - keyboard and mouse event handling. The mouse wheel routes through the same
App::scroll_line_up/scroll_line_downas the arrow keys, so it scrolls whatever has focus (help, an overlay, or the active view); other mouse events are ignored
Owns:
Appstate- active view selection
- region selection and persistence
- profile selection and persistence
- refresh orchestration
- cached cost loading
- resource describe/open/SSH actions
- overlay state
Region and profile switching share one path: rebuild_aws_clients rebuilds the
SDK client bundle for the current region and current profile, so a selected
profile is preserved across region changes.
This is the highest-leverage file in the app. Many user-visible workflows converge here.
Owns service integration and data fetching. Common patterns:
- build or reuse AWS SDK clients
- fetch raw AWS data
- map AWS responses into local model structs
- handle access denied or unavailable states where the view expects status feedback
This layer should remain primarily factual. As the product shifts toward triage, avoid burying prioritization logic deep inside fetchers unless it is tightly coupled to the AWS response itself.
Representative files:
src/aws/account.rs: account overview fan-out, plusassemble_overviewand the summary builders that derive the overview from already-fetched inventoriessrc/aws/clients.rs: shared SDK client bundle,build_sdk_config(the single region/profile-aware config builder), andclients_for_region, the one definition global fan-outs use to build a per-region bundle. Every SDK config in the app is built here, so it is also where the retry and timeout policy lives: adaptive retry (client-side rate limiting that backs off when AWS throttles) plus connect, per-attempt, and overall operation timeouts, which matter because the app fans out across every enabled region and issues a describe per resourcesrc/aws/profiles.rs: discovers AWS profile names from the shared config and credentials files for the in-app pickersrc/aws/cost.rs: cost explorer queries for budget, forecast, trailing spend, and usage-aware service cost insightsrc/aws/ec2.rs: EC2 inventory and global aggregationsrc/aws/ecr.rs: ECR repositories, each rolled up from a fullDescribeImageslisting (image and untagged counts, total size, latest push and recorded pull) plus its lifecycle policy and tags. A failed image listing or policy lookup is recorded as unknown (None) rather than as zero images or a missing policy, so it cannot raise a finding
Owns app-facing types used by both fetchers and UI views:
- resource rows
- summary cards
- finding rows and finding routing metadata
- service status types
- traits such as resource description and console linking behavior
Owns presentation concerns:
- screen layout
- headers and footers
- shared navigation metadata and command palette rendering
- the key registry (
src/ui/keys.rs): one table ofkey -> action, label, help, scope. The event loop dispatches every bound character key through it, and the footer hints and help screen render from it, so an advertised binding cannot drift from the one that runs. Adding a key is one registry entry plus one arm in the dispatcher, whose exhaustive match overKeyActionmeans the compiler catches an unhandled action. Keys whose meaning depends on the surface (Enter,Esc,v, arrows, the SSH picker digits) stay contextual and are listed inCONTEXTUAL_KEYSso help still describes them in one place - theme definitions and theme switching presentation
- individual views
- the shared list-view renderer (
src/ui/views/list_table.rs): every service list view goes throughrender_list_table, which owns selection clamping, the visible-height math, keeping the selected row on screen, windowing, the empty state, and table assembly. Views supply only their columns and content styling; selection styling is applied centrally so it looks the same everywhere. Note the windowing enumerates before skipping, so the index compared against the selection is the absolute row index - help and overlay rendering
- transient notifications (
src/ui/notification.rs): a non-modal toast for surfacing failed actions and bad command input, sinceeprintln!is invisible behind the alternate screen. CallApp::notify_errororApp::notify_warningto raise one; it auto-dismisses after a few seconds viaApp::clear_expired_notificationin the event loop. - terminal suspend/resume helpers used for shell execution
Owns helpers shared across domains:
- multi-region aggregation:
region_aggregate::fetch_all_regionsis the single status-preserving fan-out. EC2, RDS, and Lambda all run their global fetch through it, so one region answering yieldsOkwith partial data, and when nothing answers a denial outranks a generic failure - SSH command context construction
This area is a good home for future shared action helpers such as CLI command generation or resource-specific operator pivots.
Currently used for cost caching. The cache now stores budget, forecast, trailing spend, and usage-aware service cost insight so cost-oriented views can open quickly without re-querying Cost Explorer every time. The cache file is keyed by profile and region (cost-<profile>-<region>.json) and the stored scope is re-checked on load, so cached spend from one profile is never shown while another profile is active.
At startup, main:
- handles
--help,--version, and--profile <name> - loads config from
~/.seamless-glance/config.json - loads persisted theme preference from
~/.seamless-glance/config.json - resolves the AWS profile from
--profileor the persisted config value - discovers available AWS profiles for the in-app picker
- fetches enabled AWS regions using the resolved profile; if
ec2:DescribeRegionsfails or returns nothing, it falls back to the documentedregions::FALLBACK_REGIONSlist and raises a visible warning so the operator knows global views may be incomplete - creates
AwsClientsfor the selected region and profile - constructs
App - loads cost data and usage-aware service cost insight from the on-disk cache only; it never calls Cost Explorer before the first frame, since the cost views force a live fetch when entered
- triggers the first refresh
The AWS fetches run off the draw thread so the UI stays responsive and the per-service progress phases actually render. App::trigger_refresh (see src/app/refresh.rs):
- clears stale inventory if the account context (profile + region) changed since the held data was fetched
- spawns a worker task on a throwaway clone of the account context (
AwsClientsis cheap to clone; the SDK clients areArc-backed) - stores the receiving end of an
mpscchannel on the app
The worker fetches the active view's service data, sending a RefreshUpdate per service, then Done. When the account overview is needed it runs on its own task alongside the view's fetches rather than ahead of them, and Done waits for it because findings gate on the overview. Findings is the exception: it already fetches nearly every service the overview summarizes, so it fetches only the caller identity and ECS on top and assembles the overview from its own inventories (aws::account::assemble_overview plus the per-service summary builders there), rather than fetching each service twice. Navigation (on_view_enter) reuses a fresh overview instead of refetching it, since the overview alone is about ten service fetches; a manual refresh or a profile or region switch always refetches it. A same-context refresh keeps the previous overview on screen until the new one lands; only a profile or region change clears it. For the multi-service views (Findings and Cost Savings) the independent fetches run concurrently via tokio::join!, so refresh latency is the slowest single fetch rather than the sum of ~11. Target groups and load balancers are fetched as one unit (health folding needs both) that joins the concurrent set. Each frame, App::drain_refresh_updates applies whatever has arrived (so refresh_phase advances and views populate) without ever blocking the loop. On Done it rebuilds findings and cost savings, updates last_refresh, and clears the refreshing flag.
Step 1 matters because a single-service refresh only fetches its own service but rebuild_findings reads several service inventories. Tracking the context the data was fetched under (data_context) and clearing on change ensures findings are never built from a prior region or profile's data and mislabeled with the current region.
trigger_refresh records the (profile, region, view) it is fetching for in in_flight_refresh: an identical re-trigger is deduped, while a context or view change (now possible mid-refresh, since the UI is responsive) supersedes the in-flight worker by replacing the receiver.
Cost views are a partial exception: they still rely on cached Cost Explorer data, but force a fresh reload when the active view is Cost Overview or Cost Savings.
App::on_view_enter resets selection state and schedules a refresh only when the new view's data is stale. Each applied RefreshUpdate stamps an in-memory inventory_fetched_at timestamp per service; active_view_is_fresh checks that every inventory the view needs (see required_inventories) was fetched within INVENTORY_TTL. If so, the cached data is served with no AWS call; otherwise the spawned worker refetches. The freshness map is cleared alongside the inventory on a profile or region change, so cached data is never served across contexts. A manual refresh (r) and profile/region switches call trigger_refresh directly and always refetch. The header shows the data's relative age (for example Last updated 14:32:10 UTC (2m ago)) so staleness is explicit.
Navigation is currently shared across three layers:
- direct key shortcuts such as
fand the transitional1through9 - a slash command palette with grouped service metadata and aliases
Tab/Shift+Tabcycling through major views
The command palette, help overlay, and header/footer cues should prefer shared navigation metadata rather than hand-maintained duplicated strings.
User actions typically route through App helpers:
trigger_describetrigger_clitrigger_opentrigger_sshopen_selected_findingopen_selected_cost_savings_opportunity
Those helpers pull the selected row, derive a description request, CLI command, or console URL, and open an overlay or external browser action as needed.
For region-aware resources, action handlers should prefer the row's own region over the currently selected UI region. This matters most in global aggregation views, where a generic fallback region can otherwise produce misleading console links or failed describe calls.
The app maintains a list of enabled AWS regions plus a synthetic global slot.
Important nuance:
- the global slot is a UI concept, not an AWS SDK region; it sits one index past the last real region
- it is persisted as the
globalsentinel and restored on startup (the initial client bundle still binds a real region, since global fans out per region separately) - only some service fetchers currently aggregate across all enabled regions
- EC2, Lambda, and RDS implement global aggregation today
When expanding global behavior, keep the user-facing label honest and update the docs.
This is especially important for a triage tool because incorrect region context breaks trust in both console and CLI pivots.
The codebase uses a mix of strategies:
- list fetchers return
(Vec<_>, ServiceStatus); the app stores the status in a companion*_statusfield and the view guards on it viaui::views::status::render_unavailablebefore drawing the table, so an access denial or throttle reads as such rather than as an empty list - list fetchers page through the full result set (
.into_paginator().items().send(), or a manual next-token loop where the SDK has no generated paginator, such asapigatewayv2::get_apis), so large accounts are not silently truncated to the first page - where a fetcher needs a follow-up describe per resource (target-group health, SQS queue attributes, ECR images and lifecycle policies), those calls run concurrently but bounded by
aws::DESCRIBE_CONCURRENCYrather than one round trip at a time, so large accounts stay fast without amplifying throttling. A failed per-resource describe drops that row, as before - cost fetchers return
(data, ServiceStatus)too;refresh_cost_datacomposes a singlecost_status, the cost overview and cost savings views guard on it, and only a successful fetch is written to the on-disk cost cache so a denied fetch is not persisted as a misleading $0 overview - account overview fans out with concurrent calls and composes summary status. It needs counts, not detail, so it skips per-resource follow-ups it does not use (Lambda counts skip the per-function
ListTags) - SDK errors are classified centrally by
ServiceStatus::from_sdk_error, which inspects the error code viaProvideErrorMetadata(recognizingAccessDenied,AccessDeniedException,UnauthorizedOperation, and related authz codes) rather than substring-matching the display string
Contributors adding a new list view should follow the same shape: carry a ServiceStatus out of the fetcher, store it alongside the data, and call render_unavailable at the top of the view so AWS failures never render as misleading empty-state success UIs.
To add a new service view cleanly:
- create or extend the model types
- add a fetcher under
src/aws/ - extend
AwsClientsif a new SDK client is needed - add app state fields and refresh hooks in
src/app/mod.rs - add a
src/ui/views/*.rsrenderer - wire navigation and command palette support
- update docs
To add a future triage or waste feature cleanly:
- keep resource collection in
src/aws/ - derive findings from collected facts in a dedicated layer rather than directly in view code
- attach operator actions to each finding
- make severity, category, and next step obvious in the UI
The initial findings implementation follows that pattern with:
- a
Findingmodel insrc/models/finding.rs - a rule registry in
src/app/findings.rs: each rule is a smallfn(&FindingContext) -> Vec<Finding>listed inFINDING_RULES, so rules are individually unit-testable and adding one is a function plus a registry entry rather than more inline branches.FindingContextcarries borrowed inputs (the account overview and the service inventories), andsample_listis the shared "a, b, c (+N more)" helper every rule uses for its summary. Rules that read the account overview return nothing when it is absent - derived findings stored in
App;App::rebuild_findingsjust builds the context and callsbuild_findings - a dedicated
Findingsview that routes into related service screens
The cost-savings implementation follows a similar pattern:
- usage-aware service spend is cached through
src/cache/cost.rs Appderives cost-savings opportunities by combining spend, usage types, and waste-oriented resource signals- a dedicated
Cost Savingsview routes operators into the underlying service screen for action
- automated tests are minimal
- global aggregation support is incomplete across services
- service coverage is still missing several high-value AWS domains like EBS, Elastic IPs, CloudWatch Logs, and S3
- the findings backlog is still much smaller than the intended triage and waste catalog
- refresh and error handling conventions are not fully uniform across all fetchers
Those are good places to tighten over time, but any changes should preserve the current operator-friendly speed and simplicity.