xcvr tui
1package main
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "os"
11 "strconv"
12 "strings"
13 "time"
14 "unicode/utf16"
15
16 "github.com/bluesky-social/indigo/api/atproto"
17 "github.com/bluesky-social/indigo/atproto/client"
18 "github.com/bluesky-social/indigo/atproto/identity"
19 "github.com/bluesky-social/indigo/atproto/syntax"
20 "github.com/bluesky-social/indigo/lex/util"
21 "github.com/charmbracelet/bubbles/list"
22 "github.com/charmbracelet/bubbles/textinput"
23 "github.com/charmbracelet/bubbles/viewport"
24 tea "github.com/charmbracelet/bubbletea"
25 "github.com/charmbracelet/lipgloss"
26 "github.com/gorilla/websocket"
27 "github.com/rachel-mp4/lrcproto/gen/go"
28 "github.com/rachel-mp4/ttyxcvr/lex"
29 "google.golang.org/protobuf/proto"
30)
31
32const White = lipgloss.Color("#ffffff")
33const Black = lipgloss.Color("#000000")
34const Olive = lipgloss.Color("#c6c013")
35const Forest = lipgloss.Color("#034732")
36const Green = lipgloss.Color("#008148")
37const Orange = lipgloss.Color("#ef8a17")
38
39var verySubduedColor = lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
40var subduedColor = lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}
41var subduedStyle = lipgloss.NewStyle().Foreground(subduedColor)
42
43var PlainStyle = lipgloss.NewStyle().Foreground(White).Background(Black)
44
45type txstate int
46
47const (
48 Splash txstate = iota
49 Error
50 GettingChannels
51 ChannelList
52 ResolvingChannel
53 ConnectingToChannel
54 DialingChannel
55 Connected
56)
57
58type txmode int
59
60const (
61 Normal txmode = iota
62 Insert
63)
64
65type 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
75type channellistmodel struct {
76 channels []Channel
77 list list.Model
78 gsd *globalsettingsdata
79}
80
81type 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
100type 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
110type Message struct {
111 nick *string
112 handle *string
113 color *uint32
114 active bool
115 text string
116 rendered *string
117}
118
119type Profile struct {
120 Type string `json:"$type"`
121 Did string `json:"did"`
122 Handle *string `json:"handle,omitempty"`
123 DisplayName *string `json:"displayName,omitempty"`
124 Status *string `json:"status,omitempty"`
125 Color *uint32 `json:"color"`
126}
127
128type Channel struct {
129 Type string `json:"$type"`
130 URI string `json:"uri"`
131 Host string `json:"host"`
132 Creator Profile `json:"creator"`
133 Title string `json:"title"`
134 Topic *string `json:"topic,omitempty"`
135 CreatedAt time.Time `json:"createdAt"`
136}
137
138type ChannelItem struct {
139 channel Channel
140}
141
142func (c ChannelItem) Title() string {
143 return c.channel.Title
144}
145
146func (c ChannelItem) Description() string {
147 if c.channel.Topic != nil {
148 return *c.channel.Topic
149 }
150 return ""
151}
152
153func (c ChannelItem) URI() string {
154 return c.channel.URI
155}
156func (c ChannelItem) Host() string {
157 return c.channel.Host
158}
159
160func (c ChannelItem) FilterValue() string {
161 return c.channel.Title
162}
163
164type ChannelItemDelegate struct{}
165
166func (d ChannelItemDelegate) Height() int { return 3 }
167func (d ChannelItemDelegate) Spacing() int { return 0 }
168func (d ChannelItemDelegate) Update(msg tea.Msg, list *list.Model) tea.Cmd { return nil }
169func (d ChannelItemDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
170 var title string
171 var desc string
172 var uri string
173 var host string
174 var color *uint32
175 var author string
176 if i, ok := item.(ChannelItem); ok {
177 title = i.Title()
178 desc = i.Description()
179 author = fmt.Sprintf("(%s)", renderName(i.channel.Creator.DisplayName, i.channel.Creator.Handle))
180 host = subduedStyle.Render(fmt.Sprintf("(hosted on %s)", i.Host()))
181 if desc == "" {
182 desc = subduedStyle.Render("no provided description")
183 }
184 uri = i.URI()
185 color = i.channel.Creator.Color
186 } else {
187 return
188 }
189 if index == m.Index() {
190 greenStyle := lipgloss.NewStyle().Foreground(ColorFromInt(color))
191 title = fmt.Sprintf("│%s %s", greenStyle.Render(title), author)
192 desc = fmt.Sprintf("│%s", desc)
193 uri = fmt.Sprintf("└%s", strings.Repeat("─", m.Width()-1))
194 } else {
195 s := lipgloss.NewStyle()
196 s = s.Foreground(subduedColor)
197 uri = s.Render(uri)
198 host = subduedStyle.Render(author)
199 }
200 fmt.Fprintf(w, "%s %s\n%s\n%s", title, host, desc, uri)
201}
202
203func 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,
219 }
220}
221func (m model) Init() tea.Cmd {
222 return nil
223}
224
225func (m model) updateSplash(msg tea.Msg) (tea.Model, tea.Cmd) {
226 switch msg := msg.(type) {
227 case tea.KeyMsg:
228 switch msg.String() {
229 case "q":
230 return m, tea.Quit
231 default:
232 m.gsd.state = GettingChannels
233 return m, GetChannels
234 }
235 }
236 return m, nil
237}
238
239func GetChannels() tea.Msg {
240 c := &http.Client{Timeout: 10 * time.Second}
241 res, err := c.Get("http://xcvr.org/xrpc/org.xcvr.feed.getChannels")
242
243 if err != nil {
244 return errMsg{err}
245 }
246 if res.StatusCode != 200 {
247 return errMsg{errors.New(fmt.Sprintf("error getting channels: %d", res.StatusCode))}
248 }
249 decoder := json.NewDecoder(res.Body)
250 var channels []Channel
251 err = decoder.Decode(&channels)
252 if err != nil {
253 return errMsg{err}
254 }
255 return channelsMsg{channels}
256}
257
258type channelsMsg struct{ channels []Channel }
259
260type errMsg struct{ err error }
261
262func login(handle string, secret string) tea.Cmd {
263 return func() tea.Msg {
264 hdl, err := syntax.ParseHandle(handle)
265 if err != nil {
266 err = errors.New("handle failed to parse: " + err.Error())
267 return errMsg{err}
268 }
269 id, err := identity.DefaultDirectory().LookupHandle(context.Background(), hdl)
270 if err != nil {
271 err = errors.New("handle failed to loopup: " + err.Error())
272 return errMsg{err}
273 }
274 xrpc := NewPasswordClient(id.DID.String(), id.PDSEndpoint())
275 err = xrpc.CreateSession(context.Background(), handle, secret)
276 if err != nil {
277 return errMsg{err}
278 }
279 return loggedInMsg{xrpc}
280 }
281}
282
283type loggedInMsg struct {
284 xrpc *PasswordClient
285}
286
287func (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
296func (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 m.gsd.state = DialingChannel
344 return m, m.dialingChannel(msg.value)
345
346 case loginMsg:
347 if len(msg.value) == 2 {
348 return m, login(msg.value[0], msg.value[1])
349 }
350 case loggedInMsg:
351 m.gsd.xrpc = msg.xrpc
352 return m, nil
353
354 case setMsg:
355 key, val, found := strings.Cut(msg.value, "=")
356 if !found {
357 return m, nil
358 }
359 switch key {
360 case "color", "c":
361 var b uint32
362
363 if len(val) == 7 && val[0] == '#' {
364 b64, err := strconv.ParseUint(val[1:], 16, 0)
365 if err != nil {
366 return m, nil
367 }
368 b = uint32(b64)
369 } else {
370 i, err := strconv.Atoi(val)
371 if err != nil {
372 return m, nil
373 }
374 b = uint32(i)
375 }
376 m.gsd.color = &b
377 if m.cm != nil {
378 m.cm.draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(&b))
379 }
380 m.cm.updateLRCIdentity()
381 return m, nil
382 case "nick", "name", "n":
383 m.gsd.nick = &val
384 if m.cm != nil {
385 m.cm.draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
386 m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1
387 }
388 m.cm.updateLRCIdentity()
389 return m, nil
390 case "handle", "h", "at", "@":
391 m.gsd.handle = &val
392 if m.cm != nil {
393 m.cm.draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
394 m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1
395 }
396 m.cm.updateLRCIdentity()
397 return m, nil
398 }
399
400 case tea.WindowSizeMsg:
401 m.gsd.height = msg.Height
402 m.gsd.width = msg.Width
403 m.prompt.Width = msg.Width - 2
404 if m.clm != nil {
405 m.clm.list.SetSize(msg.Width, msg.Height-1)
406 }
407 if m.cm != nil {
408 m.cm.vp.Width = msg.Width
409 m.cm.vp.Height = msg.Height - 2
410 m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1
411 if m.cm.render != nil {
412 for _, message := range m.cm.msgs {
413 message.renderMessage(msg.Width)
414 }
415 m.cm.vp.SetContent(JoinDeref(m.cm.render, ""))
416 }
417 }
418 return m, nil
419 }
420
421 switch m.gsd.state {
422 case Splash:
423 return m.updateSplash(msg)
424 case GettingChannels:
425 return m.updateGettingChannels(msg)
426 case ChannelList:
427 clm, cmd, err := m.clm.updateChannelList(msg)
428 if err != nil {
429 m.gsd.state = Error
430 m.error = &err
431 return m, nil
432 }
433 m.clm = &clm
434 return m, cmd
435 case ResolvingChannel:
436 return m.updateResolvingChannel(msg)
437 case ConnectingToChannel:
438 return m.updateConnectingToChannel(msg)
439 case DialingChannel:
440 return m.updateDialingChannel(msg)
441
442 case Connected:
443 cm, cmd, err := m.cm.updateConnected(msg)
444 if err != nil {
445 m.gsd.state = Error
446 m.error = &err
447 return m, nil
448 }
449 m.cm = &cm
450 return m, cmd
451 }
452
453 return m, nil
454}
455
456func (cm channelmodel) updateConnected(msg tea.Msg) (channelmodel, tea.Cmd, error) {
457 switch msg := msg.(type) {
458 case lrcEvent:
459 if msg.e == nil {
460 return cm, nil, errors.New("nil lrcEvent")
461 }
462 id := msg.e.Id
463 switch msg := msg.e.Msg.(type) {
464 case *lrcpb.Event_Ping:
465 return cm, nil, nil
466 case *lrcpb.Event_Pong:
467 return cm, nil, nil
468 case *lrcpb.Event_Init:
469 err := initMessage(msg.Init, cm.msgs, &cm.render, cm.gsd.width)
470 if err != nil {
471 return cm, nil, err
472 }
473 if msg.Init.Echoed != nil && *msg.Init.Echoed {
474 cm.myid = msg.Init.Id
475 }
476 ab := cm.vp.AtBottom()
477 cm.vp.SetContent(JoinDeref(cm.render, ""))
478 if ab {
479 cm.vp.GotoBottom()
480 }
481 return cm, nil, nil
482 case *lrcpb.Event_Pub:
483 err := pubMessage(msg.Pub, cm.msgs, cm.gsd.width)
484 if err != nil {
485 return cm, nil, err
486 }
487 cm.vp.SetContent(JoinDeref(cm.render, ""))
488 return cm, nil, err
489 case *lrcpb.Event_Insert:
490 err := insertMessage(msg.Insert, cm.msgs, &cm.render, cm.gsd.width)
491 if err != nil {
492 return cm, nil, err
493 }
494 ab := cm.vp.AtBottom()
495 cm.vp.SetContent(JoinDeref(cm.render, ""))
496 if ab {
497 cm.vp.GotoBottom()
498 }
499 return cm, nil, nil
500 case *lrcpb.Event_Delete:
501 err := deleteMessage(msg.Delete, cm.msgs, &cm.render, cm.gsd.width)
502 if err != nil {
503 return cm, nil, err
504 }
505 ab := cm.vp.AtBottom()
506 cm.vp.SetContent(JoinDeref(cm.render, ""))
507 if ab {
508 cm.vp.GotoBottom()
509 }
510 return cm, nil, nil
511 case *lrcpb.Event_Mute:
512 return cm, nil, nil
513 case *lrcpb.Event_Unmute:
514 return cm, nil, nil
515 case *lrcpb.Event_Set:
516 return cm, nil, nil
517 case *lrcpb.Event_Get:
518 if msg.Get.Topic != nil {
519 cm.topic = msg.Get.Topic
520 }
521 return cm, nil, nil
522 case *lrcpb.Event_Editbatch:
523 if id == nil {
524 return cm, nil, nil
525 }
526 err := editMessage(*id, msg.Editbatch.Edits, cm.msgs, &cm.render, cm.gsd.width)
527 if err != nil {
528 return cm, nil, err
529 }
530 ab := cm.vp.AtBottom()
531 cm.vp.SetContent(JoinDeref(cm.render, ""))
532 if ab {
533 cm.vp.GotoBottom()
534 }
535 return cm, nil, nil
536 }
537 case tea.KeyMsg:
538 switch cm.mode {
539 case Normal:
540 switch msg.String() {
541 case "i", "a":
542 cm.mode = Insert
543 return cm, cm.draft.Focus(), nil
544 case "I":
545 cm.mode = Insert
546 cm.draft.CursorStart()
547 return cm, cm.draft.Focus(), nil
548 case "A":
549 cm.mode = Insert
550 cm.draft.CursorEnd()
551 return cm, cm.draft.Focus(), nil
552 }
553 case Insert:
554 switch msg.String() {
555 case "esc":
556 cm.mode = Normal
557 cm.draft.Blur()
558 return cm, nil, nil
559 case "enter":
560 if cm.sentmsg != nil {
561 if cm.gsd.xrpc != nil && cm.signeturi != nil {
562 var color64 *uint64
563 if cm.gsd.color != nil {
564 c64 := uint64(*cm.gsd.color)
565 color64 = &c64
566 }
567 lmr := lex.MessageRecord{
568 SignetURI: *cm.signeturi,
569 Body: *cm.sentmsg,
570 Nick: cm.gsd.nick,
571 Color: color64,
572 PostedAt: syntax.DatetimeNow().String(),
573 }
574 cm.draft.SetValue("")
575 cm.sentmsg = nil
576 cm.myid = nil
577 cm.signeturi = nil
578 return cm, tea.Batch(sendPub(cm.lrcconn), createMSGCmd(cm.gsd.xrpc, &lmr)), nil
579 }
580 cm.draft.SetValue("")
581 cm.sentmsg = nil
582 return cm, sendPub(cm.lrcconn), nil
583 }
584 return cm, nil, nil
585 }
586 }
587 }
588 switch cm.mode {
589 case Normal:
590 vp, cmd := cm.vp.Update(msg)
591 cm.vp = vp
592 return cm, cmd, nil
593 case Insert:
594 draft, cmd := cm.draft.Update(msg)
595 if cm.sentmsg == nil && draft.Value() != "" {
596 nv := draft.Value()
597 cm.sentmsg = &nv
598 cm.draft = draft
599 return cm, tea.Batch(cmd, sendInsert(cm.lrcconn, nv, 0, true)), nil
600 }
601 if cm.sentmsg != nil && *cm.sentmsg != draft.Value() {
602 draftutf16 := utf16.Encode([]rune(draft.Value()))
603 sentutf16 := utf16.Encode([]rune(*cm.sentmsg))
604 edits := Diff(sentutf16, draftutf16)
605 cm.draft = draft
606 sentmsg := draft.Value()
607 cm.sentmsg = &sentmsg
608 return cm, tea.Batch(cmd, sendEditBatch(cm.datachan, edits)), nil
609 }
610 cm.draft = draft
611 return cm, cmd, nil
612 }
613 return cm, nil, nil
614}
615
616func createMSGCmd(xrpc *PasswordClient, lmr *lex.MessageRecord) tea.Cmd {
617 return func() tea.Msg {
618 _, _, err := xrpc.CreateXCVRMessage(lmr, context.Background())
619 if err != nil {
620 return errMsg{err}
621 }
622 return nil
623 }
624}
625
626func sendEditBatch(datachan chan []byte, edits []Edit) tea.Cmd {
627 return func() tea.Msg {
628 idx := 0
629 batch := make([]*lrcpb.Edit, 0)
630 for _, edit := range edits {
631 switch edit.EditType {
632 case EditDel:
633 idx2 := idx + len(edit.Utf16Text)
634 evt := makeDelete(uint32(idx), uint32(idx2))
635 edit := lrcpb.Edit{Edit: &lrcpb.Edit_Delete{Delete: evt.GetDelete()}}
636 batch = append(batch, &edit)
637 case EditKeep:
638 idx = idx + len(edit.Utf16Text)
639 case EditAdd:
640 evt := makeInsert(string(utf16.Decode(edit.Utf16Text)), uint32(idx))
641 idx = idx + len(edit.Utf16Text)
642 edit := lrcpb.Edit{Edit: &lrcpb.Edit_Insert{Insert: evt.GetInsert()}}
643 batch = append(batch, &edit)
644 }
645 }
646 evt := lrcpb.Event{Msg: &lrcpb.Event_Editbatch{Editbatch: &lrcpb.EditBatch{Edits: batch}}}
647 data, err := proto.Marshal(&evt)
648 if err != nil {
649 return errMsg{err}
650 }
651 datachan <- data
652 return nil
653 }
654}
655
656func sendPub(conn *websocket.Conn) tea.Cmd {
657 return func() tea.Msg {
658 evt := &lrcpb.Event{Msg: &lrcpb.Event_Pub{Pub: &lrcpb.Pub{}}}
659 data, err := proto.Marshal(evt)
660 if err != nil {
661 return errMsg{err}
662 }
663 err = conn.WriteMessage(websocket.BinaryMessage, data)
664 if err != nil {
665 return errMsg{err}
666 }
667 return nil
668 }
669}
670
671func makeDelete(start uint32, end uint32) *lrcpb.Event {
672 evt := &lrcpb.Event{Msg: &lrcpb.Event_Delete{Delete: &lrcpb.Delete{Utf16Start: start, Utf16End: end}}}
673 return evt
674}
675
676func makeInsert(body string, idx uint32) *lrcpb.Event {
677 evt := &lrcpb.Event{Msg: &lrcpb.Event_Insert{Insert: &lrcpb.Insert{Body: body, Utf16Index: idx}}}
678 return evt
679}
680
681func sendInsert(conn *websocket.Conn, body string, utf16idx uint32, init bool) tea.Cmd {
682 return func() tea.Msg {
683 if init {
684 evt := &lrcpb.Event{Msg: &lrcpb.Event_Init{Init: &lrcpb.Init{}}}
685 data, err := proto.Marshal(evt)
686 if err != nil {
687 return errMsg{err}
688 }
689 if conn == nil {
690 return nil
691 }
692 err = conn.WriteMessage(websocket.BinaryMessage, data)
693 if err != nil {
694 return errMsg{err}
695 }
696 }
697 evt := &lrcpb.Event{Msg: &lrcpb.Event_Insert{Insert: &lrcpb.Insert{Body: body, Utf16Index: utf16idx}}}
698 data, err := proto.Marshal(evt)
699 if err != nil {
700 return errMsg{err}
701 }
702 err = conn.WriteMessage(websocket.BinaryMessage, data)
703 if err != nil {
704 return errMsg{err}
705 }
706 return nil
707 }
708}
709
710func (m model) evaluateCommand(command string) tea.Cmd {
711 return func() tea.Msg {
712 parts := strings.Split(command, " ")
713 if parts == nil {
714 return nil
715 }
716 switch parts[0] {
717 case "q":
718 return tea.QuitMsg{}
719 case "se", "set":
720 if len(parts) != 1 {
721 return setMsg{parts[1]}
722 }
723 case "resize":
724 return tea.WindowSize()
725 case "login":
726 if len(parts) != 1 {
727 return loginMsg{parts[1:]}
728 }
729 case "dial":
730 if len(parts) != 1 {
731 return dialMsg{parts[1]}
732 }
733 }
734 return nil
735 }
736}
737
738type dialMsg struct {
739 value string
740}
741
742type loginMsg struct {
743 value []string
744}
745
746type setMsg struct {
747 value string
748}
749
750// i think that the type of renders is a bit awkward, but i want deletemessage + friends to just modify the rendered
751// messages slice in place in the event that we create a new msg. i think ideally the way to go is to make a more
752// encapsulated data structure for the map + renders which still allows edits to the messages without requiring
753// rerendering every message
754func deleteMessage(msg *lrcpb.Delete, msgmap map[uint32]*Message, renders *[]*string, width int) error {
755 if msg == nil {
756 return errors.New("no insert")
757 }
758 id := msg.Id
759 if id == nil {
760 return errors.New("no insert id")
761 }
762 m := msgmap[*id]
763 atr := false
764 if m == nil {
765 atr = true
766 renderedDefault := ""
767 m = &Message{
768 nick: nil,
769 handle: nil,
770 color: nil,
771 active: true,
772 text: "",
773 rendered: &renderedDefault,
774 }
775 msgmap[*id] = m
776 }
777 start := msg.Utf16Start
778 end := msg.Utf16End
779 m.text = deleteBtwnUTF16Indices(m.text, start, end)
780 m.renderMessage(width)
781 if atr {
782 *renders = append(*renders, m.rendered)
783 }
784 return nil
785}
786func deleteBtwnUTF16Indices(base string, start uint32, end uint32) string {
787 if end <= start {
788 return base
789 }
790 runes := []rune(base)
791 baseUTF16Units := utf16.Encode(runes)
792 if uint32(len(baseUTF16Units)) < end {
793 end = uint32(len(baseUTF16Units))
794 }
795 if uint32(len(baseUTF16Units)) < start {
796 return base
797 }
798 result := make([]uint16, 0, uint32(len(baseUTF16Units))+start-end)
799 result = append(result, baseUTF16Units[:start]...)
800 result = append(result, baseUTF16Units[end:]...)
801 resultRunes := utf16.Decode(result)
802 return string(resultRunes)
803}
804
805func editMessage(id uint32, edits []*lrcpb.Edit, msgmap map[uint32]*Message, renders *[]*string, width int) error {
806 for _, edit := range edits {
807 switch e := edit.Edit.(type) {
808 case *lrcpb.Edit_Insert:
809 ins := e.Insert
810 ins.Id = &id
811 err := insertMessage(ins, msgmap, renders, width)
812 if err != nil {
813 return err
814 }
815 case *lrcpb.Edit_Delete:
816 del := e.Delete
817 del.Id = &id
818 err := deleteMessage(del, msgmap, renders, width)
819 if err != nil {
820 return err
821 }
822 }
823 }
824 return nil
825}
826
827func insertMessage(msg *lrcpb.Insert, msgmap map[uint32]*Message, renders *[]*string, width int) error {
828 if msg == nil {
829 return errors.New("no insert")
830 }
831 id := msg.Id
832 if id == nil {
833 return errors.New("no insert id")
834 }
835 m := msgmap[*id]
836 atr := false
837 if m == nil {
838 atr = true
839 renderedDefault := ""
840 m = &Message{
841 nick: nil,
842 handle: nil,
843 color: nil,
844 active: true,
845 text: "",
846 rendered: &renderedDefault,
847 }
848 msgmap[*id] = m
849 }
850 idx := msg.Utf16Index
851 body := msg.Body
852 m.text = insertAtUTF16Index(m.text, idx, body)
853
854 m.renderMessage(width)
855 if atr {
856 *renders = append(*renders, m.rendered)
857 }
858 return nil
859}
860
861func insertAtUTF16Index(base string, index uint32, insert string) string {
862 runes := []rune(base)
863 baseUTF16Units := utf16.Encode(runes)
864 if uint32(len(baseUTF16Units)) < index {
865 spacesNeeded := index - uint32(len(baseUTF16Units))
866 padding := strings.Repeat(" ", int(spacesNeeded))
867 base = base + padding
868
869 runes = []rune(base)
870 baseUTF16Units = utf16.Encode(runes)
871 }
872
873 insertRunes := []rune(insert)
874 insertUTF16Units := utf16.Encode(insertRunes)
875 result := make([]uint16, 0, len(baseUTF16Units)+len(insertUTF16Units))
876 result = append(result, baseUTF16Units[:index]...)
877 result = append(result, insertUTF16Units...)
878 result = append(result, baseUTF16Units[index:]...)
879 resultRunes := utf16.Decode(result)
880 return string(resultRunes)
881}
882
883func pubMessage(msg *lrcpb.Pub, msgmap map[uint32]*Message, width int) error {
884 if msg == nil {
885 return errors.New("no pub")
886 }
887 id := msg.Id
888 if id == nil {
889 return errors.New("no pub id")
890 }
891 m := msgmap[*id]
892 if m != nil {
893 m.active = false
894 m.renderMessage(width)
895 }
896 return nil
897}
898
899func initMessage(msg *lrcpb.Init, msgmap map[uint32]*Message, renders *[]*string, width int) error {
900 if msg == nil {
901 return errors.New("beeped tf up")
902 }
903 id := msg.Id
904 if id == nil {
905 return errors.New("beeped up")
906 }
907 renderedDefault := ""
908 m := &Message{
909 nick: msg.Nick,
910 handle: msg.ExternalID,
911 color: msg.Color,
912 active: true,
913 text: "",
914 rendered: &renderedDefault,
915 }
916 m.renderMessage(width)
917 msgmap[*id] = m
918 *renders = append(*renders, m.rendered)
919 return nil
920}
921
922func (m *Message) renderMessage(width int) {
923 if m == nil {
924 return
925 }
926 stylem := lipgloss.NewStyle().Width(width).Align(lipgloss.Left)
927 styleh := stylem.Foreground(ColorFromInt(m.color))
928 if m.active {
929 styleh = styleh.Reverse(true)
930 stylem = styleh
931 }
932 header := styleh.Render(renderName(m.nick, m.handle))
933 body := stylem.Render(m.text)
934 *m.rendered = fmt.Sprintf("%s\n%s\n", header, body)
935}
936
937func (m model) updateConnectingToChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
938 switch msg := msg.(type) {
939 case connMsg:
940 m.gsd.state = Connected
941 cm := channelmodel{}
942 cm.wsurl = msg.wsurl
943 cm.gsd = m.gsd
944 cm.cancel = msg.cancel
945 cm.msgs = make(map[uint32]*Message)
946 vp := viewport.New(m.gsd.width, m.gsd.height-2)
947 cm.vp = vp
948 draft := textinput.New()
949 draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
950 draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color))
951 draft.Placeholder = "press i to start typing"
952 draft.Width = m.gsd.width - len(draft.Prompt) - 1
953 cm.draft = draft
954 go startLRCHandlers(msg.conn, m.gsd.nick, m.gsd.handle, m.gsd.color)
955 cm.lrcconn = msg.conn
956 cm.lexconn = msg.lexconn
957 cm.datachan = make(chan []byte)
958 go listenToLexConn(msg.lexconn)
959 go LRCWriter(cm.lrcconn, cm.datachan)
960 m.cm = &cm
961 m.clm = nil
962 return m, nil
963 }
964 return m, nil
965}
966
967func (m model) updateDialingChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
968 switch msg := msg.(type) {
969 case connSimpleMsg:
970 m.gsd.state = Connected
971 cm := channelmodel{}
972 cm.gsd = m.gsd
973 cm.cancel = msg.cancel
974 cm.msgs = make(map[uint32]*Message)
975 vp := viewport.New(m.gsd.width, m.gsd.height-2)
976 cm.vp = vp
977 draft := textinput.New()
978 draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " "
979 draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color))
980 draft.Placeholder = "press i to start typing"
981 draft.Width = m.gsd.width - len(draft.Prompt) - 1
982 cm.draft = draft
983 go startLRCHandlers(msg.conn, m.gsd.nick, m.gsd.handle, m.gsd.color)
984 cm.lrcconn = msg.conn
985 cm.datachan = make(chan []byte)
986 cm.wsurl = msg.wsurl
987 m.cm = &cm
988 m.clm = nil
989 go LRCWriter(cm.lrcconn, cm.datachan)
990 }
991 return m, nil
992}
993
994func LRCWriter(conn *websocket.Conn, datachan chan []byte) {
995 for data := range datachan {
996 err := conn.WriteMessage(websocket.BinaryMessage, data)
997 if err != nil {
998 send(errMsg{err})
999 return
1000 }
1001 }
1002}
1003
1004func renderName(nick *string, handle *string) string {
1005 var n string
1006 if nick != nil {
1007 n = *nick
1008 }
1009 var h string
1010 if handle != nil {
1011 h = fmt.Sprintf("@%s", *handle)
1012 }
1013 return fmt.Sprintf("%s%s", n, h)
1014}
1015
1016func sendSet(datachan chan []byte, nick *string, handle *string, color *uint32) error {
1017 evt := &lrcpb.Event{Msg: &lrcpb.Event_Set{Set: &lrcpb.Set{Nick: nick, ExternalID: handle, Color: color}}}
1018 data, err := proto.Marshal(evt)
1019 if err != nil {
1020 return err
1021 }
1022 datachan <- data
1023 return nil
1024
1025}
1026
1027func startLRCHandlers(conn *websocket.Conn, nick *string, handle *string, color *uint32) {
1028 if conn == nil {
1029 send(errMsg{errors.New("provided nil conn")})
1030 return
1031 }
1032 evt := &lrcpb.Event{Msg: &lrcpb.Event_Set{Set: &lrcpb.Set{Nick: nick, ExternalID: handle, Color: color}}}
1033 data, err := proto.Marshal(evt)
1034 if err != nil {
1035 send(errMsg{errors.New("failed to marshal: " + err.Error())})
1036 return
1037 }
1038 conn.WriteMessage(websocket.BinaryMessage, data)
1039
1040 bep := "bep"
1041 evt = &lrcpb.Event{Msg: &lrcpb.Event_Get{Get: &lrcpb.Get{Topic: &bep}}}
1042 data, err = proto.Marshal(evt)
1043 if err != nil {
1044 send(errMsg{errors.New("failed to marshal: " + err.Error())})
1045 return
1046 }
1047 conn.WriteMessage(websocket.BinaryMessage, data)
1048 go listenToConn(conn)
1049}
1050
1051type typedJSON struct {
1052 Type string `json:"$type"`
1053}
1054
1055func listenToLexConn(conn *websocket.Conn) {
1056 for {
1057 var rawMsg json.RawMessage
1058 err := conn.ReadJSON(&rawMsg)
1059 if err != nil {
1060 send(errMsg{err})
1061 return
1062 }
1063 var typed typedJSON
1064 err = json.Unmarshal(rawMsg, &typed)
1065 if err != nil {
1066 send(errMsg{err})
1067 return
1068 }
1069 switch typed.Type {
1070 case "org.xcvr.lrc.defs#signetView":
1071 var sv SignetView
1072 err = json.Unmarshal(rawMsg, &sv)
1073 if err != nil {
1074 send(errMsg{err})
1075 return
1076 }
1077 send(svMsg{&sv})
1078 }
1079 }
1080}
1081
1082type svMsg struct {
1083 signetView *SignetView
1084}
1085
1086type SignetView struct {
1087 Type string `json:"$type,const=org.xcvr.lrc.defs#signetView"`
1088 URI string `json:"uri"`
1089 IssuerHandle string `json:"issuerHandle"`
1090 ChannelURI string `json:"channelURI"`
1091 LrcId uint32 `json:"lrcID"`
1092 AuthorHandle string `json:"authorHandle"`
1093 StartedAt time.Time `json:"startedAt"`
1094}
1095
1096func listenToConn(conn *websocket.Conn) {
1097 for {
1098 _, data, err := conn.ReadMessage()
1099 if err != nil {
1100 send(errMsg{err})
1101 }
1102 var e lrcpb.Event
1103 err = proto.Unmarshal(data, &e)
1104 send(lrcEvent{&e})
1105 }
1106}
1107
1108type lrcEvent struct{ e *lrcpb.Event }
1109type connlistenerexitMsg struct{}
1110type connwriterexitMsg struct{}
1111
1112func (m model) updateResolvingChannel(msg tea.Msg) (tea.Model, tea.Cmd) {
1113 switch msg := msg.(type) {
1114 case resolutionMsg:
1115 wsurl := fmt.Sprintf("%s%s", strings.TrimPrefix(msg.channelhost, "https://"), msg.resolution.URL)
1116 m.gsd.state = ConnectingToChannel
1117 ctx, cancel := context.WithCancel(context.Background())
1118 return m, m.connectToChannel(ctx, cancel, wsurl)
1119 }
1120 return m, nil
1121}
1122
1123func (m model) dialingChannel(url string) tea.Cmd {
1124 return func() tea.Msg {
1125 dialer := websocket.DefaultDialer
1126 dialer.Subprotocols = []string{"lrc.v1"}
1127 ctx, cancel := context.WithCancel(context.Background())
1128 conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", url), http.Header{})
1129 if err != nil {
1130 cancel()
1131 return errMsg{err}
1132 }
1133 return connSimpleMsg{conn, cancel, url}
1134 }
1135}
1136
1137type connSimpleMsg struct {
1138 conn *websocket.Conn
1139 cancel func()
1140 wsurl string
1141}
1142
1143func (m model) connectToChannel(ctx context.Context, cancel func(), wsurl string) tea.Cmd {
1144 return func() tea.Msg {
1145 dialer := websocket.DefaultDialer
1146 dialer.Subprotocols = []string{"lrc.v1"}
1147 conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", wsurl), http.Header{})
1148 if err != nil {
1149 return errMsg{err}
1150 }
1151
1152 dialer = websocket.DefaultDialer
1153 c := m.clm.curchannel()
1154 var uri string
1155 if c != nil {
1156 uri = c.URI
1157 }
1158 lexconn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://xcvr.org/xrpc/org.xcvr.lrc.subscribeLexStream?uri=%s", uri), http.Header{})
1159 if err != nil {
1160 return errMsg{err}
1161 }
1162 return connMsg{conn, lexconn, cancel, wsurl}
1163 }
1164}
1165
1166type connMsg struct {
1167 conn *websocket.Conn
1168 lexconn *websocket.Conn
1169 cancel func()
1170 wsurl string
1171}
1172
1173const (
1174 bullet = "•"
1175 ellipsis = "…"
1176)
1177
1178func (m model) updateGettingChannels(msg tea.Msg) (tea.Model, tea.Cmd) {
1179 switch msg := msg.(type) {
1180 case channelsMsg:
1181 clm := channellistmodel{}
1182 items := make([]list.Item, 0, len(msg.channels))
1183 for _, channel := range msg.channels {
1184 items = append(items, ChannelItem{channel})
1185 }
1186 list := list.New(items, ChannelItemDelegate{}, m.gsd.width, m.gsd.height-1)
1187 list.Styles = defaultStyles()
1188 list.Title = "org.xcvr.feed.getChannels"
1189 clm.list = list
1190 m.gsd.state = ChannelList
1191 clm.gsd = m.gsd
1192 m.clm = &clm
1193 return m, nil
1194 }
1195 return m, nil
1196}
1197
1198func (clm channellistmodel) curchannel() *Channel {
1199 switch i := clm.list.SelectedItem().(type) {
1200 case ChannelItem:
1201 return &i.channel
1202 }
1203 return nil
1204}
1205
1206func (clm channellistmodel) updateChannelList(msg tea.Msg) (channellistmodel, tea.Cmd, error) {
1207 switch msg := msg.(type) {
1208 case tea.KeyMsg:
1209 switch msg.String() {
1210 case "enter":
1211 if clm.list.FilterState() == list.Filtering {
1212 break
1213 }
1214 clm.gsd.state = ResolvingChannel
1215 cc := clm.curchannel()
1216 if cc != nil {
1217 uri := cc.URI
1218 did, _ := DidFromUri(uri)
1219 rkey, err := RkeyFromUri(uri)
1220 if err != nil {
1221 return clm, nil, err
1222 }
1223 return clm, ResolveChannel(cc.Host, did, rkey), nil
1224 } else {
1225 err := errors.New("bad list type")
1226 return clm, nil, err
1227 }
1228 }
1229 }
1230 list, cmd := clm.list.Update(msg)
1231 clm.list = list
1232 return clm, cmd, nil
1233}
1234
1235func ResolveChannel(host string, did string, rkey string) tea.Cmd {
1236 return func() tea.Msg {
1237 c := &http.Client{Timeout: 10 * time.Second}
1238 var res *http.Response
1239 var err error
1240 if strings.HasPrefix(host, "did:web:") {
1241 res, err = c.Get(fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(host, "did:web:")))
1242 } else if !strings.HasPrefix(host, "did:plc") {
1243 res, err = c.Get(fmt.Sprintf("https://plc.directory/%s", host))
1244 } else {
1245 err = errors.New("unsupported method")
1246 }
1247 if err != nil {
1248 return errMsg{err}
1249 }
1250 dec := json.NewDecoder(res.Body)
1251 var diddoc struct {
1252 Services []struct {
1253 Id string `json:"id"`
1254 ServiceEndpoint string `json:"serviceEndpoint"`
1255 } `json:"service"`
1256 }
1257 dec.Decode(&diddoc)
1258 if diddoc.Services == nil {
1259 return errMsg{errors.New("no services")}
1260
1261 }
1262 var url string
1263 for _, s := range diddoc.Services {
1264 if s.Id != "#xcvr_ch" {
1265 continue
1266 }
1267 url = s.ServiceEndpoint
1268 }
1269 if url == "" {
1270 return errMsg{errors.New("no serviceEndpoint")}
1271 }
1272 res, err = c.Get(fmt.Sprintf("%s/xrpc/org.xcvr.actor.resolveChannel?did=%s&rkey=%s", url, did, rkey))
1273
1274 if res.StatusCode != 200 {
1275 return errMsg{errors.New(fmt.Sprintf("error resolving channel: %d", res.StatusCode))}
1276 }
1277 decoder := json.NewDecoder(res.Body)
1278 var resolution Resolution
1279 err = decoder.Decode(&resolution)
1280 if err != nil {
1281 return errMsg{err}
1282 }
1283 return resolutionMsg{resolution, url}
1284 }
1285}
1286
1287type resolutionMsg struct {
1288 resolution Resolution
1289 channelhost string
1290}
1291
1292type Resolution struct {
1293 URL string `json:"url"`
1294 URI *string `json:"uri,omitempty"`
1295}
1296
1297func (m model) View() string {
1298 var pv string
1299 if m.cmding {
1300 pv = m.prompt.View()
1301 }
1302 switch m.gsd.state {
1303 case Splash:
1304 return m.splashView()
1305 case GettingChannels:
1306 return "loading..."
1307 case Error:
1308 if m.error != nil {
1309 return (*m.error).Error()
1310 }
1311 return "broke so bad there isn't an error"
1312 case ChannelList:
1313 return m.clm.channelListView(m.cmding, pv)
1314 case ResolvingChannel:
1315 return "resolving channel"
1316 case DialingChannel:
1317 return "dialing channel"
1318 case ConnectingToChannel:
1319 return m.connectingView()
1320 case Connected:
1321 return m.cm.connectedView(m.cmding, pv)
1322 default:
1323 return "under construction"
1324 }
1325}
1326
1327func (cm channelmodel) connectedView(cmding bool, prompt string) string {
1328 vpt := cm.vp.View()
1329 var footer string
1330 if cmding {
1331 footer = prompt
1332 } else {
1333 address := "lrc://"
1334 address = fmt.Sprintf("%s%s", address, cm.wsurl)
1335 var topic string
1336 if cm.topic != nil {
1337 topic = *cm.topic
1338 }
1339 remainingspace := cm.gsd.width - len(address) - len(topic)
1340 var footertext string
1341 if remainingspace < 1 {
1342 addressremaining := cm.gsd.width - len(address)
1343 if addressremaining < 0 {
1344 footertext = strings.Repeat(" ", cm.gsd.width)
1345 } else {
1346 footertext = fmt.Sprintf("%s%s", address, strings.Repeat(" ", cm.gsd.width-len(address)))
1347 }
1348 } else {
1349 footertext = fmt.Sprintf("%s%s%s", address, strings.Repeat(" ", remainingspace), topic)
1350 }
1351 insert := cm.mode == Insert
1352 footerstyle := lipgloss.NewStyle().Reverse(insert)
1353 footerstyle = footerstyle.Foreground(ColorFromInt(cm.gsd.color))
1354 footer = footerstyle.Render(footertext)
1355 }
1356 draftText := cm.draft.View()
1357 return fmt.Sprintf("%s\n%s\n%s", vpt, draftText, footer)
1358}
1359
1360func (m model) connectingView() string {
1361 return "resolving channel\nconnecting to channel"
1362}
1363
1364func (clm channellistmodel) channelListView(cmding bool, prompt string) string {
1365 lv := clm.list.View()
1366 cv := ""
1367 if cmding {
1368 cv = prompt
1369 }
1370 return fmt.Sprintf("%s\n%s", lv, cv)
1371}
1372
1373func (m model) splashView() string {
1374 style := lipgloss.NewStyle().Foreground(Green)
1375 part00 := "\n ⣰⡀ ⢀⣀ ⡇ ⡇⡠ ⣰⡀ ⢀⡀ ⡀⢀ ⢀⡀ ⡀⢀ ⡇"
1376 part01 := "\n ⠘⠤ ⠣⠼ ⠣ ⠏⠢ ⠘⠤ ⠣⠜ ⣑⡺ ⠣⠜ ⠣⠼ ⠅"
1377 part02 := "\n ⣰⡀ ⡀⣀ ⢀⣀ ⣀⡀ ⢀⣀ ⢀⣀ ⢀⡀ ⠄ ⡀⢀ ⢀⡀ ⡀⣀"
1378 part03 := "\n ⠘⠤ ⠏ ⠣⠼ ⠇⠸ ⠭⠕ ⠣⠤ ⠣⠭ ⠇ ⠱⠃ ⠣⠭ ⠏"
1379 part1 := "\n\n %%%%%%% "
1380 text1 := "tty!xcvr\n"
1381 part2 := ` %%%%%%%%%% %%% % % %%%%%%%%
1382 %%%%%%%%%%%%%%%%% %%%% %%%%%%%%%%%%%%%%%%
1383 %%%%%%%%%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%%%%%%%%%%%
1384 %%%%%%%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%%%%%%%%
1385 %%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%%%%%%
1386 %%%%%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%
1387 %%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%
1388 %%%%%%%%%%%% %%%%%%%%%%%%%%%%`
1389 part25 := "\n %%%%% "
1390 text2 := `made with`
1391 part3 := " %%%%%%%%%\n"
1392 text3 := " love by moth11."
1393
1394 part4 := " %\n\n"
1395
1396 text4 := ` talk to you!
1397 transceiver
1398 press a key
1399 to start!
1400 `
1401 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)
1402 offset := lipgloss.NewStyle().MarginLeft((m.gsd.width - 58) / 2)
1403 return offset.Render(s)
1404}
1405
1406var send func(msg tea.Msg)
1407
1408func main() {
1409 fmt.Println("if you can see me before program quits i think that you should find a better terminal,")
1410 p := tea.NewProgram(initialModel(), tea.WithAltScreen())
1411 send = p.Send
1412 if _, err := p.Run(); err != nil {
1413 fmt.Printf("Alas, there's been an error: %v", err)
1414 os.Exit(1)
1415 }
1416 fmt.Println("kthxbai!")
1417}
1418
1419func DidFromUri(uri string) (did string, err error) {
1420 s, err := trimScheme(uri)
1421 if err != nil {
1422 return
1423 }
1424 ss, err := uriFragSplit(s)
1425 if err != nil {
1426 return
1427 }
1428 did = ss[0]
1429 return
1430}
1431
1432func trimScheme(uri string) (string, error) {
1433 s, ok := strings.CutPrefix(uri, "at://")
1434 if !ok {
1435 return "", errors.New("not a uri, missing at:// scheme")
1436 }
1437 return s, nil
1438}
1439
1440func uriFragSplit(urifrag string) ([]string, error) {
1441 ss := strings.Split(urifrag, "/")
1442 if len(ss) != 3 {
1443 return nil, errors.New("not a urifrag, incorrect number of bits")
1444 }
1445 return ss, nil
1446}
1447
1448func RkeyFromUri(uri string) (rkey string, err error) {
1449 s, err := trimScheme(uri)
1450 if err != nil {
1451 return
1452 }
1453 ss, err := uriFragSplit(s)
1454 if err != nil {
1455 return
1456 }
1457 rkey = ss[2]
1458 return
1459}
1460
1461func defaultStyles() (s list.Styles) {
1462 s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 0, 0) //nolint:mnd
1463
1464 s.Title = lipgloss.NewStyle().
1465 Foreground(subduedColor).
1466 Padding(0, 0)
1467
1468 s.Spinner = lipgloss.NewStyle().
1469 Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"})
1470
1471 s.FilterPrompt = lipgloss.NewStyle().
1472 Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"})
1473
1474 s.FilterCursor = lipgloss.NewStyle().
1475 Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"})
1476
1477 s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true)
1478
1479 s.StatusBar = lipgloss.NewStyle().
1480 Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
1481 Padding(0, 0, 0, 0) //nolint:mnd
1482
1483 s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor)
1484
1485 s.StatusBarActiveFilter = lipgloss.NewStyle().
1486 Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
1487
1488 s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor)
1489
1490 s.NoItems = lipgloss.NewStyle().
1491 Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"})
1492
1493 s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor)
1494
1495 s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:mnd
1496
1497 s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) //nolint:mnd
1498
1499 s.ActivePaginationDot = lipgloss.NewStyle().
1500 Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}).
1501 SetString(bullet)
1502
1503 s.InactivePaginationDot = lipgloss.NewStyle().
1504 Foreground(verySubduedColor).
1505 SetString(bullet)
1506
1507 s.DividerDot = lipgloss.NewStyle().
1508 Foreground(verySubduedColor).
1509 SetString(" " + bullet + " ")
1510
1511 return s
1512}
1513
1514const maxInt = int(^uint(0) >> 1)
1515
1516func JoinDeref(elems []*string, sep string) string {
1517 switch len(elems) {
1518 case 0:
1519 return ""
1520 case 1:
1521 return *elems[0]
1522 }
1523
1524 var n int
1525 if len(sep) > 0 {
1526 if len(sep) >= maxInt/(len(elems)-1) {
1527 panic("strings: Join output length overflow")
1528 }
1529 n += len(sep) * (len(elems) - 1)
1530 }
1531 for _, elem := range elems {
1532 if len(*elem) > maxInt-n {
1533 panic("strings: Join output length overflow")
1534 }
1535 n += len(*elem)
1536 }
1537
1538 var b strings.Builder
1539 b.Grow(n)
1540 b.WriteString(*elems[0])
1541 for _, s := range elems[1:] {
1542 b.WriteString(sep)
1543 b.WriteString(*s)
1544 }
1545 return b.String()
1546}
1547
1548type PasswordClient struct {
1549 xrpc *client.APIClient
1550 accessjwt *string
1551 refreshjwt *string
1552 did *string
1553}
1554
1555func NewPasswordClient(did string, host string) *PasswordClient {
1556 return &PasswordClient{
1557 xrpc: client.NewAPIClient(host),
1558 did: &did,
1559 }
1560}
1561
1562func (c *PasswordClient) CreateSession(ctx context.Context, identity string, secret string) error {
1563 input := atproto.ServerCreateSession_Input{
1564 Identifier: identity,
1565 Password: secret,
1566 }
1567 var out atproto.ServerCreateSession_Output
1568 err := c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.server.createSession", nil, input, &out)
1569 if err != nil {
1570 return errors.New("I couldn't create a session: " + err.Error())
1571 }
1572 c.accessjwt = &out.AccessJwt
1573 c.refreshjwt = &out.RefreshJwt
1574 return nil
1575}
1576
1577func (c *PasswordClient) RefreshSession(ctx context.Context) error {
1578 c.xrpc.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", *c.refreshjwt))
1579 var out atproto.ServerRefreshSession_Output
1580 err := c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.server.refreshSession", nil, nil, &out)
1581 if err != nil {
1582 return errors.New("failed to refresh session! " + err.Error())
1583 }
1584 c.accessjwt = &out.AccessJwt
1585 c.refreshjwt = &out.RefreshJwt
1586 return nil
1587}
1588
1589func (c *PasswordClient) CreateXCVRMessage(message *lex.MessageRecord, ctx context.Context) (cid string, uri string, err error) {
1590 input := atproto.RepoCreateRecord_Input{
1591 Collection: "org.xcvr.lrc.message",
1592 Repo: *c.did,
1593 Record: &util.LexiconTypeDecoder{Val: message},
1594 }
1595 return c.createMyRecord(input, ctx)
1596}
1597
1598func (c *PasswordClient) createMyRecord(input atproto.RepoCreateRecord_Input, ctx context.Context) (cid string, uri string, err error) {
1599 if c.accessjwt == nil {
1600 err = errors.New("must create a session first")
1601 return
1602 }
1603 c.xrpc.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", *c.accessjwt))
1604 var out atproto.RepoCreateRecord_Output
1605 err = c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.repo.createRecord", nil, input, &out)
1606 if err != nil {
1607 err1 := err.Error()
1608 err = c.RefreshSession(ctx)
1609 if err != nil {
1610 err = errors.New(fmt.Sprintf("failed to refresh session while creating %s! first %s then %s", input.Collection, err1, err.Error()))
1611 return
1612 }
1613 c.xrpc.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", *c.accessjwt))
1614 out = atproto.RepoCreateRecord_Output{}
1615 err = c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.repo.createRecord", nil, input, &out)
1616 if err != nil {
1617 err = errors.New(fmt.Sprintf("not good, failed to create %s after failing then refreshing session! first %s then %s", input.Collection, err1, err.Error()))
1618 return
1619 }
1620 cid = out.Cid
1621 uri = out.Uri
1622 return
1623 }
1624 cid = out.Cid
1625 uri = out.Uri
1626 return
1627}
1628
1629func ColorFromInt(c *uint32) lipgloss.Color {
1630 if c == nil {
1631 return Green
1632 }
1633 ui := *c
1634 guess := fmt.Sprintf("#%06x", ui)
1635 return lipgloss.Color(guess[0:7])
1636}