Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ taking an injected app reference: `controllers/lists.py` (`app.list_ctl`), `dash
(`app.dash_ctl`), `graphs.py` (`app.graph_ctl`), `connection.py` (`app.conn_ctl` — the HA websocket
message pump, `handle_ha_message`/`_HA_MESSAGE_HANDLERS`), `notifications.py` (`app.notify_ctl`),
`logbook.py` (`app.log_ctl` — the activity log's scope/paging/fetch/subscription state machine,
shared by `HACLI`'s docked panel, `GraphPreviewScreen`'s, and `DashboardScreen`'s). **`HACLI` keeps its old attribute surface
via property pairs** (`app.dashboards`, `app.current_list_name`, `app._detail_entity_id`, …) so
screens and tests read/assign through the app unchanged; new UI code should call controllers
directly instead (`self.app.dash_ctl.set_slot(...)`).
shared by `HACLI`'s docked panel, `GraphPreviewScreen`'s, and `DashboardScreen`'s), `keybindings.py`
(`app.keys_ctl` — owns the user's keybinding overrides and pushes the resulting keymap onto the
running app via `App.set_keymap`; every screen's `BINDINGS` is `bindings_for(scope)` from this
module's `REGISTRY`, the single source of truth for all ~220 bindings in the app, rebindable from
Configuration ▸ Keybindings). Like `const.py`/`types.py`, `keybindings.py`'s registry half is
cycle-safe (no `hatty.ui`/`hatty.main` imports) since it's imported at class-definition time by
every screen module. **`HACLI` keeps its old attribute surface via property pairs**
(`app.dashboards`, `app.current_list_name`, `app._detail_entity_id`, …) so screens and tests
read/assign through the app unchanged; new UI code should call controllers directly instead
(`self.app.dash_ctl.set_slot(...)`).

**Two-tier config persistence.** `config.yaml` is lean — connection settings and display
preferences only. The user-data collections (`lists`, `entity_names`, `dashboards`, `saved_graphs`,
Expand All @@ -65,7 +71,7 @@ params typed as `Entity` (not bare `dict`) and read `total=False` fields via `.g
- `src/hatty/main.py` — the `HACLI` Textual app: keybindings, message routing, entity-table state,
cross-cutting plumbing (`spawn(coro)` for tracked fire-and-forget tasks — never bare
`asyncio.create_task`; `persist(*keys)` to mirror + save a collection).
- `src/hatty/controllers/` — the four controllers above.
- `src/hatty/controllers/` — the controllers above.
- `src/hatty/client.py` — `HAClient`: websocket auth/requests, REST history/logbook fetchers (swallow
errors, return `None`).
- `src/hatty/config.py` / `storage.py` — YAML config and SQLite collection persistence.
Expand Down
5 changes: 5 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ theme: null
graph_type: line
graph_hours: 4
log_hours: 24
# Custom keybindings, also editable from Configuration > Keybindings in the app.
# Maps a keybinding id to a Textual key string; omitted ids keep their default.
# keybindings:
# log.toggle: "A"
# nav.back: "backspace"
2 changes: 2 additions & 0 deletions src/hatty/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CONFIG_KEY_GRAPH_HOURS,
CONFIG_KEY_GRAPH_TYPE,
CONFIG_KEY_HOME_ASSISTANT,
CONFIG_KEY_KEYBINDINGS,
CONFIG_KEY_LISTS,
CONFIG_KEY_LOG_HOURS,
CONFIG_KEY_MANUAL_LISTS,
Expand Down Expand Up @@ -80,6 +81,7 @@ def default_config() -> dict:
CONFIG_KEY_NOTIFICATIONS: dict(DEFAULT_NOTIFICATIONS),
CONFIG_KEY_TERMINAL_TITLE_ENABLED: True,
CONFIG_KEY_TERMINAL_TITLE: DEFAULT_TERMINAL_TITLE,
CONFIG_KEY_KEYBINDINGS: {},
}


Expand Down
7 changes: 7 additions & 0 deletions src/hatty/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def binary_state_label(state: str, device_class: str) -> str:
return off_label
return state


# Dashboard slot widget types offered by DashboardSlotPopup ("split" is not
# assignable — split slots are created via SplitSlotPopup).
WIDGET_TYPES = [
Expand Down Expand Up @@ -169,6 +170,11 @@ def binary_state_label(state: str, device_class: str) -> str:
# Fallback for the global "log_hours" config value (the activity log's window size).
DEFAULT_LOG_HOURS = 24

# GraphPreviewScreen's shift+left/shift+right "fast page" multiplier over the
# normal left/right page. Lives here (not preview_screen.py) so the keybinding
# registry can reference it in a binding description without an import cycle.
FAST_PAGE_MULTIPLIER = 6

# Canonical names for the top-level app_config keys, so a rename is one edit and a
# typo is a NameError instead of a silent None. config.default_config() and
# storage.PERSISTED reference these, keeping them the single literal definition.
Expand All @@ -194,6 +200,7 @@ def binary_state_label(state: str, device_class: str) -> str:
CONFIG_KEY_NOTIFY_LISTS = "notify_lists"
CONFIG_KEY_TERMINAL_TITLE_ENABLED = "terminal_title_enabled"
CONFIG_KEY_TERMINAL_TITLE = "terminal_title"
CONFIG_KEY_KEYBINDINGS = "keybindings"

# Fallback/default value for the "terminal_title" config key (issue: set tmux
# title to hatty or pref).
Expand Down
Loading
Loading