aboutsummaryrefslogtreecommitdiff
path: root/xA
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2024-11-04 07:40:00 +0100
committerPřemysl Eric Janouch <p@janouch.name>2024-11-09 17:20:18 +0100
commit498dfdfca47085c00ad7d60b03c510050c39d512 (patch)
tree04cfd97184b8d67e43c35f0d39dc1cf7a5fe553c /xA
parentbb26887492a6ac5844b85f60b03fbefc34f529a4 (diff)
downloadxK-498dfdfca47085c00ad7d60b03c510050c39d512.tar.gz
xK-498dfdfca47085c00ad7d60b03c510050c39d512.tar.xz
xK-498dfdfca47085c00ad7d60b03c510050c39d512.zip
WIP: xA: connection prototype
Diffstat (limited to 'xA')
-rw-r--r--xA/xA.go271
1 files changed, 241 insertions, 30 deletions
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()
}