-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrepl.go
More file actions
171 lines (157 loc) · 5.3 KB
/
Copy pathrepl.go
File metadata and controls
171 lines (157 loc) · 5.3 KB
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
package main
import (
"path/filepath"
"github.com/chzyer/readline"
)
// Single readline.Instance for the whole interactive session. Two configs
// swap in via rl.SetConfig: promptConfig at the top-level "blick> " prompt
// (history file + autocompleter), bodyConfig for the inline .-sentinel
// composers in replyTo / replEmail / replChat (no history, no completion).
// The body-mode switch keeps quoted bodies and one-off drafts out of the
// persisted history file, which the user explicitly wants.
var (
rl *readline.Instance
promptConfig *readline.Config
bodyConfig *readline.Config
)
// identityPainter is a no-op Painter for bodyConfig. readline's NewEx
// auto-installs a default painter when its initial config has none, but
// SetConfig does not — so a config that arrives only via SetConfig keeps
// a nil Painter and RuneBuffer.output crashes on first keystroke. We
// supply our own so the body config is whole on arrival.
type identityPainter struct{}
func (identityPainter) Paint(line []rune, _ int) []rune { return line }
// replVerbs is the verb table for tab completion at REPL position 0.
// Single-letter aliases are included so e/c/r/t/j/x/H/q complete the
// same way as their long forms. Numeric forms (<N>, <N>r, <N>d) are
// deliberately excluded — completion can't usefully suggest live item
// numbers from the dashboard.
var replVerbs = []string{
"view", "reply", "done",
"forward",
"attach",
"search",
"inbox", "i",
"email", "e",
"chat", "c",
"refresh", "r",
"today", "t",
"join", "j",
"presence", "p",
"exit", "x",
"help", "h",
"quit", "q",
}
// setupReadline builds the singleton readline.Instance with the prompt
// config and pre-builds the body config. contactKeys is snapshotted once
// at REPL start; later `contacts add` in this session won't show in
// completion until the next launch — confirmed acceptable.
func setupReadline(historyPath string, contactKeys []string) error {
promptConfig = &readline.Config{
Prompt: bold + "blick> " + reset,
HistoryFile: historyPath,
HistoryLimit: 1000,
HistorySearchFold: true,
AutoComplete: newReplCompleter(contactKeys),
InterruptPrompt: "^C",
EOFPrompt: "exit",
}
bodyConfig = &readline.Config{
Prompt: " " + cyan + "> " + reset,
DisableAutoSaveHistory: true,
AutoComplete: nil,
InterruptPrompt: "^C",
Painter: identityPainter{},
}
var err error
rl, err = readline.NewEx(promptConfig)
return err
}
// enterBodyMode swaps the singleton readline.Instance to body config.
// Callers must defer exitBodyMode to restore the top-level prompt.
func enterBodyMode() {
_ = rl.SetConfig(bodyConfig)
}
func exitBodyMode() {
_ = rl.SetConfig(promptConfig)
}
// readBodyDraft loops on rl.Readline() under body-mode config until the
// user types "." on a line by itself. Returns the joined body (un-
// trimmed; trimming is the caller's choice) and true. Returns "" and
// false on Ctrl-C or EOF — caller renders "(cancelled)" or similar.
//
// Body mode must already be active before calling. The .-sentinel
// protocol matches the legacy stdinLines-fed loop byte for byte: each
// line is appended; "." closes the draft; mid-draft Ctrl-C/EOF cancels.
func readBodyDraft() (string, bool) {
var lines []string
for {
line, err := rl.Readline()
if err != nil {
return "", false
}
if line == "." {
return joinLines(lines), true
}
lines = append(lines, line)
}
}
func joinLines(lines []string) string {
if len(lines) == 0 {
return ""
}
out := lines[0]
for _, l := range lines[1:] {
out += "\n" + l
}
return out
}
// newReplCompleter wires verb completion at position 0 and contact
// handle completion at position 1 for the email/chat verbs (full forms
// and aliases). Numeric/short item-keyed forms get no completion —
// they'd flood the suggestion list with no useful filtering.
func newReplCompleter(contactKeys []string) readline.AutoCompleter {
contactPC := make([]readline.PrefixCompleterInterface, len(contactKeys))
for i, k := range contactKeys {
contactPC[i] = readline.PcItem(k)
}
composeVerbs := map[string]bool{
"email": true, "e": true,
"chat": true, "c": true,
}
presenceVerbs := map[string]bool{"presence": true, "p": true}
presencePC := make([]readline.PrefixCompleterInterface, len(presenceOptions))
for i, o := range presenceOptions {
presencePC[i] = readline.PcItem(o.key)
}
items := make([]readline.PrefixCompleterInterface, 0, len(replVerbs))
for _, v := range replVerbs {
switch {
case composeVerbs[v]:
items = append(items, readline.PcItem(v, contactPC...))
case presenceVerbs[v]:
items = append(items, readline.PcItem(v, presencePC...))
default:
items = append(items, readline.PcItem(v))
}
}
return readline.NewPrefixCompleter(items...)
}
// loadContactKeys snapshots address-book keys for tab completion.
// A failure to load is non-fatal: the REPL still boots, completion just
// doesn't include contacts. The user can always type the handle in full
// or fix contacts.json by hand.
func loadContactKeys() []string {
store, err := LoadContacts()
if err != nil {
return nil
}
out := make([]string, 0, len(store.Contacts))
for _, c := range store.Sorted() {
out = append(out, c.Key)
}
return out
}
func replHistoryPath() string {
return filepath.Join(configDir(), "history")
}