commit 35964d1108a5661f75dc062d8e33b49169833a7d
parent e39fd98ee2632ece169c40b1b85c960866329fbf
Author: Damian Myrda <damian@prime8.dev>
Date: Wed, 21 Oct 2026 01:57:04 +0000
feat: window title shows the current path
The browser tab and the terminal window title read like a shell
prompt, e.g. moncheeta@prime8:~/projects/janitor. The web title is
rendered server-side from the request path and kept in sync on htmx
navigation; the TUI sets it with tea.SetWindowTitle when it changes.
Diffstat:
7 files changed, 45 insertions(+), 68 deletions(-)
diff --git a/cmd/server/handlers/render.go b/cmd/server/handlers/render.go
@@ -15,6 +15,7 @@ func (h *Handler) renderTemplate(w http.ResponseWriter, r *http.Request, name st
maxAgeSeconds = 0
}
+ data.Path = r.URL.Path
body, err := h.renderer.Render(name, data)
if err != nil {
http.Error(w, "Failed to render content", http.StatusInternalServerError)
diff --git a/internal/tui/types.go b/internal/tui/types.go
@@ -36,6 +36,7 @@ type portfolioModel struct {
experience []appconfig.Experience
activeTab int
+ windowTitleSet string
projectIndex int
projectReadmeScroll bool
experienceIndex int
diff --git a/internal/tui/update.go b/internal/tui/update.go
@@ -15,6 +15,33 @@ func tickCmd() tea.Cmd {
}
func (m portfolioModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ next, cmd := m.update(msg)
+ pm, ok := next.(portfolioModel)
+ if !ok {
+ return next, cmd
+ }
+ if title := pm.windowTitle(); title != pm.windowTitleSet {
+ pm.windowTitleSet = title
+ cmd = tea.Batch(cmd, tea.SetWindowTitle(title))
+ }
+ return pm, cmd
+}
+
+func (m portfolioModel) windowTitle() string {
+ path := "~"
+ switch m.activeTab {
+ case tabExperience:
+ path += "/experience"
+ case tabProjects:
+ path += "/projects"
+ if p := m.selectedProject(); p != nil {
+ path += "/" + p.ID
+ }
+ }
+ return m.about.Handle + "@prime8:" + path
+}
+
+func (m portfolioModel) update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
diff --git a/internal/web/renderer.go b/internal/web/renderer.go
@@ -19,6 +19,7 @@ var templatesFS embed.FS
type Data struct {
Title string
+ Path string
Description string
ActiveTab string
ContentTemplate string
@@ -57,6 +58,7 @@ func New() (*Renderer, error) {
"tabURL": utils.TabURL,
"tagClass": utils.TagClass,
"tagColor": func(label string) template.CSS { return template.CSS(utils.TagColor(label)) },
+ "shellPath": ShellPath,
"tagCSS": func() template.CSS { return template.CSS(utils.TagCSS()) },
"inc": func(i int) int { return i + 1 },
"techMeta": utils.TechMeta,
@@ -141,3 +143,8 @@ func loadInlineStyles() (map[string]template.CSS, error) {
return styles, nil
}
+
+// ShellPath renders a URL path the way a shell prompt shows cwd: "/" is "~".
+func ShellPath(path string) string {
+ return "~" + strings.TrimRight(path, "/")
+}
diff --git a/internal/web/scripts/modules/header.js b/internal/web/scripts/modules/header.js
@@ -1,6 +1,5 @@
import { isEditableTarget } from "./dom.js";
-const detailTitles = {};
const headerHandlers = {
keydown: null,
afterSwap: null,
@@ -116,53 +115,10 @@ export function initHeader() {
setActiveTabUI(currentFromPath(order, fallback));
}
-export function updateAppTitle(activeTab = "") {
- const headerEl = document.getElementById("tabs");
- if (!headerEl) return;
-
- const order = (headerEl.getAttribute("data-tabs") || "about").split(",").filter(Boolean);
- const fallback = order[0] || "about";
- const tab = activeTab || currentFromPath(order, fallback);
-
- const tabEl = document.querySelector(`.tab[data-tab="${tab}"]`);
- const tabLabel = tabEl?.getAttribute("data-label") || tab;
- const siteName = document.getElementById("app")?.dataset.bootTitle || "prime8.dev";
-
- let detailLabel = "";
- const path = window.location.pathname;
-
- switch (tab) {
- case "projects":
- if (/^\/projects\/[^/]+$/.test(path)) {
- const projectID = path.match(/^\/projects\/([^/]+)$/)?.[1] || "";
- detailLabel =
- detailTitles.projects ||
- document.querySelector("#project-detail h2")?.textContent?.trim() ||
- selectedListTitle(".list-item[data-id]", ".list-title", projectID);
- }
- break;
- default:
- break;
- }
-
- if (detailLabel) {
- document.title = `${detailLabel} | ${tabLabel} | ${siteName}`;
- return;
- }
-
- document.title = `${tabLabel} | ${siteName}`;
-}
-
-export function setAppTitleDetail(tab, value) {
- if (value) {
- detailTitles[tab] = value;
- } else {
- delete detailTitles[tab];
- }
-}
-
-export function clearAppTitleDetail(tab) {
- delete detailTitles[tab];
+export function updateAppTitle() {
+ const prefix = document.getElementById("app")?.dataset.titlePrefix || "prime8";
+ const path = window.location.pathname.replace(/\/+$/, "");
+ document.title = `${prefix}:~${path}`;
}
function setActiveTabUI(tab) {
@@ -176,7 +132,7 @@ function setActiveTabUI(tab) {
}
});
- updateAppTitle(tab);
+ updateAppTitle();
}
function currentFromPath(order, fallback) {
@@ -185,14 +141,3 @@ function currentFromPath(order, fallback) {
const tab = path.replace(/^\//, "").split("/")[0];
return order.includes(tab) ? tab : fallback;
}
-
-function selectedListTitle(selector, titleSelector, id) {
- if (!id) return "";
-
- const normalized = decodeURIComponent(id);
- const items = Array.from(document.querySelectorAll(selector));
- const item = items.find((el) => (el.dataset.id || "") === normalized);
- if (!item) return "";
-
- return item.querySelector(titleSelector)?.textContent?.trim() || "";
-}
diff --git a/internal/web/scripts/modules/projects.js b/internal/web/scripts/modules/projects.js
@@ -1,5 +1,5 @@
import { isEditableTarget } from "./dom.js";
-import { setAppTitleDetail, updateAppTitle } from "./header.js";
+import { updateAppTitle } from "./header.js";
const projectsHandlers = {
keydown: null,
@@ -37,10 +37,6 @@ export function initProjects() {
if (selectedIndex < 0) selectedIndex = 0;
selectedItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
-
- const detail = selectedItem.querySelector(".list-title")?.textContent?.trim() || "";
- setAppTitleDetail("projects", detail);
- updateAppTitle("projects");
}
function updateProjectURL(projectID) {
@@ -48,7 +44,7 @@ export function initProjects() {
const path = `/projects/${encodeURIComponent(projectID)}`;
if (window.location.pathname !== path) {
history.replaceState({ tab: "projects" }, "", path);
- updateAppTitle("projects");
+ updateAppTitle();
}
}
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>{{.Title}} | {{.About.Name}}</title>
+ <title>{{.About.Handle}}@prime8:{{shellPath .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-boot-title="{{.About.Name}}" data-boot-subtitle="www.prime8.dev" hx-ext="preload">
+ <div id="app" data-title-prefix="{{.About.Handle}}@prime8" data-boot-title="{{.About.Name}}" data-boot-subtitle="www.prime8.dev" hx-ext="preload">
{{template "header" .}}
<main id="content">
{{if eq .ContentTemplate "about"}}