mirror of
				https://github.com/shokinn/hosts-go.git
				synced 2025-11-04 12:38:34 +00:00 
			
		
		
		
	Merge e813704f81 into e59af1cb02
				
					
				
			This commit is contained in:
		
						commit
						d82ddad9c9
					
				
					 6 changed files with 60 additions and 23 deletions
				
			
		| 
						 | 
					@ -15,7 +15,7 @@ func main() {
 | 
				
			||||||
		log.Fatalf("failed to parse hosts file: %v", err)
 | 
							log.Fatalf("failed to parse hosts file: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p := tea.NewProgram(tui.NewModel(hostsFile))
 | 
						p := tea.NewProgram(tui.NewModel(hostsFile, "/etc/hosts"))
 | 
				
			||||||
	if err := p.Start(); err != nil {
 | 
						if err := p.Start(); err != nil {
 | 
				
			||||||
		log.Fatalf("failed to start TUI: %v", err)
 | 
							log.Fatalf("failed to start TUI: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,17 +40,18 @@ const (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Model struct {
 | 
					type Model struct {
 | 
				
			||||||
	list   list.Model
 | 
						list      list.Model
 | 
				
			||||||
	detail viewport.Model
 | 
						detail    viewport.Model
 | 
				
			||||||
	hosts  *core.HostsFile
 | 
						hosts     *core.HostsFile
 | 
				
			||||||
	width  int
 | 
						hostsPath string
 | 
				
			||||||
	height int
 | 
						width     int
 | 
				
			||||||
	mode   Mode
 | 
						height    int
 | 
				
			||||||
	focus  pane
 | 
						mode      Mode
 | 
				
			||||||
 | 
						focus     pane
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewModel constructs the TUI model from a parsed HostsFile.
 | 
					// NewModel constructs the TUI model from a parsed HostsFile and its path.
 | 
				
			||||||
func NewModel(hf *core.HostsFile) Model {
 | 
					func NewModel(hf *core.HostsFile, path string) Model {
 | 
				
			||||||
	items := make([]list.Item, len(hf.Entries))
 | 
						items := make([]list.Item, len(hf.Entries))
 | 
				
			||||||
	for i, e := range hf.Entries {
 | 
						for i, e := range hf.Entries {
 | 
				
			||||||
		items[i] = entryItem{entry: e}
 | 
							items[i] = entryItem{entry: e}
 | 
				
			||||||
| 
						 | 
					@ -63,11 +64,12 @@ func NewModel(hf *core.HostsFile) Model {
 | 
				
			||||||
	l.SetShowPagination(false)
 | 
						l.SetShowPagination(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m := Model{
 | 
						m := Model{
 | 
				
			||||||
		list:   l,
 | 
							list:      l,
 | 
				
			||||||
		detail: viewport.New(0, 0),
 | 
							detail:    viewport.New(0, 0),
 | 
				
			||||||
		hosts:  hf,
 | 
							hosts:     hf,
 | 
				
			||||||
		mode:   ViewMode,
 | 
							hostsPath: path,
 | 
				
			||||||
		focus:  listPane,
 | 
							mode:      ViewMode,
 | 
				
			||||||
 | 
							focus:     listPane,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	m.refreshDetail()
 | 
						m.refreshDetail()
 | 
				
			||||||
	return m
 | 
						return m
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,10 @@
 | 
				
			||||||
package tui
 | 
					package tui
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"hosts-go/internal/core"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tea "github.com/charmbracelet/bubbletea"
 | 
						tea "github.com/charmbracelet/bubbletea"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +28,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				m.mode = ViewMode
 | 
									m.mode = ViewMode
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							case "ctrl+s":
 | 
				
			||||||
 | 
								if m.mode == EditMode && m.hostsPath != "" {
 | 
				
			||||||
 | 
									if err := core.WriteHostsFile(m.hostsPath, m.hosts); err != nil {
 | 
				
			||||||
 | 
										fmt.Println("save failed:", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		case " ":
 | 
							case " ":
 | 
				
			||||||
			if m.mode == EditMode {
 | 
								if m.mode == EditMode {
 | 
				
			||||||
				if entry := m.SelectedEntry(); entry != nil {
 | 
									if entry := m.SelectedEntry(); entry != nil {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,7 +53,7 @@
 | 
				
			||||||
   - 🔄 Entry modification forms with validation
 | 
					   - 🔄 Entry modification forms with validation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. **File Integration**
 | 
					2. **File Integration**
 | 
				
			||||||
   - 🔄 Connect TUI with existing parser functionality for writes
 | 
					   - ✅ Connect TUI with parser to persist changes (Ctrl+S)
 | 
				
			||||||
   - 🔄 Real-time display of actual `/etc/hosts` content
 | 
					   - 🔄 Real-time display of actual `/etc/hosts` content
 | 
				
			||||||
   - 🔄 Live validation and formatting preview
 | 
					   - 🔄 Live validation and formatting preview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@
 | 
				
			||||||
- [ ] **Permission handling**: Request sudo access when entering edit mode
 | 
					- [ ] **Permission handling**: Request sudo access when entering edit mode
 | 
				
			||||||
- [x] **Entry modification**: Toggle active status
 | 
					- [x] **Entry modification**: Toggle active status
 | 
				
			||||||
- [ ] **Entry modification**: Add, edit, delete operations
 | 
					- [ ] **Entry modification**: Add, edit, delete operations
 | 
				
			||||||
- [ ] **File writing**: Atomic updates with backup and rollback
 | 
					- [x] **File writing**: Atomic updates with backup and rollback
 | 
				
			||||||
- [ ] **Input validation**: Real-time validation of IP and hostname inputs
 | 
					- [ ] **Input validation**: Real-time validation of IP and hostname inputs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 🌐 Advanced Features (Phase 4)
 | 
					### 🌐 Advanced Features (Phase 4)
 | 
				
			||||||
| 
						 | 
					@ -77,10 +77,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)**
 | 
					### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)**
 | 
				
			||||||
- **Completion**: ~80% (parser, TUI, and basic edit mode implemented)
 | 
					- **Completion**: ~80% (parser, TUI, and basic edit mode implemented)
 | 
				
			||||||
- **Active work**: Expand edit mode with file integration and advanced editing
 | 
					- **Active work**: Expand edit mode with advanced editing features
 | 
				
			||||||
- **Blockers**: None - comprehensive parser foundation with 54 tests completed
 | 
					- **Blockers**: None - comprehensive parser foundation with 54 tests completed
 | 
				
			||||||
- **Parser status**: Production-ready with all safety features implemented
 | 
					- **Parser status**: Production-ready with all safety features implemented
 | 
				
			||||||
- **Key bindings updated**: `ctrl+e` for edit mode, spacebar to toggle entries
 | 
					- **Key bindings updated**: `ctrl+e` for edit mode, spacebar to toggle entries, `ctrl+s` to save
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Development Readiness
 | 
					### Development Readiness
 | 
				
			||||||
- ✅ **Architecture designed**: Clear technical approach documented
 | 
					- ✅ **Architecture designed**: Clear technical approach documented
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
package tests
 | 
					package tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +20,7 @@ func TestModelSelection(t *testing.T) {
 | 
				
			||||||
	hf, _, err := core.ParseHostsContent(lines)
 | 
						hf, _, err := core.ParseHostsContent(lines)
 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m := tui.NewModel(hf)
 | 
						m := tui.NewModel(hf, "")
 | 
				
			||||||
	require.NotNil(t, m.SelectedEntry())
 | 
						require.NotNil(t, m.SelectedEntry())
 | 
				
			||||||
	assert.Equal(t, "localhost", m.SelectedEntry().Hostname)
 | 
						assert.Equal(t, "localhost", m.SelectedEntry().Hostname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +37,7 @@ func TestViewModeStatusBar(t *testing.T) {
 | 
				
			||||||
	hf, _, err := core.ParseHostsContent(lines)
 | 
						hf, _, err := core.ParseHostsContent(lines)
 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m := tui.NewModel(hf)
 | 
						m := tui.NewModel(hf, "")
 | 
				
			||||||
	nm, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
 | 
						nm, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
 | 
				
			||||||
	m = nm.(tui.Model)
 | 
						m = nm.(tui.Model)
 | 
				
			||||||
	view := m.View()
 | 
						view := m.View()
 | 
				
			||||||
| 
						 | 
					@ -52,7 +53,7 @@ func TestPaneSwitching(t *testing.T) {
 | 
				
			||||||
	hf, _, err := core.ParseHostsContent(lines)
 | 
						hf, _, err := core.ParseHostsContent(lines)
 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m := tui.NewModel(hf)
 | 
						m := tui.NewModel(hf, "")
 | 
				
			||||||
	// Switch focus to detail pane
 | 
						// Switch focus to detail pane
 | 
				
			||||||
	nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab})
 | 
						nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab})
 | 
				
			||||||
	m = nm.(tui.Model)
 | 
						m = nm.(tui.Model)
 | 
				
			||||||
| 
						 | 
					@ -69,7 +70,7 @@ func TestEditModeToggleAndActivation(t *testing.T) {
 | 
				
			||||||
	hf, _, err := core.ParseHostsContent(lines)
 | 
						hf, _, err := core.ParseHostsContent(lines)
 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m := tui.NewModel(hf)
 | 
						m := tui.NewModel(hf, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// enter edit mode
 | 
						// enter edit mode
 | 
				
			||||||
	nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlE})
 | 
						nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlE})
 | 
				
			||||||
| 
						 | 
					@ -85,3 +86,27 @@ func TestEditModeToggleAndActivation(t *testing.T) {
 | 
				
			||||||
	view := m.View()
 | 
						view := m.View()
 | 
				
			||||||
	assert.Contains(t, view, "EDIT MODE")
 | 
						assert.Contains(t, view, "EDIT MODE")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSaveWritesToFile(t *testing.T) {
 | 
				
			||||||
 | 
						content := "127.0.0.1 localhost\n192.168.1.10 example.com\n"
 | 
				
			||||||
 | 
						tmp, err := os.CreateTemp(t.TempDir(), "hosts")
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						_, err = tmp.WriteString(content)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						require.NoError(t, tmp.Close())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hf, _, err := core.ParseHostsFile(tmp.Name())
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m := tui.NewModel(hf, tmp.Name())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlE})
 | 
				
			||||||
 | 
						m = nm.(tui.Model)
 | 
				
			||||||
 | 
						nm, _ = m.Update(tea.KeyMsg{Type: tea.KeySpace})
 | 
				
			||||||
 | 
						m = nm.(tui.Model)
 | 
				
			||||||
 | 
						_, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						saved, _, err := core.ParseHostsFile(tmp.Name())
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assert.False(t, saved.Entries[0].Active)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue