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.
1274 lines
39 KiB
1274 lines
39 KiB
package config |
|
|
|
import ( |
|
"embed" |
|
"errors" |
|
"fmt" |
|
"io/ioutil" |
|
"log" |
|
"os" |
|
"path/filepath" |
|
"regexp" |
|
"strings" |
|
"text/template" |
|
|
|
"github.com/RasmusLindroth/tut/feed" |
|
"github.com/gdamore/tcell/v2" |
|
"github.com/gobwas/glob" |
|
"gopkg.in/ini.v1" |
|
) |
|
|
|
//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 |
|
LeaderHome |
|
LeaderDirect |
|
LeaderLocal |
|
LeaderFederated |
|
LeaderCompose |
|
LeaderBlocking |
|
LeaderBookmarks |
|
LeaderSaved |
|
LeaderFavorited |
|
LeaderBoosts |
|
LeaderFavorites |
|
LeaderFollowing |
|
LeaderFollowers |
|
LeaderListPlacement |
|
LeaderListSplit |
|
LeaderMuting |
|
LeaderPreferences |
|
LeaderProfile |
|
LeaderProportions |
|
LeaderNotifications |
|
LeaderLists |
|
LeaderTag |
|
LeaderUser |
|
LeaderWindow |
|
) |
|
|
|
type Timeline struct { |
|
FeedType feed.FeedType |
|
Subaction string |
|
Name string |
|
Key Key |
|
} |
|
|
|
type General struct { |
|
Confirmation bool |
|
DateTodayFormat string |
|
DateFormat string |
|
DateRelative int |
|
MaxWidth int |
|
StartTimeline feed.FeedType |
|
NotificationFeed bool |
|
QuoteReply bool |
|
CharLimit int |
|
ShortHints bool |
|
ShowFilterPhrase bool |
|
ListPlacement ListPlacement |
|
ListSplit ListSplit |
|
ListProportion int |
|
ContentProportion int |
|
ShowIcons bool |
|
ShowHelp bool |
|
RedrawUI bool |
|
LeaderKey rune |
|
LeaderTimeout int64 |
|
LeaderActions []LeaderAction |
|
TimelineName bool |
|
Timelines []Timeline |
|
} |
|
|
|
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 |
|
} |
|
|
|
type Media struct { |
|
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 { |
|
Pattern string |
|
Open string |
|
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 |
|
} |
|
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 |
|
NotificationBoost |
|
NotificationPoll |
|
NotificationPost |
|
) |
|
|
|
type Notification struct { |
|
NotificationFollower bool |
|
NotificationFavorite bool |
|
NotificationMention bool |
|
NotificationBoost bool |
|
NotificationPoll bool |
|
NotificationPost bool |
|
} |
|
|
|
type Templates struct { |
|
Toot *template.Template |
|
User *template.Template |
|
Help *template.Template |
|
} |
|
|
|
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(s []string, double bool) (Key, error) { |
|
var k Key |
|
if len(s) < 2 { |
|
return k, errors.New("key must have a minimum length of 2") |
|
} |
|
var start int |
|
if double { |
|
start = 1 |
|
k = Key{ |
|
Hint: [][]string{newHint(s[0]), newHint(s[1])}, |
|
} |
|
} else { |
|
start = 0 |
|
k = Key{ |
|
Hint: [][]string{newHint(s[0])}, |
|
} |
|
} |
|
var runes []rune |
|
var keys []tcell.Key |
|
for _, v := range s[start+1:] { |
|
value := []rune(strings.TrimSpace(v)) |
|
if len(value) < 3 { |
|
return k, errors.New("key value must have a minimum length of 3") |
|
} |
|
if value[0] == '\'' { |
|
if len(value) != 3 { |
|
return k, fmt.Errorf("rune %s must only contain one char", string(value)) |
|
} |
|
runes = append(runes, value[1]) |
|
} else if value[0] == '"' { |
|
if value[len(value)-1] != '"' { |
|
return k, fmt.Errorf("key %s must end with \"", string(value)) |
|
} |
|
keyName := string(value[1 : len(value)-1]) |
|
found := false |
|
var fk tcell.Key |
|
for tk, tv := range tcell.KeyNames { |
|
if tv == keyName { |
|
found = true |
|
fk = tk |
|
break |
|
} |
|
} |
|
if found { |
|
keys = append(keys, fk) |
|
} else { |
|
return k, fmt.Errorf("no key named %s", keyName) |
|
} |
|
} else { |
|
return k, fmt.Errorf("input %s is in the wrong format", string(value)) |
|
} |
|
} |
|
k.Runes = runes |
|
k.Keys = keys |
|
|
|
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 |
|
MainPrevWindow Key |
|
MainNextWindow Key |
|
MainCompose Key |
|
|
|
StatusAvatar Key |
|
StatusBoost Key |
|
StatusDelete Key |
|
StatusFavorite Key |
|
StatusMedia Key |
|
StatusLinks Key |
|
StatusPoll Key |
|
StatusReply Key |
|
StatusBookmark Key |
|
StatusThread Key |
|
StatusUser Key |
|
StatusViewFocus Key |
|
StatusYank Key |
|
StatusToggleSpoiler Key |
|
|
|
UserAvatar Key |
|
UserBlock Key |
|
UserFollow Key |
|
UserFollowRequestDecide Key |
|
UserMute Key |
|
UserLinks Key |
|
UserUser Key |
|
UserViewFocus Key |
|
UserYank Key |
|
|
|
ListOpenFeed Key |
|
|
|
LinkOpen Key |
|
LinkYank Key |
|
|
|
ComposeEditSpoiler Key |
|
ComposeEditText Key |
|
ComposeIncludeQuote Key |
|
ComposeMediaFocus Key |
|
ComposePost Key |
|
ComposeToggleContentWarning Key |
|
ComposeVisibility 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 |
|
} |
|
|
|
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 parseStyle(cfg *ini.File) Style { |
|
var xrdbColors map[string]string |
|
xrdbMap, _ := GetXrdbColors() |
|
prefix := cfg.Section("style").Key("xrdb-prefix").String() |
|
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 := cfg.Section("style").Key("theme").String() |
|
if theme != "none" && theme != "" { |
|
themes, err := getThemes() |
|
if err != nil { |
|
log.Fatalf("Couldn't load themes. Error: %s\n", err) |
|
} |
|
found := false |
|
for _, t := range themes { |
|
if filepath.Base(t) == fmt.Sprintf("%s.ini", theme) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
log.Fatalf("Couldn't find theme %s\n", theme) |
|
} |
|
tcfg, err := getTheme(theme) |
|
if err != nil { |
|
log.Fatalf("Couldn't load theme. Error: %s\n", err) |
|
} |
|
bg := tcfg.Section("").Key("background").String() |
|
style.Background = parseColor(bg, "default", xrdbColors) |
|
|
|
text := tcfg.Section("").Key("text").String() |
|
style.Text = tcell.GetColor(text) |
|
|
|
subtle := tcfg.Section("").Key("subtle").String() |
|
style.Subtle = tcell.GetColor(subtle) |
|
|
|
warningText := tcfg.Section("").Key("warning-text").String() |
|
style.WarningText = tcell.GetColor(warningText) |
|
|
|
textSpecial1 := tcfg.Section("").Key("text-special-one").String() |
|
style.TextSpecial1 = tcell.GetColor(textSpecial1) |
|
|
|
textSpecial2 := tcfg.Section("").Key("text-special-two").String() |
|
style.TextSpecial2 = tcell.GetColor(textSpecial2) |
|
|
|
topBarBackround := tcfg.Section("").Key("top-bar-background").String() |
|
style.TopBarBackground = tcell.GetColor(topBarBackround) |
|
|
|
topBarText := tcfg.Section("").Key("top-bar-text").String() |
|
style.TopBarText = tcell.GetColor(topBarText) |
|
|
|
statusBarBackround := tcfg.Section("").Key("status-bar-background").String() |
|
style.StatusBarBackground = tcell.GetColor(statusBarBackround) |
|
|
|
statusBarText := tcfg.Section("").Key("status-bar-text").String() |
|
style.StatusBarText = tcell.GetColor(statusBarText) |
|
|
|
statusBarViewBackround := tcfg.Section("").Key("status-bar-view-background").String() |
|
style.StatusBarViewBackground = tcell.GetColor(statusBarViewBackround) |
|
|
|
statusBarViewText := tcfg.Section("").Key("status-bar-view-text").String() |
|
style.StatusBarViewText = tcell.GetColor(statusBarViewText) |
|
|
|
listSelectedBackground := tcfg.Section("").Key("list-selected-background").String() |
|
style.ListSelectedBackground = tcell.GetColor(listSelectedBackground) |
|
|
|
listSelectedText := tcfg.Section("").Key("list-selected-text").String() |
|
style.ListSelectedText = tcell.GetColor(listSelectedText) |
|
} else { |
|
bg := cfg.Section("style").Key("background").String() |
|
style.Background = parseColor(bg, "default", xrdbColors) |
|
|
|
text := cfg.Section("style").Key("text").String() |
|
style.Text = parseColor(text, "white", xrdbColors) |
|
|
|
subtle := cfg.Section("style").Key("subtle").String() |
|
style.Subtle = parseColor(subtle, "gray", xrdbColors) |
|
|
|
warningText := cfg.Section("style").Key("warning-text").String() |
|
style.WarningText = parseColor(warningText, "#f92672", xrdbColors) |
|
|
|
textSpecial1 := cfg.Section("style").Key("text-special-one").String() |
|
style.TextSpecial1 = parseColor(textSpecial1, "#ae81ff", xrdbColors) |
|
|
|
textSpecial2 := cfg.Section("style").Key("text-special-two").String() |
|
style.TextSpecial2 = parseColor(textSpecial2, "#a6e22e", xrdbColors) |
|
|
|
topBarBackround := cfg.Section("style").Key("top-bar-background").String() |
|
style.TopBarBackground = parseColor(topBarBackround, "#f92672", xrdbColors) |
|
|
|
topBarText := cfg.Section("style").Key("top-bar-text").String() |
|
style.TopBarText = parseColor(topBarText, "white", xrdbColors) |
|
|
|
statusBarBackround := cfg.Section("style").Key("status-bar-background").String() |
|
style.StatusBarBackground = parseColor(statusBarBackround, "#f92672", xrdbColors) |
|
|
|
statusBarText := cfg.Section("style").Key("status-bar-text").String() |
|
style.StatusBarText = parseColor(statusBarText, "white", xrdbColors) |
|
|
|
statusBarViewBackround := cfg.Section("style").Key("status-bar-view-background").String() |
|
style.StatusBarViewBackground = parseColor(statusBarViewBackround, "#ae81ff", xrdbColors) |
|
|
|
statusBarViewText := cfg.Section("style").Key("status-bar-view-text").String() |
|
style.StatusBarViewText = parseColor(statusBarViewText, "white", xrdbColors) |
|
|
|
listSelectedBackground := cfg.Section("style").Key("list-selected-background").String() |
|
style.ListSelectedBackground = parseColor(listSelectedBackground, "#f92672", xrdbColors) |
|
|
|
listSelectedText := cfg.Section("style").Key("list-selected-text").String() |
|
style.ListSelectedText = parseColor(listSelectedText, "white", xrdbColors) |
|
} |
|
|
|
return style |
|
} |
|
|
|
func parseGeneral(cfg *ini.File) General { |
|
general := General{} |
|
|
|
general.Confirmation = cfg.Section("general").Key("confirmation").MustBool(true) |
|
dateFormat := cfg.Section("general").Key("date-format").String() |
|
if dateFormat == "" { |
|
dateFormat = "2006-01-02 15:04" |
|
} |
|
general.DateFormat = dateFormat |
|
|
|
dateTodayFormat := cfg.Section("general").Key("date-today-format").String() |
|
if dateTodayFormat == "" { |
|
dateTodayFormat = "15:04" |
|
} |
|
general.DateTodayFormat = dateTodayFormat |
|
|
|
dateRelative, err := cfg.Section("general").Key("date-relative").Int() |
|
if err != nil { |
|
dateRelative = -1 |
|
} |
|
general.DateRelative = dateRelative |
|
|
|
tl := cfg.Section("general").Key("timeline").In("home", []string{"home", "direct", "local", "federated"}) |
|
switch tl { |
|
case "direct": |
|
general.StartTimeline = feed.Conversations |
|
case "local": |
|
general.StartTimeline = feed.TimelineLocal |
|
case "federated": |
|
general.StartTimeline = feed.TimelineFederated |
|
default: |
|
general.StartTimeline = feed.TimelineHome |
|
} |
|
|
|
general.NotificationFeed = cfg.Section("general").Key("notification-feed").MustBool(true) |
|
general.QuoteReply = cfg.Section("general").Key("quote-reply").MustBool(false) |
|
general.CharLimit = cfg.Section("general").Key("char-limit").MustInt(500) |
|
general.MaxWidth = cfg.Section("general").Key("max-width").MustInt(0) |
|
general.ShortHints = cfg.Section("general").Key("short-hints").MustBool(false) |
|
general.ShowFilterPhrase = cfg.Section("general").Key("show-filter-phrase").MustBool(true) |
|
general.ShowIcons = cfg.Section("general").Key("show-icons").MustBool(true) |
|
general.ShowHelp = cfg.Section("general").Key("show-help").MustBool(true) |
|
general.RedrawUI = cfg.Section("general").Key("redraw-ui").MustBool(true) |
|
|
|
lp := cfg.Section("general").Key("list-placement").In("left", []string{"left", "right", "top", "bottom"}) |
|
switch lp { |
|
case "left": |
|
general.ListPlacement = ListPlacementLeft |
|
case "right": |
|
general.ListPlacement = ListPlacementRight |
|
case "top": |
|
general.ListPlacement = ListPlacementTop |
|
case "bottom": |
|
general.ListPlacement = ListPlacementBottom |
|
} |
|
ls := cfg.Section("general").Key("list-split").In("row", []string{"row", "column"}) |
|
switch ls { |
|
case "row": |
|
general.ListSplit = ListRow |
|
case "column": |
|
general.ListSplit = ListColumn |
|
} |
|
|
|
listProp := cfg.Section("general").Key("list-proportion").MustInt(1) |
|
if listProp < 1 { |
|
listProp = 1 |
|
} |
|
contentProp := cfg.Section("general").Key("content-proportion").MustInt(2) |
|
if contentProp < 1 { |
|
contentProp = 1 |
|
} |
|
general.ListProportion = listProp |
|
general.ContentProportion = contentProp |
|
|
|
leaderString := cfg.Section("general").Key("leader-key").MustString("") |
|
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] |
|
} |
|
if general.LeaderKey != rune(0) { |
|
general.LeaderTimeout = cfg.Section("general").Key("leader-timeout").MustInt64(1000) |
|
lactions := cfg.Section("general").Key("leader-action").ValueWithShadows() |
|
var las []LeaderAction |
|
for _, l := range lactions { |
|
parts := strings.Split(l, ",") |
|
if len(parts) != 2 { |
|
fmt.Printf("leader-action must consist of two parts separated by a comma. Your value is: %s\n", strings.Join(parts, ",")) |
|
os.Exit(1) |
|
} |
|
for i, p := range parts { |
|
parts[i] = strings.TrimSpace(p) |
|
} |
|
cmd := parts[0] |
|
var subaction string |
|
if strings.Contains(parts[0], " ") { |
|
p := strings.Split(cmd, " ") |
|
cmd = p[0] |
|
subaction = strings.Join(p[1:], " ") |
|
} |
|
la := LeaderAction{} |
|
switch cmd { |
|
case "home": |
|
la.Command = LeaderHome |
|
case "direct": |
|
la.Command = LeaderDirect |
|
case "local": |
|
la.Command = LeaderLocal |
|
case "federated": |
|
la.Command = LeaderFederated |
|
case "compose": |
|
la.Command = LeaderCompose |
|
case "blocking": |
|
la.Command = LeaderBlocking |
|
case "bookmarks": |
|
la.Command = LeaderBookmarks |
|
case "saved": |
|
la.Command = LeaderSaved |
|
case "favorited": |
|
la.Command = LeaderFavorited |
|
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 "notifications": |
|
la.Command = LeaderNotifications |
|
case "lists": |
|
la.Command = LeaderLists |
|
case "tag": |
|
la.Command = LeaderTag |
|
la.Subaction = subaction |
|
case "list-placement": |
|
la.Command = LeaderListPlacement |
|
la.Subaction = subaction |
|
case "list-split": |
|
la.Command = LeaderListSplit |
|
la.Subaction = subaction |
|
case "proportions": |
|
la.Command = LeaderProportions |
|
la.Subaction = subaction |
|
case "window": |
|
la.Command = LeaderWindow |
|
la.Subaction = subaction |
|
default: |
|
fmt.Printf("leader-action %s is invalid\n", parts[0]) |
|
os.Exit(1) |
|
} |
|
la.Shortcut = parts[1] |
|
las = append(las, la) |
|
} |
|
general.LeaderActions = las |
|
} |
|
|
|
general.TimelineName = cfg.Section("general").Key("timeline-show-name").MustBool(true) |
|
var tls []Timeline |
|
timelines := cfg.Section("general").Key("timelines").ValueWithShadows() |
|
for _, l := range timelines { |
|
parts := strings.Split(l, ",") |
|
for i, p := range parts { |
|
parts[i] = strings.TrimSpace(p) |
|
} |
|
if len(parts) == 0 { |
|
fmt.Printf("timelines must consist of atleast one part seperated by a comma. Your value is: %s\n", strings.Join(parts, ",")) |
|
os.Exit(1) |
|
} |
|
if len(parts) == 1 { |
|
parts = append(parts, "") |
|
} |
|
cmd := parts[0] |
|
var subaction string |
|
if strings.Contains(parts[0], " ") { |
|
p := strings.Split(cmd, " ") |
|
cmd = p[0] |
|
subaction = strings.Join(p[1:], " ") |
|
} |
|
tl := Timeline{} |
|
switch cmd { |
|
case "home": |
|
tl.FeedType = feed.TimelineHome |
|
case "direct": |
|
tl.FeedType = feed.Conversations |
|
case "local": |
|
tl.FeedType = feed.TimelineLocal |
|
case "federated": |
|
tl.FeedType = feed.TimelineFederated |
|
case "bookmarks": |
|
tl.FeedType = feed.Saved |
|
case "saved": |
|
tl.FeedType = feed.Saved |
|
case "favorited": |
|
tl.FeedType = feed.Favorited |
|
case "notifications": |
|
tl.FeedType = feed.Notification |
|
case "lists": |
|
tl.FeedType = feed.Lists |
|
case "tag": |
|
tl.FeedType = feed.Tag |
|
tl.Subaction = subaction |
|
default: |
|
fmt.Printf("timeline %s is invalid\n", parts[0]) |
|
os.Exit(1) |
|
} |
|
tl.Name = parts[1] |
|
if len(parts) > 2 { |
|
vals := []string{""} |
|
vals = append(vals, parts[2:]...) |
|
tl.Key = inputStrOrErr(vals, false) |
|
} |
|
tls = append(tls, tl) |
|
} |
|
if len(tls) == 0 { |
|
tls = append(tls, |
|
Timeline{ |
|
FeedType: feed.TimelineHome, |
|
Name: "", |
|
}, |
|
) |
|
tls = append(tls, |
|
Timeline{ |
|
FeedType: feed.Notification, |
|
Name: "[N]otifications", |
|
Key: inputStrOrErr([]string{"", "'n'", "'N'"}, false), |
|
}, |
|
) |
|
} |
|
general.Timelines = tls |
|
|
|
return general |
|
} |
|
|
|
func parseMedia(cfg *ini.File) Media { |
|
media := Media{} |
|
imageViewerComponents := strings.Fields(cfg.Section("media").Key("image-viewer").String()) |
|
if len(imageViewerComponents) == 0 { |
|
media.ImageViewer = "xdg-open" |
|
media.ImageArgs = []string{} |
|
} else { |
|
media.ImageViewer = imageViewerComponents[0] |
|
media.ImageArgs = imageViewerComponents[1:] |
|
} |
|
media.ImageTerminal = cfg.Section("media").Key("image-terminal").MustBool(false) |
|
media.ImageSingle = cfg.Section("media").Key("image-single").MustBool(true) |
|
media.ImageReverse = cfg.Section("media").Key("image-reverse").MustBool(false) |
|
|
|
videoViewerComponents := strings.Fields(cfg.Section("media").Key("video-viewer").String()) |
|
if len(videoViewerComponents) == 0 { |
|
media.VideoViewer = "xdg-open" |
|
media.VideoArgs = []string{} |
|
} else { |
|
media.VideoViewer = videoViewerComponents[0] |
|
media.VideoArgs = videoViewerComponents[1:] |
|
} |
|
media.VideoTerminal = cfg.Section("media").Key("video-terminal").MustBool(false) |
|
media.VideoSingle = cfg.Section("media").Key("video-single").MustBool(true) |
|
media.VideoReverse = cfg.Section("media").Key("video-reverse").MustBool(false) |
|
|
|
audioViewerComponents := strings.Fields(cfg.Section("media").Key("audio-viewer").String()) |
|
if len(audioViewerComponents) == 0 { |
|
media.AudioViewer = "xdg-open" |
|
media.AudioArgs = []string{} |
|
} else { |
|
media.AudioViewer = audioViewerComponents[0] |
|
media.AudioArgs = audioViewerComponents[1:] |
|
} |
|
media.AudioTerminal = cfg.Section("media").Key("audio-terminal").MustBool(false) |
|
media.AudioSingle = cfg.Section("media").Key("audio-single").MustBool(true) |
|
media.AudioReverse = cfg.Section("media").Key("audio-reverse").MustBool(false) |
|
|
|
linkViewerComponents := strings.Fields(cfg.Section("media").Key("link-viewer").String()) |
|
if len(linkViewerComponents) == 0 { |
|
media.LinkViewer = "xdg-open" |
|
media.LinkArgs = []string{} |
|
} else { |
|
media.LinkViewer = linkViewerComponents[0] |
|
media.LinkArgs = linkViewerComponents[1:] |
|
} |
|
media.LinkTerminal = cfg.Section("media").Key("link-terminal").MustBool(false) |
|
|
|
return media |
|
} |
|
|
|
func parseOpenPattern(cfg *ini.File) OpenPattern { |
|
om := OpenPattern{} |
|
|
|
keys := cfg.Section("open-pattern").KeyStrings() |
|
pairs := make(map[string]Pattern) |
|
for _, s := range keys { |
|
parts := strings.Split(s, "-") |
|
if len(parts) < 2 { |
|
panic(fmt.Sprintf("Invalid key %s in config. Must end in -pattern, -use or -terminal", s)) |
|
} |
|
last := parts[len(parts)-1] |
|
if last != "pattern" && last != "use" && last != "terminal" { |
|
panic(fmt.Sprintf("Invalid key %s in config. Must end in -pattern, -use or -terminal", s)) |
|
} |
|
|
|
name := strings.Join(parts[:len(parts)-1], "-") |
|
if _, ok := pairs[name]; !ok { |
|
pairs[name] = Pattern{} |
|
} |
|
if last == "pattern" { |
|
tmp := pairs[name] |
|
tmp.Pattern = cfg.Section("open-pattern").Key(s).MustString("") |
|
pairs[name] = tmp |
|
} |
|
if last == "use" { |
|
tmp := pairs[name] |
|
tmp.Open = cfg.Section("open-pattern").Key(s).MustString("") |
|
pairs[name] = tmp |
|
} |
|
if last == "terminal" { |
|
tmp := pairs[name] |
|
tmp.Terminal = cfg.Section("open-pattern").Key(s).MustBool(false) |
|
pairs[name] = tmp |
|
} |
|
} |
|
|
|
for key := range pairs { |
|
if pairs[key].Pattern == "" { |
|
panic(fmt.Sprintf("Invalid value for key %s in config. Can't be empty", key+"-pattern")) |
|
} |
|
if pairs[key].Open == "" { |
|
panic(fmt.Sprintf("Invalid value for key %s in config. Can't be empty", key+"-use")) |
|
} |
|
|
|
compiled, err := glob.Compile(pairs[key].Pattern) |
|
if err != nil { |
|
panic(fmt.Sprintf("Couldn't compile pattern for key %s in config. Error: %v", key+"-pattern", err)) |
|
} |
|
tmp := pairs[key] |
|
tmp.Compiled = compiled |
|
comp := strings.Fields(tmp.Open) |
|
tmp.Program = comp[0] |
|
tmp.Args = comp[1:] |
|
om.Patterns = append(om.Patterns, tmp) |
|
} |
|
|
|
return om |
|
} |
|
|
|
func parseCustom(cfg *ini.File) OpenCustom { |
|
oc := OpenCustom{} |
|
|
|
for i := 1; i < 6; i++ { |
|
name := cfg.Section("open-custom").Key(fmt.Sprintf("c%d-name", i)).MustString("") |
|
use := cfg.Section("open-custom").Key(fmt.Sprintf("c%d-use", i)).MustString("") |
|
terminal := cfg.Section("open-custom").Key(fmt.Sprintf("c%d-terminal", i)).MustBool(false) |
|
if use == "" { |
|
continue |
|
} |
|
comp := strings.Fields(use) |
|
c := Custom{} |
|
c.Index = i |
|
c.Name = name |
|
c.Program = comp[0] |
|
c.Args = comp[1:] |
|
c.Terminal = terminal |
|
oc.OpenCustoms = append(oc.OpenCustoms, c) |
|
} |
|
return oc |
|
} |
|
|
|
func parseNotifications(cfg *ini.File) Notification { |
|
nc := Notification{} |
|
nc.NotificationFollower = cfg.Section("desktop-notification").Key("followers").MustBool(false) |
|
nc.NotificationFavorite = cfg.Section("desktop-notification").Key("favorite").MustBool(false) |
|
nc.NotificationMention = cfg.Section("desktop-notification").Key("mention").MustBool(false) |
|
nc.NotificationBoost = cfg.Section("desktop-notification").Key("boost").MustBool(false) |
|
nc.NotificationPoll = cfg.Section("desktop-notification").Key("poll").MustBool(false) |
|
nc.NotificationPost = cfg.Section("desktop-notification").Key("posts").MustBool(false) |
|
return nc |
|
} |
|
|
|
func parseTemplates(cfg *ini.File) Templates { |
|
var tootTmpl *template.Template |
|
tootTmplPath, exists, err := checkConfig("toot.tmpl") |
|
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") |
|
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 inputOrErr(cfg *ini.File, key string, double bool, def Key) Key { |
|
if !cfg.Section("input").HasKey(key) { |
|
return def |
|
} |
|
vals := cfg.Section("input").Key(key).Strings(",") |
|
k, err := NewKey(vals, double) |
|
if err != nil { |
|
fmt.Printf("error parsing config for key %s. Error: %v\n", key, err) |
|
os.Exit(1) |
|
} |
|
return k |
|
} |
|
func inputStrOrErr(vals []string, double bool) Key { |
|
k, err := NewKey(vals, double) |
|
if err != nil { |
|
fmt.Printf("error parsing config. Error: %v\n", err) |
|
os.Exit(1) |
|
} |
|
return k |
|
} |
|
|
|
func parseInput(cfg *ini.File) Input { |
|
ic := Input{ |
|
GlobalDown: inputStrOrErr([]string{"\"\"", "'j'", "'J'", "\"Down\""}, false), |
|
GlobalUp: inputStrOrErr([]string{"\"\"", "'k'", "'k'", "\"Up\""}, false), |
|
GlobalEnter: inputStrOrErr([]string{"\"\"", "\"Enter\""}, false), |
|
GlobalBack: inputStrOrErr([]string{"\"[Esc]\"", "\"Esc\""}, false), |
|
GlobalExit: inputStrOrErr([]string{"\"[Q]uit\"", "'q'", "'Q'"}, false), |
|
|
|
MainHome: inputStrOrErr([]string{"\"\"", "'g'", "\"Home\""}, false), |
|
MainEnd: inputStrOrErr([]string{"\"\"", "'G'", "\"End\""}, false), |
|
MainPrevFeed: inputStrOrErr([]string{"\"\"", "'h'", "'H'", "\"Left\""}, false), |
|
MainNextFeed: inputStrOrErr([]string{"\"\"", "'l'", "'L'", "\"Right\""}, false), |
|
MainPrevWindow: inputStrOrErr([]string{"\"\"", "\"Backtab\""}, false), |
|
MainNextWindow: inputStrOrErr([]string{"\"\"", "\"Tab\""}, false), |
|
MainCompose: inputStrOrErr([]string{"\"\"", "'c'", "'C'"}, false), |
|
|
|
StatusAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), |
|
StatusBoost: inputStrOrErr([]string{"\"[B]oost\"", "\"Un[B]oost\"", "'b'", "'B'"}, true), |
|
StatusDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), |
|
StatusFavorite: inputStrOrErr([]string{"\"[F]avorite\"", "\"Un[F]avorite\"", "'f'", "'F'"}, true), |
|
StatusMedia: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false), |
|
StatusLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), |
|
StatusPoll: inputStrOrErr([]string{"\"[P]oll\"", "'p'", "'P'"}, false), |
|
StatusReply: inputStrOrErr([]string{"\"[R]eply\"", "'r'", "'R'"}, false), |
|
StatusBookmark: inputStrOrErr([]string{"\"[S]ave\"", "\"Un[S]ave\"", "'s'", "'S'"}, true), |
|
StatusThread: inputStrOrErr([]string{"\"[T]hread\"", "'t'", "'T'"}, false), |
|
StatusUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false), |
|
StatusViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), |
|
StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), |
|
StatusToggleSpoiler: inputStrOrErr([]string{"\"Press [Z] to toggle spoiler\"", "'z'", "'Z'"}, false), |
|
|
|
UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), |
|
UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true), |
|
UserFollow: inputStrOrErr([]string{"\"[F]ollow\"", "\"Un[F]ollow\"", "'f'", "'F'"}, true), |
|
UserFollowRequestDecide: inputStrOrErr([]string{"\"Follow [R]equest\"", "\"Follow [R]equest\"", "'r'", "'R'"}, true), |
|
UserMute: inputStrOrErr([]string{"\"[M]ute\"", "\"Un[M]ute\"", "'m'", "'M'"}, true), |
|
UserLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), |
|
UserUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false), |
|
UserViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), |
|
UserYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), |
|
|
|
ListOpenFeed: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), |
|
|
|
LinkOpen: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), |
|
LinkYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), |
|
|
|
ComposeEditSpoiler: inputStrOrErr([]string{"\"[C]W Text\"", "'c'", "'C'"}, false), |
|
ComposeEditText: inputStrOrErr([]string{"\"[E]dit text\"", "'e'", "'E'"}, false), |
|
ComposeIncludeQuote: inputStrOrErr([]string{"\"[I]nclude quote\"", "'i'", "'I'"}, false), |
|
ComposeMediaFocus: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false), |
|
ComposePost: inputStrOrErr([]string{"\"[P]ost\"", "'p'", "'P'"}, false), |
|
ComposeToggleContentWarning: inputStrOrErr([]string{"\"[T]oggle CW\"", "'t'", "'T'"}, false), |
|
ComposeVisibility: inputStrOrErr([]string{"\"[V]isibility\"", "'v'", "'V'"}, false), |
|
ComposePoll: inputStrOrErr([]string{"\"P[O]ll\"", "'o'", "'O'"}, false), |
|
|
|
MediaDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), |
|
MediaEditDesc: inputStrOrErr([]string{"\"[E]dit desc\"", "'e'", "'E'"}, false), |
|
MediaAdd: inputStrOrErr([]string{"\"[A]dd\"", "'a'", "'A'"}, false), |
|
|
|
VoteVote: inputStrOrErr([]string{"\"[V]ote\"", "'v'", "'V'"}, false), |
|
VoteSelect: inputStrOrErr([]string{"\"[Enter] to select\"", "' '", "\"Enter\""}, false), |
|
|
|
PollAdd: inputStrOrErr([]string{"\"[A]dd\"", "'a'", "'A'"}, false), |
|
PollEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false), |
|
PollDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), |
|
PollMultiToggle: inputStrOrErr([]string{"\"Toggle [M]ultiple\"", "'m'", "'M'"}, false), |
|
PollExpiration: inputStrOrErr([]string{"\"E[X]pires\"", "'x'", "'X'"}, false), |
|
|
|
PreferenceName: inputStrOrErr([]string{"\"[N]ame\"", "'n'", "'N'"}, false), |
|
PreferenceBio: inputStrOrErr([]string{"\"[B]io\"", "'b'", "'B'"}, false), |
|
PreferenceVisibility: inputStrOrErr([]string{"\"[V]isibility\"", "'v'", "'V'"}, false), |
|
PreferenceSave: inputStrOrErr([]string{"\"[S]ave\"", "'s'", "'S'"}, false), |
|
PreferenceFields: inputStrOrErr([]string{"\"[F]ields\"", "'f'", "'F'"}, false), |
|
PreferenceFieldsAdd: inputStrOrErr([]string{"\"[A]dd\"", "'a'", "'A'"}, false), |
|
PreferenceFieldsEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false), |
|
PreferenceFieldsDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), |
|
} |
|
ic.GlobalDown = inputOrErr(cfg, "global-down", false, ic.GlobalDown) |
|
ic.GlobalUp = inputOrErr(cfg, "global-up", false, ic.GlobalUp) |
|
ic.GlobalEnter = inputOrErr(cfg, "global-enter", false, ic.GlobalEnter) |
|
ic.GlobalBack = inputOrErr(cfg, "global-back", false, ic.GlobalBack) |
|
ic.GlobalExit = inputOrErr(cfg, "global-exit", false, ic.GlobalExit) |
|
|
|
ic.MainHome = inputOrErr(cfg, "main-home", false, ic.MainHome) |
|
ic.MainEnd = inputOrErr(cfg, "main-end", false, ic.MainEnd) |
|
ic.MainPrevFeed = inputOrErr(cfg, "main-prev-feed", false, ic.MainPrevFeed) |
|
ic.MainNextFeed = inputOrErr(cfg, "main-next-feed", false, ic.MainNextFeed) |
|
ic.MainCompose = inputOrErr(cfg, "main-compose", false, ic.MainCompose) |
|
|
|
ic.StatusAvatar = inputOrErr(cfg, "status-avatar", false, ic.StatusAvatar) |
|
ic.StatusBoost = inputOrErr(cfg, "status-boost", true, ic.StatusBoost) |
|
ic.StatusDelete = inputOrErr(cfg, "status-delete", false, ic.StatusDelete) |
|
ic.StatusFavorite = inputOrErr(cfg, "status-favorite", true, ic.StatusFavorite) |
|
ic.StatusMedia = inputOrErr(cfg, "status-media", false, ic.StatusMedia) |
|
ic.StatusLinks = inputOrErr(cfg, "status-links", false, ic.StatusLinks) |
|
ic.StatusPoll = inputOrErr(cfg, "status-poll", false, ic.StatusPoll) |
|
ic.StatusReply = inputOrErr(cfg, "status-reply", false, ic.StatusReply) |
|
ic.StatusBookmark = inputOrErr(cfg, "status-bookmark", true, ic.StatusBookmark) |
|
ic.StatusThread = inputOrErr(cfg, "status-thread", false, ic.StatusThread) |
|
ic.StatusUser = inputOrErr(cfg, "status-user", false, ic.StatusUser) |
|
ic.StatusViewFocus = inputOrErr(cfg, "status-view-focus", false, ic.StatusViewFocus) |
|
ic.StatusYank = inputOrErr(cfg, "status-yank", false, ic.StatusYank) |
|
ic.StatusToggleSpoiler = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleSpoiler) |
|
|
|
ic.UserAvatar = inputOrErr(cfg, "user-avatar", false, ic.UserAvatar) |
|
ic.UserBlock = inputOrErr(cfg, "user-block", true, ic.UserBlock) |
|
ic.UserFollow = inputOrErr(cfg, "user-follow", true, ic.UserFollow) |
|
ic.UserFollowRequestDecide = inputOrErr(cfg, "user-follow-request-decide", true, ic.UserFollowRequestDecide) |
|
ic.UserMute = inputOrErr(cfg, "user-mute", true, ic.UserMute) |
|
ic.UserLinks = inputOrErr(cfg, "user-links", false, ic.UserLinks) |
|
ic.UserUser = inputOrErr(cfg, "user-user", false, ic.UserUser) |
|
ic.UserViewFocus = inputOrErr(cfg, "user-view-focus", false, ic.UserViewFocus) |
|
ic.UserYank = inputOrErr(cfg, "user-yank", false, ic.UserYank) |
|
|
|
ic.ListOpenFeed = inputOrErr(cfg, "list-open-feed", false, ic.ListOpenFeed) |
|
|
|
ic.LinkOpen = inputOrErr(cfg, "link-open", false, ic.LinkOpen) |
|
ic.LinkYank = inputOrErr(cfg, "link-yank", false, ic.LinkYank) |
|
|
|
ic.ComposeEditSpoiler = inputOrErr(cfg, "compose-edit-spoiler", false, ic.ComposeEditSpoiler) |
|
ic.ComposeEditText = inputOrErr(cfg, "compose-edit-text", false, ic.ComposeEditText) |
|
ic.ComposeIncludeQuote = inputOrErr(cfg, "compose-include-quote", false, ic.ComposeIncludeQuote) |
|
ic.ComposeMediaFocus = inputOrErr(cfg, "compose-media-focus", false, ic.ComposeMediaFocus) |
|
ic.ComposePost = inputOrErr(cfg, "compose-post", false, ic.ComposePost) |
|
ic.ComposeToggleContentWarning = inputOrErr(cfg, "compose-toggle-content-warning", false, ic.ComposeToggleContentWarning) |
|
ic.ComposeVisibility = inputOrErr(cfg, "compose-visibility", false, ic.ComposeVisibility) |
|
ic.ComposePoll = inputOrErr(cfg, "compose-poll", false, ic.ComposePoll) |
|
|
|
ic.MediaDelete = inputOrErr(cfg, "media-delete", false, ic.MediaDelete) |
|
ic.MediaEditDesc = inputOrErr(cfg, "media-edit-desc", false, ic.MediaEditDesc) |
|
ic.MediaAdd = inputOrErr(cfg, "media-add", false, ic.MediaAdd) |
|
|
|
ic.VoteVote = inputOrErr(cfg, "vote-vote", false, ic.VoteVote) |
|
ic.VoteSelect = inputOrErr(cfg, "vote-select", false, ic.VoteSelect) |
|
|
|
ic.PollAdd = inputOrErr(cfg, "poll-add", false, ic.PollAdd) |
|
ic.PollEdit = inputOrErr(cfg, "poll-edit", false, ic.PollEdit) |
|
ic.PollDelete = inputOrErr(cfg, "poll-delete", false, ic.PollDelete) |
|
ic.PollMultiToggle = inputOrErr(cfg, "poll-multi-toggle", false, ic.PollMultiToggle) |
|
ic.PollExpiration = inputOrErr(cfg, "poll-expiration", false, ic.PollExpiration) |
|
|
|
ic.PreferenceName = inputOrErr(cfg, "preference-name", false, ic.PreferenceName) |
|
ic.PreferenceVisibility = inputOrErr(cfg, "preference-visibility", false, ic.PreferenceVisibility) |
|
ic.PreferenceBio = inputOrErr(cfg, "preference-bio", false, ic.PreferenceBio) |
|
ic.PreferenceSave = inputOrErr(cfg, "preference-save", false, ic.PreferenceSave) |
|
ic.PreferenceFields = inputOrErr(cfg, "preference-fields", false, ic.PreferenceFields) |
|
ic.PreferenceFieldsAdd = inputOrErr(cfg, "preference-fields-add", false, ic.PreferenceFieldsAdd) |
|
ic.PreferenceFieldsEdit = inputOrErr(cfg, "preference-fields-edit", false, ic.PreferenceFieldsEdit) |
|
ic.PreferenceFieldsDelete = inputOrErr(cfg, "preference-fields-delete", false, ic.PreferenceFieldsDelete) |
|
return ic |
|
} |
|
|
|
func parseConfig(filepath string) (Config, error) { |
|
cfg, err := ini.LoadSources(ini.LoadOptions{ |
|
SpaceBeforeInlineComment: true, |
|
AllowShadows: true, |
|
}, filepath) |
|
conf := Config{} |
|
if err != nil { |
|
return conf, err |
|
} |
|
conf.General = parseGeneral(cfg) |
|
conf.Media = parseMedia(cfg) |
|
conf.Style = parseStyle(cfg) |
|
conf.OpenPattern = parseOpenPattern(cfg) |
|
conf.OpenCustom = parseCustom(cfg) |
|
conf.NotificationConfig = parseNotifications(cfg) |
|
conf.Templates = parseTemplates(cfg) |
|
conf.Input = parseInput(cfg) |
|
|
|
return conf, nil |
|
} |
|
|
|
func createConfigDir() error { |
|
cd, err := os.UserConfigDir() |
|
if err != nil { |
|
log.Fatalf("couldn't find $HOME. Err %v", err) |
|
} |
|
path := cd + "/tut" |
|
return os.MkdirAll(path, os.ModePerm) |
|
} |
|
|
|
func checkConfig(filename string) (path string, exists bool, err error) { |
|
cd, err := os.UserConfigDir() |
|
if err != nil { |
|
log.Fatalf("couldn't find $HOME. 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() ([]string, error) { |
|
entries, err := themesFS.ReadDir("themes") |
|
files := []string{} |
|
if err != nil { |
|
return []string{}, err |
|
} |
|
for _, entry := range entries { |
|
if entry.IsDir() { |
|
continue |
|
} |
|
fp := filepath.Join("themes/", entry.Name()) |
|
files = append(files, fp) |
|
} |
|
return files, nil |
|
} |
|
|
|
func getTheme(fname string) (*ini.File, error) { |
|
f, err := themesFS.Open(fmt.Sprintf("themes/%s.ini", strings.TrimSpace(fname))) |
|
if err != nil { |
|
return nil, err |
|
} |
|
content, err := ioutil.ReadAll(f) |
|
if err != nil { |
|
return nil, err |
|
} |
|
cfg, err := ini.LoadSources(ini.LoadOptions{ |
|
SpaceBeforeInlineComment: true, |
|
}, content) |
|
if err != nil { |
|
return nil, err |
|
} |
|
keys := []string{ |
|
"background", |
|
"text", |
|
"subtle", |
|
"warning-text", |
|
"text-special-one", |
|
"text-special-two", |
|
"top-bar-background", |
|
"top-bar-text", |
|
"status-bar-background", |
|
"status-bar-text", |
|
"status-bar-view-background", |
|
"status-bar-view-text", |
|
"list-selected-background", |
|
"list-selected-text", |
|
} |
|
for _, k := range keys { |
|
if !cfg.Section("").HasKey(k) { |
|
return nil, fmt.Errorf("theme %s is missing %s", fname, k) |
|
} |
|
} |
|
return cfg, nil |
|
}
|
|
|