diff options
| author | Dax Raad <[email protected]> | 2025-11-02 18:43:17 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-11-02 18:43:33 -0500 |
| commit | f68374ad2223ddc213bdea9519ca6a699819ee0e (patch) | |
| tree | 04f0fe21b8e12cd62d7274961bb0cff64f966f40 /packages/tui/internal/components/status | |
| parent | 5e86c9b7916f75c7ad227b80eab18c7c54fc8ffe (diff) | |
| download | opencode-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.go | 340 | ||||
| -rw-r--r-- | packages/tui/internal/components/status/status_test.go | 100 |
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") - } -} |
