From 72e6ad3bef8911a821e3b61b7407bb870e5ee896 Mon Sep 17 00:00:00 2001 From: Filippo Trotter Date: Thu, 11 Jul 2024 15:47:56 +0200 Subject: [PATCH 1/6] fix: remove terminal resize problems during file selection --- cmd/root.go | 2 +- utils/server/server.go | 2 +- utils/tui/model_test.go | 14 ++++----- utils/tui/modelutils/file.go | 47 +++++++++++++++++++++++++------ utils/tui/modelutils/file_test.go | 32 ++++++++++----------- 5 files changed, 63 insertions(+), 34 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index ceab267..21b703d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -100,7 +100,7 @@ func ReadFlags(cmd *cobra.Command) { // Initialize your model with the current directory model := tui.Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(currentDir, 20), + FilesSelector: modelutils.InitialModel(currentDir, 20, 20), } clearScreen() // Bubble Tea program diff --git a/utils/server/server.go b/utils/server/server.go index ea7defd..112cc11 100644 --- a/utils/server/server.go +++ b/utils/server/server.go @@ -44,7 +44,7 @@ func StartServer() { // Initialize the file selector model with the directory argument model := tui.Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(dir, pty.Window.Height-5), // Initialize the FilesSelector model with window height + FilesSelector: modelutils.InitialModel(dir, pty.Window.Height-5, pty.Window.Width-5), // Initialize the FilesSelector model with window height } if model.Error != nil { wish.Println(s, model.Error.Error()) diff --git a/utils/tui/model_test.go b/utils/tui/model_test.go index e5fadc1..c9652ce 100644 --- a/utils/tui/model_test.go +++ b/utils/tui/model_test.go @@ -13,7 +13,7 @@ import ( func TestModel(t *testing.T) { t.Run("Init", func(t *testing.T) { - model := Model{FilesSelector: modelutils.InitialModel(".", 10)} + model := Model{FilesSelector: modelutils.InitialModel(".", 10, 10)} cmd := model.Init() assert.Nil(t, cmd) }) @@ -32,7 +32,7 @@ func TestModel(t *testing.T) { name: "FileSelection to ModeSelection", model: Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(".", 10), + FilesSelector: modelutils.InitialModel(".", 10, 10), }, setup: func(m *Model) { m.FilesSelector.FilesPath = []string{"path/test/file1", "path/test/file2"} @@ -51,7 +51,7 @@ func TestModel(t *testing.T) { name: "FileSelection to ActionSelection", model: Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(".", 10), + FilesSelector: modelutils.InitialModel(".", 10, 10), }, setup: func(m *Model) { m.FilesSelector.FilesPath = []string{"path/test/file1"} @@ -69,7 +69,7 @@ func TestModel(t *testing.T) { name: "No file selected", model: Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(".", 10), + FilesSelector: modelutils.InitialModel(".", 10, 10), }, msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, verify: func(t *testing.T, m Model) { @@ -156,7 +156,7 @@ func TestModel(t *testing.T) { name: "ModeSelection to FileSelection", model: Model{ State: "ModeSelection", - FilesSelector: modelutils.InitialModel(".", 10), + FilesSelector: modelutils.InitialModel(".", 10, 10), SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", ""), Files: []string{"file1.txt", "file2.txt"}, }, @@ -174,7 +174,7 @@ func TestModel(t *testing.T) { name: "ActionSelection to FileSelection", model: Model{ State: "ActionSelection", - FilesSelector: modelutils.InitialModel(".", 10), + FilesSelector: modelutils.InitialModel(".", 10, 10), SpeedSelector: modelutils.ModeSelector{Selected: "Fast mode"}, ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", ""), Files: []string{"file1.txt"}, @@ -424,7 +424,7 @@ func TestModel(t *testing.T) { tests := []viewTest{ { name: "FileSelection View", - model: Model{State: "FileSelection", FilesSelector: modelutils.InitialModel(".", 10)}, + model: Model{State: "FileSelection", FilesSelector: modelutils.InitialModel(".", 10, 10)}, expected: "Select the files you want to modify", }, { diff --git a/utils/tui/modelutils/file.go b/utils/tui/modelutils/file.go index 54ad8b3..95b95b6 100644 --- a/utils/tui/modelutils/file.go +++ b/utils/tui/modelutils/file.go @@ -3,6 +3,7 @@ package modelutils import ( "fmt" "os" + "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -19,9 +20,10 @@ type FilesSelector struct { WindowHeight int Error error NoFileSelected bool + WindowWidth int } -func InitialModel(currentDir string, windowHeight int) FilesSelector { +func InitialModel(currentDir string, windowHeight int, windowWidth int) FilesSelector { var filesAndDir []string selectedFilesAndDir := make(map[int]bool) @@ -47,6 +49,7 @@ func InitialModel(currentDir string, windowHeight int) FilesSelector { FilesAndDir: filesAndDir, SelectedFilesAndDir: selectedFilesAndDir, WindowHeight: windowHeight, + WindowWidth: windowWidth, } } @@ -111,24 +114,41 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Done = true } } + case tea.WindowSizeMsg: + return m, m.doResize(msg) } return m, nil } +func (m *FilesSelector) doResize(msg tea.WindowSizeMsg) tea.Cmd { + m.WindowHeight = msg.Height + m.WindowWidth = msg.Width + return nil +} + func (m FilesSelector) View() string { if m.Error != nil { return Paint("red").Render(fmt.Sprintf("An error occurred: %v", m.Error)) } - s := Paint("silver").Render("\n Select the files you want to modify...") + "\n" - s += Paint("silver").Render("\n Selected files till now:") + "\n" + // Help messages + helpMessages := []string{ + "'q' to quit 'esc' to move to parent directory", + "'↑' to go up 'x' to modify selected files", + "'↓' to go down 'enter' to select pointed file/move to pointed sub folder", + } + + // File selection and error messages + var sb strings.Builder + sb.WriteString(Paint("silver").Render("\n Select the files you want to modify...") + "\n") + sb.WriteString(Paint("silver").Render("\n Selected files till now:") + "\n") if m.NoFileSelected { - s += Paint("red").Render("\n No file selected. Please select at least one file or quit.") + "\n" + sb.WriteString(Paint("red").Render("\n No file selected. Please select at least one file or quit.") + "\n") } for i := 0; i < len(m.FilesPath); i++ { - s += fmt.Sprintf(" %s\n", Paint("green").Render(m.FilesPath[i])) + sb.WriteString(fmt.Sprintf(" %s\n", Paint("green").Render(m.FilesPath[i]))) } - s += "\n" + sb.WriteString("\n") for i := m.scrollOffset; i < m.scrollOffset+m.WindowHeight && i < len(m.FilesAndDir); i++ { choice := m.FilesAndDir[i] @@ -150,10 +170,19 @@ func (m FilesSelector) View() string { cursor = Paint("red").Render(" ➪") } - s += fmt.Sprintf("%s %s\n", cursor, choice) + sb.WriteString(fmt.Sprintf("%s %s\n", cursor, choice)) } - s += Paint("silver").Render("\n 'q' to quit 'esc' to move to parent directory\n '↑' to go up 'x' to modify selected files\n '↓' to go down 'enter' to select pointed file/move to pointed sub folder") - return s + + fileSelection := sb.String() + + // Combine file selection with help messages + helpView := lipgloss.JoinVertical(lipgloss.Left, helpMessages...) + content := lipgloss.JoinVertical(lipgloss.Left, fileSelection, helpView) + + // Place the content in the center of the screen + fullView := lipgloss.Place(m.WindowWidth, m.WindowHeight, lipgloss.Left, lipgloss.Left, content) + + return fullView } func Paint(color string) lipgloss.Style { diff --git a/utils/tui/modelutils/file_test.go b/utils/tui/modelutils/file_test.go index adff2f9..8c21401 100644 --- a/utils/tui/modelutils/file_test.go +++ b/utils/tui/modelutils/file_test.go @@ -30,14 +30,14 @@ func TestFilesSelector(t *testing.T) { { name: "InitialModel", setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) }, verify: func(t *testing.T, m FilesSelector) { assert.Equal(t, tempDir, m.CurrentDir) assert.Contains(t, m.FilesAndDir, subDir) assert.NotNil(t, m.SelectedFilesAndDir) assert.Equal(t, 0, m.cursor) - assert.Equal(t, 10, m.WindowHeight) + assert.Equal(t, 10, 10, m.WindowHeight) assert.NoError(t, m.Error) }, }, @@ -45,7 +45,7 @@ func TestFilesSelector(t *testing.T) { name: "KeyDown", msg: tea.KeyMsg{Type: tea.KeyDown}, setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) }, verify: func(t *testing.T, m FilesSelector) { @@ -56,7 +56,7 @@ func TestFilesSelector(t *testing.T) { name: "KeyUp", msg: tea.KeyMsg{Type: tea.KeyUp}, setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) m.cursor = 1 }, verify: func(t *testing.T, m FilesSelector) { @@ -67,7 +67,7 @@ func TestFilesSelector(t *testing.T) { name: "EnterDirectory", msg: tea.KeyMsg{Type: tea.KeyEnter}, setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) m.cursor = 1 }, verify: func(t *testing.T, m FilesSelector) { @@ -78,7 +78,7 @@ func TestFilesSelector(t *testing.T) { name: "SelectFile", msg: tea.KeyMsg{Type: tea.KeyEnter}, setup: func(m *FilesSelector) { - *m = InitialModel(subDir, 10) + *m = InitialModel(subDir, 10, 10) m.cursor = 0 }, verify: func(t *testing.T, m FilesSelector) { @@ -89,7 +89,7 @@ func TestFilesSelector(t *testing.T) { name: "Exit", msg: tea.KeyMsg{Type: tea.KeyCtrlC}, setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) }, verify: func(t *testing.T, m FilesSelector) { // Call Update with the exit message @@ -105,7 +105,7 @@ func TestFilesSelector(t *testing.T) { name: "MoveToPreviousDir", msg: tea.KeyMsg{Type: tea.KeyEsc}, setup: func(m *FilesSelector) { - *m = InitialModel(subDir, 10) + *m = InitialModel(subDir, 10, 10) }, verify: func(t *testing.T, m FilesSelector) { assert.Equal(t, tempDir, m.CurrentDir) @@ -115,7 +115,7 @@ func TestFilesSelector(t *testing.T) { name: "Confirm", msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) m.FilesPath = []string{tempFile} }, verify: func(t *testing.T, m FilesSelector) { @@ -127,7 +127,7 @@ func TestFilesSelector(t *testing.T) { name: "No file selected", msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) }, verify: func(t *testing.T, m FilesSelector) { assert.Equal(t, tempDir, m.CurrentDir) @@ -138,7 +138,7 @@ func TestFilesSelector(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - model := InitialModel(tempDir, 10) + model := InitialModel(tempDir, 10, 10) if tt.setup != nil { tt.setup(&model) } @@ -173,7 +173,7 @@ func TestFilesSelectorView(t *testing.T) { { name: "No selected file", setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) }, verify: func(t *testing.T, view string) { assert.Contains(t, view, "Select the files you want to modify...") @@ -184,7 +184,7 @@ func TestFilesSelectorView(t *testing.T) { { name: " with a selected file", setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) m.cursor = 1 m.FilesPath = append(m.FilesPath, tempFile1) }, @@ -198,7 +198,7 @@ func TestFilesSelectorView(t *testing.T) { { name: " inside subdir", setup: func(m *FilesSelector) { - *m = InitialModel(tempDir, 10) + *m = InitialModel(tempDir, 10, 10) msg := tea.KeyMsg{Type: tea.KeyEnter} m.cursor = 1 newModel, _ := m.Update(msg) @@ -212,7 +212,7 @@ func TestFilesSelectorView(t *testing.T) { { name: "Navigate above root directory", setup: func(m *FilesSelector) { - *m = InitialModel("/", 10) + *m = InitialModel("/", 10, 10) msg := tea.KeyMsg{Type: tea.KeyEsc} m.cursor = 1 newModel, _ := m.Update(msg) @@ -225,7 +225,7 @@ func TestFilesSelectorView(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - model := InitialModel(tempDir, 10) + model := InitialModel(tempDir, 10, 10) if tt.setup != nil { tt.setup(&model) } From da64d449dc8139d5ca94b0e6337b5e4e63fb7e5b Mon Sep 17 00:00:00 2001 From: Filippo Trotter Date: Thu, 11 Jul 2024 17:48:14 +0200 Subject: [PATCH 2/6] feat: maintain file view during changes choices --- utils/server/server.go | 4 +- utils/tui/model.go | 65 +++++++++++++++++------------ utils/tui/model_test.go | 28 ++++++------- utils/tui/modelutils/file.go | 4 ++ utils/tui/modelutils/option.go | 19 +++++++-- utils/tui/modelutils/option_test.go | 16 +++---- utils/tui/modelutils/text.go | 12 +++++- utils/tui/modelutils/text_test.go | 16 +++---- 8 files changed, 101 insertions(+), 63 deletions(-) diff --git a/utils/server/server.go b/utils/server/server.go index 112cc11..68a0b1e 100644 --- a/utils/server/server.go +++ b/utils/server/server.go @@ -44,7 +44,9 @@ func StartServer() { // Initialize the file selector model with the directory argument model := tui.Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(dir, pty.Window.Height-5, pty.Window.Width-5), // Initialize the FilesSelector model with window height + FilesSelector: modelutils.InitialModel(dir, pty.Window.Height-5, pty.Window.Width-5), + Height: pty.Window.Height, + Width: pty.Window.Width, } if model.Error != nil { wish.Println(s, model.Error.Error()) diff --git a/utils/tui/model.go b/utils/tui/model.go index 5b5ee89..123f90b 100644 --- a/utils/tui/model.go +++ b/utils/tui/model.go @@ -6,6 +6,7 @@ import ( "strings" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/dyne/tgcom/utils/modfile" "github.com/dyne/tgcom/utils/tui/modelutils" ) @@ -20,6 +21,8 @@ type Model struct { LabelType []bool CurrentDir string // Current directory for file selection Error error + Width int + Height int // Models for different selection steps FilesSelector modelutils.FilesSelector @@ -46,17 +49,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch msg := msg.(type) { + case tea.KeyMsg: + if m.State == "Final" { + return m, tea.Quit + } case applyChangesMsg: if msg.err != nil { m.Error = msg.err } m.State = "Final" return m, nil - - case tea.KeyMsg: - if m.State == "Final" { - return m, tea.Quit - } } switch m.State { @@ -77,11 +79,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { Speed: "", } m.State = "ActionSelection" - m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[0]), m.SpeedSelector.Selected) + m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[0]), m.SpeedSelector.Selected, m.Width, m.Height) } else { - m.State = "ModeSelection" - m.SpeedSelector = modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "") + m.SpeedSelector = modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "", m.Width, m.Height) } } return m, cmd @@ -95,7 +96,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.SpeedSelector.Done { m.State = "ActionSelection" - m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[0]), m.SpeedSelector.Selected) + m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[0]), m.SpeedSelector.Selected, m.Width, m.Height) } return m, cmd @@ -113,18 +114,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ActionSelector.Done = false m.Actions = m.Actions[:len(m.Actions)-1] m.State = "ActionSelection" - m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[len(m.Actions)]), m.SpeedSelector.Selected) - + m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[len(m.Actions)]), m.SpeedSelector.Selected, m.Width, m.Height) } } if m.ActionSelector.Done { m.Actions = append(m.Actions, m.ActionSelector.Selected) if len(m.Actions) == len(m.Files) { m.State = "LabelInput" - m.LabelInput = modelutils.NewLabelInput(filepath.Base(m.Files[0])) + m.LabelInput = modelutils.NewLabelInput(filepath.Base(m.Files[0]), m.Width, m.Height) } else { - m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[len(m.Actions)]), m.SpeedSelector.Selected) - + m.ActionSelector = modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, filepath.Base(m.Files[len(m.Actions)]), m.SpeedSelector.Selected, m.Width, m.Height) } } return m, cmd @@ -146,7 +145,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Actions = append(m.Actions, m.ActionSelector.Selected) } m.State = "LabelInput" - m.LabelInput = modelutils.NewLabelInput("") + m.LabelInput = modelutils.NewLabelInput("", m.Width, m.Height) } return m, cmd } @@ -167,7 +166,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Labels = m.Labels[:len(m.Labels)-1] m.LabelType = m.LabelType[:len(m.LabelType)-1] m.State = "LabelInput" - m.LabelInput = modelutils.NewLabelInput(filepath.Base(m.Files[len(m.Labels)])) + m.LabelInput = modelutils.NewLabelInput(filepath.Base(m.Files[len(m.Labels)]), m.Width, m.Height) } } if m.LabelInput.Done { @@ -181,8 +180,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.State = "ApplyChanges" return m, m.applyChanges() } else { - m.LabelInput = modelutils.NewLabelInput(filepath.Base(m.Files[len(m.Labels)])) - + m.LabelInput = modelutils.NewLabelInput(filepath.Base(m.Files[len(m.Labels)]), m.Width, m.Height) } } return m, cmd @@ -218,24 +216,39 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the view based on the current state func (m Model) View() string { + var rightPane string + fileSelectionPane := m.FilesSelector.View() + var halfWidth int switch m.State { case "FileSelection": - return m.FilesSelector.View() + return fileSelectionPane case "ModeSelection": - return m.SpeedSelector.View() + halfWidth = m.SpeedSelector.Width + rightPane = m.SpeedSelector.View() case "ActionSelection": - return m.ActionSelector.View() + halfWidth = m.ActionSelector.Width + rightPane = m.ActionSelector.View() case "LabelInput": - return m.LabelInput.View() + halfWidth = m.LabelInput.Width + rightPane = m.LabelInput.View() case "ApplyChanges": - return "Applying changes..." + rightPane = "Applying changes..." case "Final": if m.Error != nil { - return modelutils.Paint("red").Render(fmt.Sprintf("An error occurred: %v\nPress any key to exit.", m.Error)) + rightPane = modelutils.Paint("red").Render(fmt.Sprintf("An error occurred: %v\nPress any key to exit.", m.Error)) + } else { + rightPane = "Changes applied successfully!\nPress any key to exit." } - return "Changes applied successfully!\nPress any key to exit." } - return "" + + // Use a style for the layout + layout := lipgloss.JoinHorizontal( + lipgloss.Top, + lipgloss.NewStyle().Width(halfWidth).Render(fileSelectionPane), + lipgloss.NewStyle().Width(halfWidth).Render(rightPane), + ) + + return layout } // applyChanges applies changes to selected files based on user inputs diff --git a/utils/tui/model_test.go b/utils/tui/model_test.go index c9652ce..d7b9da1 100644 --- a/utils/tui/model_test.go +++ b/utils/tui/model_test.go @@ -82,7 +82,7 @@ func TestModel(t *testing.T) { name: "ModeSelection to ActionSelection", model: Model{ State: "ModeSelection", - SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", ""), + SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "", 10, 10), Files: []string{"file1.txt", "file2.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEnter}, @@ -97,7 +97,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ActionSelection", SpeedSelector: modelutils.ModeSelector{Selected: "Slow mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode"), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode", 10, 10), Files: []string{"file1.txt", "file2.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEnter}, @@ -111,7 +111,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ActionSelection", SpeedSelector: modelutils.ModeSelector{Selected: "Fast mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode"), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode", 10, 10), Files: []string{"file1.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEnter}, @@ -125,7 +125,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ActionSelection", SpeedSelector: modelutils.ModeSelector{Selected: "Slow mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode"), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode", 10, 10), Files: []string{"file1.txt", "file2.txt"}, Actions: []string{"test"}, }, @@ -157,7 +157,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ModeSelection", FilesSelector: modelutils.InitialModel(".", 10, 10), - SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", ""), + SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "", 10, 10), Files: []string{"file1.txt", "file2.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEsc}, @@ -176,7 +176,7 @@ func TestModel(t *testing.T) { State: "ActionSelection", FilesSelector: modelutils.InitialModel(".", 10, 10), SpeedSelector: modelutils.ModeSelector{Selected: "Fast mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", ""), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "", 10, 10), Files: []string{"file1.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEsc}, @@ -192,7 +192,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ActionSelection", SpeedSelector: modelutils.ModeSelector{Selected: "Fast mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", ""), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "", 10, 10), Files: []string{"file1.txt", "file2.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEsc}, @@ -208,7 +208,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ActionSelection", SpeedSelector: modelutils.ModeSelector{Selected: "Slow mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", ""), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "", 10, 10), Files: []string{"file1.txt", "file2.txt"}, }, msg: tea.KeyMsg{Type: tea.KeyEsc}, @@ -223,7 +223,7 @@ func TestModel(t *testing.T) { model: Model{ State: "ActionSelection", SpeedSelector: modelutils.ModeSelector{Selected: "Slow mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode"), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode", 10, 10), Files: []string{"file1.txt", "file2.txt"}, Actions: []string{"test", "comment"}, }, @@ -240,7 +240,7 @@ func TestModel(t *testing.T) { model: Model{ State: "LabelInput", SpeedSelector: modelutils.ModeSelector{Selected: "Fast mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode"), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode", 10, 10), LabelInput: modelutils.LabelInput{Input: "1-3"}, Files: []string{"file1.txt"}, }, @@ -255,7 +255,7 @@ func TestModel(t *testing.T) { model: Model{ State: "LabelInput", SpeedSelector: modelutils.ModeSelector{Selected: "Slow mode"}, - ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode"), + ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Slow mode", 10, 10), LabelInput: modelutils.LabelInput{Input: "1-3"}, Files: []string{"file1.txt", "file2.txt"}, Actions: []string{"test", "comment"}, @@ -429,17 +429,17 @@ func TestModel(t *testing.T) { }, { name: "ModeSelection View", - model: Model{State: "ModeSelection", SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "")}, + model: Model{State: "ModeSelection", SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "", 10, 10)}, expected: "Select 'Fast mode'", }, { name: "ActionSelection View", - model: Model{State: "ActionSelection", ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode")}, + model: Model{State: "ActionSelection", ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode", 10, 10)}, expected: "Select action", }, { name: "LabelInput View", - model: Model{State: "LabelInput", LabelInput: modelutils.NewLabelInput("")}, + model: Model{State: "LabelInput", LabelInput: modelutils.NewLabelInput("", 10, 10)}, expected: "Type below the section to modify", }, diff --git a/utils/tui/modelutils/file.go b/utils/tui/modelutils/file.go index 95b95b6..cce0ee6 100644 --- a/utils/tui/modelutils/file.go +++ b/utils/tui/modelutils/file.go @@ -112,6 +112,7 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.NoFileSelected = true } else { m.Done = true + m.WindowWidth /= 2 } } case tea.WindowSizeMsg: @@ -123,6 +124,9 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *FilesSelector) doResize(msg tea.WindowSizeMsg) tea.Cmd { m.WindowHeight = msg.Height m.WindowWidth = msg.Width + if m.Done { + m.WindowHeight /= 2 + } return nil } diff --git a/utils/tui/modelutils/option.go b/utils/tui/modelutils/option.go index 8f2e2e7..d1280e5 100644 --- a/utils/tui/modelutils/option.go +++ b/utils/tui/modelutils/option.go @@ -1,6 +1,9 @@ package modelutils -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) type ModeSelector struct { File string @@ -10,14 +13,18 @@ type ModeSelector struct { Done bool Speed string Back bool + Width int + Height int } -func NewModeSelector(choices []string, file string, speed string) ModeSelector { +func NewModeSelector(choices []string, file string, speed string, width, height int) ModeSelector { return ModeSelector{ File: file, Choices: choices, Selected: "", Speed: speed, + Height: height, + Width: width / 2, } } @@ -48,6 +55,9 @@ func (m ModeSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Selected = m.Choices[m.cursor] m.Done = true } + case tea.WindowSizeMsg: + m.Width = msg.Width / 2 + m.Height = msg.Height } return m, nil } @@ -62,7 +72,7 @@ func (m ModeSelector) View() string { } s += cursor + " " + choice + "\n" } - return s + return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Center, s) } else { s := "" switch m.Speed { @@ -87,7 +97,8 @@ func (m ModeSelector) View() string { s += cursor + " " + choice + "\n" } } - return s + Paint("silver").Render("\n 'q' to quit 'enter' to modify selected files 'esc' to go back\n '↑' to go up\n '↓' to go down") + s = s + Paint("silver").Render("\n 'q' to quit 'enter' to modify selected files 'esc' to go back\n '↑' to go up\n '↓' to go down") + return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Center, s) } } diff --git a/utils/tui/modelutils/option_test.go b/utils/tui/modelutils/option_test.go index dac2a7d..cf5c544 100644 --- a/utils/tui/modelutils/option_test.go +++ b/utils/tui/modelutils/option_test.go @@ -33,7 +33,7 @@ func TestNewModeSelector(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - selector := NewModeSelector(tt.choices, tt.file, tt.speed) + selector := NewModeSelector(tt.choices, tt.file, tt.speed, 10, 10) assert.Equal(t, tt.expected.File, selector.File) assert.Equal(t, tt.expected.Choices, selector.Choices) assert.Equal(t, tt.expected.Selected, selector.Selected) @@ -45,7 +45,7 @@ func TestNewModeSelector(t *testing.T) { } func TestInit(t *testing.T) { - selector := NewModeSelector([]string{"Option1", "Option2"}, "", "") + selector := NewModeSelector([]string{"Option1", "Option2"}, "", "", 10, 10) cmd := selector.Init() assert.Nil(t, cmd) } @@ -60,7 +60,7 @@ func TestUpdate(t *testing.T) { }{ { name: "Test up key", - initial: NewModeSelector([]string{"Option1", "Option2"}, "", ""), + initial: NewModeSelector([]string{"Option1", "Option2"}, "", "", 10, 10), msg: tea.KeyMsg{Type: tea.KeyUp}, expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, @@ -69,7 +69,7 @@ func TestUpdate(t *testing.T) { }, { name: "Test down key", - initial: NewModeSelector([]string{"Option1", "Option2"}, "", ""), + initial: NewModeSelector([]string{"Option1", "Option2"}, "", "", 10, 10), msg: tea.KeyMsg{Type: tea.KeyDown}, expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, @@ -78,7 +78,7 @@ func TestUpdate(t *testing.T) { }, { name: "Test enter key", - initial: NewModeSelector([]string{"Option1", "Option2"}, "", ""), + initial: NewModeSelector([]string{"Option1", "Option2"}, "", "", 10, 10), msg: tea.KeyMsg{Type: tea.KeyEnter}, expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, @@ -89,7 +89,7 @@ func TestUpdate(t *testing.T) { }, { name: "Test esc key", - initial: NewModeSelector([]string{"Option1", "Option2"}, "", ""), + initial: NewModeSelector([]string{"Option1", "Option2"}, "", "", 10, 10), msg: tea.KeyMsg{Type: tea.KeyEsc}, expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, @@ -99,7 +99,7 @@ func TestUpdate(t *testing.T) { }, { name: "Test quit keys", - initial: NewModeSelector([]string{"Option1", "Option2"}, "", ""), + initial: NewModeSelector([]string{"Option1", "Option2"}, "", "", 10, 10), msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}, Alt: false}, expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, @@ -136,7 +136,7 @@ func TestView(t *testing.T) { }{ { name: "View with cursor at default position", - selector: NewModeSelector([]string{"Option1", "Option2"}, "testfile", "Fast mode"), + selector: NewModeSelector([]string{"Option1", "Option2"}, "testfile", "Fast mode", 10, 10), expected: "Select 'Fast mode' if you want to toggle all your files by giving just indications about start label and end label. Select 'Slow mode' if you want to specify what action to perform file by file. > Option1 Option2", }, { diff --git a/utils/tui/modelutils/text.go b/utils/tui/modelutils/text.go index afb9009..42a9532 100644 --- a/utils/tui/modelutils/text.go +++ b/utils/tui/modelutils/text.go @@ -7,6 +7,7 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type LabelInput struct { @@ -17,14 +18,18 @@ type LabelInput struct { flash bool Error error Back bool + Width int + Height int } -func NewLabelInput(File string) LabelInput { +func NewLabelInput(File string, width, height int) LabelInput { return LabelInput{ File: File, Input: "", Done: false, IsLabel: false, + Height: height, + Width: width / 2, } } @@ -56,6 +61,9 @@ func (m LabelInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyEsc: m.Back = true } + case tea.WindowSizeMsg: + m.Width = msg.Width / 2 + m.Height = msg.Height case tickMsg: m.flash = !m.flash @@ -84,7 +92,7 @@ func (m LabelInput) View() string { } s += Paint("silver").Render("\n 'ctrl +c' to quit 'enter' to select the lines/labels indicated 'esc' to go back\n '↑' to go up\n '↓' to go down") - return s + return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Center, s) } func StartTicker() tea.Cmd { diff --git a/utils/tui/modelutils/text_test.go b/utils/tui/modelutils/text_test.go index edeb150..2fbfe01 100644 --- a/utils/tui/modelutils/text_test.go +++ b/utils/tui/modelutils/text_test.go @@ -48,14 +48,14 @@ func TestNewLabelInput(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - input := NewLabelInput(tt.file) + input := NewLabelInput(tt.file, 10, 10) assert.Equal(t, tt.expected, input) }) } } func TestInitLabelInput(t *testing.T) { - input := NewLabelInput("testfile") + input := NewLabelInput("testfile", 10, 10) cmd := input.Init() assert.NotNil(t, cmd) } @@ -69,7 +69,7 @@ func TestUpdateLabelInput(t *testing.T) { }{ { name: "Test KeyEnter with valid input label", - initial: setInput(NewLabelInput(""), "test;test"), + initial: setInput(NewLabelInput("", 10, 10), "test;test"), msg: tea.KeyMsg{Type: tea.KeyEnter}, expected: LabelInput{ File: "", @@ -80,7 +80,7 @@ func TestUpdateLabelInput(t *testing.T) { }, { name: "Test KeyEnter with valid input lines", - initial: setInput(NewLabelInput(""), "1"), + initial: setInput(NewLabelInput("", 10, 10), "1"), msg: tea.KeyMsg{Type: tea.KeyEnter}, expected: LabelInput{ File: "", @@ -91,7 +91,7 @@ func TestUpdateLabelInput(t *testing.T) { }, { name: "Test KeyEnter with invalid input", - initial: setInput(NewLabelInput(""), ""), + initial: setInput(NewLabelInput("", 10, 10), ""), msg: tea.KeyMsg{Type: tea.KeyEnter}, expected: LabelInput{ File: "", @@ -103,7 +103,7 @@ func TestUpdateLabelInput(t *testing.T) { }, { name: "Test KeyBackspace", - initial: NewLabelInput(""), + initial: NewLabelInput("", 10, 10), msg: tea.KeyMsg{Type: tea.KeyBackspace}, expected: LabelInput{ File: "", @@ -114,7 +114,7 @@ func TestUpdateLabelInput(t *testing.T) { }, { name: "Test KeyRunes", - initial: NewLabelInput(""), + initial: NewLabelInput("", 10, 10), msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t', 'e', 's', 't'}}, expected: LabelInput{ File: "", @@ -125,7 +125,7 @@ func TestUpdateLabelInput(t *testing.T) { }, { name: "Test KeyEsc", - initial: NewLabelInput(""), + initial: NewLabelInput("", 10, 10), msg: tea.KeyMsg{Type: tea.KeyEsc}, expected: LabelInput{ File: "", From 48b57d2644158a6b79385626cd15df07a8081c8d Mon Sep 17 00:00:00 2001 From: Filippo Trotter Date: Tue, 16 Jul 2024 09:56:19 +0200 Subject: [PATCH 3/6] update test --- utils/tui/modelutils/option_test.go | 12 ++++++++++++ utils/tui/modelutils/text_test.go | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/utils/tui/modelutils/option_test.go b/utils/tui/modelutils/option_test.go index cf5c544..fb5d678 100644 --- a/utils/tui/modelutils/option_test.go +++ b/utils/tui/modelutils/option_test.go @@ -27,6 +27,8 @@ func TestNewModeSelector(t *testing.T) { Speed: "", Done: false, Back: false, + Width: 5, + Height: 10, }, }, } @@ -65,6 +67,8 @@ func TestUpdate(t *testing.T) { expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, cursor: 0, + Width: 5, + Height: 10, }, }, { @@ -74,6 +78,8 @@ func TestUpdate(t *testing.T) { expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, cursor: 1, + Width: 5, + Height: 10, }, }, { @@ -85,6 +91,8 @@ func TestUpdate(t *testing.T) { cursor: 0, Selected: "Option1", Done: true, + Width: 5, + Height: 10, }, }, { @@ -95,6 +103,8 @@ func TestUpdate(t *testing.T) { Choices: []string{"Option1", "Option2"}, cursor: 0, Back: true, + Width: 5, + Height: 10, }, }, { @@ -104,6 +114,8 @@ func TestUpdate(t *testing.T) { expected: ModeSelector{ Choices: []string{"Option1", "Option2"}, cursor: 0, + Width: 5, + Height: 10, }, cmdChecker: func(cmd tea.Cmd) { if cmd != nil { diff --git a/utils/tui/modelutils/text_test.go b/utils/tui/modelutils/text_test.go index 2fbfe01..23c60ad 100644 --- a/utils/tui/modelutils/text_test.go +++ b/utils/tui/modelutils/text_test.go @@ -42,6 +42,8 @@ func TestNewLabelInput(t *testing.T) { Input: "", Done: false, IsLabel: false, + Width: 5, + Height: 10, }, }, } @@ -76,6 +78,8 @@ func TestUpdateLabelInput(t *testing.T) { Input: "test;test", Done: true, IsLabel: true, + Width: 5, + Height: 10, }, }, { @@ -87,6 +91,8 @@ func TestUpdateLabelInput(t *testing.T) { Input: "1", Done: true, IsLabel: false, + Width: 5, + Height: 10, }, }, { @@ -99,6 +105,8 @@ func TestUpdateLabelInput(t *testing.T) { Done: false, IsLabel: false, Error: fmt.Errorf("input does not match expected format (e.g., 'start';'end' or 'x-y' or single line number)"), + Width: 5, + Height: 10, }, }, { @@ -110,6 +118,8 @@ func TestUpdateLabelInput(t *testing.T) { Input: "", Done: false, IsLabel: false, + Width: 5, + Height: 10, }, }, { @@ -121,6 +131,8 @@ func TestUpdateLabelInput(t *testing.T) { Input: "test", Done: false, IsLabel: false, + Width: 5, + Height: 10, }, }, { @@ -133,6 +145,8 @@ func TestUpdateLabelInput(t *testing.T) { Done: false, IsLabel: false, Back: true, + Width: 5, + Height: 10, }, }, } From 4bfd850815c1a5599da4fcc2f29d11c60be183a8 Mon Sep 17 00:00:00 2001 From: Filippo Trotter Date: Tue, 16 Jul 2024 10:14:03 +0200 Subject: [PATCH 4/6] feat: add possibility of directly select a single file --- utils/tui/model.go | 3 +++ utils/tui/model_test.go | 42 +++++++++++-------------------- utils/tui/modelutils/file.go | 28 ++++++++++++++++----- utils/tui/modelutils/file_test.go | 4 ++- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/utils/tui/model.go b/utils/tui/model.go index 123f90b..fbcd410 100644 --- a/utils/tui/model.go +++ b/utils/tui/model.go @@ -132,6 +132,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ActionSelector = newActionSelector.(modelutils.ModeSelector) if m.ActionSelector.Back { if len(m.Files) == 1 { + if !m.FilesSelector.MultipleSelection { + m.FilesSelector.FilesPath = []string{} + } m.State = "FileSelection" m.FilesSelector.Done = false } else { diff --git a/utils/tui/model_test.go b/utils/tui/model_test.go index d7b9da1..0a0ca95 100644 --- a/utils/tui/model_test.go +++ b/utils/tui/model_test.go @@ -12,8 +12,13 @@ import ( ) func TestModel(t *testing.T) { + tempDir := t.TempDir() + tempFile := filepath.Join(tempDir, "file.txt") + _, err := os.Create(tempFile) + assert.NoError(t, err) + t.Run("Init", func(t *testing.T) { - model := Model{FilesSelector: modelutils.InitialModel(".", 10, 10)} + model := Model{FilesSelector: modelutils.InitialModel(tempDir, 10, 10)} cmd := model.Init() assert.Nil(t, cmd) }) @@ -32,9 +37,10 @@ func TestModel(t *testing.T) { name: "FileSelection to ModeSelection", model: Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(".", 10, 10), + FilesSelector: modelutils.InitialModel(tempDir, 10, 10), }, setup: func(m *Model) { + m.FilesSelector.MultipleSelection = true m.FilesSelector.FilesPath = []string{"path/test/file1", "path/test/file2"} }, @@ -51,16 +57,14 @@ func TestModel(t *testing.T) { name: "FileSelection to ActionSelection", model: Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(".", 10, 10), + FilesSelector: modelutils.InitialModel(tempDir, 10, 10), }, setup: func(m *Model) { - m.FilesSelector.FilesPath = []string{"path/test/file1"} - }, - msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, + msg: tea.KeyMsg{Type: tea.KeyEnter}, verify: func(t *testing.T, m Model) { assert.True(t, m.FilesSelector.Done) - assert.Contains(t, m.Files, "path/test/file1") + assert.Contains(t, m.Files, tempFile) assert.Equal(t, "ActionSelection", m.State) }, @@ -69,7 +73,7 @@ func TestModel(t *testing.T) { name: "No file selected", model: Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(".", 10, 10), + FilesSelector: modelutils.InitialModel(tempDir, 10, 10), }, msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, verify: func(t *testing.T, m Model) { @@ -156,7 +160,7 @@ func TestModel(t *testing.T) { name: "ModeSelection to FileSelection", model: Model{ State: "ModeSelection", - FilesSelector: modelutils.InitialModel(".", 10, 10), + FilesSelector: modelutils.InitialModel(tempDir, 10, 10), SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "", 10, 10), Files: []string{"file1.txt", "file2.txt"}, }, @@ -174,7 +178,7 @@ func TestModel(t *testing.T) { name: "ActionSelection to FileSelection", model: Model{ State: "ActionSelection", - FilesSelector: modelutils.InitialModel(".", 10, 10), + FilesSelector: modelutils.InitialModel(tempDir, 10, 10), SpeedSelector: modelutils.ModeSelector{Selected: "Fast mode"}, ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "", 10, 10), Files: []string{"file1.txt"}, @@ -424,25 +428,9 @@ func TestModel(t *testing.T) { tests := []viewTest{ { name: "FileSelection View", - model: Model{State: "FileSelection", FilesSelector: modelutils.InitialModel(".", 10, 10)}, + model: Model{State: "FileSelection", FilesSelector: modelutils.InitialModel(tempDir, 10, 10)}, expected: "Select the files you want to modify", }, - { - name: "ModeSelection View", - model: Model{State: "ModeSelection", SpeedSelector: modelutils.NewModeSelector([]string{"Fast mode", "Slow mode"}, "", "", 10, 10)}, - expected: "Select 'Fast mode'", - }, - { - name: "ActionSelection View", - model: Model{State: "ActionSelection", ActionSelector: modelutils.NewModeSelector([]string{"toggle", "comment", "uncomment"}, "", "Fast mode", 10, 10)}, - expected: "Select action", - }, - { - name: "LabelInput View", - model: Model{State: "LabelInput", LabelInput: modelutils.NewLabelInput("", 10, 10)}, - expected: "Type below the section to modify", - }, - { name: "Final View with Error", model: Model{State: "Final", Error: fmt.Errorf("test error")}, diff --git a/utils/tui/modelutils/file.go b/utils/tui/modelutils/file.go index cce0ee6..3fa740f 100644 --- a/utils/tui/modelutils/file.go +++ b/utils/tui/modelutils/file.go @@ -21,6 +21,7 @@ type FilesSelector struct { Error error NoFileSelected bool WindowWidth int + MultipleSelection bool } func InitialModel(currentDir string, windowHeight int, windowWidth int) FilesSelector { @@ -66,6 +67,14 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c", "q": return m, tea.Quit + case " ": + if !m.MultipleSelection { + m.MultipleSelection = true + } else { + m.MultipleSelection = false + m.FilesPath = []string{} + } + case "up": if m.cursor > 0 { m.cursor-- @@ -98,6 +107,10 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.FilesPath = Remove(m.FilesPath, m.FilesAndDir[m.cursor]) } else { m.FilesPath = append(m.FilesPath, m.FilesAndDir[m.cursor]) + if !m.MultipleSelection { + m.Done = true + m.WindowWidth /= 2 + } } m.SelectedFilesAndDir[m.cursor] = !m.SelectedFilesAndDir[m.cursor] } @@ -108,11 +121,13 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } case "x": - if len(m.FilesPath) == 0 { - m.NoFileSelected = true - } else { - m.Done = true - m.WindowWidth /= 2 + if m.MultipleSelection { + if len(m.FilesPath) == 0 { + m.NoFileSelected = true + } else { + m.Done = true + m.WindowWidth /= 2 + } } } case tea.WindowSizeMsg: @@ -138,8 +153,9 @@ func (m FilesSelector) View() string { // Help messages helpMessages := []string{ "'q' to quit 'esc' to move to parent directory", - "'↑' to go up 'x' to modify selected files", + "'↑' to go up 'space' to select multiple files", "'↓' to go down 'enter' to select pointed file/move to pointed sub folder", + "'x' to modify select files", } // File selection and error messages diff --git a/utils/tui/modelutils/file_test.go b/utils/tui/modelutils/file_test.go index 8c21401..d7638db 100644 --- a/utils/tui/modelutils/file_test.go +++ b/utils/tui/modelutils/file_test.go @@ -83,6 +83,7 @@ func TestFilesSelector(t *testing.T) { }, verify: func(t *testing.T, m FilesSelector) { assert.Contains(t, m.FilesPath, tempFile) + assert.True(t, m.Done) }, }, { @@ -112,10 +113,11 @@ func TestFilesSelector(t *testing.T) { }, }, { - name: "Confirm", + name: "Confirm multiple file", msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, setup: func(m *FilesSelector) { *m = InitialModel(tempDir, 10, 10) + m.MultipleSelection = true m.FilesPath = []string{tempFile} }, verify: func(t *testing.T, m FilesSelector) { From 18ee858b3d95cb1c1186731bf7cb20474fcb91a5 Mon Sep 17 00:00:00 2001 From: FilippoTrotter Date: Wed, 17 Jul 2024 10:45:32 +0200 Subject: [PATCH 5/6] use terminal real dimensions --- cmd/root.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 21b703d..5063831 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "runtime" + "strconv" "strings" tea "github.com/charmbracelet/bubbletea" @@ -96,11 +97,17 @@ func ReadFlags(cmd *cobra.Command) { fmt.Fprintf(os.Stderr, "Error getting current working directory: %v\n", err) os.Exit(1) } + // Get terminal dimensions dynamically + termWidth, termHeight, err := getTerminalSize() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting terminal size: %v\n", err) + os.Exit(1) + } // Initialize your model with the current directory model := tui.Model{ State: "FileSelection", - FilesSelector: modelutils.InitialModel(currentDir, 20, 20), + FilesSelector: modelutils.InitialModel(currentDir, termHeight, termWidth), } clearScreen() // Bubble Tea program @@ -212,3 +219,28 @@ func clearScreen() { cmd.Stdout = os.Stdout cmd.Run() } +func getTerminalSize() (width, height int, err error) { + cmd := exec.Command("tput", "cols") + cmd.Stdin = os.Stdin + out, err := cmd.Output() + if err != nil { + return 0, 0, err + } + width, err = strconv.Atoi(strings.TrimSpace(string(out))) + if err != nil { + return 0, 0, err + } + + cmd = exec.Command("tput", "lines") + cmd.Stdin = os.Stdin + out, err = cmd.Output() + if err != nil { + return 0, 0, err + } + height, err = strconv.Atoi(strings.TrimSpace(string(out))) + if err != nil { + return 0, 0, err + } + + return width, height, nil +} From bc34aef8cfe4a2ffdc60fa8bd6b2e6afa903138d Mon Sep 17 00:00:00 2001 From: FilippoTrotter Date: Wed, 17 Jul 2024 11:24:26 +0200 Subject: [PATCH 6/6] fix: terminal resize --- cmd/root.go | 2 ++ utils/tui/model.go | 2 ++ utils/tui/modelutils/file.go | 5 ----- utils/tui/modelutils/option.go | 8 ++++---- utils/tui/modelutils/text.go | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5063831..dda087b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -108,6 +108,8 @@ func ReadFlags(cmd *cobra.Command) { model := tui.Model{ State: "FileSelection", FilesSelector: modelutils.InitialModel(currentDir, termHeight, termWidth), + Width: termWidth, + Height: termHeight, } clearScreen() // Bubble Tea program diff --git a/utils/tui/model.go b/utils/tui/model.go index fbcd410..d413b31 100644 --- a/utils/tui/model.go +++ b/utils/tui/model.go @@ -70,6 +70,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Error = m.FilesSelector.Error return m, tea.Quit } + m.Width = m.FilesSelector.WindowWidth + m.Height = m.FilesSelector.WindowHeight m.Files = m.FilesSelector.FilesPath if len(m.Files) == 1 { m.SpeedSelector = modelutils.ModeSelector{ diff --git a/utils/tui/modelutils/file.go b/utils/tui/modelutils/file.go index 3fa740f..21b755c 100644 --- a/utils/tui/modelutils/file.go +++ b/utils/tui/modelutils/file.go @@ -109,7 +109,6 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.FilesPath = append(m.FilesPath, m.FilesAndDir[m.cursor]) if !m.MultipleSelection { m.Done = true - m.WindowWidth /= 2 } } m.SelectedFilesAndDir[m.cursor] = !m.SelectedFilesAndDir[m.cursor] @@ -126,7 +125,6 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.NoFileSelected = true } else { m.Done = true - m.WindowWidth /= 2 } } } @@ -139,9 +137,6 @@ func (m FilesSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *FilesSelector) doResize(msg tea.WindowSizeMsg) tea.Cmd { m.WindowHeight = msg.Height m.WindowWidth = msg.Width - if m.Done { - m.WindowHeight /= 2 - } return nil } diff --git a/utils/tui/modelutils/option.go b/utils/tui/modelutils/option.go index d1280e5..5b56e11 100644 --- a/utils/tui/modelutils/option.go +++ b/utils/tui/modelutils/option.go @@ -64,7 +64,7 @@ func (m ModeSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m ModeSelector) View() string { if len(m.Choices) == 2 { - s := Paint("silver").Render("Select 'Fast mode' if you want to toggle all your files by giving just indications about start label and end label.\nSelect 'Slow mode' if you want to specify what action to perform file by file.") + "\n" + s := Paint("silver").Render("\n Select 'Fast mode' if you want to toggle all your files by giving just indications about start label and end label.\nSelect 'Slow mode' if you want to specify what action to perform file by file.") + "\n" for i, choice := range m.Choices { cursor := " " if m.cursor == i { @@ -78,7 +78,7 @@ func (m ModeSelector) View() string { switch m.Speed { case "Slow mode": - s += Paint("silver").Render("Select action for file: "+m.File) + "\n\n" + s += Paint("silver").Render("\n Select action for file: "+m.File) + "\n\n" for i, choice := range m.Choices { cursor := " " if m.cursor == i { @@ -88,7 +88,7 @@ func (m ModeSelector) View() string { } case "Fast mode": - s += Paint("silver").Render("Select action:") + "\n\n" + s += Paint("silver").Render("\n Select action:") + "\n\n" for i, choice := range m.Choices { cursor := " " if m.cursor == i { @@ -98,7 +98,7 @@ func (m ModeSelector) View() string { } } s = s + Paint("silver").Render("\n 'q' to quit 'enter' to modify selected files 'esc' to go back\n '↑' to go up\n '↓' to go down") - return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Center, s) + return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Top, s) } } diff --git a/utils/tui/modelutils/text.go b/utils/tui/modelutils/text.go index 42a9532..a3a1a24 100644 --- a/utils/tui/modelutils/text.go +++ b/utils/tui/modelutils/text.go @@ -80,7 +80,7 @@ func (m LabelInput) View() string { flash = Paint("green").Render("▎") } - s := Paint("silver").Render("Type below the section to modify. You can insert your start label\nand your end label using the syntax 'start';'end' or you can modify\n a single line by entering the line number or a range of lines using the syntax x-y") + "\n\n" + s := Paint("silver").Render("\n Type below the section to modify. You can insert your start label\nand your end label using the syntax 'start';'end' or you can modify\n a single line by entering the line number or a range of lines using the syntax x-y") + "\n\n" if m.File != "" { s += Paint("green").Render(m.File+": ✏ "+m.Input) + flash + "\n" } else { @@ -92,7 +92,7 @@ func (m LabelInput) View() string { } s += Paint("silver").Render("\n 'ctrl +c' to quit 'enter' to select the lines/labels indicated 'esc' to go back\n '↑' to go up\n '↓' to go down") - return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Center, s) + return lipgloss.Place(m.Width, m.Height, lipgloss.Left, lipgloss.Top, s) } func StartTicker() tea.Cmd {