diff options
Diffstat (limited to 'xA')
-rw-r--r-- | xA/Makefile | 3 | ||||
-rw-r--r-- | xA/xA.go | 481 |
2 files changed, 329 insertions, 155 deletions
diff --git a/xA/Makefile b/xA/Makefile index 070f5ad..0a1b90e 100644 --- a/xA/Makefile +++ b/xA/Makefile @@ -18,7 +18,8 @@ FyneApp.toml: ../xK-version GenericName = 'IRC Client'\n\ Categories = ['Network', 'Chat', 'IRCClient']\n" > $@ xA: xA.go proto.go ../xK-version - go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ + go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ \ + -gcflags=all="-N -l" proto.go: $(tools)/lxdrgen.awk $(tools)/lxdrgen-go.awk ../xC.lxdr $(AWK) -f $(tools)/lxdrgen.awk -f $(tools)/lxdrgen-go.awk \ -v PrefixCamel=Relay ../xC.lxdr > $@ @@ -13,7 +13,6 @@ import ( "io" "log" "net" - "net/url" "os" "slices" "time" @@ -182,6 +181,7 @@ var ( // Widgets: + wWindow fyne.Window wRichText *widget.RichText wRichScroll *container.Scroll wEntry *widget.Entry @@ -189,6 +189,142 @@ var ( // ----------------------------------------------------------------------------- +func beep() { + // TODO(p): Probably implement using https://github.com/ebitengine/oto + // and a sample generated from the Makefile like with xW. +} + +// --- Networking -------------------------------------------------------------- + +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 + } + b := make([]byte, length) + if _, err := io.ReadFull(r, b); err != nil { + log.Println("Event receive failed: " + err.Error()) + 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) + + j, err := m.MarshalJSON() + if err != nil { + log.Println("Event marshalling failed: " + err.Error()) + return + } + + log.Printf("<- %s\n", j) + } + return m, true +} + +func relayWriteMessage(conn net.Conn, data RelayCommandData) bool { + m := RelayCommandMessage{ + CommandSeq: commandSeq, + Data: data, + } + 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 relaySend(data RelayCommandData, callback callback) { + if callback != nil { + commandCallbacks[commandSeq] = callback + } + + // TODO(p): Get the net.Conn from somewhere. + //relayWriteMessage(nil, data) +} + +// --- Buffers ----------------------------------------------------------------- + +func bufferByName(name string) *buffer { + for i := range buffers { + if buffers[i].bufferName == name { + return &buffers[i] + } + } + return nil +} + +func bufferActivate(name string) { + relaySend(RelayCommandData{ + Variant: &RelayCommandDataBufferActivate{BufferName: name}, + }, nil) +} + +func bufferToggleUnimportant(name string) { + relaySend(RelayCommandData{ + Variant: &RelayCommandDataBufferToggleUnimportant{BufferName: name}, + }, nil) +} + +// --- Current buffer ---------------------------------------------------------- + +func bufferAtBottom() bool { + // TODO(p): Figure out how to implement this. + return false +} + +func bufferScrollToBottom() { + // XXX: Doing it once is not reliable, something's amiss. + // (In particular, nothing happens when we switch from an empty buffer + // to a buffer than needs scrolling.) + wRichScroll.ScrollToBottom() + wRichScroll.ScrollToBottom() +} + +// --- UI state refresh -------------------------------------------------------- + +func refreshIcon() { + // TODO(p): We can have an icon. +} + +func refreshTopic(topic []bufferLineItem) { + // TODO(p): First off, add a topic, second, refresh it. +} + +func refreshBufferList() { + // TODO(p): First off, add a buffer list, second, refresh it. +} + +func refreshPrompt() { + // TODO(p): First off, add a prompt, second, refresh it. +} + +func refreshStatus() { + // TODO(p): First off, add a status, second, refresh it. +} + +// --- RichText formatting ----------------------------------------------------- + func defaultBufferLineItem() bufferLineItem { return bufferLineItem{} } func convertItemFormatting( @@ -243,18 +379,103 @@ func convertItems(items []RelayItemData) []bufferLineItem { return result } -// ----------------------------------------------------------------------------- +// --- Buffer output ----------------------------------------------------------- + +func convertBufferLine(m *RelayEventDataBufferLine) bufferLine { + return bufferLine{ + items: convertItems(m.Items), + isUnimportant: m.IsUnimportant, + isHighlight: m.IsHighlight, + rendition: m.Rendition, + when: time.UnixMilli(int64(m.When)), + } +} + +func bufferPrintDateChange(sameline *bool, last, current time.Time) { + last, current = last.Local(), current.Local() + if last.Year() == current.Year() && + last.Month() == current.Month() && + last.Day() == current.Day() { + return + } + + // TODO(p): Also print it. + // XXX: Not sure if "sameline" is appropriate with RichText. + _ = sameline + _ = current.Format(time.DateOnly) + *sameline = false +} + +func bufferPrintAndWatchTrailingDateChanges() { + current := time.Now() + b := bufferByName(bufferCurrent) + if b != nil && len(b.lines) != 0 { + last := b.lines[len(b.lines)-1].when + sameline := len(wRichText.Segments) == 0 + bufferPrintDateChange(&sameline, last, current) + } + + // TODO(p): The watching part. +} func bufferPrintLine(lines []bufferLine, index int) { - // TODO(p): Date change. line := &lines[index] - // TODO(p): Timestamp. - line.when.Format("\n15:04:05") - - // TODO(p): Rendition. + last, current := time.Time{}, line.when + if index == 0 { + last = time.Now() + } else { + last = lines[index-1].when + } + + // XXX: Not sure if "sameline" is appropriate with RichText. + sameline := len(wRichText.Segments) == 0 + bufferPrintDateChange(&sameline, last, current) + + // TODO(p): Why don't the colour names work? + texts := []widget.RichTextSegment{&widget.TextSegment{ + Text: line.when.Format("15:04:05 "), + Style: widget.RichTextStyle{ + Alignment: fyne.TextAlignLeading, + ColorName: "", + Inline: true, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{}, + }}} + + // Tabstops won't quite help us here, since we need it centred. + // TODO(p): Why don't the colour names work? prefix := "" - + pcf := widget.RichTextStyle{ + Alignment: fyne.TextAlignLeading, + Inline: true, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{Monospace: true}, + } + switch line.rendition { + case RelayRenditionBare: + case RelayRenditionIndent: + prefix = " " + case RelayRenditionStatus: + prefix = " - " + case RelayRenditionError: + prefix = "=!= " + //pcf.ColorName = theme.ColorRed + case RelayRenditionJoin: + prefix = "--> " + //pcf.ColorName = theme.ColorGreen + case RelayRenditionPart: + prefix = "<-- " + //pcf.ColorName = theme.ColorRed + case RelayRenditionAction: + prefix = " * " + //pcf.ColorName = theme.ColorRed + } + + // TODO(p): Also scan for URLs, and transform line.items before use. + // &widget.HyperlinkSegment{Text: "X", URL: testURL}, + + // TODO(p): Render timestamps as well. if line.leaked { // TODO(p): Similar as below, but everything greyed out. if prefix != "" { @@ -263,136 +484,57 @@ func bufferPrintLine(lines []bufferLine, index int) { _ = item } } else { - // TODO(p): Render text. if prefix != "" { + texts = append(texts, &widget.TextSegment{ + Text: prefix, + Style: pcf, + }) } for _, item := range line.items { - _ = item + texts = append(texts, &widget.TextSegment{ + Text: item.text, + Style: widget.RichTextStyle{ + Alignment: fyne.TextAlignLeading, + ColorName: item.color, + Inline: true, + SizeName: theme.SizeNameText, + TextStyle: item.format, + }, + }) } } + + wRichText.Segments = append(wRichText.Segments, + &widget.ParagraphSegment{Texts: texts}, + &widget.TextSegment{Style: widget.RichTextStyleParagraph}) } func bufferPrintSeparator() { - // TODO(p) + // TODO(p): Formatting. wRichText.Segments = append(wRichText.Segments, &widget.TextSegment{Style: widget.RichTextStyleParagraph}, &widget.SeparatorSegment{}) } func refreshBuffer(b *buffer) { - // TODO(p): See xW, rewrite the whole buffer view. -} - -func refreshBufferList() { - // TODO(p): First off, add a buffer list, second, refresh it. -} - -func refreshTopic(topic []bufferLineItem) { - // TODO(p): First off, add a topic, second, refresh it. -} - -func refreshStatus() { - // TODO(p): First off, add a status, second, refresh it. -} + wRichText.Segments = nil -func refreshIcon() { - // TODO(p): Can we have an icon at all? -} - -func refreshPrompt() { - // TODO(p): First off, add a prompt, second, refresh it. -} - -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 - } - b := make([]byte, length) - if _, err := io.ReadFull(r, b); err != nil { - log.Println("Event receive failed: " + err.Error()) - 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) - - j, err := m.MarshalJSON() - if err != nil { - log.Println("Event marshalling failed: " + err.Error()) - return + markBefore := len(b.lines) - b.newMessages - b.newUnimportantMessages + for i, line := range b.lines { + if i == markBefore { + bufferPrintSeparator() } - - log.Printf("<- %s\n", j) - } - return m, true -} - -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 RelayEventMessage, 1000) - r := bufio.NewReaderSize(conn, 65536) - go func() { - defer close(p) - for { - m, ok := relayReadMessage(r) - if !ok { - return - } - select { - case p <- m: - case <-ctx.Done(): - return - } + if !line.isUnimportant || !b.hideUnimportant { + bufferPrintLine(b.lines, i) } - }() - return p -} - -func relayWriteMessage(conn net.Conn, data RelayCommandData) bool { - m := RelayCommandMessage{ - CommandSeq: commandSeq, - Data: data, - } - 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 + bufferPrintAndWatchTrailingDateChanges() + wRichText.Refresh() + bufferScrollToBottom() } -func convertBufferLine(m *RelayEventDataBufferLine) bufferLine { - return bufferLine{ - items: convertItems(m.Items), - isUnimportant: m.IsUnimportant, - isHighlight: m.IsHighlight, - rendition: m.Rendition, - when: time.UnixMilli(int64(m.When)), - } -} +// --- Event processing -------------------------------------------------------- func relayProcessBufferLine(b *buffer, m *RelayEventDataBufferLine) { line := convertBufferLine(m) @@ -437,18 +579,19 @@ func relayProcessBufferLine(b *buffer, m *RelayEventDataBufferLine) { } if separate { - // TODO(p) + bufferPrintSeparator() } if display { bufferPrintLine(bc.lines, len(bc.lines)-1) + wRichText.Refresh() } if toBottom { - // TODO(p) + bufferScrollToBottom() } if line.isHighlight || (!visible && !line.isUnimportant && b.kind == RelayBufferKindPrivateMessage) { - // TODO(p): beep(), probably using https://github.com/ebitengine/oto + beep() if !visible { b.highlighted = true @@ -481,15 +624,6 @@ func relayProcessCallbacks( } } -func bufferByName(name string) *buffer { - for i := range buffers { - if buffers[i].bufferName == name { - return &buffers[i] - } - } - return nil -} - func relayProcessMessage(m *RelayEventMessage) { switch data := m.Data.Variant.(type) { case *RelayEventDataError: @@ -498,10 +632,9 @@ func relayProcessMessage(m *RelayEventMessage) { relayProcessCallbacks(data.CommandSeq, "", &data.Data) case *RelayEventDataPing: - // TODO(p): Send the command. - _ = RelayCommandData{ + relaySend(RelayCommandData{ Variant: &RelayCommandDataPingResponse{EventSeq: m.EventSeq}, - } + }, nil) case *RelayEventDataBufferLine: b := bufferByName(data.BufferName) @@ -513,7 +646,7 @@ func relayProcessMessage(m *RelayEventMessage) { case *RelayEventDataBufferUpdate: b := bufferByName(data.BufferName) if b == nil { - buffers = append(buffers, buffer{}) + buffers = append(buffers, buffer{bufferName: data.BufferName}) b = &buffers[len(buffers)-1] refreshBufferList() } @@ -576,7 +709,38 @@ func relayProcessMessage(m *RelayEventMessage) { refreshBufferList() refreshIcon() case *RelayEventDataBufferActivate: - // TODO(p): Process all remaining message kinds. + old := bufferByName(bufferCurrent) + bufferLast = bufferCurrent + bufferCurrent = data.BufferName + b := bufferByName(data.BufferName) + if b == nil { + return + } + + if old != nil { + old.newMessages = 0 + old.newUnimportantMessages = 0 + old.highlighted = false + + old.input = wEntry.Text + // TODO(p): At least store Cursor{Row,Column}. + + // Note that we effectively overwrite the newest line + // with the current textarea contents, and jump there. + old.historyAt = len(old.history) + } + + // TODO(p): Port the rest as well. + + refreshIcon() + refreshTopic(b.topic) + refreshBuffer(b) + refreshPrompt() + refreshStatus() + + // TODO(p): At least set Cursor{Row,Column}, and apply it. + wEntry.SetText(b.input) + wWindow.Canvas().Focus(wEntry) case *RelayEventDataBufferInput: b := bufferByName(data.BufferName) if b == nil { @@ -624,6 +788,31 @@ func relayProcessMessage(m *RelayEventMessage) { } } +// --- Networking -------------------------------------------------------------- + +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 RelayEventMessage, 1000) + r := bufio.NewReaderSize(conn, 65536) + go func() { + defer close(p) + for { + m, ok := relayReadMessage(r) + if !ok { + return + } + select { + case p <- m: + case <-ctx.Done(): + return + } + } + }() + return p +} + func relayRun() { // TODO(p): Maybe reset state, and indicate in the UI that we're connecting. @@ -683,33 +872,17 @@ func main() { a := app.New() a.Settings().SetTheme(&customTheme{}) - w := a.NewWindow(projectName) + wWindow = a.NewWindow(projectName) // TODO(p): There should also be a widget.NewLabel() next to the entry. // - Probably another Border, even though this seems odd. wRichText = widget.NewRichText() + wRichText.Wrapping = fyne.TextWrapWord wRichScroll = container.NewVScroll(wRichText) wEntry = widget.NewMultiLineEntry() - w.SetContent(container.NewBorder(nil, wEntry, nil, nil, wRichScroll)) - - testURL, _ := url.Parse("https://x.com") - wRichText.Segments = []widget.RichTextSegment{ - &widget.ParagraphSegment{Texts: []widget.RichTextSegment{ - &widget.TextSegment{Text: "Test"}, - &widget.HyperlinkSegment{Text: "X", URL: testURL}, - &widget.TextSegment{ - Text: " is a website, certainly", - Style: widget.RichTextStyleInline, - }, - }}, - &widget.TextSegment{Style: widget.RichTextStyleParagraph}, - &widget.SeparatorSegment{}, - &widget.TextSegment{Text: "Paragraph"}, - } - wRichText.Wrapping = fyne.TextWrapWord - wRichText.Refresh() + wWindow.SetContent(container.NewBorder(nil, wEntry, nil, nil, wRichScroll)) go relayRun() - w.ShowAndRun() + wWindow.ShowAndRun() } |