1package shell
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "sync"
8
9 "github.com/charmbracelet/hotdiva2000"
10)
11
12// BackgroundShell represents a shell running in the background.
13type BackgroundShell struct {
14 ID string
15 Shell *Shell
16 ctx context.Context
17 cancel context.CancelFunc
18 stdout *bytes.Buffer
19 stderr *bytes.Buffer
20 mu sync.RWMutex
21 done chan struct{}
22 exitErr error
23 workingDir string
24}
25
26// BackgroundShellManager manages background shell instances.
27type BackgroundShellManager struct {
28 shells map[string]*BackgroundShell
29 mu sync.RWMutex
30}
31
32var (
33 backgroundManager *BackgroundShellManager
34 backgroundManagerOnce sync.Once
35)
36
37// GetBackgroundShellManager returns the singleton background shell manager.
38func GetBackgroundShellManager() *BackgroundShellManager {
39 backgroundManagerOnce.Do(func() {
40 backgroundManager = &BackgroundShellManager{
41 shells: make(map[string]*BackgroundShell),
42 }
43 })
44 return backgroundManager
45}
46
47// Start creates and starts a new background shell with the given command.
48func (m *BackgroundShellManager) Start(ctx context.Context, workingDir string, blockFuncs []BlockFunc, command string) (*BackgroundShell, error) {
49 id := hotdiva2000.Generate()
50
51 shell := NewShell(&Options{
52 WorkingDir: workingDir,
53 BlockFuncs: blockFuncs,
54 })
55
56 shellCtx, cancel := context.WithCancel(ctx)
57
58 bgShell := &BackgroundShell{
59 ID: id,
60 Shell: shell,
61 ctx: shellCtx,
62 cancel: cancel,
63 stdout: &bytes.Buffer{},
64 stderr: &bytes.Buffer{},
65 done: make(chan struct{}),
66 workingDir: workingDir,
67 }
68
69 m.mu.Lock()
70 m.shells[id] = bgShell
71 m.mu.Unlock()
72
73 go func() {
74 defer close(bgShell.done)
75
76 stdout, stderr, err := shell.Exec(shellCtx, command)
77
78 bgShell.mu.Lock()
79 bgShell.stdout.WriteString(stdout)
80 bgShell.stderr.WriteString(stderr)
81 bgShell.exitErr = err
82 bgShell.mu.Unlock()
83 }()
84
85 return bgShell, nil
86}
87
88// Get retrieves a background shell by ID.
89func (m *BackgroundShellManager) Get(id string) (*BackgroundShell, bool) {
90 m.mu.RLock()
91 defer m.mu.RUnlock()
92 shell, ok := m.shells[id]
93 return shell, ok
94}
95
96// Kill terminates a background shell by ID.
97func (m *BackgroundShellManager) Kill(id string) error {
98 m.mu.Lock()
99 shell, ok := m.shells[id]
100 if !ok {
101 m.mu.Unlock()
102 return fmt.Errorf("background shell not found: %s", id)
103 }
104 delete(m.shells, id)
105 m.mu.Unlock()
106
107 shell.cancel()
108 <-shell.done
109 return nil
110}
111
112// List returns all background shell IDs.
113func (m *BackgroundShellManager) List() []string {
114 m.mu.RLock()
115 defer m.mu.RUnlock()
116
117 ids := make([]string, 0, len(m.shells))
118 for id := range m.shells {
119 ids = append(ids, id)
120 }
121 return ids
122}
123
124// GetOutput returns the current output of a background shell.
125func (bs *BackgroundShell) GetOutput() (stdout string, stderr string, done bool, err error) {
126 bs.mu.RLock()
127 defer bs.mu.RUnlock()
128
129 select {
130 case <-bs.done:
131 return bs.stdout.String(), bs.stderr.String(), true, bs.exitErr
132 default:
133 return bs.stdout.String(), bs.stderr.String(), false, nil
134 }
135}
136
137// IsDone checks if the background shell has finished execution.
138func (bs *BackgroundShell) IsDone() bool {
139 select {
140 case <-bs.done:
141 return true
142 default:
143 return false
144 }
145}
146
147// Wait blocks until the background shell completes.
148func (bs *BackgroundShell) Wait() {
149 <-bs.done
150}
151
152// GetWorkingDir returns the current working directory of the background shell.
153func (bs *BackgroundShell) GetWorkingDir() string {
154 bs.mu.RLock()
155 defer bs.mu.RUnlock()
156 return bs.workingDir
157}