▍ humdrum codex / glint v1.1.2
license AGPL-3.0
3.3 KB raw
  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()
}