Start moving to mauview

This commit is contained in:
Tulir Asokan 2019-03-26 00:37:35 +02:00
parent ae36b9cddd
commit 8aa134b8b2
19 changed files with 554 additions and 1110 deletions

3
go.mod
View File

@ -11,9 +11,8 @@ require (
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b
golang.org/x/net v0.0.0-20190110200230-915654e7eabc
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2
maunium.net/go/mautrix v0.1.0-alpha.3
maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686
maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b
)

18
go.sum
View File

@ -10,6 +10,8 @@ github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC1
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@ -18,29 +20,17 @@ github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maunium.net/go/mautrix v0.1.0-alpha.2 h1:NsLc5tyrp5tyrKTvFSmqcLi+FISQ+FsuWC/ycL08PzI=
maunium.net/go/mautrix v0.1.0-alpha.2/go.mod h1:C8akEpHpmmO8gQhLvmInr3HujhUXyKvCoCAzFsxHjGE=
maunium.net/go/mautrix v0.1.0-alpha.3 h1:kBz7M63hRetQnAnYK+gVmuSxsmZesX6xERphVgEn324=
maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI=
maunium.net/go/tcell v0.0.0-20190111210611-542340841245 h1:JIekRWZ4na6cZJa5VMwLFpuiGzmeX+5Xx+WRVOHQHuk=
maunium.net/go/tcell v0.0.0-20190111210611-542340841245/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM=
maunium.net/go/tcell v0.0.0-20190111212645-703b3f6ecec9 h1:aSZPhcBmGu8MIddPxWNksfjbXHmlRL0yUddwB9CP4s0=
maunium.net/go/tcell v0.0.0-20190111212645-703b3f6ecec9/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM=
maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686 h1:kFgijToFPbMQGIMElizZGPQsffu+ZqO0olORXnfj1g4=
maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686/go.mod h1:Uw1CaNoCs9id/rKBF3Eg9KhhFVg+3akJTebZomFKW+4=
maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009 h1:4lojuJmNSun1nUB67m3DGg+RkYg1MUO6aUxgKQU5iZk=
maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM=
maunium.net/go/tview v0.0.0-20190111211351-2f23a5129af0 h1:xHG0S9ExKp+6dkhasnK/fgO9mLHSSSqVoAymjyUtDdI=
maunium.net/go/tview v0.0.0-20190111211351-2f23a5129af0/go.mod h1:ypYT6Dn71E7sVv6NxCjNo2cBJWJa257VSHCGOssGbV0=
maunium.net/go/tview v0.0.0-20190111212720-d6aa1eac1b9a h1:f4JVX4GHJH/wMcL9VACMVXT0eaWhxx9a7OT/NLcxsw8=
maunium.net/go/tview v0.0.0-20190111212720-d6aa1eac1b9a/go.mod h1:CTOF8OnDeK31Wl25GXdbYzTDvvZoiazmKNZdwkVUmzE=
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b h1:misvyPolT0TVGAtjc9Lr+oEYqGsV4YDByJVqZHxuu70=
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b/go.mod h1:Oi2eW32B8/cE7ZYXL6jyHMrXJL8ARDOQk/3aDvLEyVs=

View File

@ -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

View File

@ -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)
}

View File

@ -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.")

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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++
}
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}