373 lines
8.8 KiB
Go
373 lines
8.8 KiB
Go
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 I’ve 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(
|
||
wish.WithAddress("0.0.0.0:2222"),
|
||
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(),
|
||
}
|
||
}
|