From c666f093c48246ca2c24a0f4bac524f04b5e5658 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sat, 26 Jul 2025 19:00:56 +0530 Subject: [PATCH 1/9] basic ui model --- go.mod | 24 +++++++++++++++++++++++- go.sum | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1930f91..41f05a5 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,36 @@ module github.com/maniac-en/req go 1.24.4 require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/lipgloss v1.1.0 github.com/mattn/go-sqlite3 v1.14.29 github.com/pressly/goose/v3 v3.24.3 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/mfridman/interpolate v0.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index 56cf19c..8dbf694 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,53 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ= github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -18,18 +56,29 @@ github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwW github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index 82e3734..fe85635 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" + tea "github.com/charmbracelet/bubbletea" "github.com/maniac-en/req/internal/collections" "github.com/maniac-en/req/internal/database" "github.com/maniac-en/req/internal/log" @@ -136,4 +137,35 @@ func main() { log.Info("application initialized", "components", []string{"database", "collections", "logging"}) log.Debug("configuration loaded", "collections_manager", config.Collections != nil, "database", config.DB != nil) log.Info("application started successfully") + + // Entry point for UI + program := tea.NewProgram(initialModel(), tea.WithAltScreen()) + if _, err := program.Run(); err != nil { + log.Fatal("Fatal error:", err) + } +} + +// UI Stuff +type model struct{} + +func initialModel() model { + return model{} +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "q" { + return m, tea.Quit + } + } + return m, nil +} + +func (m model) View() string { + return "Hello world!" } From b3e20c1aa5415c29b0bca46956f371e74262996e Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sat, 26 Jul 2025 20:01:03 +0530 Subject: [PATCH 2/9] moved the Model init to its own file, created a basic list collections page to see and pick collections --- internal/app/model.go | 68 ++++++++++++++++++ internal/tabs/collections.go | 82 ++++++++++++++++++++++ internal/tabs/components.go | 129 +++++++++++++++++++++++++++++++++++ internal/tabs/tab.go | 15 ++++ main.go | 28 +------- 5 files changed, 296 insertions(+), 26 deletions(-) create mode 100644 internal/app/model.go create mode 100644 internal/tabs/collections.go create mode 100644 internal/tabs/components.go create mode 100644 internal/tabs/tab.go diff --git a/internal/app/model.go b/internal/app/model.go new file mode 100644 index 0000000..9ead0f0 --- /dev/null +++ b/internal/app/model.go @@ -0,0 +1,68 @@ +package app + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/maniac-en/req/internal/tabs" +) + +type Model struct { + tabs []tabs.Tab + activeTab int +} + +func InitialModel() Model { + tabList := []tabs.Tab{ + tabs.NewCollectionsTab(), + } + + return Model{ + tabs: tabList, + activeTab: 0, + } +} + +func (m Model) Init() tea.Cmd { + return m.tabs[m.activeTab].Init() +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + default: + var cmd tea.Cmd + m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) + return m, cmd + } + default: + var cmd tea.Cmd + m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) + return m, cmd + } +} + +func (m Model) View() string { + var tabHeaders strings.Builder + for i, tab := range m.tabs { + style := lipgloss.NewStyle().Padding(0, 2) + if i == m.activeTab { + style = style.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230")) + } else { + style = style.Background(lipgloss.Color("240")).Foreground(lipgloss.Color("255")) + } + tabHeaders.WriteString(style.Render(tab.Name())) + } + + content := m.tabs[m.activeTab].View() + + return fmt.Sprintf("%s\n\n%s", + tabHeaders.String(), + content, + ) +} diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go new file mode 100644 index 0000000..6ed7f08 --- /dev/null +++ b/internal/tabs/collections.go @@ -0,0 +1,82 @@ +package tabs + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +type collectionsOpts struct { + options []string +} + +type CollectionsTab struct { + name string + selectUI SelectInput + loaded bool +} + +func NewCollectionsTab() *CollectionsTab { + return &CollectionsTab{ + name: "Collections", + selectUI: NewSelectInput(), + loaded: false, + } +} + +func (c *CollectionsTab) fetchOptions() tea.Cmd { + // this is here for now to replicate what a db call would look like + return tea.Tick(time.Millisecond*1000, func(time.Time) tea.Msg { + return collectionsOpts{ + options: []string{ + "Collection 1", + "Collection 2", + "Collection 3", + "Collection 4", + }, + } + }) +} + +func (c *CollectionsTab) Name() string { + return c.name +} + +func (c *CollectionsTab) Init() tea.Cmd { + c.selectUI.Focus() + return tea.Batch( + c.selectUI.Init(), + c.fetchOptions(), + ) +} + +func (c *CollectionsTab) OnFocus() tea.Cmd { + c.selectUI.Focus() + if !c.loaded { + return c.fetchOptions() + } + return nil +} + +func (c *CollectionsTab) OnBlur() tea.Cmd { + c.selectUI.Blur() + return nil +} + +func (c *CollectionsTab) Update(msg tea.Msg) (Tab, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case collectionsOpts: + c.selectUI.SetOptions(msg.options) + default: + c.selectUI, cmd = c.selectUI.Update(msg) + } + return c, cmd +} + +func (c *CollectionsTab) View() string { + content := "Select Collection:\n\n" + c.selectUI.View() + + return content +} diff --git a/internal/tabs/components.go b/internal/tabs/components.go new file mode 100644 index 0000000..e9bf7b0 --- /dev/null +++ b/internal/tabs/components.go @@ -0,0 +1,129 @@ +package tabs + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// for now the focus is just on the collections tab +// we'll see how we can change this around to accommodate +// more tabs +type collection string + +type renderMethod struct{} + +func (c collection) FilterValue() string { return string(c) } + +func (r renderMethod) Height() int { + return 1 +} + +func (r renderMethod) Spacing() int { + return 0 +} + +func (r renderMethod) Update(_ tea.Msg, _ *list.Model) tea.Cmd { + return nil +} +func (r renderMethod) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(collection) + if !ok { + return + } + + str := fmt.Sprintf("%s", i) + + fn := lipgloss.NewStyle().PaddingLeft(4).Render + if index == m.Index() { + fn = func(s ...string) string { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("170")). + Bold(true). + PaddingLeft(2). + Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +type SelectInput struct { + list list.Model + loading bool + focused bool + spinner spinner.Model +} + +func NewSelectInput() SelectInput { + l := list.New([]list.Item{}, renderMethod{}, 50, 14) + l.SetShowStatusBar(true) + l.SetFilteringEnabled(true) + l.SetShowHelp(true) + l.SetShowTitle(false) + + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + + return SelectInput{ + list: l, + loading: true, + focused: false, + spinner: s, + } +} +func (s SelectInput) Init() tea.Cmd { + return s.spinner.Tick +} + +func (s SelectInput) Update(msg tea.Msg) (SelectInput, tea.Cmd) { + var cmd tea.Cmd + + if s.loading { + s.spinner, cmd = s.spinner.Update(msg) + return s, cmd + } + + if s.focused && !s.loading { + s.list, cmd = s.list.Update(msg) + } + + return s, cmd +} + +func (s SelectInput) View() string { + if s.loading { + return fmt.Sprintf("%s Loading options...", s.spinner.View()) + } + return s.list.View() +} + +func (s SelectInput) Focused() bool { return s.focused } +func (s *SelectInput) Focus() { s.focused = true } +func (s *SelectInput) Blur() { s.focused = false } +func (s SelectInput) IsLoading() bool { return s.loading } + +func (s *SelectInput) SetOptions(options []string) { + items := make([]list.Item, len(options)) + for i, option := range options { + items[i] = collection(option) + } + s.list.SetItems(items) + s.loading = false +} + +func (s SelectInput) GetSelected() string { + if s.loading || len(s.list.Items()) == 0 { + return "" + } + if selectedItem := s.list.SelectedItem(); selectedItem != nil { + return string(selectedItem.(collection)) + } + return "" +} diff --git a/internal/tabs/tab.go b/internal/tabs/tab.go new file mode 100644 index 0000000..9ba061c --- /dev/null +++ b/internal/tabs/tab.go @@ -0,0 +1,15 @@ +package tabs + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +// this is what a tab is loosely defined as +type Tab interface { + Name() string + Init() tea.Cmd + Update(tea.Msg) (Tab, tea.Cmd) + View() string + OnFocus() tea.Cmd + OnBlur() tea.Cmd +} diff --git a/main.go b/main.go index fe85635..3866665 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "path/filepath" tea "github.com/charmbracelet/bubbletea" + "github.com/maniac-en/req/internal/app" "github.com/maniac-en/req/internal/collections" "github.com/maniac-en/req/internal/database" "github.com/maniac-en/req/internal/log" @@ -139,33 +140,8 @@ func main() { log.Info("application started successfully") // Entry point for UI - program := tea.NewProgram(initialModel(), tea.WithAltScreen()) + program := tea.NewProgram(app.InitialModel(), tea.WithAltScreen()) if _, err := program.Run(); err != nil { log.Fatal("Fatal error:", err) } } - -// UI Stuff -type model struct{} - -func initialModel() model { - return model{} -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.String() == "q" { - return m, tea.Quit - } - } - return m, nil -} - -func (m model) View() string { - return "Hello world!" -} From 2063a30d12539266a9bc46112ea97c965137d970 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sat, 26 Jul 2025 23:38:24 +0530 Subject: [PATCH 3/9] collections list page done --- internal/app/model.go | 39 ++++++++++++++++++-------------- internal/tabs/collections.go | 43 +++++++++++++++++++++++++++-------- internal/tabs/components.go | 44 ++++++++++++++++++++++++++++-------- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/internal/app/model.go b/internal/app/model.go index 9ead0f0..e87d636 100644 --- a/internal/app/model.go +++ b/internal/app/model.go @@ -1,9 +1,6 @@ package app import ( - "fmt" - "strings" - tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/maniac-en/req/internal/tabs" @@ -12,6 +9,8 @@ import ( type Model struct { tabs []tabs.Tab activeTab int + width int + height int } func InitialModel() Model { @@ -40,6 +39,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) return m, cmd } + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil default: var cmd tea.Cmd m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) @@ -48,21 +51,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m Model) View() string { - var tabHeaders strings.Builder - for i, tab := range m.tabs { - style := lipgloss.NewStyle().Padding(0, 2) - if i == m.activeTab { - style = style.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230")) - } else { - style = style.Background(lipgloss.Color("240")).Foreground(lipgloss.Color("255")) - } - tabHeaders.WriteString(style.Render(tab.Name())) - } + const headerHeight = 1 + headerText := m.tabs[m.activeTab].Name() + + headerStyle := lipgloss.NewStyle(). + Padding(0, 2). + Background(lipgloss.Color("62")). + Foreground(lipgloss.Color("230")). + Height(headerHeight). + Width(len(headerText)+10). + Align(lipgloss.Center, lipgloss.Top) content := m.tabs[m.activeTab].View() - return fmt.Sprintf("%s\n\n%s", - tabHeaders.String(), - content, - ) + contentStyle := lipgloss.NewStyle(). + Width(m.width). + Height(m.height-headerHeight). + Align(lipgloss.Center, lipgloss.Center) + + return lipgloss.JoinVertical(lipgloss.Center, headerStyle.Render(headerText), contentStyle.Render(content)) } diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go index 6ed7f08..30587e0 100644 --- a/internal/tabs/collections.go +++ b/internal/tabs/collections.go @@ -4,10 +4,24 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type collectionsOpts struct { - options []string + options []OptionPair +} + +type OptionPair struct { + Label string + Value string +} + +var opts = []OptionPair{ + {Label: "Collection 1", Value: "1"}, + {Label: "Collection 2", Value: "2"}, + {Label: "Collection 3", Value: "3"}, + {Label: "Collection 4", Value: "4"}, + {Label: "Collection 5", Value: "5"}, } type CollectionsTab struct { @@ -28,12 +42,7 @@ func (c *CollectionsTab) fetchOptions() tea.Cmd { // this is here for now to replicate what a db call would look like return tea.Tick(time.Millisecond*1000, func(time.Time) tea.Msg { return collectionsOpts{ - options: []string{ - "Collection 1", - "Collection 2", - "Collection 3", - "Collection 4", - }, + options: opts, } }) } @@ -69,14 +78,30 @@ func (c *CollectionsTab) Update(msg tea.Msg) (Tab, tea.Cmd) { switch msg := msg.(type) { case collectionsOpts: c.selectUI.SetOptions(msg.options) + c.loaded = true default: c.selectUI, cmd = c.selectUI.Update(msg) } + return c, cmd } func (c *CollectionsTab) View() string { - content := "Select Collection:\n\n" + c.selectUI.View() - return content + if c.selectUI.IsLoading() { + return c.selectUI.View() + } + + selectContent := c.selectUI.View() + + style := lipgloss.NewStyle().PaddingRight(4) + + if !c.selectUI.IsLoading() && len(c.selectUI.list.Items()) > 0 { + title := "Select Collection:\n\n" + instructions := "\n ↑/k - up | ↓/j - down | / - search | + - add collection | enter - select" + return title + style.Render(selectContent) + instructions + } + + return style.Render(selectContent) + } diff --git a/internal/tabs/components.go b/internal/tabs/components.go index e9bf7b0..5ffad69 100644 --- a/internal/tabs/components.go +++ b/internal/tabs/components.go @@ -14,11 +14,16 @@ import ( // for now the focus is just on the collections tab // we'll see how we can change this around to accommodate // more tabs -type collection string +type collection struct { + label string + value string +} type renderMethod struct{} -func (c collection) FilterValue() string { return string(c) } +func (c collection) FilterValue() string { return c.label } +func (c collection) Label() string { return c.label } +func (c collection) Value() string { return c.value } func (r renderMethod) Height() int { return 1 @@ -37,7 +42,7 @@ func (r renderMethod) Render(w io.Writer, m list.Model, index int, listItem list return } - str := fmt.Sprintf("%s", i) + str := i.Label() fn := lipgloss.NewStyle().PaddingLeft(4).Render if index == m.Index() { @@ -62,9 +67,9 @@ type SelectInput struct { func NewSelectInput() SelectInput { l := list.New([]list.Item{}, renderMethod{}, 50, 14) - l.SetShowStatusBar(true) + l.SetShowStatusBar(false) l.SetFilteringEnabled(true) - l.SetShowHelp(true) + l.SetShowHelp(false) l.SetShowTitle(false) s := spinner.New() @@ -101,6 +106,25 @@ func (s SelectInput) View() string { if s.loading { return fmt.Sprintf("%s Loading options...", s.spinner.View()) } + + // Add this check for empty options + if len(s.list.Items()) == 0 { + const bodyText = "No options available\nCreate your first option to get started!" + const instruction = "\n\n\n\n\n\n\n+ - add a collection" + emptyStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")). + Italic(true). + Align(lipgloss.Center). + PaddingTop(5). + Render(bodyText) + normalStyle := lipgloss.NewStyle(). + Italic(true). + Align(lipgloss.Center). + Render(instruction) + + return lipgloss.JoinVertical(lipgloss.Center, emptyStyle, normalStyle) + } + return s.list.View() } @@ -109,12 +133,12 @@ func (s *SelectInput) Focus() { s.focused = true } func (s *SelectInput) Blur() { s.focused = false } func (s SelectInput) IsLoading() bool { return s.loading } -func (s *SelectInput) SetOptions(options []string) { - items := make([]list.Item, len(options)) +func (s *SelectInput) SetOptions(options []OptionPair) { + collections := make([]list.Item, len(options)) for i, option := range options { - items[i] = collection(option) + collections[i] = collection{label: option.Label, value: option.Value} } - s.list.SetItems(items) + s.list.SetItems(collections) s.loading = false } @@ -123,7 +147,7 @@ func (s SelectInput) GetSelected() string { return "" } if selectedItem := s.list.SelectedItem(); selectedItem != nil { - return string(selectedItem.(collection)) + return selectedItem.(collection).Value() } return "" } From 65483edf35ed415d3329555ffeec1609f2f74edd Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sun, 27 Jul 2025 00:53:52 +0530 Subject: [PATCH 4/9] added add collections page, centralised collection opts temporarily, created a messages package to handle page switching --- internal/app/model.go | 13 +++- internal/messages/messages.go | 6 ++ internal/tabs/add-collections.go | 114 +++++++++++++++++++++++++++++++ internal/tabs/collections.go | 28 ++++---- internal/tabs/tab.go | 8 +++ 5 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 internal/messages/messages.go create mode 100644 internal/tabs/add-collections.go diff --git a/internal/app/model.go b/internal/app/model.go index e87d636..4f2f0f3 100644 --- a/internal/app/model.go +++ b/internal/app/model.go @@ -3,6 +3,7 @@ package app import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/maniac-en/req/internal/messages" "github.com/maniac-en/req/internal/tabs" ) @@ -16,6 +17,7 @@ type Model struct { func InitialModel() Model { tabList := []tabs.Tab{ tabs.NewCollectionsTab(), + tabs.NewAddCollectionTab(), } return Model{ @@ -29,13 +31,21 @@ func (m Model) Init() tea.Cmd { } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd switch msg := msg.(type) { + + case messages.SwitchTabMsg: + if msg.TabIndex >= 0 && msg.TabIndex < len(m.tabs) { + m.activeTab = msg.TabIndex + return m, m.tabs[m.activeTab].OnFocus() + } + return m, nil + case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit default: - var cmd tea.Cmd m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) return m, cmd } @@ -44,7 +54,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil default: - var cmd tea.Cmd m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) return m, cmd } diff --git a/internal/messages/messages.go b/internal/messages/messages.go new file mode 100644 index 0000000..1a7dede --- /dev/null +++ b/internal/messages/messages.go @@ -0,0 +1,6 @@ +package messages + +// SwitchTabMsg is used to switch between tabs +type SwitchTabMsg struct { + TabIndex int +} diff --git a/internal/tabs/add-collections.go b/internal/tabs/add-collections.go new file mode 100644 index 0000000..5572220 --- /dev/null +++ b/internal/tabs/add-collections.go @@ -0,0 +1,114 @@ +package tabs + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/maniac-en/req/internal/messages" +) + +type AddCollectionTab struct { + name string + nameInput textinput.Model + focused bool +} + +func NewAddCollectionTab() *AddCollectionTab { + textInput := textinput.New() + textInput.Placeholder = "Enter your collection's name... " + textInput.Focus() + + textInput.CharLimit = 100 + textInput.Width = 50 + + return &AddCollectionTab{ + name: "Add Collection", + nameInput: textInput, + focused: true, + } +} + +func (a *AddCollectionTab) Name() string { + return a.name +} + +func (a *AddCollectionTab) Init() tea.Cmd { + return textinput.Blink +} + +func (a *AddCollectionTab) OnFocus() tea.Cmd { + a.nameInput.Focus() + a.focused = true + return textinput.Blink +} + +func (a *AddCollectionTab) OnBlur() tea.Cmd { + a.nameInput.Blur() + a.focused = false + return nil +} + +func (a *AddCollectionTab) Update(msg tea.Msg) (Tab, tea.Cmd) { + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + if a.nameInput.Value() != "" { + return a.addCollection(a.nameInput.Value()) + } + case "esc": + return a, func() tea.Msg { + return messages.SwitchTabMsg{TabIndex: 0} + } + } + } + + a.nameInput, _ = a.nameInput.Update(msg) + return a, nil +} + +func (a *AddCollectionTab) View() string { + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("205")). + MarginBottom(2) + + inputStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2). + MarginBottom(2) + + helpStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")). + MarginTop(1) + + form := lipgloss.JoinVertical(lipgloss.Center, + titleStyle.Render("Create New Collection"), + inputStyle.Render(a.nameInput.View()), + helpStyle.Render("Press Enter to create • Esc to cancel"), + ) + + containerStyle := lipgloss.NewStyle(). + Width(60). + Height(20). + Align(lipgloss.Center, lipgloss.Center) + + return containerStyle.Render(form) +} + +func (a *AddCollectionTab) addCollection(name string) (Tab, tea.Cmd) { + newOption := OptionPair{ + Label: name, + Value: name, + } + + GlobalCollections = append(GlobalCollections, newOption) + + a.nameInput.SetValue("") + + return a, func() tea.Msg { + return messages.SwitchTabMsg{TabIndex: 0} + } +} diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go index 30587e0..3c052f9 100644 --- a/internal/tabs/collections.go +++ b/internal/tabs/collections.go @@ -5,6 +5,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/maniac-en/req/internal/messages" ) type collectionsOpts struct { @@ -16,14 +17,6 @@ type OptionPair struct { Value string } -var opts = []OptionPair{ - {Label: "Collection 1", Value: "1"}, - {Label: "Collection 2", Value: "2"}, - {Label: "Collection 3", Value: "3"}, - {Label: "Collection 4", Value: "4"}, - {Label: "Collection 5", Value: "5"}, -} - type CollectionsTab struct { name string selectUI SelectInput @@ -42,7 +35,7 @@ func (c *CollectionsTab) fetchOptions() tea.Cmd { // this is here for now to replicate what a db call would look like return tea.Tick(time.Millisecond*1000, func(time.Time) tea.Msg { return collectionsOpts{ - options: opts, + options: GlobalCollections, } }) } @@ -61,10 +54,8 @@ func (c *CollectionsTab) Init() tea.Cmd { func (c *CollectionsTab) OnFocus() tea.Cmd { c.selectUI.Focus() - if !c.loaded { - return c.fetchOptions() - } - return nil + + return c.fetchOptions() } func (c *CollectionsTab) OnBlur() tea.Cmd { @@ -79,6 +70,17 @@ func (c *CollectionsTab) Update(msg tea.Msg) (Tab, tea.Cmd) { case collectionsOpts: c.selectUI.SetOptions(msg.options) c.loaded = true + + case tea.KeyMsg: + switch msg.String() { + case "+": + return c, func() tea.Msg { + return messages.SwitchTabMsg{TabIndex: 1} + } + default: + c.selectUI, cmd = c.selectUI.Update(msg) + } + default: c.selectUI, cmd = c.selectUI.Update(msg) } diff --git a/internal/tabs/tab.go b/internal/tabs/tab.go index 9ba061c..80f805c 100644 --- a/internal/tabs/tab.go +++ b/internal/tabs/tab.go @@ -4,6 +4,14 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +var GlobalCollections = []OptionPair{ + {Label: "Collection 1", Value: "1"}, + {Label: "Collection 2", Value: "2"}, + {Label: "Collection 3", Value: "3"}, + {Label: "Collection 4", Value: "4"}, + {Label: "Collection 5", Value: "5"}, +} + // this is what a tab is loosely defined as type Tab interface { Name() string From 72e41c4ff7a82b48a60ba4ce72cb1ed68cddd3d4 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sun, 27 Jul 2025 01:17:42 +0530 Subject: [PATCH 5/9] added delete collection --- internal/tabs/collections.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go index 3c052f9..eddc4d0 100644 --- a/internal/tabs/collections.go +++ b/internal/tabs/collections.go @@ -73,10 +73,14 @@ func (c *CollectionsTab) Update(msg tea.Msg) (Tab, tea.Cmd) { case tea.KeyMsg: switch msg.String() { - case "+": + case "a": return c, func() tea.Msg { return messages.SwitchTabMsg{TabIndex: 1} } + case "d": + if selected := c.selectUI.GetSelected(); selected != "" { + return c, c.deleteCollection(selected) + } default: c.selectUI, cmd = c.selectUI.Update(msg) } @@ -100,10 +104,20 @@ func (c *CollectionsTab) View() string { if !c.selectUI.IsLoading() && len(c.selectUI.list.Items()) > 0 { title := "Select Collection:\n\n" - instructions := "\n ↑/k - up | ↓/j - down | / - search | + - add collection | enter - select" + instructions := "\n ↑/k - up | ↓/j - down | / - search | + - add collection | enter - select | d - delete collection" return title + style.Render(selectContent) + instructions } return style.Render(selectContent) } + +func (c *CollectionsTab) deleteCollection(value string) tea.Cmd { + for i, collection := range GlobalCollections { + if collection.Value == value { + GlobalCollections = append(GlobalCollections[:i], GlobalCollections[i+1:]...) + break + } + } + return c.fetchOptions() +} From 2adb692b59088e3926f924e2a069bc515fae31c2 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sun, 27 Jul 2025 01:30:47 +0530 Subject: [PATCH 6/9] added edit collection page --- internal/app/model.go | 7 ++ internal/messages/messages.go | 6 +- internal/tabs/collections.go | 28 ++++++- internal/tabs/edit-collections.go | 124 ++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 internal/tabs/edit-collections.go diff --git a/internal/app/model.go b/internal/app/model.go index 4f2f0f3..cc4e959 100644 --- a/internal/app/model.go +++ b/internal/app/model.go @@ -18,6 +18,7 @@ func InitialModel() Model { tabList := []tabs.Tab{ tabs.NewCollectionsTab(), tabs.NewAddCollectionTab(), + tabs.NewEditCollectionTab(), } return Model{ @@ -41,6 +42,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil + case messages.EditCollectionMsg: + if editTab, ok := m.tabs[2].(*tabs.EditCollectionTab); ok { + editTab.SetEditingCollection(msg.Label, msg.Value) + } + return m, nil + case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": diff --git a/internal/messages/messages.go b/internal/messages/messages.go index 1a7dede..4001b20 100644 --- a/internal/messages/messages.go +++ b/internal/messages/messages.go @@ -1,6 +1,10 @@ package messages -// SwitchTabMsg is used to switch between tabs type SwitchTabMsg struct { TabIndex int } + +type EditCollectionMsg struct { + Label string + Value string +} diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go index eddc4d0..fcdd015 100644 --- a/internal/tabs/collections.go +++ b/internal/tabs/collections.go @@ -81,6 +81,10 @@ func (c *CollectionsTab) Update(msg tea.Msg) (Tab, tea.Cmd) { if selected := c.selectUI.GetSelected(); selected != "" { return c, c.deleteCollection(selected) } + case "e": // Add edit key handling + if selected := c.selectUI.GetSelected(); selected != "" { + return c, c.editCollection(selected) + } default: c.selectUI, cmd = c.selectUI.Update(msg) } @@ -104,7 +108,7 @@ func (c *CollectionsTab) View() string { if !c.selectUI.IsLoading() && len(c.selectUI.list.Items()) > 0 { title := "Select Collection:\n\n" - instructions := "\n ↑/k - up | ↓/j - down | / - search | + - add collection | enter - select | d - delete collection" + instructions := "\n ↑/k - up | ↓/j - down | / - search | + - add collection | enter - select | d - delete collection | e - edit collection" return title + style.Render(selectContent) + instructions } @@ -121,3 +125,25 @@ func (c *CollectionsTab) deleteCollection(value string) tea.Cmd { } return c.fetchOptions() } + +func (c *CollectionsTab) editCollection(value string) tea.Cmd { + var label string + for _, collection := range GlobalCollections { + if collection.Value == value { + label = collection.Label + break + } + } + + return tea.Batch( + func() tea.Msg { + return messages.EditCollectionMsg{ + Label: label, + Value: value, + } + }, + func() tea.Msg { + return messages.SwitchTabMsg{TabIndex: 2} + }, + ) +} diff --git a/internal/tabs/edit-collections.go b/internal/tabs/edit-collections.go new file mode 100644 index 0000000..0445252 --- /dev/null +++ b/internal/tabs/edit-collections.go @@ -0,0 +1,124 @@ +package tabs + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/maniac-en/req/internal/messages" +) + +type EditCollectionTab struct { + name string + nameInput textinput.Model + originalValue string + focused bool +} + +func NewEditCollectionTab() *EditCollectionTab { + textInput := textinput.New() + textInput.Placeholder = "Enter collection name..." + textInput.CharLimit = 50 + textInput.Width = 30 + + return &EditCollectionTab{ + name: "Edit Collection", + nameInput: textInput, + focused: true, + } +} + +func (e *EditCollectionTab) Name() string { + return e.name +} + +func (e *EditCollectionTab) Init() tea.Cmd { + return textinput.Blink +} + +func (e *EditCollectionTab) OnFocus() tea.Cmd { + e.nameInput.Focus() + e.focused = true + return textinput.Blink +} + +func (e *EditCollectionTab) OnBlur() tea.Cmd { + e.nameInput.Blur() + e.focused = false + return nil +} + +func (e *EditCollectionTab) SetEditingCollection(label, value string) { + e.nameInput.SetValue(label) + e.originalValue = value +} + +func (e *EditCollectionTab) Update(msg tea.Msg) (Tab, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + if e.nameInput.Value() != "" { + return e.updateCollection(e.nameInput.Value()) + } + return e, nil + case "esc": + return e, func() tea.Msg { + return messages.SwitchTabMsg{TabIndex: 0} + } + default: + e.nameInput, cmd = e.nameInput.Update(msg) + return e, cmd + } + default: + e.nameInput, cmd = e.nameInput.Update(msg) + return e, cmd + } +} + +func (e *EditCollectionTab) View() string { + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("205")). + MarginBottom(2) + + inputStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2). + MarginBottom(2) + + helpStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")). + MarginTop(1) + + form := lipgloss.JoinVertical(lipgloss.Center, + titleStyle.Render("Edit Collection"), + inputStyle.Render(e.nameInput.View()), + helpStyle.Render("Press Enter to save • Esc to cancel"), + ) + + containerStyle := lipgloss.NewStyle(). + Width(60). + Height(20). + Align(lipgloss.Center, lipgloss.Center) + + return containerStyle.Render(form) +} + +func (e *EditCollectionTab) updateCollection(newName string) (Tab, tea.Cmd) { + for i, collection := range GlobalCollections { + if collection.Value == e.originalValue { + GlobalCollections[i] = OptionPair{ + Label: newName, + Value: e.originalValue, + } + break + } + } + + return e, func() tea.Msg { + return messages.SwitchTabMsg{TabIndex: 0} + } +} From 2de51386089f057f67f2cfd0e46f7afe728b4867 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sun, 27 Jul 2025 02:15:18 +0530 Subject: [PATCH 7/9] footer is now rendered separate from content --- internal/app/model.go | 28 ++++++++++++++++++++++------ internal/tabs/add-collections.go | 9 ++++----- internal/tabs/collections.go | 12 ++++++++---- internal/tabs/edit-collections.go | 9 ++++----- internal/tabs/tab.go | 1 + 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/internal/app/model.go b/internal/app/model.go index cc4e959..cfc81ea 100644 --- a/internal/app/model.go +++ b/internal/app/model.go @@ -67,23 +67,39 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m Model) View() string { - const headerHeight = 1 + const headerFooterHeight = 1 + const padding = 1 headerText := m.tabs[m.activeTab].Name() + instructions := m.tabs[m.activeTab].Instructions() headerStyle := lipgloss.NewStyle(). - Padding(0, 2). + Padding(1, 0). Background(lipgloss.Color("62")). Foreground(lipgloss.Color("230")). - Height(headerHeight). + Height(headerFooterHeight). Width(len(headerText)+10). Align(lipgloss.Center, lipgloss.Top) - content := m.tabs[m.activeTab].View() + footerStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("45")). + Width(m.width-50). + PaddingBottom(1). + Height(headerFooterHeight). + Align(lipgloss.Center, lipgloss.Center) + + renderedHeader := headerStyle.Render(headerText) + renderedFooter := footerStyle.Render(instructions) + headerHeight := lipgloss.Height(renderedHeader) + footerHeight := lipgloss.Height(renderedFooter) + + contentHeight := m.height - headerHeight - footerHeight contentStyle := lipgloss.NewStyle(). Width(m.width). - Height(m.height-headerHeight). + Height(contentHeight). Align(lipgloss.Center, lipgloss.Center) - return lipgloss.JoinVertical(lipgloss.Center, headerStyle.Render(headerText), contentStyle.Render(content)) + content := m.tabs[m.activeTab].View() + + return lipgloss.JoinVertical(lipgloss.Center, renderedHeader, contentStyle.Render(content), renderedFooter) } diff --git a/internal/tabs/add-collections.go b/internal/tabs/add-collections.go index 5572220..98c8fc3 100644 --- a/internal/tabs/add-collections.go +++ b/internal/tabs/add-collections.go @@ -32,6 +32,10 @@ func (a *AddCollectionTab) Name() string { return a.name } +func (a *AddCollectionTab) Instructions() string { + return "Enter - create • Esc - cancel" +} + func (a *AddCollectionTab) Init() tea.Cmd { return textinput.Blink } @@ -80,14 +84,9 @@ func (a *AddCollectionTab) View() string { Padding(1, 2). MarginBottom(2) - helpStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("240")). - MarginTop(1) - form := lipgloss.JoinVertical(lipgloss.Center, titleStyle.Render("Create New Collection"), inputStyle.Render(a.nameInput.View()), - helpStyle.Render("Press Enter to create • Esc to cancel"), ) containerStyle := lipgloss.NewStyle(). diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go index fcdd015..d50cfaa 100644 --- a/internal/tabs/collections.go +++ b/internal/tabs/collections.go @@ -44,6 +44,10 @@ func (c *CollectionsTab) Name() string { return c.name } +func (c *CollectionsTab) Instructions() string { + return "\n k - up • j - down • / - search • + - add collection • enter - select • d - delete collection • e - edit collection" +} + func (c *CollectionsTab) Init() tea.Cmd { c.selectUI.Focus() return tea.Batch( @@ -104,12 +108,12 @@ func (c *CollectionsTab) View() string { selectContent := c.selectUI.View() - style := lipgloss.NewStyle().PaddingRight(4) + style := lipgloss.NewStyle(). + PaddingRight(4) if !c.selectUI.IsLoading() && len(c.selectUI.list.Items()) > 0 { - title := "Select Collection:\n\n" - instructions := "\n ↑/k - up | ↓/j - down | / - search | + - add collection | enter - select | d - delete collection | e - edit collection" - return title + style.Render(selectContent) + instructions + title := "\n\n\n\n\n\n\nSelect Collection:\n\n" + return title + style.Render(selectContent) } return style.Render(selectContent) diff --git a/internal/tabs/edit-collections.go b/internal/tabs/edit-collections.go index 0445252..ddb4784 100644 --- a/internal/tabs/edit-collections.go +++ b/internal/tabs/edit-collections.go @@ -31,6 +31,10 @@ func (e *EditCollectionTab) Name() string { return e.name } +func (e *EditCollectionTab) Instructions() string { + return "Press Enter to create • Esc to cancel" +} + func (e *EditCollectionTab) Init() tea.Cmd { return textinput.Blink } @@ -89,14 +93,9 @@ func (e *EditCollectionTab) View() string { Padding(1, 2). MarginBottom(2) - helpStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("240")). - MarginTop(1) - form := lipgloss.JoinVertical(lipgloss.Center, titleStyle.Render("Edit Collection"), inputStyle.Render(e.nameInput.View()), - helpStyle.Render("Press Enter to save • Esc to cancel"), ) containerStyle := lipgloss.NewStyle(). diff --git a/internal/tabs/tab.go b/internal/tabs/tab.go index 80f805c..141c8fe 100644 --- a/internal/tabs/tab.go +++ b/internal/tabs/tab.go @@ -15,6 +15,7 @@ var GlobalCollections = []OptionPair{ // this is what a tab is loosely defined as type Tab interface { Name() string + Instructions() string Init() tea.Cmd Update(tea.Msg) (Tab, tea.Cmd) View() string From 9aec6e61eb6ad696e4d1c13623f4b167bcbd2d79 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sun, 27 Jul 2025 02:18:13 +0530 Subject: [PATCH 8/9] small fix to instructions --- internal/tabs/edit-collections.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tabs/edit-collections.go b/internal/tabs/edit-collections.go index ddb4784..aae10fe 100644 --- a/internal/tabs/edit-collections.go +++ b/internal/tabs/edit-collections.go @@ -32,7 +32,7 @@ func (e *EditCollectionTab) Name() string { } func (e *EditCollectionTab) Instructions() string { - return "Press Enter to create • Esc to cancel" + return "Enter - create • Esc - cancel" } func (e *EditCollectionTab) Init() tea.Cmd { From d78a31e3b444568743eae740431a0ece230fccbe Mon Sep 17 00:00:00 2001 From: Mudassir Date: Sun, 27 Jul 2025 03:14:34 +0500 Subject: [PATCH 9/9] fix: fixed q interfering with input fields --- internal/app/model.go | 3 ++- internal/tabs/collections.go | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/app/model.go b/internal/app/model.go index cfc81ea..bf5af8f 100644 --- a/internal/app/model.go +++ b/internal/app/model.go @@ -50,7 +50,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "q": + // removed q here because that was causing issues with input fields + case "ctrl+c": return m, tea.Quit default: m.tabs[m.activeTab], cmd = m.tabs[m.activeTab].Update(msg) diff --git a/internal/tabs/collections.go b/internal/tabs/collections.go index d50cfaa..014cb83 100644 --- a/internal/tabs/collections.go +++ b/internal/tabs/collections.go @@ -3,6 +3,7 @@ package tabs import ( "time" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/maniac-en/req/internal/messages" @@ -31,6 +32,10 @@ func NewCollectionsTab() *CollectionsTab { } } +func (c *CollectionsTab) IsFiltering() bool { + return c.selectUI.list.FilterState() == list.Filtering +} + func (c *CollectionsTab) fetchOptions() tea.Cmd { // this is here for now to replicate what a db call would look like return tea.Tick(time.Millisecond*1000, func(time.Time) tea.Msg { @@ -76,8 +81,14 @@ func (c *CollectionsTab) Update(msg tea.Msg) (Tab, tea.Cmd) { c.loaded = true case tea.KeyMsg: + // Check if list is filtering otherwise the keybinds wouldn't let us type + if c.IsFiltering() { + c.selectUI, cmd = c.selectUI.Update(msg) + return c, cmd + } + switch msg.String() { - case "a": + case "+": return c, func() tea.Msg { return messages.SwitchTabMsg{TabIndex: 1} }