apps/jotts/tui/update.go 7.1 K raw
1
package tui
2
3
import (
4
	"strings"
5
	"time"
6
7
	"charm.land/bubbles/v2/key"
8
	tea "charm.land/bubbletea/v2"
9
	sharedtui "github.com/stevedylandev/andromeda/pkg/tui"
10
)
11
12
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
13
	switch msg := msg.(type) {
14
15
	case tea.WindowSizeMsg:
16
		m.width, m.height = msg.Width, msg.Height
17
		m.ready = true
18
		m.applyLayout()
19
		return m, nil
20
21
	case notesLoadedMsg:
22
		if msg.Err != nil {
23
			return m, m.setStatus("load: "+msg.Err.Error(), false)
24
		}
25
		cmd := m.list.SetNotes(msg.Notes)
26
		if n, ok := m.list.Selected(); ok {
27
			m.cont.SetNote(&n)
28
		} else {
29
			m.cont.SetNote(nil)
30
		}
31
		return m, cmd
32
33
	case noteSavedMsg:
34
		if msg.Err != nil {
35
			return m, m.setStatus("save: "+msg.Err.Error(), false)
36
		}
37
		if msg.Note != nil {
38
			m.cont.Invalidate(msg.Note.ShortID)
39
		}
40
		m.state = stateList
41
		m.form.Blur()
42
		return m, tea.Batch(loadNotesCmd(m.backend), m.setStatus("saved", true))
43
44
	case noteDeletedMsg:
45
		if msg.Err != nil {
46
			return m, m.setStatus("delete: "+msg.Err.Error(), false)
47
		}
48
		m.cont.Invalidate(msg.ShortID)
49
		m.state = stateList
50
		return m, tea.Batch(loadNotesCmd(m.backend), m.setStatus("deleted", true))
51
52
	case editorFinishedMsg:
53
		if msg.Err != nil {
54
			return m, m.setStatus("editor: "+msg.Err.Error(), false)
55
		}
56
		if msg.Tag == "" {
57
			m.form.SetContent(msg.Content)
58
			return m, nil
59
		}
60
		var orig *Note
61
		for _, it := range m.list.inner.Items() {
62
			ni, ok := it.(noteItem)
63
			if ok && ni.note.ShortID == msg.Tag {
64
				n := ni.note
65
				orig = &n
66
				break
67
			}
68
		}
69
		if orig == nil || strings.TrimRight(orig.Content, "\n") == strings.TrimRight(msg.Content, "\n") {
70
			return m, nil
71
		}
72
		return m, saveNoteCmd(m.backend, msg.Tag, orig.Title, msg.Content)
73
74
	case submitFormMsg:
75
		return m, saveNoteCmd(m.backend, msg.ShortID, msg.Title, msg.Content)
76
77
	case cancelFormMsg:
78
		m.state = stateList
79
		return m, nil
80
81
	case statusMsg:
82
		return m, m.setStatus(msg.Text, msg.OK)
83
84
	case clearStatusMsg:
85
		if time.Now().Before(m.statusUntil) {
86
			return m, nil
87
		}
88
		m.status = ""
89
		return m, nil
90
91
	case tea.KeyPressMsg:
92
		return m.handleKey(msg)
93
	}
94
95
	var cmd tea.Cmd
96
	m.list, cmd = m.list.Update(msg)
97
	return m, cmd
98
}
99
100
func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
101
	if m.confirmDelete {
102
		switch msg.String() {
103
		case "y", "Y":
104
			m.confirmDelete = false
105
			n, ok := m.list.Selected()
106
			if !ok {
107
				return m, nil
108
			}
109
			return m, deleteNoteCmd(m.backend, n.ShortID)
110
		case "n", "N", "esc", "q":
111
			m.confirmDelete = false
112
		}
113
		return m, nil
114
	}
115
116
	if m.showHelp {
117
		if key.Matches(msg, m.keys.Help) || msg.String() == "esc" || msg.String() == "q" {
118
			m.showHelp = false
119
		}
120
		return m, nil
121
	}
122
123
	switch m.state {
124
	case stateList:
125
		return m.handleListKey(msg)
126
	case stateContent:
127
		return m.handleContentKey(msg)
128
	case stateForm:
129
		var cmd tea.Cmd
130
		m.form, cmd = m.form.Update(msg)
131
		return m, cmd
132
	}
133
	return m, nil
134
}
135
136
func (m Model) handleListKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
137
	// While the list is in filter-entry mode, route every key to it
138
	// so typing and esc/enter behave as expected.
139
	if m.list.IsFiltering() {
140
		var cmd tea.Cmd
141
		m.list, cmd = m.list.Update(msg)
142
		m.refreshContentFromSelection()
143
		return m, cmd
144
	}
145
146
	switch {
147
	case key.Matches(msg, m.keys.Quit):
148
		return m, tea.Quit
149
	case key.Matches(msg, m.keys.Open):
150
		if n, ok := m.list.Selected(); ok {
151
			m.cont.SetNote(&n)
152
			m.state = stateContent
153
		}
154
		return m, nil
155
	case key.Matches(msg, m.keys.Create):
156
		m.form.StartCreate()
157
		m.state = stateForm
158
		return m, nil
159
	case key.Matches(msg, m.keys.Edit):
160
		if n, ok := m.list.Selected(); ok {
161
			m.form.StartEdit(n)
162
			m.state = stateForm
163
		}
164
		return m, nil
165
	case key.Matches(msg, m.keys.ExtEdit):
166
		if n, ok := m.list.Selected(); ok {
167
			return m, openExternalEditor(n.ShortID, n.Content)
168
		}
169
		return m, nil
170
	case key.Matches(msg, m.keys.Delete):
171
		if _, ok := m.list.Selected(); ok {
172
			m.confirmDelete = true
173
		}
174
		return m, nil
175
	case key.Matches(msg, m.keys.Copy):
176
		if n, ok := m.list.Selected(); ok {
177
			return m, sharedtui.CopyToClipboardCmd(n.Content, "copied text")
178
		}
179
		return m, nil
180
	case key.Matches(msg, m.keys.CopyLink):
181
		if !m.isRemote {
182
			return m, m.setStatus("local mode: no link", false)
183
		}
184
		if n, ok := m.list.Selected(); ok {
185
			return m, sharedtui.CopyToClipboardCmd(noteLinkURL(m.backend.RemoteURL(), n.ShortID), "copied link")
186
		}
187
		return m, nil
188
	case key.Matches(msg, m.keys.OpenBrowser):
189
		if !m.isRemote {
190
			return m, nil
191
		}
192
		if n, ok := m.list.Selected(); ok {
193
			return m, sharedtui.OpenURLCmd(noteLinkURL(m.backend.RemoteURL(), n.ShortID))
194
		}
195
		return m, nil
196
	case key.Matches(msg, m.keys.Refresh):
197
		if m.isRemote {
198
			return m, loadNotesCmd(m.backend)
199
		}
200
		return m, nil
201
	case key.Matches(msg, m.keys.Help):
202
		m.showHelp = true
203
		return m, nil
204
	}
205
206
	var cmd tea.Cmd
207
	m.list, cmd = m.list.Update(msg)
208
	m.refreshContentFromSelection()
209
	return m, cmd
210
}
211
212
func (m Model) handleContentKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
213
	switch {
214
	case key.Matches(msg, m.keys.Quit), key.Matches(msg, m.keys.Back):
215
		m.state = stateList
216
		return m, nil
217
	case key.Matches(msg, m.keys.ScrollDown):
218
		m.cont = m.cont.ScrollDown(1)
219
		return m, nil
220
	case key.Matches(msg, m.keys.ScrollUp):
221
		m.cont = m.cont.ScrollUp(1)
222
		return m, nil
223
	case key.Matches(msg, m.keys.Edit):
224
		if n, ok := m.list.Selected(); ok {
225
			m.form.StartEdit(n)
226
			m.state = stateForm
227
		}
228
		return m, nil
229
	case key.Matches(msg, m.keys.ExtEdit):
230
		if n, ok := m.list.Selected(); ok {
231
			return m, openExternalEditor(n.ShortID, n.Content)
232
		}
233
		return m, nil
234
	case key.Matches(msg, m.keys.Copy):
235
		if n, ok := m.list.Selected(); ok {
236
			return m, sharedtui.CopyToClipboardCmd(n.Content, "copied text")
237
		}
238
		return m, nil
239
	case key.Matches(msg, m.keys.CopyLink):
240
		if !m.isRemote {
241
			return m, m.setStatus("local mode: no link", false)
242
		}
243
		if n, ok := m.list.Selected(); ok {
244
			return m, sharedtui.CopyToClipboardCmd(noteLinkURL(m.backend.RemoteURL(), n.ShortID), "copied link")
245
		}
246
		return m, nil
247
	case key.Matches(msg, m.keys.OpenBrowser):
248
		if !m.isRemote {
249
			return m, nil
250
		}
251
		if n, ok := m.list.Selected(); ok {
252
			return m, sharedtui.OpenURLCmd(noteLinkURL(m.backend.RemoteURL(), n.ShortID))
253
		}
254
		return m, nil
255
	case key.Matches(msg, m.keys.ToggleWrap):
256
		m.cont.ToggleWrap()
257
		return m, nil
258
	case key.Matches(msg, m.keys.Help):
259
		m.showHelp = true
260
		return m, nil
261
	}
262
263
	var cmd tea.Cmd
264
	m.cont, cmd = m.cont.Update(msg)
265
	return m, cmd
266
}
267
268
func (m *Model) refreshContentFromSelection() {
269
	if n, ok := m.list.Selected(); ok {
270
		m.cont.SetNote(&n)
271
	} else {
272
		m.cont.SetNote(nil)
273
	}
274
}
275
276
func (m *Model) applyLayout() {
277
	if !m.ready {
278
		return
279
	}
280
	listW, contentW := splitWidths(m.width)
281
	bodyH := splitBodyHeight(m.height - 1)
282
283
	listInnerW := max(listW-paneFrameWidth(), 1)
284
	listInnerH := max(bodyH-paneFrameHeight(), 1)
285
	m.list.SetSize(listInnerW, listInnerH)
286
287
	contentInnerW := max(contentW-paneFrameWidth(), 20)
288
	contentInnerH := max(bodyH-paneFrameHeight(), 3)
289
	m.cont.SetSize(contentInnerW, max(contentInnerH-1, 1))
290
	m.form.SetSize(contentInnerW, contentInnerH)
291
}
292
293
func (m *Model) setStatus(text string, ok bool) tea.Cmd {
294
	m.status = text
295
	m.statusOK = ok
296
	m.statusUntil = time.Now().Add(2 * time.Second)
297
	return tea.Tick(2*time.Second, func(time.Time) tea.Msg { return clearStatusMsg{} })
298
}