tangled
alpha
login
or
join now
moth11.net
/
ttyxcvr
0
fork
atom
xcvr tui
0
fork
atom
overview
issues
pulls
pipelines
some refactors etc
moth11.net
5 months ago
377d6390
6aad60ae
+410
-306
1 changed file
expand all
collapse all
unified
split
main.go
+410
-306
main.go
···
51
ChannelList
52
ResolvingChannel
53
ConnectingToChannel
0
54
Connected
55
)
56
···
58
59
const (
60
Normal txmode = iota
61
-
Command
62
Insert
63
)
64
65
type model struct {
66
-
state txstate
67
-
mode txmode
68
-
width int
69
-
height int
70
-
error *error
71
-
prompt textinput.Model
72
-
draft *textinput.Model
73
-
sentmsg *string
74
-
channels *[]Channel
75
-
list *list.Model
76
-
curchannel *Channel
77
-
wsurl *string
78
-
lrcconn *websocket.Conn
79
-
lexconn *websocket.Conn
80
-
evtchan chan []byte
81
-
cancel func()
82
-
vp *viewport.Model
83
-
msgs map[uint32]*Message
84
-
myid *uint32
85
-
renders []*string
86
-
topic *string
87
-
color *uint32
88
-
nick *string
89
-
handle *string
90
-
signeturi *string
91
-
xrpc *PasswordClient
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
92
}
93
94
type Message struct {
···
187
func initialModel() model {
188
prompt := textinput.New()
189
prompt.Prompt = ":"
0
190
nick := "wanderer"
191
color := uint32(33096)
192
-
return model{
0
0
0
0
193
state: Splash,
194
-
mode: Normal,
0
195
prompt: prompt,
196
-
width: 30,
197
-
height: 20,
198
-
nick: &nick,
199
-
color: &color,
200
}
201
}
202
func (m model) Init() tea.Cmd {
···
210
case "q":
211
return m, tea.Quit
212
default:
213
-
m.state = GettingChannels
214
return m, GetChannels
215
}
216
}
···
265
xrpc *PasswordClient
266
}
267
0
0
0
0
0
0
0
0
0
268
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
269
switch msg := msg.(type) {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
270
case errMsg:
271
-
m.state = Error
272
m.error = &msg.err
273
return m, nil
274
case svMsg:
275
-
if m.myid != nil && msg.signetView.LrcId == *m.myid {
276
-
m.signeturi = &msg.signetView.URI
277
return m, nil
278
}
0
0
0
0
0
279
280
case loginMsg:
281
if len(msg.value) == 2 {
282
return m, login(msg.value[0], msg.value[1])
283
}
284
case loggedInMsg:
285
-
m.xrpc = msg.xrpc
286
return m, nil
287
288
case setMsg:
···
292
}
293
switch key {
294
case "color", "c":
295
-
i, err := strconv.Atoi(val)
296
-
if err != nil {
297
-
return m, nil
298
-
}
299
-
b := uint32(i)
300
-
m.color = &b
301
-
if m.draft != nil {
302
-
m.draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(&b))
0
0
0
0
0
0
303
}
304
-
err = sendSet(m.evtchan, m.nick, m.handle, m.color)
305
-
if err != nil {
306
-
send(errMsg{err})
307
}
0
308
return m, nil
309
case "nick", "name", "n":
310
-
m.nick = &val
311
-
if m.draft != nil {
312
-
m.draft.Prompt = renderName(m.nick, m.handle) + " "
313
-
m.draft.Width = m.width - len(m.draft.Prompt) - 1
314
-
}
315
-
err := sendSet(m.evtchan, m.nick, m.handle, m.color)
316
-
if err != nil {
317
-
send(errMsg{err})
318
}
0
319
return m, nil
320
case "handle", "h", "at", "@":
321
-
m.handle = &val
322
-
if m.draft != nil {
323
-
m.draft.Prompt = renderName(m.nick, m.handle) + " "
324
-
m.draft.Width = m.width - len(m.draft.Prompt) - 1
325
}
326
-
err := sendSet(m.evtchan, m.nick, m.handle, m.color)
327
-
if err != nil {
328
-
send(errMsg{err})
329
-
}
330
return m, nil
331
}
332
333
case tea.WindowSizeMsg:
334
-
m.height = msg.Height
335
-
m.width = msg.Width
336
-
if m.vp != nil {
337
-
m.vp.Width = msg.Width
338
-
m.vp.Height = msg.Height - 2
339
}
340
-
if m.draft != nil {
341
-
m.draft.Width = m.width - len(m.draft.Prompt) - 1
342
-
}
343
-
if m.renders != nil {
344
-
for _, message := range m.msgs {
345
-
message.renderMessage(msg.Width)
0
0
0
346
}
347
-
m.vp.SetContent(JoinDeref(m.renders, ""))
348
-
}
349
-
if m.list != nil {
350
-
m.list.SetSize(msg.Width, msg.Height)
351
}
352
return m, nil
353
-
354
-
case tea.KeyMsg:
355
-
switch msg.String() {
356
-
case "ctrl+c":
357
-
return m, tea.Quit
358
-
}
359
}
360
361
-
switch m.state {
362
case Splash:
363
return m.updateSplash(msg)
364
case GettingChannels:
365
return m.updateGettingChannels(msg)
366
case ChannelList:
367
-
return m.updateChannelList(msg)
0
0
0
0
0
0
0
368
case ResolvingChannel:
369
return m.updateResolvingChannel(msg)
370
case ConnectingToChannel:
371
return m.updateConnectingToChannel(msg)
0
0
372
case Connected:
373
-
return m.updateConnected(msg)
0
0
0
0
0
0
0
374
}
375
376
return m, nil
377
}
378
379
-
func (m model) updateConnected(msg tea.Msg) (tea.Model, tea.Cmd) {
380
switch msg := msg.(type) {
381
case lrcEvent:
382
if msg.e == nil {
383
-
m.state = Error
384
-
err := errors.New("nil lrcEvent")
385
-
m.error = &err
386
-
return m, nil
387
}
388
id := msg.e.Id
389
switch msg := msg.e.Msg.(type) {
390
case *lrcpb.Event_Ping:
391
-
return m, nil
392
case *lrcpb.Event_Pong:
393
-
return m, nil
394
case *lrcpb.Event_Init:
395
-
err := initMessage(msg.Init, m.msgs, &m.renders, m.width)
396
if err != nil {
397
-
m.state = Error
398
-
m.error = &err
399
-
return m, nil
400
}
401
if msg.Init.Echoed != nil && *msg.Init.Echoed {
402
-
m.myid = msg.Init.Id
403
}
404
-
ab := m.vp.AtBottom()
405
-
m.vp.SetContent(JoinDeref(m.renders, ""))
406
if ab {
407
-
m.vp.GotoBottom()
408
}
409
-
return m, nil
410
case *lrcpb.Event_Pub:
411
-
err := pubMessage(msg.Pub, m.msgs, m.width)
412
if err != nil {
413
-
m.state = Error
414
-
m.error = &err
415
-
return m, nil
416
}
417
-
m.vp.SetContent(JoinDeref(m.renders, ""))
418
-
return m, nil
419
case *lrcpb.Event_Insert:
420
-
err := insertMessage(msg.Insert, m.msgs, &m.renders, m.width)
421
if err != nil {
422
-
m.state = Error
423
-
m.error = &err
424
-
return m, nil
425
}
426
-
ab := m.vp.AtBottom()
427
-
m.vp.SetContent(JoinDeref(m.renders, ""))
428
if ab {
429
-
m.vp.GotoBottom()
430
}
431
-
return m, nil
432
case *lrcpb.Event_Delete:
433
-
err := deleteMessage(msg.Delete, m.msgs, &m.renders, m.width)
434
if err != nil {
435
-
m.state = Error
436
-
m.error = &err
437
-
return m, nil
438
}
439
-
ab := m.vp.AtBottom()
440
-
m.vp.SetContent(JoinDeref(m.renders, ""))
441
if ab {
442
-
m.vp.GotoBottom()
443
}
444
-
return m, nil
445
case *lrcpb.Event_Mute:
446
-
return m, nil
447
case *lrcpb.Event_Unmute:
448
-
return m, nil
449
case *lrcpb.Event_Set:
450
-
return m, nil
451
case *lrcpb.Event_Get:
452
if msg.Get.Topic != nil {
453
-
m.topic = msg.Get.Topic
454
}
455
-
return m, nil
456
case *lrcpb.Event_Editbatch:
457
if id == nil {
458
-
return m, nil
459
}
460
-
err := editMessage(*id, msg.Editbatch.Edits, m.msgs, &m.renders, m.width)
461
if err != nil {
462
-
m.state = Error
463
-
m.error = &err
464
-
return m, nil
465
}
466
-
ab := m.vp.AtBottom()
467
-
m.vp.SetContent(JoinDeref(m.renders, ""))
468
if ab {
469
-
m.vp.GotoBottom()
470
}
471
-
return m, nil
472
}
473
case tea.KeyMsg:
474
-
switch m.mode {
475
case Normal:
476
switch msg.String() {
477
case "i", "a":
478
-
m.mode = Insert
479
-
return m, m.draft.Focus()
480
case "I":
481
-
m.mode = Insert
482
-
m.draft.CursorStart()
483
-
return m, m.draft.Focus()
484
case "A":
485
-
m.mode = Insert
486
-
m.draft.CursorEnd()
487
-
return m, m.draft.Focus()
488
-
case ":":
489
-
m.mode = Command
490
-
return m, m.prompt.Focus()
491
}
492
case Insert:
493
switch msg.String() {
494
case "esc":
495
-
m.mode = Normal
496
-
m.draft.Blur()
497
-
return m, nil
498
case "enter":
499
-
if m.sentmsg != nil {
500
-
if m.xrpc != nil && m.signeturi != nil {
501
var color64 *uint64
502
-
if m.color != nil {
503
-
c64 := uint64(*m.color)
504
color64 = &c64
505
}
506
lmr := lex.MessageRecord{
507
-
SignetURI: *m.signeturi,
508
-
Body: *m.sentmsg,
509
-
Nick: m.nick,
510
Color: color64,
511
PostedAt: syntax.DatetimeNow().String(),
512
}
513
-
m.draft.SetValue("")
514
-
m.sentmsg = nil
515
-
m.myid = nil
516
-
m.signeturi = nil
517
-
return m, tea.Batch(sendPub(m.lrcconn), createMSGCmd(m.xrpc, &lmr))
518
}
519
-
m.draft.SetValue("")
520
-
m.sentmsg = nil
521
-
return m, sendPub(m.lrcconn)
522
}
523
-
return m, nil
524
-
}
525
-
case Command:
526
-
switch msg.String() {
527
-
case "esc":
528
-
m.mode = Normal
529
-
m.prompt.Blur()
530
-
m.prompt.SetValue("")
531
-
return m, nil
532
-
case "enter":
533
-
m.mode = Normal
534
-
m.prompt.Blur()
535
-
v := m.prompt.Value()
536
-
m.prompt.SetValue("")
537
-
return m, evaluateCommand(v)
538
-
default:
539
}
540
}
541
}
542
-
switch m.mode {
543
case Normal:
544
-
vp, cmd := m.vp.Update(msg)
545
-
m.vp = &vp
546
-
return m, cmd
547
-
case Command:
548
-
prompt, cmd := m.prompt.Update(msg)
549
-
m.prompt = prompt
550
-
return m, cmd
551
case Insert:
552
-
draft, cmd := m.draft.Update(msg)
553
-
if m.sentmsg == nil && draft.Value() != "" {
554
nv := draft.Value()
555
-
m.sentmsg = &nv
556
-
m.draft = &draft
557
-
return m, tea.Batch(cmd, sendInsert(m.lrcconn, nv, 0, true))
558
}
559
-
if m.sentmsg != nil && *m.sentmsg != draft.Value() {
560
draftutf16 := utf16.Encode([]rune(draft.Value()))
561
-
sentutf16 := utf16.Encode([]rune(*m.sentmsg))
562
edits := Diff(sentutf16, draftutf16)
563
-
m.draft = &draft
564
sentmsg := draft.Value()
565
-
m.sentmsg = &sentmsg
566
-
return m, tea.Batch(cmd, sendEditBatch(m.evtchan, edits))
567
}
568
-
m.draft = &draft
569
-
return m, cmd
570
}
571
-
return m, nil
572
}
573
574
func createMSGCmd(xrpc *PasswordClient, lmr *lex.MessageRecord) tea.Cmd {
···
665
}
666
}
667
668
-
func evaluateCommand(command string) tea.Cmd {
669
return func() tea.Msg {
670
parts := strings.Split(command, " ")
671
if parts == nil {
···
684
if len(parts) != 1 {
685
return loginMsg{parts[1:]}
686
}
0
0
0
0
687
}
688
return nil
689
}
0
0
0
0
690
}
691
692
type loginMsg struct {
···
887
func (m model) updateConnectingToChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
888
switch msg := msg.(type) {
889
case connMsg:
890
-
m.state = Connected
891
-
m.cancel = msg.cancel
892
-
m.msgs = make(map[uint32]*Message)
893
-
vp := viewport.New(m.width, m.height-2)
894
-
m.vp = &vp
0
0
0
895
draft := textinput.New()
896
-
draft.Prompt = renderName(m.nick, m.handle) + " "
897
-
draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.color))
898
draft.Placeholder = "press i to start typing"
899
-
draft.Width = m.width - len(draft.Prompt) - 1
900
-
m.draft = &draft
901
-
go startLRCHandlers(msg.conn, msg.lexconn, m.nick, m.handle, m.color)
902
-
m.lrcconn = msg.conn
903
-
m.lexconn = msg.lexconn
904
-
m.evtchan = make(chan []byte)
905
-
go LRCWriter(m.lrcconn, m.evtchan)
0
0
0
906
return m, nil
907
}
908
return m, nil
909
}
910
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
911
func LRCWriter(conn *websocket.Conn, datachan chan []byte) {
912
for data := range datachan {
913
err := conn.WriteMessage(websocket.BinaryMessage, data)
···
941
942
}
943
944
-
func startLRCHandlers(conn *websocket.Conn, lexconn *websocket.Conn, nick *string, handle *string, color *uint32) {
945
if conn == nil {
946
send(errMsg{errors.New("provided nil conn")})
947
return
···
963
}
964
conn.WriteMessage(websocket.BinaryMessage, data)
965
go listenToConn(conn)
966
-
go listenToLexConn(lexconn)
967
}
968
969
type typedJSON struct {
···
1030
func (m model) updateResolvingChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
1031
switch msg := msg.(type) {
1032
case resolutionMsg:
1033
-
wsurl := fmt.Sprintf("%s%s", m.curchannel.Host, msg.resolution.URL)
1034
-
m.wsurl = &wsurl
1035
-
m.state = ConnectingToChannel
0
0
0
0
1036
ctx, cancel := context.WithCancel(context.Background())
1037
-
return m, m.connectToChannel(ctx, cancel)
1038
}
1039
return m, nil
1040
}
1041
1042
-
func (m model) connectToChannel(ctx context.Context, cancel func()) tea.Cmd {
1043
return func() tea.Msg {
1044
dialer := websocket.DefaultDialer
1045
dialer.Subprotocols = []string{"lrc.v1"}
1046
-
if m.wsurl == nil {
1047
-
return errMsg{errors.New("nil wsurl!")}
0
0
0
1048
}
1049
-
conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", *m.wsurl), http.Header{})
0
0
0
0
0
0
0
0
0
0
0
0
0
1050
if err != nil {
1051
return errMsg{err}
1052
}
1053
1054
dialer = websocket.DefaultDialer
1055
-
lexconn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://xcvr.org/xrpc/org.xcvr.lrc.subscribeLexStream?uri=%s", m.curchannel.URI), http.Header{})
0
0
0
0
0
1056
if err != nil {
1057
return errMsg{err}
1058
}
1059
-
return connMsg{conn, lexconn, cancel}
1060
}
1061
}
1062
···
1064
conn *websocket.Conn
1065
lexconn *websocket.Conn
1066
cancel func()
0
1067
}
1068
1069
const (
···
1074
func (m model) updateGettingChannels(msg tea.Msg) (tea.Model, tea.Cmd) {
1075
switch msg := msg.(type) {
1076
case channelsMsg:
0
1077
items := make([]list.Item, 0, len(msg.channels))
1078
for _, channel := range msg.channels {
1079
items = append(items, ChannelItem{channel})
1080
}
1081
-
list := list.New(items, ChannelItemDelegate{}, m.width, m.height)
1082
list.Styles = defaultStyles()
1083
list.Title = "org.xcvr.feed.getChannels"
1084
-
m.list = &list
1085
-
m.state = ChannelList
0
0
1086
return m, nil
1087
}
1088
return m, nil
1089
}
1090
1091
-
func (m model) updateChannelList(msg tea.Msg) (tea.Model, tea.Cmd) {
1092
-
if m.list == nil {
1093
-
err := errors.New("no list!")
1094
-
m.error = &err
1095
-
m.state = Error
1096
-
return m, nil
1097
}
0
0
0
0
1098
switch msg := msg.(type) {
1099
case tea.KeyMsg:
1100
switch msg.String() {
1101
case "enter":
1102
-
m.state = ResolvingChannel
1103
-
i, ok := m.list.SelectedItem().(ChannelItem)
1104
-
if ok {
1105
-
uri := i.URI()
0
0
0
1106
did, _ := DidFromUri(uri)
1107
rkey, err := RkeyFromUri(uri)
1108
if err != nil {
1109
-
m.error = &err
1110
-
m.state = Error
1111
-
return m, nil
1112
}
1113
-
m.curchannel = &i.channel
1114
-
m.list = nil
1115
-
m.channels = nil
1116
-
return m, ResolveChannel(i.Host(), did, rkey)
1117
} else {
1118
err := errors.New("bad list type")
1119
-
m.error = &err
1120
-
m.state = Error
1121
-
return m, nil
1122
}
1123
}
1124
}
1125
-
list, cmd := m.list.Update(msg)
1126
-
m.list = &list
1127
-
return m, cmd
1128
}
1129
1130
func ResolveChannel(host string, did string, rkey string) tea.Cmd {
···
1158
}
1159
1160
func (m model) View() string {
1161
-
switch m.state {
0
0
0
0
1162
case Splash:
1163
return m.splashView()
1164
case GettingChannels:
···
1169
}
1170
return "broke so bad there isn't an error"
1171
case ChannelList:
1172
-
return m.channelListView()
1173
case ResolvingChannel:
1174
return "resolving channel"
1175
case ConnectingToChannel:
1176
return m.connectingView()
1177
case Connected:
1178
-
return m.connectedView()
1179
default:
1180
return "under construction"
1181
}
1182
}
1183
1184
-
func (m model) connectedView() string {
1185
-
var vpt string
1186
-
if m.vp != nil {
1187
-
vpt = m.vp.View()
1188
-
}
1189
-
address := "lrc://"
1190
-
if m.wsurl != nil {
1191
-
address = fmt.Sprintf("%s%s", address, *m.wsurl)
1192
-
}
1193
-
var topic string
1194
-
if m.topic != nil {
1195
-
topic = *m.topic
1196
-
}
1197
-
remainingspace := m.width - len(address) - len(topic)
1198
-
var footertext string
1199
-
if m.mode == Command {
1200
-
footertext = m.prompt.View()
1201
-
} else if remainingspace < 1 {
1202
-
addressremaining := m.width - len(address)
1203
-
if addressremaining < 0 {
1204
-
footertext = strings.Repeat(" ", m.width)
1205
} else {
1206
-
footertext = fmt.Sprintf("%s%s", address, strings.Repeat(" ", m.width-len(address)))
1207
}
1208
-
} else {
1209
-
footertext = fmt.Sprintf("%s%s%s", address, strings.Repeat(" ", remainingspace), topic)
1210
-
}
1211
-
insert := m.mode == Insert
1212
-
footerstyle := lipgloss.NewStyle().Reverse(insert)
1213
-
if m.mode != Command {
1214
-
footerstyle = footerstyle.Foreground(ColorFromInt(m.color))
1215
-
}
1216
-
footer := footerstyle.Render(footertext)
1217
-
var draftText string
1218
-
if m.draft != nil {
1219
-
draftText = m.draft.View()
1220
}
0
1221
return fmt.Sprintf("%s\n%s\n%s", vpt, draftText, footer)
1222
}
1223
1224
func (m model) connectingView() string {
1225
-
blip := m.wsurl
1226
-
if blip == nil {
1227
-
return "resolving channel\nSOMETHING WENT HORRIBLY WRONG"
1228
-
}
1229
-
return fmt.Sprintf("resolving channel\nconnecting to %s", *m.wsurl)
1230
}
1231
1232
-
func (m model) channelListView() string {
1233
-
return m.list.View()
0
0
0
0
0
1234
}
1235
1236
func (m model) splashView() string {
···
1262
to start!
1263
`
1264
s := fmt.Sprintf("\n\n\n\n%s%s%s%s%s%s%s%s%s%s%s%s%s", style.Render(part00), style.Render(part01), style.Render(part02), style.Render(part03), style.Render(part1), text1, style.Render(part2), style.Render(part25), text2, style.Render(part3), text3, style.Render(part4), text4)
1265
-
offset := lipgloss.NewStyle().MarginLeft((m.width - 58) / 2)
1266
return offset.Render(s)
1267
}
1268
···
51
ChannelList
52
ResolvingChannel
53
ConnectingToChannel
54
+
DialingChannel
55
Connected
56
)
57
···
59
60
const (
61
Normal txmode = iota
0
62
Insert
63
)
64
65
type model struct {
66
+
cmding bool
67
+
cmdout *string
68
+
error *error
69
+
prompt textinput.Model
70
+
clm *channellistmodel
71
+
cm *channelmodel
72
+
gsd *globalsettingsdata
73
+
}
74
+
75
+
type channellistmodel struct {
76
+
channels []Channel
77
+
list list.Model
78
+
gsd *globalsettingsdata
79
+
}
80
+
81
+
type channelmodel struct {
82
+
channel Channel
83
+
mode txmode
84
+
wsurl string
85
+
lrcconn *websocket.Conn
86
+
lexconn *websocket.Conn
87
+
cancel func()
88
+
vp viewport.Model
89
+
draft textinput.Model
90
+
msgs map[uint32]*Message
91
+
myid *uint32
92
+
render []*string
93
+
sentmsg *string
94
+
topic *string
95
+
signeturi *string
96
+
datachan chan []byte
97
+
gsd *globalsettingsdata
98
+
}
99
+
100
+
type globalsettingsdata struct {
101
+
color *uint32
102
+
nick *string
103
+
handle *string
104
+
xrpc *PasswordClient
105
+
width int
106
+
height int
107
+
state txstate
108
}
109
110
type Message struct {
···
203
func initialModel() model {
204
prompt := textinput.New()
205
prompt.Prompt = ":"
206
+
prompt.Width = 28 //: + prompt.Width + 1 left over for blinky = initialWidth
207
nick := "wanderer"
208
color := uint32(33096)
209
+
gsd := globalsettingsdata{
210
+
nick: &nick,
211
+
color: &color,
212
+
width: 30,
213
+
height: 20,
214
state: Splash,
215
+
}
216
+
return model{
217
prompt: prompt,
218
+
gsd: &gsd,
0
0
0
219
}
220
}
221
func (m model) Init() tea.Cmd {
···
229
case "q":
230
return m, tea.Quit
231
default:
232
+
m.gsd.state = GettingChannels
233
return m, GetChannels
234
}
235
}
···
284
xrpc *PasswordClient
285
}
286
287
+
func (cm *channelmodel) updateLRCIdentity() {
288
+
if cm != nil && cm.lrcconn != nil {
289
+
err := sendSet(cm.datachan, cm.gsd.nick, cm.gsd.handle, cm.gsd.color)
290
+
if err != nil {
291
+
send(errMsg{err})
292
+
}
293
+
}
294
+
}
295
+
296
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
297
switch msg := msg.(type) {
298
+
case tea.KeyMsg:
299
+
if msg.String() == "ctrl+c" {
300
+
return m, tea.Quit
301
+
}
302
+
if m.cmdout != nil {
303
+
m.cmdout = nil
304
+
return m, nil
305
+
}
306
+
if (m.cm != nil && m.cm.mode == Insert) || (m.clm != nil && m.clm.list.FilterState() == list.Filtering) {
307
+
break
308
+
}
309
+
if !m.cmding {
310
+
if msg.String() == ":" {
311
+
m.cmding = true
312
+
return m, m.prompt.Focus()
313
+
}
314
+
} else {
315
+
switch msg.String() {
316
+
case "esc":
317
+
m.cmding = false
318
+
m.prompt.Blur()
319
+
m.prompt.SetValue("")
320
+
return m, nil
321
+
case "enter":
322
+
m.cmding = false
323
+
m.prompt.Blur()
324
+
v := m.prompt.Value()
325
+
m.prompt.SetValue("")
326
+
return m, m.evaluateCommand(v)
327
+
default:
328
+
p, cmd := m.prompt.Update(msg)
329
+
m.prompt = p
330
+
return m, cmd
331
+
}
332
+
}
333
case errMsg:
334
+
m.gsd.state = Error
335
m.error = &msg.err
336
return m, nil
337
case svMsg:
338
+
if m.cm != nil && m.cm.myid != nil && msg.signetView.LrcId == *m.cm.myid {
339
+
m.cm.signeturi = &msg.signetView.URI
340
return m, nil
341
}
342
+
case dialMsg:
343
+
if len(msg.value) == 1 {
344
+
m.gsd.state = DialingChannel
345
+
return m, m.dialingChannel(msg.value)
346
+
}
347
348
case loginMsg:
349
if len(msg.value) == 2 {
350
return m, login(msg.value[0], msg.value[1])
351
}
352
case loggedInMsg:
353
+
m.gsd.xrpc = msg.xrpc
354
return m, nil
355
356
case setMsg:
···
360
}
361
switch key {
362
case "color", "c":
363
+
var b uint32
364
+
365
+
if len(val) == 7 && val[0] == '#' {
366
+
b64, err := strconv.ParseUint(val[1:], 16, 0)
367
+
if err != nil {
368
+
return m, nil
369
+
}
370
+
b = uint32(b64)
371
+
} else {
372
+
i, err := strconv.Atoi(val)
373
+
if err != nil {
374
+
return m, nil
375
+
}
376
+
b = uint32(i)
377
}
378
+
m.gsd.color = &b
379
+
if m.cm != nil {
380
+
m.cm.draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(&b))
381
}
382
+
m.cm.updateLRCIdentity()
383
return m, nil
384
case "nick", "name", "n":
385
+
m.gsd.nick = &val
386
+
if m.cm != nil {
387
+
m.cm.draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
388
+
m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1
0
0
0
0
389
}
390
+
m.cm.updateLRCIdentity()
391
return m, nil
392
case "handle", "h", "at", "@":
393
+
m.gsd.handle = &val
394
+
if m.cm != nil {
395
+
m.cm.draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
396
+
m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1
397
}
398
+
m.cm.updateLRCIdentity()
0
0
0
399
return m, nil
400
}
401
402
case tea.WindowSizeMsg:
403
+
m.gsd.height = msg.Height
404
+
m.gsd.width = msg.Width
405
+
m.prompt.Width = msg.Width - 2
406
+
if m.clm != nil {
407
+
m.clm.list.SetSize(msg.Width, msg.Height-1)
408
}
409
+
if m.cm != nil {
410
+
m.cm.vp.Width = msg.Width
411
+
m.cm.vp.Height = msg.Height - 2
412
+
m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1
413
+
if m.cm.render != nil {
414
+
for _, message := range m.cm.msgs {
415
+
message.renderMessage(msg.Width)
416
+
}
417
+
m.cm.vp.SetContent(JoinDeref(m.cm.render, ""))
418
}
0
0
0
0
419
}
420
return m, nil
0
0
0
0
0
0
421
}
422
423
+
switch m.gsd.state {
424
case Splash:
425
return m.updateSplash(msg)
426
case GettingChannels:
427
return m.updateGettingChannels(msg)
428
case ChannelList:
429
+
clm, cmd, err := m.clm.updateChannelList(msg)
430
+
if err != nil {
431
+
m.gsd.state = Error
432
+
m.error = &err
433
+
return m, nil
434
+
}
435
+
m.clm = &clm
436
+
return m, cmd
437
case ResolvingChannel:
438
return m.updateResolvingChannel(msg)
439
case ConnectingToChannel:
440
return m.updateConnectingToChannel(msg)
441
+
case DialingChannel:
442
+
443
case Connected:
444
+
cm, cmd, err := m.cm.updateConnected(msg)
445
+
if err != nil {
446
+
m.gsd.state = Error
447
+
m.error = &err
448
+
return m, nil
449
+
}
450
+
m.cm = &cm
451
+
return m, cmd
452
}
453
454
return m, nil
455
}
456
457
+
func (cm channelmodel) updateConnected(msg tea.Msg) (channelmodel, tea.Cmd, error) {
458
switch msg := msg.(type) {
459
case lrcEvent:
460
if msg.e == nil {
461
+
return cm, nil, errors.New("nil lrcEvent")
0
0
0
462
}
463
id := msg.e.Id
464
switch msg := msg.e.Msg.(type) {
465
case *lrcpb.Event_Ping:
466
+
return cm, nil, nil
467
case *lrcpb.Event_Pong:
468
+
return cm, nil, nil
469
case *lrcpb.Event_Init:
470
+
err := initMessage(msg.Init, cm.msgs, &cm.render, cm.gsd.width)
471
if err != nil {
472
+
return cm, nil, err
0
0
473
}
474
if msg.Init.Echoed != nil && *msg.Init.Echoed {
475
+
cm.myid = msg.Init.Id
476
}
477
+
ab := cm.vp.AtBottom()
478
+
cm.vp.SetContent(JoinDeref(cm.render, ""))
479
if ab {
480
+
cm.vp.GotoBottom()
481
}
482
+
return cm, nil, nil
483
case *lrcpb.Event_Pub:
484
+
err := pubMessage(msg.Pub, cm.msgs, cm.gsd.width)
485
if err != nil {
486
+
return cm, nil, err
0
0
487
}
488
+
cm.vp.SetContent(JoinDeref(cm.render, ""))
489
+
return cm, nil, err
490
case *lrcpb.Event_Insert:
491
+
err := insertMessage(msg.Insert, cm.msgs, &cm.render, cm.gsd.width)
492
if err != nil {
493
+
return cm, nil, err
0
0
494
}
495
+
ab := cm.vp.AtBottom()
496
+
cm.vp.SetContent(JoinDeref(cm.render, ""))
497
if ab {
498
+
cm.vp.GotoBottom()
499
}
500
+
return cm, nil, nil
501
case *lrcpb.Event_Delete:
502
+
err := deleteMessage(msg.Delete, cm.msgs, &cm.render, cm.gsd.width)
503
if err != nil {
504
+
return cm, nil, err
0
0
505
}
506
+
ab := cm.vp.AtBottom()
507
+
cm.vp.SetContent(JoinDeref(cm.render, ""))
508
if ab {
509
+
cm.vp.GotoBottom()
510
}
511
+
return cm, nil, nil
512
case *lrcpb.Event_Mute:
513
+
return cm, nil, nil
514
case *lrcpb.Event_Unmute:
515
+
return cm, nil, nil
516
case *lrcpb.Event_Set:
517
+
return cm, nil, nil
518
case *lrcpb.Event_Get:
519
if msg.Get.Topic != nil {
520
+
cm.topic = msg.Get.Topic
521
}
522
+
return cm, nil, nil
523
case *lrcpb.Event_Editbatch:
524
if id == nil {
525
+
return cm, nil, nil
526
}
527
+
err := editMessage(*id, msg.Editbatch.Edits, cm.msgs, &cm.render, cm.gsd.width)
528
if err != nil {
529
+
return cm, nil, err
0
0
530
}
531
+
ab := cm.vp.AtBottom()
532
+
cm.vp.SetContent(JoinDeref(cm.render, ""))
533
if ab {
534
+
cm.vp.GotoBottom()
535
}
536
+
return cm, nil, nil
537
}
538
case tea.KeyMsg:
539
+
switch cm.mode {
540
case Normal:
541
switch msg.String() {
542
case "i", "a":
543
+
cm.mode = Insert
544
+
return cm, cm.draft.Focus(), nil
545
case "I":
546
+
cm.mode = Insert
547
+
cm.draft.CursorStart()
548
+
return cm, cm.draft.Focus(), nil
549
case "A":
550
+
cm.mode = Insert
551
+
cm.draft.CursorEnd()
552
+
return cm, cm.draft.Focus(), nil
0
0
0
553
}
554
case Insert:
555
switch msg.String() {
556
case "esc":
557
+
cm.mode = Normal
558
+
cm.draft.Blur()
559
+
return cm, nil, nil
560
case "enter":
561
+
if cm.sentmsg != nil {
562
+
if cm.gsd.xrpc != nil && cm.signeturi != nil {
563
var color64 *uint64
564
+
if cm.gsd.color != nil {
565
+
c64 := uint64(*cm.gsd.color)
566
color64 = &c64
567
}
568
lmr := lex.MessageRecord{
569
+
SignetURI: *cm.signeturi,
570
+
Body: *cm.sentmsg,
571
+
Nick: cm.gsd.nick,
572
Color: color64,
573
PostedAt: syntax.DatetimeNow().String(),
574
}
575
+
cm.draft.SetValue("")
576
+
cm.sentmsg = nil
577
+
cm.myid = nil
578
+
cm.signeturi = nil
579
+
return cm, tea.Batch(sendPub(cm.lrcconn), createMSGCmd(cm.gsd.xrpc, &lmr)), nil
580
}
581
+
cm.draft.SetValue("")
582
+
cm.sentmsg = nil
583
+
return cm, sendPub(cm.lrcconn), nil
584
}
585
+
return cm, nil, nil
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
586
}
587
}
588
}
589
+
switch cm.mode {
590
case Normal:
591
+
vp, cmd := cm.vp.Update(msg)
592
+
cm.vp = vp
593
+
return cm, cmd, nil
0
0
0
0
594
case Insert:
595
+
draft, cmd := cm.draft.Update(msg)
596
+
if cm.sentmsg == nil && draft.Value() != "" {
597
nv := draft.Value()
598
+
cm.sentmsg = &nv
599
+
cm.draft = draft
600
+
return cm, tea.Batch(cmd, sendInsert(cm.lrcconn, nv, 0, true)), nil
601
}
602
+
if cm.sentmsg != nil && *cm.sentmsg != draft.Value() {
603
draftutf16 := utf16.Encode([]rune(draft.Value()))
604
+
sentutf16 := utf16.Encode([]rune(*cm.sentmsg))
605
edits := Diff(sentutf16, draftutf16)
606
+
cm.draft = draft
607
sentmsg := draft.Value()
608
+
cm.sentmsg = &sentmsg
609
+
return cm, tea.Batch(cmd, sendEditBatch(cm.datachan, edits)), nil
610
}
611
+
cm.draft = draft
612
+
return cm, cmd, nil
613
}
614
+
return cm, nil, nil
615
}
616
617
func createMSGCmd(xrpc *PasswordClient, lmr *lex.MessageRecord) tea.Cmd {
···
708
}
709
}
710
711
+
func (m model) evaluateCommand(command string) tea.Cmd {
712
return func() tea.Msg {
713
parts := strings.Split(command, " ")
714
if parts == nil {
···
727
if len(parts) != 1 {
728
return loginMsg{parts[1:]}
729
}
730
+
case "dial":
731
+
if len(parts) != 1 {
732
+
return dialMsg{parts[1]}
733
+
}
734
}
735
return nil
736
}
737
+
}
738
+
739
+
type dialMsg struct {
740
+
value string
741
}
742
743
type loginMsg struct {
···
938
func (m model) updateConnectingToChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
939
switch msg := msg.(type) {
940
case connMsg:
941
+
m.gsd.state = Connected
942
+
cm := channelmodel{}
943
+
cm.wsurl = msg.wsurl
944
+
cm.gsd = m.gsd
945
+
cm.cancel = msg.cancel
946
+
cm.msgs = make(map[uint32]*Message)
947
+
vp := viewport.New(m.gsd.width, m.gsd.height-2)
948
+
cm.vp = vp
949
draft := textinput.New()
950
+
draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
951
+
draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color))
952
draft.Placeholder = "press i to start typing"
953
+
draft.Width = m.gsd.width - len(draft.Prompt) - 1
954
+
cm.draft = draft
955
+
go startLRCHandlers(msg.conn, m.gsd.nick, m.gsd.handle, m.gsd.color)
956
+
cm.lrcconn = msg.conn
957
+
cm.lexconn = msg.lexconn
958
+
cm.datachan = make(chan []byte)
959
+
go listenToLexConn(msg.lexconn)
960
+
go LRCWriter(cm.lrcconn, cm.datachan)
961
+
m.cm = &cm
962
+
m.clm = nil
963
return m, nil
964
}
965
return m, nil
966
}
967
968
+
func (m model) updateDialingChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
969
+
switch msg := msg.(type) {
970
+
case connSimpleMsg:
971
+
m.gsd.state = Connected
972
+
cm := channelmodel{}
973
+
cm.gsd = m.gsd
974
+
cm.cancel = msg.cancel
975
+
cm.msgs = make(map[uint32]*Message)
976
+
vp := viewport.New(m.gsd.width, m.gsd.height-2)
977
+
cm.vp = vp
978
+
draft := textinput.New()
979
+
draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
980
+
draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color))
981
+
draft.Placeholder = "press i to start typing"
982
+
draft.Width = m.gsd.width - len(draft.Prompt) - 1
983
+
cm.draft = draft
984
+
go startLRCHandlers(msg.conn, m.gsd.nick, m.gsd.handle, m.gsd.color)
985
+
m.cm = &cm
986
+
m.clm = nil
987
+
}
988
+
return m, nil
989
+
}
990
+
991
func LRCWriter(conn *websocket.Conn, datachan chan []byte) {
992
for data := range datachan {
993
err := conn.WriteMessage(websocket.BinaryMessage, data)
···
1021
1022
}
1023
1024
+
func startLRCHandlers(conn *websocket.Conn, nick *string, handle *string, color *uint32) {
1025
if conn == nil {
1026
send(errMsg{errors.New("provided nil conn")})
1027
return
···
1043
}
1044
conn.WriteMessage(websocket.BinaryMessage, data)
1045
go listenToConn(conn)
0
1046
}
1047
1048
type typedJSON struct {
···
1109
func (m model) updateResolvingChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
1110
switch msg := msg.(type) {
1111
case resolutionMsg:
1112
+
c := m.clm.curchannel()
1113
+
var host string
1114
+
if c != nil {
1115
+
host = c.Host
1116
+
}
1117
+
wsurl := fmt.Sprintf("%s%s", host, msg.resolution.URL)
1118
+
m.gsd.state = ConnectingToChannel
1119
ctx, cancel := context.WithCancel(context.Background())
1120
+
return m, m.connectToChannel(ctx, cancel, wsurl)
1121
}
1122
return m, nil
1123
}
1124
1125
+
func (m model) dialingChannel(url string) tea.Cmd {
1126
return func() tea.Msg {
1127
dialer := websocket.DefaultDialer
1128
dialer.Subprotocols = []string{"lrc.v1"}
1129
+
ctx, cancel := context.WithCancel(context.Background())
1130
+
conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", url), http.Header{})
1131
+
if err != nil {
1132
+
cancel()
1133
+
return errMsg{err}
1134
}
1135
+
return connSimpleMsg{conn, cancel}
1136
+
}
1137
+
}
1138
+
1139
+
type connSimpleMsg struct {
1140
+
conn *websocket.Conn
1141
+
cancel func()
1142
+
}
1143
+
1144
+
func (m model) connectToChannel(ctx context.Context, cancel func(), wsurl string) tea.Cmd {
1145
+
return func() tea.Msg {
1146
+
dialer := websocket.DefaultDialer
1147
+
dialer.Subprotocols = []string{"lrc.v1"}
1148
+
conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", wsurl), http.Header{})
1149
if err != nil {
1150
return errMsg{err}
1151
}
1152
1153
dialer = websocket.DefaultDialer
1154
+
c := m.clm.curchannel()
1155
+
var uri string
1156
+
if c != nil {
1157
+
uri = c.URI
1158
+
}
1159
+
lexconn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://xcvr.org/xrpc/org.xcvr.lrc.subscribeLexStream?uri=%s", uri), http.Header{})
1160
if err != nil {
1161
return errMsg{err}
1162
}
1163
+
return connMsg{conn, lexconn, cancel, wsurl}
1164
}
1165
}
1166
···
1168
conn *websocket.Conn
1169
lexconn *websocket.Conn
1170
cancel func()
1171
+
wsurl string
1172
}
1173
1174
const (
···
1179
func (m model) updateGettingChannels(msg tea.Msg) (tea.Model, tea.Cmd) {
1180
switch msg := msg.(type) {
1181
case channelsMsg:
1182
+
clm := channellistmodel{}
1183
items := make([]list.Item, 0, len(msg.channels))
1184
for _, channel := range msg.channels {
1185
items = append(items, ChannelItem{channel})
1186
}
1187
+
list := list.New(items, ChannelItemDelegate{}, m.gsd.width, m.gsd.height-1)
1188
list.Styles = defaultStyles()
1189
list.Title = "org.xcvr.feed.getChannels"
1190
+
clm.list = list
1191
+
m.gsd.state = ChannelList
1192
+
clm.gsd = m.gsd
1193
+
m.clm = &clm
1194
return m, nil
1195
}
1196
return m, nil
1197
}
1198
1199
+
func (clm channellistmodel) curchannel() *Channel {
1200
+
switch i := clm.list.SelectedItem().(type) {
1201
+
case ChannelItem:
1202
+
return &i.channel
0
0
1203
}
1204
+
return nil
1205
+
}
1206
+
1207
+
func (clm channellistmodel) updateChannelList(msg tea.Msg) (channellistmodel, tea.Cmd, error) {
1208
switch msg := msg.(type) {
1209
case tea.KeyMsg:
1210
switch msg.String() {
1211
case "enter":
1212
+
if clm.list.FilterState() == list.Filtering {
1213
+
break
1214
+
}
1215
+
clm.gsd.state = ResolvingChannel
1216
+
cc := clm.curchannel()
1217
+
if cc != nil {
1218
+
uri := cc.URI
1219
did, _ := DidFromUri(uri)
1220
rkey, err := RkeyFromUri(uri)
1221
if err != nil {
1222
+
return clm, nil, err
0
0
1223
}
1224
+
return clm, ResolveChannel(cc.Host, did, rkey), nil
0
0
0
1225
} else {
1226
err := errors.New("bad list type")
1227
+
return clm, nil, err
0
0
1228
}
1229
}
1230
}
1231
+
list, cmd := clm.list.Update(msg)
1232
+
clm.list = list
1233
+
return clm, cmd, nil
1234
}
1235
1236
func ResolveChannel(host string, did string, rkey string) tea.Cmd {
···
1264
}
1265
1266
func (m model) View() string {
1267
+
var pv string
1268
+
if m.cmding {
1269
+
pv = m.prompt.View()
1270
+
}
1271
+
switch m.gsd.state {
1272
case Splash:
1273
return m.splashView()
1274
case GettingChannels:
···
1279
}
1280
return "broke so bad there isn't an error"
1281
case ChannelList:
1282
+
return m.clm.channelListView(m.cmding, pv)
1283
case ResolvingChannel:
1284
return "resolving channel"
1285
case ConnectingToChannel:
1286
return m.connectingView()
1287
case Connected:
1288
+
return m.cm.connectedView(m.cmding, pv)
1289
default:
1290
return "under construction"
1291
}
1292
}
1293
1294
+
func (cm channelmodel) connectedView(cmding bool, prompt string) string {
1295
+
vpt := cm.vp.View()
1296
+
var footer string
1297
+
if cmding {
1298
+
footer = prompt
1299
+
} else {
1300
+
address := "lrc://"
1301
+
address = fmt.Sprintf("%s%s", address, cm.wsurl)
1302
+
var topic string
1303
+
if cm.topic != nil {
1304
+
topic = *cm.topic
1305
+
}
1306
+
remainingspace := cm.gsd.width - len(address) - len(topic)
1307
+
var footertext string
1308
+
if remainingspace < 1 {
1309
+
addressremaining := cm.gsd.width - len(address)
1310
+
if addressremaining < 0 {
1311
+
footertext = strings.Repeat(" ", cm.gsd.width)
1312
+
} else {
1313
+
footertext = fmt.Sprintf("%s%s", address, strings.Repeat(" ", cm.gsd.width-len(address)))
1314
+
}
1315
} else {
1316
+
footertext = fmt.Sprintf("%s%s%s", address, strings.Repeat(" ", remainingspace), topic)
1317
}
1318
+
insert := cm.mode == Insert
1319
+
footerstyle := lipgloss.NewStyle().Reverse(insert)
1320
+
footerstyle = footerstyle.Foreground(ColorFromInt(cm.gsd.color))
1321
+
footer = footerstyle.Render(footertext)
0
0
0
0
0
0
0
0
1322
}
1323
+
draftText := cm.draft.View()
1324
return fmt.Sprintf("%s\n%s\n%s", vpt, draftText, footer)
1325
}
1326
1327
func (m model) connectingView() string {
1328
+
return "resolving channel\nconnecting to channel"
0
0
0
0
1329
}
1330
1331
+
func (clm channellistmodel) channelListView(cmding bool, prompt string) string {
1332
+
lv := clm.list.View()
1333
+
cv := ""
1334
+
if cmding {
1335
+
cv = prompt
1336
+
}
1337
+
return fmt.Sprintf("%s\n%s", lv, cv)
1338
}
1339
1340
func (m model) splashView() string {
···
1366
to start!
1367
`
1368
s := fmt.Sprintf("\n\n\n\n%s%s%s%s%s%s%s%s%s%s%s%s%s", style.Render(part00), style.Render(part01), style.Render(part02), style.Render(part03), style.Render(part1), text1, style.Render(part2), style.Render(part25), text2, style.Render(part3), text3, style.Render(part4), text4)
1369
+
offset := lipgloss.NewStyle().MarginLeft((m.gsd.width - 58) / 2)
1370
return offset.Render(s)
1371
}
1372