-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsip.go
More file actions
236 lines (199 loc) · 6.31 KB
/
sip.go
File metadata and controls
236 lines (199 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Package sip serves Bubble Tea applications through a web browser.
//
// Sip provides a simple way to make any Bubble Tea TUI application accessible
// through a web browser with full terminal emulation, mouse support, and
// hardware-accelerated rendering via xterm.js.
//
// Basic usage:
//
// server := sip.NewServer(sip.DefaultConfig())
// server.Serve(context.Background(), func(sess sip.Session) (tea.Model, []tea.ProgramOption) {
// pty := sess.Pty()
// return myModel{width: pty.Width, height: pty.Height}, nil
// })
//
// Or with a ProgramHandler for more control:
//
// server.ServeWithProgram(ctx, func(sess sip.Session) *tea.Program {
// return tea.NewProgram(myModel{}, sip.MakeOptions(sess)...)
// })
package sip
import (
"context"
"io"
"os"
"time"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/log"
)
// Session represents a web terminal session, similar to ssh.Session in Wish.
// It provides access to terminal dimensions and other session metadata.
type Session interface {
// Pty returns the pseudo-terminal information for this session.
Pty() Pty
// Context returns the session's context, which is cancelled when the
// session ends.
Context() context.Context
// Read reads input from the web terminal.
Read(p []byte) (n int, err error)
// Write writes output to the web terminal.
Write(p []byte) (n int, err error)
// Fd returns the file descriptor for TTY detection.
// This is required for Bubble Tea to properly detect terminal mode.
Fd() uintptr
// PtySlave returns the underlying PTY slave file for direct I/O.
// Bubble Tea requires the actual *os.File to set raw mode properly.
PtySlave() *os.File
// WindowChanges returns a channel that receives window size changes.
WindowChanges() <-chan WindowSize
}
// Pty represents pseudo-terminal information.
type Pty struct {
Width int
Height int
}
// WindowSize represents a terminal window size change.
type WindowSize struct {
Width int
Height int
}
// Handler is the function Bubble Tea apps implement to hook into sip.
// This will create a new tea.Program for every browser connection and
// start it with the tea.ProgramOptions returned.
type Handler func(sess Session) (tea.Model, []tea.ProgramOption)
// ProgramHandler allows creating custom tea.Program instances.
// Use this for more control over program initialization.
// Make sure to use MakeOptions to properly configure I/O.
type ProgramHandler func(sess Session) *tea.Program
// Config holds the web server configuration.
type Config struct {
// Host to bind to (default: "localhost")
Host string
// Port to listen on (default: "7681")
Port string
// ReadOnly disables input from clients when true
ReadOnly bool
// MaxConnections limits concurrent connections (0 = unlimited)
MaxConnections int
// IdleTimeout for connections (0 = no timeout)
IdleTimeout time.Duration
// AllowOrigins for CORS (empty = all origins allowed)
AllowOrigins []string
// TLSCert path to TLS certificate (enables HTTPS)
TLSCert string
// TLSKey path to TLS private key
TLSKey string
// Debug enables verbose logging
Debug bool
}
// DefaultConfig returns sensible default configuration.
func DefaultConfig() Config {
return Config{
Host: "localhost",
Port: "7681",
ReadOnly: false,
MaxConnections: 0,
IdleTimeout: 0,
AllowOrigins: nil,
Debug: false,
}
}
// Server represents the web terminal server.
type Server struct {
config Config
handler ProgramHandler
server *httpServer
}
// NewServer creates a new web terminal server with the given configuration.
func NewServer(config Config) *Server {
if config.Host == "" {
config.Host = "localhost"
}
if config.Port == "" {
config.Port = "7681"
}
if config.Debug {
logger.SetLevel(log.DebugLevel)
}
return &Server{
config: config,
}
}
// Serve starts the server and serves the Bubble Tea application.
// The handler is called for each new browser session to create a model.
// This method blocks until the context is cancelled.
func (s *Server) Serve(ctx context.Context, handler Handler) error {
return s.ServeWithProgram(ctx, newDefaultProgramHandler(handler))
}
// ServeWithProgram starts the server with a custom ProgramHandler.
// Use this for more control over tea.Program creation.
func (s *Server) ServeWithProgram(ctx context.Context, handler ProgramHandler) error {
s.handler = handler
s.server = newHTTPServer(s.config, handler)
return s.server.start(ctx)
}
// MakeOptions returns tea.ProgramOptions configured for the web session.
// On Unix, this uses the PTY slave file for proper raw mode support.
// On Windows, this uses the Session's Reader/Writer interface with pipes.
func MakeOptions(sess Session) []tea.ProgramOption {
pty := sess.Pty()
ptySlave := sess.PtySlave()
// Start with real environment, filtering out terminal-related vars
var envs []string
for _, e := range os.Environ() {
// Skip terminal vars - we'll set our own
if len(e) >= 5 && e[:5] == "TERM=" {
continue
}
if len(e) >= 10 && e[:10] == "COLORTERM=" {
continue
}
envs = append(envs, e)
}
// Add terminal settings LAST so they take precedence
envs = append(envs,
"TERM=xterm-256color",
"COLORTERM=truecolor",
)
// Determine input/output based on platform
var input io.Reader
var output io.Writer
if ptySlave != nil {
// Unix: use the actual PTY slave file for raw mode support
input = ptySlave
output = ptySlave
} else {
// Windows: use the Session's Reader/Writer (pipe-based)
input = sess.(io.Reader)
output = sess.(io.Writer)
}
opts := []tea.ProgramOption{
tea.WithInput(input),
tea.WithOutput(output),
tea.WithColorProfile(colorprofile.TrueColor),
tea.WithWindowSize(pty.Width, pty.Height),
tea.WithEnvironment(envs),
tea.WithFilter(func(_ tea.Model, msg tea.Msg) tea.Msg {
if _, ok := msg.(tea.SuspendMsg); ok {
return tea.ResumeMsg{}
}
return msg
}),
}
return opts
}
// newDefaultProgramHandler wraps a Handler into a ProgramHandler.
func newDefaultProgramHandler(handler Handler) ProgramHandler {
return func(sess Session) *tea.Program {
m, opts := handler(sess)
if m == nil {
return nil
}
return tea.NewProgram(m, append(opts, MakeOptions(sess)...)...)
}
}
// SetLogLevel sets the logging verbosity for the sip package.
func SetLogLevel(level log.Level) {
logger.SetLevel(level)
}