Revert "Add TUI foundation (Phase 1: Harness Foundation)"

This reverts commit 0ae44e9a64.
This commit is contained in:
wbtiger 2026-05-17 14:14:32 +08:00
parent 0ae44e9a64
commit 795d00302e
11 changed files with 0 additions and 969 deletions

23
go.mod
View File

@ -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
)

48
go.sum
View File

@ -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=

27
main.go
View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")),
}

View File

@ -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"))
)

View File

@ -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"),
}

View File

@ -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)
}