aboutsummaryrefslogtreecommitdiff
path: root/xA/xA.go
diff options
context:
space:
mode:
Diffstat (limited to 'xA/xA.go')
-rw-r--r--xA/xA.go320
1 files changed, 290 insertions, 30 deletions
diff --git a/xA/xA.go b/xA/xA.go
index 0e2cf36..4d4a5f0 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,214 @@ var (
debug = flag.Bool("debug", false, "enable debug output")
projectName = "xA"
projectVersion = "?"
- addressConnect string
+)
+
+// --- Theme -------------------------------------------------------------------
+
+type customTheme struct{}
+
+func convertColor(c int) color.Color {
+ base16 := []uint16{
+ 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
+ 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
+ }
+ if c < 16 {
+ r := 0xf & uint8(base16[c]>>8)
+ g := 0xf & uint8(base16[c]>>4)
+ b := 0xf & uint8(base16[c])
+ return color.RGBA{r * 0x11, g * 0x11, b * 0x11, 0xff}
+ }
+ if c >= 216 {
+ return color.Gray{8 + uint8(c-216)*10}
+ }
+
+ var (
+ i = uint8(c - 16)
+ r = i / 36 >> 0
+ g = (i / 6 >> 0) % 6
+ b = i % 6
+ )
+ if r != 0 {
+ r = 55 + 40*r
+ }
+ if g != 0 {
+ g = 55 + 40*g
+ }
+ if b != 0 {
+ b = 55 + 40*b
+ }
+ return color.RGBA{r, g, b, 0xff}
+}
+
+var ircColors = make(map[fyne.ThemeColorName]color.Color)
+
+func ircColorName(color int) fyne.ThemeColorName {
+ return fyne.ThemeColorName(fmt.Sprintf("irc%02x", color))
+}
+
+func init() {
+ for color := 0; color < 256; color++ {
+ ircColors[ircColorName(color)] = convertColor(color)
+ }
+}
+
+func (t *customTheme) Color(
+ name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
+ // Fuck this low contrast shit, text must be black.
+ if name == theme.ColorNameForeground &&
+ variant == theme.VariantLight {
+ return color.Black
+ }
+
+ // TODO(p): Consider constants for stuff like timestamps.
+ if c, ok := ircColors[name]; ok {
+ return c
+ }
+ 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 {
+ format fyne.TextStyle
+ // For RichTextStyle.ColorName.
+ // XXX: Fyne's RichText doesn't support background colours.
+ color fyne.ThemeColorName
+ 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 +244,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 +359,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 +386,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()
}