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