Organize files

This commit is contained in:
Tulir Asokan 2018-03-18 21:24:03 +02:00
parent 0509b19562
commit 72945c9a28
23 changed files with 620 additions and 392 deletions

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package config
import ( import (
"fmt" "fmt"
@ -23,22 +23,19 @@ import (
"path/filepath" "path/filepath"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"maunium.net/go/gomuks/ui/debug"
) )
type Config struct { type Config struct {
MXID string `yaml:"mxid"` MXID string `yaml:"mxid"`
HS string `yaml:"homeserver"` HS string `yaml:"homeserver"`
dir string `yaml:"-"` dir string `yaml:"-"`
gmx Gomuks `yaml:"-"` Session *Session `yaml:"-"`
debug DebugPrinter `yaml:"-"`
Session *Session `yaml:"-"`
} }
func NewConfig(gmx Gomuks, dir string) *Config { func NewConfig(dir string) *Config {
return &Config{ return &Config{
gmx: gmx,
debug: gmx.Debug(),
dir: dir, dir: dir,
} }
} }
@ -67,14 +64,14 @@ func (config *Config) Save() {
os.MkdirAll(config.dir, 0700) os.MkdirAll(config.dir, 0700)
data, err := yaml.Marshal(&config) data, err := yaml.Marshal(&config)
if err != nil { if err != nil {
config.debug.Print("Failed to marshal config") debug.Print("Failed to marshal config")
panic(err) panic(err)
} }
path := filepath.Join(config.dir, "config.yaml") path := filepath.Join(config.dir, "config.yaml")
err = ioutil.WriteFile(path, data, 0600) err = ioutil.WriteFile(path, data, 0600)
if err != nil { if err != nil {
config.debug.Print("Failed to write config to", path) debug.Print("Failed to write config to", path)
panic(err) panic(err)
} }
} }

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package config
import ( import (
"encoding/json" "encoding/json"
@ -22,6 +22,8 @@ import (
"path/filepath" "path/filepath"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
rooms "maunium.net/go/gomuks/matrix/room"
"maunium.net/go/gomuks/ui/debug"
) )
type Session struct { type Session struct {
@ -30,9 +32,7 @@ type Session struct {
AccessToken string AccessToken string
NextBatch string NextBatch string
FilterID string FilterID string
Rooms map[string]*Room Rooms map[string]*rooms.Room
debug DebugPrinter `json:"-"`
} }
func (config *Config) LoadSession(mxid string) { func (config *Config) LoadSession(mxid string) {
@ -44,13 +44,12 @@ func (config *Config) NewSession(mxid string) *Session {
return &Session{ return &Session{
MXID: mxid, MXID: mxid,
path: filepath.Join(config.dir, mxid+".session"), path: filepath.Join(config.dir, mxid+".session"),
Rooms: make(map[string]*Room), Rooms: make(map[string]*rooms.Room),
debug: config.debug,
} }
} }
func (s *Session) Clear() { func (s *Session) Clear() {
s.Rooms = make(map[string]*Room) s.Rooms = make(map[string]*rooms.Room)
s.NextBatch = "" s.NextBatch = ""
s.FilterID = "" s.FilterID = ""
s.Save() s.Save()
@ -59,13 +58,13 @@ func (s *Session) Clear() {
func (s *Session) Load() { func (s *Session) Load() {
data, err := ioutil.ReadFile(s.path) data, err := ioutil.ReadFile(s.path)
if err != nil { if err != nil {
s.debug.Print("Failed to read session from", s.path) debug.Print("Failed to read session from", s.path)
panic(err) panic(err)
} }
err = json.Unmarshal(data, s) err = json.Unmarshal(data, s)
if err != nil { if err != nil {
s.debug.Print("Failed to parse session at", s.path) debug.Print("Failed to parse session at", s.path)
panic(err) panic(err)
} }
} }
@ -73,13 +72,13 @@ func (s *Session) Load() {
func (s *Session) Save() { func (s *Session) Save() {
data, err := json.Marshal(s) data, err := json.Marshal(s)
if err != nil { if err != nil {
s.debug.Print("Failed to marshal session of", s.MXID) debug.Print("Failed to marshal session of", s.MXID)
panic(err) panic(err)
} }
err = ioutil.WriteFile(s.path, data, 0600) err = ioutil.WriteFile(s.path, data, 0600)
if err != nil { if err != nil {
s.debug.Print("Failed to write session to", s.path) debug.Print("Failed to write session to", s.path)
panic(err) panic(err)
} }
} }
@ -92,16 +91,16 @@ func (s *Session) LoadNextBatch(_ string) string {
return s.NextBatch return s.NextBatch
} }
func (s *Session) GetRoom(mxid string) *Room { func (s *Session) GetRoom(mxid string) *rooms.Room {
room, _ := s.Rooms[mxid] room, _ := s.Rooms[mxid]
if room == nil { if room == nil {
room = NewRoom(mxid) room = rooms.NewRoom(mxid)
s.Rooms[room.ID] = room s.Rooms[room.ID] = room
} }
return room return room
} }
func (s *Session) PutRoom(room *Room) { func (s *Session) PutRoom(room *rooms.Room) {
s.Rooms[room.ID] = room s.Rooms[room.ID] = room
s.Save() s.Save()
} }

14
go.mod
View File

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

View File

@ -21,43 +21,37 @@ import (
"path/filepath" "path/filepath"
"maunium.net/go/gomatrix" "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" "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 { type gomuks struct {
app *tview.Application app *tview.Application
ui *GomuksUI ui *ui.GomuksUI
matrix *MatrixContainer matrix *matrix.Container
debug *DebugPane debug *debug.Pane
config *Config config *config.Config
} }
var gdebug DebugPrinter func NewGomuks(enableDebug bool) *gomuks {
func NewGomuks(debug bool) *gomuks {
configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks")
gmx := &gomuks{ gmx := &gomuks{
app: tview.NewApplication(), app: tview.NewApplication(),
} }
gmx.debug = NewDebugPane(gmx)
gdebug = gmx.debug gmx.debug = debug.NewPane()
gmx.config = NewConfig(gmx, configDir) gmx.debug.SetChangedFunc(func() {
gmx.ui = NewGomuksUI(gmx) gmx.ui.Render()
gmx.matrix = NewMatrixContainer(gmx) })
gmx.ui.matrix = gmx.matrix debug.Default = gmx.debug
gmx.config = config.NewConfig(configDir)
gmx.ui = ui.NewGomuksUI(gmx)
gmx.matrix = matrix.NewMatrixContainer(gmx)
gmx.config.Load() gmx.config.Load()
if len(gmx.config.MXID) > 0 { if len(gmx.config.MXID) > 0 {
@ -67,7 +61,7 @@ func NewGomuks(debug bool) *gomuks {
gmx.matrix.InitClient() gmx.matrix.InitClient()
main := gmx.ui.InitViews() main := gmx.ui.InitViews()
if debug { if enableDebug {
main = gmx.debug.Wrap(main) main = gmx.debug.Wrap(main)
} }
gmx.app.SetRoot(main, true) 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 { 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 return gmx.matrix
} }
@ -117,11 +107,11 @@ func (gmx *gomuks) App() *tview.Application {
return gmx.app return gmx.app
} }
func (gmx *gomuks) Config() *Config { func (gmx *gomuks) Config() *config.Config {
return gmx.config return gmx.config
} }
func (gmx *gomuks) UI() *GomuksUI { func (gmx *gomuks) UI() ifc.GomuksUI {
return gmx.ui return gmx.ui
} }

35
interface/gomuks.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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()
}

40
interface/matrix.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

54
interface/ui.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package matrix
import ( import (
"fmt" "fmt"
@ -22,24 +22,27 @@ import (
"time" "time"
"maunium.net/go/gomatrix" "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 client *gomatrix.Client
gmx Gomuks gmx ifc.Gomuks
ui *GomuksUI ui ifc.GomuksUI
debug DebugPrinter config *config.Config
config *Config
running bool running bool
stop chan bool stop chan bool
typing int64 typing int64
} }
func NewMatrixContainer(gmx Gomuks) *MatrixContainer { func NewMatrixContainer(gmx ifc.Gomuks) *Container {
c := &MatrixContainer{ c := &Container{
config: gmx.Config(), config: gmx.Config(),
debug: gmx.Debug(),
ui: gmx.UI(), ui: gmx.UI(),
gmx: gmx, gmx: gmx,
} }
@ -47,7 +50,7 @@ func NewMatrixContainer(gmx Gomuks) *MatrixContainer {
return c return c
} }
func (c *MatrixContainer) InitClient() error { func (c *Container) InitClient() error {
if len(c.config.HS) == 0 { if len(c.config.HS) == 0 {
return fmt.Errorf("no homeserver in config") return fmt.Errorf("no homeserver in config")
} }
@ -77,11 +80,11 @@ func (c *MatrixContainer) InitClient() error {
return nil return nil
} }
func (c *MatrixContainer) Initialized() bool { func (c *Container) Initialized() bool {
return c.client != nil 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{ resp, err := c.client.Login(&gomatrix.ReqLogin{
Type: "m.login.password", Type: "m.login.password",
User: user, User: user,
@ -103,24 +106,28 @@ func (c *MatrixContainer) Login(user, password string) error {
return nil return nil
} }
func (c *MatrixContainer) Stop() { func (c *Container) Stop() {
if c.running { if c.running {
c.stop <- true c.stop <- true
c.client.StopSync() c.client.StopSync()
} }
} }
func (c *MatrixContainer) UpdateRoomList() { func (c *Container) Client() *gomatrix.Client {
rooms, err := c.client.JoinedRooms() return c.client
}
func (c *Container) UpdateRoomList() {
resp, err := c.client.JoinedRooms()
if err != nil { if err != nil {
c.debug.Print("Error fetching room list:", err) debug.Print("Error fetching room list:", err)
return 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 c.client.Store = c.config.Session
syncer := NewGomuksSyncer(c.config.Session) syncer := NewGomuksSyncer(c.config.Session)
@ -132,38 +139,38 @@ func (c *MatrixContainer) OnLogin() {
c.UpdateRoomList() c.UpdateRoomList()
} }
func (c *MatrixContainer) Start() { func (c *Container) Start() {
defer c.gmx.Recover() defer c.gmx.Recover()
c.ui.SetView(ifc.ViewMain)
c.OnLogin() c.OnLogin()
c.debug.Print("Starting sync...") debug.Print("Starting sync...")
c.running = true c.running = true
c.ui.SetView(ViewMain)
for { for {
select { select {
case <-c.stop: case <-c.stop:
c.debug.Print("Stopping sync...") debug.Print("Stopping sync...")
c.running = false c.running = false
return return
default: default:
if err := c.client.Sync(); err != nil { if err := c.client.Sync(); err != nil {
c.debug.Print("Sync() errored", err) debug.Print("Sync() errored", err)
} else { } 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) room, message := c.ui.MainView().ProcessMessageEvent(evt)
if room != nil { 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 const Hour = 1 * 60 * 60 * 1000
if evt.Unsigned.Age > Hour { if evt.Unsigned.Age > Hour {
return return
@ -172,15 +179,15 @@ func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) {
room, message := c.ui.MainView().ProcessMembershipEvent(evt, true) room, message := c.ui.MainView().ProcessMembershipEvent(evt, true)
if room != nil { if room != nil {
// TODO this shouldn't be necessary // TODO this shouldn't be necessary
room.room.UpdateState(evt) room.Room.UpdateState(evt)
// TODO This should probably also be in a different place // TODO This should probably also be in a different place
room.UpdateUserList() 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{}) users := evt.Content["user_ids"].([]interface{})
strUsers := make([]string, len(users)) strUsers := make([]string, len(users))
@ -190,29 +197,29 @@ func (c *MatrixContainer) HandleTyping(evt *gomatrix.Event) {
c.ui.MainView().SetTyping(evt.RoomID, strUsers) 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.gmx.Recover()
c.SendTyping(roomID, false) c.SendTyping(roomID, false)
c.client.SendText(roomID, message) c.client.SendText(roomID, message)
} }
func (c *MatrixContainer) SendTyping(roomID string, typing bool) { func (c *Container) SendTyping(roomID string, typing bool) {
c.gmx.Recover() c.gmx.Recover()
time := time.Now().Unix() ts := time.Now().Unix()
if c.typing > time && typing { if c.typing > ts && typing {
return return
} }
if typing { if typing {
c.client.UserTyping(roomID, true, 5000) c.client.UserTyping(roomID, true, 5000)
c.typing = time + 5 c.typing = ts + 5
} else { } else {
c.client.UserTyping(roomID, false, 0) c.client.UserTyping(roomID, false, 0)
c.typing = 0 c.typing = 0
} }
} }
func (c *MatrixContainer) JoinRoom(roomID string) error { func (c *Container) JoinRoom(roomID string) error {
if len(roomID) == 0 { if len(roomID) == 0 {
return fmt.Errorf("invalid room ID") return fmt.Errorf("invalid room ID")
} }
@ -222,26 +229,40 @@ func (c *MatrixContainer) JoinRoom(roomID string) error {
server = roomID[strings.Index(roomID, ":")+1:] server = roomID[strings.Index(roomID, ":")+1:]
} }
resp, err := c.client.JoinRoom(roomID, server, nil) _, err := c.client.JoinRoom(roomID, server, nil)
if err != nil { if err != nil {
return err return err
} }
c.ui.MainView().AddRoom(resp.RoomID) // TODO probably safe to remove
// c.ui.MainView().AddRoom(resp.RoomID)
return nil 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) content := make([]*gomatrix.Event, 0)
err := c.client.StateEvent(roomID, "", "", &content) err := c.client.StateEvent(roomID, "", "", &content)
if err != nil { if err != nil {
c.debug.Print("Error getting state of", roomID, err) debug.Print("Error getting state of", roomID, err)
return nil return nil
} }
return content 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) resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -249,7 +270,7 @@ func (c *MatrixContainer) GetHistory(roomID, prevBatch string, limit int) ([]gom
return resp.Chunk, resp.End, nil 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) room := c.config.Session.GetRoom(roomID)
if room != nil && len(room.State) == 0 { if room != nil && len(room.State) == 0 {
events := c.getState(room.ID) events := c.getState(room.ID)

51
matrix/room/member.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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,
}
}

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package room
import ( import (
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
@ -25,7 +25,7 @@ type Room struct {
*gomatrix.Room *gomatrix.Room
PrevBatch string PrevBatch string
memberCache map[string]*RoomMember memberCache map[string]*Member
nameCache string nameCache string
topicCache string topicCache string
} }
@ -107,38 +107,8 @@ func (room *Room) GetTitle() string {
return room.nameCache return room.nameCache
} }
type RoomMember struct { func (room *Room) createMemberCache() map[string]*Member {
UserID string `json:"-"` cache := make(map[string]*Member)
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)
events := room.GetStateEvents("m.room.member") events := room.GetStateEvents("m.room.member")
if events != nil { if events != nil {
for userID, event := range events { for userID, event := range events {
@ -152,14 +122,14 @@ func (room *Room) createMemberCache() map[string]*RoomMember {
return cache return cache
} }
func (room *Room) GetMembers() map[string]*RoomMember { func (room *Room) GetMembers() map[string]*Member {
if len(room.memberCache) == 0 { if len(room.memberCache) == 0 {
room.createMemberCache() room.createMemberCache()
} }
return room.memberCache return room.memberCache
} }
func (room *Room) GetMember(userID string) *RoomMember { func (room *Room) GetMember(userID string) *Member {
if len(room.memberCache) == 0 { if len(room.memberCache) == 0 {
room.createMemberCache() room.createMemberCache()
} }

View File

@ -1,4 +1,4 @@
package main package matrix
import ( import (
"encoding/json" "encoding/json"
@ -7,20 +7,21 @@ import (
"time" "time"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config"
) )
// GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively // 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 // 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. // pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information.
type GomuksSyncer struct { type GomuksSyncer struct {
Session *Session Session *config.Session
listeners map[string][]gomatrix.OnEventListener // event type to listeners array listeners map[string][]gomatrix.OnEventListener // event type to listeners array
} }
// NewGomuksSyncer returns an instantiated GomuksSyncer // NewGomuksSyncer returns an instantiated GomuksSyncer
func NewGomuksSyncer(session *Session) *GomuksSyncer { func NewGomuksSyncer(session *config.Session) *GomuksSyncer {
return &GomuksSyncer{ return &GomuksSyncer{
Session: session, Session: session,
listeners: make(map[string][]gomatrix.OnEventListener), 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 { for _, event := range res.Presence.Events {
s.notifyListeners(&event) s.notifyListeners(event)
} }
for roomID, roomData := range res.Rooms.Join { for roomID, roomData := range res.Rooms.Join {
room := s.Session.GetRoom(roomID) room := s.Session.GetRoom(roomID)
for _, event := range roomData.State.Events { for _, event := range roomData.State.Events {
event.RoomID = roomID event.RoomID = roomID
room.UpdateState(&event) room.UpdateState(event)
s.notifyListeners(&event) s.notifyListeners(event)
} }
for _, event := range roomData.Timeline.Events { for _, event := range roomData.Timeline.Events {
event.RoomID = roomID event.RoomID = roomID
s.notifyListeners(&event) s.notifyListeners(event)
} }
for _, event := range roomData.Ephemeral.Events { for _, event := range roomData.Ephemeral.Events {
event.RoomID = roomID event.RoomID = roomID
s.notifyListeners(&event) s.notifyListeners(event)
} }
if len(room.PrevBatch) == 0 { if len(room.PrevBatch) == 0 {
@ -64,8 +65,8 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er
room := s.Session.GetRoom(roomID) room := s.Session.GetRoom(roomID)
for _, event := range roomData.State.Events { for _, event := range roomData.State.Events {
event.RoomID = roomID event.RoomID = roomID
room.UpdateState(&event) room.UpdateState(event)
s.notifyListeners(&event) s.notifyListeners(event)
} }
} }
for roomID, roomData := range res.Rooms.Leave { 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 { for _, event := range roomData.Timeline.Events {
if event.StateKey != nil { if event.StateKey != nil {
event.RoomID = roomID event.RoomID = roomID
room.UpdateState(&event) room.UpdateState(event)
s.notifyListeners(&event) s.notifyListeners(event)
} }
} }

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package debug
import ( import (
"fmt" "fmt"
@ -22,54 +22,62 @@ import (
"maunium.net/go/tview" "maunium.net/go/tview"
) )
const DebugPaneHeight = 35 type Printer interface {
type DebugPrinter interface {
Printf(text string, args ...interface{}) Printf(text string, args ...interface{})
Print(text ...interface{}) Print(text ...interface{})
} }
type DebugPane struct { type Pane struct {
pane *tview.TextView *tview.TextView
Height int
num int num int
gmx Gomuks
} }
func NewDebugPane(gmx Gomuks) *DebugPane { var Default Printer
func NewPane() *Pane {
pane := tview.NewTextView() pane := tview.NewTextView()
pane. pane.
SetScrollable(true). SetScrollable(true).
SetWrap(true) SetWrap(true).
pane.SetChangedFunc(func() { SetBorder(true).
gmx.App().Draw() SetTitle("Debug output")
})
pane.SetBorder(true).SetTitle("Debug output")
fmt.Fprintln(pane, "[0] Debug pane initialized") fmt.Fprintln(pane, "[0] Debug pane initialized")
return &DebugPane{ return &Pane{
pane: pane, TextView: pane,
Height: 35,
num: 0, num: 0,
gmx: gmx,
} }
} }
func (db *DebugPane) Printf(text string, args ...interface{}) { func (db *Pane) Printf(text string, args ...interface{}) {
db.Write(fmt.Sprintf(text, args...) + "\n") db.WriteString(fmt.Sprintf(text, args...) + "\n")
} }
func (db *DebugPane) Print(text ...interface{}) { func (db *Pane) Print(text ...interface{}) {
db.Write(fmt.Sprintln(text...)) db.WriteString(fmt.Sprintln(text...))
} }
func (db *DebugPane) Write(text string) { func (db *Pane) WriteString(text string) {
if db.pane != nil { db.num++
db.num++ fmt.Fprintf(db, "[%d] %s", db.num, text)
fmt.Fprintf(db.pane, "[%d] %s", db.num, text)
}
} }
func (db *DebugPane) Wrap(main tview.Primitive) tview.Primitive { func (db *Pane) Wrap(main tview.Primitive) tview.Primitive {
return tview.NewGrid().SetRows(0, DebugPaneHeight).SetColumns(0). return tview.NewGrid().SetRows(0, db.Height).SetColumns(0).
AddItem(main, 0, 0, 1, 1, 1, 1, true). 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...)
}
} }

85
ui/types/message.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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):]
}
}
}

View File

@ -14,25 +14,17 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package ui
import ( import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/gomuks/interface"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
// Allowed views in GomuksUI
const (
ViewLogin = "login"
ViewMain = "main"
)
type GomuksUI struct { type GomuksUI struct {
gmx Gomuks gmx ifc.Gomuks
app *tview.Application app *tview.Application
matrix *MatrixContainer
debug DebugPrinter
config *Config
views *tview.Pages views *tview.Pages
mainView *MainView mainView *MainView
@ -44,13 +36,10 @@ func init() {
tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
} }
func NewGomuksUI(gmx Gomuks) (ui *GomuksUI) { func NewGomuksUI(gmx ifc.Gomuks) (ui *GomuksUI) {
ui = &GomuksUI{ ui = &GomuksUI{
gmx: gmx, gmx: gmx,
app: gmx.App(), app: gmx.App(),
matrix: gmx.MatrixContainer(),
debug: gmx.Debug(),
config: gmx.Config(),
views: tview.NewPages(), views: tview.NewPages(),
} }
ui.views.SetChangedFunc(ui.Render) ui.views.SetChangedFunc(ui.Render)
@ -61,16 +50,16 @@ func (ui *GomuksUI) Render() {
ui.app.Draw() ui.app.Draw()
} }
func (ui *GomuksUI) SetView(name string) { func (ui *GomuksUI) SetView(name ifc.View) {
ui.views.SwitchToPage(name) ui.views.SwitchToPage(string(name))
} }
func (ui *GomuksUI) InitViews() tview.Primitive { func (ui *GomuksUI) InitViews() tview.Primitive {
ui.views.AddPage(ViewLogin, ui.NewLoginView(), true, true) ui.views.AddPage(string(ifc.ViewLogin), ui.NewLoginView(), true, true)
ui.views.AddPage(ViewMain, ui.NewMainView(), true, false) ui.views.AddPage(string(ifc.ViewMain), ui.NewMainView(), true, false)
return ui.views return ui.views
} }
func (ui *GomuksUI) MainView() *MainView { func (ui *GomuksUI) MainView() ifc.MainView {
return ui.mainView return ui.mainView
} }

View File

@ -14,14 +14,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package ui
import ( import (
"maunium.net/go/gomuks/ui/debug"
"maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
func (ui *GomuksUI) NewLoginView() tview.Primitive { func (ui *GomuksUI) NewLoginView() tview.Primitive {
hs := ui.config.HS hs := ui.gmx.Config().HS
if len(hs) == 0 { if len(hs) == 0 {
hs = "https://matrix.org" hs = "https://matrix.org"
} }
@ -29,13 +31,13 @@ func (ui *GomuksUI) NewLoginView() tview.Primitive {
ui.loginView = tview.NewForm() ui.loginView = tview.NewForm()
ui.loginView. ui.loginView.
AddInputField("Homeserver", hs, 30, nil, nil). 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). AddPasswordField("Password", "", 30, '*', nil).
AddButton("Log in", ui.login). AddButton("Log in", ui.login).
AddButton("Quit", ui.gmx.Stop). AddButton("Quit", ui.gmx.Stop).
SetButtonsAlign(tview.AlignCenter). SetButtonsAlign(tview.AlignCenter).
SetBorder(true).SetTitle("Log in to Matrix") SetBorder(true).SetTitle("Log in to Matrix")
return Center(45, 11, ui.loginView) return widget.Center(45, 11, ui.loginView)
} }
func (ui *GomuksUI) login() { func (ui *GomuksUI) login() {
@ -43,8 +45,9 @@ func (ui *GomuksUI) login() {
mxid := ui.loginView.GetFormItem(1).(*tview.InputField).GetText() mxid := ui.loginView.GetFormItem(1).(*tview.InputField).GetText()
password := ui.loginView.GetFormItem(2).(*tview.InputField).GetText() password := ui.loginView.GetFormItem(2).(*tview.InputField).GetText()
ui.debug.Printf("Logging into %s as %s...", hs, mxid) debug.Printf("Logging into %s as %s...", hs, mxid)
ui.config.HS = hs ui.gmx.Config().HS = hs
ui.debug.Print("Connect result:", ui.matrix.InitClient()) mx := ui.gmx.MatrixContainer()
ui.debug.Print("Login result:", ui.matrix.Login(mxid, password)) debug.Print("Connect result:", mx.InitClient())
debug.Print("Login result:", mx.Login(mxid, password))
} }

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package ui
import ( import (
"fmt" "fmt"
@ -26,6 +26,11 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/gomatrix" "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" "maunium.net/go/tview"
) )
@ -34,15 +39,14 @@ type MainView struct {
roomList *tview.List roomList *tview.List
roomView *tview.Pages roomView *tview.Pages
rooms map[string]*RoomView rooms map[string]*widget.RoomView
input *AdvancedInputField input *widget.AdvancedInputField
currentRoomIndex int currentRoomIndex int
roomIDs []string roomIDs []string
matrix *MatrixContainer matrix ifc.MatrixContainer
debug DebugPrinter gmx ifc.Gomuks
gmx Gomuks config *config.Config
config *Config
parent *GomuksUI parent *GomuksUI
} }
@ -55,13 +59,12 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
Grid: tview.NewGrid(), Grid: tview.NewGrid(),
roomList: tview.NewList(), roomList: tview.NewList(),
roomView: tview.NewPages(), roomView: tview.NewPages(),
rooms: make(map[string]*RoomView), rooms: make(map[string]*widget.RoomView),
input: NewAdvancedInputField(), input: widget.NewAdvancedInputField(),
matrix: ui.matrix, matrix: ui.gmx.MatrixContainer(),
debug: ui.debug,
gmx: ui.gmx, gmx: ui.gmx,
config: ui.config, config: ui.gmx.Config(),
parent: ui, parent: ui,
} }
@ -83,7 +86,7 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
SetInputCapture(mainView.InputCapture) SetInputCapture(mainView.InputCapture)
mainView.addItem(mainView.roomList, 0, 0, 2, 1) 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.roomView, 0, 2, 1, 1)
mainView.AddItem(mainView.input, 1, 2, 1, 1, 0, 0, true) 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 { if len(userCompletions) == 1 {
text = str[0:len(str)-len(word)] + userCompletions[0] + text[len(str):] text = str[0:len(str)-len(word)] + userCompletions[0] + text[len(str):]
} else if len(userCompletions) > 1 && len(userCompletions) < 6 { } else if len(userCompletions) > 1 && len(userCompletions) < 6 {
roomView.status.Clear() roomView.SetStatus(fmt.Sprintf("Completions: %s", strings.Join(userCompletions, ", ")))
fmt.Fprintf(roomView.status, "Completions: %s", strings.Join(userCompletions, ", "))
} }
} }
return text return text
@ -147,7 +149,7 @@ func (view *MainView) InputDone(key tcell.Key) {
func (view *MainView) HandleCommand(room, command string, args []string) { func (view *MainView) HandleCommand(room, command string, args []string) {
view.gmx.Recover() view.gmx.Recover()
view.debug.Print("Handling command", command, args) debug.Print("Handling command", command, args)
switch command { switch command {
case "/quit": case "/quit":
view.gmx.Stop() view.gmx.Stop()
@ -157,13 +159,13 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
case "/part": case "/part":
fallthrough fallthrough
case "/leave": case "/leave":
view.matrix.client.LeaveRoom(room) debug.Print(view.matrix.LeaveRoom(room))
case "/join": case "/join":
if len(args) == 0 { if len(args) == 0 {
view.AddServiceMessage(room, "Usage: /join <room>") view.AddServiceMessage(room, "Usage: /join <room>")
break break
} }
view.debug.Print(view.matrix.JoinRoom(args[0])) debug.Print(view.matrix.JoinRoom(args[0]))
default: default:
view.AddServiceMessage(room, "Unknown command.") view.AddServiceMessage(room, "Unknown command.")
} }
@ -218,7 +220,7 @@ func (view *MainView) addRoom(index int, room string) {
view.SwitchRoom(index) view.SwitchRoom(index)
}) })
if !view.roomView.HasPage(room) { if !view.roomView.HasPage(room) {
roomView := NewRoomView(view, roomStore) roomView := widget.NewRoomView(view, roomStore)
view.rooms[room] = roomView view.rooms[room] = roomView
view.roomView.AddPage(room, roomView, true, false) view.roomView.AddPage(room, roomView, true, false)
roomView.UpdateUserList() 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] return view.rooms[id]
} }
@ -265,11 +267,11 @@ func (view *MainView) RemoveRoom(room string) {
view.Render() view.Render()
} }
func (view *MainView) SetRoomList(rooms []string) { func (view *MainView) SetRooms(rooms []string) {
view.roomIDs = rooms view.roomIDs = rooms
view.roomList.Clear() view.roomList.Clear()
view.roomView.Clear() view.roomView.Clear()
view.rooms = make(map[string]*RoomView) view.rooms = make(map[string]*widget.RoomView)
for index, room := range rooms { for index, room := range rooms {
view.addRoom(index, room) view.addRoom(index, room)
} }
@ -289,7 +291,7 @@ func (view *MainView) AddServiceMessage(room, message string) {
if ok { if ok {
messageView := roomView.MessageView() messageView := roomView.MessageView()
message := messageView.NewMessage("", "*", message, time.Now()) message := messageView.NewMessage("", "*", message, time.Now())
messageView.AddMessage(message, AppendMessage) messageView.AddMessage(message, widget.AppendMessage)
view.parent.Render() view.parent.Render()
} }
} }
@ -300,29 +302,29 @@ func (view *MainView) Render() {
func (view *MainView) GetHistory(room string) { func (view *MainView) GetHistory(room string) {
roomView := view.rooms[room] 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 { 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 return
} }
for _, evt := range history { for _, evt := range history {
var room *RoomView var room *widget.RoomView
var message *Message var message *types.Message
if evt.Type == "m.room.message" { if evt.Type == "m.room.message" {
room, message = view.ProcessMessageEvent(&evt) room, message = view.ProcessMessageEvent(&evt)
} else if evt.Type == "m.room.member" { } else if evt.Type == "m.room.member" {
room, message = view.ProcessMembershipEvent(&evt, false) room, message = view.ProcessMembershipEvent(&evt, false)
} }
if room != nil && message != nil { 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) room = view.GetRoom(evt.RoomID)
if room != nil { if room != nil {
text := evt.Content["body"].(string) text, _ := evt.Content["body"].(string)
message = room.NewMessage(evt.ID, evt.Sender, text, unixToTime(evt.Timestamp)) message = room.NewMessage(evt.ID, evt.Sender, text, unixToTime(evt.Timestamp))
} }
return 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 { if new && evt.StateKey != nil && *evt.StateKey == view.config.Session.MXID {
view.processOwnMembershipChange(evt) view.processOwnMembershipChange(evt)
} }

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package widget
import ( import (
"math" "math"

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package widget
import ( import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"

32
ui/widget/center.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}

60
ui/widget/color.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View File

@ -14,24 +14,13 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package widget
import ( import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/tview" "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 { type FormTextView struct {
*tview.TextView *tview.TextView
} }

View File

@ -14,79 +14,19 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package widget
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/ui/types"
"maunium.net/go/tview" "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 { type MessageView struct {
*tview.Box *tview.Box
@ -106,7 +46,7 @@ type MessageView struct {
totalHeight int totalHeight int
messageIDs map[string]bool messageIDs map[string]bool
messages []*Message messages []*types.Message
} }
func NewMessageView() *MessageView { func NewMessageView() *MessageView {
@ -119,7 +59,7 @@ func NewMessageView() *MessageView {
Separator: '|', Separator: '|',
ScrollOffset: 0, ScrollOffset: 0,
messages: make([]*Message, 0), messages: make([]*types.Message, 0),
messageIDs: make(map[string]bool), messageIDs: make(map[string]bool),
widestSender: 5, widestSender: 5,
@ -132,11 +72,11 @@ func NewMessageView() *MessageView {
} }
} }
func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *Message { func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *types.Message {
return NewMessage(id, sender, text, return types.NewMessage(id, sender, text,
timestamp.Format(view.TimestampFormat), timestamp.Format(view.TimestampFormat),
timestamp.Format(view.DateFormat), timestamp.Format(view.DateFormat),
getColor(sender)) GetHashColor(sender))
} }
func (view *MessageView) recalculateBuffers() { func (view *MessageView) recalculateBuffers() {
@ -144,7 +84,7 @@ func (view *MessageView) recalculateBuffers() {
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
if width != view.prevWidth { if width != view.prevWidth {
for _, message := range view.messages { for _, message := range view.messages {
message.calculateBuffer(width) message.CalculateBuffer(width)
} }
view.prevWidth = width view.prevWidth = width
} }
@ -164,7 +104,7 @@ const (
PrependMessage PrependMessage
) )
func (view *MessageView) AddMessage(message *Message, direction int) { func (view *MessageView) AddMessage(message *types.Message, direction int) {
_, messageExists := view.messageIDs[message.ID] _, messageExists := view.messageIDs[message.ID]
if messageExists { if messageExists {
return return
@ -174,15 +114,15 @@ func (view *MessageView) AddMessage(message *Message, direction int) {
_, _, width, _ := view.GetInnerRect() _, _, width, _ := view.GetInnerRect()
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
message.calculateBuffer(width) message.CalculateBuffer(width)
if direction == AppendMessage { if direction == AppendMessage {
if view.ScrollOffset > 0 { if view.ScrollOffset > 0 {
view.ScrollOffset += len(message.buffer) view.ScrollOffset += len(message.Buffer)
} }
view.messages = append(view.messages, message) view.messages = append(view.messages, message)
} else if direction == PrependMessage { } else if direction == PrependMessage {
view.messages = append([]*Message{message}, view.messages...) view.messages = append([]*types.Message{message}, view.messages...)
} }
view.messageIDs[message.ID] = true view.messageIDs[message.ID] = true
@ -199,7 +139,7 @@ func (view *MessageView) recalculateHeight() {
for i := len(view.messages) - 1; i >= 0; i-- { for i := len(view.messages) - 1; i >= 0; i-- {
prevTotalHeight := view.totalHeight prevTotalHeight := view.totalHeight
message := view.messages[i] message := view.messages[i]
view.totalHeight += len(message.buffer) view.totalHeight += len(message.Buffer)
if message.Date != prevDate { if message.Date != prevDate {
if len(prevDate) != 0 { if len(prevDate) != 0 {
view.totalHeight++ 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)) screen.SetContent(x+offsetX+localOffset, y, ch, nil, tcell.StyleDefault.Foreground(color))
} }
offsetX += chWidth offsetX += chWidth
if offsetX > maxWidth {
break
}
} }
} }
@ -302,7 +245,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
prevSenderLine := -1 prevSenderLine := -1
for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- { for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- {
message := view.messages[i] message := view.messages[i]
messageHeight := len(message.buffer) messageHeight := len(message.Buffer)
// Show message when the date changes. // Show message when the date changes.
if message.Date != prevDate { 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.writeLine(screen, message.Timestamp, x, senderAtLine, tcell.ColorDefault)
view.writeLineRight(screen, message.Sender, view.writeLineRight(screen, message.Sender,
x+usernameOffsetX, senderAtLine, x+usernameOffsetX, senderAtLine,
view.widestSender, message.senderColor) view.widestSender, message.SenderColor)
if message.Sender == prevSender { if message.Sender == prevSender {
// Sender is same as previous. We're looping from bottom to top, and we want the // 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. // below.
view.writeLineRight(screen, strings.Repeat(" ", view.widestSender), view.writeLineRight(screen, strings.Repeat(" ", view.widestSender),
x+usernameOffsetX, prevSenderLine, x+usernameOffsetX, prevSenderLine,
view.widestSender, message.senderColor) view.widestSender, message.SenderColor)
} }
prevSender = message.Sender prevSender = message.Sender
prevSenderLine = senderAtLine prevSenderLine = senderAtLine
for num, line := range message.buffer { for num, line := range message.Buffer {
offsetY := height - messageHeight - writeOffset + num offsetY := height - messageHeight - writeOffset + num
// Only render message if it's within the message view. // Only render message if it's within the message view.
if offsetY >= 0 { if offsetY >= 0 {

View File

@ -14,19 +14,23 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package widget
import ( import (
"fmt" "fmt"
"hash/fnv"
"sort"
"strings" "strings"
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
rooms "maunium.net/go/gomuks/matrix/room"
"maunium.net/go/gomuks/ui/types"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
type Renderable interface {
Render()
}
type RoomView struct { type RoomView struct {
*tview.Box *tview.Box
@ -34,31 +38,19 @@ type RoomView struct {
content *MessageView content *MessageView
status *tview.TextView status *tview.TextView
userList *tview.TextView userList *tview.TextView
room *Room Room *rooms.Room
parent *MainView parent Renderable
} }
var colorNames []string func NewRoomView(parent Renderable, room *rooms.Room) *RoomView {
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 {
view := &RoomView{ view := &RoomView{
Box: tview.NewBox(), Box: tview.NewBox(),
topic: tview.NewTextView(), topic: tview.NewTextView(),
content: NewMessageView(), content: NewMessageView(),
status: tview.NewTextView(), status: tview.NewTextView(),
userList: tview.NewTextView(), userList: tview.NewTextView(),
room: room, Room: room,
parent: parent, parent: parent,
} }
view.topic. view.topic.
@ -90,9 +82,13 @@ func (view *RoomView) Draw(screen tcell.Screen) {
view.userList.Draw(screen) view.userList.Draw(screen)
} }
func (view *RoomView) SetStatus(status string) {
view.status.SetText(status)
}
func (view *RoomView) SetTyping(users []string) { func (view *RoomView) SetTyping(users []string) {
for index, user := range users { for index, user := range users {
member := view.room.GetMember(user) member := view.Room.GetMember(user)
if member != nil { if member != nil {
users[index] = member.DisplayName users[index] = member.DisplayName
} }
@ -109,7 +105,7 @@ func (view *RoomView) SetTyping(users []string) {
} }
func (view *RoomView) AutocompleteUser(existingText string) (completions []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) { if strings.HasPrefix(user.DisplayName, existingText) {
completions = append(completions, user.DisplayName) completions = append(completions, user.DisplayName)
} else if strings.HasPrefix(user.UserID, existingText) { } else if strings.HasPrefix(user.UserID, existingText) {
@ -123,38 +119,15 @@ func (view *RoomView) MessageView() *MessageView {
return view.content 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() { func (view *RoomView) UpdateUserList() {
var joined strings.Builder var joined strings.Builder
var invited strings.Builder var invited strings.Builder
for _, user := range view.room.GetMembers() { for _, user := range view.Room.GetMembers() {
if user.Membership == "join" { if user.Membership == "join" {
joined.WriteString(color(user.DisplayName)) joined.WriteString(AddHashColor(user.DisplayName))
joined.WriteRune('\n') joined.WriteRune('\n')
} else if user.Membership == "invite" { } else if user.Membership == "invite" {
invited.WriteString(color(user.DisplayName)) invited.WriteString(AddHashColor(user.DisplayName))
invited.WriteRune('\n') invited.WriteRune('\n')
} }
} }
@ -165,15 +138,15 @@ func (view *RoomView) UpdateUserList() {
} }
} }
func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) *Message { func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) *types.Message {
member := view.room.GetMember(sender) member := view.Room.GetMember(sender)
if member != nil { if member != nil {
sender = member.DisplayName sender = member.DisplayName
} }
return view.content.NewMessage(id, sender, text, timestamp) 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.content.AddMessage(message, direction)
view.parent.Render() view.parent.Render()
} }