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 workingDir string
25}
26
27// BackgroundShellManager manages background shell instances.
28type BackgroundShellManager struct {
29 shells *csync.Map[string, *BackgroundShell]
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: csync.NewMap[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.shells.Set(id, bgShell)
70
71 go func() {
72 defer close(bgShell.done)
73
74 stdout, stderr, err := shell.Exec(shellCtx, command)
75
76 bgShell.stdout.WriteString(stdout)
77 bgShell.stderr.WriteString(stderr)
78 bgShell.exitErr = err
79 }()
80
81 return bgShell, nil
82}
83
84// Get retrieves a background shell by ID.
85func (m *BackgroundShellManager) Get(id string) (*BackgroundShell, bool) {
86 return m.shells.Get(id)
87}
88
89// Kill terminates a background shell by ID.
90func (m *BackgroundShellManager) Kill(id string) error {
91 shell, ok := m.shells.Take(id)
92 if !ok {
93 return fmt.Errorf("background shell not found: %s", id)
94 }
95
96 shell.cancel()
97 <-shell.done
98 return nil
99}
100
101// List returns all background shell IDs.
102func (m *BackgroundShellManager) List() []string {
103 ids := make([]string, 0, m.shells.Len())
104 for id := range m.shells.Seq2() {
105 ids = append(ids, id)
106 }
107 return ids
108}
109
110// KillAll terminates all background shells.
111func (m *BackgroundShellManager) KillAll() {
112 shells := make([]*BackgroundShell, 0, m.shells.Len())
113 for shell := range m.shells.Seq() {
114 shells = append(shells, shell)
115 }
116 m.shells.Reset(map[string]*BackgroundShell{})
117
118 for _, shell := range shells {
119 shell.cancel()
120 <-shell.done
121 }
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 select {
127 case <-bs.done:
128 return bs.stdout.String(), bs.stderr.String(), true, bs.exitErr
129 default:
130 return bs.stdout.String(), bs.stderr.String(), false, nil
131 }
132}
133
134// IsDone checks if the background shell has finished execution.
135func (bs *BackgroundShell) IsDone() bool {
136 select {
137 case <-bs.done:
138 return true
139 default:
140 return false
141 }
142}
143
144// Wait blocks until the background shell completes.
145func (bs *BackgroundShell) Wait() {
146 <-bs.done
147}
148
149// GetWorkingDir returns the current working directory of the background shell.
150func (bs *BackgroundShell) GetWorkingDir() string {
151 return bs.workingDir
152}