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( //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(), } }