All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m25s
248 lines
4.9 KiB
Go
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
|
|
}
|