236 lines
5.7 KiB
Go
236 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/pem"
|
|
"errors"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"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/wish/recover"
|
|
gossh "golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
type Theme struct {
|
|
Bg lipgloss.Color
|
|
Fg lipgloss.Color
|
|
Red lipgloss.Color
|
|
Green lipgloss.Color
|
|
Yellow lipgloss.Color
|
|
Blue lipgloss.Color
|
|
Purple lipgloss.Color
|
|
Aqua lipgloss.Color
|
|
Orange lipgloss.Color
|
|
Gray lipgloss.Color
|
|
}
|
|
|
|
func gruvboxDark() Theme {
|
|
return Theme{
|
|
Bg: lipgloss.Color("#282828"),
|
|
Fg: lipgloss.Color("#ebdbb2"),
|
|
Red: lipgloss.Color("#cc241d"),
|
|
Green: lipgloss.Color("#98971a"),
|
|
Yellow: lipgloss.Color("#d79921"),
|
|
Blue: lipgloss.Color("#458588"),
|
|
Purple: lipgloss.Color("#b16286"),
|
|
Aqua: lipgloss.Color("#689d6a"),
|
|
Orange: lipgloss.Color("#d65d0e"),
|
|
Gray: lipgloss.Color("#928374"),
|
|
}
|
|
}
|
|
|
|
type model struct {
|
|
showWelcome bool
|
|
focus int
|
|
showHelp bool
|
|
theme Theme
|
|
blink bool
|
|
blinkCount int
|
|
}
|
|
|
|
const numBoxes = 4
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
return tea.Tick(time.Millisecond*70, func(t time.Time) tea.Msg {
|
|
return tickMsg(t)
|
|
})
|
|
}
|
|
|
|
type tickMsg time.Time
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tickMsg:
|
|
m.blinkCount++
|
|
m.blink = (m.blinkCount / 3) % 2 == 0
|
|
return m, tea.Tick(time.Millisecond*70, func(t time.Time) tea.Msg {
|
|
return tickMsg(t)
|
|
})
|
|
case tea.KeyMsg:
|
|
if m.showWelcome {
|
|
m.showWelcome = false
|
|
return m, nil
|
|
}
|
|
if m.showHelp {
|
|
switch msg.String() {
|
|
case "q", "esc", "?", "enter", "backspace":
|
|
m.showHelp = false
|
|
}
|
|
return m, nil
|
|
}
|
|
switch msg.String() {
|
|
case "h", "left":
|
|
m.focus = (m.focus + 3) % numBoxes
|
|
case "l", "right":
|
|
m.focus = (m.focus + 1) % numBoxes
|
|
case "j", "down":
|
|
if m.focus < 2 {
|
|
m.focus += 2
|
|
} else {
|
|
m.focus -= 2
|
|
}
|
|
case "k", "up":
|
|
if m.focus >= 2 {
|
|
m.focus -= 2
|
|
} else {
|
|
m.focus += 2
|
|
}
|
|
case "?":
|
|
m.showHelp = true
|
|
case "q", "esc":
|
|
return m, tea.Quit
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) View() string {
|
|
if m.showWelcome {
|
|
fg := lipgloss.NewStyle().Foreground(m.theme.Fg).Background(m.theme.Bg)
|
|
accentFg := lipgloss.NewStyle().Foreground(m.theme.Blue).Background(m.theme.Bg)
|
|
if m.blink {
|
|
accentFg = lipgloss.NewStyle().Foreground(m.theme.Bg).Background(m.theme.Bg)
|
|
}
|
|
welcome := fg.Render("Welcome to ") +
|
|
lipgloss.NewStyle().Foreground(m.theme.Orange).Background(m.theme.Bg).Render("dcorral.com") +
|
|
fg.Render("!") + "\n" +
|
|
fg.Render("press ") + accentFg.Render("any key") + fg.Render(" to continue")
|
|
return welcome
|
|
}
|
|
if m.showHelp {
|
|
return lipgloss.NewStyle().Foreground(m.theme.Fg).Background(m.theme.Bg).Render(
|
|
"Navigation:\n" +
|
|
"h / ←: Move left\n" +
|
|
"l / →: Move right\n" +
|
|
"j / ↓: Move down\n" +
|
|
"k / ↑: Move up\n" +
|
|
"? : Show help\n" +
|
|
"q / Esc: Quit",
|
|
)
|
|
}
|
|
// Render the boxes
|
|
boxes := make([]string, numBoxes)
|
|
for i := range boxes {
|
|
var style lipgloss.Style
|
|
if i == m.focus {
|
|
style = lipgloss.NewStyle().Background(m.theme.Blue).Foreground(m.theme.Bg).Border(lipgloss.NormalBorder()).Padding(0, 1)
|
|
} else {
|
|
style = lipgloss.NewStyle().Background(m.theme.Bg).Foreground(m.theme.Fg).Border(lipgloss.NormalBorder()).Padding(0, 1)
|
|
}
|
|
boxes[i] = style.Render("Box " + string(rune('0'+i)))
|
|
}
|
|
grid := lipgloss.JoinHorizontal(lipgloss.Top, boxes[0], " ", boxes[1]) + "\n" +
|
|
lipgloss.JoinHorizontal(lipgloss.Top, boxes[2], " ", boxes[3]) + "\n" +
|
|
lipgloss.NewStyle().Foreground(m.theme.Fg).Background(m.theme.Bg).Render("Use hjkl or arrows to navigate, ? for help, q to quit")
|
|
return grid
|
|
}
|
|
|
|
func main() {
|
|
// Generate host key
|
|
_, key, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
log.Error("Failed to generate host key", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
block, err := gossh.MarshalPrivateKey(key, "")
|
|
if err != nil {
|
|
log.Error("Failed to marshal host key", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
privateKeyBytes := pem.EncodeToMemory(block)
|
|
log.Info("Generated host key")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
signalChan := make(chan os.Signal, 1)
|
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
<-signalChan
|
|
cancel()
|
|
}()
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "22"
|
|
}
|
|
|
|
s, err := wish.NewServer(
|
|
wish.WithAddress(net.JoinHostPort("0.0.0.0", port)),
|
|
wish.WithHostKeyPEM(privateKeyBytes),
|
|
wish.WithMiddleware(
|
|
recover.Middleware(
|
|
bubbletea.Middleware(teaHandler),
|
|
activeterm.Middleware(),
|
|
logging.Middleware(),
|
|
),
|
|
),
|
|
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
|
return true
|
|
}),
|
|
wish.WithKeyboardInteractiveAuth(
|
|
func(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) bool {
|
|
return true
|
|
},
|
|
),
|
|
)
|
|
if err != nil {
|
|
log.Error("Could not start server", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
log.Info("Starting SSH server", "port", port)
|
|
go func() {
|
|
if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
|
|
log.Error("Could not start server", "error", err)
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
s.Shutdown(ctx)
|
|
slog.Info("Shutting down server")
|
|
}
|
|
|
|
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
|
return model{
|
|
showWelcome: true,
|
|
focus: 0,
|
|
showHelp: false,
|
|
theme: gruvboxDark(),
|
|
blink: false,
|
|
blinkCount: 0,
|
|
}, []tea.ProgramOption{tea.WithAltScreen()}
|
|
} |