config.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. package config
  2. import (
  3. _ "embed"
  4. "fmt"
  5. "log/slog"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "unicode/utf8"
  10. "github.com/BurntSushi/toml"
  11. "github.com/ayn2op/discordo/internal/consts"
  12. "github.com/diamondburned/arikawa/v3/discord"
  13. )
  14. const fileName = "config.toml"
  15. type (
  16. Timestamps struct {
  17. Enabled bool `toml:"enabled"`
  18. Format string `toml:"format"`
  19. }
  20. DateSeparator struct {
  21. Enabled bool `toml:"enabled"`
  22. Format string `toml:"format"`
  23. Character string `toml:"character"`
  24. }
  25. Notifications struct {
  26. Enabled bool `toml:"enabled"`
  27. Duration int `toml:"duration"`
  28. Sound Sound `toml:"sound"`
  29. }
  30. Sound struct {
  31. Enabled bool `toml:"enabled"`
  32. OnlyOnPing bool `toml:"only_on_ping"`
  33. }
  34. TypingIndicator struct {
  35. Send bool `toml:"send"`
  36. Receive bool `toml:"receive"`
  37. }
  38. Icons struct {
  39. GuildCategory string `toml:"guild_category"`
  40. GuildText string `toml:"guild_text"`
  41. GuildVoice string `toml:"guild_voice"`
  42. GuildStageVoice string `toml:"guild_stage_voice"`
  43. GuildAnnouncementThread string `toml:"guild_announcement_thread"`
  44. GuildPublicThread string `toml:"guild_public_thread"`
  45. GuildPrivateThread string `toml:"guild_private_thread"`
  46. GuildAnnouncement string `toml:"guild_announcement"`
  47. GuildForum string `toml:"guild_forum"`
  48. GuildStore string `toml:"guild_store"`
  49. }
  50. PickerConfig struct {
  51. Width int `toml:"width"`
  52. Height int `toml:"height"`
  53. }
  54. MarkdownConfig struct {
  55. Enabled bool `toml:"enabled"`
  56. Theme string `toml:"theme"`
  57. }
  58. HelpConfig struct {
  59. CompactModifiers bool `toml:"compact_modifiers"`
  60. Padding [2]int `toml:"padding"`
  61. Separator string `toml:"separator"`
  62. }
  63. SidebarMarkersConfig struct {
  64. Expanded string `toml:"expanded"`
  65. Collapsed string `toml:"collapsed"`
  66. Leaf string `toml:"leaf"`
  67. }
  68. SidebarConfig struct {
  69. Markers SidebarMarkersConfig `toml:"markers"`
  70. }
  71. Config struct {
  72. Path string `toml:"-"` // set programmatically, not from TOML
  73. AutoFocus bool `toml:"auto_focus"`
  74. Mouse bool `toml:"mouse"`
  75. Editor string `toml:"editor"`
  76. Status discord.Status `toml:"status"`
  77. HideBlockedUsers bool `toml:"hide_blocked_users"`
  78. ShowAttachmentLinks bool `toml:"show_attachment_links"`
  79. ImageViewer string `toml:"image_viewer"`
  80. ImageViewerArgs []string `toml:"image_viewer_args"`
  81. ImageSaveDir string `toml:"image_save_dir"`
  82. // Use 0 to disable. Maximum practical value is ~255 (uint8).
  83. AutocompleteLimit uint8 `toml:"autocomplete_limit"`
  84. // Range: 1-100 (clamped by the Discord API). Use 0 to fall back to the default.
  85. MessagesLimit uint8 `toml:"messages_limit"`
  86. Markdown MarkdownConfig `toml:"markdown"`
  87. Help HelpConfig `toml:"help"`
  88. Picker PickerConfig `toml:"picker"`
  89. Timestamps Timestamps `toml:"timestamps"`
  90. DateSeparator DateSeparator `toml:"date_separator"`
  91. Notifications Notifications `toml:"notifications"`
  92. TypingIndicator TypingIndicator `toml:"typing_indicator"`
  93. Sidebar SidebarConfig `toml:"sidebar"`
  94. Icons Icons `toml:"icons"`
  95. Keybinds Keybinds `toml:"keybinds"`
  96. Theme Theme `toml:"theme"`
  97. }
  98. )
  99. //go:embed config.toml
  100. var defaultCfg []byte
  101. func DefaultPath() string {
  102. path, err := os.UserConfigDir()
  103. if err != nil {
  104. slog.Info(
  105. "user config dir cannot be determined; falling back to the current dir",
  106. "err", err,
  107. )
  108. path = "."
  109. }
  110. return filepath.Join(path, consts.Name, fileName)
  111. }
  112. // Load reads the configuration file and parses it.
  113. func Load(path string) (*Config, error) {
  114. cfg := Config{
  115. Keybinds: defaultKeybinds(),
  116. }
  117. if err := toml.Unmarshal(defaultCfg, &cfg); err != nil {
  118. return nil, fmt.Errorf("failed to unmarshal default config: %w", err)
  119. }
  120. file, err := os.Open(path)
  121. if os.IsNotExist(err) {
  122. slog.Info(
  123. "config file does not exist, falling back to the default config",
  124. "path",
  125. path,
  126. "err",
  127. err,
  128. )
  129. } else {
  130. if err != nil {
  131. return nil, fmt.Errorf("failed to open config file: %w", err)
  132. }
  133. defer file.Close()
  134. if _, err := toml.NewDecoder(file).Decode(&cfg); err != nil {
  135. return nil, fmt.Errorf("failed to decode config: %w", err)
  136. }
  137. }
  138. cfg.Path = path
  139. applyDefaults(&cfg)
  140. return &cfg, nil
  141. }
  142. func applyDefaults(cfg *Config) {
  143. if cfg.Editor == "default" {
  144. if editor := os.Getenv("EDITOR"); editor != "" {
  145. cfg.Editor = editor
  146. } else {
  147. cfg.Editor = "vim"
  148. }
  149. }
  150. if cfg.Status == "default" {
  151. cfg.Status = discord.UnknownStatus
  152. }
  153. if cfg.ImageViewer == "default" {
  154. cfg.ImageViewer = ""
  155. }
  156. if strings.HasPrefix(cfg.ImageSaveDir, "~/") {
  157. if home, err := os.UserHomeDir(); err == nil {
  158. cfg.ImageSaveDir = filepath.Join(home, cfg.ImageSaveDir[2:])
  159. }
  160. }
  161. if cfg.DateSeparator.Format == "" {
  162. cfg.DateSeparator.Format = "January 2, 2006"
  163. }
  164. if cfg.DateSeparator.Character == "" {
  165. cfg.DateSeparator.Character = "─"
  166. return
  167. }
  168. r, _ := utf8.DecodeRuneInString(cfg.DateSeparator.Character)
  169. if r == utf8.RuneError {
  170. cfg.DateSeparator.Character = "─"
  171. return
  172. }
  173. cfg.DateSeparator.Character = string(r)
  174. }