Disconnect UI from main Gomuks struct. Fixes #21

This commit is contained in:
Tulir Asokan 2018-04-18 18:35:24 +03:00
parent 3b2d5fa034
commit b17ff318c2
15 changed files with 137 additions and 97 deletions

View File

@ -29,6 +29,8 @@ import (
) )
var writer io.Writer var writer io.Writer
var RecoverPrettyPanic bool
var OnRecover func()
func init() { func init() {
var err error var err error
@ -59,6 +61,22 @@ func PrintStack() {
} }
} }
// Recover recovers a panic, runs the OnRecover handler and either re-panics or
// shows an user-friendly message about the panic depending on whether or not
// the pretty panic mode is enabled.
func Recover() {
if p := recover(); p != nil {
if OnRecover != nil {
OnRecover()
}
if RecoverPrettyPanic {
PrettyPanic(p)
} else {
panic(p)
}
}
}
const Oops = ` __________ const Oops = ` __________
< Oh noes! > < Oh noes! >
\ \

View File

@ -17,7 +17,6 @@
package main package main
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -26,44 +25,36 @@ import (
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix" "maunium.net/go/gomuks/matrix"
"maunium.net/go/gomuks/ui"
"maunium.net/go/tview"
) )
// Gomuks is the wrapper for everything. // Gomuks is the wrapper for everything.
type Gomuks struct { type Gomuks struct {
app *tview.Application ui ifc.GomuksUI
ui *ui.GomuksUI
matrix *matrix.Container matrix *matrix.Container
debugMode bool
config *config.Config config *config.Config
stop chan bool stop chan bool
} }
// NewGomuks creates a new Gomuks instance with everything initialized, // NewGomuks creates a new Gomuks instance with everything initialized,
// but does not start it. // but does not start it.
func NewGomuks(enableDebug bool) *Gomuks { func NewGomuks(uiProvider ifc.UIProvider) *Gomuks {
configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks")
gmx := &Gomuks{ gmx := &Gomuks{
app: tview.NewApplication(),
stop: make(chan bool, 1), stop: make(chan bool, 1),
debugMode: enableDebug,
} }
gmx.config = config.NewConfig(configDir) gmx.config = config.NewConfig(configDir)
gmx.ui = ui.NewGomuksUI(gmx) gmx.ui = uiProvider(gmx)
gmx.matrix = matrix.NewContainer(gmx) gmx.matrix = matrix.NewContainer(gmx)
gmx.ui.Init()
debug.OnRecover = gmx.ui.Finish
gmx.config.Load() gmx.config.Load()
if len(gmx.config.UserID) > 0 { if len(gmx.config.UserID) > 0 {
_ = gmx.config.LoadSession(gmx.config.UserID) _ = gmx.config.LoadSession(gmx.config.UserID)
} }
_ = gmx.matrix.InitClient()
main := gmx.ui.InitViews()
gmx.app.SetRoot(main, true)
return gmx return gmx
} }
@ -80,7 +71,7 @@ func (gmx *Gomuks) Save() {
// StartAutosave calls Save() every minute until it receives a stop signal // StartAutosave calls Save() every minute until it receives a stop signal
// on the Gomuks.stop channel. // on the Gomuks.stop channel.
func (gmx *Gomuks) StartAutosave() { func (gmx *Gomuks) StartAutosave() {
defer gmx.Recover() defer debug.Recover()
ticker := time.NewTicker(time.Minute) ticker := time.NewTicker(time.Minute)
for { for {
select { select {
@ -100,36 +91,21 @@ func (gmx *Gomuks) Stop() {
debug.Print("Disconnecting from Matrix...") debug.Print("Disconnecting from Matrix...")
gmx.matrix.Stop() gmx.matrix.Stop()
debug.Print("Cleaning up UI...") debug.Print("Cleaning up UI...")
gmx.app.Stop() gmx.ui.Stop()
gmx.stop <- true gmx.stop <- true
gmx.Save() gmx.Save()
os.Exit(0) os.Exit(0)
} }
// Recover recovers a panic, closes the tcell screen and either re-panics or
// shows an user-friendly message about the panic depending on whether or not
// the debug mode is enabled.
func (gmx *Gomuks) Recover() {
if p := recover(); p != nil {
if gmx.App().GetScreen() != nil {
gmx.App().GetScreen().Fini()
}
if gmx.debugMode {
panic(p)
} else {
debug.PrettyPanic(p)
}
}
}
// Start opens a goroutine for the autosave loop and starts the tview app. // Start opens a goroutine for the autosave loop and starts the tview app.
// //
// If the tview app returns an error, it will be passed into panic(), which // If the tview app returns an error, it will be passed into panic(), which
// will be recovered as specified in Recover(). // will be recovered as specified in Recover().
func (gmx *Gomuks) Start() { func (gmx *Gomuks) Start() {
defer gmx.Recover() _ = gmx.matrix.InitClient()
go gmx.StartAutosave() go gmx.StartAutosave()
if err := gmx.app.Run(); err != nil { if err := gmx.ui.Start(); err != nil {
panic(err) panic(err)
} }
} }
@ -139,11 +115,6 @@ func (gmx *Gomuks) Matrix() ifc.MatrixContainer {
return gmx.matrix return gmx.matrix
} }
// App returns the tview Application instance.
func (gmx *Gomuks) App() *tview.Application {
return gmx.app
}
// Config returns the Gomuks config instance. // Config returns the Gomuks config instance.
func (gmx *Gomuks) Config() *config.Config { func (gmx *Gomuks) Config() *config.Config {
return gmx.config return gmx.config
@ -153,13 +124,3 @@ func (gmx *Gomuks) Config() *config.Config {
func (gmx *Gomuks) UI() ifc.GomuksUI { func (gmx *Gomuks) UI() ifc.GomuksUI {
return gmx.ui return gmx.ui
} }
func main() {
enableDebug := len(os.Getenv("DEBUG")) > 0
NewGomuks(enableDebug).Start()
// We use os.Exit() everywhere, so exiting by returning from Start() shouldn't happen.
time.Sleep(5 * time.Second)
fmt.Println("Unexpected exit by return from Gomuks#Start().")
os.Exit(2)
}

View File

@ -18,17 +18,14 @@ package ifc
import ( import (
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/tview"
) )
// Gomuks is the wrapper for everything. // Gomuks is the wrapper for everything.
type Gomuks interface { type Gomuks interface {
Matrix() MatrixContainer Matrix() MatrixContainer
App() *tview.Application
UI() GomuksUI UI() GomuksUI
Config() *config.Config Config() *config.Config
Start() Start()
Stop() Stop()
Recover()
} }

View File

@ -33,11 +33,18 @@ const (
ViewMain View = "main" ViewMain View = "main"
) )
type UIProvider func(gmx Gomuks) GomuksUI
type GomuksUI interface { type GomuksUI interface {
Render() Render()
SetView(name View) SetView(name View)
MainView() MainView MainView() MainView
LoginView() LoginView LoginView() LoginView
Init()
Start() error
Stop()
Finish()
} }
type MainView interface { type MainView interface {
@ -50,8 +57,7 @@ type MainView interface {
SetTyping(roomID string, users []string) SetTyping(roomID string, users []string)
ParseEvent(roomView RoomView, evt *gomatrix.Event) Message ParseEvent(roomView RoomView, evt *gomatrix.Event) Message
//ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message
//ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message
NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
} }
@ -69,7 +75,7 @@ const (
type RoomView interface { type RoomView interface {
MxRoom() *rooms.Room MxRoom() *rooms.Room
SaveHistory(dir string) error SaveHistory(dir string) error
LoadHistory(gmx Gomuks, dir string) (int, error) LoadHistory(matrix MatrixContainer, dir string) (int, error)
SetStatus(status string) SetStatus(status string)
SetTyping(users []string) SetTyping(users []string)

44
main.go Normal file
View File

@ -0,0 +1,44 @@
// 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"
"os"
"time"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui"
)
var MainUIProvider ifc.UIProvider = ui.NewGomuksUI
func main() {
defer debug.Recover()
enableDebug := len(os.Getenv("DEBUG")) > 0
debug.RecoverPrettyPanic = !enableDebug
gmx := NewGomuks(MainUIProvider)
gmx.Start()
// We use os.Exit() everywhere, so exiting by returning from Start() shouldn't happen.
time.Sleep(5 * time.Second)
fmt.Println("Unexpected exit by return from gmx.Start().")
os.Exit(2)
}

View File

@ -196,7 +196,7 @@ func (c *Container) OnLogin() {
// Start moves the UI to the main view, calls OnLogin() and runs the syncer forever until stopped with Stop() // Start moves the UI to the main view, calls OnLogin() and runs the syncer forever until stopped with Stop()
func (c *Container) Start() { func (c *Container) Start() {
defer c.gmx.Recover() defer debug.Recover()
c.ui.SetView(ifc.ViewMain) c.ui.SetView(ifc.ViewMain)
c.OnLogin() c.OnLogin()
@ -317,7 +317,7 @@ func (c *Container) HandleTyping(evt *gomatrix.Event) {
// SendMessage sends a message with the given text to the given room. // SendMessage sends a message with the given text to the given room.
func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) { func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) {
defer c.gmx.Recover() defer debug.Recover()
c.SendTyping(roomID, false) c.SendTyping(roomID, false)
resp, err := c.client.SendMessageEvent(roomID, "m.room.message", resp, err := c.client.SendMessageEvent(roomID, "m.room.message",
gomatrix.TextMessage{MsgType: msgtype, Body: text}) gomatrix.TextMessage{MsgType: msgtype, Body: text})
@ -328,7 +328,7 @@ func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) {
} }
func (c *Container) SendMarkdownMessage(roomID, msgtype, text string) (string, error) { func (c *Container) SendMarkdownMessage(roomID, msgtype, text string) (string, error) {
defer c.gmx.Recover() defer debug.Recover()
html := string(blackfriday.Run([]byte(text))) html := string(blackfriday.Run([]byte(text)))
if html == text { if html == text {
@ -351,7 +351,7 @@ func (c *Container) SendMarkdownMessage(roomID, msgtype, text string) (string, e
// SendTyping sets whether or not the user is typing in the given room. // SendTyping sets whether or not the user is typing in the given room.
func (c *Container) SendTyping(roomID string, typing bool) { func (c *Container) SendTyping(roomID string, typing bool) {
defer c.gmx.Recover() defer debug.Recover()
ts := time.Now().Unix() ts := time.Now().Unix()
if c.typing > ts && typing { if c.typing > ts && typing {
return return

View File

@ -95,7 +95,7 @@ func (view *MessageView) SaveHistory(path string) error {
return nil return nil
} }
func (view *MessageView) LoadHistory(gmx ifc.Gomuks, path string) (int, error) { func (view *MessageView) LoadHistory(matrix ifc.MatrixContainer, path string) (int, error) {
file, err := os.OpenFile(path, os.O_RDONLY, 0600) file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -119,7 +119,7 @@ func (view *MessageView) LoadHistory(gmx ifc.Gomuks, path string) (int, error) {
if message != nil { if message != nil {
view.messages[index-indexOffset] = message view.messages[index-indexOffset] = message
view.updateWidestSender(message.Sender()) view.updateWidestSender(message.Sender())
message.RegisterGomuks(gmx) message.RegisterMatrix(matrix)
} else { } else {
indexOffset++ indexOffset++
} }

View File

@ -59,7 +59,7 @@ func newBaseMessage(id, sender, displayname, msgtype string, timestamp time.Time
} }
} }
func (msg *BaseMessage) RegisterGomuks(gmx ifc.Gomuks) {} func (msg *BaseMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
// Sender gets the string that should be displayed as the sender of this message. // Sender gets the string that should be displayed as the sender of this message.
// //

View File

@ -41,27 +41,26 @@ type ImageMessage struct {
FileID string FileID string
data []byte data []byte
gmx ifc.Gomuks matrix ifc.MatrixContainer
} }
// NewImageMessage creates a new ImageMessage object with the provided values and the default state. // NewImageMessage creates a new ImageMessage object with the provided values and the default state.
func NewImageMessage(gmx ifc.Gomuks, id, sender, displayname, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { func NewImageMessage(matrix ifc.MatrixContainer, id, sender, displayname, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage {
return &ImageMessage{ return &ImageMessage{
newBaseMessage(id, sender, displayname, msgtype, timestamp), newBaseMessage(id, sender, displayname, msgtype, timestamp),
homeserver, homeserver,
fileID, fileID,
data, data,
gmx, matrix,
} }
} }
func (msg *ImageMessage) RegisterGomuks(gmx ifc.Gomuks) { func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) {
msg.gmx = gmx msg.matrix = matrix
debug.Print(len(msg.data), msg.data)
if len(msg.data) == 0 { if len(msg.data) == 0 {
go func() { go func() {
defer gmx.Recover() defer debug.Recover()
msg.updateData() msg.updateData()
}() }()
} }
@ -73,7 +72,7 @@ func (msg *ImageMessage) NotificationContent() string {
func (msg *ImageMessage) updateData() { func (msg *ImageMessage) updateData() {
debug.Print("Loading image:", msg.Homeserver, msg.FileID) debug.Print("Loading image:", msg.Homeserver, msg.FileID)
data, _, _, err := msg.gmx.Matrix().Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID)) data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID))
if err != nil { if err != nil {
debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err) debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
return return
@ -82,7 +81,7 @@ func (msg *ImageMessage) updateData() {
} }
func (msg *ImageMessage) Path() string { func (msg *ImageMessage) Path() string {
return msg.gmx.Matrix().GetCachePath(msg.Homeserver, msg.FileID) return msg.matrix.GetCachePath(msg.Homeserver, msg.FileID)
} }
// CalculateBuffer generates the internal buffer for this message that consists // CalculateBuffer generates the internal buffer for this message that consists
@ -108,4 +107,3 @@ func (msg *ImageMessage) CalculateBuffer(width int) {
func (msg *ImageMessage) RecalculateBuffer() { func (msg *ImageMessage) RecalculateBuffer() {
msg.CalculateBuffer(msg.prevBufferWidth) msg.CalculateBuffer(msg.prevBufferWidth)
} }

View File

@ -32,7 +32,7 @@ type UIMessage interface {
SenderID() string SenderID() string
RealSender() string RealSender() string
RegisterGomuks(gmx ifc.Gomuks) RegisterMatrix(matrix ifc.MatrixContainer)
} }
const DateFormat = "January _2, 2006" const DateFormat = "January _2, 2006"

View File

@ -30,10 +30,10 @@ import (
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
switch evt.Type { switch evt.Type {
case "m.room.message": case "m.room.message":
return ParseMessage(gmx, room, evt) return ParseMessage(matrix, room, evt)
case "m.room.member": case "m.room.member":
return ParseMembershipEvent(room, evt) return ParseMembershipEvent(room, evt)
} }
@ -48,7 +48,7 @@ func unixToTime(unix int64) time.Time {
return timestamp return timestamp
} }
func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
displayname := evt.Sender displayname := evt.Sender
member := room.GetMember(evt.Sender) member := room.GetMember(evt.Sender)
if member != nil { if member != nil {
@ -68,11 +68,11 @@ func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) message
} }
case "m.image": case "m.image":
url, _ := evt.Content["url"].(string) url, _ := evt.Content["url"].(string)
data, hs, id, err := gmx.Matrix().Download(url) data, hs, id, err := matrix.Download(url)
if err != nil { if err != nil {
debug.Printf("Failed to download %s: %v", url, err) debug.Printf("Failed to download %s: %v", url, err)
} }
return messages.NewImageMessage(gmx, evt.ID, evt.Sender, displayname, msgtype, hs, id, data, ts) return messages.NewImageMessage(matrix, evt.ID, evt.Sender, displayname, msgtype, hs, id, data, ts)
} }
return nil return nil
} }

View File

@ -81,8 +81,8 @@ func (view *RoomView) SaveHistory(dir string) error {
return view.MessageView().SaveHistory(view.logPath(dir)) return view.MessageView().SaveHistory(view.logPath(dir))
} }
func (view *RoomView) LoadHistory(gmx ifc.Gomuks, dir string) (int, error) { func (view *RoomView) LoadHistory(matrix ifc.MatrixContainer, dir string) (int, error) {
return view.MessageView().LoadHistory(gmx, view.logPath(dir)) return view.MessageView().LoadHistory(matrix, view.logPath(dir))
} }
func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView { func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView {

View File

@ -36,14 +36,32 @@ func init() {
tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
} }
func NewGomuksUI(gmx ifc.Gomuks) (ui *GomuksUI) { func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI {
ui = &GomuksUI{ ui := &GomuksUI{
gmx: gmx, gmx: gmx,
app: gmx.App(), app: tview.NewApplication(),
views: tview.NewPages(), views: tview.NewPages(),
} }
ui.views.SetChangedFunc(ui.Render) ui.views.SetChangedFunc(ui.Render)
return return ui
}
func (ui *GomuksUI) Init() {
ui.app.SetRoot(ui.InitViews(), true)
}
func (ui *GomuksUI) Start() error {
return ui.app.Run()
}
func (ui *GomuksUI) Stop() {
ui.app.Stop()
}
func (ui *GomuksUI) Finish() {
if ui.app.GetScreen() != nil {
ui.app.GetScreen().Fini()
}
} }
func (ui *GomuksUI) Render() { func (ui *GomuksUI) Render() {

View File

@ -143,7 +143,7 @@ func (view *MainView) SendMessage(roomView *RoomView, text string) {
} }
func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Message, text string) { func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Message, text string) {
defer view.gmx.Recover() defer debug.Recover()
eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text) eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text)
if err != nil { if err != nil {
tempMessage.SetState(ifc.MessageStateFailed) tempMessage.SetState(ifc.MessageStateFailed)
@ -154,7 +154,7 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag
} }
func (view *MainView) HandleCommand(roomView *RoomView, command string, args []string) { func (view *MainView) HandleCommand(roomView *RoomView, command string, args []string) {
defer view.gmx.Recover() defer debug.Recover()
debug.Print("Handling command", command, args) debug.Print("Handling command", command, args)
switch command { switch command {
case "/me": case "/me":
@ -286,7 +286,7 @@ func (view *MainView) SwitchRoom(roomIndex int) {
roomView.Room.MarkRead() roomView.Room.MarkRead()
} }
view.roomList.SetSelected(roomView.Room) view.roomList.SetSelected(roomView.Room)
view.gmx.App().SetFocus(view) view.parent.app.SetFocus(view)
view.parent.Render() view.parent.Render()
} }
@ -321,7 +321,7 @@ func (view *MainView) addRoom(index int, room string) {
view.roomView.AddPage(room, roomView, true, false) view.roomView.AddPage(room, roomView, true, false)
roomView.UpdateUserList() roomView.UpdateUserList()
count, err := roomView.LoadHistory(view.gmx, view.config.HistoryDir) count, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir)
if err != nil { if err != nil {
debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err) debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err)
} else if count <= 0 { } else if count <= 0 {
@ -424,7 +424,7 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul
} }
func (view *MainView) LoadHistory(room string, initial bool) { func (view *MainView) LoadHistory(room string, initial bool) {
defer view.gmx.Recover() defer debug.Recover()
roomView := view.rooms[room] roomView := view.rooms[room]
batch := roomView.Room.PrevBatch batch := roomView.Room.PrevBatch
@ -472,5 +472,5 @@ func (view *MainView) LoadHistory(room string, initial bool) {
} }
func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message {
return parser.ParseEvent(view.gmx, roomView.MxRoom(), evt) return parser.ParseEvent(view.matrix, roomView.MxRoom(), evt)
} }

View File

@ -21,7 +21,6 @@ import (
"hash/fnv" "hash/fnv"
"sort" "sort"
"maunium.net/go/gomuks/debug"
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
@ -52,7 +51,6 @@ func init() {
// <-- = red // <-- = red
// --- = yellow // --- = yellow
func GetHashColorName(s string) string { func GetHashColorName(s string) string {
debug.Print("Getting color for", s)
switch s { switch s {
case "-->": case "-->":
return "green" return "green"