1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
package app
import (
"os"
"path/filepath"
"strings"
"glint/internal/dialog"
tea "github.com/charmbracelet/bubbletea"
)
// namingKind is what an open naming dialog will do when confirmed.
type namingKind int
const (
namingNew namingKind = iota // create dialogDir/<name>.md and open it
namingRename // rename the current file to <name>.md
)
// openNamePrompt shows the centered naming dialog. dir is the folder a new name
// resolves under; prefill seeds the input (the picker query, or the current
// basename for a rename). Cancelling returns to whatever mode was active.
func (a *App) openNamePrompt(kind namingKind, dir, prefill string) {
title := "New note"
if kind == namingRename {
title = "Rename"
}
// Re-entering the dialog (e.g. a second Ctrl+N while it's already open)
// must not make the dialog its own return mode — that would strand Esc
// and confirm in ModeNamePrompt forever.
if a.mode != ModeNamePrompt {
a.prevMode = a.mode
}
a.dialogKind = kind
a.dialogDir = dir
a.dialog.SetWidth(a.contentWidth())
a.dialog.Open(title, a.dirLabel(dir), prefill)
a.mode = ModeNamePrompt
a.status = ""
}
// renameFile is the F2 handler: rename the open file, or name a buffer that has
// no path yet. Only meaningful over the editor.
func (a *App) renameFile() (tea.Model, tea.Cmd) {
if a.mode == ModePicker || a.mode == ModeNamePrompt {
return a, nil
}
if a.path == "" {
return a.nameCurrentBuffer()
}
base := filepath.Base(a.path)
a.openNamePrompt(namingRename, filepath.Dir(a.path), strings.TrimSuffix(base, filepath.Ext(base)))
return a, nil
}
// nameCurrentBuffer names a pathless buffer, then writes it — the job the
// bottom-bar save-as prompt used to do.
func (a *App) nameCurrentBuffer() (tea.Model, tea.Cmd) {
dir := a.saveDir
if dir == "" {
dir = a.cfg.InboxRoot()
}
a.openNamePrompt(namingRename, dir, "")
return a, nil
}
// handleNameKey routes a key into the dialog and acts on what it reports.
func (a *App) handleNameKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
res, cmd := a.dialog.Update(msg)
switch res {
case dialog.ResultConfirm:
return a.confirmName()
case dialog.ResultCancel:
return a.cancelName()
}
return a, cmd
}
// cancelName closes the dialog and restores the mode it opened over, writing
// nothing.
func (a *App) cancelName() (tea.Model, tea.Cmd) {
a.mode = a.prevMode
a.status = ""
return a, nil
}
// dirLabel renders dir for the dialog's hint: $HOME collapsed to ~, one
// trailing separator so it reads as a folder.
func (a *App) dirLabel(dir string) string {
if home, err := os.UserHomeDir(); err == nil && home != "" && isHomeOrChild(dir, home) {
dir = "~" + strings.TrimPrefix(dir, home)
}
return strings.TrimSuffix(dir, string(filepath.Separator)) + string(filepath.Separator)
}
// isHomeOrChild reports whether dir is home itself or a path under it,
// requiring the match to end at a path separator rather than merely sharing a
// string prefix (so /Users/foobar does not collapse under home=/Users/foo).
func isHomeOrChild(dir, home string) bool {
if dir == home {
return true
}
return strings.HasPrefix(dir, home+string(filepath.Separator))
}
// confirmName applies the dialog's name. Implemented in the next task.
func (a *App) confirmName() (tea.Model, tea.Cmd) {
return a.cancelName()
}
|