Code additions/edits

This commit is contained in:
Tulir Asokan 2018-03-13 21:58:43 +02:00
parent f0333df1b2
commit 90629c5c78
9 changed files with 506 additions and 113 deletions

View File

@ -30,13 +30,22 @@ type Config struct {
HS string `yaml:"homeserver"`
dir string `yaml:"-"`
gmx Gomuks `yaml:"-"`
debug DebugPrinter `yaml:"-"`
Session *Session `yaml:"-"`
}
func (config *Config) Load(dir string) {
config.dir = dir
os.MkdirAll(dir, 0700)
configPath := filepath.Join(dir, "config.yaml")
func NewConfig(gmx Gomuks, dir string) *Config {
return &Config{
gmx: gmx,
debug: gmx.Debug(),
dir: dir,
}
}
func (config *Config) Load() {
os.MkdirAll(config.dir, 0700)
configPath := filepath.Join(config.dir, "config.yaml")
data, err := ioutil.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
@ -55,16 +64,17 @@ func (config *Config) Load(dir string) {
}
func (config *Config) Save() {
os.MkdirAll(config.dir, 0700)
data, err := yaml.Marshal(&config)
if err != nil {
debug.Print("Failed to marshal config")
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 {
debug.Print("Failed to write config to", path)
config.debug.Print("Failed to write config to", path)
panic(err)
}
}

View File

@ -22,35 +22,54 @@ import (
"github.com/rivo/tview"
)
const DebugPaneHeight = 40
type DebugPrinter interface {
Printf(text string, args ...interface{})
Print(text ...interface{})
}
type DebugPane struct {
text string
pane *tview.TextView
num int
gmx Gomuks
}
func NewDebugPane(gmx Gomuks) *DebugPane {
pane := tview.NewTextView()
pane.
SetScrollable(true).
SetWrap(true)
pane.SetChangedFunc(func() {
gmx.App().Draw()
})
pane.SetBorder(true).SetTitle("Debug output")
fmt.Fprintln(pane, "[0] Debug pane initialized")
return &DebugPane{
pane: pane,
num: 0,
gmx: gmx,
}
}
func (db *DebugPane) Printf(text string, args ...interface{}) {
db.num++
db.Write(fmt.Sprintf("[%d] %s\n", db.num, fmt.Sprintf(text, args...)))
db.Write(fmt.Sprintf(text, args...))
}
func (db *DebugPane) Print(text ...interface{}) {
db.num++
db.Write(fmt.Sprintf("[%d] %s", db.num, fmt.Sprintln(text...)))
db.Write(fmt.Sprint(text...))
}
func (db *DebugPane) Write(text string) {
if db.pane != nil {
db.text += text
db.pane.SetText(db.text)
db.num++
fmt.Fprintf(db.pane, "[%d] %s\n", db.num, text)
}
}
func (db *DebugPane) Wrap(main *tview.Pages) tview.Primitive {
db.pane = tview.NewTextView()
db.pane.SetBorder(true).SetTitle("Debug output")
db.text += "[0] Debug pane initialized\n"
db.pane.SetText(db.text)
return tview.NewGrid().SetRows(0, 20).SetColumns(0).
func (db *DebugPane) Wrap(main tview.Primitive) tview.Primitive {
return tview.NewGrid().SetRows(0, DebugPaneHeight).SetColumns(0).
AddItem(main, 0, 0, 1, 1, 1, 1, true).
AddItem(db.pane, 1, 0, 1, 1, 1, 1, false)
}

124
gomuks.go
View File

@ -20,94 +20,84 @@ import (
"os"
"path/filepath"
"github.com/gdamore/tcell"
"github.com/matrix-org/gomatrix"
"github.com/rivo/tview"
)
var matrix = new(MatrixContainer)
var config = new(Config)
var debug = new(DebugPane)
type Gomuks interface {
Debug() DebugPrinter
Matrix() *gomatrix.Client
MatrixContainer() *MatrixContainer
App() *tview.Application
UI() *GomuksUI
Config() *Config
}
func main() {
type gomuks struct {
app *tview.Application
ui *GomuksUI
matrix *MatrixContainer
debug *DebugPane
config *Config
}
func NewGomuks(debug bool) *gomuks {
configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks")
os.MkdirAll(configDir, 0700)
config.Load(configDir)
views := tview.NewPages()
InitUI(views)
main := debug.Wrap(views)
if len(config.MXID) > 0 {
config.LoadSession(config.MXID)
gmx := &gomuks{
app: tview.NewApplication(),
}
matrix.Init(config)
gmx.debug = NewDebugPane(gmx)
gmx.config = NewConfig(gmx, configDir)
gmx.ui = NewGomuksUI(gmx)
gmx.matrix = NewMatrixContainer(gmx)
gmx.ui.matrix = gmx.matrix
if err := tview.NewApplication().SetRoot(main, true).Run(); err != nil {
gmx.config.Load()
if len(gmx.config.MXID) > 0 {
gmx.config.LoadSession(gmx.config.MXID)
}
gmx.matrix.InitClient()
main := gmx.ui.InitViews()
if debug {
main = gmx.debug.Wrap(main)
}
gmx.app.SetRoot(main, true)
return gmx
}
func (gmx *gomuks) Start() {
if err := gmx.app.Run(); err != nil {
panic(err)
}
}
func InitUI(views *tview.Pages) {
views.AddPage("login", InitLoginUI(), true, true)
func (gmx *gomuks) Debug() DebugPrinter {
return gmx.debug
}
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)
func (gmx *gomuks) Matrix() *gomatrix.Client {
return gmx.matrix.client
}
type FormTextView struct {
*tview.TextView
func (gmx *gomuks) MatrixContainer() *MatrixContainer {
return gmx.matrix
}
func (ftv *FormTextView) GetLabel() string {
return ""
func (gmx *gomuks) App() *tview.Application {
return gmx.app
}
func (ftv *FormTextView) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
return ftv
func (gmx *gomuks) Config() *Config {
return gmx.config
}
func (ftv *FormTextView) GetFieldWidth() int {
_, _, w, _ := ftv.TextView.GetRect()
return w
func (gmx *gomuks) UI() *GomuksUI {
return gmx.ui
}
func (ftv *FormTextView) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
ftv.SetDoneFunc(handler)
return ftv
}
func login(form *tview.Form) func() {
return func() {
hs := form.GetFormItem(0).(*tview.InputField).GetText()
mxid := form.GetFormItem(1).(*tview.InputField).GetText()
password := form.GetFormItem(2).(*tview.InputField).GetText()
debug.Printf("%s %s %s", hs, mxid, password)
config.HS = hs
debug.Print(matrix.Init(config))
debug.Print(matrix.Login(mxid, password))
}
}
func InitLoginUI() tview.Primitive {
form := tview.NewForm().SetButtonsAlign(tview.AlignCenter)
hs := config.HS
if len(hs) == 0 {
hs = "https://matrix.org"
}
form.
AddInputField("Homeserver", hs, 30, nil, nil).
AddInputField("Username", config.MXID, 30, nil, nil).
AddPasswordField("Password", "", 30, '*', nil).
AddButton("Log in", login(form))
form.SetBorder(true).SetTitle("Log in to Matrix")
return Center(45, 13, form)
func main() {
NewGomuks(true).Start()
}

View File

@ -23,27 +23,36 @@ import (
)
type MatrixContainer struct {
lient *gomatrix.Client
client *gomatrix.Client
gmx Gomuks
ui *GomuksUI
debug DebugPrinter
config *Config
running bool
stop chan bool
}
func (c *MatrixContainer) Initialized() bool {
return c.lient != nil
}
func (c *MatrixContainer) Init(config *Config) error {
c.config = config
if c.lient != nil {
c.lient.StopSync()
func NewMatrixContainer(gmx Gomuks) *MatrixContainer {
c := &MatrixContainer{
config: gmx.Config(),
debug: gmx.Debug(),
ui: gmx.UI(),
gmx: gmx,
}
return c
}
func (c *MatrixContainer) InitClient() error {
if len(c.config.HS) == 0 {
return fmt.Errorf("no homeserver in config")
}
if c.client != nil {
c.Stop()
c.client = nil
}
var mxid, accessToken string
if c.config.Session != nil {
accessToken = c.config.Session.AccessToken
@ -51,7 +60,7 @@ func (c *MatrixContainer) Init(config *Config) error {
}
var err error
c.lient, err = gomatrix.NewClient(c.config.HS, mxid, accessToken)
c.client, err = gomatrix.NewClient(c.config.HS, mxid, accessToken)
if err != nil {
return err
}
@ -64,8 +73,12 @@ func (c *MatrixContainer) Init(config *Config) error {
return nil
}
func (c *MatrixContainer) Initialized() bool {
return c.client != nil
}
func (c *MatrixContainer) Login(user, password string) error {
resp, err := c.lient.Login(&gomatrix.ReqLogin{
resp, err := c.client.Login(&gomatrix.ReqLogin{
Type: "m.login.password",
User: user,
Password: password,
@ -73,7 +86,7 @@ func (c *MatrixContainer) Login(user, password string) error {
if err != nil {
return err
}
c.lient.SetCredentials(resp.UserID, resp.AccessToken)
c.client.SetCredentials(resp.UserID, resp.AccessToken)
c.config.MXID = resp.UserID
c.config.Save()
@ -88,31 +101,50 @@ func (c *MatrixContainer) Login(user, password string) error {
func (c *MatrixContainer) Stop() {
c.stop <- true
c.lient.StopSync()
c.client.StopSync()
}
func (c *MatrixContainer) UpdateRoomList() {
rooms, err := c.client.JoinedRooms()
if err != nil {
c.debug.Print(err)
}
c.ui.SetRoomList(rooms.JoinedRooms)
}
func (c *MatrixContainer) Start() {
debug.Print("Starting sync...")
c.debug.Print("Starting sync...")
c.running = true
c.lient.Store = c.config.Session
c.ui.SetView(ViewMain)
c.client.Store = c.config.Session
syncer := c.lient.Syncer.(*gomatrix.DefaultSyncer)
c.UpdateRoomList()
syncer := c.client.Syncer.(*gomatrix.DefaultSyncer)
syncer.OnEventType("m.room.message", c.HandleMessage)
for {
select {
case <-c.stop:
debug.Print("Stopping sync...")
c.debug.Print("Stopping sync...")
c.running = false
return
default:
if err := c.lient.Sync(); err != nil {
debug.Print("Sync() errored", err)
if err := c.client.Sync(); err != nil {
c.debug.Print("Sync() errored", err)
} else {
c.debug.Print("Sync() returned without error")
}
}
}
}
func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) {
debug.Print("Message received")
message, _ := evt.Content["body"].(string)
c.ui.Append(evt.RoomID, evt.Sender, message)
}
func (c *MatrixContainer) SendMessage(roomID, message string) {
c.client.SendText(roomID, message)
}

View File

@ -31,6 +31,8 @@ type Session struct {
NextBatch string
FilterID string
Rooms map[string]*gomatrix.Room
debug DebugPrinter `json:"-"`
}
func (config *Config) LoadSession(mxid string) {
@ -43,19 +45,20 @@ func (config *Config) NewSession(mxid string) *Session {
MXID: mxid,
path: filepath.Join(config.dir, mxid+".session"),
Rooms: make(map[string]*gomatrix.Room),
debug: config.debug,
}
}
func (s *Session) Load() {
data, err := ioutil.ReadFile(s.path)
if err != nil {
debug.Print("Failed to read session from", s.path)
s.debug.Print("Failed to read session from", s.path)
panic(err)
}
err = json.Unmarshal(data, s)
if err != nil {
debug.Print("Failed to parse session at", s.path)
s.debug.Print("Failed to parse session at", s.path)
panic(err)
}
}
@ -63,13 +66,13 @@ func (s *Session) Load() {
func (s *Session) Save() {
data, err := json.Marshal(s)
if err != nil {
debug.Print("Failed to marshal session of", s.MXID)
s.debug.Print("Failed to marshal session of", s.MXID)
panic(err)
}
err = ioutil.WriteFile(s.path, data, 0600)
if err != nil {
debug.Print("Failed to write session to", s.path)
s.debug.Print("Failed to write session to", s.path)
panic(err)
}
}

71
ui.go Normal file
View File

@ -0,0 +1,71 @@
// 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 main
import (
"github.com/rivo/tview"
)
// Allowed views in GomuksUI
const (
ViewLogin = "login"
ViewMain = "main"
)
type GomuksUI struct {
gmx Gomuks
app *tview.Application
matrix *MatrixContainer
debug DebugPrinter
config *Config
views *tview.Pages
mainView *tview.Grid
mainViewRoomList *tview.List
mainViewRoomView *tview.Pages
mainViewInput *tview.InputField
mainViewRooms map[string]*RoomView
currentRoomIndex int
roomList []string
}
func NewGomuksUI(gmx 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)
return
}
func (ui *GomuksUI) Render() {
ui.app.Draw()
}
func (ui *GomuksUI) SetView(name string) {
ui.views.SwitchToPage(name)
}
func (ui *GomuksUI) InitViews() tview.Primitive {
ui.views.AddPage(ViewLogin, ui.MakeLoginUI(), true, true)
ui.views.AddPage(ViewMain, ui.MakeMainUI(), true, false)
return ui.views
}

55
uiutil.go Normal file
View File

@ -0,0 +1,55 @@
// 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 main
import (
"github.com/gdamore/tcell"
"github.com/rivo/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
}
func (ftv *FormTextView) GetLabel() string {
return ""
}
func (ftv *FormTextView) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
return ftv
}
func (ftv *FormTextView) GetFieldWidth() int {
_, _, w, _ := ftv.TextView.GetRect()
return w
}
func (ftv *FormTextView) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
ftv.SetDoneFunc(handler)
return ftv
}

49
view-login.go Normal file
View File

@ -0,0 +1,49 @@
// 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 main
import (
"github.com/rivo/tview"
)
func (ui *GomuksUI) MakeLoginUI() tview.Primitive {
form := tview.NewForm().SetButtonsAlign(tview.AlignCenter)
hs := ui.config.HS
if len(hs) == 0 {
hs = "https://matrix.org"
}
form.
AddInputField("Homeserver", hs, 30, nil, nil).
AddInputField("Username", ui.config.MXID, 30, nil, nil).
AddPasswordField("Password", "", 30, '*', nil).
AddButton("Log in", ui.login(form))
form.SetBorder(true).SetTitle("Log in to Matrix")
return Center(45, 13, form)
}
func (ui *GomuksUI) login(form *tview.Form) func() {
return func() {
hs := form.GetFormItem(0).(*tview.InputField).GetText()
mxid := form.GetFormItem(1).(*tview.InputField).GetText()
password := form.GetFormItem(2).(*tview.InputField).GetText()
ui.debug.Printf("Logging into %s as %s...", hs, mxid)
ui.config.HS = hs
ui.debug.Print(ui.matrix.InitClient())
ui.debug.Print(ui.matrix.Login(mxid, password))
}
}

164
view-main.go Normal file
View File

@ -0,0 +1,164 @@
// 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 main
import (
"fmt"
"strings"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
type RoomView struct {
*tview.Grid
sender, message *tview.TextView
}
func NewRoomView() *RoomView {
view := &RoomView{
tview.NewGrid(),
tview.NewTextView(),
tview.NewTextView(),
}
view.SetColumns(30, 0).SetRows(0)
view.sender.SetTextAlign(tview.AlignRight)
view.sender.SetScrollable(true)
view.message.SetScrollable(true)
view.AddItem(view.sender, 0, 0, 1, 1, 1, 1, false)
view.AddItem(view.message, 0, 1, 1, 1, 1, 1, false)
return view
}
func (ui *GomuksUI) MakeMainUI() tview.Primitive {
ui.mainView = tview.NewGrid().SetColumns(40, 0).SetRows(0, 2)
ui.mainViewRoomList = tview.NewList().ShowSecondaryText(false)
ui.mainViewRoomList.SetBorderPadding(1, 1, 1, 1)
ui.mainView.AddItem(ui.mainViewRoomList, 0, 0, 2, 1, 2, 1, false)
ui.mainViewRoomView = tview.NewPages()
ui.mainViewRoomView.SetChangedFunc(ui.Render)
ui.mainView.AddItem(ui.mainViewRoomView, 0, 1, 1, 1, 1, 1, false)
ui.mainViewInput = tview.NewInputField()
ui.mainViewInput.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEnter {
room, text := ui.currentRoom(), ui.mainViewInput.GetText()
if len(text) == 0 {
return
} else if text[0] == '/' {
args := strings.SplitN(text, " ", 2)
command := strings.ToLower(args[0])
args = args[1:]
ui.HandleCommand(room, command, args)
} else {
ui.matrix.SendMessage(room, text)
}
ui.mainViewInput.SetText("")
}
})
ui.mainView.AddItem(ui.mainViewInput, 1, 1, 1, 1, 1, 1, true)
ui.debug.Print(ui.mainViewInput.SetInputCapture(ui.MainUIKeyHandler))
ui.mainViewRooms = make(map[string]*RoomView)
return ui.mainView
}
func (ui *GomuksUI) HandleCommand(room, command string, args []string) {
switch command {
case "quit":
ui.matrix.Stop()
ui.app.Stop()
case "part":
case "leave":
ui.matrix.client.LeaveRoom(room)
case "join":
if len(args) == 0 {
ui.Append(room, "*", "Usage: /join <room>")
}
mxid := args[0]
server := mxid[strings.Index(mxid, ":")+1:]
ui.matrix.client.JoinRoom(mxid, server, nil)
}
}
func (ui *GomuksUI) MainUIKeyHandler(key *tcell.EventKey) *tcell.EventKey {
ui.debug.Print(key)
if key.Modifiers() == tcell.ModCtrl {
if key.Key() == tcell.KeyDown {
ui.SwitchRoom(ui.currentRoomIndex + 1)
ui.mainViewRoomList.SetCurrentItem(ui.currentRoomIndex)
} else if key.Key() == tcell.KeyUp {
ui.SwitchRoom(ui.currentRoomIndex - 1)
ui.mainViewRoomList.SetCurrentItem(ui.currentRoomIndex)
}
} else if key.Key() == tcell.KeyPgUp || key.Key() == tcell.KeyPgDn {
ui.mainViewRooms[ui.currentRoom()].sender.InputHandler()(key, nil)
ui.mainViewRooms[ui.currentRoom()].message.InputHandler()(key, nil)
} else {
return key
}
return nil
}
func (ui *GomuksUI) SetRoomList(rooms []string) {
ui.roomList = rooms
ui.mainViewRoomList.Clear()
for index, room := range rooms {
localRoomIndex := index
ui.mainViewRoomList.AddItem(room, "", 0, func() {
ui.SwitchRoom(localRoomIndex)
})
if !ui.mainViewRoomView.HasPage(room) {
roomView := NewRoomView()
ui.mainViewRooms[room] = roomView
ui.mainViewRoomView.AddPage(room, roomView, true, false)
}
}
ui.SwitchRoom(0)
}
func (ui *GomuksUI) currentRoom() string {
if len(ui.roomList) == 0 {
return ""
}
return ui.roomList[ui.currentRoomIndex]
}
func (ui *GomuksUI) SwitchRoom(roomIndex int) {
if roomIndex < 0 {
roomIndex = len(ui.roomList) - 1
}
ui.currentRoomIndex = roomIndex % len(ui.roomList)
ui.mainViewRoomView.SwitchToPage(ui.roomList[ui.currentRoomIndex])
}
func (ui *GomuksUI) Append(room, sender, message string) {
roomView, ok := ui.mainViewRooms[room]
if ok {
fmt.Fprintf(roomView.sender, sender)
fmt.Fprintf(roomView.message, sender)
ui.Render()
}
}