Compare commits

..

No commits in common. "b0abec730ccaaae517a3791032a9110c97b4256c" and "5a117fb6241cd7e8cecd69cf0511b7598b5532bf" have entirely different histories.

9 changed files with 315 additions and 958 deletions

6
main_old.py Normal file
View file

@ -0,0 +1,6 @@
def main():
print("Hello from hosts!")
if __name__ == "__main__":
main()

View file

@ -2,59 +2,10 @@
## Current Work Focus ## Current Work Focus
**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. **Phase 3 Complete - Edit Mode Foundation**: The hosts TUI application now has a complete edit mode foundation with permission management, entry manipulation, and safe file operations. All keyboard shortcuts are implemented and tested. The application is ready for Phase 4 advanced edit features.
## Immediate Next Steps
### Priority 1: Code Quality Cleanup
1. **Fix linting issues**: Run `uv run ruff check --fix` to address 20 unused import and variable warnings
2. **Validate fixes**: Ensure all tests still pass (149 tests) after cleanup
3. **Confirm application functionality**: Test that `uv run hosts` still works perfectly
4. **Commit clean state**: Create commit with "Fix linting issues" once cleanup is complete
### Priority 2: Phase 4 Planning
Once code quality is restored:
1. **Advanced entry operations**: Add/delete entries with validation
2. **Search functionality**: Find entries by hostname or IP address
3. **Bulk operations**: Select and modify multiple entries
4. **Help modal**: Proper modal dialog with keyboard shortcuts
## Memory Bank Update Summary
### Files Updated
- ✅ **activeContext.md**: Updated current focus and next steps
- ✅ **progress.md**: Corrected test count (149 vs 97), added code quality status
- ✅ **techContext.md**: Updated development workflow and code quality status
- ✅ **systemPatterns.md**: Added edit mode and permission management patterns
- ✅ **projectbrief.md**: Updated test coverage details and current status
### Key Corrections Made
- **Test count**: Updated from 97 to 149 tests across all files
- **Code quality**: Noted 20 linting issues requiring cleanup
- **Project stage**: Clarified completion of Phase 3 with save confirmation
- **Current status**: Maintenance phase before Phase 4 development
- **Recent commits**: Reflected completion of save confirmation modal
### Architecture Insights Confirmed
- **Textual framework**: Excellent for complex TUI applications with modal dialogs
- **Layered architecture**: Proven effective for maintainable, testable code
- **Test-driven development**: 149 comprehensive tests enable confident refactoring
- **Configuration system**: JSON-based persistence working reliably
- **Permission management**: Sudo handling implemented safely and securely
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
### Phase 3 Save Confirmation Enhancement ✅ COMPLETE
- ✅ **Save confirmation modal**: Professional modal dialog asking to save, discard, or cancel when exiting edit entry mode
- ✅ **Change detection system**: Intelligent tracking of original entry values vs. current form values
- ✅ **No auto-save behavior**: Changes are only saved when explicitly confirmed by the user
- ✅ **Graceful exit handling**: ESC key in edit entry mode now triggers save confirmation instead of auto-exiting
- ✅ **Validation integration**: Full validation before saving with clear error messages for invalid data
- ✅ **Comprehensive testing**: Save confirmation functionality fully tested (149 total tests)
- ✅ **Modal keyboard shortcuts**: Save (S), Discard (D), Cancel (ESC) with intuitive button labels
### Phase 2 Implementation Complete ### Phase 2 Implementation Complete
- ✅ **Advanced configuration system**: Complete Config class with JSON persistence to ~/.config/hosts-manager/ - ✅ **Advanced configuration system**: Complete Config class with JSON persistence to ~/.config/hosts-manager/
- ✅ **Professional configuration modal**: Modal dialog with keyboard bindings for settings management - ✅ **Professional configuration modal**: Modal dialog with keyboard bindings for settings management
@ -63,7 +14,7 @@ The memory bank now accurately reflects the current state of the project, ready
- ✅ **Rich visual interface**: Color-coded entries with professional DataTable styling - ✅ **Rich visual interface**: Color-coded entries with professional DataTable styling
- ✅ **Interactive column headers**: Click headers to sort data with visual feedback - ✅ **Interactive column headers**: Click headers to sort data with visual feedback
- ✅ **Enhanced status bar**: Detailed information including entry counts and file path - ✅ **Enhanced status bar**: Detailed information including entry counts and file path
- ✅ **Comprehensive testing**: 149 tests covering all functionality including new features - ✅ **Comprehensive testing**: 97 tests covering all functionality including new features
### Current Project State ### Current Project State
- **Production-ready application**: `uv run hosts` launches polished TUI with advanced features - **Production-ready application**: `uv run hosts` launches polished TUI with advanced features
@ -71,24 +22,38 @@ The memory bank now accurately reflects the current state of the project, ready
- **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**: 149 tests with 100% pass rate covering all components - **Comprehensive test coverage**: 97 tests with 100% pass rate covering all components
- **Code quality maintenance needed**: 20 linting issues (unused imports/variables) require cleanup - **Perfect code quality**: All linting and formatting standards maintained
- **Robust architecture**: Clean layered design ready for Phase 4 advanced features - **Robust architecture**: Clean layered design ready for edit mode extension
## Next Steps ## Next Steps
### Immediate Priority: Code Quality Cleanup ### Phase 3: Edit Mode Foundation (Current Priority)
1. **Fix linting issues**: Address 20 unused import and variable warnings 1. **Permission management system**:
- Remove unused imports in core/config.py, test files - Implement sudo request and validation
- Clean up unused variables in exception handling - Edit mode toggle with proper security handling
- Run `uv run ruff check --fix` to auto-fix issues - Permission validation and error handling
- Graceful fallback for permission denied scenarios
2. **Code quality validation**: 2. **Basic editing operations**:
- Ensure all ruff checks pass with zero issues - Toggle entries active/inactive with visual feedback
- Maintain perfect test coverage (149 tests passing) - Entry editing interface for IP addresses, hostnames, and comments
- Verify application functionality after cleanup - Real-time validation of IP addresses and hostnames
- Safe state management during editing
### Phase 4: Advanced Edit Features (Next Phase) 3. **File safety and backup**:
- Automatic backup before any modifications
- Atomic file operations with rollback capability
- Validation before writing changes to disk
- Error recovery and restoration mechanisms
4. **Edit mode user interface**:
- Clear visual indicators for edit mode vs read-only mode
- Edit forms and dialogs for entry modification
- Confirmation dialogs for destructive operations
- Enhanced status feedback during edit operations
### Phase 4: Advanced Edit Features (Future)
1. **Advanced editing operations**: 1. **Advanced editing operations**:
- Add new entries with validation - Add new entries with validation
- Delete entries with confirmation - Delete entries with confirmation
@ -122,37 +87,8 @@ The memory bank now accurately reflects the current state of the project, ready
- ✅ **Error handling**: Graceful degradation and user feedback throughout - ✅ **Error handling**: Graceful degradation and user feedback throughout
- ✅ **Modal pattern**: Professional modal dialogs with proper lifecycle management - ✅ **Modal pattern**: Professional modal dialogs with proper lifecycle management
- ✅ **Configuration pattern**: Centralized settings with persistence and defaults - ✅ **Configuration pattern**: Centralized settings with persistence and defaults
- ✅ **Command pattern**: Implemented for edit operations with save confirmation - 🔄 **Command pattern**: Planned for Phase 3 edit operations with undo/redo
- 🔄 **Observer pattern**: Will implement for state change notifications in advanced features - 🔄 **Observer pattern**: Will implement for state change notifications in edit mode
## Important Patterns and Preferences
### Code Quality Standards
- **Zero tolerance for linting issues**: All ruff checks must pass before commits
- **Comprehensive testing**: Maintain 100% test pass rate with meaningful coverage
- **Type safety**: Full type hints throughout codebase
- **Documentation**: Clear docstrings and inline comments for complex logic
- **Error handling**: Graceful degradation with informative user feedback
### Development Workflow
- **Test-driven development**: Write tests before implementing features
- **Incremental implementation**: Small, focused changes with immediate testing
- **Clean commits**: Each commit should represent a complete, working feature
- **Memory bank maintenance**: Update documentation after significant changes
## Learnings and Project Insights
### Technical Insights
- **Textual framework**: Excellent for rich TUI applications with reactive state management
- **Modal system**: Professional dialog implementation requires careful focus and lifecycle management
- **File operations**: Atomic operations and backup systems essential for system file modification
- **Permission management**: Sudo handling requires careful security consideration and user experience design
### Process Insights
- **Memory bank value**: Documentation consistency crucial for maintaining project context
- **Testing strategy**: Comprehensive test coverage enables confident refactoring and feature addition
- **Code quality**: Automated linting and formatting tools essential for maintaining standards
- **Incremental development**: Small, focused phases enable better quality and easier debugging
### Technical Constraints Confirmed ### Technical Constraints Confirmed
- ✅ **Python 3.13+**: Excellent choice with modern features working perfectly - ✅ **Python 3.13+**: Excellent choice with modern features working perfectly

View file

@ -39,6 +39,16 @@
- ✅ **Modal system**: Proper modal dialogs with keyboard bindings - ✅ **Modal system**: Proper modal dialogs with keyboard bindings
- ✅ **Configuration persistence**: Settings saved to ~/.config/hosts-manager/ - ✅ **Configuration persistence**: Settings saved to ~/.config/hosts-manager/
### Documentation
- ✅ **Project brief**: Comprehensive project definition and requirements
- ✅ **Product context**: User experience goals and problem definition
- ✅ **Technical context**: Technology stack and development setup
- ✅ **System patterns**: Architecture, design patterns, and implementation paths
- ✅ **Active context**: Current work focus and next steps
## What's Left to Build
### Phase 3: Edit Mode Foundation ✅ COMPLETE ### Phase 3: Edit Mode Foundation ✅ COMPLETE
- ✅ **Permission management**: Sudo request and management with PermissionManager class - ✅ **Permission management**: Sudo request and management with PermissionManager class
- ✅ **Edit mode toggle**: Switch between read-only and edit modes with 'e' key - ✅ **Edit mode toggle**: Switch between read-only and edit modes with 'e' key
@ -54,23 +64,7 @@
- ✅ **Live testing**: Manual testing confirms all functionality works correctly - ✅ **Live testing**: Manual testing confirms all functionality works correctly
- ✅ **Human-readable formatting**: Tab-based column alignment with proper spacing - ✅ **Human-readable formatting**: Tab-based column alignment with proper spacing
- ✅ **Management header**: Automatic addition of management header to hosts files - ✅ **Management header**: Automatic addition of management header to hosts files
- ✅ **Save confirmation modal**: Professional modal dialog for save/discard/cancel when exiting edit entry mode - ✅ **Auto-save functionality**: Immediate saving of changes when entries are edited
- ✅ **Change detection**: Intelligent tracking of original vs. current entry values
- ✅ **No auto-save**: Changes saved only when explicitly confirmed by user
### Documentation
- ✅ **Project brief**: Comprehensive project definition and requirements
- ✅ **Product context**: User experience goals and problem definition
- ✅ **Technical context**: Technology stack and development setup
- ✅ **System patterns**: Architecture, design patterns, and implementation paths
- ✅ **Active context**: Current work focus and next steps
## What's Left to Build
### Immediate Priority: Code Quality Cleanup
- ❌ **Fix linting issues**: Address 20 unused import and variable warnings
- ❌ **Code quality validation**: Ensure all ruff checks pass with zero issues
- ❌ **Maintain test coverage**: Keep 149 tests passing during cleanup
### Phase 4: Advanced Edit Features ### Phase 4: Advanced Edit Features
- ❌ **Add new entries**: Create new host entries - ❌ **Add new entries**: Create new host entries
@ -95,9 +89,19 @@
## Current Status ## Current Status
### Development Stage ### Development Stage
**Stage**: Phase 3 Complete with Code Quality Maintenance Required **Stage**: Phase 3 Complete - Moving to Phase 4
**Progress**: 82% (Complete edit mode foundation with save confirmation, code cleanup needed) **Progress**: 75% (Complete edit mode foundation with permission management)
**Next Milestone**: Code quality cleanup, then Phase 4 advanced edit features **Next Milestone**: Advanced edit features (add/delete entries, bulk operations)
### Phase 2 Final Achievements
1. ✅ **Advanced configuration system**: Complete settings management with persistence
2. ✅ **Professional modal dialogs**: Configuration modal with proper keyboard bindings
3. ✅ **Intelligent filtering**: Hide/show default system entries based on user preference
4. ✅ **Complete sorting system**: Sort by IP address and hostname with direction toggle
5. ✅ **Rich visual interface**: Color-coded entries with professional DataTable styling
6. ✅ **Interactive headers**: Click column headers to sort data
7. ✅ **Enhanced user experience**: Visual sort indicators and comprehensive status information
8. ✅ **Robust configuration**: JSON-based settings with graceful error handling
### 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
@ -108,17 +112,18 @@
6. ✅ **Manager module**: Complete HostsManager class for all edit operations 6. ✅ **Manager module**: Complete HostsManager class for all edit operations
7. ✅ **Safe file operations**: Atomic file writing with rollback capability 7. ✅ **Safe file operations**: Atomic file writing with rollback capability
8. ✅ **Enhanced error messages**: Professional read-only mode error messages with clear instructions 8. ✅ **Enhanced error messages**: Professional read-only mode error messages with clear instructions
9. ✅ **Comprehensive testing**: Full test coverage for manager module (149 total tests) 9. ✅ **Comprehensive testing**: 38 new tests for manager module (135 total tests)
10. ✅ **Save confirmation modal**: Professional save/discard/cancel dialog when exiting edit entry mode
11. ✅ **Change detection system**: Intelligent tracking of original vs. current entry values
12. ✅ **No auto-save behavior**: User-controlled saving with explicit confirmation
### Current Project State ### Recent Major Accomplishments
- **Application functionality**: Fully functional with advanced edit capabilities - ✅ **Complete Phase 3 implementation**: Full edit mode foundation with permission management
- **Test coverage**: 149 tests with 100% pass rate covering all functionality - ✅ **Manager module**: PermissionManager and HostsManager classes with comprehensive functionality
- **Code quality issue**: 20 linting issues (unused imports/variables) need cleanup - ✅ **Edit mode integration**: Seamless integration with main TUI application
- **Architecture**: Robust layered design ready for Phase 4 advanced features - ✅ **Permission system**: Robust sudo request, validation, and release functionality
- **User experience**: Professional TUI with modal dialogs and comprehensive keyboard shortcuts - ✅ **File backup system**: Automatic backup creation with timestamp naming
- ✅ **Entry manipulation**: Toggle active/inactive and reorder entries safely
- ✅ **Comprehensive testing**: 135 total tests with 100% pass rate including edit operations
- ✅ **Error handling**: Graceful handling of permission errors and file operations
- ✅ **Keyboard shortcuts**: Complete set of edit mode bindings (e, space, Ctrl+Up/Down, Ctrl+S)
## Technical Implementation Details ## Technical Implementation Details
@ -131,13 +136,11 @@
### Test Coverage Excellence ### Test Coverage Excellence
- **Models**: 27 comprehensive tests covering all data model edge cases - **Models**: 27 comprehensive tests covering all data model edge cases
- **Parser**: 15 tests covering file operations, permissions, and error conditions - **Parser**: 15 tests covering file operations, permissions, and error conditions
- **Manager**: Comprehensive tests for permission management and edit operations - **Coverage**: 100% of core functionality with edge case validation
- **UI Components**: Full coverage of modal dialogs and TUI interactions
- **Total**: 149 tests with 100% pass rate and comprehensive edge case coverage
- **Quality**: All tests passing consistently with fast execution - **Quality**: All tests passing consistently with fast execution
### Code Quality Standards ### Code Quality Standards
- **Linting**: 20 minor issues (unused imports/variables) requiring cleanup - **Linting**: Perfect ruff compliance with zero issues
- **Type hints**: Complete type coverage throughout entire codebase - **Type hints**: Complete type coverage throughout entire codebase
- **Documentation**: Comprehensive docstrings and inline comments - **Documentation**: Comprehensive docstrings and inline comments
- **Error handling**: Graceful exception handling with user feedback - **Error handling**: Graceful exception handling with user feedback
@ -145,14 +148,7 @@
## Known Issues ## Known Issues
### Immediate Code Quality Issues ### Phase 3 Enhancement Opportunities
- **Linting warnings**: 20 unused import and variable warnings in core and test files
- **Code standard compliance**: Must address before proceeding to Phase 4
- **Auto-fixable**: All issues can be resolved with `uv run ruff check --fix`
### Phase 4 Enhancement Opportunities
- **Add new entries**: Create new host entries with validation (planned for Phase 4)
- **Delete entries**: Remove host entries with confirmation (planned for Phase 4)
- **Search functionality**: Find entries by hostname or IP address (planned for Phase 4) - **Search functionality**: Find entries by hostname or IP address (planned for Phase 4)
- **Help modal**: Proper help dialog instead of status message (planned for Phase 4) - **Help modal**: Proper help dialog instead of status message (planned for Phase 4)
- **Large file performance**: Not yet tested with very large hosts files - **Large file performance**: Not yet tested with very large hosts files

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) ### Implemented Tests (97 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
@ -101,39 +101,20 @@ hosts/
- Configuration loading and saving - Configuration loading and saving
- Default entry detection and filtering - Default entry detection and filtering
4. **TUI Application Tests** (28 tests): 4. **Modal Dialog Tests** (15 tests):
- Main application initialization and startup - Configuration modal lifecycle
- User interaction handling
- Keyboard binding validation
- State management during configuration changes
5. **Main Application Tests** (18 tests):
- Application initialization and startup
- File loading and error handling - File loading and error handling
- User interface state management - User interface state management
- Sorting and navigation functionality - Sorting and navigation functionality
- Modal dialog lifecycle and interactions
- Keyboard binding validation
5. **Manager Module Tests** (38 tests):
- Permission management and sudo handling
- Edit mode operations and state transitions
- File backup and atomic operations
- Entry manipulation and validation
6. **Save Confirmation Tests** (13 tests):
- Modal dialog lifecycle and user interactions
- Change detection and validation
- Save/discard/cancel functionality
- Integration with edit workflow
7. **Configuration Modal Tests** (6 tests):
- Modal configuration interface
- Settings persistence and validation
- User interaction handling
### Current Test Coverage Status
- **Total Tests**: 149 comprehensive tests
- **Pass Rate**: 100% (all tests passing)
- **Coverage Areas**: Core models, file parsing, configuration, TUI components, edit operations, modal dialogs
- **Code Quality**: 20 minor linting issues (unused imports/variables) requiring cleanup
### Future Test Areas (Planned) ### Future Test Areas (Planned)
- **Advanced Edit Tests**: Add/delete entries, bulk operations - **Edit Mode Tests**: Permission management and file modification
- **DNS Resolution Tests**: Hostname resolution and IP comparison - **DNS Resolution Tests**: Hostname resolution and IP comparison
- **Performance Tests**: Large file handling and optimization - **Performance Tests**: Large file handling and optimization
- **Search Functionality Tests**: Entry searching and filtering - **Integration Tests**: End-to-end workflow testing

View file

@ -34,9 +34,8 @@
#### System Layer (Implemented) #### System Layer (Implemented)
- ✅ **File I/O**: Atomic file operations with backup support - ✅ **File I/O**: Atomic file operations with backup support
- ✅ **Permission checking**: Validation of file access permissions - ✅ **Permission checking**: Validation of file access permissions
- ✅ **Permission management**: Sudo request and handling for edit mode
- ✅ **Backup system**: Automatic backup creation before modifications
- 🔄 **DNS Resolution**: Planned for Phase 5 advanced features - 🔄 **DNS Resolution**: Planned for Phase 5 advanced features
- 🔄 **Permission Management**: Sudo handling planned for Phase 3 edit mode
## Key Technical Decisions ## Key Technical Decisions
@ -94,13 +93,10 @@ class Config:
- ✅ **Reactive state**: Using Textual's reactive attributes for complex UI updates - ✅ **Reactive state**: Using Textual's reactive attributes for complex UI updates
- ✅ **Configuration state**: Persistent settings with JSON storage and graceful error handling - ✅ **Configuration state**: Persistent settings with JSON storage and graceful error handling
- ✅ **Sorting state**: Reactive sort column and direction with visual indicators - ✅ **Sorting state**: Reactive sort column and direction with visual indicators
- ✅ **Edit mode state**: Safe transitions between read-only and edit modes
- ✅ **Permission state**: Sudo request, validation, and release management
- ✅ **Validation pipeline**: All data validated in models, parser, and configuration - ✅ **Validation pipeline**: All data validated in models, parser, and configuration
- ✅ **File integrity**: Atomic operations preserve file structure - ✅ **File integrity**: Atomic operations preserve file structure
- ✅ **Error handling**: Graceful degradation for all error conditions - ✅ **Error handling**: Graceful degradation for all error conditions
- ✅ **Modal state**: Professional modal dialog lifecycle management - ✅ **Modal state**: Professional modal dialog lifecycle management
- ✅ **Change detection**: Intelligent tracking for save confirmation
- 🔄 **Undo/Redo capability**: Planned for Phase 4 with command pattern - 🔄 **Undo/Redo capability**: Planned for Phase 4 with command pattern
- 🔄 **Dirty state tracking**: Will be implemented in Phase 3 edit mode - 🔄 **Dirty state tracking**: Will be implemented in Phase 3 edit mode

View file

@ -32,13 +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
- ✅ **Code quality maintenance required**: 20 linting issues (unused imports/variables) need cleanup - ✅ **Perfect code quality**: All ruff checks passing with zero issues
- ✅ **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
- ✅ **Comprehensive testing**: 149 tests covering all functionality including new features - ✅ **Comprehensive testing**: 97 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 dialogs with keyboard bindings - ✅ **Modal interface**: Professional configuration dialogs with keyboard bindings
- ✅ **Advanced features**: Sorting, filtering, edit mode, and save confirmation - ✅ **Advanced features**: Sorting, filtering, and rich visual interface
### Runtime Management ### Runtime Management
- ✅ **uv run hosts**: Command executes application instantly - ✅ **uv run hosts**: Command executes application instantly
@ -94,17 +94,11 @@ 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 - 20 issues need fixing 2. **uv run ruff check**: Lint code - all checks passing perfectly
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 - 149 tests passing with 100% success rate 4. ✅ **uv run pytest**: Run test suite - 42 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
- **Current issues**: 20 linting warnings (unused imports and variables)
- **Auto-fixable**: All issues can be resolved with `uv run ruff check --fix`
- **Test coverage**: 149 comprehensive tests with 100% pass rate
- **Code formatting**: Perfect formatting compliance maintained
### Code Quality Achieved ### Code Quality Achieved
- ✅ **ruff configuration**: Perfect compliance with zero issues across all modules - ✅ **ruff configuration**: Perfect compliance with zero issues across all modules
- ✅ **Type hints**: Complete type coverage throughout entire codebase including new components - ✅ **Type hints**: Complete type coverage throughout entire codebase including new components

View file

@ -18,7 +18,6 @@ from .core.models import HostsFile
from .core.config import Config from .core.config import Config
from .core.manager import HostsManager from .core.manager import HostsManager
from .tui.config_modal import ConfigModal from .tui.config_modal import ConfigModal
from .tui.save_confirmation_modal import SaveConfirmationModal
class HostsManagerApp(App): class HostsManagerApp(App):
@ -152,9 +151,6 @@ class HostsManagerApp(App):
self.title = "Hosts Manager" self.title = "Hosts Manager"
self.sub_title = "Read-only mode" self.sub_title = "Read-only mode"
# Track original entry values for change detection
self.original_entry_values = None
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Create the application layout.""" """Create the application layout."""
yield Header() yield Header()
@ -177,9 +173,7 @@ class HostsManagerApp(App):
yield Label("Hostname:") yield Label("Hostname:")
yield Input(id="hostname-input", placeholder="Enter hostname") yield Input(id="hostname-input", placeholder="Enter hostname")
yield Label("Comment:") yield Label("Comment:")
yield Input( yield Input(id="comment-input", placeholder="Enter comment (optional)")
id="comment-input", placeholder="Enter comment (optional)"
)
yield Label("Active:") yield Label("Active:")
yield Checkbox(id="active-checkbox", value=True) yield Checkbox(id="active-checkbox", value=True)
yield right_pane yield right_pane
@ -197,9 +191,7 @@ class HostsManagerApp(App):
"""Load the hosts file and populate the interface.""" """Load the hosts file and populate the interface."""
# Remember current selection for restoration # Remember current selection for restoration
current_entry = None current_entry = None
if self.hosts_file.entries and self.selected_entry_index < len( if self.hosts_file.entries and self.selected_entry_index < len(self.hosts_file.entries):
self.hosts_file.entries
):
current_entry = self.hosts_file.entries[self.selected_entry_index] current_entry = self.hosts_file.entries[self.selected_entry_index]
try: try:
@ -229,9 +221,7 @@ class HostsManagerApp(App):
for entry in self.hosts_file.entries: for entry in self.hosts_file.entries:
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 # Skip default entries if configured to hide them
if not show_defaults and self.config.is_default_entry( if not show_defaults and self.config.is_default_entry(entry.ip_address, canonical_hostname):
entry.ip_address, canonical_hostname
):
continue continue
visible_entries.append(entry) visible_entries.append(entry)
@ -244,9 +234,7 @@ class HostsManagerApp(App):
for i, entry in enumerate(self.hosts_file.entries): for i, entry in enumerate(self.hosts_file.entries):
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 # Skip default entries if configured to hide them
if not show_defaults and self.config.is_default_entry( if not show_defaults and self.config.is_default_entry(entry.ip_address, canonical_hostname):
entry.ip_address, canonical_hostname
):
continue continue
return i return i
@ -352,11 +340,9 @@ class HostsManagerApp(App):
else: else:
# Try to find the same entry in the reloaded file # Try to find the same entry in the reloaded file
for i, entry in enumerate(self.hosts_file.entries): for i, entry in enumerate(self.hosts_file.entries):
if ( if (entry.ip_address == previous_entry.ip_address and
entry.ip_address == previous_entry.ip_address entry.hostnames == previous_entry.hostnames and
and entry.hostnames == previous_entry.hostnames entry.comment == previous_entry.comment):
and entry.comment == previous_entry.comment
):
self.selected_entry_index = i self.selected_entry_index = i
break break
else: else:
@ -404,12 +390,8 @@ class HostsManagerApp(App):
show_defaults = self.config.should_show_default_entries() show_defaults = self.config.should_show_default_entries()
if not show_defaults: if not show_defaults:
# Check if the currently selected entry is a default entry (hidden) # Check if the currently selected entry is a default entry (hidden)
if ( if (self.selected_entry_index < len(self.hosts_file.entries) and
self.selected_entry_index < len(self.hosts_file.entries) self.hosts_file.entries[self.selected_entry_index].is_default_entry()):
and self.hosts_file.entries[
self.selected_entry_index
].is_default_entry()
):
# The selected entry is hidden, so we should show the first visible entry instead # The selected entry is hidden, so we should show the first visible entry instead
if visible_entries: if visible_entries:
# Find the first visible entry in the hosts file # Find the first visible entry in the hosts file
@ -433,9 +415,7 @@ class HostsManagerApp(App):
if entry.is_default_entry(): if entry.is_default_entry():
details_lines.append("") details_lines.append("")
details_lines.append("⚠️ SYSTEM DEFAULT ENTRY") details_lines.append("⚠️ SYSTEM DEFAULT ENTRY")
details_lines.append( details_lines.append("This is a default system entry and cannot be modified.")
"This is a default system entry and cannot be modified."
)
if entry.comment: if entry.comment:
details_lines.append(f"Comment: {entry.comment}") details_lines.append(f"Comment: {entry.comment}")
@ -454,9 +434,7 @@ class HostsManagerApp(App):
details_widget.add_class("hidden") details_widget.add_class("hidden")
edit_form.remove_class("hidden") edit_form.remove_class("hidden")
if not self.hosts_file.entries or self.selected_entry_index >= len( if not self.hosts_file.entries or self.selected_entry_index >= len(self.hosts_file.entries):
self.hosts_file.entries
):
return return
entry = self.hosts_file.entries[self.selected_entry_index] entry = self.hosts_file.entries[self.selected_entry_index]
@ -468,7 +446,7 @@ class HostsManagerApp(App):
active_checkbox = self.query_one("#active-checkbox", Checkbox) active_checkbox = self.query_one("#active-checkbox", Checkbox)
ip_input.value = entry.ip_address ip_input.value = entry.ip_address
hostname_input.value = ", ".join(entry.hostnames) hostname_input.value = ', '.join(entry.hostnames)
comment_input.value = entry.comment or "" comment_input.value = entry.comment or ""
active_checkbox.value = entry.is_active active_checkbox.value = entry.is_active
@ -505,7 +483,7 @@ class HostsManagerApp(App):
# Add file info # Add file info
file_info = self.parser.get_file_info() file_info = self.parser.get_file_info()
if file_info["exists"]: if file_info['exists']:
status_text += f" | {file_info['path']}" status_text += f" | {file_info['path']}"
status_widget.update(status_text) status_widget.update(status_text)
@ -514,18 +492,14 @@ class HostsManagerApp(App):
"""Handle row highlighting (cursor movement) in the DataTable.""" """Handle row highlighting (cursor movement) in the DataTable."""
if event.data_table.id == "entries-table": if event.data_table.id == "entries-table":
# Convert display index to actual index # Convert display index to actual index
self.selected_entry_index = self.display_index_to_actual_index( self.selected_entry_index = self.display_index_to_actual_index(event.cursor_row)
event.cursor_row
)
self.update_entry_details() self.update_entry_details()
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
"""Handle row selection in the DataTable.""" """Handle row selection in the DataTable."""
if event.data_table.id == "entries-table": if event.data_table.id == "entries-table":
# Convert display index to actual index # Convert display index to actual index
self.selected_entry_index = self.display_index_to_actual_index( self.selected_entry_index = self.display_index_to_actual_index(event.cursor_row)
event.cursor_row
)
self.update_entry_details() self.update_entry_details()
def action_reload(self) -> None: def action_reload(self) -> None:
@ -539,13 +513,10 @@ 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( self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config, e Edit")
"Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config, e Edit"
)
def action_config(self) -> None: def action_config(self) -> None:
"""Show configuration modal.""" """Show configuration modal."""
def handle_config_result(config_changed: bool) -> None: def handle_config_result(config_changed: bool) -> None:
if config_changed: if config_changed:
# Reload the table to apply new filtering # Reload the table to apply new filtering
@ -619,9 +590,7 @@ class HostsManagerApp(App):
def action_edit_entry(self) -> None: def action_edit_entry(self) -> None:
"""Enter edit mode for the selected entry.""" """Enter edit mode for the selected entry."""
if not self.edit_mode: if not self.edit_mode:
self.update_status( self.update_status("❌ Cannot edit entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
"❌ Cannot edit entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
)
return return
if not self.hosts_file.entries: if not self.hosts_file.entries:
@ -637,14 +606,6 @@ class HostsManagerApp(App):
self.update_status("❌ Cannot edit system default entry") self.update_status("❌ Cannot edit system default entry")
return return
# Store original values for change detection
self.original_entry_values = {
"ip_address": entry.ip_address,
"hostnames": entry.hostnames.copy(),
"comment": entry.comment,
"is_active": entry.is_active,
}
self.entry_edit_mode = True self.entry_edit_mode = True
self.update_entry_details() self.update_entry_details()
@ -654,148 +615,17 @@ class HostsManagerApp(App):
self.update_status("Editing entry - Use Tab/Shift+Tab to navigate, ESC to exit") self.update_status("Editing entry - Use Tab/Shift+Tab to navigate, ESC to exit")
def has_entry_changes(self) -> bool:
"""Check if the current entry has been modified from its original values."""
if not self.original_entry_values or not self.entry_edit_mode:
return False
# Get current values from form fields
ip_input = self.query_one("#ip-input", Input)
hostname_input = self.query_one("#hostname-input", Input)
comment_input = self.query_one("#comment-input", Input)
active_checkbox = self.query_one("#active-checkbox", Checkbox)
current_hostnames = [
h.strip() for h in hostname_input.value.split(",") if h.strip()
]
current_comment = comment_input.value.strip() or None
# Compare with original values
return (
ip_input.value.strip() != self.original_entry_values["ip_address"]
or current_hostnames != self.original_entry_values["hostnames"]
or current_comment != self.original_entry_values["comment"]
or active_checkbox.value != self.original_entry_values["is_active"]
)
def action_exit_edit_entry(self) -> None: def action_exit_edit_entry(self) -> None:
"""Exit entry edit mode and return focus to the entries table.""" """Exit entry edit mode and return focus to the entries table."""
if not self.entry_edit_mode: if self.entry_edit_mode:
return self.entry_edit_mode = False
self.update_entry_details()
# Check if there are unsaved changes # Return focus to the entries table
if self.has_entry_changes():
# Show save confirmation modal
def handle_save_confirmation(result):
if result == "save":
# Validate and save changes
if self.validate_and_save_entry_changes():
self.exit_edit_entry_mode()
elif result == "discard":
# Restore original values and exit
self.restore_original_entry_values()
self.exit_edit_entry_mode()
elif result == "cancel":
# Do nothing, stay in edit mode
pass
self.push_screen(SaveConfirmationModal(), handle_save_confirmation)
else:
# No changes, exit directly
self.exit_edit_entry_mode()
def exit_edit_entry_mode(self) -> None:
"""Helper method to exit entry edit mode and clean up."""
self.entry_edit_mode = False
self.original_entry_values = None
self.update_entry_details()
# Return focus to the entries table
table = self.query_one("#entries-table", DataTable)
table.focus()
self.update_status("Exited entry edit mode")
def restore_original_entry_values(self) -> None:
"""Restore the original values to the form fields."""
if not self.original_entry_values:
return
# Update form fields with original values
ip_input = self.query_one("#ip-input", Input)
hostname_input = self.query_one("#hostname-input", Input)
comment_input = self.query_one("#comment-input", Input)
active_checkbox = self.query_one("#active-checkbox", Checkbox)
ip_input.value = self.original_entry_values["ip_address"]
hostname_input.value = ", ".join(self.original_entry_values["hostnames"])
comment_input.value = self.original_entry_values["comment"] or ""
active_checkbox.value = self.original_entry_values["is_active"]
def validate_and_save_entry_changes(self) -> bool:
"""Validate current entry values and save if valid."""
if not self.hosts_file.entries or self.selected_entry_index >= len(
self.hosts_file.entries
):
return False
entry = self.hosts_file.entries[self.selected_entry_index]
# Get values from form fields
ip_input = self.query_one("#ip-input", Input)
hostname_input = self.query_one("#hostname-input", Input)
comment_input = self.query_one("#comment-input", Input)
active_checkbox = self.query_one("#active-checkbox", Checkbox)
# Validate IP address
try:
ipaddress.ip_address(ip_input.value.strip())
except ValueError:
self.update_status("❌ Invalid IP address - changes not saved")
return False
# Validate hostname(s)
hostnames = [h.strip() for h in hostname_input.value.split(",") if h.strip()]
if not hostnames:
self.update_status(
"❌ At least one hostname is required - changes not saved"
)
return False
hostname_pattern = re.compile(
r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$"
)
for hostname in hostnames:
if not hostname_pattern.match(hostname):
self.update_status(
f"❌ Invalid hostname: {hostname} - changes not saved"
)
return False
# Update the entry
entry.ip_address = ip_input.value.strip()
entry.hostnames = hostnames
entry.comment = comment_input.value.strip() or None
entry.is_active = active_checkbox.value
# Save to file
success, message = self.manager.save_hosts_file(self.hosts_file)
if success:
# Update the table display
self.populate_entries_table()
# Restore cursor position
table = self.query_one("#entries-table", DataTable) table = self.query_one("#entries-table", DataTable)
display_index = self.actual_index_to_display_index( table.focus()
self.selected_entry_index
) self.update_status("Exited entry edit mode")
if table.row_count > 0 and display_index < table.row_count:
table.move_cursor(row=display_index)
self.update_status("Entry saved successfully")
return True
else:
self.update_status(f"❌ Error saving entry: {message}")
return False
def action_next_field(self) -> None: def action_next_field(self) -> None:
"""Move to the next field in edit mode.""" """Move to the next field in edit mode."""
@ -807,7 +637,7 @@ class HostsManagerApp(App):
self.query_one("#ip-input", Input), self.query_one("#ip-input", Input),
self.query_one("#hostname-input", Input), self.query_one("#hostname-input", Input),
self.query_one("#comment-input", Input), self.query_one("#comment-input", Input),
self.query_one("#active-checkbox", Checkbox), self.query_one("#active-checkbox", Checkbox)
] ]
# Find currently focused field and move to next # Find currently focused field and move to next
@ -827,7 +657,7 @@ class HostsManagerApp(App):
self.query_one("#ip-input", Input), self.query_one("#ip-input", Input),
self.query_one("#hostname-input", Input), self.query_one("#hostname-input", Input),
self.query_one("#comment-input", Input), self.query_one("#comment-input", Input),
self.query_one("#active-checkbox", Checkbox), self.query_one("#active-checkbox", Checkbox)
] ]
# Find currently focused field and move to previous # Find currently focused field and move to previous
@ -839,38 +669,90 @@ class HostsManagerApp(App):
def on_key(self, event) -> None: def on_key(self, event) -> None:
"""Handle key events to override default tab behavior in edit mode.""" """Handle key events to override default tab behavior in edit mode."""
# Only handle custom tab navigation if in entry edit mode AND no modal is open if self.entry_edit_mode and event.key == "tab":
if self.entry_edit_mode and len(self.screen_stack) == 1 and event.key == "tab":
# Prevent default tab behavior and use our custom navigation # Prevent default tab behavior and use our custom navigation
event.prevent_default() event.prevent_default()
self.action_next_field() self.action_next_field()
elif ( elif self.entry_edit_mode and event.key == "shift+tab":
self.entry_edit_mode
and len(self.screen_stack) == 1
and event.key == "shift+tab"
):
# Prevent default shift+tab behavior and use our custom navigation # Prevent default shift+tab behavior and use our custom navigation
event.prevent_default() event.prevent_default()
self.action_prev_field() self.action_prev_field()
def on_input_changed(self, event: Input.Changed) -> None: def on_input_changed(self, event: Input.Changed) -> None:
"""Handle input field changes (no auto-save - changes saved on exit).""" """Handle input field changes and auto-save."""
# Input changes are tracked but not automatically saved if not self.entry_edit_mode or not self.edit_mode:
# Changes will be validated and saved when exiting edit mode return
pass
if event.input.id in ["ip-input", "hostname-input", "comment-input"]:
self.save_entry_changes()
def on_checkbox_changed(self, event: Checkbox.Changed) -> None: def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
"""Handle checkbox changes (no auto-save - changes saved on exit).""" """Handle checkbox changes and auto-save."""
# Checkbox changes are tracked but not automatically saved if not self.entry_edit_mode or not self.edit_mode:
# Changes will be validated and saved when exiting edit mode return
pass
if event.checkbox.id == "active-checkbox":
self.save_entry_changes()
def save_entry_changes(self) -> None:
"""Save the current entry changes."""
if not self.hosts_file.entries or self.selected_entry_index >= len(self.hosts_file.entries):
return
entry = self.hosts_file.entries[self.selected_entry_index]
# Get values from form fields
ip_input = self.query_one("#ip-input", Input)
hostname_input = self.query_one("#hostname-input", Input)
comment_input = self.query_one("#comment-input", Input)
active_checkbox = self.query_one("#active-checkbox", Checkbox)
# Validate IP address
try:
ipaddress.ip_address(ip_input.value.strip())
except ValueError:
self.update_status("❌ Invalid IP address")
return
# Validate hostname(s)
hostnames = [h.strip() for h in hostname_input.value.split(',') if h.strip()]
if not hostnames:
self.update_status("❌ At least one hostname is required")
return
hostname_pattern = re.compile(
r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
)
for hostname in hostnames:
if not hostname_pattern.match(hostname):
self.update_status(f"❌ Invalid hostname: {hostname}")
return
# Update the entry
entry.ip_address = ip_input.value.strip()
entry.hostnames = hostnames
entry.comment = comment_input.value.strip() or None
entry.is_active = active_checkbox.value
# Save to file
success, message = self.manager.save_hosts_file(self.hosts_file)
if success:
# Update the table display
self.populate_entries_table()
# Restore cursor position
table = self.query_one("#entries-table", DataTable)
display_index = self.actual_index_to_display_index(self.selected_entry_index)
if table.row_count > 0 and display_index < table.row_count:
table.move_cursor(row=display_index)
self.update_status("Entry saved successfully")
else:
self.update_status(f"❌ Error saving entry: {message}")
def action_toggle_entry(self) -> None: def action_toggle_entry(self) -> None:
"""Toggle the active state of the selected entry.""" """Toggle the active state of the selected entry."""
if not self.edit_mode: if not self.edit_mode:
self.update_status( self.update_status("❌ Cannot toggle entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
"❌ Cannot toggle entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
)
return return
if not self.hosts_file.entries: if not self.hosts_file.entries:
@ -880,9 +762,7 @@ class HostsManagerApp(App):
# Remember current entry for cursor position restoration # Remember current entry for cursor position restoration
current_entry = self.hosts_file.entries[self.selected_entry_index] current_entry = self.hosts_file.entries[self.selected_entry_index]
success, message = self.manager.toggle_entry( success, message = self.manager.toggle_entry(self.hosts_file, self.selected_entry_index)
self.hosts_file, self.selected_entry_index
)
if success: if success:
# Auto-save the changes immediately # Auto-save the changes immediately
save_success, save_message = self.manager.save_hosts_file(self.hosts_file) save_success, save_message = self.manager.save_hosts_file(self.hosts_file)
@ -900,18 +780,14 @@ class HostsManagerApp(App):
def action_move_entry_up(self) -> None: def action_move_entry_up(self) -> None:
"""Move the selected entry up in the list.""" """Move the selected entry up in the list."""
if not self.edit_mode: if not self.edit_mode:
self.update_status( self.update_status("❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
"❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
)
return return
if not self.hosts_file.entries: if not self.hosts_file.entries:
self.update_status("No entries to move") self.update_status("No entries to move")
return return
success, message = self.manager.move_entry_up( success, message = self.manager.move_entry_up(self.hosts_file, self.selected_entry_index)
self.hosts_file, self.selected_entry_index
)
if success: if success:
# Auto-save the changes immediately # Auto-save the changes immediately
save_success, save_message = self.manager.save_hosts_file(self.hosts_file) save_success, save_message = self.manager.save_hosts_file(self.hosts_file)
@ -922,9 +798,7 @@ class HostsManagerApp(App):
self.populate_entries_table() self.populate_entries_table()
# Update the DataTable cursor position to follow the moved entry # Update the DataTable cursor position to follow the moved entry
table = self.query_one("#entries-table", DataTable) table = self.query_one("#entries-table", DataTable)
display_index = self.actual_index_to_display_index( display_index = self.actual_index_to_display_index(self.selected_entry_index)
self.selected_entry_index
)
if table.row_count > 0 and display_index < table.row_count: if table.row_count > 0 and display_index < table.row_count:
table.move_cursor(row=display_index) table.move_cursor(row=display_index)
self.update_entry_details() self.update_entry_details()
@ -937,18 +811,14 @@ class HostsManagerApp(App):
def action_move_entry_down(self) -> None: def action_move_entry_down(self) -> None:
"""Move the selected entry down in the list.""" """Move the selected entry down in the list."""
if not self.edit_mode: if not self.edit_mode:
self.update_status( self.update_status("❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
"❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
)
return return
if not self.hosts_file.entries: if not self.hosts_file.entries:
self.update_status("No entries to move") self.update_status("No entries to move")
return return
success, message = self.manager.move_entry_down( success, message = self.manager.move_entry_down(self.hosts_file, self.selected_entry_index)
self.hosts_file, self.selected_entry_index
)
if success: if success:
# Auto-save the changes immediately # Auto-save the changes immediately
save_success, save_message = self.manager.save_hosts_file(self.hosts_file) save_success, save_message = self.manager.save_hosts_file(self.hosts_file)
@ -959,9 +829,7 @@ class HostsManagerApp(App):
self.populate_entries_table() self.populate_entries_table()
# Update the DataTable cursor position to follow the moved entry # Update the DataTable cursor position to follow the moved entry
table = self.query_one("#entries-table", DataTable) table = self.query_one("#entries-table", DataTable)
display_index = self.actual_index_to_display_index( display_index = self.actual_index_to_display_index(self.selected_entry_index)
self.selected_entry_index
)
if table.row_count > 0 and display_index < table.row_count: if table.row_count > 0 and display_index < table.row_count:
table.move_cursor(row=display_index) table.move_cursor(row=display_index)
self.update_entry_details() self.update_entry_details()
@ -974,9 +842,7 @@ class HostsManagerApp(App):
def action_save_file(self) -> None: def action_save_file(self) -> None:
"""Save the hosts file to disk.""" """Save the hosts file to disk."""
if not self.edit_mode: if not self.edit_mode:
self.update_status( self.update_status("❌ Cannot save: Application is in read-only mode. No changes to save.")
"❌ Cannot save: Application is in read-only mode. No changes to save."
)
return return
success, message = self.manager.save_hosts_file(self.hosts_file) success, message = self.manager.save_hosts_file(self.hosts_file)

View file

@ -1,122 +0,0 @@
"""
Save confirmation modal for the hosts TUI application.
This module provides a modal dialog to confirm saving changes when exiting edit entry mode.
"""
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Static, Button, Label
from textual.screen import ModalScreen
from textual.binding import Binding
class SaveConfirmationModal(ModalScreen):
"""
Modal screen for save confirmation when exiting edit entry mode.
Provides a confirmation dialog asking whether to save or discard changes.
"""
CSS = """
SaveConfirmationModal {
align: center middle;
}
.save-confirmation-container {
width: 60;
height: 15;
background: $surface;
border: thick $primary;
padding: 1;
}
.save-confirmation-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.save-confirmation-message {
text-align: center;
margin-bottom: 2;
color: $text;
}
.button-row {
align: center middle;
}
.save-confirmation-button {
margin: 0 1;
min-width: 12;
}
.save-confirmation-button:focus {
border: thick $accent;
}
"""
BINDINGS = [
Binding("escape", "cancel", "Cancel"),
Binding("enter", "save", "Save"),
Binding("s", "save", "Save"),
Binding("d", "discard", "Discard"),
]
def compose(self) -> ComposeResult:
"""Create the save confirmation modal layout."""
with Vertical(classes="save-confirmation-container"):
yield Static("Save Changes?", classes="save-confirmation-title")
yield Label(
"You have made changes to this entry.\nDo you want to save or discard them?",
classes="save-confirmation-message",
)
with Horizontal(classes="button-row"):
yield Button(
"Save (S)",
variant="primary",
id="save-button",
classes="save-confirmation-button",
)
yield Button(
"Discard (D)",
variant="default",
id="discard-button",
classes="save-confirmation-button",
)
yield Button(
"Cancel (ESC)",
variant="default",
id="cancel-button",
classes="save-confirmation-button",
)
def on_mount(self) -> None:
"""Called when the modal is mounted. Set focus to the first button."""
# Focus on the Save button by default
save_button = self.query_one("#save-button", Button)
save_button.focus()
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 == "discard-button":
self.action_discard()
elif event.button.id == "cancel-button":
self.action_cancel()
def action_save(self) -> None:
"""Save changes and close modal."""
self.dismiss("save")
def action_discard(self) -> None:
"""Discard changes and close modal."""
self.dismiss("discard")
def action_cancel(self) -> None:
"""Cancel operation and close modal."""
self.dismiss("cancel")

View file

@ -1,296 +0,0 @@
"""
Tests for the save confirmation modal.
This module tests the save confirmation functionality when exiting edit entry mode.
"""
import pytest
from unittest.mock import Mock, patch
from textual.widgets import Input, Checkbox, Button
from hosts.main import HostsManagerApp
from hosts.core.models import HostsFile, HostEntry
from hosts.tui.save_confirmation_modal import SaveConfirmationModal
class TestSaveConfirmationModal:
"""Test cases for the SaveConfirmationModal class."""
def test_modal_creation(self):
"""Test that the modal can be created."""
modal = SaveConfirmationModal()
assert modal is not None
def test_modal_compose(self):
"""Test that the modal composes correctly."""
# Note: Cannot test compose() directly without app context
# This is a basic existence check for the modal
modal = SaveConfirmationModal()
assert hasattr(modal, "compose")
assert callable(modal.compose)
def test_action_save(self):
"""Test save action dismisses with 'save'."""
modal = SaveConfirmationModal()
modal.dismiss = Mock()
modal.action_save()
modal.dismiss.assert_called_once_with("save")
def test_action_discard(self):
"""Test discard action dismisses with 'discard'."""
modal = SaveConfirmationModal()
modal.dismiss = Mock()
modal.action_discard()
modal.dismiss.assert_called_once_with("discard")
def test_action_cancel(self):
"""Test cancel action dismisses with 'cancel'."""
modal = SaveConfirmationModal()
modal.dismiss = Mock()
modal.action_cancel()
modal.dismiss.assert_called_once_with("cancel")
@patch.object(SaveConfirmationModal, "query_one")
def test_on_mount_sets_focus(self, mock_query_one):
"""Test that on_mount sets focus to the save button."""
modal = SaveConfirmationModal()
mock_save_button = Mock()
mock_query_one.return_value = mock_save_button
modal.on_mount()
mock_query_one.assert_called_once_with("#save-button", Button)
mock_save_button.focus.assert_called_once()
class TestSaveConfirmationIntegration:
"""Test cases for save confirmation integration with the main app."""
@pytest.fixture
def app(self):
"""Create a test app instance."""
return HostsManagerApp()
def test_has_entry_changes_no_original_values(self, app):
"""Test has_entry_changes returns False when no original values stored."""
app.original_entry_values = None
app.entry_edit_mode = True
assert not app.has_entry_changes()
def test_has_entry_changes_not_in_edit_mode(self, app):
"""Test has_entry_changes returns False when not in edit mode."""
app.original_entry_values = {
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": None,
"is_active": True,
}
app.entry_edit_mode = False
assert not app.has_entry_changes()
@patch.object(HostsManagerApp, "query_one")
def test_has_entry_changes_no_changes(self, mock_query_one, app):
"""Test has_entry_changes returns False when no changes made."""
# Setup original values
app.original_entry_values = {
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": None,
"is_active": True,
}
app.entry_edit_mode = True
# Mock form fields with original values
mock_ip_input = Mock()
mock_ip_input.value = "127.0.0.1"
mock_hostname_input = Mock()
mock_hostname_input.value = "localhost"
mock_comment_input = Mock()
mock_comment_input.value = ""
mock_checkbox = Mock()
mock_checkbox.value = True
def mock_query_side_effect(selector, widget_type=None):
if selector == "#ip-input":
return mock_ip_input
elif selector == "#hostname-input":
return mock_hostname_input
elif selector == "#comment-input":
return mock_comment_input
elif selector == "#active-checkbox":
return mock_checkbox
mock_query_one.side_effect = mock_query_side_effect
assert not app.has_entry_changes()
@patch.object(HostsManagerApp, "query_one")
def test_has_entry_changes_ip_changed(self, mock_query_one, app):
"""Test has_entry_changes returns True when IP address changed."""
# Setup original values
app.original_entry_values = {
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": None,
"is_active": True,
}
app.entry_edit_mode = True
# Mock form fields with changed IP
mock_ip_input = Mock()
mock_ip_input.value = "192.168.1.1" # Changed IP
mock_hostname_input = Mock()
mock_hostname_input.value = "localhost"
mock_comment_input = Mock()
mock_comment_input.value = ""
mock_checkbox = Mock()
mock_checkbox.value = True
def mock_query_side_effect(selector, widget_type=None):
if selector == "#ip-input":
return mock_ip_input
elif selector == "#hostname-input":
return mock_hostname_input
elif selector == "#comment-input":
return mock_comment_input
elif selector == "#active-checkbox":
return mock_checkbox
mock_query_one.side_effect = mock_query_side_effect
assert app.has_entry_changes()
@patch.object(HostsManagerApp, "query_one")
def test_has_entry_changes_hostname_changed(self, mock_query_one, app):
"""Test has_entry_changes returns True when hostname changed."""
# Setup original values
app.original_entry_values = {
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": None,
"is_active": True,
}
app.entry_edit_mode = True
# Mock form fields with changed hostname
mock_ip_input = Mock()
mock_ip_input.value = "127.0.0.1"
mock_hostname_input = Mock()
mock_hostname_input.value = "localhost, test.local" # Added hostname
mock_comment_input = Mock()
mock_comment_input.value = ""
mock_checkbox = Mock()
mock_checkbox.value = True
def mock_query_side_effect(selector, widget_type=None):
if selector == "#ip-input":
return mock_ip_input
elif selector == "#hostname-input":
return mock_hostname_input
elif selector == "#comment-input":
return mock_comment_input
elif selector == "#active-checkbox":
return mock_checkbox
mock_query_one.side_effect = mock_query_side_effect
assert app.has_entry_changes()
@patch.object(HostsManagerApp, "query_one")
def test_has_entry_changes_comment_added(self, mock_query_one, app):
"""Test has_entry_changes returns True when comment added."""
# Setup original values
app.original_entry_values = {
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": None,
"is_active": True,
}
app.entry_edit_mode = True
# Mock form fields with added comment
mock_ip_input = Mock()
mock_ip_input.value = "127.0.0.1"
mock_hostname_input = Mock()
mock_hostname_input.value = "localhost"
mock_comment_input = Mock()
mock_comment_input.value = "Test comment" # Added comment
mock_checkbox = Mock()
mock_checkbox.value = True
def mock_query_side_effect(selector, widget_type=None):
if selector == "#ip-input":
return mock_ip_input
elif selector == "#hostname-input":
return mock_hostname_input
elif selector == "#comment-input":
return mock_comment_input
elif selector == "#active-checkbox":
return mock_checkbox
mock_query_one.side_effect = mock_query_side_effect
assert app.has_entry_changes()
@patch.object(HostsManagerApp, "query_one")
def test_has_entry_changes_active_state_changed(self, mock_query_one, app):
"""Test has_entry_changes returns True when active state changed."""
# Setup original values
app.original_entry_values = {
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": None,
"is_active": True,
}
app.entry_edit_mode = True
# Mock form fields with changed active state
mock_ip_input = Mock()
mock_ip_input.value = "127.0.0.1"
mock_hostname_input = Mock()
mock_hostname_input.value = "localhost"
mock_comment_input = Mock()
mock_comment_input.value = ""
mock_checkbox = Mock()
mock_checkbox.value = False # Changed active state
def mock_query_side_effect(selector, widget_type=None):
if selector == "#ip-input":
return mock_ip_input
elif selector == "#hostname-input":
return mock_hostname_input
elif selector == "#comment-input":
return mock_comment_input
elif selector == "#active-checkbox":
return mock_checkbox
mock_query_one.side_effect = mock_query_side_effect
assert app.has_entry_changes()
def test_exit_edit_entry_mode(self, app):
"""Test exit_edit_entry_mode cleans up properly."""
app.entry_edit_mode = True
app.original_entry_values = {"test": "data"}
app.update_entry_details = Mock()
app.query_one = Mock()
app.update_status = Mock()
mock_table = Mock()
app.query_one.return_value = mock_table
app.exit_edit_entry_mode()
assert not app.entry_edit_mode
assert app.original_entry_values is None
app.update_entry_details.assert_called_once()
mock_table.focus.assert_called_once()
app.update_status.assert_called_once_with("Exited entry edit mode")