Skip to content

Commit 4b4b9e0

Browse files
committed
🧹 fix rendering of cat
1 parent 3c10f25 commit 4b4b9e0

File tree

2 files changed

+155
-61
lines changed

2 files changed

+155
-61
lines changed

cli/shell/model.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ type shellModel struct {
8686
executing bool
8787
spinner spinner.Model
8888
compileError string // Current compile error (if any)
89+
90+
// Nyanya animation (easter egg)
91+
nyanya *nyanyaState
8992
}
9093

9194
// newShellModel creates a new shell model
@@ -252,6 +255,23 @@ func (m *shellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
252255
m.spinner, cmd = m.spinner.Update(msg)
253256
return m, cmd
254257

258+
case nyanyaTickMsg:
259+
// Advance the nyanya animation
260+
if m.nyanya != nil {
261+
m.nyanya.currentFrame++
262+
if m.nyanya.currentFrame >= len(m.nyanya.frames) {
263+
m.nyanya.currentFrame = 0
264+
m.nyanya.loopCount++
265+
if m.nyanya.loopCount >= m.nyanya.maxLoops {
266+
// Animation complete
267+
m.nyanya = nil
268+
return m, nil
269+
}
270+
}
271+
return m, nyanyaTick()
272+
}
273+
return m, nil
274+
255275
case printOutputMsg:
256276
// Print output directly to terminal
257277
return m, tea.Println(msg.output)
@@ -265,6 +285,12 @@ func (m *shellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
265285

266286
// handleKeyMsg processes keyboard input
267287
func (m *shellModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
288+
// Handle nyanya animation - any key exits
289+
if m.nyanya != nil {
290+
m.nyanya = nil
291+
return m, nil
292+
}
293+
268294
// Handle history search mode (ctrl+r)
269295
if m.searchMode {
270296
return m.handleSearchKey(msg)
@@ -424,13 +450,14 @@ func (m *shellModel) handleSubmit() (tea.Model, tea.Cmd) {
424450
)
425451
case "nyanya":
426452
m.input.SetValue("")
427-
// Run the nyancat animation
428-
return m, tea.Sequence(
453+
// Initialize and start the nyancat animation
454+
m.nyanya = initNyanya()
455+
if m.nyanya == nil {
456+
return m, tea.Println(m.theme.ErrorText("Failed to initialize nyanya animation"))
457+
}
458+
return m, tea.Batch(
429459
tea.Println(echoInput),
430-
func() tea.Msg {
431-
nyago(m.width, m.height)
432-
return nil
433-
},
460+
nyanyaTick(),
434461
)
435462
}
436463

@@ -731,6 +758,11 @@ func (m *shellModel) View() string {
731758
return "Loading..."
732759
}
733760

761+
// Render nyanya animation if active (full screen modal)
762+
if m.nyanya != nil {
763+
return renderNyanya(m.nyanya, m.width, m.height)
764+
}
765+
734766
var b strings.Builder
735767

736768
// Show spinner when executing, otherwise show input

cli/shell/nyago.go

Lines changed: 117 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,51 @@ import (
1313
"strings"
1414
"time"
1515

16+
tea "github.com/charmbracelet/bubbletea"
17+
"github.com/charmbracelet/lipgloss"
1618
"github.com/pierrec/lz4/v4"
1719
)
1820

19-
func nyago(width, height int) {
21+
// nyanyaState holds the animation state
22+
type nyanyaState struct {
23+
frames [][]string
24+
currentFrame int
25+
loopCount int
26+
maxLoops int
27+
}
28+
29+
// nyanyaTickMsg is sent when it's time to advance the frame
30+
type nyanyaTickMsg time.Time
31+
32+
// nyanyaColors maps characters to 256-color codes
33+
var nyanyaColors = map[string]string{
34+
"'": "0", // outline
35+
".": "15", // white
36+
",": "234", // bg
37+
">": "198", // lightred (rainbow 1)
38+
"&": "211", // lightorange (rainbow 2)
39+
"+": "222", // lightyellow (rainbow 3)
40+
"#": "86", // lightgreen (rainbow 4)
41+
"=": "45", // lightblue (rainbow 5)
42+
";": "32", // lightpurple (rainbow 6)
43+
"@": "224", // outer body
44+
"$": "217", // inner body
45+
"-": "204", // dots on the cat
46+
"%": "210", // cheeks
47+
"*": "248", // grey
48+
}
49+
50+
// initNyanya initializes the nyanya animation state
51+
func initNyanya() *nyanyaState {
2052
cdec, err := base64.StdEncoding.DecodeString(c)
2153
if err != nil {
22-
return
54+
return nil
2355
}
2456

2557
reader := lz4.NewReader(bytes.NewReader(cdec))
2658
all := make([]byte, 50000)
2759
if _, err := reader.Read(all); err != nil && err != io.EOF {
28-
return
60+
return nil
2961
}
3062

3163
framesRaw := strings.Split(string(all), "z")
@@ -34,71 +66,101 @@ func nyago(width, height int) {
3466
frames[i] = strings.Split(framesRaw[i], "\n")
3567
}
3668

37-
fmt.Printf("%+v\n", frames)
38-
39-
stop := make(chan struct{}, 1)
40-
captureSIGINTonce(stop)
41-
42-
colors := map[string]string{
43-
"'": "0", // outline
44-
".": "15", // white
45-
",": "234", // bg
46-
">": "198", // lightred (rainbow 1)
47-
"&": "211", // lightorange (rainbow 2)
48-
"+": "222", // lightyellow (rainbow 3)
49-
"#": "86", // lightgreen (rainbow 4)
50-
"=": "45", // lightblue (rainbow 5)
51-
";": "32", // lightpurple (rainbow 6)
52-
"@": "224", // outer body
53-
"$": "217", // inner body
54-
"-": "204", // dots on the cat
55-
"%": "210", // cheeks
56-
"*": "248", // grey
69+
return &nyanyaState{
70+
frames: frames,
71+
maxLoops: 3,
72+
}
73+
}
74+
75+
// nyanyaTick returns a command that ticks the animation
76+
func nyanyaTick() tea.Cmd {
77+
return tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg {
78+
return nyanyaTickMsg(t)
79+
})
80+
}
81+
82+
// renderNyanya renders the current frame centered on screen
83+
func renderNyanya(state *nyanyaState, width, height int) string {
84+
if state == nil || len(state.frames) == 0 {
85+
return ""
5786
}
5887

59-
fmt.Print("\033[H\033[2J\033[?25l")
60-
const outputChar = " "
88+
frame := state.frames[state.currentFrame]
89+
90+
// Build the frame with colors, filtering empty lines
91+
var frameLines []string
92+
for _, line := range frame {
93+
if len(line) == 0 {
94+
continue // Skip empty lines
95+
}
96+
var lineBuilder strings.Builder
97+
for _, char := range line {
98+
colorCode := nyanyaColors[string(char)]
99+
if colorCode == "" {
100+
colorCode = "234" // default bg
101+
}
102+
// Use ANSI 256-color background
103+
lineBuilder.WriteString(fmt.Sprintf("\033[48;5;%sm \033[0m", colorCode))
104+
}
105+
frameLines = append(frameLines, lineBuilder.String())
106+
}
61107

62-
y0 := 0
63-
y1 := len(frames[0])
108+
frameHeight := len(frameLines)
109+
frameWidth := 0
110+
if frameHeight > 0 && len(frame) > 0 {
111+
// Find the first non-empty line to calculate width
112+
for _, line := range frame {
113+
if len(line) > 0 {
114+
frameWidth = len(line) * 2 // Each char becomes 2 spaces wide
115+
break
116+
}
117+
}
118+
}
64119

65-
x0 := 0
66-
x1 := len(frames[0][0])
120+
// Add instruction at bottom
121+
instruction := lipgloss.NewStyle().
122+
Foreground(lipgloss.Color("241")).
123+
Render("Press any key to exit")
67124

68-
if y1 > height {
69-
y0 = (y1 - height) / 2
70-
y1 = y0 + height
125+
// Calculate vertical positioning
126+
topPadding := (height - frameHeight - 2) / 2
127+
if topPadding < 0 {
128+
topPadding = 0
71129
}
72130

73-
if x1 > width {
74-
x0 = (x1 - width) / 2
75-
x1 = x0 + width
131+
// Center each frame line horizontally
132+
leftPadding := (width - frameWidth) / 2
133+
if leftPadding < 0 {
134+
leftPadding = 0
76135
}
136+
padding := strings.Repeat(" ", leftPadding)
77137

78-
ticker := time.NewTicker(90 * time.Millisecond)
79-
defer func() { ticker.Stop() }()
80-
81-
for i := 0; i < 3; i++ {
82-
for _, frame := range frames {
83-
// Print the next frame
84-
for _, line := range frame[y0:y1] {
85-
for _, char := range line[x0:x1] {
86-
fmt.Printf("\033[48;5;%sm%s", colors[string(char)], outputChar)
87-
}
88-
fmt.Println("\033[m")
89-
}
138+
var result strings.Builder
90139

91-
// Reset the frame and sleep
92-
fmt.Print("\033[H")
93-
time.Sleep(90 * time.Millisecond)
140+
// Add top padding (empty lines)
141+
for i := 0; i < topPadding; i++ {
142+
result.WriteString("\n")
143+
}
94144

95-
select {
96-
case <-stop:
97-
return
98-
case <-ticker.C:
99-
}
145+
// Add frame lines (no extra newline after the last one)
146+
for i, line := range frameLines {
147+
result.WriteString(padding + line)
148+
if i < len(frameLines)-1 {
149+
result.WriteString("\n")
100150
}
101151
}
152+
153+
// Add spacing before instruction
154+
result.WriteString("\n\n")
155+
156+
// Add centered instruction
157+
instructionPadding := (width - lipgloss.Width(instruction)) / 2
158+
if instructionPadding < 0 {
159+
instructionPadding = 0
160+
}
161+
result.WriteString(strings.Repeat(" ", instructionPadding) + instruction)
162+
163+
return result.String()
102164
}
103165

104166
const c = "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"

0 commit comments

Comments
 (0)