2026-04-14 13:32:17 +02:00

374 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
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 {
title string
sections []string
selected int
currentScreen string
width int
height int
mouseX int
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", "Surprise"},
currentScreen: "menu",
mouseActive: true,
}
}
func (m model) Init() tea.Cmd {
return nil
}
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
m.height = msg.Height
if m.mouseX < 0 || m.mouseX > m.width || m.mouseY < 0 || m.mouseY > m.height {
m.mouseActive = false
}
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
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--
}
}
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++
}
}
case "enter":
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":
case "right", "l":
case "b", "u", "backspace", "esc":
m.currentScreen = "menu"
return m, nil
default:
fmt.Println("click")
}
//TODO add mouse logic
case tea.MouseMsg:
m.mouseX = msg.X
m.mouseY = msg.Y
m.mouseActive = m.mouseX >= 0 && m.mouseX < m.width && m.mouseY >= 0 && m.mouseY < m.height
if msg.Button == tea.MouseButtonLeft && msg.Type == tea.MouseEventType(tea.MouseActionRelease) {
fmt.Println("mouse click")
}
return m, nil
}
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")).
Background(lipgloss.Color("#0F172A")).
Padding(0, 3)
sectionStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#94A3B8"))
selectedSectionStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#8A1D7D")).
Bold(true)
footerStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#64748B"))
var output string
if m.currentScreen == "menu" {
output = titleStyle.Render(m.title) + "\n\n"
output += "Sections:\n"
for i, section := range m.sections {
if i == m.selected {
output += selectedSectionStyle.Render("-> "+section) + "\n"
} else {
output += sectionStyle.Render("- "+section) + "\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.center(m.renderProjects())
case "Contact":
output = m.renderContact()
}
}
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
output,
)
}
var aboutText = `
Tbh I don't even know anymore. <3
`
var contactText = `
Get in touch with me at djordje@ksan.dev
`
var projectsText = `
Here are some projects Ive worked on...
`
var (
titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#E2E8F0")).
Background(lipgloss.Color("#0F172A")).
Padding(0, 3)
sectionStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#94A3B8"))
selectedSectionStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#8A1D7D")).
Bold(true)
footerStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#64748B"))
contentStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF"))
)
func (m model) renderAbout() string {
var str strings.Builder
str.WriteString(titleStyle.Render("━━━ About Me ━━━"))
str.WriteString("\n")
str.WriteString(contentStyle.Render(aboutText))
str.WriteString("\n")
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 {
output := titleStyle.Render("━━━ Projects ━━━") + "\n\n"
output += sectionStyle.Render("Projects I worked on:") + "\n\n"
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)
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 {
var str strings.Builder
str.WriteString(titleStyle.Render("━━━ Contact ━━━"))
str.WriteString("\n")
str.WriteString(contentStyle.Render(contactText))
str.WriteString("\n")
str.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
return str.String()
}
func main() {
initLogger()
server, _ := wish.NewServer(
//TODO change port later cant currently will break ssh connection to remote server
wish.WithAddress("0.0.0.0:25565"),
wish.WithHostKeyPath(".ssh/host_ed25519"),
wish.WithMiddleware(wishtea.Middleware(teaHandler)),
)
_ = 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()
log.Printf("New connection | user=%s ip=%s", user, ip)
return initialModel(), []tea.ProgramOption{
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
}
}