prime8dev

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

commit 2bc7dc7a975c67df35ae2f22333b1b99ed52c299
parent 86d3ddca052e417ba0d3e70ff6af4c36c4423365
Author: Damian Myrda <damian@prime8.dev>
Date:   Wed, 21 Oct 2026 08:40:11 +0000

feat: shorter window titles

Drop the user from the terminal (osc) and browser tab title and collapse
deep paths to their last segment, e.g. prime8:~/projects or
prime8:…/janitor, capped at 20 characters so tabs don't cut it.

Diffstat:
Minternal/tui/program.go | 4++--
Minternal/tui/shell_test.go | 18+++++++++++++++++-
Minternal/tui/update.go | 4+++-
Ainternal/utils/title.go | 31+++++++++++++++++++++++++++++++
Ainternal/utils/title_test.go | 27+++++++++++++++++++++++++++
Minternal/web/renderer.go | 1+
Minternal/web/scripts/modules/header.js | 16++++++++++++++--
Minternal/web/templates/layout.gohtml | 4++--
8 files changed, 97 insertions(+), 8 deletions(-)

diff --git a/internal/tui/program.go b/internal/tui/program.go @@ -25,8 +25,8 @@ func (m portfolioModel) Init() tea.Cmd { ) } -// defaultUser is the SSH session user as shown in prompts, the window title, -// whoami and the shell's home directory, sanitized to a safe path segment. +// defaultUser is the SSH session user as shown in prompts, whoami and the +// shell's home directory, sanitized to a safe path segment. func defaultUser(user string) string { return vfs.SanitizeUser(user) } diff --git a/internal/tui/shell_test.go b/internal/tui/shell_test.go @@ -212,7 +212,7 @@ func TestShellUsesSessionUser(t *testing.T) { if strings.Contains(out, "guest") { t.Errorf("guest leaked:\n%s", out) } - if got := m.windowTitle(); got != "damian@prime8:~" { + if got := m.windowTitle(); got != "prime8:~" { t.Errorf("window title = %q", got) } m.shell = newShellState(m.user) @@ -222,6 +222,22 @@ func TestShellUsesSessionUser(t *testing.T) { } } +func TestWindowTitleIsShort(t *testing.T) { + m := testModel(t, 120, 40) + m.activateTab(tabExperience) + if got := m.windowTitle(); got != "prime8:~/experience" { + t.Errorf("experience title = %q", got) + } + m.activateTab(tabProjects) + want := "prime8:…/" + m.selectedProject().ID + if got := m.windowTitle(); got != want { + t.Errorf("project title = %q, want %q", got, want) + } + if n := len([]rune(m.windowTitle())); n > 20 { + t.Errorf("title too long: %d", n) + } +} + func TestProjectsSharedBorderIsAccent(t *testing.T) { accent := "\x1b[38;5;179m" for _, w := range []int{120, 90} { diff --git a/internal/tui/update.go b/internal/tui/update.go @@ -3,6 +3,8 @@ package tui import ( "time" + "git.prime8.dev/moncheeta/prime8dev/internal/utils" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -38,7 +40,7 @@ func (m portfolioModel) windowTitle() string { path += "/" + p.ID } } - return m.user + "@prime8:" + path + return utils.WindowTitle(path) } func (m portfolioModel) update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/internal/utils/title.go b/internal/utils/title.go @@ -0,0 +1,31 @@ +package utils + +import "strings" + +// MaxWindowTitle is the longest window/tab title we set, in runes, so it +// fits terminal and browser tabs without being cut. +const MaxWindowTitle = 20 + +const windowTitleHost = "prime8:" + +// WindowTitle is the terminal/browser title for a shell-style path such as +// "~/projects/mos": "prime8:<path>", collapsed to "prime8:…/<last segment>" +// (and truncated with … if still too long) when it would exceed +// MaxWindowTitle runes. +func WindowTitle(path string) string { + full := windowTitleHost + path + if runeLen(full) <= MaxWindowTitle { + return full + } + seg := path + if i := strings.LastIndex(strings.TrimRight(path, "/"), "/"); i >= 0 { + seg = strings.TrimRight(path, "/")[i+1:] + } + short := []rune(windowTitleHost + "…/" + seg) + if len(short) > MaxWindowTitle { + short = append(short[:MaxWindowTitle-1], '…') + } + return string(short) +} + +func runeLen(s string) int { return len([]rune(s)) } diff --git a/internal/utils/title_test.go b/internal/utils/title_test.go @@ -0,0 +1,27 @@ +package utils + +import ( + "testing" + "unicode/utf8" +) + +func TestWindowTitle(t *testing.T) { + for in, want := range map[string]string{ + "~": "prime8:~", + "~/projects": "prime8:~/projects", + "~/experience": "prime8:~/experience", + "~/projects/mos": "prime8:…/mos", + "~/projects/janitor": "prime8:…/janitor", + "~/projects/summarizer": "prime8:…/summarizer", + "~/experience/hark": "prime8:…/hark", + "~/projects/a-very-long-project-name": "prime8:…/a-very-lon…", + } { + got := WindowTitle(in) + if got != want { + t.Errorf("WindowTitle(%q) = %q, want %q", in, got, want) + } + if n := utf8.RuneCountInString(got); n > MaxWindowTitle { + t.Errorf("WindowTitle(%q) is %d runes", in, n) + } + } +} diff --git a/internal/web/renderer.go b/internal/web/renderer.go @@ -60,6 +60,7 @@ func New() (*Renderer, error) { "tagClass": utils.TagClass, "tagColor": func(label string) template.CSS { return template.CSS(utils.TagColor(label)) }, "shellPath": ShellPath, + "windowTitle": func(p string) string { return utils.WindowTitle(ShellPath(p)) }, "shellUser": func() string { return vfs.FallbackUser }, "tagCSS": func() template.CSS { return template.CSS(utils.TagCSS()) }, "inc": func(i int) int { return i + 1 }, diff --git a/internal/web/scripts/modules/header.js b/internal/web/scripts/modules/header.js @@ -115,10 +115,22 @@ export function initHeader() { setActiveTabUI(currentFromPath(order, fallback)); } +// Same rules as utils.WindowTitle: "prime8:~/projects", collapsed to +// "prime8:…/<last segment>" past 20 characters so browser tabs don't cut it. +const MAX_TITLE = 20; + +export function windowTitle(path) { + const full = `prime8:${path}`; + if ([...full].length <= MAX_TITLE) return full; + const trimmed = path.replace(/\/+$/, ""); + const seg = trimmed.slice(trimmed.lastIndexOf("/") + 1); + const short = [...`prime8:…/${seg}`]; + return short.length > MAX_TITLE ? short.slice(0, MAX_TITLE - 1).join("") + "…" : short.join(""); +} + export function updateAppTitle() { - const prefix = document.getElementById("app")?.dataset.titlePrefix || "prime8"; const path = window.location.pathname.replace(/\/+$/, ""); - document.title = `${prefix}:~${path}`; + document.title = windowTitle(`~${path}`); } function setActiveTabUI(tab) { diff --git a/internal/web/templates/layout.gohtml b/internal/web/templates/layout.gohtml @@ -4,7 +4,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>{{.About.Handle}}@prime8:{{shellPath .Path}}</title> + <title>{{windowTitle .Path}}</title> <meta name="description" content="{{.Description}}"> <link rel="canonical" href="{{tabURL .ActiveTab}}"> <link rel="alternate" hreflang="en" href="{{tabURL .ActiveTab}}"> @@ -39,7 +39,7 @@ {{tagCSS}}</style> </head> <body> - <div id="app" data-title-prefix="{{.About.Handle}}@prime8" data-boot-title="{{.About.Name}}" data-boot-subtitle="www.prime8.dev" hx-ext="preload"> + <div id="app" data-boot-title="{{.About.Name}}" data-boot-subtitle="www.prime8.dev" hx-ext="preload"> {{template "header" .}} <main id="content"> {{if eq .ContentTemplate "about"}}