From c9f03ce98036d02f7e4f96fa2da6328454698acc Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 7 Aug 2024 13:16:48 -0300 Subject: [PATCH] docs: wish-exec and bubbletea-exec examples refs #303 --- examples/bubbletea-exec/main.go | 150 ++++++++++++++++++++++++++++++++ examples/wish-exec/example.sh | 3 + examples/wish-exec/main.go | 100 ++------------------- 3 files changed, 162 insertions(+), 91 deletions(-) create mode 100644 examples/bubbletea-exec/main.go create mode 100755 examples/wish-exec/example.sh diff --git a/examples/bubbletea-exec/main.go b/examples/bubbletea-exec/main.go new file mode 100644 index 0000000..90ba571 --- /dev/null +++ b/examples/bubbletea-exec/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "context" + "errors" + "net" + "os" + "os/signal" + "runtime" + "syscall" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" + "github.com/charmbracelet/ssh" + "github.com/charmbracelet/wish" + "github.com/charmbracelet/wish/activeterm" + "github.com/charmbracelet/wish/bubbletea" + "github.com/charmbracelet/wish/logging" + "github.com/charmbracelet/x/editor" +) + +const ( + host = "localhost" + port = "23234" +) + +func main() { + s, err := wish.NewServer( + wish.WithAddress(net.JoinHostPort(host, port)), + + // Allocate a pty. + // This creates a pseudoconsole on windows, compatibility is limited in + // that case, see the open issues for more details. + ssh.AllocatePty(), + wish.WithMiddleware( + // run our Bubble Tea handler + bubbletea.Middleware(teaHandler), + + // ensure the user has requested a tty + activeterm.Middleware(), + logging.Middleware(), + ), + ) + if err != nil { + log.Error("Could not start server", "error", err) + } + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + log.Info("Starting SSH server", "host", host, "port", port) + go func() { + if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { + log.Error("Could not start server", "error", err) + done <- nil + } + }() + + <-done + log.Info("Stopping SSH server") + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer func() { cancel() }() + if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) { + log.Error("Could not stop server", "error", err) + } +} + +func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { + // Create a lipgloss.Renderer for the session + renderer := bubbletea.MakeRenderer(s) + // Set up the model with the current session and styles. + // We'll use the session to call wish.Command, which makes it compatible + // with tea.Command. + m := model{ + sess: s, + style: renderer.NewStyle().Foreground(lipgloss.Color("8")), + errStyle: renderer.NewStyle().Foreground(lipgloss.Color("3")), + } + return m, []tea.ProgramOption{tea.WithAltScreen()} +} + +type model struct { + err error + sess ssh.Session + style lipgloss.Style + errStyle lipgloss.Style +} + +func (m model) Init() tea.Cmd { + return nil +} + +type cmdFinishedMsg struct{ err error } + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "e": + // Open file.txt in the default editor. + edit, err := editor.Cmd("wish", "file.txt") + if err != nil { + m.err = err + return m, nil + } + // Creates a wish.Cmd from the exec.Cmd + wishCmd := wish.Command(m.sess, edit.Path, edit.Args...) + // Runs the cmd through Bubble Tea. + // Bubble Tea should handle the IO to the program, and get it back + // once the program quits. + cmd := tea.Exec(wishCmd, func(err error) tea.Msg { + if err != nil { + log.Error("editor finished", "error", err) + } + return cmdFinishedMsg{err: err} + }) + return m, cmd + case "s": + // We can also execute a shell and give it over to the user. + // Note that this session won't have control, so it can't run tasks + // in background, suspend, etc. + c := wish.Command(m.sess, "bash", "-im") + if runtime.GOOS == "windows" { + c = wish.Command(m.sess, "powershell") + } + cmd := tea.Exec(c, func(err error) tea.Msg { + if err != nil { + log.Error("shell finished", "error", err) + } + return cmdFinishedMsg{err: err} + }) + return m, cmd + case "q", "ctrl+c": + return m, tea.Quit + } + case cmdFinishedMsg: + m.err = msg.err + return m, nil + } + + return m, nil +} + +func (m model) View() string { + if m.err != nil { + return m.errStyle.Render(m.err.Error() + "\n") + } + return m.style.Render("Press 'e' to edit, 's' to hop into a shell, or 'q' to quit...\n") +} diff --git a/examples/wish-exec/example.sh b/examples/wish-exec/example.sh new file mode 100755 index 0000000..b7441b7 --- /dev/null +++ b/examples/wish-exec/example.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +gum choose a b c d diff --git a/examples/wish-exec/main.go b/examples/wish-exec/main.go index 90ba571..f98af06 100644 --- a/examples/wish-exec/main.go +++ b/examples/wish-exec/main.go @@ -6,19 +6,14 @@ import ( "net" "os" "os/signal" - "runtime" "syscall" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" "github.com/charmbracelet/wish/activeterm" - "github.com/charmbracelet/wish/bubbletea" "github.com/charmbracelet/wish/logging" - "github.com/charmbracelet/x/editor" ) const ( @@ -35,9 +30,15 @@ func main() { // that case, see the open issues for more details. ssh.AllocatePty(), wish.WithMiddleware( - // run our Bubble Tea handler - bubbletea.Middleware(teaHandler), - + func(next ssh.Handler) ssh.Handler { + return func(s ssh.Session) { + cmd := wish.Command(s, "bash", "example.sh") + if err := cmd.Run(); err != nil { + wish.Fatalln(s, err) + } + next(s) + } + }, // ensure the user has requested a tty activeterm.Middleware(), logging.Middleware(), @@ -65,86 +66,3 @@ func main() { log.Error("Could not stop server", "error", err) } } - -func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { - // Create a lipgloss.Renderer for the session - renderer := bubbletea.MakeRenderer(s) - // Set up the model with the current session and styles. - // We'll use the session to call wish.Command, which makes it compatible - // with tea.Command. - m := model{ - sess: s, - style: renderer.NewStyle().Foreground(lipgloss.Color("8")), - errStyle: renderer.NewStyle().Foreground(lipgloss.Color("3")), - } - return m, []tea.ProgramOption{tea.WithAltScreen()} -} - -type model struct { - err error - sess ssh.Session - style lipgloss.Style - errStyle lipgloss.Style -} - -func (m model) Init() tea.Cmd { - return nil -} - -type cmdFinishedMsg struct{ err error } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "e": - // Open file.txt in the default editor. - edit, err := editor.Cmd("wish", "file.txt") - if err != nil { - m.err = err - return m, nil - } - // Creates a wish.Cmd from the exec.Cmd - wishCmd := wish.Command(m.sess, edit.Path, edit.Args...) - // Runs the cmd through Bubble Tea. - // Bubble Tea should handle the IO to the program, and get it back - // once the program quits. - cmd := tea.Exec(wishCmd, func(err error) tea.Msg { - if err != nil { - log.Error("editor finished", "error", err) - } - return cmdFinishedMsg{err: err} - }) - return m, cmd - case "s": - // We can also execute a shell and give it over to the user. - // Note that this session won't have control, so it can't run tasks - // in background, suspend, etc. - c := wish.Command(m.sess, "bash", "-im") - if runtime.GOOS == "windows" { - c = wish.Command(m.sess, "powershell") - } - cmd := tea.Exec(c, func(err error) tea.Msg { - if err != nil { - log.Error("shell finished", "error", err) - } - return cmdFinishedMsg{err: err} - }) - return m, cmd - case "q", "ctrl+c": - return m, tea.Quit - } - case cmdFinishedMsg: - m.err = msg.err - return m, nil - } - - return m, nil -} - -func (m model) View() string { - if m.err != nil { - return m.errStyle.Render(m.err.Error() + "\n") - } - return m.style.Render("Press 'e' to edit, 's' to hop into a shell, or 'q' to quit...\n") -}