You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1539 lines
41 KiB

package config
import (
"embed"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
"github.com/RasmusLindroth/tut/util"
"github.com/gdamore/tcell/v2"
"github.com/gobwas/glob"
"github.com/pelletier/go-toml/v2"
)
//go:embed toot.tmpl
var tootTemplate string
//go:embed user.tmpl
var userTemplate string
//go:embed help.tmpl
var helpTemplate string
//go:embed themes/*
var themesFS embed.FS
type Config struct {
General General
Style Style
Media Media
OpenPattern OpenPattern
OpenCustom OpenCustom
NotificationConfig Notification
Templates Templates
Input Input
}
type LeaderAction struct {
Command LeaderCommand
Subaction string
Shortcut string
}
type LeaderCommand uint
const (
LeaderNone LeaderCommand = iota
LeaderClearNotifications
LeaderCompose
LeaderEdit
LeaderBlocking
LeaderFavorited
LeaderBoosts
LeaderFavorites
LeaderFollowing
LeaderFollowers
LeaderTags
LeaderListPlacement
LeaderListSplit
LeaderMuting
LeaderPreferences
LeaderProfile
LeaderProportions
LeaderMentions
LeaderRefetch
LeaderStickToTop
LeaderHistory
LeaderLoadNewer
LeaderPane
LeaderClosePane
LeaderMovePaneLeft
LeaderMovePaneRight
LeaderMovePaneHome
LeaderMovePaneEnd
)
type FeedType uint
const (
Favorites FeedType = iota
Favorited
Boosts
Followers
Following
FollowRequests
Blocking
Muting
History
InvalidFeed
Notifications
Saved
Tag
Tags
Thread
TimelineFederated
TimelineHome
TimelineHomeSpecial
TimelineLocal
Mentions
Conversations
User
UserList
Lists
List
ListUsersIn
ListUsersAdd
)
type NotificationToHide string
const (
HideMention NotificationToHide = "mention"
HideStatus NotificationToHide = "status"
HideBoost NotificationToHide = "reblog"
HideFollow NotificationToHide = "follow"
HideFollowRequest NotificationToHide = "follow_request"
HideFavorite NotificationToHide = "favourite"
HidePoll NotificationToHide = "poll"
HideEdited NotificationToHide = "update"
)
var timelineID uint = 0
var timelineIDMux sync.Mutex
func newTimelineID() uint {
timelineIDMux.Lock()
defer timelineIDMux.Unlock()
timelineID = timelineID + 1
return timelineID
}
func NewTimeline(tl Timeline) *Timeline {
tl.ID = newTimelineID()
return &tl
}
type OnTimelineFocus uint
const (
TimelineFocusPane OnTimelineFocus = iota
TimelineFocusTimeline
)
type OnTimelineCreationClosed uint
const (
TimelineCreationClosedNewPane OnTimelineCreationClosed = iota
TimelineCreationClosedCurrentPane
)
type Timeline struct {
ID uint
FeedType FeedType
Subaction string
Name string
Key Key
Shortcut string
HideBoosts bool
HideReplies bool
Closed bool
OnFocus OnTimelineFocus
OnCreationClosed OnTimelineCreationClosed
}
type General struct {
Editor string
UseInternalEditor bool
Confirmation bool
MouseSupport bool
DateTodayFormat string
DateFormat string
DateRelative int
MaxWidth int
QuoteReply bool
ShortHints bool
ShowFilterPhrase bool
ListPlacement ListPlacement
ListSplit ListSplit
ListProportion int
ContentProportion int
TerminalTitle int
ShowIcons bool
ShowHelp bool
RedrawUI bool
LeaderKey rune
LeaderTimeout int64
LeaderActions []LeaderAction
Timelines []*Timeline
StickToTop bool
NotificationsToHide []NotificationToHide
ShowBoostedUser bool
DynamicTimelineName bool
CommandsInNewPane bool
}
type Style struct {
Theme string
Background tcell.Color
Text tcell.Color
Subtle tcell.Color
WarningText tcell.Color
TextSpecial1 tcell.Color
TextSpecial2 tcell.Color
TopBarBackground tcell.Color
TopBarText tcell.Color
StatusBarBackground tcell.Color
StatusBarText tcell.Color
StatusBarViewBackground tcell.Color
StatusBarViewText tcell.Color
ListSelectedBackground tcell.Color
ListSelectedText tcell.Color
ListSelectedInactiveBackground tcell.Color
ListSelectedInactiveText tcell.Color
ControlsText tcell.Color
ControlsHighlight tcell.Color
AutocompleteBackground tcell.Color
AutocompleteText tcell.Color
AutocompleteSelectedBackground tcell.Color
AutocompleteSelectedText tcell.Color
ButtonColorOne tcell.Color
ButtonColorTwo tcell.Color
TimelineNameBackground tcell.Color
TimelineNameText tcell.Color
IconColor tcell.Color
CommandText tcell.Color
}
type Media struct {
DeleteTmpFiles bool
ImageViewer string
ImageArgs []string
ImageTerminal bool
ImageSingle bool
ImageReverse bool
VideoViewer string
VideoArgs []string
VideoTerminal bool
VideoSingle bool
VideoReverse bool
AudioViewer string
AudioArgs []string
AudioTerminal bool
AudioSingle bool
AudioReverse bool
LinkViewer string
LinkArgs []string
LinkTerminal bool
}
type Pattern struct {
Compiled glob.Glob
Program string
Args []string
Terminal bool
}
type OpenPattern struct {
Patterns []Pattern
}
type Custom struct {
Index int
Name string
Program string
Args []string
Terminal bool
Key Key
}
type OpenCustom struct {
OpenCustoms []Custom
}
type ListPlacement uint
const (
ListPlacementTop ListPlacement = iota
ListPlacementBottom
ListPlacementLeft
ListPlacementRight
)
type ListSplit uint
const (
ListRow ListSplit = iota
ListColumn
)
type NotificationType uint
const (
NotificationFollower NotificationType = iota
NotificationFavorite
NotificationMention
NotificationUpdate
NotificationBoost
NotificationPoll
NotificationPost
)
type Notification struct {
NotificationFollower bool
NotificationFavorite bool
NotificationMention bool
NotificationUpdate bool
NotificationBoost bool
NotificationPoll bool
NotificationPost bool
}
type Templates struct {
Toot *template.Template
User *template.Template
Help *template.Template
}
func NilDefaultBool(x *bool, def *bool) bool {
if x == nil {
return *def
}
return *x
}
func NilDefaultString(x *string, def *string) string {
if x == nil {
return *def
}
return *x
}
func NilDefaultInt(x *int, def *int) int {
if x == nil {
return *def
}
return *x
}
func NilDefaultInt64(x *int64, def *int64) int64 {
if x == nil {
return *def
}
return *x
}
var keyMatch = regexp.MustCompile(`^(.*?)\[(.*?)\](.*?)$`)
func newHint(s string) []string {
matches := keyMatch.FindAllStringSubmatch(s, -1)
if len(matches) == 0 {
return []string{"", "", ""}
}
if len(matches[0]) != 4 {
return []string{"", "", ""}
}
return []string{matches[0][1], matches[0][2], matches[0][3]}
}
func NewKey(hint string, hintAlt string, keys []string, special []string) (Key, error) {
k := Key{}
if len(hint) > 0 && len(hintAlt) > 0 {
k.Hint = [][]string{newHint(hint), newHint(hintAlt)}
} else if len(hint) > 0 {
k.Hint = [][]string{newHint(hint), newHint(hintAlt)}
}
var runes []rune
var keysTcell []tcell.Key
for _, r := range keys {
if len(r) > 1 {
return k, fmt.Errorf("key %s can only be one char", r)
}
if len(r) == 0 {
continue
}
runes = append(runes, rune(r[0]))
}
for _, s := range special {
found := false
var fk tcell.Key
for tk, tv := range tcell.KeyNames {
if tv == s {
found = true
fk = tk
break
}
}
if found {
keysTcell = append(keysTcell, fk)
} else {
return k, fmt.Errorf("no key named %s", s)
}
}
k.Runes = runes
k.Keys = keysTcell
return k, nil
}
type Key struct {
Hint [][]string
Runes []rune
Keys []tcell.Key
}
func (k Key) Match(kb tcell.Key, rb rune) bool {
for _, ka := range k.Keys {
if ka == kb {
return true
}
}
for _, ra := range k.Runes {
if ra == rb {
return true
}
}
return false
}
type Input struct {
GlobalDown Key
GlobalUp Key
GlobalEnter Key
GlobalBack Key
GlobalExit Key
MainHome Key
MainEnd Key
MainPrevFeed Key
MainNextFeed Key
MainPrevPane Key
MainNextPane Key
MainCompose Key
MainNextAccount Key
MainPrevAccount Key
StatusAvatar Key
StatusBoost Key
StatusDelete Key
StatusEdit Key
StatusFavorite Key
StatusMedia Key
StatusLinks Key
StatusPoll Key
StatusReply Key
StatusBookmark Key
StatusThread Key
StatusUser Key
StatusViewFocus Key
StatusYank Key
StatusToggleCW Key
StatusShowFiltered Key
UserAvatar Key
UserBlock Key
UserFollow Key
UserFollowRequestDecide Key
UserMute Key
UserLinks Key
UserUser Key
UserViewFocus Key
UserYank Key
ListOpenFeed Key
ListUserList Key
ListUserAdd Key
ListUserDelete Key
TagOpenFeed Key
TagFollow Key
LinkOpen Key
LinkYank Key
ComposeEditCW Key
ComposeEditText Key
ComposeIncludeQuote Key
ComposeMediaFocus Key
ComposePost Key
ComposeToggleContentWarning Key
ComposeVisibility Key
ComposeLanguage Key
ComposePoll Key
MediaDelete Key
MediaEditDesc Key
MediaAdd Key
VoteVote Key
VoteSelect Key
PollAdd Key
PollEdit Key
PollDelete Key
PollMultiToggle Key
PollExpiration Key
PreferenceName Key
PreferenceVisibility Key
PreferenceBio Key
PreferenceSave Key
PreferenceFields Key
PreferenceFieldsAdd Key
PreferenceFieldsEdit Key
PreferenceFieldsDelete Key
EditorExit Key
}
func parseColor(input string, def string, xrdb map[string]string) tcell.Color {
if input == "" {
return tcell.GetColor(def)
}
if strings.HasPrefix(input, "xrdb:") {
key := strings.TrimPrefix(input, "xrdb:")
if c, ok := xrdb[key]; ok {
return tcell.GetColor(c)
} else {
return tcell.GetColor(def)
}
}
return tcell.GetColor(input)
}
func parseTheme(cfg StyleTOML, xrdbColors map[string]string) Style {
var style Style
def := ConfigDefault.Style
s := NilDefaultString(cfg.Background, def.Background)
style.Background = parseColor(s, "#27822", xrdbColors)
s = NilDefaultString(cfg.Text, def.Text)
style.Text = parseColor(s, "#f8f8f2", xrdbColors)
s = NilDefaultString(cfg.Subtle, def.Subtle)
style.Subtle = parseColor(s, "#808080", xrdbColors)
s = NilDefaultString(cfg.WarningText, def.WarningText)
style.WarningText = parseColor(s, "#f92672", xrdbColors)
s = NilDefaultString(cfg.TextSpecial1, def.TextSpecial1)
style.TextSpecial1 = parseColor(s, "#ae81ff", xrdbColors)
s = NilDefaultString(cfg.TextSpecial2, def.TextSpecial2)
style.TextSpecial2 = parseColor(s, "#a6e22e", xrdbColors)
s = NilDefaultString(cfg.TopBarBackground, def.TopBarBackground)
style.TopBarBackground = parseColor(s, "#f92672", xrdbColors)
s = NilDefaultString(cfg.TopBarText, def.TopBarText)
style.TopBarText = parseColor(s, "#f8f8f2", xrdbColors)
s = NilDefaultString(cfg.StatusBarBackground, def.StatusBarBackground)
style.StatusBarBackground = parseColor(s, "#f92672", xrdbColors)
s = NilDefaultString(cfg.StatusBarText, def.StatusBarText)
style.StatusBarText = parseColor(s, "#f8f8f3", xrdbColors)
s = NilDefaultString(cfg.StatusBarViewBackground, def.StatusBarViewBackground)
style.StatusBarViewBackground = parseColor(s, "#ae81ff", xrdbColors)
s = NilDefaultString(cfg.StatusBarViewText, def.StatusBarViewText)
style.StatusBarViewText = parseColor(s, "#f8f8f2", xrdbColors)
s = NilDefaultString(cfg.ListSelectedBackground, def.ListSelectedBackground)
style.ListSelectedBackground = parseColor(s, "#f92672", xrdbColors)
s = NilDefaultString(cfg.ListSelectedText, def.ListSelectedText)
style.ListSelectedText = parseColor(s, "#f8f8f2", xrdbColors)
s = NilDefaultString(cfg.ListSelectedInactiveBackground, sp(""))
if len(s) > 0 {
style.ListSelectedInactiveBackground = parseColor(s, "#ae81ff", xrdbColors)
} else {
style.ListSelectedInactiveBackground = style.StatusBarViewBackground
}
s = NilDefaultString(cfg.ListSelectedInactiveText, def.ListSelectedInactiveText)
if len(s) > 0 {
style.ListSelectedInactiveText = parseColor(s, "#f8f8f2", xrdbColors)
} else {
style.ListSelectedInactiveText = style.StatusBarViewText
}
s = NilDefaultString(cfg.ControlsText, sp(""))
if len(s) > 0 {
style.ControlsText = parseColor(s, "#f8f8f2", xrdbColors)
} else {
style.ControlsText = style.Text
}
s = NilDefaultString(cfg.ControlsHighlight, sp(""))
if len(s) > 0 {
style.ControlsHighlight = parseColor(s, "#a6e22e", xrdbColors)
} else {
style.ControlsHighlight = style.TextSpecial2
}
s = NilDefaultString(cfg.AutocompleteBackground, sp(""))
if len(s) > 0 {
style.AutocompleteBackground = parseColor(s, "#272822", xrdbColors)
} else {
style.AutocompleteBackground = style.Background
}
s = NilDefaultString(cfg.AutocompleteText, sp(""))
if len(s) > 0 {
style.AutocompleteText = parseColor(s, "#f8f8f2", xrdbColors)
} else {
style.AutocompleteText = style.Text
}
s = NilDefaultString(cfg.AutocompleteSelectedBackground, sp(""))
if len(s) > 0 {
style.AutocompleteSelectedBackground = parseColor(s, "#ae81ff", xrdbColors)
} else {
style.AutocompleteSelectedBackground = style.StatusBarViewBackground
}
s = NilDefaultString(cfg.AutocompleteSelectedText, sp(""))
if len(s) > 0 {
style.AutocompleteSelectedText = parseColor(s, "#f8f8f2", xrdbColors)
} else {
style.AutocompleteSelectedText = style.StatusBarViewText
}
s = NilDefaultString(cfg.ButtonColorOne, sp(""))
if len(s) > 0 {
style.ButtonColorOne = parseColor(s, "#ae81ff", xrdbColors)
} else {
style.ButtonColorOne = style.StatusBarViewBackground
}
s = NilDefaultString(cfg.ButtonColorTwo, sp(""))
if len(s) > 0 {
style.ButtonColorTwo = parseColor(s, "#272822", xrdbColors)
} else {
style.ButtonColorTwo = style.Background
}
s = NilDefaultString(cfg.TimelineNameBackground, sp(""))
if len(s) > 0 {
style.TimelineNameBackground = parseColor(s, "#272822", xrdbColors)
} else {
style.TimelineNameBackground = style.Background
}
s = NilDefaultString(cfg.TimelineNameText, sp(""))
if len(s) > 0 {
style.TimelineNameText = parseColor(s, "#808080", xrdbColors)
} else {
style.TimelineNameText = style.Subtle
}
s = NilDefaultString(cfg.CommandText, sp(""))
if len(s) > 0 {
style.CommandText = parseColor(s, "#f8f8f2", xrdbColors)
} else {
style.CommandText = style.StatusBarText
}
return style
}
func parseStyle(cfg StyleTOML, cnfPath string, cnfDir string) Style {
var xrdbColors map[string]string
xrdbMap, _ := GetXrdbColors()
def := ConfigDefault.Style
prefix := NilDefaultString(cfg.XrdbPrefix, def.XrdbPrefix)
if prefix == "" {
prefix = "guess"
}
if prefix == "guess" {
if m, ok := xrdbMap["*"]; ok {
xrdbColors = m
} else if m, ok := xrdbMap["URxvt"]; ok {
xrdbColors = m
} else if m, ok := xrdbMap["XTerm"]; ok {
xrdbColors = m
}
} else {
if m, ok := xrdbMap[prefix]; ok {
xrdbColors = m
}
}
style := Style{}
theme := NilDefaultString(cfg.Theme, def.Theme)
if theme != "none" && theme != "" {
bundled, local, err := getThemes(cnfPath, cnfDir)
if err != nil {
log.Fatalf("Couldn't load themes. Error: %s\n", err)
}
found := false
isLocal := false
for _, t := range local {
if filepath.Base(t) == fmt.Sprintf("%s.toml", theme) {
found = true
isLocal = true
break
}
}
if !found {
for _, t := range bundled {
if filepath.Base(t) == fmt.Sprintf("%s.toml", theme) {
found = true
break
}
}
}
if !found {
log.Fatalf("Couldn't find theme %s\n", theme)
}
tcfg, err := getTheme(theme, isLocal, cnfDir)
if err != nil {
log.Fatalf("Couldn't load theme. Error: %s\n", err)
}
style = parseTheme(tcfg, xrdbColors)
} else {
style = parseTheme(cfg, xrdbColors)
}
return style
}
func getViewer(v *ViewerTOML, def *ViewerTOML) (program, args string, terminal, single, reverse bool) {
program = *def.Program
args = *def.Args
terminal = *def.Terminal
single = *def.Single
reverse = *def.Reverse
if v == nil {
return
}
if v.Program != nil {
program = *v.Program
}
if v.Args != nil {
args = *v.Args
}
if v.Terminal != nil {
terminal = *v.Terminal
}
if v.Single != nil {
single = *v.Single
}
if v.Reverse != nil {
reverse = *v.Reverse
}
if *v.Program == "TUT_OS_DEFAULT" {
var argsSlice []string
program, argsSlice = util.GetDefaultForOS()
args = strings.Join(argsSlice, " ")
}
return
}
func parseMedia(cfg MediaTOML) Media {
media := Media{}
media.DeleteTmpFiles = NilDefaultBool(cfg.DeleteTmpFiles, ConfigDefault.Media.DeleteTmpFiles)
var program, args string
var terminal, single, reverse bool
program, args, terminal, single, reverse = getViewer(cfg.Image, ConfigDefault.Media.Image)
media.ImageViewer = program
media.ImageArgs = strings.Fields(args)
media.ImageTerminal = terminal
media.ImageSingle = single
media.ImageReverse = reverse
program, args, terminal, single, reverse = getViewer(cfg.Video, ConfigDefault.Media.Video)
media.VideoViewer = program
media.VideoArgs = strings.Fields(args)
media.VideoTerminal = terminal
media.VideoSingle = single
media.VideoReverse = reverse
program, args, terminal, single, reverse = getViewer(cfg.Audio, ConfigDefault.Media.Audio)
media.AudioViewer = program
media.AudioArgs = strings.Fields(args)
media.AudioTerminal = terminal
media.AudioSingle = single
media.AudioReverse = reverse
program, args, terminal, _, _ = getViewer(cfg.Link, ConfigDefault.Media.Link)
media.LinkViewer = program
media.LinkArgs = strings.Fields(args)
media.LinkTerminal = terminal
return media
}
func parseGeneral(cfg GeneralTOML) General {
general := General{}
def := ConfigDefault.General
general.Editor = NilDefaultString(cfg.Editor, def.Editor)
if general.Editor == "TUT_USE_INTERNAL" {
general.UseInternalEditor = true
}
general.Confirmation = NilDefaultBool(cfg.Confirmation, def.Confirmation)
general.MouseSupport = NilDefaultBool(cfg.MouseSupport, def.MouseSupport)
dateFormat := NilDefaultString(cfg.DateFormat, def.DateFormat)
if dateFormat == "" {
dateFormat = "2006-01-02 15:04"
}
general.DateFormat = dateFormat
dateTodayFormat := NilDefaultString(cfg.DateTodayFormat, def.DateTodayFormat)
if dateTodayFormat == "" {
dateTodayFormat = "15:04"
}
general.DateTodayFormat = dateTodayFormat
general.DateRelative = NilDefaultInt(cfg.DateRelative, def.DateRelative)
general.QuoteReply = NilDefaultBool(cfg.QuoteReply, def.QuoteReply)
general.MaxWidth = NilDefaultInt(cfg.MaxWidth, def.MaxWidth)
general.ShortHints = NilDefaultBool(cfg.ShortHints, def.ShortHints)
general.ShowFilterPhrase = NilDefaultBool(cfg.ShowFilterPhrase, def.ShowFilterPhrase)
general.ShowIcons = NilDefaultBool(cfg.ShowIcons, def.ShowIcons)
general.ShowHelp = NilDefaultBool(cfg.ShowHelp, def.ShowHelp)
general.RedrawUI = NilDefaultBool(cfg.RedrawUI, def.RedrawUI)
general.StickToTop = NilDefaultBool(cfg.StickToTop, def.StickToTop)
general.ShowBoostedUser = NilDefaultBool(cfg.ShowBoostedUser, def.ShowBoostedUser)
general.DynamicTimelineName = NilDefaultBool(cfg.DynamicTimelineName, def.DynamicTimelineName)
general.CommandsInNewPane = NilDefaultBool(cfg.CommandsInNewPane, def.CommandsInNewPane)
lp := NilDefaultString(cfg.ListPlacement, def.ListPlacement)
switch lp {
case "left":
general.ListPlacement = ListPlacementLeft
case "right":
general.ListPlacement = ListPlacementRight
case "top":
general.ListPlacement = ListPlacementTop
case "bottom":
general.ListPlacement = ListPlacementBottom
default:
general.ListPlacement = ListPlacementLeft
}
ls := NilDefaultString(cfg.ListSplit, def.ListSplit)
switch ls {
case "row":
general.ListSplit = ListRow
case "column":
general.ListSplit = ListColumn
}
listProp := NilDefaultInt(cfg.ListProportion, def.ListProportion)
if listProp < 1 {
listProp = 1
}
contentProp := NilDefaultInt(cfg.ContentProportion, def.ContentProportion)
if contentProp < 1 {
contentProp = 1
}
general.ListProportion = listProp
general.ContentProportion = contentProp
leaderString := NilDefaultString(cfg.LeaderKey, def.LeaderKey)
leaderRunes := []rune(leaderString)
if len(leaderRunes) > 1 {
leaderRunes = []rune(strings.TrimSpace(leaderString))
}
if len(leaderRunes) > 1 {
fmt.Println("error parsing leader-key. Error: leader-key can only be one char long")
os.Exit(1)
}
if len(leaderRunes) == 1 {
general.LeaderKey = leaderRunes[0]
}
general.LeaderTimeout = NilDefaultInt64(cfg.LeaderTimeout, def.LeaderTimeout)
if general.LeaderKey != rune(0) {
var las []LeaderAction
if cfg.LeaderActions != nil {
lactions := *cfg.LeaderActions
for _, l := range lactions {
la := LeaderAction{}
ltype := NilDefaultString(l.Type, sp(""))
ldata := NilDefaultString(l.Data, sp(""))
lshortcut := NilDefaultString(l.Shortcut, sp(""))
switch ltype {
case "clear-notifications":
la.Command = LeaderClearNotifications
case "compose":
la.Command = LeaderCompose
case "edit":
la.Command = LeaderEdit
case "blocking":
la.Command = LeaderBlocking
case "favorited":
la.Command = LeaderFavorited
case "history":
la.Command = LeaderHistory
case "boosts":
la.Command = LeaderBoosts
case "favorites":
la.Command = LeaderFavorites
case "following":
la.Command = LeaderFollowing
case "followers":
la.Command = LeaderFollowers
case "muting":
la.Command = LeaderMuting
case "preferences":
la.Command = LeaderPreferences
case "profile":
la.Command = LeaderProfile
case "mentions":
la.Command = LeaderMentions
case "stick-to-top":
la.Command = LeaderStickToTop
case "refetch":
la.Command = LeaderRefetch
case "tags":
la.Command = LeaderTags
case "list-placement":
la.Command = LeaderListPlacement
la.Subaction = ldata
case "list-split":
la.Command = LeaderListSplit
la.Subaction = ldata
case "proportions":
la.Command = LeaderProportions
la.Subaction = ldata
case "pane":
la.Command = LeaderPane
la.Subaction = ldata
case "close-pane":
la.Command = LeaderClosePane
case "move-pane-left", "move-pane-up":
la.Command = LeaderMovePaneLeft
case "move-pane-right", "move-pane-down":
la.Command = LeaderMovePaneRight
case "move-pane-home":
la.Command = LeaderMovePaneHome
case "move-pane-end":
la.Command = LeaderMovePaneEnd
case "newer":
la.Command = LeaderLoadNewer
default:
fmt.Printf("leader-action %s is invalid\n", ltype)
os.Exit(1)
}
la.Shortcut = lshortcut
las = append(las, la)
}
}
general.LeaderActions = las
}
var tls []*Timeline
timelines := cfg.Timelines
if cfg.Timelines != nil {
for _, l := range *timelines {
tl := NewTimeline(Timeline{})
if l.Type == nil {
fmt.Println("timelines must have a type")
os.Exit(1)
}
switch *l.Type {
case "home":
tl.FeedType = TimelineHome
case "special":
tl.FeedType = TimelineHomeSpecial
case "direct":
tl.FeedType = Conversations
case "local":
tl.FeedType = TimelineLocal
case "federated":
tl.FeedType = TimelineFederated
case "bookmarks":
tl.FeedType = Saved
case "saved":
tl.FeedType = Saved
case "favorited":
tl.FeedType = Favorited
case "notifications":
tl.FeedType = Notifications
case "mentions":
tl.FeedType = Mentions
case "lists":
tl.FeedType = Lists
case "tag":
tl.FeedType = Tag
tl.Subaction = NilDefaultString(l.Data, sp(""))
default:
fmt.Printf("timeline %s is invalid\n", *l.Type)
os.Exit(1)
}
tl.Name = NilDefaultString(l.Name, sp(""))
tl.HideBoosts = NilDefaultBool(l.HideBoosts, bf)
tl.HideReplies = NilDefaultBool(l.HideReplies, bf)
tl.Closed = NilDefaultBool(l.Closed, bf)
tl.Shortcut = NilDefaultString(l.Shortcut, sp(""))
onFocus := NilDefaultString(l.OnFocus, sp(""))
if onFocus != "" {
switch onFocus {
case "focus-pane":
tl.OnFocus = TimelineFocusPane
case "focus-self":
tl.OnFocus = TimelineFocusTimeline
default:
fmt.Printf("on-focus: %s in timelines is invalid\n", onFocus)
os.Exit(1)
}
}
onCreation := NilDefaultString(l.OnCreationClosed, sp(""))
if onCreation != "" {
switch onCreation {
case "current-pane":
tl.OnCreationClosed = TimelineCreationClosedCurrentPane
case "new-pane":
tl.OnCreationClosed = TimelineCreationClosedNewPane
default:
fmt.Printf("on-creation-closed: %s in timelines is invalid\n", onCreation)
os.Exit(1)
}
}
if l.Keys != nil {
var keys []string
var special []string
if l.Keys != nil {
keys = *l.Keys
}
if l.SpecialKeys != nil {
special = *l.SpecialKeys
}
var err error
tl.Key, err = NewKey("", "", keys, special)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
tls = append(tls, tl)
}
}
shown := 0
for _, tl := range tls {
if !tl.Closed {
shown += 1
}
}
if len(tls) == 0 || shown == 0 {
tls = append(tls,
NewTimeline(Timeline{
FeedType: TimelineHome,
Name: "Home",
}),
)
kn, err := NewKey("", "", []string{"n", "N"}, []string{})
if err != nil {
fmt.Println(err)
os.Exit(1)
}
tls = append(tls,
NewTimeline(Timeline{
FeedType: Notifications,
Name: "[N]otifications",
Key: kn,
}),
)
}
general.Timelines = tls
general.TerminalTitle = NilDefaultInt(cfg.TerminalTitle, def.TerminalTitle)
/*
0 = No terminal title
1 = Show title in terminal and top bar
2 = Only show terminal title, and no top bar
*/
if general.TerminalTitle < 0 || general.TerminalTitle > 3 {
general.TerminalTitle = 0
}
nths := []NotificationToHide{}
nth := cfg.NotificationsToHide
if nth != nil {
for _, n := range *nth {
switch n {
case "mention":
nths = append(nths, HideMention)
case "status":
nths = append(nths, HideStatus)
case "boost":
nths = append(nths, HideBoost)
case "follow":
nths = append(nths, HideFollow)
case "follow_request":
nths = append(nths, HideFollowRequest)
case "favorite":
nths = append(nths, HideFavorite)
case "poll":
nths = append(nths, HidePoll)
case "edit":
nths = append(nths, HideEdited)
default:
log.Fatalf("%s in notifications-to-hide is invalid\n", n)
os.Exit(1)
}
general.NotificationsToHide = nths
}
}
return general
}
func parseOpenPattern(cfg OpenPatternTOML) OpenPattern {
om := OpenPattern{}
if cfg.Patterns == nil {
return om
}
for _, p := range *cfg.Patterns {
pattern := Pattern{
Program: NilDefaultString(p.Program, sp("")),
Terminal: NilDefaultBool(p.Terminal, bf),
}
if p.Args != nil {
pattern.Args = strings.Fields(*p.Args)
}
pg := NilDefaultString(p.Matching, sp(""))
compiled, err := glob.Compile(pg)
if err != nil {
panic(fmt.Sprintf("Couldn't compile pattern %s in config. Error: %v", pg, err))
}
pattern.Compiled = compiled
om.Patterns = append(om.Patterns, pattern)
}
return om
}
func parseCustom(cfg OpenCustomTOML) OpenCustom {
oc := OpenCustom{}
if cfg.Programs == nil {
return oc
}
for _, x := range *cfg.Programs {
keys, special := []string{}, []string{}
if x.Keys != nil {
keys = *x.Keys
}
if x.SpecialKeys != nil {
special = *x.SpecialKeys
}
key, err := NewKey(
NilDefaultString(x.Hint, sp("")),
"", keys, special,
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
use := NilDefaultString(x.Program, sp(""))
terminal := NilDefaultBool(x.Terminal, bf)
if use == "" {
continue
}
args := strings.Fields(NilDefaultString(x.Args, sp("")))
c := Custom{
Program: use,
Args: args,
Terminal: terminal,
Key: key,
}
oc.OpenCustoms = append(oc.OpenCustoms, c)
}
return oc
}
func parseNotifications(cfg NotificationsTOML) Notification {
nc := Notification{}
def := ConfigDefault.NotificationConfig
nc.NotificationFollower = NilDefaultBool(cfg.Followers, def.Followers)
nc.NotificationFavorite = NilDefaultBool(cfg.Favorite, def.Favorite)
nc.NotificationMention = NilDefaultBool(cfg.Mention, def.Mention)
nc.NotificationUpdate = NilDefaultBool(cfg.Update, def.Update)
nc.NotificationBoost = NilDefaultBool(cfg.Boost, def.Followers)
nc.NotificationPoll = NilDefaultBool(cfg.Poll, def.Poll)
nc.NotificationPost = NilDefaultBool(cfg.Posts, def.Posts)
return nc
}
func parseTemplates(cfg ConfigTOML, cnfPath string, cnfDir string) Templates {
var tootTmpl *template.Template
tootTmplPath, exists, err := checkConfig("toot.tmpl", cnfPath, cnfDir)
if err != nil {
log.Fatalf(
fmt.Sprintf("Couldn't access toot.tmpl. Error: %v", err),
)
}
if exists {
tootTmpl, err = template.New("toot.tmpl").Funcs(template.FuncMap{
"Color": ColorMark,
"Flags": TextFlags,
}).ParseFiles(tootTmplPath)
}
if !exists || err != nil {
tootTmpl, err = template.New("toot.tmpl").Funcs(template.FuncMap{
"Color": ColorMark,
"Flags": TextFlags,
}).Parse(tootTemplate)
}
if err != nil {
log.Fatalf("Couldn't parse toot.tmpl. Error: %v", err)
}
var userTmpl *template.Template
userTmplPath, exists, err := checkConfig("user.tmpl", cnfPath, cnfDir)
if err != nil {
log.Fatalf(
fmt.Sprintf("Couldn't access user.tmpl. Error: %v", err),
)
}
if exists {
userTmpl, err = template.New("user.tmpl").Funcs(template.FuncMap{
"Color": ColorMark,
"Flags": TextFlags,
}).ParseFiles(userTmplPath)
}
if !exists || err != nil {
userTmpl, err = template.New("user.tmpl").Funcs(template.FuncMap{
"Color": ColorMark,
"Flags": TextFlags,
}).Parse(userTemplate)
}
if err != nil {
log.Fatalf("Couldn't parse user.tmpl. Error: %v", err)
}
var helpTmpl *template.Template
helpTmpl, err = template.New("help.tmpl").Funcs(template.FuncMap{
"Color": ColorMark,
"Flags": TextFlags,
}).Parse(helpTemplate)
if err != nil {
log.Fatalf("Couldn't parse help.tmpl. Error: %v", err)
}
return Templates{
Toot: tootTmpl,
User: userTmpl,
Help: helpTmpl,
}
}
func inputOrDef(keyName string, user *KeyHintTOML, def *KeyHintTOML, double bool) Key {
values := *def
if user != nil {
values = *user
}
keys, special := []string{}, []string{}
if values.Keys != nil {
keys = *values.Keys
}
if values.SpecialKeys != nil {
special = *values.SpecialKeys
}
key, err := NewKey(
NilDefaultString(values.Hint, sp("")),
NilDefaultString(values.HintAlt, sp("")),
keys, special,
)
if err != nil {
fmt.Printf("error parsing config for key %s. Error: %v\n", keyName, err)
os.Exit(1)
}
return key
}
func parseInput(cfg InputTOML) Input {
def := ConfigDefault.Input
ic := Input{}
ic.GlobalDown = inputOrDef("global-down", cfg.GlobalDown, def.GlobalDown, false)
ic.GlobalUp = inputOrDef("global-up", cfg.GlobalUp, def.GlobalUp, false)
ic.GlobalEnter = inputOrDef("global-enter", cfg.GlobalEnter, def.GlobalEnter, false)
ic.GlobalBack = inputOrDef("global-back", cfg.GlobalBack, def.GlobalBack, false)
ic.GlobalExit = inputOrDef("global-exit", cfg.GlobalExit, def.GlobalExit, false)
ic.MainHome = inputOrDef("main-home", cfg.MainHome, def.MainHome, false)
ic.MainEnd = inputOrDef("main-end", cfg.MainEnd, def.MainEnd, false)
ic.MainPrevFeed = inputOrDef("main-prev-feed", cfg.MainPrevFeed, def.MainPrevFeed, false)
ic.MainNextFeed = inputOrDef("main-next-feed", cfg.MainNextFeed, def.MainNextFeed, false)
ic.MainNextPane = inputOrDef("main-next-pane", cfg.MainNextPane, def.MainNextPane, false)
ic.MainPrevPane = inputOrDef("main-prev-pane", cfg.MainPrevPane, def.MainPrevPane, false)
ic.MainCompose = inputOrDef("main-compose", cfg.MainCompose, def.MainCompose, false)
ic.MainNextAccount = inputOrDef("main-next-account", cfg.MainNextAccount, def.MainNextAccount, false)
ic.MainPrevAccount = inputOrDef("main-prev-account", cfg.MainPrevAccount, def.MainPrevAccount, false)
ic.StatusAvatar = inputOrDef("status-avatar", cfg.StatusAvatar, def.StatusAvatar, false)
ic.StatusBoost = inputOrDef("status-boost", cfg.StatusBoost, def.StatusBoost, true)
ic.StatusDelete = inputOrDef("status-delete", cfg.StatusDelete, def.StatusDelete, false)
ic.StatusEdit = inputOrDef("status-edit", cfg.StatusEdit, def.StatusEdit, false)
ic.StatusFavorite = inputOrDef("status-favorite", cfg.StatusFavorite, def.StatusFavorite, true)
ic.StatusMedia = inputOrDef("status-media", cfg.StatusMedia, def.StatusMedia, false)
ic.StatusLinks = inputOrDef("status-links", cfg.StatusLinks, def.StatusLinks, false)
ic.StatusPoll = inputOrDef("status-poll", cfg.StatusPoll, def.StatusPoll, false)
ic.StatusReply = inputOrDef("status-reply", cfg.StatusReply, def.StatusReply, false)
ic.StatusBookmark = inputOrDef("status-bookmark", cfg.StatusBookmark, def.StatusBookmark, true)
ic.StatusThread = inputOrDef("status-thread", cfg.StatusThread, def.StatusThread, false)
ic.StatusUser = inputOrDef("status-user", cfg.StatusUser, def.StatusUser, false)
ic.StatusViewFocus = inputOrDef("status-view-focus", cfg.StatusViewFocus, def.StatusViewFocus, false)
ic.StatusYank = inputOrDef("status-yank", cfg.StatusYank, def.StatusYank, false)
ic.StatusToggleCW = inputOrDef("status-toggle-cw", cfg.StatusToggleCW, def.StatusToggleCW, false)
ic.StatusShowFiltered = inputOrDef("status-show-filtered", cfg.StatusShowFiltered, def.StatusShowFiltered, false)
ic.UserAvatar = inputOrDef("user-avatar", cfg.UserAvatar, def.UserAvatar, false)
ic.UserBlock = inputOrDef("user-block", cfg.UserBlock, def.UserBlock, true)
ic.UserFollow = inputOrDef("user-follow", cfg.UserFollow, def.UserFollow, true)
ic.UserFollowRequestDecide = inputOrDef("user-follow-request-decide", cfg.UserFollowRequestDecide, def.UserFollowRequestDecide, true)
ic.UserMute = inputOrDef("user-mute", cfg.UserMute, def.UserMute, true)
ic.UserLinks = inputOrDef("user-links", cfg.UserLinks, def.UserLinks, false)
ic.UserUser = inputOrDef("user-user", cfg.UserUser, def.UserUser, false)
ic.UserViewFocus = inputOrDef("user-view-focus", cfg.UserViewFocus, def.UserViewFocus, false)
ic.UserYank = inputOrDef("user-yank", cfg.UserYank, def.UserYank, false)
ic.ListOpenFeed = inputOrDef("list-open-feed", cfg.ListOpenFeed, def.ListOpenFeed, false)
ic.ListUserList = inputOrDef("list-user-list", cfg.ListUserList, def.ListUserList, false)
ic.ListUserAdd = inputOrDef("list-user-add", cfg.ListUserAdd, def.ListUserAdd, false)
ic.ListUserDelete = inputOrDef("list-user-delete", cfg.ListUserDelete, def.ListUserDelete, false)
ic.TagOpenFeed = inputOrDef("tag-open-feed", cfg.TagOpenFeed, def.TagOpenFeed, false)
ic.TagFollow = inputOrDef("tag-follow", cfg.TagFollow, def.TagFollow, true)
ic.LinkOpen = inputOrDef("link-open", cfg.LinkOpen, def.LinkOpen, false)
ic.LinkYank = inputOrDef("link-yank", cfg.LinkYank, def.LinkYank, false)
ic.ComposeEditCW = inputOrDef("compose-edit-cw", cfg.ComposeEditCW, def.ComposeEditCW, false)
ic.ComposeEditText = inputOrDef("compose-edit-text", cfg.ComposeEditText, def.ComposeEditText, false)
ic.ComposeIncludeQuote = inputOrDef("compose-include-quote", cfg.ComposeIncludeQuote, def.ComposeIncludeQuote, false)
ic.ComposeMediaFocus = inputOrDef("compose-media-focus", cfg.ComposeMediaFocus, def.ComposeMediaFocus, false)
ic.ComposePost = inputOrDef("compose-post", cfg.ComposePost, def.ComposePost, false)
ic.ComposeToggleContentWarning = inputOrDef("compose-toggle-content-warning", cfg.ComposeToggleContentWarning, def.ComposeToggleContentWarning, false)
ic.ComposeVisibility = inputOrDef("compose-visibility", cfg.ComposeVisibility, def.ComposeVisibility, false)
ic.ComposeLanguage = inputOrDef("compose-language", cfg.ComposeLanguage, def.ComposeLanguage, false)
ic.ComposePoll = inputOrDef("compose-poll", cfg.ComposePoll, def.ComposePoll, false)
ic.MediaDelete = inputOrDef("media-delete", cfg.MediaDelete, def.MediaDelete, false)
ic.MediaEditDesc = inputOrDef("media-edit-desc", cfg.MediaEditDesc, def.MediaEditDesc, false)
ic.MediaAdd = inputOrDef("media-add", cfg.MediaAdd, def.MediaAdd, false)
ic.VoteVote = inputOrDef("vote-vote", cfg.VoteVote, def.VoteVote, false)
ic.VoteSelect = inputOrDef("vote-select", cfg.VoteSelect, def.VoteSelect, false)
ic.PollAdd = inputOrDef("poll-add", cfg.PollAdd, def.PollAdd, false)
ic.PollEdit = inputOrDef("poll-edit", cfg.PollEdit, def.PollEdit, false)
ic.PollDelete = inputOrDef("poll-delete", cfg.PollDelete, def.PollDelete, false)
ic.PollMultiToggle = inputOrDef("poll-multi-toggle", cfg.PollMultiToggle, def.PollMultiToggle, false)
ic.PollExpiration = inputOrDef("poll-expiration", cfg.PollExpiration, def.PollExpiration, false)
ic.PreferenceName = inputOrDef("preference-name", cfg.PreferenceName, def.PreferenceName, false)
ic.PreferenceVisibility = inputOrDef("preference-visibility", cfg.PreferenceVisibility, def.PreferenceVisibility, false)
ic.PreferenceBio = inputOrDef("preference-bio", cfg.PreferenceBio, def.PreferenceBio, false)
ic.PreferenceSave = inputOrDef("preference-save", cfg.PreferenceSave, def.PreferenceSave, false)
ic.PreferenceFields = inputOrDef("preference-fields", cfg.PreferenceFields, def.PreferenceFields, false)
ic.PreferenceFieldsAdd = inputOrDef("preference-fields-add", cfg.PreferenceFieldsAdd, def.PreferenceFieldsAdd, false)
ic.PreferenceFieldsEdit = inputOrDef("preference-fields-edit", cfg.PreferenceFieldsEdit, def.PreferenceFieldsEdit, false)
ic.PreferenceFieldsDelete = inputOrDef("preference-fields-delete", cfg.PreferenceFieldsDelete, def.PreferenceFieldsDelete, false)
ic.EditorExit = inputOrDef("editor-exit", cfg.EditorExit, def.EditorExit, false)
return ic
}
func parseConfig(filepath string, cnfPath string, cnfDir string) (Config, error) {
conf := Config{}
f, err := os.Open(filepath)
if err != nil {
log.Fatalln(err)
return conf, err
}
var cnf ConfigTOML
d := toml.NewDecoder(f)
err = d.Decode(&cnf)
if err != nil {
fmt.Print("Error while parsing your config:\n")
fmt.Println(err)
fmt.Println("\nThis message can be a bit unclear. If you don't understand the error you can open up an issue and I'll try to help you. Please post your config.toml in the issue.")
fmt.Println("https://github.com/RasmusLindroth/tut/issues")
os.Exit(1)
}
f.Close()
conf.General = parseGeneral(cnf.General)
conf.Media = parseMedia(cnf.Media)
conf.Style = parseStyle(cnf.Style, cnfPath, cnfDir)
conf.OpenPattern = parseOpenPattern(cnf.OpenPattern)
conf.OpenCustom = parseCustom(cnf.OpenCustom)
conf.NotificationConfig = parseNotifications(cnf.NotificationConfig)
conf.Templates = parseTemplates(cnf, cnfPath, cnfDir)
conf.Input = parseInput(cnf.Input)
return conf, nil
}
func createConfigDir() error {
cd, err := util.GetConfigDir()
if err != nil {
log.Fatalf("couldn't find config dir. Err %v", err)
}
path := cd + "/tut"
return os.MkdirAll(path, os.ModePerm)
}
func checkConfig(filename string, cnfPath string, cnfDir string) (path string, exists bool, err error) {
if cnfPath != "" && filename == "config.toml" {
_, err = os.Stat(cnfPath)
if os.IsNotExist(err) {
return cnfPath, false, nil
} else if err != nil {
return cnfPath, true, err
}
return cnfPath, true, err
}
if cnfDir != "" {
p := filepath.Join(cnfDir, filename)
if os.IsNotExist(err) {
return p, false, nil
} else if err != nil {
return p, true, err
}
return p, true, err
}
cd, err := util.GetConfigDir()
if err != nil {
log.Fatalf("couldn't find config dir. Err %v", err)
}
dir := cd + "/tut/"
path = dir + filename
_, err = os.Stat(path)
if os.IsNotExist(err) {
return path, false, nil
} else if err != nil {
return path, true, err
}
return path, true, err
}
func CreateDefaultConfig(filepath string) error {
f, err := os.Create(filepath)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(conftext)
if err != nil {
return err
}
return nil
}
func getThemes(cnfPath string, cnfDir string) (bundled []string, local []string, err error) {
entries, err := themesFS.ReadDir("themes")
if err != nil {
return bundled, local, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
fp := filepath.Join("themes/", entry.Name())
bundled = append(bundled, fp)
}
_, exists, err := checkConfig("themes", cnfPath, cnfDir)
if err != nil {
return bundled, local, err
}
if !exists {
return bundled, local, err
}
var dir string
if cnfDir != "" {
dir = filepath.Join(cnfDir, "themes")
} else {
cd, err := util.GetConfigDir()
if err != nil {
log.Fatalf("couldn't find config dir. Err %v", err)
}
dir = filepath.Join(cd, "/tut/themes")
}
entries, err = os.ReadDir(dir)
if err != nil {
return bundled, local, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
fp := filepath.Join(dir, entry.Name())
local = append(local, fp)
}
return bundled, local, nil
}
func getTheme(fname string, isLocal bool, cnfDir string) (StyleTOML, error) {
var f io.Reader
var err error
if isLocal {
var dir string
if cnfDir != "" {
dir = filepath.Join(cnfDir, "themes")
} else {
cd, err := util.GetConfigDir()
if err != nil {
log.Fatalf("couldn't find config dir. Err %v", err)
}
dir = filepath.Join(cd, "/tut/themes")
}
f, err = os.Open(
filepath.Join(dir, fmt.Sprintf("%s.toml", strings.TrimSpace(fname))),
)
} else {
f, err = themesFS.Open(fmt.Sprintf("themes/%s.toml", strings.TrimSpace(fname)))
}
if err != nil {
return StyleTOML{}, err
}
var style StyleTOML
toml.NewDecoder(f).Decode(&style)
if err != nil {
return style, err
}
switch x := f.(type) {
case *os.File:
x.Close()
}
return style, nil
}