added snake and added projects section, still need a bit of work
This commit is contained in:
parent
a7f1bbdd19
commit
e7af8511d1
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
||||
snake.go
|
||||
connections.log
|
||||
|
||||
166
main.go
166
main.go
@ -2,17 +2,40 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"log"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/ssh"
|
||||
"github.com/charmbracelet/wish"
|
||||
wishtea "github.com/charmbracelet/wish/bubbletea"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type project struct {
|
||||
name string
|
||||
description string
|
||||
tech string
|
||||
url string
|
||||
}
|
||||
|
||||
var projects = []project{
|
||||
{
|
||||
name: "SSH-portfolio",
|
||||
description: "This very terminal — a Bubbletea + Wish SSH app.",
|
||||
tech: "Go · Bubbletea · Lipgloss · Wish",
|
||||
url: "https://git.ksan.dev/ksan/ssh-portfolio",
|
||||
},
|
||||
{
|
||||
name: "E-Voting",
|
||||
description: "A secure e-voting application for creating and participating in elections. It uses certificates, encryption, and digital signatures to protect identity, privacy, and data integrity.",
|
||||
tech: "Java · BoucyCastle · JavaFx · Gradle",
|
||||
url: "https://git.ksan.dev/ksan/e-voting",
|
||||
},
|
||||
}
|
||||
|
||||
const giteaLink = "https://git.ksan.dev/ksan"
|
||||
|
||||
var asciiArt = []string{}
|
||||
|
||||
type model struct {
|
||||
@ -26,12 +49,14 @@ type model struct {
|
||||
mouseY int
|
||||
mouseActive bool
|
||||
isClicked bool
|
||||
snake SnakeModel
|
||||
projectCursor int
|
||||
}
|
||||
|
||||
func initialModel() model {
|
||||
return model{
|
||||
title: "Đorđe Kšan",
|
||||
sections: []string{"About", "Projects", "Contact"},
|
||||
sections: []string{"About", "Projects", "Contact", "Surprise"},
|
||||
currentScreen: "menu",
|
||||
mouseActive: true,
|
||||
}
|
||||
@ -42,6 +67,24 @@ func (m model) Init() tea.Cmd {
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
if m.currentScreen == "Surprise" {
|
||||
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
switch key.String() {
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "b", "backspace":
|
||||
m.currentScreen = "menu"
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.snake, cmd = m.snake.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
@ -57,18 +100,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Quit
|
||||
|
||||
case "up", "k":
|
||||
if m.currentScreen == "Projects" {
|
||||
if m.projectCursor > 0 {
|
||||
m.projectCursor--
|
||||
}
|
||||
} else if m.currentScreen == "menu" {
|
||||
if m.selected > 0 {
|
||||
m.selected--
|
||||
fmt.Println("up")
|
||||
}
|
||||
}
|
||||
case "down", "j":
|
||||
if m.currentScreen == "Projects" {
|
||||
if m.projectCursor < len(projects)-1 {
|
||||
m.projectCursor++
|
||||
}
|
||||
} else if m.currentScreen == "menu" {
|
||||
if m.selected < len(m.sections)-1 {
|
||||
m.selected++
|
||||
fmt.Println("down")
|
||||
}
|
||||
}
|
||||
case "enter":
|
||||
m.currentScreen = m.sections[m.selected]
|
||||
|
||||
if m.currentScreen == "menu" {
|
||||
chosen := m.sections[m.selected]
|
||||
m.currentScreen = chosen
|
||||
if chosen == "Surprise" {
|
||||
m.snake = newSnakeModel()
|
||||
return m, m.snake.Init()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "left", "h":
|
||||
@ -96,7 +156,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) center(content string) string {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
return content
|
||||
}
|
||||
return lipgloss.Place(m.width, m.height,
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
|
||||
if m.currentScreen == "Surprise" {
|
||||
return m.center(m.snake.View())
|
||||
}
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#E2E8F0")).
|
||||
@ -129,14 +204,15 @@ func (m model) View() string {
|
||||
}
|
||||
}
|
||||
|
||||
output += "\n" + footerStyle.Render("Press q to quit.") + "\n"
|
||||
output += "\n" + footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n"
|
||||
|
||||
} else {
|
||||
switch m.currentScreen {
|
||||
case "About":
|
||||
output = m.renderAbout()
|
||||
|
||||
case "Projects":
|
||||
output = m.renderProjects()
|
||||
output = m.center(m.renderProjects())
|
||||
case "Contact":
|
||||
output = m.renderContact()
|
||||
|
||||
@ -158,7 +234,7 @@ var aboutText = `
|
||||
`
|
||||
|
||||
var contactText = `
|
||||
Get in touch with me at djordje@ksan.dev.
|
||||
Get in touch with me at djordje@ksan.dev
|
||||
`
|
||||
|
||||
var projectsText = `
|
||||
@ -193,23 +269,56 @@ func (m model) renderAbout() string {
|
||||
str.WriteString("\n")
|
||||
str.WriteString(contentStyle.Render(aboutText))
|
||||
str.WriteString("\n")
|
||||
str.WriteString(footerStyle.Render("esc: back to menu"))
|
||||
|
||||
str.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
|
||||
|
||||
return str.String()
|
||||
|
||||
}
|
||||
|
||||
func link(url, text string) string {
|
||||
return "\033]8;;" + url + "\033\\" + text + "\033]8;;\033\\"
|
||||
}
|
||||
|
||||
func (m model) renderProjects() string {
|
||||
|
||||
var str strings.Builder
|
||||
output := titleStyle.Render("━━━ Projects ━━━") + "\n\n"
|
||||
output += sectionStyle.Render("Projects I worked on:") + "\n\n"
|
||||
|
||||
str.WriteString(titleStyle.Render("━━━ Projects ━━━"))
|
||||
str.WriteString("\n")
|
||||
str.WriteString(contentStyle.Render(projectsText))
|
||||
str.WriteString("\n")
|
||||
str.WriteString(footerStyle.Render("esc: back to menu"))
|
||||
cursorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8A1D7D")).Bold(true)
|
||||
expandedNameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#E2E8F0")).Bold(true)
|
||||
collapsedNameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#94A3B8"))
|
||||
descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#CBD5E1")).PaddingLeft(4).Width(55).Align(lipgloss.Center)
|
||||
techStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8A1D7D")).PaddingLeft(4)
|
||||
dividerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#1E293B"))
|
||||
urlStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#38BDF8")).Underline(true).PaddingLeft(4).Align(lipgloss.Center)
|
||||
urlStyle2 := lipgloss.NewStyle().Foreground(lipgloss.Color("#38BDF8")).Underline(true).Align(lipgloss.Center).MaxWidth(55)
|
||||
|
||||
return str.String()
|
||||
for i, p := range projects {
|
||||
if i == m.projectCursor {
|
||||
output += cursorStyle.Render("▶ ") + expandedNameStyle.Render(link(p.url, p.name)) + "\n"
|
||||
output += descStyle.Render(p.description) + "\n"
|
||||
output += techStyle.Render("[ "+p.tech+" ]") + "\n"
|
||||
|
||||
output += link(p.url, urlStyle.Render("→ Check it out here ←")) + "\n"
|
||||
output += urlStyle2.Render(p.url) + "\n"
|
||||
} else {
|
||||
output += collapsedNameStyle.Render(" ▸ "+p.name) + "\n"
|
||||
}
|
||||
if i < len(projects)-1 {
|
||||
output += dividerStyle.Render(" "+"─────────────────────────────") + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
gitStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#38BDF8")).
|
||||
Underline(true).
|
||||
MarginTop(1)
|
||||
output += "\n" + sectionStyle.Render("More on: ") + gitStyle.Render(giteaLink) + "\n"
|
||||
|
||||
output += "\n" + footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit")
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (m model) renderContact() string {
|
||||
@ -219,12 +328,13 @@ func (m model) renderContact() string {
|
||||
str.WriteString("\n")
|
||||
str.WriteString(contentStyle.Render(contactText))
|
||||
str.WriteString("\n")
|
||||
str.WriteString(footerStyle.Render("esc: back to menu"))
|
||||
str.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
|
||||
|
||||
return str.String()
|
||||
}
|
||||
|
||||
func main() {
|
||||
initLogger()
|
||||
|
||||
server, _ := wish.NewServer(
|
||||
wish.WithAddress("0.0.0.0:2222"),
|
||||
@ -235,13 +345,25 @@ func main() {
|
||||
_ = server.ListenAndServe()
|
||||
}
|
||||
|
||||
func initLogger() {
|
||||
f, err := os.OpenFile("connections.log",
|
||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
|
||||
0644,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.SetOutput(f)
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
}
|
||||
|
||||
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
|
||||
ip := s.RemoteAddr().String()
|
||||
user := s.User()
|
||||
|
||||
println("New connection:")
|
||||
log.Println("User:", user, "IP:", ip)
|
||||
log.Printf("New connection | user=%s ip=%s", user, ip)
|
||||
|
||||
return initialModel(), []tea.ProgramOption{
|
||||
tea.WithAltScreen(),
|
||||
|
||||
222
snake.go
Normal file
222
snake.go
Normal file
@ -0,0 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const (
|
||||
gridWidth = 40
|
||||
gridHeight = 20
|
||||
)
|
||||
|
||||
type point struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
type snakeDirection int
|
||||
|
||||
const (
|
||||
dirUp snakeDirection = iota
|
||||
dirDown
|
||||
dirLeft
|
||||
dirRight
|
||||
)
|
||||
|
||||
type snakeTickMsg time.Time
|
||||
|
||||
type SnakeModel struct {
|
||||
snake []point
|
||||
dir snakeDirection
|
||||
nextDir snakeDirection
|
||||
food point
|
||||
score int
|
||||
gameOver bool
|
||||
rng *rand.Rand
|
||||
}
|
||||
|
||||
func newSnakeModel() SnakeModel {
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
snake := []point{
|
||||
{gridWidth/2 + 1, gridHeight / 2},
|
||||
{gridWidth / 2, gridHeight / 2},
|
||||
{gridWidth/2 - 1, gridHeight / 2},
|
||||
}
|
||||
m := SnakeModel{
|
||||
snake: snake,
|
||||
dir: dirRight,
|
||||
nextDir: dirRight,
|
||||
rng: rng,
|
||||
}
|
||||
m.food = m.spawnFood()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m SnakeModel) spawnFood() point {
|
||||
occupied := make(map[point]bool, len(m.snake))
|
||||
for _, s := range m.snake {
|
||||
occupied[s] = true
|
||||
}
|
||||
for {
|
||||
f := point{m.rng.Intn(gridWidth), m.rng.Intn(gridHeight)}
|
||||
if !occupied[f] {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func snakeTickCmd() tea.Cmd {
|
||||
return tea.Tick(130*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return snakeTickMsg(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (m SnakeModel) Init() tea.Cmd {
|
||||
return snakeTickCmd()
|
||||
}
|
||||
|
||||
func (m SnakeModel) Update(msg tea.Msg) (SnakeModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.dir != dirDown {
|
||||
m.nextDir = dirUp
|
||||
}
|
||||
case "down", "j":
|
||||
if m.dir != dirUp {
|
||||
m.nextDir = dirDown
|
||||
}
|
||||
case "left", "h":
|
||||
if m.dir != dirRight {
|
||||
m.nextDir = dirLeft
|
||||
}
|
||||
case "right", "l":
|
||||
if m.dir != dirLeft {
|
||||
m.nextDir = dirRight
|
||||
}
|
||||
case "r":
|
||||
if m.gameOver {
|
||||
fresh := newSnakeModel()
|
||||
return fresh, fresh.Init()
|
||||
}
|
||||
}
|
||||
|
||||
case snakeTickMsg:
|
||||
if m.gameOver {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.dir = m.nextDir
|
||||
head := m.snake[0]
|
||||
|
||||
var next point
|
||||
switch m.dir {
|
||||
case dirUp:
|
||||
next = point{head.x, head.y - 1}
|
||||
case dirDown:
|
||||
next = point{head.x, head.y + 1}
|
||||
case dirLeft:
|
||||
next = point{head.x - 1, head.y}
|
||||
case dirRight:
|
||||
next = point{head.x + 1, head.y}
|
||||
}
|
||||
|
||||
if next.x < 0 || next.x >= gridWidth || next.y < 0 || next.y >= gridHeight {
|
||||
m.gameOver = true
|
||||
return m, nil
|
||||
}
|
||||
|
||||
for _, s := range m.snake {
|
||||
if s == next {
|
||||
m.gameOver = true
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if next == m.food {
|
||||
m.snake = append([]point{next}, m.snake...)
|
||||
m.score++
|
||||
m.food = m.spawnFood()
|
||||
} else {
|
||||
m.snake = append([]point{next}, m.snake[:len(m.snake)-1]...)
|
||||
}
|
||||
|
||||
return m, snakeTickCmd()
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
//TODO test if the characters are rendered on different terminals
|
||||
func (m SnakeModel) View() string {
|
||||
|
||||
snakeIndex := make(map[point]int, len(m.snake))
|
||||
for i, s := range m.snake {
|
||||
snakeIndex[s] = i
|
||||
}
|
||||
|
||||
//TODO move to top
|
||||
borderStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8A1D7D"))
|
||||
headStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#4ADE80")).Bold(true)
|
||||
bodyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#16A34A"))
|
||||
foodStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FB923C"))
|
||||
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#E2E8F0"))
|
||||
scoreStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#94A3B8"))
|
||||
footerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#64748B"))
|
||||
gameOverStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#EF4444"))
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(
|
||||
titleStyle.Render(" SNAKE") +
|
||||
" " +
|
||||
scoreStyle.Render(fmt.Sprintf("Score: %d", m.score)) +
|
||||
"\n",
|
||||
)
|
||||
|
||||
sb.WriteString(borderStyle.Render("┌"+strings.Repeat("─", gridWidth)+"┐") + "\n")
|
||||
|
||||
for y := 0; y < gridHeight; y++ {
|
||||
sb.WriteString(borderStyle.Render("│"))
|
||||
for x := 0; x < gridWidth; x++ {
|
||||
p := point{x, y}
|
||||
if idx, isSnake := snakeIndex[p]; isSnake {
|
||||
if idx == 0 {
|
||||
sb.WriteString(headStyle.Render("■"))
|
||||
} else {
|
||||
sb.WriteString(bodyStyle.Render("□"))
|
||||
}
|
||||
} else if p == m.food {
|
||||
sb.WriteString(foodStyle.Render("●"))
|
||||
} else {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
sb.WriteString(borderStyle.Render("│") + "\n")
|
||||
}
|
||||
|
||||
sb.WriteString(borderStyle.Render("└"+strings.Repeat("─", gridWidth)+"┘") + "\n")
|
||||
|
||||
if m.gameOver {
|
||||
sb.WriteString(
|
||||
gameOverStyle.Render(" GAME OVER") +
|
||||
" " +
|
||||
scoreStyle.Render(fmt.Sprintf("Final score: %d", m.score)) +
|
||||
"\n",
|
||||
)
|
||||
sb.WriteString(footerStyle.Render(" r to restart · b to go back · q to quit") + "\n")
|
||||
} else {
|
||||
sb.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user