From 72945c9a284b6858594f1e8a43743c397e90c380 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 18 Mar 2018 21:24:03 +0200 Subject: [PATCH] Organize files --- config.go => config/config.go | 17 ++- session.go => config/session.go | 27 +++-- go.mod | 14 --- gomuks.go | 60 +++++----- interface/gomuks.go | 35 ++++++ interface/matrix.go | 40 +++++++ interface/ui.go | 54 +++++++++ matrix.go => matrix/matrix.go | 105 +++++++++++------- matrix/room/member.go | 51 +++++++++ room.go => matrix/room/room.go | 42 +------ sync.go => matrix/sync.go | 27 ++--- debug.go => ui/debug/debug.go | 64 ++++++----- ui/types/message.go | 85 ++++++++++++++ ui.go => ui/ui.go | 29 ++--- view-login.go => ui/view-login.go | 19 ++-- view-main.go => ui/view-main.go | 64 +++++------ .../widget/advanced-inputfield.go | 2 +- border.go => ui/widget/border.go | 2 +- ui/widget/center.go | 32 ++++++ ui/widget/color.go | 60 ++++++++++ uiutil.go => ui/widget/form-text-view.go | 13 +-- message-view.go => ui/widget/message-view.go | 97 ++++------------ room-view.go => ui/widget/room-view.go | 73 ++++-------- 23 files changed, 620 insertions(+), 392 deletions(-) rename config.go => config/config.go (82%) rename session.go => config/session.go (80%) delete mode 100644 go.mod create mode 100644 interface/gomuks.go create mode 100644 interface/matrix.go create mode 100644 interface/ui.go rename matrix.go => matrix/matrix.go (64%) create mode 100644 matrix/room/member.go rename room.go => matrix/room/room.go (80%) rename sync.go => matrix/sync.go (88%) rename debug.go => ui/debug/debug.go (54%) create mode 100644 ui/types/message.go rename ui.go => ui/ui.go (71%) rename view-login.go => ui/view-login.go (76%) rename view-main.go => ui/view-main.go (86%) rename advanced-inputfield.go => ui/widget/advanced-inputfield.go (99%) rename border.go => ui/widget/border.go (98%) create mode 100644 ui/widget/center.go create mode 100644 ui/widget/color.go rename uiutil.go => ui/widget/form-text-view.go (77%) rename message-view.go => ui/widget/message-view.go (78%) rename room-view.go => ui/widget/room-view.go (75%) diff --git a/config.go b/config/config.go similarity index 82% rename from config.go rename to config/config.go index f8696a4..d4ede80 100644 --- a/config.go +++ b/config/config.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package config import ( "fmt" @@ -23,22 +23,19 @@ import ( "path/filepath" "gopkg.in/yaml.v2" + "maunium.net/go/gomuks/ui/debug" ) type Config struct { MXID string `yaml:"mxid"` HS string `yaml:"homeserver"` - dir string `yaml:"-"` - gmx Gomuks `yaml:"-"` - debug DebugPrinter `yaml:"-"` - Session *Session `yaml:"-"` + dir string `yaml:"-"` + Session *Session `yaml:"-"` } -func NewConfig(gmx Gomuks, dir string) *Config { +func NewConfig(dir string) *Config { return &Config{ - gmx: gmx, - debug: gmx.Debug(), dir: dir, } } @@ -67,14 +64,14 @@ func (config *Config) Save() { os.MkdirAll(config.dir, 0700) data, err := yaml.Marshal(&config) if err != nil { - config.debug.Print("Failed to marshal config") + debug.Print("Failed to marshal config") panic(err) } path := filepath.Join(config.dir, "config.yaml") err = ioutil.WriteFile(path, data, 0600) if err != nil { - config.debug.Print("Failed to write config to", path) + debug.Print("Failed to write config to", path) panic(err) } } diff --git a/session.go b/config/session.go similarity index 80% rename from session.go rename to config/session.go index eda49dc..a90fc20 100644 --- a/session.go +++ b/config/session.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package config import ( "encoding/json" @@ -22,6 +22,8 @@ import ( "path/filepath" "maunium.net/go/gomatrix" + rooms "maunium.net/go/gomuks/matrix/room" + "maunium.net/go/gomuks/ui/debug" ) type Session struct { @@ -30,9 +32,7 @@ type Session struct { AccessToken string NextBatch string FilterID string - Rooms map[string]*Room - - debug DebugPrinter `json:"-"` + Rooms map[string]*rooms.Room } func (config *Config) LoadSession(mxid string) { @@ -44,13 +44,12 @@ func (config *Config) NewSession(mxid string) *Session { return &Session{ MXID: mxid, path: filepath.Join(config.dir, mxid+".session"), - Rooms: make(map[string]*Room), - debug: config.debug, + Rooms: make(map[string]*rooms.Room), } } func (s *Session) Clear() { - s.Rooms = make(map[string]*Room) + s.Rooms = make(map[string]*rooms.Room) s.NextBatch = "" s.FilterID = "" s.Save() @@ -59,13 +58,13 @@ func (s *Session) Clear() { func (s *Session) Load() { data, err := ioutil.ReadFile(s.path) if err != nil { - s.debug.Print("Failed to read session from", s.path) + debug.Print("Failed to read session from", s.path) panic(err) } err = json.Unmarshal(data, s) if err != nil { - s.debug.Print("Failed to parse session at", s.path) + debug.Print("Failed to parse session at", s.path) panic(err) } } @@ -73,13 +72,13 @@ func (s *Session) Load() { func (s *Session) Save() { data, err := json.Marshal(s) if err != nil { - s.debug.Print("Failed to marshal session of", s.MXID) + debug.Print("Failed to marshal session of", s.MXID) panic(err) } err = ioutil.WriteFile(s.path, data, 0600) if err != nil { - s.debug.Print("Failed to write session to", s.path) + debug.Print("Failed to write session to", s.path) panic(err) } } @@ -92,16 +91,16 @@ func (s *Session) LoadNextBatch(_ string) string { return s.NextBatch } -func (s *Session) GetRoom(mxid string) *Room { +func (s *Session) GetRoom(mxid string) *rooms.Room { room, _ := s.Rooms[mxid] if room == nil { - room = NewRoom(mxid) + room = rooms.NewRoom(mxid) s.Rooms[room.ID] = room } return room } -func (s *Session) PutRoom(room *Room) { +func (s *Session) PutRoom(room *rooms.Room) { s.Rooms[room.ID] = room s.Save() } diff --git a/go.mod b/go.mod deleted file mode 100644 index 4888c39..0000000 --- a/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module "maunium.net/go/gomuks" - -require ( - "github.com/gdamore/encoding" v0.0.0-20151215212835-b23993cbb635 - "github.com/gdamore/tcell" v1.0.0 - "github.com/jroimartin/gocui" v0.0.0-20170827195011-4f518eddb04b - "github.com/lucasb-eyer/go-colorful" v0.0.0-20170903184257-231272389856 - "github.com/matrix-org/gomatrix" v0.0.0-20171003113848-a7fc80c8060c - "github.com/mattn/go-runewidth" v0.0.2 - "github.com/nsf/termbox-go" v0.0.0-20180303152453-e2050e41c884 - "github.com/rivo/tview" v0.0.0-20180313071706-0b69b9b58142 - "golang.org/x/text" v0.0.0-20171214130843-f21a4dfb5e38 - "gopkg.in/yaml.v2" v1.1.1-gopkgin-v2.1.1 -) diff --git a/gomuks.go b/gomuks.go index 5fa0a8a..fecb67a 100644 --- a/gomuks.go +++ b/gomuks.go @@ -21,43 +21,37 @@ import ( "path/filepath" "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/config" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/matrix" + "maunium.net/go/gomuks/ui" + "maunium.net/go/gomuks/ui/debug" "maunium.net/go/tview" ) -type Gomuks interface { - Debug() DebugPrinter - Matrix() *gomatrix.Client - MatrixContainer() *MatrixContainer - App() *tview.Application - UI() *GomuksUI - Config() *Config - - Start() - Stop() - Recover() -} - type gomuks struct { app *tview.Application - ui *GomuksUI - matrix *MatrixContainer - debug *DebugPane - config *Config + ui *ui.GomuksUI + matrix *matrix.Container + debug *debug.Pane + config *config.Config } -var gdebug DebugPrinter - -func NewGomuks(debug bool) *gomuks { +func NewGomuks(enableDebug bool) *gomuks { configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") gmx := &gomuks{ app: tview.NewApplication(), } - gmx.debug = NewDebugPane(gmx) - gdebug = gmx.debug - gmx.config = NewConfig(gmx, configDir) - gmx.ui = NewGomuksUI(gmx) - gmx.matrix = NewMatrixContainer(gmx) - gmx.ui.matrix = gmx.matrix + + gmx.debug = debug.NewPane() + gmx.debug.SetChangedFunc(func() { + gmx.ui.Render() + }) + debug.Default = gmx.debug + + gmx.config = config.NewConfig(configDir) + gmx.ui = ui.NewGomuksUI(gmx) + gmx.matrix = matrix.NewMatrixContainer(gmx) gmx.config.Load() if len(gmx.config.MXID) > 0 { @@ -67,7 +61,7 @@ func NewGomuks(debug bool) *gomuks { gmx.matrix.InitClient() main := gmx.ui.InitViews() - if debug { + if enableDebug { main = gmx.debug.Wrap(main) } gmx.app.SetRoot(main, true) @@ -101,15 +95,11 @@ func (gmx *gomuks) Start() { } } -func (gmx *gomuks) Debug() DebugPrinter { - return gmx.debug -} - func (gmx *gomuks) Matrix() *gomatrix.Client { - return gmx.matrix.client + return gmx.matrix.Client() } -func (gmx *gomuks) MatrixContainer() *MatrixContainer { +func (gmx *gomuks) MatrixContainer() ifc.MatrixContainer { return gmx.matrix } @@ -117,11 +107,11 @@ func (gmx *gomuks) App() *tview.Application { return gmx.app } -func (gmx *gomuks) Config() *Config { +func (gmx *gomuks) Config() *config.Config { return gmx.config } -func (gmx *gomuks) UI() *GomuksUI { +func (gmx *gomuks) UI() ifc.GomuksUI { return gmx.ui } diff --git a/interface/gomuks.go b/interface/gomuks.go new file mode 100644 index 0000000..b90aa88 --- /dev/null +++ b/interface/gomuks.go @@ -0,0 +1,35 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ifc + +import ( + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/config" + "maunium.net/go/tview" +) + +type Gomuks interface { + Matrix() *gomatrix.Client + MatrixContainer() MatrixContainer + App() *tview.Application + UI() GomuksUI + Config() *config.Config + + Start() + Stop() + Recover() +} diff --git a/interface/matrix.go b/interface/matrix.go new file mode 100644 index 0000000..4c30b5e --- /dev/null +++ b/interface/matrix.go @@ -0,0 +1,40 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ifc + +import ( + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/room" +) + +type MatrixContainer interface { + Client() *gomatrix.Client + InitClient() error + Initialized() bool + Login(user, password string) error + Start() + Stop() + // HandleMessage(evt *gomatrix.Event) + // HandleMembership(evt *gomatrix.Event) + // HandleTyping(evt *gomatrix.Event) + SendMessage(roomID, message string) + SendTyping(roomID string, typing bool) + JoinRoom(roomID string) error + LeaveRoom(roomID string) error + GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) + GetRoom(roomID string) *room.Room +} diff --git a/interface/ui.go b/interface/ui.go new file mode 100644 index 0000000..406aa2f --- /dev/null +++ b/interface/ui.go @@ -0,0 +1,54 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ifc + +import ( + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/ui/types" + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tview" +) + +type View string + +// Allowed views in GomuksUI +const ( + ViewLogin View = "login" + ViewMain View = "main" +) + +type GomuksUI interface { + Render() + SetView(name View) + InitViews() tview.Primitive + MainView() MainView +} + +type MainView interface { + InputTabComplete(text string, cursorOffset int) string + GetRoom(roomID string) *widget.RoomView + HasRoom(roomID string) bool + AddRoom(roomID string) + RemoveRoom(roomID string) + SetRooms(roomIDs []string) + + SetTyping(roomID string, users []string) + AddServiceMessage(roomID string, message string) + GetHistory(room string) + ProcessMessageEvent(evt *gomatrix.Event) (*widget.RoomView, *types.Message) + ProcessMembershipEvent(evt *gomatrix.Event, new bool) (*widget.RoomView, *types.Message) +} diff --git a/matrix.go b/matrix/matrix.go similarity index 64% rename from matrix.go rename to matrix/matrix.go index ea1c5c6..7652188 100644 --- a/matrix.go +++ b/matrix/matrix.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package matrix import ( "fmt" @@ -22,24 +22,27 @@ import ( "time" "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/config" + "maunium.net/go/gomuks/interface" + rooms "maunium.net/go/gomuks/matrix/room" + "maunium.net/go/gomuks/ui/debug" + "maunium.net/go/gomuks/ui/widget" ) -type MatrixContainer struct { +type Container struct { client *gomatrix.Client - gmx Gomuks - ui *GomuksUI - debug DebugPrinter - config *Config + gmx ifc.Gomuks + ui ifc.GomuksUI + config *config.Config running bool stop chan bool typing int64 } -func NewMatrixContainer(gmx Gomuks) *MatrixContainer { - c := &MatrixContainer{ +func NewMatrixContainer(gmx ifc.Gomuks) *Container { + c := &Container{ config: gmx.Config(), - debug: gmx.Debug(), ui: gmx.UI(), gmx: gmx, } @@ -47,7 +50,7 @@ func NewMatrixContainer(gmx Gomuks) *MatrixContainer { return c } -func (c *MatrixContainer) InitClient() error { +func (c *Container) InitClient() error { if len(c.config.HS) == 0 { return fmt.Errorf("no homeserver in config") } @@ -77,11 +80,11 @@ func (c *MatrixContainer) InitClient() error { return nil } -func (c *MatrixContainer) Initialized() bool { +func (c *Container) Initialized() bool { return c.client != nil } -func (c *MatrixContainer) Login(user, password string) error { +func (c *Container) Login(user, password string) error { resp, err := c.client.Login(&gomatrix.ReqLogin{ Type: "m.login.password", User: user, @@ -103,24 +106,28 @@ func (c *MatrixContainer) Login(user, password string) error { return nil } -func (c *MatrixContainer) Stop() { +func (c *Container) Stop() { if c.running { c.stop <- true c.client.StopSync() } } -func (c *MatrixContainer) UpdateRoomList() { - rooms, err := c.client.JoinedRooms() +func (c *Container) Client() *gomatrix.Client { + return c.client +} + +func (c *Container) UpdateRoomList() { + resp, err := c.client.JoinedRooms() if err != nil { - c.debug.Print("Error fetching room list:", err) + debug.Print("Error fetching room list:", err) return } - c.ui.MainView().SetRoomList(rooms.JoinedRooms) + c.ui.MainView().SetRooms(resp.JoinedRooms) } -func (c *MatrixContainer) OnLogin() { +func (c *Container) OnLogin() { c.client.Store = c.config.Session syncer := NewGomuksSyncer(c.config.Session) @@ -132,38 +139,38 @@ func (c *MatrixContainer) OnLogin() { c.UpdateRoomList() } -func (c *MatrixContainer) Start() { +func (c *Container) Start() { defer c.gmx.Recover() + c.ui.SetView(ifc.ViewMain) c.OnLogin() - c.debug.Print("Starting sync...") + debug.Print("Starting sync...") c.running = true - c.ui.SetView(ViewMain) for { select { case <-c.stop: - c.debug.Print("Stopping sync...") + debug.Print("Stopping sync...") c.running = false return default: if err := c.client.Sync(); err != nil { - c.debug.Print("Sync() errored", err) + debug.Print("Sync() errored", err) } else { - c.debug.Print("Sync() returned without error") + debug.Print("Sync() returned without error") } } } } -func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) { +func (c *Container) HandleMessage(evt *gomatrix.Event) { room, message := c.ui.MainView().ProcessMessageEvent(evt) if room != nil { - room.AddMessage(message, AppendMessage) + room.AddMessage(message, widget.AppendMessage) } } -func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) { +func (c *Container) HandleMembership(evt *gomatrix.Event) { const Hour = 1 * 60 * 60 * 1000 if evt.Unsigned.Age > Hour { return @@ -172,15 +179,15 @@ func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) { room, message := c.ui.MainView().ProcessMembershipEvent(evt, true) if room != nil { // TODO this shouldn't be necessary - room.room.UpdateState(evt) + room.Room.UpdateState(evt) // TODO This should probably also be in a different place room.UpdateUserList() - room.AddMessage(message, AppendMessage) + room.AddMessage(message, widget.AppendMessage) } } -func (c *MatrixContainer) HandleTyping(evt *gomatrix.Event) { +func (c *Container) HandleTyping(evt *gomatrix.Event) { users := evt.Content["user_ids"].([]interface{}) strUsers := make([]string, len(users)) @@ -190,29 +197,29 @@ func (c *MatrixContainer) HandleTyping(evt *gomatrix.Event) { c.ui.MainView().SetTyping(evt.RoomID, strUsers) } -func (c *MatrixContainer) SendMessage(roomID, message string) { +func (c *Container) SendMessage(roomID, message string) { c.gmx.Recover() c.SendTyping(roomID, false) c.client.SendText(roomID, message) } -func (c *MatrixContainer) SendTyping(roomID string, typing bool) { +func (c *Container) SendTyping(roomID string, typing bool) { c.gmx.Recover() - time := time.Now().Unix() - if c.typing > time && typing { + ts := time.Now().Unix() + if c.typing > ts && typing { return } if typing { c.client.UserTyping(roomID, true, 5000) - c.typing = time + 5 + c.typing = ts + 5 } else { c.client.UserTyping(roomID, false, 0) c.typing = 0 } } -func (c *MatrixContainer) JoinRoom(roomID string) error { +func (c *Container) JoinRoom(roomID string) error { if len(roomID) == 0 { return fmt.Errorf("invalid room ID") } @@ -222,26 +229,40 @@ func (c *MatrixContainer) JoinRoom(roomID string) error { server = roomID[strings.Index(roomID, ":")+1:] } - resp, err := c.client.JoinRoom(roomID, server, nil) + _, err := c.client.JoinRoom(roomID, server, nil) if err != nil { return err } - c.ui.MainView().AddRoom(resp.RoomID) + // TODO probably safe to remove + // c.ui.MainView().AddRoom(resp.RoomID) return nil } -func (c *MatrixContainer) getState(roomID string) []*gomatrix.Event { +func (c *Container) LeaveRoom(roomID string) error { + if len(roomID) == 0 { + return fmt.Errorf("invalid room ID") + } + + _, err := c.client.LeaveRoom(roomID) + if err != nil { + return err + } + + return nil +} + +func (c *Container) getState(roomID string) []*gomatrix.Event { content := make([]*gomatrix.Event, 0) err := c.client.StateEvent(roomID, "", "", &content) if err != nil { - c.debug.Print("Error getting state of", roomID, err) + debug.Print("Error getting state of", roomID, err) return nil } return content } -func (c *MatrixContainer) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) { +func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) { resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit) if err != nil { return nil, "", err @@ -249,7 +270,7 @@ func (c *MatrixContainer) GetHistory(roomID, prevBatch string, limit int) ([]gom return resp.Chunk, resp.End, nil } -func (c *MatrixContainer) GetRoom(roomID string) *Room { +func (c *Container) GetRoom(roomID string) *rooms.Room { room := c.config.Session.GetRoom(roomID) if room != nil && len(room.State) == 0 { events := c.getState(room.ID) diff --git a/matrix/room/member.go b/matrix/room/member.go new file mode 100644 index 0000000..474d2fd --- /dev/null +++ b/matrix/room/member.go @@ -0,0 +1,51 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package room + +import ( + "maunium.net/go/gomatrix" +) + +type Member struct { + UserID string `json:"-"` + Membership string `json:"membership"` + DisplayName string `json:"displayname"` + AvatarURL string `json:"avatar_url"` +} + +func eventToRoomMember(userID string, event *gomatrix.Event) *Member { + if event == nil { + return &Member{ + UserID: userID, + Membership: "leave", + } + } + membership, _ := event.Content["membership"].(string) + avatarURL, _ := event.Content["avatar_url"].(string) + + displayName, _ := event.Content["displayname"].(string) + if len(displayName) == 0 { + displayName = userID + } + + return &Member{ + UserID: userID, + Membership: membership, + DisplayName: displayName, + AvatarURL: avatarURL, + } +} diff --git a/room.go b/matrix/room/room.go similarity index 80% rename from room.go rename to matrix/room/room.go index 19c6865..4b3bda2 100644 --- a/room.go +++ b/matrix/room/room.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package room import ( "maunium.net/go/gomatrix" @@ -25,7 +25,7 @@ type Room struct { *gomatrix.Room PrevBatch string - memberCache map[string]*RoomMember + memberCache map[string]*Member nameCache string topicCache string } @@ -107,38 +107,8 @@ func (room *Room) GetTitle() string { return room.nameCache } -type RoomMember struct { - UserID string `json:"-"` - Membership string `json:"membership"` - DisplayName string `json:"displayname"` - AvatarURL string `json:"avatar_url"` -} - -func eventToRoomMember(userID string, event *gomatrix.Event) *RoomMember { - if event == nil { - return &RoomMember{ - UserID: userID, - Membership: "leave", - } - } - membership, _ := event.Content["membership"].(string) - avatarURL, _ := event.Content["avatar_url"].(string) - - displayName, _ := event.Content["displayname"].(string) - if len(displayName) == 0 { - displayName = userID - } - - return &RoomMember{ - UserID: userID, - Membership: membership, - DisplayName: displayName, - AvatarURL: avatarURL, - } -} - -func (room *Room) createMemberCache() map[string]*RoomMember { - cache := make(map[string]*RoomMember) +func (room *Room) createMemberCache() map[string]*Member { + cache := make(map[string]*Member) events := room.GetStateEvents("m.room.member") if events != nil { for userID, event := range events { @@ -152,14 +122,14 @@ func (room *Room) createMemberCache() map[string]*RoomMember { return cache } -func (room *Room) GetMembers() map[string]*RoomMember { +func (room *Room) GetMembers() map[string]*Member { if len(room.memberCache) == 0 { room.createMemberCache() } return room.memberCache } -func (room *Room) GetMember(userID string) *RoomMember { +func (room *Room) GetMember(userID string) *Member { if len(room.memberCache) == 0 { room.createMemberCache() } diff --git a/sync.go b/matrix/sync.go similarity index 88% rename from sync.go rename to matrix/sync.go index 2e0bbcf..ab5d047 100644 --- a/sync.go +++ b/matrix/sync.go @@ -1,4 +1,4 @@ -package main +package matrix import ( "encoding/json" @@ -7,20 +7,21 @@ import ( "time" "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/config" ) // GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively // replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer // pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information. type GomuksSyncer struct { - Session *Session + Session *config.Session listeners map[string][]gomatrix.OnEventListener // event type to listeners array } // NewGomuksSyncer returns an instantiated GomuksSyncer -func NewGomuksSyncer(session *Session) *GomuksSyncer { +func NewGomuksSyncer(session *config.Session) *GomuksSyncer { return &GomuksSyncer{ - Session: session, + Session: session, listeners: make(map[string][]gomatrix.OnEventListener), } } @@ -38,22 +39,22 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er }() for _, event := range res.Presence.Events { - s.notifyListeners(&event) + s.notifyListeners(event) } for roomID, roomData := range res.Rooms.Join { room := s.Session.GetRoom(roomID) for _, event := range roomData.State.Events { event.RoomID = roomID - room.UpdateState(&event) - s.notifyListeners(&event) + room.UpdateState(event) + s.notifyListeners(event) } for _, event := range roomData.Timeline.Events { event.RoomID = roomID - s.notifyListeners(&event) + s.notifyListeners(event) } for _, event := range roomData.Ephemeral.Events { event.RoomID = roomID - s.notifyListeners(&event) + s.notifyListeners(event) } if len(room.PrevBatch) == 0 { @@ -64,8 +65,8 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er room := s.Session.GetRoom(roomID) for _, event := range roomData.State.Events { event.RoomID = roomID - room.UpdateState(&event) - s.notifyListeners(&event) + room.UpdateState(event) + s.notifyListeners(event) } } for roomID, roomData := range res.Rooms.Leave { @@ -73,8 +74,8 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er for _, event := range roomData.Timeline.Events { if event.StateKey != nil { event.RoomID = roomID - room.UpdateState(&event) - s.notifyListeners(&event) + room.UpdateState(event) + s.notifyListeners(event) } } diff --git a/debug.go b/ui/debug/debug.go similarity index 54% rename from debug.go rename to ui/debug/debug.go index 15aac60..c855897 100644 --- a/debug.go +++ b/ui/debug/debug.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package debug import ( "fmt" @@ -22,54 +22,62 @@ import ( "maunium.net/go/tview" ) -const DebugPaneHeight = 35 - -type DebugPrinter interface { +type Printer interface { Printf(text string, args ...interface{}) Print(text ...interface{}) } -type DebugPane struct { - pane *tview.TextView +type Pane struct { + *tview.TextView + Height int num int - gmx Gomuks } -func NewDebugPane(gmx Gomuks) *DebugPane { +var Default Printer + +func NewPane() *Pane { pane := tview.NewTextView() pane. SetScrollable(true). - SetWrap(true) - pane.SetChangedFunc(func() { - gmx.App().Draw() - }) - pane.SetBorder(true).SetTitle("Debug output") + SetWrap(true). + SetBorder(true). + SetTitle("Debug output") fmt.Fprintln(pane, "[0] Debug pane initialized") - return &DebugPane{ - pane: pane, + return &Pane{ + TextView: pane, + Height: 35, num: 0, - gmx: gmx, } } -func (db *DebugPane) Printf(text string, args ...interface{}) { - db.Write(fmt.Sprintf(text, args...) + "\n") +func (db *Pane) Printf(text string, args ...interface{}) { + db.WriteString(fmt.Sprintf(text, args...) + "\n") } -func (db *DebugPane) Print(text ...interface{}) { - db.Write(fmt.Sprintln(text...)) +func (db *Pane) Print(text ...interface{}) { + db.WriteString(fmt.Sprintln(text...)) } -func (db *DebugPane) Write(text string) { - if db.pane != nil { - db.num++ - fmt.Fprintf(db.pane, "[%d] %s", db.num, text) - } +func (db *Pane) WriteString(text string) { + db.num++ + fmt.Fprintf(db, "[%d] %s", db.num, text) } -func (db *DebugPane) Wrap(main tview.Primitive) tview.Primitive { - return tview.NewGrid().SetRows(0, DebugPaneHeight).SetColumns(0). +func (db *Pane) Wrap(main tview.Primitive) tview.Primitive { + return tview.NewGrid().SetRows(0, db.Height).SetColumns(0). AddItem(main, 0, 0, 1, 1, 1, 1, true). - AddItem(db.pane, 1, 0, 1, 1, 1, 1, false) + AddItem(db, 1, 0, 1, 1, 1, 1, false) +} + +func Printf(text string, args ...interface{}) { + if Default != nil { + Default.Printf(text, args...) + } +} + +func Print(text ...interface{}) { + if Default != nil { + Default.Print(text...) + } } diff --git a/ui/types/message.go b/ui/types/message.go new file mode 100644 index 0000000..b69eab2 --- /dev/null +++ b/ui/types/message.go @@ -0,0 +1,85 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package types + +import ( + "regexp" + "strings" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" +) + +type Message struct { + ID string + Sender string + Text string + Timestamp string + Date string + + Buffer []string + SenderColor tcell.Color +} + +func NewMessage(id, sender, text, timestamp, date string, senderColor tcell.Color) *Message { + return &Message{ + ID: id, + Sender: sender, + Text: text, + Timestamp: timestamp, + Date: date, + SenderColor: senderColor, + } +} + +var ( + boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") + spacePattern = regexp.MustCompile(`\s+`) +) + +func (message *Message) CalculateBuffer(width int) { + if width < 1 { + return + } + message.Buffer = []string{} + forcedLinebreaks := strings.Split(message.Text, "\n") + newlines := 0 + for _, str := range forcedLinebreaks { + if len(str) == 0 && newlines < 1 { + message.Buffer = append(message.Buffer, "") + newlines++ + } else { + newlines = 0 + } + // From tview/textview.go#reindexBuffer() + for len(str) > 0 { + extract := runewidth.Truncate(str, width, "") + if len(extract) < len(str) { + if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { + extract = str[:len(extract)+spaces[1]] + } + + matches := boundaryPattern.FindAllStringIndex(extract, -1) + if len(matches) > 0 { + extract = extract[:matches[len(matches)-1][1]] + } + } + message.Buffer = append(message.Buffer, extract) + str = str[len(extract):] + } + } +} diff --git a/ui.go b/ui/ui.go similarity index 71% rename from ui.go rename to ui/ui.go index 96b4e41..eab7642 100644 --- a/ui.go +++ b/ui/ui.go @@ -14,25 +14,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package ui import ( "github.com/gdamore/tcell" + "maunium.net/go/gomuks/interface" "maunium.net/go/tview" ) -// Allowed views in GomuksUI -const ( - ViewLogin = "login" - ViewMain = "main" -) - type GomuksUI struct { - gmx Gomuks + gmx ifc.Gomuks app *tview.Application - matrix *MatrixContainer - debug DebugPrinter - config *Config views *tview.Pages mainView *MainView @@ -44,13 +36,10 @@ func init() { tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen } -func NewGomuksUI(gmx Gomuks) (ui *GomuksUI) { +func NewGomuksUI(gmx ifc.Gomuks) (ui *GomuksUI) { ui = &GomuksUI{ gmx: gmx, app: gmx.App(), - matrix: gmx.MatrixContainer(), - debug: gmx.Debug(), - config: gmx.Config(), views: tview.NewPages(), } ui.views.SetChangedFunc(ui.Render) @@ -61,16 +50,16 @@ func (ui *GomuksUI) Render() { ui.app.Draw() } -func (ui *GomuksUI) SetView(name string) { - ui.views.SwitchToPage(name) +func (ui *GomuksUI) SetView(name ifc.View) { + ui.views.SwitchToPage(string(name)) } func (ui *GomuksUI) InitViews() tview.Primitive { - ui.views.AddPage(ViewLogin, ui.NewLoginView(), true, true) - ui.views.AddPage(ViewMain, ui.NewMainView(), true, false) + ui.views.AddPage(string(ifc.ViewLogin), ui.NewLoginView(), true, true) + ui.views.AddPage(string(ifc.ViewMain), ui.NewMainView(), true, false) return ui.views } -func (ui *GomuksUI) MainView() *MainView { +func (ui *GomuksUI) MainView() ifc.MainView { return ui.mainView } diff --git a/view-login.go b/ui/view-login.go similarity index 76% rename from view-login.go rename to ui/view-login.go index 0c18fbc..2a19d3b 100644 --- a/view-login.go +++ b/ui/view-login.go @@ -14,14 +14,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package ui import ( + "maunium.net/go/gomuks/ui/debug" + "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tview" ) func (ui *GomuksUI) NewLoginView() tview.Primitive { - hs := ui.config.HS + hs := ui.gmx.Config().HS if len(hs) == 0 { hs = "https://matrix.org" } @@ -29,13 +31,13 @@ func (ui *GomuksUI) NewLoginView() tview.Primitive { ui.loginView = tview.NewForm() ui.loginView. AddInputField("Homeserver", hs, 30, nil, nil). - AddInputField("Username", ui.config.MXID, 30, nil, nil). + AddInputField("Username", ui.gmx.Config().MXID, 30, nil, nil). AddPasswordField("Password", "", 30, '*', nil). AddButton("Log in", ui.login). AddButton("Quit", ui.gmx.Stop). SetButtonsAlign(tview.AlignCenter). SetBorder(true).SetTitle("Log in to Matrix") - return Center(45, 11, ui.loginView) + return widget.Center(45, 11, ui.loginView) } func (ui *GomuksUI) login() { @@ -43,8 +45,9 @@ func (ui *GomuksUI) login() { mxid := ui.loginView.GetFormItem(1).(*tview.InputField).GetText() password := ui.loginView.GetFormItem(2).(*tview.InputField).GetText() - ui.debug.Printf("Logging into %s as %s...", hs, mxid) - ui.config.HS = hs - ui.debug.Print("Connect result:", ui.matrix.InitClient()) - ui.debug.Print("Login result:", ui.matrix.Login(mxid, password)) + debug.Printf("Logging into %s as %s...", hs, mxid) + ui.gmx.Config().HS = hs + mx := ui.gmx.MatrixContainer() + debug.Print("Connect result:", mx.InitClient()) + debug.Print("Login result:", mx.Login(mxid, password)) } diff --git a/view-main.go b/ui/view-main.go similarity index 86% rename from view-main.go rename to ui/view-main.go index 2fd503a..8ec482e 100644 --- a/view-main.go +++ b/ui/view-main.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package ui import ( "fmt" @@ -26,6 +26,11 @@ import ( "github.com/gdamore/tcell" "github.com/mattn/go-runewidth" "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/config" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/debug" + "maunium.net/go/gomuks/ui/types" + "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tview" ) @@ -34,15 +39,14 @@ type MainView struct { roomList *tview.List roomView *tview.Pages - rooms map[string]*RoomView - input *AdvancedInputField + rooms map[string]*widget.RoomView + input *widget.AdvancedInputField currentRoomIndex int roomIDs []string - matrix *MatrixContainer - debug DebugPrinter - gmx Gomuks - config *Config + matrix ifc.MatrixContainer + gmx ifc.Gomuks + config *config.Config parent *GomuksUI } @@ -55,13 +59,12 @@ func (ui *GomuksUI) NewMainView() tview.Primitive { Grid: tview.NewGrid(), roomList: tview.NewList(), roomView: tview.NewPages(), - rooms: make(map[string]*RoomView), - input: NewAdvancedInputField(), + rooms: make(map[string]*widget.RoomView), + input: widget.NewAdvancedInputField(), - matrix: ui.matrix, - debug: ui.debug, + matrix: ui.gmx.MatrixContainer(), gmx: ui.gmx, - config: ui.config, + config: ui.gmx.Config(), parent: ui, } @@ -83,7 +86,7 @@ func (ui *GomuksUI) NewMainView() tview.Primitive { SetInputCapture(mainView.InputCapture) mainView.addItem(mainView.roomList, 0, 0, 2, 1) - mainView.addItem(NewBorder(), 0, 1, 2, 1) + mainView.addItem(widget.NewBorder(), 0, 1, 2, 1) mainView.addItem(mainView.roomView, 0, 2, 1, 1) mainView.AddItem(mainView.input, 1, 2, 1, 1, 0, 0, true) @@ -121,8 +124,7 @@ func (view *MainView) InputTabComplete(text string, cursorOffset int) string { if len(userCompletions) == 1 { text = str[0:len(str)-len(word)] + userCompletions[0] + text[len(str):] } else if len(userCompletions) > 1 && len(userCompletions) < 6 { - roomView.status.Clear() - fmt.Fprintf(roomView.status, "Completions: %s", strings.Join(userCompletions, ", ")) + roomView.SetStatus(fmt.Sprintf("Completions: %s", strings.Join(userCompletions, ", "))) } } return text @@ -147,7 +149,7 @@ func (view *MainView) InputDone(key tcell.Key) { func (view *MainView) HandleCommand(room, command string, args []string) { view.gmx.Recover() - view.debug.Print("Handling command", command, args) + debug.Print("Handling command", command, args) switch command { case "/quit": view.gmx.Stop() @@ -157,13 +159,13 @@ func (view *MainView) HandleCommand(room, command string, args []string) { case "/part": fallthrough case "/leave": - view.matrix.client.LeaveRoom(room) + debug.Print(view.matrix.LeaveRoom(room)) case "/join": if len(args) == 0 { view.AddServiceMessage(room, "Usage: /join ") break } - view.debug.Print(view.matrix.JoinRoom(args[0])) + debug.Print(view.matrix.JoinRoom(args[0])) default: view.AddServiceMessage(room, "Unknown command.") } @@ -218,7 +220,7 @@ func (view *MainView) addRoom(index int, room string) { view.SwitchRoom(index) }) if !view.roomView.HasPage(room) { - roomView := NewRoomView(view, roomStore) + roomView := widget.NewRoomView(view, roomStore) view.rooms[room] = roomView view.roomView.AddPage(room, roomView, true, false) roomView.UpdateUserList() @@ -226,7 +228,7 @@ func (view *MainView) addRoom(index int, room string) { } } -func (view *MainView) GetRoom(id string) *RoomView { +func (view *MainView) GetRoom(id string) *widget.RoomView { return view.rooms[id] } @@ -265,11 +267,11 @@ func (view *MainView) RemoveRoom(room string) { view.Render() } -func (view *MainView) SetRoomList(rooms []string) { +func (view *MainView) SetRooms(rooms []string) { view.roomIDs = rooms view.roomList.Clear() view.roomView.Clear() - view.rooms = make(map[string]*RoomView) + view.rooms = make(map[string]*widget.RoomView) for index, room := range rooms { view.addRoom(index, room) } @@ -289,7 +291,7 @@ func (view *MainView) AddServiceMessage(room, message string) { if ok { messageView := roomView.MessageView() message := messageView.NewMessage("", "*", message, time.Now()) - messageView.AddMessage(message, AppendMessage) + messageView.AddMessage(message, widget.AppendMessage) view.parent.Render() } } @@ -300,29 +302,29 @@ func (view *MainView) Render() { func (view *MainView) GetHistory(room string) { roomView := view.rooms[room] - history, _, err := view.matrix.GetHistory(roomView.room.ID, view.config.Session.NextBatch, 50) + history, _, err := view.matrix.GetHistory(roomView.Room.ID, view.config.Session.NextBatch, 50) if err != nil { - view.debug.Print("Failed to fetch history for", roomView.room.ID, err) + debug.Print("Failed to fetch history for", roomView.Room.ID, err) return } for _, evt := range history { - var room *RoomView - var message *Message + var room *widget.RoomView + var message *types.Message if evt.Type == "m.room.message" { room, message = view.ProcessMessageEvent(&evt) } else if evt.Type == "m.room.member" { room, message = view.ProcessMembershipEvent(&evt, false) } if room != nil && message != nil { - room.AddMessage(message, PrependMessage) + room.AddMessage(message, widget.PrependMessage) } } } -func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *RoomView, message *Message) { +func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *widget.RoomView, message *types.Message) { room = view.GetRoom(evt.RoomID) if room != nil { - text := evt.Content["body"].(string) + text, _ := evt.Content["body"].(string) message = room.NewMessage(evt.ID, evt.Sender, text, unixToTime(evt.Timestamp)) } return @@ -344,7 +346,7 @@ func (view *MainView) processOwnMembershipChange(evt *gomatrix.Event) { } } -func (view *MainView) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (room *RoomView, message *Message) { +func (view *MainView) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (room *widget.RoomView, message *types.Message) { if new && evt.StateKey != nil && *evt.StateKey == view.config.Session.MXID { view.processOwnMembershipChange(evt) } diff --git a/advanced-inputfield.go b/ui/widget/advanced-inputfield.go similarity index 99% rename from advanced-inputfield.go rename to ui/widget/advanced-inputfield.go index 8b5b47a..6928c27 100644 --- a/advanced-inputfield.go +++ b/ui/widget/advanced-inputfield.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package widget import ( "math" diff --git a/border.go b/ui/widget/border.go similarity index 98% rename from border.go rename to ui/widget/border.go index cd0b8a1..a32f4dd 100644 --- a/border.go +++ b/ui/widget/border.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package widget import ( "github.com/gdamore/tcell" diff --git a/ui/widget/center.go b/ui/widget/center.go new file mode 100644 index 0000000..41181a2 --- /dev/null +++ b/ui/widget/center.go @@ -0,0 +1,32 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package widget + +import ( + "maunium.net/go/tview" +) + +func Center(width, height int, p tview.Primitive) tview.Primitive { + return tview.NewFlex(). + AddItem(tview.NewBox(), 0, 1, false). + AddItem(tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(tview.NewBox(), 0, 1, false). + AddItem(p, height, 1, true). + AddItem(tview.NewBox(), 0, 1, false), width, 1, true). + AddItem(tview.NewBox(), 0, 1, false) +} diff --git a/ui/widget/color.go b/ui/widget/color.go new file mode 100644 index 0000000..874b93d --- /dev/null +++ b/ui/widget/color.go @@ -0,0 +1,60 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package widget + +import ( + "fmt" + "hash/fnv" + "sort" + + "github.com/gdamore/tcell" +) + +var colorNames []string + +func init() { + colorNames = make([]string, len(tcell.ColorNames)) + i := 0 + for name, _ := range tcell.ColorNames { + colorNames[i] = name + i++ + } + sort.Sort(sort.StringSlice(colorNames)) +} + +func GetHashColorName(s string) string { + switch s { + case "-->": + return "green" + case "<--": + return "red" + case "---": + return "yellow" + default: + h := fnv.New32a() + h.Write([]byte(s)) + return colorNames[int(h.Sum32())%len(colorNames)] + } +} + +func GetHashColor(s string) tcell.Color { + return tcell.ColorNames[GetHashColorName(s)] +} + +func AddHashColor(s string) string { + return fmt.Sprintf("[%s]%s[white]", GetHashColorName(s), s) +} diff --git a/uiutil.go b/ui/widget/form-text-view.go similarity index 77% rename from uiutil.go rename to ui/widget/form-text-view.go index 0ba37ef..58046e9 100644 --- a/uiutil.go +++ b/ui/widget/form-text-view.go @@ -14,24 +14,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package widget import ( "github.com/gdamore/tcell" "maunium.net/go/tview" ) -func Center(width, height int, p tview.Primitive) tview.Primitive { - return tview.NewFlex(). - AddItem(tview.NewBox(), 0, 1, false). - AddItem(tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(tview.NewBox(), 0, 1, false). - AddItem(p, height, 1, true). - AddItem(tview.NewBox(), 0, 1, false), width, 1, true). - AddItem(tview.NewBox(), 0, 1, false) -} - type FormTextView struct { *tview.TextView } diff --git a/message-view.go b/ui/widget/message-view.go similarity index 78% rename from message-view.go rename to ui/widget/message-view.go index d3d2df9..3503a5f 100644 --- a/message-view.go +++ b/ui/widget/message-view.go @@ -14,79 +14,19 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package widget import ( "fmt" - "regexp" "strings" "time" "github.com/gdamore/tcell" "github.com/mattn/go-runewidth" + "maunium.net/go/gomuks/ui/types" "maunium.net/go/tview" ) -type Message struct { - ID string - Sender string - Text string - Timestamp string - Date string - - buffer []string - senderColor tcell.Color -} - -func NewMessage(id, sender, text, timestamp, date string, senderColor tcell.Color) *Message { - return &Message{ - ID: id, - Sender: sender, - Text: text, - Timestamp: timestamp, - Date: date, - senderColor: senderColor, - } -} - -var ( - boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") - spacePattern = regexp.MustCompile(`\s+`) -) - -func (message *Message) calculateBuffer(width int) { - if width < 1 { - return - } - message.buffer = []string{} - forcedLinebreaks := strings.Split(message.Text, "\n") - newlines := 0 - for _, str := range forcedLinebreaks { - if len(str) == 0 && newlines < 1 { - message.buffer = append(message.buffer, "") - newlines++ - } else { - newlines = 0 - } - // From tview/textview.go#reindexBuffer() - for len(str) > 0 { - extract := runewidth.Truncate(str, width, "") - if len(extract) < len(str) { - if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { - extract = str[:len(extract)+spaces[1]] - } - - matches := boundaryPattern.FindAllStringIndex(extract, -1) - if len(matches) > 0 { - extract = extract[:matches[len(matches)-1][1]] - } - } - message.buffer = append(message.buffer, extract) - str = str[len(extract):] - } - } -} - type MessageView struct { *tview.Box @@ -106,7 +46,7 @@ type MessageView struct { totalHeight int messageIDs map[string]bool - messages []*Message + messages []*types.Message } func NewMessageView() *MessageView { @@ -119,7 +59,7 @@ func NewMessageView() *MessageView { Separator: '|', ScrollOffset: 0, - messages: make([]*Message, 0), + messages: make([]*types.Message, 0), messageIDs: make(map[string]bool), widestSender: 5, @@ -132,11 +72,11 @@ func NewMessageView() *MessageView { } } -func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *Message { - return NewMessage(id, sender, text, +func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *types.Message { + return types.NewMessage(id, sender, text, timestamp.Format(view.TimestampFormat), timestamp.Format(view.DateFormat), - getColor(sender)) + GetHashColor(sender)) } func (view *MessageView) recalculateBuffers() { @@ -144,7 +84,7 @@ func (view *MessageView) recalculateBuffers() { width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap if width != view.prevWidth { for _, message := range view.messages { - message.calculateBuffer(width) + message.CalculateBuffer(width) } view.prevWidth = width } @@ -164,7 +104,7 @@ const ( PrependMessage ) -func (view *MessageView) AddMessage(message *Message, direction int) { +func (view *MessageView) AddMessage(message *types.Message, direction int) { _, messageExists := view.messageIDs[message.ID] if messageExists { return @@ -174,15 +114,15 @@ func (view *MessageView) AddMessage(message *Message, direction int) { _, _, width, _ := view.GetInnerRect() width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap - message.calculateBuffer(width) + message.CalculateBuffer(width) if direction == AppendMessage { if view.ScrollOffset > 0 { - view.ScrollOffset += len(message.buffer) + view.ScrollOffset += len(message.Buffer) } view.messages = append(view.messages, message) } else if direction == PrependMessage { - view.messages = append([]*Message{message}, view.messages...) + view.messages = append([]*types.Message{message}, view.messages...) } view.messageIDs[message.ID] = true @@ -199,7 +139,7 @@ func (view *MessageView) recalculateHeight() { for i := len(view.messages) - 1; i >= 0; i-- { prevTotalHeight := view.totalHeight message := view.messages[i] - view.totalHeight += len(message.buffer) + view.totalHeight += len(message.Buffer) if message.Date != prevDate { if len(prevDate) != 0 { view.totalHeight++ @@ -268,6 +208,9 @@ func (view *MessageView) writeLineRight(screen tcell.Screen, line string, x, y, screen.SetContent(x+offsetX+localOffset, y, ch, nil, tcell.StyleDefault.Foreground(color)) } offsetX += chWidth + if offsetX > maxWidth { + break + } } } @@ -302,7 +245,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { prevSenderLine := -1 for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- { message := view.messages[i] - messageHeight := len(message.buffer) + messageHeight := len(message.Buffer) // Show message when the date changes. if message.Date != prevDate { @@ -325,7 +268,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { view.writeLine(screen, message.Timestamp, x, senderAtLine, tcell.ColorDefault) view.writeLineRight(screen, message.Sender, x+usernameOffsetX, senderAtLine, - view.widestSender, message.senderColor) + view.widestSender, message.SenderColor) if message.Sender == prevSender { // Sender is same as previous. We're looping from bottom to top, and we want the @@ -333,12 +276,12 @@ func (view *MessageView) Draw(screen tcell.Screen) { // below. view.writeLineRight(screen, strings.Repeat(" ", view.widestSender), x+usernameOffsetX, prevSenderLine, - view.widestSender, message.senderColor) + view.widestSender, message.SenderColor) } prevSender = message.Sender prevSenderLine = senderAtLine - for num, line := range message.buffer { + for num, line := range message.Buffer { offsetY := height - messageHeight - writeOffset + num // Only render message if it's within the message view. if offsetY >= 0 { diff --git a/room-view.go b/ui/widget/room-view.go similarity index 75% rename from room-view.go rename to ui/widget/room-view.go index 5710788..eeab7b2 100644 --- a/room-view.go +++ b/ui/widget/room-view.go @@ -14,19 +14,23 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package main +package widget import ( "fmt" - "hash/fnv" - "sort" "strings" "time" "github.com/gdamore/tcell" + rooms "maunium.net/go/gomuks/matrix/room" + "maunium.net/go/gomuks/ui/types" "maunium.net/go/tview" ) +type Renderable interface { + Render() +} + type RoomView struct { *tview.Box @@ -34,31 +38,19 @@ type RoomView struct { content *MessageView status *tview.TextView userList *tview.TextView - room *Room + Room *rooms.Room - parent *MainView + parent Renderable } -var colorNames []string - -func init() { - colorNames = make([]string, len(tcell.ColorNames)) - i := 0 - for name, _ := range tcell.ColorNames { - colorNames[i] = name - i++ - } - sort.Sort(sort.StringSlice(colorNames)) -} - -func NewRoomView(parent *MainView, room *Room) *RoomView { +func NewRoomView(parent Renderable, room *rooms.Room) *RoomView { view := &RoomView{ Box: tview.NewBox(), topic: tview.NewTextView(), content: NewMessageView(), status: tview.NewTextView(), userList: tview.NewTextView(), - room: room, + Room: room, parent: parent, } view.topic. @@ -90,9 +82,13 @@ func (view *RoomView) Draw(screen tcell.Screen) { view.userList.Draw(screen) } +func (view *RoomView) SetStatus(status string) { + view.status.SetText(status) +} + func (view *RoomView) SetTyping(users []string) { for index, user := range users { - member := view.room.GetMember(user) + member := view.Room.GetMember(user) if member != nil { users[index] = member.DisplayName } @@ -109,7 +105,7 @@ func (view *RoomView) SetTyping(users []string) { } func (view *RoomView) AutocompleteUser(existingText string) (completions []string) { - for _, user := range view.room.GetMembers() { + for _, user := range view.Room.GetMembers() { if strings.HasPrefix(user.DisplayName, existingText) { completions = append(completions, user.DisplayName) } else if strings.HasPrefix(user.UserID, existingText) { @@ -123,38 +119,15 @@ func (view *RoomView) MessageView() *MessageView { return view.content } -func getColorName(s string) string { - switch s { - case "-->": - return "green" - case "<--": - return "red" - case "---": - return "yellow" - default: - h := fnv.New32a() - h.Write([]byte(s)) - return colorNames[int(h.Sum32())%len(colorNames)] - } -} - -func getColor(s string) tcell.Color { - return tcell.ColorNames[getColorName(s)] -} - -func color(s string) string { - return fmt.Sprintf("[%s]%s[white]", getColorName(s), s) -} - func (view *RoomView) UpdateUserList() { var joined strings.Builder var invited strings.Builder - for _, user := range view.room.GetMembers() { + for _, user := range view.Room.GetMembers() { if user.Membership == "join" { - joined.WriteString(color(user.DisplayName)) + joined.WriteString(AddHashColor(user.DisplayName)) joined.WriteRune('\n') } else if user.Membership == "invite" { - invited.WriteString(color(user.DisplayName)) + invited.WriteString(AddHashColor(user.DisplayName)) invited.WriteRune('\n') } } @@ -165,15 +138,15 @@ func (view *RoomView) UpdateUserList() { } } -func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) *Message { - member := view.room.GetMember(sender) +func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) *types.Message { + member := view.Room.GetMember(sender) if member != nil { sender = member.DisplayName } return view.content.NewMessage(id, sender, text, timestamp) } -func (view *RoomView) AddMessage(message *Message, direction int) { +func (view *RoomView) AddMessage(message *types.Message, direction int) { view.content.AddMessage(message, direction) view.parent.Render() }