aboutsummaryrefslogtreecommitdiff
path: root/xA/xA.go
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2024-11-09 08:30:16 +0100
committerPřemysl Eric Janouch <p@janouch.name>2024-11-09 17:20:19 +0100
commit4048a346dfa6b84e3b1542872b64b29c69f119f4 (patch)
tree84297576e47d901d4f04196450bf910a98b741ad /xA/xA.go
parent9796c5c24a4ca2ffb901adbbf7268f350cc2fff7 (diff)
downloadxK-4048a346dfa6b84e3b1542872b64b29c69f119f4.tar.gz
xK-4048a346dfa6b84e3b1542872b64b29c69f119f4.tar.xz
xK-4048a346dfa6b84e3b1542872b64b29c69f119f4.zip
WIP: xA: port more stuff
Diffstat (limited to 'xA/xA.go')
-rw-r--r--xA/xA.go481
1 files changed, 327 insertions, 154 deletions
diff --git a/xA/xA.go b/xA/xA.go
index 0149fde..438c9c4 100644
--- a/xA/xA.go
+++ b/xA/xA.go
@@ -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()
}