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:
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"}}