diff --git a/src/hosts/core/config.py b/src/hosts/core/config.py new file mode 100644 index 0000000..aef6855 --- /dev/null +++ b/src/hosts/core/config.py @@ -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() diff --git a/src/hosts/main.py b/src/hosts/main.py index 2acd220..a31bab0 100644 --- a/src/hosts/main.py +++ b/src/hosts/main.py @@ -13,6 +13,8 @@ from rich.text import Text from .core.parser import HostsParser from .core.models import HostsFile +from .core.config import Config +from .tui.config_modal import ConfigModal class HostsManagerApp(App): @@ -84,6 +86,7 @@ class HostsManagerApp(App): Binding("h", "help", "Help"), Binding("i", "sort_by_ip", "Sort by IP"), Binding("n", "sort_by_hostname", "Sort by Hostname"), + Binding("c", "config", "Config"), ("ctrl+c", "quit", "Quit"), ] @@ -97,6 +100,7 @@ class HostsManagerApp(App): def __init__(self): super().__init__() self.parser = HostsParser() + self.config = Config() self.title = "Hosts Manager" self.sub_title = "Read-only mode" @@ -177,11 +181,18 @@ class HostsManagerApp(App): # Add columns with proper labels (Active column first) table.add_columns(active_label, ip_label, hostname_label) + # Filter entries based on configuration + show_defaults = self.config.should_show_default_entries() + # Add rows for entry in self.hosts_file.entries: # Get the canonical hostname (first hostname) 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 if entry.is_active: # Active entries in green with checkmark @@ -293,7 +304,17 @@ class HostsManagerApp(App): def action_help(self) -> None: """Show help information.""" # 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: diff --git a/src/hosts/tui/config_modal.py b/src/hosts/tui/config_modal.py new file mode 100644 index 0000000..d7f2245 --- /dev/null +++ b/src/hosts/tui/config_modal.py @@ -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)