feat(tui): implement Settings screen and session management screen #10651
@@ -0,0 +1,278 @@
|
||||
"""Step definitions for tui_settings_session_screens.feature."""
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.tui.commands import TuiCommandRouter
|
||||
from cleveragents.tui.screens.session_management import (
|
||||
SessionInfo,
|
||||
SessionManagementScreen,
|
||||
)
|
||||
from cleveragents.tui.screens.settings import SettingsScreen
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background
|
||||
# ---------------------------------------------------------------------------
|
||||
@given("the TUI screens module is imported")
|
||||
def step_tui_screens_imported(context):
|
||||
"""Verify the screens module is importable."""
|
||||
assert SettingsScreen is not None
|
||||
assert SessionManagementScreen is not None
|
||||
assert SessionInfo is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SettingsScreen scenarios
|
||||
# ---------------------------------------------------------------------------
|
||||
@given("a SettingsScreen widget")
|
||||
def step_create_settings_screen(context):
|
||||
"""Create a SettingsScreen widget."""
|
||||
context.settings_screen = SettingsScreen()
|
||||
|
||||
|
||||
@given("a SettingsScreen widget that is visible")
|
||||
def step_create_visible_settings_screen(context):
|
||||
"""Create and show a SettingsScreen widget."""
|
||||
context.settings_screen = SettingsScreen()
|
||||
context.settings_screen.show()
|
||||
|
||||
|
||||
@when("I show the settings screen")
|
||||
def step_show_settings_screen(context):
|
||||
"""Show the settings screen."""
|
||||
context.settings_screen.show()
|
||||
|
||||
|
||||
@when("I hide the settings screen")
|
||||
def step_hide_settings_screen(context):
|
||||
"""Hide the settings screen."""
|
||||
context.settings_screen.hide()
|
||||
|
||||
|
||||
@when("I navigate to the next setting")
|
||||
def step_navigate_next_setting(context):
|
||||
"""Navigate to the next setting."""
|
||||
context.settings_screen.navigate_next()
|
||||
|
||||
|
||||
@when("I navigate to the previous setting")
|
||||
def step_navigate_prev_setting(context):
|
||||
"""Navigate to the previous setting."""
|
||||
context.settings_screen.navigate_prev()
|
||||
|
||||
|
||||
@when('I set the theme to "{theme}"')
|
||||
def step_set_theme(context, theme):
|
||||
"""Set the theme."""
|
||||
context.settings_screen.set_theme(theme)
|
||||
|
||||
|
||||
@when('I set the default_actor to "{actor}"')
|
||||
def step_set_default_actor(context, actor):
|
||||
"""Set the default actor."""
|
||||
context.settings_screen.set_default_actor(actor)
|
||||
|
||||
|
||||
@when('I set the safety_level to "{level}"')
|
||||
def step_set_safety_level(context, level):
|
||||
"""Set the safety level."""
|
||||
context.settings_screen.set_safety_level(level)
|
||||
|
||||
|
||||
@then('the theme should be "{expected}"')
|
||||
def step_verify_theme(context, expected):
|
||||
"""Verify the theme setting."""
|
||||
assert context.settings_screen.theme == expected
|
||||
|
||||
|
||||
@then('the default_actor should be "{expected}"')
|
||||
def step_verify_default_actor(context, expected):
|
||||
"""Verify the default actor setting."""
|
||||
assert context.settings_screen.default_actor == expected
|
||||
|
||||
|
||||
@then('the safety_level should be "{expected}"')
|
||||
def step_verify_safety_level(context, expected):
|
||||
"""Verify the safety level setting."""
|
||||
assert context.settings_screen.safety_level == expected
|
||||
|
||||
|
||||
@then("the settings screen should be visible")
|
||||
def step_verify_settings_visible(context):
|
||||
"""Verify the settings screen is visible."""
|
||||
assert context.settings_screen.visible is True
|
||||
|
||||
|
||||
@then("the settings screen should not be visible")
|
||||
def step_verify_settings_not_visible(context):
|
||||
"""Verify the settings screen is not visible."""
|
||||
assert context.settings_screen.visible is False
|
||||
|
||||
|
||||
@then("the selected_index should be {index:d}")
|
||||
def step_verify_selected_index(context, index):
|
||||
"""Verify the selected index on whichever screen was created in this scenario."""
|
||||
screen = getattr(context, "session_screen", None) or getattr(
|
||||
context, "settings_screen", None
|
||||
)
|
||||
assert screen is not None, "no screen widget created in this scenario"
|
||||
assert screen.selected_index == index, (
|
||||
f"expected selected_index={index}, got {screen.selected_index}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SessionManagementScreen scenarios
|
||||
# ---------------------------------------------------------------------------
|
||||
@given("a SessionManagementScreen widget")
|
||||
def step_create_session_screen(context):
|
||||
"""Create a SessionManagementScreen widget."""
|
||||
context.session_screen = SessionManagementScreen()
|
||||
|
||||
|
||||
@given("a SessionManagementScreen widget with 3 sessions")
|
||||
def step_create_session_screen_with_sessions(context):
|
||||
"""Create a SessionManagementScreen widget with 3 sessions."""
|
||||
context.session_screen = SessionManagementScreen()
|
||||
sessions = [
|
||||
SessionInfo(
|
||||
session_id=f"session-{i}",
|
||||
name=f"Session {i}",
|
||||
message_count=i * 10,
|
||||
created_at="2026-04-19T00:00:00Z",
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
context.session_screen.load_sessions(sessions, "session-0")
|
||||
|
||||
|
||||
@when("I show the session management screen")
|
||||
def step_show_session_screen(context):
|
||||
"""Show the session management screen."""
|
||||
context.session_screen.show()
|
||||
|
||||
|
||||
@when("I hide the session management screen")
|
||||
def step_hide_session_screen(context):
|
||||
"""Hide the session management screen."""
|
||||
context.session_screen.hide()
|
||||
|
||||
|
||||
@when("I load {count:d} sessions")
|
||||
def step_load_sessions(context, count):
|
||||
"""Load sessions into the screen."""
|
||||
sessions = [
|
||||
SessionInfo(
|
||||
session_id=f"session-{i}",
|
||||
name=f"Session {i}",
|
||||
message_count=i * 10,
|
||||
created_at="2026-04-19T00:00:00Z",
|
||||
)
|
||||
for i in range(count)
|
||||
]
|
||||
context.session_screen.load_sessions(sessions, "session-0")
|
||||
|
||||
|
||||
@when("I navigate to the next session")
|
||||
def step_navigate_next_session(context):
|
||||
"""Navigate to the next session."""
|
||||
context.session_screen.navigate_next()
|
||||
|
||||
|
||||
@when("I navigate to the previous session")
|
||||
def step_navigate_prev_session(context):
|
||||
"""Navigate to the previous session."""
|
||||
context.session_screen.navigate_prev()
|
||||
|
||||
|
||||
@then("the sessions list should be empty")
|
||||
def step_verify_sessions_empty(context):
|
||||
"""Verify the sessions list is empty."""
|
||||
assert len(context.session_screen.sessions) == 0
|
||||
|
||||
|
||||
@then("the sessions list should have {count:d} items")
|
||||
def step_verify_sessions_count(context, count):
|
||||
"""Verify the sessions list has the expected count."""
|
||||
assert len(context.session_screen.sessions) == count
|
||||
|
||||
|
||||
@then('the current_session_id should be "{expected}"')
|
||||
def step_verify_current_session_id(context, expected):
|
||||
"""Verify the current session ID."""
|
||||
assert context.session_screen.current_session_id == expected
|
||||
|
||||
|
||||
@then("the session management screen should be visible")
|
||||
def step_verify_session_screen_visible(context):
|
||||
"""Verify the session management screen is visible."""
|
||||
assert context.session_screen.visible is True
|
||||
|
||||
|
||||
@then("the session management screen should not be visible")
|
||||
def step_verify_session_screen_not_visible(context):
|
||||
"""Verify the session management screen is not visible."""
|
||||
assert context.session_screen.visible is False
|
||||
|
||||
|
||||
@then("get_selected_session should return a SessionInfo object")
|
||||
def step_verify_selected_session_info(context):
|
||||
"""Verify get_selected_session returns a SessionInfo object."""
|
||||
session = context.session_screen.get_selected_session()
|
||||
assert session is not None
|
||||
assert isinstance(session, SessionInfo)
|
||||
|
||||
|
||||
@then("get_selected_session should return None")
|
||||
def step_verify_selected_session_none(context):
|
||||
"""Verify get_selected_session returns None."""
|
||||
session = context.session_screen.get_selected_session()
|
||||
assert session is None
|
||||
|
||||
|
||||
@then(
|
||||
'get_settings should return theme "{theme}", default_actor "{actor}", '
|
||||
'safety_level "{level}"'
|
||||
)
|
||||
def step_verify_get_settings(context, theme, actor, level):
|
||||
"""Verify get_settings returns the expected settings dict."""
|
||||
settings = context.settings_screen.get_settings()
|
||||
assert settings == {
|
||||
"theme": theme,
|
||||
"default_actor": actor,
|
||||
"safety_level": level,
|
||||
}
|
||||
|
||||
|
||||
@then('the session screen rendered text should contain "{text}"')
|
||||
def step_verify_session_rendered_text_contains(context, text):
|
||||
"""Verify the rendered SessionManagementScreen text contains the given fragment."""
|
||||
assert text in context.session_screen._text, (
|
||||
f"expected {text!r} in rendered text, got: {context.session_screen._text!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TuiCommandRouter /settings route scenarios
|
||||
# ---------------------------------------------------------------------------
|
||||
class _FakePersonaRegistry:
|
||||
"""Minimal persona registry stub for /settings routing tests."""
|
||||
|
||||
def list_personas(self):
|
||||
return []
|
||||
|
||||
|
||||
class _FakePersonaState:
|
||||
"""Minimal persona state stub for /settings routing tests."""
|
||||
|
||||
def get_active(self, session_id):
|
||||
return None
|
||||
|
||||
|
||||
@given("a TuiCommandRouter for settings tests")
|
||||
def step_router_for_settings(context):
|
||||
"""Create a TuiCommandRouter with minimal stubs for the /settings route."""
|
||||
context.router = TuiCommandRouter(
|
||||
persona_registry=_FakePersonaRegistry(), # type: ignore[arg-type]
|
||||
persona_state=_FakePersonaState(), # type: ignore[arg-type]
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
Feature: TUI Settings and Session Management Screens
|
||||
Implements the Settings screen and Session Management screen
|
||||
for TUI configuration and session management.
|
||||
|
||||
Background:
|
||||
Given the TUI screens module is imported
|
||||
|
||||
# ---------- SettingsScreen ----------
|
||||
|
||||
Scenario: SettingsScreen initializes with default settings
|
||||
Given a SettingsScreen widget
|
||||
Then the theme should be "dracula"
|
||||
And the default_actor should be "default"
|
||||
And the safety_level should be "medium"
|
||||
|
||||
Scenario: SettingsScreen shows and hides
|
||||
Given a SettingsScreen widget
|
||||
When I show the settings screen
|
||||
Then the settings screen should be visible
|
||||
When I hide the settings screen
|
||||
Then the settings screen should not be visible
|
||||
|
||||
Scenario: SettingsScreen navigates settings
|
||||
Given a SettingsScreen widget that is visible
|
||||
When I navigate to the next setting
|
||||
Then the selected_index should be 1
|
||||
When I navigate to the previous setting
|
||||
Then the selected_index should be 0
|
||||
|
||||
Scenario: SettingsScreen allows setting configuration
|
||||
Given a SettingsScreen widget
|
||||
When I set the theme to "light"
|
||||
And I set the default_actor to "admin"
|
||||
And I set the safety_level to "high"
|
||||
Then the theme should be "light"
|
||||
And the default_actor should be "admin"
|
||||
And the safety_level should be "high"
|
||||
|
||||
Scenario: SettingsScreen returns current settings as dict
|
||||
Given a SettingsScreen widget
|
||||
When I set the theme to "light"
|
||||
And I set the default_actor to "admin"
|
||||
And I set the safety_level to "high"
|
||||
Then get_settings should return theme "light", default_actor "admin", safety_level "high"
|
||||
|
||||
# ---------- SessionManagementScreen ----------
|
||||
|
||||
Scenario: SessionManagementScreen initializes empty
|
||||
Given a SessionManagementScreen widget
|
||||
Then the sessions list should be empty
|
||||
And the current_session_id should be "default"
|
||||
|
||||
Scenario: SessionManagementScreen shows and hides
|
||||
Given a SessionManagementScreen widget
|
||||
When I show the session management screen
|
||||
Then the session management screen should be visible
|
||||
When I hide the session management screen
|
||||
Then the session management screen should not be visible
|
||||
|
||||
Scenario: SessionManagementScreen loads and navigates sessions
|
||||
Given a SessionManagementScreen widget
|
||||
When I load 3 sessions
|
||||
Then the sessions list should have 3 items
|
||||
When I navigate to the next session
|
||||
Then the selected_index should be 1
|
||||
When I navigate to the previous session
|
||||
Then the selected_index should be 0
|
||||
|
||||
Scenario: SessionManagementScreen returns selected session
|
||||
Given a SessionManagementScreen widget with 3 sessions
|
||||
When I navigate to the next session
|
||||
Then get_selected_session should return a SessionInfo object
|
||||
|
||||
Scenario: SessionManagementScreen returns None when no sessions
|
||||
Given a SessionManagementScreen widget
|
||||
Then get_selected_session should return None
|
||||
|
||||
Scenario: SessionManagementScreen renders sessions when visible
|
||||
Given a SessionManagementScreen widget
|
||||
When I show the session management screen
|
||||
And I load 3 sessions
|
||||
Then the session screen rendered text should contain "Session 1"
|
||||
And the session screen rendered text should contain "10 msgs"
|
||||
And the session screen rendered text should contain "[CURRENT]"
|
||||
|
||||
# ---------- TuiCommandRouter /settings route ----------
|
||||
|
||||
Scenario: TuiCommandRouter routes /settings to settings command
|
||||
Given a TuiCommandRouter for settings tests
|
||||
When I call handle with raw input "settings"
|
||||
Then the handle result should be "Settings screen opened"
|
||||
@@ -86,10 +86,19 @@ class TuiCommandRouter:
|
||||
return self._persona_command(tokens[1:], session_id=session_id)
|
||||
if tokens[0] == "session":
|
||||
return self._session_command(tokens[1:], session_id=session_id)
|
||||
if tokens[0] == "settings":
|
||||
return self._settings_command(tokens[1:])
|
||||
if tokens[0] == "help":
|
||||
return self._help_command(tokens[1:])
|
||||
return f"Unknown command: /{raw}"
|
||||
|
||||
def _settings_command(self, tokens: list[str]) -> str:
|
||||
"""Handle /settings command.
|
||||
|
||||
Opens the settings screen for TUI configuration.
|
||||
"""
|
||||
return "Settings screen opened"
|
||||
|
||||
def _help_command(self, tokens: list[str]) -> str:
|
||||
"""Handle /help [command].
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""TUI screen widgets for CleverAgents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["SessionManagementScreen", "SettingsScreen"]
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Session management screen widget for managing TUI sessions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["SessionInfo", "SessionManagementScreen"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SessionInfo:
|
||||
"""Information about a single session."""
|
||||
|
||||
session_id: str
|
||||
name: str
|
||||
message_count: int
|
||||
created_at: str
|
||||
|
||||
|
||||
def _load_static_base() -> type[Any]:
|
||||
"""Load the Textual Static widget base class."""
|
||||
try:
|
||||
return importlib.import_module("textual.widgets").Static
|
||||
except Exception: # pragma: no cover
|
||||
|
||||
class _FallbackStatic:
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
self._text = ""
|
||||
|
||||
def update(self, text: str) -> None:
|
||||
self._text = text
|
||||
|
||||
return _FallbackStatic
|
||||
|
||||
|
||||
_StaticBase = _load_static_base()
|
||||
|
||||
|
||||
def _render_session_list(
|
||||
sessions: list[SessionInfo],
|
||||
selected_index: int,
|
||||
current_session_id: str,
|
||||
) -> str:
|
||||
"""Render the session list panel."""
|
||||
lines = [f"Sessions ({len(sessions)} total):"]
|
||||
for i, session in enumerate(sessions):
|
||||
prefix = "❯ " if i == selected_index else " " # noqa: RUF001
|
||||
current = " [CURRENT]" if session.session_id == current_session_id else ""
|
||||
lines.append(f"{prefix}{session.name} ({session.message_count} msgs){current}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _render_session_details(session: SessionInfo | None) -> str:
|
||||
"""Render the session details panel."""
|
||||
if session is None:
|
||||
return "(no session selected)"
|
||||
|
||||
lines = [
|
||||
f"Session: {session.name}",
|
||||
f"ID: {session.session_id}",
|
||||
f"Messages: {session.message_count}",
|
||||
f"Created: {session.created_at}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _render_status_bar() -> str:
|
||||
"""Render the status bar at the bottom of the screen."""
|
||||
return "j/k Nav │ enter Switch │ r Rename │ d Delete │ n New │ esc Close"
|
||||
|
||||
|
||||
def _render_screen(
|
||||
sessions: list[SessionInfo],
|
||||
selected_index: int,
|
||||
current_session_id: str,
|
||||
) -> str:
|
||||
"""Render the full SessionManagementScreen content as text."""
|
||||
session_list = _render_session_list(sessions, selected_index, current_session_id)
|
||||
selected_session = sessions[selected_index] if sessions else None
|
||||
session_details = _render_session_details(selected_session)
|
||||
status_bar = _render_status_bar()
|
||||
return f"Session Management\n\n{session_list}\n\n{session_details}\n\n{status_bar}"
|
||||
|
||||
|
||||
class SessionManagementScreen(_StaticBase):
|
||||
"""TUI widget for managing sessions.
|
||||
|
||||
Allows users to:
|
||||
- View all sessions
|
||||
- Rename sessions
|
||||
- Delete sessions
|
||||
- Switch between sessions
|
||||
- Create new sessions
|
||||
|
||||
Keyboard bindings:
|
||||
- ``j`` / ``k``: navigate session list (next / previous)
|
||||
- ``enter``: switch to selected session
|
||||
- ``r``: rename selected session
|
||||
- ``d``: delete selected session
|
||||
- ``n``: create new session
|
||||
- ``escape``: close session management screen
|
||||
"""
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
"""Initialize the SessionManagementScreen."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._sessions: list[SessionInfo] = []
|
||||
self._selected_index: int = 0
|
||||
self._current_session_id: str = "default"
|
||||
self._text: str = ""
|
||||
self._visible: bool = False
|
||||
self._refresh()
|
||||
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
"""Return whether the session management screen is currently visible."""
|
||||
return self._visible
|
||||
|
||||
@property
|
||||
def selected_index(self) -> int:
|
||||
"""Return the index of the currently selected session."""
|
||||
return self._selected_index
|
||||
|
||||
@property
|
||||
def current_session_id(self) -> str:
|
||||
"""Return the ID of the currently active session."""
|
||||
return self._current_session_id
|
||||
|
||||
@property
|
||||
def sessions(self) -> list[SessionInfo]:
|
||||
"""Return the list of available sessions."""
|
||||
return self._sessions.copy()
|
||||
|
||||
def show(self) -> None:
|
||||
"""Show the session management screen."""
|
||||
self._visible = True
|
||||
self._refresh()
|
||||
|
||||
def hide(self) -> None:
|
||||
"""Hide the session management screen."""
|
||||
self._visible = False
|
||||
self._text = ""
|
||||
self.update("")
|
||||
|
||||
def load_sessions(
|
||||
self, sessions: list[SessionInfo], current_session_id: str
|
||||
) -> None:
|
||||
"""Load a list of sessions and set the current session."""
|
||||
self._sessions = sessions
|
||||
self._current_session_id = current_session_id
|
||||
self._selected_index = 0
|
||||
self._refresh()
|
||||
|
||||
def navigate_next(self) -> None:
|
||||
"""Move selection to the next session (``j`` key)."""
|
||||
if self._sessions:
|
||||
self._selected_index = (self._selected_index + 1) % len(self._sessions)
|
||||
self._refresh()
|
||||
|
||||
def navigate_prev(self) -> None:
|
||||
"""Move selection to the previous session (``k`` key)."""
|
||||
if self._sessions:
|
||||
self._selected_index = (self._selected_index - 1) % len(self._sessions)
|
||||
self._refresh()
|
||||
|
||||
def get_selected_session(self) -> SessionInfo | None:
|
||||
"""Return the currently selected session, or None if no sessions."""
|
||||
if self._sessions and 0 <= self._selected_index < len(self._sessions):
|
||||
return self._sessions[self._selected_index]
|
||||
return None
|
||||
|
||||
def _refresh(self) -> None:
|
||||
"""Re-render the screen content."""
|
||||
if not self._visible:
|
||||
self._text = ""
|
||||
self.update("")
|
||||
return
|
||||
|
||||
self._text = _render_screen(
|
||||
self._sessions,
|
||||
self._selected_index,
|
||||
self._current_session_id,
|
||||
)
|
||||
self.update(self._text)
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Settings screen widget for TUI configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["SettingsScreen"]
|
||||
|
||||
|
||||
def _load_static_base() -> type[Any]:
|
||||
"""Load the Textual Static widget base class."""
|
||||
try:
|
||||
return importlib.import_module("textual.widgets").Static
|
||||
except Exception: # pragma: no cover
|
||||
|
||||
class _FallbackStatic:
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
self._text = ""
|
||||
|
||||
def update(self, text: str) -> None:
|
||||
self._text = text
|
||||
|
||||
return _FallbackStatic
|
||||
|
||||
|
||||
_StaticBase = _load_static_base()
|
||||
|
||||
|
||||
def _render_settings_screen(
|
||||
selected_index: int,
|
||||
theme: str,
|
||||
default_actor: str,
|
||||
safety_level: str,
|
||||
) -> str:
|
||||
"""Render the settings screen content."""
|
||||
settings = [
|
||||
("Theme", theme),
|
||||
("Default Actor", default_actor),
|
||||
("Safety Level", safety_level),
|
||||
]
|
||||
|
||||
lines = ["Settings", ""]
|
||||
for i, (name, value) in enumerate(settings):
|
||||
prefix = "❯ " if i == selected_index else " " # noqa: RUF001
|
||||
lines.append(f"{prefix}{name}: {value}")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"Navigation: j/k or ↑/↓ to move",
|
||||
"Edit: enter to modify selected setting",
|
||||
"Save: s to save changes",
|
||||
"Cancel: esc to discard changes",
|
||||
]
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class SettingsScreen(_StaticBase):
|
||||
"""TUI widget for displaying and editing application settings.
|
||||
|
||||
Allows users to configure:
|
||||
- Theme (color scheme)
|
||||
- Default actor
|
||||
- Safety level
|
||||
- Other TUI preferences
|
||||
|
||||
Keyboard bindings:
|
||||
- ``j`` / ``k`` or ``↑`` / ``↓``: navigate settings
|
||||
- ``enter``: edit selected setting
|
||||
- ``s``: save changes
|
||||
- ``escape``: discard changes and close
|
||||
"""
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
"""Initialize the SettingsScreen."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._selected_index: int = 0
|
||||
self._theme: str = "dracula"
|
||||
self._default_actor: str = "default"
|
||||
self._safety_level: str = "medium"
|
||||
self._text: str = ""
|
||||
self._visible: bool = False
|
||||
self._refresh()
|
||||
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
"""Return whether the settings screen is currently visible."""
|
||||
return self._visible
|
||||
|
||||
@property
|
||||
def selected_index(self) -> int:
|
||||
"""Return the index of the currently selected setting."""
|
||||
return self._selected_index
|
||||
|
||||
@property
|
||||
def theme(self) -> str:
|
||||
"""Return the current theme setting."""
|
||||
return self._theme
|
||||
|
||||
@property
|
||||
def default_actor(self) -> str:
|
||||
"""Return the current default actor setting."""
|
||||
return self._default_actor
|
||||
|
||||
@property
|
||||
def safety_level(self) -> str:
|
||||
"""Return the current safety level setting."""
|
||||
return self._safety_level
|
||||
|
||||
def show(self) -> None:
|
||||
"""Show the settings screen."""
|
||||
self._visible = True
|
||||
self._refresh()
|
||||
|
||||
def hide(self) -> None:
|
||||
"""Hide the settings screen."""
|
||||
self._visible = False
|
||||
self._text = ""
|
||||
self.update("")
|
||||
|
||||
def navigate_next(self) -> None:
|
||||
"""Move selection to the next setting (``j`` or ``↓`` key)."""
|
||||
self._selected_index = (self._selected_index + 1) % 3
|
||||
self._refresh()
|
||||
|
||||
def navigate_prev(self) -> None:
|
||||
"""Move selection to the previous setting (``k`` or ``↑`` key)."""
|
||||
self._selected_index = (self._selected_index - 1) % 3
|
||||
self._refresh()
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""Set the theme setting."""
|
||||
self._theme = theme
|
||||
self._refresh()
|
||||
|
||||
def set_default_actor(self, actor: str) -> None:
|
||||
"""Set the default actor setting."""
|
||||
self._default_actor = actor
|
||||
self._refresh()
|
||||
|
||||
def set_safety_level(self, level: str) -> None:
|
||||
"""Set the safety level setting."""
|
||||
self._safety_level = level
|
||||
self._refresh()
|
||||
|
||||
def get_settings(self) -> dict[str, str]:
|
||||
"""Return the current settings as a dictionary."""
|
||||
return {
|
||||
"theme": self._theme,
|
||||
"default_actor": self._default_actor,
|
||||
"safety_level": self._safety_level,
|
||||
}
|
||||
|
||||
def _refresh(self) -> None:
|
||||
"""Re-render the screen content."""
|
||||
if not self._visible:
|
||||
self._text = ""
|
||||
self.update("")
|
||||
return
|
||||
|
||||
self._text = _render_settings_screen(
|
||||
self._selected_index,
|
||||
self._theme,
|
||||
self._default_actor,
|
||||
self._safety_level,
|
||||
)
|
||||
self.update(self._text)
|
||||
Reference in New Issue
Block a user