apps/sipp/tui/update.go 7.2 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 snippetsLoadedMsg:
22
		m.loading = false
23
		if msg.Err != nil {
24
			return m, m.setStatus("load: "+msg.Err.Error(), false)
25
		}
26
		cmd := m.list.SetSnippets(msg.Snippets)
27
		m.refreshContentFromSelection()
28
		return m, cmd
29
30
	case snippetSavedMsg:
31
		if msg.Err != nil {
32
			return m, m.setStatus("save: "+msg.Err.Error(), false)
33
		}
34
		if msg.Snippet != nil {
35
			m.cont.Invalidate(msg.Snippet.ShortID)
36
		}
37
		m.state = stateList
38
		m.form.Blur()
39
		return m, tea.Batch(loadSnippetsCmd(m.backend), m.setStatus("saved", true))
40
41
	case snippetDeletedMsg:
42
		if msg.Err != nil {
43
			return m, m.setStatus("delete: "+msg.Err.Error(), false)
44
		}
45
		m.cont.Invalidate(msg.ShortID)
46
		m.state = stateList
47
		return m, tea.Batch(loadSnippetsCmd(m.backend), m.setStatus("deleted", true))
48
49
	case editorFinishedMsg:
50
		if msg.Err != nil {
51
			return m, m.setStatus("editor: "+msg.Err.Error(), false)
52
		}
53
		if msg.Tag == "" {
54
			m.form.SetContent(msg.Content)
55
			return m, nil
56
		}
57
		var orig *Snippet
58
		for _, it := range m.list.inner.Items() {
59
			si, ok := it.(snippetItem)
60
			if ok && si.snippet.ShortID == msg.Tag {
61
				s := si.snippet
62
				orig = &s
63
				break
64
			}
65
		}
66
		if orig == nil || strings.TrimRight(orig.Content, "\n") == strings.TrimRight(msg.Content, "\n") {
67
			return m, nil
68
		}
69
		return m, saveSnippetCmd(m.backend, msg.Tag, orig.Name, msg.Content)
70
71
	case submitFormMsg:
72
		return m, saveSnippetCmd(m.backend, msg.ShortID, msg.Name, msg.Content)
73
74
	case cancelFormMsg:
75
		m.state = stateList
76
		return m, nil
77
78
	case statusMsg:
79
		return m, m.setStatus(msg.Text, msg.OK)
80
81
	case clearStatusMsg:
82
		if time.Now().Before(m.statusUntil) {
83
			return m, nil
84
		}
85
		m.status = ""
86
		return m, nil
87
88
	case tea.KeyPressMsg:
89
		return m.handleKey(msg)
90
	}
91
92
	var cmd tea.Cmd
93
	m.list, cmd = m.list.Update(msg)
94
	return m, cmd
95
}
96
97
func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
98
	if msg.String() == "ctrl+c" {
99
		return m, tea.Quit
100
	}
101
102
	if m.confirmDelete {
103
		switch msg.String() {
104
		case "y", "Y":
105
			m.confirmDelete = false
106
			s, ok := m.list.Selected()
107
			if !ok {
108
				return m, nil
109
			}
110
			return m, deleteSnippetCmd(m.backend, s.ShortID)
111
		case "n", "N", "esc", "q":
112
			m.confirmDelete = false
113
		}
114
		return m, nil
115
	}
116
117
	if m.showHelp {
118
		if key.Matches(msg, m.keys.Help) || msg.String() == "esc" || msg.String() == "q" {
119
			m.showHelp = false
120
		}
121
		return m, nil
122
	}
123
124
	switch m.state {
125
	case stateList:
126
		return m.handleListKey(msg)
127
	case stateContent:
128
		return m.handleContentKey(msg)
129
	case stateForm:
130
		var cmd tea.Cmd
131
		m.form, cmd = m.form.Update(msg)
132
		return m, cmd
133
	}
134
	return m, nil
135
}
136
137
func (m Model) handleListKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
138
	if m.list.IsFiltering() {
139
		var cmd tea.Cmd
140
		m.list, cmd = m.list.Update(msg)
141
		m.refreshContentFromSelection()
142
		return m, cmd
143
	}
144
145
	switch {
146
	case key.Matches(msg, m.keys.Quit):
147
		return m, tea.Quit
148
	case key.Matches(msg, m.keys.Open):
149
		if s, ok := m.list.Selected(); ok {
150
			m.cont.SetSnippet(&s)
151
			m.state = stateContent
152
		}
153
		return m, nil
154
	case key.Matches(msg, m.keys.Create):
155
		m.form.StartCreate()
156
		m.state = stateForm
157
		return m, nil
158
	case key.Matches(msg, m.keys.Edit):
159
		if s, ok := m.list.Selected(); ok {
160
			m.form.StartEdit(s)
161
			m.state = stateForm
162
		}
163
		return m, nil
164
	case key.Matches(msg, m.keys.ExtEdit):
165
		if s, ok := m.list.Selected(); ok {
166
			return m, openExternalEditor(s.ShortID, s.Name, s.Content)
167
		}
168
		return m, nil
169
	case key.Matches(msg, m.keys.Delete):
170
		if _, ok := m.list.Selected(); ok {
171
			m.confirmDelete = true
172
		}
173
		return m, nil
174
	case key.Matches(msg, m.keys.Copy):
175
		if s, ok := m.list.Selected(); ok {
176
			return m, sharedtui.CopyToClipboardCmd(s.Content, "copied text")
177
		}
178
		return m, nil
179
	case key.Matches(msg, m.keys.CopyLink):
180
		if !m.isRemote {
181
			return m, m.setStatus("local mode: no link", false)
182
		}
183
		if s, ok := m.list.Selected(); ok {
184
			return m, sharedtui.CopyToClipboardCmd(shareLinkURL(m.backend.RemoteURL(), s.ShortID), "copied link")
185
		}
186
		return m, nil
187
	case key.Matches(msg, m.keys.OpenBrowser):
188
		if !m.isRemote {
189
			return m, nil
190
		}
191
		if s, ok := m.list.Selected(); ok {
192
			return m, sharedtui.OpenURLCmd(shareLinkURL(m.backend.RemoteURL(), s.ShortID))
193
		}
194
		return m, nil
195
	case key.Matches(msg, m.keys.Refresh):
196
		m.loading = true
197
		return m, loadSnippetsCmd(m.backend)
198
	case key.Matches(msg, m.keys.Help):
199
		m.showHelp = true
200
		return m, nil
201
	}
202
203
	var cmd tea.Cmd
204
	m.list, cmd = m.list.Update(msg)
205
	m.refreshContentFromSelection()
206
	return m, cmd
207
}
208
209
func (m Model) handleContentKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
210
	switch {
211
	case key.Matches(msg, m.keys.ToggleWrap):
212
		m.cont.ToggleWrap()
213
		if m.cont.Wrap() {
214
			return m, m.setStatus("wrap on", true)
215
		}
216
		return m, m.setStatus("wrap off", true)
217
	case key.Matches(msg, m.keys.Quit), key.Matches(msg, m.keys.Back):
218
		m.state = stateList
219
		return m, nil
220
	case key.Matches(msg, m.keys.ScrollDown):
221
		m.cont = m.cont.ScrollDown(1)
222
		return m, nil
223
	case key.Matches(msg, m.keys.ScrollUp):
224
		m.cont = m.cont.ScrollUp(1)
225
		return m, nil
226
	case key.Matches(msg, m.keys.Edit):
227
		if s, ok := m.list.Selected(); ok {
228
			m.form.StartEdit(s)
229
			m.state = stateForm
230
		}
231
		return m, nil
232
	case key.Matches(msg, m.keys.ExtEdit):
233
		if s, ok := m.list.Selected(); ok {
234
			return m, openExternalEditor(s.ShortID, s.Name, s.Content)
235
		}
236
		return m, nil
237
	case key.Matches(msg, m.keys.Copy):
238
		if s, ok := m.list.Selected(); ok {
239
			return m, sharedtui.CopyToClipboardCmd(s.Content, "copied text")
240
		}
241
		return m, nil
242
	case key.Matches(msg, m.keys.CopyLink):
243
		if !m.isRemote {
244
			return m, m.setStatus("local mode: no link", false)
245
		}
246
		if s, ok := m.list.Selected(); ok {
247
			return m, sharedtui.CopyToClipboardCmd(shareLinkURL(m.backend.RemoteURL(), s.ShortID), "copied link")
248
		}
249
		return m, nil
250
	case key.Matches(msg, m.keys.OpenBrowser):
251
		if !m.isRemote {
252
			return m, nil
253
		}
254
		if s, ok := m.list.Selected(); ok {
255
			return m, sharedtui.OpenURLCmd(shareLinkURL(m.backend.RemoteURL(), s.ShortID))
256
		}
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 s, ok := m.list.Selected(); ok {
270
		m.cont.SetSnippet(&s)
271
	} else {
272
		m.cont.SetSnippet(nil)
273
	}
274
}
275
276
func (m *Model) applyLayout() {
277
	if !m.ready {
278
		return
279
	}
280
	listW := m.width * 30 / 100
281
	if listW < 24 {
282
		listW = 24
283
	}
284
	contentW := m.width - listW
285
	if contentW < 20 {
286
		contentW = 20
287
	}
288
	bodyH := m.height - 1
289
	if bodyH < 5 {
290
		bodyH = 5
291
	}
292
293
	m.list.SetSize(max(listW-paneFrameWidth(), 1), max(bodyH-paneFrameHeight(), 1))
294
	m.cont.SetSize(max(contentW-paneFrameWidth(), 1), max(bodyH-paneFrameHeight()-1, 1))
295
	m.form.SetSize(max(contentW-paneFrameWidth(), 1), max(bodyH-paneFrameHeight(), 1))
296
}
297
298
func (m *Model) setStatus(text string, ok bool) tea.Cmd {
299
	m.status = text
300
	m.statusOK = ok
301
	m.statusUntil = time.Now().Add(2 * time.Second)
302
	return tea.Tick(2*time.Second, func(time.Time) tea.Msg { return clearStatusMsg{} })
303
}