Skip to content

Commit 23261db

Browse files
docs: improving examples and docs (#235)
* docs: improving examples and docs Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * refactor: renamed some vars Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * docs: more examples Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * chore: typo * docs: wording * Apply suggestions from code review Co-authored-by: Ayman Bagabas <ayman.bagabas@gmail.com> --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Ayman Bagabas <ayman.bagabas@gmail.com>
1 parent 5790d56 commit 23261db

File tree

22 files changed

+483
-253
lines changed

22 files changed

+483
-253
lines changed

activeterm/activeterm.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22
package activeterm
33

44
import (
5-
"fmt"
6-
75
"github.com/charmbracelet/ssh"
86
"github.com/charmbracelet/wish"
97
)
108

119
// Middleware will exit 1 connections trying with no active terminals.
1210
func Middleware() wish.Middleware {
13-
return func(sh ssh.Handler) ssh.Handler {
14-
return func(s ssh.Session) {
15-
_, _, active := s.Pty()
16-
if !active {
17-
fmt.Fprintln(s, "Requires an active PTY")
18-
s.Exit(1) // nolint: errcheck
19-
return // unreachable
11+
return func(next ssh.Handler) ssh.Handler {
12+
return func(sess ssh.Session) {
13+
_, _, active := sess.Pty()
14+
if active {
15+
next(sess)
16+
return
2017
}
21-
sh(s)
18+
wish.Println(sess, "Requires an active PTY")
19+
_ = sess.Exit(1)
2220
}
2321
}
2422
}

bubbletea/tea.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type BubbleTeaHandler = Handler // nolint: revive
2323
// Handler is the function Bubble Tea apps implement to hook into the
2424
// SSH Middleware. This will create a new tea.Program for every connection and
2525
// start it with the tea.ProgramOptions returned.
26-
type Handler func(ssh.Session) (tea.Model, []tea.ProgramOption)
26+
type Handler func(sess ssh.Session) (tea.Model, []tea.ProgramOption)
2727

2828
// ProgramHandler is the function Bubble Tea apps implement to hook into the SSH
2929
// Middleware. This should return a new tea.Program. This handler is different
@@ -32,24 +32,24 @@ type Handler func(ssh.Session) (tea.Model, []tea.ProgramOption)
3232
//
3333
// Make sure to set the tea.WithInput and tea.WithOutput to the ssh.Session
3434
// otherwise the program will not function properly.
35-
type ProgramHandler func(ssh.Session) *tea.Program
35+
type ProgramHandler func(sess ssh.Session) *tea.Program
3636

3737
// Middleware takes a Handler and hooks the input and output for the
3838
// ssh.Session into the tea.Program.
3939
//
4040
// It also captures window resize events and sends them to the tea.Program
4141
// as tea.WindowSizeMsgs.
42-
func Middleware(bth Handler) wish.Middleware {
43-
return MiddlewareWithProgramHandler(newDefaultProgramHandler(bth), termenv.Ascii)
42+
func Middleware(handler Handler) wish.Middleware {
43+
return MiddlewareWithProgramHandler(newDefaultProgramHandler(handler), termenv.Ascii)
4444
}
4545

4646
// MiddlewareWithColorProfile allows you to specify the minimum number of colors
4747
// this program needs to work properly.
4848
//
4949
// If the client's color profile has less colors than p, p will be forced.
5050
// Use with caution.
51-
func MiddlewareWithColorProfile(bth Handler, p termenv.Profile) wish.Middleware {
52-
return MiddlewareWithProgramHandler(newDefaultProgramHandler(bth), p)
51+
func MiddlewareWithColorProfile(handler Handler, profile termenv.Profile) wish.Middleware {
52+
return MiddlewareWithProgramHandler(newDefaultProgramHandler(handler), profile)
5353
}
5454

5555
// MiddlewareWithProgramHandler allows you to specify the ProgramHandler to be
@@ -65,41 +65,41 @@ func MiddlewareWithColorProfile(bth Handler, p termenv.Profile) wish.Middleware
6565
//
6666
// If the client's color profile has less colors than p, p will be forced.
6767
// Use with caution.
68-
func MiddlewareWithProgramHandler(bth ProgramHandler, p termenv.Profile) wish.Middleware {
69-
return func(h ssh.Handler) ssh.Handler {
70-
return func(s ssh.Session) {
71-
s.Context().SetValue(minColorProfileKey, p)
72-
_, windowChanges, ok := s.Pty()
68+
func MiddlewareWithProgramHandler(handler ProgramHandler, profile termenv.Profile) wish.Middleware {
69+
return func(next ssh.Handler) ssh.Handler {
70+
return func(sess ssh.Session) {
71+
sess.Context().SetValue(minColorProfileKey, profile)
72+
_, windowChanges, ok := sess.Pty()
7373
if !ok {
74-
wish.Fatalln(s, "no active terminal, skipping")
74+
wish.Fatalln(sess, "no active terminal, skipping")
7575
return
7676
}
77-
p := bth(s)
78-
if p == nil {
79-
h(s)
77+
program := handler(sess)
78+
if program == nil {
79+
next(sess)
8080
return
8181
}
82-
ctx, cancel := context.WithCancel(s.Context())
82+
ctx, cancel := context.WithCancel(sess.Context())
8383
go func() {
8484
for {
8585
select {
8686
case <-ctx.Done():
87-
p.Quit()
87+
program.Quit()
8888
return
8989
case w := <-windowChanges:
90-
p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
90+
program.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
9191
}
9292
}
9393
}()
94-
if _, err := p.Run(); err != nil {
94+
if _, err := program.Run(); err != nil {
9595
log.Error("app exit with error", "error", err)
9696
}
9797
// p.Kill() will force kill the program if it's still running,
9898
// and restore the terminal to its original state in case of a
9999
// tui crash
100-
p.Kill()
100+
program.Kill()
101101
cancel()
102-
h(s)
102+
next(sess)
103103
}
104104
}
105105
}
@@ -110,23 +110,23 @@ var profileNames = [4]string{"TrueColor", "ANSI256", "ANSI", "Ascii"}
110110

111111
// MakeRenderer returns a lipgloss renderer for the current session.
112112
// This function handle PTYs as well, and should be used to style your application.
113-
func MakeRenderer(s ssh.Session) *lipgloss.Renderer {
114-
cp, ok := s.Context().Value(minColorProfileKey).(termenv.Profile)
113+
func MakeRenderer(sess ssh.Session) *lipgloss.Renderer {
114+
cp, ok := sess.Context().Value(minColorProfileKey).(termenv.Profile)
115115
if !ok {
116116
cp = termenv.Ascii
117117
}
118-
r := newRenderer(s)
118+
r := newRenderer(sess)
119119
if r.ColorProfile() > cp {
120-
wish.Printf(s, "Warning: Client's terminal is %q, forcing %q\r\n", profileNames[r.ColorProfile()], profileNames[cp])
120+
wish.Printf(sess, "Warning: Client's terminal is %q, forcing %q\r\n", profileNames[r.ColorProfile()], profileNames[cp])
121121
r.SetColorProfile(cp)
122122
}
123123
return r
124124
}
125125

126126
// MakeOptions returns the tea.WithInput and tea.WithOutput program options
127127
// taking into account possible Emulated or Allocated PTYs.
128-
func MakeOptions(s ssh.Session) []tea.ProgramOption {
129-
return makeOpts(s)
128+
func MakeOptions(sess ssh.Session) []tea.ProgramOption {
129+
return makeOpts(sess)
130130
}
131131

132132
type sshEnviron []string
@@ -148,9 +148,9 @@ func (e sshEnviron) Getenv(k string) string {
148148
return ""
149149
}
150150

151-
func newDefaultProgramHandler(bth Handler) ProgramHandler {
151+
func newDefaultProgramHandler(handler Handler) ProgramHandler {
152152
return func(s ssh.Session) *tea.Program {
153-
m, opts := bth(s)
153+
m, opts := handler(s)
154154
if m == nil {
155155
return nil
156156
}

examples/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Wish Examples
2+
3+
We recommend you follow the examples in the following order:
4+
5+
## Basics
6+
7+
1. [Simple](./simple)
8+
1. [Server banner and middleware](./banner)
9+
1. [Identifying Users](./identity)
10+
1. [Multiple authentication types](./multi-auth)
11+
12+
## Making SSH apps
13+
14+
1. [Using spf13/cobra](./cobra)
15+
1. [Serving Bubble Tea apps](./bubbletea)
16+
1. [Serving Bubble Tea programs](./bubbleteaprogram)
17+
1. [Reverse Port Forwarding](./forward)
18+
1. [Multichat](./multichat)
19+
20+
## SCP, SFTP, and Git
21+
22+
1. [Serving a Git repository](./git)
23+
1. [SCP and SFTP](./scp)
24+
25+
## Pseudo Terminals
26+
27+
1. [Allocate a PTY](./pty)
28+
1. [Running Bubble Tea, and executing another program on an allocated PTY](./wish-exec)
Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"net"
78
"os"
89
"os/signal"
910
"syscall"
@@ -20,43 +21,45 @@ import (
2021

2122
const (
2223
host = "localhost"
23-
port = 23234
24+
port = "23234"
2425
)
2526

2627
//go:embed banner.txt
2728
var banner string
2829

2930
func main() {
3031
s, err := wish.NewServer(
31-
wish.WithAddress(fmt.Sprintf("%s:%d", host, port)),
32-
wish.WithHostKeyPath(".ssh/term_info_ed25519"),
32+
wish.WithAddress(net.JoinHostPort(host, port)),
33+
wish.WithHostKeyPath(".ssh/id_ed25519"),
34+
// A banner is always shown, even before authentication.
3335
wish.WithBannerHandler(func(ctx ssh.Context) string {
3436
return fmt.Sprintf(banner, ctx.User())
3537
}),
3638
wish.WithPasswordAuth(func(ctx ssh.Context, password string) bool {
3739
return password == "asd123"
3840
}),
3941
wish.WithMiddleware(
40-
func(h ssh.Handler) ssh.Handler {
41-
return func(s ssh.Session) {
42-
wish.Println(s, "Hello, world!")
43-
h(s)
42+
func(next ssh.Handler) ssh.Handler {
43+
return func(sess ssh.Session) {
44+
wish.Println(sess, fmt.Sprintf("Hello, %s!", sess.User()))
45+
next(sess)
4446
}
4547
},
46-
elapsed.Middleware(),
4748
logging.Middleware(),
49+
// This middleware prints the session duration before disconnecting.
50+
elapsed.Middleware(),
4851
),
4952
)
5053
if err != nil {
51-
log.Error("could not start server", "error", err)
54+
log.Error("Could not start server", "error", err)
5255
}
5356

5457
done := make(chan os.Signal, 1)
5558
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
5659
log.Info("Starting SSH server", "host", host, "port", port)
5760
go func() {
5861
if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
59-
log.Error("could not start server", "error", err)
62+
log.Error("Could not start server", "error", err)
6063
done <- nil
6164
}
6265
}()
@@ -66,6 +69,6 @@ func main() {
6669
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
6770
defer func() { cancel() }()
6871
if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
69-
log.Error("could not stop server", "error", err)
72+
log.Error("Could not stop server", "error", err)
7073
}
7174
}

examples/bubbletea/main.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"net"
1011
"os"
1112
"os/signal"
1213
"syscall"
@@ -17,34 +18,36 @@ import (
1718
"github.com/charmbracelet/log"
1819
"github.com/charmbracelet/ssh"
1920
"github.com/charmbracelet/wish"
20-
bm "github.com/charmbracelet/wish/bubbletea"
21-
lm "github.com/charmbracelet/wish/logging"
21+
"github.com/charmbracelet/wish/activeterm"
22+
"github.com/charmbracelet/wish/bubbletea"
23+
"github.com/charmbracelet/wish/logging"
2224
)
2325

2426
const (
2527
host = "localhost"
26-
port = 23234
28+
port = "23234"
2729
)
2830

2931
func main() {
3032
s, err := wish.NewServer(
31-
wish.WithAddress(fmt.Sprintf("%s:%d", host, port)),
32-
wish.WithHostKeyPath(".ssh/term_info_ed25519"),
33+
wish.WithAddress(net.JoinHostPort(host, port)),
34+
wish.WithHostKeyPath(".ssh/id_ed25519"),
3335
wish.WithMiddleware(
34-
bm.Middleware(teaHandler),
35-
lm.Middleware(),
36+
bubbletea.Middleware(teaHandler),
37+
activeterm.Middleware(), // Bubble Tea apps usually require a PTY.
38+
logging.Middleware(),
3639
),
3740
)
3841
if err != nil {
39-
log.Error("could not start server", "error", err)
42+
log.Error("Could not start server", "error", err)
4043
}
4144

4245
done := make(chan os.Signal, 1)
4346
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
4447
log.Info("Starting SSH server", "host", host, "port", port)
4548
go func() {
4649
if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
47-
log.Error("could not start server", "error", err)
50+
log.Error("Could not start server", "error", err)
4851
done <- nil
4952
}
5053
}()
@@ -54,7 +57,7 @@ func main() {
5457
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
5558
defer func() { cancel() }()
5659
if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
57-
log.Error("could not stop server", "error", err)
60+
log.Error("Could not stop server", "error", err)
5861
}
5962
}
6063

@@ -63,18 +66,28 @@ func main() {
6366
// pass it to the new model. You can also return tea.ProgramOptions (such as
6467
// tea.WithAltScreen) on a session by session basis.
6568
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
66-
pty, _, active := s.Pty()
67-
if !active {
68-
wish.Fatalln(s, "no active terminal, skipping")
69-
return nil, nil
70-
}
71-
renderer := bm.MakeRenderer(s)
69+
// This should never fail, as we are using the activeterm middleware.
70+
pty, _, _ := s.Pty()
71+
72+
// When running a Bubble Tea app over SSH, you shouldn't use the default
73+
// lipgloss.NewStyle function.
74+
// That function will use the color profile from the os.Stdin, which is the
75+
// server, not the client.
76+
// We provide a MakeRenderer function in the bubbletea middleware package,
77+
// so you can easily get the correct renderer for the current session, and
78+
// use it to create the styles.
79+
// The recommended way to use these styles is to then pass them down to
80+
// your Bubble Tea model.
81+
renderer := bubbletea.MakeRenderer(s)
82+
txtStyle := renderer.NewStyle().Foreground(lipgloss.Color("10"))
83+
quitStyle := renderer.NewStyle().Foreground(lipgloss.Color("8"))
84+
7285
m := model{
7386
term: pty.Term,
7487
width: pty.Window.Width,
7588
height: pty.Window.Height,
76-
txtStyle: renderer.NewStyle().Foreground(lipgloss.Color("10")),
77-
quitStyle: renderer.NewStyle().Foreground(lipgloss.Color("8")),
89+
txtStyle: txtStyle,
90+
quitStyle: quitStyle,
7891
}
7992
return m, []tea.ProgramOption{tea.WithAltScreen()}
8093
}

0 commit comments

Comments
 (0)