Prechádzať zdrojové kódy

Tracked display type via golang interface

- Squashes a bug wherein one could return to a broken state of the
  IssueCollection view by pressing the scroll keys. Now those keys will
  not do anything when viewing an issue.

- Implements the golang interface `widget` to allow for type-switching
  based on what kind of widget is stored. Widgets are simple interfaces
  of the Issue and IssueCollection structs that implements a view
  function that returns a string as a tea.KeyMsg signal
arianagiroux 3 týždňov pred
rodič
commit
0b23ed339c
4 zmenil súbory, kde vykonal 117 pridanie a 62 odobranie
  1. 2 0
      Readme.md
  2. 8 3
      issue.go
  3. 54 36
      tui.go
  4. 53 23
      tui_test.go

+ 2 - 0
Readme.md

@@ -18,6 +18,8 @@ go install cmd/issues.go
 - `cmd/issues.go:// TODO implement attempt to auto load issues folder if no arg specified`
 - `io.go:func WriteIssue(issue Issue) (success bool, err error) { return false, nil } // TODO: implement`
 - `io.go:func DeleteIssue(issue Issue) (success bool, err error) { return false, nil } // TODO: implement`
+- `tui.go:// TODO enable collection recursing (i.e, embeded collections)`
+- `tui.go:// TODO enable scroll/viewport logic on issues!!!`
 
 ## See also
 

+ 8 - 3
issue.go

@@ -220,9 +220,14 @@ func (vf VariadicField) NewFromPath(pathOnDisk string) (v VariadicField, err err
 // IssueCollection Definitions ------------------------------------------------
 // ----------------------------------------------------------------------------
 
-type IssueCollection []Issue
+type IssueCollection struct {
+	Collection []Issue
+	Path       string
+	selection  int
+}
 
 func (ic IssueCollection) NewFromPath(path string) (collection IssueCollection, err error) {
+	ic.Path = path
 	files, err := os.ReadDir(path)
 	if err != nil {
 		return IssueCollection{}, err
@@ -236,11 +241,11 @@ func (ic IssueCollection) NewFromPath(path string) (collection IssueCollection,
 				continue
 			}
 
-			collection = append(collection, issue)
+			ic.Collection = append(ic.Collection, issue)
 		}
 	}
 
-	return collection, nil
+	return ic, nil
 }
 
 // Util Definitions -----------------------------------------------------------

+ 54 - 36
tui.go

@@ -1,5 +1,9 @@
 // The package defines an extensible TUI via the bubbletea framework.
 //
+// TODO enable collection recursing (i.e, embeded collections)
+//
+// TODO enable scroll/viewport logic on issues!!!
+//
 // While the package remains in v0.0.X releases, this TUI may be undocumented.
 package issues
 
@@ -11,13 +15,15 @@ import (
 	"github.com/charmbracelet/lipgloss"
 )
 
+type widget interface {
+	view() tea.Msg
+}
+
 // The main bubbletea Model
 type Model struct {
-	issue      Issue
-	collection IssueCollection
-	content    string
-	Path       string
-	selection  int // index for the collection browser
+	widget  widget
+	content string
+	Path    string
 	// viewport viewport.Model
 }
 
@@ -31,29 +37,41 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case "q":
 			return m, tea.Quit
 		case "k":
-			if m.selection+1 < len(m.collection) {
-				m.selection = m.selection + 1
-			} else {
-				m.selection = 0
+			if collection, ok := m.widget.(IssueCollection); ok {
+				if collection.selection+1 < len(collection.Collection) {
+					collection.selection = collection.selection + 1
+				} else {
+					collection.selection = 0
+				}
+				m.widget = collection
+				return m, collection.view
 			}
-			return m, m.renderIssueCollection
+			return m, nil
 		case "j":
-			if m.selection != 0 {
-				m.selection = m.selection - 1
-			} else {
-				m.selection = len(m.collection) - 1
+			// do something only if widget is collection
+			if collection, ok := m.widget.(IssueCollection); ok {
+				if collection.selection != 0 {
+					collection.selection = collection.selection - 1
+				} else {
+					collection.selection = len(collection.Collection) - 1
+				}
+				m.widget = collection
+				return m, collection.view
 			}
-			return m, m.renderIssueCollection
+			return m, nil
 		case "enter":
-			m.Path = m.collection[m.selection].Path
+			m.Path = m.widget.(IssueCollection).Collection[m.widget.(IssueCollection).selection].Path
 			return m, m.load
 		}
-	case Issue:
-		m.issue = msg
-		return m, m.renderIssue
-	case IssueCollection:
-		m.collection = msg
-		return m, m.renderIssueCollection
+	case widget:
+		switch T := msg.(type) {
+		case Issue:
+			m.widget = T
+			return m, T.view
+		case IssueCollection:
+			m.widget = T
+			return m, T.view
+		}
 	case string:
 		m.content = msg
 		return m, nil
@@ -65,7 +83,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 // Handles load logic
 func (m Model) load() tea.Msg {
 	if IsIssue(m.Path) {
-		issue, err := m.issue.NewFromPath(m.Path)
+		issue, err := Issue{}.NewFromPath(m.Path)
 
 		if err != nil {
 			return nil
@@ -75,7 +93,7 @@ func (m Model) load() tea.Msg {
 	}
 
 	if IsIssueCollection(m.Path) {
-		collection, err := m.collection.NewFromPath(m.Path)
+		collection, err := IssueCollection{}.NewFromPath(m.Path)
 
 		if err != nil {
 			return nil
@@ -121,52 +139,52 @@ var (
 )
 
 // Handles all view logic for Issue structs
-func (m Model) renderIssue() tea.Msg {
+func (i Issue) view() tea.Msg {
 	var output string
 	// title
-	output = output + titleStyle.Render(m.issue.Title)
+	output = output + titleStyle.Render(i.Title)
 
 	// status
-	output = output + fmt.Sprintf("\n%s", statusStyle.Render(m.issue.Status.Data))
+	output = output + fmt.Sprintf("\n%s", statusStyle.Render(i.Status.Data))
 
 	// variadics
 	var tags string
-	for _, field := range m.issue.Tags.Fields {
+	for _, field := range i.Tags.Fields {
 		tags = tags + field.Path + ", "
 	}
 	tags = strings.TrimRight(tags, ", ")
 
 	var blockedby string
-	for _, field := range m.issue.Blockedby.Fields {
+	for _, field := range i.Blockedby.Fields {
 		blockedby = blockedby + field.Path + ", "
 	}
 	blockedby = strings.TrimRight(blockedby, ", ")
 
-	if len(m.issue.Tags.Fields) > 0 {
+	if len(i.Tags.Fields) > 0 {
 		output = output + variadicTitleStyle.Render("\n\nTags:")
 		output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(tags))
 	}
 
-	if len(m.issue.Blockedby.Fields) > 0 {
+	if len(i.Blockedby.Fields) > 0 {
 		output = output + variadicTitleStyle.Render("\n\nBlockedby:")
 		output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(blockedby))
 	}
 
 	// description
 	output = output + titleStyle.Render("\n\nDescription:\n")
-	output = output + fmt.Sprintf("\n%s", m.issue.Description.Data)
+	output = output + fmt.Sprintf("\n%s", i.Description.Data)
 
 	return borderStyle.Render(output)
 }
 
 // Handles all view logic for IssueCollection structs.
-func (m Model) renderIssueCollection() tea.Msg {
+func (ic IssueCollection) view() tea.Msg {
 	var output string
 	var left string
-	output = output + "Issues in " + m.Path + "...\n\n"
-	for i, issue := range m.collection {
+	output = output + "Issues in " + ic.Path + "...\n\n"
+	for i, issue := range ic.Collection {
 		// pointer render
-		if i == m.selection {
+		if i == ic.selection {
 			left = left + pointerStyle.Render("-> ")
 		} else {
 			left = left + pointerStyle.Render("   ")

+ 53 - 23
tui_test.go

@@ -36,21 +36,24 @@ func Test_Model_Update_quit_on_keymsg(t *testing.T) {
 
 func Test_Model_Update_scroll_on_k(t *testing.T) {
 	ic, _ := IssueCollection.NewFromPath(IssueCollection{}, "tests/bugs/")
-	testModel := Model{collection: ic}
+	testModel := Model{widget: ic}
 	testKey := tea.Key{Type: tea.KeyRunes, Runes: []rune{'k'}}
 	testMsg := tea.KeyMsg(testKey)
 
 	model, cmd := testModel.Update(testMsg)
-	assert.Equal(t, testModel.selection+1, model.(Model).selection)
+	widget, _ := model.(Model).widget.(IssueCollection)
+	assert.Equal(t, ic.selection+1, widget.selection)
 
 	if cmd == nil {
 		assert.Fail(t, "should return tea.Cmd")
 	}
 
 	// test select wraparound
-	testModel.selection = len(testModel.collection) - 1
+	ic.selection = len(ic.Collection) - 1
+	testModel = Model{widget: ic}
 	model, cmd = testModel.Update(testMsg)
-	assert.Equal(t, 0, model.(Model).selection)
+	widget, _ = model.(Model).widget.(IssueCollection)
+	assert.Equal(t, 0, widget.selection)
 
 	if cmd == nil {
 		assert.Fail(t, "should return tea.Cmd")
@@ -59,31 +62,55 @@ func Test_Model_Update_scroll_on_k(t *testing.T) {
 
 func Test_Model_Update_scroll_on_j(t *testing.T) {
 	ic, _ := IssueCollection.NewFromPath(IssueCollection{}, "tests/bugs/")
-	testModel := Model{collection: ic}
+	testModel := Model{widget: ic}
 	testKey := tea.Key{Type: tea.KeyRunes, Runes: []rune{'j'}}
 	testMsg := tea.KeyMsg(testKey)
 
 	model, cmd := testModel.Update(testMsg)
-	assert.Equal(t, len(testModel.collection)-1, model.(Model).selection)
+	widget, _ := model.(Model).widget.(IssueCollection)
+	assert.Equal(t, len(ic.Collection)-1, widget.selection)
 
 	if cmd == nil {
 		assert.Fail(t, "should return tea.Cmd")
 	}
 
-	model, cmd = model.(Model).Update(testMsg)
-	assert.Equal(t, len(testModel.collection)-2, model.(Model).selection)
-
-	if cmd == nil {
-		assert.Fail(t, "should return tea.Cmd")
-	}
+	// test select wraparound
+	ic.selection = len(ic.Collection) - 1
+	testModel = Model{widget: ic}
+	model, _ = testModel.Update(testMsg)
+	widget, _ = model.(Model).widget.(IssueCollection)
+	assert.Equal(t, len(ic.Collection)-2, widget.selection)
+
+	// if cmd == nil {
+	// 	assert.Fail(t, "should return tea.Cmd")
+	// }
+	// ic, _ := IssueCollection.NewFromPath(IssueCollection{}, "tests/bugs/")
+	// testModel := Model{widget: ic}
+	// testKey := tea.Key{Type: tea.KeyRunes, Runes: []rune{'j'}}
+	// testMsg := tea.KeyMsg(testKey)
+
+	// model, cmd := testModel.Update(testMsg)
+	// widget, _ := testModel.widget.(IssueCollection)
+	// assert.Equal(t, len(ic.Collection)-1, widget.selection)
+
+	// if cmd == nil {
+	// 	assert.Fail(t, "should return tea.Cmd")
+	// }
+
+	// model, cmd = model.Update(testMsg)
+	// assert.Equal(t, len(ic.Collection)-2, widget.selection)
+
+	// if cmd == nil {
+	// 	assert.Fail(t, "should return tea.Cmd")
+	// }
 }
 
 func Test_Model_Update_load_on_enter(t *testing.T) {
 	ic, _ := IssueCollection.NewFromPath(IssueCollection{}, "tests/bugs")
-	testModel := Model{collection: ic}
+	testModel := Model{widget: ic}
 	testKey := tea.Key{Type: tea.KeyEnter, Runes: []rune{}}
 	testMsg := tea.KeyMsg(testKey)
-	testPath := testModel.collection[testModel.selection].Path
+	testPath := ic.Collection[ic.selection].Path
 
 	model, cmd := testModel.Update(testMsg)
 	assert.Equal(t, testPath, model.(Model).Path)
@@ -98,12 +125,13 @@ func Test_Model_Update_renderIssue(t *testing.T) {
 	testModel := Model{}
 
 	model, cmd := testModel.Update(tea.Msg(testIssue))
+	widget, _ := model.(Model).widget.(Issue)
 	if cmd == nil {
 		assert.Fail(t, "should return cmd")
 	}
 
-	assert.Equal(t, model.(Model).issue.Title, testIssue.Title)
-	assert.Equal(t, Model{issue: testIssue}.renderIssue(), cmd())
+	assert.Equal(t, testIssue.Title, widget.Title)
+	assert.Equal(t, testIssue.view(), cmd())
 }
 
 func Test_Model_Update_renderIssueCollection(t *testing.T) {
@@ -111,19 +139,21 @@ func Test_Model_Update_renderIssueCollection(t *testing.T) {
 	testModel := Model{}
 
 	model, cmd := testModel.Update(tea.Msg(testCollection))
+	widget, _ := model.(Model).widget.(IssueCollection)
 	if cmd == nil {
 		assert.Fail(t, "should return cmd")
 	}
 
-	assert.Equal(t, len(testCollection), len(model.(Model).collection))
-	assert.Equal(t, Model{collection: testCollection}.renderIssueCollection(), cmd())
+	assert.Equal(t, len(testCollection.Collection), len(widget.Collection))
+	assert.Equal(t, testCollection.view(), cmd())
 }
 
 func Test_Model_Update_updates_content(t *testing.T) {
 	testIssue, _ := Issue{}.NewFromPath("tests/bugs/test-1")
-	testModel := Model{issue: testIssue}
+	testModel := Model{widget: testIssue}
+	testWidget := testModel.widget.(Issue)
 
-	testModel.content = testModel.renderIssue().(string)
+	testModel.content = testWidget.view().(string)
 
 	model, cmd := testModel.Update(tea.Msg(testModel.content))
 	if cmd != nil {
@@ -135,7 +165,7 @@ func Test_Model_Update_updates_content(t *testing.T) {
 
 func Test_Model_Update_do_nothing(t *testing.T) {
 	testIssue, _ := Issue.NewFromPath(Issue{}, "tests/bugs/test-1")
-	testModel := Model{issue: testIssue}
+	testModel := Model{widget: testIssue}
 	var testMsg int
 
 	model, cmd := testModel.Update(testMsg)
@@ -146,7 +176,7 @@ func Test_Model_Update_do_nothing(t *testing.T) {
 
 func Test_Model_renderIssue(t *testing.T) {
 	testIssue, _ := Issue.NewFromPath(Issue{}, "tests/bugs/test-1")
-	testRender := Model{issue: testIssue}.renderIssue()
+	testRender := testIssue.view()
 
 	renderContent, _ := testRender.(string)
 	assert.True(t, strings.Contains(renderContent, "test description"))
@@ -160,7 +190,7 @@ func Test_Model_View(t *testing.T) {
 		assert.Fail(t, "should not return cmd")
 	}
 
-	assert.Equal(t, Model{issue: testIssue}.renderIssue().(string), model.(Model).View())
+	assert.Equal(t, testIssue.view().(string), model.(Model).View())
 
 	render2 := Model{}.View()
 	assert.Equal(t, "loading...", render2)