diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 186383d..ee71f45 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -5,45 +5,34 @@ on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ go-version: ["1.22", "1.23"]
+ name: Lint and test ${{ matrix.go-version == '1.23' && '(latest)' || '(old)' }}
+
steps:
- uses: actions/checkout@v4
- name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: "1.22"
-
- - name: Install goimports
- run: |
- go install golang.org/x/tools/cmd/goimports@latest
- export PATH="$HOME/go/bin:$PATH"
-
- - name: Install pre-commit
- run: pip install pre-commit
-
- - name: Lint
- run: pre-commit run -a
-
- build:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- go-version: ["1.21", "1.22"]
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
+ cache: true
- - name: Install libolm
- run: sudo apt-get install libolm-dev libolm3
+ - name: Install dependencies
+ run: |
+ sudo apt-get install libolm-dev libolm3
+ go install golang.org/x/tools/cmd/goimports@latest
+ go install honnef.co/go/tools/cmd/staticcheck@latest
+ pip install pre-commit
+ export PATH="$HOME/go/bin:$PATH"
- name: Build
run: go build -v ./...
+ - name: Lint
+ run: pre-commit run -a
+
- name: Test
run: go test -v ./...
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7ec01c1..11d0782 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,5 @@
stages:
- build
-- package
default:
before_script:
@@ -14,7 +13,7 @@ cache:
.build-linux: &build-linux
stage: build
before_script:
- - export GO_LDFLAGS="-s -w -linkmode external -extldflags -static -X main.Tag=$CI_COMMIT_TAG -X main.Commit=$CI_COMMIT_SHA -X 'main.BuildTime=`date '+%b %_d %Y, %H:%M:%S'`'"
+ - export GO_LDFLAGS="-s -w -linkmode external -extldflags -static -X main.Tag=$CI_COMMIT_TAG -X main.Commit=$CI_COMMIT_SHA -X 'main.BuildTime=`date -Iseconds`'"
script:
- go build -ldflags "$GO_LDFLAGS" -o gomuks
artifacts:
@@ -24,10 +23,16 @@ cache:
linux/amd64:
<<: *build-linux
image: dock.mau.dev/tulir/gomuks-build-docker:linux-amd64
+ tags:
+ - linux
+ - amd64
linux/arm:
<<: *build-linux
image: dock.mau.dev/tulir/gomuks-build-docker:linux-arm
+ tags:
+ - linux
+ - amd64
linux/arm64:
<<: *build-linux
@@ -36,15 +41,6 @@ linux/arm64:
- linux
- arm64
-windows/amd64:
- image: dock.mau.dev/tulir/gomuks-build-docker:windows-amd64
- stage: build
- script:
- - go build -o gomuks.exe
- artifacts:
- paths:
- - gomuks.exe
-
macos/arm64:
stage: build
tags:
@@ -54,31 +50,15 @@ macos/arm64:
- export LIBRARY_PATH=/opt/homebrew/lib
- export CPATH=/opt/homebrew/include
- export PATH=/opt/homebrew/bin:$PATH
- - export GO_LDFLAGS="-X main.Tag=$CI_COMMIT_TAG -X main.Commit=$CI_COMMIT_SHA -X 'main.BuildTime=`date '+%b %_d %Y, %H:%M:%S'`'"
+ - export GO_LDFLAGS="-X main.Tag=$CI_COMMIT_TAG -X main.Commit=$CI_COMMIT_SHA -X 'main.BuildTime=`date '-Iseconds'`'"
script:
- - mkdir gomuks-macos-arm64
- - go build -ldflags "$GO_LDFLAGS" -o gomuks-macos-arm64/gomuks
- - install_name_tool -change /opt/homebrew/opt/libolm/lib/libolm.3.dylib @rpath/libolm.3.dylib gomuks-macos-arm64/gomuks
- - install_name_tool -add_rpath @executable_path gomuks-macos-arm64/gomuks
- - install_name_tool -add_rpath /opt/homebrew/opt/libolm/lib gomuks-macos-arm64/gomuks
- - install_name_tool -add_rpath /usr/local/opt/libolm/lib gomuks-macos-arm64/gomuks
- - cp /opt/homebrew/opt/libolm/lib/libolm.3.dylib gomuks-macos-arm64/
+ - go build -ldflags "$GO_LDFLAGS" -o gomuks
+ - install_name_tool -change /opt/homebrew/opt/libolm/lib/libolm.3.dylib @rpath/libolm.3.dylib gomuks
+ - install_name_tool -add_rpath @executable_path gomuks
+ - install_name_tool -add_rpath /opt/homebrew/opt/libolm/lib gomuks
+ - install_name_tool -add_rpath /usr/local/opt/libolm/lib gomuks
+ - cp /opt/homebrew/opt/libolm/lib/libolm.3.dylib .
artifacts:
paths:
- - gomuks-macos-arm64
-
-debian:
- image: debian
- stage: package
- dependencies:
- - linux/amd64
- only:
- - tags
- script:
- - mkdir -p deb/usr/bin
- - cp gomuks deb/usr/bin/gomuks
- - chmod -R -s deb/DEBIAN && chmod -R 0755 deb/DEBIAN
- - dpkg-deb --build deb gomuks.deb
- artifacts:
- paths:
- - gomuks.deb
+ - gomuks
+ - libolm.3.dylib
diff --git a/README.md b/README.md
index cbec5d3..a6ba8ba 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,9 @@
[](LICENSE)
[](https://github.com/tulir/gomuks/releases)
[](https://mau.dev/tulir/gomuks/pipelines)
-[](https://codeclimate.com/github/tulir/gomuks)
[](https://repology.org/project/gomuks/versions)
-
-
-A terminal Matrix client written in Go using [mautrix](https://github.com/tulir/mautrix-go) and [mauview](https://github.com/tulir/mauview).
-
-## Docs
-For installation and usage instructions, see [docs.mau.fi](https://docs.mau.fi/gomuks/).
+A Matrix client written in Go using [mautrix](https://github.com/tulir/mautrix-go).
## Discussion
Matrix room: [#gomuks:maunium.net](https://matrix.to/#/#gomuks:maunium.net)
diff --git a/build.sh b/build.sh
deleted file mode 100755
index 2409c5b..0000000
--- a/build.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-go build -ldflags "-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date '+%b %_d %Y, %H:%M:%S'`'" "$@"
diff --git a/chat-preview.png b/chat-preview.png
deleted file mode 100644
index 4f7c875..0000000
Binary files a/chat-preview.png and /dev/null differ
diff --git a/config/config.go b/config/config.go
deleted file mode 100644
index b3de936..0000000
--- a/config/config.go
+++ /dev/null
@@ -1,401 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package config
-
-import (
- _ "embed"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-
- "gopkg.in/yaml.v3"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/pushrules"
-
- "go.mau.fi/cbind"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/debug"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-type AuthCache struct {
- NextBatch string `yaml:"next_batch"`
- FilterID string `yaml:"filter_id"`
- FilterVersion int `yaml:"filter_version"`
- InitialSyncDone bool `yaml:"initial_sync_done"`
-}
-
-type UserPreferences struct {
- HideUserList bool `yaml:"hide_user_list"`
- HideRoomList bool `yaml:"hide_room_list"`
- HideTimestamp bool `yaml:"hide_timestamp"`
- BareMessageView bool `yaml:"bare_message_view"`
- DisableImages bool `yaml:"disable_images"`
- DisableTypingNotifs bool `yaml:"disable_typing_notifs"`
- DisableEmojis bool `yaml:"disable_emojis"`
- DisableMarkdown bool `yaml:"disable_markdown"`
- DisableHTML bool `yaml:"disable_html"`
- DisableDownloads bool `yaml:"disable_downloads"`
- DisableNotifications bool `yaml:"disable_notifications"`
- DisableShowURLs bool `yaml:"disable_show_urls"`
-
- InlineURLMode string `yaml:"inline_url_mode"`
-}
-
-var InlineURLsProbablySupported bool
-
-func init() {
- vteVersion, _ := strconv.Atoi(os.Getenv("VTE_VERSION"))
- term := os.Getenv("TERM")
- // Enable inline URLs by default on VTE 0.50.0+
- InlineURLsProbablySupported = vteVersion > 5000 ||
- os.Getenv("TERM_PROGRAM") == "iTerm.app" ||
- term == "foot" ||
- term == "xterm-kitty"
-}
-
-func (up *UserPreferences) EnableInlineURLs() bool {
- return up.InlineURLMode == "enable" || (InlineURLsProbablySupported && up.InlineURLMode != "disable")
-}
-
-type Keybind struct {
- Mod tcell.ModMask
- Key tcell.Key
- Ch rune
-}
-
-type ParsedKeybindings struct {
- Main map[Keybind]string
- Room map[Keybind]string
- Modal map[Keybind]string
- Visual map[Keybind]string
-}
-
-type RawKeybindings struct {
- Main map[string]string `yaml:"main,omitempty"`
- Room map[string]string `yaml:"room,omitempty"`
- Modal map[string]string `yaml:"modal,omitempty"`
- Visual map[string]string `yaml:"visual,omitempty"`
-}
-
-// Config contains the main config of gomuks.
-type Config struct {
- UserID id.UserID `yaml:"mxid"`
- DeviceID id.DeviceID `yaml:"device_id"`
- AccessToken string `yaml:"access_token"`
- HS string `yaml:"homeserver"`
-
- RoomCacheSize int `yaml:"room_cache_size"`
- RoomCacheAge int64 `yaml:"room_cache_age"`
-
- NotifySound bool `yaml:"notify_sound"`
- SendToVerifiedOnly bool `yaml:"send_to_verified_only"`
-
- Backspace1RemovesWord bool `yaml:"backspace1_removes_word"`
- Backspace2RemovesWord bool `yaml:"backspace2_removes_word"`
-
- AlwaysClearScreen bool `yaml:"always_clear_screen"`
-
- Dir string `yaml:"-"`
- DataDir string `yaml:"data_dir"`
- CacheDir string `yaml:"cache_dir"`
- HistoryPath string `yaml:"history_path"`
- RoomListPath string `yaml:"room_list_path"`
- MediaDir string `yaml:"media_dir"`
- DownloadDir string `yaml:"download_dir"`
- StateDir string `yaml:"state_dir"`
-
- Preferences UserPreferences `yaml:"-"`
- AuthCache AuthCache `yaml:"-"`
- Rooms *rooms.RoomCache `yaml:"-"`
- PushRules *pushrules.PushRuleset `yaml:"-"`
- Keybindings ParsedKeybindings `yaml:"-"`
-
- nosave bool
-}
-
-// NewConfig creates a config that loads data from the given directory.
-func NewConfig(configDir, dataDir, cacheDir, downloadDir string) *Config {
- return &Config{
- Dir: configDir,
- DataDir: dataDir,
- CacheDir: cacheDir,
- DownloadDir: downloadDir,
- HistoryPath: filepath.Join(cacheDir, "history.db"),
- RoomListPath: filepath.Join(cacheDir, "rooms.gob.gz"),
- StateDir: filepath.Join(cacheDir, "state"),
- MediaDir: filepath.Join(cacheDir, "media"),
-
- RoomCacheSize: 32,
- RoomCacheAge: 1 * 60,
-
- NotifySound: true,
- SendToVerifiedOnly: false,
- Backspace1RemovesWord: true,
- AlwaysClearScreen: true,
- }
-}
-
-// Clear clears the session cache and removes all history.
-func (config *Config) Clear() {
- _ = os.Remove(config.HistoryPath)
- _ = os.Remove(config.RoomListPath)
- _ = os.RemoveAll(config.StateDir)
- _ = os.RemoveAll(config.MediaDir)
- _ = os.RemoveAll(config.CacheDir)
- config.nosave = true
-}
-
-// ClearData clears non-temporary session data.
-func (config *Config) ClearData() {
- _ = os.RemoveAll(config.DataDir)
-}
-
-func (config *Config) CreateCacheDirs() {
- _ = os.MkdirAll(config.CacheDir, 0700)
- _ = os.MkdirAll(config.DataDir, 0700)
- _ = os.MkdirAll(config.StateDir, 0700)
- _ = os.MkdirAll(config.MediaDir, 0700)
-}
-
-func (config *Config) DeleteSession() {
- config.AuthCache.NextBatch = ""
- config.AuthCache.InitialSyncDone = false
- config.AccessToken = ""
- config.DeviceID = ""
- config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID)
- config.PushRules = nil
-
- config.ClearData()
- config.Clear()
- config.nosave = false
- config.CreateCacheDirs()
-}
-
-func (config *Config) LoadAll() {
- config.Load()
- config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID)
- config.LoadAuthCache()
- config.LoadPushRules()
- config.LoadPreferences()
- config.LoadKeybindings()
- err := config.Rooms.LoadList()
- if err != nil {
- panic(err)
- }
-}
-
-// Load loads the config from config.yaml in the directory given to the config struct.
-func (config *Config) Load() {
- err := config.load("config", config.Dir, "config.yaml", config)
- if err != nil {
- panic(fmt.Errorf("failed to load config.yaml: %w", err))
- }
- config.CreateCacheDirs()
-}
-
-func (config *Config) SaveAll() {
- config.Save()
- config.SaveAuthCache()
- config.SavePushRules()
- config.SavePreferences()
- err := config.Rooms.SaveList()
- if err != nil {
- panic(err)
- }
- config.Rooms.SaveLoadedRooms()
-}
-
-// Save saves this config to config.yaml in the directory given to the config struct.
-func (config *Config) Save() {
- config.save("config", config.Dir, "config.yaml", config)
-}
-
-func (config *Config) LoadPreferences() {
- _ = config.load("user preferences", config.CacheDir, "preferences.yaml", &config.Preferences)
-}
-
-func (config *Config) SavePreferences() {
- config.save("user preferences", config.CacheDir, "preferences.yaml", &config.Preferences)
-}
-
-//go:embed keybindings.yaml
-var DefaultKeybindings string
-
-func parseKeybindings(input map[string]string) (output map[Keybind]string) {
- output = make(map[Keybind]string, len(input))
- for shortcut, action := range input {
- mod, key, ch, err := cbind.Decode(shortcut)
- if err != nil {
- panic(fmt.Errorf("failed to parse keybinding %s -> %s: %w", shortcut, action, err))
- }
- // TODO find out if other keys are parsed incorrectly like this
- if key == tcell.KeyEscape {
- ch = 0
- }
- parsedShortcut := Keybind{
- Mod: mod,
- Key: key,
- Ch: ch,
- }
- output[parsedShortcut] = action
- }
- return
-}
-
-func (config *Config) LoadKeybindings() {
- var inputConfig RawKeybindings
-
- err := yaml.Unmarshal([]byte(DefaultKeybindings), &inputConfig)
- if err != nil {
- panic(fmt.Errorf("failed to unmarshal default keybindings: %w", err))
- }
- _ = config.load("keybindings", config.Dir, "keybindings.yaml", &inputConfig)
-
- config.Keybindings.Main = parseKeybindings(inputConfig.Main)
- config.Keybindings.Room = parseKeybindings(inputConfig.Room)
- config.Keybindings.Modal = parseKeybindings(inputConfig.Modal)
- config.Keybindings.Visual = parseKeybindings(inputConfig.Visual)
-}
-
-func (config *Config) SaveKeybindings() {
- config.save("keybindings", config.Dir, "keybindings.yaml", &config.Keybindings)
-}
-
-func (config *Config) LoadAuthCache() {
- err := config.load("auth cache", config.CacheDir, "auth-cache.yaml", &config.AuthCache)
- if err != nil {
- panic(fmt.Errorf("failed to load auth-cache.yaml: %w", err))
- }
-}
-
-func (config *Config) SaveAuthCache() {
- config.save("auth cache", config.CacheDir, "auth-cache.yaml", &config.AuthCache)
-}
-
-func (config *Config) LoadPushRules() {
- _ = config.load("push rules", config.CacheDir, "pushrules.json", &config.PushRules)
-
-}
-
-func (config *Config) SavePushRules() {
- if config.PushRules == nil {
- return
- }
- config.save("push rules", config.CacheDir, "pushrules.json", &config.PushRules)
-}
-
-func (config *Config) load(name, dir, file string, target interface{}) error {
- err := os.MkdirAll(dir, 0700)
- if err != nil {
- debug.Print("Failed to create", dir)
- return err
- }
-
- path := filepath.Join(dir, file)
- data, err := ioutil.ReadFile(path)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
- debug.Print("Failed to read", name, "from", path)
- return err
- }
-
- if strings.HasSuffix(file, ".yaml") {
- err = yaml.Unmarshal(data, target)
- } else {
- err = json.Unmarshal(data, target)
- }
- if err != nil {
- debug.Print("Failed to parse", name, "at", path)
- return err
- }
- return nil
-}
-
-func (config *Config) save(name, dir, file string, source interface{}) {
- if config.nosave {
- return
- }
-
- err := os.MkdirAll(dir, 0700)
- if err != nil {
- debug.Print("Failed to create", dir)
- panic(err)
- }
- var data []byte
- if strings.HasSuffix(file, ".yaml") {
- data, err = yaml.Marshal(source)
- } else {
- data, err = json.Marshal(source)
- }
- if err != nil {
- debug.Print("Failed to marshal", name)
- panic(err)
- }
-
- path := filepath.Join(dir, file)
- err = ioutil.WriteFile(path, data, 0600)
- if err != nil {
- debug.Print("Failed to write", name, "to", path)
- panic(err)
- }
-}
-
-func (config *Config) GetUserID() id.UserID {
- return config.UserID
-}
-
-const FilterVersion = 1
-
-func (config *Config) SaveFilterID(_ id.UserID, filterID string) {
- config.AuthCache.FilterID = filterID
- config.AuthCache.FilterVersion = FilterVersion
- config.SaveAuthCache()
-}
-
-func (config *Config) LoadFilterID(_ id.UserID) string {
- if config.AuthCache.FilterVersion != FilterVersion {
- return ""
- }
- return config.AuthCache.FilterID
-}
-
-func (config *Config) SaveNextBatch(_ id.UserID, nextBatch string) {
- config.AuthCache.NextBatch = nextBatch
- config.SaveAuthCache()
-}
-
-func (config *Config) LoadNextBatch(_ id.UserID) string {
- return config.AuthCache.NextBatch
-}
-
-func (config *Config) SaveRoom(_ *mautrix.Room) {
- panic("SaveRoom is not supported")
-}
-
-func (config *Config) LoadRoom(_ id.RoomID) *mautrix.Room {
- panic("LoadRoom is not supported")
-}
diff --git a/config/doc.go b/config/doc.go
deleted file mode 100644
index e570e0d..0000000
--- a/config/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package config contains the wrappers for gomuks configurations and sessions.
-package config
diff --git a/config/keybindings.yaml b/config/keybindings.yaml
deleted file mode 100644
index 3e6cf32..0000000
--- a/config/keybindings.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-main:
- 'Ctrl+Down': next_room
- 'Ctrl+Up': prev_room
- 'Ctrl+k': search_rooms
- 'Ctrl+Home': scroll_up
- 'Ctrl+End': scroll_down
- 'Ctrl+Enter': add_newline
- 'Ctrl+l': show_bare
- 'Alt+Down': next_room
- 'Alt+Up': prev_room
- 'Alt+k': search_rooms
- 'Alt+Home': scroll_up
- 'Alt+End': scroll_down
- 'Alt+Enter': add_newline
- 'Alt+a': next_active_room
- 'Alt+l': show_bare
-
-modal:
- 'Tab': select_next
- 'Down': select_next
- 'Backtab': select_prev
- 'Up': select_prev
- 'Enter': confirm
- 'Escape': cancel
-
-visual:
- 'Escape': clear
- 'h': clear
- 'Up': select_prev
- 'k': select_prev
- 'Down': select_next
- 'j': select_next
- 'Enter': confirm
- 'l': confirm
-
-room:
- 'Escape': clear
- 'Ctrl+p': scroll_up
- 'Ctrl+n': scroll_down
- 'PageUp': scroll_up
- 'PageDown': scroll_down
- 'Enter': send
diff --git a/deb/DEBIAN/control b/deb/DEBIAN/control
deleted file mode 100644
index 2bfcbc2..0000000
--- a/deb/DEBIAN/control
+++ /dev/null
@@ -1,7 +0,0 @@
-Package: gomuks
-Version: 0.3.1-1
-Section: net
-Priority: optional
-Architecture: amd64
-Maintainer: Tulir Asokan
-Description: A terminal based Matrix client written in Go.
diff --git a/debug/debug.go b/debug/debug.go
deleted file mode 100644
index f350124..0000000
--- a/debug/debug.go
+++ /dev/null
@@ -1,184 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package debug
-
-import (
- "bytes"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "os/user"
- "path/filepath"
- "runtime"
- "runtime/debug"
- "time"
-
- "github.com/sasha-s/go-deadlock"
-)
-
-var writer io.Writer
-var RecoverPrettyPanic bool = true
-var DeadlockDetection bool
-var WriteLogs bool
-var OnRecover func()
-var LogDirectory = GetUserDebugDir()
-
-func GetUserDebugDir() string {
- if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
- return filepath.Join(os.TempDir(), "gomuks-"+getUname())
- }
- // See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
- if xdgStateHome := os.Getenv("XDG_STATE_HOME"); xdgStateHome != "" {
- return filepath.Join(xdgStateHome, "gomuks")
- }
- home := os.Getenv("HOME")
- if home == "" {
- fmt.Println("XDG_STATE_HOME and HOME are both unset")
- os.Exit(1)
- }
- return filepath.Join(home, ".local", "state", "gomuks")
-}
-
-func getUname() string {
- currUser, err := user.Current()
- if err != nil {
- fmt.Println(err)
- os.Exit(1)
- }
-
- return currUser.Username
-}
-
-func Initialize() {
- err := os.MkdirAll(LogDirectory, 0750)
- if err != nil {
- RecoverPrettyPanic = false
- DeadlockDetection = false
- WriteLogs = false
- return
- }
-
- if WriteLogs {
- writer, err = os.OpenFile(filepath.Join(LogDirectory, "debug.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
- if err != nil {
- panic(err)
- }
- _, _ = fmt.Fprintf(writer, "======================= Debug init @ %s =======================\n", time.Now().Format("2006-01-02 15:04:05"))
- }
-
- if DeadlockDetection {
- deadlocks, err := os.OpenFile(filepath.Join(LogDirectory, "deadlock.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
- if err != nil {
- panic(err)
- }
- deadlock.Opts.LogBuf = deadlocks
- deadlock.Opts.OnPotentialDeadlock = func() {
- if OnRecover != nil {
- OnRecover()
- }
- _, _ = fmt.Fprintf(os.Stderr, "Potential deadlock detected. See %s/deadlock.log for more information.", LogDirectory)
- os.Exit(88)
- }
- _, err = fmt.Fprintf(deadlocks, "======================= Debug init @ %s =======================\n", time.Now().Format("2006-01-02 15:04:05"))
- if err != nil {
- panic(err)
- }
- } else {
- deadlock.Opts.Disable = true
- }
-}
-
-func Printf(text string, args ...interface{}) {
- if writer != nil {
- _, _ = fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] "))
- _, _ = fmt.Fprintf(writer, text+"\n", args...)
- }
-}
-
-func Print(text ...interface{}) {
- if writer != nil {
- _, _ = fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] "))
- _, _ = fmt.Fprintln(writer, text...)
- }
-}
-
-func PrintStack() {
- if writer != nil {
- _, _ = writer.Write(debug.Stack())
- }
-}
-
-// 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! >
- ‾‾‾\‾‾‾‾‾‾
- \ ^__^
- \ (XX)\_______
- (__)\ )\/\
- U ||----W |
- || ||
-
-A fatal error has occurred.
-
-`
-
-func PrettyPanic(panic interface{}) {
- fmt.Print(Oops)
- traceFile := fmt.Sprintf(filepath.Join(LogDirectory, "panic-%s.txt"), time.Now().Format("2006-01-02--15-04-05"))
-
- var buf bytes.Buffer
- _, _ = fmt.Fprintln(&buf, panic)
- buf.Write(debug.Stack())
- err := ioutil.WriteFile(traceFile, buf.Bytes(), 0640)
-
- if err != nil {
- fmt.Println("Saving the stack trace to", traceFile, "failed:")
- fmt.Println("--------------------------------------------------------------------------------")
- fmt.Println(err)
- fmt.Println("--------------------------------------------------------------------------------")
- fmt.Println("")
- fmt.Println("You can file an issue at https://github.com/tulir/gomuks/issues.")
- fmt.Println("Please provide the file save error (above) and the stack trace of the original error (below) when filing an issue.")
- fmt.Println("")
- fmt.Println("--------------------------------------------------------------------------------")
- fmt.Println(panic)
- debug.PrintStack()
- fmt.Println("--------------------------------------------------------------------------------")
- } else {
- fmt.Println("The stack trace has been saved to", traceFile)
- fmt.Println("")
- fmt.Println("You can file an issue at https://github.com/tulir/gomuks/issues.")
- fmt.Println("Please provide the contents of that file when filing an issue.")
- }
- os.Exit(1)
-}
diff --git a/debug/doc.go b/debug/doc.go
deleted file mode 100644
index 253441c..0000000
--- a/debug/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package debug contains utilities to log debug messages and display panics nicely.
-package debug
diff --git a/go.mod b/go.mod
index df2b20c..3d0c041 100644
--- a/go.mod
+++ b/go.mod
@@ -1,51 +1,5 @@
-module maunium.net/go/gomuks
+module go.mau.fi/gomuks
-go 1.21
+go 1.23.0
-require (
- github.com/alecthomas/chroma v0.10.0
- github.com/disintegration/imaging v1.6.2
- github.com/gabriel-vasile/mimetype v1.4.4
- github.com/kyokomi/emoji/v2 v2.2.13
- github.com/lithammer/fuzzysearch v1.1.8
- github.com/lucasb-eyer/go-colorful v1.2.0
- github.com/mattn/go-runewidth v0.0.15
- github.com/mattn/go-sqlite3 v1.14.22
- github.com/rivo/uniseg v0.4.7
- github.com/sasha-s/go-deadlock v0.3.1
- github.com/yuin/goldmark v1.7.4
- github.com/zyedidia/clipboard v1.0.4
- go.etcd.io/bbolt v1.3.10
- go.mau.fi/cbind v0.0.0-20220415094356-e1d579b7925e
- go.mau.fi/mauview v0.2.1
- go.mau.fi/tcell v0.4.0
- golang.org/x/image v0.18.0
- golang.org/x/net v0.27.0
- gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
- gopkg.in/vansante/go-ffprobe.v2 v2.2.0
- gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mauflag v1.0.0
- maunium.net/go/mautrix v0.11.2-0.20240620211416-fa19263891f5
- mvdan.cc/xurls/v2 v2.5.0
-)
-
-require (
- github.com/dlclark/regexp2 v1.4.0 // indirect
- github.com/gdamore/encoding v1.0.0 // indirect
- github.com/gorilla/mux v1.8.0 // indirect
- github.com/gorilla/websocket v1.5.0 // indirect
- github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
- github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
- github.com/tidwall/gjson v1.17.1 // indirect
- github.com/tidwall/match v1.1.1 // indirect
- github.com/tidwall/pretty v1.2.1 // indirect
- github.com/tidwall/sjson v1.2.5 // indirect
- golang.org/x/crypto v0.25.0 // indirect
- golang.org/x/sys v0.22.0 // indirect
- golang.org/x/term v0.22.0 // indirect
- golang.org/x/text v0.16.0 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
- maunium.net/go/maulogger/v2 v2.3.2 // indirect
-)
-
-replace github.com/mattn/go-runewidth => github.com/tulir/go-runewidth v0.0.14-0.20221113132156-dc2fc6d28246
+toolchain go1.23.2
diff --git a/go.sum b/go.sum
index c63c45d..e69de29 100644
--- a/go.sum
+++ b/go.sum
@@ -1,129 +0,0 @@
-github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
-github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
-github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
-github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
-github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
-github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
-github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
-github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
-github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
-github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
-github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
-github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
-github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
-github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
-github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
-github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
-github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
-github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
-github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
-github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
-github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
-github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
-github.com/tulir/go-runewidth v0.0.14-0.20221113132156-dc2fc6d28246 h1:WjkNcgoEaoL7i9mJuH+ff/hZHkSBR1KDdvoOoLpG6vs=
-github.com/tulir/go-runewidth v0.0.14-0.20221113132156-dc2fc6d28246/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
-github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-github.com/zyedidia/clipboard v1.0.4 h1:r6GUQOyPtIaApRLeD56/U+2uJbXis6ANGbKWCljULEo=
-github.com/zyedidia/clipboard v1.0.4/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
-go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
-go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
-go.mau.fi/cbind v0.0.0-20220415094356-e1d579b7925e h1:zY4TZmHAaUhrMFJQfh02dqxDYSfnnXlw/qRoFanxZTw=
-go.mau.fi/cbind v0.0.0-20220415094356-e1d579b7925e/go.mod h1:9nnzlslhUo/xO+8tsQgkFqG/W+SgD+r0iTYAuglzlmA=
-go.mau.fi/mauview v0.2.1 h1:Sv+L3MQoo0VWuqgO/SIzhTzDcd7iqPGZgxH3au2kUGw=
-go.mau.fi/mauview v0.2.1/go.mod h1:aTb1VjsjFmZ5YsdMQTIHrma9Ki2O0WwkS2Er7bIgoUs=
-go.mau.fi/tcell v0.4.0 h1:IPFKhkzF3yZkcRYjzgYBWWiW0JWPTwEBoXlWTBT8o/4=
-go.mau.fi/tcell v0.4.0/go.mod h1:77zV/6KL4Zip1u9ndjswACmu/LWwZ/oe3BE188uWMrA=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
-golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
-golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
-golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
-golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo=
-gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o=
-gopkg.in/vansante/go-ffprobe.v2 v2.2.0 h1:iuOqTsbfYuqIz4tAU9NWh22CmBGxlGHdgj4iqP+NUmY=
-gopkg.in/vansante/go-ffprobe.v2 v2.2.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
-maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
-maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
-maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/mautrix v0.11.2-0.20240620211416-fa19263891f5 h1:zAELWR3594qziixinqE+CgKZzgQwpiubArNZXXTmfIs=
-maunium.net/go/mautrix v0.11.2-0.20240620211416-fa19263891f5/go.mod h1:K29EcHwsNg6r7fMfwvi0GHQ9o5wSjqB9+Q8RjCIQEjA=
-mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
-mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
diff --git a/gomuks.go b/gomuks.go
deleted file mode 100644
index 392944f..0000000
--- a/gomuks.go
+++ /dev/null
@@ -1,192 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package main
-
-import (
- "errors"
- "fmt"
- "os"
- "os/signal"
- "path/filepath"
- "runtime"
- "strings"
- "syscall"
- "time"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/matrix"
-)
-
-// Information to find out exactly which commit gomuks was built from.
-// These are filled at build time with the -X linker flag.
-var (
- Tag = "unknown"
- Commit = "unknown"
- BuildTime = "unknown"
-)
-
-var (
- // Version is the version number of gomuks. Changed manually when making a release.
- Version = "0.3.1"
- // VersionString is the gomuks version, plus commit information. Filled in init() using the build-time values.
- VersionString = ""
-)
-
-func init() {
- if len(Tag) > 0 && Tag[0] == 'v' {
- Tag = Tag[1:]
- }
- if Tag != Version {
- suffix := ""
- if !strings.HasSuffix(Version, "+dev") {
- suffix = "+dev"
- }
- if len(Commit) > 8 {
- Version = fmt.Sprintf("%s%s.%s", Version, suffix, Commit[:8])
- } else {
- Version = fmt.Sprintf("%s%s.unknown", Version, suffix)
- }
- }
- VersionString = fmt.Sprintf("gomuks %s (%s with %s)", Version, BuildTime, runtime.Version())
-}
-
-// Gomuks is the wrapper for everything.
-type Gomuks struct {
- 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(uiProvider ifc.UIProvider, configDir, dataDir, cacheDir, downloadDir string) *Gomuks {
- gmx := &Gomuks{
- stop: make(chan bool, 1),
- }
-
- gmx.config = config.NewConfig(configDir, dataDir, cacheDir, downloadDir)
- gmx.ui = uiProvider(gmx)
- gmx.matrix = matrix.NewContainer(gmx)
-
- gmx.config.LoadAll()
- gmx.ui.Init()
-
- debug.OnRecover = gmx.ui.Finish
-
- return gmx
-}
-
-func (gmx *Gomuks) Version() string {
- return Version
-}
-
-// Save saves the active session and message history.
-func (gmx *Gomuks) Save() {
- gmx.config.SaveAll()
-}
-
-// StartAutosave calls Save() every minute until it receives a stop signal
-// on the Gomuks.stop channel.
-func (gmx *Gomuks) StartAutosave() {
- defer debug.Recover()
- ticker := time.NewTicker(time.Minute)
- for {
- select {
- case <-ticker.C:
- if gmx.config.AuthCache.InitialSyncDone {
- gmx.Save()
- }
- case val := <-gmx.stop:
- if val {
- return
- }
- }
- }
-}
-
-// Stop stops the Matrix syncer, the tview app and the autosave goroutine,
-// then saves everything and calls os.Exit(0).
-func (gmx *Gomuks) Stop(save bool) {
- go gmx.internalStop(save)
-}
-
-func (gmx *Gomuks) internalStop(save bool) {
- debug.Print("Disconnecting from Matrix...")
- gmx.matrix.Stop()
- debug.Print("Cleaning up UI...")
- gmx.ui.Stop()
- gmx.stop <- true
- if save {
- gmx.Save()
- }
- debug.Print("Exiting process")
- os.Exit(0)
-}
-
-// 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() {
- err := gmx.matrix.InitClient(true)
- if err != nil {
- if errors.Is(err, matrix.ErrServerOutdated) {
- _, _ = fmt.Fprintln(os.Stderr, strings.Replace(err.Error(), "homeserver", gmx.config.HS, 1))
- _, _ = fmt.Fprintln(os.Stderr)
- _, _ = fmt.Fprintf(os.Stderr, "See `%s --help` if you want to skip this check or clear all data.\n", os.Args[0])
- os.Exit(4)
- } else if strings.HasPrefix(err.Error(), "failed to check server versions") {
- _, _ = fmt.Fprintln(os.Stderr, "Failed to check versions supported by server:", errors.Unwrap(err))
- _, _ = fmt.Fprintln(os.Stderr)
- _, _ = fmt.Fprintf(os.Stderr, "Modify %s if the server has moved.\n", filepath.Join(gmx.config.Dir, "config.yaml"))
- _, _ = fmt.Fprintf(os.Stderr, "See `%s --help` if you want to skip this check or clear all data.\n", os.Args[0])
- os.Exit(5)
- } else {
- panic(err)
- }
- }
-
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt, syscall.SIGTERM)
- go func() {
- <-c
- gmx.Stop(true)
- }()
-
- go gmx.StartAutosave()
- if err = gmx.ui.Start(); err != nil {
- panic(err)
- }
-}
-
-// Matrix returns the MatrixContainer instance.
-func (gmx *Gomuks) Matrix() ifc.MatrixContainer {
- return gmx.matrix
-}
-
-// Config returns the Gomuks config instance.
-func (gmx *Gomuks) Config() *config.Config {
- return gmx.config
-}
-
-// UI returns the Gomuks UI instance.
-func (gmx *Gomuks) UI() ifc.GomuksUI {
- return gmx.ui
-}
diff --git a/interface/doc.go b/interface/doc.go
deleted file mode 100644
index cc09d44..0000000
--- a/interface/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package ifc contains interfaces to allow circular function calls without circular imports.
-package ifc
diff --git a/interface/gomuks.go b/interface/gomuks.go
deleted file mode 100644
index e269071..0000000
--- a/interface/gomuks.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ifc
-
-import (
- "maunium.net/go/gomuks/config"
-)
-
-// Gomuks is the wrapper for everything.
-type Gomuks interface {
- Matrix() MatrixContainer
- UI() GomuksUI
- Config() *config.Config
- Version() string
-
- Start()
- Stop(save bool)
-}
diff --git a/interface/matrix.go b/interface/matrix.go
deleted file mode 100644
index 58c428f..0000000
--- a/interface/matrix.go
+++ /dev/null
@@ -1,92 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ifc
-
-import (
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/crypto/attachment"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-type Relation struct {
- Type event.RelationType
- Event *muksevt.Event
-}
-
-type UploadedMediaInfo struct {
- *mautrix.RespMediaUpload
- EncryptionInfo *attachment.EncryptedFile
- MsgType event.MessageType
- Name string
- Info *event.FileInfo
-}
-
-type MatrixContainer interface {
- Client() *mautrix.Client
- Preferences() *config.UserPreferences
- InitClient(isStartup bool) error
- Initialized() bool
-
- Start()
- Stop()
-
- Login(user, password string) error
- Logout()
- UIAFallback(authType mautrix.AuthType, sessionID string) error
-
- SendPreferencesToMatrix()
- PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, relation *Relation) *muksevt.Event
- PrepareMediaMessage(room *rooms.Room, path string, relation *Relation) (*muksevt.Event, error)
- SendEvent(evt *muksevt.Event) (id.EventID, error)
- Redact(roomID id.RoomID, eventID id.EventID, reason string) error
- SendTyping(roomID id.RoomID, typing bool)
- MarkRead(roomID id.RoomID, eventID id.EventID)
- JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error)
- LeaveRoom(roomID id.RoomID) error
- CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error)
-
- FetchMembers(room *rooms.Room) error
- GetHistory(room *rooms.Room, limit int, dbPointer uint64) ([]*muksevt.Event, uint64, error)
- GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error)
- GetRoom(roomID id.RoomID) *rooms.Room
- GetOrCreateRoom(roomID id.RoomID) *rooms.Room
-
- UploadMedia(path string, encrypt bool) (*UploadedMediaInfo, error)
- Download(uri id.ContentURI, file *attachment.EncryptedFile) ([]byte, error)
- DownloadToDisk(uri id.ContentURI, file *attachment.EncryptedFile, target string) (string, error)
- GetDownloadURL(uri id.ContentURI, file *attachment.EncryptedFile) string
- GetCachePath(uri id.ContentURI) string
-
- Crypto() Crypto
-}
-
-type Crypto interface {
- Load() error
- FlushStore() error
- ProcessSyncResponse(resp *mautrix.RespSync, since string) bool
- ProcessInRoomVerification(evt *event.Event) error
- HandleMemberEvent(*event.Event)
- DecryptMegolmEvent(*event.Event) (*event.Event, error)
- EncryptMegolmEvent(id.RoomID, event.Type, interface{}) (*event.EncryptedEventContent, error)
- ShareGroupSession(id.RoomID, []id.UserID) error
- Fingerprint() string
-}
diff --git a/interface/ui.go b/interface/ui.go
deleted file mode 100644
index 460da0e..0000000
--- a/interface/ui.go
+++ /dev/null
@@ -1,89 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ifc
-
-import (
- "time"
-
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/pushrules"
-)
-
-type UIProvider func(gmx Gomuks) GomuksUI
-
-type GomuksUI interface {
- Render()
- HandleNewPreferences()
- OnLogin()
- OnLogout()
- MainView() MainView
-
- Init()
- Start() error
- Stop()
- Finish()
-}
-
-type SyncingModal interface {
- SetIndeterminate()
- SetMessage(string)
- SetSteps(int)
- Step()
- Close()
-}
-
-type MainView interface {
- GetRoom(roomID id.RoomID) RoomView
- AddRoom(room *rooms.Room)
- RemoveRoom(room *rooms.Room)
- SetRooms(rooms *rooms.RoomCache)
- Bump(room *rooms.Room)
-
- UpdateTags(room *rooms.Room)
-
- SetTyping(roomID id.RoomID, users []id.UserID)
- OpenSyncingModal() SyncingModal
-
- NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
-}
-
-type RoomView interface {
- MxRoom() *rooms.Room
-
- SetCompletions(completions []string)
- SetTyping(users []id.UserID)
- UpdateUserList()
-
- AddEvent(evt *muksevt.Event) Message
- AddRedaction(evt *muksevt.Event)
- AddEdit(evt *muksevt.Event)
- AddReaction(evt *muksevt.Event, key string)
- GetEvent(eventID id.EventID) Message
- AddServiceMessage(message string)
-}
-
-type Message interface {
- ID() id.EventID
- Time() time.Time
- NotificationSenderName() string
- NotificationContent() string
-
- SetIsHighlight(highlight bool)
- SetID(id id.EventID)
-}
diff --git a/lib/ansimage/LICENSE b/lib/ansimage/LICENSE
deleted file mode 100644
index a612ad9..0000000
--- a/lib/ansimage/LICENSE
+++ /dev/null
@@ -1,373 +0,0 @@
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
- means each individual or legal entity that creates, contributes to
- the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
- means the combination of the Contributions of others (if any) used
- by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
- means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
- means Source Code Form to which the initial Contributor has attached
- the notice in Exhibit A, the Executable Form of such Source Code
- Form, and Modifications of such Source Code Form, in each case
- including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
- means
-
- (a) that the initial Contributor has attached the notice described
- in Exhibit B to the Covered Software; or
-
- (b) that the Covered Software was made available under the terms of
- version 1.1 or earlier of the License, but not also under the
- terms of a Secondary License.
-
-1.6. "Executable Form"
- means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
- a separate file or files, that is not Covered Software.
-
-1.8. "License"
- means this document.
-
-1.9. "Licensable"
- means having the right to grant, to the maximum extent possible,
- whether at the time of the initial grant or subsequently, any and
- all of the rights conveyed by this License.
-
-1.10. "Modifications"
- means any of the following:
-
- (a) any file in Source Code Form that results from an addition to,
- deletion from, or modification of the contents of Covered
- Software; or
-
- (b) any new file in Source Code Form that contains any Covered
- Software.
-
-1.11. "Patent Claims" of a Contributor
- means any patent claim(s), including without limitation, method,
- process, and apparatus claims, in any patent Licensable by such
- Contributor that would be infringed, but for the grant of the
- License, by the making, using, selling, offering for sale, having
- made, import, or transfer of either its Contributions or its
- Contributor Version.
-
-1.12. "Secondary License"
- means either the GNU General Public License, Version 2.0, the GNU
- Lesser General Public License, Version 2.1, the GNU Affero General
- Public License, Version 3.0, or any later versions of those
- licenses.
-
-1.13. "Source Code Form"
- means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
- means an individual or a legal entity exercising rights under this
- License. For legal entities, "You" includes any entity that
- controls, is controlled by, or is under common control with You. For
- purposes of this definition, "control" means (a) the power, direct
- or indirect, to cause the direction or management of such entity,
- whether by contract or otherwise, or (b) ownership of more than
- fifty percent (50%) of the outstanding shares or beneficial
- ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
- Licensable by such Contributor to use, reproduce, make available,
- modify, display, perform, distribute, and otherwise exploit its
- Contributions, either on an unmodified basis, with Modifications, or
- as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
- for sale, have made, import, and otherwise transfer either its
- Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
- or
-
-(b) for infringements caused by: (i) Your and any other third party's
- modifications of Covered Software, or (ii) the combination of its
- Contributions with other software (except as part of its Contributor
- Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
- its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
- Form, as described in Section 3.1, and You must inform recipients of
- the Executable Form how they can obtain a copy of such Source Code
- Form by reasonable means in a timely manner, at a charge no more
- than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
- License, or sublicense it under different terms, provided that the
- license for the Executable Form does not attempt to limit or alter
- the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-* *
-* 6. Disclaimer of Warranty *
-* ------------------------- *
-* *
-* Covered Software is provided under this License on an "as is" *
-* basis, without warranty of any kind, either expressed, implied, or *
-* statutory, including, without limitation, warranties that the *
-* Covered Software is free of defects, merchantable, fit for a *
-* particular purpose or non-infringing. The entire risk as to the *
-* quality and performance of the Covered Software is with You. *
-* Should any Covered Software prove defective in any respect, You *
-* (not any Contributor) assume the cost of any necessary servicing, *
-* repair, or correction. This disclaimer of warranty constitutes an *
-* essential part of this License. No use of any Covered Software is *
-* authorized under this License except under this disclaimer. *
-* *
-************************************************************************
-
-************************************************************************
-* *
-* 7. Limitation of Liability *
-* -------------------------- *
-* *
-* Under no circumstances and under no legal theory, whether tort *
-* (including negligence), contract, or otherwise, shall any *
-* Contributor, or anyone who distributes Covered Software as *
-* permitted above, be liable to You for any direct, indirect, *
-* special, incidental, or consequential damages of any character *
-* including, without limitation, damages for lost profits, loss of *
-* goodwill, work stoppage, computer failure or malfunction, or any *
-* and all other commercial damages or losses, even if such party *
-* shall have been informed of the possibility of such damages. This *
-* limitation of liability shall not apply to liability for death or *
-* personal injury resulting from such party's negligence to the *
-* extent applicable law prohibits such limitation. Some *
-* jurisdictions do not allow the exclusion or limitation of *
-* incidental or consequential damages, so this exclusion and *
-* limitation may not apply to You. *
-* *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
- This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
diff --git a/lib/ansimage/ansimage.go b/lib/ansimage/ansimage.go
deleted file mode 100644
index 1001fe9..0000000
--- a/lib/ansimage/ansimage.go
+++ /dev/null
@@ -1,297 +0,0 @@
-// ___ _____ ____
-// / _ \/ _/ |/_/ /____ ______ _
-// / ___// /_> __/ -_) __/ ' \
-// /_/ /___/_/|_|\__/\__/_/ /_/_/_/
-//
-// Copyright 2017 Eliuk Blau
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package ansimage
-
-import (
- "errors"
- "image"
- "image/color"
- "image/draw"
- _ "image/gif" // initialize decoder
- _ "image/jpeg" // initialize decoder
- _ "image/png" // initialize decoder
- "io"
- "os"
-
- "github.com/disintegration/imaging"
- _ "golang.org/x/image/bmp" // initialize decoder
- _ "golang.org/x/image/tiff" // initialize decoder
- _ "golang.org/x/image/webp" // initialize decoder
-
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/debug"
- "maunium.net/go/gomuks/ui/messages/tstring"
-)
-
-var (
- // ErrHeightNonMoT happens when ANSImage height is not a Multiple of Two value.
- ErrHeightNonMoT = errors.New("ANSImage: height must be a Multiple of Two value")
-
- // ErrInvalidBoundsMoT happens when ANSImage height or width are invalid values (Multiple of Two).
- ErrInvalidBoundsMoT = errors.New("ANSImage: height or width must be >=2")
-
- // ErrOutOfBounds happens when ANSI-pixel coordinates are out of ANSImage bounds.
- ErrOutOfBounds = errors.New("ANSImage: out of bounds")
-)
-
-// ANSIpixel represents a pixel of an ANSImage.
-type ANSIpixel struct {
- Brightness uint8
- R, G, B uint8
- upper bool
- source *ANSImage
-}
-
-// ANSImage represents an image encoded in ANSI escape codes.
-type ANSImage struct {
- h, w int
- maxprocs int
- bgR uint8
- bgG uint8
- bgB uint8
- pixmap [][]*ANSIpixel
-}
-
-func (ai *ANSImage) Pixmap() [][]*ANSIpixel {
- return ai.pixmap
-}
-
-// Height gets total rows of ANSImage.
-func (ai *ANSImage) Height() int {
- return ai.h
-}
-
-// Width gets total columns of ANSImage.
-func (ai *ANSImage) Width() int {
- return ai.w
-}
-
-// SetMaxProcs sets the maximum number of parallel goroutines to render the ANSImage
-// (user should manually sets `runtime.GOMAXPROCS(max)` before to this change takes effect).
-func (ai *ANSImage) SetMaxProcs(max int) {
- ai.maxprocs = max
-}
-
-// GetMaxProcs gets the maximum number of parallels goroutines to render the ANSImage.
-func (ai *ANSImage) GetMaxProcs() int {
- return ai.maxprocs
-}
-
-// SetAt sets ANSI-pixel color (RBG) and brightness in coordinates (y,x).
-func (ai *ANSImage) SetAt(y, x int, r, g, b, brightness uint8) error {
- if y >= 0 && y < ai.h && x >= 0 && x < ai.w {
- ai.pixmap[y][x].R = r
- ai.pixmap[y][x].G = g
- ai.pixmap[y][x].B = b
- ai.pixmap[y][x].Brightness = brightness
- ai.pixmap[y][x].upper = y%2 == 0
- return nil
- }
- return ErrOutOfBounds
-}
-
-// GetAt gets ANSI-pixel in coordinates (y,x).
-func (ai *ANSImage) GetAt(y, x int) (*ANSIpixel, error) {
- if y >= 0 && y < ai.h && x >= 0 && x < ai.w {
- return &ANSIpixel{
- R: ai.pixmap[y][x].R,
- G: ai.pixmap[y][x].G,
- B: ai.pixmap[y][x].B,
- Brightness: ai.pixmap[y][x].Brightness,
- upper: ai.pixmap[y][x].upper,
- source: ai.pixmap[y][x].source,
- },
- nil
- }
- return nil, ErrOutOfBounds
-}
-
-// Render returns the ANSI-compatible string form of ANSImage.
-// (Nice info for ANSI True Colour - https://gist.github.com/XVilka/8346728)
-func (ai *ANSImage) Render() []tstring.TString {
- type renderData struct {
- row int
- render tstring.TString
- }
-
- rows := make([]tstring.TString, ai.h/2)
- for y := 0; y < ai.h; y += ai.maxprocs {
- ch := make(chan renderData, ai.maxprocs)
- for n, row := 0, y; (n <= ai.maxprocs) && (2*row+1 < ai.h); n, row = n+1, y+n {
- go func(row, y int) {
- defer func() {
- err := recover()
- if err != nil {
- debug.Print("Panic rendering ANSImage:", err)
- ch <- renderData{row: row, render: tstring.NewColorTString("ERROR", tcell.ColorRed)}
- }
- }()
- str := make(tstring.TString, ai.w)
- for x := 0; x < ai.w; x++ {
- topPixel := ai.pixmap[y][x]
- topColor := tcell.NewRGBColor(int32(topPixel.R), int32(topPixel.G), int32(topPixel.B))
-
- bottomPixel := ai.pixmap[y+1][x]
- bottomColor := tcell.NewRGBColor(int32(bottomPixel.R), int32(bottomPixel.G), int32(bottomPixel.B))
-
- str[x] = tstring.Cell{
- Char: '▄',
- Style: tcell.StyleDefault.Background(topColor).Foreground(bottomColor),
- }
- }
- ch <- renderData{row: row, render: str}
- }(row, 2*row)
- }
- for n, row := 0, y; (n <= ai.maxprocs) && (2*row+1 < ai.h); n, row = n+1, y+n {
- data := <-ch
- rows[data.row] = data.render
- }
- }
- return rows
-}
-
-// New creates a new empty ANSImage ready to draw on it.
-func New(h, w int, bg color.Color) (*ANSImage, error) {
- if h%2 != 0 {
- return nil, ErrHeightNonMoT
- }
-
- if h < 2 || w < 2 {
- return nil, ErrInvalidBoundsMoT
- }
-
- r, g, b, _ := bg.RGBA()
- ansimage := &ANSImage{
- h: h, w: w,
- maxprocs: 1,
- bgR: uint8(r),
- bgG: uint8(g),
- bgB: uint8(b),
- pixmap: nil,
- }
-
- ansimage.pixmap = func() [][]*ANSIpixel {
- v := make([][]*ANSIpixel, h)
- for y := 0; y < h; y++ {
- v[y] = make([]*ANSIpixel, w)
- for x := 0; x < w; x++ {
- v[y][x] = &ANSIpixel{
- R: 0,
- G: 0,
- B: 0,
- Brightness: 0,
- source: ansimage,
- upper: y%2 == 0,
- }
- }
- }
- return v
- }()
-
- return ansimage, nil
-}
-
-// NewFromReader creates a new ANSImage from an io.Reader.
-// Background color is used to fill when image has transparency or dithering mode is enabled
-// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
-func NewFromReader(reader io.Reader, bg color.Color) (*ANSImage, error) {
- img, _, err := image.Decode(reader)
- if err != nil {
- return nil, err
- }
-
- return createANSImage(img, bg)
-}
-
-// NewScaledFromReader creates a new scaled ANSImage from an io.Reader.
-// Background color is used to fill when image has transparency or dithering mode is enabled
-// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
-func NewScaledFromReader(reader io.Reader, y, x int, bg color.Color) (*ANSImage, error) {
- img, _, err := image.Decode(reader)
- if err != nil {
- return nil, err
- }
-
- img = imaging.Resize(img, x, y, imaging.Lanczos)
-
- return createANSImage(img, bg)
-}
-
-// NewFromFile creates a new ANSImage from a file.
-// Background color is used to fill when image has transparency or dithering mode is enabled
-// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
-func NewFromFile(name string, bg color.Color) (*ANSImage, error) {
- reader, err := os.Open(name)
- if err != nil {
- return nil, err
- }
- defer reader.Close()
- return NewFromReader(reader, bg)
-}
-
-// NewScaledFromFile creates a new scaled ANSImage from a file.
-// Background color is used to fill when image has transparency or dithering mode is enabled
-// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
-func NewScaledFromFile(name string, y, x int, bg color.Color) (*ANSImage, error) {
- reader, err := os.Open(name)
- if err != nil {
- return nil, err
- }
- defer reader.Close()
- return NewScaledFromReader(reader, y, x, bg)
-}
-
-// createANSImage loads data from an image and returns an ANSImage.
-// Background color is used to fill when image has transparency or dithering mode is enabled
-// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
-func createANSImage(img image.Image, bg color.Color) (*ANSImage, error) {
- var rgbaOut *image.RGBA
- bounds := img.Bounds()
-
- // do compositing only if background color has no transparency (thank you @disq for the idea!)
- // (info - http://stackoverflow.com/questions/36595687/transparent-pixel-color-go-lang-image)
- if _, _, _, a := bg.RGBA(); a >= 0xffff {
- rgbaOut = image.NewRGBA(bounds)
- draw.Draw(rgbaOut, bounds, image.NewUniform(bg), image.ZP, draw.Src)
- draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Over)
- } else {
- if v, ok := img.(*image.RGBA); ok {
- rgbaOut = v
- } else {
- rgbaOut = image.NewRGBA(bounds)
- draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Src)
- }
- }
-
- yMin, xMin := bounds.Min.Y, bounds.Min.X
- yMax, xMax := bounds.Max.Y, bounds.Max.X
-
- // always sets an even number of ANSIPixel rows...
- yMax = yMax - yMax%2 // one for upper pixel and another for lower pixel --> without dithering
-
- ansimage, err := New(yMax, xMax, bg)
- if err != nil {
- return nil, err
- }
-
- for y := yMin; y < yMax; y++ {
- for x := xMin; x < xMax; x++ {
- v := rgbaOut.RGBAAt(x, y)
- if err := ansimage.SetAt(y, x, v.R, v.G, v.B, 0); err != nil {
- return nil, err
- }
- }
- }
-
- return ansimage, nil
-}
diff --git a/lib/ansimage/doc.go b/lib/ansimage/doc.go
deleted file mode 100644
index dfc0c43..0000000
--- a/lib/ansimage/doc.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// Package ansimage is a simplified version of the ansimage package
-// in https://github.com/eliukblau/pixterm focused in rendering images
-// to a tcell-based TUI app.
-//
-// ___ _____ ____
-// / _ \/ _/ |/_/ /____ ______ _
-// / ___// /_> __/ -_) __/ ' \
-// /_/ /___/_/|_|\__/\__/_/ /_/_/_/
-//
-// This package is licensed under the Mozilla Public License v2.0.
-package ansimage
diff --git a/lib/filepicker/filepicker.go b/lib/filepicker/filepicker.go
deleted file mode 100644
index 3f07431..0000000
--- a/lib/filepicker/filepicker.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2022 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package filepicker
-
-import (
- "bytes"
- "errors"
- "os/exec"
- "strings"
-)
-
-var zenity string
-
-func init() {
- zenity, _ = exec.LookPath("zenity")
-}
-
-func IsSupported() bool {
- return len(zenity) > 0
-}
-
-func Open() (string, error) {
- cmd := exec.Command(zenity, "--file-selection")
- var output bytes.Buffer
- cmd.Stdout = &output
- err := cmd.Run()
- if err != nil {
- var exitErr *exec.ExitError
- if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
- return "", nil
- }
- return "", err
- }
- return strings.TrimSpace(output.String()), nil
-}
diff --git a/lib/notification/doc.go b/lib/notification/doc.go
deleted file mode 100644
index 05295c6..0000000
--- a/lib/notification/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package notification contains a simple cross-platform desktop notification sending function.
-package notification
diff --git a/lib/notification/notify_darwin.go b/lib/notification/notify_darwin.go
deleted file mode 100644
index 317641c..0000000
--- a/lib/notification/notify_darwin.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package notification
-
-import (
- "fmt"
- "os/exec"
-)
-
-var terminalNotifierAvailable = false
-
-func init() {
- if err := exec.Command("which", "terminal-notifier").Run(); err != nil {
- terminalNotifierAvailable = false
- }
- terminalNotifierAvailable = true
-}
-
-const sendScript = `on run {notifText, notifTitle}
- display notification notifText with title "gomuks" subtitle notifTitle
-end run`
-
-func Send(title, text string, critical, sound bool) error {
- if terminalNotifierAvailable {
- args := []string{"-title", "gomuks", "-subtitle", title, "-message", text}
- if critical {
- args = append(args, "-timeout", "15")
- } else {
- args = append(args, "-timeout", "4")
- }
- if sound {
- args = append(args, "-sound", "default")
- }
- //if len(iconPath) > 0 {
- // args = append(args, "-appIcon", iconPath)
- //}
- return exec.Command("terminal-notifier", args...).Run()
- }
- cmd := exec.Command("osascript", "-", text, title)
- if stdin, err := cmd.StdinPipe(); err != nil {
- return fmt.Errorf("failed to get stdin pipe for osascript: %w", err)
- } else if _, err = stdin.Write([]byte(sendScript)); err != nil {
- return fmt.Errorf("failed to write notification script to osascript: %w", err)
- } else if err = cmd.Run(); err != nil {
- return fmt.Errorf("failed to run notification script: %w", err)
- } else if !cmd.ProcessState.Success() {
- return fmt.Errorf("notification script exited unsuccessfully")
- } else {
- return nil
- }
-}
diff --git a/lib/notification/notify_windows.go b/lib/notification/notify_windows.go
deleted file mode 100644
index 954788b..0000000
--- a/lib/notification/notify_windows.go
+++ /dev/null
@@ -1,39 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package notification
-
-import (
- "gopkg.in/toast.v1"
-)
-
-func Send(title, text string, critical, sound bool) error {
- notification := toast.Notification{
- AppID: "gomuks",
- Title: title,
- Message: text,
- Audio: toast.Silent,
- Duration: toast.Short,
- // Icon: ...,
- }
- if sound {
- notification.Audio = toast.IM
- }
- if critical {
- notification.Duration = toast.Long
- }
- return notification.Push()
-}
diff --git a/lib/notification/notify_xdg.go b/lib/notification/notify_xdg.go
deleted file mode 100644
index 5320ee8..0000000
--- a/lib/notification/notify_xdg.go
+++ /dev/null
@@ -1,84 +0,0 @@
-//go:build !windows && !darwin
-
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package notification
-
-import (
- "os"
- "os/exec"
-)
-
-var notifySendPath string
-var audioCommand string
-var tryAudioCommands = []string{"ogg123", "paplay"}
-var soundNormal = "/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"
-var soundCritical = "/usr/share/sounds/freedesktop/stereo/complete.oga"
-
-func getSoundPath(env, defaultPath string) string {
- if path, ok := os.LookupEnv(env); ok {
- // Sound file overriden by environment
- return path
- } else if _, err := os.Stat(defaultPath); os.IsNotExist(err) {
- // Sound file doesn't exist, disable it
- return ""
- } else {
- // Default sound file exists and wasn't overridden by environment
- return defaultPath
- }
-}
-
-func init() {
- var err error
-
- if notifySendPath, err = exec.LookPath("notify-send"); err != nil {
- return
- }
-
- for _, cmd := range tryAudioCommands {
- if audioCommand, err = exec.LookPath(cmd); err == nil {
- break
- }
- }
- soundNormal = getSoundPath("GOMUKS_SOUND_NORMAL", soundNormal)
- soundCritical = getSoundPath("GOMUKS_SOUND_CRITICAL", soundCritical)
-}
-
-func Send(title, text string, critical, sound bool) error {
- if len(notifySendPath) == 0 {
- return nil
- }
-
- args := []string{"-a", "gomuks"}
- if !critical {
- args = append(args, "-u", "low")
- }
- //if iconPath {
- // args = append(args, "-i", iconPath)
- //}
- args = append(args, title, text)
- if sound && len(audioCommand) > 0 && len(soundNormal) > 0 {
- audioFile := soundNormal
- if critical && len(soundCritical) > 0 {
- audioFile = soundCritical
- }
- go func() {
- _ = exec.Command(audioCommand, audioFile).Run()
- }()
- }
- return exec.Command(notifySendPath, args...).Run()
-}
diff --git a/lib/open/doc.go b/lib/open/doc.go
deleted file mode 100644
index 367ffb7..0000000
--- a/lib/open/doc.go
+++ /dev/null
@@ -1,4 +0,0 @@
-// Package open contains a simple cross-platform way to open files in the program the OS wants to use.
-//
-// Based on https://github.com/skratchdot/open-golang
-package open
diff --git a/lib/open/open.go b/lib/open/open.go
deleted file mode 100644
index c128494..0000000
--- a/lib/open/open.go
+++ /dev/null
@@ -1,39 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package open
-
-import (
- "os/exec"
-
- "maunium.net/go/gomuks/debug"
-)
-
-func Open(input string) error {
- cmd := exec.Command(Command, append(Args, input)...)
- err := cmd.Start()
- if err != nil {
- debug.Printf("Failed to start %s: %v", Command, err)
- } else {
- go func() {
- waitErr := cmd.Wait()
- if waitErr != nil {
- debug.Printf("Failed to run %s: %v", Command, err)
- }
- }()
- }
- return err
-}
diff --git a/lib/open/open_darwin.go b/lib/open/open_darwin.go
deleted file mode 100644
index 8947413..0000000
--- a/lib/open/open_darwin.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package open
-
-const Command = "open"
-
-var Args []string
diff --git a/lib/open/open_windows.go b/lib/open/open_windows.go
deleted file mode 100644
index 1586e2b..0000000
--- a/lib/open/open_windows.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package open
-
-import (
- "os"
- "path/filepath"
-)
-
-const FileProtocolHandler = "url.dll,FileProtocolHandler"
-
-var Command = filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe")
-var Args = []string{FileProtocolHandler}
diff --git a/lib/open/open_xdg.go b/lib/open/open_xdg.go
deleted file mode 100644
index bf0796e..0000000
--- a/lib/open/open_xdg.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build !windows && !darwin
-
-package open
-
-const Command = "xdg-open"
-
-var Args []string
diff --git a/lib/util/doc.go b/lib/util/doc.go
deleted file mode 100644
index 8db1325..0000000
--- a/lib/util/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package util contains miscellaneous utilities
-package util
diff --git a/lib/util/lcp.go b/lib/util/lcp.go
deleted file mode 100644
index 2e2e690..0000000
--- a/lib/util/lcp.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Licensed under the GNU Free Documentation License 1.2
-// https://www.gnu.org/licenses/old-licenses/fdl-1.2.en.html
-//
-// Source: https://rosettacode.org/wiki/Longest_common_prefix#Go
-
-package util
-
-func LongestCommonPrefix(list []string) string {
- // Special cases first
- switch len(list) {
- case 0:
- return ""
- case 1:
- return list[0]
- }
-
- // LCP of min and max (lexigraphically)
- // is the LCP of the whole set.
- min, max := list[0], list[0]
- for _, s := range list[1:] {
- switch {
- case s < min:
- min = s
- case s > max:
- max = s
- }
- }
-
- for i := 0; i < len(min) && i < len(max); i++ {
- if min[i] != max[i] {
- return min[:i]
- }
- }
-
- // In the case where lengths are not equal but all bytes
- // are equal, min is the answer ("foo" < "foobar").
- return min
-}
diff --git a/main.go b/main.go
deleted file mode 100644
index 58401b5..0000000
--- a/main.go
+++ /dev/null
@@ -1,203 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package main
-
-import (
- "errors"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "time"
-
- flag "maunium.net/go/mauflag"
-
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/matrix"
- "maunium.net/go/gomuks/ui"
-)
-
-var MainUIProvider ifc.UIProvider = ui.NewGomuksUI
-
-var wantVersion = flag.MakeFull("v", "version", "Show the version of gomuks", "false").Bool()
-var clearCache = flag.MakeFull("c", "clear-cache", "Clear the cache directory instead of starting", "false").Bool()
-var clearData = flag.Make().LongKey("clear-all-data").Usage("Clear all data instead of starting").Default("false").Bool()
-var skipVersionCheck = flag.MakeFull("s", "skip-version-check", "Skip the homeserver version checks at startup and login", "false").Bool()
-var wantHelp, _ = flag.MakeHelpFlag()
-
-func main() {
- flag.SetHelpTitles(
- "gomuks - A terminal Matrix client written in Go.",
- "gomuks [-vch] [--clear-all-data]",
- )
- err := flag.Parse()
- if err != nil {
- fmt.Println(err)
- os.Exit(1)
- } else if *wantHelp {
- flag.PrintHelp()
- return
- } else if *wantVersion {
- fmt.Println(VersionString)
- return
- }
- debugDir := os.Getenv("DEBUG_DIR")
- if len(debugDir) > 0 {
- debug.LogDirectory = debugDir
- }
- debugLevel := strings.ToLower(os.Getenv("DEBUG"))
- if debugLevel == "1" || debugLevel == "t" || debugLevel == "true" {
- debug.RecoverPrettyPanic = false
- debug.DeadlockDetection = true
- debug.WriteLogs = true
- }
- debug.Initialize()
- defer debug.Recover()
-
- var configDir, dataDir, cacheDir, downloadDir string
-
- configDir, err = UserConfigDir()
- if err != nil {
- _, _ = fmt.Fprintln(os.Stderr, "Failed to get config directory:", err)
- os.Exit(3)
- }
- dataDir, err = UserDataDir()
- if err != nil {
- _, _ = fmt.Fprintln(os.Stderr, "Failed to get data directory:", err)
- os.Exit(3)
- }
- cacheDir, err = UserCacheDir()
- if err != nil {
- _, _ = fmt.Fprintln(os.Stderr, "Failed to get cache directory:", err)
- os.Exit(3)
- }
- downloadDir, err = UserDownloadDir()
- if err != nil {
- _, _ = fmt.Fprintln(os.Stderr, "Failed to get download directory:", err)
- os.Exit(3)
- }
-
- debug.Print("Config directory:", configDir)
- debug.Print("Data directory:", dataDir)
- debug.Print("Cache directory:", cacheDir)
- debug.Print("Download directory:", downloadDir)
-
- matrix.SkipVersionCheck = *skipVersionCheck
- gmx := NewGomuks(MainUIProvider, configDir, dataDir, cacheDir, downloadDir)
-
- if *clearCache {
- debug.Print("Clearing cache as requested by CLI flag")
- gmx.config.Clear()
- fmt.Printf("Cleared cache at %s\n", gmx.config.CacheDir)
- return
- } else if *clearData {
- debug.Print("Clearing all data as requested by CLI flag")
- gmx.config.Clear()
- gmx.config.ClearData()
- _ = os.RemoveAll(gmx.config.Dir)
- fmt.Printf("Cleared cache at %s, data at %s and config at %s\n", gmx.config.CacheDir, gmx.config.DataDir, gmx.config.Dir)
- return
- }
-
- 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)
-}
-
-func getRootDir(subdir string) string {
- rootDir := os.Getenv("GOMUKS_ROOT")
- if rootDir == "" {
- return ""
- }
- return filepath.Join(rootDir, subdir)
-}
-
-func UserCacheDir() (dir string, err error) {
- dir = os.Getenv("GOMUKS_CACHE_HOME")
- if dir == "" {
- dir = getRootDir("cache")
- }
- if dir == "" {
- dir, err = os.UserCacheDir()
- dir = filepath.Join(dir, "gomuks")
- }
- return
-}
-
-func UserDataDir() (dir string, err error) {
- dir = os.Getenv("GOMUKS_DATA_HOME")
- if dir != "" {
- return
- }
- if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
- return UserConfigDir()
- }
- dir = getRootDir("data")
- if dir == "" {
- dir = os.Getenv("XDG_DATA_HOME")
- }
- if dir == "" {
- dir = os.Getenv("HOME")
- if dir == "" {
- return "", errors.New("neither $XDG_DATA_HOME nor $HOME are defined")
- }
- dir = filepath.Join(dir, ".local", "share")
- }
- dir = filepath.Join(dir, "gomuks")
- return
-}
-
-func getXDGUserDir(name string) (dir string, err error) {
- cmd := exec.Command("xdg-user-dir", name)
- var out strings.Builder
- cmd.Stdout = &out
- err = cmd.Run()
- dir = strings.TrimSpace(out.String())
- return
-}
-
-func UserDownloadDir() (dir string, err error) {
- dir = os.Getenv("GOMUKS_DOWNLOAD_HOME")
- if dir != "" {
- return
- }
- dir, _ = getXDGUserDir("DOWNLOAD")
- if dir != "" {
- return
- }
- dir, err = os.UserHomeDir()
- dir = filepath.Join(dir, "Downloads")
- return
-}
-
-func UserConfigDir() (dir string, err error) {
- dir = os.Getenv("GOMUKS_CONFIG_HOME")
- if dir == "" {
- dir = getRootDir("config")
- }
- if dir == "" {
- dir, err = os.UserConfigDir()
- dir = filepath.Join(dir, "gomuks")
- }
- return
-}
diff --git a/matrix/crypto.go b/matrix/crypto.go
deleted file mode 100644
index 87406e0..0000000
--- a/matrix/crypto.go
+++ /dev/null
@@ -1,100 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-//go:build cgo
-
-package matrix
-
-import (
- "database/sql"
- "fmt"
- "os"
- "path/filepath"
-
- _ "github.com/mattn/go-sqlite3"
-
- "maunium.net/go/mautrix/crypto"
-
- "maunium.net/go/gomuks/debug"
-)
-
-type cryptoLogger struct {
- prefix string
-}
-
-func (c cryptoLogger) Error(message string, args ...interface{}) {
- debug.Printf(fmt.Sprintf("[%s/Error] %s", c.prefix, message), args...)
-}
-
-func (c cryptoLogger) Warn(message string, args ...interface{}) {
- debug.Printf(fmt.Sprintf("[%s/Warn] %s", c.prefix, message), args...)
-}
-
-func (c cryptoLogger) Debug(message string, args ...interface{}) {
- debug.Printf(fmt.Sprintf("[%s/Debug] %s", c.prefix, message), args...)
-}
-
-func (c cryptoLogger) Trace(message string, args ...interface{}) {
- debug.Printf(fmt.Sprintf("[%s/Trace] %s", c.prefix, message), args...)
-}
-
-func isBadEncryptError(err error) bool {
- return err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession
-}
-
-func (c *Container) initCrypto() error {
- var cryptoStore crypto.Store
- var err error
- legacyStorePath := filepath.Join(c.config.DataDir, "crypto.gob")
- if _, err = os.Stat(legacyStorePath); err == nil {
- debug.Printf("Using legacy crypto store as %s exists", legacyStorePath)
- cryptoStore, err = crypto.NewGobStore(legacyStorePath)
- if err != nil {
- return fmt.Errorf("file open: %w", err)
- }
- } else {
- debug.Printf("Using SQLite crypto store")
- newStorePath := filepath.Join(c.config.DataDir, "crypto.db")
- db, err := sql.Open("sqlite3", newStorePath)
- if err != nil {
- return fmt.Errorf("sql open: %w", err)
- }
- accID := fmt.Sprintf("%s/%s", c.config.UserID.String(), c.config.DeviceID)
- sqlStore := crypto.NewSQLCryptoStore(db, "sqlite3", accID, c.config.DeviceID, []byte("fi.mau.gomuks"), cryptoLogger{"Crypto/DB"})
- err = sqlStore.CreateTables()
- if err != nil {
- return fmt.Errorf("create table: %w", err)
- }
- cryptoStore = sqlStore
- }
- crypt := crypto.NewOlmMachine(c.client, cryptoLogger{"Crypto"}, cryptoStore, c.config.Rooms)
- crypt.AllowUnverifiedDevices = !c.config.SendToVerifiedOnly
- c.crypto = crypt
- err = c.crypto.Load()
- if err != nil {
- return fmt.Errorf("failed to create olm machine: %w", err)
- }
- return nil
-}
-
-func (c *Container) cryptoOnLogin() {
- sqlStore, ok := c.crypto.(*crypto.OlmMachine).CryptoStore.(*crypto.SQLCryptoStore)
- if !ok {
- return
- }
- sqlStore.DeviceID = c.config.DeviceID
- sqlStore.AccountID = fmt.Sprintf("%s/%s", c.config.UserID.String(), c.config.DeviceID)
-}
diff --git a/matrix/doc.go b/matrix/doc.go
deleted file mode 100644
index f789895..0000000
--- a/matrix/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package matrix contains wrappers for mautrix for use by the UI of gomuks.
-package matrix
diff --git a/matrix/history.go b/matrix/history.go
deleted file mode 100644
index b1e5842..0000000
--- a/matrix/history.go
+++ /dev/null
@@ -1,316 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package matrix
-
-import (
- "bytes"
- "compress/gzip"
- "encoding/binary"
- "encoding/gob"
- "errors"
-
- sync "github.com/sasha-s/go-deadlock"
- bolt "go.etcd.io/bbolt"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-type HistoryManager struct {
- sync.Mutex
-
- db *bolt.DB
-
- historyEndPtr map[*rooms.Room]uint64
-}
-
-var bucketRoomStreams = []byte("room_streams")
-var bucketRoomEventIDs = []byte("room_event_ids")
-var bucketStreamPointers = []byte("room_stream_pointers")
-
-const halfUint64 = ^uint64(0) >> 1
-
-func NewHistoryManager(dbPath string) (*HistoryManager, error) {
- hm := &HistoryManager{
- historyEndPtr: make(map[*rooms.Room]uint64),
- }
- db, err := bolt.Open(dbPath, 0600, &bolt.Options{
- Timeout: 1,
- NoGrowSync: false,
- FreelistType: bolt.FreelistArrayType,
- })
- if err != nil {
- return nil, err
- }
- err = db.Update(func(tx *bolt.Tx) error {
- _, err = tx.CreateBucketIfNotExists(bucketRoomStreams)
- if err != nil {
- return err
- }
- _, err = tx.CreateBucketIfNotExists(bucketRoomEventIDs)
- if err != nil {
- return err
- }
- _, err = tx.CreateBucketIfNotExists(bucketStreamPointers)
- if err != nil {
- return err
- }
- return nil
- })
- if err != nil {
- return nil, err
- }
- hm.db = db
- return hm, nil
-}
-
-func (hm *HistoryManager) Close() error {
- return hm.db.Close()
-}
-
-var (
- EventNotFoundError = errors.New("event not found")
- RoomNotFoundError = errors.New("room not found")
-)
-
-func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []byte) (*bolt.Bucket, []byte, error) {
- eventIDs := tx.Bucket(bucketRoomEventIDs).Bucket(roomID)
- if eventIDs == nil {
- return nil, nil, RoomNotFoundError
- }
- index := eventIDs.Get(eventID)
- if index == nil {
- return nil, nil, EventNotFoundError
- }
- stream := tx.Bucket(bucketRoomStreams).Bucket(roomID)
- return stream, index, nil
-}
-
-func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*muksevt.Event, error) {
- eventData := stream.Get(index)
- if eventData == nil || len(eventData) == 0 {
- return nil, EventNotFoundError
- }
- return unmarshalEvent(eventData)
-}
-
-func (hm *HistoryManager) Get(room *rooms.Room, eventID id.EventID) (evt *muksevt.Event, err error) {
- err = hm.db.View(func(tx *bolt.Tx) error {
- if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil {
- return err
- } else if evt, err = hm.getEvent(tx, stream, index); err != nil {
- return err
- }
- return nil
- })
- return
-}
-
-func (hm *HistoryManager) Update(room *rooms.Room, eventID id.EventID, update func(evt *muksevt.Event) error) error {
- return hm.db.Update(func(tx *bolt.Tx) error {
- if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil {
- return err
- } else if evt, err := hm.getEvent(tx, stream, index); err != nil {
- return err
- } else if err = update(evt); err != nil {
- return err
- } else if eventData, err := marshalEvent(evt); err != nil {
- return err
- } else if err := stream.Put(index, eventData); err != nil {
- return err
- }
- return nil
- })
-}
-
-func (hm *HistoryManager) Append(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) {
- muksEvts, _, err := hm.store(room, events, true)
- return muksEvts, err
-}
-
-func (hm *HistoryManager) Prepend(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, uint64, error) {
- return hm.store(room, events, false)
-}
-
-func (hm *HistoryManager) store(room *rooms.Room, events []*event.Event, append bool) (newEvents []*muksevt.Event, newPtrStart uint64, err error) {
- hm.Lock()
- defer hm.Unlock()
- newEvents = make([]*muksevt.Event, len(events))
- err = hm.db.Update(func(tx *bolt.Tx) error {
- streamPointers := tx.Bucket(bucketStreamPointers)
- rid := []byte(room.ID)
- stream, err := tx.Bucket(bucketRoomStreams).CreateBucketIfNotExists(rid)
- if err != nil {
- return err
- }
- eventIDs, err := tx.Bucket(bucketRoomEventIDs).CreateBucketIfNotExists(rid)
- if err != nil {
- return err
- }
- if stream.Sequence() < halfUint64 {
- // The sequence counter (i.e. the future) the part after 2^63, i.e. the second half of uint64
- // We set it to -1 because NextSequence will increment it by one.
- err = stream.SetSequence(halfUint64 - 1)
- if err != nil {
- return err
- }
- }
- if append {
- ptrStart, err := stream.NextSequence()
- if err != nil {
- return err
- }
- for i, evt := range events {
- newEvents[i] = muksevt.Wrap(evt)
- if err := put(stream, eventIDs, newEvents[i], ptrStart+uint64(i)); err != nil {
- return err
- }
- }
- err = stream.SetSequence(ptrStart + uint64(len(events)) - 1)
- if err != nil {
- return err
- }
- } else {
- ptrStart, ok := hm.historyEndPtr[room]
- if !ok {
- ptrStartRaw := streamPointers.Get(rid)
- if ptrStartRaw != nil {
- ptrStart = btoi(ptrStartRaw)
- } else {
- ptrStart = halfUint64 - 1
- }
- }
- eventCount := uint64(len(events))
- for i, evt := range events {
- newEvents[i] = muksevt.Wrap(evt)
- if err := put(stream, eventIDs, newEvents[i], -ptrStart-uint64(i)); err != nil {
- return err
- }
- }
- hm.historyEndPtr[room] = ptrStart + eventCount
- // TODO this is not the correct value for newPtrStart, figure out what the f*ck is going on here
- newPtrStart = ptrStart + eventCount
- err := streamPointers.Put(rid, itob(ptrStart+eventCount))
- if err != nil {
- return err
- }
- }
-
- return nil
- })
- return
-}
-
-func (hm *HistoryManager) Load(room *rooms.Room, num int, ptrStart uint64) (events []*muksevt.Event, newPtrStart uint64, err error) {
- hm.Lock()
- defer hm.Unlock()
- err = hm.db.View(func(tx *bolt.Tx) error {
- stream := tx.Bucket(bucketRoomStreams).Bucket([]byte(room.ID))
- if stream == nil {
- return nil
- }
- if ptrStart == 0 {
- ptrStart = stream.Sequence() + 1
- }
- c := stream.Cursor()
- k, v := c.Seek(itob(ptrStart - uint64(num)))
- ptrStartFound := btoi(k)
- if k == nil || ptrStartFound >= ptrStart {
- return nil
- }
- newPtrStart = ptrStartFound
- for ; k != nil && btoi(k) < ptrStart; k, v = c.Next() {
- evt, parseError := unmarshalEvent(v)
- if parseError != nil {
- return parseError
- }
- events = append(events, evt)
- }
- return nil
- })
- // Reverse array because we read/append the history in reverse order.
- i := 0
- j := len(events) - 1
- for i < j {
- events[i], events[j] = events[j], events[i]
- i++
- j--
- }
- return
-}
-
-func itob(v uint64) []byte {
- b := make([]byte, 8)
- binary.BigEndian.PutUint64(b, v)
- return b
-}
-
-func btoi(b []byte) uint64 {
- return binary.BigEndian.Uint64(b)
-}
-
-func stripRaw(evt *muksevt.Event) {
- evtCopy := *evt.Event
- evtCopy.Content = event.Content{
- Parsed: evt.Content.Parsed,
- }
- evt.Event = &evtCopy
-}
-
-func marshalEvent(evt *muksevt.Event) ([]byte, error) {
- stripRaw(evt)
- var buf bytes.Buffer
- enc, _ := gzip.NewWriterLevel(&buf, gzip.BestSpeed)
- if err := gob.NewEncoder(enc).Encode(evt); err != nil {
- _ = enc.Close()
- return nil, err
- } else if err := enc.Close(); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
-
-func unmarshalEvent(data []byte) (*muksevt.Event, error) {
- evt := &muksevt.Event{}
- if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil {
- return nil, err
- } else if err := gob.NewDecoder(cmpReader).Decode(evt); err != nil {
- _ = cmpReader.Close()
- return nil, err
- } else if err := cmpReader.Close(); err != nil {
- return nil, err
- }
- return evt, nil
-}
-
-func put(streams, eventIDs *bolt.Bucket, evt *muksevt.Event, key uint64) error {
- data, err := marshalEvent(evt)
- if err != nil {
- return err
- }
- keyBytes := itob(key)
- if err = streams.Put(keyBytes, data); err != nil {
- return err
- }
- if err = eventIDs.Put([]byte(evt.ID), keyBytes); err != nil {
- return err
- }
- return nil
-}
diff --git a/matrix/matrix.go b/matrix/matrix.go
deleted file mode 100644
index 8ecf145..0000000
--- a/matrix/matrix.go
+++ /dev/null
@@ -1,1418 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package matrix
-
-import (
- "context"
- "crypto/tls"
- "encoding/gob"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "reflect"
- "runtime"
- dbg "runtime/debug"
- "strconv"
- "strings"
- "time"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/crypto/attachment"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/format"
- "maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/pushrules"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/lib/open"
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-// Container is a wrapper for a mautrix Client and some other stuff.
-//
-// It is used for all Matrix calls from the UI and Matrix event handlers.
-type Container struct {
- client *mautrix.Client
- crypto ifc.Crypto
- syncer *GomuksSyncer
- gmx ifc.Gomuks
- ui ifc.GomuksUI
- config *config.Config
- history *HistoryManager
- running bool
- stop chan bool
-
- mediaProxyURL string
-
- typing int64
-}
-
-// NewContainer creates a new Container for the given Gomuks instance.
-func NewContainer(gmx ifc.Gomuks) *Container {
- c := &Container{
- config: gmx.Config(),
- ui: gmx.UI(),
- gmx: gmx,
- }
-
- return c
-}
-
-// Client returns the underlying mautrix Client.
-func (c *Container) Client() *mautrix.Client {
- return c.client
-}
-
-type mxLogger struct{}
-
-func (log mxLogger) Debugfln(message string, args ...interface{}) {
- debug.Printf("[Matrix] "+message, args...)
-}
-
-func (c *Container) Crypto() ifc.Crypto {
- return c.crypto
-}
-
-var (
- ErrNoHomeserver = errors.New("no homeserver entered")
- ErrServerOutdated = errors.New("homeserver is outdated")
-)
-
-var MinSpecVersion = mautrix.SpecV11
-var SkipVersionCheck = false
-
-// InitClient initializes the mautrix client and connects to the homeserver specified in the config.
-func (c *Container) InitClient(isStartup bool) error {
- if len(c.config.HS) == 0 {
- if isStartup {
- return nil
- }
- return ErrNoHomeserver
- }
-
- if c.client != nil {
- c.Stop()
- c.client = nil
- c.crypto = nil
- }
-
- var mxid id.UserID
- var accessToken string
- if len(c.config.AccessToken) > 0 {
- accessToken = c.config.AccessToken
- mxid = c.config.UserID
- }
-
- var err error
- c.client, err = mautrix.NewClient(c.config.HS, mxid, accessToken)
- if err != nil {
- return fmt.Errorf("failed to create mautrix client: %w", err)
- }
- c.client.UserAgent = fmt.Sprintf("gomuks/%s %s", c.gmx.Version(), mautrix.DefaultUserAgent)
- c.client.Logger = mxLogger{}
- c.client.DeviceID = c.config.DeviceID
-
- err = c.initCrypto()
- if err != nil {
- return fmt.Errorf("failed to initialize crypto: %w", err)
- }
-
- if c.history == nil {
- c.history, err = NewHistoryManager(c.config.HistoryPath)
- if err != nil {
- return fmt.Errorf("failed to initialize history: %w", err)
- }
- }
-
- allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0
- if allowInsecure {
- c.client.Client = &http.Client{
- Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
- }
- }
-
- if !SkipVersionCheck && (!isStartup || len(c.client.AccessToken) > 0) {
- debug.Printf("Checking versions that %s supports", c.client.HomeserverURL)
- resp, err := c.client.Versions()
- if err != nil {
- debug.Print("Error checking supported versions:", err)
- return fmt.Errorf("failed to check server versions: %w", err)
- } else if !resp.ContainsGreaterOrEqual(MinSpecVersion) {
- debug.Print("Server doesn't support modern spec versions")
- bestVersionStr := "nothing"
- bestVersion := mautrix.MustParseSpecVersion("r0.0.0")
- for _, ver := range resp.Versions {
- if ver.GreaterThan(bestVersion) {
- bestVersion = ver
- bestVersionStr = ver.String()
- }
- }
- return fmt.Errorf("%w (it only supports %s, while gomuks requires %s)", ErrServerOutdated, bestVersionStr, MinSpecVersion.String())
- } else {
- debug.Print("Server supports modern spec versions")
- }
- }
-
- if c.mediaProxyURL == "" {
- server := httptest.NewServer(http.HandlerFunc(c.doMediaProxy))
- c.mediaProxyURL = server.URL
- debug.Print("Started media proxy server at", c.mediaProxyURL)
- }
-
- c.stop = make(chan bool, 1)
-
- if len(accessToken) > 0 {
- go c.Start()
- }
- return nil
-}
-
-func (c *Container) doMediaProxy(w http.ResponseWriter, r *http.Request) {
- parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
- if len(parts) != 2 {
- w.WriteHeader(http.StatusNotFound)
- _, _ = w.Write([]byte("Invalid path\n"))
- return
- }
- uri := id.ContentURI{
- Homeserver: parts[0],
- FileID: parts[1],
- }
- key := r.URL.Query().Get("k")
- iv := r.URL.Query().Get("iv")
- hash := r.URL.Query().Get("hash")
- var file *attachment.EncryptedFile
- if key != "" && iv != "" && hash != "" {
- file = &attachment.EncryptedFile{
- Key: attachment.JSONWebKey{
- Key: key,
- Algorithm: "A256CTR",
- Extractable: true,
- KeyType: "oct",
- KeyOps: []string{"encrypt", "decrypt"},
- },
- InitVector: iv,
- Hashes: attachment.EncryptedFileHashes{
- SHA256: hash,
- },
- Version: "v2",
- }
- }
- data, err := c.Download(uri, file)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- _, _ = w.Write([]byte(fmt.Sprintf("Failed to download media: %v\n", err)))
- return
- }
- mime := http.DetectContentType(data)
- w.Header().Add("Content-Length", strconv.Itoa(len(data)))
- w.Header().Add("Content-Type", mime)
- switch mime {
- case "text/css", "text/plain", "text/csv",
- "application/json", "application/ld+json",
- "image/jpeg", "image/gif", "image/png", "image/apng", "image/webp", "image/avif",
- "video/mp4", "video/webm", "video/ogg", "video/quicktime",
- "audio/mp4", "audio/webm", "audio/aac", "audio/mpeg", "audio/ogg", "audio/wave",
- "audio/wav", "audio/x-wav", "audio/x-pn-wav", "audio/flac", "audio/x-flac":
- w.Header().Add("Content-Disposition", "inline")
- default:
- w.Header().Add("Content-Disposition", "attachment")
- }
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write(data)
-}
-
-// Initialized returns whether or not the mautrix client is initialized (see InitClient())
-func (c *Container) Initialized() bool {
- return c.client != nil
-}
-
-func (c *Container) PasswordLogin(user, password string) error {
- resp, err := c.client.Login(&mautrix.ReqLogin{
- Type: "m.login.password",
- Identifier: mautrix.UserIdentifier{
- Type: "m.id.user",
- User: user,
- },
- Password: password,
- InitialDeviceDisplayName: "gomuks",
-
- StoreCredentials: true,
- StoreHomeserverURL: true,
- })
- if err != nil {
- return err
- }
- c.finishLogin(resp)
- return nil
-}
-
-func (c *Container) finishLogin(resp *mautrix.RespLogin) {
- c.config.UserID = resp.UserID
- c.config.DeviceID = resp.DeviceID
- c.config.AccessToken = resp.AccessToken
- if resp.WellKnown != nil && len(resp.WellKnown.Homeserver.BaseURL) > 0 {
- c.config.HS = resp.WellKnown.Homeserver.BaseURL
- }
- c.config.Save()
-
- go c.Start()
-}
-
-func respondHTML(w http.ResponseWriter, status int, message string) {
- w.Header().Add("Content-Type", "text/html")
- w.WriteHeader(status)
- _, _ = w.Write([]byte(fmt.Sprintf(`
-
-
- gomuks single-sign on
-
-
-
-
-
%s
-
-
-`, message)))
-}
-
-func (c *Container) SingleSignOn() error {
- loginURL := c.client.BuildURLWithQuery(mautrix.ClientURLPath{"v3", "login", "sso", "redirect"}, map[string]string{
- "redirectUrl": "http://localhost:29325",
- })
- err := open.Open(loginURL)
- if err != nil {
- return err
- }
- errChan := make(chan error, 1)
- server := &http.Server{Addr: ":29325"}
- server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- loginToken := r.URL.Query().Get("loginToken")
- if len(loginToken) == 0 {
- respondHTML(w, http.StatusBadRequest, "Missing loginToken parameter")
- return
- }
- resp, err := c.client.Login(&mautrix.ReqLogin{
- Type: "m.login.token",
- Token: loginToken,
- InitialDeviceDisplayName: "gomuks",
-
- StoreCredentials: true,
- StoreHomeserverURL: true,
- })
- if err != nil {
- respondHTML(w, http.StatusForbidden, err.Error())
- errChan <- err
- return
- }
- respondHTML(w, http.StatusOK, fmt.Sprintf("Successfully logged in as %s", resp.UserID))
- c.finishLogin(resp)
- go func() {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- err = server.Shutdown(ctx)
- if err != nil {
- debug.Printf("Failed to shut down SSO server: %v\n", err)
- }
- errChan <- err
- }()
- })
- err = server.ListenAndServe()
- if err != nil {
- return err
- }
- err = <-errChan
- return err
-}
-
-// Login sends a password login request with the given username and password.
-func (c *Container) Login(user, password string) error {
- resp, err := c.client.GetLoginFlows()
- if err != nil {
- return err
- }
- ssoSkippedBecausePassword := false
- for _, flow := range resp.Flows {
- if flow.Type == "m.login.password" {
- return c.PasswordLogin(user, password)
- } else if flow.Type == "m.login.sso" {
- if len(password) == 0 {
- return c.SingleSignOn()
- } else {
- ssoSkippedBecausePassword = true
- }
- }
- }
- if ssoSkippedBecausePassword {
- return fmt.Errorf("password login is not supported\nleave the password field blank to use SSO")
- }
- return fmt.Errorf("no supported login flows")
-}
-
-// Logout revokes the access token, stops the syncer and calls the OnLogout() method of the UI.
-func (c *Container) Logout() {
- c.client.Logout()
- c.Stop()
- c.config.DeleteSession()
- c.client = nil
- c.crypto = nil
- c.ui.OnLogout()
-}
-
-// Stop stops the Matrix syncer.
-func (c *Container) Stop() {
- if c.running {
- debug.Print("Stopping Matrix container...")
- select {
- case c.stop <- true:
- default:
- }
- c.client.StopSync()
- debug.Print("Closing history manager...")
- err := c.history.Close()
- if err != nil {
- debug.Print("Error closing history manager:", err)
- }
- c.history = nil
- if c.crypto != nil {
- debug.Print("Flushing crypto store")
- err = c.crypto.FlushStore()
- if err != nil {
- debug.Print("Error flushing crypto store:", err)
- }
- }
- }
-}
-
-// UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object.
-func (c *Container) UpdatePushRules() {
- debug.Print("Updating push rules...")
- resp, err := c.client.GetPushRules()
- if err != nil {
- debug.Print("Failed to fetch push rules:", err)
- c.config.PushRules = &pushrules.PushRuleset{}
- } else {
- c.config.PushRules = resp
- }
- c.config.SavePushRules()
-}
-
-// PushRules returns the push notification rules. If no push rules are cached, UpdatePushRules() will be called first.
-func (c *Container) PushRules() *pushrules.PushRuleset {
- if c.config.PushRules == nil {
- c.UpdatePushRules()
- }
- return c.config.PushRules
-}
-
-var AccountDataGomuksPreferences = event.Type{
- Type: "net.maunium.gomuks.preferences",
- Class: event.AccountDataEventType,
-}
-
-func init() {
- event.TypeMap[AccountDataGomuksPreferences] = reflect.TypeOf(config.UserPreferences{})
- gob.Register(&config.UserPreferences{})
-}
-
-type StubSyncingModal struct{}
-
-func (s StubSyncingModal) SetIndeterminate() {}
-func (s StubSyncingModal) SetMessage(s2 string) {}
-func (s StubSyncingModal) SetSteps(i int) {}
-func (s StubSyncingModal) Step() {}
-func (s StubSyncingModal) Close() {}
-
-// OnLogin initializes the syncer and updates the room list.
-func (c *Container) OnLogin() {
- c.cryptoOnLogin()
- c.ui.OnLogin()
-
- c.client.Store = c.config
-
- debug.Print("Initializing syncer")
- c.syncer = NewGomuksSyncer(c.config.Rooms)
- if c.crypto != nil {
- c.syncer.OnSync(c.crypto.ProcessSyncResponse)
- c.syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
- // Don't spam the crypto module with member events of an initial sync
- // TODO invalidate all group sessions when clearing cache?
- if c.config.AuthCache.InitialSyncDone {
- c.crypto.HandleMemberEvent(evt)
- }
- })
- c.syncer.OnEventType(event.EventEncrypted, c.HandleEncrypted)
- } else {
- c.syncer.OnEventType(event.EventEncrypted, c.HandleEncryptedUnsupported)
- }
- c.syncer.OnEventType(event.EventMessage, c.HandleMessage)
- c.syncer.OnEventType(event.EventSticker, c.HandleMessage)
- c.syncer.OnEventType(event.EventReaction, c.HandleMessage)
- c.syncer.OnEventType(event.EventRedaction, c.HandleRedaction)
- c.syncer.OnEventType(event.StateAliases, c.HandleMessage)
- c.syncer.OnEventType(event.StateCanonicalAlias, c.HandleMessage)
- c.syncer.OnEventType(event.StateTopic, c.HandleMessage)
- c.syncer.OnEventType(event.StateRoomName, c.HandleMessage)
- c.syncer.OnEventType(event.StateMember, c.HandleMembership)
- c.syncer.OnEventType(event.EphemeralEventReceipt, c.HandleReadReceipt)
- c.syncer.OnEventType(event.EphemeralEventTyping, c.HandleTyping)
- c.syncer.OnEventType(event.AccountDataDirectChats, c.HandleDirectChatInfo)
- c.syncer.OnEventType(event.AccountDataPushRules, c.HandlePushRules)
- c.syncer.OnEventType(event.AccountDataRoomTags, c.HandleTag)
- c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences)
- if len(c.config.AuthCache.NextBatch) == 0 {
- c.syncer.Progress = c.ui.MainView().OpenSyncingModal()
- c.syncer.Progress.SetMessage("Waiting for /sync response from server")
- c.syncer.Progress.SetIndeterminate()
- c.syncer.FirstDoneCallback = func() {
- c.syncer.Progress.Close()
- c.syncer.Progress = StubSyncingModal{}
- c.syncer.FirstDoneCallback = nil
- }
- }
- c.syncer.InitDoneCallback = func() {
- debug.Print("Initial sync done")
- c.config.AuthCache.InitialSyncDone = true
- debug.Print("Updating title caches")
- for _, room := range c.config.Rooms.Map {
- room.GetTitle()
- }
- debug.Print("Cleaning cached rooms from memory")
- c.config.Rooms.ForceClean()
- debug.Print("Saving all data")
- c.config.SaveAll()
- debug.Print("Adding rooms to UI")
- c.ui.MainView().SetRooms(c.config.Rooms)
- c.ui.Render()
- // The initial sync can be a bit heavy, so we force run the GC here
- // after cleaning up rooms from memory above.
- debug.Print("Running GC")
- runtime.GC()
- dbg.FreeOSMemory()
- }
- c.client.Syncer = c.syncer
-
- debug.Print("Setting existing rooms")
- c.ui.MainView().SetRooms(c.config.Rooms)
-
- debug.Print("OnLogin() done.")
-}
-
-// Start moves the UI to the main view, calls OnLogin() and runs the syncer forever until stopped with Stop()
-func (c *Container) Start() {
- defer debug.Recover()
-
- c.OnLogin()
-
- if c.client == nil {
- return
- }
-
- debug.Print("Starting sync...")
- c.running = true
- c.client.StreamSyncMinAge = 30 * time.Minute
- for {
- select {
- case <-c.stop:
- debug.Print("Stopping sync...")
- c.running = false
- return
- default:
- if err := c.client.Sync(); err != nil {
- if errors.Is(err, mautrix.MUnknownToken) {
- debug.Print("Sync() errored with ", err, " -> logging out")
- // TODO support soft logout
- c.Logout()
- } else {
- debug.Print("Sync() errored", err)
- }
- } else {
- debug.Print("Sync() returned without error")
- }
- }
- }
-}
-
-func (c *Container) HandlePreferences(source mautrix.EventSource, evt *event.Event) {
- if source&mautrix.EventSourceAccountData == 0 {
- return
- }
- orig := c.config.Preferences
- err := json.Unmarshal(evt.Content.VeryRaw, &c.config.Preferences)
- if err != nil {
- debug.Print("Failed to parse updated preferences:", err)
- return
- }
- debug.Printf("Updated preferences: %#v -> %#v", orig, c.config.Preferences)
- if c.config.AuthCache.InitialSyncDone {
- c.ui.HandleNewPreferences()
- }
-}
-
-func (c *Container) Preferences() *config.UserPreferences {
- return &c.config.Preferences
-}
-
-func (c *Container) SendPreferencesToMatrix() {
- defer debug.Recover()
- debug.Printf("Sending updated preferences: %#v", c.config.Preferences)
- err := c.client.SetAccountData(AccountDataGomuksPreferences.Type, &c.config.Preferences)
- if err != nil {
- debug.Print("Failed to update preferences:", err)
- }
-}
-
-func (c *Container) HandleRedaction(source mautrix.EventSource, evt *event.Event) {
- room := c.GetOrCreateRoom(evt.RoomID)
- var redactedEvt *muksevt.Event
- err := c.history.Update(room, evt.Redacts, func(redacted *muksevt.Event) error {
- redacted.Unsigned.RedactedBecause = evt
- redactedEvt = redacted
- return nil
- })
- if err != nil {
- debug.Print("Failed to mark", evt.Redacts, "as redacted:", err)
- return
- } else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
- return
- }
-
- roomView := c.ui.MainView().GetRoom(evt.RoomID)
- if roomView == nil {
- debug.Printf("Failed to handle event %v: No room view found.", evt)
- return
- }
-
- roomView.AddRedaction(redactedEvt)
- if c.syncer.FirstSyncDone {
- c.ui.Render()
- }
-}
-
-var ErrCantEditOthersMessage = errors.New("can't edit message sent by someone else")
-
-func (c *Container) HandleEdit(room *rooms.Room, editsID id.EventID, editEvent *muksevt.Event) {
- var origEvt *muksevt.Event
- err := c.history.Update(room, editsID, func(evt *muksevt.Event) error {
- if editEvent.Sender != evt.Sender {
- return ErrCantEditOthersMessage
- }
- evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent)
- origEvt = evt
- return nil
- })
- if err == ErrCantEditOthersMessage {
- debug.Printf("Ignoring edit %s of %s by %s in %s: original event was sent by someone else", editEvent.ID, editsID, editEvent.Sender, editEvent.RoomID)
- return
- } else if err != nil {
- debug.Print("Failed to store edit in history db:", err)
- return
- } else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
- return
- }
-
- roomView := c.ui.MainView().GetRoom(editEvent.RoomID)
- if roomView == nil {
- debug.Printf("Failed to handle edit event %v: No room view found.", editEvent)
- return
- }
-
- roomView.AddEdit(origEvt)
- if c.syncer.FirstSyncDone {
- c.ui.Render()
- }
-}
-
-func (c *Container) HandleReaction(room *rooms.Room, reactsTo id.EventID, reactEvent *muksevt.Event) {
- rel := reactEvent.Content.AsReaction().RelatesTo
- var origEvt *muksevt.Event
- err := c.history.Update(room, reactsTo, func(evt *muksevt.Event) error {
- if evt.Unsigned.Relations.Annotations.Map == nil {
- evt.Unsigned.Relations.Annotations.Map = make(map[string]int)
- }
- val, _ := evt.Unsigned.Relations.Annotations.Map[rel.Key]
- evt.Unsigned.Relations.Annotations.Map[rel.Key] = val + 1
- origEvt = evt
- return nil
- })
- if err != nil {
- debug.Print("Failed to store reaction in history db:", err)
- return
- } else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
- return
- }
-
- roomView := c.ui.MainView().GetRoom(reactEvent.RoomID)
- if roomView == nil {
- debug.Printf("Failed to handle edit event %v: No room view found.", reactEvent)
- return
- }
-
- roomView.AddReaction(origEvt, rel.Key)
- if c.syncer.FirstSyncDone {
- c.ui.Render()
- }
-}
-
-func (c *Container) HandleEncryptedUnsupported(source mautrix.EventSource, mxEvent *event.Event) {
- mxEvent.Type = muksevt.EventEncryptionUnsupported
- origContent, _ := mxEvent.Content.Parsed.(*event.EncryptedEventContent)
- mxEvent.Content.Parsed = muksevt.EncryptionUnsupportedContent{Original: origContent}
- c.HandleMessage(source, mxEvent)
-}
-
-func (c *Container) HandleEncrypted(source mautrix.EventSource, mxEvent *event.Event) {
- evt, err := c.crypto.DecryptMegolmEvent(mxEvent)
- if err != nil {
- debug.Printf("Failed to decrypt event %s: %v", mxEvent.ID, err)
- mxEvent.Type = muksevt.EventBadEncrypted
- origContent, _ := mxEvent.Content.Parsed.(*event.EncryptedEventContent)
- mxEvent.Content.Parsed = &muksevt.BadEncryptedContent{
- Original: origContent,
- Reason: err.Error(),
- }
- c.HandleMessage(source, mxEvent)
- return
- }
- if evt.Type.IsInRoomVerification() {
- err := c.crypto.ProcessInRoomVerification(evt)
- if err != nil {
- debug.Printf("[Crypto/Error] Failed to process in-room verification event %s of type %s: %v", evt.ID, evt.Type.String(), err)
- } else {
- debug.Printf("[Crypto/Debug] Processed in-room verification event %s of type %s", evt.ID, evt.Type.String())
- }
- } else {
- c.HandleMessage(source, evt)
- }
-}
-
-// HandleMessage is the event handler for the m.room.message timeline event.
-func (c *Container) HandleMessage(source mautrix.EventSource, mxEvent *event.Event) {
- room := c.GetOrCreateRoom(mxEvent.RoomID)
- if source&mautrix.EventSourceLeave != 0 {
- room.HasLeft = true
- return
- } else if source&mautrix.EventSourceState != 0 {
- return
- }
-
- relatable, ok := mxEvent.Content.Parsed.(event.Relatable)
- if ok {
- rel := relatable.GetRelatesTo()
- if editID := rel.GetReplaceID(); len(editID) > 0 {
- c.HandleEdit(room, editID, muksevt.Wrap(mxEvent))
- return
- } else if reactionID := rel.GetAnnotationID(); mxEvent.Type == event.EventReaction && len(reactionID) > 0 {
- c.HandleReaction(room, reactionID, muksevt.Wrap(mxEvent))
- return
- }
- }
-
- events, err := c.history.Append(room, []*event.Event{mxEvent})
- if err != nil {
- debug.Printf("Failed to add event %s to history: %v", mxEvent.ID, err)
- }
- evt := events[0]
-
- if !c.config.AuthCache.InitialSyncDone {
- room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000)
- return
- }
-
- mainView := c.ui.MainView()
-
- roomView := mainView.GetRoom(evt.RoomID)
- if roomView == nil {
- debug.Printf("Failed to handle event %v: No room view found.", evt)
- return
- }
-
- if !room.Loaded() {
- pushRules := c.PushRules().GetActions(room, evt.Event).Should()
- if !pushRules.Notify {
- room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000)
- room.AddUnread(evt.ID, pushRules.Notify, pushRules.Highlight)
- mainView.Bump(room)
- return
- }
- }
-
- message := roomView.AddEvent(evt)
- if message != nil {
- roomView.MxRoom().LastReceivedMessage = message.Time()
- if c.syncer.FirstSyncDone && evt.Sender != c.config.UserID {
- pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt.Event).Should()
- mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
- c.ui.Render()
- }
- } else {
- debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type.Repr(), evt.Content.Raw, evt.Sender, evt.RoomID)
- }
-}
-
-// HandleMembership is the event handler for the m.room.member state event.
-func (c *Container) HandleMembership(source mautrix.EventSource, evt *event.Event) {
- isLeave := source&mautrix.EventSourceLeave != 0
- isTimeline := source&mautrix.EventSourceTimeline != 0
- if isLeave {
- c.GetOrCreateRoom(evt.RoomID).HasLeft = true
- }
- isNonTimelineLeave := isLeave && !isTimeline
- if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave {
- return
- } else if evt.StateKey != nil && id.UserID(*evt.StateKey) == c.config.UserID {
- c.processOwnMembershipChange(evt)
- } else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) {
- // We don't care about other users' membership events in the initial sync or chats we've left.
- return
- }
-
- c.HandleMessage(source, evt)
-}
-
-func (c *Container) processOwnMembershipChange(evt *event.Event) {
- membership := evt.Content.AsMember().Membership
- prevMembership := event.MembershipLeave
- if evt.Unsigned.PrevContent != nil {
- prevMembership = evt.Unsigned.PrevContent.AsMember().Membership
- }
- debug.Printf("Processing own membership change: %s->%s in %s", prevMembership, membership, evt.RoomID)
- if membership == prevMembership {
- return
- }
- room := c.GetRoom(evt.RoomID)
- switch membership {
- case "join":
- room.HasLeft = false
- if c.config.AuthCache.InitialSyncDone {
- c.ui.MainView().UpdateTags(room)
- }
- fallthrough
- case "invite":
- if c.config.AuthCache.InitialSyncDone {
- c.ui.MainView().AddRoom(room)
- }
- case "leave":
- case "ban":
- if c.config.AuthCache.InitialSyncDone {
- c.ui.MainView().RemoveRoom(room)
- }
- room.HasLeft = true
- room.Unload()
- default:
- return
- }
- c.ui.Render()
-}
-
-func (c *Container) parseReadReceipt(evt *event.Event) (largestTimestampEvent id.EventID) {
- var largestTimestamp int64
-
- for eventID, receipts := range *evt.Content.AsReceipt() {
- myInfo, ok := receipts.Read[c.config.UserID]
- if !ok {
- continue
- }
-
- if myInfo.Timestamp > largestTimestamp {
- largestTimestamp = myInfo.Timestamp
- largestTimestampEvent = eventID
- }
- }
- return
-}
-
-func (c *Container) HandleReadReceipt(source mautrix.EventSource, evt *event.Event) {
- if source&mautrix.EventSourceLeave != 0 {
- return
- }
-
- lastReadEvent := c.parseReadReceipt(evt)
- if len(lastReadEvent) == 0 {
- return
- }
-
- room := c.GetRoom(evt.RoomID)
- if room != nil {
- room.MarkRead(lastReadEvent)
- if c.config.AuthCache.InitialSyncDone {
- c.ui.Render()
- }
- }
-}
-
-func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]id.UserID {
- directChats := make(map[*rooms.Room]id.UserID)
- for userID, roomIDList := range *evt.Content.AsDirectChats() {
- for _, roomID := range roomIDList {
- // TODO we shouldn't create direct chat rooms that we aren't in
- room := c.GetOrCreateRoom(roomID)
- if room != nil && !room.HasLeft {
- directChats[room] = userID
- }
- }
- }
- return directChats
-}
-
-func (c *Container) HandleDirectChatInfo(_ mautrix.EventSource, evt *event.Event) {
- directChats := c.parseDirectChatInfo(evt)
- for _, room := range c.config.Rooms.Map {
- userID, isDirect := directChats[room]
- if isDirect != room.IsDirect {
- room.IsDirect = isDirect
- room.OtherUser = userID
- if c.config.AuthCache.InitialSyncDone {
- c.ui.MainView().UpdateTags(room)
- }
- }
- }
-}
-
-// HandlePushRules is the event handler for the m.push_rules account data event.
-func (c *Container) HandlePushRules(_ mautrix.EventSource, evt *event.Event) {
- debug.Print("Received updated push rules")
- var err error
- c.config.PushRules, err = pushrules.EventToPushRules(evt)
- if err != nil {
- debug.Print("Failed to convert event to push rules:", err)
- return
- }
- c.config.SavePushRules()
-}
-
-// HandleTag is the event handler for the m.tag account data event.
-func (c *Container) HandleTag(_ mautrix.EventSource, evt *event.Event) {
- room := c.GetOrCreateRoom(evt.RoomID)
-
- tags := evt.Content.AsTag().Tags
-
- newTags := make([]rooms.RoomTag, len(tags))
- index := 0
- for tag, info := range tags {
- order := json.Number("0.5")
- if len(info.Order) > 0 {
- order = info.Order
- }
- newTags[index] = rooms.RoomTag{
- Tag: tag,
- Order: order,
- }
- index++
- }
- room.RawTags = newTags
-
- if c.config.AuthCache.InitialSyncDone {
- mainView := c.ui.MainView()
- mainView.UpdateTags(room)
- }
-}
-
-// HandleTyping is the event handler for the m.typing event.
-func (c *Container) HandleTyping(_ mautrix.EventSource, evt *event.Event) {
- if !c.config.AuthCache.InitialSyncDone {
- return
- }
- c.ui.MainView().SetTyping(evt.RoomID, evt.Content.AsTyping().UserIDs)
-}
-
-func (c *Container) MarkRead(roomID id.RoomID, eventID id.EventID) {
- go func() {
- defer debug.Recover()
- err := c.client.MarkRead(roomID, eventID)
- if err != nil {
- debug.Printf("Failed to mark %s in %s as read: %v", eventID, roomID, err)
- }
- }()
-}
-
-func (c *Container) PrepareMediaMessage(room *rooms.Room, path string, rel *ifc.Relation) (*muksevt.Event, error) {
- resp, err := c.UploadMedia(path, room.Encrypted)
- if err != nil {
- return nil, err
- }
- content := event.MessageEventContent{
- MsgType: resp.MsgType,
- Body: resp.Name,
- Info: resp.Info,
- }
- if resp.EncryptionInfo != nil {
- content.File = &event.EncryptedFileInfo{
- EncryptedFile: *resp.EncryptionInfo,
- URL: resp.ContentURI.CUString(),
- }
- } else {
- content.URL = resp.ContentURI.CUString()
- }
-
- return c.prepareEvent(room.ID, &content, rel), nil
-}
-
-func (c *Container) PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, rel *ifc.Relation) *muksevt.Event {
- var content event.MessageEventContent
- if html != "" {
- content = event.MessageEventContent{
- FormattedBody: html,
- Format: event.FormatHTML,
- Body: text,
- MsgType: msgtype,
- }
- } else {
- content = format.RenderMarkdown(text, !c.config.Preferences.DisableMarkdown, !c.config.Preferences.DisableHTML)
- content.MsgType = msgtype
- }
-
- return c.prepareEvent(roomID, &content, rel)
-}
-
-func (c *Container) prepareEvent(roomID id.RoomID, content *event.MessageEventContent, rel *ifc.Relation) *muksevt.Event {
- if rel != nil && rel.Type == event.RelReplace {
- contentCopy := *content
- content.NewContent = &contentCopy
- content.Body = "* " + content.Body
- if len(content.FormattedBody) > 0 {
- content.FormattedBody = "* " + content.FormattedBody
- }
- content.RelatesTo = &event.RelatesTo{
- Type: event.RelReplace,
- EventID: rel.Event.ID,
- }
- } else if rel != nil && rel.Type == event.RelReply {
- content.SetReply(rel.Event.Event)
- }
-
- txnID := c.client.TxnID()
- localEcho := muksevt.Wrap(&event.Event{
- ID: id.EventID(txnID),
- Sender: c.config.UserID,
- Type: event.EventMessage,
- Timestamp: time.Now().UnixNano() / 1e6,
- RoomID: roomID,
- Content: event.Content{Parsed: content},
- Unsigned: event.Unsigned{TransactionID: txnID},
- })
- localEcho.Gomuks.OutgoingState = muksevt.StateLocalEcho
- if rel != nil && rel.Type == event.RelReplace {
- localEcho.ID = rel.Event.ID
- localEcho.Gomuks.Edits = []*muksevt.Event{localEcho}
- }
- return localEcho
-}
-
-func (c *Container) Redact(roomID id.RoomID, eventID id.EventID, reason string) error {
- defer debug.Recover()
- _, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason})
- return err
-}
-
-// SendMessage sends the given event.
-func (c *Container) SendEvent(evt *muksevt.Event) (id.EventID, error) {
- defer debug.Recover()
-
- _, _ = c.client.UserTyping(evt.RoomID, false, 0)
- c.typing = 0
- room := c.GetRoom(evt.RoomID)
- if room != nil && room.Encrypted && c.crypto != nil && evt.Type != event.EventReaction {
- encrypted, err := c.crypto.EncryptMegolmEvent(evt.RoomID, evt.Type, &evt.Content)
- if err != nil {
- if isBadEncryptError(err) {
- return "", err
- }
- debug.Print("Got", err, "while trying to encrypt message, sharing group session and trying again...")
- err = c.crypto.ShareGroupSession(room.ID, room.GetMemberList())
- if err != nil {
- return "", err
- }
- encrypted, err = c.crypto.EncryptMegolmEvent(evt.RoomID, evt.Type, &evt.Content)
- if err != nil {
- return "", err
- }
- }
- evt.Type = event.EventEncrypted
- evt.Content = event.Content{Parsed: encrypted}
- }
- resp, err := c.client.SendMessageEvent(evt.RoomID, evt.Type, &evt.Content, mautrix.ReqSendEvent{TransactionID: evt.Unsigned.TransactionID})
- if err != nil {
- return "", err
- }
- return resp.EventID, nil
-}
-
-func (c *Container) UploadMedia(path string, encrypt bool) (*ifc.UploadedMediaInfo, error) {
- var err error
- path, err = filepath.Abs(path)
- if err != nil {
- return nil, fmt.Errorf("failed to get absolute path: %w", err)
- }
-
- msgtype, info, err := getMediaInfo(path)
- if err != nil {
- return nil, err
- }
-
- file, err := os.Open(path)
- if err != nil {
- return nil, fmt.Errorf("failed to open file: %w", err)
- }
-
- stat, err := file.Stat()
- if err != nil {
- return nil, fmt.Errorf("failed to get file info: %w", err)
- }
-
- uploadFileName := stat.Name()
- uploadMimeType := info.MimeType
-
- var content io.Reader
- var encryptionInfo *attachment.EncryptedFile
- if encrypt {
- uploadMimeType = "application/octet-stream"
- uploadFileName = ""
- encryptionInfo = attachment.NewEncryptedFile()
- content = encryptionInfo.EncryptStream(file)
- } else {
- content = file
- }
-
- resp, err := c.client.UploadMedia(mautrix.ReqUploadMedia{
- Content: content,
- ContentLength: stat.Size(),
- ContentType: uploadMimeType,
- FileName: uploadFileName,
- })
-
- if err != nil {
- return nil, err
- }
-
- return &ifc.UploadedMediaInfo{
- RespMediaUpload: resp,
- EncryptionInfo: encryptionInfo,
- Name: stat.Name(),
- MsgType: msgtype,
- Info: &info,
- }, nil
-}
-
-func (c *Container) sendTypingAsync(roomID id.RoomID, typing bool, timeout int64) {
- defer debug.Recover()
- _, _ = c.client.UserTyping(roomID, typing, timeout)
-}
-
-// SendTyping sets whether or not the user is typing in the given room.
-func (c *Container) SendTyping(roomID id.RoomID, typing bool) {
- ts := time.Now().Unix()
- if (c.typing > ts && typing) || (c.typing == 0 && !typing) {
- return
- }
-
- if typing {
- go c.sendTypingAsync(roomID, true, 20000)
- c.typing = ts + 15
- } else {
- go c.sendTypingAsync(roomID, false, 0)
- c.typing = 0
- }
-}
-
-// CreateRoom attempts to create a new room and join the user.
-func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) {
- resp, err := c.client.CreateRoom(req)
- if err != nil {
- return nil, err
- }
- room := c.GetOrCreateRoom(resp.RoomID)
- return room, nil
-}
-
-// JoinRoom makes the current user try to join the given room.
-func (c *Container) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) {
- resp, err := c.client.JoinRoom(string(roomID), server, nil)
- if err != nil {
- return nil, err
- }
-
- room := c.GetOrCreateRoom(resp.RoomID)
- room.HasLeft = false
- return room, nil
-}
-
-// LeaveRoom makes the current user leave the given room.
-func (c *Container) LeaveRoom(roomID id.RoomID) error {
- _, err := c.client.LeaveRoom(roomID)
- if err != nil {
- return err
- }
-
- node := c.GetOrCreateRoom(roomID)
- node.HasLeft = true
- node.Unload()
- return nil
-}
-
-func (c *Container) FetchMembers(room *rooms.Room) error {
- debug.Print("Fetching member list for", room.ID)
- members, err := c.client.Members(room.ID, mautrix.ReqMembers{At: room.LastPrevBatch})
- if err != nil {
- return err
- }
- debug.Printf("Fetched %d members for %s", len(members.Chunk), room.ID)
- for _, evt := range members.Chunk {
- err := evt.Content.ParseRaw(evt.Type)
- if err != nil {
- debug.Printf("Failed to parse member event of %s: %v", evt.GetStateKey(), err)
- continue
- }
- room.UpdateState(evt)
- }
- room.MembersFetched = true
- return nil
-}
-
-// GetHistory fetches room history.
-func (c *Container) GetHistory(room *rooms.Room, limit int, dbPointer uint64) ([]*muksevt.Event, uint64, error) {
- events, newDBPointer, err := c.history.Load(room, limit, dbPointer)
- if err != nil {
- return nil, dbPointer, err
- }
- if len(events) > 0 {
- debug.Printf("Loaded %d events for %s from local cache", len(events), room.ID)
- return events, newDBPointer, nil
- }
- resp, err := c.client.Messages(room.ID, room.PrevBatch, "", 'b', nil, limit)
- if err != nil {
- return nil, dbPointer, err
- }
- debug.Printf("Loaded %d events for %s from server from %s to %s", len(resp.Chunk), room.ID, resp.Start, resp.End)
- for i, evt := range resp.Chunk {
- err := evt.Content.ParseRaw(evt.Type)
- if err != nil {
- debug.Printf("Failed to unmarshal content of event %s (type %s) by %s in %s: %v\n%s", evt.ID, evt.Type.Repr(), evt.Sender, evt.RoomID, err, string(evt.Content.VeryRaw))
- }
-
- if evt.Type == event.EventEncrypted {
- if c.crypto == nil {
- evt.Type = muksevt.EventEncryptionUnsupported
- origContent, _ := evt.Content.Parsed.(*event.EncryptedEventContent)
- evt.Content.Parsed = muksevt.EncryptionUnsupportedContent{Original: origContent}
- } else {
- decrypted, err := c.crypto.DecryptMegolmEvent(evt)
- if err != nil {
- debug.Printf("Failed to decrypt event %s: %v", evt.ID, err)
- evt.Type = muksevt.EventBadEncrypted
- origContent, _ := evt.Content.Parsed.(*event.EncryptedEventContent)
- evt.Content.Parsed = &muksevt.BadEncryptedContent{
- Original: origContent,
- Reason: err.Error(),
- }
- } else {
- resp.Chunk[i] = decrypted
- }
- }
- }
- }
- for _, evt := range resp.State {
- room.UpdateState(evt)
- }
- room.PrevBatch = resp.End
- c.config.Rooms.Put(room)
- if len(resp.Chunk) == 0 {
- return []*muksevt.Event{}, dbPointer, nil
- }
- // TODO newDBPointer isn't accurate in this case yet, fix later
- events, newDBPointer, err = c.history.Prepend(room, resp.Chunk)
- if err != nil {
- return nil, dbPointer, err
- }
- return events, dbPointer, nil
-}
-
-func (c *Container) GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error) {
- evt, err := c.history.Get(room, eventID)
- if err != nil && err != EventNotFoundError {
- debug.Printf("Failed to get event %s from local cache: %v", eventID, err)
- } else if evt != nil {
- debug.Printf("Found event %s in local cache", eventID)
- return evt, err
- }
- mxEvent, err := c.client.GetEvent(room.ID, eventID)
- if err != nil {
- return nil, err
- }
- err = mxEvent.Content.ParseRaw(mxEvent.Type)
- if err != nil {
- return nil, err
- }
- debug.Printf("Loaded event %s from server", eventID)
- return muksevt.Wrap(mxEvent), nil
-}
-
-// GetOrCreateRoom gets the room instance stored in the session.
-func (c *Container) GetOrCreateRoom(roomID id.RoomID) *rooms.Room {
- return c.config.Rooms.GetOrCreate(roomID)
-}
-
-// GetRoom gets the room instance stored in the session.
-func (c *Container) GetRoom(roomID id.RoomID) *rooms.Room {
- return c.config.Rooms.Get(roomID)
-}
-
-func cp(src, dst string) error {
- in, err := os.Open(src)
- if err != nil {
- return err
- }
- defer in.Close()
-
- out, err := os.Create(dst)
- if err != nil {
- return err
- }
- defer out.Close()
-
- _, err = io.Copy(out, in)
- if err != nil {
- return err
- }
- return out.Close()
-}
-
-func (c *Container) DownloadToDisk(uri id.ContentURI, file *attachment.EncryptedFile, target string) (fullPath string, err error) {
- cachePath := c.GetCachePath(uri)
- if target == "" {
- fullPath = cachePath
- } else if !path.IsAbs(target) {
- fullPath = path.Join(c.config.DownloadDir, target)
- } else {
- fullPath = target
- }
-
- if _, statErr := os.Stat(cachePath); os.IsNotExist(statErr) {
- var body io.ReadCloser
- body, err = c.client.Download(uri)
- if err != nil {
- return
- }
-
- var data []byte
- data, err = ioutil.ReadAll(body)
- _ = body.Close()
- if err != nil {
- return
- }
-
- if file != nil {
- err = file.DecryptInPlace(data)
- if err != nil {
- return
- }
- }
-
- err = ioutil.WriteFile(cachePath, data, 0600)
- if err != nil {
- return
- }
- }
-
- if fullPath != cachePath {
- err = os.MkdirAll(path.Dir(fullPath), 0700)
- if err != nil {
- return
- }
- err = cp(cachePath, fullPath)
- }
-
- return
-}
-
-// Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors.
-//
-// The file will be either read from the media cache (if found) or downloaded from the server.
-func (c *Container) Download(uri id.ContentURI, file *attachment.EncryptedFile) (data []byte, err error) {
- cacheFile := c.GetCachePath(uri)
- var info os.FileInfo
- if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() {
- data, err = ioutil.ReadFile(cacheFile)
- if err == nil {
- return
- }
- }
-
- data, err = c.download(uri, file, cacheFile)
- return
-}
-
-func (c *Container) GetDownloadURL(uri id.ContentURI, file *attachment.EncryptedFile) string {
- addr, _ := url.Parse(c.mediaProxyURL)
- addr.Path = path.Join(addr.Path, uri.Homeserver, uri.FileID)
- if file != nil {
- addr.RawQuery = (&url.Values{
- "k": {file.Key.Key},
- "iv": {file.InitVector},
- "hash": {file.Hashes.SHA256},
- }).Encode()
- }
- return addr.String()
-}
-
-func (c *Container) download(uri id.ContentURI, file *attachment.EncryptedFile, cacheFile string) (data []byte, err error) {
- var body io.ReadCloser
- body, err = c.client.Download(uri)
- if err != nil {
- return
- }
-
- data, err = ioutil.ReadAll(body)
- _ = body.Close()
- if err != nil {
- return
- }
-
- if file != nil {
- err = file.DecryptInPlace(data)
- if err != nil {
- return
- }
- }
-
- err = ioutil.WriteFile(cacheFile, data, 0600)
- return
-}
-
-// GetCachePath gets the path to the cached version of the given homeserver:fileID combination.
-// The file may or may not exist, use Download() to ensure it has been cached.
-func (c *Container) GetCachePath(uri id.ContentURI) string {
- dir := filepath.Join(c.config.MediaDir, uri.Homeserver)
-
- err := os.MkdirAll(dir, 0700)
- if err != nil {
- return ""
- }
-
- return filepath.Join(dir, uri.FileID)
-}
diff --git a/matrix/mediainfo.go b/matrix/mediainfo.go
deleted file mode 100644
index 5de4171..0000000
--- a/matrix/mediainfo.go
+++ /dev/null
@@ -1,106 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package matrix
-
-import (
- "context"
- "fmt"
- "image"
- "os"
- "strings"
- "time"
-
- "github.com/gabriel-vasile/mimetype"
- "gopkg.in/vansante/go-ffprobe.v2"
-
- "maunium.net/go/mautrix/event"
-
- "maunium.net/go/gomuks/debug"
-)
-
-func getImageInfo(path string) (event.FileInfo, error) {
- var info event.FileInfo
- file, err := os.Open(path)
- if err != nil {
- return info, fmt.Errorf("failed to open image to get info: %w", err)
- }
- cfg, _, err := image.DecodeConfig(file)
- if err != nil {
- return info, fmt.Errorf("failed to get image info: %w", err)
- }
- info.Width = cfg.Width
- info.Height = cfg.Height
- return info, nil
-}
-
-func getFFProbeInfo(mimeClass, path string) (msgtype event.MessageType, info event.FileInfo, err error) {
- ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelFn()
- var probedInfo *ffprobe.ProbeData
- probedInfo, err = ffprobe.ProbeURL(ctx, path)
- if err != nil {
- err = fmt.Errorf("failed to get %s info with ffprobe: %w", mimeClass, err)
- return
- }
- if mimeClass == "audio" {
- msgtype = event.MsgAudio
- stream := probedInfo.FirstAudioStream()
- if stream != nil {
- info.Duration = int(stream.DurationTs)
- }
- } else {
- msgtype = event.MsgVideo
- stream := probedInfo.FirstVideoStream()
- if stream != nil {
- info.Duration = int(stream.DurationTs)
- info.Width = stream.Width
- info.Height = stream.Height
- }
- }
- return
-}
-
-func getMediaInfo(path string) (msgtype event.MessageType, info event.FileInfo, err error) {
- var mime *mimetype.MIME
- mime, err = mimetype.DetectFile(path)
- if err != nil {
- err = fmt.Errorf("failed to get content type: %w", err)
- return
- }
-
- mimeClass := strings.SplitN(mime.String(), "/", 2)[0]
- switch mimeClass {
- case "image":
- msgtype = event.MsgImage
- info, err = getImageInfo(path)
- if err != nil {
- debug.Printf("Failed to get image info for %s: %v", err)
- err = nil
- }
- case "audio", "video":
- msgtype, info, err = getFFProbeInfo(mimeClass, path)
- if err != nil {
- debug.Printf("Failed to get ffprobe info for %s: %v", err)
- err = nil
- }
- default:
- msgtype = event.MsgFile
- }
- info.MimeType = mime.String()
-
- return
-}
diff --git a/matrix/muksevt/content.go b/matrix/muksevt/content.go
deleted file mode 100644
index fb78323..0000000
--- a/matrix/muksevt/content.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package muksevt
-
-import (
- "encoding/gob"
- "reflect"
-
- "maunium.net/go/mautrix/event"
-)
-
-var EventBadEncrypted = event.Type{Type: "net.maunium.gomuks.bad_encrypted", Class: event.MessageEventType}
-var EventEncryptionUnsupported = event.Type{Type: "net.maunium.gomuks.encryption_unsupported", Class: event.MessageEventType}
-
-type BadEncryptedContent struct {
- Original *event.EncryptedEventContent `json:"-"`
-
- Reason string `json:"-"`
-}
-
-type EncryptionUnsupportedContent struct {
- Original *event.EncryptedEventContent `json:"-"`
-}
-
-func init() {
- gob.Register(&BadEncryptedContent{})
- gob.Register(&EncryptionUnsupportedContent{})
- event.TypeMap[EventBadEncrypted] = reflect.TypeOf(&BadEncryptedContent{})
- event.TypeMap[EventEncryptionUnsupported] = reflect.TypeOf(&EncryptionUnsupportedContent{})
-}
diff --git a/matrix/muksevt/event.go b/matrix/muksevt/event.go
deleted file mode 100644
index 0d3303d..0000000
--- a/matrix/muksevt/event.go
+++ /dev/null
@@ -1,53 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package muksevt
-
-import (
- "maunium.net/go/mautrix/event"
-)
-
-type Event struct {
- *event.Event
- Gomuks GomuksContent `json:"-"`
-}
-
-func (evt *Event) SomewhatDangerousCopy() *Event {
- base := *evt.Event
- content := *base.Content.Parsed.(*event.MessageEventContent)
- evt.Content.Parsed = &content
- return &Event{
- Event: &base,
- Gomuks: evt.Gomuks,
- }
-}
-
-func Wrap(event *event.Event) *Event {
- return &Event{Event: event}
-}
-
-type OutgoingState int
-
-const (
- StateDefault OutgoingState = iota
- StateLocalEcho
- StateSendFail
-)
-
-type GomuksContent struct {
- OutgoingState OutgoingState
- Edits []*Event
-}
diff --git a/matrix/nocrypto.go b/matrix/nocrypto.go
deleted file mode 100644
index 90b3dd9..0000000
--- a/matrix/nocrypto.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// This contains no-op stubs of the methods in crypto.go for non-cgo builds with crypto disabled.
-
-//go:build !cgo
-
-package matrix
-
-func isBadEncryptError(err error) bool {
- return false
-}
-
-func (c *Container) initCrypto() error {
- return nil
-}
-
-func (c *Container) cryptoOnLogin() {}
diff --git a/matrix/rooms/doc.go b/matrix/rooms/doc.go
deleted file mode 100644
index 2a4ff4b..0000000
--- a/matrix/rooms/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package rooms contains a representation for Matrix rooms and utilities to parse state events.
-package rooms
diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go
deleted file mode 100644
index da39e3e..0000000
--- a/matrix/rooms/room.go
+++ /dev/null
@@ -1,715 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package rooms
-
-import (
- "compress/gzip"
- "encoding/gob"
- "encoding/json"
- "fmt"
- "os"
- "time"
-
- sync "github.com/sasha-s/go-deadlock"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/debug"
-)
-
-func init() {
- gob.Register(map[string]interface{}{})
- gob.Register([]interface{}{})
-}
-
-type RoomNameSource int
-
-const (
- UnknownRoomName RoomNameSource = iota
- MemberRoomName
- CanonicalAliasRoomName
- ExplicitRoomName
-)
-
-// RoomTag is a tag given to a specific room.
-type RoomTag struct {
- // The name of the tag.
- Tag string
- // The order of the tag.
- Order json.Number
-}
-
-type UnreadMessage struct {
- EventID id.EventID
- Counted bool
- Highlight bool
-}
-
-type Member struct {
- event.MemberEventContent
-
- // The user who sent the membership event
- Sender id.UserID `json:"-"`
-}
-
-// Room represents a single Matrix room.
-type Room struct {
- // The room ID.
- ID id.RoomID
-
- // Whether or not the user has left the room.
- HasLeft bool
- // Whether or not the room is encrypted.
- Encrypted bool
-
- // The first batch of events that has been fetched for this room.
- // Used for fetching additional history.
- PrevBatch string
- // The last_batch field from the most recent sync. Used for fetching member lists.
- LastPrevBatch string
- // The MXID of the user whose session this room was created for.
- SessionUserID id.UserID
- SessionMember *Member
-
- // The number of unread messages that were notified about.
- UnreadMessages []UnreadMessage
- unreadCountCache *int
- highlightCache *bool
- lastMarkedRead id.EventID
- // Whether or not this room is marked as a direct chat.
- IsDirect bool
- OtherUser id.UserID
-
- // List of tags given to this room.
- RawTags []RoomTag
- // Timestamp of previously received actual message.
- LastReceivedMessage time.Time
-
- // The lazy loading summary for this room.
- Summary mautrix.LazyLoadSummary
- // Whether or not the members for this room have been fetched from the server.
- MembersFetched bool
- // Room state cache.
- state map[event.Type]map[string]*event.Event
- // MXID -> Member cache calculated from membership events.
- memberCache map[id.UserID]*Member
- exMemberCache map[id.UserID]*Member
- // The first two non-SessionUserID members in the room. Calculated at
- // the same time as memberCache.
- firstMemberCache *Member
- secondMemberCache *Member
- // The name of the room. Calculated from the state event name,
- // canonical_alias or alias or the member cache.
- NameCache string
- // The event type from which the name cache was calculated from.
- nameCacheSource RoomNameSource
- // The topic of the room. Directly fetched from the m.room.topic state event.
- topicCache string
- // The canonical alias of the room. Directly fetched from the m.room.canonical_alias state event.
- CanonicalAliasCache id.RoomAlias
- // Whether or not the room has been tombstoned.
- replacedCache bool
- // The room ID that replaced this room.
- replacedByCache *id.RoomID
-
- // Path for state store file.
- path string
- // Room cache object
- cache *RoomCache
- // Lock for state and other room stuff.
- lock sync.RWMutex
- // Pre/post un/load hooks
- preUnload func() bool
- preLoad func() bool
- postUnload func()
- postLoad func()
- // Whether or not the room state has changed
- changed bool
-
- // Room state cache linked list.
- prev *Room
- next *Room
- touch int64
-}
-
-func debugPrintError(fn func() error, message string) {
- if err := fn(); err != nil {
- debug.Printf("%s: %v", message, err)
- }
-}
-
-func (room *Room) Loaded() bool {
- return room.state != nil
-}
-
-func (room *Room) Load() {
- room.cache.TouchNode(room)
- if room.Loaded() {
- return
- }
- if room.preLoad != nil && !room.preLoad() {
- return
- }
- room.lock.Lock()
- room.load()
- room.lock.Unlock()
- if room.postLoad != nil {
- room.postLoad()
- }
-}
-
-func (room *Room) load() {
- if room.Loaded() {
- return
- }
- debug.Print("Loading state for room", room.ID, "from disk")
- room.state = make(map[event.Type]map[string]*event.Event)
- file, err := os.OpenFile(room.path, os.O_RDONLY, 0600)
- if err != nil {
- if !os.IsNotExist(err) {
- debug.Print("Failed to open room state file for reading:", err)
- } else {
- debug.Print("Room state file for", room.ID, "does not exist")
- }
- return
- }
- defer debugPrintError(file.Close, "Failed to close room state file after reading")
- cmpReader, err := gzip.NewReader(file)
- if err != nil {
- debug.Print("Failed to open room state gzip reader:", err)
- return
- }
- defer debugPrintError(cmpReader.Close, "Failed to close room state gzip reader")
- dec := gob.NewDecoder(cmpReader)
- if err = dec.Decode(&room.state); err != nil {
- debug.Print("Failed to decode room state:", err)
- }
- room.changed = false
-}
-
-func (room *Room) Touch() {
- room.cache.TouchNode(room)
-}
-
-func (room *Room) Unload() bool {
- if room.preUnload != nil && !room.preUnload() {
- return false
- }
- debug.Print("Unloading", room.ID)
- room.Save()
- room.state = nil
- room.memberCache = nil
- room.exMemberCache = nil
- room.firstMemberCache = nil
- room.secondMemberCache = nil
- if room.postUnload != nil {
- room.postUnload()
- }
- return true
-}
-
-func (room *Room) SetPreUnload(fn func() bool) {
- room.preUnload = fn
-}
-
-func (room *Room) SetPreLoad(fn func() bool) {
- room.preLoad = fn
-}
-
-func (room *Room) SetPostUnload(fn func()) {
- room.postUnload = fn
-}
-
-func (room *Room) SetPostLoad(fn func()) {
- room.postLoad = fn
-}
-
-func (room *Room) Save() {
- if !room.Loaded() {
- debug.Print("Failed to save room", room.ID, "state: room not loaded")
- return
- }
- if !room.changed {
- debug.Print("Not saving", room.ID, "as state hasn't changed")
- return
- }
- debug.Print("Saving state for room", room.ID, "to disk")
- file, err := os.OpenFile(room.path, os.O_WRONLY|os.O_CREATE, 0600)
- if err != nil {
- debug.Print("Failed to open room state file for writing:", err)
- return
- }
- defer debugPrintError(file.Close, "Failed to close room state file after writing")
- cmpWriter := gzip.NewWriter(file)
- defer debugPrintError(cmpWriter.Close, "Failed to close room state gzip writer")
- enc := gob.NewEncoder(cmpWriter)
- room.lock.RLock()
- defer room.lock.RUnlock()
- if err := enc.Encode(&room.state); err != nil {
- debug.Print("Failed to encode room state:", err)
- }
-}
-
-// MarkRead clears the new message statuses on this room.
-func (room *Room) MarkRead(eventID id.EventID) bool {
- room.lock.Lock()
- defer room.lock.Unlock()
- if room.lastMarkedRead == eventID {
- return false
- }
- room.lastMarkedRead = eventID
- readToIndex := -1
- for index, unreadMessage := range room.UnreadMessages {
- if unreadMessage.EventID == eventID {
- readToIndex = index
- }
- }
- if readToIndex >= 0 {
- room.UnreadMessages = room.UnreadMessages[readToIndex+1:]
- room.highlightCache = nil
- room.unreadCountCache = nil
- }
- return true
-}
-
-func (room *Room) UnreadCount() int {
- room.lock.Lock()
- defer room.lock.Unlock()
- if room.unreadCountCache == nil {
- room.unreadCountCache = new(int)
- for _, unreadMessage := range room.UnreadMessages {
- if unreadMessage.Counted {
- *room.unreadCountCache++
- }
- }
- }
- return *room.unreadCountCache
-}
-
-func (room *Room) Highlighted() bool {
- room.lock.Lock()
- defer room.lock.Unlock()
- if room.highlightCache == nil {
- room.highlightCache = new(bool)
- for _, unreadMessage := range room.UnreadMessages {
- if unreadMessage.Highlight {
- *room.highlightCache = true
- break
- }
- }
- }
- return *room.highlightCache
-}
-
-func (room *Room) HasNewMessages() bool {
- return len(room.UnreadMessages) > 0
-}
-
-func (room *Room) AddUnread(eventID id.EventID, counted, highlight bool) {
- room.lock.Lock()
- defer room.lock.Unlock()
- room.UnreadMessages = append(room.UnreadMessages, UnreadMessage{
- EventID: eventID,
- Counted: counted,
- Highlight: highlight,
- })
- if counted {
- if room.unreadCountCache == nil {
- room.unreadCountCache = new(int)
- }
- *room.unreadCountCache++
- }
- if highlight {
- if room.highlightCache == nil {
- room.highlightCache = new(bool)
- }
- *room.highlightCache = true
- }
-}
-
-var (
- tagDirect = RoomTag{"net.maunium.gomuks.fake.direct", "0.5"}
- tagInvite = RoomTag{"net.maunium.gomuks.fake.invite", "0.5"}
- tagDefault = RoomTag{"", "0.5"}
- tagLeave = RoomTag{"net.maunium.gomuks.fake.leave", "0.5"}
-)
-
-func (room *Room) Tags() []RoomTag {
- room.lock.RLock()
- defer room.lock.RUnlock()
- if len(room.RawTags) == 0 {
- if room.IsDirect {
- return []RoomTag{tagDirect}
- } else if room.SessionMember != nil && room.SessionMember.Membership == event.MembershipInvite {
- return []RoomTag{tagInvite}
- } else if room.SessionMember != nil && room.SessionMember.Membership != event.MembershipJoin {
- return []RoomTag{tagLeave}
- }
- return []RoomTag{tagDefault}
- }
- return room.RawTags
-}
-
-func (room *Room) UpdateSummary(summary mautrix.LazyLoadSummary) {
- if summary.JoinedMemberCount != nil {
- room.Summary.JoinedMemberCount = summary.JoinedMemberCount
- }
- if summary.InvitedMemberCount != nil {
- room.Summary.InvitedMemberCount = summary.InvitedMemberCount
- }
- if summary.Heroes != nil {
- room.Summary.Heroes = summary.Heroes
- }
- if room.nameCacheSource <= MemberRoomName {
- room.NameCache = ""
- }
-}
-
-// UpdateState updates the room's current state with the given Event. This will clobber events based
-// on the type/state_key combination.
-func (room *Room) UpdateState(evt *event.Event) {
- if evt.StateKey == nil {
- panic("Tried to UpdateState() event with no state key.")
- }
- room.Load()
- room.lock.Lock()
- defer room.lock.Unlock()
- room.changed = true
- _, exists := room.state[evt.Type]
- if !exists {
- room.state[evt.Type] = make(map[string]*event.Event)
- }
- switch content := evt.Content.Parsed.(type) {
- case *event.RoomNameEventContent:
- room.NameCache = content.Name
- room.nameCacheSource = ExplicitRoomName
- case *event.CanonicalAliasEventContent:
- if room.nameCacheSource <= CanonicalAliasRoomName {
- room.NameCache = string(content.Alias)
- room.nameCacheSource = CanonicalAliasRoomName
- }
- room.CanonicalAliasCache = content.Alias
- case *event.MemberEventContent:
- if room.nameCacheSource <= MemberRoomName {
- room.NameCache = ""
- }
- room.updateMemberState(id.UserID(evt.GetStateKey()), evt.Sender, content)
- case *event.TopicEventContent:
- room.topicCache = content.Topic
- case *event.EncryptionEventContent:
- if content.Algorithm == id.AlgorithmMegolmV1 {
- room.Encrypted = true
- }
- }
-
- if evt.Type != event.StateMember {
- debug.Printf("Updating state %s#%s for %s", evt.Type.String(), evt.GetStateKey(), room.ID)
- }
-
- room.state[evt.Type][*evt.StateKey] = evt
-}
-
-func (room *Room) updateMemberState(userID, sender id.UserID, content *event.MemberEventContent) {
- if userID == room.SessionUserID {
- debug.Print("Updating session user state:", content)
- room.SessionMember = room.eventToMember(userID, sender, content)
- }
- if room.memberCache != nil {
- member := room.eventToMember(userID, sender, content)
- if member.Membership.IsInviteOrJoin() {
- existingMember, ok := room.memberCache[userID]
- if ok {
- *existingMember = *member
- } else {
- delete(room.exMemberCache, userID)
- room.memberCache[userID] = member
- room.updateNthMemberCache(userID, member)
- }
- } else {
- existingExMember, ok := room.exMemberCache[userID]
- if ok {
- *existingExMember = *member
- } else {
- delete(room.memberCache, userID)
- room.exMemberCache[userID] = member
- }
- }
- }
-}
-
-// GetStateEvent returns the state event for the given type/state_key combo, or nil.
-func (room *Room) GetStateEvent(eventType event.Type, stateKey string) *event.Event {
- room.Load()
- room.lock.RLock()
- defer room.lock.RUnlock()
- stateEventMap, _ := room.state[eventType]
- evt, _ := stateEventMap[stateKey]
- return evt
-}
-
-// getStateEvents returns the state events for the given type.
-func (room *Room) getStateEvents(eventType event.Type) map[string]*event.Event {
- stateEventMap, _ := room.state[eventType]
- return stateEventMap
-}
-
-// GetTopic returns the topic of the room.
-func (room *Room) GetTopic() string {
- if len(room.topicCache) == 0 {
- topicEvt := room.GetStateEvent(event.StateTopic, "")
- if topicEvt != nil {
- room.topicCache = topicEvt.Content.AsTopic().Topic
- }
- }
- return room.topicCache
-}
-
-func (room *Room) GetCanonicalAlias() id.RoomAlias {
- if len(room.CanonicalAliasCache) == 0 {
- canonicalAliasEvt := room.GetStateEvent(event.StateCanonicalAlias, "")
- if canonicalAliasEvt != nil {
- room.CanonicalAliasCache = canonicalAliasEvt.Content.AsCanonicalAlias().Alias
- } else {
- room.CanonicalAliasCache = "-"
- }
- }
- if room.CanonicalAliasCache == "-" {
- return ""
- }
- return room.CanonicalAliasCache
-}
-
-// updateNameFromNameEvent updates the room display name to be the name set in the name event.
-func (room *Room) updateNameFromNameEvent() {
- nameEvt := room.GetStateEvent(event.StateRoomName, "")
- if nameEvt != nil {
- room.NameCache = nameEvt.Content.AsRoomName().Name
- }
-}
-
-// updateNameFromMembers updates the room display name based on the members in this room.
-//
-// The room name depends on the number of users:
-//
-// Less than two users -> "Empty room"
-// Exactly two users -> The display name of the other user.
-// More than two users -> The display name of one of the other users, followed
-// by "and X others", where X is the number of users
-// excluding the local user and the named user.
-func (room *Room) updateNameFromMembers() {
- members := room.GetMembers()
- if len(members) <= 1 {
- room.NameCache = "Empty room"
- } else if room.firstMemberCache == nil {
- room.NameCache = "Room"
- } else if len(members) == 2 {
- room.NameCache = room.firstMemberCache.Displayname
- } else if len(members) == 3 && room.secondMemberCache != nil {
- room.NameCache = fmt.Sprintf("%s and %s", room.firstMemberCache.Displayname, room.secondMemberCache.Displayname)
- } else {
- members := room.firstMemberCache.Displayname
- count := len(members) - 2
- if room.secondMemberCache != nil {
- members += ", " + room.secondMemberCache.Displayname
- count--
- }
- room.NameCache = fmt.Sprintf("%s and %d others", members, count)
- }
-}
-
-// updateNameCache updates the room display name based on the room state in the order
-// specified in spec section 11.2.2.5.
-func (room *Room) updateNameCache() {
- if len(room.NameCache) == 0 {
- room.updateNameFromNameEvent()
- room.nameCacheSource = ExplicitRoomName
- }
- if len(room.NameCache) == 0 {
- room.NameCache = string(room.GetCanonicalAlias())
- room.nameCacheSource = CanonicalAliasRoomName
- }
- if len(room.NameCache) == 0 {
- room.updateNameFromMembers()
- room.nameCacheSource = MemberRoomName
- }
-}
-
-// GetTitle returns the display name of the room.
-//
-// The display name is returned from the cache.
-// If the cache is empty, it is updated first.
-func (room *Room) GetTitle() string {
- room.updateNameCache()
- return room.NameCache
-}
-
-func (room *Room) IsReplaced() bool {
- if room.replacedByCache == nil {
- evt := room.GetStateEvent(event.StateTombstone, "")
- var replacement id.RoomID
- if evt != nil {
- content, ok := evt.Content.Parsed.(*event.TombstoneEventContent)
- if ok {
- replacement = content.ReplacementRoom
- }
- }
- room.replacedCache = evt != nil
- room.replacedByCache = &replacement
- }
- return room.replacedCache
-}
-
-func (room *Room) ReplacedBy() id.RoomID {
- if room.replacedByCache == nil {
- room.IsReplaced()
- }
- return *room.replacedByCache
-}
-
-func (room *Room) eventToMember(userID, sender id.UserID, member *event.MemberEventContent) *Member {
- if len(member.Displayname) == 0 {
- member.Displayname = string(userID)
- }
- return &Member{
- MemberEventContent: *member,
- Sender: sender,
- }
-}
-
-func (room *Room) updateNthMemberCache(userID id.UserID, member *Member) {
- if userID != room.SessionUserID {
- if room.firstMemberCache == nil {
- room.firstMemberCache = member
- } else if room.secondMemberCache == nil {
- room.secondMemberCache = member
- }
- }
-}
-
-// createMemberCache caches all member events into a easily processable MXID -> *Member map.
-func (room *Room) createMemberCache() map[id.UserID]*Member {
- if len(room.memberCache) > 0 {
- return room.memberCache
- }
- cache := make(map[id.UserID]*Member)
- exCache := make(map[id.UserID]*Member)
- room.lock.RLock()
- memberEvents := room.getStateEvents(event.StateMember)
- room.firstMemberCache = nil
- room.secondMemberCache = nil
- if memberEvents != nil {
- for userIDStr, evt := range memberEvents {
- userID := id.UserID(userIDStr)
- member := room.eventToMember(userID, evt.Sender, evt.Content.AsMember())
- if member.Membership.IsInviteOrJoin() {
- cache[userID] = member
- room.updateNthMemberCache(userID, member)
- } else {
- exCache[userID] = member
- }
- if userID == room.SessionUserID {
- room.SessionMember = member
- }
- }
- }
- if len(room.Summary.Heroes) > 1 {
- room.firstMemberCache, _ = cache[room.Summary.Heroes[0]]
- }
- if len(room.Summary.Heroes) > 2 {
- room.secondMemberCache, _ = cache[room.Summary.Heroes[1]]
- }
- room.lock.RUnlock()
- room.lock.Lock()
- room.memberCache = cache
- room.exMemberCache = exCache
- room.lock.Unlock()
- return cache
-}
-
-// GetMembers returns the members in this room.
-//
-// The members are returned from the cache.
-// If the cache is empty, it is updated first.
-func (room *Room) GetMembers() map[id.UserID]*Member {
- room.Load()
- room.createMemberCache()
- return room.memberCache
-}
-
-func (room *Room) GetMemberList() []id.UserID {
- members := room.GetMembers()
- memberList := make([]id.UserID, len(members))
- index := 0
- for userID, _ := range members {
- memberList[index] = userID
- index++
- }
- return memberList
-}
-
-// GetMember returns the member with the given MXID.
-// If the member doesn't exist, nil is returned.
-func (room *Room) GetMember(userID id.UserID) *Member {
- if userID == room.SessionUserID && room.SessionMember != nil {
- return room.SessionMember
- }
- room.Load()
- room.createMemberCache()
- room.lock.RLock()
- member, ok := room.memberCache[userID]
- if ok {
- room.lock.RUnlock()
- return member
- }
- exMember, ok := room.exMemberCache[userID]
- if ok {
- room.lock.RUnlock()
- return exMember
- }
- room.lock.RUnlock()
- return nil
-}
-
-func (room *Room) GetMemberCount() int {
- if room.memberCache == nil && room.Summary.JoinedMemberCount != nil {
- return *room.Summary.JoinedMemberCount
- }
- return len(room.GetMembers())
-}
-
-// GetSessionOwner returns the ID of the user whose session this room was created for.
-func (room *Room) GetOwnDisplayname() string {
- member := room.GetMember(room.SessionUserID)
- if member != nil {
- return member.Displayname
- }
- return ""
-}
-
-// NewRoom creates a new Room with the given ID
-func NewRoom(roomID id.RoomID, cache *RoomCache) *Room {
- return &Room{
- ID: roomID,
- state: make(map[event.Type]map[string]*event.Event),
- path: cache.roomPath(roomID),
- cache: cache,
-
- SessionUserID: cache.getOwner(),
- }
-}
diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go
deleted file mode 100644
index cafa262..0000000
--- a/matrix/rooms/roomcache.go
+++ /dev/null
@@ -1,376 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package rooms
-
-import (
- "compress/gzip"
- "encoding/gob"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- sync "github.com/sasha-s/go-deadlock"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/debug"
-)
-
-// RoomCache contains room state info in a hashmap and linked list.
-type RoomCache struct {
- sync.Mutex
-
- listPath string
- directory string
- maxSize int
- maxAge int64
- getOwner func() id.UserID
- noUnload bool
-
- Map map[id.RoomID]*Room
- head *Room
- tail *Room
- size int
-}
-
-func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() id.UserID) *RoomCache {
- return &RoomCache{
- listPath: listPath,
- directory: directory,
- maxSize: maxSize,
- maxAge: maxAge,
- getOwner: getOwner,
-
- Map: make(map[id.RoomID]*Room),
- }
-}
-
-func (cache *RoomCache) DisableUnloading() {
- cache.noUnload = true
-}
-
-func (cache *RoomCache) EnableUnloading() {
- cache.noUnload = false
-}
-
-func (cache *RoomCache) IsEncrypted(roomID id.RoomID) bool {
- room := cache.Get(roomID)
- return room != nil && room.Encrypted
-}
-
-func (cache *RoomCache) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent {
- room := cache.Get(roomID)
- evt := room.GetStateEvent(event.StateEncryption, "")
- if evt == nil {
- return nil
- }
- content, ok := evt.Content.Parsed.(*event.EncryptionEventContent)
- if !ok {
- return nil
- }
- return content
-}
-
-func (cache *RoomCache) FindSharedRooms(userID id.UserID) (shared []id.RoomID) {
- // FIXME this disables unloading so TouchNode wouldn't try to double-lock
- cache.DisableUnloading()
- cache.Lock()
- for _, room := range cache.Map {
- if !room.Encrypted {
- continue
- }
- member, ok := room.GetMembers()[userID]
- if ok && member.Membership == event.MembershipJoin {
- shared = append(shared, room.ID)
- }
- }
- cache.Unlock()
- cache.EnableUnloading()
- return
-}
-
-func (cache *RoomCache) LoadList() error {
- cache.Lock()
- defer cache.Unlock()
-
- // Open room list file
- file, err := os.OpenFile(cache.listPath, os.O_RDONLY, 0600)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
- return fmt.Errorf("failed to open room list file for reading: %w", err)
- }
- defer debugPrintError(file.Close, "Failed to close room list file after reading")
-
- // Open gzip reader for room list file
- cmpReader, err := gzip.NewReader(file)
- if err != nil {
- return fmt.Errorf("failed to read gzip room list: %w", err)
- }
- defer debugPrintError(cmpReader.Close, "Failed to close room list gzip reader")
-
- // Open gob decoder for gzip reader
- dec := gob.NewDecoder(cmpReader)
- // Read number of items in list
- var size int
- err = dec.Decode(&size)
- if err != nil {
- return fmt.Errorf("failed to read size of room list: %w", err)
- }
-
- // Read list
- cache.Map = make(map[id.RoomID]*Room, size)
- for i := 0; i < size; i++ {
- room := &Room{}
- err = dec.Decode(room)
- if err != nil {
- debug.Printf("Failed to decode %dth room list entry: %v", i+1, err)
- continue
- }
- room.path = cache.roomPath(room.ID)
- room.cache = cache
- cache.Map[room.ID] = room
- }
- return nil
-}
-
-func (cache *RoomCache) SaveLoadedRooms() {
- cache.Lock()
- cache.clean(false)
- for node := cache.head; node != nil; node = node.prev {
- node.Save()
- }
- cache.Unlock()
-}
-
-func (cache *RoomCache) SaveList() error {
- cache.Lock()
- defer cache.Unlock()
-
- debug.Print("Saving room list...")
- // Open room list file
- file, err := os.OpenFile(cache.listPath, os.O_WRONLY|os.O_CREATE, 0600)
- if err != nil {
- return fmt.Errorf("failed to open room list file for writing: %w", err)
- }
- defer debugPrintError(file.Close, "Failed to close room list file after writing")
-
- // Open gzip writer for room list file
- cmpWriter := gzip.NewWriter(file)
- defer debugPrintError(cmpWriter.Close, "Failed to close room list gzip writer")
-
- // Open gob encoder for gzip writer
- enc := gob.NewEncoder(cmpWriter)
- // Write number of items in list
- err = enc.Encode(len(cache.Map))
- if err != nil {
- return fmt.Errorf("failed to write size of room list: %w", err)
- }
-
- // Write list
- for _, node := range cache.Map {
- err = enc.Encode(node)
- if err != nil {
- debug.Printf("Failed to encode room list entry of %s: %v", node.ID, err)
- }
- }
- debug.Print("Room list saved to", cache.listPath, len(cache.Map), cache.size)
- return nil
-}
-
-func (cache *RoomCache) Touch(roomID id.RoomID) {
- cache.Lock()
- node, ok := cache.Map[roomID]
- if !ok || node == nil {
- cache.Unlock()
- return
- }
- cache.touch(node)
- cache.Unlock()
-}
-
-func (cache *RoomCache) TouchNode(node *Room) {
- if cache.noUnload || node.touch+2 > time.Now().Unix() {
- return
- }
- cache.Lock()
- cache.touch(node)
- cache.Unlock()
-}
-
-func (cache *RoomCache) touch(node *Room) {
- if node == cache.head {
- return
- }
- debug.Print("Touching", node.ID)
- cache.llPop(node)
- cache.llPush(node)
- node.touch = time.Now().Unix()
-}
-
-func (cache *RoomCache) Get(roomID id.RoomID) *Room {
- cache.Lock()
- node := cache.get(roomID)
- cache.Unlock()
- return node
-}
-
-func (cache *RoomCache) GetOrCreate(roomID id.RoomID) *Room {
- cache.Lock()
- node := cache.get(roomID)
- if node == nil {
- node = cache.newRoom(roomID)
- cache.llPush(node)
- }
- cache.Unlock()
- return node
-}
-
-func (cache *RoomCache) get(roomID id.RoomID) *Room {
- node, ok := cache.Map[roomID]
- if ok && node != nil {
- return node
- }
- return nil
-}
-
-func (cache *RoomCache) Put(room *Room) {
- cache.Lock()
- node := cache.get(room.ID)
- if node != nil {
- cache.touch(node)
- } else {
- cache.Map[room.ID] = room
- if room.Loaded() {
- cache.llPush(room)
- }
- node = room
- }
- cache.Unlock()
- node.Save()
-}
-
-func (cache *RoomCache) roomPath(roomID id.RoomID) string {
- escapedRoomID := strings.ReplaceAll(strings.ReplaceAll(string(roomID), "%", "%25"), "/", "%2F")
- return filepath.Join(cache.directory, escapedRoomID+".gob.gz")
-}
-
-func (cache *RoomCache) Load(roomID id.RoomID) *Room {
- cache.Lock()
- defer cache.Unlock()
- node, ok := cache.Map[roomID]
- if ok {
- return node
- }
-
- node = NewRoom(roomID, cache)
- node.Load()
- return node
-}
-
-func (cache *RoomCache) llPop(node *Room) {
- if node.prev == nil && node.next == nil {
- return
- }
- if node.prev != nil {
- node.prev.next = node.next
- }
- if node.next != nil {
- node.next.prev = node.prev
- }
- if node == cache.tail {
- cache.tail = node.next
- }
- if node == cache.head {
- cache.head = node.prev
- }
- node.next = nil
- node.prev = nil
- cache.size--
-}
-
-func (cache *RoomCache) llPush(node *Room) {
- if node.next != nil || node.prev != nil {
- debug.PrintStack()
- debug.Print("Tried to llPush node that is already in stack")
- return
- }
- if node == cache.head {
- return
- }
- if cache.head != nil {
- cache.head.next = node
- }
- node.prev = cache.head
- node.next = nil
- cache.head = node
- if cache.tail == nil {
- cache.tail = node
- }
- cache.size++
- cache.clean(false)
-}
-
-func (cache *RoomCache) ForceClean() {
- cache.Lock()
- cache.clean(true)
- cache.Unlock()
-}
-
-func (cache *RoomCache) clean(force bool) {
- if cache.noUnload && !force {
- return
- }
- origSize := cache.size
- maxTS := time.Now().Unix() - cache.maxAge
- for cache.size > cache.maxSize {
- if cache.tail.touch > maxTS && !force {
- break
- }
- ok := cache.tail.Unload()
- node := cache.tail
- cache.llPop(node)
- if !ok {
- debug.Print("Unload returned false, pushing node back")
- cache.llPush(node)
- }
- }
- if cleaned := origSize - cache.size; cleaned > 0 {
- debug.Print("Cleaned", cleaned, "rooms")
- }
-}
-
-func (cache *RoomCache) Unload(node *Room) {
- cache.Lock()
- defer cache.Unlock()
- cache.llPop(node)
- ok := node.Unload()
- if !ok {
- debug.Print("Unload returned false, pushing node back")
- cache.llPush(node)
- }
-}
-
-func (cache *RoomCache) newRoom(roomID id.RoomID) *Room {
- node := NewRoom(roomID, cache)
- cache.Map[node.ID] = node
- return node
-}
diff --git a/matrix/sync.go b/matrix/sync.go
deleted file mode 100644
index 7c1d879..0000000
--- a/matrix/sync.go
+++ /dev/null
@@ -1,267 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-// Based on https://github.com/matrix-org/mautrix/blob/master/sync.go
-
-package matrix
-
-import (
- "sync"
- "time"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-type GomuksSyncer struct {
- rooms *rooms.RoomCache
- globalListeners []mautrix.SyncHandler
- listeners map[event.Type][]mautrix.EventHandler // event type to listeners array
- FirstSyncDone bool
- InitDoneCallback func()
- FirstDoneCallback func()
- Progress ifc.SyncingModal
-}
-
-// NewGomuksSyncer returns an instantiated GomuksSyncer
-func NewGomuksSyncer(rooms *rooms.RoomCache) *GomuksSyncer {
- return &GomuksSyncer{
- rooms: rooms,
- globalListeners: []mautrix.SyncHandler{},
- listeners: make(map[event.Type][]mautrix.EventHandler),
- FirstSyncDone: false,
- Progress: StubSyncingModal{},
- }
-}
-
-// ProcessResponse processes a Matrix sync response.
-func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) {
- if since == "" {
- s.rooms.DisableUnloading()
- }
- debug.Print("Received sync response")
- s.Progress.SetMessage("Processing sync response")
- steps := len(res.Rooms.Join) + len(res.Rooms.Invite) + len(res.Rooms.Leave)
- s.Progress.SetSteps(steps + 2 + len(s.globalListeners))
-
- wait := &sync.WaitGroup{}
- callback := func() {
- wait.Done()
- s.Progress.Step()
- }
- wait.Add(len(s.globalListeners))
- s.notifyGlobalListeners(res, since, callback)
- wait.Wait()
-
- s.processSyncEvents(nil, res.Presence.Events, mautrix.EventSourcePresence)
- s.Progress.Step()
- s.processSyncEvents(nil, res.AccountData.Events, mautrix.EventSourceAccountData)
- s.Progress.Step()
-
- wait.Add(steps)
-
- for roomID, roomData := range res.Rooms.Join {
- go s.processJoinedRoom(roomID, roomData, callback)
- }
-
- for roomID, roomData := range res.Rooms.Invite {
- go s.processInvitedRoom(roomID, roomData, callback)
- }
-
- for roomID, roomData := range res.Rooms.Leave {
- go s.processLeftRoom(roomID, roomData, callback)
- }
-
- wait.Wait()
- s.Progress.SetMessage("Finishing sync")
-
- if since == "" && s.InitDoneCallback != nil {
- s.InitDoneCallback()
- s.rooms.EnableUnloading()
- }
- if !s.FirstSyncDone && s.FirstDoneCallback != nil {
- s.FirstDoneCallback()
- }
- s.FirstSyncDone = true
- return
-}
-
-func (s *GomuksSyncer) notifyGlobalListeners(res *mautrix.RespSync, since string, callback func()) {
- for _, listener := range s.globalListeners {
- go func(listener mautrix.SyncHandler) {
- listener(res, since)
- callback()
- }(listener)
- }
-}
-
-func (s *GomuksSyncer) processJoinedRoom(roomID id.RoomID, roomData mautrix.SyncJoinedRoom, callback func()) {
- defer debug.Recover()
- room := s.rooms.GetOrCreate(roomID)
- room.UpdateSummary(roomData.Summary)
- s.processSyncEvents(room, roomData.State.Events, mautrix.EventSourceJoin|mautrix.EventSourceState)
- s.processSyncEvents(room, roomData.Timeline.Events, mautrix.EventSourceJoin|mautrix.EventSourceTimeline)
- s.processSyncEvents(room, roomData.Ephemeral.Events, mautrix.EventSourceJoin|mautrix.EventSourceEphemeral)
- s.processSyncEvents(room, roomData.AccountData.Events, mautrix.EventSourceJoin|mautrix.EventSourceAccountData)
-
- if len(room.PrevBatch) == 0 {
- room.PrevBatch = roomData.Timeline.PrevBatch
- }
- room.LastPrevBatch = roomData.Timeline.PrevBatch
- callback()
-}
-
-func (s *GomuksSyncer) processInvitedRoom(roomID id.RoomID, roomData mautrix.SyncInvitedRoom, callback func()) {
- defer debug.Recover()
- room := s.rooms.GetOrCreate(roomID)
- room.UpdateSummary(roomData.Summary)
- s.processSyncEvents(room, roomData.State.Events, mautrix.EventSourceInvite|mautrix.EventSourceState)
- callback()
-}
-
-func (s *GomuksSyncer) processLeftRoom(roomID id.RoomID, roomData mautrix.SyncLeftRoom, callback func()) {
- defer debug.Recover()
- room := s.rooms.GetOrCreate(roomID)
- room.HasLeft = true
- room.UpdateSummary(roomData.Summary)
- s.processSyncEvents(room, roomData.State.Events, mautrix.EventSourceLeave|mautrix.EventSourceState)
- s.processSyncEvents(room, roomData.Timeline.Events, mautrix.EventSourceLeave|mautrix.EventSourceTimeline)
-
- if len(room.PrevBatch) == 0 {
- room.PrevBatch = roomData.Timeline.PrevBatch
- }
- room.LastPrevBatch = roomData.Timeline.PrevBatch
- callback()
-}
-
-func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*event.Event, source mautrix.EventSource) {
- for _, evt := range events {
- s.processSyncEvent(room, evt, source)
- }
-}
-
-func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, evt *event.Event, source mautrix.EventSource) {
- if room != nil {
- evt.RoomID = room.ID
- }
- // Ensure the type class is correct. It's safe to mutate since it's not a pointer.
- // Listeners are keyed by type structs, which means only the correct class will pass.
- switch {
- case evt.StateKey != nil:
- evt.Type.Class = event.StateEventType
- case source == mautrix.EventSourcePresence, source&mautrix.EventSourceEphemeral != 0:
- evt.Type.Class = event.EphemeralEventType
- case source&mautrix.EventSourceAccountData != 0:
- evt.Type.Class = event.AccountDataEventType
- case source == mautrix.EventSourceToDevice:
- evt.Type.Class = event.ToDeviceEventType
- default:
- evt.Type.Class = event.MessageEventType
- }
-
- err := evt.Content.ParseRaw(evt.Type)
- if err != nil {
- debug.Printf("Failed to unmarshal content of event %s (type %s) by %s in %s: %v\n%s", evt.ID, evt.Type.Repr(), evt.Sender, evt.RoomID, err, string(evt.Content.VeryRaw))
- // TODO might be good to let these pass to allow handling invalid events too
- return
- }
-
- if room != nil && evt.Type.IsState() {
- room.UpdateState(evt)
- }
- s.notifyListeners(source, evt)
-}
-
-// OnEventType allows callers to be notified when there are new events for the given event type.
-// There are no duplicate checks.
-func (s *GomuksSyncer) OnEventType(eventType event.Type, callback mautrix.EventHandler) {
- _, exists := s.listeners[eventType]
- if !exists {
- s.listeners[eventType] = []mautrix.EventHandler{}
- }
- s.listeners[eventType] = append(s.listeners[eventType], callback)
-}
-
-func (s *GomuksSyncer) OnSync(callback mautrix.SyncHandler) {
- s.globalListeners = append(s.globalListeners, callback)
-}
-
-func (s *GomuksSyncer) notifyListeners(source mautrix.EventSource, evt *event.Event) {
- listeners, exists := s.listeners[evt.Type]
- if !exists {
- return
- }
- for _, fn := range listeners {
- fn(source, evt)
- }
-}
-
-// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
-func (s *GomuksSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) {
- debug.Printf("Sync failed: %v", err)
- return 10 * time.Second, nil
-}
-
-// GetFilterJSON returns a filter with a timeline limit of 50.
-func (s *GomuksSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
- stateEvents := []event.Type{
- event.StateMember,
- event.StateRoomName,
- event.StateTopic,
- event.StateCanonicalAlias,
- event.StatePowerLevels,
- event.StateTombstone,
- event.StateEncryption,
- }
- messageEvents := []event.Type{
- event.EventMessage,
- event.EventRedaction,
- event.EventEncrypted,
- event.EventSticker,
- event.EventReaction,
- }
- return &mautrix.Filter{
- Room: mautrix.RoomFilter{
- IncludeLeave: false,
- State: mautrix.FilterPart{
- LazyLoadMembers: true,
- Types: stateEvents,
- },
- Timeline: mautrix.FilterPart{
- LazyLoadMembers: true,
- Types: append(messageEvents, stateEvents...),
- Limit: 50,
- },
- Ephemeral: mautrix.FilterPart{
- Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt},
- },
- AccountData: mautrix.FilterPart{
- Types: []event.Type{event.AccountDataRoomTags},
- },
- },
- AccountData: mautrix.FilterPart{
- Types: []event.Type{event.AccountDataPushRules, event.AccountDataDirectChats, AccountDataGomuksPreferences},
- },
- Presence: mautrix.FilterPart{
- NotTypes: []event.Type{event.NewEventType("*")},
- },
- }
-}
diff --git a/matrix/uia-fallback.go b/matrix/uia-fallback.go
deleted file mode 100644
index f958ba1..0000000
--- a/matrix/uia-fallback.go
+++ /dev/null
@@ -1,115 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package matrix
-
-import (
- "context"
- "errors"
- "net/http"
- "net/url"
- "time"
-
- "maunium.net/go/mautrix"
-
- "maunium.net/go/gomuks/debug"
- "maunium.net/go/gomuks/lib/open"
-)
-
-const uiaFallbackPage = `
-
-
- gomuks user-interactive auth
-
-
-
-
-
Please complete the login in the popup window
-
Keep this page open while logging in, it will close automatically after the login finishes.
-
-
-
-
-
-`
-
-func (c *Container) UIAFallback(loginType mautrix.AuthType, sessionID string) error {
- errChan := make(chan error, 1)
- server := &http.Server{Addr: ":29325"}
- server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.Method == "GET" {
- w.Header().Add("Content-Type", "text/html")
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(uiaFallbackPage))
- } else if r.Method == "POST" || r.Method == "DELETE" {
- w.Header().Add("Content-Type", "text/html")
- w.WriteHeader(http.StatusOK)
-
- go func() {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- err := server.Shutdown(ctx)
- if err != nil {
- debug.Printf("Failed to shut down SSO server: %v\n", err)
- }
- if r.Method == "DELETE" {
- errChan <- errors.New("login cancelled")
- } else {
- errChan <- nil
- }
- }()
- } else {
- w.WriteHeader(http.StatusMethodNotAllowed)
- }
- })
- go server.ListenAndServe()
- defer server.Close()
- authURL := c.client.BuildURLWithQuery(mautrix.ClientURLPath{"v3", "auth", loginType, "fallback", "web"}, map[string]string{
- "session": sessionID,
- })
- link := url.URL{
- Scheme: "http",
- Host: "localhost:29325",
- Path: "/",
- Fragment: authURL,
- }
- err := open.Open(link.String())
- if err != nil {
- return err
- }
- err = <-errChan
- return err
-}
diff --git a/ui/autocomplete.go b/ui/autocomplete.go
deleted file mode 100644
index 5cacace..0000000
--- a/ui/autocomplete.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "io/ioutil"
- "path/filepath"
- "strings"
-)
-
-func autocompleteFile(cmd *CommandAutocomplete) (completions []string, newText string) {
- inputPath, err := filepath.Abs(cmd.RawArgs)
- if err != nil {
- return
- }
-
- var searchNamePrefix, searchDir string
- if strings.HasSuffix(cmd.RawArgs, "/") {
- searchDir = inputPath
- } else {
- searchNamePrefix = filepath.Base(inputPath)
- searchDir = filepath.Dir(inputPath)
- }
- files, err := ioutil.ReadDir(searchDir)
- if err != nil {
- return
- }
- for _, file := range files {
- name := file.Name()
- if !strings.HasPrefix(name, searchNamePrefix) || (name[0] == '.' && searchNamePrefix == "") {
- continue
- }
- fullPath := filepath.Join(searchDir, name)
- if file.IsDir() {
- fullPath += "/"
- }
- completions = append(completions, fullPath)
- }
- if len(completions) == 1 {
- newText = fmt.Sprintf("/%s %s", cmd.OrigCommand, completions[0])
- }
- return
-}
-
-func autocompleteToggle(cmd *CommandAutocomplete) (completions []string, newText string) {
- completions = make([]string, 0, len(toggleMsg))
- for k := range toggleMsg {
- if strings.HasPrefix(k, cmd.RawArgs) {
- completions = append(completions, k)
- }
- }
- if len(completions) == 1 {
- newText = fmt.Sprintf("/%s %s", cmd.OrigCommand, completions[0])
- }
- return
-}
-
-var staticPowerLevelKeys = []string{"ban", "kick", "redact", "invite", "state_default", "events_default", "users_default"}
-
-func autocompletePowerLevel(cmd *CommandAutocomplete) (completions []string, newText string) {
- if len(cmd.Args) > 1 {
- return
- }
- for _, staticKey := range staticPowerLevelKeys {
- if strings.HasPrefix(staticKey, cmd.RawArgs) {
- completions = append(completions, staticKey)
- }
- }
- for _, cpl := range cmd.Room.AutocompleteUser(cmd.RawArgs) {
- completions = append(completions, cpl.id)
- }
- return
-}
diff --git a/ui/command-processor.go b/ui/command-processor.go
deleted file mode 100644
index f0591f7..0000000
--- a/ui/command-processor.go
+++ /dev/null
@@ -1,290 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "strings"
-
- "github.com/mattn/go-runewidth"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
-)
-
-type gomuksPointerContainer struct {
- MainView *MainView
- UI *GomuksUI
- Matrix ifc.MatrixContainer
- Config *config.Config
- Gomuks ifc.Gomuks
-}
-
-type Command struct {
- gomuksPointerContainer
- Handler *CommandProcessor
-
- Room *RoomView
- Command string
- OrigCommand string
- Args []string
- RawArgs string
- OrigText string
-}
-
-type CommandAutocomplete Command
-
-func (cmd *Command) Reply(message string, args ...interface{}) {
- if len(args) > 0 {
- message = fmt.Sprintf(message, args...)
- }
- cmd.Room.AddServiceMessage(message)
- cmd.UI.Render()
-}
-
-type Alias struct {
- NewCommand string
-}
-
-func (alias *Alias) Process(cmd *Command) *Command {
- cmd.Command = alias.NewCommand
- return cmd
-}
-
-type CommandHandler func(cmd *Command)
-type CommandAutocompleter func(cmd *CommandAutocomplete) (completions []string, newText string)
-
-type CommandProcessor struct {
- gomuksPointerContainer
-
- aliases map[string]*Alias
- commands map[string]CommandHandler
-
- autocompleters map[string]CommandAutocompleter
-}
-
-func NewCommandProcessor(parent *MainView) *CommandProcessor {
- return &CommandProcessor{
- gomuksPointerContainer: gomuksPointerContainer{
- MainView: parent,
- UI: parent.parent,
- Matrix: parent.matrix,
- Config: parent.config,
- Gomuks: parent.gmx,
- },
- aliases: map[string]*Alias{
- "part": {"leave"},
- "send": {"sendevent"},
- "msend": {"msendevent"},
- "state": {"setstate"},
- "mstate": {"msetstate"},
- "rb": {"rainbow"},
- "rbme": {"rainbowme"},
- "rbn": {"rainbownotice"},
- "myroomnick": {"roomnick"},
- "createroom": {"create"},
- "dm": {"pm"},
- "query": {"pm"},
- "r": {"reply"},
- "delete": {"redact"},
- "remove": {"redact"},
- "rm": {"redact"},
- "del": {"redact"},
- "e": {"edit"},
- "dl": {"download"},
- "o": {"open"},
- "4s": {"ssss"},
- "s4": {"ssss"},
- "cs": {"cross-signing"},
- "power": {"powerlevel"},
- "pl": {"powerlevel"},
- },
- autocompleters: map[string]CommandAutocompleter{
- "devices": autocompleteUser,
- "device": autocompleteDevice,
- "verify": autocompleteUser,
- "verify-device": autocompleteDevice,
- "unverify": autocompleteDevice,
- "blacklist": autocompleteDevice,
- "upload": autocompleteFile,
- "download": autocompleteFile,
- "open": autocompleteFile,
- "import": autocompleteFile,
- "export": autocompleteFile,
- "export-room": autocompleteFile,
- "toggle": autocompleteToggle,
- "powerlevel": autocompletePowerLevel,
- },
- commands: map[string]CommandHandler{
- "unknown-command": cmdUnknownCommand,
-
- "id": cmdID,
- "help": cmdHelp,
- "me": cmdMe,
- "quit": cmdQuit,
- "clearcache": cmdClearCache,
- "leave": cmdLeave,
- "create": cmdCreateRoom,
- "pm": cmdPrivateMessage,
- "join": cmdJoin,
- "kick": cmdKick,
- "ban": cmdBan,
- "unban": cmdUnban,
- "powerlevel": cmdPowerLevel,
- "toggle": cmdToggle,
- "logout": cmdLogout,
- "accept": cmdAccept,
- "reject": cmdReject,
- "reply": cmdReply,
- "redact": cmdRedact,
- "react": cmdReact,
- "edit": cmdEdit,
- "external": cmdExternalEditor,
- "download": cmdDownload,
- "upload": cmdUpload,
- "open": cmdOpen,
- "copy": cmdCopy,
- "sendevent": cmdSendEvent,
- "msendevent": cmdMSendEvent,
- "setstate": cmdSetState,
- "msetstate": cmdMSetState,
- "roomnick": cmdRoomNick,
- "rainbow": cmdRainbow,
- "rainbowme": cmdRainbowMe,
- "notice": cmdNotice,
- "alias": cmdAlias,
- "tags": cmdTags,
- "tag": cmdTag,
- "untag": cmdUntag,
- "invite": cmdInvite,
- "hprof": cmdHeapProfile,
- "cprof": cmdCPUProfile,
- "trace": cmdTrace,
- "panic": func(cmd *Command) {
- panic("hello world")
- },
-
- "rainbownotice": cmdRainbowNotice,
-
- "fingerprint": cmdFingerprint,
- "devices": cmdDevices,
- "verify-device": cmdVerifyDevice,
- "verify": cmdVerify,
- "device": cmdDevice,
- "unverify": cmdUnverify,
- "blacklist": cmdBlacklist,
- "reset-session": cmdResetSession,
- "import": cmdImportKeys,
- "export": cmdExportKeys,
- "export-room": cmdExportRoomKeys,
- "ssss": cmdSSSS,
- "cross-signing": cmdCrossSigning,
- },
- }
-}
-
-func (ch *CommandProcessor) ParseCommand(roomView *RoomView, text string) *Command {
- if text[0] != '/' || len(text) < 2 {
- return nil
- }
- text = text[1:]
- split := strings.Fields(text)
- command := split[0]
- args := split[1:]
- var rawArgs string
- if len(text) > len(command)+1 {
- rawArgs = text[len(command)+1:]
- }
- return &Command{
- gomuksPointerContainer: ch.gomuksPointerContainer,
- Handler: ch,
-
- Room: roomView,
- Command: strings.ToLower(command),
- OrigCommand: command,
- Args: args,
- RawArgs: rawArgs,
- OrigText: text,
- }
-}
-
-func (ch *CommandProcessor) Autocomplete(roomView *RoomView, text string, cursorOffset int) ([]string, string, bool) {
- var completions []string
- if cursorOffset != runewidth.StringWidth(text) {
- return completions, text, false
- }
-
- var cmd *Command
- if cmd = ch.ParseCommand(roomView, text); cmd == nil {
- return completions, text, false
- } else if alias, ok := ch.aliases[cmd.Command]; ok {
- cmd = alias.Process(cmd)
- }
-
- handler, ok := ch.autocompleters[cmd.Command]
- if ok {
- var newText string
- completions, newText = handler((*CommandAutocomplete)(cmd))
- if newText != "" {
- text = newText
- }
- }
- return completions, text, ok
-}
-
-func (ch *CommandProcessor) AutocompleteCommand(word string) (completions []string) {
- if word[0] != '/' {
- return
- }
- word = word[1:]
- for alias := range ch.aliases {
- if alias == word {
- return []string{"/" + alias}
- }
- if strings.HasPrefix(alias, word) {
- completions = append(completions, "/"+alias)
- }
- }
- for command := range ch.commands {
- if command == word {
- return []string{"/" + command}
- }
- if strings.HasPrefix(command, word) {
- completions = append(completions, "/"+command)
- }
- }
- return
-}
-
-func (ch *CommandProcessor) HandleCommand(cmd *Command) {
- defer debug.Recover()
- if cmd == nil {
- return
- }
- if alias, ok := ch.aliases[cmd.Command]; ok {
- cmd = alias.Process(cmd)
- }
- if cmd == nil {
- return
- }
- if handler, ok := ch.commands[cmd.Command]; ok {
- handler(cmd)
- return
- }
- cmdUnknownCommand(cmd)
-}
diff --git a/ui/commands.go b/ui/commands.go
deleted file mode 100644
index 37eb4e3..0000000
--- a/ui/commands.go
+++ /dev/null
@@ -1,1046 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "math"
- "os"
- "os/exec"
- "path/filepath"
- "regexp"
- "runtime"
- dbg "runtime/debug"
- "runtime/pprof"
- "runtime/trace"
- "strconv"
- "strings"
- "time"
- "unicode"
-
- "github.com/lucasb-eyer/go-colorful"
- "github.com/yuin/goldmark"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/format"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/debug"
- "maunium.net/go/gomuks/lib/filepicker"
-)
-
-func cmdMe(cmd *Command) {
- text := strings.Join(cmd.Args, " ")
- go cmd.Room.SendMessage(event.MsgEmote, text)
-}
-
-// GradientTable from https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go
-type GradientTable []struct {
- Col colorful.Color
- Pos float64
-}
-
-func (gt GradientTable) GetInterpolatedColorFor(t float64) colorful.Color {
- for i := 0; i < len(gt)-1; i++ {
- c1 := gt[i]
- c2 := gt[i+1]
- if c1.Pos <= t && t <= c2.Pos {
- t := (t - c1.Pos) / (c2.Pos - c1.Pos)
- return c1.Col.BlendHcl(c2.Col, t).Clamped()
- }
- }
- return gt[len(gt)-1].Col
-}
-
-var rainbow = GradientTable{
- {colorful.LinearRgb(1, 0, 0), 0 / 11.0},
- {colorful.LinearRgb(1, 0.5, 0), 1 / 11.0},
- {colorful.LinearRgb(1, 1, 0), 2 / 11.0},
- {colorful.LinearRgb(0.5, 1, 0), 3 / 11.0},
- {colorful.LinearRgb(0, 1, 0), 4 / 11.0},
- {colorful.LinearRgb(0, 1, 0.5), 5 / 11.0},
- {colorful.LinearRgb(0, 1, 1), 6 / 11.0},
- {colorful.LinearRgb(0, 0.5, 1), 7 / 11.0},
- {colorful.LinearRgb(0, 0, 1), 8 / 11.0},
- {colorful.LinearRgb(0.5, 0, 1), 9 / 11.0},
- {colorful.LinearRgb(1, 0, 1), 10 / 11.0},
- {colorful.LinearRgb(1, 0, 0.5), 11 / 11.0},
-}
-
-var rainbowMark = goldmark.New(format.Extensions, format.HTMLOptions, goldmark.WithExtensions(ExtensionRainbow))
-
-// TODO this command definitely belongs in a plugin once we have a plugin system.
-func makeRainbow(cmd *Command, msgtype event.MessageType) {
- text := strings.Join(cmd.Args, " ")
-
- var buf strings.Builder
- _ = rainbowMark.Convert([]byte(text), &buf)
-
- htmlBody := strings.TrimRight(buf.String(), "\n")
- htmlBody = format.AntiParagraphRegex.ReplaceAllString(htmlBody, "$1")
- text = format.HTMLToText(htmlBody)
-
- count := strings.Count(htmlBody, defaultRB.ColorID)
- i := -1
- htmlBody = regexp.MustCompile(defaultRB.ColorID).ReplaceAllStringFunc(htmlBody, func(match string) string {
- i++
- return rainbow.GetInterpolatedColorFor(float64(i) / float64(count)).Hex()
- })
-
- go cmd.Room.SendMessageHTML(msgtype, text, htmlBody)
-}
-
-func cmdRainbow(cmd *Command) {
- makeRainbow(cmd, event.MsgText)
-}
-
-func cmdRainbowMe(cmd *Command) {
- makeRainbow(cmd, event.MsgEmote)
-}
-
-func cmdRainbowNotice(cmd *Command) {
- makeRainbow(cmd, event.MsgNotice)
-}
-
-func cmdNotice(cmd *Command) {
- go cmd.Room.SendMessage(event.MsgNotice, strings.Join(cmd.Args, " "))
-}
-
-func cmdAccept(cmd *Command) {
- room := cmd.Room.MxRoom()
- if room.SessionMember.Membership != "invite" {
- cmd.Reply("/accept can only be used in rooms you're invited to")
- return
- }
- _, server, _ := room.SessionMember.Sender.Parse()
- _, err := cmd.Matrix.JoinRoom(room.ID, server)
- if err != nil {
- cmd.Reply("Failed to accept invite: %v", err)
- } else {
- cmd.Reply("Successfully accepted invite")
- }
- cmd.MainView.UpdateTags(room)
- go cmd.MainView.LoadHistory(room.ID)
-}
-
-func cmdReject(cmd *Command) {
- room := cmd.Room.MxRoom()
- if room.SessionMember.Membership != "invite" {
- cmd.Reply("/reject can only be used in rooms you're invited to")
- return
- }
- err := cmd.Matrix.LeaveRoom(room.ID)
- if err != nil {
- cmd.Reply("Failed to reject invite: %v", err)
- } else {
- cmd.Reply("Successfully rejected invite")
- }
- cmd.MainView.RemoveRoom(room)
-}
-
-func cmdID(cmd *Command) {
- cmd.Reply("The internal ID of this room is %s", cmd.Room.MxRoom().ID)
-}
-
-type SelectReason string
-
-const (
- SelectReply SelectReason = "reply to"
- SelectReact = "react to"
- SelectRedact = "redact"
- SelectEdit = "edit"
- SelectDownload = "download"
- SelectOpen = "open"
- SelectCopy = "copy"
-)
-
-func cmdReply(cmd *Command) {
- cmd.Room.StartSelecting(SelectReply, strings.Join(cmd.Args, " "))
-}
-
-func cmdEdit(cmd *Command) {
- cmd.Room.StartSelecting(SelectEdit, "")
-}
-
-func findEditorExecutable() (string, string, error) {
- if editor := os.Getenv("VISUAL"); len(editor) > 0 {
- if path, err := exec.LookPath(editor); err != nil {
- return "", "", fmt.Errorf("$VISUAL ('%s') not found in $PATH", editor)
- } else {
- return editor, path, nil
- }
- } else if editor = os.Getenv("EDITOR"); len(editor) > 0 {
- if path, err := exec.LookPath(editor); err != nil {
- return "", "", fmt.Errorf("$EDITOR ('%s') not found in $PATH", editor)
- } else {
- return editor, path, nil
- }
- } else if path, _ := exec.LookPath("nano"); len(path) > 0 {
- return "nano", path, nil
- } else if path, _ = exec.LookPath("vi"); len(path) > 0 {
- return "vi", path, nil
- } else {
- return "", "", fmt.Errorf("$VISUAL and $EDITOR not set, nano and vi not found in $PATH")
- }
-}
-
-func cmdExternalEditor(cmd *Command) {
- var file *os.File
- defer func() {
- if file != nil {
- _ = file.Close()
- _ = os.Remove(file.Name())
- }
- }()
-
- fileExtension := "md"
- if cmd.Config.Preferences.DisableMarkdown {
- if cmd.Config.Preferences.DisableHTML {
- fileExtension = "txt"
- } else {
- fileExtension = "html"
- }
- }
-
- if editorName, executablePath, err := findEditorExecutable(); err != nil {
- cmd.Reply("Couldn't find editor to use: %v", err)
- return
- } else if file, err = os.CreateTemp("", fmt.Sprintf("gomuks-draft-*.%s", fileExtension)); err != nil {
- cmd.Reply("Failed to create temp file: %v", err)
- return
- } else if _, err = file.WriteString(cmd.RawArgs); err != nil {
- cmd.Reply("Failed to write to temp file: %v", err)
- } else if err = file.Close(); err != nil {
- cmd.Reply("Failed to close temp file: %v", err)
- } else if err = cmd.UI.RunExternal(executablePath, file.Name()); err != nil {
- var exitErr *exec.ExitError
- if isExit := errors.As(err, &exitErr); isExit {
- cmd.Reply("%s exited with non-zero status %d", editorName, exitErr.ExitCode())
- } else {
- cmd.Reply("Failed to run %s: %v", editorName, err)
- }
- } else if data, err := os.ReadFile(file.Name()); err != nil {
- cmd.Reply("Failed to read temp file: %v", err)
- } else if len(bytes.TrimSpace(data)) > 0 {
- cmd.Room.InputSubmit(string(data))
- } else {
- cmd.Reply("Temp file was blank, sending cancelled")
- if cmd.Room.editing != nil {
- cmd.Room.SetEditing(nil)
- }
- }
-}
-
-func cmdRedact(cmd *Command) {
- cmd.Room.StartSelecting(SelectRedact, strings.Join(cmd.Args, " "))
-}
-
-func cmdDownload(cmd *Command) {
- cmd.Room.StartSelecting(SelectDownload, strings.Join(cmd.Args, " "))
-}
-
-func cmdUpload(cmd *Command) {
- var path string
- var err error
- if len(cmd.Args) == 0 {
- if filepicker.IsSupported() {
- path, err = filepicker.Open()
- if err != nil {
- cmd.Reply("Failed to open file picker: %v", err)
- return
- } else if len(path) == 0 {
- cmd.Reply("File picking cancelled")
- return
- }
- } else {
- cmd.Reply("Usage: /upload ")
- return
- }
- } else {
- path, err = filepath.Abs(cmd.RawArgs)
- if err != nil {
- cmd.Reply("Failed to get absolute path: %v", err)
- return
- }
- }
-
- go cmd.Room.SendMessageMedia(path)
-}
-
-func cmdOpen(cmd *Command) {
- cmd.Room.StartSelecting(SelectOpen, strings.Join(cmd.Args, " "))
-}
-
-func cmdCopy(cmd *Command) {
- register := strings.Join(cmd.Args, " ")
- if len(register) == 0 {
- register = "clipboard"
- }
- if register == "clipboard" || register == "primary" {
- cmd.Room.StartSelecting(SelectCopy, register)
- } else {
- cmd.Reply("Usage: /copy [register], where register is either \"clipboard\" or \"primary\". Defaults to \"clipboard\".")
- }
-}
-
-func cmdReact(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /react ")
- return
- }
-
- cmd.Room.StartSelecting(SelectReact, strings.Join(cmd.Args, " "))
-}
-
-func readRoomAlias(cmd *Command) (alias id.RoomAlias, err error) {
- param := strings.Join(cmd.Args[1:], " ")
- if strings.ContainsRune(param, ':') {
- if param[0] != '#' {
- return "", errors.New("full aliases must start with #")
- }
-
- alias = id.RoomAlias(param)
- } else {
- _, homeserver, _ := cmd.Matrix.Client().UserID.Parse()
- alias = id.NewRoomAlias(param, homeserver)
- }
- return
-}
-
-func cmdAlias(cmd *Command) {
- if len(cmd.Args) < 2 {
- cmd.Reply("Usage: /alias ")
- return
- }
-
- alias, err := readRoomAlias(cmd)
- if err != nil {
- cmd.Reply(err.Error())
- return
- }
-
- subcmd := strings.ToLower(cmd.Args[0])
- switch subcmd {
- case "add", "create":
- cmdAddAlias(cmd, alias)
- case "remove", "delete", "del", "rm":
- cmdRemoveAlias(cmd, alias)
- case "resolve", "get":
- cmdResolveAlias(cmd, alias)
- default:
- cmd.Reply("Usage: /alias ")
- }
-}
-
-func niceError(err error) string {
- httpErr, ok := err.(mautrix.HTTPError)
- if ok && httpErr.RespError != nil {
- return httpErr.RespError.Error()
- }
- return err.Error()
-}
-
-func cmdAddAlias(cmd *Command, alias id.RoomAlias) {
- _, err := cmd.Matrix.Client().CreateAlias(alias, cmd.Room.MxRoom().ID)
- if err != nil {
- cmd.Reply("Failed to create alias: %v", niceError(err))
- } else {
- cmd.Reply("Created alias %s", alias)
- }
-}
-
-func cmdRemoveAlias(cmd *Command, alias id.RoomAlias) {
- _, err := cmd.Matrix.Client().DeleteAlias(alias)
- if err != nil {
- cmd.Reply("Failed to delete alias: %v", niceError(err))
- } else {
- cmd.Reply("Deleted alias %s", alias)
- }
-}
-
-func cmdResolveAlias(cmd *Command, alias id.RoomAlias) {
- resp, err := cmd.Matrix.Client().ResolveAlias(alias)
- if err != nil {
- cmd.Reply("Failed to resolve alias: %v", niceError(err))
- } else {
- roomIDText := string(resp.RoomID)
- if resp.RoomID == cmd.Room.MxRoom().ID {
- roomIDText += " (this room)"
- }
- cmd.Reply("Alias %s points to room %s\nThere are %d servers in the room.", alias, roomIDText, len(resp.Servers))
- }
-}
-
-func cmdTags(cmd *Command) {
- tags := cmd.Room.MxRoom().RawTags
- if len(cmd.Args) > 0 && cmd.Args[0] == "--internal" {
- tags = cmd.Room.MxRoom().Tags()
- }
- if len(tags) == 0 {
- if cmd.Room.MxRoom().IsDirect {
- cmd.Reply("This room has no tags, but it's marked as a direct chat.")
- } else {
- cmd.Reply("This room has no tags.")
- }
- return
- }
- var resp strings.Builder
- resp.WriteString("Tags in this room:\n")
- for _, tag := range tags {
- if tag.Order != "" {
- _, _ = fmt.Fprintf(&resp, "%s (order: %s)\n", tag.Tag, tag.Order)
- } else {
- _, _ = fmt.Fprintf(&resp, "%s (no order)\n", tag.Tag)
- }
- }
- cmd.Reply(strings.TrimSpace(resp.String()))
-}
-
-func cmdTag(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /tag [order]")
- return
- }
- order := math.NaN()
- if len(cmd.Args) > 1 {
- var err error
- order, err = strconv.ParseFloat(cmd.Args[1], 64)
- if err != nil {
- cmd.Reply("%s is not a valid order: %v", cmd.Args[1], err)
- return
- }
- }
- var err error
- if len(cmd.Args) > 2 && cmd.Args[2] == "--reset" {
- tags := event.Tags{
- cmd.Args[0]: {Order: json.Number(fmt.Sprintf("%f", order))},
- }
- for _, tag := range cmd.Room.MxRoom().RawTags {
- tags[tag.Tag] = event.Tag{Order: tag.Order}
- }
- err = cmd.Matrix.Client().SetTags(cmd.Room.MxRoom().ID, tags)
- } else {
- err = cmd.Matrix.Client().AddTag(cmd.Room.MxRoom().ID, cmd.Args[0], order)
- }
- if err != nil {
- cmd.Reply("Failed to add tag: %v", err)
- }
-}
-
-func cmdUntag(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /untag ")
- return
- }
- err := cmd.Matrix.Client().RemoveTag(cmd.Room.MxRoom().ID, cmd.Args[0])
- if err != nil {
- cmd.Reply("Failed to remove tag: %v", err)
- }
-}
-
-func cmdRoomNick(cmd *Command) {
- room := cmd.Room.MxRoom()
- member := room.GetMember(room.SessionUserID)
- member.Displayname = strings.Join(cmd.Args, " ")
- _, err := cmd.Matrix.Client().SendStateEvent(room.ID, event.StateMember, string(room.SessionUserID), member)
- if err != nil {
- cmd.Reply("Failed to set room nick: %v", err)
- }
-}
-
-func cmdFingerprint(cmd *Command) {
- c := cmd.Matrix.Crypto()
- if c == nil {
- cmd.Reply("Encryption support is not enabled")
- } else {
- cmd.Reply("Device ID: %s\nFingerprint: %s", cmd.Matrix.Client().DeviceID, c.Fingerprint())
- }
-}
-
-func cmdHeapProfile(cmd *Command) {
- if len(cmd.Args) == 0 || cmd.Args[0] != "nogc" {
- runtime.GC()
- dbg.FreeOSMemory()
- }
- memProfile, err := os.Create("gomuks.heap.prof")
- if err != nil {
- debug.Print("Failed to open gomuks.heap.prof:", err)
- return
- }
- defer func() {
- err := memProfile.Close()
- if err != nil {
- debug.Print("Failed to close gomuks.heap.prof:", err)
- }
- }()
- if err := pprof.WriteHeapProfile(memProfile); err != nil {
- debug.Print("Heap profile error:", err)
- }
-}
-
-func runTimedProfile(cmd *Command, start func(writer io.Writer) error, stop func(), task, file string) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /%s ", cmd.Command)
- } else if dur, err := strconv.Atoi(cmd.Args[0]); err != nil || dur < 0 {
- cmd.Reply("Usage: /%s ", cmd.Command)
- } else if cpuProfile, err := os.Create(file); err != nil {
- debug.Printf("Failed to open %s: %v", file, err)
- } else if err = start(cpuProfile); err != nil {
- _ = cpuProfile.Close()
- debug.Print(task, "error:", err)
- } else {
- cmd.Reply("Started %s for %d seconds", task, dur)
- go func() {
- time.Sleep(time.Duration(dur) * time.Second)
- stop()
- cmd.Reply("%s finished.", task)
-
- err := cpuProfile.Close()
- if err != nil {
- debug.Print("Failed to close gomuks.cpu.prof:", err)
- }
- }()
- }
-}
-
-func cmdCPUProfile(cmd *Command) {
- runTimedProfile(cmd, pprof.StartCPUProfile, pprof.StopCPUProfile, "CPU profiling", "gomuks.cpu.prof")
-}
-
-func cmdTrace(cmd *Command) {
- runTimedProfile(cmd, trace.Start, trace.Stop, "Call tracing", "gomuks.trace")
-}
-
-func cmdQuit(cmd *Command) {
- cmd.Gomuks.Stop(true)
-}
-
-func cmdClearCache(cmd *Command) {
- cmd.Config.Clear()
- cmd.Gomuks.Stop(false)
-}
-
-func cmdUnknownCommand(cmd *Command) {
- cmd.Reply(`Unknown command "/%s". Try "/help" for help.`, cmd.Command)
-}
-
-func cmdHelp(cmd *Command) {
- view := cmd.MainView
- view.ShowModal(NewHelpModal(view))
-}
-
-func cmdLeave(cmd *Command) {
- err := cmd.Matrix.LeaveRoom(cmd.Room.MxRoom().ID)
- debug.Print("Leave room error:", err)
- if err == nil {
- cmd.MainView.RemoveRoom(cmd.Room.MxRoom())
- }
-}
-
-func cmdInvite(cmd *Command) {
- if len(cmd.Args) != 1 {
- cmd.Reply("Usage: /invite ")
- return
- }
- _, err := cmd.Matrix.Client().InviteUser(cmd.Room.MxRoom().ID, &mautrix.ReqInviteUser{UserID: id.UserID(cmd.Args[0])})
- if err != nil {
- debug.Print("Error in invite call:", err)
- cmd.Reply("Failed to invite user: %v", err)
- }
-}
-
-func cmdBan(cmd *Command) {
- if len(cmd.Args) < 1 {
- cmd.Reply("Usage: /ban [reason]")
- return
- }
- reason := "you are the weakest link, goodbye!"
- if len(cmd.Args) >= 2 {
- reason = strings.Join(cmd.Args[1:], " ")
- }
- _, err := cmd.Matrix.Client().BanUser(cmd.Room.MxRoom().ID, &mautrix.ReqBanUser{Reason: reason, UserID: id.UserID(cmd.Args[0])})
- if err != nil {
- debug.Print("Error in ban call:", err)
- cmd.Reply("Failed to ban user: %v", err)
- }
-
-}
-
-func cmdUnban(cmd *Command) {
- if len(cmd.Args) != 1 {
- cmd.Reply("Usage: /unban ")
- return
- }
- _, err := cmd.Matrix.Client().UnbanUser(cmd.Room.MxRoom().ID, &mautrix.ReqUnbanUser{UserID: id.UserID(cmd.Args[0])})
- if err != nil {
- debug.Print("Error in unban call:", err)
- cmd.Reply("Failed to unban user: %v", err)
- }
-}
-
-func cmdKick(cmd *Command) {
- if len(cmd.Args) < 1 {
- cmd.Reply("Usage: /kick [reason]")
- return
- }
- reason := "you are the weakest link, goodbye!"
- if len(cmd.Args) >= 2 {
- reason = strings.Join(cmd.Args[1:], " ")
- }
- _, err := cmd.Matrix.Client().KickUser(cmd.Room.MxRoom().ID, &mautrix.ReqKickUser{Reason: reason, UserID: id.UserID(cmd.Args[0])})
- if err != nil {
- debug.Print("Error in kick call:", err)
- debug.Print("Failed to kick user:", err)
- }
-}
-
-func formatPowerLevels(pl *event.PowerLevelsEventContent) string {
- var buf strings.Builder
- buf.WriteString("Membership actions:\n")
- _, _ = fmt.Fprintf(&buf, " Invite: %d\n", pl.Invite())
- _, _ = fmt.Fprintf(&buf, " Kick: %d\n", pl.Kick())
- _, _ = fmt.Fprintf(&buf, " Ban: %d\n", pl.Ban())
- buf.WriteString("Events:\n")
- _, _ = fmt.Fprintf(&buf, " Redact: %d\n", pl.Redact())
- _, _ = fmt.Fprintf(&buf, " State default: %d\n", pl.StateDefault())
- _, _ = fmt.Fprintf(&buf, " Event default: %d\n", pl.EventsDefault)
- for evtType, level := range pl.Events {
- _, _ = fmt.Fprintf(&buf, " %s: %d\n", evtType, level)
- }
- buf.WriteString("Users:\n")
- _, _ = fmt.Fprintf(&buf, " Default: %d\n", pl.UsersDefault)
- for userID, level := range pl.Users {
- _, _ = fmt.Fprintf(&buf, " %s: %d\n", userID, level)
- }
- return strings.TrimSpace(buf.String())
-}
-
-func copyPtr(ptr *int) *int {
- if ptr == nil {
- return nil
- }
- val := *ptr
- return &val
-}
-
-func copyMap[Key comparable](m map[Key]int) map[Key]int {
- if m == nil {
- return nil
- }
- copied := make(map[Key]int, len(m))
- for k, v := range m {
- copied[k] = v
- }
- return copied
-}
-
-func copyPowerLevels(pl *event.PowerLevelsEventContent) *event.PowerLevelsEventContent {
- return &event.PowerLevelsEventContent{
- Users: copyMap(pl.Users),
- Events: copyMap(pl.Events),
- InvitePtr: copyPtr(pl.InvitePtr),
- KickPtr: copyPtr(pl.KickPtr),
- BanPtr: copyPtr(pl.BanPtr),
- RedactPtr: copyPtr(pl.RedactPtr),
- StateDefaultPtr: copyPtr(pl.StateDefaultPtr),
- EventsDefault: pl.EventsDefault,
- UsersDefault: pl.UsersDefault,
- }
-}
-
-var things = `
-[thing] can be one of the following
-
-Literals:
-* invite, kick, ban, redact - special moderation action levels
-* state_default, events_default - default level for state and non-state events
-* users_default - default level for users
-
-Patterns:
-* user ID - specific user level
-* event type - specific event type level
-
-The default levels are 0 for users, 50 for moderators and 100 for admins.`
-
-func cmdPowerLevel(cmd *Command) {
- evt := cmd.Room.MxRoom().GetStateEvent(event.StatePowerLevels, "")
- pl := copyPowerLevels(evt.Content.AsPowerLevels())
- if len(cmd.Args) == 0 {
- // TODO open in modal?
- cmd.Reply(formatPowerLevels(pl))
- return
- } else if len(cmd.Args) < 2 {
- cmd.Reply("Usage: /%s [thing] [level]\n%s", cmd.Command, things)
- return
- }
-
- value, err := strconv.Atoi(cmd.Args[1])
- if err != nil {
- cmd.Reply("Invalid power level %q: %v", cmd.Args[1], err)
- return
- }
-
- ownLevel := pl.GetUserLevel(cmd.Matrix.Client().UserID)
- plChangeLevel := pl.GetEventLevel(event.StatePowerLevels)
- if ownLevel < plChangeLevel {
- cmd.Reply("Can't modify power levels (own level is %d, modifying requires %d)", ownLevel, plChangeLevel)
- return
- } else if value > ownLevel {
- cmd.Reply("Can't set level to be higher than own level (%d > %d)", value, ownLevel)
- return
- }
-
- var oldValue int
- var thing string
- switch cmd.Args[0] {
- case "invite":
- oldValue = pl.Invite()
- pl.InvitePtr = &value
- thing = "invite level"
- case "kick":
- oldValue = pl.Kick()
- pl.KickPtr = &value
- thing = "kick level"
- case "ban":
- oldValue = pl.Ban()
- pl.BanPtr = &value
- thing = "ban level"
- case "redact":
- oldValue = pl.Redact()
- pl.RedactPtr = &value
- thing = "level for redacting other users' events"
- case "state_default":
- oldValue = pl.StateDefault()
- pl.StateDefaultPtr = &value
- thing = "default level for state events"
- case "events_default":
- oldValue = pl.EventsDefault
- pl.EventsDefault = value
- thing = "default level for normal events"
- case "users_default":
- oldValue = pl.UsersDefault
- pl.UsersDefault = value
- thing = "default level for users"
- default:
- userID := id.UserID(cmd.Args[0])
- if _, _, err = userID.Parse(); err == nil {
- if pl.Users == nil {
- pl.Users = make(map[id.UserID]int)
- }
- oldValue = pl.Users[userID]
- if oldValue == ownLevel && userID != cmd.Matrix.Client().UserID {
- cmd.Reply("Can't change level of another user which is equal to own level (%d)", ownLevel)
- return
- }
- pl.Users[userID] = value
- thing = fmt.Sprintf("level of user %s", userID)
- } else {
- if pl.Events == nil {
- pl.Events = make(map[string]int)
- }
- oldValue = pl.Events[cmd.Args[0]]
- pl.Events[cmd.Args[0]] = value
- thing = fmt.Sprintf("level for event %s", cmd.Args[0])
- }
- }
-
- if oldValue == value {
- cmd.Reply("%s is already %d", strings.ToUpper(thing[0:1])+thing[1:], value)
- } else if oldValue > ownLevel {
- cmd.Reply("Can't change level which is higher than own level (%d > %d)", oldValue, ownLevel)
- } else if resp, err := cmd.Matrix.Client().SendStateEvent(cmd.Room.MxRoom().ID, event.StatePowerLevels, "", pl); err != nil {
- if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil {
- err = httpErr.RespError
- }
- cmd.Reply("Failed to set %s to %d: %v", thing, value, err)
- } else {
- cmd.Reply("Successfully set %s to %d\n(event ID: %s)", thing, value, resp.EventID)
- }
-}
-
-func cmdCreateRoom(cmd *Command) {
- req := &mautrix.ReqCreateRoom{}
- if len(cmd.Args) > 0 {
- req.Name = strings.Join(cmd.Args, " ")
- }
- room, err := cmd.Matrix.CreateRoom(req)
- if err != nil {
- cmd.Reply("Failed to create room: %v", err)
- return
- }
- cmd.MainView.SwitchRoom("", room)
-}
-
-func cmdPrivateMessage(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /pm [more user ids...]")
- }
- invites := make([]id.UserID, len(cmd.Args))
- for i, userID := range cmd.Args {
- invites[i] = id.UserID(userID)
- _, _, err := invites[i].Parse()
- if err != nil {
- cmd.Reply("%s isn't a valid user ID", userID)
- return
- }
- }
- req := &mautrix.ReqCreateRoom{
- Preset: "trusted_private_chat",
- Invite: invites,
- IsDirect: true,
- }
- room, err := cmd.Matrix.CreateRoom(req)
- if err != nil {
- cmd.Reply("Failed to create room: %v", err)
- return
- }
- cmd.MainView.SwitchRoom("", room)
-}
-
-func cmdJoin(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /join ")
- return
- }
- identifer := id.RoomID(cmd.Args[0])
- server := ""
- if len(cmd.Args) > 1 {
- server = cmd.Args[1]
- }
- room, err := cmd.Matrix.JoinRoom(identifer, server)
- debug.Print("Join room error:", err)
- if err == nil {
- cmd.MainView.AddRoom(room)
- }
-}
-
-func cmdMSendEvent(cmd *Command) {
- if len(cmd.Args) < 2 {
- cmd.Reply("Usage: /msend ")
- return
- }
- cmd.Args = append([]string{string(cmd.Room.MxRoom().ID)}, cmd.Args...)
- cmdSendEvent(cmd)
-}
-
-func cmdSendEvent(cmd *Command) {
- if len(cmd.Args) < 3 {
- cmd.Reply("Usage: /send ")
- return
- }
- roomID := id.RoomID(cmd.Args[0])
- eventType := event.NewEventType(cmd.Args[1])
- rawContent := strings.Join(cmd.Args[2:], " ")
-
- var content interface{}
- err := json.Unmarshal([]byte(rawContent), &content)
- debug.Print(err)
- if err != nil {
- cmd.Reply("Failed to parse content: %v", err)
- return
- }
- debug.Print("Sending event to", roomID, eventType, content)
-
- resp, err := cmd.Matrix.Client().SendMessageEvent(roomID, eventType, content)
- debug.Print(resp, err)
- if err != nil {
- cmd.Reply("Error from server: %v", err)
- } else {
- cmd.Reply("Event sent, ID: %s", resp.EventID)
- }
-}
-
-func cmdMSetState(cmd *Command) {
- if len(cmd.Args) < 2 {
- cmd.Reply("Usage: /msetstate ")
- return
- }
- cmd.Args = append([]string{string(cmd.Room.MxRoom().ID)}, cmd.Args...)
- cmdSetState(cmd)
-}
-
-func cmdSetState(cmd *Command) {
- if len(cmd.Args) < 4 {
- cmd.Reply("Usage: /setstate ")
- return
- }
-
- roomID := id.RoomID(cmd.Args[0])
- eventType := event.NewEventType(cmd.Args[1])
- stateKey := cmd.Args[2]
- if stateKey == "-" {
- stateKey = ""
- }
- rawContent := strings.Join(cmd.Args[3:], " ")
-
- var content interface{}
- err := json.Unmarshal([]byte(rawContent), &content)
- if err != nil {
- cmd.Reply("Failed to parse content: %v", err)
- return
- }
- debug.Print("Sending state event to", roomID, eventType, stateKey, content)
- resp, err := cmd.Matrix.Client().SendStateEvent(roomID, eventType, stateKey, content)
- if err != nil {
- cmd.Reply("Error from server: %v", err)
- } else {
- cmd.Reply("State event sent, ID: %s", resp.EventID)
- }
-}
-
-type ToggleMessage interface {
- Name() string
- Format(state bool) string
-}
-
-type HideMessage string
-
-func (hm HideMessage) Format(state bool) string {
- if state {
- return string(hm) + " is now hidden"
- } else {
- return string(hm) + " is now visible"
- }
-}
-
-func (hm HideMessage) Name() string {
- return string(hm)
-}
-
-type SimpleToggleMessage string
-
-func (stm SimpleToggleMessage) Format(state bool) string {
- if state {
- return "Disabled " + string(stm)
- } else {
- return "Enabled " + string(stm)
- }
-}
-
-func (stm SimpleToggleMessage) Name() string {
- return string(unicode.ToUpper(rune(stm[0]))) + string(stm[1:])
-}
-
-type InvertedToggleMessage string
-
-func (itm InvertedToggleMessage) Format(state bool) string {
- if state {
- return "Enabled " + string(itm)
- } else {
- return "Disabled " + string(itm)
- }
-}
-
-func (itm InvertedToggleMessage) Name() string {
- return string(unicode.ToUpper(rune(itm[0]))) + string(itm[1:])
-}
-
-var toggleMsg = map[string]ToggleMessage{
- "rooms": HideMessage("Room list sidebar"),
- "users": HideMessage("User list sidebar"),
- "timestamps": HideMessage("message timestamps"),
- "baremessages": InvertedToggleMessage("bare message view"),
- "images": SimpleToggleMessage("image rendering"),
- "typingnotif": SimpleToggleMessage("typing notifications"),
- "emojis": SimpleToggleMessage("emoji shortcode conversion"),
- "html": SimpleToggleMessage("HTML input"),
- "markdown": SimpleToggleMessage("markdown input"),
- "downloads": SimpleToggleMessage("automatic downloads"),
- "notifications": SimpleToggleMessage("desktop notifications"),
- "unverified": SimpleToggleMessage("sending messages to unverified devices"),
- "showurls": SimpleToggleMessage("show URLs in text format"),
- "inlineurls": InvertedToggleMessage("use fancy terminal features to render URLs inside text"),
-}
-
-func makeUsage() string {
- var buf strings.Builder
- buf.WriteString("Usage: /toggle \n\n")
- buf.WriteString("List of Things:\n")
- for key, value := range toggleMsg {
- _, _ = fmt.Fprintf(&buf, "* %s - %s\n", key, value.Name())
- }
- return buf.String()[:buf.Len()-1]
-}
-
-func cmdToggle(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply(makeUsage())
- return
- }
- for _, thing := range cmd.Args {
- var val *bool
- switch thing {
- case "rooms":
- val = &cmd.Config.Preferences.HideRoomList
- case "users":
- val = &cmd.Config.Preferences.HideUserList
- case "timestamps":
- val = &cmd.Config.Preferences.HideTimestamp
- case "baremessages":
- val = &cmd.Config.Preferences.BareMessageView
- case "images":
- val = &cmd.Config.Preferences.DisableImages
- case "typingnotif":
- val = &cmd.Config.Preferences.DisableTypingNotifs
- case "emojis":
- val = &cmd.Config.Preferences.DisableEmojis
- case "html":
- val = &cmd.Config.Preferences.DisableHTML
- case "markdown":
- val = &cmd.Config.Preferences.DisableMarkdown
- case "downloads":
- val = &cmd.Config.Preferences.DisableDownloads
- case "notifications":
- val = &cmd.Config.Preferences.DisableNotifications
- case "unverified":
- val = &cmd.Config.SendToVerifiedOnly
- case "showurls":
- val = &cmd.Config.Preferences.DisableShowURLs
- case "inlineurls":
- switch cmd.Config.Preferences.InlineURLMode {
- case "enable":
- cmd.Config.Preferences.InlineURLMode = "disable"
- cmd.Reply("Force-disabled using fancy terminal features to render URLs inside text. Restart gomuks to apply changes.")
- default:
- cmd.Config.Preferences.InlineURLMode = "enable"
- cmd.Reply("Force-enabled using fancy terminal features to render URLs inside text. Restart gomuks to apply changes.")
- }
- continue
- default:
- cmd.Reply("Unknown toggle %s. Use /toggle without arguments for a list of togglable things.", thing)
- return
- }
- *val = !(*val)
- debug.Print(thing, *val)
- cmd.Reply(toggleMsg[thing].Format(*val))
- if thing == "rooms" {
- // Update topic string to include or not include room name
- cmd.Room.Update()
- }
- }
- cmd.UI.Render()
- go cmd.Matrix.SendPreferencesToMatrix()
-}
-
-func cmdLogout(cmd *Command) {
- cmd.Matrix.Logout()
-}
diff --git a/ui/crypto-commands.go b/ui/crypto-commands.go
deleted file mode 100644
index 9722dcc..0000000
--- a/ui/crypto-commands.go
+++ /dev/null
@@ -1,698 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-//go:build cgo
-
-package ui
-
-import (
- "errors"
- "fmt"
- "io/ioutil"
- "path/filepath"
- "strings"
- "time"
- "unicode"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/crypto"
- "maunium.net/go/mautrix/crypto/ssss"
- "maunium.net/go/mautrix/id"
-
- ifc "maunium.net/go/gomuks/interface"
-)
-
-func autocompleteDeviceUserID(cmd *CommandAutocomplete) (completions []string, newText string) {
- userCompletions := cmd.Room.AutocompleteUser(cmd.Args[0])
- if len(userCompletions) == 1 {
- newText = fmt.Sprintf("/%s %s ", cmd.OrigCommand, userCompletions[0].id)
- } else {
- completions = make([]string, len(userCompletions))
- for i, completion := range userCompletions {
- completions[i] = completion.id
- }
- }
- return
-}
-
-func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, newText string) {
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- devices, err := mach.CryptoStore.GetDevices(id.UserID(cmd.Args[0]))
- if len(devices) == 0 || err != nil {
- return
- }
- var completedDeviceID id.DeviceID
- if len(cmd.Args) > 1 {
- existingID := strings.ToUpper(cmd.Args[1])
- for _, device := range devices {
- deviceIDStr := string(device.DeviceID)
- if deviceIDStr == existingID {
- // We don't want to do any autocompletion if there's already a full device ID there.
- return []string{}, ""
- } else if strings.HasPrefix(strings.ToUpper(device.Name), existingID) || strings.HasPrefix(deviceIDStr, existingID) {
- completedDeviceID = device.DeviceID
- completions = append(completions, fmt.Sprintf("%s (%s)", device.DeviceID, device.Name))
- }
- }
- } else {
- completions = make([]string, len(devices))
- i := 0
- for _, device := range devices {
- completedDeviceID = device.DeviceID
- completions[i] = fmt.Sprintf("%s (%s)", device.DeviceID, device.Name)
- i++
- }
- }
- if len(completions) == 1 {
- newText = fmt.Sprintf("/%s %s %s ", cmd.OrigCommand, cmd.Args[0], completedDeviceID)
- }
- return
-}
-
-func autocompleteUser(cmd *CommandAutocomplete) ([]string, string) {
- if len(cmd.Args) == 1 && !unicode.IsSpace(rune(cmd.RawArgs[len(cmd.RawArgs)-1])) {
- return autocompleteDeviceUserID(cmd)
- }
- return []string{}, ""
-}
-
-func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) {
- if len(cmd.Args) == 0 {
- return []string{}, ""
- } else if len(cmd.Args) == 1 && !unicode.IsSpace(rune(cmd.RawArgs[len(cmd.RawArgs)-1])) {
- return autocompleteDeviceUserID(cmd)
- }
- return autocompleteDeviceDeviceID(cmd)
-}
-
-func getDevice(cmd *Command) *crypto.DeviceIdentity {
- if len(cmd.Args) < 2 {
- cmd.Reply("Usage: /%s [fingerprint]", cmd.Command)
- return nil
- }
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- device, err := mach.GetOrFetchDevice(id.UserID(cmd.Args[0]), id.DeviceID(cmd.Args[1]))
- if err != nil {
- cmd.Reply("Failed to get device: %v", err)
- return nil
- }
- return device
-}
-
-func putDevice(cmd *Command, device *crypto.DeviceIdentity, action string) {
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- err := mach.CryptoStore.PutDevice(device.UserID, device)
- if err != nil {
- cmd.Reply("Failed to save device: %v", err)
- } else {
- cmd.Reply("Successfully %s %s/%s (%s)", action, device.UserID, device.DeviceID, device.Name)
- }
- mach.OnDevicesChanged(device.UserID)
-}
-
-func cmdDevices(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply("Usage: /devices ")
- return
- }
- userID := id.UserID(cmd.Args[0])
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- devices, err := mach.CryptoStore.GetDevices(userID)
- if err != nil {
- cmd.Reply("Failed to get device list: %v", err)
- }
- if len(devices) == 0 {
- cmd.Reply("Fetching device list from server...")
- devices = mach.LoadDevices(userID)
- }
- if len(devices) == 0 {
- cmd.Reply("No devices found for %s", userID)
- return
- }
- var buf strings.Builder
- for _, device := range devices {
- trust := device.Trust.String()
- if device.Trust == crypto.TrustStateUnset && mach.IsDeviceTrusted(device) {
- trust = "verified (transitive)"
- }
- _, _ = fmt.Fprintf(&buf, "%s (%s) - %s\n Fingerprint: %s\n", device.DeviceID, device.Name, trust, device.Fingerprint())
- }
- resp := buf.String()
- cmd.Reply("%s", resp[:len(resp)-1])
-}
-
-func cmdDevice(cmd *Command) {
- device := getDevice(cmd)
- if device == nil {
- return
- }
- deviceType := "Device"
- if device.Deleted {
- deviceType = "Deleted device"
- }
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- trustState := device.Trust.String()
- if device.Trust == crypto.TrustStateUnset && mach.IsDeviceTrusted(device) {
- trustState = "verified (transitive)"
- }
- cmd.Reply("%s %s of %s\nFingerprint: %s\nIdentity key: %s\nDevice name: %s\nTrust state: %s",
- deviceType, device.DeviceID, device.UserID,
- device.Fingerprint(), device.IdentityKey,
- device.Name, trustState)
-}
-
-func crossSignDevice(cmd *Command, device *crypto.DeviceIdentity) {
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- err := mach.SignOwnDevice(device)
- if err != nil {
- cmd.Reply("Failed to upload cross-signing signature: %v", err)
- } else {
- cmd.Reply("Successfully cross-signed %s (%s)", device.DeviceID, device.Name)
- }
-}
-
-func cmdVerifyDevice(cmd *Command) {
- device := getDevice(cmd)
- if device == nil {
- return
- }
- if device.Trust == crypto.TrustStateVerified {
- cmd.Reply("That device is already verified")
- return
- }
- if len(cmd.Args) == 2 {
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- mach.DefaultSASTimeout = 120 * time.Second
- modal := NewVerificationModal(cmd.MainView, device, mach.DefaultSASTimeout)
- cmd.MainView.ShowModal(modal)
- _, err := mach.NewSimpleSASVerificationWith(device, modal)
- if err != nil {
- cmd.Reply("Failed to start interactive verification: %v", err)
- return
- }
- } else {
- fingerprint := strings.Join(cmd.Args[2:], "")
- if string(device.SigningKey) != fingerprint {
- cmd.Reply("Mismatching fingerprint")
- return
- }
- action := "verified"
- if device.Trust == crypto.TrustStateBlacklisted {
- action = "unblacklisted and verified"
- }
- if device.UserID == cmd.Matrix.Client().UserID {
- crossSignDevice(cmd, device)
- device.Trust = crypto.TrustStateVerified
- putDevice(cmd, device, action)
- } else {
- putDevice(cmd, device, action)
- cmd.Reply("Warning: verifying individual devices of other users is not synced with cross-signing")
- }
- }
-}
-
-func cmdVerify(cmd *Command) {
- if len(cmd.Args) < 1 {
- cmd.Reply("Usage: /%s [--force]", cmd.OrigCommand)
- return
- }
- force := len(cmd.Args) >= 2 && strings.ToLower(cmd.Args[1]) == "--force"
- userID := id.UserID(cmd.Args[0])
- room := cmd.Room.Room
- if !room.Encrypted {
- cmd.Reply("In-room verification is only supported in encrypted rooms")
- return
- }
- if (!room.IsDirect || room.OtherUser != userID) && !force {
- cmd.Reply("This doesn't seem to be a direct chat. Either switch to a direct chat with %s, "+
- "or use `--force` to start the verification anyway.", userID)
- return
- }
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- if mach.CrossSigningKeys == nil && !force {
- cmd.Reply("Cross-signing private keys not cached. Generate or fetch cross-signing keys with `/cross-signing`, " +
- "or use `--force` to start the verification anyway")
- return
- }
- modal := NewVerificationModal(cmd.MainView, &crypto.DeviceIdentity{UserID: userID}, mach.DefaultSASTimeout)
- _, err := mach.NewInRoomSASVerificationWith(cmd.Room.Room.ID, userID, modal, 120*time.Second)
- if err != nil {
- cmd.Reply("Failed to start in-room verification: %v", err)
- return
- }
- cmd.MainView.ShowModal(modal)
-}
-
-func cmdUnverify(cmd *Command) {
- device := getDevice(cmd)
- if device == nil {
- return
- }
- if device.Trust == crypto.TrustStateUnset {
- cmd.Reply("That device is already not verified")
- return
- }
- action := "unverified"
- if device.Trust == crypto.TrustStateBlacklisted {
- action = "unblacklisted"
- }
- device.Trust = crypto.TrustStateUnset
- putDevice(cmd, device, action)
-}
-
-func cmdBlacklist(cmd *Command) {
- device := getDevice(cmd)
- if device == nil {
- return
- }
- if device.Trust == crypto.TrustStateBlacklisted {
- cmd.Reply("That device is already blacklisted")
- return
- }
- action := "blacklisted"
- if device.Trust == crypto.TrustStateVerified {
- action = "unverified and blacklisted"
- }
- device.Trust = crypto.TrustStateBlacklisted
- putDevice(cmd, device, action)
-}
-
-func cmdResetSession(cmd *Command) {
- err := cmd.Matrix.Crypto().(*crypto.OlmMachine).CryptoStore.RemoveOutboundGroupSession(cmd.Room.Room.ID)
- if err != nil {
- cmd.Reply("Failed to remove outbound group session: %v", err)
- } else {
- cmd.Reply("Removed outbound group session for this room")
- }
-}
-
-func cmdImportKeys(cmd *Command) {
- path, err := filepath.Abs(cmd.RawArgs)
- if err != nil {
- cmd.Reply("Failed to get absolute path: %v", err)
- return
- }
- data, err := ioutil.ReadFile(path)
- if err != nil {
- cmd.Reply("Failed to read %s: %v", path, err)
- return
- }
- passphrase, ok := cmd.MainView.AskPassword("Key import", "passphrase", "", false)
- if !ok {
- cmd.Reply("Passphrase entry cancelled")
- return
- }
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- imported, total, err := mach.ImportKeys(passphrase, data)
- if err != nil {
- cmd.Reply("Failed to import sessions: %v", err)
- } else {
- cmd.Reply("Successfully imported %d/%d sessions", imported, total)
- }
-}
-
-func exportKeys(cmd *Command, sessions []*crypto.InboundGroupSession) {
- path, err := filepath.Abs(cmd.RawArgs)
- if err != nil {
- cmd.Reply("Failed to get absolute path: %v", err)
- return
- }
- passphrase, ok := cmd.MainView.AskPassword("Key export", "passphrase", "", true)
- if !ok {
- cmd.Reply("Passphrase entry cancelled")
- return
- }
- export, err := crypto.ExportKeys(passphrase, sessions)
- if err != nil {
- cmd.Reply("Failed to export sessions: %v", err)
- }
- err = ioutil.WriteFile(path, export, 0400)
- if err != nil {
- cmd.Reply("Failed to write sessions to %s: %v", path, err)
- } else {
- cmd.Reply("Successfully exported %d sessions to %s", len(sessions), path)
- }
-}
-
-func cmdExportKeys(cmd *Command) {
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- sessions, err := mach.CryptoStore.GetAllGroupSessions()
- if err != nil {
- cmd.Reply("Failed to get sessions to export: %v", err)
- return
- }
- exportKeys(cmd, sessions)
-}
-
-func cmdExportRoomKeys(cmd *Command) {
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
- sessions, err := mach.CryptoStore.GetGroupSessionsForRoom(cmd.Room.MxRoom().ID)
- if err != nil {
- cmd.Reply("Failed to get sessions to export: %v", err)
- return
- }
- exportKeys(cmd, sessions)
-}
-
-const ssssHelp = `Usage: /%s [...]
-
-Subcommands:
-* status [key ID] - Check the status of your SSSS.
-* generate [--set-default] - Generate a SSSS key and optionally set it as the default.
-* set-default - Set a SSSS key as the default.`
-
-func cmdSSSS(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply(ssssHelp, cmd.OrigCommand)
- return
- }
-
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
-
- switch strings.ToLower(cmd.Args[0]) {
- case "status":
- keyID := ""
- if len(cmd.Args) > 1 {
- keyID = cmd.Args[1]
- }
- cmdS4Status(cmd, mach, keyID)
- case "generate":
- setDefault := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--set-default"
- cmdS4Generate(cmd, mach, setDefault)
- case "set-default":
- if len(cmd.Args) < 2 {
- cmd.Reply("Usage: /%s set-default ", cmd.OrigCommand)
- return
- }
- cmdS4SetDefault(cmd, mach, cmd.Args[1])
- default:
- cmd.Reply(ssssHelp, cmd.OrigCommand)
- }
-}
-
-func cmdS4Status(cmd *Command, mach *crypto.OlmMachine, keyID string) {
- var keyData *ssss.KeyMetadata
- var err error
- if len(keyID) == 0 {
- keyID, keyData, err = mach.SSSS.GetDefaultKeyData()
- } else {
- keyData, err = mach.SSSS.GetKeyData(keyID)
- }
- if errors.Is(err, ssss.ErrNoDefaultKeyAccountDataEvent) {
- cmd.Reply("SSSS is not set up: no default key set")
- return
- } else if err != nil {
- cmd.Reply("Failed to get key data: %v", err)
- return
- }
- hasPassphrase := "no"
- if keyData.Passphrase != nil {
- hasPassphrase = fmt.Sprintf("yes (alg=%s,bits=%d,iter=%d)", keyData.Passphrase.Algorithm, keyData.Passphrase.Bits, keyData.Passphrase.Iterations)
- }
- algorithm := keyData.Algorithm
- if algorithm != ssss.AlgorithmAESHMACSHA2 {
- algorithm += " (not supported!)"
- }
- cmd.Reply("Default key is set.\n Key ID: %s\n Has passphrase: %s\n Algorithm: %s", keyID, hasPassphrase, algorithm)
-}
-
-func cmdS4Generate(cmd *Command, mach *crypto.OlmMachine, setDefault bool) {
- passphrase, ok := cmd.MainView.AskPassword("Passphrase", "", "", true)
- if !ok {
- return
- }
-
- key, err := ssss.NewKey(passphrase)
- if err != nil {
- cmd.Reply("Failed to generate new key: %v", err)
- return
- }
-
- err = mach.SSSS.SetKeyData(key.ID, key.Metadata)
- if err != nil {
- cmd.Reply("Failed to upload key metadata: %v", err)
- return
- }
-
- // TODO if we start persisting command replies, the recovery key needs to be moved into a popup
- cmd.Reply("Successfully generated key %s\nRecovery key: %s", key.ID, key.RecoveryKey())
-
- if setDefault {
- err = mach.SSSS.SetDefaultKeyID(key.ID)
- if err != nil {
- cmd.Reply("Failed to set key as default: %v", err)
- }
- } else {
- cmd.Reply("You can use `/%s set-default %s` to set it as the default", cmd.OrigCommand, key.ID)
- }
-}
-
-func cmdS4SetDefault(cmd *Command, mach *crypto.OlmMachine, keyID string) {
- _, err := mach.SSSS.GetKeyData(keyID)
- if err != nil {
- if errors.Is(err, mautrix.MNotFound) {
- cmd.Reply("Couldn't find key data on server")
- } else {
- cmd.Reply("Failed to fetch key data: %v", err)
- }
- return
- }
-
- err = mach.SSSS.SetDefaultKeyID(keyID)
- if err != nil {
- cmd.Reply("Failed to set key as default: %v", err)
- } else {
- cmd.Reply("Successfully set key %s as default", keyID)
- }
-}
-
-const crossSigningHelp = `Usage: /%s [...]
-
-Subcommands:
-* status
- Check the status of your own cross-signing keys.
-* generate [--force]
- Generate and upload new cross-signing keys.
- This will prompt you to enter your account password.
- If you already have existing keys, --force is required.
-* self-sign
- Sign the current device with cached cross-signing keys.
-* fetch [--save-to-disk]
- Fetch your cross-signing keys from SSSS and decrypt them.
- If --save-to-disk is specified, the keys are saved to disk.
-* upload
- Upload your cross-signing keys to SSSS.`
-
-func cmdCrossSigning(cmd *Command) {
- if len(cmd.Args) == 0 {
- cmd.Reply(crossSigningHelp, cmd.OrigCommand)
- return
- }
-
- client := cmd.Matrix.Client()
- mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
-
- switch strings.ToLower(cmd.Args[0]) {
- case "status":
- cmdCrossSigningStatus(cmd, mach)
- case "generate":
- force := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--force"
- cmdCrossSigningGenerate(cmd, cmd.Matrix, mach, client, force)
- case "fetch":
- saveToDisk := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--save-to-disk"
- cmdCrossSigningFetch(cmd, mach, saveToDisk)
- case "upload":
- cmdCrossSigningUpload(cmd, mach)
- case "self-sign":
- cmdCrossSigningSelfSign(cmd, mach)
- default:
- cmd.Reply(crossSigningHelp, cmd.OrigCommand)
- }
-}
-
-func cmdCrossSigningStatus(cmd *Command, mach *crypto.OlmMachine) {
- keys := mach.GetOwnCrossSigningPublicKeys()
- if keys == nil {
- if mach.CrossSigningKeys != nil {
- cmd.Reply("Cross-signing keys are cached, but not published")
- } else {
- cmd.Reply("Didn't find published cross-signing keys")
- }
- return
- }
- if mach.CrossSigningKeys != nil {
- cmd.Reply("Cross-signing keys are published and private keys are cached")
- } else {
- cmd.Reply("Cross-signing keys are published, but private keys are not cached")
- }
- cmd.Reply("Master key: %s", keys.MasterKey)
- cmd.Reply("User signing key: %s", keys.UserSigningKey)
- cmd.Reply("Self-signing key: %s", keys.SelfSigningKey)
-}
-
-func cmdCrossSigningFetch(cmd *Command, mach *crypto.OlmMachine, saveToDisk bool) {
- key := getSSSS(cmd, mach)
- if key == nil {
- return
- }
-
- err := mach.FetchCrossSigningKeysFromSSSS(key)
- if err != nil {
- cmd.Reply("Error fetching cross-signing keys: %v", err)
- return
- }
- if saveToDisk {
- cmd.Reply("Saving keys to disk is not yet implemented")
- }
- cmd.Reply("Successfully unlocked cross-signing keys")
-}
-
-func cmdCrossSigningGenerate(cmd *Command, container ifc.MatrixContainer, mach *crypto.OlmMachine, client *mautrix.Client, force bool) {
- if !force {
- existingKeys := mach.GetOwnCrossSigningPublicKeys()
- if existingKeys != nil {
- cmd.Reply("Found existing cross-signing keys. Use `--force` if you want to overwrite them.")
- return
- }
- }
-
- keys, err := mach.GenerateCrossSigningKeys()
- if err != nil {
- cmd.Reply("Failed to generate cross-signing keys: %v", err)
- return
- }
-
- err = mach.PublishCrossSigningKeys(keys, func(uia *mautrix.RespUserInteractive) interface{} {
- if !uia.HasSingleStageFlow(mautrix.AuthTypePassword) {
- for _, flow := range uia.Flows {
- if len(flow.Stages) != 1 {
- return nil
- }
- cmd.Reply("Opening browser for authentication")
- err := container.UIAFallback(flow.Stages[0], uia.Session)
- if err != nil {
- cmd.Reply("Authentication failed: %v", err)
- return nil
- }
- return &mautrix.ReqUIAuthFallback{
- Session: uia.Session,
- User: mach.Client.UserID.String(),
- }
- }
- cmd.Reply("No supported authentication mechanisms found")
- return nil
- }
- password, ok := cmd.MainView.AskPassword("Account password", "", "correct horse battery staple", false)
- if !ok {
- return nil
- }
- return &mautrix.ReqUIAuthLogin{
- BaseAuthData: mautrix.BaseAuthData{
- Type: mautrix.AuthTypePassword,
- Session: uia.Session,
- },
- User: mach.Client.UserID.String(),
- Password: password,
- }
- })
- if err != nil {
- cmd.Reply("Failed to publish cross-signing keys: %v", err)
- return
- }
- cmd.Reply("Successfully generated and published cross-signing keys")
-
- err = mach.SignOwnMasterKey()
- if err != nil {
- cmd.Reply("Failed to sign master key with device key: %v", err)
- }
-}
-
-func getSSSS(cmd *Command, mach *crypto.OlmMachine) *ssss.Key {
- _, keyData, err := mach.SSSS.GetDefaultKeyData()
- if err != nil {
- if errors.Is(err, mautrix.MNotFound) {
- cmd.Reply("SSSS not set up, use `!ssss generate --set-default` first")
- } else {
- cmd.Reply("Failed to fetch default SSSS key data: %v", err)
- }
- return nil
- }
-
- var key *ssss.Key
- if keyData.Passphrase != nil && keyData.Passphrase.Algorithm == ssss.PassphraseAlgorithmPBKDF2 {
- passphrase, ok := cmd.MainView.AskPassword("Passphrase", "", "correct horse battery staple", false)
- if !ok {
- return nil
- }
- key, err = keyData.VerifyPassphrase(passphrase)
- if errors.Is(err, ssss.ErrIncorrectSSSSKey) {
- cmd.Reply("Incorrect passphrase")
- return nil
- }
- } else {
- recoveryKey, ok := cmd.MainView.AskPassword("Recovery key", "", "tDAK LMRH PiYE bdzi maCe xLX5 wV6P Nmfd c5mC wLef 15Fs VVSc", false)
- if !ok {
- return nil
- }
- key, err = keyData.VerifyRecoveryKey(recoveryKey)
- if errors.Is(err, ssss.ErrInvalidRecoveryKey) {
- cmd.Reply("Malformed recovery key")
- return nil
- } else if errors.Is(err, ssss.ErrIncorrectSSSSKey) {
- cmd.Reply("Incorrect recovery key")
- return nil
- }
- }
- // All the errors should already be handled above, this is just for backup
- if err != nil {
- cmd.Reply("Failed to get SSSS key: %v", err)
- return nil
- }
- return key
-}
-
-func cmdCrossSigningUpload(cmd *Command, mach *crypto.OlmMachine) {
- if mach.CrossSigningKeys == nil {
- cmd.Reply("Cross-signing keys not cached, use `!%s generate` first", cmd.OrigCommand)
- return
- }
-
- key := getSSSS(cmd, mach)
- if key == nil {
- return
- }
-
- err := mach.UploadCrossSigningKeysToSSSS(key, mach.CrossSigningKeys)
- if err != nil {
- cmd.Reply("Failed to upload keys to SSSS: %v", err)
- } else {
- cmd.Reply("Successfully uploaded cross-signing keys to SSSS")
- }
-}
-
-func cmdCrossSigningSelfSign(cmd *Command, mach *crypto.OlmMachine) {
- if mach.CrossSigningKeys == nil {
- cmd.Reply("Cross-signing keys not cached")
- return
- }
-
- err := mach.SignOwnDevice(mach.OwnIdentity())
- if err != nil {
- cmd.Reply("Failed to self-sign: %v", err)
- } else {
- cmd.Reply("Successfully self-signed. This device is now trusted by other devices")
- }
-}
diff --git a/ui/doc.go b/ui/doc.go
deleted file mode 100644
index 804b334..0000000
--- a/ui/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package ui contains the main gomuks UI.
-package ui
diff --git a/ui/fuzzy-search-modal.go b/ui/fuzzy-search-modal.go
deleted file mode 100644
index 07f510f..0000000
--- a/ui/fuzzy-search-modal.go
+++ /dev/null
@@ -1,165 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "sort"
- "strconv"
-
- "github.com/lithammer/fuzzysearch/fuzzy"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-type FuzzySearchModal struct {
- mauview.Component
-
- container *mauview.Box
-
- search *mauview.InputArea
- results *mauview.TextView
-
- matches fuzzy.Ranks
- selected int
-
- roomList []*rooms.Room
- roomTitles []string
-
- parent *MainView
-}
-
-func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal {
- fs := &FuzzySearchModal{
- parent: mainView,
- }
-
- fs.InitList(mainView.rooms)
-
- fs.results = mauview.NewTextView().SetRegions(true)
- fs.search = mauview.NewInputArea().
- SetChangedFunc(fs.changeHandler).
- SetTextColor(tcell.ColorWhite).
- SetBackgroundColor(tcell.ColorDarkCyan)
- fs.search.Focus()
-
- flex := mauview.NewFlex().
- SetDirection(mauview.FlexRow).
- AddFixedComponent(fs.search, 1).
- AddProportionalComponent(fs.results, 1)
-
- fs.container = mauview.NewBox(flex).
- SetBorder(true).
- SetTitle("Quick Room Switcher").
- SetBlurCaptureFunc(func() bool {
- fs.parent.HideModal()
- return true
- })
-
- fs.Component = mauview.Center(fs.container, width, height).SetAlwaysFocusChild(true)
-
- return fs
-}
-
-func (fs *FuzzySearchModal) Focus() {
- fs.container.Focus()
-}
-
-func (fs *FuzzySearchModal) Blur() {
- fs.container.Blur()
-}
-
-func (fs *FuzzySearchModal) InitList(rooms map[id.RoomID]*RoomView) {
- for _, room := range rooms {
- if room.Room.IsReplaced() {
- //if _, ok := rooms[room.Room.ReplacedBy()]; ok
- continue
- }
- fs.roomList = append(fs.roomList, room.Room)
- fs.roomTitles = append(fs.roomTitles, room.Room.GetTitle())
- }
-}
-
-func (fs *FuzzySearchModal) changeHandler(str string) {
- // Get matches and display in result box
- fs.matches = fuzzy.RankFindFold(str, fs.roomTitles)
- if len(str) > 0 && len(fs.matches) > 0 {
- sort.Sort(fs.matches)
- fs.results.Clear()
- for _, match := range fs.matches {
- fmt.Fprintf(fs.results, `["%d"]%s[""]%s`, match.OriginalIndex, match.Target, "\n")
- }
- //fs.parent.parent.Render()
- fs.results.Highlight(strconv.Itoa(fs.matches[0].OriginalIndex))
- fs.selected = 0
- fs.results.ScrollToBeginning()
- } else {
- fs.results.Clear()
- fs.results.Highlight()
- }
-}
-
-func (fs *FuzzySearchModal) OnKeyEvent(event mauview.KeyEvent) bool {
- highlights := fs.results.GetHighlights()
- kb := config.Keybind{
- Key: event.Key(),
- Ch: event.Rune(),
- Mod: event.Modifiers(),
- }
- switch fs.parent.config.Keybindings.Modal[kb] {
- case "cancel":
- // Close room finder
- fs.parent.HideModal()
- return true
- case "select_next":
- // Cycle highlighted area to next match
- if len(highlights) > 0 {
- fs.selected = (fs.selected + 1) % len(fs.matches)
- fs.results.Highlight(strconv.Itoa(fs.matches[fs.selected].OriginalIndex))
- fs.results.ScrollToHighlight()
- }
- return true
- case "select_prev":
- if len(highlights) > 0 {
- fs.selected = (fs.selected - 1) % len(fs.matches)
- if fs.selected < 0 {
- fs.selected += len(fs.matches)
- }
- fs.results.Highlight(strconv.Itoa(fs.matches[fs.selected].OriginalIndex))
- fs.results.ScrollToHighlight()
- }
- return true
- case "confirm":
- // Switch room to currently selected room
- if len(highlights) > 0 {
- debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].OriginalIndex].GetTitle())
- fs.parent.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex])
- }
- fs.parent.HideModal()
- fs.results.Clear()
- fs.search.SetText("")
- return true
- }
- return fs.search.OnKeyEvent(event)
-}
diff --git a/ui/help-modal.go b/ui/help-modal.go
deleted file mode 100644
index 8aec753..0000000
--- a/ui/help-modal.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package ui
-
-import (
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/config"
-)
-
-const helpText = `# General
-/help - Show this help dialog.
-/quit - Quit gomuks.
-/clearcache - Clear cache and quit gomuks.
-/logout - Log out of Matrix.
-/toggle - Temporary command to toggle various UI features.
- Run /toggle without arguments to see the list of toggles.
-
-# Media
-/download [path] - Downloads file from selected message.
-/open [path] - Download file from selected message and open it with xdg-open.
-/upload - Upload the file at the given path to the current room.
-
-# Sending special messages
-/me - Send an emote message.
-/notice - Send a notice (generally used for bot messages).
-/rainbow - Send rainbow text.
-/rainbowme - Send rainbow text in an emote.
-/reply [text] - Reply to the selected message.
-/react - React to the selected message.
-/redact [reason] - Redact the selected message.
-/edit - Edit the selected message.
-
-# Encryption
-/fingerprint - View the fingerprint of your device.
-
-/devices - View the device list of a user.
-/device - Show info about a specific device.
-/unverify - Un-verify a device.
-/blacklist - Blacklist a device.
-/verify - Verify a user with in-room verification. Probably broken.
-/verify-device [fingerprint]
- - Verify a device. If the fingerprint is not provided,
- interactive emoji verification will be started.
-/reset-session - Reset the outbound Megolm session in the current room.
-
-/import - Import encryption keys
-/export - Export encryption keys
-/export-room - Export encryption keys for the current room.
-
-/cross-signing [...]
- - Cross-signing commands. Somewhat experimental.
- Run without arguments for help. (alias: /cs)
-/ssss [...]
- - Secure Secret Storage (and Sharing) commands. Very experimental.
- Run without arguments for help.
-
-# Rooms
-/pm <...> - Create a private chat with the given user(s).
-/create [room name] - Create a room.
-
-/join [server] - Join a room.
-/accept - Accept the invite.
-/reject - Reject the invite.
-
-/invite - Invite the given user to the room.
-/roomnick - Change your per-room displayname.
-/tag - Add the room to .
-/untag - Remove the room from .
-/tags - List the tags the room is in.
-/alias - Add or remove local addresses.
-
-/leave - Leave the current room.
-/kick [reason] - Kick a user.
-/ban [reason] - Ban a user.
-/unban - Unban a user.`
-
-type HelpModal struct {
- mauview.FocusableComponent
- parent *MainView
-}
-
-func NewHelpModal(parent *MainView) *HelpModal {
- hm := &HelpModal{parent: parent}
-
- text := mauview.NewTextView().
- SetText(helpText).
- SetScrollable(true).
- SetWrap(false).
- SetTextColor(tcell.ColorDefault)
-
- box := mauview.NewBox(text).
- SetBorder(true).
- SetTitle("Help").
- SetBlurCaptureFunc(func() bool {
- hm.parent.HideModal()
- return true
- })
- box.Focus()
-
- hm.FocusableComponent = mauview.FractionalCenter(box, 42, 10, 0.5, 0.5)
-
- return hm
-}
-
-func (hm *HelpModal) OnKeyEvent(event mauview.KeyEvent) bool {
- kb := config.Keybind{
- Key: event.Key(),
- Ch: event.Rune(),
- Mod: event.Modifiers(),
- }
- // TODO unhardcode q
- if hm.parent.config.Keybindings.Modal[kb] == "cancel" || event.Rune() == 'q' {
- hm.parent.HideModal()
- return true
- }
- return hm.FocusableComponent.OnKeyEvent(event)
-}
diff --git a/ui/member-list.go b/ui/member-list.go
deleted file mode 100644
index 089c156..0000000
--- a/ui/member-list.go
+++ /dev/null
@@ -1,128 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "math"
- "sort"
- "strings"
-
- "github.com/mattn/go-runewidth"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type MemberList struct {
- list roomMemberList
-}
-
-func NewMemberList() *MemberList {
- return &MemberList{}
-}
-
-type memberListItem struct {
- rooms.Member
- PowerLevel int
- Sigil rune
- UserID id.UserID
- Color tcell.Color
-}
-
-type roomMemberList []*memberListItem
-
-func (rml roomMemberList) Len() int {
- return len(rml)
-}
-
-func (rml roomMemberList) Less(i, j int) bool {
- if rml[i].PowerLevel != rml[j].PowerLevel {
- return rml[i].PowerLevel > rml[j].PowerLevel
- }
- return strings.Compare(strings.ToLower(rml[i].Displayname), strings.ToLower(rml[j].Displayname)) < 0
-}
-
-func (rml roomMemberList) Swap(i, j int) {
- rml[i], rml[j] = rml[j], rml[i]
-}
-
-func (ml *MemberList) Update(data map[id.UserID]*rooms.Member, levels *event.PowerLevelsEventContent) *MemberList {
- ml.list = make(roomMemberList, len(data))
- i := 0
- highestLevel := math.MinInt32
- count := 0
- for _, level := range levels.Users {
- if level > highestLevel {
- highestLevel = level
- count = 1
- } else if level == highestLevel {
- count++
- }
- }
- for userID, member := range data {
- level := levels.GetUserLevel(userID)
- sigil := ' '
- if level == highestLevel && count == 1 {
- sigil = '~'
- } else if level > levels.StateDefault() {
- sigil = '&'
- } else if level >= levels.Ban() {
- sigil = '@'
- } else if level >= levels.Kick() || level >= levels.Redact() {
- sigil = '%'
- } else if level > levels.UsersDefault {
- sigil = '+'
- }
- ml.list[i] = &memberListItem{
- Member: *member,
- UserID: userID,
- PowerLevel: level,
- Sigil: sigil,
- Color: widget.GetHashColor(userID),
- }
- i++
- }
- sort.Sort(ml.list)
- return ml
-}
-
-func (ml *MemberList) Draw(screen mauview.Screen) {
- width, _ := screen.Size()
- sigilStyle := tcell.StyleDefault.Background(tcell.ColorGreen).Foreground(tcell.ColorDefault)
- for y, member := range ml.list {
- if member.Sigil != ' ' {
- screen.SetCell(0, y, sigilStyle, member.Sigil)
- }
- if member.Membership == "invite" {
- widget.WriteLineSimpleColor(screen, member.Displayname, 2, y, member.Color)
- screen.SetCell(1, y, tcell.StyleDefault, '(')
- if sw := runewidth.StringWidth(member.Displayname); sw+2 < width {
- screen.SetCell(sw+2, y, tcell.StyleDefault, ')')
- } else {
- screen.SetCell(width-1, y, tcell.StyleDefault, ')')
- }
- } else {
- widget.WriteLineSimpleColor(screen, member.Displayname, 1, y, member.Color)
- }
- }
-}
diff --git a/ui/message-view.go b/ui/message-view.go
deleted file mode 100644
index 6d05546..0000000
--- a/ui/message-view.go
+++ /dev/null
@@ -1,682 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "math"
- "strings"
- "sync/atomic"
-
- "github.com/mattn/go-runewidth"
- sync "github.com/sasha-s/go-deadlock"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/lib/open"
- "maunium.net/go/gomuks/ui/messages"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type MessageView struct {
- parent *RoomView
- config *config.Config
-
- ScrollOffset int
- MaxSenderWidth int
- DateFormat string
- TimestampFormat string
- TimestampWidth int
-
- // Used for locking
- loadingMessages int32
- historyLoadPtr uint64
-
- _widestSender uint32
- _prevWidestSender uint32
-
- _width uint32
- _height uint32
- _prevWidth uint32
- _prevHeight uint32
-
- prevMsgCount int
- prevPrefs config.UserPreferences
-
- messageIDLock sync.RWMutex
- messageIDs map[id.EventID]*messages.UIMessage
- messagesLock sync.RWMutex
- messages []*messages.UIMessage
- msgBufferLock sync.RWMutex
- msgBuffer []*messages.UIMessage
- selected *messages.UIMessage
-
- initialHistoryLoaded bool
-}
-
-func NewMessageView(parent *RoomView) *MessageView {
- return &MessageView{
- parent: parent,
- config: parent.config,
-
- MaxSenderWidth: 15,
- TimestampWidth: len(messages.TimeFormat),
- ScrollOffset: 0,
-
- messages: make([]*messages.UIMessage, 0),
- messageIDs: make(map[id.EventID]*messages.UIMessage),
- msgBuffer: make([]*messages.UIMessage, 0),
-
- _widestSender: 5,
- _prevWidestSender: 0,
-
- _width: 80,
- _prevWidth: 0,
- _prevHeight: 0,
- prevMsgCount: -1,
- }
-}
-
-func (view *MessageView) Unload() {
- debug.Print("Unloading message view", view.parent.Room.ID)
- view.messagesLock.Lock()
- view.msgBufferLock.Lock()
- view.messageIDLock.Lock()
- view.messageIDs = make(map[id.EventID]*messages.UIMessage)
- view.msgBuffer = make([]*messages.UIMessage, 0)
- view.messages = make([]*messages.UIMessage, 0)
- view.initialHistoryLoaded = false
- view.ScrollOffset = 0
- view._widestSender = 5
- view.prevMsgCount = -1
- view.historyLoadPtr = 0
- view.messagesLock.Unlock()
- view.msgBufferLock.Unlock()
- view.messageIDLock.Unlock()
-}
-
-func (view *MessageView) updateWidestSender(sender string) {
- if len(sender) > int(view._widestSender) {
- if len(sender) > view.MaxSenderWidth {
- atomic.StoreUint32(&view._widestSender, uint32(view.MaxSenderWidth))
- } else {
- atomic.StoreUint32(&view._widestSender, uint32(len(sender)))
- }
- }
-}
-
-type MessageDirection int
-
-const (
- AppendMessage MessageDirection = iota
- PrependMessage
- IgnoreMessage
-)
-
-func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDirection) {
- if ifcMessage == nil {
- return
- }
- message, ok := ifcMessage.(*messages.UIMessage)
- if !ok || message == nil {
- debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().")
- debug.PrintStack()
- return
- }
-
- var oldMsg *messages.UIMessage
- if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil {
- view.replaceMessage(oldMsg, message)
- direction = IgnoreMessage
- } else if oldMsg = view.getMessageByID(id.EventID(message.TxnID)); oldMsg != nil {
- view.replaceMessage(oldMsg, message)
- view.deleteMessageID(id.EventID(message.TxnID))
- direction = IgnoreMessage
- }
-
- view.updateWidestSender(message.Sender())
-
- width := view.width()
- bare := view.config.Preferences.BareMessageView
- if !bare {
- width -= view.widestSender() + SenderMessageGap
- if !view.config.Preferences.HideTimestamp {
- width -= view.TimestampWidth + TimestampSenderGap
- }
- }
- message.CalculateBuffer(view.config.Preferences, width)
-
- makeDateChange := func(msg *messages.UIMessage) *messages.UIMessage {
- dateChange := messages.NewDateChangeMessage(
- fmt.Sprintf("Date changed to %s", msg.FormatDate()))
- dateChange.CalculateBuffer(view.config.Preferences, width)
- view.appendBuffer(dateChange)
- return dateChange
- }
-
- if direction == AppendMessage {
- if view.ScrollOffset > 0 {
- view.ScrollOffset += message.Height()
- }
- view.messagesLock.Lock()
- if len(view.messages) > 0 && !view.messages[len(view.messages)-1].SameDate(message) {
- view.messages = append(view.messages, makeDateChange(message), message)
- } else {
- view.messages = append(view.messages, message)
- }
- view.messagesLock.Unlock()
- view.appendBuffer(message)
- } else if direction == PrependMessage {
- view.messagesLock.Lock()
- if len(view.messages) > 0 && !view.messages[0].SameDate(message) {
- view.messages = append([]*messages.UIMessage{message, makeDateChange(view.messages[0])}, view.messages...)
- } else {
- view.messages = append([]*messages.UIMessage{message}, view.messages...)
- }
- view.messagesLock.Unlock()
- } else if oldMsg != nil {
- view.replaceBuffer(oldMsg, message)
- } else {
- debug.Print("Unexpected AddMessage() call: Direction is not append or prepend, but message is new.")
- debug.PrintStack()
- }
-
- if len(message.ID()) > 0 {
- view.setMessageID(message)
- }
-}
-
-func (view *MessageView) replaceMessage(original *messages.UIMessage, new *messages.UIMessage) {
- if len(new.ID()) > 0 {
- view.setMessageID(new)
- }
- view.messagesLock.Lock()
- for index, msg := range view.messages {
- if msg == original {
- view.messages[index] = new
- }
- }
- view.messagesLock.Unlock()
-}
-
-func (view *MessageView) getMessageByID(id id.EventID) *messages.UIMessage {
- if id == "" {
- return nil
- }
- view.messageIDLock.RLock()
- defer view.messageIDLock.RUnlock()
- msg, ok := view.messageIDs[id]
- if !ok {
- return nil
- }
- return msg
-}
-
-func (view *MessageView) deleteMessageID(id id.EventID) {
- if id == "" {
- return
- }
- view.messageIDLock.Lock()
- delete(view.messageIDs, id)
- view.messageIDLock.Unlock()
-}
-
-func (view *MessageView) setMessageID(message *messages.UIMessage) {
- if message.ID() == "" {
- return
- }
- view.messageIDLock.Lock()
- view.messageIDs[message.ID()] = message
- view.messageIDLock.Unlock()
-}
-
-func (view *MessageView) appendBuffer(message *messages.UIMessage) {
- view.msgBufferLock.Lock()
- view.appendBufferUnlocked(message)
- view.msgBufferLock.Unlock()
-}
-
-func (view *MessageView) appendBufferUnlocked(message *messages.UIMessage) {
- for i := 0; i < message.Height(); i++ {
- view.msgBuffer = append(view.msgBuffer, message)
- }
- view.prevMsgCount++
-}
-
-func (view *MessageView) replaceBuffer(original *messages.UIMessage, new *messages.UIMessage) {
- start := -1
- end := -1
- view.msgBufferLock.RLock()
- for index, meta := range view.msgBuffer {
- if meta == original {
- if start == -1 {
- start = index
- }
- end = index
- } else if start != -1 {
- break
- }
- }
- view.msgBufferLock.RUnlock()
-
- if start == -1 {
- debug.Print("Called replaceBuffer() with message that was not in the buffer:", original)
- //debug.PrintStack()
- view.appendBuffer(new)
- return
- }
-
- if len(view.msgBuffer) > end {
- end++
- }
-
- if new.Height() == 0 {
- new.CalculateBuffer(view.prevPrefs, view.prevWidth())
- }
-
- view.msgBufferLock.Lock()
- if new.Height() != end-start {
- height := new.Height()
-
- newBuffer := make([]*messages.UIMessage, height+len(view.msgBuffer)-end)
- for i := 0; i < height; i++ {
- newBuffer[i] = new
- }
- for i := height; i < len(newBuffer); i++ {
- newBuffer[i] = view.msgBuffer[end+(i-height)]
- }
- view.msgBuffer = append(view.msgBuffer[0:start], newBuffer...)
- } else {
- for i := start; i < end; i++ {
- view.msgBuffer[i] = new
- }
- }
- view.msgBufferLock.Unlock()
-}
-
-func (view *MessageView) recalculateBuffers() {
- prefs := view.config.Preferences
- recalculateMessageBuffers := view.width() != view.prevWidth() ||
- view.widestSender() != view.prevWidestSender() ||
- view.prevPrefs.BareMessageView != prefs.BareMessageView ||
- view.prevPrefs.DisableImages != prefs.DisableImages
- view.messagesLock.RLock()
- view.msgBufferLock.Lock()
- if recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
- width := view.width()
- if !prefs.BareMessageView {
- width -= view.widestSender() + SenderMessageGap
- if !prefs.HideTimestamp {
- width -= view.TimestampWidth + TimestampSenderGap
- }
- }
- view.msgBuffer = []*messages.UIMessage{}
- view.prevMsgCount = 0
- for i, message := range view.messages {
- if message == nil {
- debug.Print("O.o found nil message at", i)
- break
- }
- if recalculateMessageBuffers {
- message.CalculateBuffer(prefs, width)
- }
- view.appendBufferUnlocked(message)
- }
- }
- view.msgBufferLock.Unlock()
- view.messagesLock.RUnlock()
- view.updatePrevSize()
- view.prevPrefs = prefs
-}
-
-func (view *MessageView) SetSelected(message *messages.UIMessage) {
- if view.selected != nil {
- view.selected.IsSelected = false
- }
- if message != nil && (view.selected == message || message.IsService) {
- view.selected = nil
- } else {
- view.selected = message
- }
- if view.selected != nil {
- view.selected.IsSelected = true
- }
-}
-
-func (view *MessageView) handleMessageClick(message *messages.UIMessage, mod tcell.ModMask) bool {
- if msg, ok := message.Renderer.(*messages.FileMessage); ok && mod > 0 && !msg.Thumbnail.IsEmpty() {
- debug.Print("Opening thumbnail", msg.ThumbnailPath())
- open.Open(msg.ThumbnailPath())
- // No need to re-render
- return false
- }
- view.SetSelected(message)
- view.parent.OnSelect(view.selected)
- return true
-}
-
-func (view *MessageView) handleUsernameClick(message *messages.UIMessage, prevMessage *messages.UIMessage) bool {
- // TODO this is needed if senders are hidden for messages from the same sender (see Draw method)
- //if prevMessage != nil && prevMessage.SenderName == message.SenderName {
- // return false
- //}
-
- if message.SenderName == "---" || message.SenderName == "-->" || message.SenderName == "<--" || message.Type == event.MsgEmote {
- return false
- }
-
- sender := fmt.Sprintf("[%s](https://matrix.to/#/%s)", message.SenderName, message.SenderID)
-
- cursorPos := view.parent.input.GetCursorOffset()
- text := view.parent.input.GetText()
- var buf strings.Builder
- if cursorPos == 0 {
- buf.WriteString(sender)
- buf.WriteRune(':')
- buf.WriteRune(' ')
- buf.WriteString(text)
- } else {
- textBefore := runewidth.Truncate(text, cursorPos, "")
- textAfter := text[len(textBefore):]
- buf.WriteString(textBefore)
- buf.WriteString(sender)
- buf.WriteRune(' ')
- buf.WriteString(textAfter)
- }
- newText := buf.String()
- view.parent.input.SetText(string(newText))
- view.parent.input.SetCursorOffset(cursorPos + len(newText) - len(text))
- return true
-}
-
-func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool {
- if event.HasMotion() {
- return false
- }
- switch event.Buttons() {
- case tcell.WheelUp:
- if view.IsAtTop() {
- go view.parent.parent.LoadHistory(view.parent.Room.ID)
- } else {
- view.AddScrollOffset(WheelScrollOffsetDiff)
- return true
- }
- case tcell.WheelDown:
- view.AddScrollOffset(-WheelScrollOffsetDiff)
- view.parent.parent.MarkRead(view.parent)
- return true
- case tcell.Button1:
- x, y := event.Position()
- line := view.TotalHeight() - view.ScrollOffset - view.Height() + y
- if line < 0 || line >= view.TotalHeight() {
- return false
- }
-
- view.msgBufferLock.RLock()
- message := view.msgBuffer[line]
- var prevMessage *messages.UIMessage
- if y != 0 && line > 0 {
- prevMessage = view.msgBuffer[line-1]
- }
- view.msgBufferLock.RUnlock()
-
- usernameX := 0
- if !view.config.Preferences.HideTimestamp {
- usernameX += view.TimestampWidth + TimestampSenderGap
- }
- messageX := usernameX + view.widestSender() + SenderMessageGap
-
- if x >= messageX {
- return view.handleMessageClick(message, event.Modifiers())
- } else if x >= usernameX {
- return view.handleUsernameClick(message, prevMessage)
- }
- }
- return false
-}
-
-const PaddingAtTop = 5
-
-func (view *MessageView) AddScrollOffset(diff int) {
- totalHeight := view.TotalHeight()
- height := view.Height()
- if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
- view.ScrollOffset = totalHeight - height + PaddingAtTop
- } else {
- view.ScrollOffset += diff
- }
-
- if view.ScrollOffset > totalHeight-height+PaddingAtTop {
- view.ScrollOffset = totalHeight - height + PaddingAtTop
- }
- if view.ScrollOffset < 0 {
- view.ScrollOffset = 0
- }
-}
-
-func (view *MessageView) setSize(width, height int) {
- atomic.StoreUint32(&view._width, uint32(width))
- atomic.StoreUint32(&view._height, uint32(height))
-}
-
-func (view *MessageView) updatePrevSize() {
- atomic.StoreUint32(&view._prevWidth, atomic.LoadUint32(&view._width))
- atomic.StoreUint32(&view._prevHeight, atomic.LoadUint32(&view._height))
- atomic.StoreUint32(&view._prevWidestSender, atomic.LoadUint32(&view._widestSender))
-}
-
-func (view *MessageView) prevHeight() int {
- return int(atomic.LoadUint32(&view._prevHeight))
-}
-
-func (view *MessageView) prevWidth() int {
- return int(atomic.LoadUint32(&view._prevWidth))
-}
-
-func (view *MessageView) prevWidestSender() int {
- return int(atomic.LoadUint32(&view._prevWidestSender))
-}
-
-func (view *MessageView) widestSender() int {
- return int(atomic.LoadUint32(&view._widestSender))
-}
-
-func (view *MessageView) Height() int {
- return int(atomic.LoadUint32(&view._height))
-}
-
-func (view *MessageView) width() int {
- return int(atomic.LoadUint32(&view._width))
-}
-
-func (view *MessageView) TotalHeight() int {
- view.msgBufferLock.RLock()
- defer view.msgBufferLock.RUnlock()
- return len(view.msgBuffer)
-}
-
-func (view *MessageView) IsAtTop() bool {
- return view.ScrollOffset >= view.TotalHeight()-view.Height()+PaddingAtTop
-}
-
-const (
- TimestampSenderGap = 1
- SenderSeparatorGap = 1
- SenderMessageGap = 3
-)
-
-func getScrollbarStyle(scrollbarHere, isTop, isBottom bool) (char rune, style tcell.Style) {
- char = '│'
- style = tcell.StyleDefault
- if scrollbarHere {
- style = style.Foreground(tcell.ColorGreen)
- }
- if isTop {
- if scrollbarHere {
- char = '╥'
- } else {
- char = '┬'
- }
- } else if isBottom {
- if scrollbarHere {
- char = '╨'
- } else {
- char = '┴'
- }
- } else if scrollbarHere {
- char = '║'
- }
- return
-}
-
-func (view *MessageView) calculateScrollBar(height int) (scrollBarHeight, scrollBarPos int) {
- viewportHeight := float64(height)
- contentHeight := float64(view.TotalHeight())
-
- scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight)))
-
- scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
-
- return
-}
-
-func (view *MessageView) getIndexOffset(screen mauview.Screen, height, messageX int) (indexOffset int) {
- indexOffset = view.TotalHeight() - view.ScrollOffset - height
- if indexOffset <= -PaddingAtTop {
- message := "Scroll up to load more messages."
- if atomic.LoadInt32(&view.loadingMessages) == 1 {
- message = "Loading more messages..."
- }
- widget.WriteLineSimpleColor(screen, message, messageX, 0, tcell.ColorGreen)
- }
- return
-}
-
-func (view *MessageView) CapturePlaintext(height int) string {
- var buf strings.Builder
- indexOffset := view.TotalHeight() - view.ScrollOffset - height
- var prevMessage *messages.UIMessage
- view.msgBufferLock.RLock()
- for line := 0; line < height; line++ {
- index := indexOffset + line
- if index < 0 {
- continue
- }
-
- message := view.msgBuffer[index]
- if message != prevMessage {
- var sender string
- if len(message.Sender()) > 0 {
- sender = fmt.Sprintf(" <%s>", message.Sender())
- } else if message.Type == event.MsgEmote {
- sender = fmt.Sprintf(" * %s", message.SenderName)
- }
- fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText())
- prevMessage = message
- }
- }
- view.msgBufferLock.RUnlock()
- return buf.String()
-}
-
-func (view *MessageView) Draw(screen mauview.Screen) {
- view.setSize(screen.Size())
- view.recalculateBuffers()
-
- height := view.Height()
- if view.TotalHeight() == 0 {
- widget.WriteLineSimple(screen, "It's quite empty in here.", 0, height)
- return
- }
-
- usernameX := 0
- if !view.config.Preferences.HideTimestamp {
- usernameX += view.TimestampWidth + TimestampSenderGap
- }
- messageX := usernameX + view.widestSender() + SenderMessageGap
-
- bareMode := view.config.Preferences.BareMessageView
- if bareMode {
- messageX = 0
- }
-
- indexOffset := view.getIndexOffset(screen, height, messageX)
-
- viewStart := 0
- if indexOffset < 0 {
- viewStart = -indexOffset
- }
-
- if !bareMode {
- separatorX := usernameX + view.widestSender() + SenderSeparatorGap
- scrollBarHeight, scrollBarPos := view.calculateScrollBar(height)
-
- for line := viewStart; line < height; line++ {
- showScrollbar := line-viewStart >= scrollBarPos-scrollBarHeight && line-viewStart < scrollBarPos
- isTop := line == viewStart && view.ScrollOffset+height >= view.TotalHeight()
- isBottom := line == height-1 && view.ScrollOffset == 0
-
- borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
-
- screen.SetContent(separatorX, line, borderChar, nil, borderStyle)
- }
- }
-
- var prevMsg *messages.UIMessage
- view.msgBufferLock.RLock()
- for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); {
- index := indexOffset + line
-
- msg := view.msgBuffer[index]
- if msg == prevMsg {
- debug.Print("Unexpected re-encounter of", msg, msg.Height(), "at", line, index)
- line++
- continue
- }
-
- if len(msg.FormatTime()) > 0 && !view.config.Preferences.HideTimestamp {
- widget.WriteLineSimpleColor(screen, msg.FormatTime(), 0, line, msg.TimestampColor())
- }
- // TODO hiding senders might not be that nice after all, maybe an option? (disabled for now)
- //if !bareMode && (prevMsg == nil || meta.Sender() != prevMsg.Sender()) {
- widget.WriteLineColor(
- screen, mauview.AlignRight, msg.Sender(),
- usernameX, line, view.widestSender(),
- msg.SenderColor())
- //}
- if msg.Edited {
- // TODO add better indicator for edits
- screen.SetCell(usernameX+view.widestSender(), line, tcell.StyleDefault.Foreground(tcell.ColorDarkRed), '*')
- }
-
- for i := index - 1; i >= 0 && view.msgBuffer[i] == msg; i-- {
- line--
- }
- msg.Draw(mauview.NewProxyScreen(screen, messageX, line, view.width()-messageX, msg.Height()))
- line += msg.Height()
-
- prevMsg = msg
- }
- view.msgBufferLock.RUnlock()
-}
diff --git a/ui/messages/base.go b/ui/messages/base.go
deleted file mode 100644
index 85b1219..0000000
--- a/ui/messages/base.go
+++ /dev/null
@@ -1,396 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "fmt"
- "sort"
- "time"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/matrix/muksevt"
-
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type MessageRenderer interface {
- Draw(screen mauview.Screen, msg *UIMessage)
- NotificationContent() string
- PlainText() string
- CalculateBuffer(prefs config.UserPreferences, width int, msg *UIMessage)
- Height() int
- Clone() MessageRenderer
- String() string
-}
-
-type ReactionItem struct {
- Key string
- Count int
-}
-
-func (ri ReactionItem) String() string {
- return fmt.Sprintf("%d×%s", ri.Count, ri.Key)
-}
-
-type ReactionSlice []ReactionItem
-
-func (rs ReactionSlice) Len() int {
- return len(rs)
-}
-
-func (rs ReactionSlice) Less(i, j int) bool {
- return rs[i].Key < rs[j].Key
-}
-
-func (rs ReactionSlice) Swap(i, j int) {
- rs[i], rs[j] = rs[j], rs[i]
-}
-
-type UIMessage struct {
- EventID id.EventID
- TxnID string
- Relation event.RelatesTo
- Type event.MessageType
- SenderID id.UserID
- SenderName string
- DefaultSenderColor tcell.Color
- Timestamp time.Time
- State muksevt.OutgoingState
- IsHighlight bool
- IsService bool
- IsSelected bool
- Edited bool
- Event *muksevt.Event
- ReplyTo *UIMessage
- Reactions ReactionSlice
- Renderer MessageRenderer
-}
-
-func (msg *UIMessage) GetEvent() *muksevt.Event {
- if msg == nil {
- return nil
- }
- return msg.Event
-}
-
-const DateFormat = "January _2, 2006"
-const TimeFormat = "15:04:05"
-
-func newUIMessage(evt *muksevt.Event, displayname string, renderer MessageRenderer) *UIMessage {
- msgContent := evt.Content.AsMessage()
- msgtype := msgContent.MsgType
- if len(msgtype) == 0 {
- msgtype = event.MessageType(evt.Type.String())
- }
-
- reactions := make(ReactionSlice, 0, len(evt.Unsigned.Relations.Annotations.Map))
- for key, count := range evt.Unsigned.Relations.Annotations.Map {
- reactions = append(reactions, ReactionItem{
- Key: key,
- Count: count,
- })
- }
- sort.Sort(reactions)
-
- return &UIMessage{
- SenderID: evt.Sender,
- SenderName: displayname,
- Timestamp: unixToTime(evt.Timestamp),
- DefaultSenderColor: widget.GetHashColor(evt.Sender),
- Type: msgtype,
- EventID: evt.ID,
- TxnID: evt.Unsigned.TransactionID,
- Relation: *msgContent.GetRelatesTo(),
- State: evt.Gomuks.OutgoingState,
- IsHighlight: false,
- IsService: false,
- Edited: len(evt.Gomuks.Edits) > 0,
- Reactions: reactions,
- Event: evt,
- Renderer: renderer,
- }
-}
-
-func (msg *UIMessage) AddReaction(key string) {
- found := false
- for i, rs := range msg.Reactions {
- if rs.Key == key {
- rs.Count++
- msg.Reactions[i] = rs
- found = true
- break
- }
- }
- if !found {
- msg.Reactions = append(msg.Reactions, ReactionItem{
- Key: key,
- Count: 1,
- })
- }
- sort.Sort(msg.Reactions)
-}
-
-func unixToTime(unix int64) time.Time {
- timestamp := time.Now()
- if unix != 0 {
- timestamp = time.Unix(unix/1000, unix%1000*1000)
- }
- return timestamp
-}
-
-// Sender gets the string that should be displayed as the sender of this message.
-//
-// If the message is being sent, the sender is "Sending...".
-// If sending has failed, the sender is "Error".
-// If the message is an emote, the sender is blank.
-// In any other case, the sender is the display name of the user who sent the message.
-func (msg *UIMessage) Sender() string {
- switch msg.State {
- case muksevt.StateLocalEcho:
- return "Sending..."
- case muksevt.StateSendFail:
- return "Error"
- }
- switch msg.Type {
- case "m.emote":
- // Emotes don't show a separate sender, it's included in the buffer.
- return ""
- default:
- return msg.SenderName
- }
-}
-
-func (msg *UIMessage) NotificationSenderName() string {
- return msg.SenderName
-}
-
-func (msg *UIMessage) NotificationContent() string {
- return msg.Renderer.NotificationContent()
-}
-
-func (msg *UIMessage) getStateSpecificColor() tcell.Color {
- switch msg.State {
- case muksevt.StateLocalEcho:
- return tcell.ColorGray
- case muksevt.StateSendFail:
- return tcell.ColorRed
- case muksevt.StateDefault:
- fallthrough
- default:
- return tcell.ColorDefault
- }
-}
-
-// SenderColor returns the color the name of the sender should be shown in.
-//
-// If the message is being sent, the color is gray.
-// If sending has failed, the color is red.
-//
-// In any other case, the color is whatever is specified in the Message struct.
-// Usually that means it is the hash-based color of the sender (see ui/widget/color.go)
-func (msg *UIMessage) SenderColor() tcell.Color {
- stateColor := msg.getStateSpecificColor()
- switch {
- case stateColor != tcell.ColorDefault:
- return stateColor
- case msg.Type == "m.room.member":
- return widget.GetHashColor(msg.SenderName)
- case msg.IsService:
- return tcell.ColorGray
- default:
- return msg.DefaultSenderColor
- }
-}
-
-// TextColor returns the color the actual content of the message should be shown in.
-func (msg *UIMessage) TextColor() tcell.Color {
- stateColor := msg.getStateSpecificColor()
- switch {
- case stateColor != tcell.ColorDefault:
- return stateColor
- case msg.IsService, msg.Type == "m.notice":
- return tcell.ColorGray
- case msg.IsHighlight:
- return tcell.ColorYellow
- case msg.Type == "m.room.member":
- return tcell.ColorGreen
- default:
- return tcell.ColorDefault
- }
-}
-
-// TimestampColor returns the color the timestamp should be shown in.
-//
-// As with SenderColor(), messages being sent and messages that failed to be sent are
-// gray and red respectively.
-//
-// However, other messages are the default color instead of a color stored in the struct.
-func (msg *UIMessage) TimestampColor() tcell.Color {
- if msg.IsService {
- return tcell.ColorGray
- }
- return msg.getStateSpecificColor()
-}
-
-func (msg *UIMessage) ReplyHeight() int {
- if msg.ReplyTo != nil {
- return 1 + msg.ReplyTo.Height()
- }
- return 0
-}
-
-func (msg *UIMessage) ReactionHeight() int {
- if len(msg.Reactions) > 0 {
- return 1
- }
- return 0
-}
-
-// Height returns the number of rows in the computed buffer (see Buffer()).
-func (msg *UIMessage) Height() int {
- return msg.ReplyHeight() + msg.Renderer.Height() + msg.ReactionHeight()
-}
-
-func (msg *UIMessage) Time() time.Time {
- return msg.Timestamp
-}
-
-// FormatTime returns the formatted time when the message was sent.
-func (msg *UIMessage) FormatTime() string {
- return msg.Timestamp.Format(TimeFormat)
-}
-
-// FormatDate returns the formatted date when the message was sent.
-func (msg *UIMessage) FormatDate() string {
- return msg.Timestamp.Format(DateFormat)
-}
-
-func (msg *UIMessage) SameDate(message *UIMessage) bool {
- year1, month1, day1 := msg.Timestamp.Date()
- year2, month2, day2 := message.Timestamp.Date()
- return day1 == day2 && month1 == month2 && year1 == year2
-}
-
-func (msg *UIMessage) ID() id.EventID {
- if len(msg.EventID) == 0 {
- return id.EventID(msg.TxnID)
- }
- return msg.EventID
-}
-
-func (msg *UIMessage) SetID(id id.EventID) {
- msg.EventID = id
-}
-
-func (msg *UIMessage) SetIsHighlight(isHighlight bool) {
- msg.IsHighlight = isHighlight
-}
-
-func (msg *UIMessage) DrawReactions(screen mauview.Screen) {
- if len(msg.Reactions) == 0 {
- return
- }
- width, height := screen.Size()
- screen = mauview.NewProxyScreen(screen, 0, height-1, width, 1)
-
- x := 0
- for _, reaction := range msg.Reactions {
- _, drawn := mauview.PrintWithStyle(screen, reaction.String(), x, 0, width-x, mauview.AlignLeft, tcell.StyleDefault.Foreground(mauview.Styles.PrimaryTextColor).Background(tcell.ColorDarkGreen))
- x += drawn + 1
- if x >= width {
- break
- }
- }
-}
-
-func (msg *UIMessage) Draw(screen mauview.Screen) {
- proxyScreen := msg.DrawReply(screen)
- msg.Renderer.Draw(proxyScreen, msg)
- msg.DrawReactions(proxyScreen)
- if msg.IsSelected {
- w, h := screen.Size()
- for x := 0; x < w; x++ {
- for y := 0; y < h; y++ {
- mainc, combc, style, _ := screen.GetContent(x, y)
- _, bg, _ := style.Decompose()
- if bg == tcell.ColorDefault {
- screen.SetContent(x, y, mainc, combc, style.Background(tcell.ColorDarkGreen))
- }
- }
- }
- }
-}
-
-func (msg *UIMessage) Clone() *UIMessage {
- clone := *msg
- clone.ReplyTo = nil
- clone.Reactions = nil
- clone.Renderer = clone.Renderer.Clone()
- return &clone
-}
-
-func (msg *UIMessage) CalculateReplyBuffer(preferences config.UserPreferences, width int) {
- if msg.ReplyTo == nil {
- return
- }
- msg.ReplyTo.CalculateBuffer(preferences, width-1)
-}
-
-func (msg *UIMessage) CalculateBuffer(preferences config.UserPreferences, width int) {
- msg.Renderer.CalculateBuffer(preferences, width, msg)
- msg.CalculateReplyBuffer(preferences, width)
-}
-
-func (msg *UIMessage) DrawReply(screen mauview.Screen) mauview.Screen {
- if msg.ReplyTo == nil {
- return screen
- }
- width, height := screen.Size()
- replyHeight := msg.ReplyTo.Height()
- widget.WriteLineSimpleColor(screen, "In reply to", 1, 0, tcell.ColorGreen)
- widget.WriteLineSimpleColor(screen, msg.ReplyTo.SenderName, 13, 0, msg.ReplyTo.SenderColor())
- for y := 0; y < 1+replyHeight; y++ {
- screen.SetCell(0, y, tcell.StyleDefault, '▊')
- }
- replyScreen := mauview.NewProxyScreen(screen, 1, 1, width-1, replyHeight)
- msg.ReplyTo.Draw(replyScreen)
- return mauview.NewProxyScreen(screen, 0, replyHeight+1, width, height-replyHeight-1)
-}
-
-func (msg *UIMessage) String() string {
- return fmt.Sprintf(`&messages.UIMessage{
- ID="%s", TxnID="%s",
- Type="%s", Timestamp=%s,
- Sender={ID="%s", Name="%s", Color=#%X},
- IsService=%t, IsHighlight=%t,
- Renderer=%s,
-}`,
- msg.EventID, msg.TxnID,
- msg.Type, msg.Timestamp.String(),
- msg.SenderID, msg.SenderName, msg.DefaultSenderColor.Hex(),
- msg.IsService, msg.IsHighlight, msg.Renderer.String())
-}
-
-func (msg *UIMessage) PlainText() string {
- return msg.Renderer.PlainText()
-}
diff --git a/ui/messages/doc.go b/ui/messages/doc.go
deleted file mode 100644
index 289c308..0000000
--- a/ui/messages/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package messages contains different message types and code to generate and render them.
-package messages
diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go
deleted file mode 100644
index ea001ab..0000000
--- a/ui/messages/expandedtextmessage.go
+++ /dev/null
@@ -1,102 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "fmt"
- "time"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/matrix/muksevt"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/ui/messages/tstring"
-)
-
-type ExpandedTextMessage struct {
- Text tstring.TString
- buffer []tstring.TString
-}
-
-// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state.
-func NewExpandedTextMessage(evt *muksevt.Event, displayname string, text tstring.TString) *UIMessage {
- return newUIMessage(evt, displayname, &ExpandedTextMessage{
- Text: text,
- })
-}
-
-func NewServiceMessage(text string) *UIMessage {
- return &UIMessage{
- SenderID: "*",
- SenderName: "*",
- Timestamp: time.Now(),
- IsService: true,
- Renderer: &ExpandedTextMessage{
- Text: tstring.NewTString(text),
- },
- }
-}
-
-func NewDateChangeMessage(text string) *UIMessage {
- midnight := time.Now()
- midnight = time.Date(midnight.Year(), midnight.Month(), midnight.Day(),
- 0, 0, 0, 0,
- midnight.Location())
- return &UIMessage{
- SenderID: "*",
- SenderName: "*",
- Timestamp: midnight,
- IsService: true,
- Renderer: &ExpandedTextMessage{
- Text: tstring.NewColorTString(text, tcell.ColorGreen),
- },
- }
-}
-
-func (msg *ExpandedTextMessage) Clone() MessageRenderer {
- return &ExpandedTextMessage{
- Text: msg.Text.Clone(),
- }
-}
-
-func (msg *ExpandedTextMessage) NotificationContent() string {
- return msg.Text.String()
-}
-
-func (msg *ExpandedTextMessage) PlainText() string {
- return msg.Text.String()
-}
-
-func (msg *ExpandedTextMessage) String() string {
- return fmt.Sprintf(`&messages.ExpandedTextMessage{Text="%s"}`, msg.Text.String())
-}
-
-func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
- msg.buffer = calculateBufferWithText(prefs, msg.Text, width, uiMsg)
-}
-
-func (msg *ExpandedTextMessage) Height() int {
- return len(msg.buffer)
-}
-
-func (msg *ExpandedTextMessage) Draw(screen mauview.Screen, _ *UIMessage) {
- for y, line := range msg.buffer {
- line.Draw(screen, 0, y)
- }
-}
diff --git a/ui/messages/filemessage.go b/ui/messages/filemessage.go
deleted file mode 100644
index abc0b52..0000000
--- a/ui/messages/filemessage.go
+++ /dev/null
@@ -1,190 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "bytes"
- "fmt"
- "image"
- "image/color"
-
- "maunium.net/go/mautrix/crypto/attachment"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/lib/ansimage"
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/ui/messages/tstring"
-)
-
-type FileMessage struct {
- Type event.MessageType
- Body string
-
- URL id.ContentURI
- File *attachment.EncryptedFile
- Thumbnail id.ContentURI
- ThumbnailFile *attachment.EncryptedFile
-
- eventID id.EventID
-
- imageData []byte
- buffer []tstring.TString
-
- matrix ifc.MatrixContainer
-}
-
-// NewFileMessage creates a new FileMessage object with the provided values and the default state.
-func NewFileMessage(matrix ifc.MatrixContainer, evt *muksevt.Event, displayname string) *UIMessage {
- content := evt.Content.AsMessage()
- var file, thumbnailFile *attachment.EncryptedFile
- if content.File != nil {
- file = &content.File.EncryptedFile
- content.URL = content.File.URL
- }
- if content.GetInfo().ThumbnailFile != nil {
- thumbnailFile = &content.Info.ThumbnailFile.EncryptedFile
- content.Info.ThumbnailURL = content.Info.ThumbnailFile.URL
- }
- return newUIMessage(evt, displayname, &FileMessage{
- Type: content.MsgType,
- Body: content.Body,
- URL: content.URL.ParseOrIgnore(),
- File: file,
- Thumbnail: content.GetInfo().ThumbnailURL.ParseOrIgnore(),
- ThumbnailFile: thumbnailFile,
- eventID: evt.ID,
- matrix: matrix,
- })
-}
-
-func (msg *FileMessage) Clone() MessageRenderer {
- data := make([]byte, len(msg.imageData))
- copy(data, msg.imageData)
- return &FileMessage{
- Body: msg.Body,
- URL: msg.URL,
- Thumbnail: msg.Thumbnail,
- imageData: data,
- matrix: msg.matrix,
- }
-}
-
-func (msg *FileMessage) NotificationContent() string {
- switch msg.Type {
- case event.MsgImage:
- return "Sent an image"
- case event.MsgAudio:
- return "Sent an audio file"
- case event.MsgVideo:
- return "Sent a video"
- case event.MsgFile:
- fallthrough
- default:
- return "Sent a file"
- }
-}
-
-func (msg *FileMessage) PlainText() string {
- return fmt.Sprintf("%s: %s", msg.Body, msg.matrix.GetDownloadURL(msg.URL, msg.File))
-}
-
-func (msg *FileMessage) String() string {
- return fmt.Sprintf(`&messages.FileMessage{Body="%s", URL="%s", Thumbnail="%s"}`, msg.Body, msg.URL, msg.Thumbnail)
-}
-
-func (msg *FileMessage) DownloadPreview() {
- var url id.ContentURI
- var file *attachment.EncryptedFile
- if !msg.Thumbnail.IsEmpty() {
- url = msg.Thumbnail
- file = msg.ThumbnailFile
- } else if msg.Type == event.MsgImage && !msg.URL.IsEmpty() {
- msg.Thumbnail = msg.URL
- url = msg.URL
- file = msg.File
- } else {
- return
- }
- debug.Print("Loading file:", url)
- data, err := msg.matrix.Download(url, file)
- if err != nil {
- debug.Printf("Failed to download file %s: %v", url, err)
- return
- }
- debug.Print("File", url, "loaded.")
- msg.imageData = data
-}
-
-func (msg *FileMessage) ThumbnailPath() string {
- return msg.matrix.GetCachePath(msg.Thumbnail)
-}
-
-func (msg *FileMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
- if width < 2 {
- return
- }
-
- if prefs.BareMessageView || prefs.DisableImages || len(msg.imageData) == 0 {
- url := msg.matrix.GetDownloadURL(msg.URL, msg.File)
- var urlTString tstring.TString
- if prefs.EnableInlineURLs() {
- urlTString = tstring.NewStyleTString(url, tcell.StyleDefault.Url(url).UrlId(msg.eventID.String()))
- } else {
- urlTString = tstring.NewTString(url)
- }
- text := tstring.NewTString(msg.Body).
- Append(": ").
- AppendTString(urlTString)
- msg.buffer = calculateBufferWithText(prefs, text, width, uiMsg)
- return
- }
-
- img, _, err := image.DecodeConfig(bytes.NewReader(msg.imageData))
- if err != nil {
- debug.Print("File could not be decoded:", err)
- }
- imgWidth := img.Width
- if img.Width > width {
- imgWidth = width / 3
- }
-
- ansFile, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.imageData), 0, imgWidth, color.Black)
- if err != nil {
- msg.buffer = []tstring.TString{tstring.NewColorTString("Failed to display image", tcell.ColorRed)}
- debug.Print("Failed to display image:", err)
- return
- }
-
- msg.buffer = ansFile.Render()
-}
-
-func (msg *FileMessage) Height() int {
- return len(msg.buffer)
-}
-
-func (msg *FileMessage) Draw(screen mauview.Screen, _ *UIMessage) {
- for y, line := range msg.buffer {
- line.Draw(screen, 0, y)
- }
-}
diff --git a/ui/messages/html/base.go b/ui/messages/html/base.go
deleted file mode 100644
index 16eb9fb..0000000
--- a/ui/messages/html/base.go
+++ /dev/null
@@ -1,101 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-package html
-
-import (
- "fmt"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-type BaseEntity struct {
- // The HTML tag of this entity.
- Tag string
- // Style for this entity.
- Style tcell.Style
- // Whether or not this is a block-type entity.
- Block bool
- // Height to use for entity if both text and children are empty.
- DefaultHeight int
-
- prevWidth int
- startX int
- height int
-}
-
-// AdjustStyle changes the style of this text entity.
-func (be *BaseEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- be.Style = fn(be.Style)
- return be
-}
-
-func (be *BaseEntity) IsEmpty() bool {
- return false
-}
-
-// IsBlock returns whether or not this is a block-type entity.
-func (be *BaseEntity) IsBlock() bool {
- return be.Block
-}
-
-// GetTag returns the HTML tag of this entity.
-func (be *BaseEntity) GetTag() string {
- return be.Tag
-}
-
-// Height returns the render height of this entity.
-func (be *BaseEntity) Height() int {
- return be.height
-}
-
-func (be *BaseEntity) getStartX() int {
- return be.startX
-}
-
-// Clone creates a copy of this base entity.
-func (be *BaseEntity) Clone() Entity {
- return &BaseEntity{
- Tag: be.Tag,
- Style: be.Style,
- Block: be.Block,
- DefaultHeight: be.DefaultHeight,
- }
-}
-
-func (be *BaseEntity) PlainText() string {
- return ""
-}
-
-// String returns a textual representation of this BaseEntity struct.
-func (be *BaseEntity) String() string {
- return fmt.Sprintf(`&html.BaseEntity{Tag="%s", Style=%#v, Block=%t, startX=%d, height=%d}`,
- be.Tag, be.Style, be.Block, be.startX, be.height)
-}
-
-// CalculateBuffer prepares this entity for rendering with the given parameters.
-func (be *BaseEntity) CalculateBuffer(width, startX int, ctx DrawContext) int {
- be.height = be.DefaultHeight
- be.startX = startX
- if be.Block {
- be.startX = 0
- }
- return be.startX
-}
-
-func (be *BaseEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- panic("Called Draw() of BaseEntity")
-}
diff --git a/ui/messages/html/blockquote.go b/ui/messages/html/blockquote.go
deleted file mode 100644
index 25123da..0000000
--- a/ui/messages/html/blockquote.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "fmt"
- "strings"
-
- "go.mau.fi/mauview"
-)
-
-type BlockquoteEntity struct {
- *ContainerEntity
-}
-
-const BlockQuoteChar = '>'
-
-func NewBlockquoteEntity(children []Entity) *BlockquoteEntity {
- return &BlockquoteEntity{&ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: "blockquote",
- Block: true,
- },
- Children: children,
- Indent: 2,
- }}
-}
-
-func (be *BlockquoteEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- be.BaseEntity = be.BaseEntity.AdjustStyle(fn, reason).(*BaseEntity)
- return be
-}
-
-func (be *BlockquoteEntity) Clone() Entity {
- return &BlockquoteEntity{ContainerEntity: be.ContainerEntity.Clone().(*ContainerEntity)}
-}
-
-func (be *BlockquoteEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- be.ContainerEntity.Draw(screen, ctx)
- for y := 0; y < be.height; y++ {
- screen.SetContent(0, y, BlockQuoteChar, nil, be.Style)
- }
-}
-
-func (be *BlockquoteEntity) PlainText() string {
- if len(be.Children) == 0 {
- return ""
- }
- var buf strings.Builder
- newlined := false
- for i, child := range be.Children {
- if i != 0 && child.IsBlock() && !newlined {
- buf.WriteRune('\n')
- }
- newlined = false
- for i, row := range strings.Split(child.PlainText(), "\n") {
- if i != 0 {
- buf.WriteRune('\n')
- }
- buf.WriteRune('>')
- buf.WriteRune(' ')
- buf.WriteString(row)
- }
- if child.IsBlock() {
- buf.WriteRune('\n')
- newlined = true
- }
- }
- return strings.TrimSpace(buf.String())
-}
-
-func (be *BlockquoteEntity) String() string {
- return fmt.Sprintf("&html.BlockquoteEntity{%s},\n", be.BaseEntity)
-}
diff --git a/ui/messages/html/break.go b/ui/messages/html/break.go
deleted file mode 100644
index f702a76..0000000
--- a/ui/messages/html/break.go
+++ /dev/null
@@ -1,54 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "go.mau.fi/mauview"
-)
-
-type BreakEntity struct {
- *BaseEntity
-}
-
-func NewBreakEntity() *BreakEntity {
- return &BreakEntity{&BaseEntity{
- Tag: "br",
- Block: true,
- }}
-}
-
-// AdjustStyle changes the style of this text entity.
-func (be *BreakEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- be.BaseEntity = be.BaseEntity.AdjustStyle(fn, reason).(*BaseEntity)
- return be
-}
-
-func (be *BreakEntity) Clone() Entity {
- return NewBreakEntity()
-}
-
-func (be *BreakEntity) PlainText() string {
- return "\n"
-}
-
-func (be *BreakEntity) String() string {
- return "&html.BreakEntity{},\n"
-}
-
-func (be *BreakEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- // No-op, the logic happens in containers
-}
diff --git a/ui/messages/html/codeblock.go b/ui/messages/html/codeblock.go
deleted file mode 100644
index 42d4433..0000000
--- a/ui/messages/html/codeblock.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-type CodeBlockEntity struct {
- *ContainerEntity
- Background tcell.Style
-}
-
-func NewCodeBlockEntity(children []Entity, background tcell.Style) *CodeBlockEntity {
- return &CodeBlockEntity{
- ContainerEntity: &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: "pre",
- Block: true,
- },
- Children: children,
- },
- Background: background,
- }
-}
-
-func (ce *CodeBlockEntity) Clone() Entity {
- return &CodeBlockEntity{
- ContainerEntity: ce.ContainerEntity.Clone().(*ContainerEntity),
- Background: ce.Background,
- }
-}
-
-func (ce *CodeBlockEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- screen.Fill(' ', ce.Background)
- ce.ContainerEntity.Draw(screen, ctx)
-}
-
-func (ce *CodeBlockEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- if reason != AdjustStyleReasonNormal {
- ce.ContainerEntity.AdjustStyle(fn, reason)
- }
- return ce
-}
diff --git a/ui/messages/html/colormap.go b/ui/messages/html/colormap.go
deleted file mode 100644
index 305309c..0000000
--- a/ui/messages/html/colormap.go
+++ /dev/null
@@ -1,156 +0,0 @@
-// From https://github.com/golang/image/blob/master/colornames/colornames.go
-package html
-
-import (
- "image/color"
-)
-
-var colorMap = map[string]color.RGBA{
- "aliceblue": {0xf0, 0xf8, 0xff, 0xff}, // rgb(240, 248, 255)
- "antiquewhite": {0xfa, 0xeb, 0xd7, 0xff}, // rgb(250, 235, 215)
- "aqua": {0x00, 0xff, 0xff, 0xff}, // rgb(0, 255, 255)
- "aquamarine": {0x7f, 0xff, 0xd4, 0xff}, // rgb(127, 255, 212)
- "azure": {0xf0, 0xff, 0xff, 0xff}, // rgb(240, 255, 255)
- "beige": {0xf5, 0xf5, 0xdc, 0xff}, // rgb(245, 245, 220)
- "bisque": {0xff, 0xe4, 0xc4, 0xff}, // rgb(255, 228, 196)
- "black": {0x00, 0x00, 0x00, 0xff}, // rgb(0, 0, 0)
- "blanchedalmond": {0xff, 0xeb, 0xcd, 0xff}, // rgb(255, 235, 205)
- "blue": {0x00, 0x00, 0xff, 0xff}, // rgb(0, 0, 255)
- "blueviolet": {0x8a, 0x2b, 0xe2, 0xff}, // rgb(138, 43, 226)
- "brown": {0xa5, 0x2a, 0x2a, 0xff}, // rgb(165, 42, 42)
- "burlywood": {0xde, 0xb8, 0x87, 0xff}, // rgb(222, 184, 135)
- "cadetblue": {0x5f, 0x9e, 0xa0, 0xff}, // rgb(95, 158, 160)
- "chartreuse": {0x7f, 0xff, 0x00, 0xff}, // rgb(127, 255, 0)
- "chocolate": {0xd2, 0x69, 0x1e, 0xff}, // rgb(210, 105, 30)
- "coral": {0xff, 0x7f, 0x50, 0xff}, // rgb(255, 127, 80)
- "cornflowerblue": {0x64, 0x95, 0xed, 0xff}, // rgb(100, 149, 237)
- "cornsilk": {0xff, 0xf8, 0xdc, 0xff}, // rgb(255, 248, 220)
- "crimson": {0xdc, 0x14, 0x3c, 0xff}, // rgb(220, 20, 60)
- "cyan": {0x00, 0xff, 0xff, 0xff}, // rgb(0, 255, 255)
- "darkblue": {0x00, 0x00, 0x8b, 0xff}, // rgb(0, 0, 139)
- "darkcyan": {0x00, 0x8b, 0x8b, 0xff}, // rgb(0, 139, 139)
- "darkgoldenrod": {0xb8, 0x86, 0x0b, 0xff}, // rgb(184, 134, 11)
- "darkgray": {0xa9, 0xa9, 0xa9, 0xff}, // rgb(169, 169, 169)
- "darkgreen": {0x00, 0x64, 0x00, 0xff}, // rgb(0, 100, 0)
- "darkgrey": {0xa9, 0xa9, 0xa9, 0xff}, // rgb(169, 169, 169)
- "darkkhaki": {0xbd, 0xb7, 0x6b, 0xff}, // rgb(189, 183, 107)
- "darkmagenta": {0x8b, 0x00, 0x8b, 0xff}, // rgb(139, 0, 139)
- "darkolivegreen": {0x55, 0x6b, 0x2f, 0xff}, // rgb(85, 107, 47)
- "darkorange": {0xff, 0x8c, 0x00, 0xff}, // rgb(255, 140, 0)
- "darkorchid": {0x99, 0x32, 0xcc, 0xff}, // rgb(153, 50, 204)
- "darkred": {0x8b, 0x00, 0x00, 0xff}, // rgb(139, 0, 0)
- "darksalmon": {0xe9, 0x96, 0x7a, 0xff}, // rgb(233, 150, 122)
- "darkseagreen": {0x8f, 0xbc, 0x8f, 0xff}, // rgb(143, 188, 143)
- "darkslateblue": {0x48, 0x3d, 0x8b, 0xff}, // rgb(72, 61, 139)
- "darkslategray": {0x2f, 0x4f, 0x4f, 0xff}, // rgb(47, 79, 79)
- "darkslategrey": {0x2f, 0x4f, 0x4f, 0xff}, // rgb(47, 79, 79)
- "darkturquoise": {0x00, 0xce, 0xd1, 0xff}, // rgb(0, 206, 209)
- "darkviolet": {0x94, 0x00, 0xd3, 0xff}, // rgb(148, 0, 211)
- "deeppink": {0xff, 0x14, 0x93, 0xff}, // rgb(255, 20, 147)
- "deepskyblue": {0x00, 0xbf, 0xff, 0xff}, // rgb(0, 191, 255)
- "dimgray": {0x69, 0x69, 0x69, 0xff}, // rgb(105, 105, 105)
- "dimgrey": {0x69, 0x69, 0x69, 0xff}, // rgb(105, 105, 105)
- "dodgerblue": {0x1e, 0x90, 0xff, 0xff}, // rgb(30, 144, 255)
- "firebrick": {0xb2, 0x22, 0x22, 0xff}, // rgb(178, 34, 34)
- "floralwhite": {0xff, 0xfa, 0xf0, 0xff}, // rgb(255, 250, 240)
- "forestgreen": {0x22, 0x8b, 0x22, 0xff}, // rgb(34, 139, 34)
- "fuchsia": {0xff, 0x00, 0xff, 0xff}, // rgb(255, 0, 255)
- "gainsboro": {0xdc, 0xdc, 0xdc, 0xff}, // rgb(220, 220, 220)
- "ghostwhite": {0xf8, 0xf8, 0xff, 0xff}, // rgb(248, 248, 255)
- "gold": {0xff, 0xd7, 0x00, 0xff}, // rgb(255, 215, 0)
- "goldenrod": {0xda, 0xa5, 0x20, 0xff}, // rgb(218, 165, 32)
- "gray": {0x80, 0x80, 0x80, 0xff}, // rgb(128, 128, 128)
- "green": {0x00, 0x80, 0x00, 0xff}, // rgb(0, 128, 0)
- "greenyellow": {0xad, 0xff, 0x2f, 0xff}, // rgb(173, 255, 47)
- "grey": {0x80, 0x80, 0x80, 0xff}, // rgb(128, 128, 128)
- "honeydew": {0xf0, 0xff, 0xf0, 0xff}, // rgb(240, 255, 240)
- "hotpink": {0xff, 0x69, 0xb4, 0xff}, // rgb(255, 105, 180)
- "indianred": {0xcd, 0x5c, 0x5c, 0xff}, // rgb(205, 92, 92)
- "indigo": {0x4b, 0x00, 0x82, 0xff}, // rgb(75, 0, 130)
- "ivory": {0xff, 0xff, 0xf0, 0xff}, // rgb(255, 255, 240)
- "khaki": {0xf0, 0xe6, 0x8c, 0xff}, // rgb(240, 230, 140)
- "lavender": {0xe6, 0xe6, 0xfa, 0xff}, // rgb(230, 230, 250)
- "lavenderblush": {0xff, 0xf0, 0xf5, 0xff}, // rgb(255, 240, 245)
- "lawngreen": {0x7c, 0xfc, 0x00, 0xff}, // rgb(124, 252, 0)
- "lemonchiffon": {0xff, 0xfa, 0xcd, 0xff}, // rgb(255, 250, 205)
- "lightblue": {0xad, 0xd8, 0xe6, 0xff}, // rgb(173, 216, 230)
- "lightcoral": {0xf0, 0x80, 0x80, 0xff}, // rgb(240, 128, 128)
- "lightcyan": {0xe0, 0xff, 0xff, 0xff}, // rgb(224, 255, 255)
- "lightgoldenrodyellow": {0xfa, 0xfa, 0xd2, 0xff}, // rgb(250, 250, 210)
- "lightgray": {0xd3, 0xd3, 0xd3, 0xff}, // rgb(211, 211, 211)
- "lightgreen": {0x90, 0xee, 0x90, 0xff}, // rgb(144, 238, 144)
- "lightgrey": {0xd3, 0xd3, 0xd3, 0xff}, // rgb(211, 211, 211)
- "lightpink": {0xff, 0xb6, 0xc1, 0xff}, // rgb(255, 182, 193)
- "lightsalmon": {0xff, 0xa0, 0x7a, 0xff}, // rgb(255, 160, 122)
- "lightseagreen": {0x20, 0xb2, 0xaa, 0xff}, // rgb(32, 178, 170)
- "lightskyblue": {0x87, 0xce, 0xfa, 0xff}, // rgb(135, 206, 250)
- "lightslategray": {0x77, 0x88, 0x99, 0xff}, // rgb(119, 136, 153)
- "lightslategrey": {0x77, 0x88, 0x99, 0xff}, // rgb(119, 136, 153)
- "lightsteelblue": {0xb0, 0xc4, 0xde, 0xff}, // rgb(176, 196, 222)
- "lightyellow": {0xff, 0xff, 0xe0, 0xff}, // rgb(255, 255, 224)
- "lime": {0x00, 0xff, 0x00, 0xff}, // rgb(0, 255, 0)
- "limegreen": {0x32, 0xcd, 0x32, 0xff}, // rgb(50, 205, 50)
- "linen": {0xfa, 0xf0, 0xe6, 0xff}, // rgb(250, 240, 230)
- "magenta": {0xff, 0x00, 0xff, 0xff}, // rgb(255, 0, 255)
- "maroon": {0x80, 0x00, 0x00, 0xff}, // rgb(128, 0, 0)
- "mediumaquamarine": {0x66, 0xcd, 0xaa, 0xff}, // rgb(102, 205, 170)
- "mediumblue": {0x00, 0x00, 0xcd, 0xff}, // rgb(0, 0, 205)
- "mediumorchid": {0xba, 0x55, 0xd3, 0xff}, // rgb(186, 85, 211)
- "mediumpurple": {0x93, 0x70, 0xdb, 0xff}, // rgb(147, 112, 219)
- "mediumseagreen": {0x3c, 0xb3, 0x71, 0xff}, // rgb(60, 179, 113)
- "mediumslateblue": {0x7b, 0x68, 0xee, 0xff}, // rgb(123, 104, 238)
- "mediumspringgreen": {0x00, 0xfa, 0x9a, 0xff}, // rgb(0, 250, 154)
- "mediumturquoise": {0x48, 0xd1, 0xcc, 0xff}, // rgb(72, 209, 204)
- "mediumvioletred": {0xc7, 0x15, 0x85, 0xff}, // rgb(199, 21, 133)
- "midnightblue": {0x19, 0x19, 0x70, 0xff}, // rgb(25, 25, 112)
- "mintcream": {0xf5, 0xff, 0xfa, 0xff}, // rgb(245, 255, 250)
- "mistyrose": {0xff, 0xe4, 0xe1, 0xff}, // rgb(255, 228, 225)
- "moccasin": {0xff, 0xe4, 0xb5, 0xff}, // rgb(255, 228, 181)
- "navajowhite": {0xff, 0xde, 0xad, 0xff}, // rgb(255, 222, 173)
- "navy": {0x00, 0x00, 0x80, 0xff}, // rgb(0, 0, 128)
- "oldlace": {0xfd, 0xf5, 0xe6, 0xff}, // rgb(253, 245, 230)
- "olive": {0x80, 0x80, 0x00, 0xff}, // rgb(128, 128, 0)
- "olivedrab": {0x6b, 0x8e, 0x23, 0xff}, // rgb(107, 142, 35)
- "orange": {0xff, 0xa5, 0x00, 0xff}, // rgb(255, 165, 0)
- "orangered": {0xff, 0x45, 0x00, 0xff}, // rgb(255, 69, 0)
- "orchid": {0xda, 0x70, 0xd6, 0xff}, // rgb(218, 112, 214)
- "palegoldenrod": {0xee, 0xe8, 0xaa, 0xff}, // rgb(238, 232, 170)
- "palegreen": {0x98, 0xfb, 0x98, 0xff}, // rgb(152, 251, 152)
- "paleturquoise": {0xaf, 0xee, 0xee, 0xff}, // rgb(175, 238, 238)
- "palevioletred": {0xdb, 0x70, 0x93, 0xff}, // rgb(219, 112, 147)
- "papayawhip": {0xff, 0xef, 0xd5, 0xff}, // rgb(255, 239, 213)
- "peachpuff": {0xff, 0xda, 0xb9, 0xff}, // rgb(255, 218, 185)
- "peru": {0xcd, 0x85, 0x3f, 0xff}, // rgb(205, 133, 63)
- "pink": {0xff, 0xc0, 0xcb, 0xff}, // rgb(255, 192, 203)
- "plum": {0xdd, 0xa0, 0xdd, 0xff}, // rgb(221, 160, 221)
- "powderblue": {0xb0, 0xe0, 0xe6, 0xff}, // rgb(176, 224, 230)
- "purple": {0x80, 0x00, 0x80, 0xff}, // rgb(128, 0, 128)
- "red": {0xff, 0x00, 0x00, 0xff}, // rgb(255, 0, 0)
- "rosybrown": {0xbc, 0x8f, 0x8f, 0xff}, // rgb(188, 143, 143)
- "royalblue": {0x41, 0x69, 0xe1, 0xff}, // rgb(65, 105, 225)
- "saddlebrown": {0x8b, 0x45, 0x13, 0xff}, // rgb(139, 69, 19)
- "salmon": {0xfa, 0x80, 0x72, 0xff}, // rgb(250, 128, 114)
- "sandybrown": {0xf4, 0xa4, 0x60, 0xff}, // rgb(244, 164, 96)
- "seagreen": {0x2e, 0x8b, 0x57, 0xff}, // rgb(46, 139, 87)
- "seashell": {0xff, 0xf5, 0xee, 0xff}, // rgb(255, 245, 238)
- "sienna": {0xa0, 0x52, 0x2d, 0xff}, // rgb(160, 82, 45)
- "silver": {0xc0, 0xc0, 0xc0, 0xff}, // rgb(192, 192, 192)
- "skyblue": {0x87, 0xce, 0xeb, 0xff}, // rgb(135, 206, 235)
- "slateblue": {0x6a, 0x5a, 0xcd, 0xff}, // rgb(106, 90, 205)
- "slategray": {0x70, 0x80, 0x90, 0xff}, // rgb(112, 128, 144)
- "slategrey": {0x70, 0x80, 0x90, 0xff}, // rgb(112, 128, 144)
- "snow": {0xff, 0xfa, 0xfa, 0xff}, // rgb(255, 250, 250)
- "springgreen": {0x00, 0xff, 0x7f, 0xff}, // rgb(0, 255, 127)
- "steelblue": {0x46, 0x82, 0xb4, 0xff}, // rgb(70, 130, 180)
- "tan": {0xd2, 0xb4, 0x8c, 0xff}, // rgb(210, 180, 140)
- "teal": {0x00, 0x80, 0x80, 0xff}, // rgb(0, 128, 128)
- "thistle": {0xd8, 0xbf, 0xd8, 0xff}, // rgb(216, 191, 216)
- "tomato": {0xff, 0x63, 0x47, 0xff}, // rgb(255, 99, 71)
- "turquoise": {0x40, 0xe0, 0xd0, 0xff}, // rgb(64, 224, 208)
- "violet": {0xee, 0x82, 0xee, 0xff}, // rgb(238, 130, 238)
- "wheat": {0xf5, 0xde, 0xb3, 0xff}, // rgb(245, 222, 179)
- "white": {0xff, 0xff, 0xff, 0xff}, // rgb(255, 255, 255)
- "whitesmoke": {0xf5, 0xf5, 0xf5, 0xff}, // rgb(245, 245, 245)
- "yellow": {0xff, 0xff, 0x00, 0xff}, // rgb(255, 255, 0)
- "yellowgreen": {0x9a, 0xcd, 0x32, 0xff}, // rgb(154, 205, 50)
-}
diff --git a/ui/messages/html/container.go b/ui/messages/html/container.go
deleted file mode 100644
index 17e7aa9..0000000
--- a/ui/messages/html/container.go
+++ /dev/null
@@ -1,148 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "fmt"
- "strings"
-
- "go.mau.fi/mauview"
-)
-
-type ContainerEntity struct {
- *BaseEntity
-
- // The children of this container entity.
- Children []Entity
- // Number of cells to indent children.
- Indent int
-}
-
-func (ce *ContainerEntity) IsEmpty() bool {
- return len(ce.Children) == 0
-}
-
-// PlainText returns the plaintext content in this entity and all its children.
-func (ce *ContainerEntity) PlainText() string {
- if len(ce.Children) == 0 {
- return ""
- }
- var buf strings.Builder
- newlined := false
- for _, child := range ce.Children {
- text := child.PlainText()
- if !strings.HasPrefix(text, "\n") && child.IsBlock() && !newlined {
- buf.WriteRune('\n')
- }
- newlined = false
- buf.WriteString(text)
- if child.IsBlock() {
- if !strings.HasSuffix(text, "\n") {
- buf.WriteRune('\n')
- }
- newlined = true
- }
- }
- return strings.TrimSpace(buf.String())
-}
-
-// AdjustStyle recursively changes the style of this entity and all its children.
-func (ce *ContainerEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- for _, child := range ce.Children {
- child.AdjustStyle(fn, reason)
- }
- ce.Style = fn(ce.Style)
- return ce
-}
-
-// Clone creates a deep copy of this base entity.
-func (ce *ContainerEntity) Clone() Entity {
- children := make([]Entity, len(ce.Children))
- for i, child := range ce.Children {
- children[i] = child.Clone()
- }
- return &ContainerEntity{
- BaseEntity: ce.BaseEntity.Clone().(*BaseEntity),
- Children: children,
- Indent: ce.Indent,
- }
-}
-
-// String returns a textual representation of this BaseEntity struct.
-func (ce *ContainerEntity) String() string {
- if len(ce.Children) == 0 {
- return fmt.Sprintf(`&html.ContainerEntity{Base=%s, Indent=%d, Children=[]}`, ce.BaseEntity, ce.Indent)
- }
- var buf strings.Builder
- _, _ = fmt.Fprintf(&buf, `&html.ContainerEntity{Base=%s, Indent=%d, Children=[`, ce.BaseEntity, ce.Indent)
- for _, child := range ce.Children {
- buf.WriteString("\n ")
- buf.WriteString(strings.Join(strings.Split(strings.TrimRight(child.String(), "\n"), "\n"), "\n "))
- }
- buf.WriteString("\n]},")
- return buf.String()
-}
-
-// Draw draws this entity onto the given mauview Screen.
-func (ce *ContainerEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- if len(ce.Children) == 0 {
- return
- }
- width, _ := screen.Size()
- prevBreak := false
- proxyScreen := &mauview.ProxyScreen{Parent: screen, OffsetX: ce.Indent, Width: width - ce.Indent, Style: ce.Style}
- for i, entity := range ce.Children {
- if i != 0 && entity.getStartX() == 0 {
- proxyScreen.OffsetY++
- }
- proxyScreen.Height = entity.Height()
- entity.Draw(proxyScreen, ctx)
- proxyScreen.SetStyle(ce.Style)
- proxyScreen.OffsetY += entity.Height() - 1
- _, isBreak := entity.(*BreakEntity)
- if prevBreak && isBreak {
- proxyScreen.OffsetY++
- }
- prevBreak = isBreak
- }
-}
-
-// CalculateBuffer prepares this entity and all its children for rendering with the given parameters
-func (ce *ContainerEntity) CalculateBuffer(width, startX int, ctx DrawContext) int {
- ce.BaseEntity.CalculateBuffer(width, startX, ctx)
- if len(ce.Children) > 0 {
- ce.height = 0
- childStartX := ce.startX
- prevBreak := false
- for _, entity := range ce.Children {
- if entity.IsBlock() || childStartX == 0 || ce.height == 0 {
- ce.height++
- }
- childStartX = entity.CalculateBuffer(width-ce.Indent, childStartX, ctx)
- ce.height += entity.Height() - 1
- _, isBreak := entity.(*BreakEntity)
- if prevBreak && isBreak {
- ce.height++
- }
- prevBreak = isBreak
- }
- if !ce.Block {
- return childStartX
- }
- }
- return ce.startX
-}
diff --git a/ui/messages/html/entity.go b/ui/messages/html/entity.go
deleted file mode 100644
index 094fa10..0000000
--- a/ui/messages/html/entity.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-// AdjustStyleFunc is a lambda function type to edit an existing tcell Style.
-type AdjustStyleFunc func(tcell.Style) tcell.Style
-
-type AdjustStyleReason int
-
-const (
- AdjustStyleReasonNormal AdjustStyleReason = iota
- AdjustStyleReasonHideSpoiler
-)
-
-type DrawContext struct {
- IsSelected bool
- BareMessages bool
-}
-
-type Entity interface {
- // AdjustStyle recursively changes the style of the entity and all its children.
- AdjustStyle(AdjustStyleFunc, AdjustStyleReason) Entity
- // Draw draws the entity onto the given mauview Screen.
- Draw(screen mauview.Screen, ctx DrawContext)
- // IsBlock returns whether or not it's a block-type entity.
- IsBlock() bool
- // GetTag returns the HTML tag of the entity.
- GetTag() string
- // PlainText returns the plaintext content in the entity and all its children.
- PlainText() string
- // String returns a string representation of the entity struct.
- String() string
- // Clone creates a deep copy of the entity.
- Clone() Entity
-
- // Height returns the render height of the entity.
- Height() int
- // CalculateBuffer prepares the entity and all its children for rendering with the given parameters
- CalculateBuffer(width, startX int, ctx DrawContext) int
-
- getStartX() int
-
- IsEmpty() bool
-}
diff --git a/ui/messages/html/horizontalline.go b/ui/messages/html/horizontalline.go
deleted file mode 100644
index ff12709..0000000
--- a/ui/messages/html/horizontalline.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "strings"
-
- "go.mau.fi/mauview"
-)
-
-type HorizontalLineEntity struct {
- *BaseEntity
-}
-
-const HorizontalLineChar = '━'
-
-func NewHorizontalLineEntity() *HorizontalLineEntity {
- return &HorizontalLineEntity{&BaseEntity{
- Tag: "hr",
- Block: true,
- DefaultHeight: 1,
- }}
-}
-
-func (he *HorizontalLineEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- he.BaseEntity = he.BaseEntity.AdjustStyle(fn, reason).(*BaseEntity)
- return he
-}
-
-func (he *HorizontalLineEntity) Clone() Entity {
- return NewHorizontalLineEntity()
-}
-
-func (he *HorizontalLineEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- width, _ := screen.Size()
- for x := 0; x < width; x++ {
- screen.SetContent(x, 0, HorizontalLineChar, nil, he.Style)
- }
-}
-
-func (he *HorizontalLineEntity) PlainText() string {
- return strings.Repeat(string(HorizontalLineChar), 5)
-}
-
-func (he *HorizontalLineEntity) String() string {
- return "&html.HorizontalLineEntity{},\n"
-}
diff --git a/ui/messages/html/list.go b/ui/messages/html/list.go
deleted file mode 100644
index b01c2d6..0000000
--- a/ui/messages/html/list.go
+++ /dev/null
@@ -1,123 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "fmt"
- "strings"
-
- "go.mau.fi/mauview"
-
- "maunium.net/go/gomuks/ui/widget"
- "maunium.net/go/mautrix/format"
-)
-
-type ListEntity struct {
- *ContainerEntity
- Ordered bool
- Start int
-}
-
-func NewListEntity(ordered bool, start int, children []Entity) *ListEntity {
- entity := &ListEntity{
- ContainerEntity: &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: "ul",
- Block: true,
- },
- Indent: 2,
- Children: children,
- },
- Ordered: ordered,
- Start: start,
- }
- if ordered {
- entity.Tag = "ol"
- entity.Indent += format.Digits(start + len(children) - 1)
- }
- return entity
-}
-
-func (le *ListEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- le.BaseEntity = le.BaseEntity.AdjustStyle(fn, reason).(*BaseEntity)
- le.ContainerEntity.AdjustStyle(fn, reason)
- return le
-}
-
-func (le *ListEntity) Clone() Entity {
- return &ListEntity{
- ContainerEntity: le.ContainerEntity.Clone().(*ContainerEntity),
- Ordered: le.Ordered,
- Start: le.Start,
- }
-}
-
-func (le *ListEntity) paddingFor(number int) string {
- padding := le.Indent - 2 - format.Digits(number)
- if padding <= 0 {
- return ""
- }
- return strings.Repeat(" ", padding)
-}
-
-func (le *ListEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- width, _ := screen.Size()
-
- proxyScreen := &mauview.ProxyScreen{Parent: screen, OffsetX: le.Indent, Width: width - le.Indent, Style: le.Style}
- for i, entity := range le.Children {
- proxyScreen.Height = entity.Height()
- if le.Ordered {
- number := le.Start + i
- line := fmt.Sprintf("%d. %s", number, le.paddingFor(number))
- widget.WriteLine(screen, mauview.AlignLeft, line, 0, proxyScreen.OffsetY, le.Indent, le.Style)
- } else {
- screen.SetContent(0, proxyScreen.OffsetY, '●', nil, le.Style)
- }
- entity.Draw(proxyScreen, ctx)
- proxyScreen.SetStyle(le.Style)
- proxyScreen.OffsetY += entity.Height()
- }
-}
-
-func (le *ListEntity) PlainText() string {
- if len(le.Children) == 0 {
- return ""
- }
- var buf strings.Builder
- for i, child := range le.Children {
- indent := strings.Repeat(" ", le.Indent)
- if le.Ordered {
- number := le.Start + i
- _, _ = fmt.Fprintf(&buf, "%d. %s", number, le.paddingFor(number))
- } else {
- buf.WriteString("● ")
- }
- for j, row := range strings.Split(child.PlainText(), "\n") {
- if j != 0 {
- buf.WriteRune('\n')
- buf.WriteString(indent)
- }
- buf.WriteString(row)
- }
- buf.WriteRune('\n')
- }
- return strings.TrimSpace(buf.String())
-}
-
-func (le *ListEntity) String() string {
- return fmt.Sprintf("&html.ListEntity{Ordered=%t, Start=%d, Base=%s},\n", le.Ordered, le.Start, le.BaseEntity)
-}
diff --git a/ui/messages/html/parser.go b/ui/messages/html/parser.go
deleted file mode 100644
index fd10c92..0000000
--- a/ui/messages/html/parser.go
+++ /dev/null
@@ -1,552 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "fmt"
- "regexp"
- "strconv"
- "strings"
-
- "github.com/alecthomas/chroma"
- "github.com/alecthomas/chroma/lexers"
- "github.com/alecthomas/chroma/styles"
- "github.com/lucasb-eyer/go-colorful"
- "golang.org/x/net/html"
- "mvdan.cc/xurls/v2"
-
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type htmlParser struct {
- prefs *config.UserPreferences
- room *rooms.Room
- evt *muksevt.Event
-
- preserveWhitespace bool
- linkIDCounter int
-}
-
-func AdjustStyleBold(style tcell.Style) tcell.Style {
- return style.Bold(true)
-}
-
-func AdjustStyleItalic(style tcell.Style) tcell.Style {
- return style.Italic(true)
-}
-
-func AdjustStyleUnderline(style tcell.Style) tcell.Style {
- return style.Underline(true)
-}
-
-func AdjustStyleStrikethrough(style tcell.Style) tcell.Style {
- return style.StrikeThrough(true)
-}
-
-func AdjustStyleTextColor(color tcell.Color) AdjustStyleFunc {
- return func(style tcell.Style) tcell.Style {
- return style.Foreground(color)
- }
-}
-
-func AdjustStyleBackgroundColor(color tcell.Color) AdjustStyleFunc {
- return func(style tcell.Style) tcell.Style {
- return style.Background(color)
- }
-}
-
-func AdjustStyleLink(url, id string) AdjustStyleFunc {
- return func(style tcell.Style) tcell.Style {
- return style.Url(url).UrlId(id)
- }
-}
-
-func (parser *htmlParser) maybeGetAttribute(node *html.Node, attribute string) (string, bool) {
- for _, attr := range node.Attr {
- if attr.Key == attribute {
- return attr.Val, true
- }
- }
- return "", false
-}
-
-func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string {
- val, _ := parser.maybeGetAttribute(node, attribute)
- return val
-}
-
-func (parser *htmlParser) hasAttribute(node *html.Node, attribute string) bool {
- _, ok := parser.maybeGetAttribute(node, attribute)
- return ok
-}
-
-func (parser *htmlParser) listToEntity(node *html.Node) Entity {
- children := parser.nodeToEntities(node.FirstChild)
- ordered := node.Data == "ol"
- start := 1
- if ordered {
- if startRaw := parser.getAttribute(node, "start"); len(startRaw) > 0 {
- var err error
- start, err = strconv.Atoi(startRaw)
- if err != nil {
- start = 1
- }
- }
- }
- listItems := children[:0]
- for _, child := range children {
- if child.GetTag() == "li" {
- listItems = append(listItems, child)
- }
- }
- return NewListEntity(ordered, start, listItems)
-}
-
-func (parser *htmlParser) basicFormatToEntity(node *html.Node) Entity {
- entity := &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: node.Data,
- },
- Children: parser.nodeToEntities(node.FirstChild),
- }
- switch node.Data {
- case "b", "strong":
- entity.AdjustStyle(AdjustStyleBold, AdjustStyleReasonNormal)
- case "i", "em":
- entity.AdjustStyle(AdjustStyleItalic, AdjustStyleReasonNormal)
- case "s", "del", "strike":
- entity.AdjustStyle(AdjustStyleStrikethrough, AdjustStyleReasonNormal)
- case "u", "ins":
- entity.AdjustStyle(AdjustStyleUnderline, AdjustStyleReasonNormal)
- case "code":
- bgColor := tcell.ColorDarkSlateGray
- fgColor := tcell.ColorWhite
- entity.AdjustStyle(AdjustStyleBackgroundColor(bgColor), AdjustStyleReasonNormal)
- entity.AdjustStyle(AdjustStyleTextColor(fgColor), AdjustStyleReasonNormal)
- case "font", "span":
- fgColor, ok := parser.parseColor(node, "data-mx-color", "color")
- if ok {
- entity.AdjustStyle(AdjustStyleTextColor(fgColor), AdjustStyleReasonNormal)
- }
- bgColor, ok := parser.parseColor(node, "data-mx-bg-color", "background-color")
- if ok {
- entity.AdjustStyle(AdjustStyleBackgroundColor(bgColor), AdjustStyleReasonNormal)
- }
- spoilerReason, isSpoiler := parser.maybeGetAttribute(node, "data-mx-spoiler")
- if isSpoiler {
- return NewSpoilerEntity(entity, spoilerReason)
- }
- }
- return entity
-}
-
-func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string) (color tcell.Color, ok bool) {
- hex := parser.getAttribute(node, mainName)
- if len(hex) == 0 {
- hex = parser.getAttribute(node, altName)
- if len(hex) == 0 {
- return
- }
- }
-
- cful, err := colorful.Hex(hex)
- if err != nil {
- color2, found := colorMap[strings.ToLower(hex)]
- if !found {
- return
- }
- cful, _ = colorful.MakeColor(color2)
- }
-
- r, g, b := cful.RGB255()
- return tcell.NewRGBColor(int32(r), int32(g), int32(b)), true
-}
-
-func (parser *htmlParser) headerToEntity(node *html.Node) Entity {
- return (&ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: node.Data,
- },
- Children: append(
- []Entity{NewTextEntity(strings.Repeat("#", int(node.Data[1]-'0')) + " ")},
- parser.nodeToEntities(node.FirstChild)...,
- ),
- }).AdjustStyle(AdjustStyleBold, AdjustStyleReasonNormal)
-}
-
-func (parser *htmlParser) blockquoteToEntity(node *html.Node) Entity {
- return NewBlockquoteEntity(parser.nodeToEntities(node.FirstChild))
-}
-
-func (parser *htmlParser) linkToEntity(node *html.Node) Entity {
- sameURL := false
- href := parser.getAttribute(node, "href")
-
- entity := &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: "a",
- },
- Children: parser.nodeToEntities(node.FirstChild),
- }
-
- if len(href) == 0 {
- return entity
- }
-
- if len(entity.Children) == 1 {
- entity, ok := entity.Children[0].(*TextEntity)
- if ok && entity.Text == href {
- sameURL = true
- }
- }
-
- matrixURI, _ := id.ParseMatrixURIOrMatrixToURL(href)
- if matrixURI != nil && (matrixURI.Sigil1 == '@' || matrixURI.Sigil1 == '#') && matrixURI.Sigil2 == 0 {
- text := NewTextEntity(matrixURI.PrimaryIdentifier())
- if matrixURI.Sigil1 == '@' {
- if member := parser.room.GetMember(matrixURI.UserID()); member != nil {
- text.Text = member.Displayname
- text.Style = text.Style.Foreground(widget.GetHashColor(matrixURI.UserID()))
- }
- entity.Children = []Entity{text}
- } else if matrixURI.Sigil1 == '#' {
- entity.Children = []Entity{text}
- }
- } else if parser.prefs.EnableInlineURLs() {
- linkID := fmt.Sprintf("%s-%d", parser.evt.ID, parser.linkIDCounter)
- parser.linkIDCounter++
- entity.AdjustStyle(AdjustStyleLink(href, linkID), AdjustStyleReasonNormal)
- } else if !sameURL && !parser.prefs.DisableShowURLs && !parser.hasAttribute(node, "data-mautrix-exclude-plaintext") {
- entity.Children = append(entity.Children, NewTextEntity(fmt.Sprintf(" (%s)", href)))
- }
- return entity
-}
-
-func (parser *htmlParser) imageToEntity(node *html.Node) Entity {
- alt := parser.getAttribute(node, "alt")
- if len(alt) == 0 {
- alt = parser.getAttribute(node, "title")
- if len(alt) == 0 {
- alt = "[inline image]"
- }
- }
- entity := &TextEntity{
- BaseEntity: &BaseEntity{
- Tag: "img",
- },
- Text: alt,
- }
- // TODO add click action and underline on hover for inline images
- return entity
-}
-
-func colourToColor(colour chroma.Colour) tcell.Color {
- if !colour.IsSet() {
- return tcell.ColorDefault
- }
- return tcell.NewRGBColor(int32(colour.Red()), int32(colour.Green()), int32(colour.Blue()))
-}
-
-func styleEntryToStyle(se chroma.StyleEntry) tcell.Style {
- return tcell.StyleDefault.
- Bold(se.Bold == chroma.Yes).
- Italic(se.Italic == chroma.Yes).
- Underline(se.Underline == chroma.Yes).
- Foreground(colourToColor(se.Colour)).
- Background(colourToColor(se.Background))
-}
-
-func tokenToTextEntity(style *chroma.Style, token *chroma.Token) *TextEntity {
- return &TextEntity{
- BaseEntity: &BaseEntity{
- Tag: token.Type.String(),
- Style: styleEntryToStyle(style.Get(token.Type)),
- DefaultHeight: 1,
- },
- Text: token.Value,
- }
-}
-
-func (parser *htmlParser) syntaxHighlight(text, language string) Entity {
- lexer := lexers.Get(strings.ToLower(language))
- if lexer == nil {
- lexer = lexers.Get("plaintext")
- }
- iter, err := lexer.Tokenise(nil, text)
- if err != nil {
- return nil
- }
- // TODO allow changing theme
- style := styles.SolarizedDark
-
- tokens := iter.Tokens()
-
- var children []Entity
- for _, token := range tokens {
- lines := strings.SplitAfter(token.Value, "\n")
- for _, line := range lines {
- line_len := len(line)
- if line_len == 0 {
- continue
- }
- t := token.Clone()
-
- if line[line_len-1:] == "\n" {
- t.Value = line[:line_len-1]
- children = append(children, tokenToTextEntity(style, &t), NewBreakEntity())
- } else {
- t.Value = line
- children = append(children, tokenToTextEntity(style, &t))
- }
- }
- }
-
- return NewCodeBlockEntity(children, styleEntryToStyle(style.Get(chroma.Background)))
-}
-
-func (parser *htmlParser) codeblockToEntity(node *html.Node) Entity {
- lang := "plaintext"
- // TODO allow disabling syntax highlighting
- if node.FirstChild != nil && node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" {
- node = node.FirstChild
- attr := parser.getAttribute(node, "class")
- for _, class := range strings.Split(attr, " ") {
- if strings.HasPrefix(class, "language-") {
- lang = class[len("language-"):]
- break
- }
- }
- }
- parser.preserveWhitespace = true
- text := (&ContainerEntity{
- Children: parser.nodeToEntities(node.FirstChild),
- }).PlainText()
- parser.preserveWhitespace = false
- return parser.syntaxHighlight(text, lang)
-}
-
-func (parser *htmlParser) tagNodeToEntity(node *html.Node) Entity {
- switch node.Data {
- case "blockquote":
- return parser.blockquoteToEntity(node)
- case "ol", "ul":
- return parser.listToEntity(node)
- case "h1", "h2", "h3", "h4", "h5", "h6":
- return parser.headerToEntity(node)
- case "br":
- return NewBreakEntity()
- case "b", "strong", "i", "em", "s", "strike", "del", "u", "ins", "font", "span", "code":
- return parser.basicFormatToEntity(node)
- case "a":
- return parser.linkToEntity(node)
- case "img":
- return parser.imageToEntity(node)
- case "pre":
- return parser.codeblockToEntity(node)
- case "hr":
- return NewHorizontalLineEntity()
- case "mx-reply":
- return nil
- default:
- return &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: node.Data,
- Block: parser.isBlockTag(node.Data),
- },
- Children: parser.nodeToEntities(node.FirstChild),
- }
- }
-}
-
-var spaces = regexp.MustCompile("\\s+")
-
-// textToHTMLEntity converts a plain text string into an HTML Entity while preserving newlines.
-func textToHTMLEntity(text string) Entity {
- if strings.Index(text, "\n") == -1 {
- return NewTextEntity(text)
- }
- return &ContainerEntity{
- BaseEntity: &BaseEntity{Tag: "span"},
- Children: textToHTMLEntities(text),
- }
-}
-
-func textToHTMLEntities(text string) []Entity {
- lines := strings.SplitAfter(text, "\n")
- entities := make([]Entity, 0, len(lines))
- for _, line := range lines {
- line_len := len(line)
- if line_len == 0 {
- continue
- }
- if line == "\n" {
- entities = append(entities, NewBreakEntity())
- } else if line[line_len-1:] == "\n" {
- entities = append(entities, NewTextEntity(line[:line_len-1]), NewBreakEntity())
- } else {
- entities = append(entities, NewTextEntity(line))
- }
- }
- return entities
-}
-
-func TextToEntity(text string, eventID id.EventID, linkify bool) Entity {
- if len(text) == 0 {
- return nil
- }
- if !linkify {
- return textToHTMLEntity(text)
- }
- indices := xurls.Strict().FindAllStringIndex(text, -1)
- if len(indices) == 0 {
- return textToHTMLEntity(text)
- }
- ent := &ContainerEntity{
- BaseEntity: &BaseEntity{Tag: "span"},
- }
- var lastEnd int
- for i, item := range indices {
- start, end := item[0], item[1]
- if start > lastEnd {
- ent.Children = append(ent.Children, textToHTMLEntities(text[lastEnd:start])...)
- }
- link := text[start:end]
- linkID := fmt.Sprintf("%s-%d", eventID, i)
- ent.Children = append(ent.Children, NewTextEntity(link).AdjustStyle(AdjustStyleLink(link, linkID), AdjustStyleReasonNormal))
- lastEnd = end
- }
- if lastEnd < len(text) {
- ent.Children = append(ent.Children, textToHTMLEntities(text[lastEnd:])...)
- }
- return ent
-}
-
-func (parser *htmlParser) singleNodeToEntity(node *html.Node) Entity {
- switch node.Type {
- case html.TextNode:
- if !parser.preserveWhitespace {
- node.Data = strings.ReplaceAll(node.Data, "\n", "")
- node.Data = spaces.ReplaceAllLiteralString(node.Data, " ")
- }
- return TextToEntity(node.Data, parser.evt.ID, parser.prefs.EnableInlineURLs())
- case html.ElementNode:
- parsed := parser.tagNodeToEntity(node)
- if parsed != nil && !parsed.IsBlock() && parsed.IsEmpty() {
- return nil
- }
- return parsed
- case html.DocumentNode:
- if node.FirstChild.Data == "html" && node.FirstChild.NextSibling == nil {
- return parser.singleNodeToEntity(node.FirstChild)
- }
- return &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: "html",
- Block: true,
- },
- Children: parser.nodeToEntities(node.FirstChild),
- }
- default:
- return nil
- }
-}
-
-func (parser *htmlParser) nodeToEntities(node *html.Node) (entities []Entity) {
- for ; node != nil; node = node.NextSibling {
- if entity := parser.singleNodeToEntity(node); entity != nil {
- entities = append(entities, entity)
- }
- }
- return
-}
-
-var BlockTags = []string{"p", "h1", "h2", "h3", "h4", "h5", "h6", "ol", "ul", "li", "pre", "blockquote", "div", "hr", "table"}
-
-func (parser *htmlParser) isBlockTag(tag string) bool {
- for _, blockTag := range BlockTags {
- if tag == blockTag {
- return true
- }
- }
- return false
-}
-
-func (parser *htmlParser) Parse(htmlData string) Entity {
- node, _ := html.Parse(strings.NewReader(htmlData))
- bodyNode := node.FirstChild.FirstChild
- for bodyNode != nil && (bodyNode.Type != html.ElementNode || bodyNode.Data != "body") {
- bodyNode = bodyNode.NextSibling
- }
- if bodyNode != nil {
- return parser.singleNodeToEntity(bodyNode)
- }
-
- return parser.singleNodeToEntity(node)
-}
-
-const TabLength = 4
-
-// Parse parses a HTML-formatted Matrix event into a UIMessage.
-func Parse(prefs *config.UserPreferences, room *rooms.Room, content *event.MessageEventContent, evt *muksevt.Event, senderDisplayname string) Entity {
- htmlData := content.FormattedBody
-
- if content.Format != event.FormatHTML {
- htmlData = strings.Replace(html.EscapeString(content.Body), "\n", " ", -1)
- }
- htmlData = strings.Replace(htmlData, "\t", strings.Repeat(" ", TabLength), -1)
-
- parser := htmlParser{room: room, prefs: prefs, evt: evt}
- root := parser.Parse(htmlData)
- if root == nil {
- return nil
- }
- beRoot, ok := root.(*ContainerEntity)
- if ok {
- beRoot.Block = false
- if len(beRoot.Children) > 0 {
- beChild, ok := beRoot.Children[0].(*ContainerEntity)
- if ok && beChild.Tag == "p" {
- // Hacky fix for m.emote
- beChild.Block = false
- }
- }
- }
-
- if content.MsgType == event.MsgEmote {
- root = &ContainerEntity{
- BaseEntity: &BaseEntity{
- Tag: "emote",
- },
- Children: []Entity{
- NewTextEntity("* "),
- NewTextEntity(senderDisplayname).AdjustStyle(AdjustStyleTextColor(widget.GetHashColor(evt.Sender)), AdjustStyleReasonNormal),
- NewTextEntity(" "),
- root,
- },
- }
- }
-
- return root
-}
diff --git a/ui/messages/html/spoiler.go b/ui/messages/html/spoiler.go
deleted file mode 100644
index f28e8b8..0000000
--- a/ui/messages/html/spoiler.go
+++ /dev/null
@@ -1,120 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2022 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "fmt"
- "strings"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-type SpoilerEntity struct {
- reason string
- hidden *ContainerEntity
- visible *ContainerEntity
-}
-
-const SpoilerColor = tcell.ColorYellow
-
-func NewSpoilerEntity(visible *ContainerEntity, reason string) *SpoilerEntity {
- hidden := visible.Clone().(*ContainerEntity)
- hidden.AdjustStyle(func(style tcell.Style) tcell.Style {
- return style.Foreground(SpoilerColor).Background(SpoilerColor)
- }, AdjustStyleReasonHideSpoiler)
- if len(reason) > 0 {
- reasonEnt := NewTextEntity(fmt.Sprintf("(%s)", reason))
- hidden.Children = append([]Entity{reasonEnt}, hidden.Children...)
- visible.Children = append([]Entity{reasonEnt}, visible.Children...)
- }
- return &SpoilerEntity{
- reason: reason,
- hidden: hidden,
- visible: visible,
- }
-}
-
-func (se *SpoilerEntity) Clone() Entity {
- return &SpoilerEntity{
- reason: se.reason,
- hidden: se.hidden.Clone().(*ContainerEntity),
- visible: se.visible.Clone().(*ContainerEntity),
- }
-}
-
-func (se *SpoilerEntity) IsBlock() bool {
- return false
-}
-
-func (se *SpoilerEntity) GetTag() string {
- return "span"
-}
-
-func (se *SpoilerEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- if ctx.IsSelected {
- se.visible.Draw(screen, ctx)
- } else {
- se.hidden.Draw(screen, ctx)
- }
-}
-
-func (se *SpoilerEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- if reason != AdjustStyleReasonHideSpoiler {
- se.hidden.AdjustStyle(func(style tcell.Style) tcell.Style {
- return fn(style).Foreground(SpoilerColor).Background(SpoilerColor)
- }, reason)
- se.visible.AdjustStyle(fn, reason)
- }
- return se
-}
-
-func (se *SpoilerEntity) PlainText() string {
- if len(se.reason) > 0 {
- return fmt.Sprintf("spoiler: %s", se.reason)
- } else {
- return "spoiler"
- }
-}
-
-func (se *SpoilerEntity) String() string {
- var buf strings.Builder
- _, _ = fmt.Fprintf(&buf, `&html.SpoilerEntity{reason=%s`, se.reason)
- buf.WriteString("\n visible=")
- buf.WriteString(strings.Join(strings.Split(strings.TrimRight(se.visible.String(), "\n"), "\n"), "\n "))
- buf.WriteString("\n hidden=")
- buf.WriteString(strings.Join(strings.Split(strings.TrimRight(se.hidden.String(), "\n"), "\n"), "\n "))
- buf.WriteString("\n]},")
- return buf.String()
-}
-
-func (se *SpoilerEntity) Height() int {
- return se.visible.Height()
-}
-
-func (se *SpoilerEntity) CalculateBuffer(width, startX int, ctx DrawContext) int {
- se.hidden.CalculateBuffer(width, startX, ctx)
- return se.visible.CalculateBuffer(width, startX, ctx)
-}
-
-func (se *SpoilerEntity) getStartX() int {
- return se.visible.getStartX()
-}
-
-func (se *SpoilerEntity) IsEmpty() bool {
- return se.visible.IsEmpty()
-}
diff --git a/ui/messages/html/text.go b/ui/messages/html/text.go
deleted file mode 100644
index 1b8860c..0000000
--- a/ui/messages/html/text.go
+++ /dev/null
@@ -1,156 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package html
-
-import (
- "fmt"
- "regexp"
-
- "github.com/mattn/go-runewidth"
-
- "go.mau.fi/mauview"
-
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type TextEntity struct {
- *BaseEntity
- // Text in this entity.
- Text string
-
- buffer []string
-}
-
-// NewTextEntity creates a new text-only Entity.
-func NewTextEntity(text string) *TextEntity {
- return &TextEntity{
- BaseEntity: &BaseEntity{
- Tag: "text",
- },
- Text: text,
- }
-}
-
-func (te *TextEntity) IsEmpty() bool {
- return len(te.Text) == 0
-}
-
-func (te *TextEntity) AdjustStyle(fn AdjustStyleFunc, reason AdjustStyleReason) Entity {
- te.BaseEntity = te.BaseEntity.AdjustStyle(fn, reason).(*BaseEntity)
- return te
-}
-
-func (te *TextEntity) Clone() Entity {
- return &TextEntity{
- BaseEntity: te.BaseEntity.Clone().(*BaseEntity),
- Text: te.Text,
- }
-}
-
-func (te *TextEntity) PlainText() string {
- return te.Text
-}
-
-func (te *TextEntity) String() string {
- return fmt.Sprintf("&html.TextEntity{Text=%s, Base=%s},\n", te.Text, te.BaseEntity)
-}
-
-func (te *TextEntity) Draw(screen mauview.Screen, ctx DrawContext) {
- width, _ := screen.Size()
- x := te.startX
- for y, line := range te.buffer {
- widget.WriteLine(screen, mauview.AlignLeft, line, x, y, width, te.Style)
- x = 0
- }
-}
-
-func (te *TextEntity) CalculateBuffer(width, startX int, ctx DrawContext) int {
- te.BaseEntity.CalculateBuffer(width, startX, ctx)
- if len(te.Text) == 0 {
- return te.startX
- }
- te.height = 0
- te.prevWidth = width
- if te.buffer == nil {
- te.buffer = []string{}
- }
- bufPtr := 0
- text := te.Text
- textStartX := te.startX
- for {
- // TODO add option no wrap and character wrap options
- extract := runewidth.Truncate(text, width-textStartX, "")
- extract, wordWrapped := trim(extract, text, ctx.BareMessages)
- if !wordWrapped && textStartX > 0 {
- if bufPtr < len(te.buffer) {
- te.buffer[bufPtr] = ""
- } else {
- te.buffer = append(te.buffer, "")
- }
- bufPtr++
- textStartX = 0
- continue
- }
- if bufPtr < len(te.buffer) {
- te.buffer[bufPtr] = extract
- } else {
- te.buffer = append(te.buffer, extract)
- }
- bufPtr++
- text = text[len(extract):]
- if len(text) == 0 {
- te.buffer = te.buffer[:bufPtr]
- te.height += len(te.buffer)
- // This entity is over, return the startX for the next entity
- if te.Block {
- // ...except if it's a block entity
- return 0
- }
- return textStartX + runewidth.StringWidth(extract)
- }
- textStartX = 0
- }
-}
-
-var (
- boundaryPattern = regexp.MustCompile(`([[:punct:]]\s*|\s+)`)
- bareBoundaryPattern = regexp.MustCompile(`(\s+)`)
- spacePattern = regexp.MustCompile(`\s+`)
-)
-
-func trim(extract, full string, bare bool) (string, bool) {
- if len(extract) == len(full) {
- return extract, true
- }
- if spaces := spacePattern.FindStringIndex(full[len(extract):]); spaces != nil && spaces[0] == 0 {
- extract = full[:len(extract)+spaces[1]]
- }
- regex := boundaryPattern
- if bare {
- regex = bareBoundaryPattern
- }
- matches := regex.FindAllStringIndex(extract, -1)
- if len(matches) > 0 {
- if match := matches[len(matches)-1]; len(match) >= 2 {
- if until := match[1]; until < len(extract) {
- extract = extract[:until]
- return extract, true
- }
- }
- }
- return extract, len(extract) > 0 && extract[len(extract)-1] == ' '
-}
diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go
deleted file mode 100644
index d3593a6..0000000
--- a/ui/messages/htmlmessage.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/matrix/muksevt"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/ui/messages/html"
-)
-
-type HTMLMessage struct {
- Root html.Entity
- TextColor tcell.Color
-}
-
-func NewHTMLMessage(evt *muksevt.Event, displayname string, root html.Entity) *UIMessage {
- return newUIMessage(evt, displayname, &HTMLMessage{
- Root: root,
- })
-}
-
-func (hw *HTMLMessage) Clone() MessageRenderer {
- return &HTMLMessage{
- Root: hw.Root.Clone(),
- }
-}
-
-func (hw *HTMLMessage) Draw(screen mauview.Screen, msg *UIMessage) {
- if hw.TextColor != tcell.ColorDefault {
- hw.Root.AdjustStyle(func(style tcell.Style) tcell.Style {
- fg, _, _ := style.Decompose()
- if fg == tcell.ColorDefault {
- return style.Foreground(hw.TextColor)
- }
- return style
- }, html.AdjustStyleReasonNormal)
- }
- screen.Clear()
- hw.Root.Draw(screen, html.DrawContext{IsSelected: msg.IsSelected})
-}
-
-func (hw *HTMLMessage) OnKeyEvent(event mauview.KeyEvent) bool {
- return false
-}
-
-func (hw *HTMLMessage) OnMouseEvent(event mauview.MouseEvent) bool {
- return false
-}
-
-func (hw *HTMLMessage) OnPasteEvent(event mauview.PasteEvent) bool {
- return false
-}
-
-func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int, msg *UIMessage) {
- if width < 2 {
- return
- }
- // TODO account for bare messages in initial startX
- startX := 0
- hw.TextColor = msg.TextColor()
- hw.Root.CalculateBuffer(width, startX, html.DrawContext{
- IsSelected: msg.IsSelected,
- BareMessages: preferences.BareMessageView,
- })
-}
-
-func (hw *HTMLMessage) Height() int {
- return hw.Root.Height()
-}
-
-func (hw *HTMLMessage) PlainText() string {
- return hw.Root.PlainText()
-}
-
-func (hw *HTMLMessage) NotificationContent() string {
- return hw.Root.PlainText()
-}
-
-func (hw *HTMLMessage) String() string {
- return hw.Root.String()
-}
diff --git a/ui/messages/parser.go b/ui/messages/parser.go
deleted file mode 100644
index dc9733e..0000000
--- a/ui/messages/parser.go
+++ /dev/null
@@ -1,324 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "fmt"
- "strings"
-
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/messages/html"
- "maunium.net/go/gomuks/ui/messages/tstring"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-func getCachedEvent(mainView ifc.MainView, roomID id.RoomID, eventID id.EventID) *UIMessage {
- if roomView := mainView.GetRoom(roomID); roomView != nil {
- if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil {
- if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil {
- return replyToMsg
- }
- }
- }
- return nil
-}
-
-func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *muksevt.Event) *UIMessage {
- msg := directParseEvent(matrix, room, evt)
- if msg == nil {
- return nil
- }
- if content, ok := evt.Content.Parsed.(*event.MessageEventContent); ok && len(content.GetReplyTo()) > 0 {
- if replyToMsg := getCachedEvent(mainView, room.ID, content.GetReplyTo()); replyToMsg != nil {
- msg.ReplyTo = replyToMsg.Clone()
- } else if replyToEvt, _ := matrix.GetEvent(room, content.GetReplyTo()); replyToEvt != nil {
- if replyToMsg = directParseEvent(matrix, room, replyToEvt); replyToMsg != nil {
- msg.ReplyTo = replyToMsg
- msg.ReplyTo.Reactions = nil
- } else {
- // TODO add unrenderable reply header
- }
- } else {
- // TODO add unknown reply header
- }
- }
- return msg
-}
-
-func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *muksevt.Event) *UIMessage {
- displayname := string(evt.Sender)
- member := room.GetMember(evt.Sender)
- if member != nil {
- displayname = member.Displayname
- }
- if evt.Unsigned.RedactedBecause != nil || evt.Type == event.EventRedaction {
- return NewRedactedMessage(evt, displayname)
- }
- switch content := evt.Content.Parsed.(type) {
- case *event.MessageEventContent:
- if evt.Type == event.EventSticker {
- content.MsgType = event.MsgImage
- }
- return ParseMessage(matrix, room, evt, displayname)
- case *muksevt.BadEncryptedContent:
- return NewExpandedTextMessage(evt, displayname, tstring.NewStyleTString(content.Reason, tcell.StyleDefault.Italic(true)))
- case *muksevt.EncryptionUnsupportedContent:
- return NewExpandedTextMessage(evt, displayname, tstring.NewStyleTString("gomuks not built with encryption support", tcell.StyleDefault.Italic(true)))
- case *event.TopicEventContent, *event.RoomNameEventContent, *event.CanonicalAliasEventContent:
- return ParseStateEvent(evt, displayname)
- case *event.MemberEventContent:
- return ParseMembershipEvent(room, evt)
- default:
- debug.Printf("Unknown event content type %T in directParseEvent", content)
- return nil
- }
-}
-
-func findAltAliasDifference(newList, oldList []id.RoomAlias) (addedStr, removedStr tstring.TString) {
- var addedList, removedList []tstring.TString
-OldLoop:
- for _, oldAlias := range oldList {
- for _, newAlias := range newList {
- if oldAlias == newAlias {
- continue OldLoop
- }
- }
- removedList = append(removedList, tstring.NewStyleTString(string(oldAlias), tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true)))
- }
-NewLoop:
- for _, newAlias := range newList {
- for _, oldAlias := range oldList {
- if newAlias == oldAlias {
- continue NewLoop
- }
- }
- addedList = append(addedList, tstring.NewStyleTString(string(newAlias), tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true)))
- }
- if len(addedList) == 1 {
- addedStr = tstring.NewColorTString("added alternative address ", tcell.ColorGreen).AppendTString(addedList[0])
- } else if len(addedList) != 0 {
- addedStr = tstring.
- Join(addedList[:len(addedList)-1], ", ").
- PrependColor("added alternative addresses ", tcell.ColorGreen).
- AppendColor(" and ", tcell.ColorGreen).
- AppendTString(addedList[len(addedList)-1])
- }
- if len(removedList) == 1 {
- removedStr = tstring.NewColorTString("removed alternative address ", tcell.ColorGreen).AppendTString(removedList[0])
- } else if len(removedList) != 0 {
- removedStr = tstring.
- Join(removedList[:len(removedList)-1], ", ").
- PrependColor("removed alternative addresses ", tcell.ColorGreen).
- AppendColor(" and ", tcell.ColorGreen).
- AppendTString(removedList[len(removedList)-1])
- }
- return
-}
-
-func ParseStateEvent(evt *muksevt.Event, displayname string) *UIMessage {
- text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender)).Append(" ")
- switch content := evt.Content.Parsed.(type) {
- case *event.TopicEventContent:
- if len(content.Topic) == 0 {
- text = text.AppendColor("removed the topic.", tcell.ColorGreen)
- } else {
- text = text.AppendColor("changed the topic to ", tcell.ColorGreen).
- AppendStyle(content.Topic, tcell.StyleDefault.Underline(true)).
- AppendColor(".", tcell.ColorGreen)
- }
- case *event.RoomNameEventContent:
- if len(content.Name) == 0 {
- text = text.AppendColor("removed the room name.", tcell.ColorGreen)
- } else {
- text = text.AppendColor("changed the room name to ", tcell.ColorGreen).
- AppendStyle(content.Name, tcell.StyleDefault.Underline(true)).
- AppendColor(".", tcell.ColorGreen)
- }
- case *event.CanonicalAliasEventContent:
- prevContent := &event.CanonicalAliasEventContent{}
- if evt.Unsigned.PrevContent != nil {
- _ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
- prevContent = evt.Unsigned.PrevContent.AsCanonicalAlias()
- }
- debug.Printf("%+v -> %+v", prevContent, content)
- if len(content.Alias) == 0 && len(prevContent.Alias) != 0 {
- text = text.AppendColor("removed the main address of the room", tcell.ColorGreen)
- } else if content.Alias != prevContent.Alias {
- text = text.
- AppendColor("changed the main address of the room to ", tcell.ColorGreen).
- AppendStyle(string(content.Alias), tcell.StyleDefault.Underline(true))
- } else {
- added, removed := findAltAliasDifference(content.AltAliases, prevContent.AltAliases)
- if len(added) > 0 {
- if len(removed) > 0 {
- text = text.
- AppendTString(added).
- AppendColor(" and ", tcell.ColorGreen).
- AppendTString(removed)
- } else {
- text = text.AppendTString(added)
- }
- } else if len(removed) > 0 {
- text = text.AppendTString(removed)
- } else {
- text = text.AppendColor("changed nothing", tcell.ColorGreen)
- }
- text = text.AppendColor(" for this room", tcell.ColorGreen)
- }
- }
- return NewExpandedTextMessage(evt, displayname, text)
-}
-
-func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *muksevt.Event, displayname string) *UIMessage {
- content := evt.Content.AsMessage()
- if len(content.GetReplyTo()) > 0 {
- content.RemoveReplyFallback()
- }
- if len(evt.Gomuks.Edits) > 0 {
- newContent := evt.Gomuks.Edits[len(evt.Gomuks.Edits)-1].Content.AsMessage().NewContent
- if newContent != nil {
- content = newContent
- }
- }
- switch content.MsgType {
- case event.MsgText, event.MsgNotice, event.MsgEmote:
- var htmlEntity html.Entity
- if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 {
- htmlEntity = html.Parse(matrix.Preferences(), room, content, evt, displayname)
- if htmlEntity == nil {
- htmlEntity = html.NewTextEntity("Malformed message")
- htmlEntity.AdjustStyle(html.AdjustStyleTextColor(tcell.ColorRed), html.AdjustStyleReasonNormal)
- }
- } else if len(content.Body) > 0 {
- content.Body = strings.Replace(content.Body, "\t", " ", -1)
- htmlEntity = html.TextToEntity(content.Body, evt.ID, matrix.Preferences().EnableInlineURLs())
- } else {
- htmlEntity = html.NewTextEntity("Blank message")
- htmlEntity.AdjustStyle(html.AdjustStyleTextColor(tcell.ColorRed), html.AdjustStyleReasonNormal)
- }
- return NewHTMLMessage(evt, displayname, htmlEntity)
- case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
- msg := NewFileMessage(matrix, evt, displayname)
- if !matrix.Preferences().DisableDownloads {
- renderer := msg.Renderer.(*FileMessage)
- renderer.DownloadPreview()
- }
- return msg
- }
- return nil
-}
-
-func getMembershipChangeMessage(evt *muksevt.Event, content *event.MemberEventContent, prevMembership event.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) {
- switch content.Membership {
- case "invite":
- sender = "---"
- text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", senderDisplayname, displayname), tcell.ColorGreen)
- text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender))
- text.Colorize(len(senderDisplayname)+len(" invited "), len(displayname), widget.GetHashColor(evt.StateKey))
- case "join":
- sender = "-->"
- if prevMembership == event.MembershipInvite {
- text = tstring.NewColorTString(fmt.Sprintf("%s accepted the invite.", displayname), tcell.ColorGreen)
- } else {
- text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen)
- }
- text.Colorize(0, len(displayname), widget.GetHashColor(evt.StateKey))
- case "leave":
- sender = "<--"
- if evt.Sender != id.UserID(*evt.StateKey) {
- if prevMembership == event.MembershipBan {
- text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen)
- text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(evt.StateKey))
- } else {
- text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", senderDisplayname, displayname, content.Reason), tcell.ColorRed)
- text.Colorize(len(senderDisplayname)+len(" kicked "), len(displayname), widget.GetHashColor(evt.StateKey))
- }
- text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender))
- } else {
- if displayname == *evt.StateKey {
- displayname = prevDisplayname
- }
- if prevMembership == event.MembershipInvite {
- text = tstring.NewColorTString(fmt.Sprintf("%s rejected the invite.", displayname), tcell.ColorRed)
- } else {
- text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed)
- }
- text.Colorize(0, len(displayname), widget.GetHashColor(evt.StateKey))
- }
- case "ban":
- text = tstring.NewColorTString(fmt.Sprintf("%s banned %s: %s", senderDisplayname, displayname, content.Reason), tcell.ColorRed)
- text.Colorize(len(senderDisplayname)+len(" banned "), len(displayname), widget.GetHashColor(evt.StateKey))
- text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender))
- }
- return
-}
-
-func getMembershipEventContent(room *rooms.Room, evt *muksevt.Event) (sender string, text tstring.TString) {
- member := room.GetMember(evt.Sender)
- senderDisplayname := string(evt.Sender)
- if member != nil {
- senderDisplayname = member.Displayname
- }
-
- content := evt.Content.AsMember()
- displayname := content.Displayname
- if len(displayname) == 0 {
- displayname = *evt.StateKey
- }
-
- prevMembership := event.MembershipLeave
- prevDisplayname := *evt.StateKey
- if evt.Unsigned.PrevContent != nil {
- _ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
- prevContent := evt.Unsigned.PrevContent.AsMember()
- prevMembership = prevContent.Membership
- prevDisplayname = prevContent.Displayname
- if len(prevDisplayname) == 0 {
- prevDisplayname = *evt.StateKey
- }
- }
-
- if content.Membership != prevMembership {
- sender, text = getMembershipChangeMessage(evt, content, prevMembership, senderDisplayname, displayname, prevDisplayname)
- } else if displayname != prevDisplayname {
- sender = "---"
- color := widget.GetHashColor(evt.StateKey)
- text = tstring.NewBlankTString().
- AppendColor(prevDisplayname, color).
- AppendColor(" changed their display name to ", tcell.ColorGreen).
- AppendColor(displayname, color).
- AppendColor(".", tcell.ColorGreen)
- }
- return
-}
-
-func ParseMembershipEvent(room *rooms.Room, evt *muksevt.Event) *UIMessage {
- displayname, text := getMembershipEventContent(room, evt)
- if len(text) == 0 {
- return nil
- }
-
- return NewExpandedTextMessage(evt, displayname, text)
-}
diff --git a/ui/messages/redactedmessage.go b/ui/messages/redactedmessage.go
deleted file mode 100644
index e3e1718..0000000
--- a/ui/messages/redactedmessage.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/matrix/muksevt"
-
- "maunium.net/go/gomuks/config"
-)
-
-type RedactedMessage struct{}
-
-func NewRedactedMessage(evt *muksevt.Event, displayname string) *UIMessage {
- return newUIMessage(evt, displayname, &RedactedMessage{})
-}
-
-func (msg *RedactedMessage) Clone() MessageRenderer {
- return &RedactedMessage{}
-}
-
-func (msg *RedactedMessage) NotificationContent() string {
- return ""
-}
-
-func (msg *RedactedMessage) PlainText() string {
- return "[redacted]"
-}
-
-func (msg *RedactedMessage) String() string {
- return "&messages.RedactedMessage{}"
-}
-
-func (msg *RedactedMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
-}
-
-func (msg *RedactedMessage) Height() int {
- return 1
-}
-
-const RedactionChar = '█'
-const RedactionMaxWidth = 40
-
-var RedactionStyle = tcell.StyleDefault.Foreground(tcell.NewRGBColor(50, 0, 0))
-
-func (msg *RedactedMessage) Draw(screen mauview.Screen, _ *UIMessage) {
- w, _ := screen.Size()
- for x := 0; x < w && x < RedactionMaxWidth; x++ {
- screen.SetContent(x, 0, RedactionChar, nil, RedactionStyle)
- }
-}
diff --git a/ui/messages/textbase.go b/ui/messages/textbase.go
deleted file mode 100644
index 1e8b376..0000000
--- a/ui/messages/textbase.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "fmt"
- "regexp"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/ui/messages/tstring"
-)
-
-// Regular expressions used to split lines when calculating the buffer.
-//
-// From tview/textview.go
-var (
- boundaryPattern = regexp.MustCompile(`([[:punct:]]\s*|\s+)`)
- bareBoundaryPattern = regexp.MustCompile(`(\s+)`)
- spacePattern = regexp.MustCompile(`\s+`)
-)
-
-func matchBoundaryPattern(bare bool, extract tstring.TString) tstring.TString {
- regex := boundaryPattern
- if bare {
- regex = bareBoundaryPattern
- }
- matches := regex.FindAllStringIndex(extract.String(), -1)
- if len(matches) > 0 {
- if match := matches[len(matches)-1]; len(match) >= 2 {
- if until := match[1]; until < len(extract) {
- extract = extract[:until]
- }
- }
- }
- return extract
-}
-
-// CalculateBuffer generates the internal buffer for this message that consists
-// of the text of this message split into lines at most as wide as the width
-// parameter.
-func calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int, msg *UIMessage) []tstring.TString {
- if width < 2 {
- return nil
- }
-
- var buffer []tstring.TString
-
- if prefs.BareMessageView {
- newText := tstring.NewTString(msg.FormatTime())
- if len(msg.Sender()) > 0 {
- newText = newText.AppendTString(tstring.NewColorTString(fmt.Sprintf(" <%s> ", msg.Sender()), msg.SenderColor()))
- } else {
- newText = newText.Append(" ")
- }
- newText = newText.AppendTString(text)
- text = newText
- }
-
- forcedLinebreaks := text.Split('\n')
- newlines := 0
- for _, str := range forcedLinebreaks {
- if len(str) == 0 && newlines < 1 {
- buffer = append(buffer, tstring.TString{})
- newlines++
- } else {
- newlines = 0
- }
- // Adapted from tview/textview.go#reindexBuffer()
- for len(str) > 0 {
- extract := str.Truncate(width)
- if len(extract) < len(str) {
- if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 {
- extract = str[:len(extract)+spaces[1]]
- }
- extract = matchBoundaryPattern(prefs.BareMessageView, extract)
- }
- buffer = append(buffer, extract)
- str = str[len(extract):]
- }
- }
- return buffer
-}
diff --git a/ui/messages/tstring/cell.go b/ui/messages/tstring/cell.go
deleted file mode 100644
index 4ed3735..0000000
--- a/ui/messages/tstring/cell.go
+++ /dev/null
@@ -1,53 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package tstring
-
-import (
- "github.com/mattn/go-runewidth"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-type Cell struct {
- Char rune
- Style tcell.Style
-}
-
-func NewStyleCell(char rune, style tcell.Style) Cell {
- return Cell{char, style}
-}
-
-func NewColorCell(char rune, color tcell.Color) Cell {
- return Cell{char, tcell.StyleDefault.Foreground(color)}
-}
-
-func NewCell(char rune) Cell {
- return Cell{char, tcell.StyleDefault}
-}
-
-func (cell Cell) RuneWidth() int {
- return runewidth.RuneWidth(cell.Char)
-}
-
-func (cell Cell) Draw(screen mauview.Screen, x, y int) (chWidth int) {
- chWidth = cell.RuneWidth()
- for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
- screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
- }
- return
-}
diff --git a/ui/messages/tstring/doc.go b/ui/messages/tstring/doc.go
deleted file mode 100644
index d03a1da..0000000
--- a/ui/messages/tstring/doc.go
+++ /dev/null
@@ -1,4 +0,0 @@
-// Package tstring contains a string type that stores style data for each
-// character, allowing it to be rendered to a tcell screen essentially
-// unmodified.
-package tstring
diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go
deleted file mode 100644
index df51d5d..0000000
--- a/ui/messages/tstring/string.go
+++ /dev/null
@@ -1,270 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package tstring
-
-import (
- "strings"
- "unicode"
-
- "github.com/mattn/go-runewidth"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-type TString []Cell
-
-func NewBlankTString() TString {
- return make(TString, 0)
-}
-
-func NewTString(str string) TString {
- newStr := make(TString, len(str))
- for i, char := range str {
- newStr[i] = NewCell(char)
- }
- return newStr
-}
-
-func NewColorTString(str string, color tcell.Color) TString {
- newStr := make(TString, len(str))
- for i, char := range str {
- newStr[i] = NewColorCell(char, color)
- }
- return newStr
-}
-
-func NewStyleTString(str string, style tcell.Style) TString {
- newStr := make(TString, len(str))
- for i, char := range str {
- newStr[i] = NewStyleCell(char, style)
- }
- return newStr
-}
-
-func Join(strings []TString, separator string) TString {
- if len(strings) == 0 {
- return NewBlankTString()
- }
-
- out := strings[0]
- strings = strings[1:]
-
- if len(separator) == 0 {
- return out.AppendTString(strings...)
- }
-
- for _, str := range strings {
- out = append(out, str.Prepend(separator)...)
- }
- return out
-}
-
-func (str TString) Clone() TString {
- newStr := make(TString, len(str))
- copy(newStr, str)
- return newStr
-}
-
-func (str TString) AppendTString(dataList ...TString) TString {
- newStr := str
- for _, data := range dataList {
- newStr = append(newStr, data...)
- }
- return newStr
-}
-
-func (str TString) PrependTString(data TString) TString {
- return append(data, str...)
-}
-
-func (str TString) Append(data string) TString {
- return str.AppendCustom(data, func(r rune) Cell {
- return NewCell(r)
- })
-}
-
-func (str TString) TrimSpace() TString {
- return str.Trim(unicode.IsSpace)
-}
-
-func (str TString) Trim(fn func(rune) bool) TString {
- return str.TrimLeft(fn).TrimRight(fn)
-}
-
-func (str TString) TrimLeft(fn func(rune) bool) TString {
- for index, cell := range str {
- if !fn(cell.Char) {
- return append(NewBlankTString(), str[index:]...)
- }
- }
- return NewBlankTString()
-}
-
-func (str TString) TrimRight(fn func(rune) bool) TString {
- for i := len(str) - 1; i >= 0; i-- {
- if !fn(str[i].Char) {
- return append(NewBlankTString(), str[:i+1]...)
- }
- }
- return NewBlankTString()
-}
-
-func (str TString) AppendColor(data string, color tcell.Color) TString {
- return str.AppendCustom(data, func(r rune) Cell {
- return NewColorCell(r, color)
- })
-}
-
-func (str TString) AppendStyle(data string, style tcell.Style) TString {
- return str.AppendCustom(data, func(r rune) Cell {
- return NewStyleCell(r, style)
- })
-}
-
-func (str TString) AppendCustom(data string, cellCreator func(rune) Cell) TString {
- newStr := make(TString, len(str)+len(data))
- copy(newStr, str)
- for i, char := range data {
- newStr[i+len(str)] = cellCreator(char)
- }
- return newStr
-}
-
-func (str TString) Prepend(data string) TString {
- return str.PrependCustom(data, func(r rune) Cell {
- return NewCell(r)
- })
-}
-
-func (str TString) PrependColor(data string, color tcell.Color) TString {
- return str.PrependCustom(data, func(r rune) Cell {
- return NewColorCell(r, color)
- })
-}
-
-func (str TString) PrependStyle(data string, style tcell.Style) TString {
- return str.PrependCustom(data, func(r rune) Cell {
- return NewStyleCell(r, style)
- })
-}
-
-func (str TString) PrependCustom(data string, cellCreator func(rune) Cell) TString {
- newStr := make(TString, len(str)+len(data))
- copy(newStr[len(data):], str)
- for i, char := range data {
- newStr[i] = cellCreator(char)
- }
- return newStr
-}
-
-func (str TString) Colorize(from, length int, color tcell.Color) {
- str.AdjustStyle(from, length, func(style tcell.Style) tcell.Style {
- return style.Foreground(color)
- })
-}
-
-func (str TString) AdjustStyle(from, length int, fn func(tcell.Style) tcell.Style) {
- for i := from; i < from+length; i++ {
- str[i].Style = fn(str[i].Style)
- }
-}
-
-func (str TString) AdjustStyleFull(fn func(tcell.Style) tcell.Style) {
- str.AdjustStyle(0, len(str), fn)
-}
-
-func (str TString) Draw(screen mauview.Screen, x, y int) {
- for _, cell := range str {
- x += cell.Draw(screen, x, y)
- }
-}
-
-func (str TString) RuneWidth() (width int) {
- for _, cell := range str {
- width += runewidth.RuneWidth(cell.Char)
- }
- return width
-}
-
-func (str TString) String() string {
- var buf strings.Builder
- for _, cell := range str {
- buf.WriteRune(cell.Char)
- }
- return buf.String()
-}
-
-// Truncate return string truncated with w cells
-func (str TString) Truncate(w int) TString {
- if str.RuneWidth() <= w {
- return str[:]
- }
- width := 0
- i := 0
- for ; i < len(str); i++ {
- cw := runewidth.RuneWidth(str[i].Char)
- if width+cw > w {
- break
- }
- width += cw
- }
- return str[0:i]
-}
-
-func (str TString) IndexFrom(r rune, from int) int {
- for i := from; i < len(str); i++ {
- if str[i].Char == r {
- return i
- }
- }
- return -1
-}
-
-func (str TString) Index(r rune) int {
- return str.IndexFrom(r, 0)
-}
-
-func (str TString) Count(r rune) (counter int) {
- index := 0
- for {
- index = str.IndexFrom(r, index)
- if index < 0 {
- break
- }
- index++
- counter++
- }
- return
-}
-
-func (str TString) Split(sep rune) []TString {
- a := make([]TString, str.Count(sep)+1)
- i := 0
- orig := str
- for {
- m := orig.Index(sep)
- if m < 0 {
- break
- }
- a[i] = orig[:m]
- orig = orig[m+1:]
- i++
- }
- a[i] = orig
- return a[:i+1]
-}
diff --git a/ui/no-crypto-commands.go b/ui/no-crypto-commands.go
deleted file mode 100644
index b98d277..0000000
--- a/ui/no-crypto-commands.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-//go:build !cgo
-
-package ui
-
-func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) {
- return []string{}, ""
-}
-
-func autocompleteUser(cmd *CommandAutocomplete) ([]string, string) {
- return []string{}, ""
-}
-
-func cmdNoCrypto(cmd *Command) {
- cmd.Reply("This gomuks was built without encryption support")
-}
-
-var (
- cmdDevices = cmdNoCrypto
- cmdDevice = cmdNoCrypto
- cmdVerifyDevice = cmdNoCrypto
- cmdVerify = cmdNoCrypto
- cmdUnverify = cmdNoCrypto
- cmdBlacklist = cmdNoCrypto
- cmdResetSession = cmdNoCrypto
- cmdImportKeys = cmdNoCrypto
- cmdExportKeys = cmdNoCrypto
- cmdExportRoomKeys = cmdNoCrypto
- cmdSSSS = cmdNoCrypto
- cmdCrossSigning = cmdNoCrypto
-)
diff --git a/ui/password-modal.go b/ui/password-modal.go
deleted file mode 100644
index ceb2f7c..0000000
--- a/ui/password-modal.go
+++ /dev/null
@@ -1,143 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "strings"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-type PasswordModal struct {
- mauview.Component
-
- outputChan chan string
- cancelChan chan struct{}
-
- form *mauview.Form
-
- text *mauview.TextField
- confirmText *mauview.TextField
-
- input *mauview.InputField
- confirmInput *mauview.InputField
-
- cancel *mauview.Button
- submit *mauview.Button
-
- parent *MainView
-}
-
-func (view *MainView) AskPassword(title, thing, placeholder string, isNew bool) (string, bool) {
- pwm := NewPasswordModal(view, title, thing, placeholder, isNew)
- view.ShowModal(pwm)
- view.parent.Render()
- return pwm.Wait()
-}
-
-func NewPasswordModal(parent *MainView, title, thing, placeholder string, isNew bool) *PasswordModal {
- if placeholder == "" {
- placeholder = "correct horse battery staple"
- }
- if thing == "" {
- thing = strings.ToLower(title)
- }
- pwm := &PasswordModal{
- parent: parent,
- form: mauview.NewForm(),
- outputChan: make(chan string, 1),
- cancelChan: make(chan struct{}, 1),
- }
-
- pwm.form.
- SetColumns([]int{1, 20, 1, 20, 1}).
- SetRows([]int{1, 1, 1, 0, 0, 0, 1, 1, 1})
-
- width := 45
- height := 8
-
- pwm.text = mauview.NewTextField()
- if isNew {
- pwm.text.SetText(fmt.Sprintf("Create a %s", thing))
- } else {
- pwm.text.SetText(fmt.Sprintf("Enter the %s", thing))
- }
- pwm.input = mauview.NewInputField().
- SetMaskCharacter('*').
- SetPlaceholder(placeholder)
- pwm.form.AddComponent(pwm.text, 1, 1, 3, 1)
- pwm.form.AddFormItem(pwm.input, 1, 2, 3, 1)
-
- if isNew {
- height += 3
- pwm.confirmInput = mauview.NewInputField().
- SetMaskCharacter('*').
- SetPlaceholder(placeholder).
- SetChangedFunc(pwm.HandleChange)
- pwm.input.SetChangedFunc(pwm.HandleChange)
- pwm.confirmText = mauview.NewTextField().SetText(fmt.Sprintf("Confirm %s", thing))
-
- pwm.form.SetRow(3, 1).SetRow(4, 1).SetRow(5, 1)
- pwm.form.AddComponent(pwm.confirmText, 1, 4, 3, 1)
- pwm.form.AddFormItem(pwm.confirmInput, 1, 5, 3, 1)
- }
-
- pwm.cancel = mauview.NewButton("Cancel").SetOnClick(pwm.ClickCancel)
- pwm.submit = mauview.NewButton("Submit").SetOnClick(pwm.ClickSubmit)
-
- pwm.form.AddFormItem(pwm.submit, 3, 7, 1, 1)
- pwm.form.AddFormItem(pwm.cancel, 1, 7, 1, 1)
-
- box := mauview.NewBox(pwm.form).SetTitle(title)
- center := mauview.Center(box, width, height).SetAlwaysFocusChild(true)
- center.Focus()
- pwm.form.FocusNextItem()
- pwm.Component = center
-
- return pwm
-}
-
-func (pwm *PasswordModal) HandleChange(_ string) {
- if pwm.input.GetText() == pwm.confirmInput.GetText() {
- pwm.submit.SetBackgroundColor(mauview.Styles.ContrastBackgroundColor)
- } else {
- pwm.submit.SetBackgroundColor(tcell.ColorDefault)
- }
-}
-
-func (pwm *PasswordModal) ClickCancel() {
- pwm.parent.HideModal()
- pwm.cancelChan <- struct{}{}
-}
-
-func (pwm *PasswordModal) ClickSubmit() {
- if pwm.confirmInput == nil || pwm.input.GetText() == pwm.confirmInput.GetText() {
- pwm.parent.HideModal()
- pwm.outputChan <- pwm.input.GetText()
- }
-}
-
-func (pwm *PasswordModal) Wait() (string, bool) {
- select {
- case result := <-pwm.outputChan:
- return result, true
- case <-pwm.cancelChan:
- return "", false
- }
-}
diff --git a/ui/rainbow.go b/ui/rainbow.go
deleted file mode 100644
index 83f2800..0000000
--- a/ui/rainbow.go
+++ /dev/null
@@ -1,135 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2022 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "math/rand"
- "unicode"
-
- "github.com/rivo/uniseg"
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/ast"
- "github.com/yuin/goldmark/renderer"
- "github.com/yuin/goldmark/renderer/html"
- "github.com/yuin/goldmark/util"
-)
-
-func Rand(n int) (str string) {
- b := make([]byte, n)
- rand.Read(b)
- str = fmt.Sprintf("%x", b)
- return
-}
-
-type extRainbow struct{}
-type rainbowRenderer struct {
- HardWraps bool
- ColorID string
-}
-
-var ExtensionRainbow = &extRainbow{}
-var defaultRB = &rainbowRenderer{HardWraps: true, ColorID: Rand(16)}
-
-func (er *extRainbow) Extend(m goldmark.Markdown) {
- m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(defaultRB, 0)))
-}
-
-func (rb *rainbowRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
- reg.Register(ast.KindText, rb.renderText)
- reg.Register(ast.KindString, rb.renderString)
-}
-
-type rainbowBufWriter struct {
- util.BufWriter
- ColorID string
-}
-
-func (rbw rainbowBufWriter) WriteString(s string) (int, error) {
- i := 0
- graphemes := uniseg.NewGraphemes(s)
- for graphemes.Next() {
- runes := graphemes.Runes()
- if len(runes) == 1 && unicode.IsSpace(runes[0]) {
- i2, err := rbw.BufWriter.WriteRune(runes[0])
- i += i2
- if err != nil {
- return i, err
- }
- continue
- }
- i2, err := fmt.Fprintf(rbw.BufWriter, "%s", rbw.ColorID, graphemes.Str())
- i += i2
- if err != nil {
- return i, err
- }
- }
- return i, nil
-}
-
-func (rbw rainbowBufWriter) Write(data []byte) (int, error) {
- return rbw.WriteString(string(data))
-}
-
-func (rbw rainbowBufWriter) WriteByte(c byte) error {
- _, err := rbw.WriteRune(rune(c))
- return err
-}
-
-func (rbw rainbowBufWriter) WriteRune(r rune) (int, error) {
- if unicode.IsSpace(r) {
- return rbw.BufWriter.WriteRune(r)
- } else {
- return fmt.Fprintf(rbw.BufWriter, "%c", rbw.ColorID, r)
- }
-}
-
-func (rb *rainbowRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- if !entering {
- return ast.WalkContinue, nil
- }
- n := node.(*ast.Text)
- segment := n.Segment
- if n.IsRaw() {
- html.DefaultWriter.RawWrite(rainbowBufWriter{w, rb.ColorID}, segment.Value(source))
- } else {
- html.DefaultWriter.Write(rainbowBufWriter{w, rb.ColorID}, segment.Value(source))
- if n.HardLineBreak() || (n.SoftLineBreak() && rb.HardWraps) {
- _, _ = w.WriteString(" \n")
- } else if n.SoftLineBreak() {
- _ = w.WriteByte('\n')
- }
- }
- return ast.WalkContinue, nil
-}
-
-func (rb *rainbowRenderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- if !entering {
- return ast.WalkContinue, nil
- }
- n := node.(*ast.String)
- if n.IsCode() {
- _, _ = w.Write(n.Value)
- } else {
- if n.IsRaw() {
- html.DefaultWriter.RawWrite(rainbowBufWriter{w, rb.ColorID}, n.Value)
- } else {
- html.DefaultWriter.Write(rainbowBufWriter{w, rb.ColorID}, n.Value)
- }
- }
- return ast.WalkContinue, nil
-}
diff --git a/ui/room-list.go b/ui/room-list.go
deleted file mode 100644
index 6425225..0000000
--- a/ui/room-list.go
+++ /dev/null
@@ -1,592 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "math"
- "regexp"
- "sort"
- "strings"
-
- sync "github.com/sasha-s/go-deadlock"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/debug"
- "maunium.net/go/gomuks/matrix/rooms"
-)
-
-var tagOrder = map[string]int{
- "net.maunium.gomuks.fake.invite": 4,
- "m.favourite": 3,
- "net.maunium.gomuks.fake.direct": 2,
- "": 1,
- "m.lowpriority": -1,
- "m.server_notice": -2,
- "net.maunium.gomuks.fake.leave": -3,
-}
-
-// TagNameList is a list of Matrix tag names where default names are sorted in a hardcoded way.
-type TagNameList []string
-
-func (tnl TagNameList) Len() int {
- return len(tnl)
-}
-
-func (tnl TagNameList) Less(i, j int) bool {
- orderI, _ := tagOrder[tnl[i]]
- orderJ, _ := tagOrder[tnl[j]]
- if orderI != orderJ {
- return orderI > orderJ
- }
- return strings.Compare(tnl[i], tnl[j]) > 0
-}
-
-func (tnl TagNameList) Swap(i, j int) {
- tnl[i], tnl[j] = tnl[j], tnl[i]
-}
-
-type RoomList struct {
- sync.RWMutex
-
- parent *MainView
-
- // The list of tags in display order.
- tags TagNameList
- // The list of rooms, in reverse order.
- items map[string]*TagRoomList
- // The selected room.
- selected *rooms.Room
- selectedTag string
-
- scrollOffset int
- height int
- width int
-
- // The item main text color.
- mainTextColor tcell.Color
- // The text color for selected items.
- selectedTextColor tcell.Color
- // The background color for selected items.
- selectedBackgroundColor tcell.Color
-}
-
-func NewRoomList(parent *MainView) *RoomList {
- list := &RoomList{
- parent: parent,
-
- items: make(map[string]*TagRoomList),
- tags: []string{},
-
- scrollOffset: 0,
-
- mainTextColor: tcell.ColorDefault,
- selectedTextColor: tcell.ColorWhite,
- selectedBackgroundColor: tcell.ColorDarkGreen,
- }
- for _, tag := range list.tags {
- list.items[tag] = NewTagRoomList(list, tag)
- }
- return list
-}
-
-func (list *RoomList) Contains(roomID id.RoomID) bool {
- list.RLock()
- defer list.RUnlock()
- for _, trl := range list.items {
- for _, room := range trl.All() {
- if room.ID == roomID {
- return true
- }
- }
- }
- return false
-}
-
-func (list *RoomList) Add(room *rooms.Room) {
- if room.IsReplaced() {
- debug.Print(room.ID, "is replaced by", room.ReplacedBy(), "-> not adding to room list")
- return
- }
- debug.Print("Adding room to list", room.ID, room.GetTitle(), room.IsDirect, room.ReplacedBy(), room.Tags())
- for _, tag := range room.Tags() {
- list.AddToTag(tag, room)
- }
-}
-
-func (list *RoomList) checkTag(tag string) {
- index := list.indexTag(tag)
-
- trl, ok := list.items[tag]
-
- if ok && trl.IsEmpty() {
- delete(list.items, tag)
- ok = false
- }
-
- if ok && index == -1 {
- list.tags = append(list.tags, tag)
- sort.Sort(list.tags)
- } else if !ok && index != -1 {
- list.tags = append(list.tags[0:index], list.tags[index+1:]...)
- }
-}
-
-func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) {
- list.Lock()
- defer list.Unlock()
- trl, ok := list.items[tag.Tag]
- if !ok {
- list.items[tag.Tag] = NewTagRoomList(list, tag.Tag, NewOrderedRoom(tag.Order, room))
- } else {
- trl.Insert(tag.Order, room)
- }
- list.checkTag(tag.Tag)
-}
-
-func (list *RoomList) Remove(room *rooms.Room) {
- for _, tag := range list.tags {
- list.RemoveFromTag(tag, room)
- }
-}
-
-func (list *RoomList) RemoveFromTag(tag string, room *rooms.Room) {
- list.Lock()
- defer list.Unlock()
- trl, ok := list.items[tag]
- if !ok {
- return
- }
-
- index := trl.Index(room)
- if index == -1 {
- return
- }
-
- trl.RemoveIndex(index)
-
- if trl.IsEmpty() {
- // delete(list.items, tag)
- }
-
- if room == list.selected {
- if index > 0 {
- list.selected = trl.All()[index-1].Room
- } else if trl.Length() > 0 {
- list.selected = trl.Visible()[0].Room
- } else if len(list.items) > 0 {
- for _, tag := range list.tags {
- moreItems := list.items[tag]
- if moreItems.Length() > 0 {
- list.selected = moreItems.Visible()[0].Room
- list.selectedTag = tag
- }
- }
- } else {
- list.selected = nil
- list.selectedTag = ""
- }
- }
- list.checkTag(tag)
-}
-
-func (list *RoomList) Bump(room *rooms.Room) {
- list.RLock()
- defer list.RUnlock()
- for _, tag := range room.Tags() {
- trl, ok := list.items[tag.Tag]
- if !ok {
- return
- }
- trl.Bump(room)
- }
-}
-
-func (list *RoomList) Clear() {
- list.Lock()
- defer list.Unlock()
- list.items = make(map[string]*TagRoomList)
- list.tags = []string{}
- for _, tag := range list.tags {
- list.items[tag] = NewTagRoomList(list, tag)
- }
- list.selected = nil
- list.selectedTag = ""
-}
-
-func (list *RoomList) SetSelected(tag string, room *rooms.Room) {
- list.selected = room
- list.selectedTag = tag
- pos := list.index(tag, room)
- if pos <= list.scrollOffset {
- list.scrollOffset = pos - 1
- } else if pos >= list.scrollOffset+list.height {
- list.scrollOffset = pos - list.height + 1
- }
- if list.scrollOffset < 0 {
- list.scrollOffset = 0
- }
- debug.Print("Selecting", room.GetTitle(), "in", list.GetTagDisplayName(tag))
-}
-
-func (list *RoomList) HasSelected() bool {
- return list.selected != nil
-}
-
-func (list *RoomList) Selected() (string, *rooms.Room) {
- return list.selectedTag, list.selected
-}
-
-func (list *RoomList) SelectedRoom() *rooms.Room {
- return list.selected
-}
-
-func (list *RoomList) AddScrollOffset(offset int) {
- list.scrollOffset += offset
- contentHeight := list.ContentHeight()
- if list.scrollOffset > contentHeight-list.height {
- list.scrollOffset = contentHeight - list.height
- }
- if list.scrollOffset < 0 {
- list.scrollOffset = 0
- }
-}
-
-func (list *RoomList) First() (string, *rooms.Room) {
- list.RLock()
- defer list.RUnlock()
- return list.first()
-}
-
-func (list *RoomList) first() (string, *rooms.Room) {
- for _, tag := range list.tags {
- trl := list.items[tag]
- if trl.HasVisibleRooms() {
- return tag, trl.FirstVisible()
- }
- }
- return "", nil
-}
-
-func (list *RoomList) Last() (string, *rooms.Room) {
- list.RLock()
- defer list.RUnlock()
- return list.last()
-}
-
-func (list *RoomList) last() (string, *rooms.Room) {
- for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- {
- tag := list.tags[tagIndex]
- trl := list.items[tag]
- if trl.HasVisibleRooms() {
- return tag, trl.LastVisible()
- }
- }
- return "", nil
-}
-
-func (list *RoomList) indexTag(tag string) int {
- for index, entry := range list.tags {
- if tag == entry {
- return index
- }
- }
- return -1
-}
-
-func (list *RoomList) Previous() (string, *rooms.Room) {
- list.RLock()
- defer list.RUnlock()
- if len(list.items) == 0 {
- return "", nil
- } else if list.selected == nil {
- return list.first()
- }
-
- trl := list.items[list.selectedTag]
- index := trl.IndexVisible(list.selected)
- indexInvisible := trl.Index(list.selected)
- if index == -1 && indexInvisible >= 0 {
- num := trl.TotalLength() - indexInvisible
- trl.maxShown = int(math.Ceil(float64(num)/10.0) * 10.0)
- index = trl.IndexVisible(list.selected)
- }
-
- if index == trl.Length()-1 {
- tagIndex := list.indexTag(list.selectedTag)
- tagIndex--
- for ; tagIndex >= 0; tagIndex-- {
- prevTag := list.tags[tagIndex]
- prevTRL := list.items[prevTag]
- if prevTRL.HasVisibleRooms() {
- return prevTag, prevTRL.LastVisible()
- }
- }
- return list.last()
- } else if index >= 0 {
- return list.selectedTag, trl.Visible()[index+1].Room
- }
- return list.first()
-}
-
-func (list *RoomList) Next() (string, *rooms.Room) {
- list.RLock()
- defer list.RUnlock()
- if len(list.items) == 0 {
- return "", nil
- } else if list.selected == nil {
- return list.first()
- }
-
- trl := list.items[list.selectedTag]
- index := trl.IndexVisible(list.selected)
- indexInvisible := trl.Index(list.selected)
- if index == -1 && indexInvisible >= 0 {
- num := trl.TotalLength() - indexInvisible + 1
- trl.maxShown = int(math.Ceil(float64(num)/10.0) * 10.0)
- index = trl.IndexVisible(list.selected)
- }
-
- if index == 0 {
- tagIndex := list.indexTag(list.selectedTag)
- tagIndex++
- for ; tagIndex < len(list.tags); tagIndex++ {
- nextTag := list.tags[tagIndex]
- nextTRL := list.items[nextTag]
- if nextTRL.HasVisibleRooms() {
- return nextTag, nextTRL.FirstVisible()
- }
- }
- return list.first()
- } else if index > 0 {
- return list.selectedTag, trl.Visible()[index-1].Room
- }
- return list.last()
-}
-
-// NextWithActivity Returns next room with activity.
-//
-// Sorted by (in priority):
-//
-// - Highlights
-// - Messages
-// - Other traffic (joins, parts, etc)
-//
-// TODO: Sorting. Now just finds first room with new messages.
-func (list *RoomList) NextWithActivity() (string, *rooms.Room) {
- list.RLock()
- defer list.RUnlock()
- for tag, trl := range list.items {
- for _, room := range trl.All() {
- if room.HasNewMessages() {
- return tag, room.Room
- }
- }
- }
- // No room with activity found
- return "", nil
-}
-
-func (list *RoomList) index(tag string, room *rooms.Room) int {
- tagIndex := list.indexTag(tag)
- if tagIndex == -1 {
- return -1
- }
-
- trl, ok := list.items[tag]
- localIndex := -1
- if ok {
- localIndex = trl.IndexVisible(room)
- }
- if localIndex == -1 {
- return -1
- }
- localIndex = trl.Length() - 1 - localIndex
-
- // Tag header
- localIndex++
-
- if tagIndex > 0 {
- for i := 0; i < tagIndex; i++ {
- prevTag := list.tags[i]
-
- prevTRL := list.items[prevTag]
- localIndex += prevTRL.RenderHeight()
- }
- }
-
- return localIndex
-}
-
-func (list *RoomList) ContentHeight() (height int) {
- list.RLock()
- for _, tag := range list.tags {
- height += list.items[tag].RenderHeight()
- }
- list.RUnlock()
- return
-}
-
-func (list *RoomList) OnKeyEvent(_ mauview.KeyEvent) bool {
- return false
-}
-
-func (list *RoomList) OnPasteEvent(_ mauview.PasteEvent) bool {
- return false
-}
-
-func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool {
- if event.HasMotion() {
- return false
- }
- switch event.Buttons() {
- case tcell.WheelUp:
- list.AddScrollOffset(-WheelScrollOffsetDiff)
- return true
- case tcell.WheelDown:
- list.AddScrollOffset(WheelScrollOffsetDiff)
- return true
- case tcell.Button1:
- x, y := event.Position()
- return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl)
- }
- return false
-}
-
-func (list *RoomList) Focus() {
-
-}
-
-func (list *RoomList) Blur() {
-
-}
-
-func (list *RoomList) clickRoom(line, column int, mod bool) bool {
- line += list.scrollOffset
- if line < 0 {
- return false
- }
- list.RLock()
- for _, tag := range list.tags {
- trl := list.items[tag]
- if line--; line == -1 {
- trl.ToggleCollapse()
- list.RUnlock()
- return true
- }
-
- if trl.IsCollapsed() {
- continue
- }
-
- if line < 0 {
- break
- } else if line < trl.Length() {
- switchToRoom := trl.Visible()[trl.Length()-1-line].Room
- list.RUnlock()
- list.parent.SwitchRoom(tag, switchToRoom)
- return true
- }
-
- // Tag items
- line -= trl.Length()
-
- hasMore := trl.HasInvisibleRooms()
- hasLess := trl.maxShown > 10
- if hasMore || hasLess {
- if line--; line == -1 {
- diff := 10
- if mod {
- diff = 100
- }
- if column <= 6 && hasLess {
- trl.maxShown -= diff
- } else if column >= list.width-6 && hasMore {
- trl.maxShown += diff
- }
- if trl.maxShown < 10 {
- trl.maxShown = 10
- }
- list.RUnlock()
- return true
- }
- }
- // Tag footer
- line--
- }
- list.RUnlock()
- return false
-}
-
-var nsRegex = regexp.MustCompile("^[a-z]+\\.[a-z]+(?:\\.[a-z]+)*$")
-
-func (list *RoomList) GetTagDisplayName(tag string) string {
- switch {
- case len(tag) == 0:
- return "Rooms"
- case tag == "m.favourite":
- return "Favorites"
- case tag == "m.lowpriority":
- return "Low Priority"
- case tag == "m.server_notice":
- return "System Alerts"
- case tag == "net.maunium.gomuks.fake.direct":
- return "People"
- case tag == "net.maunium.gomuks.fake.invite":
- return "Invites"
- case tag == "net.maunium.gomuks.fake.leave":
- return "Historical"
- case strings.HasPrefix(tag, "u."):
- return tag[len("u."):]
- case !nsRegex.MatchString(tag):
- return tag
- default:
- return ""
- }
-}
-
-// Draw draws this primitive onto the screen.
-func (list *RoomList) Draw(screen mauview.Screen) {
- list.width, list.height = screen.Size()
- y := 0
- yLimit := y + list.height
- y -= list.scrollOffset
-
- // Draw the list items.
- list.RLock()
- for _, tag := range list.tags {
- trl := list.items[tag]
- tagDisplayName := list.GetTagDisplayName(tag)
- if trl == nil || len(tagDisplayName) == 0 {
- continue
- }
-
- renderHeight := trl.RenderHeight()
- if y+renderHeight >= yLimit {
- renderHeight = yLimit - y
- }
- trl.Draw(mauview.NewProxyScreen(screen, 0, y, list.width, renderHeight))
- y += renderHeight
- if y >= yLimit {
- break
- }
- }
- list.RUnlock()
-}
diff --git a/ui/room-view.go b/ui/room-view.go
deleted file mode 100644
index 73fe967..0000000
--- a/ui/room-view.go
+++ /dev/null
@@ -1,937 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "fmt"
- "sort"
- "strings"
- "time"
- "unicode"
-
- "github.com/kyokomi/emoji/v2"
- "github.com/mattn/go-runewidth"
- "github.com/zyedidia/clipboard"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/crypto/attachment"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/format"
- "maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/util/variationselector"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/lib/open"
- "maunium.net/go/gomuks/lib/util"
- "maunium.net/go/gomuks/matrix/muksevt"
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/messages"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type RoomView struct {
- topic *mauview.TextView
- content *MessageView
- status *mauview.TextField
- userList *MemberList
- ulBorder *widget.Border
- input *mauview.InputArea
- Room *rooms.Room
-
- topicScreen *mauview.ProxyScreen
- contentScreen *mauview.ProxyScreen
- statusScreen *mauview.ProxyScreen
- inputScreen *mauview.ProxyScreen
- ulBorderScreen *mauview.ProxyScreen
- ulScreen *mauview.ProxyScreen
-
- userListLoaded bool
-
- prevScreen mauview.Screen
-
- parent *MainView
- config *config.Config
-
- typing []string
-
- selecting bool
- selectReason SelectReason
- selectContent string
-
- replying *muksevt.Event
-
- editing *muksevt.Event
- editMoveText string
-
- completions struct {
- list []string
- textCache string
- time time.Time
- }
-}
-
-func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
- view := &RoomView{
- topic: mauview.NewTextView(),
- status: mauview.NewTextField(),
- userList: NewMemberList(),
- ulBorder: widget.NewBorder(),
- input: mauview.NewInputArea(),
- Room: room,
-
- topicScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: 0, Height: TopicBarHeight},
- contentScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: StatusBarHeight},
- statusScreen: &mauview.ProxyScreen{OffsetX: 0, Height: StatusBarHeight},
- inputScreen: &mauview.ProxyScreen{OffsetX: 0},
- ulBorderScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListBorderWidth},
- ulScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListWidth},
-
- parent: parent,
- config: parent.config,
- }
- view.content = NewMessageView(view)
- view.Room.SetPreUnload(func() bool {
- if view.parent.currentRoom == view {
- return false
- }
- view.content.Unload()
- return true
- })
- view.Room.SetPostLoad(view.loadTyping)
-
- view.input.
- SetTextColor(tcell.ColorDefault).
- SetBackgroundColor(tcell.ColorDefault).
- SetPlaceholder("Send a message...").
- SetPlaceholderTextColor(tcell.ColorGray).
- SetTabCompleteFunc(view.InputTabComplete).
- SetPressKeyUpAtStartFunc(view.EditPrevious).
- SetPressKeyDownAtEndFunc(view.EditNext)
-
- if room.Encrypted {
- view.input.SetPlaceholder("Send an encrypted message...")
- }
-
- view.topic.
- SetTextColor(tcell.ColorWhite).
- SetBackgroundColor(tcell.ColorDarkGreen)
-
- view.status.SetBackgroundColor(tcell.ColorDimGray)
-
- return view
-}
-
-func (view *RoomView) SetInputChangedFunc(fn func(room *RoomView, text string)) *RoomView {
- view.input.SetChangedFunc(func(text string) {
- fn(view, text)
- })
- return view
-}
-
-func (view *RoomView) SetInputText(newText string) *RoomView {
- view.input.SetTextAndMoveCursor(newText)
- return view
-}
-
-func (view *RoomView) GetInputText() string {
- return view.input.GetText()
-}
-
-func (view *RoomView) Focus() {
- view.input.Focus()
-}
-
-func (view *RoomView) Blur() {
- view.StopSelecting()
- view.input.Blur()
-}
-
-func (view *RoomView) StartSelecting(reason SelectReason, content string) {
- view.selecting = true
- view.selectReason = reason
- view.selectContent = content
- msgView := view.MessageView()
- if msgView.selected != nil {
- view.OnSelect(msgView.selected)
- } else {
- view.input.Blur()
- view.SelectPrevious()
- }
-}
-
-func (view *RoomView) StopSelecting() {
- view.selecting = false
- view.selectContent = ""
- view.MessageView().SetSelected(nil)
-}
-
-func (view *RoomView) OnSelect(message *messages.UIMessage) {
- if !view.selecting || message == nil {
- return
- }
- switch view.selectReason {
- case SelectReply:
- view.replying = message.Event
- if len(view.selectContent) > 0 {
- go view.SendMessage(event.MsgText, view.selectContent)
- }
- case SelectEdit:
- view.SetEditing(message.Event)
- case SelectReact:
- go view.SendReaction(message.EventID, view.selectContent)
- case SelectRedact:
- go view.Redact(message.EventID, view.selectContent)
- case SelectDownload, SelectOpen:
- msg, ok := message.Renderer.(*messages.FileMessage)
- if ok {
- path := ""
- if len(view.selectContent) > 0 {
- path = view.selectContent
- } else if view.selectReason == SelectDownload {
- path = msg.Body
- }
- go view.Download(msg.URL, msg.File, path, view.selectReason == SelectOpen)
- }
- case SelectCopy:
- go view.CopyToClipboard(message.Renderer.PlainText(), view.selectContent)
- }
- view.selecting = false
- view.selectContent = ""
- view.MessageView().SetSelected(nil)
- view.input.Focus()
-}
-
-func (view *RoomView) GetStatus() string {
- var buf strings.Builder
-
- if view.editing != nil {
- buf.WriteString("Editing message - ")
- } else if view.replying != nil {
- buf.WriteString("Replying to ")
- buf.WriteString(string(view.replying.Sender))
- buf.WriteString(" - ")
- } else if view.selecting {
- buf.WriteString("Selecting message to ")
- buf.WriteString(string(view.selectReason))
- buf.WriteString(" - ")
- }
-
- if len(view.completions.list) > 0 {
- if view.completions.textCache != view.input.GetText() || view.completions.time.Add(10*time.Second).Before(time.Now()) {
- view.completions.list = []string{}
- } else {
- buf.WriteString(strings.Join(view.completions.list, ", "))
- buf.WriteString(" - ")
- }
- }
-
- if len(view.typing) == 1 {
- buf.WriteString("Typing: " + string(view.typing[0]))
- buf.WriteString(" - ")
- } else if len(view.typing) > 1 {
- buf.WriteString("Typing: ")
- for i, userID := range view.typing {
- if i == len(view.typing)-1 {
- buf.WriteString(" and ")
- } else if i > 0 {
- buf.WriteString(", ")
- }
- buf.WriteString(string(userID))
- }
- buf.WriteString(" - ")
- }
-
- return strings.TrimSuffix(buf.String(), " - ")
-}
-
-// Constants defining the size of the room view grid.
-const (
- UserListBorderWidth = 1
- UserListWidth = 20
- StaticHorizontalSpace = UserListBorderWidth + UserListWidth
-
- TopicBarHeight = 1
- StatusBarHeight = 1
-
- MaxInputHeight = 5
-)
-
-func (view *RoomView) Draw(screen mauview.Screen) {
- width, height := screen.Size()
- if width <= 0 || height <= 0 {
- return
- }
-
- if view.prevScreen != screen {
- view.topicScreen.Parent = screen
- view.contentScreen.Parent = screen
- view.statusScreen.Parent = screen
- view.inputScreen.Parent = screen
- view.ulBorderScreen.Parent = screen
- view.ulScreen.Parent = screen
- view.prevScreen = screen
- }
-
- view.input.PrepareDraw(width)
- inputHeight := view.input.GetTextHeight()
- if inputHeight > MaxInputHeight {
- inputHeight = MaxInputHeight
- } else if inputHeight < 1 {
- inputHeight = 1
- }
- contentHeight := height - inputHeight - TopicBarHeight - StatusBarHeight
- contentWidth := width - StaticHorizontalSpace
- if view.config.Preferences.HideUserList {
- contentWidth = width
- }
-
- view.topicScreen.Width = width
- view.contentScreen.Width = contentWidth
- view.contentScreen.Height = contentHeight
- view.statusScreen.OffsetY = view.contentScreen.YEnd()
- view.statusScreen.Width = width
- view.inputScreen.Width = width
- view.inputScreen.OffsetY = view.statusScreen.YEnd()
- view.inputScreen.Height = inputHeight
- view.ulBorderScreen.OffsetX = view.contentScreen.XEnd()
- view.ulBorderScreen.Height = contentHeight
- view.ulScreen.OffsetX = view.ulBorderScreen.XEnd()
- view.ulScreen.Height = contentHeight
-
- // Draw everything
- view.topic.Draw(view.topicScreen)
- view.content.Draw(view.contentScreen)
- view.status.SetText(view.GetStatus())
- view.status.Draw(view.statusScreen)
- view.input.Draw(view.inputScreen)
- if !view.config.Preferences.HideUserList {
- view.ulBorder.Draw(view.ulBorderScreen)
- view.userList.Draw(view.ulScreen)
- }
-}
-
-func (view *RoomView) ClearAllContext() {
- view.SetEditing(nil)
- view.StopSelecting()
- view.replying = nil
- view.input.Focus()
-}
-
-func (view *RoomView) OnKeyEvent(event mauview.KeyEvent) bool {
- msgView := view.MessageView()
- kb := config.Keybind{
- Key: event.Key(),
- Ch: event.Rune(),
- Mod: event.Modifiers(),
- }
-
- if view.selecting {
- switch view.config.Keybindings.Visual[kb] {
- case "clear":
- view.ClearAllContext()
- case "select_prev":
- view.SelectPrevious()
- case "select_next":
- view.SelectNext()
- case "confirm":
- view.OnSelect(msgView.selected)
- default:
- return false
- }
- return true
- }
-
- switch view.config.Keybindings.Room[kb] {
- case "clear":
- view.ClearAllContext()
- return true
- case "scroll_up":
- if msgView.IsAtTop() {
- go view.parent.LoadHistory(view.Room.ID)
- }
- msgView.AddScrollOffset(+msgView.Height() / 2)
- return true
- case "scroll_down":
- msgView.AddScrollOffset(-msgView.Height() / 2)
- return true
- case "send":
- view.InputSubmit(view.input.GetText())
- return true
- }
- return view.input.OnKeyEvent(event)
-}
-
-func (view *RoomView) OnPasteEvent(event mauview.PasteEvent) bool {
- return view.input.OnPasteEvent(event)
-}
-
-func (view *RoomView) OnMouseEvent(event mauview.MouseEvent) bool {
- switch {
- case view.contentScreen.IsInArea(event.Position()):
- return view.content.OnMouseEvent(view.contentScreen.OffsetMouseEvent(event))
- case view.topicScreen.IsInArea(event.Position()):
- return view.topic.OnMouseEvent(view.topicScreen.OffsetMouseEvent(event))
- case view.inputScreen.IsInArea(event.Position()):
- return view.input.OnMouseEvent(view.inputScreen.OffsetMouseEvent(event))
- }
- return false
-}
-
-func (view *RoomView) SetCompletions(completions []string) {
- view.completions.list = completions
- view.completions.textCache = view.input.GetText()
- view.completions.time = time.Now()
-}
-
-func (view *RoomView) loadTyping() {
- for index, user := range view.typing {
- member := view.Room.GetMember(id.UserID(user))
- if member != nil {
- view.typing[index] = member.Displayname
- }
- }
-}
-
-func (view *RoomView) SetTyping(users []id.UserID) {
- view.typing = make([]string, len(users))
- for i, user := range users {
- view.typing[i] = string(user)
- }
- if view.Room.Loaded() {
- view.loadTyping()
- }
-}
-
-var editHTMLParser = &format.HTMLParser{
- PillConverter: func(displayname, mxid, eventID string, ctx format.Context) string {
- if len(eventID) > 0 {
- return fmt.Sprintf(`[%s](https://matrix.to/#/%s/%s)`, displayname, mxid, eventID)
- } else {
- return fmt.Sprintf(`[%s](https://matrix.to/#/%s)`, displayname, mxid)
- }
- },
- Newline: "\n",
- HorizontalLine: "\n---\n",
-}
-
-func (view *RoomView) SetEditing(evt *muksevt.Event) {
- if evt == nil {
- view.editing = nil
- view.SetInputText(view.editMoveText)
- view.editMoveText = ""
- } else {
- if view.editing == nil {
- view.editMoveText = view.GetInputText()
- }
- view.editing = evt
- // replying should never be non-nil when SetEditing, but do this just to be safe
- view.replying = nil
- msgContent := view.editing.Content.AsMessage()
- if len(view.editing.Gomuks.Edits) > 0 {
- // This feels kind of dangerous, but I think it works
- msgContent = view.editing.Gomuks.Edits[len(view.editing.Gomuks.Edits)-1].Content.AsMessage().NewContent
- }
- text := msgContent.Body
- if len(msgContent.FormattedBody) > 0 && (!view.config.Preferences.DisableMarkdown || !view.config.Preferences.DisableHTML) {
- if view.config.Preferences.DisableMarkdown {
- text = msgContent.FormattedBody
- } else {
- text = editHTMLParser.Parse(msgContent.FormattedBody, make(format.Context))
- }
- }
- if msgContent.MsgType == event.MsgEmote {
- text = "/me " + text
- }
- view.input.SetText(text)
- }
- view.status.SetText(view.GetStatus())
- view.input.SetCursorOffset(-1)
-}
-
-type findFilter func(evt *muksevt.Event) bool
-
-func (view *RoomView) filterOwnOnly(evt *muksevt.Event) bool {
- return evt.Sender == view.parent.matrix.Client().UserID && evt.Type == event.EventMessage
-}
-
-func (view *RoomView) filterMediaOnly(evt *muksevt.Event) bool {
- content, ok := evt.Content.Parsed.(*event.MessageEventContent)
- return ok && (content.MsgType == event.MsgFile ||
- content.MsgType == event.MsgImage ||
- content.MsgType == event.MsgAudio ||
- content.MsgType == event.MsgVideo)
-}
-
-func (view *RoomView) findMessage(current *muksevt.Event, forward bool, allow findFilter) *messages.UIMessage {
- currentFound := current == nil
- msgs := view.MessageView().messages
- for i := 0; i < len(msgs); i++ {
- index := i
- if !forward {
- index = len(msgs) - i - 1
- }
- evt := msgs[index]
- if evt.EventID == "" || string(evt.EventID) == evt.TxnID || evt.IsService {
- continue
- } else if currentFound {
- if allow == nil || allow(evt.Event) {
- return evt
- }
- } else if evt.EventID == current.ID {
- currentFound = true
- }
- }
- return nil
-}
-
-func (view *RoomView) EditNext() {
- if view.editing == nil {
- return
- }
- foundMsg := view.findMessage(view.editing, true, view.filterOwnOnly)
- view.SetEditing(foundMsg.GetEvent())
-}
-
-func (view *RoomView) EditPrevious() {
- if view.replying != nil {
- return
- }
- foundMsg := view.findMessage(view.editing, false, view.filterOwnOnly)
- if foundMsg != nil {
- view.SetEditing(foundMsg.GetEvent())
- }
-}
-
-func (view *RoomView) SelectNext() {
- msgView := view.MessageView()
- if msgView.selected == nil {
- return
- }
- var filter findFilter
- if view.selectReason == SelectDownload || view.selectReason == SelectOpen {
- filter = view.filterMediaOnly
- }
- foundMsg := view.findMessage(msgView.selected.GetEvent(), true, filter)
- if foundMsg != nil {
- msgView.SetSelected(foundMsg)
- // TODO scroll selected message into view
- }
-}
-
-func (view *RoomView) SelectPrevious() {
- msgView := view.MessageView()
- var filter findFilter
- if view.selectReason == SelectDownload || view.selectReason == SelectOpen {
- filter = view.filterMediaOnly
- }
- foundMsg := view.findMessage(msgView.selected.GetEvent(), false, filter)
- if foundMsg != nil {
- msgView.SetSelected(foundMsg)
- // TODO scroll selected message into view
- }
-}
-
-type completion struct {
- displayName string
- id string
-}
-
-func (view *RoomView) AutocompleteUser(existingText string) (completions []completion) {
- textWithoutPrefix := strings.TrimPrefix(existingText, "@")
- for userID, user := range view.Room.GetMembers() {
- if user.Displayname == textWithoutPrefix || string(userID) == existingText {
- // Exact match, return that.
- return []completion{{user.Displayname, string(userID)}}
- }
-
- if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) {
- completions = append(completions, completion{user.Displayname, string(userID)})
- }
- }
- return
-}
-
-func (view *RoomView) AutocompleteRoom(existingText string) (completions []completion) {
- for _, room := range view.parent.rooms {
- alias := string(room.Room.GetCanonicalAlias())
- if alias == existingText {
- // Exact match, return that.
- return []completion{{alias, string(room.Room.ID)}}
- }
- if strings.HasPrefix(alias, existingText) {
- completions = append(completions, completion{alias, string(room.Room.ID)})
- continue
- }
- }
- return
-}
-
-func (view *RoomView) AutocompleteEmoji(word string) (completions []string) {
- if word[0] != ':' {
- return
- }
- var valueCompletion1 string
- var manyValues bool
- for name, value := range emoji.CodeMap() {
- if name == word {
- return []string{value}
- } else if strings.HasPrefix(name, word) {
- completions = append(completions, name)
- if valueCompletion1 == "" {
- valueCompletion1 = value
- } else if valueCompletion1 != value {
- manyValues = true
- }
- }
- }
- if !manyValues && len(completions) > 0 {
- return []string{emoji.CodeMap()[completions[0]]}
- }
- return
-}
-
-func findWordToTabComplete(text string) string {
- output := ""
- runes := []rune(text)
- for i := len(runes) - 1; i >= 0; i-- {
- if unicode.IsSpace(runes[i]) {
- break
- }
- output = string(runes[i]) + output
- }
- return output
-}
-
-var (
- mentionMarkdown = "[%[1]s](https://matrix.to/#/%[2]s)"
- mentionHTML = `%[1]s`
- mentionPlaintext = "%[1]s"
-)
-
-func (view *RoomView) defaultAutocomplete(word string, startIndex int) (strCompletions []string, strCompletion string) {
- if len(word) == 0 {
- return []string{}, ""
- }
-
- completions := view.AutocompleteUser(word)
- completions = append(completions, view.AutocompleteRoom(word)...)
-
- if len(completions) == 1 {
- completion := completions[0]
- template := mentionMarkdown
- if view.config.Preferences.DisableMarkdown {
- if view.config.Preferences.DisableHTML {
- template = mentionPlaintext
- } else {
- template = mentionHTML
- }
- }
- strCompletion = fmt.Sprintf(template, completion.displayName, completion.id)
- if startIndex == 0 && completion.id[0] == '@' {
- strCompletion = strCompletion + ":"
- }
- } else if len(completions) > 1 {
- for _, completion := range completions {
- strCompletions = append(strCompletions, completion.displayName)
- }
- }
-
- strCompletions = append(strCompletions, view.parent.cmdProcessor.AutocompleteCommand(word)...)
- strCompletions = append(strCompletions, view.AutocompleteEmoji(word)...)
-
- return
-}
-
-func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
- if len(text) == 0 {
- return
- }
-
- str := runewidth.Truncate(text, cursorOffset, "")
- word := findWordToTabComplete(str)
- startIndex := len(str) - len(word)
-
- var strCompletion string
-
- strCompletions, newText, ok := view.parent.cmdProcessor.Autocomplete(view, text, cursorOffset)
- if !ok {
- strCompletions, strCompletion = view.defaultAutocomplete(word, startIndex)
- }
-
- if len(strCompletions) > 0 {
- strCompletion = util.LongestCommonPrefix(strCompletions)
- sort.Sort(sort.StringSlice(strCompletions))
- }
- if len(strCompletion) > 0 && len(strCompletions) < 2 {
- strCompletion += " "
- strCompletions = []string{}
- }
-
- if len(strCompletion) > 0 && newText == text {
- newText = str[0:startIndex] + strCompletion + text[len(str):]
- }
-
- view.input.SetTextAndMoveCursor(newText)
- view.SetCompletions(strCompletions)
-}
-
-func (view *RoomView) InputSubmit(text string) {
- if len(text) == 0 {
- return
- } else if cmd := view.parent.cmdProcessor.ParseCommand(view, text); cmd != nil {
- go view.parent.cmdProcessor.HandleCommand(cmd)
- } else {
- go view.SendMessage(event.MsgText, text)
- }
- view.editMoveText = ""
- view.SetInputText("")
-}
-
-func (view *RoomView) CopyToClipboard(text string, register string) {
- if register == "clipboard" || register == "primary" {
- err := clipboard.WriteAll(text, register)
- if err != nil {
- view.AddServiceMessage(fmt.Sprintf("Clipboard unsupported: %v", err))
- view.parent.parent.Render()
- }
- } else {
- view.AddServiceMessage(fmt.Sprintf("Clipboard register %v unsupported", register))
- view.parent.parent.Render()
- }
-}
-
-func (view *RoomView) Download(url id.ContentURI, file *attachment.EncryptedFile, filename string, openFile bool) {
- path, err := view.parent.matrix.DownloadToDisk(url, file, filename)
- if err != nil {
- view.AddServiceMessage(fmt.Sprintf("Failed to download media: %v", err))
- view.parent.parent.Render()
- return
- }
- view.AddServiceMessage(fmt.Sprintf("File downloaded to %s", path))
- view.parent.parent.Render()
- if openFile {
- debug.Print("Opening file", path)
- open.Open(path)
- }
-}
-
-func (view *RoomView) Redact(eventID id.EventID, reason string) {
- defer debug.Recover()
- err := view.parent.matrix.Redact(view.Room.ID, eventID, reason)
- if err != nil {
- if httpErr, ok := err.(mautrix.HTTPError); ok {
- err = httpErr
- if respErr := httpErr.RespError; respErr != nil {
- err = respErr
- }
- }
- view.AddServiceMessage(fmt.Sprintf("Failed to redact message: %v", err))
- view.parent.parent.Render()
- }
-}
-
-func (view *RoomView) SendReaction(eventID id.EventID, reaction string) {
- defer debug.Recover()
- if !view.config.Preferences.DisableEmojis {
- reaction = emoji.Sprint(reaction)
- }
- reaction = variationselector.Add(strings.TrimSpace(reaction))
- debug.Print("Reacting to", eventID, "in", view.Room.ID, "with", reaction)
- eventID, err := view.parent.matrix.SendEvent(&muksevt.Event{
- Event: &event.Event{
- Type: event.EventReaction,
- RoomID: view.Room.ID,
- Content: event.Content{Parsed: &event.ReactionEventContent{RelatesTo: event.RelatesTo{
- Type: event.RelAnnotation,
- EventID: eventID,
- Key: reaction,
- }}},
- },
- })
- if err != nil {
- if httpErr, ok := err.(mautrix.HTTPError); ok {
- err = httpErr
- if respErr := httpErr.RespError; respErr != nil {
- err = respErr
- }
- }
- view.AddServiceMessage(fmt.Sprintf("Failed to send reaction: %v", err))
- view.parent.parent.Render()
- }
-}
-
-func (view *RoomView) SendMessage(msgtype event.MessageType, text string) {
- view.SendMessageHTML(msgtype, text, "")
-}
-
-func (view *RoomView) getRelationForNewEvent() *ifc.Relation {
- if view.editing != nil {
- return &ifc.Relation{
- Type: event.RelReplace,
- Event: view.editing,
- }
- } else if view.replying != nil {
- return &ifc.Relation{
- Type: event.RelReply,
- Event: view.replying,
- }
- }
- return nil
-}
-
-func (view *RoomView) SendMessageHTML(msgtype event.MessageType, text, html string) {
- defer debug.Recover()
- debug.Print("Sending message", msgtype, text, "to", view.Room.ID)
- if !view.config.Preferences.DisableEmojis {
- text = emoji.Sprint(text)
- }
- rel := view.getRelationForNewEvent()
- evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text, html, rel)
- view.addLocalEcho(evt)
-}
-
-func (view *RoomView) SendMessageMedia(path string) {
- defer debug.Recover()
- debug.Print("Sending media at", path, "to", view.Room.ID)
- rel := view.getRelationForNewEvent()
- evt, err := view.parent.matrix.PrepareMediaMessage(view.Room, path, rel)
- if err != nil {
- view.AddServiceMessage(fmt.Sprintf("Failed to upload media: %v", err))
- view.parent.parent.Render()
- return
- }
- view.addLocalEcho(evt)
-}
-
-func (view *RoomView) addLocalEcho(evt *muksevt.Event) {
- msg := view.parseEvent(evt.SomewhatDangerousCopy())
- view.content.AddMessage(msg, AppendMessage)
- view.ClearAllContext()
- view.status.SetText(view.GetStatus())
- eventID, err := view.parent.matrix.SendEvent(evt)
- if err != nil {
- msg.State = muksevt.StateSendFail
- // Show shorter version if available
- if httpErr, ok := err.(mautrix.HTTPError); ok {
- err = httpErr
- if respErr := httpErr.RespError; respErr != nil {
- err = respErr
- }
- }
- view.AddServiceMessage(fmt.Sprintf("Failed to send message: %v", err))
- view.parent.parent.Render()
- } else {
- debug.Print("Event ID received:", eventID)
- msg.EventID = eventID
- msg.State = muksevt.StateDefault
- view.MessageView().setMessageID(msg)
- view.parent.parent.Render()
- }
-}
-
-func (view *RoomView) MessageView() *MessageView {
- return view.content
-}
-
-func (view *RoomView) MxRoom() *rooms.Room {
- return view.Room
-}
-
-func (view *RoomView) Update() {
- topicStr := strings.TrimSpace(strings.ReplaceAll(view.Room.GetTopic(), "\n", " "))
- if view.config.Preferences.HideRoomList {
- if len(topicStr) > 0 {
- topicStr = fmt.Sprintf("%s - %s", view.Room.GetTitle(), topicStr)
- } else {
- topicStr = view.Room.GetTitle()
- }
- topicStr = strings.TrimSpace(topicStr)
- }
- view.topic.SetText(topicStr)
- if !view.userListLoaded {
- view.UpdateUserList()
- }
-}
-
-func (view *RoomView) UpdateUserList() {
- pls := &event.PowerLevelsEventContent{}
- if plEvent := view.Room.GetStateEvent(event.StatePowerLevels, ""); plEvent != nil {
- pls = plEvent.Content.AsPowerLevels()
- }
- view.userList.Update(view.Room.GetMembers(), pls)
- view.userListLoaded = true
-}
-
-func (view *RoomView) AddServiceMessage(text string) {
- view.content.AddMessage(messages.NewServiceMessage(text), AppendMessage)
-}
-
-func (view *RoomView) parseEvent(evt *muksevt.Event) *messages.UIMessage {
- return messages.ParseEvent(view.parent.matrix, view.parent, view.Room, evt)
-}
-
-func (view *RoomView) AddHistoryEvent(evt *muksevt.Event) {
- if msg := view.parseEvent(evt); msg != nil {
- view.content.AddMessage(msg, PrependMessage)
- }
-}
-
-func (view *RoomView) AddEvent(evt *muksevt.Event) ifc.Message {
- if msg := view.parseEvent(evt); msg != nil {
- view.content.AddMessage(msg, AppendMessage)
- return msg
- }
- return nil
-}
-
-func (view *RoomView) AddRedaction(redactedEvt *muksevt.Event) {
- view.AddEvent(redactedEvt)
-}
-
-func (view *RoomView) AddEdit(evt *muksevt.Event) {
- if msg := view.parseEvent(evt); msg != nil {
- view.content.AddMessage(msg, IgnoreMessage)
- }
-}
-
-func (view *RoomView) AddReaction(evt *muksevt.Event, key string) {
- msgView := view.MessageView()
- msg := msgView.getMessageByID(evt.ID)
- if msg == nil {
- // Message not in view, nothing to do
- return
- }
- heightChanged := len(msg.Reactions) == 0
- msg.AddReaction(key)
- if heightChanged {
- // Replace buffer to update height of message
- msgView.replaceBuffer(msg, msg)
- }
-}
-
-func (view *RoomView) GetEvent(eventID id.EventID) ifc.Message {
- message, ok := view.content.messageIDs[eventID]
- if !ok {
- return nil
- }
- return message
-}
diff --git a/ui/syncing-modal.go b/ui/syncing-modal.go
deleted file mode 100644
index 646d479..0000000
--- a/ui/syncing-modal.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "time"
-
- "go.mau.fi/mauview"
-)
-
-type SyncingModal struct {
- parent *MainView
- text *mauview.TextView
- progress *mauview.ProgressBar
-}
-
-func NewSyncingModal(parent *MainView) (mauview.Component, *SyncingModal) {
- sm := &SyncingModal{
- parent: parent,
- progress: mauview.NewProgressBar(),
- text: mauview.NewTextView(),
- }
- return mauview.Center(
- mauview.NewBox(
- mauview.NewFlex().
- SetDirection(mauview.FlexRow).
- AddFixedComponent(sm.progress, 1).
- AddFixedComponent(mauview.Center(sm.text, 40, 1), 1)).
- SetTitle("Synchronizing"),
- 42, 4).
- SetAlwaysFocusChild(true), sm
-}
-
-func (sm *SyncingModal) SetMessage(text string) {
- sm.text.SetText(text)
-}
-
-func (sm *SyncingModal) SetIndeterminate() {
- sm.progress.SetIndeterminate(true)
- sm.parent.parent.app.SetRedrawTicker(100 * time.Millisecond)
- sm.parent.parent.app.Redraw()
-}
-
-func (sm *SyncingModal) SetSteps(max int) {
- sm.progress.SetMax(max)
- sm.progress.SetIndeterminate(false)
- sm.parent.parent.app.SetRedrawTicker(1 * time.Minute)
- sm.parent.parent.Render()
-}
-
-func (sm *SyncingModal) Step() {
- sm.progress.Increment(1)
-}
-
-func (sm *SyncingModal) Close() {
- sm.parent.HideModal()
-}
diff --git a/ui/tag-room-list.go b/ui/tag-room-list.go
deleted file mode 100644
index 1a01d5d..0000000
--- a/ui/tag-room-list.go
+++ /dev/null
@@ -1,331 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "encoding/json"
- "fmt"
- "math"
- "strconv"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/gomuks/debug"
-
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type OrderedRoom struct {
- *rooms.Room
- order float64
-}
-
-func NewOrderedRoom(order json.Number, room *rooms.Room) *OrderedRoom {
- numOrder, err := order.Float64()
- if err != nil {
- numOrder = 0.5
- }
- return &OrderedRoom{
- Room: room,
- order: numOrder,
- }
-}
-
-func NewDefaultOrderedRoom(room *rooms.Room) *OrderedRoom {
- return NewOrderedRoom("0.5", room)
-}
-
-func (or *OrderedRoom) Draw(roomList *RoomList, screen mauview.Screen, x, y, lineWidth int, isSelected bool) {
- style := tcell.StyleDefault.
- Foreground(roomList.mainTextColor).
- Bold(or.HasNewMessages())
- if isSelected {
- style = style.
- Foreground(roomList.selectedTextColor).
- Background(roomList.selectedBackgroundColor)
- }
-
- unreadCount := or.UnreadCount()
-
- widget.WriteLinePadded(screen, mauview.AlignLeft, or.GetTitle(), x, y, lineWidth, style)
-
- if unreadCount > 0 {
- unreadMessageCount := "99+"
- if unreadCount < 100 {
- unreadMessageCount = strconv.Itoa(unreadCount)
- }
- if or.Highlighted() {
- unreadMessageCount += "!"
- }
- unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
- widget.WriteLine(screen, mauview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
- lineWidth -= len(unreadMessageCount)
- }
-}
-
-type TagRoomList struct {
- mauview.NoopEventHandler
- // The list of rooms in the list, in reverse order
- rooms []*OrderedRoom
- // Maximum number of rooms to show
- maxShown int
- // The internal name of this tag
- name string
- // The displayname of this tag
- displayname string
- // The parent RoomList instance
- parent *RoomList
-}
-
-func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList {
- return &TagRoomList{
- maxShown: 10,
- rooms: rooms,
- name: name,
- displayname: parent.GetTagDisplayName(name),
- parent: parent,
- }
-}
-
-func (trl *TagRoomList) Visible() []*OrderedRoom {
- return trl.rooms[len(trl.rooms)-trl.Length():]
-}
-
-func (trl *TagRoomList) FirstVisible() *rooms.Room {
- visible := trl.Visible()
- if len(visible) > 0 {
- return visible[len(visible)-1].Room
- }
- return nil
-}
-
-func (trl *TagRoomList) LastVisible() *rooms.Room {
- visible := trl.Visible()
- if len(visible) > 0 {
- return visible[0].Room
- }
- return nil
-}
-
-func (trl *TagRoomList) All() []*OrderedRoom {
- return trl.rooms
-}
-
-func (trl *TagRoomList) Length() int {
- if len(trl.rooms) < trl.maxShown {
- return len(trl.rooms)
- }
- return trl.maxShown
-}
-
-func (trl *TagRoomList) TotalLength() int {
- return len(trl.rooms)
-}
-
-func (trl *TagRoomList) IsEmpty() bool {
- return len(trl.rooms) == 0
-}
-
-func (trl *TagRoomList) IsCollapsed() bool {
- return trl.maxShown == 0
-}
-
-func (trl *TagRoomList) ToggleCollapse() {
- if trl.IsCollapsed() {
- trl.maxShown = 10
- } else {
- trl.maxShown = 0
- }
-}
-
-func (trl *TagRoomList) HasInvisibleRooms() bool {
- return trl.maxShown < trl.TotalLength()
-}
-
-func (trl *TagRoomList) HasVisibleRooms() bool {
- return !trl.IsEmpty() && trl.maxShown > 0
-}
-
-const equalityThreshold = 1e-6
-
-func almostEqual(a, b float64) bool {
- return math.Abs(a-b) <= equalityThreshold
-}
-
-// ShouldBeAfter returns if the first room should be after the second room in the room list.
-// The manual order and last received message timestamp are considered.
-func (trl *TagRoomList) ShouldBeAfter(room1 *OrderedRoom, room2 *OrderedRoom) bool {
- // Lower order value = higher in list
- return room1.order > room2.order ||
- // Equal order value and more recent message = higher in the list
- (almostEqual(room1.order, room2.order) && room2.LastReceivedMessage.After(room1.LastReceivedMessage))
-}
-
-func (trl *TagRoomList) Insert(order json.Number, mxRoom *rooms.Room) {
- room := NewOrderedRoom(order, mxRoom)
- // The default insert index is the newly added slot.
- // That index will be used if all other rooms in the list have the same LastReceivedMessage timestamp.
- insertAt := len(trl.rooms)
- // Find the spot where the new room should be put according to the last received message timestamps.
- for i := 0; i < len(trl.rooms); i++ {
- if trl.rooms[i].Room == mxRoom {
- debug.Printf("Warning: tried to re-insert room %s into tag %s", mxRoom.ID, trl.name)
- return
- } else if trl.ShouldBeAfter(room, trl.rooms[i]) {
- insertAt = i
- break
- }
- }
- trl.rooms = append(trl.rooms, nil)
- copy(trl.rooms[insertAt+1:], trl.rooms[insertAt:len(trl.rooms)-1])
- trl.rooms[insertAt] = room
-}
-
-func (trl *TagRoomList) Bump(mxRoom *rooms.Room) {
- var roomBeingBumped *OrderedRoom
- for i := 0; i < len(trl.rooms); i++ {
- currentIndexRoom := trl.rooms[i]
- if roomBeingBumped != nil {
- if trl.ShouldBeAfter(roomBeingBumped, currentIndexRoom) {
- // This room should be after the room being bumped, so insert the
- // room being bumped here and return
- trl.rooms[i-1] = roomBeingBumped
- return
- }
- // Move older rooms back in the array
- trl.rooms[i-1] = currentIndexRoom
- } else if currentIndexRoom.Room == mxRoom {
- roomBeingBumped = currentIndexRoom
- }
- }
- if roomBeingBumped == nil {
- debug.Print("Warning: couldn't find room", mxRoom.ID, mxRoom.NameCache, "to bump in tag", trl.name)
- return
- }
- // If the room being bumped should be first in the list, it won't be inserted during the loop.
- trl.rooms[len(trl.rooms)-1] = roomBeingBumped
-}
-
-func (trl *TagRoomList) Remove(room *rooms.Room) {
- trl.RemoveIndex(trl.Index(room))
-}
-
-func (trl *TagRoomList) RemoveIndex(index int) {
- if index < 0 || index > len(trl.rooms) {
- return
- }
- last := len(trl.rooms) - 1
- if index < last {
- copy(trl.rooms[index:], trl.rooms[index+1:])
- }
- trl.rooms[last] = nil
- trl.rooms = trl.rooms[:last]
-}
-
-func (trl *TagRoomList) Index(room *rooms.Room) int {
- return trl.indexInList(trl.All(), room)
-}
-
-func (trl *TagRoomList) IndexVisible(room *rooms.Room) int {
- return trl.indexInList(trl.Visible(), room)
-}
-
-func (trl *TagRoomList) indexInList(list []*OrderedRoom, room *rooms.Room) int {
- for index, entry := range list {
- if entry.Room == room {
- return index
- }
- }
- return -1
-}
-
-var TagDisplayNameStyle = tcell.StyleDefault.Underline(true).Bold(true)
-var TagRoomCountStyle = tcell.StyleDefault.Italic(true)
-
-func (trl *TagRoomList) RenderHeight() int {
- if len(trl.displayname) == 0 {
- return 0
- }
-
- if trl.IsCollapsed() {
- return 1
- }
- height := 2 + trl.Length()
- if trl.HasInvisibleRooms() || trl.maxShown > 10 {
- height++
- }
- return height
-}
-
-func (trl *TagRoomList) DrawHeader(screen mauview.Screen) {
- width, _ := screen.Size()
- roomCount := strconv.Itoa(trl.TotalLength())
-
- // Draw tag name
- displayNameWidth := width - 1 - len(roomCount)
- widget.WriteLine(screen, mauview.AlignLeft, trl.displayname, 0, 0, displayNameWidth, TagDisplayNameStyle)
-
- // Draw tag room count
- roomCountX := len(trl.displayname) + 1
- roomCountWidth := width - 2 - len(trl.displayname)
- widget.WriteLine(screen, mauview.AlignLeft, roomCount, roomCountX, 0, roomCountWidth, TagRoomCountStyle)
-}
-
-func (trl *TagRoomList) Draw(screen mauview.Screen) {
- if len(trl.displayname) == 0 {
- return
- }
-
- trl.DrawHeader(screen)
-
- width, height := screen.Size()
-
- items := trl.Visible()
-
- if trl.IsCollapsed() {
- screen.SetCell(width-1, 0, tcell.StyleDefault, '▶')
- return
- }
- screen.SetCell(width-1, 0, tcell.StyleDefault, '▼')
-
- y := 1
- for i := len(items) - 1; i >= 0; i-- {
- if y >= height {
- return
- }
-
- item := items[i]
-
- lineWidth := width
- isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected
- item.Draw(trl.parent, screen, 0, y, lineWidth, isSelected)
- y++
- }
- hasLess := trl.maxShown > 10
- hasMore := trl.HasInvisibleRooms()
- if (hasLess || hasMore) && y < height {
- if hasMore {
- widget.WriteLine(screen, mauview.AlignRight, "More ↓", 0, y, width, tcell.StyleDefault)
- }
- if hasLess {
- widget.WriteLine(screen, mauview.AlignLeft, "↑ Less", 0, y, width, tcell.StyleDefault)
- }
- y++
- }
-}
diff --git a/ui/ui.go b/ui/ui.go
deleted file mode 100644
index d823e64..0000000
--- a/ui/ui.go
+++ /dev/null
@@ -1,128 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "os"
- "os/exec"
-
- "github.com/zyedidia/clipboard"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- ifc "maunium.net/go/gomuks/interface"
-)
-
-type View string
-
-// Allowed views in GomuksUI
-const (
- ViewLogin View = "login"
- ViewMain View = "main"
-)
-
-type GomuksUI struct {
- gmx ifc.Gomuks
- app *mauview.Application
-
- mainView *MainView
- loginView *LoginView
-
- views map[View]mauview.Component
-}
-
-func init() {
- mauview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
- mauview.Styles.PrimaryTextColor = tcell.ColorDefault
- mauview.Styles.BorderColor = tcell.ColorDefault
- mauview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
- if tcellDB := os.Getenv("TCELLDB"); len(tcellDB) == 0 {
- if info, err := os.Stat("/usr/share/tcell/database"); err == nil && info.IsDir() {
- os.Setenv("TCELLDB", "/usr/share/tcell/database")
- }
- }
-}
-
-func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI {
- ui := &GomuksUI{
- gmx: gmx,
- app: mauview.NewApplication(),
- }
- return ui
-}
-
-func (ui *GomuksUI) Init() {
- mauview.Backspace2RemovesWord = ui.gmx.Config().Backspace2RemovesWord
- mauview.Backspace1RemovesWord = ui.gmx.Config().Backspace1RemovesWord
- ui.app.SetAlwaysClear(ui.gmx.Config().AlwaysClearScreen)
- clipboard.Initialize()
- ui.views = map[View]mauview.Component{
- ViewLogin: ui.NewLoginView(),
- ViewMain: ui.NewMainView(),
- }
- ui.SetView(ViewLogin)
-}
-
-func (ui *GomuksUI) Start() error {
- return ui.app.Start()
-}
-
-func (ui *GomuksUI) Stop() {
- ui.app.Stop()
-}
-
-func (ui *GomuksUI) Finish() {
- ui.app.ForceStop()
-}
-
-func (ui *GomuksUI) Render() {
- ui.app.Redraw()
-}
-
-func (ui *GomuksUI) OnLogin() {
- ui.SetView(ViewMain)
-}
-
-func (ui *GomuksUI) OnLogout() {
- ui.SetView(ViewLogin)
-}
-
-func (ui *GomuksUI) HandleNewPreferences() {
- ui.Render()
-}
-
-func (ui *GomuksUI) SetView(name View) {
- ui.app.SetRoot(ui.views[name])
-}
-
-func (ui *GomuksUI) MainView() ifc.MainView {
- return ui.mainView
-}
-
-func (ui *GomuksUI) RunExternal(executablePath string, args ...string) error {
- callback := make(chan error)
- ui.app.Suspend(func() {
- cmd := exec.Command(executablePath, args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Stdin = os.Stdin
- cmd.Env = os.Environ()
- callback <- cmd.Run()
- })
- return <-callback
-}
diff --git a/ui/verification-modal.go b/ui/verification-modal.go
deleted file mode 100644
index 0959474..0000000
--- a/ui/verification-modal.go
+++ /dev/null
@@ -1,253 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-//go:build cgo
-
-package ui
-
-import (
- "fmt"
- "strconv"
- "strings"
- "time"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/crypto"
- "maunium.net/go/mautrix/event"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
-)
-
-type EmojiView struct {
- mauview.SimpleEventHandler
- Data crypto.SASData
-}
-
-func (e *EmojiView) Draw(screen mauview.Screen) {
- if e.Data == nil {
- return
- }
- switch e.Data.Type() {
- case event.SASEmoji:
- width := 10
- for i, emoji := range e.Data.(crypto.EmojiSASData) {
- x := i*width + i
- y := 0
- if i >= 4 {
- x = (i-4)*width + i
- y = 2
- }
- mauview.Print(screen, string(emoji.Emoji), x, y, width, mauview.AlignCenter, tcell.ColorDefault)
- mauview.Print(screen, emoji.Description, x, y+1, width, mauview.AlignCenter, tcell.ColorDefault)
- }
- case event.SASDecimal:
- maxWidth := 43
- for i, number := range e.Data.(crypto.DecimalSASData) {
- mauview.Print(screen, strconv.FormatUint(uint64(number), 10), 0, i, maxWidth, mauview.AlignCenter, tcell.ColorDefault)
- }
- }
-}
-
-type VerificationModal struct {
- mauview.Component
-
- device *crypto.DeviceIdentity
-
- container *mauview.Box
-
- waitingBar *mauview.ProgressBar
- infoText *mauview.TextView
- emojiText *EmojiView
- inputBar *mauview.InputField
-
- progress int
- progressMax int
- stopWaiting chan struct{}
- confirmChan chan bool
- done bool
-
- parent *MainView
-}
-
-func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, timeout time.Duration) *VerificationModal {
- vm := &VerificationModal{
- parent: mainView,
- device: device,
- stopWaiting: make(chan struct{}),
- confirmChan: make(chan bool),
- done: false,
- }
-
- vm.progressMax = int(timeout.Seconds())
- vm.progress = vm.progressMax
- vm.waitingBar = mauview.NewProgressBar().
- SetMax(vm.progressMax).
- SetProgress(vm.progress).
- SetIndeterminate(false)
-
- vm.infoText = mauview.NewTextView()
- vm.infoText.SetText(fmt.Sprintf("Waiting for %s\nto accept", device.UserID))
-
- vm.emojiText = &EmojiView{}
-
- vm.inputBar = mauview.NewInputField().
- SetBackgroundColor(tcell.ColorDefault).
- SetPlaceholderTextColor(tcell.ColorDefault)
-
- flex := mauview.NewFlex().
- SetDirection(mauview.FlexRow).
- AddFixedComponent(vm.waitingBar, 1).
- AddFixedComponent(vm.infoText, 4).
- AddFixedComponent(vm.emojiText, 4).
- AddFixedComponent(vm.inputBar, 1)
-
- vm.container = mauview.NewBox(flex).
- SetBorder(true).
- SetTitle("Interactive verification")
-
- vm.Component = mauview.Center(vm.container, 45, 12).SetAlwaysFocusChild(true)
-
- go vm.decrementWaitingBar()
-
- return vm
-}
-
-func (vm *VerificationModal) decrementWaitingBar() {
- for {
- select {
- case <-time.Tick(time.Second):
- if vm.progress <= 0 {
- vm.waitingBar.SetIndeterminate(true)
- vm.parent.parent.app.SetRedrawTicker(100 * time.Millisecond)
- return
- }
- vm.progress--
- vm.waitingBar.SetProgress(vm.progress)
- vm.parent.parent.Render()
- case <-vm.stopWaiting:
- return
- }
- }
-}
-
-func (vm *VerificationModal) VerificationMethods() []crypto.VerificationMethod {
- return []crypto.VerificationMethod{crypto.VerificationMethodEmoji{}, crypto.VerificationMethodDecimal{}}
-}
-
-func (vm *VerificationModal) VerifySASMatch(device *crypto.DeviceIdentity, data crypto.SASData) bool {
- vm.device = device
- var typeName string
- if data.Type() == event.SASDecimal {
- typeName = "numbers"
- } else if data.Type() == event.SASEmoji {
- typeName = "emojis"
- } else {
- return false
- }
- vm.infoText.SetText(fmt.Sprintf(
- "Check if the other device is showing the\n"+
- "same %s as below, then type \"yes\" to\n"+
- "accept, or \"no\" to reject", typeName))
- vm.inputBar.
- SetTextColor(tcell.ColorDefault).
- SetBackgroundColor(tcell.ColorDarkCyan).
- SetPlaceholder("Type \"yes\" or \"no\"").
- Focus()
- vm.emojiText.Data = data
- vm.parent.parent.Render()
- vm.progress = vm.progressMax
- confirm := <-vm.confirmChan
- vm.progress = vm.progressMax
- vm.emojiText.Data = nil
- vm.infoText.SetText(fmt.Sprintf("Waiting for %s\nto confirm", vm.device.UserID))
- vm.parent.parent.Render()
- return confirm
-}
-
-func (vm *VerificationModal) OnCancel(cancelledByUs bool, reason string, _ event.VerificationCancelCode) {
- vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100)
- vm.parent.parent.app.SetRedrawTicker(1 * time.Minute)
- if cancelledByUs {
- vm.infoText.SetText(fmt.Sprintf("Verification failed: %s", reason))
- } else {
- vm.infoText.SetText(fmt.Sprintf("Verification cancelled by %s: %s", vm.device.UserID, reason))
- }
- vm.inputBar.SetPlaceholder("Press enter to close the dialog")
- vm.stopWaiting <- struct{}{}
- vm.done = true
- vm.parent.parent.Render()
-}
-
-func (vm *VerificationModal) OnSuccess() {
- vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100)
- vm.parent.parent.app.SetRedrawTicker(1 * time.Minute)
- vm.infoText.SetText(fmt.Sprintf("Successfully verified %s (%s) of %s", vm.device.Name, vm.device.DeviceID, vm.device.UserID))
- vm.inputBar.SetPlaceholder("Press enter to close the dialog")
- vm.stopWaiting <- struct{}{}
- vm.done = true
- vm.parent.parent.Render()
- if vm.parent.config.SendToVerifiedOnly {
- // Hacky way to make new group sessions after verified
- vm.parent.matrix.Crypto().(*crypto.OlmMachine).OnDevicesChanged(vm.device.UserID)
- }
-}
-
-func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool {
- kb := config.Keybind{
- Key: event.Key(),
- Ch: event.Rune(),
- Mod: event.Modifiers(),
- }
- if vm.done {
- if vm.parent.config.Keybindings.Modal[kb] == "cancel" || vm.parent.config.Keybindings.Modal[kb] == "confirm" {
- vm.parent.HideModal()
- return true
- }
- return false
- } else if vm.emojiText.Data == nil {
- debug.Print("Ignoring pre-emoji key event")
- return false
- }
- if vm.parent.config.Keybindings.Modal[kb] == "confirm" {
- text := strings.ToLower(strings.TrimSpace(vm.inputBar.GetText()))
- if text == "yes" {
- debug.Print("Confirming verification")
- vm.confirmChan <- true
- } else if text == "no" {
- debug.Print("Rejecting verification")
- vm.confirmChan <- false
- }
- vm.inputBar.
- SetPlaceholder("").
- SetTextAndMoveCursor("").
- SetBackgroundColor(tcell.ColorDefault).
- SetTextColor(tcell.ColorDefault)
- return true
- } else {
- return vm.inputBar.OnKeyEvent(event)
- }
-}
-
-func (vm *VerificationModal) Focus() {
- vm.container.Focus()
-}
-
-func (vm *VerificationModal) Blur() {
- vm.container.Blur()
-}
diff --git a/ui/view-login.go b/ui/view-login.go
deleted file mode 100644
index 100a56c..0000000
--- a/ui/view-login.go
+++ /dev/null
@@ -1,196 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "math"
-
- "github.com/mattn/go-runewidth"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/id"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
-)
-
-type LoginView struct {
- *mauview.Form
-
- container *mauview.Centerer
-
- homeserverLabel *mauview.TextField
- usernameLabel *mauview.TextField
- passwordLabel *mauview.TextField
-
- homeserver *mauview.InputField
- username *mauview.InputField
- password *mauview.InputField
- error *mauview.TextView
-
- loginButton *mauview.Button
- quitButton *mauview.Button
-
- loading bool
-
- matrix ifc.MatrixContainer
- config *config.Config
- parent *GomuksUI
-}
-
-func (ui *GomuksUI) NewLoginView() mauview.Component {
- view := &LoginView{
- Form: mauview.NewForm(),
-
- usernameLabel: mauview.NewTextField().SetText("Username"),
- passwordLabel: mauview.NewTextField().SetText("Password"),
- homeserverLabel: mauview.NewTextField().SetText("Homeserver"),
-
- username: mauview.NewInputField(),
- password: mauview.NewInputField(),
- homeserver: mauview.NewInputField(),
-
- loginButton: mauview.NewButton("Login"),
- quitButton: mauview.NewButton("Quit"),
-
- matrix: ui.gmx.Matrix(),
- config: ui.gmx.Config(),
- parent: ui,
- }
-
- hs := ui.gmx.Config().HS
- view.homeserver.SetPlaceholder("https://example.com").SetText(hs).SetTextColor(tcell.ColorWhite)
- view.username.SetPlaceholder("@user:example.com").SetText(string(ui.gmx.Config().UserID)).SetTextColor(tcell.ColorWhite)
- view.password.SetPlaceholder("correct horse battery staple").SetMaskCharacter('*').SetTextColor(tcell.ColorWhite)
-
- view.quitButton.
- SetOnClick(func() { ui.gmx.Stop(true) }).
- SetBackgroundColor(tcell.ColorDarkCyan).
- SetForegroundColor(tcell.ColorWhite).
- SetFocusedForegroundColor(tcell.ColorWhite)
- view.loginButton.
- SetOnClick(view.Login).
- SetBackgroundColor(tcell.ColorDarkCyan).
- SetForegroundColor(tcell.ColorWhite).
- SetFocusedForegroundColor(tcell.ColorWhite)
-
- view.
- SetColumns([]int{1, 10, 1, 30, 1}).
- SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
- view.
- AddFormItem(view.username, 3, 1, 1, 1).
- AddFormItem(view.password, 3, 3, 1, 1).
- AddFormItem(view.homeserver, 3, 5, 1, 1).
- AddFormItem(view.loginButton, 1, 7, 3, 1).
- AddFormItem(view.quitButton, 1, 9, 3, 1).
- AddComponent(view.usernameLabel, 1, 1, 1, 1).
- AddComponent(view.passwordLabel, 1, 3, 1, 1).
- AddComponent(view.homeserverLabel, 1, 5, 1, 1)
- view.SetOnFocusChanged(view.focusChanged)
- view.FocusNextItem()
- ui.loginView = view
-
- view.container = mauview.Center(mauview.NewBox(view).SetTitle("Log in to Matrix"), 45, 13)
- view.container.SetAlwaysFocusChild(true)
- return view.container
-}
-
-func (view *LoginView) resolveWellKnown() {
- _, homeserver, err := id.UserID(view.username.GetText()).Parse()
- if err != nil {
- return
- }
- view.homeserver.SetText("Resolving...")
- resp, err := mautrix.DiscoverClientAPI(homeserver)
- if err != nil {
- view.homeserver.SetText("")
- view.Error(err.Error())
- } else if resp != nil {
- view.homeserver.SetText(resp.Homeserver.BaseURL)
- view.parent.Render()
- }
-}
-
-func (view *LoginView) focusChanged(from, to mauview.Component) {
- if from == view.username && view.homeserver.GetText() == "" {
- go view.resolveWellKnown()
- }
-}
-
-func (view *LoginView) Error(err string) {
- if len(err) == 0 && view.error != nil {
- debug.Print("Hiding error")
- view.RemoveComponent(view.error)
- view.container.SetHeight(13)
- view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1})
- view.error = nil
- } else if len(err) > 0 {
- debug.Print("Showing error", err)
- if view.error == nil {
- view.error = mauview.NewTextView().SetTextColor(tcell.ColorRed)
- view.AddComponent(view.error, 1, 11, 3, 1)
- }
- view.error.SetText(err)
- errorHeight := int(math.Ceil(float64(runewidth.StringWidth(err)) / 41))
- view.container.SetHeight(14 + errorHeight)
- view.SetRow(11, errorHeight)
- }
-
- view.parent.Render()
-}
-
-func (view *LoginView) actuallyLogin(hs, mxid, password string) {
- debug.Printf("Logging into %s as %s...", hs, mxid)
- view.config.HS = hs
-
- if err := view.matrix.InitClient(false); err != nil {
- debug.Print("Init error:", err)
- view.Error(err.Error())
- } else if err = view.matrix.Login(mxid, password); err != nil {
- if httpErr, ok := err.(mautrix.HTTPError); ok {
- if httpErr.RespError != nil && len(httpErr.RespError.Err) > 0 {
- view.Error(httpErr.RespError.Err)
- } else if len(httpErr.Message) > 0 {
- view.Error(httpErr.Message)
- } else {
- view.Error(err.Error())
- }
- } else {
- view.Error(err.Error())
- }
- debug.Print("Login error:", err)
- }
- view.loading = false
- view.loginButton.SetText("Login")
-}
-
-func (view *LoginView) Login() {
- if view.loading {
- return
- }
- hs := view.homeserver.GetText()
- mxid := view.username.GetText()
- password := view.password.GetText()
-
- view.loading = true
- view.loginButton.SetText("Logging in...")
- go view.actuallyLogin(hs, mxid, password)
-}
diff --git a/ui/view-main.go b/ui/view-main.go
deleted file mode 100644
index ed92e3a..0000000
--- a/ui/view-main.go
+++ /dev/null
@@ -1,463 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package ui
-
-import (
- "bufio"
- "fmt"
- "os"
- "sync/atomic"
- "time"
-
- sync "github.com/sasha-s/go-deadlock"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/pushrules"
-
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/debug"
- ifc "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/lib/notification"
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/messages"
- "maunium.net/go/gomuks/ui/widget"
-)
-
-type MainView struct {
- flex *mauview.Flex
-
- roomList *RoomList
- roomView *mauview.Box
- currentRoom *RoomView
- rooms map[id.RoomID]*RoomView
- roomsLock sync.RWMutex
- cmdProcessor *CommandProcessor
- focused mauview.Focusable
-
- modal mauview.Component
-
- lastFocusTime time.Time
-
- matrix ifc.MatrixContainer
- gmx ifc.Gomuks
- config *config.Config
- parent *GomuksUI
-}
-
-func (ui *GomuksUI) NewMainView() mauview.Component {
- mainView := &MainView{
- flex: mauview.NewFlex().SetDirection(mauview.FlexColumn),
- roomView: mauview.NewBox(nil).SetBorder(false),
- rooms: make(map[id.RoomID]*RoomView),
-
- matrix: ui.gmx.Matrix(),
- gmx: ui.gmx,
- config: ui.gmx.Config(),
- parent: ui,
- }
- mainView.roomList = NewRoomList(mainView)
- mainView.cmdProcessor = NewCommandProcessor(mainView)
-
- mainView.flex.
- AddFixedComponent(mainView.roomList, 25).
- AddFixedComponent(widget.NewBorder(), 1).
- AddProportionalComponent(mainView.roomView, 1)
- mainView.BumpFocus(nil)
-
- ui.mainView = mainView
-
- return mainView
-}
-
-func (view *MainView) ShowModal(modal mauview.Component) {
- view.modal = modal
- var ok bool
- view.focused, ok = modal.(mauview.Focusable)
- if !ok {
- view.focused = nil
- } else {
- view.focused.Focus()
- }
-}
-
-func (view *MainView) HideModal() {
- view.modal = nil
- view.focused = view.roomView
-}
-
-func (view *MainView) Draw(screen mauview.Screen) {
- if view.config.Preferences.HideRoomList {
- view.roomView.Draw(screen)
- } else {
- view.flex.Draw(screen)
- }
-
- if view.modal != nil {
- view.modal.Draw(screen)
- }
-}
-
-func (view *MainView) BumpFocus(roomView *RoomView) {
- if roomView != nil {
- view.lastFocusTime = time.Now()
- view.MarkRead(roomView)
- }
-}
-
-func (view *MainView) MarkRead(roomView *RoomView) {
- if roomView != nil && roomView.Room.HasNewMessages() && roomView.MessageView().ScrollOffset == 0 {
- msgList := roomView.MessageView().messages
- if len(msgList) > 0 {
- msg := msgList[len(msgList)-1]
- if roomView.Room.MarkRead(msg.ID()) {
- view.matrix.MarkRead(roomView.Room.ID, msg.ID())
- }
- }
- }
-}
-
-func (view *MainView) InputChanged(roomView *RoomView, text string) {
- if !roomView.config.Preferences.DisableTypingNotifs {
- view.matrix.SendTyping(roomView.Room.ID, len(text) > 0 && text[0] != '/')
- }
-}
-
-func (view *MainView) ShowBare(roomView *RoomView) {
- if roomView == nil {
- return
- }
- _, height := view.parent.app.Screen().Size()
- view.parent.app.Suspend(func() {
- print("\033[2J\033[0;0H")
- // We don't know how much space there exactly is. Too few messages looks weird,
- // and too many messages shouldn't cause any problems, so we just show too many.
- height *= 2
- fmt.Println(roomView.MessageView().CapturePlaintext(height))
- fmt.Println("Press enter to return to normal mode.")
- reader := bufio.NewReader(os.Stdin)
- _, _, _ = reader.ReadRune()
- print("\033[2J\033[0;0H")
- })
-}
-
-func (view *MainView) OpenSyncingModal() ifc.SyncingModal {
- component, modal := NewSyncingModal(view)
- view.ShowModal(component)
- return modal
-}
-
-func (view *MainView) OnKeyEvent(event mauview.KeyEvent) bool {
- view.BumpFocus(view.currentRoom)
-
- if view.modal != nil {
- return view.modal.OnKeyEvent(event)
- }
-
- kb := config.Keybind{
- Key: event.Key(),
- Ch: event.Rune(),
- Mod: event.Modifiers(),
- }
- switch view.config.Keybindings.Main[kb] {
- case "next_room":
- view.SwitchRoom(view.roomList.Next())
- case "prev_room":
- view.SwitchRoom(view.roomList.Previous())
- case "search_rooms":
- view.ShowModal(NewFuzzySearchModal(view, 42, 12))
- case "scroll_up":
- msgView := view.currentRoom.MessageView()
- msgView.AddScrollOffset(msgView.TotalHeight())
- case "scroll_down":
- msgView := view.currentRoom.MessageView()
- msgView.AddScrollOffset(-msgView.TotalHeight())
- case "add_newline":
- return view.flex.OnKeyEvent(tcell.NewEventKey(tcell.KeyEnter, '\n', event.Modifiers()|tcell.ModShift))
- case "next_active_room":
- view.SwitchRoom(view.roomList.NextWithActivity())
- case "show_bare":
- view.ShowBare(view.currentRoom)
- default:
- goto defaultHandler
- }
- return true
-defaultHandler:
- if view.config.Preferences.HideRoomList {
- return view.roomView.OnKeyEvent(event)
- }
- return view.flex.OnKeyEvent(event)
-}
-
-const WheelScrollOffsetDiff = 3
-
-func (view *MainView) OnMouseEvent(event mauview.MouseEvent) bool {
- if view.modal != nil {
- return view.modal.OnMouseEvent(event)
- }
- if view.config.Preferences.HideRoomList {
- return view.roomView.OnMouseEvent(event)
- }
- return view.flex.OnMouseEvent(event)
-}
-
-func (view *MainView) OnPasteEvent(event mauview.PasteEvent) bool {
- if view.modal != nil {
- return view.modal.OnPasteEvent(event)
- } else if view.config.Preferences.HideRoomList {
- return view.roomView.OnPasteEvent(event)
- }
- return view.flex.OnPasteEvent(event)
-}
-
-func (view *MainView) Focus() {
- if view.focused != nil {
- view.focused.Focus()
- }
-}
-
-func (view *MainView) Blur() {
- if view.focused != nil {
- view.focused.Blur()
- }
-}
-
-func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
- view.switchRoom(tag, room, true)
-}
-
-func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
- if room == nil {
- return
- }
- room.Load()
-
- roomView, ok := view.getRoomView(room.ID, lock)
- if !ok {
- debug.Print("Tried to switch to room with nonexistent roomView!")
- debug.Print(tag, room)
- return
- }
- roomView.Update()
- view.roomView.SetInnerComponent(roomView)
- view.currentRoom = roomView
- view.MarkRead(roomView)
- view.roomList.SetSelected(tag, room)
- view.flex.SetFocused(view.roomView)
- view.focused = view.roomView
- view.roomView.Focus()
- view.parent.Render()
-
- if msgView := roomView.MessageView(); len(msgView.messages) < 20 && !msgView.initialHistoryLoaded {
- msgView.initialHistoryLoaded = true
- go view.LoadHistory(room.ID)
- }
- if !room.MembersFetched {
- go func() {
- err := view.matrix.FetchMembers(room)
- if err != nil {
- debug.Print("Error fetching members:", err)
- return
- }
- roomView.UpdateUserList()
- view.parent.Render()
- }()
- }
-}
-
-func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
- if _, ok := view.rooms[room.ID]; !ok {
- roomView := NewRoomView(view, room).
- SetInputChangedFunc(view.InputChanged)
- view.rooms[room.ID] = roomView
- return roomView
- }
- return nil
-}
-
-func (view *MainView) GetRoom(roomID id.RoomID) ifc.RoomView {
- room, ok := view.getRoomView(roomID, true)
- if !ok {
- return view.addRoom(view.matrix.GetOrCreateRoom(roomID))
- }
- return room
-}
-
-func (view *MainView) getRoomView(roomID id.RoomID, lock bool) (room *RoomView, ok bool) {
- if lock {
- view.roomsLock.RLock()
- room, ok = view.rooms[roomID]
- view.roomsLock.RUnlock()
- } else {
- room, ok = view.rooms[roomID]
- }
- return room, ok
-}
-
-func (view *MainView) AddRoom(room *rooms.Room) {
- view.addRoom(room)
-}
-
-func (view *MainView) RemoveRoom(room *rooms.Room) {
- view.roomsLock.Lock()
- _, ok := view.getRoomView(room.ID, false)
- if !ok {
- view.roomsLock.Unlock()
- debug.Print("Remove aborted (not found)", room.ID, room.GetTitle())
- return
- }
- debug.Print("Removing", room.ID, room.GetTitle())
-
- view.roomList.Remove(room)
- t, r := view.roomList.Selected()
- view.switchRoom(t, r, false)
- delete(view.rooms, room.ID)
- view.roomsLock.Unlock()
-
- view.parent.Render()
-}
-
-func (view *MainView) addRoom(room *rooms.Room) *RoomView {
- if view.roomList.Contains(room.ID) {
- debug.Print("Add aborted (room exists)", room.ID, room.GetTitle())
- return nil
- }
- debug.Print("Adding", room.ID, room.GetTitle())
- view.roomList.Add(room)
- view.roomsLock.Lock()
- roomView := view.addRoomPage(room)
- if !view.roomList.HasSelected() {
- t, r := view.roomList.First()
- view.switchRoom(t, r, false)
- }
- view.roomsLock.Unlock()
- return roomView
-}
-
-func (view *MainView) SetRooms(rooms *rooms.RoomCache) {
- view.roomList.Clear()
- view.roomsLock.Lock()
- view.rooms = make(map[id.RoomID]*RoomView)
- for _, room := range rooms.Map {
- if room.HasLeft {
- continue
- }
- view.roomList.Add(room)
- view.addRoomPage(room)
- }
- t, r := view.roomList.First()
- view.switchRoom(t, r, false)
- view.roomsLock.Unlock()
-}
-
-func (view *MainView) UpdateTags(room *rooms.Room) {
- if !view.roomList.Contains(room.ID) {
- return
- }
- reselect := view.roomList.selected == room
- view.roomList.Remove(room)
- view.roomList.Add(room)
- if reselect {
- view.roomList.SetSelected(room.Tags()[0].Tag, room)
- }
- view.parent.Render()
-}
-
-func (view *MainView) SetTyping(roomID id.RoomID, users []id.UserID) {
- roomView, ok := view.getRoomView(roomID, true)
- if ok {
- roomView.SetTyping(users)
- view.parent.Render()
- }
-}
-
-func sendNotification(room *rooms.Room, sender, text string, critical, sound bool) {
- if room.GetTitle() != sender {
- sender = fmt.Sprintf("%s (%s)", sender, room.GetTitle())
- }
- debug.Printf("Sending notification with body \"%s\" from %s in room ID %s (critical=%v, sound=%v)", text, sender, room.ID, critical, sound)
- notification.Send(sender, text, critical, sound)
-}
-
-func (view *MainView) Bump(room *rooms.Room) {
- view.roomList.Bump(room)
-}
-
-func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) {
- view.Bump(room)
- uiMsg, ok := message.(*messages.UIMessage)
- if ok && uiMsg.SenderID == view.config.UserID {
- return
- }
- // Whether or not the room where the message came is the currently shown room.
- isCurrent := room == view.roomList.SelectedRoom()
- // Whether or not the terminal window is focused.
- recentlyFocused := time.Now().Add(-30 * time.Second).Before(view.lastFocusTime)
- isFocused := time.Now().Add(-5 * time.Second).Before(view.lastFocusTime)
-
- if !isCurrent || !isFocused {
- // The message is not in the current room, show new message status in room list.
- room.AddUnread(message.ID(), should.Notify, should.Highlight)
- } else {
- view.matrix.MarkRead(room.ID, message.ID())
- }
-
- if should.Notify && !recentlyFocused && !view.config.Preferences.DisableNotifications {
- // Push rules say notify and the terminal is not focused, send desktop notification.
- shouldPlaySound := should.PlaySound &&
- should.SoundName == "default" &&
- view.config.NotifySound
- sendNotification(room, message.NotificationSenderName(), message.NotificationContent(), should.Highlight, shouldPlaySound)
- }
-
- // TODO this should probably happen somewhere else
- // (actually it's probably completely broken now)
- message.SetIsHighlight(should.Highlight)
-}
-
-func (view *MainView) LoadHistory(roomID id.RoomID) {
- defer debug.Recover()
- roomView, ok := view.getRoomView(roomID, true)
- if !ok {
- return
- }
- msgView := roomView.MessageView()
-
- if !atomic.CompareAndSwapInt32(&msgView.loadingMessages, 0, 1) {
- // Locked
- return
- }
- defer atomic.StoreInt32(&msgView.loadingMessages, 0)
- // Update the "Loading more messages..." text
- view.parent.Render()
-
- history, newLoadPtr, err := view.matrix.GetHistory(roomView.Room, 50, msgView.historyLoadPtr)
- if err != nil {
- roomView.AddServiceMessage("Failed to fetch history")
- debug.Print("Failed to fetch history for", roomView.Room.ID, err)
- view.parent.Render()
- return
- }
- //debug.Printf("Load pointer %d -> %d", msgView.historyLoadPtr, newLoadPtr)
- msgView.historyLoadPtr = newLoadPtr
- for _, evt := range history {
- roomView.AddHistoryEvent(evt)
- }
- view.parent.Render()
-}
diff --git a/ui/widget/border.go b/ui/widget/border.go
deleted file mode 100644
index eab22dd..0000000
--- a/ui/widget/border.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package widget
-
-import (
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-// Border is a simple tview widget that renders a horizontal or vertical bar.
-//
-// If the width of the box is 1, the bar will be vertical.
-// If the height is 1, the bar will be horizontal.
-// If the width nor the height are 1, nothing will be rendered.
-type Border struct {
- Style tcell.Style
-}
-
-// NewBorder wraps a new tview Box into a new Border.
-func NewBorder() *Border {
- return &Border{
- Style: tcell.StyleDefault.Foreground(mauview.Styles.BorderColor),
- }
-}
-
-func (border *Border) Draw(screen mauview.Screen) {
- width, height := screen.Size()
- if width == 1 {
- for borderY := 0; borderY < height; borderY++ {
- screen.SetContent(0, borderY, mauview.Borders.Vertical, nil, border.Style)
- }
- } else if height == 1 {
- for borderX := 0; borderX < width; borderX++ {
- screen.SetContent(borderX, 0, mauview.Borders.Horizontal, nil, border.Style)
- }
- }
-}
-
-func (border *Border) OnKeyEvent(event mauview.KeyEvent) bool {
- return false
-}
-
-func (border *Border) OnPasteEvent(event mauview.PasteEvent) bool {
- return false
-}
-
-func (border *Border) OnMouseEvent(event mauview.MouseEvent) bool {
- return false
-}
diff --git a/ui/widget/color.go b/ui/widget/color.go
deleted file mode 100644
index 398e43f..0000000
--- a/ui/widget/color.go
+++ /dev/null
@@ -1,224 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package widget
-
-import (
- "fmt"
- "hash/fnv"
-
- "go.mau.fi/tcell"
-
- "maunium.net/go/mautrix/id"
-)
-
-var colorNames = []string{
- "maroon",
- "green",
- "olive",
- "navy",
- "purple",
- "teal",
- "silver",
- "gray",
- "red",
- "lime",
- "yellow",
- "blue",
- "fuchsia",
- "aqua",
- "white",
- "aliceblue",
- "antiquewhite",
- "aquamarine",
- "azure",
- "beige",
- "bisque",
- "blanchedalmond",
- "blueviolet",
- "brown",
- "burlywood",
- "cadetblue",
- "chartreuse",
- "chocolate",
- "coral",
- "cornflowerblue",
- "cornsilk",
- "crimson",
- "darkblue",
- "darkcyan",
- "darkgoldenrod",
- "darkgray",
- "darkgreen",
- "darkkhaki",
- "darkmagenta",
- "darkolivegreen",
- "darkorange",
- "darkorchid",
- "darkred",
- "darksalmon",
- "darkseagreen",
- "darkslateblue",
- "darkslategray",
- "darkturquoise",
- "darkviolet",
- "deeppink",
- "deepskyblue",
- "dimgray",
- "dodgerblue",
- "firebrick",
- "floralwhite",
- "forestgreen",
- "gainsboro",
- "ghostwhite",
- "gold",
- "goldenrod",
- "greenyellow",
- "honeydew",
- "hotpink",
- "indianred",
- "indigo",
- "ivory",
- "khaki",
- "lavender",
- "lavenderblush",
- "lawngreen",
- "lemonchiffon",
- "lightblue",
- "lightcoral",
- "lightcyan",
- "lightgoldenrodyellow",
- "lightgray",
- "lightgreen",
- "lightpink",
- "lightsalmon",
- "lightseagreen",
- "lightskyblue",
- "lightslategray",
- "lightsteelblue",
- "lightyellow",
- "limegreen",
- "linen",
- "mediumaquamarine",
- "mediumblue",
- "mediumorchid",
- "mediumpurple",
- "mediumseagreen",
- "mediumslateblue",
- "mediumspringgreen",
- "mediumturquoise",
- "mediumvioletred",
- "midnightblue",
- "mintcream",
- "mistyrose",
- "moccasin",
- "navajowhite",
- "oldlace",
- "olivedrab",
- "orange",
- "orangered",
- "orchid",
- "palegoldenrod",
- "palegreen",
- "paleturquoise",
- "palevioletred",
- "papayawhip",
- "peachpuff",
- "peru",
- "pink",
- "plum",
- "powderblue",
- "rebeccapurple",
- "rosybrown",
- "royalblue",
- "saddlebrown",
- "salmon",
- "sandybrown",
- "seagreen",
- "seashell",
- "sienna",
- "skyblue",
- "slateblue",
- "slategray",
- "snow",
- "springgreen",
- "steelblue",
- "tan",
- "thistle",
- "tomato",
- "turquoise",
- "violet",
- "wheat",
- "whitesmoke",
- "yellowgreen",
- "grey",
- "dimgrey",
- "darkgrey",
- "darkslategrey",
- "lightgrey",
- "lightslategrey",
- "slategrey",
-}
-
-// GetHashColorName gets a color name for the given string based on its FNV-1 hash.
-//
-// The array of possible color names are the alphabetically ordered color
-// names specified in tcell.ColorNames.
-//
-// The algorithm to get the color is as follows:
-//
-// colorNames[ FNV1(string) % len(colorNames) ]
-//
-// With the exception of the three special cases:
-//
-// --> = green
-// <-- = red
-// --- = yellow
-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[h.Sum32()%uint32(len(colorNames))]
- }
-}
-
-// GetHashColor gets the tcell Color value for the given string.
-//
-// GetHashColor calls GetHashColorName() and gets the Color value from the tcell.ColorNames map.
-func GetHashColor(val interface{}) tcell.Color {
- switch str := val.(type) {
- case string:
- return tcell.ColorNames[GetHashColorName(str)]
- case *string:
- return tcell.ColorNames[GetHashColorName(*str)]
- case id.UserID:
- return tcell.ColorNames[GetHashColorName(string(str))]
- default:
- return tcell.ColorNames["red"]
- }
-}
-
-// AddColor adds tview color tags to the given string.
-func AddColor(s, color string) string {
- return fmt.Sprintf("[%s]%s[white]", color, s)
-}
diff --git a/ui/widget/doc.go b/ui/widget/doc.go
deleted file mode 100644
index 03d2060..0000000
--- a/ui/widget/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package widget contains additional tview widgets.
-package widget
diff --git a/ui/widget/util.go b/ui/widget/util.go
deleted file mode 100644
index e26a23d..0000000
--- a/ui/widget/util.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2020 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package widget
-
-import (
- "fmt"
- "strconv"
-
- "github.com/mattn/go-runewidth"
-
- "go.mau.fi/mauview"
- "go.mau.fi/tcell"
-)
-
-func WriteLineSimple(screen mauview.Screen, line string, x, y int) {
- WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
-}
-
-func WriteLineSimpleColor(screen mauview.Screen, line string, x, y int, color tcell.Color) {
- WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
-}
-
-func WriteLineColor(screen mauview.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
- WriteLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
-}
-
-func WriteLine(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
- offsetX := 0
- if align == mauview.AlignRight {
- offsetX = maxWidth - runewidth.StringWidth(line)
- }
- if offsetX < 0 {
- offsetX = 0
- }
- for _, ch := range line {
- chWidth := runewidth.RuneWidth(ch)
- if chWidth == 0 {
- continue
- }
-
- for localOffset := 0; localOffset < chWidth; localOffset++ {
- screen.SetContent(x+offsetX+localOffset, y, ch, nil, style)
- }
- offsetX += chWidth
- if offsetX >= maxWidth {
- break
- }
- }
-}
-
-func WriteLinePadded(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
- padding := strconv.Itoa(maxWidth)
- if align == mauview.AlignRight {
- line = fmt.Sprintf("%"+padding+"s", line)
- } else {
- line = fmt.Sprintf("%-"+padding+"s", line)
- }
- WriteLine(screen, mauview.AlignLeft, line, x, y, maxWidth, style)
-}