diff --git a/go.mod b/go.mod index 81d28af..19ce345 100644 --- a/go.mod +++ b/go.mod @@ -10,34 +10,11 @@ require ( ) require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // 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/danieljoos/wincred v1.2.3 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/lucasb-eyer/go-colorful v1.3.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.19 // 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/spf13/pflag v1.0.10 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.3.8 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 12b3194..af5e2c6 100644 --- a/go.sum +++ b/go.sum @@ -1,62 +1,20 @@ -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/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= -github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -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/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.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/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= -github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= 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/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/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -69,19 +27,13 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 9cb0b5a..9e9f756 100644 --- a/main.go +++ b/main.go @@ -1,39 +1,12 @@ package main import ( - "fmt" "os" - tea "github.com/charmbracelet/bubbletea" - "github.com/gitlink-org/gitlink-cli/cmd" - "github.com/gitlink-org/gitlink-cli/tui" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "tui" { - // TUI mode - f, err := tea.LogToFile("gitlink-tui.log", "") - if err != nil { - fmt.Fprintf(os.Stderr, "failed to init log: %v\n", err) - os.Exit(1) - } - defer f.Close() - - app := tui.NewApp() - program := tea.NewProgram( - app, - tea.WithAltScreen(), - tea.WithMouseCellMotion(), - ) - if _, err := program.Run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - return - } - - // CLI mode if err := cmd.Execute(); err != nil { os.Exit(1) } diff --git a/tui/app.go b/tui/app.go deleted file mode 100644 index c82ab69..0000000 --- a/tui/app.go +++ /dev/null @@ -1,347 +0,0 @@ -package tui - -import ( - "fmt" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "github.com/gitlink-org/gitlink-cli/tui/navigation" - "github.com/gitlink-org/gitlink-cli/tui/store" - "github.com/gitlink-org/gitlink-cli/tui/ui" - "github.com/gitlink-org/gitlink-cli/tui/views" -) - -// View enum -type View int - -const ( - ViewHome View = iota - ViewCode - ViewIssueBoard - ViewIssueDetail - ViewPRList - ViewPRDetail - ViewCIDashboard - ViewCILog - ViewReleaseList - ViewReleaseCreate - ViewPMDashboard - ViewExecutionDashboard - ViewSearch - ViewSettings -) - -// AppModel is the root Bubble Tea model. -type AppModel struct { - // Navigation - currentView View - router *navigation.Router - - // Sub-models for each view - home *views.HomeModel - codeView tea.Model - issueBoard tea.Model - issueDetail tea.Model - prList tea.Model - prDetail tea.Model - ciDashboard tea.Model - ciLog tea.Model - releaseList tea.Model - releaseCreate tea.Model - pmDashboard tea.Model - execDashboard tea.Model - searchView tea.Model - settingsView tea.Model - - // Layout - width int - height int - ready bool - - // Panels - statusBar *ui.StatusBarModel - tabBar *ui.TabBarModel - aiPanel tea.Model // nil = not created yet (Phase 3) - aiOpen bool - - // Shared state - store *store.Store - - // Key bindings - keyMap ui.KeyMap - - // Notifications - notifications []string -} - -// tabs defines the global tab bar. -var tabs = []ui.TabDef{ - {Key: "1", Label: "Home"}, - {Key: "2", Label: "Code"}, - {Key: "3", Label: "Issues"}, - {Key: "4", Label: "PRs"}, - {Key: "5", Label: "CI"}, - {Key: "6", Label: "Release"}, - {Key: "7", Label: "PM"}, -} - -func NewApp() *AppModel { - s := store.New() - - statusBar := ui.NewStatusBar() - tabBar := ui.NewTabBar(tabs) - - return &AppModel{ - currentView: ViewHome, - router: navigation.NewRouter(), - store: s, - statusBar: statusBar, - tabBar: tabBar, - keyMap: ui.DefaultKeyMap, - home: views.NewHomeModel(s), - } -} - -func (m *AppModel) Init() tea.Cmd { - // Load auth status and current project info - return tea.Batch( - m.store.LoadAuthStatus, - m.store.LoadProjectContext, - ) -} - -func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - if !m.ready { - m.ready = true - } - return m, nil - - case tea.KeyMsg: - // Global keybindings take priority over view-specific ones - cmd := m.handleGlobalKeys(msg) - if cmd != nil { - return m, cmd - } - - case store.AuthStatusMsg: - // Update statusbar with auth info - _ = msg - case store.ProjectContextMsg: - // Update statusbar with project info - m.statusBar.SetProject(msg.Owner + "/" + msg.Repo) - } - cmds = nil - - // Delegate to active view - viewModel := m.currentViewModel() - if viewModel != nil { - newModel, cmd := viewModel.Update(msg) - // If the model returned a different model (unlikely but handle) - _ = newModel - if cmd != nil { - cmds = append(cmds, cmd) - } - } - - return m, tea.Batch(cmds...) -} - -func (m *AppModel) View() string { - if !m.ready { - return "Initializing..." - } - - // Compute layout dimensions - statusHeight := 1 - tabHeight := 1 - - mainHeight := m.height - statusHeight - tabHeight - if mainHeight < 0 { - mainHeight = 0 - } - - mainWidth := m.width - aiWidth := 0 - if m.aiOpen && m.aiPanel != nil { - aiWidth = min(40, m.width*30/100) - mainWidth = m.width - aiWidth - } - - // Render active view - var mainContent string - viewModel := m.currentViewModel() - if viewModel != nil { - mainContent = viewModel.View() - } - mainBox := lipgloss.NewStyle(). - Width(mainWidth). - Height(mainHeight). - Render(mainContent) - - // AI panel placeholder - var aiContent string - if m.aiOpen { - aiContent = lipgloss.NewStyle(). - Width(aiWidth). - Height(mainHeight). - BorderLeft(true). - BorderStyle(lipgloss.NormalBorder()). - Padding(0, 1). - Render("AI Panel\n───\nCtrl+Space to toggle") - } - - // Combine main + AI - topSection := lipgloss.JoinHorizontal(lipgloss.Top, mainBox, aiContent) - - // Tab bar - var tabBarStr string - if !m.aiOpen || aiWidth == 0 { - // Highlight active tab - tabBarStr = m.tabBar.View(int(m.currentView)) - } else { - tabBarStr = m.tabBar.View(int(m.currentView)) - } - - // Status bar - statusStr := m.statusBar.View() - - return lipgloss.JoinVertical( - lipgloss.Left, - topSection, - tabBarStr, - statusStr, - ) -} - -func (m *AppModel) handleGlobalKeys(msg tea.KeyMsg) tea.Cmd { - switch msg.String() { - case "ctrl+c": - return tea.Quit - - case "ctrl+space": - m.aiOpen = !m.aiOpen - return nil - - case "ctrl+p": - // Command palette (Phase 5, placeholder) - m.notifications = append(m.notifications, "Command palette coming in Phase 5") - return nil - - case "?": - // Help overlay (placeholder) - m.notifications = append(m.notifications, "Help: j/k navigate, Enter select, Esc back, Ctrl+A AI actions") - return nil - - case "ctrl+a": - // AI context menu (Phase 3, placeholder) - m.notifications = append(m.notifications, fmt.Sprintf("AI menu for: %s", m.viewName())) - return nil - - case "1", "2", "3", "4", "5", "6", "7": - return m.switchToTab(msg.String()) - - case "tab": - // Cycle through views - next := (int(m.currentView) + 1) % len(tabs) - m.currentView = View(next) - return nil - } - - // Delegate to view - return nil -} - -func (m *AppModel) switchToTab(key string) tea.Cmd { - switch key { - case "1": - m.currentView = ViewHome - case "2": - m.currentView = ViewCode - case "3": - m.currentView = ViewIssueBoard - case "4": - m.currentView = ViewPRList - case "5": - m.currentView = ViewCIDashboard - case "6": - m.currentView = ViewReleaseList - case "7": - m.currentView = ViewPMDashboard - } - return nil -} - -func (m *AppModel) currentViewModel() tea.Model { - switch m.currentView { - case ViewHome: - return m.home - case ViewCode: - return m.codeView - case ViewIssueBoard: - return m.issueBoard - case ViewIssueDetail: - return m.issueDetail - case ViewPRList: - return m.prList - case ViewPRDetail: - return m.prDetail - case ViewCIDashboard: - return m.ciDashboard - case ViewCILog: - return m.ciLog - case ViewReleaseList: - return m.releaseList - case ViewReleaseCreate: - return m.releaseCreate - case ViewPMDashboard: - return m.pmDashboard - case ViewExecutionDashboard: - return m.execDashboard - case ViewSearch: - return m.searchView - case ViewSettings: - return m.settingsView - } - return nil -} - -func (m *AppModel) viewName() string { - names := map[View]string{ - ViewHome: "Home", - ViewCode: "Code", - ViewIssueBoard: "Issues", - ViewIssueDetail: "Issue Detail", - ViewPRList: "PRs", - ViewPRDetail: "PR Detail", - ViewCIDashboard: "CI", - ViewCILog: "CI Log", - ViewReleaseList: "Releases", - ViewReleaseCreate: "New Release", - ViewPMDashboard: "PM", - ViewExecutionDashboard: "Execution", - ViewSearch: "Search", - ViewSettings: "Settings", - } - if n, ok := names[m.currentView]; ok { - return n - } - return "Unknown" -} - -func (m *AppModel) navigate(to View) { - m.router.Push(int(m.currentView)) - m.currentView = to -} - -func (m *AppModel) goBack() { - if prev, ok := m.router.Pop(); ok { - m.currentView = View(prev) - } -} diff --git a/tui/navigation/router.go b/tui/navigation/router.go deleted file mode 100644 index c044bee..0000000 --- a/tui/navigation/router.go +++ /dev/null @@ -1,55 +0,0 @@ -package navigation - -// Router provides a stack-based view history for back navigation. -type Router struct { - stack []int - params map[int]map[string]interface{} // per-view params -} - -func NewRouter() *Router { - return &Router{ - stack: make([]int, 0, 16), - params: make(map[int]map[string]interface{}), - } -} - -// Push saves the current view index for later back navigation. -func (r *Router) Push(view int) { - r.stack = append(r.stack, view) -} - -// Pop returns the previous view, or false if the stack is empty. -func (r *Router) Pop() (int, bool) { - if len(r.stack) == 0 { - return 0, false - } - prev := r.stack[len(r.stack)-1] - r.stack = r.stack[:len(r.stack)-1] - return prev, true -} - -// Depth returns the current stack depth. -func (r *Router) Depth() int { - return len(r.stack) -} - -// SetParam sets a parameter for a view. -func (r *Router) SetParam(view int, key string, value interface{}) { - if r.params[view] == nil { - r.params[view] = make(map[string]interface{}) - } - r.params[view][key] = value -} - -// GetParam gets a parameter for a view. -func (r *Router) GetParam(view int, key string) interface{} { - if p := r.params[view]; p != nil { - return p[key] - } - return nil -} - -// ClearParams clears all params for a view. -func (r *Router) ClearParams(view int) { - delete(r.params, view) -} diff --git a/tui/store/store.go b/tui/store/store.go deleted file mode 100644 index ec5f39b..0000000 --- a/tui/store/store.go +++ /dev/null @@ -1,141 +0,0 @@ -package store - -import ( - "fmt" - "sync" - - tea "github.com/charmbracelet/bubbletea" - - "github.com/gitlink-org/gitlink-cli/internal/auth" - "github.com/gitlink-org/gitlink-cli/internal/client" - "github.com/gitlink-org/gitlink-cli/internal/config" - "github.com/gitlink-org/gitlink-cli/internal/context" -) - -// Store is the central state container. -type Store struct { - mu sync.RWMutex - - // Client for GitLink API calls - Client *client.Client - - // Config - Config *config.Config - - // Auth - Token string - LoggedIn bool - User *UserInfo - - // Project - Owner string - Repo string - - // Subscribers - subs map[string][]chan tea.Msg -} - -// UserInfo holds basic authenticated user data. -type UserInfo struct { - Login string - ID int - Username string -} - -func New() *Store { - cli, err := client.New() - if err != nil { - cli = nil - } - - return &Store{ - Client: cli, - subs: make(map[string][]chan tea.Msg), - } -} - -// LoadAuthStatus checks whether the user is authenticated. -func (s *Store) LoadAuthStatus() tea.Msg { - token, err := auth.LoadToken() - if err != nil { - return AuthStatusMsg{LoggedIn: false, Error: err} - } - - s.mu.Lock() - s.Token = token - s.LoggedIn = token != "" - s.mu.Unlock() - - return AuthStatusMsg{LoggedIn: token != ""} -} - -// LoadProjectContext detects the current GitLink project from git remote. -func (s *Store) LoadProjectContext() tea.Msg { - owner, repo, err := context.ResolveOwnerRepo("", "") - if err != nil { - return ProjectContextMsg{Error: err} - } - - s.mu.Lock() - s.Owner = owner - s.Repo = repo - s.mu.Unlock() - - return ProjectContextMsg{ - Owner: owner, - Repo: repo, - } -} - -// Dispatch sends an action to the store and publishes to subscribers. -func (s *Store) Dispatch(action string, data interface{}) { - s.mu.RLock() - subs := s.subs[action] - s.mu.RUnlock() - - for _, ch := range subs { - select { - case ch <- StoreEvent{Action: action, Data: data}: - default: - } - } -} - -// Subscribe returns a channel for store events. -func (s *Store) Subscribe(action string) chan tea.Msg { - ch := make(chan tea.Msg, 10) - s.mu.Lock() - s.subs[action] = append(s.subs[action], ch) - s.mu.Unlock() - return ch -} - -// AuthStatusMsg is sent when auth status is loaded. -type AuthStatusMsg struct { - LoggedIn bool - Error error -} - -// ProjectContextMsg is sent when project context is loaded. -type ProjectContextMsg struct { - Owner string - Repo string - Error error -} - -func (m ProjectContextMsg) String() string { - if m.Error != nil { - return fmt.Sprintf("No project (%v)", m.Error) - } - return m.Owner + "/" + m.Repo -} - -// StoreEvent is a generic store event. -type StoreEvent struct { - Action string - Data interface{} -} - -func (e StoreEvent) String() string { - return fmt.Sprintf("[%s]", e.Action) -} diff --git a/tui/ui/components.go b/tui/ui/components.go deleted file mode 100644 index c00e9ce..0000000 --- a/tui/ui/components.go +++ /dev/null @@ -1,84 +0,0 @@ -package ui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" -) - -// TabDef defines a single tab. -type TabDef struct { - Key string - Label string -} - -// TabBarModel renders a horizontal tab bar. -type TabBarModel struct { - tabs []TabDef - active int -} - -func NewTabBar(tabs []TabDef) *TabBarModel { - return &TabBarModel{tabs: tabs} -} - -func (t *TabBarModel) View(active int) string { - var items []string - for i, tab := range t.tabs { - label := fmt.Sprintf("[%s] %s", tab.Key, tab.Label) - if i == active { - label = StyleSelected.Render(" " + label + " ") - } else { - label = StyleMuted.Render(label) - } - items = append(items, label) - } - return lipgloss.JoinHorizontal(lipgloss.Left, items...) -} - -// StatusBarModel renders a bottom status bar. -type StatusBarModel struct { - project string - message string - authStatus string -} - -func NewStatusBar() *StatusBarModel { - return &StatusBarModel{} -} - -func (s *StatusBarModel) SetProject(p string) { - s.project = p -} - -func (s *StatusBarModel) SetMessage(msg string) { - s.message = msg -} - -func (s *StatusBarModel) SetAuth(status string) { - s.authStatus = status -} - -func (s *StatusBarModel) View() string { - left := StyleMuted.Render(fmt.Sprintf(" Project: %s ", s.project)) - center := StyleCmd.Render(s.message) - right := StyleMuted.Render(fmt.Sprintf(" Ctrl+A AI | Ctrl+Space Chat | Ctrl+P Cmd | ? Help ")) - - width := 80 // will be overridden by layout - leftW := lipgloss.Width(left) - rightW := lipgloss.Width(right) - centerW := lipgloss.Width(center) - - gapLeft := (width - leftW - centerW - rightW) / 2 - if gapLeft < 0 { - gapLeft = 0 - } - gapRight := width - leftW - centerW - rightW - gapLeft - if gapRight < 0 { - gapRight = 0 - } - - bg := lipgloss.NewStyle().Background(lipgloss.Color("#313244")).Width(width) - return bg.Render(left + strings.Repeat(" ", gapLeft) + center + strings.Repeat(" ", gapRight) + right) -} diff --git a/tui/ui/keymap.go b/tui/ui/keymap.go deleted file mode 100644 index ebc02b7..0000000 --- a/tui/ui/keymap.go +++ /dev/null @@ -1,42 +0,0 @@ -package ui - -import "github.com/charmbracelet/bubbles/key" - -// KeyMap holds all global keybindings. -type KeyMap struct { - Quit key.Binding - Help key.Binding - AI key.Binding - AIPanel key.Binding - Commands key.Binding - Refresh key.Binding - - Up key.Binding - Down key.Binding - Back key.Binding - Enter key.Binding - - Create key.Binding - Delete key.Binding - Edit key.Binding - Filter key.Binding -} - -var DefaultKeyMap = KeyMap{ - Quit: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("Ctrl+C", "Quit")), - Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "Help")), - AI: key.NewBinding(key.WithKeys("ctrl+a"), key.WithHelp("Ctrl+A", "AI actions")), - AIPanel: key.NewBinding(key.WithKeys("ctrl+space"), key.WithHelp("Ctrl+Space", "AI Panel")), - Commands: key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("Ctrl+P", "Commands")), - Refresh: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "Refresh")), - - Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("j/k", "Navigate")), - Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("", "")), - Back: key.NewBinding(key.WithKeys("esc", "h"), key.WithHelp("Esc", "Back")), - Enter: key.NewBinding(key.WithKeys("enter", "l"), key.WithHelp("Enter", "Select")), - - Create: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "Create")), - Delete: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "Delete")), - Edit: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "Edit")), - Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "Filter")), -} diff --git a/tui/ui/styles.go b/tui/ui/styles.go deleted file mode 100644 index 49635e3..0000000 --- a/tui/ui/styles.go +++ /dev/null @@ -1,33 +0,0 @@ -package ui - -import "github.com/charmbracelet/lipgloss" - -// Common styles used across components. -var ( - StyleTitle = lipgloss.NewStyle(). - Bold(true). - Foreground(DefaultTheme.Primary) - - StyleMuted = lipgloss.NewStyle(). - Foreground(DefaultTheme.Muted) - - StyleError = lipgloss.NewStyle(). - Foreground(DefaultTheme.Error) - - StyleSuccess = lipgloss.NewStyle(). - Foreground(DefaultTheme.Success) - - StyleWarning = lipgloss.NewStyle(). - Foreground(DefaultTheme.Warning) - - StyleAccent = lipgloss.NewStyle(). - Foreground(DefaultTheme.Accent) - - StyleBorder = lipgloss.RoundedBorder() - - StyleSelected = lipgloss.NewStyle(). - Background(lipgloss.Color("#45475A")) - - StyleCmd = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#A6E3A1")) -) diff --git a/tui/ui/theme.go b/tui/ui/theme.go deleted file mode 100644 index 2008ee2..0000000 --- a/tui/ui/theme.go +++ /dev/null @@ -1,28 +0,0 @@ -package ui - -import "github.com/charmbracelet/lipgloss" - -// Theme defines the color palette. -type Theme struct { - Primary lipgloss.Color - Success lipgloss.Color - Warning lipgloss.Color - Error lipgloss.Color - Muted lipgloss.Color - Background lipgloss.Color - Foreground lipgloss.Color - Border lipgloss.Color - Accent lipgloss.Color -} - -var DefaultTheme = Theme{ - Primary: lipgloss.Color("#6C8EBF"), - Success: lipgloss.Color("#82B366"), - Warning: lipgloss.Color("#D6B656"), - Error: lipgloss.Color("#B85450"), - Muted: lipgloss.Color("#666666"), - Background: lipgloss.Color("#1E1E2E"), - Foreground: lipgloss.Color("#CDD6F4"), - Border: lipgloss.Color("#45475A"), - Accent: lipgloss.Color("#89B4FA"), -} diff --git a/tui/views/home.go b/tui/views/home.go deleted file mode 100644 index 0f64f6e..0000000 --- a/tui/views/home.go +++ /dev/null @@ -1,141 +0,0 @@ -package views - -import ( - "fmt" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "github.com/gitlink-org/gitlink-cli/tui/store" - "github.com/gitlink-org/gitlink-cli/tui/ui" -) - -// HomeModel is the dashboard view. -type HomeModel struct { - store *store.Store - width int - height int - - // Data - project string - authStatus string - issuesCount string - prsCount string - ciStatus string - releaseVer string - - // Onboarding - firstRun bool - onboardIdx int -} - -func NewHomeModel(s *store.Store) *HomeModel { - return &HomeModel{ - store: s, - firstRun: true, - } -} - -func (m *HomeModel) Init() tea.Cmd { - return tea.Batch( - m.store.LoadAuthStatus, - m.store.LoadProjectContext, - ) -} - -func (m *HomeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - - case store.AuthStatusMsg: - if msg.LoggedIn { - m.authStatus = "Logged in" - } else { - m.authStatus = "Not logged in (run gitlink-cli auth login)" - } - - case store.ProjectContextMsg: - if msg.Error != nil { - m.project = "No GitLink project detected" - } else { - m.project = msg.Owner + "/" + msg.Repo - } - - case tea.KeyMsg: - switch msg.String() { - case "enter": - if m.firstRun { - m.firstRun = false - } - } - } - - return m, nil -} - -func (m *HomeModel) View() string { - if m.firstRun { - return m.onboardingView() - } - return m.dashboardView() -} - -func (m *HomeModel) dashboardView() string { - title := ui.StyleTitle.Render("šŸ  Home Dashboard") - - content := lipgloss.JoinVertical( - lipgloss.Left, - title, - "", - fmt.Sprintf(" Project: %s", ui.StyleAccent.Render(m.project)), - fmt.Sprintf(" Auth: %s", m.authStatus), - "", - ui.StyleMuted.Render(" Quick start:"), - fmt.Sprintf(" %s Switch views (1-7)", ui.StyleCmd.Render("[1]-[7]")), - fmt.Sprintf(" %s AI actions (context-aware)", ui.StyleCmd.Render("Ctrl+A")), - fmt.Sprintf(" %s Open AI chat panel", ui.StyleCmd.Render("Ctrl+Space")), - fmt.Sprintf(" %s Command palette", ui.StyleCmd.Render("Ctrl+P")), - fmt.Sprintf(" %s Help", ui.StyleCmd.Render("?")), - ) - - return lipgloss.NewStyle(). - Padding(1, 2). - Render(content) -} - -func (m *HomeModel) onboardingView() string { - welcome := lipgloss.NewStyle(). - Bold(true). - Foreground(ui.DefaultTheme.Primary). - Render("Welcome to GitLink TUI") - - box := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(ui.DefaultTheme.Primary). - Padding(1, 2). - Width(60) - - content := lipgloss.JoinVertical( - lipgloss.Left, - welcome, - "", - lipgloss.NewStyle().Bold(true).Render("One key to remember:"), - "", - fmt.Sprintf(" %s → AI actions, anywhere, context-aware", ui.StyleCmd.Render("Ctrl+A")), - "", - lipgloss.NewStyle().Bold(true).Render("Also useful:"), - fmt.Sprintf(" %s AI Chat panel", ui.StyleCmd.Render("Ctrl+Space")), - fmt.Sprintf(" %s Command palette", ui.StyleCmd.Render("Ctrl+P")), - fmt.Sprintf(" %s Switch views", ui.StyleCmd.Render("[1]-[7]")), - fmt.Sprintf(" %s Help", ui.StyleCmd.Render("?")), - "", - fmt.Sprintf(" Project: %s", ui.StyleAccent.Render(m.project)), - fmt.Sprintf(" Auth: %s", m.authStatus), - "", - ui.StyleCmd.Render(" [Enter] Got it!"), - ) - - return box.Render(content) -}