summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components/status
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-11-02 18:43:17 -0500
committerDax Raad <[email protected]>2025-11-02 18:43:33 -0500
commitf68374ad2223ddc213bdea9519ca6a699819ee0e (patch)
tree04f0fe21b8e12cd62d7274961bb0cff64f966f40 /packages/tui/internal/components/status
parent5e86c9b7916f75c7ad227b80eab18c7c54fc8ffe (diff)
downloadopencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.tar.gz
opencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.zip
DELETE GO BUBBLETEA CRAP HOORAY
Diffstat (limited to 'packages/tui/internal/components/status')
-rw-r--r--packages/tui/internal/components/status/status.go340
-rw-r--r--packages/tui/internal/components/status/status_test.go100
2 files changed, 0 insertions, 440 deletions
diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go
deleted file mode 100644
index aba80900b..000000000
--- a/packages/tui/internal/components/status/status.go
+++ /dev/null
@@ -1,340 +0,0 @@
-package status
-
-import (
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "time"
-
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/charmbracelet/lipgloss/v2/compat"
- "github.com/fsnotify/fsnotify"
- "github.com/sst/opencode/internal/app"
- "github.com/sst/opencode/internal/commands"
- "github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/internal/util"
-)
-
-type GitBranchUpdatedMsg struct {
- Branch string
-}
-
-type StatusComponent interface {
- tea.Model
- tea.ViewModel
- Cleanup()
-}
-
-type statusComponent struct {
- app *app.App
- width int
- cwd string
- branch string
- watcher *fsnotify.Watcher
- done chan struct{}
- lastUpdate time.Time
-}
-
-func (m *statusComponent) Init() tea.Cmd {
- return m.startGitWatcher()
-}
-
-func (m *statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- m.width = msg.Width
- return m, nil
- case GitBranchUpdatedMsg:
- if m.branch != msg.Branch {
- m.branch = msg.Branch
- }
- // Continue watching for changes (persistent watcher)
- return m, m.watchForGitChanges()
- }
- return m, nil
-}
-
-func (m *statusComponent) logo() string {
- t := theme.CurrentTheme()
- base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
- emphasis := styles.NewStyle().
- Foreground(t.Text()).
- Background(t.BackgroundElement()).
- Bold(true).
- Render
-
- open := base("open")
- code := emphasis("code")
- version := base(" " + m.app.Version)
-
- content := open + code
- if m.width > 40 {
- content += version
- }
- return styles.NewStyle().
- Background(t.BackgroundElement()).
- Padding(0, 1).
- Render(content)
-}
-
-func (m *statusComponent) collapsePath(path string, maxWidth int) string {
- if lipgloss.Width(path) <= maxWidth {
- return path
- }
-
- const ellipsis = ".."
- ellipsisLen := len(ellipsis)
-
- if maxWidth <= ellipsisLen {
- if maxWidth > 0 {
- return "..."[:maxWidth]
- }
- return ""
- }
-
- separator := string(filepath.Separator)
- parts := strings.Split(path, separator)
-
- if len(parts) == 1 {
- return path[:maxWidth-ellipsisLen] + ellipsis
- }
-
- truncatedPath := parts[len(parts)-1]
- for i := len(parts) - 2; i >= 0; i-- {
- part := parts[i]
- if len(truncatedPath)+len(separator)+len(part)+ellipsisLen > maxWidth {
- return ellipsis + separator + truncatedPath
- }
- truncatedPath = part + separator + truncatedPath
- }
- return truncatedPath
-}
-
-func (m *statusComponent) View() string {
- t := theme.CurrentTheme()
- logo := m.logo()
- logoWidth := lipgloss.Width(logo)
-
- var modeBackground compat.AdaptiveColor
- var modeForeground compat.AdaptiveColor
-
- agentColor := util.GetAgentColor(m.app.AgentIndex)
-
- if m.app.AgentIndex == 0 {
- modeBackground = t.BackgroundElement()
- modeForeground = agentColor
- } else {
- modeBackground = agentColor
- modeForeground = t.BackgroundPanel()
- }
-
- command := m.app.Commands[commands.AgentCycleCommand]
- kb := command.Keybindings[0]
- key := kb.Key
- if kb.RequiresLeader {
- key = m.app.Config.Keybinds.Leader + " " + kb.Key
- }
-
- agentStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
- agentNameStyle := agentStyle.Bold(true).Render
- agentDescStyle := agentStyle.Render
- agent := agentNameStyle(strings.ToUpper(m.app.Agent().Name)) + agentDescStyle(" AGENT")
- agent = agentStyle.
- Padding(0, 1).
- BorderLeft(true).
- BorderStyle(lipgloss.ThickBorder()).
- BorderForeground(modeBackground).
- BorderBackground(t.BackgroundPanel()).
- Render(agent)
-
- faintStyle := styles.NewStyle().
- Faint(true).
- Background(t.BackgroundPanel()).
- Foreground(t.TextMuted())
- agent = faintStyle.Render(key+" ") + agent
- modeWidth := lipgloss.Width(agent)
-
- availableWidth := m.width - logoWidth - modeWidth
- branchSuffix := ""
- if m.branch != "" {
- branchSuffix = ":" + m.branch
- }
-
- maxCwdWidth := availableWidth - lipgloss.Width(branchSuffix)
- cwdDisplay := m.collapsePath(m.cwd, maxCwdWidth)
-
- if m.branch != "" && availableWidth > lipgloss.Width(cwdDisplay)+lipgloss.Width(branchSuffix) {
- cwdDisplay += faintStyle.Render(branchSuffix)
- }
-
- cwd := styles.NewStyle().
- Foreground(t.TextMuted()).
- Background(t.BackgroundPanel()).
- Padding(0, 1).
- Render(cwdDisplay)
-
- background := t.BackgroundPanel()
- status := layout.Render(
- layout.FlexOptions{
- Background: &background,
- Direction: layout.Row,
- Justify: layout.JustifySpaceBetween,
- Align: layout.AlignStretch,
- Width: m.width,
- },
- layout.FlexItem{
- View: logo + cwd,
- },
- layout.FlexItem{
- View: agent,
- },
- )
-
- blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
- return blank + "\n" + status
-}
-
-func (m *statusComponent) startGitWatcher() tea.Cmd {
- cmd := util.CmdHandler(
- GitBranchUpdatedMsg{Branch: getCurrentGitBranch(util.CwdPath)},
- )
- if err := m.initWatcher(); err != nil {
- return cmd
- }
- return tea.Batch(cmd, m.watchForGitChanges())
-}
-
-func (m *statusComponent) initWatcher() error {
- gitDir := filepath.Join(util.CwdPath, ".git")
- headFile := filepath.Join(gitDir, "HEAD")
- if info, err := os.Stat(gitDir); err != nil || !info.IsDir() {
- return err
- }
-
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- return err
- }
-
- if err := watcher.Add(headFile); err != nil {
- watcher.Close()
- return err
- }
-
- // Also watch the ref file if HEAD points to a ref
- refFile := getGitRefFile(util.CwdPath)
- if refFile != headFile && refFile != "" {
- if _, err := os.Stat(refFile); err == nil {
- watcher.Add(refFile) // Ignore error, HEAD watching is sufficient
- }
- }
-
- m.watcher = watcher
- m.done = make(chan struct{})
- return nil
-}
-
-func (m *statusComponent) watchForGitChanges() tea.Cmd {
- if m.watcher == nil {
- return nil
- }
-
- return tea.Cmd(func() tea.Msg {
- for {
- select {
- case event, ok := <-m.watcher.Events:
- branch := getCurrentGitBranch(util.CwdPath)
- if !ok {
- return GitBranchUpdatedMsg{Branch: branch}
- }
- if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
- // Debounce updates to prevent excessive refreshes
- now := time.Now()
- if now.Sub(m.lastUpdate) < 100*time.Millisecond {
- continue
- }
- m.lastUpdate = now
- if strings.HasSuffix(event.Name, "HEAD") {
- m.updateWatchedFiles()
- }
- return GitBranchUpdatedMsg{Branch: branch}
- }
- case <-m.watcher.Errors:
- // Continue watching even on errors
- case <-m.done:
- return GitBranchUpdatedMsg{Branch: ""}
- }
- }
- })
-}
-
-func (m *statusComponent) updateWatchedFiles() {
- if m.watcher == nil {
- return
- }
- refFile := getGitRefFile(util.CwdPath)
- headFile := filepath.Join(util.CwdPath, ".git", "HEAD")
- if refFile != headFile && refFile != "" {
- if _, err := os.Stat(refFile); err == nil {
- // Try to add the new ref file (ignore error if already watching)
- m.watcher.Add(refFile)
- }
- }
-}
-
-func getCurrentGitBranch(cwd string) string {
- cmd := exec.Command("git", "branch", "--show-current")
- cmd.Dir = cwd
- output, err := cmd.Output()
- if err != nil {
- return ""
- }
- return strings.TrimSpace(string(output))
-}
-
-func getGitRefFile(cwd string) string {
- headFile := filepath.Join(cwd, ".git", "HEAD")
- content, err := os.ReadFile(headFile)
- if err != nil {
- return ""
- }
-
- headContent := strings.TrimSpace(string(content))
- if after, ok := strings.CutPrefix(headContent, "ref: "); ok {
- // HEAD points to a ref file
- refPath := after
- return filepath.Join(cwd, ".git", refPath)
- }
-
- // HEAD contains a direct commit hash
- return headFile
-}
-
-func (m *statusComponent) Cleanup() {
- if m.done != nil {
- close(m.done)
- }
- if m.watcher != nil {
- m.watcher.Close()
- }
-}
-
-func NewStatusCmp(app *app.App) StatusComponent {
- statusComponent := &statusComponent{
- app: app,
- lastUpdate: time.Now(),
- }
-
- homePath, err := os.UserHomeDir()
- cwdPath := util.CwdPath
- if err == nil && homePath != "" && strings.HasPrefix(cwdPath, homePath) {
- cwdPath = "~" + cwdPath[len(homePath):]
- }
- statusComponent.cwd = cwdPath
-
- return statusComponent
-}
diff --git a/packages/tui/internal/components/status/status_test.go b/packages/tui/internal/components/status/status_test.go
deleted file mode 100644
index 1e1caf8ac..000000000
--- a/packages/tui/internal/components/status/status_test.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package status
-
-import (
- "os"
- "path/filepath"
- "testing"
- "time"
-)
-
-func TestGetCurrentGitBranch(t *testing.T) {
- // Test in current directory (should be a git repo)
- branch := getCurrentGitBranch(".")
- if branch == "" {
- t.Skip("Not in a git repository, skipping test")
- }
- t.Logf("Current branch: %s", branch)
-}
-
-func TestGetGitRefFile(t *testing.T) {
- // Create a temporary git directory structure for testing
- tmpDir := t.TempDir()
- gitDir := filepath.Join(tmpDir, ".git")
- err := os.MkdirAll(gitDir, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- // Test case 1: HEAD points to a ref
- headFile := filepath.Join(gitDir, "HEAD")
- err = os.WriteFile(headFile, []byte("ref: refs/heads/main\n"), 0644)
- if err != nil {
- t.Fatal(err)
- }
-
- refFile := getGitRefFile(tmpDir)
- expected := filepath.Join(gitDir, "refs", "heads", "main")
- if refFile != expected {
- t.Errorf("Expected %s, got %s", expected, refFile)
- }
-
- // Test case 2: HEAD contains a direct commit hash
- err = os.WriteFile(headFile, []byte("abc123def456\n"), 0644)
- if err != nil {
- t.Fatal(err)
- }
-
- refFile = getGitRefFile(tmpDir)
- if refFile != headFile {
- t.Errorf("Expected %s, got %s", headFile, refFile)
- }
-}
-
-func TestFileWatcherIntegration(t *testing.T) {
- // This test requires being in a git repository
- if getCurrentGitBranch(".") == "" {
- t.Skip("Not in a git repository, skipping integration test")
- }
-
- // Test that the file watcher setup doesn't crash
- tmpDir := t.TempDir()
- gitDir := filepath.Join(tmpDir, ".git")
- err := os.MkdirAll(gitDir, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- headFile := filepath.Join(gitDir, "HEAD")
- err = os.WriteFile(headFile, []byte("ref: refs/heads/main\n"), 0644)
- if err != nil {
- t.Fatal(err)
- }
-
- // Create the refs directory and file
- refsDir := filepath.Join(gitDir, "refs", "heads")
- err = os.MkdirAll(refsDir, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- mainRef := filepath.Join(refsDir, "main")
- err = os.WriteFile(mainRef, []byte("abc123def456\n"), 0644)
- if err != nil {
- t.Fatal(err)
- }
-
- // Test that we can create a watcher without crashing
- // This is a basic smoke test
- done := make(chan bool, 1)
- go func() {
- time.Sleep(100 * time.Millisecond)
- done <- true
- }()
-
- select {
- case <-done:
- // Test passed - no crash
- case <-time.After(1 * time.Second):
- t.Error("Test timed out")
- }
-}