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" ) 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.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() m := newModel(pty.Window.Width, pty.Window.Height) return m, []tea.ProgramOption{tea.WithAltScreen()} } type tickMsg time.Time type model struct { width int height int viewport viewport.Model buffer []int } func newModel(width, height int) model { vp := viewport.New(width, height) size := width * height return model{ width: width, height: height, viewport: vp, buffer: make([]int, size+width+1), } } 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() } // ANSI 256-color palette for fire effect (from dark to bright) var fireStyles = []lipgloss.Style{ lipgloss.NewStyle().Foreground(lipgloss.Color("0")), // black lipgloss.NewStyle().Foreground(lipgloss.Color("52")), // dark red lipgloss.NewStyle().Foreground(lipgloss.Color("88")), // dark red 2 lipgloss.NewStyle().Foreground(lipgloss.Color("124")), // red lipgloss.NewStyle().Foreground(lipgloss.Color("160")), // red 2 lipgloss.NewStyle().Foreground(lipgloss.Color("196")), // bright red lipgloss.NewStyle().Foreground(lipgloss.Color("202")), // orange-red lipgloss.NewStyle().Foreground(lipgloss.Color("208")), // orange lipgloss.NewStyle().Foreground(lipgloss.Color("214")), // orange-yellow lipgloss.NewStyle().Foreground(lipgloss.Color("220")), // yellow lipgloss.NewStyle().Foreground(lipgloss.Color("226")), // bright yellow lipgloss.NewStyle().Foreground(lipgloss.Color("227")), // light yellow lipgloss.NewStyle().Foreground(lipgloss.Color("228")), // pale yellow lipgloss.NewStyle().Foreground(lipgloss.Color("229")), // near white lipgloss.NewStyle().Foreground(lipgloss.Color("230")), // white-yellow } var fireChars = []rune{' ', '.', ':', '^', '*', 'x', 's', 'S', '#', '$'} func getFireStyle(v int) lipgloss.Style { if v <= 0 { return fireStyles[0] } else if v <= 2 { return fireStyles[1] } else if v <= 4 { return fireStyles[2] } else if v <= 6 { return fireStyles[3] } else if v <= 8 { return fireStyles[4] } else if v <= 10 { return fireStyles[5] } else if v <= 13 { return fireStyles[6] } else if v <= 16 { return fireStyles[7] } else if v <= 20 { return fireStyles[8] } else if v <= 25 { return fireStyles[9] } else if v <= 30 { return fireStyles[10] } else if v <= 40 { return fireStyles[11] } else if v <= 50 { return fireStyles[12] } else if v <= 60 { return fireStyles[13] } return fireStyles[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] } style := getFireStyle(v) 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 := lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). Width(width). Align(lipgloss.Center) output += footerStyle.Render(footer) return output }