prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

content.go (6532B)


      1 package tui
      2 
      3 import (
      4 	"fmt"
      5 	"strings"
      6 	"time"
      7 
      8 	appconfig "git.prime8.dev/moncheeta/prime8dev/internal/config"
      9 	"git.prime8.dev/moncheeta/prime8dev/internal/utils"
     10 	"github.com/charmbracelet/lipgloss"
     11 )
     12 
     13 func (m portfolioModel) aboutDetail() string {
     14 	shellW, _ := m.detailSize()
     15 	if m.shell.cleared {
     16 		return m.shellView(shellW)
     17 	}
     18 	parts := []string{
     19 		m.aboutFetch(),
     20 		"",
     21 		renderPrompt(m.user, "cat bio"),
     22 		m.about.Bio,
     23 		"",
     24 		renderPrompt(m.user, "ls interests/"),
     25 		renderTagList(m.about.Interests),
     26 		"",
     27 		renderPrompt(m.user, "ls hobbies/"),
     28 		renderTagList(m.about.Hobbies),
     29 		"",
     30 		m.shellView(shellW),
     31 	}
     32 
     33 	return strings.Join(parts, "\n")
     34 }
     35 
     36 func (m portfolioModel) aboutFetch() string {
     37 	lines := m.aboutFetchLines()
     38 	logo := m.aboutASCIIFor(aboutLogoWidth)
     39 	if logo == "" {
     40 		return lines
     41 	}
     42 	logo = revealedLogo(logo, m.aboutLogoRows)
     43 	if m.width < 100 {
     44 		return logo + "\n\n" + lines
     45 	}
     46 	return lipgloss.JoinHorizontal(lipgloss.Top, logo, "   ", lines)
     47 }
     48 
     49 func (m portfolioModel) aboutFetchLines() string {
     50 	title := m.about.Handle + "@prime8"
     51 	role := m.about.Profession
     52 	if m.currentJob != nil {
     53 		role = m.currentJob.Role + " @ " + m.currentJob.Company
     54 	}
     55 
     56 	row := func(key, value string) string {
     57 		return fetchKeyStyle.Render(key) + ": " + value
     58 	}
     59 
     60 	lines := []string{
     61 		promptUserStyle.Render(m.about.Handle) + promptStyle.Render("@") + promptHostStyle.Render("prime8"),
     62 		dimStyle.Render(strings.Repeat("-", len(title))),
     63 		row("name", rowStyle.Render(m.about.Name)),
     64 		row("role", rowStyle.Render(role)),
     65 		row("location", rowStyle.Render(m.about.Location)),
     66 		row("uptime", rowStyle.Render(utils.Uptime(appconfig.Birthday, time.Now()))),
     67 	}
     68 	for _, kv := range m.about.Setup {
     69 		lines = append(lines, row(kv.Key, rowStyle.Render(kv.Value)))
     70 	}
     71 	lines = append(lines,
     72 		row("langs", coloredList(m.about.Languages)),
     73 		row("email", linkStyle.Render("damian@prime8.dev")),
     74 		row("git", linkStyle.Render("prime8.dev/projects")),
     75 		row("linkedin", linkStyle.Render("linkedin.com/in/damian-myrda")),
     76 	)
     77 
     78 	return strings.Join(lines, "\n")
     79 }
     80 
     81 func (m portfolioModel) projectDetail() string {
     82 	if len(m.projects) == 0 {
     83 		return "No projects available."
     84 	}
     85 	p := m.projects[m.projectIndex]
     86 
     87 	if readme, ok := m.projectReadmes[p.ID]; ok && strings.TrimSpace(readme) != "" {
     88 		if rendered, ok := m.cachedProjectMarkdown(p.ID, m.markdownWidth()); ok && strings.TrimSpace(rendered) != "" {
     89 			return rendered
     90 		}
     91 		// syncViewport renders before this runs; never fall back to raw markdown
     92 		return m.projectReadmeFallback(p, infoStyle.Render("rendering README..."))
     93 	}
     94 
     95 	if m.projectReadmeLoads[p.ID] {
     96 		return m.projectReadmeFallback(p, infoStyle.Render("loading README..."))
     97 	}
     98 
     99 	if msg, ok := m.projectReadmeErrors[p.ID]; ok && msg != "" {
    100 		return m.projectReadmeFallback(p, dimStyle.Render("README unavailable: "+msg))
    101 	}
    102 
    103 	return m.projectReadmeFallback(p, infoStyle.Render("README not loaded yet."))
    104 }
    105 
    106 func (m portfolioModel) projectReadmeFallback(project appconfig.Project, status string) string {
    107 	headerMarkdown := replaceFirstH1WithSourceLink("", project.Name, project.URL)
    108 	header := strings.TrimSpace(renderMarkdown(headerMarkdown, m.projectMarkdownWidth()))
    109 	if header == "" {
    110 		header = titleStyle.Render(defaultProjectHeader(project.Name))
    111 	}
    112 
    113 	parts := []string{header}
    114 
    115 	if tech := renderTagList(project.Tech); tech != "" {
    116 		parts = append(parts, tech)
    117 	}
    118 
    119 	if strings.TrimSpace(status) != "" {
    120 		parts = append(parts, status)
    121 	}
    122 
    123 	return strings.Join(parts, "\n\n")
    124 }
    125 
    126 func defaultProjectHeader(name string) string {
    127 	name = strings.TrimSpace(name)
    128 	if name == "" {
    129 		return "Project Source"
    130 	}
    131 	return name
    132 }
    133 
    134 func replaceFirstH1WithSourceLink(markdown string, projectName string, projectURL string) string {
    135 	if projectName == "" {
    136 		projectName = "Project Source"
    137 	}
    138 
    139 	body := removeFirstMarkdownH1(markdown)
    140 
    141 	header := "# " + projectName
    142 	if strings.TrimSpace(projectURL) != "" {
    143 		header = "# [" + projectName + "](" + projectURL + ")"
    144 	}
    145 
    146 	if strings.TrimSpace(body) == "" {
    147 		return header
    148 	}
    149 
    150 	return header + "\n\n" + body
    151 }
    152 
    153 func removeFirstMarkdownH1(markdown string) string {
    154 	lines := strings.Split(markdown, "\n")
    155 	removed := false
    156 	out := make([]string, 0, len(lines))
    157 
    158 	for _, line := range lines {
    159 		if !removed && strings.HasPrefix(strings.TrimSpace(line), "# ") {
    160 			removed = true
    161 			continue
    162 		}
    163 		out = append(out, line)
    164 	}
    165 
    166 	return strings.TrimSpace(strings.Join(out, "\n"))
    167 }
    168 
    169 func (m portfolioModel) projectMarkdownWidth() int {
    170 	return m.markdownWidth()
    171 }
    172 
    173 func (m portfolioModel) markdownWidth() int {
    174 	w, _ := m.detailSize()
    175 	return max(w, 20)
    176 }
    177 
    178 func (m portfolioModel) experiencePage() string {
    179 	if len(m.experience) == 0 {
    180 		return "No experience available."
    181 	}
    182 
    183 	// Every line starts with a two-column gutter ("> " marks the selection),
    184 	// then the graph column: "*" on a commit, "|" under it down to the next
    185 	// commit. The root commit has nothing below it, like git log --graph.
    186 	rows := []string{renderPrompt(m.user, "git log --oneline --graph --decorate")}
    187 	for i, e := range m.experience {
    188 		decorator := ""
    189 		if e.Current {
    190 			decorator = " (HEAD -> now)"
    191 		}
    192 		gutter := "  "
    193 		if i == m.experienceIndex {
    194 			gutter = "> "
    195 		}
    196 		edge := "|"
    197 		if i == len(m.experience)-1 {
    198 			edge = " "
    199 		}
    200 		body := dimStyle.Render(experienceGraphGutter + edge + "   ")
    201 		label := fmt.Sprintf("%-20s %s%s", fitRunes(e.Company, 20), fitRunes(e.Role, 22), decorator)
    202 		rangeLabel := dateRangeLabel(e)
    203 		if i == m.experienceIndex {
    204 			rows = append(rows, selectedRowStyle.Render(gutter+"* "+label))
    205 			rows = append(rows, body+selectedRowStyle.Render(rangeLabel))
    206 			for _, d := range e.Description {
    207 				rows = append(rows, body+rowStyle.Render(d))
    208 			}
    209 			if len(e.Tech) > 0 {
    210 				rows = append(rows, body+dimStyle.Render("tech: ")+renderTagList(e.Tech))
    211 			}
    212 			if e.URL != "" {
    213 				rows = append(rows, body+dimStyle.Render("url: ")+linkStyle.Render(e.URL))
    214 			}
    215 		} else {
    216 			rows = append(rows, rowStyle.Render(gutter+"* "+label))
    217 			rows = append(rows, body+dimStyle.Render(rangeLabel))
    218 		}
    219 	}
    220 	return strings.Join(rows, "\n")
    221 }
    222 
    223 // experienceGraphGutter is the selection gutter before the graph column.
    224 const experienceGraphGutter = "  "
    225 
    226 func coloredList(labels []string) string {
    227 	parts := make([]string, len(labels))
    228 	for i, l := range labels {
    229 		style := rowStyle
    230 		if c := utils.TagColor(l); c != "" {
    231 			style = lipgloss.NewStyle().Foreground(lipgloss.Color(c))
    232 		}
    233 		parts[i] = style.Render(l)
    234 	}
    235 	return strings.Join(parts, rowStyle.Render(", "))
    236 }