xcvr tui

some refactors etc

+410 -306
+410 -306
main.go
··· 51 51 ChannelList 52 52 ResolvingChannel 53 53 ConnectingToChannel 54 + DialingChannel 54 55 Connected 55 56 ) 56 57 ··· 58 59 59 60 const ( 60 61 Normal txmode = iota 61 - Command 62 62 Insert 63 63 ) 64 64 65 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 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 92 108 } 93 109 94 110 type Message struct { ··· 187 203 func initialModel() model { 188 204 prompt := textinput.New() 189 205 prompt.Prompt = ":" 206 + prompt.Width = 28 //: + prompt.Width + 1 left over for blinky = initialWidth 190 207 nick := "wanderer" 191 208 color := uint32(33096) 192 - return model{ 209 + gsd := globalsettingsdata{ 210 + nick: &nick, 211 + color: &color, 212 + width: 30, 213 + height: 20, 193 214 state: Splash, 194 - mode: Normal, 215 + } 216 + return model{ 195 217 prompt: prompt, 196 - width: 30, 197 - height: 20, 198 - nick: &nick, 199 - color: &color, 218 + gsd: &gsd, 200 219 } 201 220 } 202 221 func (m model) Init() tea.Cmd { ··· 210 229 case "q": 211 230 return m, tea.Quit 212 231 default: 213 - m.state = GettingChannels 232 + m.gsd.state = GettingChannels 214 233 return m, GetChannels 215 234 } 216 235 } ··· 265 284 xrpc *PasswordClient 266 285 } 267 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 + 268 296 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 269 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 + } 270 333 case errMsg: 271 - m.state = Error 334 + m.gsd.state = Error 272 335 m.error = &msg.err 273 336 return m, nil 274 337 case svMsg: 275 - if m.myid != nil && msg.signetView.LrcId == *m.myid { 276 - m.signeturi = &msg.signetView.URI 338 + if m.cm != nil && m.cm.myid != nil && msg.signetView.LrcId == *m.cm.myid { 339 + m.cm.signeturi = &msg.signetView.URI 277 340 return m, nil 278 341 } 342 + case dialMsg: 343 + if len(msg.value) == 1 { 344 + m.gsd.state = DialingChannel 345 + return m, m.dialingChannel(msg.value) 346 + } 279 347 280 348 case loginMsg: 281 349 if len(msg.value) == 2 { 282 350 return m, login(msg.value[0], msg.value[1]) 283 351 } 284 352 case loggedInMsg: 285 - m.xrpc = msg.xrpc 353 + m.gsd.xrpc = msg.xrpc 286 354 return m, nil 287 355 288 356 case setMsg: ··· 292 360 } 293 361 switch key { 294 362 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)) 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) 303 377 } 304 - err = sendSet(m.evtchan, m.nick, m.handle, m.color) 305 - if err != nil { 306 - send(errMsg{err}) 378 + m.gsd.color = &b 379 + if m.cm != nil { 380 + m.cm.draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(&b)) 307 381 } 382 + m.cm.updateLRCIdentity() 308 383 return m, nil 309 384 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}) 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 318 389 } 390 + m.cm.updateLRCIdentity() 319 391 return m, nil 320 392 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 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 325 397 } 326 - err := sendSet(m.evtchan, m.nick, m.handle, m.color) 327 - if err != nil { 328 - send(errMsg{err}) 329 - } 398 + m.cm.updateLRCIdentity() 330 399 return m, nil 331 400 } 332 401 333 402 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 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) 339 408 } 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) 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, "")) 346 418 } 347 - m.vp.SetContent(JoinDeref(m.renders, "")) 348 - } 349 - if m.list != nil { 350 - m.list.SetSize(msg.Width, msg.Height) 351 419 } 352 420 return m, nil 353 - 354 - case tea.KeyMsg: 355 - switch msg.String() { 356 - case "ctrl+c": 357 - return m, tea.Quit 358 - } 359 421 } 360 422 361 - switch m.state { 423 + switch m.gsd.state { 362 424 case Splash: 363 425 return m.updateSplash(msg) 364 426 case GettingChannels: 365 427 return m.updateGettingChannels(msg) 366 428 case ChannelList: 367 - return m.updateChannelList(msg) 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 368 437 case ResolvingChannel: 369 438 return m.updateResolvingChannel(msg) 370 439 case ConnectingToChannel: 371 440 return m.updateConnectingToChannel(msg) 441 + case DialingChannel: 442 + 372 443 case Connected: 373 - return m.updateConnected(msg) 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 374 452 } 375 453 376 454 return m, nil 377 455 } 378 456 379 - func (m model) updateConnected(msg tea.Msg) (tea.Model, tea.Cmd) { 457 + func (cm channelmodel) updateConnected(msg tea.Msg) (channelmodel, tea.Cmd, error) { 380 458 switch msg := msg.(type) { 381 459 case lrcEvent: 382 460 if msg.e == nil { 383 - m.state = Error 384 - err := errors.New("nil lrcEvent") 385 - m.error = &err 386 - return m, nil 461 + return cm, nil, errors.New("nil lrcEvent") 387 462 } 388 463 id := msg.e.Id 389 464 switch msg := msg.e.Msg.(type) { 390 465 case *lrcpb.Event_Ping: 391 - return m, nil 466 + return cm, nil, nil 392 467 case *lrcpb.Event_Pong: 393 - return m, nil 468 + return cm, nil, nil 394 469 case *lrcpb.Event_Init: 395 - err := initMessage(msg.Init, m.msgs, &m.renders, m.width) 470 + err := initMessage(msg.Init, cm.msgs, &cm.render, cm.gsd.width) 396 471 if err != nil { 397 - m.state = Error 398 - m.error = &err 399 - return m, nil 472 + return cm, nil, err 400 473 } 401 474 if msg.Init.Echoed != nil && *msg.Init.Echoed { 402 - m.myid = msg.Init.Id 475 + cm.myid = msg.Init.Id 403 476 } 404 - ab := m.vp.AtBottom() 405 - m.vp.SetContent(JoinDeref(m.renders, "")) 477 + ab := cm.vp.AtBottom() 478 + cm.vp.SetContent(JoinDeref(cm.render, "")) 406 479 if ab { 407 - m.vp.GotoBottom() 480 + cm.vp.GotoBottom() 408 481 } 409 - return m, nil 482 + return cm, nil, nil 410 483 case *lrcpb.Event_Pub: 411 - err := pubMessage(msg.Pub, m.msgs, m.width) 484 + err := pubMessage(msg.Pub, cm.msgs, cm.gsd.width) 412 485 if err != nil { 413 - m.state = Error 414 - m.error = &err 415 - return m, nil 486 + return cm, nil, err 416 487 } 417 - m.vp.SetContent(JoinDeref(m.renders, "")) 418 - return m, nil 488 + cm.vp.SetContent(JoinDeref(cm.render, "")) 489 + return cm, nil, err 419 490 case *lrcpb.Event_Insert: 420 - err := insertMessage(msg.Insert, m.msgs, &m.renders, m.width) 491 + err := insertMessage(msg.Insert, cm.msgs, &cm.render, cm.gsd.width) 421 492 if err != nil { 422 - m.state = Error 423 - m.error = &err 424 - return m, nil 493 + return cm, nil, err 425 494 } 426 - ab := m.vp.AtBottom() 427 - m.vp.SetContent(JoinDeref(m.renders, "")) 495 + ab := cm.vp.AtBottom() 496 + cm.vp.SetContent(JoinDeref(cm.render, "")) 428 497 if ab { 429 - m.vp.GotoBottom() 498 + cm.vp.GotoBottom() 430 499 } 431 - return m, nil 500 + return cm, nil, nil 432 501 case *lrcpb.Event_Delete: 433 - err := deleteMessage(msg.Delete, m.msgs, &m.renders, m.width) 502 + err := deleteMessage(msg.Delete, cm.msgs, &cm.render, cm.gsd.width) 434 503 if err != nil { 435 - m.state = Error 436 - m.error = &err 437 - return m, nil 504 + return cm, nil, err 438 505 } 439 - ab := m.vp.AtBottom() 440 - m.vp.SetContent(JoinDeref(m.renders, "")) 506 + ab := cm.vp.AtBottom() 507 + cm.vp.SetContent(JoinDeref(cm.render, "")) 441 508 if ab { 442 - m.vp.GotoBottom() 509 + cm.vp.GotoBottom() 443 510 } 444 - return m, nil 511 + return cm, nil, nil 445 512 case *lrcpb.Event_Mute: 446 - return m, nil 513 + return cm, nil, nil 447 514 case *lrcpb.Event_Unmute: 448 - return m, nil 515 + return cm, nil, nil 449 516 case *lrcpb.Event_Set: 450 - return m, nil 517 + return cm, nil, nil 451 518 case *lrcpb.Event_Get: 452 519 if msg.Get.Topic != nil { 453 - m.topic = msg.Get.Topic 520 + cm.topic = msg.Get.Topic 454 521 } 455 - return m, nil 522 + return cm, nil, nil 456 523 case *lrcpb.Event_Editbatch: 457 524 if id == nil { 458 - return m, nil 525 + return cm, nil, nil 459 526 } 460 - err := editMessage(*id, msg.Editbatch.Edits, m.msgs, &m.renders, m.width) 527 + err := editMessage(*id, msg.Editbatch.Edits, cm.msgs, &cm.render, cm.gsd.width) 461 528 if err != nil { 462 - m.state = Error 463 - m.error = &err 464 - return m, nil 529 + return cm, nil, err 465 530 } 466 - ab := m.vp.AtBottom() 467 - m.vp.SetContent(JoinDeref(m.renders, "")) 531 + ab := cm.vp.AtBottom() 532 + cm.vp.SetContent(JoinDeref(cm.render, "")) 468 533 if ab { 469 - m.vp.GotoBottom() 534 + cm.vp.GotoBottom() 470 535 } 471 - return m, nil 536 + return cm, nil, nil 472 537 } 473 538 case tea.KeyMsg: 474 - switch m.mode { 539 + switch cm.mode { 475 540 case Normal: 476 541 switch msg.String() { 477 542 case "i", "a": 478 - m.mode = Insert 479 - return m, m.draft.Focus() 543 + cm.mode = Insert 544 + return cm, cm.draft.Focus(), nil 480 545 case "I": 481 - m.mode = Insert 482 - m.draft.CursorStart() 483 - return m, m.draft.Focus() 546 + cm.mode = Insert 547 + cm.draft.CursorStart() 548 + return cm, cm.draft.Focus(), nil 484 549 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() 550 + cm.mode = Insert 551 + cm.draft.CursorEnd() 552 + return cm, cm.draft.Focus(), nil 491 553 } 492 554 case Insert: 493 555 switch msg.String() { 494 556 case "esc": 495 - m.mode = Normal 496 - m.draft.Blur() 497 - return m, nil 557 + cm.mode = Normal 558 + cm.draft.Blur() 559 + return cm, nil, nil 498 560 case "enter": 499 - if m.sentmsg != nil { 500 - if m.xrpc != nil && m.signeturi != nil { 561 + if cm.sentmsg != nil { 562 + if cm.gsd.xrpc != nil && cm.signeturi != nil { 501 563 var color64 *uint64 502 - if m.color != nil { 503 - c64 := uint64(*m.color) 564 + if cm.gsd.color != nil { 565 + c64 := uint64(*cm.gsd.color) 504 566 color64 = &c64 505 567 } 506 568 lmr := lex.MessageRecord{ 507 - SignetURI: *m.signeturi, 508 - Body: *m.sentmsg, 509 - Nick: m.nick, 569 + SignetURI: *cm.signeturi, 570 + Body: *cm.sentmsg, 571 + Nick: cm.gsd.nick, 510 572 Color: color64, 511 573 PostedAt: syntax.DatetimeNow().String(), 512 574 } 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)) 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 518 580 } 519 - m.draft.SetValue("") 520 - m.sentmsg = nil 521 - return m, sendPub(m.lrcconn) 581 + cm.draft.SetValue("") 582 + cm.sentmsg = nil 583 + return cm, sendPub(cm.lrcconn), nil 522 584 } 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: 585 + return cm, nil, nil 539 586 } 540 587 } 541 588 } 542 - switch m.mode { 589 + switch cm.mode { 543 590 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 591 + vp, cmd := cm.vp.Update(msg) 592 + cm.vp = vp 593 + return cm, cmd, nil 551 594 case Insert: 552 - draft, cmd := m.draft.Update(msg) 553 - if m.sentmsg == nil && draft.Value() != "" { 595 + draft, cmd := cm.draft.Update(msg) 596 + if cm.sentmsg == nil && draft.Value() != "" { 554 597 nv := draft.Value() 555 - m.sentmsg = &nv 556 - m.draft = &draft 557 - return m, tea.Batch(cmd, sendInsert(m.lrcconn, nv, 0, true)) 598 + cm.sentmsg = &nv 599 + cm.draft = draft 600 + return cm, tea.Batch(cmd, sendInsert(cm.lrcconn, nv, 0, true)), nil 558 601 } 559 - if m.sentmsg != nil && *m.sentmsg != draft.Value() { 602 + if cm.sentmsg != nil && *cm.sentmsg != draft.Value() { 560 603 draftutf16 := utf16.Encode([]rune(draft.Value())) 561 - sentutf16 := utf16.Encode([]rune(*m.sentmsg)) 604 + sentutf16 := utf16.Encode([]rune(*cm.sentmsg)) 562 605 edits := Diff(sentutf16, draftutf16) 563 - m.draft = &draft 606 + cm.draft = draft 564 607 sentmsg := draft.Value() 565 - m.sentmsg = &sentmsg 566 - return m, tea.Batch(cmd, sendEditBatch(m.evtchan, edits)) 608 + cm.sentmsg = &sentmsg 609 + return cm, tea.Batch(cmd, sendEditBatch(cm.datachan, edits)), nil 567 610 } 568 - m.draft = &draft 569 - return m, cmd 611 + cm.draft = draft 612 + return cm, cmd, nil 570 613 } 571 - return m, nil 614 + return cm, nil, nil 572 615 } 573 616 574 617 func createMSGCmd(xrpc *PasswordClient, lmr *lex.MessageRecord) tea.Cmd { ··· 665 708 } 666 709 } 667 710 668 - func evaluateCommand(command string) tea.Cmd { 711 + func (m model) evaluateCommand(command string) tea.Cmd { 669 712 return func() tea.Msg { 670 713 parts := strings.Split(command, " ") 671 714 if parts == nil { ··· 684 727 if len(parts) != 1 { 685 728 return loginMsg{parts[1:]} 686 729 } 730 + case "dial": 731 + if len(parts) != 1 { 732 + return dialMsg{parts[1]} 733 + } 687 734 } 688 735 return nil 689 736 } 737 + } 738 + 739 + type dialMsg struct { 740 + value string 690 741 } 691 742 692 743 type loginMsg struct { ··· 887 938 func (m model) updateConnectingToChannel(msg tea.Msg) (tea.Model, tea.Cmd) { 888 939 switch msg := msg.(type) { 889 940 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 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 895 949 draft := textinput.New() 896 - draft.Prompt = renderName(m.nick, m.handle) + " " 897 - draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.color)) 950 + draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " " 951 + draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color)) 898 952 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) 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 906 963 return m, nil 907 964 } 908 965 return m, nil 909 966 } 910 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 + 911 991 func LRCWriter(conn *websocket.Conn, datachan chan []byte) { 912 992 for data := range datachan { 913 993 err := conn.WriteMessage(websocket.BinaryMessage, data) ··· 941 1021 942 1022 } 943 1023 944 - func startLRCHandlers(conn *websocket.Conn, lexconn *websocket.Conn, nick *string, handle *string, color *uint32) { 1024 + func startLRCHandlers(conn *websocket.Conn, nick *string, handle *string, color *uint32) { 945 1025 if conn == nil { 946 1026 send(errMsg{errors.New("provided nil conn")}) 947 1027 return ··· 963 1043 } 964 1044 conn.WriteMessage(websocket.BinaryMessage, data) 965 1045 go listenToConn(conn) 966 - go listenToLexConn(lexconn) 967 1046 } 968 1047 969 1048 type typedJSON struct { ··· 1030 1109 func (m model) updateResolvingChannel(msg tea.Msg) (tea.Model, tea.Cmd) { 1031 1110 switch msg := msg.(type) { 1032 1111 case resolutionMsg: 1033 - wsurl := fmt.Sprintf("%s%s", m.curchannel.Host, msg.resolution.URL) 1034 - m.wsurl = &wsurl 1035 - m.state = ConnectingToChannel 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 1036 1119 ctx, cancel := context.WithCancel(context.Background()) 1037 - return m, m.connectToChannel(ctx, cancel) 1120 + return m, m.connectToChannel(ctx, cancel, wsurl) 1038 1121 } 1039 1122 return m, nil 1040 1123 } 1041 1124 1042 - func (m model) connectToChannel(ctx context.Context, cancel func()) tea.Cmd { 1125 + func (m model) dialingChannel(url string) tea.Cmd { 1043 1126 return func() tea.Msg { 1044 1127 dialer := websocket.DefaultDialer 1045 1128 dialer.Subprotocols = []string{"lrc.v1"} 1046 - if m.wsurl == nil { 1047 - return errMsg{errors.New("nil wsurl!")} 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} 1048 1134 } 1049 - conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", *m.wsurl), http.Header{}) 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{}) 1050 1149 if err != nil { 1051 1150 return errMsg{err} 1052 1151 } 1053 1152 1054 1153 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{}) 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{}) 1056 1160 if err != nil { 1057 1161 return errMsg{err} 1058 1162 } 1059 - return connMsg{conn, lexconn, cancel} 1163 + return connMsg{conn, lexconn, cancel, wsurl} 1060 1164 } 1061 1165 } 1062 1166 ··· 1064 1168 conn *websocket.Conn 1065 1169 lexconn *websocket.Conn 1066 1170 cancel func() 1171 + wsurl string 1067 1172 } 1068 1173 1069 1174 const ( ··· 1074 1179 func (m model) updateGettingChannels(msg tea.Msg) (tea.Model, tea.Cmd) { 1075 1180 switch msg := msg.(type) { 1076 1181 case channelsMsg: 1182 + clm := channellistmodel{} 1077 1183 items := make([]list.Item, 0, len(msg.channels)) 1078 1184 for _, channel := range msg.channels { 1079 1185 items = append(items, ChannelItem{channel}) 1080 1186 } 1081 - list := list.New(items, ChannelItemDelegate{}, m.width, m.height) 1187 + list := list.New(items, ChannelItemDelegate{}, m.gsd.width, m.gsd.height-1) 1082 1188 list.Styles = defaultStyles() 1083 1189 list.Title = "org.xcvr.feed.getChannels" 1084 - m.list = &list 1085 - m.state = ChannelList 1190 + clm.list = list 1191 + m.gsd.state = ChannelList 1192 + clm.gsd = m.gsd 1193 + m.clm = &clm 1086 1194 return m, nil 1087 1195 } 1088 1196 return m, nil 1089 1197 } 1090 1198 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 1199 + func (clm channellistmodel) curchannel() *Channel { 1200 + switch i := clm.list.SelectedItem().(type) { 1201 + case ChannelItem: 1202 + return &i.channel 1097 1203 } 1204 + return nil 1205 + } 1206 + 1207 + func (clm channellistmodel) updateChannelList(msg tea.Msg) (channellistmodel, tea.Cmd, error) { 1098 1208 switch msg := msg.(type) { 1099 1209 case tea.KeyMsg: 1100 1210 switch msg.String() { 1101 1211 case "enter": 1102 - m.state = ResolvingChannel 1103 - i, ok := m.list.SelectedItem().(ChannelItem) 1104 - if ok { 1105 - uri := i.URI() 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 1106 1219 did, _ := DidFromUri(uri) 1107 1220 rkey, err := RkeyFromUri(uri) 1108 1221 if err != nil { 1109 - m.error = &err 1110 - m.state = Error 1111 - return m, nil 1222 + return clm, nil, err 1112 1223 } 1113 - m.curchannel = &i.channel 1114 - m.list = nil 1115 - m.channels = nil 1116 - return m, ResolveChannel(i.Host(), did, rkey) 1224 + return clm, ResolveChannel(cc.Host, did, rkey), nil 1117 1225 } else { 1118 1226 err := errors.New("bad list type") 1119 - m.error = &err 1120 - m.state = Error 1121 - return m, nil 1227 + return clm, nil, err 1122 1228 } 1123 1229 } 1124 1230 } 1125 - list, cmd := m.list.Update(msg) 1126 - m.list = &list 1127 - return m, cmd 1231 + list, cmd := clm.list.Update(msg) 1232 + clm.list = list 1233 + return clm, cmd, nil 1128 1234 } 1129 1235 1130 1236 func ResolveChannel(host string, did string, rkey string) tea.Cmd { ··· 1158 1264 } 1159 1265 1160 1266 func (m model) View() string { 1161 - switch m.state { 1267 + var pv string 1268 + if m.cmding { 1269 + pv = m.prompt.View() 1270 + } 1271 + switch m.gsd.state { 1162 1272 case Splash: 1163 1273 return m.splashView() 1164 1274 case GettingChannels: ··· 1169 1279 } 1170 1280 return "broke so bad there isn't an error" 1171 1281 case ChannelList: 1172 - return m.channelListView() 1282 + return m.clm.channelListView(m.cmding, pv) 1173 1283 case ResolvingChannel: 1174 1284 return "resolving channel" 1175 1285 case ConnectingToChannel: 1176 1286 return m.connectingView() 1177 1287 case Connected: 1178 - return m.connectedView() 1288 + return m.cm.connectedView(m.cmding, pv) 1179 1289 default: 1180 1290 return "under construction" 1181 1291 } 1182 1292 } 1183 1293 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) 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 + } 1205 1315 } else { 1206 - footertext = fmt.Sprintf("%s%s", address, strings.Repeat(" ", m.width-len(address))) 1316 + footertext = fmt.Sprintf("%s%s%s", address, strings.Repeat(" ", remainingspace), topic) 1207 1317 } 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() 1318 + insert := cm.mode == Insert 1319 + footerstyle := lipgloss.NewStyle().Reverse(insert) 1320 + footerstyle = footerstyle.Foreground(ColorFromInt(cm.gsd.color)) 1321 + footer = footerstyle.Render(footertext) 1220 1322 } 1323 + draftText := cm.draft.View() 1221 1324 return fmt.Sprintf("%s\n%s\n%s", vpt, draftText, footer) 1222 1325 } 1223 1326 1224 1327 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) 1328 + return "resolving channel\nconnecting to channel" 1230 1329 } 1231 1330 1232 - func (m model) channelListView() string { 1233 - return m.list.View() 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) 1234 1338 } 1235 1339 1236 1340 func (m model) splashView() string { ··· 1262 1366 to start! 1263 1367 ` 1264 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) 1265 - offset := lipgloss.NewStyle().MarginLeft((m.width - 58) / 2) 1369 + offset := lipgloss.NewStyle().MarginLeft((m.gsd.width - 58) / 2) 1266 1370 return offset.Render(s) 1267 1371 } 1268 1372