diff --git a/config/config.go b/config/config.go index 7d99274..5c1d87c 100644 --- a/config/config.go +++ b/config/config.go @@ -30,19 +30,29 @@ type Config struct { UserID string `yaml:"mxid"` HS string `yaml:"homeserver"` - dir string `yaml:"-"` - Session *Session `yaml:"-"` + Dir string `yaml:"-"` + HistoryDir string `yaml:"history_dir"` + Session *Session `yaml:"-"` } func NewConfig(dir string) *Config { return &Config{ - dir: dir, + Dir: dir, + HistoryDir: filepath.Join(dir, "history"), } } +func (config *Config) Clear() { + if config.Session != nil { + config.Session.Clear() + } + os.RemoveAll(config.HistoryDir) +} + func (config *Config) Load() { - os.MkdirAll(config.dir, 0700) - configPath := filepath.Join(config.dir, "config.yaml") + os.MkdirAll(config.Dir, 0700) + os.MkdirAll(config.HistoryDir, 0700) + configPath := filepath.Join(config.Dir, "config.yaml") data, err := ioutil.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { @@ -61,14 +71,14 @@ func (config *Config) Load() { } func (config *Config) Save() { - os.MkdirAll(config.dir, 0700) + os.MkdirAll(config.Dir, 0700) data, err := yaml.Marshal(&config) if err != nil { debug.Print("Failed to marshal config") panic(err) } - path := filepath.Join(config.dir, "config.yaml") + path := filepath.Join(config.Dir, "config.yaml") err = ioutil.WriteFile(path, data, 0600) if err != nil { debug.Print("Failed to write config to", path) diff --git a/config/session.go b/config/session.go index 3fdc169..2d4b885 100644 --- a/config/session.go +++ b/config/session.go @@ -23,7 +23,7 @@ import ( "maunium.net/go/gomatrix" "maunium.net/go/gomuks/matrix/pushrules" - rooms "maunium.net/go/gomuks/matrix/room" + "maunium.net/go/gomuks/matrix/room" "maunium.net/go/gomuks/ui/debug" ) @@ -45,7 +45,7 @@ func (config *Config) LoadSession(mxid string) error { func (config *Config) NewSession(mxid string) *Session { return &Session{ UserID: mxid, - path: filepath.Join(config.dir, mxid+".session"), + path: filepath.Join(config.Dir, mxid+".session"), Rooms: make(map[string]*rooms.Room), } } @@ -61,13 +61,13 @@ func (s *Session) Clear() { func (s *Session) Load() error { data, err := ioutil.ReadFile(s.path) if err != nil { - debug.Print("Failed to read session from", s.path, err) + debug.Printf("Failed to read session from %s: %v", s.path, err) return err } err = json.Unmarshal(data, s) if err != nil { - debug.Print("Failed to parse session at", s.path, err) + debug.Printf("Failed to parse session at %s: %v", s.path, err) return err } return nil @@ -76,13 +76,13 @@ func (s *Session) Load() error { func (s *Session) Save() error { data, err := json.Marshal(s) if err != nil { - debug.Print("Failed to marshal session of", s.UserID, err) + debug.Printf("Failed to marshal session of %s: %v", s.UserID, err) return err } err = ioutil.WriteFile(s.path, data, 0600) if err != nil { - debug.Print("Failed to write session to", s.path, err) + debug.Printf("Failed to write session of %s to %s: %v", s.UserID, s.path, err) return err } return nil diff --git a/gomuks.go b/gomuks.go index 6429f24..2dbbdff 100644 --- a/gomuks.go +++ b/gomuks.go @@ -38,12 +38,14 @@ type Gomuks struct { debug *debug.Pane debugMode bool config *config.Config + stop chan bool } func NewGomuks(enableDebug bool) *Gomuks { configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") gmx := &Gomuks{ - app: tview.NewApplication(), + app: tview.NewApplication(), + stop: make(chan bool, 1), } gmx.debug = debug.NewPane() @@ -79,11 +81,33 @@ func (gmx *Gomuks) Stop() { gmx.matrix.Stop() gmx.debug.Print("Cleaning up UI...") gmx.app.Stop() + gmx.stop <- true + gmx.Save() + os.Exit(0) +} + +func (gmx *Gomuks) Save() { if gmx.config.Session != nil { gmx.debug.Print("Saving session...") gmx.config.Session.Save() } - os.Exit(0) + gmx.debug.Print("Saving history...") + gmx.ui.MainView().SaveAllHistory() +} + +func (gmx *Gomuks) StartAutosave() { + defer gmx.Recover() + ticker := time.NewTicker(time.Minute) + for { + select { + case <-ticker.C: + gmx.Save() + case val := <-gmx.stop: + if val { + return + } + } + } } func (gmx *Gomuks) Recover() { @@ -101,6 +125,7 @@ func (gmx *Gomuks) Recover() { func (gmx *Gomuks) Start() { defer gmx.Recover() + go gmx.StartAutosave() if err := gmx.app.Run(); err != nil { panic(err) } diff --git a/interface/ui.go b/interface/ui.go index 36a733b..bded310 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -44,6 +44,7 @@ type MainView interface { AddRoom(roomID string) RemoveRoom(roomID string) SetRooms(roomIDs []string) + SaveAllHistory() SetTyping(roomID string, users []string) AddServiceMessage(roomID string, message string) diff --git a/ui/types/message.go b/ui/types/message.go index 6775597..e6ded4a 100644 --- a/ui/types/message.go +++ b/ui/types/message.go @@ -28,7 +28,7 @@ type Message struct { BasicMeta ID string Text string - Buffer []string + buffer []string prevBufferWidth int } @@ -64,12 +64,12 @@ func (message *Message) CalculateBuffer(width int) { if width < 2 { return } - message.Buffer = []string{} + 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, "") + message.buffer = append(message.buffer, "") newlines++ } else { newlines = 0 @@ -87,7 +87,7 @@ func (message *Message) CalculateBuffer(width int) { extract = extract[:matches[len(matches)-1][1]] } } - message.Buffer = append(message.Buffer, extract) + message.buffer = append(message.buffer, extract) str = str[len(extract):] } } @@ -97,3 +97,11 @@ func (message *Message) CalculateBuffer(width int) { func (message *Message) RecalculateBuffer() { message.CalculateBuffer(message.prevBufferWidth) } + +func (message *Message) Buffer() []string { + return message.buffer +} + +func (message *Message) Height() int { + return len(message.buffer) +} diff --git a/ui/view-main.go b/ui/view-main.go index 0c3b0a4..36ffe28 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -162,7 +162,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) { case "/quit": view.gmx.Stop() case "/clearcache": - view.config.Session.Clear() + view.config.Clear() view.gmx.Stop() case "/panic": panic("This is a test panic.") @@ -239,6 +239,15 @@ func (view *MainView) Focus(delegate func(p tview.Primitive)) { } } +func (view *MainView) SaveAllHistory() { + for _, room := range view.rooms { + err := room.SaveHistory(view.config.HistoryDir) + if err != nil { + debug.Printf("Failed to save history of %s: %v", room.Room.GetTitle(), err) + } + } +} + func (view *MainView) addRoom(index int, room string) { roomStore := view.matrix.GetRoom(room) @@ -254,7 +263,13 @@ func (view *MainView) addRoom(index int, room string) { view.rooms[room] = roomView view.roomView.AddPage(room, roomView, true, false) roomView.UpdateUserList() - go view.LoadInitialHistory(room) + + count, err := roomView.LoadHistory(view.config.HistoryDir) + if err != nil { + debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err) + } else if count <= 0 { + go view.LoadInitialHistory(room) + } } } @@ -377,6 +392,11 @@ func (view *MainView) LoadHistory(room string, initial bool) { room.AddMessage(message, widget.PrependMessage) } } + err = roomView.SaveHistory(view.config.HistoryDir) + if err != nil { + debug.Printf("%Failed to save history of %s: %v", roomView.Room.GetTitle(), err) + } + view.config.Session.Save() view.parent.Render() } diff --git a/ui/widget/message-view.go b/ui/widget/message-view.go index f263350..fe906b5 100644 --- a/ui/widget/message-view.go +++ b/ui/widget/message-view.go @@ -17,7 +17,9 @@ package widget import ( + "encoding/gob" "fmt" + "os" "time" "github.com/gdamore/tcell" @@ -79,6 +81,45 @@ func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time GetHashColor(sender)) } +func (view *MessageView) SaveHistory(path string) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + defer file.Close() + + enc := gob.NewEncoder(file) + err = enc.Encode(view.messages) + if err != nil { + return err + } + + return nil +} + +func (view *MessageView) LoadHistory(path string) (int, error) { + file, err := os.OpenFile(path, os.O_RDONLY, 0600) + if err != nil { + if os.IsNotExist(err) { + return 0, nil + } + return -1, err + } + defer file.Close() + + dec := gob.NewDecoder(file) + err = dec.Decode(&view.messages) + if err != nil { + return -1, err + } + + for _, message := range view.messages { + view.updateWidestSender(message.Sender) + } + + return len(view.messages), nil +} + func (view *MessageView) updateWidestSender(sender string) { if len(sender) > view.widestSender { view.widestSender = len(sender) @@ -103,8 +144,12 @@ func (view *MessageView) UpdateMessageID(message *types.Message, newID string) { } func (view *MessageView) AddMessage(message *types.Message, direction MessageDirection) { + if message == nil { + return + } + msg, messageExists := view.messageIDs[message.ID] - if messageExists { + if msg != nil && messageExists { message.CopyTo(msg) direction = IgnoreMessage } @@ -117,7 +162,7 @@ func (view *MessageView) AddMessage(message *types.Message, direction MessageDir if direction == AppendMessage { if view.ScrollOffset > 0 { - view.ScrollOffset += len(message.Buffer) + view.ScrollOffset += message.Height() } view.messages = append(view.messages, message) view.appendBuffer(message) @@ -137,8 +182,8 @@ func (view *MessageView) appendBuffer(message *types.Message) { } } - view.textBuffer = append(view.textBuffer, message.Buffer...) - for range message.Buffer { + view.textBuffer = append(view.textBuffer, message.Buffer()...) + for range message.Buffer() { view.metaBuffer = append(view.metaBuffer, message) } view.prevMsgCount++ diff --git a/ui/widget/room-view.go b/ui/widget/room-view.go index 433d5dd..141e993 100644 --- a/ui/widget/room-view.go +++ b/ui/widget/room-view.go @@ -18,6 +18,7 @@ package widget import ( "fmt" + "path/filepath" "strings" "time" @@ -67,6 +68,18 @@ func NewRoomView(room *rooms.Room) *RoomView { return view } +func (view *RoomView) logPath(dir string) string { + return filepath.Join(dir, fmt.Sprintf("%s.gmxlog", view.Room.ID)) +} + +func (view *RoomView) SaveHistory(dir string) error { + return view.MessageView().SaveHistory(view.logPath(dir)) +} + +func (view *RoomView) LoadHistory(dir string) (int, error) { + return view.MessageView().LoadHistory(view.logPath(dir)) +} + func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView { view.input.SetTabCompleteFunc(func(text string, cursorOffset int) string { return fn(view, text, cursorOffset)