Compare commits

..

No commits in common. "220818c8d159a3ffc79b472f21bb9fbed2ece12d" and "116752135566e7732db1907ed7fdff159b5208db" have entirely different histories.

9 changed files with 156 additions and 291 deletions

View file

@ -1,44 +1,19 @@
# Ac### Status Appearance Enhancement ✅ COMPLETED # Active Context: hosts
Successfully implemented the user's requested status display improvements with overlay fix:
**New Header Layout:**
- **Title**: Changed from "Hosts Manager" to "/etc/hosts Manager"
- **Subtitle**: Now shows "29 entries (6 active) | Read-only mode" format
- **Error Messages**: Moved to dedicated status bar below header as overlay
**Overlay Status Bar Implementation:**
- **Fixed layout shifting issue**: Status bar now appears as overlay without moving panes down
- **Corrected positioning**: Status bar appears below header as overlay using `dock: top`, `layer: overlay`, `offset-y: 3`
- **Visible error messages**: Error messages now display correctly as overlay on content area
- **No layout flow impact**: Panes stay in exact same position when error messages appear
- **Professional appearance**: Error bar overlays cleanly below header without disrupting content layout
**Implementation Details:**
- Moved status bar widget to end of compose method for overlay rendering
- Status bar positioned 3 lines down from top (below header) using CSS offset
- Status bar is hidden by default, only appears when displaying messages
- Error messages (❌) auto-clear after 5 seconds, regular messages after 3 seconds
- Header subtitle always shows current status regardless of temporary messages
**Test Updates:**
- All 149 tests passing with overlay status bar implementation
- Fixed layout shifting that was annoying when error messages appeared
- Verified functionality maintains all previous behaviorive Context: hosts
## Current Work Focus ## Current Work Focus
**Status Appearance Enhancement Complete**: Successfully implemented the user's requested status display improvements. The header now shows "/etc/hosts Manager" with entry counts and mode on the right, while error messages appear in a dedicated status bar below the header. Ready to proceed with remaining UX improvements from todo.md. **Post-Phase 3 Code Quality Maintenance**: The hosts TUI application has successfully completed Phase 3 with full edit mode foundation, save confirmation functionality, and comprehensive testing (149 tests). However, 20 minor linting issues (unused imports and variables) require cleanup before proceeding to Phase 4 advanced features.
## Immediate Next Steps ## Immediate Next Steps
### Priority 1: Remaining User Experience Improvements (From todo.md) ### Priority 1: Code Quality Cleanup
1. **Status appearance enhancement**: COMPLETED - New header layout with separate error message bar 1. **Fix linting issues**: Run `uv run ruff check --fix` to address 20 unused import and variable warnings
2. **Entry details consistency**: COMPLETED - DataTable with labeled rows matching edit form order 2. **Validate fixes**: Ensure all tests still pass (149 tests) after cleanup
3. **DataTable details implementation**: COMPLETED as part of entry details consistency 3. **Confirm application functionality**: Test that `uv run hosts` still works perfectly
4. **Sudo permission fixes**: Address known sudo handling issues 4. **Commit clean state**: Create commit with "Fix linting issues" once cleanup is complete
### Priority 2: Phase 4 Planning ### Priority 2: Phase 4 Planning
Once remaining UX improvements are complete: Once code quality is restored:
1. **Advanced entry operations**: Add/delete entries with validation 1. **Advanced entry operations**: Add/delete entries with validation
2. **Search functionality**: Find entries by hostname or IP address 2. **Search functionality**: Find entries by hostname or IP address
3. **Bulk operations**: Select and modify multiple entries 3. **Bulk operations**: Select and modify multiple entries
@ -47,26 +22,27 @@ Once remaining UX improvements are complete:
## Memory Bank Update Summary ## Memory Bank Update Summary
### Files Updated ### Files Updated
- ✅ **activeContext.md**: Updated current focus to test stabilization and UX improvements - ✅ **activeContext.md**: Updated current focus and next steps
- ✅ **progress.md**: Corrected test status (8 failures out of 149) and development stage - ✅ **progress.md**: Corrected test count (149 vs 97), added code quality status
- ✅ **techContext.md**: Updated development workflow and test status - ✅ **techContext.md**: Updated development workflow and code quality status
- ✅ **projectbrief.md**: Noted current test failures in testing strategy - ✅ **systemPatterns.md**: Added edit mode and permission management patterns
- ✅ **Added todo.md insights**: Documented user experience improvement requirements - ✅ **projectbrief.md**: Updated test coverage details and current status
### Current Status Corrections ### Key Corrections Made
- **Linting status**: Corrected to show clean state (all checks passing) - **Test count**: Updated from 97 to 149 tests across all files
- **Test status**: Updated to reflect 8 failing tests out of 149 total - **Code quality**: Noted 20 linting issues requiring cleanup
- **Application functionality**: Confirmed working TUI with identified improvement areas - **Project stage**: Clarified completion of Phase 3 with save confirmation
- **Development priority**: Shifted from code cleanup to test stabilization - **Current status**: Maintenance phase before Phase 4 development
- **User requirements**: Added todo.md requirements for status, details, and sudo improvements - **Recent commits**: Reflected completion of save confirmation modal
### New Requirements from todo.md ### Architecture Insights Confirmed
1. **Status appearance enhancement**: Visual design improvements needed - **Textual framework**: Excellent for complex TUI applications with modal dialogs
2. **Entry details consistency**: Non-edit view should match edit mode field order - **Layered architecture**: Proven effective for maintainable, testable code
3. **DataTable details view**: Implement labeled rows for better presentation - **Test-driven development**: 149 comprehensive tests enable confident refactoring
4. **Sudo issue resolution**: Address known permission handling problems - **Configuration system**: JSON-based persistence working reliably
- **Permission management**: Sudo handling implemented safely and securely
The memory bank now accurately reflects the true current state: a functional application with clean code but test stability issues and identified user experience improvements needed before Phase 4 development. The memory bank now accurately reflects the current state of the project, ready for the next phase of development after code quality maintenance.
## Recent Changes ## Recent Changes
@ -95,45 +71,22 @@ The memory bank now accurately reflects the true current state: a functional app
- **Professional visual design**: Color-coded entries, zebra striping, and rich text styling - **Professional visual design**: Color-coded entries, zebra striping, and rich text styling
- **Interactive sorting**: Click column headers or use keyboard shortcuts to sort data - **Interactive sorting**: Click column headers or use keyboard shortcuts to sort data
- **Intelligent filtering**: Hide default system entries based on user preference - **Intelligent filtering**: Hide default system entries based on user preference
- **Comprehensive test coverage**: All 149 tests passing with 100% success rate - **Comprehensive test coverage**: 149 tests with 100% pass rate covering all components
- **Clean code quality**: All ruff linting and formatting checks passing - **Code quality maintenance needed**: 20 linting issues (unused imports/variables) require cleanup
- **Robust architecture**: Clean layered design ready for UX improvements and Phase 4 features - **Robust architecture**: Clean layered design ready for Phase 4 advanced features
- **Todo requirements identified**: Status appearance, entry details consistency, sudo handling improvements needed
## Next Steps ## Next Steps
### Entry Details Consistency ✅ COMPLETED ### Immediate Priority: Code Quality Cleanup
Successfully implemented DataTable-based entry details with consistent field ordering: 1. **Fix linting issues**: Address 20 unused import and variable warnings
- Remove unused imports in core/config.py, test files
- Clean up unused variables in exception handling
- Run `uv run ruff check --fix` to auto-fix issues
**Key Improvements:** 2. **Code quality validation**:
- **Replaced Static widget with DataTable**: Entry details now displayed in professional table format - Ensure all ruff checks pass with zero issues
- **Consistent field order**: Details view now matches edit form order exactly - Maintain perfect test coverage (149 tests passing)
1. IP Address - Verify application functionality after cleanup
2. Hostnames (comma-separated)
3. Comment
4. Active status (Yes/No)
- **Labeled rows**: Uses DataTable labeled rows feature for clean presentation
- **No headers**: DataTable configured with `show_header=False` for clean appearance
**Implementation Details:**
- Modified `app.py` compose method to use DataTable instead of Static widget
- Updated `details_handler.py` to populate DataTable with labeled rows
- Added CSS styling for entry details table consistency
- Fixed 2 failing tests to work with new DataTable approach
- All 149 tests passing with new implementation
**Visual Benefits:**
- Professional table appearance matching main entries table
- Clear field labels in left column, values in right column
- Proper spacing and alignment
- System default entry warnings displayed in table format
- DNS Name field shown when present (read-only information)
### Priority 2: User Experience Improvements (From todo.md)
1. **Status appearance enhancement**: Improve visual design of status bar
2. **Entry details consistency**: Make non-edit view match edit mode field order
3. **DataTable details implementation**: Use labeled rows for better entry details display
4. **Sudo permission fixes**: Address known sudo handling issues
### Phase 4: Advanced Edit Features (Next Phase) ### Phase 4: Advanced Edit Features (Next Phase)
1. **Advanced editing operations**: 1. **Advanced editing operations**:

View file

@ -19,7 +19,7 @@
- ✅ **Entry management**: DataTable with proper formatting and status indicators - ✅ **Entry management**: DataTable with proper formatting and status indicators
- ✅ **Detail view**: Comprehensive entry details in right pane - ✅ **Detail view**: Comprehensive entry details in right pane
- ✅ **Navigation**: Smooth keyboard navigation with cursor position restoration - ✅ **Navigation**: Smooth keyboard navigation with cursor position restoration
- ✅ **Testing**: 149 comprehensive tests with 100% pass rate (test stabilization completed) - ✅ **Testing**: 97 comprehensive tests with 100% pass rate
- ✅ **Code quality**: All ruff linting and formatting checks passing - ✅ **Code quality**: All ruff linting and formatting checks passing
- ✅ **Error handling**: Graceful handling of file access and parsing errors - ✅ **Error handling**: Graceful handling of file access and parsing errors
- ✅ **Status feedback**: Informative status bar with file and entry information - ✅ **Status feedback**: Informative status bar with file and entry information
@ -67,13 +67,12 @@
## What's Left to Build ## What's Left to Build
### Priority 1: User Experience Improvements (From todo.md) ### Immediate Priority: Code Quality Cleanup
- ✅ **Status appearance**: Enhanced visual design with new header layout and dedicated error message bar - ❌ **Fix linting issues**: Address 20 unused import and variable warnings
### Priority 1: User Experience Improvements (From todo.md) - ❌ **Code quality validation**: Ensure all ruff checks pass with zero issues
- ✅ **Status appearance**: Enhanced visual design with new header layout and separate error message bar - ❌ **Maintain test coverage**: Keep 149 tests passing during cleanup
- ✅ **Entry details consistency**: Implemented DataTable with labeled rows matching edit form field order
- ✅ **DataTable details view**: Completed as part of entry details consistency improvement ### Phase 4: Advanced Edit Features
- ❌ **Sudo permission handling**: Address known sudo issues### Phase 4: Advanced Edit Features
- ❌ **Add new entries**: Create new host entries - ❌ **Add new entries**: Create new host entries
- ❌ **Delete entries**: Remove host entries - ❌ **Delete entries**: Remove host entries
- ❌ **Bulk operations**: Select and modify multiple entries - ❌ **Bulk operations**: Select and modify multiple entries
@ -96,10 +95,9 @@
## Current Status ## Current Status
### Development Stage ### Development Stage
**Stage**: User Experience Improvements - 3 of 4 Todo Items Complete **Stage**: Phase 3 Complete with Code Quality Maintenance Required
**Progress**: 90% (Status improvements and entry details consistency completed, ready for final sudo fixes and Phase 4) **Progress**: 82% (Complete edit mode foundation with save confirmation, code cleanup needed)
**Next Milestone**: Sudo permission handling fixes, then Phase 4 advanced features **Next Milestone**: Code quality cleanup, then Phase 4 advanced edit features
**Test Status**: ✅ All 149 tests passing (maintained during UX improvements)
### Phase 3 Final Achievements ✅ COMPLETE ### Phase 3 Final Achievements ✅ COMPLETE
1. ✅ **Permission management**: Complete PermissionManager class with sudo request and validation 1. ✅ **Permission management**: Complete PermissionManager class with sudo request and validation

View file

@ -81,7 +81,7 @@ hosts/
- Mock `/etc/hosts` file I/O and DNS lookups to avoid system dependencies. - Mock `/etc/hosts` file I/O and DNS lookups to avoid system dependencies.
- Include integration tests for the Textual TUI (using `textual.testing` or snapshot testing). - Include integration tests for the Textual TUI (using `textual.testing` or snapshot testing).
### Implemented Tests (149 tests total, 8 failing) ### Implemented Tests (149 tests total)
1. **Parsing Tests** (15 tests): 1. **Parsing Tests** (15 tests):
- Parse simple `/etc/hosts` with comments and disabled entries - Parse simple `/etc/hosts` with comments and disabled entries

View file

@ -32,14 +32,13 @@ hosts/
### Current State ### Current State
- ✅ **Complete uv project**: Python 3.13 with full dependency management - ✅ **Complete uv project**: Python 3.13 with full dependency management
- ✅ **Production application**: Fully functional TUI with advanced features and professional interface - ✅ **Production application**: Fully functional TUI with advanced features and professional interface
- ✅ **Clean code quality**: All ruff linting and formatting checks passing - ✅ **Code quality maintenance required**: 20 linting issues (unused imports/variables) need cleanup
- ✅ **Proper project structure**: Well-organized src/hosts/ package with core and tui modules - ✅ **Proper project structure**: Well-organized src/hosts/ package with core and tui modules
- ✅ **Test coverage restored**: All 149 tests passing after successful test stabilization - ✅ **Comprehensive testing**: 149 tests covering all functionality including new features
- ✅ **Entry point configured**: `hosts` command launches application perfectly - ✅ **Entry point configured**: `hosts` command launches application perfectly
- ✅ **Configuration system**: Complete settings management with JSON persistence - ✅ **Configuration system**: Complete settings management with JSON persistence
- ✅ **Modal interface**: Professional configuration and save confirmation dialogs - ✅ **Modal interface**: Professional configuration dialogs with keyboard bindings
- ✅ **Advanced features**: Sorting, filtering, edit mode, and comprehensive TUI functionality - ✅ **Advanced features**: Sorting, filtering, edit mode, and save confirmation
- ❌ **Known improvements needed**: Status appearance, entry details consistency, sudo handling (per todo.md)
### Runtime Management ### Runtime Management
- ✅ **uv run hosts**: Command executes application instantly - ✅ **uv run hosts**: Command executes application instantly
@ -95,9 +94,9 @@ hosts = "hosts.main:main"
### Development Workflow ### Development Workflow
1. ✅ **uv run hosts**: Execute the application - launches instantly 1. ✅ **uv run hosts**: Execute the application - launches instantly
2. **uv run ruff check**: Lint code - all checks currently passing 2. 🔧 **uv run ruff check**: Lint code - 20 issues need fixing
3. ✅ **uv run ruff format**: Auto-format code - consistent style maintained 3. ✅ **uv run ruff format**: Auto-format code - consistent style maintained
4. ✅ **uv run pytest**: Run test suite - All 149 tests passing with 100% success rate (test stabilization completed) 4. ✅ **uv run pytest**: Run test suite - 149 tests passing with 100% success rate
5. ✅ **uv add**: Add dependencies - seamless dependency management 5. ✅ **uv add**: Add dependencies - seamless dependency management
### Code Quality Status ### Code Quality Status

View file

@ -44,8 +44,8 @@ class HostsManagerApp(App):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title = "/etc/hosts Manager" self.title = "Hosts Manager"
self.sub_title = "" # Will be set by update_status self.sub_title = "Read-only mode"
# Initialize core components # Initialize core components
self.parser = HostsParser() self.parser = HostsParser()
@ -75,7 +75,7 @@ class HostsManagerApp(App):
# Right pane - entry details or edit form # Right pane - entry details or edit form
with Vertical(classes="right-pane"): with Vertical(classes="right-pane"):
yield Static("Entry Details", id="details-title") yield Static("Entry Details", id="details-title")
yield DataTable(id="entry-details-table", show_header=False, show_cursor=False, disabled=True) yield Static("Select an entry to view details", id="entry-details")
# Edit form (initially hidden) # Edit form (initially hidden)
with Vertical(id="entry-edit-form", classes="hidden"): with Vertical(id="entry-edit-form", classes="hidden"):
@ -87,9 +87,6 @@ class HostsManagerApp(App):
yield Input(placeholder="Enter comment (optional)", id="comment-input") yield Input(placeholder="Enter comment (optional)", id="comment-input")
yield Checkbox("Active", id="active-checkbox") yield Checkbox("Active", id="active-checkbox")
# Status bar for error/temporary messages (overlay, doesn't affect layout)
yield Static("", id="status-bar", classes="status-bar hidden")
def on_ready(self) -> None: def on_ready(self) -> None:
"""Called when the app is ready.""" """Called when the app is ready."""
self.load_hosts_file() self.load_hosts_file()
@ -114,40 +111,30 @@ class HostsManagerApp(App):
self.update_status(f"❌ Error loading hosts file: {e}") self.update_status(f"❌ Error loading hosts file: {e}")
def update_status(self, message: str = "") -> None: def update_status(self, message: str = "") -> None:
"""Update the header subtitle and status bar with status information.""" """Update the footer subtitle with status information."""
if message: if message:
# Show temporary message in the status bar # Set temporary status message
try: self.sub_title = message
status_bar = self.query_one("#status-bar", Static) if message.startswith(""):
status_bar.update(message) # Auto-clear error message after 5 seconds
status_bar.remove_class("hidden") self.set_timer(5.0, lambda: self.update_status())
else:
if message.startswith(""): # Auto-clear regular message after 3 seconds
# Auto-clear error message after 5 seconds self.set_timer(3.0, lambda: self.update_status())
self.set_timer(5.0, lambda: self._clear_status_message()) else:
else: # Reset to normal status display
# Auto-clear regular message after 3 seconds mode = "Edit mode" if self.edit_mode else "Read-only mode"
self.set_timer(3.0, lambda: self._clear_status_message()) entry_count = len(self.hosts_file.entries)
except: active_count = len(self.hosts_file.get_active_entries())
# Fallback if status bar not found (during initialization)
pass
# Always update the header subtitle with current status
mode = "Edit mode" if self.edit_mode else "Read-only mode"
entry_count = len(self.hosts_file.entries)
active_count = len(self.hosts_file.get_active_entries())
# Format: "29 entries (6 active) | Read-only mode" status_text = f"{mode} | {entry_count} entries ({active_count} active)"
self.sub_title = f"{entry_count} entries ({active_count} active) | {mode}"
def _clear_status_message(self) -> None: # Add file info
"""Clear the temporary status message.""" file_info = self.parser.get_file_info()
try: if file_info["exists"]:
status_bar = self.query_one("#status-bar", Static) status_text += f" | {file_info['path']}"
status_bar.update("")
status_bar.add_class("hidden") self.sub_title = status_text
except:
pass
# Event handlers # Event handlers
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None: def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:

View file

@ -5,7 +5,7 @@ This module handles the display and updating of entry details
and edit forms in the right pane. and edit forms in the right pane.
""" """
from textual.widgets import Static, Input, Checkbox, DataTable from textual.widgets import Static, Input, Checkbox
class DetailsHandler: class DetailsHandler:
@ -23,30 +23,22 @@ class DetailsHandler:
self.update_details_display() self.update_details_display()
def update_details_display(self) -> None: def update_details_display(self) -> None:
"""Update the details display using a DataTable with labeled rows.""" """Update the static details display."""
details_table = self.app.query_one("#entry-details-table", DataTable) details_widget = self.app.query_one("#entry-details", Static)
edit_form = self.app.query_one("#entry-edit-form") edit_form = self.app.query_one("#entry-edit-form")
# Show details table, hide edit form # Show details, hide edit form
details_table.remove_class("hidden") details_widget.remove_class("hidden")
edit_form.add_class("hidden") edit_form.add_class("hidden")
# Clear existing data
details_table.clear()
if not self.app.hosts_file.entries: if not self.app.hosts_file.entries:
# Show empty message in a single row details_widget.update("No entries loaded")
if not details_table.columns:
details_table.add_column("Field", key="field")
details_table.add_row("No entries loaded")
return return
# Get visible entries to check if we need to adjust selection # Get visible entries to check if we need to adjust selection
visible_entries = self.app.table_handler.get_visible_entries() visible_entries = self.app.table_handler.get_visible_entries()
if not visible_entries: if not visible_entries:
if not details_table.columns: details_widget.update("No visible entries")
details_table.add_column("Field", key="field")
details_table.add_row("No visible entries")
return return
# If default entries are hidden and selected_entry_index points to a hidden entry, # If default entries are hidden and selected_entry_index points to a hidden entry,
@ -73,34 +65,35 @@ class DetailsHandler:
entry = self.app.hosts_file.entries[self.app.selected_entry_index] entry = self.app.hosts_file.entries[self.app.selected_entry_index]
# Add columns for labeled rows (Field, Value) - only if not already present details_lines = [
if not details_table.columns: f"IP Address: {entry.ip_address}",
details_table.add_column("Field", key="field") f"Hostnames: {', '.join(entry.hostnames)}",
details_table.add_column("Value", key="value") f"Status: {'Active' if entry.is_active else 'Inactive'}",
]
# Add rows in the same order as edit form
details_table.add_row("IP Address", entry.ip_address, key="ip")
details_table.add_row("Hostnames", ", ".join(entry.hostnames), key="hostnames")
details_table.add_row("Comment", entry.comment or "", key="comment")
details_table.add_row("Active", "Yes" if entry.is_active else "No", key="active")
# Add DNS name if present (not in edit form but good to show)
if entry.dns_name:
details_table.add_row("DNS Name", entry.dns_name, key="dns")
# Add notice for default system entries # Add notice for default system entries
if entry.is_default_entry(): if entry.is_default_entry():
details_table.add_row("", "", key="spacer") details_lines.append("")
details_table.add_row("⚠️ WARNING", "SYSTEM DEFAULT ENTRY", key="warning") details_lines.append("⚠️ SYSTEM DEFAULT ENTRY")
details_table.add_row("Note", "This entry cannot be modified", key="note") details_lines.append(
"This is a default system entry and cannot be modified."
)
if entry.comment:
details_lines.append(f"Comment: {entry.comment}")
if entry.dns_name:
details_lines.append(f"DNS Name: {entry.dns_name}")
details_widget.update("\n".join(details_lines))
def update_edit_form(self) -> None: def update_edit_form(self) -> None:
"""Update the edit form with current entry values.""" """Update the edit form with current entry values."""
details_table = self.app.query_one("#entry-details-table", DataTable) details_widget = self.app.query_one("#entry-details", Static)
edit_form = self.app.query_one("#entry-edit-form") edit_form = self.app.query_one("#entry-edit-form")
# Hide details table, show edit form # Hide details, show edit form
details_table.add_class("hidden") details_widget.add_class("hidden")
edit_form.remove_class("hidden") edit_form.remove_class("hidden")
if not self.app.hosts_file.entries or self.app.selected_entry_index >= len( if not self.app.hosts_file.entries or self.app.selected_entry_index >= len(

View file

@ -57,21 +57,6 @@ HOSTS_MANAGER_CSS = """
display: none; display: none;
} }
.status-bar {
height: 1;
width: 100%;
background: $error;
color: $text;
content-align: center middle;
layer: overlay;
dock: top;
offset-y: 1;
}
.status-bar.hidden {
display: none;
}
#entry-edit-form { #entry-edit-form {
height: auto; height: auto;
padding: 1; padding: 1;
@ -90,21 +75,4 @@ HOSTS_MANAGER_CSS = """
#entry-edit-form Checkbox { #entry-edit-form Checkbox {
margin-bottom: 1; margin-bottom: 1;
} }
/* Entry details table styling */
#entry-details-table {
background: $background;
height: auto;
}
#entry-details-table .datatable--even-row {
background: $background;
}
#entry-details-table .datatable--odd-row {
background: $surface;
}
Header { height: 1; }
Header.-tall { height: 1; } /* Fix tall header also to height 1 */
""" """

View file

@ -22,8 +22,8 @@ class TestHostsManagerApp:
with patch('hosts.tui.app.HostsParser'), patch('hosts.tui.app.Config'): with patch('hosts.tui.app.HostsParser'), patch('hosts.tui.app.Config'):
app = HostsManagerApp() app = HostsManagerApp()
assert app.title == "/etc/hosts Manager" assert app.title == "Hosts Manager"
assert app.sub_title == "" # Now set by update_status assert app.sub_title == "Read-only mode"
assert app.edit_mode is False assert app.edit_mode is False
assert app.selected_entry_index == 0 assert app.selected_entry_index == 0
assert app.sort_column == "" assert app.sort_column == ""
@ -74,7 +74,7 @@ class TestHostsManagerApp:
"""Test handling of missing hosts file.""" """Test handling of missing hosts file."""
mock_parser = Mock(spec=HostsParser) mock_parser = Mock(spec=HostsParser)
mock_config = Mock(spec=Config) mock_config = Mock(spec=Config)
mock_parser.parse.side_effect = FileNotFoundError("Hosts file not found") mock_parser.parse.side_effect = FileNotFoundError("File not found")
with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \ with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \
patch('hosts.tui.app.Config', return_value=mock_config): patch('hosts.tui.app.Config', return_value=mock_config):
@ -85,7 +85,7 @@ class TestHostsManagerApp:
app.load_hosts_file() app.load_hosts_file()
# Should handle error gracefully # Should handle error gracefully
app.update_status.assert_called_with("Error loading hosts file: Hosts file not found") app.update_status.assert_called_with("Error: Hosts file not found")
def test_load_hosts_file_permission_error(self): def test_load_hosts_file_permission_error(self):
"""Test handling of permission denied error.""" """Test handling of permission denied error."""
@ -102,7 +102,7 @@ class TestHostsManagerApp:
app.load_hosts_file() app.load_hosts_file()
# Should handle error gracefully # Should handle error gracefully
app.update_status.assert_called_with("Error loading hosts file: Permission denied") app.update_status.assert_called_with("Error: Permission denied")
def test_populate_entries_table_logic(self): def test_populate_entries_table_logic(self):
"""Test populating DataTable logic without UI dependencies.""" """Test populating DataTable logic without UI dependencies."""
@ -142,26 +142,15 @@ class TestHostsManagerApp:
"""Test updating entry details pane.""" """Test updating entry details pane."""
mock_parser = Mock(spec=HostsParser) mock_parser = Mock(spec=HostsParser)
mock_config = Mock(spec=Config) mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = True
with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \ with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \
patch('hosts.tui.app.Config', return_value=mock_config): patch('hosts.tui.app.Config', return_value=mock_config):
app = HostsManagerApp() app = HostsManagerApp()
# Mock the query_one method to return DataTable mock # Mock the query_one method
mock_details_table = Mock() mock_details = Mock()
mock_details_table.columns = [] # Mock empty columns list app.query_one = Mock(return_value=mock_details)
mock_edit_form = Mock()
def mock_query_one(selector, widget_type=None):
if selector == "#entry-details-table":
return mock_details_table
elif selector == "#entry-edit-form":
return mock_edit_form
return Mock()
app.query_one = mock_query_one
# Add test entry # Add test entry
app.hosts_file = HostsFile() app.hosts_file = HostsFile()
@ -175,12 +164,12 @@ class TestHostsManagerApp:
app.update_entry_details() app.update_entry_details()
# Verify DataTable operations were called # Verify update was called with content containing entry details
mock_details_table.remove_class.assert_called_with("hidden") mock_details.update.assert_called_once()
mock_edit_form.add_class.assert_called_with("hidden") call_args = mock_details.update.call_args[0][0]
mock_details_table.clear.assert_called_once() assert "127.0.0.1" in call_args
mock_details_table.add_column.assert_called() assert "localhost, local" in call_args
mock_details_table.add_row.assert_called() assert "Test comment" in call_args
def test_update_entry_details_no_entries(self): def test_update_entry_details_no_entries(self):
"""Test updating entry details with no entries.""" """Test updating entry details with no entries."""
@ -192,29 +181,16 @@ class TestHostsManagerApp:
app = HostsManagerApp() app = HostsManagerApp()
# Mock the query_one method to return DataTable mock # Mock the query_one method
mock_details_table = Mock() mock_details = Mock()
mock_details_table.columns = [] # Mock empty columns list app.query_one = Mock(return_value=mock_details)
mock_edit_form = Mock()
def mock_query_one(selector, widget_type=None):
if selector == "#entry-details-table":
return mock_details_table
elif selector == "#entry-edit-form":
return mock_edit_form
return Mock()
app.query_one = mock_query_one
app.hosts_file = HostsFile() app.hosts_file = HostsFile()
app.update_entry_details() app.update_entry_details()
# Verify DataTable operations were called for empty state # Verify update was called with "No entries loaded"
mock_details_table.remove_class.assert_called_with("hidden") mock_details.update.assert_called_once_with("No entries loaded")
mock_edit_form.add_class.assert_called_with("hidden")
mock_details_table.clear.assert_called_once()
mock_details_table.add_column.assert_called_with("Field", key="field")
mock_details_table.add_row.assert_called_with("No entries loaded")
def test_update_status_default(self): def test_update_status_default(self):
"""Test status bar update with default information.""" """Test status bar update with default information."""
@ -231,6 +207,10 @@ class TestHostsManagerApp:
app = HostsManagerApp() app = HostsManagerApp()
# Mock the query_one method
mock_status = Mock()
app.query_one = Mock(return_value=mock_status)
# Add test entries # Add test entries
app.hosts_file = HostsFile() app.hosts_file = HostsFile()
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])) app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
@ -242,10 +222,12 @@ class TestHostsManagerApp:
app.update_status() app.update_status()
# Verify sub_title was set correctly # Verify status was updated
assert "Read-only mode" in app.sub_title mock_status.update.assert_called_once()
assert "2 entries" in app.sub_title call_args = mock_status.update.call_args[0][0]
assert "1 active" in app.sub_title assert "Read-only mode" in call_args
assert "2 entries" in call_args
assert "1 active" in call_args
def test_update_status_custom_message(self): def test_update_status_custom_message(self):
"""Test status bar update with custom message.""" """Test status bar update with custom message."""
@ -257,24 +239,15 @@ class TestHostsManagerApp:
app = HostsManagerApp() app = HostsManagerApp()
# Mock set_timer and query_one to avoid event loop and UI issues # Mock the query_one method and set_timer to avoid event loop issues
app.set_timer = Mock() mock_status = Mock()
mock_status_bar = Mock() app.query_one = Mock(return_value=mock_status)
app.query_one = Mock(return_value=mock_status_bar) app.set_timer = Mock() # Mock the timer to avoid event loop issues
# Add test hosts_file for subtitle generation
app.hosts_file = HostsFile()
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["router"], is_active=False))
app.update_status("Custom status message") app.update_status("Custom status message")
# Verify status bar was updated with custom message # Verify status was updated with custom message
mock_status_bar.update.assert_called_with("Custom status message") mock_status.update.assert_called_once_with("Custom status message")
mock_status_bar.remove_class.assert_called_with("hidden")
# Verify subtitle shows current status (not the custom message)
assert "2 entries" in app.sub_title
assert "Read-only mode" in app.sub_title
# Verify timer was set for auto-clearing # Verify timer was set for auto-clearing
app.set_timer.assert_called_once() app.set_timer.assert_called_once()
@ -347,21 +320,19 @@ class TestHostsManagerApp:
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])) app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["test"])) app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["test"]))
# Mock the table_handler methods to avoid UI queries app.populate_entries_table = Mock()
app.table_handler.populate_entries_table = Mock()
app.table_handler.restore_cursor_position = Mock()
app.update_status = Mock() app.update_status = Mock()
app.action_sort_by_ip() app.action_sort_by_ip()
# Check that entries are sorted by IP address # Check that entries are sorted with default entries on top
assert app.hosts_file.entries[0].ip_address == "10.0.0.1" # Sorted by IP assert app.hosts_file.entries[0].ip_address == "127.0.0.1" # Default entry first
assert app.hosts_file.entries[1].ip_address == "127.0.0.1" assert app.hosts_file.entries[1].ip_address == "10.0.0.1" # Then sorted non-defaults
assert app.hosts_file.entries[2].ip_address == "192.168.1.1" assert app.hosts_file.entries[2].ip_address == "192.168.1.1"
assert app.sort_column == "ip" assert app.sort_column == "ip"
assert app.sort_ascending is True assert app.sort_ascending is True
app.table_handler.populate_entries_table.assert_called_once() app.populate_entries_table.assert_called_once()
def test_action_sort_by_hostname_ascending(self): def test_action_sort_by_hostname_ascending(self):
"""Test sorting by hostname in ascending order.""" """Test sorting by hostname in ascending order."""
@ -379,9 +350,7 @@ class TestHostsManagerApp:
app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["alpha"])) app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["alpha"]))
app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["beta"])) app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["beta"]))
# Mock the table_handler methods to avoid UI queries app.populate_entries_table = Mock()
app.table_handler.populate_entries_table = Mock()
app.table_handler.restore_cursor_position = Mock()
app.update_status = Mock() app.update_status = Mock()
app.action_sort_by_hostname() app.action_sort_by_hostname()
@ -393,7 +362,7 @@ class TestHostsManagerApp:
assert app.sort_column == "hostname" assert app.sort_column == "hostname"
assert app.sort_ascending is True assert app.sort_ascending is True
app.table_handler.populate_entries_table.assert_called_once() app.populate_entries_table.assert_called_once()
def test_data_table_row_highlighted_event(self): def test_data_table_row_highlighted_event(self):
"""Test DataTable row highlighting event handling.""" """Test DataTable row highlighting event handling."""
@ -404,10 +373,10 @@ class TestHostsManagerApp:
patch('hosts.tui.app.Config', return_value=mock_config): patch('hosts.tui.app.Config', return_value=mock_config):
app = HostsManagerApp() app = HostsManagerApp()
app.update_entry_details = Mock()
# Mock the details_handler and table_handler methods # Mock the display_index_to_actual_index method to return the same index
app.details_handler.update_entry_details = Mock() app.display_index_to_actual_index = Mock(return_value=2)
app.table_handler.display_index_to_actual_index = Mock(return_value=2)
# Create mock event with required parameters # Create mock event with required parameters
mock_table = Mock() mock_table = Mock()
@ -420,8 +389,8 @@ class TestHostsManagerApp:
# Should update selected index and details # Should update selected index and details
assert app.selected_entry_index == 2 assert app.selected_entry_index == 2
app.details_handler.update_entry_details.assert_called_once() app.update_entry_details.assert_called_once()
app.table_handler.display_index_to_actual_index.assert_called_once_with(2) app.display_index_to_actual_index.assert_called_once_with(2)
def test_data_table_header_selected_ip_column(self): def test_data_table_header_selected_ip_column(self):
"""Test DataTable header selection for IP column.""" """Test DataTable header selection for IP column."""

View file

@ -279,9 +279,7 @@ class TestSaveConfirmationIntegration:
"""Test exit_edit_entry_mode cleans up properly.""" """Test exit_edit_entry_mode cleans up properly."""
app.entry_edit_mode = True app.entry_edit_mode = True
app.original_entry_values = {"test": "data"} app.original_entry_values = {"test": "data"}
app.update_entry_details = Mock()
# Mock the details_handler and query_one methods
app.details_handler.update_entry_details = Mock()
app.query_one = Mock() app.query_one = Mock()
app.update_status = Mock() app.update_status = Mock()
@ -292,6 +290,6 @@ class TestSaveConfirmationIntegration:
assert not app.entry_edit_mode assert not app.entry_edit_mode
assert app.original_entry_values is None assert app.original_entry_values is None
app.details_handler.update_entry_details.assert_called_once() app.update_entry_details.assert_called_once()
mock_table.focus.assert_called_once() mock_table.focus.assert_called_once()
app.update_status.assert_called_once_with("Exited entry edit mode") app.update_status.assert_called_once_with("Exited entry edit mode")