Implement configuration management with modal for application settings

This commit is contained in:
Philip Henning 2025-07-29 17:25:55 +02:00
parent a399b04c99
commit 79069bc2ea
3 changed files with 219 additions and 1 deletions

89
src/hosts/core/config.py Normal file
View file

@ -0,0 +1,89 @@
"""
Configuration management for the hosts TUI application.
This module handles application settings and preferences.
"""
import json
import os
from pathlib import Path
from typing import Dict, Any
class Config:
"""
Configuration manager for the hosts application.
Handles loading, saving, and managing application settings.
"""
def __init__(self):
self.config_dir = Path.home() / ".config" / "hosts-manager"
self.config_file = self.config_dir / "config.json"
self._settings = self._load_default_settings()
self.load()
def _load_default_settings(self) -> Dict[str, Any]:
"""Load default configuration settings."""
return {
"show_default_entries": False, # Hide default entries by default
"default_entries": [
{"ip": "127.0.0.1", "hostname": "localhost"},
{"ip": "255.255.255.255", "hostname": "broadcasthost"},
{"ip": "::1", "hostname": "localhost"},
],
"window_settings": {
"last_sort_column": "",
"last_sort_ascending": True,
}
}
def load(self) -> None:
"""Load configuration from file."""
try:
if self.config_file.exists():
with open(self.config_file, 'r') as f:
loaded_settings = json.load(f)
# Merge with defaults to ensure all keys exist
self._settings.update(loaded_settings)
except (json.JSONDecodeError, IOError) as e:
# If loading fails, use defaults
pass
def save(self) -> None:
"""Save configuration to file."""
try:
# Ensure config directory exists
self.config_dir.mkdir(parents=True, exist_ok=True)
with open(self.config_file, 'w') as f:
json.dump(self._settings, f, indent=2)
except IOError as e:
# Silently fail if we can't save config
pass
def get(self, key: str, default: Any = None) -> Any:
"""Get a configuration value."""
return self._settings.get(key, default)
def set(self, key: str, value: Any) -> None:
"""Set a configuration value."""
self._settings[key] = value
def is_default_entry(self, ip_address: str, hostname: str) -> bool:
"""Check if an entry is a default system entry."""
default_entries = self.get("default_entries", [])
for entry in default_entries:
if entry["ip"] == ip_address and entry["hostname"] == hostname:
return True
return False
def should_show_default_entries(self) -> bool:
"""Check if default entries should be shown."""
return self.get("show_default_entries", False)
def toggle_show_default_entries(self) -> None:
"""Toggle the show default entries setting."""
current = self.get("show_default_entries", False)
self.set("show_default_entries", not current)
self.save()

View file

@ -13,6 +13,8 @@ from rich.text import Text
from .core.parser import HostsParser from .core.parser import HostsParser
from .core.models import HostsFile from .core.models import HostsFile
from .core.config import Config
from .tui.config_modal import ConfigModal
class HostsManagerApp(App): class HostsManagerApp(App):
@ -84,6 +86,7 @@ class HostsManagerApp(App):
Binding("h", "help", "Help"), Binding("h", "help", "Help"),
Binding("i", "sort_by_ip", "Sort by IP"), Binding("i", "sort_by_ip", "Sort by IP"),
Binding("n", "sort_by_hostname", "Sort by Hostname"), Binding("n", "sort_by_hostname", "Sort by Hostname"),
Binding("c", "config", "Config"),
("ctrl+c", "quit", "Quit"), ("ctrl+c", "quit", "Quit"),
] ]
@ -97,6 +100,7 @@ class HostsManagerApp(App):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.parser = HostsParser() self.parser = HostsParser()
self.config = Config()
self.title = "Hosts Manager" self.title = "Hosts Manager"
self.sub_title = "Read-only mode" self.sub_title = "Read-only mode"
@ -177,11 +181,18 @@ class HostsManagerApp(App):
# Add columns with proper labels (Active column first) # Add columns with proper labels (Active column first)
table.add_columns(active_label, ip_label, hostname_label) table.add_columns(active_label, ip_label, hostname_label)
# Filter entries based on configuration
show_defaults = self.config.should_show_default_entries()
# Add rows # Add rows
for entry in self.hosts_file.entries: for entry in self.hosts_file.entries:
# Get the canonical hostname (first hostname) # Get the canonical hostname (first hostname)
canonical_hostname = entry.hostnames[0] if entry.hostnames else "" canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
# Skip default entries if configured to hide them
if not show_defaults and self.config.is_default_entry(entry.ip_address, canonical_hostname):
continue
# Add row with styling based on active status # Add row with styling based on active status
if entry.is_active: if entry.is_active:
# Active entries in green with checkmark # Active entries in green with checkmark
@ -293,7 +304,17 @@ class HostsManagerApp(App):
def action_help(self) -> None: def action_help(self) -> None:
"""Show help information.""" """Show help information."""
# For now, just update the status with help info # For now, just update the status with help info
self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help") self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config")
def action_config(self) -> None:
"""Show configuration modal."""
def handle_config_result(config_changed: bool) -> None:
if config_changed:
# Reload the table to apply new filtering
self.populate_entries_table()
self.update_status("Configuration saved")
self.push_screen(ConfigModal(self.config), handle_config_result)
def action_sort_by_ip(self) -> None: def action_sort_by_ip(self) -> None:

View file

@ -0,0 +1,108 @@
"""
Configuration modal window for the hosts TUI application.
This module provides a floating configuration window for managing application settings.
"""
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Static, Button, Checkbox, Label
from textual.screen import ModalScreen
from textual.binding import Binding
from ..core.config import Config
class ConfigModal(ModalScreen):
"""
Modal screen for application configuration.
Provides a floating window with configuration options.
"""
CSS = """
ConfigModal {
align: center middle;
}
.config-container {
width: 80;
height: 20;
background: $surface;
border: thick $primary;
padding: 1;
}
.config-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.config-section {
margin: 1 0;
}
.config-option {
margin: 0 2;
}
.button-row {
margin-top: 2;
align: center middle;
}
.config-button {
margin: 0 1;
min-width: 10;
}
"""
BINDINGS = [
Binding("escape", "cancel", "Cancel"),
Binding("enter", "save", "Save"),
]
def __init__(self, config: Config):
super().__init__()
self.config = config
def compose(self) -> ComposeResult:
"""Create the configuration modal layout."""
with Vertical(classes="config-container"):
yield Static("Configuration", classes="config-title")
with Vertical(classes="config-section"):
yield Label("Display Options:")
yield Checkbox(
"Show default system entries (localhost, broadcasthost)",
value=self.config.should_show_default_entries(),
id="show-defaults-checkbox",
classes="config-option"
)
with Horizontal(classes="button-row"):
yield Button("Save", variant="primary", id="save-button", classes="config-button")
yield Button("Cancel", variant="default", id="cancel-button", classes="config-button")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "save-button":
self.action_save()
elif event.button.id == "cancel-button":
self.action_cancel()
def action_save(self) -> None:
"""Save configuration and close modal."""
# Get checkbox state
checkbox = self.query_one("#show-defaults-checkbox", Checkbox)
self.config.set("show_default_entries", checkbox.value)
self.config.save()
# Close modal and signal that config was changed
self.dismiss(True)
def action_cancel(self) -> None:
"""Cancel configuration changes and close modal."""
self.dismiss(False)