Files
yule/main.go
Atridad Lahiji 975c61cce3
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m25s
???
2025-12-23 11:07:07 -07:00

248 lines
4.9 KiB
Go

package main
import (
"context"
"errors"
"log"
"math/rand"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"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 = "2222"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
s, err := wish.NewServer(
wish.WithAddress(net.JoinHostPort(host, port)),
wish.WithHostKeyPath(".ssh/id_ed25519"),
wish.WithMiddleware(
bubbletea.Middleware(teaHandler),
activeterm.Middleware(),
logging.Middleware(),
),
)
if err != nil {
log.Fatalln(err)
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
log.Printf("🔥 Yule Log SSH Server starting on %s:%s", host, port)
log.Printf("Connect with: ssh localhost -p %s", port)
go func() {
if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
log.Fatalln(err)
}
}()
<-done
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Fatalln(err)
}
}
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
pty, _, _ := s.Pty()
renderer := bubbletea.MakeRenderer(s)
renderer.SetColorProfile(termenv.ANSI256)
m := newModel(pty.Window.Width, pty.Window.Height, renderer)
return m, []tea.ProgramOption{tea.WithAltScreen()}
}
type tickMsg time.Time
type model struct {
width int
height int
viewport viewport.Model
buffer []int
renderer *lipgloss.Renderer
}
func newModel(width, height int, renderer *lipgloss.Renderer) model {
vp := viewport.New(width, height)
size := width * height
return model{
width: width,
height: height,
viewport: vp,
buffer: make([]int, size+width+1),
renderer: renderer,
}
}
func (m model) Init() tea.Cmd {
return tick()
}
func tick() tea.Cmd {
return tea.Tick(30*time.Millisecond, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return m, tea.Quit
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height
size := msg.Width * msg.Height
m.buffer = make([]int, size+msg.Width+1)
case tickMsg:
m.updateFire()
return m, tick()
}
return m, nil
}
func (m *model) updateFire() {
width := m.width
height := m.height - 1
if width <= 0 || height <= 0 {
return
}
size := width * height
if len(m.buffer) < size+width+1 {
m.buffer = make([]int, size+width+1)
}
// Inject heat on bottom row
for i := 0; i < width/9; i++ {
idx := rand.Intn(width) + width*(height-1)
if idx >= 0 && idx < len(m.buffer) {
m.buffer[idx] = 65
}
}
// Propagate and cool
for i := 0; i < size; i++ {
if i+width+1 >= len(m.buffer) {
continue
}
b0 := m.buffer[i]
b1 := m.buffer[i+1]
b2 := m.buffer[i+width]
b3 := m.buffer[i+width+1]
v := (b0 + b1 + b2 + b3) / 4
m.buffer[i] = v
}
}
func (m model) View() string {
return m.renderFire()
}
var fireColors = []string{"0", "52", "88", "124", "160", "196", "202", "208", "214", "220", "226", "227", "228", "229", "230"}
var fireChars = []rune{' ', '.', ':', '^', '*', 'x', 's', 'S', '#', '$'}
func getFireColorIndex(v int) int {
if v <= 0 {
return 0
} else if v <= 2 {
return 1
} else if v <= 4 {
return 2
} else if v <= 6 {
return 3
} else if v <= 8 {
return 4
} else if v <= 10 {
return 5
} else if v <= 13 {
return 6
} else if v <= 16 {
return 7
} else if v <= 20 {
return 8
} else if v <= 25 {
return 9
} else if v <= 30 {
return 10
} else if v <= 40 {
return 11
} else if v <= 50 {
return 12
} else if v <= 60 {
return 13
}
return 14
}
func (m model) renderFire() string {
width := m.width
height := m.height - 1
if width < 10 || height < 5 {
return "Terminal too small"
}
var output string
for row := 0; row < height; row++ {
line := ""
for col := 0; col < width; col++ {
i := col + row*width
v := 0
if i < len(m.buffer) {
v = m.buffer[i]
}
colorIdx := getFireColorIndex(v)
style := m.renderer.NewStyle().Foreground(lipgloss.Color(fireColors[colorIdx]))
chIdx := v
if chIdx > 9 {
chIdx = 9
}
if chIdx < 0 {
chIdx = 0
}
line += style.Render(string(fireChars[chIdx]))
}
output += line + "\n"
}
footer := "Press 'q' to exit"
footerStyle := m.renderer.NewStyle().
Foreground(lipgloss.Color("240")).
Width(width).
Align(lipgloss.Center)
output += footerStyle.Render(footer)
return output
}