195 lines
3.8 KiB
Go
195 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/bubbles/textarea"
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
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/muesli/termenv"
|
|
)
|
|
|
|
const (
|
|
host = "0.0.0.0"
|
|
port = "23234"
|
|
)
|
|
|
|
// Contains a wish server and the list of running programs.
|
|
type app struct {
|
|
*ssh.Server
|
|
progs []*tea.Program
|
|
}
|
|
|
|
// Dispatches a message to all running programs.
|
|
func (a *app) send(msg tea.Msg) {
|
|
for _, p := range a.progs {
|
|
go p.Send(msg)
|
|
}
|
|
}
|
|
|
|
func newApp() *app {
|
|
a := new(app)
|
|
s, err := wish.NewServer(
|
|
wish.WithAddress(net.JoinHostPort(host, port)),
|
|
wish.WithHostKeyPath(".ssh/id_ed25519"),
|
|
wish.WithMiddleware(
|
|
bubbletea.MiddlewareWithProgramHandler(a.ProgramHandler, termenv.ANSI256),
|
|
activeterm.Middleware(),
|
|
logging.Middleware(),
|
|
),
|
|
)
|
|
if err != nil {
|
|
log.Error("Could not start server", "error", err)
|
|
}
|
|
|
|
a.Server = s
|
|
return a
|
|
}
|
|
|
|
func (a *app) Start() {
|
|
var err error
|
|
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 = a.ListenAndServe(); err != nil {
|
|
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 := a.Shutdown(ctx); err != nil {
|
|
log.Error("Could not stop server", "error", err)
|
|
}
|
|
}
|
|
|
|
func (a *app) ProgramHandler(s ssh.Session) *tea.Program {
|
|
model := initialModel()
|
|
model.app = a
|
|
model.id = s.User()
|
|
|
|
p := tea.NewProgram(model, bubbletea.MakeOptions(s)...)
|
|
a.progs = append(a.progs, p)
|
|
|
|
return p
|
|
}
|
|
|
|
func main() {
|
|
app := newApp()
|
|
app.Start()
|
|
}
|
|
|
|
type (
|
|
errMsg error
|
|
chatMsg struct {
|
|
id string
|
|
text string
|
|
}
|
|
)
|
|
|
|
type model struct {
|
|
*app
|
|
viewport viewport.Model
|
|
messages []string
|
|
id string
|
|
textarea textarea.Model
|
|
senderStyle lipgloss.Style
|
|
err error
|
|
}
|
|
|
|
func initialModel() model {
|
|
ta := textarea.New()
|
|
ta.Placeholder = "Send a message..."
|
|
ta.Focus()
|
|
|
|
ta.Prompt = "┃ "
|
|
ta.CharLimit = 280
|
|
|
|
ta.SetWidth(30)
|
|
ta.SetHeight(2)
|
|
|
|
// Remove cursor line styling
|
|
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
|
|
|
|
ta.ShowLineNumbers = false
|
|
|
|
vp := viewport.New(30, 5)
|
|
vp.SetContent(`Welcome!
|
|
Type a message and press Enter to send.`)
|
|
|
|
ta.KeyMap.InsertNewline.SetEnabled(false)
|
|
|
|
return model{
|
|
textarea: ta,
|
|
messages: []string{},
|
|
viewport: vp,
|
|
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
|
|
err: nil,
|
|
}
|
|
}
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
return textarea.Blink
|
|
}
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var (
|
|
tiCmd tea.Cmd
|
|
vpCmd tea.Cmd
|
|
)
|
|
|
|
m.textarea, tiCmd = m.textarea.Update(msg)
|
|
m.viewport, vpCmd = m.viewport.Update(msg)
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.Type {
|
|
case tea.KeyCtrlC, tea.KeyEsc:
|
|
return m, tea.Quit
|
|
case tea.KeyEnter:
|
|
m.app.send(chatMsg{
|
|
id: m.id,
|
|
text: m.textarea.Value(),
|
|
})
|
|
m.textarea.Reset()
|
|
}
|
|
|
|
case chatMsg:
|
|
m.messages = append(m.messages, m.senderStyle.Render(msg.id)+": "+msg.text)
|
|
m.viewport.SetContent(strings.Join(m.messages, "\n"))
|
|
m.viewport.GotoBottom()
|
|
|
|
// We handle errors just like any other message
|
|
case errMsg:
|
|
m.err = msg
|
|
return m, nil
|
|
}
|
|
|
|
return m, tea.Batch(tiCmd, vpCmd)
|
|
}
|
|
|
|
func (m model) View() string {
|
|
return fmt.Sprintf(
|
|
"%s\n\n%s",
|
|
m.viewport.View(),
|
|
m.textarea.View(),
|
|
) + "\n\n"
|
|
}
|