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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
// Command glint is a modeless terminal markdown editor with live styling.
package main
import (
"flag"
"fmt"
"os"
"glint/internal/app"
"glint/internal/config"
"glint/internal/configui"
"glint/internal/keyprobe"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
func init() {
// Render glint's exact theme hexes regardless of the terminal's detected
// profile or OS appearance — never downsample or adapt to the system.
lipgloss.SetColorProfile(termenv.TrueColor)
}
// version is the build version, overridden at release time via
// -ldflags "-X main.version=<v>" (the Homebrew formula sets it).
var version = "dev"
const helpText = `glint — a modeless terminal markdown editor
USAGE
glint [file] open a file, or the fuzzy picker when no file is given
glint <command>
COMMANDS (each takes a short letter or the full word, one or two dashes:
-n / --n / -new / --new all work)
-n, --new [name] new note in the current directory
combine with -i or -v to target the inbox or vault
-t, --today open today's daily note (in the vault)
-d, --daily browse the daily-notes folder
-v, --vault fuzzy picker over your vault, from anywhere
-i, --inbox fuzzy picker over your inbox
-c, --config interactive setup walkthrough (writes the config file)
-h, --help show this help
--version print the version
EDITOR KEYS
Ctrl+S save (an unnamed buffer prompts for a name)
Ctrl+P toggle the read preview
Ctrl+F fuzzy file picker
Ctrl+D today's daily note
Ctrl+N new note in the current directory
Ctrl+B new note in the inbox
Ctrl+T cycle theme (flexoki-light / flexoki-dark / charm)
Ctrl+C / Ctrl+X / Ctrl+V copy / cut / paste (system clipboard)
Shift+arrows select text (Ctrl+Shift+left/right by word)
Alt+left / Alt+right move by word
Ctrl+U / Ctrl+K delete to start / end of line
Ctrl+W delete the word before the cursor
Ctrl+Z / Ctrl+Y undo / redo
Ctrl+Q quit (press twice if there are unsaved changes)
Esc back to the editor
CONFIG
~/.config/glint/config.toml (run 'glint -c' to set it up)
`
func main() {
// Help and version short-circuit before flag parsing (-v means --vault, so
// version is long-only).
if len(os.Args) > 1 {
switch os.Args[1] {
case "-h", "--h", "-help", "--help":
fmt.Print(helpText)
return
case "--version", "-version":
fmt.Println("glint", version)
return
}
}
cfg, err := config.Load()
if err != nil {
fmt.Fprintln(os.Stderr, "glint: config:", err)
}
// Every command is a flag; both -x and --x (letter or word) work.
flagNew := boolFlag("n", "new")
flagToday := boolFlag("t", "today")
flagDaily := boolFlag("d", "daily")
flagVault := boolFlag("v", "vault")
flagConfig := boolFlag("c", "config")
flagInbox := boolFlag("i", "inbox")
flagKeys := flag.Bool("keys", false, "show what the terminal sends for each key")
flag.Parse()
isNew := *flagNew[0] || *flagNew[1]
isToday := *flagToday[0] || *flagToday[1]
isDaily := *flagDaily[0] || *flagDaily[1]
isVault := *flagVault[0] || *flagVault[1]
isConfig := *flagConfig[0] || *flagConfig[1]
isInbox := *flagInbox[0] || *flagInbox[1]
// Standalone commands (no editor TUI).
if isConfig {
runOrDie(configui.Run())
return
}
if *flagKeys {
runOrDie(keyprobe.Run())
return
}
name := ""
if args := flag.Args(); len(args) > 0 {
name = args[0]
}
a := app.New(cfg)
var startErr error
switch {
case isNew:
// New note in the current dir, or the inbox/vault when combined.
dir := cfg.WorkingDir()
if isInbox {
dir = cfg.InboxRoot()
}
if isVault {
dir = cfg.Vault()
}
startErr = a.StartNewIn(dir, name)
case isToday:
startErr = a.Start("", true) // today's daily note
case isVault:
startErr = a.StartPickerIn(cfg.Vault())
case isInbox:
startErr = a.StartPickerIn(cfg.InboxRoot())
case isDaily:
startErr = a.StartPickerIn(cfg.DailyDir()) // browse the daily folder
case name != "":
startErr = a.Start(name, false) // open a file
default:
startErr = a.Start("", false) // bare → fuzzy picker over the current dir
}
if startErr != nil {
fmt.Fprintln(os.Stderr, "glint:", startErr)
os.Exit(1)
}
run(a)
}
// boolFlag registers a short and long name for the same command and returns both
// pointers; either being set means the command was given (e.g. -n / --n / -new /
// --new). Go's flag package accepts both single- and double-dash for each name.
func boolFlag(short, long string) [2]*bool {
return [2]*bool{
flag.Bool(short, false, "command: -"+short+" / --"+long),
flag.Bool(long, false, ""),
}
}
func runOrDie(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "glint:", err)
os.Exit(1)
}
}
// run drives the Bubbletea program in the alternate screen.
func run(a *app.App) {
if _, err := tea.NewProgram(a, tea.WithAltScreen(), tea.WithMouseCellMotion()).Run(); err != nil {
fmt.Fprintln(os.Stderr, "glint:", err)
os.Exit(1)
}
}
|