From 7a3d5f2016957111943c2a486f34ce1ca7f18393 Mon Sep 17 00:00:00 2001 From: wbtiger <28288271@qq.com> Date: Sun, 17 May 2026 22:54:21 +0800 Subject: [PATCH] =?UTF-8?q?Phase=205:=20Polish=20=E2=80=94=20Markdown,=20N?= =?UTF-8?q?otifications,=20Search,=20Final=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tui/ui/markdown.go: Glamour-based terminal markdown renderer - tui/views/notifications.go: Notification center (success/error/warning/info) - tui/views/search.go: Global search view (repos + users, incremental input) - Issue detail now renders markdown descriptions via RenderMarkdown - 72 tests passing, build clean Co-Authored-By: Claude Opus 4.7 --- go.mod | 18 +++- go.sum | 33 +++++++ tui/ui/markdown.go | 30 +++++++ tui/views/issue_detail.go | 3 +- tui/views/notifications.go | 76 ++++++++++++++++ tui/views/search.go | 178 +++++++++++++++++++++++++++++++++++++ 6 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 tui/ui/markdown.go create mode 100644 tui/views/notifications.go create mode 100644 tui/views/search.go diff --git a/go.mod b/go.mod index 1ef85f8..4f4a94f 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,27 @@ module github.com/gitlink-org/gitlink-tui go 1.26.1 require ( + github.com/alecthomas/chroma/v2 v2.20.0 // indirect github.com/anthropics/anthropic-sdk-go v1.43.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/charmbracelet/bubbles v1.0.0 // indirect github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/glamour v1.0.0 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect @@ -25,8 +31,10 @@ require ( 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.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // 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.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect @@ -36,8 +44,12 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.16.0 // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 62a9a88..477560d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/anthropics/anthropic-sdk-go v1.43.0 h1:ShY3C7lafzHP0ze1dCxL3ZFZzvkGfXJN91DfZTG8zLM= github.com/anthropics/anthropic-sdk-go v1.43.0/go.mod h1:5cEaslQ6A9ajdL5YUvhNW57LKxEz0OAZ7WEzgZWLD7k= 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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= @@ -12,12 +16,18 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= 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/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= @@ -26,8 +36,12 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= @@ -41,14 +55,21 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI= @@ -67,16 +88,28 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 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= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tui/ui/markdown.go b/tui/ui/markdown.go new file mode 100644 index 0000000..0171d07 --- /dev/null +++ b/tui/ui/markdown.go @@ -0,0 +1,30 @@ +package ui + +import ( + "strings" + + "github.com/charmbracelet/glamour" +) + +// RenderMarkdown converts markdown text to terminal-formatted output. +func RenderMarkdown(md string, width int) string { + if md == "" { + return "" + } + + r, err := glamour.NewTermRenderer( + glamour.WithStandardStyle("dark"), + glamour.WithWordWrap(width-4), + ) + if err != nil { + // Fallback: plain text + return md + } + + out, err := r.Render(md) + if err != nil { + return md + } + + return strings.TrimSuffix(out, "\n") +} diff --git a/tui/views/issue_detail.go b/tui/views/issue_detail.go index 39ca6e9..967e6a0 100644 --- a/tui/views/issue_detail.go +++ b/tui/views/issue_detail.go @@ -86,7 +86,8 @@ func (m *IssueDetailModel) View() string { fmt.Sprintf("%d%%", m.issue.DoneRatio), )) - desc := fmt.Sprintf("%s\n\n%s", title, m.issue.Description) + descMD := ui.RenderMarkdown(m.issue.Description, m.width-30) + desc := fmt.Sprintf("%s\n\n%s", title, descMD) var commentStr string if len(m.comments) > 0 { diff --git a/tui/views/notifications.go b/tui/views/notifications.go new file mode 100644 index 0000000..7123a8f --- /dev/null +++ b/tui/views/notifications.go @@ -0,0 +1,76 @@ +package views + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/gitlink-org/gitlink-tui/tui/ui" +) + +type Notification struct { + Type string // "info", "success", "error", "warning" + Message string +} + +type NotificationsModel struct { + items []Notification + width int + height int +} + +func NewNotificationsModel() *NotificationsModel { + return &NotificationsModel{ + items: make([]Notification, 0), + } +} + +func (m *NotificationsModel) Add(notif Notification) { + m.items = append(m.items, notif) + if len(m.items) > 100 { + m.items = m.items[len(m.items)-50:] + } +} + +func (m *NotificationsModel) Init() tea.Cmd { return nil } + +func (m *NotificationsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + } + return m, nil +} + +func (m *NotificationsModel) View() string { + title := ui.StyleTitle.Render("Notifications") + + if len(m.items) == 0 { + return lipgloss.NewStyle().Padding(1, 2).Render( + lipgloss.JoinVertical(lipgloss.Left, title, "", ui.StyleMuted.Render("No notifications")), + ) + } + + var lines []string + lines = append(lines, title, "") + + for i := len(m.items) - 1; i >= 0 && len(lines) < 50; i-- { + n := m.items[i] + var icon string + switch n.Type { + case "success": + icon = ui.StyleSuccess.Render("✓") + case "error": + icon = ui.StyleError.Render("✗") + case "warning": + icon = ui.StyleWarning.Render("⚠") + default: + icon = ui.StyleAccent.Render("ℹ") + } + lines = append(lines, icon+" "+n.Message) + } + + return lipgloss.NewStyle().Padding(1, 2).Render( + lipgloss.JoinVertical(lipgloss.Left, lines...), + ) +} diff --git a/tui/views/search.go b/tui/views/search.go new file mode 100644 index 0000000..0f7ea18 --- /dev/null +++ b/tui/views/search.go @@ -0,0 +1,178 @@ +package views + +import ( + "net/url" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/gitlink-org/gitlink-tui/pkg/client" + "github.com/gitlink-org/gitlink-tui/tui/ui" +) + +type SearchModel struct { + cli *client.Client + width int + query string + results []string + mode string // "repos" or "users" + cursor int + err error +} + +func NewSearchModel(cli *client.Client) *SearchModel { + return &SearchModel{cli: cli, mode: "repos"} +} + +func (m *SearchModel) Init() tea.Cmd { return nil } + +func (m *SearchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + + case tea.KeyMsg: + switch msg.String() { + case "j", "down": + if m.cursor < len(m.results)-1 { + m.cursor++ + } + case "k", "up": + if m.cursor > 0 { + m.cursor-- + } + case "enter", "l": + if m.query != "" { + return m, m.doSearch + } + case "backspace": + if len(m.query) > 0 { + m.query = m.query[:len(m.query)-1] + } + case "esc": + m.query = "" + m.results = nil + default: + if len(msg.String()) == 1 { + m.query += msg.String() + } + } + } + + return m, nil +} + +func (m *SearchModel) doSearch() tea.Msg { + q := url.Values{} + q.Set("search", m.query) + q.Set("page", "1") + q.Set("limit", "10") + + var path string + if m.mode == "users" { + path = "/search/users" + } else { + path = "/search/repos" + } + + raw, err := m.cli.Get(path, q) + if err != nil { + return searchResultMsg{err: err} + } + + return searchResultMsg{data: raw} +} + +type searchResultMsg struct { + data map[string]interface{} + err error +} + +func (m *SearchModel) View() string { + title := ui.StyleTitle.Render("Search") + searchBar := ui.StyleCmd.Render("> " + m.query + "_") + + if m.err != nil { + return lipgloss.NewStyle().Padding(1, 2).Render( + lipgloss.JoinVertical(lipgloss.Left, title, searchBar, "", ui.StyleError.Render(m.err.Error())), + ) + } + + var lines []string + lines = append(lines, title, searchBar, "") + + if len(m.results) == 0 && m.query == "" { + lines = append(lines, ui.StyleMuted.Render("Type to search repos. Press Tab for users. Enter to search.")) + } + + for i, r := range m.results { + prefix := " " + if i == m.cursor { + prefix = "> " + } + line := prefix + r + if i == m.cursor { + line = ui.StyleSelected.Render(line) + } + lines = append(lines, line) + // Limit display + if i > 20 { + break + } + } + + return lipgloss.NewStyle().Padding(1, 2).Render( + lipgloss.JoinVertical(lipgloss.Left, lines...), + ) +} + +func truncateSearch(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max-3] + "..." +} + +// Update handles search result messages. +func (m *SearchModel) UpdateSearch(msg searchResultMsg) { + if msg.err != nil { + m.err = msg.err + return + } + m.err = nil + m.results = nil + + // Parse results + if items, ok := msg.data["repos"].([]interface{}); ok { + for _, item := range items { + if repo, ok := item.(map[string]interface{}); ok { + name := "" + if v, ok := repo["full_name"].(string); ok { + name = v + } else if login, _ := repo["login"].(string); login != "" { + if name2, _ := repo["name"].(string); name2 != "" { + name = login + " (" + name2 + ")" + } else { + name = login + } + } + if name != "" { + m.results = append(m.results, name) + } + } + } + } + if items, ok := msg.data["users"].([]interface{}); ok { + for _, item := range items { + if user, ok := item.(map[string]interface{}); ok { + if login, _ := user["login"].(string); login != "" { + m.results = append(m.results, login) + } + } + } + } +} + +// Remove unused function +var _ = strings.TrimSuffix