diff options
| -rw-r--r-- | xA/xA.go | 271 | 
1 files changed, 241 insertions, 30 deletions
| @@ -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()  } | 
