Start moving to mauview
This commit is contained in:
		@@ -23,19 +23,18 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/lithammer/fuzzysearch/fuzzy"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FuzzySearchModal struct {
 | 
			
		||||
	tview.Primitive
 | 
			
		||||
	mauview.Component
 | 
			
		||||
 | 
			
		||||
	search  *tview.InputField
 | 
			
		||||
	results *tview.TextView
 | 
			
		||||
	search  *mauview.InputField
 | 
			
		||||
	results *mauview.TextView
 | 
			
		||||
 | 
			
		||||
	matches  fuzzy.Ranks
 | 
			
		||||
	selected int
 | 
			
		||||
@@ -43,39 +42,39 @@ type FuzzySearchModal struct {
 | 
			
		||||
	roomList   []*rooms.Room
 | 
			
		||||
	roomTitles []string
 | 
			
		||||
 | 
			
		||||
	parent   *GomuksUI
 | 
			
		||||
	mainView *MainView
 | 
			
		||||
	parent *MainView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal {
 | 
			
		||||
	fs := &FuzzySearchModal{
 | 
			
		||||
		parent:   mainView.parent,
 | 
			
		||||
		mainView: mainView,
 | 
			
		||||
		parent: mainView,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fs.InitList(mainView.rooms)
 | 
			
		||||
 | 
			
		||||
	fs.search = tview.NewInputField().
 | 
			
		||||
		SetLabel("Room: ")
 | 
			
		||||
	fs.search.
 | 
			
		||||
		SetChangedFunc(fs.changeHandler).
 | 
			
		||||
		SetInputCapture(fs.keyHandler)
 | 
			
		||||
	fs.search = mauview.NewInputField().SetChangedFunc(fs.changeHandler)
 | 
			
		||||
	wrappedSearch := mauview.NewBox(fs.search).SetKeyCaptureFunc(fs.keyHandler)
 | 
			
		||||
	searchLabel := mauview.NewTextField().SetText("Room")
 | 
			
		||||
	combinedSearch := mauview.NewFlex().
 | 
			
		||||
		SetDirection(mauview.FlexColumn).
 | 
			
		||||
		AddFixedComponent(searchLabel, 5).
 | 
			
		||||
		AddProportionalComponent(wrappedSearch, 1)
 | 
			
		||||
 | 
			
		||||
	fs.results = tview.NewTextView().
 | 
			
		||||
		SetRegions(true)
 | 
			
		||||
	fs.results.SetBorderPadding(1, 0, 0, 0)
 | 
			
		||||
	fs.results = mauview.NewTextView().SetRegions(true)
 | 
			
		||||
 | 
			
		||||
	// Flex widget containing input box and results
 | 
			
		||||
	container := tview.NewFlex().
 | 
			
		||||
		SetDirection(tview.FlexRow).
 | 
			
		||||
		AddItem(fs.search, 1, 0, true).
 | 
			
		||||
		AddItem(fs.results, 0, 1, false)
 | 
			
		||||
	container.
 | 
			
		||||
	container := mauview.NewBox(mauview.NewFlex().
 | 
			
		||||
		SetDirection(mauview.FlexRow).
 | 
			
		||||
		AddFixedComponent(combinedSearch, 1).
 | 
			
		||||
		AddProportionalComponent(fs.results, 1)).
 | 
			
		||||
		SetBorder(true).
 | 
			
		||||
		SetBorderPadding(1, 1, 1, 1).
 | 
			
		||||
		SetTitle("Quick Room Switcher")
 | 
			
		||||
		SetTitle("Quick Room Switcher").
 | 
			
		||||
		SetBlurCaptureFunc(func() bool {
 | 
			
		||||
			fs.parent.HideModal()
 | 
			
		||||
			return true
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	fs.Primitive = widget.TransparentCenter(width, height, container)
 | 
			
		||||
	fs.Component = mauview.Center(container, width, height)
 | 
			
		||||
 | 
			
		||||
	return fs
 | 
			
		||||
}
 | 
			
		||||
@@ -96,7 +95,7 @@ func (fs *FuzzySearchModal) changeHandler(str string) {
 | 
			
		||||
		for _, match := range fs.matches {
 | 
			
		||||
			fmt.Fprintf(fs.results, `["%d"]%s[""]%s`, match.OriginalIndex, match.Target, "\n")
 | 
			
		||||
		}
 | 
			
		||||
		fs.parent.Render()
 | 
			
		||||
		fs.parent.parent.Render()
 | 
			
		||||
		fs.results.Highlight(strconv.Itoa(fs.matches[0].OriginalIndex))
 | 
			
		||||
		fs.results.ScrollToBeginning()
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -105,13 +104,12 @@ func (fs *FuzzySearchModal) changeHandler(str string) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey {
 | 
			
		||||
func (fs *FuzzySearchModal) keyHandler(event mauview.KeyEvent) mauview.KeyEvent {
 | 
			
		||||
	highlights := fs.results.GetHighlights()
 | 
			
		||||
	switch event.Key() {
 | 
			
		||||
	case tcell.KeyEsc:
 | 
			
		||||
		// Close room finder
 | 
			
		||||
		fs.parent.views.RemovePage("fuzzy-search-modal")
 | 
			
		||||
		fs.parent.app.SetFocus(fs.parent.views)
 | 
			
		||||
		fs.parent.HideModal()
 | 
			
		||||
		return nil
 | 
			
		||||
	case tcell.KeyTab:
 | 
			
		||||
		// Cycle highlighted area to next match
 | 
			
		||||
@@ -125,10 +123,9 @@ func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey {
 | 
			
		||||
		// Switch room to currently selected room
 | 
			
		||||
		if len(highlights) > 0 {
 | 
			
		||||
			debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].OriginalIndex].GetTitle())
 | 
			
		||||
			fs.mainView.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex])
 | 
			
		||||
			fs.parent.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex])
 | 
			
		||||
		}
 | 
			
		||||
		fs.parent.views.RemovePage("fuzzy-search-modal")
 | 
			
		||||
		fs.parent.app.SetFocus(fs.parent.views)
 | 
			
		||||
		fs.parent.HideModal()
 | 
			
		||||
		fs.results.Clear()
 | 
			
		||||
		fs.search.SetText("")
 | 
			
		||||
		return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,8 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/config"
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
@@ -38,8 +38,6 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MessageView struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
 | 
			
		||||
	parent *RoomView
 | 
			
		||||
	config *config.Config
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +49,8 @@ type MessageView struct {
 | 
			
		||||
	LoadingMessages bool
 | 
			
		||||
 | 
			
		||||
	widestSender int
 | 
			
		||||
	width        int
 | 
			
		||||
	height       int
 | 
			
		||||
	prevWidth    int
 | 
			
		||||
	prevHeight   int
 | 
			
		||||
	prevMsgCount int
 | 
			
		||||
@@ -65,7 +65,6 @@ type MessageView struct {
 | 
			
		||||
 | 
			
		||||
func NewMessageView(parent *RoomView) *MessageView {
 | 
			
		||||
	return &MessageView{
 | 
			
		||||
		Box:    tview.NewBox(),
 | 
			
		||||
		parent: parent,
 | 
			
		||||
		config: parent.config,
 | 
			
		||||
 | 
			
		||||
@@ -174,7 +173,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag
 | 
			
		||||
 | 
			
		||||
	view.updateWidestSender(message.Sender())
 | 
			
		||||
 | 
			
		||||
	_, _, width, _ := view.GetRect()
 | 
			
		||||
	width := view.width
 | 
			
		||||
	bare := view.config.Preferences.BareMessageView
 | 
			
		||||
	if !bare {
 | 
			
		||||
		width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
@@ -267,15 +266,15 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) recalculateBuffers() {
 | 
			
		||||
	_, _, width, height := view.GetRect()
 | 
			
		||||
	prefs := view.config.Preferences
 | 
			
		||||
	if !prefs.BareMessageView {
 | 
			
		||||
		width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
	}
 | 
			
		||||
	recalculateMessageBuffers := width != view.prevWidth ||
 | 
			
		||||
	recalculateMessageBuffers := view.width != view.prevWidth ||
 | 
			
		||||
		view.prevPrefs.BareMessageView != prefs.BareMessageView ||
 | 
			
		||||
		view.prevPrefs.DisableImages != prefs.DisableImages
 | 
			
		||||
	if recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
 | 
			
		||||
		width := view.width
 | 
			
		||||
		if !prefs.BareMessageView {
 | 
			
		||||
			width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
		}
 | 
			
		||||
		view.textBuffer = []tstring.TString{}
 | 
			
		||||
		view.metaBuffer = []ifc.MessageMeta{}
 | 
			
		||||
		view.prevMsgCount = 0
 | 
			
		||||
@@ -290,8 +289,8 @@ func (view *MessageView) recalculateBuffers() {
 | 
			
		||||
			view.appendBuffer(message)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	view.prevHeight = height
 | 
			
		||||
	view.prevWidth = width
 | 
			
		||||
	view.prevHeight = view.height
 | 
			
		||||
	view.prevWidth = view.width
 | 
			
		||||
	view.prevPrefs = prefs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -343,50 +342,56 @@ func (view *MessageView) handleUsernameClick(message ifc.MessageMeta, prevMessag
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) bool {
 | 
			
		||||
	if button != tcell.Button1 {
 | 
			
		||||
		return false
 | 
			
		||||
func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool {
 | 
			
		||||
	switch event.Buttons() {
 | 
			
		||||
	case tcell.WheelUp:
 | 
			
		||||
		if view.IsAtTop() {
 | 
			
		||||
			go view.parent.parent.LoadHistory(view.parent.Room.ID)
 | 
			
		||||
		} else {
 | 
			
		||||
			view.AddScrollOffset(WheelScrollOffsetDiff)
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	case tcell.WheelDown:
 | 
			
		||||
		view.AddScrollOffset(-WheelScrollOffsetDiff)
 | 
			
		||||
		view.parent.parent.MarkRead(view.parent)
 | 
			
		||||
		return true
 | 
			
		||||
	case tcell.Button1:
 | 
			
		||||
		x, y := event.Position()
 | 
			
		||||
		line := view.TotalHeight() - view.ScrollOffset - view.height + y
 | 
			
		||||
		if line < 0 || line >= view.TotalHeight() {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		message := view.metaBuffer[line]
 | 
			
		||||
		var prevMessage ifc.MessageMeta
 | 
			
		||||
		if y != 0 && line > 0 {
 | 
			
		||||
			prevMessage = view.metaBuffer[line-1]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		usernameX := view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
		messageX := usernameX + view.widestSender + SenderMessageGap
 | 
			
		||||
 | 
			
		||||
		if x >= messageX {
 | 
			
		||||
			return view.handleMessageClick(message)
 | 
			
		||||
		} else if x >= usernameX {
 | 
			
		||||
			return view.handleUsernameClick(message, prevMessage)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, _, _, height := view.GetRect()
 | 
			
		||||
	line := view.TotalHeight() - view.ScrollOffset - height + y
 | 
			
		||||
	if line < 0 || line >= view.TotalHeight() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message := view.metaBuffer[line]
 | 
			
		||||
	var prevMessage ifc.MessageMeta
 | 
			
		||||
	if y != 0 && line > 0 {
 | 
			
		||||
		prevMessage = view.metaBuffer[line-1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usernameX := view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
	messageX := usernameX + view.widestSender + SenderMessageGap
 | 
			
		||||
 | 
			
		||||
	shouldRerender := false
 | 
			
		||||
	if x >= messageX {
 | 
			
		||||
		shouldRerender = view.handleMessageClick(message)
 | 
			
		||||
	} else if x >= usernameX {
 | 
			
		||||
		shouldRerender = view.handleUsernameClick(message, prevMessage)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return shouldRerender
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PaddingAtTop = 5
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) AddScrollOffset(diff int) {
 | 
			
		||||
	_, _, _, height := view.GetRect()
 | 
			
		||||
 | 
			
		||||
	totalHeight := view.TotalHeight()
 | 
			
		||||
	if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - height + PaddingAtTop
 | 
			
		||||
	if diff >= 0 && view.ScrollOffset+diff >= totalHeight-view.height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - view.height + PaddingAtTop
 | 
			
		||||
	} else {
 | 
			
		||||
		view.ScrollOffset += diff
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if view.ScrollOffset > totalHeight-height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - height + PaddingAtTop
 | 
			
		||||
	if view.ScrollOffset > totalHeight-view.height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - view.height + PaddingAtTop
 | 
			
		||||
	}
 | 
			
		||||
	if view.ScrollOffset < 0 {
 | 
			
		||||
		view.ScrollOffset = 0
 | 
			
		||||
@@ -394,8 +399,7 @@ func (view *MessageView) AddScrollOffset(diff int) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) Height() int {
 | 
			
		||||
	_, _, _, height := view.GetRect()
 | 
			
		||||
	return height
 | 
			
		||||
	return view.height
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) TotalHeight() int {
 | 
			
		||||
@@ -403,9 +407,8 @@ func (view *MessageView) TotalHeight() int {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) IsAtTop() bool {
 | 
			
		||||
	_, _, _, height := view.GetRect()
 | 
			
		||||
	totalHeight := len(view.textBuffer)
 | 
			
		||||
	return view.ScrollOffset >= totalHeight-height+PaddingAtTop
 | 
			
		||||
	return view.ScrollOffset >= totalHeight-view.height+PaddingAtTop
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -449,15 +452,14 @@ func (view *MessageView) calculateScrollBar(height int) (scrollBarHeight, scroll
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) getIndexOffset(screen tcell.Screen, height, messageX int) (indexOffset int) {
 | 
			
		||||
func (view *MessageView) getIndexOffset(screen mauview.Screen, height, messageX int) (indexOffset int) {
 | 
			
		||||
	indexOffset = view.TotalHeight() - view.ScrollOffset - height
 | 
			
		||||
	if indexOffset <= -PaddingAtTop {
 | 
			
		||||
		message := "Scroll up to load more messages."
 | 
			
		||||
		if view.LoadingMessages {
 | 
			
		||||
			message = "Loading more messages..."
 | 
			
		||||
		}
 | 
			
		||||
		_, y, _, _ := view.GetRect()
 | 
			
		||||
		widget.WriteLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
 | 
			
		||||
		widget.WriteLineSimpleColor(screen, message, messageX, 0, tcell.ColorGreen)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@@ -488,25 +490,25 @@ func (view *MessageView) CapturePlaintext(height int) string {
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) Draw(screen tcell.Screen) {
 | 
			
		||||
	x, y, _, height := view.GetRect()
 | 
			
		||||
func (view *MessageView) Draw(screen mauview.Screen) {
 | 
			
		||||
	view.width, view.height = screen.Size()
 | 
			
		||||
	view.recalculateBuffers()
 | 
			
		||||
 | 
			
		||||
	if view.TotalHeight() == 0 {
 | 
			
		||||
		widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height)
 | 
			
		||||
		widget.WriteLineSimple(screen, "It's quite empty in here.", 0, view.height)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usernameX := x + view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
	usernameX := view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
	messageX := usernameX + view.widestSender + SenderMessageGap
 | 
			
		||||
	separatorX := usernameX + view.widestSender + SenderSeparatorGap
 | 
			
		||||
 | 
			
		||||
	bareMode := view.config.Preferences.BareMessageView
 | 
			
		||||
	if bareMode {
 | 
			
		||||
		messageX = x
 | 
			
		||||
		messageX = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indexOffset := view.getIndexOffset(screen, height, messageX)
 | 
			
		||||
	indexOffset := view.getIndexOffset(screen, view.height, messageX)
 | 
			
		||||
 | 
			
		||||
	if len(view.textBuffer) != len(view.metaBuffer) {
 | 
			
		||||
		debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
 | 
			
		||||
@@ -514,13 +516,13 @@ func (view *MessageView) Draw(screen tcell.Screen) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scrollBarHeight, scrollBarPos := view.calculateScrollBar(height)
 | 
			
		||||
	scrollBarHeight, scrollBarPos := view.calculateScrollBar(view.height)
 | 
			
		||||
 | 
			
		||||
	var prevMeta ifc.MessageMeta
 | 
			
		||||
	firstLine := true
 | 
			
		||||
	skippedLines := 0
 | 
			
		||||
 | 
			
		||||
	for line := 0; line < height; line++ {
 | 
			
		||||
	for line := 0; line < view.height; line++ {
 | 
			
		||||
		index := indexOffset + line
 | 
			
		||||
		if index < 0 {
 | 
			
		||||
			skippedLines++
 | 
			
		||||
@@ -530,31 +532,32 @@ func (view *MessageView) Draw(screen tcell.Screen) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
 | 
			
		||||
		isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight()
 | 
			
		||||
		isBottom := line == height-1 && view.ScrollOffset == 0
 | 
			
		||||
		isTop := firstLine && view.ScrollOffset+view.height >= view.TotalHeight()
 | 
			
		||||
		isBottom := line == view.height-1 && view.ScrollOffset == 0
 | 
			
		||||
 | 
			
		||||
		borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
 | 
			
		||||
 | 
			
		||||
		firstLine = false
 | 
			
		||||
 | 
			
		||||
		if !bareMode {
 | 
			
		||||
			screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle)
 | 
			
		||||
			screen.SetContent(separatorX, line, borderChar, nil, borderStyle)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		text, meta := view.textBuffer[index], view.metaBuffer[index]
 | 
			
		||||
		if meta != prevMeta {
 | 
			
		||||
			if len(meta.FormatTime()) > 0 {
 | 
			
		||||
				widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor())
 | 
			
		||||
				widget.WriteLineSimpleColor(screen, meta.FormatTime(), 0, line, meta.TimestampColor())
 | 
			
		||||
			}
 | 
			
		||||
			if !bareMode && (prevMeta == nil || meta.Sender() != prevMeta.Sender()) {
 | 
			
		||||
				widget.WriteLineColor(
 | 
			
		||||
					screen, tview.AlignRight, meta.Sender(),
 | 
			
		||||
					usernameX, y+line, view.widestSender,
 | 
			
		||||
					screen, mauview.AlignRight, meta.Sender(),
 | 
			
		||||
					usernameX, line, view.widestSender,
 | 
			
		||||
					meta.SenderColor())
 | 
			
		||||
			}
 | 
			
		||||
			prevMeta = meta
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		text.Draw(screen, messageX, y+line)
 | 
			
		||||
		text.Draw(screen, messageX, line)
 | 
			
		||||
	}
 | 
			
		||||
	debug.Print(screen)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ func (msg *ImageMessage) updateData() {
 | 
			
		||||
	debug.Print("Loading image:", msg.Homeserver, msg.FileID)
 | 
			
		||||
	data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
 | 
			
		||||
		debug.Printf("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	debug.Print("Image", msg.Homeserver, msg.FileID, "loaded.")
 | 
			
		||||
 
 | 
			
		||||
@@ -102,14 +102,24 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Eve
 | 
			
		||||
	}
 | 
			
		||||
	if len(evt.Content.GetReplyTo()) > 0 {
 | 
			
		||||
		evt.Content.RemoveReplyFallback()
 | 
			
		||||
		replyToEvt, _ := matrix.Client().GetEvent(room.ID, evt.Content.GetReplyTo())
 | 
			
		||||
		replyToEvt.Content.RemoveReplyFallback()
 | 
			
		||||
		if len(replyToEvt.Content.FormattedBody) == 0 {
 | 
			
		||||
			replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body)
 | 
			
		||||
		roomID := evt.Content.RelatesTo.InReplyTo.RoomID
 | 
			
		||||
		if len(roomID) == 0 {
 | 
			
		||||
			roomID = room.ID
 | 
			
		||||
		}
 | 
			
		||||
		replyToEvt, _ := matrix.Client().GetEvent(roomID, evt.Content.GetReplyTo())
 | 
			
		||||
		if replyToEvt != nil {
 | 
			
		||||
			replyToEvt.Content.RemoveReplyFallback()
 | 
			
		||||
			if len(replyToEvt.Content.FormattedBody) == 0 {
 | 
			
		||||
				replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body)
 | 
			
		||||
			}
 | 
			
		||||
			evt.Content.FormattedBody = fmt.Sprintf(
 | 
			
		||||
				"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s",
 | 
			
		||||
				replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
 | 
			
		||||
		} else {
 | 
			
		||||
			evt.Content.FormattedBody = fmt.Sprintf(
 | 
			
		||||
				"In reply to unknown event https://matrix.to/#/%[1]s/%[2]s<br/>%[3]s",
 | 
			
		||||
				roomID, evt.Content.GetReplyTo(), evt.Content.FormattedBody)
 | 
			
		||||
		}
 | 
			
		||||
		evt.Content.FormattedBody = fmt.Sprintf(
 | 
			
		||||
			"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s",
 | 
			
		||||
			replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
 | 
			
		||||
	}
 | 
			
		||||
	ts := unixToTime(evt.Timestamp)
 | 
			
		||||
	switch evt.Content.MsgType {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ package tstring
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
@@ -43,7 +44,7 @@ func (cell Cell) RuneWidth() int {
 | 
			
		||||
	return runewidth.RuneWidth(cell.Char)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) {
 | 
			
		||||
func (cell Cell) Draw(screen mauview.Screen, x, y int) (chWidth int) {
 | 
			
		||||
	chWidth = cell.RuneWidth()
 | 
			
		||||
	for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
 | 
			
		||||
		screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"unicode"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
@@ -181,7 +182,7 @@ func (str TString) AdjustStyleFull(fn func(tcell.Style) tcell.Style) {
 | 
			
		||||
	str.AdjustStyle(0, len(str), fn)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Draw(screen tcell.Screen, x, y int) {
 | 
			
		||||
func (str TString) Draw(screen mauview.Screen, x, y int) {
 | 
			
		||||
	offsetX := 0
 | 
			
		||||
	for _, cell := range str {
 | 
			
		||||
		offsetX += cell.Draw(screen, x+offsetX, y)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,15 +21,15 @@ import (
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RoomList struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
	parent *MainView
 | 
			
		||||
 | 
			
		||||
	// The list of tags in display order.
 | 
			
		||||
	tags []string
 | 
			
		||||
@@ -40,6 +40,8 @@ type RoomList struct {
 | 
			
		||||
	selectedTag string
 | 
			
		||||
 | 
			
		||||
	scrollOffset int
 | 
			
		||||
	height       int
 | 
			
		||||
	width        int
 | 
			
		||||
 | 
			
		||||
	// The item main text color.
 | 
			
		||||
	mainTextColor tcell.Color
 | 
			
		||||
@@ -49,9 +51,10 @@ type RoomList struct {
 | 
			
		||||
	selectedBackgroundColor tcell.Color
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRoomList() *RoomList {
 | 
			
		||||
func NewRoomList(parent *MainView) *RoomList {
 | 
			
		||||
	list := &RoomList{
 | 
			
		||||
		Box:   tview.NewBox(),
 | 
			
		||||
		parent: parent,
 | 
			
		||||
 | 
			
		||||
		items: make(map[string]*TagRoomList),
 | 
			
		||||
		tags:  []string{"m.favourite", "net.maunium.gomuks.fake.direct", "", "m.lowpriority"},
 | 
			
		||||
 | 
			
		||||
@@ -182,11 +185,10 @@ func (list *RoomList) SetSelected(tag string, room *rooms.Room) {
 | 
			
		||||
	list.selected = room
 | 
			
		||||
	list.selectedTag = tag
 | 
			
		||||
	pos := list.index(tag, room)
 | 
			
		||||
	_, _, _, height := list.GetRect()
 | 
			
		||||
	if pos <= list.scrollOffset {
 | 
			
		||||
		list.scrollOffset = pos - 1
 | 
			
		||||
	} else if pos >= list.scrollOffset+height {
 | 
			
		||||
		list.scrollOffset = pos - height + 1
 | 
			
		||||
	} else if pos >= list.scrollOffset+list.height {
 | 
			
		||||
		list.scrollOffset = pos - list.height + 1
 | 
			
		||||
	}
 | 
			
		||||
	if list.scrollOffset < 0 {
 | 
			
		||||
		list.scrollOffset = 0
 | 
			
		||||
@@ -208,10 +210,9 @@ func (list *RoomList) SelectedRoom() *rooms.Room {
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) AddScrollOffset(offset int) {
 | 
			
		||||
	list.scrollOffset += offset
 | 
			
		||||
	_, _, _, viewHeight := list.GetRect()
 | 
			
		||||
	contentHeight := list.ContentHeight()
 | 
			
		||||
	if list.scrollOffset > contentHeight-viewHeight {
 | 
			
		||||
		list.scrollOffset = contentHeight - viewHeight
 | 
			
		||||
	if list.scrollOffset > contentHeight-list.height {
 | 
			
		||||
		list.scrollOffset = contentHeight - list.height
 | 
			
		||||
	}
 | 
			
		||||
	if list.scrollOffset < 0 {
 | 
			
		||||
		list.scrollOffset = 0
 | 
			
		||||
@@ -372,10 +373,39 @@ func (list *RoomList) ContentHeight() (height int) {
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Room) {
 | 
			
		||||
func (list *RoomList) OnKeyEvent(event mauview.KeyEvent) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) OnPasteEvent(event mauview.PasteEvent) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool {
 | 
			
		||||
	switch event.Buttons() {
 | 
			
		||||
	case tcell.WheelUp:
 | 
			
		||||
		list.AddScrollOffset(-WheelScrollOffsetDiff)
 | 
			
		||||
	case tcell.WheelDown:
 | 
			
		||||
		list.AddScrollOffset(WheelScrollOffsetDiff)
 | 
			
		||||
	case tcell.Button1:
 | 
			
		||||
		x, y := event.Position()
 | 
			
		||||
		return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl)
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) Focus() {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) Blur() {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) clickRoom(line, column int, mod bool) bool {
 | 
			
		||||
	line += list.scrollOffset
 | 
			
		||||
	if line < 0 {
 | 
			
		||||
		return "", nil
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	for _, tag := range list.tags {
 | 
			
		||||
		trl := list.items[tag]
 | 
			
		||||
@@ -391,7 +421,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
 | 
			
		||||
		if line < 0 {
 | 
			
		||||
			break
 | 
			
		||||
		} else if line < trl.Length() {
 | 
			
		||||
			return tag, trl.Visible()[trl.Length()-1-line].Room
 | 
			
		||||
			list.parent.SwitchRoom(tag, trl.Visible()[trl.Length()-1-line].Room)
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Tag items
 | 
			
		||||
@@ -405,10 +436,9 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
 | 
			
		||||
				if mod {
 | 
			
		||||
					diff = 100
 | 
			
		||||
				}
 | 
			
		||||
				_, _, width, _ := list.GetRect()
 | 
			
		||||
				if column <= 6 && hasLess {
 | 
			
		||||
					trl.maxShown -= diff
 | 
			
		||||
				} else if column >= width-6 && hasMore {
 | 
			
		||||
				} else if column >= list.width-6 && hasMore {
 | 
			
		||||
					trl.maxShown += diff
 | 
			
		||||
				}
 | 
			
		||||
				if trl.maxShown < 10 {
 | 
			
		||||
@@ -420,9 +450,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
 | 
			
		||||
		// Tag footer
 | 
			
		||||
		line--
 | 
			
		||||
	}
 | 
			
		||||
	return "", nil
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var nsRegex = regexp.MustCompile("^[a-z]\\.[a-z](?:\\.[a-z])*$")
 | 
			
		||||
 | 
			
		||||
func (list *RoomList) GetTagDisplayName(tag string) string {
 | 
			
		||||
@@ -445,11 +474,10 @@ func (list *RoomList) GetTagDisplayName(tag string) string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Draw draws this primitive onto the screen.
 | 
			
		||||
func (list *RoomList) Draw(screen tcell.Screen) {
 | 
			
		||||
	list.Box.Draw(screen)
 | 
			
		||||
 | 
			
		||||
	x, y, width, height := list.GetRect()
 | 
			
		||||
	yLimit := y + height
 | 
			
		||||
func (list *RoomList) Draw(screen mauview.Screen) {
 | 
			
		||||
	list.width, list.height = screen.Size()
 | 
			
		||||
	y := 0
 | 
			
		||||
	yLimit := y + list.height
 | 
			
		||||
	y -= list.scrollOffset
 | 
			
		||||
 | 
			
		||||
	// Draw the list items.
 | 
			
		||||
@@ -464,8 +492,7 @@ func (list *RoomList) Draw(screen tcell.Screen) {
 | 
			
		||||
		if y+renderHeight >= yLimit {
 | 
			
		||||
			renderHeight = yLimit - y
 | 
			
		||||
		}
 | 
			
		||||
		trl.SetRect(x, y, width, renderHeight)
 | 
			
		||||
		trl.Draw(screen)
 | 
			
		||||
		trl.Draw(mauview.NewProxyScreen(screen, 0, y, list.width, renderHeight))
 | 
			
		||||
		y += renderHeight
 | 
			
		||||
		if y >= yLimit {
 | 
			
		||||
			break
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								ui/room-view.go
									
									
									
									
									
								
							
							
						
						
									
										177
									
								
								ui/room-view.go
									
									
									
									
									
								
							@@ -26,9 +26,10 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mautrix"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/config"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
@@ -39,16 +40,23 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RoomView struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
 | 
			
		||||
	topic    *tview.TextView
 | 
			
		||||
	topic    *mauview.TextView
 | 
			
		||||
	content  *MessageView
 | 
			
		||||
	status   *tview.TextView
 | 
			
		||||
	userList *tview.TextView
 | 
			
		||||
	status   *mauview.TextField
 | 
			
		||||
	userList *mauview.TextView
 | 
			
		||||
	ulBorder *widget.Border
 | 
			
		||||
	input    *widget.AdvancedInputField
 | 
			
		||||
	input    *mauview.InputArea
 | 
			
		||||
	Room     *rooms.Room
 | 
			
		||||
 | 
			
		||||
	topicScreen    *mauview.ProxyScreen
 | 
			
		||||
	contentScreen  *mauview.ProxyScreen
 | 
			
		||||
	statusScreen   *mauview.ProxyScreen
 | 
			
		||||
	inputScreen    *mauview.ProxyScreen
 | 
			
		||||
	ulBorderScreen *mauview.ProxyScreen
 | 
			
		||||
	ulScreen       *mauview.ProxyScreen
 | 
			
		||||
 | 
			
		||||
	prevScreen mauview.Screen
 | 
			
		||||
 | 
			
		||||
	parent *MainView
 | 
			
		||||
	config *config.Config
 | 
			
		||||
 | 
			
		||||
@@ -63,27 +71,34 @@ type RoomView struct {
 | 
			
		||||
 | 
			
		||||
func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
 | 
			
		||||
	view := &RoomView{
 | 
			
		||||
		Box:      tview.NewBox(),
 | 
			
		||||
		topic:    tview.NewTextView(),
 | 
			
		||||
		status:   tview.NewTextView(),
 | 
			
		||||
		userList: tview.NewTextView(),
 | 
			
		||||
		topic:    mauview.NewTextView(),
 | 
			
		||||
		status:   mauview.NewTextField(),
 | 
			
		||||
		userList: mauview.NewTextView(),
 | 
			
		||||
		ulBorder: widget.NewBorder(),
 | 
			
		||||
		input:    widget.NewAdvancedInputField(),
 | 
			
		||||
		input:    mauview.NewInputArea(),
 | 
			
		||||
		Room:     room,
 | 
			
		||||
 | 
			
		||||
		topicScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: 0, Height: TopicBarHeight},
 | 
			
		||||
		contentScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: StatusBarHeight},
 | 
			
		||||
		statusScreen: &mauview.ProxyScreen{OffsetX: 0, Height: StatusBarHeight},
 | 
			
		||||
		inputScreen: &mauview.ProxyScreen{OffsetX: 0},
 | 
			
		||||
		ulBorderScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListBorderWidth},
 | 
			
		||||
		ulScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListWidth},
 | 
			
		||||
 | 
			
		||||
		parent:   parent,
 | 
			
		||||
		config:   parent.config,
 | 
			
		||||
	}
 | 
			
		||||
	view.content = NewMessageView(view)
 | 
			
		||||
 | 
			
		||||
	view.input.
 | 
			
		||||
		SetFieldBackgroundColor(tcell.ColorDefault).
 | 
			
		||||
		SetBackgroundColor(tcell.ColorDefault).
 | 
			
		||||
		SetPlaceholder("Send a message...").
 | 
			
		||||
		SetPlaceholderExtColor(tcell.ColorGray).
 | 
			
		||||
		SetPlaceholderTextColor(tcell.ColorGray).
 | 
			
		||||
		SetTabCompleteFunc(view.InputTabComplete)
 | 
			
		||||
 | 
			
		||||
	view.topic.
 | 
			
		||||
		SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)).
 | 
			
		||||
		SetBackgroundColor(tcell.ColorDarkGreen)
 | 
			
		||||
		SetTextColor(tcell.ColorDarkGreen)
 | 
			
		||||
 | 
			
		||||
	view.status.SetBackgroundColor(tcell.ColorDimGray)
 | 
			
		||||
 | 
			
		||||
@@ -106,26 +121,13 @@ func (view *RoomView) LoadHistory(matrix ifc.MatrixContainer, dir string) (int,
 | 
			
		||||
	return view.MessageView().LoadHistory(matrix, view.logPath(dir))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) SetInputCapture(fn func(room *RoomView, event *tcell.EventKey) *tcell.EventKey) *RoomView {
 | 
			
		||||
	view.input.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
 | 
			
		||||
		return fn(view, event)
 | 
			
		||||
	})
 | 
			
		||||
	return view
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) SetMouseCapture(fn func(room *RoomView, event *tcell.EventMouse) *tcell.EventMouse) *RoomView {
 | 
			
		||||
	view.input.SetMouseCapture(func(event *tcell.EventMouse) *tcell.EventMouse {
 | 
			
		||||
		return fn(view, event)
 | 
			
		||||
	})
 | 
			
		||||
	return view
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) SetInputSubmitFunc(fn func(room *RoomView, text string)) *RoomView {
 | 
			
		||||
	view.input.SetDoneFunc(func(key tcell.Key) {
 | 
			
		||||
	// FIXME
 | 
			
		||||
	/*view.input.SetDoneFunc(func(key tcell.Key) {
 | 
			
		||||
		if key == tcell.KeyEnter {
 | 
			
		||||
			fn(view, view.input.GetText())
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	})*/
 | 
			
		||||
	return view
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -137,7 +139,7 @@ func (view *RoomView) SetInputChangedFunc(fn func(room *RoomView, text string))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) SetInputText(newText string) *RoomView {
 | 
			
		||||
	view.input.SetText(newText)
 | 
			
		||||
	view.input.SetTextAndMoveCursor(newText)
 | 
			
		||||
	return view
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -145,12 +147,12 @@ func (view *RoomView) GetInputText() string {
 | 
			
		||||
	return view.input.GetText()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) GetInputField() *widget.AdvancedInputField {
 | 
			
		||||
	return view.input
 | 
			
		||||
func (view *RoomView) Focus() {
 | 
			
		||||
	view.input.Focus()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) Focus(delegate func(p tview.Primitive)) {
 | 
			
		||||
	delegate(view.input)
 | 
			
		||||
func (view *RoomView) Blur() {
 | 
			
		||||
	view.input.Blur()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) GetStatus() string {
 | 
			
		||||
@@ -169,7 +171,7 @@ func (view *RoomView) GetStatus() string {
 | 
			
		||||
		buf.WriteString("Typing: " + view.typing[0])
 | 
			
		||||
		buf.WriteString(" - ")
 | 
			
		||||
	} else if len(view.typing) > 1 {
 | 
			
		||||
		fmt.Fprintf(&buf,
 | 
			
		||||
		_, _ = fmt.Fprintf(&buf,
 | 
			
		||||
			"Typing: %s and %s - ",
 | 
			
		||||
			strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1])
 | 
			
		||||
	}
 | 
			
		||||
@@ -177,64 +179,91 @@ func (view *RoomView) GetStatus() string {
 | 
			
		||||
	return strings.TrimSuffix(buf.String(), " - ")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) Draw(screen tcell.Screen) {
 | 
			
		||||
	x, y, width, height := view.GetRect()
 | 
			
		||||
// Constants defining the size of the room view grid.
 | 
			
		||||
const (
 | 
			
		||||
	UserListBorderWidth   = 1
 | 
			
		||||
	UserListWidth         = 20
 | 
			
		||||
	StaticHorizontalSpace = UserListBorderWidth + UserListWidth
 | 
			
		||||
 | 
			
		||||
	TopicBarHeight      = 1
 | 
			
		||||
	StatusBarHeight     = 1
 | 
			
		||||
	InputBarHeight      = 1
 | 
			
		||||
	StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight
 | 
			
		||||
 | 
			
		||||
	MaxInputHeight
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) Draw(screen mauview.Screen) {
 | 
			
		||||
	width, height := screen.Size()
 | 
			
		||||
	if width <= 0 || height <= 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Constants defining the size of the room view grid.
 | 
			
		||||
	const (
 | 
			
		||||
		UserListBorderWidth   = 1
 | 
			
		||||
		UserListWidth         = 20
 | 
			
		||||
		StaticHorizontalSpace = UserListBorderWidth + UserListWidth
 | 
			
		||||
 | 
			
		||||
		TopicBarHeight      = 1
 | 
			
		||||
		StatusBarHeight     = 1
 | 
			
		||||
		InputBarHeight      = 1
 | 
			
		||||
		StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Calculate actual grid based on view rectangle and constants defined above.
 | 
			
		||||
	var (
 | 
			
		||||
		contentHeight = height - StaticVerticalSpace
 | 
			
		||||
		contentWidth  = width - StaticHorizontalSpace
 | 
			
		||||
 | 
			
		||||
		userListBorderColumn = x + contentWidth
 | 
			
		||||
		userListColumn       = userListBorderColumn + UserListBorderWidth
 | 
			
		||||
 | 
			
		||||
		topicRow   = y
 | 
			
		||||
		contentRow = topicRow + TopicBarHeight
 | 
			
		||||
		statusRow  = contentRow + contentHeight
 | 
			
		||||
		inputRow   = statusRow + StatusBarHeight
 | 
			
		||||
	)
 | 
			
		||||
	if view.config.Preferences.HideUserList {
 | 
			
		||||
		contentWidth = width
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update the rectangles of all the children.
 | 
			
		||||
	view.topic.SetRect(x, topicRow, width, TopicBarHeight)
 | 
			
		||||
	view.content.SetRect(x, contentRow, contentWidth, contentHeight)
 | 
			
		||||
	view.status.SetRect(x, statusRow, width, StatusBarHeight)
 | 
			
		||||
	if !view.config.Preferences.HideUserList && userListColumn > x {
 | 
			
		||||
		view.userList.SetRect(userListColumn, contentRow, UserListWidth, contentHeight)
 | 
			
		||||
		view.ulBorder.SetRect(userListBorderColumn, contentRow, UserListBorderWidth, contentHeight)
 | 
			
		||||
	if view.prevScreen != screen {
 | 
			
		||||
		view.topicScreen.Parent = screen
 | 
			
		||||
		view.contentScreen.Parent = screen
 | 
			
		||||
		view.statusScreen.Parent = screen
 | 
			
		||||
		view.inputScreen.Parent = screen
 | 
			
		||||
		view.ulBorderScreen.Parent = screen
 | 
			
		||||
		view.ulScreen.Parent = screen
 | 
			
		||||
		view.prevScreen = screen
 | 
			
		||||
	}
 | 
			
		||||
	view.input.SetRect(x, inputRow, width, InputBarHeight)
 | 
			
		||||
 | 
			
		||||
	view.input.PrepareDraw(width)
 | 
			
		||||
	inputHeight := view.input.GetTextHeight()
 | 
			
		||||
	if inputHeight > MaxInputHeight {
 | 
			
		||||
		inputHeight = MaxInputHeight
 | 
			
		||||
	} else if inputHeight < 1 {
 | 
			
		||||
		inputHeight = 1
 | 
			
		||||
	}
 | 
			
		||||
	contentHeight -= inputHeight
 | 
			
		||||
 | 
			
		||||
	view.topicScreen.Width = width
 | 
			
		||||
	view.contentScreen.Width = contentWidth
 | 
			
		||||
	view.contentScreen.Height = contentHeight
 | 
			
		||||
	view.statusScreen.OffsetY = view.contentScreen.YEnd()
 | 
			
		||||
	view.statusScreen.Width = width
 | 
			
		||||
	view.inputScreen.Width = width
 | 
			
		||||
	view.inputScreen.OffsetY = view.statusScreen.YEnd()
 | 
			
		||||
	view.inputScreen.Height = inputHeight
 | 
			
		||||
	view.ulBorderScreen.OffsetX = view.contentScreen.XEnd()
 | 
			
		||||
	view.ulBorderScreen.Height = contentHeight
 | 
			
		||||
	view.ulScreen.OffsetX = view.ulBorderScreen.XEnd()
 | 
			
		||||
	view.ulScreen.Height = contentHeight
 | 
			
		||||
 | 
			
		||||
	// Draw everything
 | 
			
		||||
	view.Box.Draw(screen)
 | 
			
		||||
	view.topic.Draw(screen)
 | 
			
		||||
	view.content.Draw(screen)
 | 
			
		||||
	view.topic.Draw(view.topicScreen)
 | 
			
		||||
	view.content.Draw(view.contentScreen)
 | 
			
		||||
	view.status.SetText(view.GetStatus())
 | 
			
		||||
	view.status.Draw(screen)
 | 
			
		||||
	view.input.Draw(screen)
 | 
			
		||||
	view.status.Draw(view.statusScreen)
 | 
			
		||||
	view.input.Draw(view.inputScreen)
 | 
			
		||||
	if !view.config.Preferences.HideUserList {
 | 
			
		||||
		view.ulBorder.Draw(screen)
 | 
			
		||||
		view.userList.Draw(screen)
 | 
			
		||||
		view.ulBorder.Draw(view.ulBorderScreen)
 | 
			
		||||
		view.userList.Draw(view.ulScreen)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) OnKeyEvent(event mauview.KeyEvent) bool {
 | 
			
		||||
	return view.input.OnKeyEvent(event)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) OnPasteEvent(event mauview.PasteEvent) bool {
 | 
			
		||||
	return view.input.OnPasteEvent(event)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) OnMouseEvent(event mauview.MouseEvent) bool {
 | 
			
		||||
	return view.content.OnMouseEvent(event)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) SetCompletions(completions []string) {
 | 
			
		||||
	view.completions.list = completions
 | 
			
		||||
	view.completions.textCache = view.input.GetText()
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
@@ -44,7 +44,7 @@ func NewDefaultOrderedRoom(room *rooms.Room) *OrderedRoom {
 | 
			
		||||
	return NewOrderedRoom("0.5", room)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineWidth int, isSelected bool) {
 | 
			
		||||
func (or *OrderedRoom) Draw(roomList *RoomList, screen mauview.Screen, x, y, lineWidth int, isSelected bool) {
 | 
			
		||||
	style := tcell.StyleDefault.
 | 
			
		||||
		Foreground(roomList.mainTextColor).
 | 
			
		||||
		Bold(or.HasNewMessages())
 | 
			
		||||
@@ -56,7 +56,7 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW
 | 
			
		||||
 | 
			
		||||
	unreadCount := or.UnreadCount()
 | 
			
		||||
 | 
			
		||||
	widget.WriteLinePadded(screen, tview.AlignLeft, or.GetTitle(), x, y, lineWidth, style)
 | 
			
		||||
	widget.WriteLinePadded(screen, mauview.AlignLeft, or.GetTitle(), x, y, lineWidth, style)
 | 
			
		||||
 | 
			
		||||
	if unreadCount > 0 {
 | 
			
		||||
		unreadMessageCount := "99+"
 | 
			
		||||
@@ -67,13 +67,13 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW
 | 
			
		||||
			unreadMessageCount += "!"
 | 
			
		||||
		}
 | 
			
		||||
		unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
 | 
			
		||||
		widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
 | 
			
		||||
		widget.WriteLine(screen, mauview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
 | 
			
		||||
		lineWidth -= len(unreadMessageCount)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TagRoomList struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
	mauview.NoopEventHandler
 | 
			
		||||
	rooms       []*OrderedRoom
 | 
			
		||||
	maxShown    int
 | 
			
		||||
	name        string
 | 
			
		||||
@@ -83,7 +83,6 @@ type TagRoomList struct {
 | 
			
		||||
 | 
			
		||||
func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList {
 | 
			
		||||
	return &TagRoomList{
 | 
			
		||||
		Box:         tview.NewBox(),
 | 
			
		||||
		maxShown:    10,
 | 
			
		||||
		rooms:       rooms,
 | 
			
		||||
		name:        name,
 | 
			
		||||
@@ -246,41 +245,40 @@ func (trl *TagRoomList) RenderHeight() int {
 | 
			
		||||
	return height
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (trl *TagRoomList) DrawHeader(screen tcell.Screen) {
 | 
			
		||||
	x, y, width, _ := trl.GetRect()
 | 
			
		||||
func (trl *TagRoomList) DrawHeader(screen mauview.Screen) {
 | 
			
		||||
	width, _ := screen.Size()
 | 
			
		||||
	roomCount := strconv.Itoa(trl.TotalLength())
 | 
			
		||||
 | 
			
		||||
	// Draw tag name
 | 
			
		||||
	displayNameWidth := width - 1 - len(roomCount)
 | 
			
		||||
	widget.WriteLine(screen, tview.AlignLeft, trl.displayname, x, y, displayNameWidth, TagDisplayNameStyle)
 | 
			
		||||
	widget.WriteLine(screen, mauview.AlignLeft, trl.displayname, 0, 0, displayNameWidth, TagDisplayNameStyle)
 | 
			
		||||
 | 
			
		||||
	// Draw tag room count
 | 
			
		||||
	roomCountX := x + len(trl.displayname) + 1
 | 
			
		||||
	roomCountX := len(trl.displayname) + 1
 | 
			
		||||
	roomCountWidth := width - 2 - len(trl.displayname)
 | 
			
		||||
	widget.WriteLine(screen, tview.AlignLeft, roomCount, roomCountX, y, roomCountWidth, TagRoomCountStyle)
 | 
			
		||||
	widget.WriteLine(screen, mauview.AlignLeft, roomCount, roomCountX, 0, roomCountWidth, TagRoomCountStyle)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (trl *TagRoomList) Draw(screen tcell.Screen) {
 | 
			
		||||
func (trl *TagRoomList) Draw(screen mauview.Screen) {
 | 
			
		||||
	if len(trl.displayname) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	trl.DrawHeader(screen)
 | 
			
		||||
 | 
			
		||||
	x, y, width, height := trl.GetRect()
 | 
			
		||||
	yLimit := y + height
 | 
			
		||||
	width, height := screen.Size()
 | 
			
		||||
 | 
			
		||||
	items := trl.Visible()
 | 
			
		||||
 | 
			
		||||
	if trl.IsCollapsed() {
 | 
			
		||||
		screen.SetCell(x+width-1, y, tcell.StyleDefault, '▶')
 | 
			
		||||
		screen.SetCell(width-1, 0, tcell.StyleDefault, '▶')
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	screen.SetCell(x+width-1, y, tcell.StyleDefault, '▼')
 | 
			
		||||
	screen.SetCell(width-1, 0, tcell.StyleDefault, '▼')
 | 
			
		||||
 | 
			
		||||
	offsetY := 1
 | 
			
		||||
	y := 1
 | 
			
		||||
	for i := trl.Length() - 1; i >= 0; i-- {
 | 
			
		||||
		if y+offsetY >= yLimit {
 | 
			
		||||
		if y >= height {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -288,18 +286,18 @@ func (trl *TagRoomList) Draw(screen tcell.Screen) {
 | 
			
		||||
 | 
			
		||||
		lineWidth := width
 | 
			
		||||
		isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected
 | 
			
		||||
		item.Draw(trl.parent, screen, x, y+offsetY, lineWidth, isSelected)
 | 
			
		||||
		offsetY++
 | 
			
		||||
		item.Draw(trl.parent, screen, 0, y, lineWidth, isSelected)
 | 
			
		||||
		y++
 | 
			
		||||
	}
 | 
			
		||||
	hasLess := trl.maxShown > 10
 | 
			
		||||
	hasMore := trl.HasInvisibleRooms()
 | 
			
		||||
	if (hasLess || hasMore) && y+offsetY < yLimit {
 | 
			
		||||
	if (hasLess || hasMore) && y < height {
 | 
			
		||||
		if hasMore {
 | 
			
		||||
			widget.WriteLine(screen, tview.AlignRight, "More ↓", x, y+offsetY, width, tcell.StyleDefault)
 | 
			
		||||
			widget.WriteLine(screen, mauview.AlignRight, "More ↓", 0, y, width, tcell.StyleDefault)
 | 
			
		||||
		}
 | 
			
		||||
		if hasLess {
 | 
			
		||||
			widget.WriteLine(screen, tview.AlignLeft, "↑ Less", x, y+offsetY, width, tcell.StyleDefault)
 | 
			
		||||
			widget.WriteLine(screen, mauview.AlignLeft, "↑ Less", 0, y, width, tcell.StyleDefault)
 | 
			
		||||
		}
 | 
			
		||||
		offsetY++
 | 
			
		||||
		y++
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								ui/ui.go
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								ui/ui.go
									
									
									
									
									
								
							@@ -19,8 +19,8 @@ package ui
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
)
 | 
			
		||||
@@ -34,17 +34,18 @@ const (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type GomuksUI struct {
 | 
			
		||||
	gmx   ifc.Gomuks
 | 
			
		||||
	app   *tview.Application
 | 
			
		||||
	views *tview.Pages
 | 
			
		||||
	gmx ifc.Gomuks
 | 
			
		||||
	app *mauview.Application
 | 
			
		||||
 | 
			
		||||
	mainView  *MainView
 | 
			
		||||
	loginView *LoginView
 | 
			
		||||
 | 
			
		||||
	views map[View]mauview.Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
 | 
			
		||||
	tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
 | 
			
		||||
	mauview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
 | 
			
		||||
	mauview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
 | 
			
		||||
	if tcellDB := os.Getenv("TCELLDB"); len(tcellDB) == 0 {
 | 
			
		||||
		if info, err := os.Stat("/usr/share/tcell/database"); err == nil && info.IsDir() {
 | 
			
		||||
			os.Setenv("TCELLDB", "/usr/share/tcell/database")
 | 
			
		||||
@@ -54,20 +55,22 @@ func init() {
 | 
			
		||||
 | 
			
		||||
func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI {
 | 
			
		||||
	ui := &GomuksUI{
 | 
			
		||||
		gmx:   gmx,
 | 
			
		||||
		app:   tview.NewApplication(),
 | 
			
		||||
		views: tview.NewPages(),
 | 
			
		||||
		gmx: gmx,
 | 
			
		||||
		app: mauview.NewApplication(),
 | 
			
		||||
	}
 | 
			
		||||
	ui.views.SetChangedFunc(ui.Render)
 | 
			
		||||
	return ui
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) Init() {
 | 
			
		||||
	ui.app.SetRoot(ui.InitViews(), true)
 | 
			
		||||
	ui.views = map[View]mauview.Component{
 | 
			
		||||
		ViewLogin: ui.NewLoginView(),
 | 
			
		||||
		ViewMain:  ui.NewMainView(),
 | 
			
		||||
	}
 | 
			
		||||
	ui.app.Root = ui.views[ViewLogin]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) Start() error {
 | 
			
		||||
	return ui.app.Run()
 | 
			
		||||
	return ui.app.Start()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) Stop() {
 | 
			
		||||
@@ -75,23 +78,21 @@ func (ui *GomuksUI) Stop() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) Finish() {
 | 
			
		||||
	if ui.app.GetScreen() != nil {
 | 
			
		||||
		ui.app.GetScreen().Fini()
 | 
			
		||||
	if ui.app.Screen() != nil {
 | 
			
		||||
		ui.app.Screen().Fini()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) Render() {
 | 
			
		||||
	ui.app.Draw()
 | 
			
		||||
	ui.app.Redraw()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) OnLogin() {
 | 
			
		||||
	ui.SetView(ViewMain)
 | 
			
		||||
	ui.app.SetFocus(ui.mainView)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) OnLogout() {
 | 
			
		||||
	ui.SetView(ViewLogin)
 | 
			
		||||
	ui.app.SetFocus(ui.loginView)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) HandleNewPreferences() {
 | 
			
		||||
@@ -99,13 +100,11 @@ func (ui *GomuksUI) HandleNewPreferences() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) SetView(name View) {
 | 
			
		||||
	ui.views.SwitchToPage(string(name))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) InitViews() tview.Primitive {
 | 
			
		||||
	ui.views.AddPage(string(ViewLogin), ui.NewLoginView(), true, true)
 | 
			
		||||
	ui.views.AddPage(string(ViewMain), ui.NewMainView(), true, false)
 | 
			
		||||
	return ui.views
 | 
			
		||||
	ui.app.Root = ui.views[name]
 | 
			
		||||
	focusable, ok := ui.app.Root.(mauview.Focusable)
 | 
			
		||||
	if ok {
 | 
			
		||||
		focusable.Focus()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) MainView() ifc.MainView {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,66 +17,97 @@
 | 
			
		||||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mautrix"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/config"
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LoginView struct {
 | 
			
		||||
	*tview.Form
 | 
			
		||||
	*mauview.Form
 | 
			
		||||
 | 
			
		||||
	homeserver *widget.AdvancedInputField
 | 
			
		||||
	username   *widget.AdvancedInputField
 | 
			
		||||
	password   *widget.AdvancedInputField
 | 
			
		||||
	error      *widget.FormTextView
 | 
			
		||||
	container *mauview.Centerer
 | 
			
		||||
 | 
			
		||||
	homeserverLabel *mauview.TextField
 | 
			
		||||
	usernameLabel   *mauview.TextField
 | 
			
		||||
	passwordLabel   *mauview.TextField
 | 
			
		||||
 | 
			
		||||
	homeserver *mauview.InputField
 | 
			
		||||
	username   *mauview.InputField
 | 
			
		||||
	password   *mauview.InputField
 | 
			
		||||
	error      *mauview.TextField
 | 
			
		||||
 | 
			
		||||
	loginButton *mauview.Button
 | 
			
		||||
	quitButton  *mauview.Button
 | 
			
		||||
 | 
			
		||||
	matrix ifc.MatrixContainer
 | 
			
		||||
	config *config.Config
 | 
			
		||||
	parent *GomuksUI
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) NewLoginView() tview.Primitive {
 | 
			
		||||
func (ui *GomuksUI) NewLoginView() mauview.Component {
 | 
			
		||||
	view := &LoginView{
 | 
			
		||||
		Form: tview.NewForm(),
 | 
			
		||||
		Form: mauview.NewForm(),
 | 
			
		||||
 | 
			
		||||
		homeserver: widget.NewAdvancedInputField(),
 | 
			
		||||
		username:   widget.NewAdvancedInputField(),
 | 
			
		||||
		password:   widget.NewAdvancedInputField(),
 | 
			
		||||
		usernameLabel:   mauview.NewTextField().SetText("Username"),
 | 
			
		||||
		passwordLabel:   mauview.NewTextField().SetText("Password"),
 | 
			
		||||
		homeserverLabel: mauview.NewTextField().SetText("Homeserver"),
 | 
			
		||||
 | 
			
		||||
		username:   mauview.NewInputField(),
 | 
			
		||||
		password:   mauview.NewInputField(),
 | 
			
		||||
		homeserver: mauview.NewInputField(),
 | 
			
		||||
 | 
			
		||||
		loginButton: mauview.NewButton("Login"),
 | 
			
		||||
		quitButton:  mauview.NewButton("Quit"),
 | 
			
		||||
 | 
			
		||||
		matrix: ui.gmx.Matrix(),
 | 
			
		||||
		config: ui.gmx.Config(),
 | 
			
		||||
		parent: ui,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hs := ui.gmx.Config().HS
 | 
			
		||||
	if len(hs) == 0 {
 | 
			
		||||
		hs = "https://matrix.org"
 | 
			
		||||
	}
 | 
			
		||||
	view.homeserver.SetLabel("Homeserver").SetText(hs).SetFieldWidth(30)
 | 
			
		||||
	view.username.SetLabel("Username").SetText(ui.gmx.Config().UserID).SetFieldWidth(30)
 | 
			
		||||
	view.password.SetLabel("Password").SetMaskCharacter('*').SetFieldWidth(30)
 | 
			
		||||
	view.homeserver.SetText(hs)
 | 
			
		||||
	view.username.SetText(ui.gmx.Config().UserID)
 | 
			
		||||
	view.password.SetMaskCharacter('*')
 | 
			
		||||
 | 
			
		||||
	view.
 | 
			
		||||
		AddFormItem(view.homeserver).AddFormItem(view.username).AddFormItem(view.password).
 | 
			
		||||
		AddButton("Log in", view.Login).
 | 
			
		||||
		AddButton("Quit", ui.gmx.Stop).
 | 
			
		||||
		SetButtonsAlign(tview.AlignCenter).
 | 
			
		||||
		SetBorder(true).SetTitle("Log in to Matrix")
 | 
			
		||||
	view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorBlue)
 | 
			
		||||
	view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorBlue)
 | 
			
		||||
 | 
			
		||||
	view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1})
 | 
			
		||||
	view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
 | 
			
		||||
	view.AddFormItem(view.username, 3, 1, 5, 1).
 | 
			
		||||
		AddFormItem(view.password, 3, 3, 5, 1).
 | 
			
		||||
		AddFormItem(view.homeserver, 3, 5, 5, 1).
 | 
			
		||||
		AddFormItem(view.loginButton, 5, 7, 3, 1).
 | 
			
		||||
		AddFormItem(view.quitButton, 1, 7, 3, 1).
 | 
			
		||||
		AddComponent(view.usernameLabel, 1, 1, 1, 1).
 | 
			
		||||
		AddComponent(view.passwordLabel, 1, 3, 1, 1).
 | 
			
		||||
		AddComponent(view.homeserverLabel, 1, 5, 1, 1)
 | 
			
		||||
	ui.loginView = view
 | 
			
		||||
 | 
			
		||||
	return widget.Center(45, 13, ui.loginView)
 | 
			
		||||
	view.container = mauview.Center(mauview.NewBox(view).SetTitle("Log in to Matrix"), 45, 13)
 | 
			
		||||
	return view.container
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *LoginView) Error(err string) {
 | 
			
		||||
	if len(err) == 0 {
 | 
			
		||||
		debug.Print("Hiding error")
 | 
			
		||||
		view.RemoveComponent(view.error)
 | 
			
		||||
		view.error = nil
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	debug.Print("Showing error", err)
 | 
			
		||||
	if view.error == nil {
 | 
			
		||||
		view.error = &widget.FormTextView{TextView: tview.NewTextView()}
 | 
			
		||||
		view.AddFormItem(view.error)
 | 
			
		||||
		view.error = mauview.NewTextField().SetTextColor(tcell.ColorRed)
 | 
			
		||||
		view.AddComponent(view.error, 1, 9, 7, 1)
 | 
			
		||||
	}
 | 
			
		||||
	view.error.SetText(err)
 | 
			
		||||
 | 
			
		||||
	view.parent.Render()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *LoginView) Login() {
 | 
			
		||||
@@ -91,8 +122,8 @@ func (view *LoginView) Login() {
 | 
			
		||||
	err = view.matrix.Login(mxid, password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if httpErr, ok := err.(mautrix.HTTPError); ok {
 | 
			
		||||
			if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok {
 | 
			
		||||
				view.Error(respErr.Err)
 | 
			
		||||
			if httpErr.RespError != nil {
 | 
			
		||||
				view.Error(httpErr.RespError.Err)
 | 
			
		||||
			} else {
 | 
			
		||||
				view.Error(httpErr.Message)
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										239
									
								
								ui/view-main.go
									
									
									
									
									
								
							
							
						
						
									
										239
									
								
								ui/view-main.go
									
									
									
									
									
								
							@@ -26,8 +26,8 @@ import (
 | 
			
		||||
	"github.com/kyokomi/emoji"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mautrix"
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/config"
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
@@ -40,12 +40,16 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MainView struct {
 | 
			
		||||
	*tview.Flex
 | 
			
		||||
	flex *mauview.Flex
 | 
			
		||||
 | 
			
		||||
	roomList     *RoomList
 | 
			
		||||
	roomView     *tview.Pages
 | 
			
		||||
	roomView     *mauview.Box
 | 
			
		||||
	currentRoom  *RoomView
 | 
			
		||||
	rooms        map[string]*RoomView
 | 
			
		||||
	cmdProcessor *CommandProcessor
 | 
			
		||||
	focused      mauview.Focusable
 | 
			
		||||
 | 
			
		||||
	modal mauview.Component
 | 
			
		||||
 | 
			
		||||
	lastFocusTime time.Time
 | 
			
		||||
 | 
			
		||||
@@ -55,11 +59,10 @@ type MainView struct {
 | 
			
		||||
	parent *GomuksUI
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ui *GomuksUI) NewMainView() tview.Primitive {
 | 
			
		||||
func (ui *GomuksUI) NewMainView() mauview.Component {
 | 
			
		||||
	mainView := &MainView{
 | 
			
		||||
		Flex:     tview.NewFlex(),
 | 
			
		||||
		roomList: NewRoomList(),
 | 
			
		||||
		roomView: tview.NewPages(),
 | 
			
		||||
		flex:     mauview.NewFlex().SetDirection(mauview.FlexColumn),
 | 
			
		||||
		roomView: mauview.NewBox(nil).SetBorder(false),
 | 
			
		||||
		rooms:    make(map[string]*RoomView),
 | 
			
		||||
 | 
			
		||||
		matrix: ui.gmx.Matrix(),
 | 
			
		||||
@@ -67,13 +70,13 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
 | 
			
		||||
		config: ui.gmx.Config(),
 | 
			
		||||
		parent: ui,
 | 
			
		||||
	}
 | 
			
		||||
	mainView.roomList = NewRoomList(mainView)
 | 
			
		||||
	mainView.cmdProcessor = NewCommandProcessor(mainView)
 | 
			
		||||
 | 
			
		||||
	mainView.
 | 
			
		||||
		SetDirection(tview.FlexColumn).
 | 
			
		||||
		AddItem(mainView.roomList, 25, 0, false).
 | 
			
		||||
		AddItem(widget.NewBorder(), 1, 0, false).
 | 
			
		||||
		AddItem(mainView.roomView, 0, 1, true)
 | 
			
		||||
	mainView.flex.
 | 
			
		||||
		AddFixedComponent(mainView.roomList, 25).
 | 
			
		||||
		AddFixedComponent(widget.NewBorder(), 1).
 | 
			
		||||
		AddProportionalComponent(mainView.roomView, 1)
 | 
			
		||||
	mainView.BumpFocus(nil)
 | 
			
		||||
 | 
			
		||||
	ui.mainView = mainView
 | 
			
		||||
@@ -81,18 +84,37 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
 | 
			
		||||
	return mainView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) Draw(screen tcell.Screen) {
 | 
			
		||||
func (view *MainView) ShowModal(modal mauview.Component) {
 | 
			
		||||
	view.modal = modal
 | 
			
		||||
	var ok bool
 | 
			
		||||
	view.focused, ok = modal.(mauview.Focusable)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		view.focused = nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) HideModal() {
 | 
			
		||||
	view.modal = nil
 | 
			
		||||
	view.focused = view.roomView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) Draw(screen mauview.Screen) {
 | 
			
		||||
	if view.config.Preferences.HideRoomList {
 | 
			
		||||
		view.roomView.SetRect(view.GetRect())
 | 
			
		||||
		view.roomView.Draw(screen)
 | 
			
		||||
	} else {
 | 
			
		||||
		view.Flex.Draw(screen)
 | 
			
		||||
		view.flex.Draw(screen)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if view.modal != nil {
 | 
			
		||||
		view.modal.Draw(screen)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) BumpFocus(roomView *RoomView) {
 | 
			
		||||
	view.lastFocusTime = time.Now()
 | 
			
		||||
	view.MarkRead(roomView)
 | 
			
		||||
	if roomView != nil {
 | 
			
		||||
		view.lastFocusTime = time.Now()
 | 
			
		||||
		view.MarkRead(roomView)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) MarkRead(roomView *RoomView) {
 | 
			
		||||
@@ -167,7 +189,10 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) ShowBare(roomView *RoomView) {
 | 
			
		||||
	_, height := view.parent.app.GetScreen().Size()
 | 
			
		||||
	if roomView == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	_, height := view.parent.app.Screen().Size()
 | 
			
		||||
	view.parent.app.Suspend(func() {
 | 
			
		||||
		print("\033[2J\033[0;0H")
 | 
			
		||||
		// We don't know how much space there exactly is. Too few messages looks weird,
 | 
			
		||||
@@ -181,37 +206,54 @@ func (view *MainView) ShowBare(roomView *RoomView) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey {
 | 
			
		||||
	view.BumpFocus(roomView)
 | 
			
		||||
func (view *MainView) OnKeyEvent(event mauview.KeyEvent) bool {
 | 
			
		||||
	view.BumpFocus(view.currentRoom)
 | 
			
		||||
 | 
			
		||||
	k := key.Key()
 | 
			
		||||
	c := key.Rune()
 | 
			
		||||
	if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt {
 | 
			
		||||
	if view.modal != nil {
 | 
			
		||||
		return view.modal.OnKeyEvent(event)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k := event.Key()
 | 
			
		||||
	c := event.Rune()
 | 
			
		||||
	if event.Modifiers() == tcell.ModCtrl || event.Modifiers() == tcell.ModAlt {
 | 
			
		||||
		switch {
 | 
			
		||||
		case k == tcell.KeyDown:
 | 
			
		||||
			view.SwitchRoom(view.roomList.Next())
 | 
			
		||||
		case k == tcell.KeyUp:
 | 
			
		||||
			view.SwitchRoom(view.roomList.Previous())
 | 
			
		||||
		case k == tcell.KeyEnter:
 | 
			
		||||
			searchModal := NewFuzzySearchModal(view, 42, 12)
 | 
			
		||||
			view.parent.views.AddPage("fuzzy-search-modal", searchModal, true, true)
 | 
			
		||||
			view.parent.app.SetFocus(searchModal)
 | 
			
		||||
			view.ShowModal(NewFuzzySearchModal(view, 42, 12))
 | 
			
		||||
		case k == tcell.KeyHome:
 | 
			
		||||
			msgView := view.currentRoom.MessageView()
 | 
			
		||||
			msgView.AddScrollOffset(msgView.TotalHeight())
 | 
			
		||||
		case k == tcell.KeyEnd:
 | 
			
		||||
			msgView := view.currentRoom.MessageView()
 | 
			
		||||
			msgView.AddScrollOffset(-msgView.TotalHeight())
 | 
			
		||||
		case k == tcell.KeyCtrlN:
 | 
			
		||||
			return view.flex.OnKeyEvent(tcell.NewEventKey(tcell.KeyEnter, '\n', event.Modifiers()))
 | 
			
		||||
		case c == 'a':
 | 
			
		||||
			view.SwitchRoom(view.roomList.NextWithActivity())
 | 
			
		||||
		case c == 'l':
 | 
			
		||||
			view.ShowBare(roomView)
 | 
			
		||||
			view.ShowBare(view.currentRoom)
 | 
			
		||||
		default:
 | 
			
		||||
			return key
 | 
			
		||||
			goto defaultHandler
 | 
			
		||||
		}
 | 
			
		||||
		return true
 | 
			
		||||
	} else if k == tcell.KeyAltDown || k == tcell.KeyCtrlDown {
 | 
			
		||||
		view.SwitchRoom(view.roomList.Next())
 | 
			
		||||
		return true
 | 
			
		||||
	} else if k == tcell.KeyAltUp || k == tcell.KeyCtrlUp {
 | 
			
		||||
		view.SwitchRoom(view.roomList.Previous())
 | 
			
		||||
	} else if k == tcell.KeyPgUp || k == tcell.KeyPgDn || k == tcell.KeyUp || k == tcell.KeyDown || k == tcell.KeyEnd || k == tcell.KeyHome {
 | 
			
		||||
		msgView := roomView.MessageView()
 | 
			
		||||
		return true
 | 
			
		||||
	} else if view.currentRoom != nil &&
 | 
			
		||||
		(k == tcell.KeyPgUp || k == tcell.KeyPgDn ||
 | 
			
		||||
			k == tcell.KeyUp || k == tcell.KeyDown ||
 | 
			
		||||
			k == tcell.KeyEnd || k == tcell.KeyHome) {
 | 
			
		||||
		// TODO these should be in the RoomView key handler
 | 
			
		||||
		msgView := view.currentRoom.MessageView()
 | 
			
		||||
 | 
			
		||||
		if msgView.IsAtTop() && (k == tcell.KeyPgUp || k == tcell.KeyUp) {
 | 
			
		||||
			go view.LoadHistory(roomView.Room.ID)
 | 
			
		||||
			go view.LoadHistory(view.currentRoom.Room.ID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch k {
 | 
			
		||||
@@ -219,80 +261,72 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *
 | 
			
		||||
			msgView.AddScrollOffset(msgView.Height() / 2)
 | 
			
		||||
		case tcell.KeyPgDn:
 | 
			
		||||
			msgView.AddScrollOffset(-msgView.Height() / 2)
 | 
			
		||||
		case tcell.KeyUp:
 | 
			
		||||
			msgView.AddScrollOffset(1)
 | 
			
		||||
		case tcell.KeyDown:
 | 
			
		||||
			msgView.AddScrollOffset(-1)
 | 
			
		||||
		case tcell.KeyHome:
 | 
			
		||||
			msgView.AddScrollOffset(msgView.TotalHeight())
 | 
			
		||||
		case tcell.KeyEnd:
 | 
			
		||||
			msgView.AddScrollOffset(-msgView.TotalHeight())
 | 
			
		||||
		default:
 | 
			
		||||
			goto defaultHandler
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return key
 | 
			
		||||
		return true
 | 
			
		||||
	} else if k == tcell.KeyEnter {
 | 
			
		||||
		view.InputSubmit(view.currentRoom, view.currentRoom.input.GetText())
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
defaultHandler:
 | 
			
		||||
	if view.config.Preferences.HideRoomList {
 | 
			
		||||
		debug.Print("Key event going to default handler (direct to roomview)", event)
 | 
			
		||||
		return view.roomView.OnKeyEvent(event)
 | 
			
		||||
	}
 | 
			
		||||
	debug.Print("Key event going to default handler (flex)", event)
 | 
			
		||||
	return view.flex.OnKeyEvent(event)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const WheelScrollOffsetDiff = 3
 | 
			
		||||
 | 
			
		||||
func isInArea(x, y int, p tview.Primitive) bool {
 | 
			
		||||
	rx, ry, rw, rh := p.GetRect()
 | 
			
		||||
	return x >= rx && y >= ry && x < rx+rw && y < ry+rh
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse {
 | 
			
		||||
	if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
 | 
			
		||||
		return event
 | 
			
		||||
func (view *MainView) OnMouseEvent(event mauview.MouseEvent) bool {
 | 
			
		||||
	if view.config.Preferences.HideRoomList {
 | 
			
		||||
		return view.roomView.OnMouseEvent(event)
 | 
			
		||||
	}
 | 
			
		||||
	return view.flex.OnMouseEvent(event)
 | 
			
		||||
	/*if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	view.BumpFocus(roomView)
 | 
			
		||||
 | 
			
		||||
	msgView := roomView.MessageView()
 | 
			
		||||
	view.BumpFocus(view.currentRoom)
 | 
			
		||||
 | 
			
		||||
	x, y := event.Position()
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case isInArea(x, y, msgView):
 | 
			
		||||
		mx, my, _, _ := msgView.GetRect()
 | 
			
		||||
		switch event.Buttons() {
 | 
			
		||||
		case tcell.WheelUp:
 | 
			
		||||
			if msgView.IsAtTop() {
 | 
			
		||||
				go view.LoadHistory(roomView.Room.ID)
 | 
			
		||||
			} else {
 | 
			
		||||
				msgView.AddScrollOffset(WheelScrollOffsetDiff)
 | 
			
		||||
 | 
			
		||||
				view.parent.Render()
 | 
			
		||||
			}
 | 
			
		||||
		case tcell.WheelDown:
 | 
			
		||||
			msgView.AddScrollOffset(-WheelScrollOffsetDiff)
 | 
			
		||||
			view.parent.Render()
 | 
			
		||||
			view.MarkRead(roomView)
 | 
			
		||||
		default:
 | 
			
		||||
			if msgView.HandleClick(x-mx, y-my, event.Buttons()) {
 | 
			
		||||
				view.parent.Render()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case isInArea(x, y, view.roomList):
 | 
			
		||||
		switch event.Buttons() {
 | 
			
		||||
		case tcell.WheelUp:
 | 
			
		||||
			view.roomList.AddScrollOffset(-WheelScrollOffsetDiff)
 | 
			
		||||
			view.parent.Render()
 | 
			
		||||
		case tcell.WheelDown:
 | 
			
		||||
			view.roomList.AddScrollOffset(WheelScrollOffsetDiff)
 | 
			
		||||
			view.parent.Render()
 | 
			
		||||
		case tcell.Button1:
 | 
			
		||||
			_, rly, _, _ := msgView.GetRect()
 | 
			
		||||
			line := y - rly + 1
 | 
			
		||||
			switchToTag, switchToRoom := view.roomList.HandleClick(x, line, event.Modifiers() == tcell.ModCtrl)
 | 
			
		||||
			if switchToRoom != nil {
 | 
			
		||||
				view.SwitchRoom(switchToTag, switchToRoom)
 | 
			
		||||
			} else {
 | 
			
		||||
				view.parent.Render()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case x >= 27:
 | 
			
		||||
		view.roomView.OnMouseEvent(mauview.OffsetMouseEvent(event, -27, 0))
 | 
			
		||||
		view.roomView.Focus()
 | 
			
		||||
		view.focused = view.roomView
 | 
			
		||||
	case x <= 25:
 | 
			
		||||
		view.roomList.OnMouseEvent(event)
 | 
			
		||||
		view.roomList.Focus()
 | 
			
		||||
		view.focused = view.roomList
 | 
			
		||||
	default:
 | 
			
		||||
		debug.Print("Unhandled mouse event:", event.Buttons(), event.Modifiers(), x, y)
 | 
			
		||||
	}
 | 
			
		||||
	return event
 | 
			
		||||
	return false*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) OnPasteEvent(event mauview.PasteEvent) bool {
 | 
			
		||||
	if view.modal != nil {
 | 
			
		||||
		return view.modal.OnPasteEvent(event)
 | 
			
		||||
	} else if view.config.Preferences.HideRoomList {
 | 
			
		||||
		return view.roomView.OnPasteEvent(event)
 | 
			
		||||
	}
 | 
			
		||||
	return view.flex.OnPasteEvent(event)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) Focus() {
 | 
			
		||||
	if view.focused != nil {
 | 
			
		||||
		view.focused.Focus()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) Blur() {
 | 
			
		||||
	if view.focused != nil {
 | 
			
		||||
		view.focused.Blur()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
 | 
			
		||||
@@ -300,29 +334,19 @@ func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.roomView.SwitchToPage(room.ID)
 | 
			
		||||
	roomView := view.rooms[room.ID]
 | 
			
		||||
	if roomView == nil {
 | 
			
		||||
		debug.Print("Tried to switch to non-nil room with nil roomView!")
 | 
			
		||||
		debug.Print(tag, room)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	view.roomView.SetInnerComponent(roomView)
 | 
			
		||||
	view.currentRoom = roomView
 | 
			
		||||
	view.MarkRead(roomView)
 | 
			
		||||
	view.roomList.SetSelected(tag, room)
 | 
			
		||||
	view.parent.app.SetFocus(view)
 | 
			
		||||
	view.parent.Render()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) Focus(delegate func(p tview.Primitive)) {
 | 
			
		||||
	room := view.roomList.SelectedRoom()
 | 
			
		||||
	if room != nil {
 | 
			
		||||
		roomView, ok := view.rooms[room.ID]
 | 
			
		||||
		if ok {
 | 
			
		||||
			delegate(roomView)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) SaveAllHistory() {
 | 
			
		||||
	for _, room := range view.rooms {
 | 
			
		||||
		err := room.SaveHistory(view.config.HistoryDir)
 | 
			
		||||
@@ -333,14 +357,11 @@ func (view *MainView) SaveAllHistory() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) addRoomPage(room *rooms.Room) {
 | 
			
		||||
	if !view.roomView.HasPage(room.ID) {
 | 
			
		||||
	if _, ok := view.rooms[room.ID]; !ok {
 | 
			
		||||
		roomView := NewRoomView(view, room).
 | 
			
		||||
			SetInputSubmitFunc(view.InputSubmit).
 | 
			
		||||
			SetInputChangedFunc(view.InputChanged).
 | 
			
		||||
			SetInputCapture(view.KeyEventHandler).
 | 
			
		||||
			SetMouseCapture(view.MouseEventHandler)
 | 
			
		||||
			SetInputChangedFunc(view.InputChanged)
 | 
			
		||||
		view.rooms[room.ID] = roomView
 | 
			
		||||
		view.roomView.AddPage(room.ID, roomView, true, false)
 | 
			
		||||
		roomView.UpdateUserList()
 | 
			
		||||
 | 
			
		||||
		_, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir)
 | 
			
		||||
@@ -387,7 +408,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) {
 | 
			
		||||
	view.roomList.Remove(room)
 | 
			
		||||
	view.SwitchRoom(view.roomList.Selected())
 | 
			
		||||
 | 
			
		||||
	view.roomView.RemovePage(room.ID)
 | 
			
		||||
	delete(view.rooms, room.ID)
 | 
			
		||||
 | 
			
		||||
	view.parent.Render()
 | 
			
		||||
@@ -395,7 +415,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) {
 | 
			
		||||
 | 
			
		||||
func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
 | 
			
		||||
	view.roomList.Clear()
 | 
			
		||||
	view.roomView.Clear()
 | 
			
		||||
	view.rooms = make(map[string]*RoomView)
 | 
			
		||||
	for _, room := range rooms {
 | 
			
		||||
		if room.HasLeft {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,562 +0,0 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2019 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU Affero General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
// Based on https://github.com/rivo/tview/blob/master/inputfield.go
 | 
			
		||||
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"math"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"github.com/zyedidia/clipboard"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AdvancedInputField is a multi-line user-editable text area.
 | 
			
		||||
//
 | 
			
		||||
// Use SetMaskCharacter() to hide input from onlookers (e.g. for password
 | 
			
		||||
// input).
 | 
			
		||||
type AdvancedInputField struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
 | 
			
		||||
	// Cursor position
 | 
			
		||||
	cursorOffset int
 | 
			
		||||
	viewOffset   int
 | 
			
		||||
 | 
			
		||||
	// The text that was entered.
 | 
			
		||||
	text string
 | 
			
		||||
 | 
			
		||||
	// The text to be displayed before the input area.
 | 
			
		||||
	label string
 | 
			
		||||
 | 
			
		||||
	// The text to be displayed in the input area when "text" is empty.
 | 
			
		||||
	placeholder string
 | 
			
		||||
 | 
			
		||||
	// The label color.
 | 
			
		||||
	labelColor tcell.Color
 | 
			
		||||
 | 
			
		||||
	// The background color of the input area.
 | 
			
		||||
	fieldBackgroundColor tcell.Color
 | 
			
		||||
 | 
			
		||||
	// The text color of the input area.
 | 
			
		||||
	fieldTextColor tcell.Color
 | 
			
		||||
 | 
			
		||||
	// The text color of the placeholder.
 | 
			
		||||
	placeholderTextColor tcell.Color
 | 
			
		||||
 | 
			
		||||
	// The screen width of the label area. A value of 0 means use the width of
 | 
			
		||||
	// the label text.
 | 
			
		||||
	labelWidth int
 | 
			
		||||
 | 
			
		||||
	// The screen width of the input area. A value of 0 means extend as much as
 | 
			
		||||
	// possible.
 | 
			
		||||
	fieldWidth int
 | 
			
		||||
 | 
			
		||||
	// A character to mask entered text (useful for password fields). A value of 0
 | 
			
		||||
	// disables masking.
 | 
			
		||||
	maskCharacter rune
 | 
			
		||||
 | 
			
		||||
	// Whether or not to enable vim-style keybindings.
 | 
			
		||||
	vimBindings bool
 | 
			
		||||
 | 
			
		||||
	// An optional function which may reject the last character that was entered.
 | 
			
		||||
	accept func(text string, ch rune) bool
 | 
			
		||||
 | 
			
		||||
	// An optional function which is called when the input has changed.
 | 
			
		||||
	changed func(text string)
 | 
			
		||||
 | 
			
		||||
	// An optional function which is called when the user indicated that they
 | 
			
		||||
	// are done entering text. The key which was pressed is provided (enter, tab, backtab or escape).
 | 
			
		||||
	done func(tcell.Key)
 | 
			
		||||
 | 
			
		||||
	// An optional function which is called when the user presses tab.
 | 
			
		||||
	tabComplete func(text string, cursorOffset int)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAdvancedInputField returns a new input field.
 | 
			
		||||
func NewAdvancedInputField() *AdvancedInputField {
 | 
			
		||||
	return &AdvancedInputField{
 | 
			
		||||
		Box:                  tview.NewBox(),
 | 
			
		||||
		labelColor:           tview.Styles.SecondaryTextColor,
 | 
			
		||||
		fieldBackgroundColor: tview.Styles.ContrastBackgroundColor,
 | 
			
		||||
		fieldTextColor:       tview.Styles.PrimaryTextColor,
 | 
			
		||||
		placeholderTextColor: tview.Styles.ContrastSecondaryTextColor,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetText sets the current text of the input field.
 | 
			
		||||
func (field *AdvancedInputField) SetText(text string) *AdvancedInputField {
 | 
			
		||||
	field.text = text
 | 
			
		||||
	if field.changed != nil {
 | 
			
		||||
		field.changed(text)
 | 
			
		||||
	}
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetTextAndMoveCursor sets the current text of the input field and moves the cursor with the width difference.
 | 
			
		||||
func (field *AdvancedInputField) SetTextAndMoveCursor(text string) *AdvancedInputField {
 | 
			
		||||
	oldWidth := runewidth.StringWidth(field.text)
 | 
			
		||||
	field.text = text
 | 
			
		||||
	newWidth := runewidth.StringWidth(field.text)
 | 
			
		||||
	if oldWidth != newWidth {
 | 
			
		||||
		field.cursorOffset += newWidth - oldWidth
 | 
			
		||||
	}
 | 
			
		||||
	if field.changed != nil {
 | 
			
		||||
		field.changed(field.text)
 | 
			
		||||
	}
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetText returns the current text of the input field.
 | 
			
		||||
func (field *AdvancedInputField) GetText() string {
 | 
			
		||||
	return field.text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetLabel sets the text to be displayed before the input area.
 | 
			
		||||
func (field *AdvancedInputField) SetLabel(label string) *AdvancedInputField {
 | 
			
		||||
	field.label = label
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
 | 
			
		||||
// primitive to use the width of the label string.
 | 
			
		||||
func (field *AdvancedInputField) SetLabelWidth(width int) *AdvancedInputField {
 | 
			
		||||
	field.labelWidth = width
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetLabel returns the text to be displayed before the input area.
 | 
			
		||||
func (field *AdvancedInputField) GetLabel() string {
 | 
			
		||||
	return field.label
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPlaceholder sets the text to be displayed when the input text is empty.
 | 
			
		||||
func (field *AdvancedInputField) SetPlaceholder(text string) *AdvancedInputField {
 | 
			
		||||
	field.placeholder = text
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetLabelColor sets the color of the label.
 | 
			
		||||
func (field *AdvancedInputField) SetLabelColor(color tcell.Color) *AdvancedInputField {
 | 
			
		||||
	field.labelColor = color
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFieldBackgroundColor sets the background color of the input area.
 | 
			
		||||
func (field *AdvancedInputField) SetFieldBackgroundColor(color tcell.Color) *AdvancedInputField {
 | 
			
		||||
	field.fieldBackgroundColor = color
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFieldTextColor sets the text color of the input area.
 | 
			
		||||
func (field *AdvancedInputField) SetFieldTextColor(color tcell.Color) *AdvancedInputField {
 | 
			
		||||
	field.fieldTextColor = color
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPlaceholderExtColor sets the text color of placeholder text.
 | 
			
		||||
func (field *AdvancedInputField) SetPlaceholderExtColor(color tcell.Color) *AdvancedInputField {
 | 
			
		||||
	field.placeholderTextColor = color
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFormAttributes sets attributes shared by all form items.
 | 
			
		||||
func (field *AdvancedInputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
 | 
			
		||||
	field.labelWidth = labelWidth
 | 
			
		||||
	field.labelColor = labelColor
 | 
			
		||||
	field.SetBackgroundColor(bgColor)
 | 
			
		||||
	field.fieldTextColor = fieldTextColor
 | 
			
		||||
	field.fieldBackgroundColor = fieldBgColor
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFieldWidth sets the screen width of the input area. A value of 0 means
 | 
			
		||||
// extend as much as possible.
 | 
			
		||||
func (field *AdvancedInputField) SetFieldWidth(width int) *AdvancedInputField {
 | 
			
		||||
	field.fieldWidth = width
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFieldWidth returns this primitive's field width.
 | 
			
		||||
func (field *AdvancedInputField) GetFieldWidth() int {
 | 
			
		||||
	return field.fieldWidth
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetMaskCharacter sets a character that masks user input on a screen. A value
 | 
			
		||||
// of 0 disables masking.
 | 
			
		||||
func (field *AdvancedInputField) SetMaskCharacter(mask rune) *AdvancedInputField {
 | 
			
		||||
	field.maskCharacter = mask
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetAcceptanceFunc sets a handler which may reject the last character that was
 | 
			
		||||
// entered (by returning false).
 | 
			
		||||
//
 | 
			
		||||
// This package defines a number of variables Prefixed with AdvancedInputField which may
 | 
			
		||||
// be used for common input (e.g. numbers, maximum text length).
 | 
			
		||||
func (field *AdvancedInputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *AdvancedInputField {
 | 
			
		||||
	field.accept = handler
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetChangedFunc sets a handler which is called whenever the text of the input
 | 
			
		||||
// field has changed. It receives the current text (after the change).
 | 
			
		||||
func (field *AdvancedInputField) SetChangedFunc(handler func(text string)) *AdvancedInputField {
 | 
			
		||||
	field.changed = handler
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetDoneFunc sets a handler which is called when the user is done entering
 | 
			
		||||
// text. The callback function is provided with the key that was pressed, which
 | 
			
		||||
// is one of the following:
 | 
			
		||||
//
 | 
			
		||||
//   - KeyEnter: Done entering text.
 | 
			
		||||
//   - KeyEscape: Abort text input.
 | 
			
		||||
//   - KeyTab: Tab
 | 
			
		||||
//   - KeyBacktab: Shift + Tab
 | 
			
		||||
func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *AdvancedInputField {
 | 
			
		||||
	field.done = handler
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *AdvancedInputField {
 | 
			
		||||
	field.tabComplete = handler
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFinishedFunc calls SetDoneFunc().
 | 
			
		||||
func (field *AdvancedInputField) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
 | 
			
		||||
	return field.SetDoneFunc(handler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// drawInput calculates the field width and draws the background.
 | 
			
		||||
func (field *AdvancedInputField) drawInput(screen tcell.Screen, rightLimit, x, y int) (fieldWidth int) {
 | 
			
		||||
	fieldWidth = field.fieldWidth
 | 
			
		||||
	if fieldWidth == 0 {
 | 
			
		||||
		fieldWidth = math.MaxInt32
 | 
			
		||||
	}
 | 
			
		||||
	if rightLimit-x < fieldWidth {
 | 
			
		||||
		fieldWidth = rightLimit - x
 | 
			
		||||
	}
 | 
			
		||||
	fieldStyle := tcell.StyleDefault.Background(field.fieldBackgroundColor)
 | 
			
		||||
	for index := 0; index < fieldWidth; index++ {
 | 
			
		||||
		screen.SetContent(x+index, y, ' ', nil, fieldStyle)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// prepareText prepares the text to be displayed and recalculates the view and cursor offsets.
 | 
			
		||||
func (field *AdvancedInputField) prepareText(screen tcell.Screen, fieldWidth, x, y int) (text string) {
 | 
			
		||||
	text = field.text
 | 
			
		||||
	if text == "" && field.placeholder != "" {
 | 
			
		||||
		tview.Print(screen, field.placeholder, x, y, fieldWidth, tview.AlignLeft, field.placeholderTextColor)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if field.maskCharacter > 0 {
 | 
			
		||||
		text = strings.Repeat(string(field.maskCharacter), utf8.RuneCountInString(field.text))
 | 
			
		||||
	}
 | 
			
		||||
	textWidth := runewidth.StringWidth(text)
 | 
			
		||||
	if field.cursorOffset >= textWidth {
 | 
			
		||||
		fieldWidth--
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if field.cursorOffset < field.viewOffset {
 | 
			
		||||
		field.viewOffset = field.cursorOffset
 | 
			
		||||
	} else if field.cursorOffset > field.viewOffset+fieldWidth {
 | 
			
		||||
		field.viewOffset = field.cursorOffset - fieldWidth
 | 
			
		||||
	} else if textWidth-field.viewOffset < fieldWidth {
 | 
			
		||||
		field.viewOffset = textWidth - fieldWidth
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if field.viewOffset < 0 {
 | 
			
		||||
		field.viewOffset = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// drawText draws the text and the cursor.
 | 
			
		||||
func (field *AdvancedInputField) drawText(screen tcell.Screen, fieldWidth, x, y int, text string) {
 | 
			
		||||
	runes := []rune(text)
 | 
			
		||||
	relPos := 0
 | 
			
		||||
	for pos := field.viewOffset; pos <= fieldWidth+field.viewOffset && pos < len(runes); pos++ {
 | 
			
		||||
		ch := runes[pos]
 | 
			
		||||
		w := runewidth.RuneWidth(ch)
 | 
			
		||||
		_, _, style, _ := screen.GetContent(x+relPos, y)
 | 
			
		||||
		style = style.Foreground(field.fieldTextColor)
 | 
			
		||||
		for w > 0 {
 | 
			
		||||
			screen.SetContent(x+relPos, y, ch, nil, style)
 | 
			
		||||
			relPos++
 | 
			
		||||
			w--
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set cursor.
 | 
			
		||||
	if field.GetFocusable().HasFocus() {
 | 
			
		||||
		field.setCursor(screen)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Draw draws this primitive onto the screen.
 | 
			
		||||
func (field *AdvancedInputField) Draw(screen tcell.Screen) {
 | 
			
		||||
	field.Box.Draw(screen)
 | 
			
		||||
 | 
			
		||||
	x, y, width, height := field.GetInnerRect()
 | 
			
		||||
	rightLimit := x + width
 | 
			
		||||
	if height < 1 || rightLimit <= x {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Draw label.
 | 
			
		||||
	if field.labelWidth > 0 {
 | 
			
		||||
		labelWidth := field.labelWidth
 | 
			
		||||
		if labelWidth > rightLimit-x {
 | 
			
		||||
			labelWidth = rightLimit - x
 | 
			
		||||
		}
 | 
			
		||||
		tview.Print(screen, field.label, x, y, labelWidth, tview.AlignLeft, field.labelColor)
 | 
			
		||||
		x += labelWidth
 | 
			
		||||
	} else {
 | 
			
		||||
		_, drawnWidth := tview.Print(screen, field.label, x, y, rightLimit-x, tview.AlignLeft, field.labelColor)
 | 
			
		||||
		x += drawnWidth
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fieldWidth := field.drawInput(screen, rightLimit, x, y)
 | 
			
		||||
	text := field.prepareText(screen, fieldWidth, x, y)
 | 
			
		||||
	field.drawText(screen, fieldWidth, x, y, text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) GetCursorOffset() int {
 | 
			
		||||
	return field.cursorOffset
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) SetCursorOffset(offset int) *AdvancedInputField {
 | 
			
		||||
	if offset < 0 {
 | 
			
		||||
		offset = 0
 | 
			
		||||
	} else {
 | 
			
		||||
		width := runewidth.StringWidth(field.text)
 | 
			
		||||
		if offset >= width {
 | 
			
		||||
			offset = width
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	field.cursorOffset = offset
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setCursor sets the cursor position.
 | 
			
		||||
func (field *AdvancedInputField) setCursor(screen tcell.Screen) {
 | 
			
		||||
	x, y, width, _ := field.GetRect()
 | 
			
		||||
	origX, origY := x, y
 | 
			
		||||
	rightLimit := x + width
 | 
			
		||||
	if field.HasBorder() {
 | 
			
		||||
		x++
 | 
			
		||||
		y++
 | 
			
		||||
		rightLimit -= 2
 | 
			
		||||
	}
 | 
			
		||||
	labelWidth := field.labelWidth
 | 
			
		||||
	if labelWidth == 0 {
 | 
			
		||||
		labelWidth = tview.StringWidth(field.label)
 | 
			
		||||
	}
 | 
			
		||||
	x = x + labelWidth + field.cursorOffset - field.viewOffset
 | 
			
		||||
	if x >= rightLimit {
 | 
			
		||||
		x = rightLimit - 1
 | 
			
		||||
	} else if x < origX {
 | 
			
		||||
		x = origY
 | 
			
		||||
	}
 | 
			
		||||
	screen.ShowCursor(x, y)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	lastWord  = regexp.MustCompile(`\S+\s*$`)
 | 
			
		||||
	firstWord = regexp.MustCompile(`^\s*\S+`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SubstringBefore(s string, w int) string {
 | 
			
		||||
	return runewidth.Truncate(s, w, "")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) TypeRune(ch rune) {
 | 
			
		||||
	leftPart := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	newText := leftPart + string(ch) + field.text[len(leftPart):]
 | 
			
		||||
	if field.accept != nil {
 | 
			
		||||
		if !field.accept(newText, ch) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	field.text = newText
 | 
			
		||||
	field.cursorOffset += runewidth.RuneWidth(ch)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) PasteClipboard() {
 | 
			
		||||
	clip, _ := clipboard.ReadAll("clipboard")
 | 
			
		||||
	leftPart := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	field.text = leftPart + clip + field.text[len(leftPart):]
 | 
			
		||||
	field.cursorOffset += runewidth.StringWidth(clip)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) MoveCursorLeft(moveWord bool) {
 | 
			
		||||
	before := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	if moveWord {
 | 
			
		||||
		found := lastWord.FindString(before)
 | 
			
		||||
		field.cursorOffset -= runewidth.StringWidth(found)
 | 
			
		||||
	} else if len(before) > 0 {
 | 
			
		||||
		beforeRunes := []rune(before)
 | 
			
		||||
		char := beforeRunes[len(beforeRunes)-1]
 | 
			
		||||
		field.cursorOffset -= runewidth.RuneWidth(char)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) MoveCursorRight(moveWord bool) {
 | 
			
		||||
	before := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	after := field.text[len(before):]
 | 
			
		||||
	if moveWord {
 | 
			
		||||
		found := firstWord.FindString(after)
 | 
			
		||||
		field.cursorOffset += runewidth.StringWidth(found)
 | 
			
		||||
	} else if len(after) > 0 {
 | 
			
		||||
		char := []rune(after)[0]
 | 
			
		||||
		field.cursorOffset += runewidth.RuneWidth(char)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) RemoveNextCharacter() {
 | 
			
		||||
	if field.cursorOffset >= runewidth.StringWidth(field.text) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	leftPart := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	// Take everything after the left part minus the first character.
 | 
			
		||||
	rightPart := string([]rune(field.text[len(leftPart):])[1:])
 | 
			
		||||
 | 
			
		||||
	field.text = leftPart + rightPart
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) Clear() {
 | 
			
		||||
	field.text = ""
 | 
			
		||||
	field.cursorOffset = 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) RemovePreviousWord() {
 | 
			
		||||
	leftPart := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	rightPart := field.text[len(leftPart):]
 | 
			
		||||
	replacement := lastWord.ReplaceAllString(leftPart, "")
 | 
			
		||||
	field.text = replacement + rightPart
 | 
			
		||||
 | 
			
		||||
	field.cursorOffset -= runewidth.StringWidth(leftPart) - runewidth.StringWidth(replacement)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) RemovePreviousCharacter() {
 | 
			
		||||
	if field.cursorOffset == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	leftPart := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	rightPart := field.text[len(leftPart):]
 | 
			
		||||
 | 
			
		||||
	// Take everything before the right part minus the last character.
 | 
			
		||||
	leftPartRunes := []rune(leftPart)
 | 
			
		||||
	leftPartRunes = leftPartRunes[0 : len(leftPartRunes)-1]
 | 
			
		||||
	leftPart = string(leftPartRunes)
 | 
			
		||||
 | 
			
		||||
	// Figure out what character was removed to correctly decrease cursorOffset.
 | 
			
		||||
	removedChar := field.text[len(leftPart) : len(field.text)-len(rightPart)]
 | 
			
		||||
 | 
			
		||||
	field.text = leftPart + rightPart
 | 
			
		||||
 | 
			
		||||
	field.cursorOffset -= runewidth.StringWidth(removedChar)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) TriggerTabComplete() bool {
 | 
			
		||||
	if field.tabComplete != nil {
 | 
			
		||||
		field.tabComplete(field.text, field.cursorOffset)
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) handleInputChanges(originalText string) {
 | 
			
		||||
	// Trigger changed events.
 | 
			
		||||
	if field.text != originalText && field.changed != nil {
 | 
			
		||||
		field.changed(field.text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make sure cursor offset is valid
 | 
			
		||||
	if field.cursorOffset < 0 {
 | 
			
		||||
		field.cursorOffset = 0
 | 
			
		||||
	}
 | 
			
		||||
	width := runewidth.StringWidth(field.text)
 | 
			
		||||
	if field.cursorOffset > width {
 | 
			
		||||
		field.cursorOffset = width
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InputHandler returns the handler for this primitive.
 | 
			
		||||
func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
 | 
			
		||||
	return field.WrapInputHandler(field.inputHandler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) PasteHandler() func(event *tcell.EventPaste) {
 | 
			
		||||
	return field.WrapPasteHandler(field.pasteHandler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) pasteHandler(event *tcell.EventPaste) {
 | 
			
		||||
	defer field.handleInputChanges(field.text)
 | 
			
		||||
	clip := event.Text()
 | 
			
		||||
	leftPart := SubstringBefore(field.text, field.cursorOffset)
 | 
			
		||||
	field.text = leftPart + clip + field.text[len(leftPart):]
 | 
			
		||||
	field.cursorOffset += runewidth.StringWidth(clip)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (field *AdvancedInputField) inputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
 | 
			
		||||
	defer field.handleInputChanges(field.text)
 | 
			
		||||
 | 
			
		||||
	// Process key event.
 | 
			
		||||
	switch key := event.Key(); key {
 | 
			
		||||
	case tcell.KeyRune:
 | 
			
		||||
		field.TypeRune(event.Rune())
 | 
			
		||||
	case tcell.KeyCtrlV:
 | 
			
		||||
		field.PasteClipboard()
 | 
			
		||||
	case tcell.KeyLeft:
 | 
			
		||||
		field.MoveCursorLeft(event.Modifiers() == tcell.ModCtrl)
 | 
			
		||||
	case tcell.KeyRight:
 | 
			
		||||
		field.MoveCursorRight(event.Modifiers() == tcell.ModCtrl)
 | 
			
		||||
	case tcell.KeyDelete:
 | 
			
		||||
		field.RemoveNextCharacter()
 | 
			
		||||
	case tcell.KeyCtrlU:
 | 
			
		||||
		if field.vimBindings {
 | 
			
		||||
			field.Clear()
 | 
			
		||||
		}
 | 
			
		||||
	case tcell.KeyCtrlW:
 | 
			
		||||
		if field.vimBindings {
 | 
			
		||||
			field.RemovePreviousWord()
 | 
			
		||||
		}
 | 
			
		||||
	case tcell.KeyBackspace:
 | 
			
		||||
		field.RemovePreviousWord()
 | 
			
		||||
	case tcell.KeyBackspace2:
 | 
			
		||||
		field.RemovePreviousCharacter()
 | 
			
		||||
	case tcell.KeyTab:
 | 
			
		||||
		if field.TriggerTabComplete() {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyBacktab:
 | 
			
		||||
		if field.done != nil {
 | 
			
		||||
			field.done(key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -17,8 +17,8 @@
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Border is a simple tview widget that renders a horizontal or vertical bar.
 | 
			
		||||
@@ -27,24 +27,37 @@ import (
 | 
			
		||||
// If the height is 1, the bar will be horizontal.
 | 
			
		||||
// If the width nor the height are 1, nothing will be rendered.
 | 
			
		||||
type Border struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
	Style tcell.Style
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBorder wraps a new tview Box into a new Border.
 | 
			
		||||
func NewBorder() *Border {
 | 
			
		||||
	return &Border{tview.NewBox()}
 | 
			
		||||
	return &Border{
 | 
			
		||||
		Style: tcell.StyleDefault.Foreground(mauview.Styles.BorderColor),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (border *Border) Draw(screen tcell.Screen) {
 | 
			
		||||
	background := tcell.StyleDefault.Background(border.GetBackgroundColor()).Foreground(border.GetBorderColor())
 | 
			
		||||
	x, y, width, height := border.GetRect()
 | 
			
		||||
func (border *Border) Draw(screen mauview.Screen) {
 | 
			
		||||
	width, height := screen.Size()
 | 
			
		||||
	if width == 1 {
 | 
			
		||||
		for borderY := y; borderY < y+height; borderY++ {
 | 
			
		||||
			screen.SetContent(x, borderY, tview.Borders.Vertical, nil, background)
 | 
			
		||||
		for borderY := 0; borderY < height; borderY++ {
 | 
			
		||||
			screen.SetContent(0, borderY, mauview.Borders.Vertical, nil, border.Style)
 | 
			
		||||
		}
 | 
			
		||||
	} else if height == 1 {
 | 
			
		||||
		for borderX := x; borderX < x+width; borderX++ {
 | 
			
		||||
			screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background)
 | 
			
		||||
		for borderX := 0; borderX < width; borderX++ {
 | 
			
		||||
			screen.SetContent(borderX, 0, mauview.Borders.Horizontal, nil, border.Style)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (border *Border) OnKeyEvent(event mauview.KeyEvent) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (border *Border) OnPasteEvent(event mauview.PasteEvent) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (border *Border) OnMouseEvent(event mauview.MouseEvent) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2019 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU Affero General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Center wraps the given tview primitive into a Flex element in order to
 | 
			
		||||
// vertically and horizontally center the given primitive.
 | 
			
		||||
func Center(width, height int, p tview.Primitive) tview.Primitive {
 | 
			
		||||
	return tview.NewFlex().
 | 
			
		||||
		AddItem(nil, 0, 1, false).
 | 
			
		||||
		AddItem(tview.NewFlex().
 | 
			
		||||
			SetDirection(tview.FlexRow).
 | 
			
		||||
			AddItem(nil, 0, 1, false).
 | 
			
		||||
			AddItem(p, height, 1, true).
 | 
			
		||||
			AddItem(nil, 0, 1, false), width, 1, true).
 | 
			
		||||
		AddItem(nil, 0, 1, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type transparentCenter struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
	prefWidth, prefHeight int
 | 
			
		||||
	p                     tview.Primitive
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TransparentCenter(width, height int, p tview.Primitive) tview.Primitive {
 | 
			
		||||
	return &transparentCenter{
 | 
			
		||||
		Box:        tview.NewBox(),
 | 
			
		||||
		prefWidth:  width,
 | 
			
		||||
		prefHeight: height,
 | 
			
		||||
		p:          p,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tc *transparentCenter) Draw(screen tcell.Screen) {
 | 
			
		||||
	x, y, width, height := tc.GetRect()
 | 
			
		||||
	if width > tc.prefWidth {
 | 
			
		||||
		x += (width - tc.prefWidth) / 2
 | 
			
		||||
		width = tc.prefWidth
 | 
			
		||||
	}
 | 
			
		||||
	if height > tc.prefHeight {
 | 
			
		||||
		y += (height - tc.prefHeight) / 2
 | 
			
		||||
		height = tc.prefHeight
 | 
			
		||||
	}
 | 
			
		||||
	tc.p.SetRect(x, y, width, height)
 | 
			
		||||
	tc.p.Draw(screen)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tc *transparentCenter) Focus(delegate func(p tview.Primitive)) {
 | 
			
		||||
	if delegate != nil {
 | 
			
		||||
		delegate(tc.p)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2019 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU Affero General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FormTextView struct {
 | 
			
		||||
	*tview.TextView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ftv *FormTextView) GetLabel() string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ftv *FormTextView) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
 | 
			
		||||
	return ftv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ftv *FormTextView) GetFieldWidth() int {
 | 
			
		||||
	_, _, w, _ := ftv.TextView.GetRect()
 | 
			
		||||
	return w
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ftv *FormTextView) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
 | 
			
		||||
	ftv.SetDoneFunc(handler)
 | 
			
		||||
	return ftv
 | 
			
		||||
}
 | 
			
		||||
@@ -18,28 +18,31 @@ package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func WriteLineSimple(screen tcell.Screen, line string, x, y int) {
 | 
			
		||||
	WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
 | 
			
		||||
func WriteLineSimple(screen mauview.Screen, line string, x, y int) {
 | 
			
		||||
	WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WriteLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) {
 | 
			
		||||
	WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
func WriteLineSimpleColor(screen mauview.Screen, line string, x, y int, color tcell.Color) {
 | 
			
		||||
	WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WriteLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
 | 
			
		||||
func WriteLineColor(screen mauview.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
 | 
			
		||||
	WriteLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
 | 
			
		||||
func WriteLine(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
 | 
			
		||||
	offsetX := 0
 | 
			
		||||
	if align == tview.AlignRight {
 | 
			
		||||
		offsetX = maxWidth - runewidth.StringWidth(line)
 | 
			
		||||
	if align == mauview.AlignRight {
 | 
			
		||||
		// TODO is mauview.StringWidth correct here?
 | 
			
		||||
		offsetX = maxWidth - mauview.StringWidth(line)
 | 
			
		||||
	}
 | 
			
		||||
	if offsetX < 0 {
 | 
			
		||||
		offsetX = 0
 | 
			
		||||
@@ -60,12 +63,12 @@ func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WriteLinePadded(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
 | 
			
		||||
func WriteLinePadded(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
 | 
			
		||||
	padding := strconv.Itoa(maxWidth)
 | 
			
		||||
	if align == tview.AlignRight {
 | 
			
		||||
	if align == mauview.AlignRight {
 | 
			
		||||
		line = fmt.Sprintf("%"+padding+"s", line)
 | 
			
		||||
	} else {
 | 
			
		||||
		line = fmt.Sprintf("%-"+padding+"s", line)
 | 
			
		||||
	}
 | 
			
		||||
	WriteLine(screen, tview.AlignLeft, line, x, y, maxWidth, style)
 | 
			
		||||
	WriteLine(screen, mauview.AlignLeft, line, x, y, maxWidth, style)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user