package senpai import ( "errors" "fmt" "net/url" "os" "os/exec" "path" "strconv" "strings" "git.sr.ht/~taiite/senpai/ui" "github.com/gdamore/tcell/v2" "git.sr.ht/~emersion/go-scfg" ) func parseColor(s string, c *tcell.Color) error { if strings.HasPrefix(s, "#") { hex, err := strconv.ParseInt(s[1:], 16, 32) if err != nil { return err } *c = tcell.NewHexColor(int32(hex)) return nil } code, err := strconv.Atoi(s) if err != nil { return err } if code == -1 { *c = tcell.ColorDefault return nil } if code < 0 || code > 255 { return fmt.Errorf("color code must be between 0-255. If you meant to use true colors, use #aabbcc notation") } *c = tcell.PaletteColor(code) return nil } type ConfigColors struct { Prompt tcell.Color Unread tcell.Color Nicks ui.ColorScheme } type Config struct { Addr string Nick string Real string User string Password *string TLS bool Channels []string Typings bool Mouse bool Highlights []string OnHighlightPath string OnHighlightBeep bool NickColWidth int ChanColWidth int ChanColEnabled bool MemberColWidth int MemberColEnabled bool TextMaxWidth int Colors ConfigColors Debug bool } func DefaultHighlightPath() (string, error) { configDir, err := os.UserConfigDir() if err != nil { return "", err } return path.Join(configDir, "senpai", "highlight"), nil } func Defaults() (cfg Config, err error) { cfg = Config{ Addr: "", Nick: "", Real: "", User: "", Password: nil, TLS: true, Channels: nil, Typings: true, Mouse: true, Highlights: nil, OnHighlightPath: "", OnHighlightBeep: false, NickColWidth: 14, ChanColWidth: 16, ChanColEnabled: true, MemberColWidth: 16, MemberColEnabled: true, TextMaxWidth: 0, Colors: ConfigColors{ Prompt: tcell.ColorDefault, Unread: tcell.ColorDefault, Nicks: ui.ColorSchemeBase, }, Debug: false, } return } func LoadConfigFile(filename string) (cfg Config, err error) { cfg, err = Defaults() if err != nil { return } err = unmarshal(filename, &cfg) if err != nil { return cfg, err } if cfg.Addr == "" { return cfg, errors.New("addr is required") } if cfg.Nick == "" { return cfg, errors.New("nick is required") } if cfg.User == "" { cfg.User = cfg.Nick } if cfg.Real == "" { cfg.Real = cfg.Nick } if u, err := url.Parse(cfg.Addr); err == nil && u.Scheme != "" { switch u.Scheme { case "ircs": cfg.TLS = true case "irc+insecure": cfg.TLS = false case "irc": // Could be TLS or plaintext, keep TLS as is. default: if u.Host != "" { return cfg, fmt.Errorf("invalid IRC addr scheme: %v", cfg.Addr) } } if u.Host != "" { cfg.Addr = u.Host } } return } func unmarshal(filename string, cfg *Config) (err error) { directives, err := scfg.Load(filename) if err != nil { return fmt.Errorf("error parsing scfg: %s", err) } for _, d := range directives { switch d.Name { case "address": if err := d.ParseParams(&cfg.Addr); err != nil { return err } case "nickname": if err := d.ParseParams(&cfg.Nick); err != nil { return err } case "username": if err := d.ParseParams(&cfg.User); err != nil { return err } case "realname": if err := d.ParseParams(&cfg.Real); err != nil { return err } case "password": // if a password-cmd is provided, don't use this value if directives.Get("password-cmd") != nil { continue } var password string if err := d.ParseParams(&password); err != nil { return err } cfg.Password = &password case "password-cmd": var cmdName string if err := d.ParseParams(&cmdName); err != nil { return err } cmd := exec.Command(cmdName, d.Params[1:]...) var stdout []byte if stdout, err = cmd.Output(); err != nil { return fmt.Errorf("error running password command: %s", err) } passCmdOut := strings.Split(string(stdout), "\n") if len(passCmdOut) >= 1 { cfg.Password = &passCmdOut[0] } case "channel": // TODO: does this work with soju.im/bouncer-networks extension? cfg.Channels = append(cfg.Channels, d.Params...) case "highlight": cfg.Highlights = append(cfg.Highlights, d.Params...) case "on-highlight-path": if err := d.ParseParams(&cfg.OnHighlightPath); err != nil { return err } case "on-highlight-beep": var onHighlightBeep string if err := d.ParseParams(&onHighlightBeep); err != nil { return err } if cfg.OnHighlightBeep, err = strconv.ParseBool(onHighlightBeep); err != nil { return err } case "pane-widths": for _, child := range d.Children { switch child.Name { case "nicknames": var nicknames string if err := child.ParseParams(&nicknames); err != nil { return err } if cfg.NickColWidth, err = strconv.Atoi(nicknames); err != nil { return err } case "channels": var channelsStr string if err := child.ParseParams(&channelsStr); err != nil { return err } channels, err := strconv.Atoi(channelsStr) if err != nil { return err } if channels <= 0 { cfg.ChanColEnabled = false if channels < 0 { cfg.ChanColWidth = -channels } } else { cfg.ChanColWidth = channels } case "members": var membersStr string if err := child.ParseParams(&membersStr); err != nil { return err } members, err := strconv.Atoi(membersStr) if err != nil { return err } if members <= 0 { cfg.MemberColEnabled = false if members < 0 { cfg.MemberColWidth = -members } } else { cfg.MemberColWidth = members } case "text": var text string if err := child.ParseParams(&text); err != nil { return err } if cfg.TextMaxWidth, err = strconv.Atoi(text); err != nil { return err } default: return fmt.Errorf("unknown directive %q", child.Name) } } case "tls": var tls string if err := d.ParseParams(&tls); err != nil { return err } if cfg.TLS, err = strconv.ParseBool(tls); err != nil { return err } case "typings": var typings string if err := d.ParseParams(&typings); err != nil { return err } if cfg.Typings, err = strconv.ParseBool(typings); err != nil { return err } case "mouse": var mouse string if err := d.ParseParams(&mouse); err != nil { return err } if cfg.Mouse, err = strconv.ParseBool(mouse); err != nil { return err } case "colors": for _, child := range d.Children { var colorStr string if err := child.ParseParams(&colorStr); err != nil { return err } switch child.Name { case "nicks": switch colorStr { case "base": cfg.Colors.Nicks = ui.ColorSchemeBase case "extended": cfg.Colors.Nicks = ui.ColorSchemeExtended default: return fmt.Errorf("unknown nick color scheme %q", colorStr) } continue } var color tcell.Color if err = parseColor(colorStr, &color); err != nil { return err } switch child.Name { case "prompt": cfg.Colors.Prompt = color case "unread": cfg.Colors.Unread = color default: return fmt.Errorf("unknown directive %q", child.Name) } } case "debug": var debug string if err := d.ParseParams(&debug); err != nil { return err } if cfg.Debug, err = strconv.ParseBool(debug); err != nil { return err } default: return fmt.Errorf("unknown directive %q", d.Name) } } return }