diff --git a/go.mod b/go.mod index 039d546..86915c5 100644 --- a/go.mod +++ b/go.mod @@ -8,73 +8,41 @@ require ( github.com/charmbracelet/glamour v0.8.0 github.com/charmbracelet/lipgloss v0.13.1 github.com/charmbracelet/x/exp/teatest v0.0.0-20241024145942-ad25fd0d5a9e + github.com/charmbracelet/x/term v0.2.0 github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3 github.com/spf13/cobra v1.8.1 ) require ( - dagger.io/dagger v0.13.6 // indirect - github.com/99designs/gqlgen v0.17.55 // indirect - github.com/Khan/genqlient v0.7.0 // indirect - github.com/adrg/xdg v0.5.1 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/charmbracelet/x/ansi v0.4.0 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect - github.com/charmbracelet/x/term v0.2.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/vektah/gqlparser/v2 v2.5.17 // indirect github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect - go.opentelemetry.io/otel/log v0.3.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/sdk v1.27.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.3.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.24.0 // indirect golang.org/x/text v0.19.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/src/cmd/tui/tui_groups.go b/src/cmd/tui/tui_groups.go index 61ece6b..f94a317 100644 --- a/src/cmd/tui/tui_groups.go +++ b/src/cmd/tui/tui_groups.go @@ -5,8 +5,10 @@ import ( "strings" "github.com/Slug-Boi/cocommit/src/cmd/utils" + "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/term" ) // sessionState is used to track which model is focused @@ -27,10 +29,13 @@ var ( ) type mainModel struct { - content []string - index int + content []string + index int + paginator paginator.Model } +var cap, lines int + func newModel() mainModel { groups := utils.Groups @@ -47,7 +52,44 @@ func newModel() mainModel { slices.Sort(content) - m := mainModel{content: content} + // check if terminal 0 is a terminal + var w,h int + var err error + if ok := term.IsTerminal(0); ok { + // calculate term size + w, h, err = term.GetSize(0) + if err != nil { + panic(err) + } + } + + // 25 is a magic number in terms of height but is roughly based on the element height + // of the group squares + if h > 25 { + lines = 2 + } else { + lines = 1 + } + + // if the terminal size is 0, then skip + if w > 0 { + // 30 is a magic number don't question it + cap = min(5, w/30) + } + cap = max(1, cap) + + if cap * 2 > len(content) { + cap = len(content) + } + + p := paginator.New() + p.Type = paginator.Dots + p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "170", Dark: "170"}).Render("•") + p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•") + p.PerPage = cap * lines + p.SetTotalPages(len(content)) + + m := mainModel{content: content, paginator: p} return m } @@ -57,7 +99,7 @@ func (m mainModel) Init() tea.Cmd { } func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - //var cmd tea.Cmd + var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: @@ -89,15 +131,16 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return nil, nil case "tab", "right": - m.Next() + m.next() case "left": - if m.index == 0 { - m.index = len(m.content) - 1 - } else { - m.index-- - } + m.previous() } } + + // Adrian is a fucking genius thanks for the idea :) + m.paginator.Page = m.index / (cap * lines) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } @@ -115,17 +158,17 @@ func (m mainModel) View() string { // Take the first 5 elements and join them horizontally // then take the next 5 and join them horizontally if there are more than 5 // then join vertically - //TODO: Figure out what width is measured in and tie the number 5 to a variable that - // is width_of_term/item_width - for len(squares) > 5 { - s += lipgloss.JoinHorizontal(lipgloss.Top, squares[:5]...) + + start, end := m.paginator.GetSliceBounds(len(m.content)) + end = end - 1 + //println(start, end) + for start <= end { + s += lipgloss.JoinHorizontal(lipgloss.Top, squares[start:start+cap]...) s += "\n" - squares = squares[5:] + start += cap } - s += lipgloss.JoinHorizontal(lipgloss.Top, squares...) - - //s += lipgloss.JoinHorizontal(lipgloss.Top, squares...) + s += "\n" + m.paginator.View() s += helpStyle.Render("\ntab/right: focus next • left: focus previous • enter: select group • q/esq: exit\n") return s @@ -138,10 +181,18 @@ func (m mainModel) currentFocusedModel() string { return "" } -func (m *mainModel) Next() { +func (m *mainModel) next() { if m.index == len(m.content)-1 { m.index = 0 } else { m.index++ } } + +func (m *mainModel) previous() { + if m.index == 0 { + m.index = len(m.content) - 1 + } else { + m.index-- + } +} diff --git a/src/cmd/tui/tui_list.go b/src/cmd/tui/tui_list.go index 1fbc027..b14d132 100644 --- a/src/cmd/tui/tui_list.go +++ b/src/cmd/tui/tui_list.go @@ -25,8 +25,8 @@ var ( selectedHighlightStyle = lipgloss.NewStyle().PaddingLeft(2).Background(lipgloss.Color("206")).Foreground(lipgloss.Color("90")) deletionStyle = lipgloss.NewStyle().MarginLeft(2).Foreground(lipgloss.Color("9")) paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + ActivePaginationDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "170", Dark: "170"}) helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) - //quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) ) type item string @@ -301,6 +301,7 @@ func listModel() model { l.SetFilteringEnabled(true) // Enable filtering l.Styles.Title = titleStyle l.Styles.PaginationStyle = paginationStyle + l.Paginator.ActiveDot = ActivePaginationDot.Render("•") l.AdditionalShortHelpKeys = // Add help keys (main page) func() []key.Binding { return []key.Binding{ diff --git a/src/cmd/tui/tui_test.go b/src/cmd/tui/tui_test.go index 4de9be9..526a0df 100644 --- a/src/cmd/tui/tui_test.go +++ b/src/cmd/tui/tui_test.go @@ -13,7 +13,7 @@ import ( ) const author_data = `syntax for the test file -te|testing|TestUser|test@test.test|ex +te|testing|TestUser|test@test.test|ex;;gr0 ti|testtest|UserName2|testing@user.io;;gr1` var envVar string @@ -36,6 +36,13 @@ func teardown() { os.Setenv("author_file", envVar) } +func keyPress(tm *teatest.TestModel, key string) { + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune(key), + }) +} + // tui_show_users TESTS BEGIN func TestShowUser(t *testing.T) { setup() @@ -50,10 +57,7 @@ func TestShowUser(t *testing.T) { return bytes.Contains(bts, []byte("syntax for the test file")) }, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2)) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("q"), - }) + keyPress(tm, "q") tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) @@ -72,34 +76,19 @@ func TestEntryTA(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("T"), - }) + keyPress(tm, "T") tm.Type("test") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") tm.Type("testtest@temp.io") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("esc"), - }) + keyPress(tm, "esc") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -133,57 +122,27 @@ func Test_EntryCA(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("C"), - }) + keyPress(tm, "C") tm.Type("test") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") tm.Type("testing2") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") tm.Type("TestUser") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") tm.Type("test@temp.io") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") tm.Type("gr6") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("tab"), - }) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("esc"), - }) + keyPress(tm, "enter") + keyPress(tm, "enter") + keyPress(tm, "tab") + keyPress(tm, "enter") + keyPress(tm, "esc") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -230,10 +189,7 @@ func Test_EntryCM(t *testing.T) { ) tm.Type("test commit message") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") fm := tm.FinalModel(t) m, ok := fm.(model_cm) @@ -259,15 +215,9 @@ func Test_EntrySelectUsers(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune(" "), - }) + keyPress(tm, " ") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -295,15 +245,9 @@ func Test_EntrySelectAll(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("A"), - }) + keyPress(tm, "A") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -330,15 +274,9 @@ func Test_EntryNegation(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("n"), - }) + keyPress(tm, "n") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -365,20 +303,11 @@ func Test_EntryDeleteAuthor(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("D"), - }) + keyPress(tm, "D") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("D"), - }) + keyPress(tm, "D") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -407,20 +336,11 @@ func Test_GroupSelection(t *testing.T) { tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(300, 300), ) - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("f"), - }) + keyPress(tm, "f") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("enter"), - }) + keyPress(tm, "enter") fm := tm.FinalModel(t) m, ok := fm.(model) @@ -429,7 +349,31 @@ func Test_GroupSelection(t *testing.T) { } if len(selected) != 1 { - t.Errorf("Expected 1 selected item, got %d", len(selected)) + t.Errorf("Expected not 1 selected item, got %d", len(selected)) + } +} + +func Test_pagination(t *testing.T) { + setup() + defer teardown() + + m := mainModel{} + + tm := teatest.NewTestModel( + t, m, teatest.WithInitialTermSize(25, 25), + ) + + keyPress(tm, "right") + tm.Quit() + + fm := tm.FinalModel(t) + m, ok := fm.(mainModel) + if !ok { + t.Errorf("Expected model, got %T", fm) + } + + if m.paginator.Page != 1 { + t.Errorf("Expected page 1, got %d", m.paginator.Page) } }