Prechádzať zdrojové kódy

Implemented viewport & scrolling!

arianagiroux 2 týždňov pred
rodič
commit
2f2c5116ba
4 zmenil súbory, kde vykonal 78 pridanie a 42 odobranie
  1. 0 3
      Readme.md
  2. 1 1
      cmd/issues.go
  3. 76 21
      tui.go
  4. 1 17
      tui_test.go

+ 0 - 3
Readme.md

@@ -82,9 +82,6 @@ issues PATH
 - `io.go:// TODO(InvokeEditor) implement a channel & goroutine based concurrency lifecycle`
 - `io.go:func DeleteIssue(issue Issue) (success bool, err error) { return false, nil } // TODO: implement`
 - `tui.go:// TODO enable collection recursing (i.e, embedded collections)`
-- `tui.go:// TODO enable scroll/viewport logic`
-- `tui.go:// TODO(create widget) implement description field in create.create`
-- `tui_test.go:func Test_Model_View(t *testing.T) { // TODO DEPRECATE`
 
 ### v0.1 Roadmap
 

+ 1 - 1
cmd/issues.go

@@ -53,7 +53,7 @@ func main() {
 
 	p := tea.NewProgram(
 		issues.Model{Path: path},
-		tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
+		// tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
 		// tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
 	)
 

+ 76 - 21
tui.go

@@ -3,8 +3,6 @@
 // While the package remains in v0.0.X releases, this TUI may be undocumented.
 //
 // TODO enable collection recursing (i.e, embedded collections)
-//
-// TODO enable scroll/viewport logic
 package issues
 
 import (
@@ -12,9 +10,10 @@ import (
 	"path/filepath"
 	"strings"
 
+	"charm.land/bubbles/v2/viewport"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/bubbles/textinput"
 	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
 )
 
 // Type and Style definitions -------------------------------------------------
@@ -63,10 +62,12 @@ var (
 
 // The main bubbletea Model
 type Model struct {
-	widget  widget
-	content string
-	Path    string
-	// viewport viewport.Model
+	widget        widget
+	content       string
+	Path          string
+	viewport      viewport.Model
+	viewportReady bool
+	width         int
 }
 
 // The bubbletea init function
@@ -78,11 +79,34 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 	// general message handling
 	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		m.width = msg.Width
+		headerHeight := lipgloss.Height(m.header())
+		footerHeight := lipgloss.Height(m.footer())
+		verticalMarginHeight := headerHeight + footerHeight
+		if !m.viewportReady {
+			m.viewport = viewport.New(
+				viewport.WithWidth(msg.Width),
+				viewport.WithHeight(msg.Height-verticalMarginHeight),
+			)
+			m.viewport.YPosition = headerHeight
+			m.viewport.SetContent(m.content)
+			m.viewportReady = true
+		} else {
+			m.viewport.SetWidth(msg.Width)
+			m.viewport.SetHeight(msg.Height - verticalMarginHeight)
+		}
 	case tea.KeyMsg: // KeyMsg capture that is always present
 		switch msg.String() {
 		case "ctrl+c":
 			cmds = append(cmds, tea.Quit)
 		}
+		switch msg.Type {
+		case tea.KeyUp:
+			m.viewport.ScrollUp(1)
+		case tea.KeyDown:
+			m.viewport.ScrollDown(1)
+		}
 	case widget: // widget is initialized from m.load()
 		switch T := msg.(type) {
 		case edit:
@@ -106,6 +130,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return m, func() tea.Msg { return wg }
 	case string:
 		m.content = msg
+		m.viewport.SetContent(m.content)
+		cmds = append(cmds, tea.EnterAltScreen)
 	}
 
 	// finally, pass msg to widget
@@ -133,10 +159,24 @@ func (m Model) View() string {
 	var output string
 	if len(m.content) == 0 {
 		return "loading..."
+	} else {
+		output = output + m.header()
+		output = output + m.viewport.View()
+
+		footerLeft := lipgloss.NewStyle().Faint(true).Margin(1).Render(m.widget.keyhelp())
+		footerRight := lipgloss.NewStyle().Faint(true).Margin(1).Render(m.footer())
+
+		line := strings.Repeat("─",
+			max(0, m.viewport.Width()-lipgloss.Width(footerLeft)-lipgloss.Width(footerRight)))
+		line = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(line)
+		output = output + lipgloss.JoinHorizontal(lipgloss.Center,
+			footerLeft,
+			line,
+			footerRight,
+		)
+		// output = output + lipgloss.NewStyle().Faint(true).Margin(0, 1, 1, 1).Render(m.footer())
+		return output
 	}
-	output = output + m.content
-	output = output + lipgloss.NewStyle().Faint(true).Margin(1).Render(m.widget.keyhelp())
-	return output
 }
 
 // Handles load logic
@@ -164,6 +204,24 @@ func (m Model) load() tea.Msg {
 	return newEditWidget(m.Path)
 }
 
+// renders a header for the program
+func (m Model) header() string {
+	var borderStyle = lipgloss.NewStyle().
+		BorderStyle(lipgloss.NormalBorder()).
+		BorderForeground(lipgloss.Color("8")).
+		Padding(0, 1)
+
+	left := "tissues v0.0"
+	left = borderStyle.Render(left)
+
+	line := strings.Repeat("─", max(0, m.viewport.Width()-lipgloss.Width(left)))//                                   -lipgloss.Width(right)))
+	line = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(line)
+	return lipgloss.JoinHorizontal(lipgloss.Center, line, left)
+}
+
+// renders a footer for the program
+func (m Model) footer() string { return "j/k: scroll\t\tctrl+c: quit" }
+
 // WIDGET DEFINITIONS ---------------------------------------------------------
 // ----------------------------------------------------------------------------
 
@@ -176,7 +234,6 @@ type widget interface {
 
 // -------- edit widget definitions -----------------------------------------
 // ----------------------------------------------------------------------------
-// TODO(create widget) implement description field in create.create
 
 type ( // Type definitions for use in tea.Msg life cycle for create widget.
 	createResult Issue    // type wrapper for create.create() result
@@ -347,9 +404,9 @@ func (e edit) update(msg tea.Msg) (widget, tea.Cmd) {
 		cmds = append(cmds, e.render)
 	case createResult:
 		if !e.existing {
-			cmds = append(cmds, e.editBlankDescription(Issue(msg)))
+			cmds = append(cmds, tea.ExitAltScreen, e.editBlankDescription(Issue(msg)))
 		} else {
-			cmds = append(cmds, e.editExistingDescription(Issue(msg)))
+			cmds = append(cmds, tea.ExitAltScreen, e.editExistingDescription(Issue(msg)))
 		}
 	case editorResult:
 		if msg.err != nil {
@@ -523,7 +580,7 @@ func (e edit) render() tea.Msg {
 // keyhelp cmd for create widget
 func (e edit) keyhelp() string {
 	var output string
-	output = output + "\ntab/shift+tab: down/up\t\tenter: input value\t\tesc: reset\t\tctrl+e: quit"
+	output = output + "tab/shift+tab: down/up\t\tenter: input value\t\tesc: reset\t\tctrl+e: quit"
 	return output
 }
 
@@ -603,7 +660,7 @@ func (i Issue) keyhelp() string {
 	if IsIssueCollection(remainder) {
 		escStr = "\t\tesc: back"
 	}
-	output = output + fmt.Sprintf("\ne: edit issue%s\t\tctrl+c: quit", escStr)
+	output = output + fmt.Sprintf("e: edit issue%s\t\tctrl+c: quit", escStr)
 	return output
 }
 
@@ -620,14 +677,14 @@ func (ic IssueCollection) update(msg tea.Msg) (widget, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.KeyMsg:
 		switch msg.String() {
-		case "j":
+		case "tab":
 			if ic.selection+1 < len(ic.Collection) {
 				ic.selection = ic.selection + 1
 			} else {
 				ic.selection = 0
 			}
 			return ic, ic.render
-		case "k":
+		case "shift+tab":
 			if ic.selection != 0 {
 				ic.selection = ic.selection - 1
 			} else {
@@ -637,8 +694,6 @@ func (ic IssueCollection) update(msg tea.Msg) (widget, tea.Cmd) {
 		case "enter":
 			ic.Path = ic.Collection[ic.selection].Path
 			return ic, ic.sendLoad
-		case "q":
-			return ic, tea.Quit
 		case "c":
 			return ic, ic.newIssueInCollection
 		case "e":
@@ -660,7 +715,7 @@ func (ic IssueCollection) edit() tea.Msg { return newEditWidget(ic.Path) }
 func (ic IssueCollection) render() tea.Msg {
 	var output string
 	var left string
-	output = output + "Issues in " + ic.Path + "...\n\n"
+	output = output + "\nIssues in " + ic.Path + "...\n\n"
 	for i, issue := range ic.Collection {
 		// pointer render
 		if i == ic.selection {
@@ -686,7 +741,7 @@ func (ic IssueCollection) render() tea.Msg {
 // keyhelp cmd for IssueCollection widget
 func (ic IssueCollection) keyhelp() string {
 	var output string
-	output = output + "\nc: create new issue\t\tenter: view issue\t\tj/k: down/up\t\tq/ctrl+c: quit"
+	output = output + "tab/shift+tab: select\t\tenter: view issue\t\tc: create new issue\t\te: edit selected issue"
 	return output
 }
 

+ 1 - 17
tui_test.go

@@ -224,9 +224,9 @@ func Test_IssueCollection_keyhelp(t *testing.T) {}
 
 func Test_edit_update(t *testing.T) {}
 func Test_edit_init(t *testing.T) {
+	t.Skip()
 	testWidget := newEditWidget("tests/bugs/test-1")
 	result := testWidget.(edit).init()
-	t.Skip()
 
 	assert.NotNil(t, result, result)
 	assert.IsType(t, cursor.BlinkMsg{}, result())
@@ -292,19 +292,3 @@ func Test_edit_keyhelp(t *testing.T) {}
 
 // misc...---------------------------------------------------------------------
 // ----------------------------------------------------------------------------
-
-// duplicated by Test_Model_update_render*
-func Test_Model_View(t *testing.T) { // TODO DEPRECATE
-	testIssue, _ := Issue.NewFromPath(Issue{}, "tests/bugs/test-1")
-	testModel := Model{widget: testIssue}
-	model, cmd := Model{}.Update(tea.Msg(testIssue)) // gives render cmd
-	model, cmd = model.Update(cmd())                 // handle internal render
-	if cmd != nil {
-		assert.Fail(t, "should return not cmds")
-	}
-
-	assert.NotEqual(t, testModel.View(), model.(Model).View())
-
-	render2 := Model{}.View()
-	assert.Equal(t, "loading...", testModel.View(), render2)
-}