diff options
| -rw-r--r-- | xA/Makefile | 3 | ||||
| -rw-r--r-- | xA/xA.go | 477 | 
2 files changed, 327 insertions, 153 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") +	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{}, +		}}} -	// TODO(p): Rendition. +	// 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. -} +	wRichText.Segments = nil -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. -} - -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()  } | 
