diff --git a/debug/debug.go b/debug/debug.go index c6f367e..3e32125 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -29,6 +29,8 @@ import ( ) var writer io.Writer +var RecoverPrettyPanic bool +var OnRecover func() func init() { 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 = ` __________ < Oh noes! > ‾‾‾\‾‾‾‾‾‾ diff --git a/gomuks.go b/gomuks.go index 494f182..ffdfad3 100644 --- a/gomuks.go +++ b/gomuks.go @@ -17,7 +17,6 @@ package main import ( - "fmt" "os" "path/filepath" "time" @@ -26,44 +25,36 @@ import ( "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix" - "maunium.net/go/gomuks/ui" - "maunium.net/go/tview" ) // Gomuks is the wrapper for everything. type Gomuks struct { - app *tview.Application - ui *ui.GomuksUI - matrix *matrix.Container - debugMode bool - config *config.Config - stop chan bool + ui ifc.GomuksUI + matrix *matrix.Container + config *config.Config + stop chan bool } // NewGomuks creates a new Gomuks instance with everything initialized, // but does not start it. -func NewGomuks(enableDebug bool) *Gomuks { +func NewGomuks(uiProvider ifc.UIProvider) *Gomuks { configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") gmx := &Gomuks{ - app: tview.NewApplication(), - stop: make(chan bool, 1), - debugMode: enableDebug, + stop: make(chan bool, 1), } gmx.config = config.NewConfig(configDir) - gmx.ui = ui.NewGomuksUI(gmx) + gmx.ui = uiProvider(gmx) gmx.matrix = matrix.NewContainer(gmx) + gmx.ui.Init() + + debug.OnRecover = gmx.ui.Finish gmx.config.Load() if len(gmx.config.UserID) > 0 { _ = gmx.config.LoadSession(gmx.config.UserID) } - _ = gmx.matrix.InitClient() - - main := gmx.ui.InitViews() - gmx.app.SetRoot(main, true) - return gmx } @@ -80,7 +71,7 @@ func (gmx *Gomuks) Save() { // StartAutosave calls Save() every minute until it receives a stop signal // on the Gomuks.stop channel. func (gmx *Gomuks) StartAutosave() { - defer gmx.Recover() + defer debug.Recover() ticker := time.NewTicker(time.Minute) for { select { @@ -100,36 +91,21 @@ func (gmx *Gomuks) Stop() { debug.Print("Disconnecting from Matrix...") gmx.matrix.Stop() debug.Print("Cleaning up UI...") - gmx.app.Stop() + gmx.ui.Stop() gmx.stop <- true gmx.Save() 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. // // If the tview app returns an error, it will be passed into panic(), which // will be recovered as specified in Recover(). func (gmx *Gomuks) Start() { - defer gmx.Recover() + _ = gmx.matrix.InitClient() + go gmx.StartAutosave() - if err := gmx.app.Run(); err != nil { + if err := gmx.ui.Start(); err != nil { panic(err) } } @@ -139,11 +115,6 @@ func (gmx *Gomuks) Matrix() ifc.MatrixContainer { return gmx.matrix } -// App returns the tview Application instance. -func (gmx *Gomuks) App() *tview.Application { - return gmx.app -} - // Config returns the Gomuks config instance. func (gmx *Gomuks) Config() *config.Config { return gmx.config @@ -153,13 +124,3 @@ func (gmx *Gomuks) Config() *config.Config { func (gmx *Gomuks) UI() ifc.GomuksUI { 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) -} diff --git a/interface/gomuks.go b/interface/gomuks.go index fbe05e1..90a5c4c 100644 --- a/interface/gomuks.go +++ b/interface/gomuks.go @@ -18,17 +18,14 @@ package ifc import ( "maunium.net/go/gomuks/config" - "maunium.net/go/tview" ) // Gomuks is the wrapper for everything. type Gomuks interface { Matrix() MatrixContainer - App() *tview.Application UI() GomuksUI Config() *config.Config Start() Stop() - Recover() } diff --git a/interface/ui.go b/interface/ui.go index 271dbd2..731e321 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -33,11 +33,18 @@ const ( ViewMain View = "main" ) +type UIProvider func(gmx Gomuks) GomuksUI + type GomuksUI interface { Render() SetView(name View) MainView() MainView LoginView() LoginView + + Init() + Start() error + Stop() + Finish() } type MainView interface { @@ -50,8 +57,7 @@ type MainView interface { SetTyping(roomID string, users []string) 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) } @@ -69,7 +75,7 @@ const ( type RoomView interface { MxRoom() *rooms.Room SaveHistory(dir string) error - LoadHistory(gmx Gomuks, dir string) (int, error) + LoadHistory(matrix MatrixContainer, dir string) (int, error) SetStatus(status string) SetTyping(users []string) diff --git a/main.go b/main.go new file mode 100644 index 0000000..f9955a1 --- /dev/null +++ b/main.go @@ -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 . + +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) +} diff --git a/matrix/matrix.go b/matrix/matrix.go index a2e3767..a38b439 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -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() func (c *Container) Start() { - defer c.gmx.Recover() + defer debug.Recover() c.ui.SetView(ifc.ViewMain) 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. func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) { - defer c.gmx.Recover() + defer debug.Recover() c.SendTyping(roomID, false) resp, err := c.client.SendMessageEvent(roomID, "m.room.message", 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) { - defer c.gmx.Recover() + defer debug.Recover() html := string(blackfriday.Run([]byte(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. func (c *Container) SendTyping(roomID string, typing bool) { - defer c.gmx.Recover() + defer debug.Recover() ts := time.Now().Unix() if c.typing > ts && typing { return diff --git a/ui/message-view.go b/ui/message-view.go index 4b67c6a..71d1cba 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -95,7 +95,7 @@ func (view *MessageView) SaveHistory(path string) error { 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) if err != nil { if os.IsNotExist(err) { @@ -119,7 +119,7 @@ func (view *MessageView) LoadHistory(gmx ifc.Gomuks, path string) (int, error) { if message != nil { view.messages[index-indexOffset] = message view.updateWidestSender(message.Sender()) - message.RegisterGomuks(gmx) + message.RegisterMatrix(matrix) } else { indexOffset++ } diff --git a/ui/messages/base.go b/ui/messages/base.go index 5322b34..cf698db 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -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. // diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index cd8c2fe..c9301c5 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -41,27 +41,26 @@ type ImageMessage struct { FileID string data []byte - gmx ifc.Gomuks + matrix ifc.MatrixContainer } // 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{ newBaseMessage(id, sender, displayname, msgtype, timestamp), homeserver, fileID, data, - gmx, + matrix, } } -func (msg *ImageMessage) RegisterGomuks(gmx ifc.Gomuks) { - msg.gmx = gmx +func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) { + msg.matrix = matrix - debug.Print(len(msg.data), msg.data) if len(msg.data) == 0 { go func() { - defer gmx.Recover() + defer debug.Recover() msg.updateData() }() } @@ -73,7 +72,7 @@ func (msg *ImageMessage) NotificationContent() string { func (msg *ImageMessage) updateData() { 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 { debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err) return @@ -82,7 +81,7 @@ func (msg *ImageMessage) updateData() { } 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 @@ -108,4 +107,3 @@ func (msg *ImageMessage) CalculateBuffer(width int) { func (msg *ImageMessage) RecalculateBuffer() { msg.CalculateBuffer(msg.prevBufferWidth) } - diff --git a/ui/messages/message.go b/ui/messages/message.go index 0d5c9e4..d3f2db4 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -32,7 +32,7 @@ type UIMessage interface { SenderID() string RealSender() string - RegisterGomuks(gmx ifc.Gomuks) + RegisterMatrix(matrix ifc.MatrixContainer) } const DateFormat = "January _2, 2006" diff --git a/ui/messages/parser/parser.go b/ui/messages/parser/parser.go index fbb2bf5..9c833f5 100644 --- a/ui/messages/parser/parser.go +++ b/ui/messages/parser/parser.go @@ -30,10 +30,10 @@ import ( "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 { case "m.room.message": - return ParseMessage(gmx, room, evt) + return ParseMessage(matrix, room, evt) case "m.room.member": return ParseMembershipEvent(room, evt) } @@ -48,7 +48,7 @@ func unixToTime(unix int64) time.Time { 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 member := room.GetMember(evt.Sender) if member != nil { @@ -68,11 +68,11 @@ func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) message } case "m.image": url, _ := evt.Content["url"].(string) - data, hs, id, err := gmx.Matrix().Download(url) + data, hs, id, err := matrix.Download(url) if err != nil { 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 } diff --git a/ui/room-view.go b/ui/room-view.go index d1e5a16..5268682 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -81,8 +81,8 @@ func (view *RoomView) SaveHistory(dir string) error { return view.MessageView().SaveHistory(view.logPath(dir)) } -func (view *RoomView) LoadHistory(gmx ifc.Gomuks, dir string) (int, error) { - return view.MessageView().LoadHistory(gmx, view.logPath(dir)) +func (view *RoomView) LoadHistory(matrix ifc.MatrixContainer, dir string) (int, error) { + return view.MessageView().LoadHistory(matrix, view.logPath(dir)) } func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView { diff --git a/ui/ui.go b/ui/ui.go index b5f847d..75fd8bf 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -36,14 +36,32 @@ func init() { tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen } -func NewGomuksUI(gmx ifc.Gomuks) (ui *GomuksUI) { - ui = &GomuksUI{ +func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI { + ui := &GomuksUI{ gmx: gmx, - app: gmx.App(), + app: tview.NewApplication(), views: tview.NewPages(), } 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() { diff --git a/ui/view-main.go b/ui/view-main.go index f54654a..c3e94a5 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -143,7 +143,7 @@ func (view *MainView) SendMessage(roomView *RoomView, 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) if err != nil { 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) { - defer view.gmx.Recover() + defer debug.Recover() debug.Print("Handling command", command, args) switch command { case "/me": @@ -286,7 +286,7 @@ func (view *MainView) SwitchRoom(roomIndex int) { roomView.Room.MarkRead() } view.roomList.SetSelected(roomView.Room) - view.gmx.App().SetFocus(view) + view.parent.app.SetFocus(view) view.parent.Render() } @@ -321,7 +321,7 @@ func (view *MainView) addRoom(index int, room string) { view.roomView.AddPage(room, roomView, true, false) roomView.UpdateUserList() - count, err := roomView.LoadHistory(view.gmx, view.config.HistoryDir) + count, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir) if err != nil { debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err) } 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) { - defer view.gmx.Recover() + defer debug.Recover() roomView := view.rooms[room] 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 { - return parser.ParseEvent(view.gmx, roomView.MxRoom(), evt) + return parser.ParseEvent(view.matrix, roomView.MxRoom(), evt) } diff --git a/ui/widget/color.go b/ui/widget/color.go index c46377a..e2c2b68 100644 --- a/ui/widget/color.go +++ b/ui/widget/color.go @@ -21,7 +21,6 @@ import ( "hash/fnv" "sort" - "maunium.net/go/gomuks/debug" "maunium.net/go/tcell" ) @@ -52,7 +51,6 @@ func init() { // <-- = red // --- = yellow func GetHashColorName(s string) string { - debug.Print("Getting color for", s) switch s { case "-->": return "green"