New-file naming dialog & editor filename header
Date: 2026-07-30
Problem
Creating a note in glint leaves the buffer unnamed. The name is asked for later, at
the first Ctrl+S, through a one-line prompt in the bottom status bar (ModeSaveAs).
Two consequences:
- The name is an afterthought. A new note has no identity until it is saved, and the Harper language server sees it under an untitled URI until then.
- Editor mode never shows the filename anywhere. Preview mode does — it renders a full-width title bar above the body — so the two modes disagree about whether the document has a visible name.
Goals
- Ask for the filename up front, in a centered dialog, whenever a new file is made.
- Show the filename in editor mode as an uneditable header bar, styled like the title bar preview already renders.
- Collapse the two naming UIs (save-as, new-file) into one component, and reuse it for renaming an existing file.
Design
Approach
A new internal/dialog package holds the centered prompt as a self-contained
Bubbletea sub-model: prompt title, target-directory hint, text input, validation,
and Enter/Esc handling. App owns a *dialog.Model and a new ModeNamePrompt.
legibleText and hexToRGB move from internal/preview into internal/theme, so
the editor header and the preview title bar derive their colors from one place and
cannot drift apart.
Alternatives considered and rejected:
- Everything inline in
app.go. Fewest files, butapp.gois already the largest file in the repo (1182 lines) and the dialog would only be testable by driving the whole app. The color logic would also be duplicated against preview's. - Header owned by the
editorpackage. Rejected: the header is document chrome, not buffer text.editoralready carries wrap, scroll, and undercurl geometry, and the header's text comes fromApp.path, whicheditordoes not know.
The dialog
A centered bordered box on the canvas, in the same visual family as helpOverlay:
rounded border in theme.Heading, themed background.
╭────────────────────────────────╮
│ New note │
│ in ~/Humdrum/Inbox/ │
│ │
│ > my-new-note▌ │
│ │
│ Enter to create · Esc cancel │
╰────────────────────────────────╯
One component, two uses:
| Use | Title | Prefill | Confirm |
|---|---|---|---|
| New | New note |
picker query, if any | create empty file, open it |
| Rename | Rename |
current basename | os.Rename, rebind path |
Behavior:
- The target directory is displayed, so it is clear whether Ctrl+N (current dir) or Ctrl+B (inbox) is in play.
- Enter on an existing name opens that file (new) or refuses with
Name taken(rename). This matches today'sopenNoteAt. - Slashes nest subdirectories, via the existing
picker.NewNotePathplusMkdirAll. - Enter with an empty input reports
Type a name firstand keeps the dialog open. - Esc returns to the previous mode. Nothing is written, and no unnamed buffer is left behind.
The header
The header occupies the existing three-row top pad (canvasTopPad = 3), so it costs
no text rows and the editor's height is unchanged:
row 0 (blank pad)
row 1 ██ my-note ████████████████████ ← header bar
row 2 (blank pad)
row 3+ document text (unchanged height)
- Spans the content column, aligned inside the canvas margins — the same width as the
text, matching preview's
titleBar. - Bold,
theme.Headingbackground,theme.LegibleText(Heading)foreground, one space of padding each side, truncated with…when the name overflows. - Text is the basename minus
.md. A frontmattertitle:property does not override it: edit mode shows what is on disk. - A pathless buffer renders
Untitledintheme.Mutedrather than the heading bar, so "unsaved and unnamed" reads at a glance. - A dirty buffer appends a bullet:
my-note •. The status bar keeps its own indicator. - Rendered in
ModeEditorand the overlay modes that keep the editor visible beneath (find, goto, spell). Not in the picker, not in help (the body is replaced), and not in preview (which has its own title bar).
Wiring
ModeNamePromptis added.ModeSaveAs,saveInput, andsaveBarare deleted;saveAs()folds into the dialog's confirm handler.newFile()opens the dialog instead of callingstartBlankIn(). The dirty-buffer discard confirmation stays in front of the dialog: press the same key again to discard, then the dialog opens.- Picker Ctrl+N opens the dialog prefilled with the typed query, cursor at the end. (Today it creates the file with no prompt.)
glint -nwith no name boots straight into the dialog over an empty editor.F2opens the rename dialog, prefilled with the current basename. On confirm:os.Rename, rebindApp.path, reopen Harper under the new URI viagrammarOpen, and update the preview title. On a pathless buffer, F2 names and writes it.- Ctrl+S on a pathless buffer opens the same dialog — one naming UI, not two.
startBlankIn()survives for the pathless case.
Testing
internal/dialog: unit tests for validation, prefill, Esc, and slash-nested paths.internal/app: header text and dirty-marker rendering; the F2 rename round-trip against a temp directory; the new-file flow from both the editor and the picker.- Existing
ModeSaveAstests are rewritten against the dialog.