Compare commits
5 commits
5a117fb624
...
b0abec730c
Author | SHA1 | Date | |
---|---|---|---|
b0abec730c | |||
0ce58cbe68 | |||
1dd54b0cb5 | |||
77d8e647f2 | |||
f7671db43e |
9 changed files with 958 additions and 315 deletions
|
@ -1,6 +0,0 @@
|
|||
def main():
|
||||
print("Hello from hosts!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -2,10 +2,59 @@
|
|||
|
||||
## Current Work Focus
|
||||
|
||||
**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.
|
||||
**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
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
- ✅ **Advanced configuration system**: Complete Config class with JSON persistence to ~/.config/hosts-manager/
|
||||
- ✅ **Professional configuration modal**: Modal dialog with keyboard bindings for settings management
|
||||
|
@ -14,7 +63,7 @@
|
|||
- ✅ **Rich visual interface**: Color-coded entries with professional DataTable styling
|
||||
- ✅ **Interactive column headers**: Click headers to sort data with visual feedback
|
||||
- ✅ **Enhanced status bar**: Detailed information including entry counts and file path
|
||||
- ✅ **Comprehensive testing**: 97 tests covering all functionality including new features
|
||||
- ✅ **Comprehensive testing**: 149 tests covering all functionality including new features
|
||||
|
||||
### Current Project State
|
||||
- **Production-ready application**: `uv run hosts` launches polished TUI with advanced features
|
||||
|
@ -22,38 +71,24 @@
|
|||
- **Professional visual design**: Color-coded entries, zebra striping, and rich text styling
|
||||
- **Interactive sorting**: Click column headers or use keyboard shortcuts to sort data
|
||||
- **Intelligent filtering**: Hide default system entries based on user preference
|
||||
- **Comprehensive test coverage**: 97 tests with 100% pass rate covering all components
|
||||
- **Perfect code quality**: All linting and formatting standards maintained
|
||||
- **Robust architecture**: Clean layered design ready for edit mode extension
|
||||
- **Comprehensive test coverage**: 149 tests with 100% pass rate covering all components
|
||||
- **Code quality maintenance needed**: 20 linting issues (unused imports/variables) require cleanup
|
||||
- **Robust architecture**: Clean layered design ready for Phase 4 advanced features
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 3: Edit Mode Foundation (Current Priority)
|
||||
1. **Permission management system**:
|
||||
- Implement sudo request and validation
|
||||
- Edit mode toggle with proper security handling
|
||||
- Permission validation and error handling
|
||||
- Graceful fallback for permission denied scenarios
|
||||
### Immediate Priority: Code Quality Cleanup
|
||||
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
|
||||
|
||||
2. **Basic editing operations**:
|
||||
- Toggle entries active/inactive with visual feedback
|
||||
- Entry editing interface for IP addresses, hostnames, and comments
|
||||
- Real-time validation of IP addresses and hostnames
|
||||
- Safe state management during editing
|
||||
2. **Code quality validation**:
|
||||
- Ensure all ruff checks pass with zero issues
|
||||
- Maintain perfect test coverage (149 tests passing)
|
||||
- Verify application functionality after cleanup
|
||||
|
||||
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)
|
||||
### Phase 4: Advanced Edit Features (Next Phase)
|
||||
1. **Advanced editing operations**:
|
||||
- Add new entries with validation
|
||||
- Delete entries with confirmation
|
||||
|
@ -87,8 +122,37 @@
|
|||
- ✅ **Error handling**: Graceful degradation and user feedback throughout
|
||||
- ✅ **Modal pattern**: Professional modal dialogs with proper lifecycle management
|
||||
- ✅ **Configuration pattern**: Centralized settings with persistence and defaults
|
||||
- 🔄 **Command pattern**: Planned for Phase 3 edit operations with undo/redo
|
||||
- 🔄 **Observer pattern**: Will implement for state change notifications in edit mode
|
||||
- ✅ **Command pattern**: Implemented for edit operations with save confirmation
|
||||
- 🔄 **Observer pattern**: Will implement for state change notifications in advanced features
|
||||
|
||||
## 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
|
||||
- ✅ **Python 3.13+**: Excellent choice with modern features working perfectly
|
||||
|
|
|
@ -39,16 +39,6 @@
|
|||
- ✅ **Modal system**: Proper modal dialogs with keyboard bindings
|
||||
- ✅ **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
|
||||
- ✅ **Permission management**: Sudo request and management with PermissionManager class
|
||||
- ✅ **Edit mode toggle**: Switch between read-only and edit modes with 'e' key
|
||||
|
@ -64,7 +54,23 @@
|
|||
- ✅ **Live testing**: Manual testing confirms all functionality works correctly
|
||||
- ✅ **Human-readable formatting**: Tab-based column alignment with proper spacing
|
||||
- ✅ **Management header**: Automatic addition of management header to hosts files
|
||||
- ✅ **Auto-save functionality**: Immediate saving of changes when entries are edited
|
||||
- ✅ **Save confirmation modal**: Professional modal dialog for save/discard/cancel when exiting edit entry mode
|
||||
- ✅ **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
|
||||
- ❌ **Add new entries**: Create new host entries
|
||||
|
@ -89,19 +95,9 @@
|
|||
## Current Status
|
||||
|
||||
### Development Stage
|
||||
**Stage**: Phase 3 Complete - Moving to Phase 4
|
||||
**Progress**: 75% (Complete edit mode foundation with permission management)
|
||||
**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
|
||||
**Stage**: Phase 3 Complete with Code Quality Maintenance Required
|
||||
**Progress**: 82% (Complete edit mode foundation with save confirmation, code cleanup needed)
|
||||
**Next Milestone**: Code quality cleanup, then Phase 4 advanced edit features
|
||||
|
||||
### Phase 3 Final Achievements ✅ COMPLETE
|
||||
1. ✅ **Permission management**: Complete PermissionManager class with sudo request and validation
|
||||
|
@ -112,18 +108,17 @@
|
|||
6. ✅ **Manager module**: Complete HostsManager class for all edit operations
|
||||
7. ✅ **Safe file operations**: Atomic file writing with rollback capability
|
||||
8. ✅ **Enhanced error messages**: Professional read-only mode error messages with clear instructions
|
||||
9. ✅ **Comprehensive testing**: 38 new tests for manager module (135 total tests)
|
||||
9. ✅ **Comprehensive testing**: Full test coverage for manager module (149 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
|
||||
|
||||
### Recent Major Accomplishments
|
||||
- ✅ **Complete Phase 3 implementation**: Full edit mode foundation with permission management
|
||||
- ✅ **Manager module**: PermissionManager and HostsManager classes with comprehensive functionality
|
||||
- ✅ **Edit mode integration**: Seamless integration with main TUI application
|
||||
- ✅ **Permission system**: Robust sudo request, validation, and release functionality
|
||||
- ✅ **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)
|
||||
### Current Project State
|
||||
- **Application functionality**: Fully functional with advanced edit capabilities
|
||||
- **Test coverage**: 149 tests with 100% pass rate covering all functionality
|
||||
- **Code quality issue**: 20 linting issues (unused imports/variables) need cleanup
|
||||
- **Architecture**: Robust layered design ready for Phase 4 advanced features
|
||||
- **User experience**: Professional TUI with modal dialogs and comprehensive keyboard shortcuts
|
||||
|
||||
## Technical Implementation Details
|
||||
|
||||
|
@ -136,11 +131,13 @@
|
|||
### Test Coverage Excellence
|
||||
- **Models**: 27 comprehensive tests covering all data model edge cases
|
||||
- **Parser**: 15 tests covering file operations, permissions, and error conditions
|
||||
- **Coverage**: 100% of core functionality with edge case validation
|
||||
- **Manager**: Comprehensive tests for permission management and edit operations
|
||||
- **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
|
||||
|
||||
### Code Quality Standards
|
||||
- **Linting**: Perfect ruff compliance with zero issues
|
||||
- **Linting**: 20 minor issues (unused imports/variables) requiring cleanup
|
||||
- **Type hints**: Complete type coverage throughout entire codebase
|
||||
- **Documentation**: Comprehensive docstrings and inline comments
|
||||
- **Error handling**: Graceful exception handling with user feedback
|
||||
|
@ -148,7 +145,14 @@
|
|||
|
||||
## Known Issues
|
||||
|
||||
### Phase 3 Enhancement Opportunities
|
||||
### Immediate Code Quality Issues
|
||||
- **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)
|
||||
- **Help modal**: Proper help dialog instead of status message (planned for Phase 4)
|
||||
- **Large file performance**: Not yet tested with very large hosts files
|
||||
|
|
|
@ -81,7 +81,7 @@ hosts/
|
|||
- 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).
|
||||
|
||||
### Implemented Tests (97 tests total)
|
||||
### Implemented Tests (149 tests total)
|
||||
|
||||
1. **Parsing Tests** (15 tests):
|
||||
- Parse simple `/etc/hosts` with comments and disabled entries
|
||||
|
@ -101,20 +101,39 @@ hosts/
|
|||
- Configuration loading and saving
|
||||
- Default entry detection and filtering
|
||||
|
||||
4. **Modal Dialog Tests** (15 tests):
|
||||
- Configuration modal lifecycle
|
||||
- User interaction handling
|
||||
- Keyboard binding validation
|
||||
- State management during configuration changes
|
||||
|
||||
5. **Main Application Tests** (18 tests):
|
||||
- Application initialization and startup
|
||||
4. **TUI Application Tests** (28 tests):
|
||||
- Main application initialization and startup
|
||||
- File loading and error handling
|
||||
- User interface state management
|
||||
- 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)
|
||||
- **Edit Mode Tests**: Permission management and file modification
|
||||
- **Advanced Edit Tests**: Add/delete entries, bulk operations
|
||||
- **DNS Resolution Tests**: Hostname resolution and IP comparison
|
||||
- **Performance Tests**: Large file handling and optimization
|
||||
- **Integration Tests**: End-to-end workflow testing
|
||||
- **Search Functionality Tests**: Entry searching and filtering
|
||||
|
|
|
@ -34,8 +34,9 @@
|
|||
#### System Layer (Implemented)
|
||||
- ✅ **File I/O**: Atomic file operations with backup support
|
||||
- ✅ **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
|
||||
- 🔄 **Permission Management**: Sudo handling planned for Phase 3 edit mode
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
|
@ -93,10 +94,13 @@ class Config:
|
|||
- ✅ **Reactive state**: Using Textual's reactive attributes for complex UI updates
|
||||
- ✅ **Configuration state**: Persistent settings with JSON storage and graceful error handling
|
||||
- ✅ **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
|
||||
- ✅ **File integrity**: Atomic operations preserve file structure
|
||||
- ✅ **Error handling**: Graceful degradation for all error conditions
|
||||
- ✅ **Modal state**: Professional modal dialog lifecycle management
|
||||
- ✅ **Change detection**: Intelligent tracking for save confirmation
|
||||
- 🔄 **Undo/Redo capability**: Planned for Phase 4 with command pattern
|
||||
- 🔄 **Dirty state tracking**: Will be implemented in Phase 3 edit mode
|
||||
|
||||
|
|
|
@ -32,13 +32,13 @@ hosts/
|
|||
### Current State
|
||||
- ✅ **Complete uv project**: Python 3.13 with full dependency management
|
||||
- ✅ **Production application**: Fully functional TUI with advanced features and professional interface
|
||||
- ✅ **Perfect code quality**: All ruff checks passing with zero issues
|
||||
- ✅ **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
|
||||
- ✅ **Comprehensive testing**: 97 tests covering all functionality including new features
|
||||
- ✅ **Comprehensive testing**: 149 tests covering all functionality including new features
|
||||
- ✅ **Entry point configured**: `hosts` command launches application perfectly
|
||||
- ✅ **Configuration system**: Complete settings management with JSON persistence
|
||||
- ✅ **Modal interface**: Professional configuration dialogs with keyboard bindings
|
||||
- ✅ **Advanced features**: Sorting, filtering, and rich visual interface
|
||||
- ✅ **Advanced features**: Sorting, filtering, edit mode, and save confirmation
|
||||
|
||||
### Runtime Management
|
||||
- ✅ **uv run hosts**: Command executes application instantly
|
||||
|
@ -94,11 +94,17 @@ hosts = "hosts.main:main"
|
|||
|
||||
### Development Workflow
|
||||
1. ✅ **uv run hosts**: Execute the application - launches instantly
|
||||
2. ✅ **uv run ruff check**: Lint code - all checks passing perfectly
|
||||
2. 🔧 **uv run ruff check**: Lint code - 20 issues need fixing
|
||||
3. ✅ **uv run ruff format**: Auto-format code - consistent style maintained
|
||||
4. ✅ **uv run pytest**: Run test suite - 42 tests passing with 100% success rate
|
||||
4. ✅ **uv run pytest**: Run test suite - 149 tests passing with 100% success rate
|
||||
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
|
||||
- ✅ **ruff configuration**: Perfect compliance with zero issues across all modules
|
||||
- ✅ **Type hints**: Complete type coverage throughout entire codebase including new components
|
||||
|
|
|
@ -18,6 +18,7 @@ from .core.models import HostsFile
|
|||
from .core.config import Config
|
||||
from .core.manager import HostsManager
|
||||
from .tui.config_modal import ConfigModal
|
||||
from .tui.save_confirmation_modal import SaveConfirmationModal
|
||||
|
||||
|
||||
class HostsManagerApp(App):
|
||||
|
@ -151,6 +152,9 @@ class HostsManagerApp(App):
|
|||
self.title = "Hosts Manager"
|
||||
self.sub_title = "Read-only mode"
|
||||
|
||||
# Track original entry values for change detection
|
||||
self.original_entry_values = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create the application layout."""
|
||||
yield Header()
|
||||
|
@ -173,7 +177,9 @@ class HostsManagerApp(App):
|
|||
yield Label("Hostname:")
|
||||
yield Input(id="hostname-input", placeholder="Enter hostname")
|
||||
yield Label("Comment:")
|
||||
yield Input(id="comment-input", placeholder="Enter comment (optional)")
|
||||
yield Input(
|
||||
id="comment-input", placeholder="Enter comment (optional)"
|
||||
)
|
||||
yield Label("Active:")
|
||||
yield Checkbox(id="active-checkbox", value=True)
|
||||
yield right_pane
|
||||
|
@ -191,7 +197,9 @@ class HostsManagerApp(App):
|
|||
"""Load the hosts file and populate the interface."""
|
||||
# Remember current selection for restoration
|
||||
current_entry = None
|
||||
if self.hosts_file.entries and self.selected_entry_index < len(self.hosts_file.entries):
|
||||
if self.hosts_file.entries and self.selected_entry_index < len(
|
||||
self.hosts_file.entries
|
||||
):
|
||||
current_entry = self.hosts_file.entries[self.selected_entry_index]
|
||||
|
||||
try:
|
||||
|
@ -221,7 +229,9 @@ class HostsManagerApp(App):
|
|||
for entry in self.hosts_file.entries:
|
||||
canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
|
||||
# Skip default entries if configured to hide them
|
||||
if not show_defaults and self.config.is_default_entry(entry.ip_address, canonical_hostname):
|
||||
if not show_defaults and self.config.is_default_entry(
|
||||
entry.ip_address, canonical_hostname
|
||||
):
|
||||
continue
|
||||
visible_entries.append(entry)
|
||||
|
||||
|
@ -234,7 +244,9 @@ class HostsManagerApp(App):
|
|||
for i, entry in enumerate(self.hosts_file.entries):
|
||||
canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
|
||||
# Skip default entries if configured to hide them
|
||||
if not show_defaults and self.config.is_default_entry(entry.ip_address, canonical_hostname):
|
||||
if not show_defaults and self.config.is_default_entry(
|
||||
entry.ip_address, canonical_hostname
|
||||
):
|
||||
continue
|
||||
return i
|
||||
|
||||
|
@ -340,9 +352,11 @@ class HostsManagerApp(App):
|
|||
else:
|
||||
# Try to find the same entry in the reloaded file
|
||||
for i, entry in enumerate(self.hosts_file.entries):
|
||||
if (entry.ip_address == previous_entry.ip_address and
|
||||
entry.hostnames == previous_entry.hostnames and
|
||||
entry.comment == previous_entry.comment):
|
||||
if (
|
||||
entry.ip_address == previous_entry.ip_address
|
||||
and entry.hostnames == previous_entry.hostnames
|
||||
and entry.comment == previous_entry.comment
|
||||
):
|
||||
self.selected_entry_index = i
|
||||
break
|
||||
else:
|
||||
|
@ -390,8 +404,12 @@ class HostsManagerApp(App):
|
|||
show_defaults = self.config.should_show_default_entries()
|
||||
if not show_defaults:
|
||||
# Check if the currently selected entry is a default entry (hidden)
|
||||
if (self.selected_entry_index < len(self.hosts_file.entries) and
|
||||
self.hosts_file.entries[self.selected_entry_index].is_default_entry()):
|
||||
if (
|
||||
self.selected_entry_index < len(self.hosts_file.entries)
|
||||
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
|
||||
if visible_entries:
|
||||
# Find the first visible entry in the hosts file
|
||||
|
@ -415,7 +433,9 @@ class HostsManagerApp(App):
|
|||
if entry.is_default_entry():
|
||||
details_lines.append("")
|
||||
details_lines.append("⚠️ SYSTEM DEFAULT ENTRY")
|
||||
details_lines.append("This is a default system entry and cannot be modified.")
|
||||
details_lines.append(
|
||||
"This is a default system entry and cannot be modified."
|
||||
)
|
||||
|
||||
if entry.comment:
|
||||
details_lines.append(f"Comment: {entry.comment}")
|
||||
|
@ -434,7 +454,9 @@ class HostsManagerApp(App):
|
|||
details_widget.add_class("hidden")
|
||||
edit_form.remove_class("hidden")
|
||||
|
||||
if not self.hosts_file.entries or self.selected_entry_index >= len(self.hosts_file.entries):
|
||||
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]
|
||||
|
@ -446,7 +468,7 @@ class HostsManagerApp(App):
|
|||
active_checkbox = self.query_one("#active-checkbox", Checkbox)
|
||||
|
||||
ip_input.value = entry.ip_address
|
||||
hostname_input.value = ', '.join(entry.hostnames)
|
||||
hostname_input.value = ", ".join(entry.hostnames)
|
||||
comment_input.value = entry.comment or ""
|
||||
active_checkbox.value = entry.is_active
|
||||
|
||||
|
@ -483,7 +505,7 @@ class HostsManagerApp(App):
|
|||
|
||||
# Add file info
|
||||
file_info = self.parser.get_file_info()
|
||||
if file_info['exists']:
|
||||
if file_info["exists"]:
|
||||
status_text += f" | {file_info['path']}"
|
||||
|
||||
status_widget.update(status_text)
|
||||
|
@ -492,14 +514,18 @@ class HostsManagerApp(App):
|
|||
"""Handle row highlighting (cursor movement) in the DataTable."""
|
||||
if event.data_table.id == "entries-table":
|
||||
# Convert display index to actual index
|
||||
self.selected_entry_index = self.display_index_to_actual_index(event.cursor_row)
|
||||
self.selected_entry_index = self.display_index_to_actual_index(
|
||||
event.cursor_row
|
||||
)
|
||||
self.update_entry_details()
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Handle row selection in the DataTable."""
|
||||
if event.data_table.id == "entries-table":
|
||||
# Convert display index to actual index
|
||||
self.selected_entry_index = self.display_index_to_actual_index(event.cursor_row)
|
||||
self.selected_entry_index = self.display_index_to_actual_index(
|
||||
event.cursor_row
|
||||
)
|
||||
self.update_entry_details()
|
||||
|
||||
def action_reload(self) -> None:
|
||||
|
@ -513,10 +539,13 @@ class HostsManagerApp(App):
|
|||
def action_help(self) -> None:
|
||||
"""Show help information."""
|
||||
# For now, just update the status with help info
|
||||
self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config, e Edit")
|
||||
self.update_status(
|
||||
"Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config, e Edit"
|
||||
)
|
||||
|
||||
def action_config(self) -> None:
|
||||
"""Show configuration modal."""
|
||||
|
||||
def handle_config_result(config_changed: bool) -> None:
|
||||
if config_changed:
|
||||
# Reload the table to apply new filtering
|
||||
|
@ -590,7 +619,9 @@ class HostsManagerApp(App):
|
|||
def action_edit_entry(self) -> None:
|
||||
"""Enter edit mode for the selected entry."""
|
||||
if not self.edit_mode:
|
||||
self.update_status("❌ Cannot edit entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
|
||||
self.update_status(
|
||||
"❌ Cannot edit entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
|
||||
)
|
||||
return
|
||||
|
||||
if not self.hosts_file.entries:
|
||||
|
@ -606,6 +637,14 @@ class HostsManagerApp(App):
|
|||
self.update_status("❌ Cannot edit system default entry")
|
||||
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.update_entry_details()
|
||||
|
||||
|
@ -615,17 +654,148 @@ class HostsManagerApp(App):
|
|||
|
||||
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:
|
||||
"""Exit entry edit mode and return focus to the entries table."""
|
||||
if self.entry_edit_mode:
|
||||
self.entry_edit_mode = False
|
||||
self.update_entry_details()
|
||||
if not self.entry_edit_mode:
|
||||
return
|
||||
|
||||
# Return focus to the entries table
|
||||
# Check if there are unsaved changes
|
||||
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.focus()
|
||||
|
||||
self.update_status("Exited entry edit mode")
|
||||
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")
|
||||
return True
|
||||
else:
|
||||
self.update_status(f"❌ Error saving entry: {message}")
|
||||
return False
|
||||
|
||||
def action_next_field(self) -> None:
|
||||
"""Move to the next field in edit mode."""
|
||||
|
@ -637,7 +807,7 @@ class HostsManagerApp(App):
|
|||
self.query_one("#ip-input", Input),
|
||||
self.query_one("#hostname-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
|
||||
|
@ -657,7 +827,7 @@ class HostsManagerApp(App):
|
|||
self.query_one("#ip-input", Input),
|
||||
self.query_one("#hostname-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
|
||||
|
@ -669,90 +839,38 @@ class HostsManagerApp(App):
|
|||
|
||||
def on_key(self, event) -> None:
|
||||
"""Handle key events to override default tab behavior in edit mode."""
|
||||
if self.entry_edit_mode and event.key == "tab":
|
||||
# Only handle custom tab navigation if in entry edit mode AND no modal is open
|
||||
if self.entry_edit_mode and len(self.screen_stack) == 1 and event.key == "tab":
|
||||
# Prevent default tab behavior and use our custom navigation
|
||||
event.prevent_default()
|
||||
self.action_next_field()
|
||||
elif self.entry_edit_mode and event.key == "shift+tab":
|
||||
elif (
|
||||
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
|
||||
event.prevent_default()
|
||||
self.action_prev_field()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Handle input field changes and auto-save."""
|
||||
if not self.entry_edit_mode or not self.edit_mode:
|
||||
return
|
||||
|
||||
if event.input.id in ["ip-input", "hostname-input", "comment-input"]:
|
||||
self.save_entry_changes()
|
||||
"""Handle input field changes (no auto-save - changes saved on exit)."""
|
||||
# Input changes are tracked but not automatically saved
|
||||
# Changes will be validated and saved when exiting edit mode
|
||||
pass
|
||||
|
||||
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
||||
"""Handle checkbox changes and auto-save."""
|
||||
if not self.entry_edit_mode or not self.edit_mode:
|
||||
return
|
||||
|
||||
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}")
|
||||
"""Handle checkbox changes (no auto-save - changes saved on exit)."""
|
||||
# Checkbox changes are tracked but not automatically saved
|
||||
# Changes will be validated and saved when exiting edit mode
|
||||
pass
|
||||
|
||||
def action_toggle_entry(self) -> None:
|
||||
"""Toggle the active state of the selected entry."""
|
||||
if not self.edit_mode:
|
||||
self.update_status("❌ Cannot toggle entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
|
||||
self.update_status(
|
||||
"❌ Cannot toggle entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
|
||||
)
|
||||
return
|
||||
|
||||
if not self.hosts_file.entries:
|
||||
|
@ -762,7 +880,9 @@ class HostsManagerApp(App):
|
|||
# Remember current entry for cursor position restoration
|
||||
current_entry = self.hosts_file.entries[self.selected_entry_index]
|
||||
|
||||
success, message = self.manager.toggle_entry(self.hosts_file, self.selected_entry_index)
|
||||
success, message = self.manager.toggle_entry(
|
||||
self.hosts_file, self.selected_entry_index
|
||||
)
|
||||
if success:
|
||||
# Auto-save the changes immediately
|
||||
save_success, save_message = self.manager.save_hosts_file(self.hosts_file)
|
||||
|
@ -780,14 +900,18 @@ class HostsManagerApp(App):
|
|||
def action_move_entry_up(self) -> None:
|
||||
"""Move the selected entry up in the list."""
|
||||
if not self.edit_mode:
|
||||
self.update_status("❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
|
||||
self.update_status(
|
||||
"❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
|
||||
)
|
||||
return
|
||||
|
||||
if not self.hosts_file.entries:
|
||||
self.update_status("No entries to move")
|
||||
return
|
||||
|
||||
success, message = self.manager.move_entry_up(self.hosts_file, self.selected_entry_index)
|
||||
success, message = self.manager.move_entry_up(
|
||||
self.hosts_file, self.selected_entry_index
|
||||
)
|
||||
if success:
|
||||
# Auto-save the changes immediately
|
||||
save_success, save_message = self.manager.save_hosts_file(self.hosts_file)
|
||||
|
@ -798,7 +922,9 @@ class HostsManagerApp(App):
|
|||
self.populate_entries_table()
|
||||
# Update the DataTable cursor position to follow the moved entry
|
||||
table = self.query_one("#entries-table", DataTable)
|
||||
display_index = self.actual_index_to_display_index(self.selected_entry_index)
|
||||
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_entry_details()
|
||||
|
@ -811,14 +937,18 @@ class HostsManagerApp(App):
|
|||
def action_move_entry_down(self) -> None:
|
||||
"""Move the selected entry down in the list."""
|
||||
if not self.edit_mode:
|
||||
self.update_status("❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
|
||||
self.update_status(
|
||||
"❌ Cannot move entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
|
||||
)
|
||||
return
|
||||
|
||||
if not self.hosts_file.entries:
|
||||
self.update_status("No entries to move")
|
||||
return
|
||||
|
||||
success, message = self.manager.move_entry_down(self.hosts_file, self.selected_entry_index)
|
||||
success, message = self.manager.move_entry_down(
|
||||
self.hosts_file, self.selected_entry_index
|
||||
)
|
||||
if success:
|
||||
# Auto-save the changes immediately
|
||||
save_success, save_message = self.manager.save_hosts_file(self.hosts_file)
|
||||
|
@ -829,7 +959,9 @@ class HostsManagerApp(App):
|
|||
self.populate_entries_table()
|
||||
# Update the DataTable cursor position to follow the moved entry
|
||||
table = self.query_one("#entries-table", DataTable)
|
||||
display_index = self.actual_index_to_display_index(self.selected_entry_index)
|
||||
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_entry_details()
|
||||
|
@ -842,7 +974,9 @@ class HostsManagerApp(App):
|
|||
def action_save_file(self) -> None:
|
||||
"""Save the hosts file to disk."""
|
||||
if not self.edit_mode:
|
||||
self.update_status("❌ Cannot save: Application is in read-only mode. No changes to save.")
|
||||
self.update_status(
|
||||
"❌ Cannot save: Application is in read-only mode. No changes to save."
|
||||
)
|
||||
return
|
||||
|
||||
success, message = self.manager.save_hosts_file(self.hosts_file)
|
||||
|
|
122
src/hosts/tui/save_confirmation_modal.py
Normal file
122
src/hosts/tui/save_confirmation_modal.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
"""
|
||||
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")
|
296
tests/test_save_confirmation_modal.py
Normal file
296
tests/test_save_confirmation_modal.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
"""
|
||||
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")
|
Loading…
Add table
Add a link
Reference in a new issue