From 498dfdfca47085c00ad7d60b03c510050c39d512 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch
Date: Mon, 4 Nov 2024 07:40:00 +0100 Subject: WIP: xA: connection prototype --- xA/xA.go | 271 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 241 insertions(+), 30 deletions(-) (limited to 'xA') diff --git a/xA/xA.go b/xA/xA.go index 0e2cf36..20d8351 100644 --- a/xA/xA.go +++ b/xA/xA.go @@ -9,15 +9,18 @@ import ( "encoding/binary" "flag" "fmt" + "image/color" "io" "log" "net" "net/url" "os" + "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -25,60 +28,165 @@ var ( debug = flag.Bool("debug", false, "enable debug output") projectName = "xA" projectVersion = "?" - addressConnect string +) + +// --- Theme ------------------------------------------------------------------- + +type customTheme struct{} + +func (t *customTheme) Color( + name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { + if name == theme.ColorNameForeground && + variant == theme.VariantLight { + return color.Black + } + return theme.DefaultTheme().Color(name, variant) +} + +func (t *customTheme) Font(style fyne.TextStyle) fyne.Resource { + return theme.DefaultTheme().Font(style) +} + +func (t *customTheme) Icon(i fyne.ThemeIconName) fyne.Resource { + return theme.DefaultTheme().Icon(i) +} + +func (t *customTheme) Size(s fyne.ThemeSizeName) float32 { + return theme.DefaultTheme().Size(s) +} + +// --- Relay state ------------------------------------------------------------- + +type server struct { + state RelayServerState + user string + userModes string +} + +type bufferLineItem struct { + // TODO(p): Figure out how to store formatting. + // → fyne.TextStyle, however this doesn't store bg or fg colour, + // which is only a widget.{Custom,}TextGridStyle thing. + // - I'll likely have to create 257^2 name combinations, + // plus perhaps add names for "internal" stuff like timestamps. + // - theme.ColorForWidget + text string +} + +type bufferLine struct { + /// Leaked from another buffer, but temporarily staying in another one. + leaked bool + + isUnimportant bool + isHighlight bool + rendition RelayRendition + when time.Time + items []bufferLineItem +} + +type buffer struct { + bufferName string + hideUnimportant bool + kind RelayBufferKind + serverName string + lines []bufferLine + // TODO(p): Server by name or by pointer? + + // Channel: + + topic []bufferLineItem + modes string + + // Stats: + + newMessages int + newUnimportantMessages int + highlighted bool + + // Input: + + input string + inputStart, inputEnd int + history []string + historyAt int +} + +type callback func(err error, response *RelayResponseData) + +var ( + backendAddress string + backendContext context.Context + backendCancel context.CancelFunc + + // Connection state: + + commandSeq uint32 + commandCallbacks map[uint32]callback + + buffers []buffer + bufferCurrent string + bufferLast string + + servers map[string]server + + // Widgets: + + wRichText *widget.RichText + wRichScroll *container.Scroll + wEntry *widget.Entry ) // ----------------------------------------------------------------------------- -func relayReadFrame(r io.Reader) []byte { +func relayReadMessage(r io.Reader) (m RelayEventMessage, ok bool) { var length uint32 if err := binary.Read(r, binary.BigEndian, &length); err != nil { log.Println("Event receive failed: " + err.Error()) - return nil + return } b := make([]byte, length) if _, err := io.ReadFull(r, b); err != nil { log.Println("Event receive failed: " + err.Error()) - return nil + return + } + + if after, ok2 := m.ConsumeFrom(b); !ok2 { + log.Println("Event deserialization failed") + return + } else if len(after) != 0 { + log.Println("Event deserialization failed: trailing data") + return } if *debug { log.Printf(" %v\n", b) - var m RelayEventMessage - if after, ok := m.ConsumeFrom(b); !ok { - log.Println("Event deserialization failed") - return nil - } else if len(after) != 0 { - log.Println("Event deserialization failed: trailing data") - return nil - } - j, err := m.MarshalJSON() if err != nil { log.Println("Event marshalling failed: " + err.Error()) - return nil + return } log.Printf("<- %s\n", j) } - return b + return m, true } -func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte { +func relayMakeReceiver( + ctx context.Context, conn net.Conn) <-chan RelayEventMessage { // The usual event message rarely gets above 1 kilobyte, // thus this is set to buffer up at most 1 megabyte or so. - p := make(chan []byte, 1000) + p := make(chan RelayEventMessage, 1000) r := bufio.NewReaderSize(conn, 65536) go func() { defer close(p) for { - j := relayReadFrame(r) - if j == nil { + m, ok := relayReadMessage(r) + if !ok { return } select { - case p <- j: + case p <- m: case <-ctx.Done(): return } @@ -87,6 +195,108 @@ func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte { return p } +func relayWriteMessage(conn net.Conn, commandData any) bool { + m := RelayCommandMessage{ + CommandSeq: commandSeq, + Data: RelayCommandData{commandData}, + } + commandSeq++ + + b, ok := m.AppendTo(make([]byte, 4)) + if !ok { + log.Println("Command serialization failed") + return false + } + binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4)) + if _, err := conn.Write(b); err != nil { + log.Println("Command send failed: " + err.Error()) + return false + } + + if *debug { + log.Printf("-> %v\n", b) + } + return true +} + +func relayProcessMessage(m *RelayEventMessage) { + switch data := m.Data.Interface.(type) { + case RelayEventDataError: + // TODO(p): Process callbacks. + _ = data.CommandSeq + _ = data.Error + case RelayEventDataResponse: + // TODO(p): Process callbacks. + _ = data.CommandSeq + _ = data.Data + + case RelayEventDataPing: + // TODO(p): Send the command. + _ = RelayCommandDataPingResponse{ + Command: RelayCommandPingResponse, + EventSeq: m.EventSeq, + } + + // TODO(p): Process all remaining message kinds. + case RelayEventDataBufferLine: + case RelayEventDataBufferUpdate: + case RelayEventDataBufferStats: + case RelayEventDataBufferRename: + case RelayEventDataBufferRemove: + case RelayEventDataBufferActivate: + case RelayEventDataBufferInput: + case RelayEventDataBufferClear: + + case RelayEventDataServerUpdate: + case RelayEventDataServerRename: + case RelayEventDataServerRemove: + } + +} + +func relayRun() { + // TODO(p): Maybe reset state, and indicate in the UI that we're connecting. + + backendContext, backendCancel = context.WithCancel(context.Background()) + defer backendCancel() + + conn, err := net.Dial("tcp", backendAddress) + if err != nil { + log.Println("Connection failed: " + err.Error()) + // TODO(p): Display errors to the user. + return + } + defer conn.Close() + + // TODO(p): How to send messages? + // - It would probably make the most sense to have either a chan + // (which makes code synchronize), or a locked slice. + // - But I also need to wake the sender up somehow, + // so maybe use a channel after all. + // - Or maybe use a channel just for the signalling. + // - Sending (semi-)synchronously is also an option, perhaps. + // TODO(p): Handle any errors here. + _ = relayWriteMessage(conn, &RelayCommandDataHello{ + Command: RelayCommandHello, + Version: RelayVersion, + }) + + relayMessages := relayMakeReceiver(backendContext, conn) + for { + select { + case m, ok := <-relayMessages: + if !ok { + break + } + relayProcessMessage(&m) + default: + break + } + } + + // TODO(p): Indicate in the UI that we're no longer connected. +} + func main() { flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), @@ -100,22 +310,21 @@ func main() { os.Exit(1) } - addressConnect = flag.Arg(0) + backendAddress = flag.Arg(0) a := app.New() + a.Settings().SetTheme(&customTheme{}) w := a.NewWindow(projectName) // TODO(p): There should also be a widget.NewLabel() next to the entry. // - Probably another Border, even though this seems odd. - var ( - richtext = widget.NewRichText() - richscroll = container.NewVScroll(richtext) - entry = widget.NewMultiLineEntry() - ) - w.SetContent(container.NewBorder(nil, entry, nil, nil, richscroll)) + wRichText = widget.NewRichText() + wRichScroll = container.NewVScroll(wRichText) + wEntry = widget.NewMultiLineEntry() + w.SetContent(container.NewBorder(nil, wEntry, nil, nil, wRichScroll)) testURL, _ := url.Parse("https://x.com") - richtext.Segments = []widget.RichTextSegment{ + wRichText.Segments = []widget.RichTextSegment{ &widget.ParagraphSegment{Texts: []widget.RichTextSegment{ &widget.TextSegment{Text: "Test"}, &widget.HyperlinkSegment{Text: "X", URL: testURL}, @@ -128,8 +337,10 @@ func main() { &widget.SeparatorSegment{}, &widget.TextSegment{Text: "Paragraph"}, } - richtext.Wrapping = fyne.TextWrapWord - richtext.Refresh() + wRichText.Wrapping = fyne.TextWrapWord + wRichText.Refresh() + + go relayRun() w.ShowAndRun() } -- cgit v1.2.3-70-g09d2