This commit is contained in:
258
main.go
Normal file
258
main.go
Normal file
@@ -0,0 +1,258 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user