Skip to content

Commit 9d51ec9

Browse files
committed
shell: Migrate from portal
0 parents  commit 9d51ec9

3 files changed

Lines changed: 364 additions & 0 deletions

File tree

go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module github.com/FrameworkOSS/feature_shell
2+
3+
go 1.25.4
4+
5+
require (
6+
github.com/FrameworkOSS/portal v0.0.0-20260128064008-86f8a0104e4d
7+
github.com/fatih/color v1.18.0
8+
)
9+
10+
require (
11+
github.com/JoshuaDoes/crunchio v0.0.4 // indirect
12+
github.com/mattn/go-colorable v0.1.13 // indirect
13+
github.com/mattn/go-isatty v0.0.20 // indirect
14+
github.com/superwhiskers/crunch/v3 v3.5.7 // indirect
15+
golang.org/x/sys v0.25.0 // indirect
16+
)

go.sum

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
github.com/FrameworkOSS/portal v0.0.0-20260128064008-86f8a0104e4d h1:CjPmUDTYLosI4QMDXbzlAKGv6kgPwz8/HgFxLSCG7r4=
2+
github.com/FrameworkOSS/portal v0.0.0-20260128064008-86f8a0104e4d/go.mod h1:IfeRYR2FUMmnnrdyrTePXWa77sEVICG5LaEpafLRUmI=
3+
github.com/JoshuaDoes/crunchio v0.0.4 h1:gHMvTT0rHCITPjhfxZ0huykyAUBnP94xzExyhnHcBCM=
4+
github.com/JoshuaDoes/crunchio v0.0.4/go.mod h1:Zk8lv1MlNNb5WpMcNSr8uBgleoE5G3PpXi6MD3JImOA=
5+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
6+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
7+
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
8+
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
9+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
10+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
11+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
12+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
13+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
14+
github.com/superwhiskers/crunch/v3 v3.5.7 h1:N9RLxaR65C36i26BUIpzPXGy2f6pQ7wisu2bawbKNqg=
15+
github.com/superwhiskers/crunch/v3 v3.5.7/go.mod h1:4ub2EKgF1MAhTjoOCTU4b9uLMsAweHEa89aRrfAypXA=
16+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
17+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
18+
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
19+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

shell.go

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
package shell
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/FrameworkOSS/portal/features/commands"
14+
"github.com/FrameworkOSS/portal/portal"
15+
"github.com/fatih/color"
16+
)
17+
18+
var (
19+
cyan = color.New(color.FgCyan).SprintFunc()
20+
green = color.New(color.FgGreen).SprintFunc()
21+
red = color.New(color.FgRed).SprintFunc()
22+
yellow = color.New(color.FgYellow).SprintFunc()
23+
)
24+
25+
type Shell struct {
26+
lockResp sync.Mutex
27+
resps []*portal.Event
28+
processor *portal.EventHandler
29+
30+
running bool
31+
canType bool
32+
prompted bool
33+
lastPrompted bool
34+
canPrompt bool
35+
36+
press bool
37+
instance string
38+
workdir string
39+
initCmds []string
40+
41+
p *portal.Portal
42+
c *commands.Commands
43+
}
44+
45+
func NewShell(p *portal.Portal, instance string, requireStart, stdCmds bool, initCmds ...string) (sh *Shell) {
46+
if p == nil {
47+
panic("shell: portal is nil")
48+
}
49+
50+
sh = new(Shell)
51+
sh.p = p
52+
sh.resps = make([]*portal.Event, 0)
53+
54+
sh.processor = portal.NewEventHandler().
55+
Handle(sh.eWorkdir, "workdir").
56+
Handle(sh.eSuccess, "success").
57+
Handle(sh.eResp, "resp", "shell").
58+
Handle(sh.eError, "error")
59+
60+
if instance == "" {
61+
instance = "shell"
62+
}
63+
sh.instance = instance
64+
sh.press = requireStart
65+
sh.initCmds = initCmds
66+
67+
if stdCmds {
68+
sh.c = commands.NewCommands(p)
69+
p.FeatureAdd(sh.c)
70+
}
71+
72+
return
73+
}
74+
75+
func (sh *Shell) eWorkdir(e *portal.Event) error {
76+
wd := sh.workdir
77+
sh.workdir = string(e.GetData())
78+
if wd != sh.workdir {
79+
sh.lastPrompted = false
80+
}
81+
sh.prompt(true)
82+
return nil
83+
}
84+
85+
func (sh *Shell) eSuccess(e *portal.Event) error {
86+
fmt.Printf("Success: %s (%s)\n", e.GetProducer(), e.GetChannel())
87+
sh.lastPrompted = false
88+
sh.prompt(true)
89+
return nil
90+
}
91+
92+
func (sh *Shell) eResp(e *portal.Event) error {
93+
if e.GetDataSize() > 0 {
94+
str := string(e.GetData())
95+
if e.GetID() != "shell" && str[len(str)-1] != '\n' {
96+
str += "\n"
97+
}
98+
fmt.Printf("%s", str)
99+
sh.lastPrompted = false
100+
}
101+
sh.prompt(true)
102+
return nil
103+
}
104+
105+
func (sh *Shell) eError(e *portal.Event) error {
106+
if e.GetDataSize() > 0 {
107+
fmt.Printf("%s\n", red(string(e.GetData())))
108+
} else {
109+
fmt.Println(red(fmt.Sprintf("Error: %s", e.GetProducer())))
110+
}
111+
sh.lastPrompted = false
112+
sh.prompt(true)
113+
return nil
114+
}
115+
116+
func (sh *Shell) API() int {
117+
return 0
118+
}
119+
120+
func (sh *Shell) ID() string {
121+
return "shell"
122+
}
123+
124+
func (sh *Shell) Name() string {
125+
return "Shell"
126+
}
127+
128+
func (sh *Shell) Authors() []string {
129+
return []string{"JoshuaDoes"}
130+
}
131+
132+
func (sh *Shell) Description() string {
133+
return "An interactive shell to translate stdin to commands."
134+
}
135+
136+
func (sh *Shell) Version() string {
137+
return "v0.0.1"
138+
}
139+
140+
func (sh *Shell) Open() error {
141+
if sh.running {
142+
return fmt.Errorf("%s: already open", sh.ID())
143+
}
144+
go sh.loopStdin()
145+
return nil
146+
}
147+
148+
func (sh *Shell) Close() (errs []error, retry bool) {
149+
sh.running = false
150+
return
151+
}
152+
153+
func (sh *Shell) Input(e *portal.Event) error {
154+
sh.processor.Process(e)
155+
return nil
156+
}
157+
158+
func (sh *Shell) Output() (*portal.Event, error) {
159+
sh.lockResp.Lock()
160+
defer sh.lockResp.Unlock()
161+
if len(sh.resps) == 0 {
162+
return nil, nil
163+
}
164+
e := sh.resps[0]
165+
sh.resps = sh.resps[1:]
166+
return e, nil
167+
}
168+
169+
func (sh *Shell) storeResp(e *portal.Event) {
170+
sh.lockResp.Lock()
171+
defer sh.lockResp.Unlock()
172+
sh.resps = append(sh.resps, e)
173+
}
174+
175+
func (sh *Shell) ready() {
176+
sh.running = true
177+
sh.storeResp(portal.NewEventReady(sh.ID(), true))
178+
}
179+
180+
func (sh *Shell) unready() {
181+
sh.running = false
182+
sh.storeResp(portal.NewEventReady(sh.ID(), false))
183+
}
184+
185+
func (sh *Shell) prompt(reset bool) {
186+
prompted := sh.prompted
187+
if reset {
188+
prompted = false
189+
sh.prompted = false
190+
}
191+
if !prompted {
192+
if !sh.lastPrompted && sh.canPrompt {
193+
prompt := fmt.Sprintf("%s%s%s:%s$ ", green(sh.ID()), yellow("@"), green(sh.p.ID()), cyan(filepath.Base(sh.workdir)))
194+
fmt.Fprintf(color.Output, "%s", prompt)
195+
}
196+
sh.lastPrompted = true
197+
sh.prompted = true
198+
}
199+
sh.canType = true
200+
}
201+
202+
func (sh *Shell) loopStdin() {
203+
sh.ready()
204+
reader := bufio.NewReader(os.Stdin)
205+
206+
if sh.press {
207+
fmt.Println("Press enter to start the shell. Press CTRL+D to stop the shell.")
208+
_, err := reader.ReadString('\n')
209+
if err != nil {
210+
sh.unready()
211+
return
212+
}
213+
}
214+
215+
//Wait for a response about the current workdir from files
216+
sh.storeResp(portal.NewEvent().SetID("workdir").AddParticipants("files"))
217+
for {
218+
if !sh.running {
219+
return
220+
}
221+
if sh.canType {
222+
break
223+
}
224+
}
225+
226+
//Start off by executing input commands!
227+
if len(sh.initCmds) > 0 {
228+
for i := 0; i < len(sh.initCmds); i++ {
229+
if err := sh.exec(sh.initCmds[i]); err != nil {
230+
sh.Input(portal.NewEventError(sh.ID(), err))
231+
}
232+
if !sh.running {
233+
return
234+
}
235+
}
236+
}
237+
238+
sh.lastPrompted = false
239+
sh.canPrompt = true
240+
sh.prompt(true)
241+
242+
for {
243+
for {
244+
if !sh.running {
245+
return
246+
}
247+
if sh.canType {
248+
break
249+
}
250+
}
251+
sh.prompt(false)
252+
253+
line, err := reader.ReadString('\n')
254+
if err != nil {
255+
sh.running = false
256+
break
257+
}
258+
line = strings.TrimSpace(line)
259+
if line == "" {
260+
sh.lastPrompted = false
261+
sh.prompt(true)
262+
continue
263+
}
264+
265+
if err := sh.exec(line); err != nil {
266+
sh.Input(portal.NewEventError(sh.ID(), err))
267+
}
268+
}
269+
270+
sh.unready()
271+
fmt.Println("")
272+
}
273+
274+
func (sh *Shell) exec(cmd string) error {
275+
sh.canType = false
276+
op := strings.Split(cmd, " ")
277+
if !sh.handle(op...) {
278+
e, err := sh.c.NewCommandLineEvent(sh.ID(), op...)
279+
if err != nil {
280+
return err
281+
}
282+
sh.storeResp(e)
283+
}
284+
for {
285+
if !sh.running || sh.canType {
286+
break
287+
}
288+
}
289+
return nil
290+
}
291+
292+
func (sh *Shell) handle(op ...string) bool {
293+
resp := ""
294+
295+
cmd := strings.ToLower(op[0])
296+
switch cmd {
297+
// --- Add custom cases here! ---
298+
case "clear", "cls", "c", "reset":
299+
resp = "\033[H\033[2J"
300+
case "echo":
301+
resp = "\n"
302+
if len(op) > 1 {
303+
resp = strings.Join(op[1:], " ") + "\n"
304+
}
305+
case "sleep", "sleepms", "sleepmilli", "wait", "waitms", "waitmilli":
306+
if len(op) > 1 {
307+
duration, err := strconv.Atoi(op[1])
308+
if err != nil {
309+
sh.Input(portal.NewEventError(sh.ID(), err))
310+
}
311+
time.Sleep(time.Millisecond * time.Duration(duration))
312+
} else {
313+
time.Sleep(time.Second * 1)
314+
}
315+
resp = " "
316+
case "dump":
317+
resp = fmt.Sprintf("Features:\n-%s\n", strings.Join(sh.p.Features(), "\n-"))
318+
}
319+
320+
if resp != "" {
321+
r := portal.NewEventResponse(sh.ID(), nil).SetID("shell")
322+
if resp != " " {
323+
r.SetData([]byte(resp))
324+
}
325+
sh.Input(r)
326+
return true
327+
}
328+
return false
329+
}

0 commit comments

Comments
 (0)