diff options
author | Robin Jarry <robin@jarry.cc> | 2023-10-09 13:52:20 +0200 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-10-10 11:37:56 +0200 |
commit | 598e4a5803578ab3e291f232d6aad31b4efd8ea4 (patch) | |
tree | c55e16d60e2c3eea2d6de27d1bac18db5670ec77 /widgets/aerc.go | |
parent | 61bca76423ee87bd59084a146eca71c6bae085e1 (diff) | |
download | aerc-598e4a5803578ab3e291f232d6aad31b4efd8ea4.tar.gz |
widgets: rename package to app
This is the central point of all aerc. Having it named widgets is
confusing. Rename it to app. It will make a cleaner transition when
making the app.Aerc object available globally in the next commit.
Signed-off-by: Robin Jarry <robin@jarry.cc>
Acked-by: Moritz Poldrack <moritz@poldrack.dev>
Diffstat (limited to 'widgets/aerc.go')
-rw-r--r-- | widgets/aerc.go | 908 |
1 files changed, 0 insertions, 908 deletions
diff --git a/widgets/aerc.go b/widgets/aerc.go deleted file mode 100644 index efa13194..00000000 --- a/widgets/aerc.go +++ /dev/null @@ -1,908 +0,0 @@ -package widgets - -import ( - "errors" - "fmt" - "io" - "net/url" - "os/exec" - "sort" - "strings" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/emersion/go-message/mail" - "github.com/gdamore/tcell/v2" - "github.com/google/shlex" - - "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~rjarry/aerc/lib" - "git.sr.ht/~rjarry/aerc/lib/crypto" - "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/log" - "git.sr.ht/~rjarry/aerc/models" - "git.sr.ht/~rjarry/aerc/worker/types" -) - -type Aerc struct { - accounts map[string]*AccountView - cmd func([]string, *config.AccountConfig, *models.MessageInfo) error - cmdHistory lib.History - complete func(cmd string) ([]string, string) - focused ui.Interactive - grid *ui.Grid - simulating int - statusbar *ui.Stack - statusline *StatusLine - pasting bool - pendingKeys []config.KeyStroke - prompts *ui.Stack - tabs *ui.Tabs - ui *ui.UI - beep func() error - dialog ui.DrawableInteractive - - Crypto crypto.Provider -} - -type Choice struct { - Key string - Text string - Command []string -} - -func NewAerc( - crypto crypto.Provider, - cmd func([]string, *config.AccountConfig, *models.MessageInfo) error, - complete func(cmd string) ([]string, string), cmdHistory lib.History, - deferLoop chan struct{}, -) *Aerc { - tabs := ui.NewTabs(config.Ui) - - statusbar := ui.NewStack(config.Ui) - statusline := &StatusLine{} - statusbar.Push(statusline) - - grid := ui.NewGrid().Rows([]ui.GridSpec{ - {Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, - {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, - {Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, - }).Columns([]ui.GridSpec{ - {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, - }) - grid.AddChild(tabs.TabStrip) - grid.AddChild(tabs.TabContent).At(1, 0) - grid.AddChild(statusbar).At(2, 0) - - aerc := &Aerc{ - accounts: make(map[string]*AccountView), - cmd: cmd, - cmdHistory: cmdHistory, - complete: complete, - grid: grid, - statusbar: statusbar, - statusline: statusline, - prompts: ui.NewStack(config.Ui), - tabs: tabs, - Crypto: crypto, - } - - statusline.SetAerc(aerc) - - for _, acct := range config.Accounts { - view, err := NewAccountView(aerc, acct, aerc, deferLoop) - if err != nil { - tabs.Add(errorScreen(err.Error()), acct.Name, nil) - } else { - aerc.accounts[acct.Name] = view - view.tab = tabs.Add(view, acct.Name, view.UiConfig()) - } - } - - if len(config.Accounts) == 0 { - wizard := NewAccountWizard(aerc) - wizard.Focus(true) - aerc.NewTab(wizard, "New account") - } - - tabs.Select(0) - - tabs.CloseTab = func(index int) { - tab := aerc.tabs.Get(index) - if tab == nil { - return - } - switch content := tab.Content.(type) { - case *AccountView: - return - case *AccountWizard: - return - default: - aerc.RemoveTab(content, true) - } - } - - aerc.showConfigWarnings() - - return aerc -} - -func (aerc *Aerc) showConfigWarnings() { - var dialogs []ui.DrawableInteractive - - callback := func(string, error) { - aerc.CloseDialog() - if len(dialogs) > 0 { - d := dialogs[0] - dialogs = dialogs[1:] - aerc.AddDialog(d) - } - } - - for _, w := range config.Warnings { - dialogs = append(dialogs, NewSelectorDialog( - w.Title, w.Body, []string{"OK"}, 0, - aerc.SelectedAccountUiConfig(), - callback, - )) - } - - callback("", nil) -} - -func (aerc *Aerc) OnBeep(f func() error) { - aerc.beep = f -} - -func (aerc *Aerc) Beep() { - if aerc.beep == nil { - log.Warnf("should beep, but no beeper") - return - } - if err := aerc.beep(); err != nil { - log.Errorf("tried to beep, but could not: %v", err) - } -} - -func (aerc *Aerc) HandleMessage(msg types.WorkerMessage) { - if acct, ok := aerc.accounts[msg.Account()]; ok { - acct.onMessage(msg) - } -} - -func (aerc *Aerc) Invalidate() { - ui.Invalidate() -} - -func (aerc *Aerc) Focus(focus bool) { - // who cares -} - -func (aerc *Aerc) Draw(ctx *ui.Context) { - if len(aerc.prompts.Children()) > 0 { - previous := aerc.focused - prompt := aerc.prompts.Pop().(*ExLine) - prompt.finish = func() { - aerc.statusbar.Pop() - aerc.focus(previous) - } - - aerc.statusbar.Push(prompt) - aerc.focus(prompt) - } - aerc.grid.Draw(ctx) - if aerc.dialog != nil { - if w, h := ctx.Width(), ctx.Height(); w > 8 && h > 4 { - if d, ok := aerc.dialog.(Dialog); ok { - start, height := d.ContextHeight() - aerc.dialog.Draw( - ctx.Subcontext(4, start(h), - w-8, height(h))) - } else { - aerc.dialog.Draw(ctx.Subcontext(4, h/2-2, w-8, 4)) - } - } - } -} - -func (aerc *Aerc) HumanReadableBindings() []string { - var result []string - binds := aerc.getBindings() - format := func(s string) string { - return strings.ReplaceAll(s, "%", "%%") - } - fmtStr := "%10s %s" - for _, bind := range binds.Bindings { - result = append(result, fmt.Sprintf(fmtStr, - format(config.FormatKeyStrokes(bind.Input)), - format(config.FormatKeyStrokes(bind.Output)), - )) - } - if binds.Globals && config.Binds.Global != nil { - for _, bind := range config.Binds.Global.Bindings { - result = append(result, fmt.Sprintf(fmtStr+" (Globals)", - format(config.FormatKeyStrokes(bind.Input)), - format(config.FormatKeyStrokes(bind.Output)), - )) - } - } - result = append(result, fmt.Sprintf(fmtStr, - "$ex", - fmt.Sprintf("'%c'", binds.ExKey.Rune), - )) - result = append(result, fmt.Sprintf(fmtStr, - "Globals", - fmt.Sprintf("%v", binds.Globals), - )) - sort.Strings(result) - return result -} - -func (aerc *Aerc) getBindings() *config.KeyBindings { - selectedAccountName := "" - if aerc.SelectedAccount() != nil { - selectedAccountName = aerc.SelectedAccount().acct.Name - } - switch view := aerc.SelectedTabContent().(type) { - case *AccountView: - binds := config.Binds.MessageList.ForAccount(selectedAccountName) - return binds.ForFolder(view.SelectedDirectory()) - case *AccountWizard: - return config.Binds.AccountWizard - case *Composer: - switch view.Bindings() { - case "compose::editor": - return config.Binds.ComposeEditor.ForAccount( - selectedAccountName) - case "compose::review": - return config.Binds.ComposeReview.ForAccount( - selectedAccountName) - default: - return config.Binds.Compose.ForAccount( - selectedAccountName) - } - case *MessageViewer: - switch view.Bindings() { - case "view::passthrough": - return config.Binds.MessageViewPassthrough.ForAccount( - selectedAccountName) - default: - return config.Binds.MessageView.ForAccount( - selectedAccountName) - } - case *Terminal: - return config.Binds.Terminal - default: - return config.Binds.Global - } -} - -func (aerc *Aerc) simulate(strokes []config.KeyStroke) { - aerc.pendingKeys = []config.KeyStroke{} - aerc.simulating += 1 - for _, stroke := range strokes { - simulated := tcell.NewEventKey( - stroke.Key, stroke.Rune, tcell.ModNone) - aerc.Event(simulated) - } - aerc.simulating -= 1 - // If we are still focused on the exline, turn on tab complete - if exline, ok := aerc.focused.(*ExLine); ok { - exline.TabComplete(func(cmd string) ([]string, string) { - return aerc.complete(cmd) - }) - // send tab to text input to trigger completion - exline.Event(tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)) - } -} - -func (aerc *Aerc) Event(event tcell.Event) bool { - if aerc.dialog != nil { - return aerc.dialog.Event(event) - } - - if aerc.focused != nil { - return aerc.focused.Event(event) - } - - switch event := event.(type) { - case *tcell.EventKey: - // If we are in a bracketed paste, don't process the keys for - // bindings - if aerc.pasting { - interactive, ok := aerc.SelectedTabContent().(ui.Interactive) - if ok { - return interactive.Event(event) - } - return false - } - aerc.statusline.Expire() - aerc.pendingKeys = append(aerc.pendingKeys, config.KeyStroke{ - Modifiers: event.Modifiers(), - Key: event.Key(), - Rune: event.Rune(), - }) - ui.Invalidate() - bindings := aerc.getBindings() - incomplete := false - result, strokes := bindings.GetBinding(aerc.pendingKeys) - switch result { - case config.BINDING_FOUND: - aerc.simulate(strokes) - return true - case config.BINDING_INCOMPLETE: - incomplete = true - case config.BINDING_NOT_FOUND: - } - if bindings.Globals { - result, strokes = config.Binds.Global.GetBinding(aerc.pendingKeys) - switch result { - case config.BINDING_FOUND: - aerc.simulate(strokes) - return true - case config.BINDING_INCOMPLETE: - incomplete = true - case config.BINDING_NOT_FOUND: - } - } - if !incomplete { - aerc.pendingKeys = []config.KeyStroke{} - exKey := bindings.ExKey - if aerc.simulating > 0 { - // Keybindings still use : even if you change the ex key - exKey = config.Binds.Global.ExKey - } - if aerc.isExKey(event, exKey) { - aerc.BeginExCommand("") - return true - } - interactive, ok := aerc.SelectedTabContent().(ui.Interactive) - if ok { - return interactive.Event(event) - } - return false - } - case *tcell.EventMouse: - x, y := event.Position() - aerc.grid.MouseEvent(x, y, event) - return true - case *tcell.EventPaste: - if event.Start() { - aerc.pasting = true - } - if event.End() { - aerc.pasting = false - } - interactive, ok := aerc.SelectedTabContent().(ui.Interactive) - if ok { - return interactive.Event(event) - } - return false - } - return false -} - -func (aerc *Aerc) SelectedAccount() *AccountView { - return aerc.account(aerc.SelectedTabContent()) -} - -func (aerc *Aerc) Account(name string) (*AccountView, error) { - if acct, ok := aerc.accounts[name]; ok { - return acct, nil - } - return nil, fmt.Errorf("account <%s> not found", name) -} - -func (aerc *Aerc) PrevAccount() (*AccountView, error) { - cur := aerc.SelectedAccount() - if cur == nil { - return nil, fmt.Errorf("no account selected, cannot get prev") - } - for i, conf := range config.Accounts { - if conf.Name == cur.Name() { - i -= 1 - if i == -1 { - i = len(config.Accounts) - 1 - } - conf = config.Accounts[i] - return aerc.Account(conf.Name) - } - } - return nil, fmt.Errorf("no prev account") -} - -func (aerc *Aerc) NextAccount() (*AccountView, error) { - cur := aerc.SelectedAccount() - if cur == nil { - return nil, fmt.Errorf("no account selected, cannot get next") - } - for i, conf := range config.Accounts { - if conf.Name == cur.Name() { - i += 1 - if i == len(config.Accounts) { - i = 0 - } - conf = config.Accounts[i] - return aerc.Account(conf.Name) - } - } - return nil, fmt.Errorf("no next account") -} - -func (aerc *Aerc) AccountNames() []string { - results := make([]string, 0) - for name := range aerc.accounts { - results = append(results, name) - } - return results -} - -func (aerc *Aerc) account(d ui.Drawable) *AccountView { - switch tab := d.(type) { - case *AccountView: - return tab - case *MessageViewer: - return tab.SelectedAccount() - case *Composer: - return tab.Account() - } - return nil -} - -func (aerc *Aerc) SelectedAccountUiConfig() *config.UIConfig { - acct := aerc.SelectedAccount() - if acct == nil { - return config.Ui - } - return acct.UiConfig() -} - -func (aerc *Aerc) SelectedTabContent() ui.Drawable { - tab := aerc.tabs.Selected() - if tab == nil { - return nil - } - return tab.Content -} - -func (aerc *Aerc) SelectedTab() *ui.Tab { - return aerc.tabs.Selected() -} - -func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab { - uiConf := config.Ui - if acct := aerc.account(clickable); acct != nil { - uiConf = acct.UiConfig() - } - tab := aerc.tabs.Add(clickable, name, uiConf) - aerc.UpdateStatus() - return tab -} - -func (aerc *Aerc) RemoveTab(tab ui.Drawable, closeContent bool) { - aerc.tabs.Remove(tab) - aerc.UpdateStatus() - if content, ok := tab.(ui.Closeable); ok && closeContent { - content.Close() - } -} - -func (aerc *Aerc) ReplaceTab(tabSrc ui.Drawable, tabTarget ui.Drawable, name string, closeSrc bool) { - aerc.tabs.Replace(tabSrc, tabTarget, name) - if content, ok := tabSrc.(ui.Closeable); ok && closeSrc { - content.Close() - } -} - -func (aerc *Aerc) MoveTab(i int, relative bool) { - aerc.tabs.MoveTab(i, relative) -} - -func (aerc *Aerc) PinTab() { - aerc.tabs.PinTab() -} - -func (aerc *Aerc) UnpinTab() { - aerc.tabs.UnpinTab() -} - -func (aerc *Aerc) NextTab() { - aerc.tabs.NextTab() -} - -func (aerc *Aerc) PrevTab() { - aerc.tabs.PrevTab() -} - -func (aerc *Aerc) SelectTab(name string) bool { - ok := aerc.tabs.SelectName(name) - if ok { - aerc.UpdateStatus() - } - return ok -} - -func (aerc *Aerc) SelectTabIndex(index int) bool { - ok := aerc.tabs.Select(index) - if ok { - aerc.UpdateStatus() - } - return ok -} - -func (aerc *Aerc) TabNames() []string { - return aerc.tabs.Names() -} - -func (aerc *Aerc) SelectPreviousTab() bool { - return aerc.tabs.SelectPrevious() -} - -func (aerc *Aerc) UpdateStatus() { - if acct := aerc.SelectedAccount(); acct != nil { - aerc.statusline.Update(acct) - } else { - aerc.statusline.Clear() - } -} - -func (aerc *Aerc) SetError(err string) { - aerc.statusline.SetError(err) -} - -func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage { - return aerc.statusline.Push(text, expiry) -} - -func (aerc *Aerc) PushError(text string) *StatusMessage { - return aerc.statusline.PushError(text) -} - -func (aerc *Aerc) PushWarning(text string) *StatusMessage { - return aerc.statusline.PushWarning(text) -} - -func (aerc *Aerc) PushSuccess(text string) *StatusMessage { - return aerc.statusline.PushSuccess(text) -} - -func (aerc *Aerc) focus(item ui.Interactive) { - if aerc.focused == item { - return - } - if aerc.focused != nil { - aerc.focused.Focus(false) - } - aerc.focused = item - interactive, ok := aerc.SelectedTabContent().(ui.Interactive) - if item != nil { - item.Focus(true) - if ok { - interactive.Focus(false) - } - } else if ok { - interactive.Focus(true) - } -} - -func (aerc *Aerc) BeginExCommand(cmd string) { - previous := aerc.focused - var tabComplete func(string) ([]string, string) - if aerc.simulating != 0 { - // Don't try to draw completions for simulated events - tabComplete = nil - } else { - tabComplete = func(cmd string) ([]string, string) { - return aerc.complete(cmd) - } - } - exline := NewExLine(cmd, func(cmd string) { - parts, err := shlex.Split(cmd) - if err != nil { - aerc.PushError(err.Error()) - } - err = aerc.cmd(parts, nil, nil) - if err != nil { - aerc.PushError(err.Error()) - } - // only add to history if this is an unsimulated command, - // ie one not executed from a keybinding - if aerc.simulating == 0 { - aerc.cmdHistory.Add(cmd) - } - }, func() { - aerc.statusbar.Pop() - aerc.focus(previous) - }, tabComplete, aerc.cmdHistory) - aerc.statusbar.Push(exline) - aerc.focus(exline) -} - -func (aerc *Aerc) PushPrompt(prompt *ExLine) { - aerc.prompts.Push(prompt) -} - -func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) { - p := NewPrompt(prompt, func(text string) { - if text != "" { - cmd = append(cmd, text) - } - err := aerc.cmd(cmd, nil, nil) - if err != nil { - aerc.PushError(err.Error()) - } - }, func(cmd string) ([]string, string) { - return nil, "" // TODO: completions - }) - aerc.prompts.Push(p) -} - -func (aerc *Aerc) RegisterChoices(choices []Choice) { - cmds := make(map[string][]string) - texts := []string{} - for _, c := range choices { - text := fmt.Sprintf("[%s] %s", c.Key, c.Text) - if strings.Contains(c.Text, c.Key) { - text = strings.Replace(c.Text, c.Key, "["+c.Key+"]", 1) - } - texts = append(texts, text) - cmds[c.Key] = c.Command - } - prompt := strings.Join(texts, ", ") + "? " - p := NewPrompt(prompt, func(text string) { - cmd, ok := cmds[text] - if !ok { - return - } - err := aerc.cmd(cmd, nil, nil) - if err != nil { - aerc.PushError(err.Error()) - } - }, func(cmd string) ([]string, string) { - return nil, "" // TODO: completions - }) - aerc.prompts.Push(p) -} - -func (aerc *Aerc) Mailto(addr *url.URL) error { - var subject string - var body string - var acctName string - var attachments []string - h := &mail.Header{} - to, err := mail.ParseAddressList(addr.Opaque) - if err != nil && addr.Opaque != "" { - return fmt.Errorf("Could not parse to: %w", err) - } - h.SetAddressList("to", to) - template := config.Templates.NewMessage - for key, vals := range addr.Query() { - switch strings.ToLower(key) { - case "account": - acctName = strings.Join(vals, "") - case "bcc": - list, err := mail.ParseAddressList(strings.Join(vals, ",")) - if err != nil { - break - } - h.SetAddressList("Bcc", list) - case "body": - body = strings.Join(vals, "\n") - case "cc": - list, err := mail.ParseAddressList(strings.Join(vals, ",")) - if err != nil { - break - } - h.SetAddressList("Cc", list) - case "in-reply-to": - for i, msgID := range vals { - if len(msgID) > 1 && msgID[0] == '<' && - msgID[len(msgID)-1] == '>' { - vals[i] = msgID[1 : len(msgID)-1] - } - } - h.SetMsgIDList("In-Reply-To", vals) - case "subject": - subject = strings.Join(vals, ",") - h.SetText("Subject", subject) - case "template": - template = strings.Join(vals, "") - log.Tracef("template set to %s", template) - case "attach": - for _, path := range vals { - // remove a potential file:// prefix. - attachments = append(attachments, strings.TrimPrefix(path, "file://")) - } - default: - // any other header gets ignored on purpose to avoid control headers - // being injected - } - } - - acct := aerc.SelectedAccount() - if acctName != "" { - if a, ok := aerc.accounts[acctName]; ok && a != nil { - acct = a - } - } - - if acct == nil { - return errors.New("No account selected") - } - - defer ui.Invalidate() - - composer, err := NewComposer(aerc, acct, - acct.AccountConfig(), acct.Worker(), - config.Compose.EditHeaders, template, h, nil, - strings.NewReader(body)) - if err != nil { - return err - } - composer.FocusEditor("subject") - title := "New email" - if subject != "" { - title = subject - composer.FocusTerminal() - } - if to == nil { - composer.FocusEditor("to") - } - composer.Tab = aerc.NewTab(composer, title) - - for _, file := range attachments { - composer.AddAttachment(file) - } - return nil -} - -func (aerc *Aerc) Mbox(source string) error { - acctConf := config.AccountConfig{} - if selectedAcct := aerc.SelectedAccount(); selectedAcct != nil { - acctConf = *selectedAcct.acct - info := fmt.Sprintf("Loading outgoing mbox mail settings from account [%s]", selectedAcct.Name()) - aerc.PushStatus(info, 10*time.Second) - log.Debugf(info) - } else { - acctConf.From = &mail.Address{Address: "user@localhost"} - } - acctConf.Name = "mbox" - acctConf.Source = source - acctConf.Default = "INBOX" - acctConf.Archive = "Archive" - acctConf.Postpone = "Drafts" - acctConf.CopyTo = "Sent" - - defer ui.Invalidate() - - mboxView, err := NewAccountView(aerc, &acctConf, aerc, nil) - if err != nil { - aerc.NewTab(errorScreen(err.Error()), acctConf.Name) - } else { - aerc.accounts[acctConf.Name] = mboxView - aerc.NewTab(mboxView, acctConf.Name) - } - return nil -} - -func (aerc *Aerc) Command(args []string) error { - defer ui.Invalidate() - return aerc.cmd(args, nil, nil) -} - -func (aerc *Aerc) CloseBackends() error { - var returnErr error - for _, acct := range aerc.accounts { - var raw interface{} = acct.worker.Backend - c, ok := raw.(io.Closer) - if !ok { - continue - } - err := c.Close() - if err != nil { - returnErr = err - log.Errorf("Closing backend failed for %s: %v", acct.Name(), err) - } - } - return returnErr -} - -func (aerc *Aerc) AddDialog(d ui.DrawableInteractive) { - aerc.dialog = d - aerc.Invalidate() -} - -func (aerc *Aerc) CloseDialog() { - aerc.dialog = nil - aerc.Invalidate() -} - -func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) { - chText = make(chan string, 1) - chErr = make(chan error, 1) - getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) { - defer func() { - close(chErr) - close(chText) - aerc.CloseDialog() - }() - if err != nil { - chErr <- err - return - } - chErr <- nil - chText <- pw - }) - aerc.AddDialog(getPasswd) - - return -} - -func (aerc *Aerc) Initialize(ui *ui.UI) { - aerc.ui = ui -} - -func (aerc *Aerc) DecryptKeys(keys []openpgp.Key, symmetric bool) (b []byte, err error) { - for _, key := range keys { - ident := key.Entity.PrimaryIdentity() - chPass, chErr := aerc.GetPassword("Decrypt PGP private key", - fmt.Sprintf("Enter password for %s (%8X)\nPress <ESC> to cancel", - ident.Name, key.PublicKey.KeyId)) - - for err := range chErr { - if err != nil { - return nil, err - } - pass := <-chPass - err = key.PrivateKey.Decrypt([]byte(pass)) - return nil, err - } - } - return nil, err -} - -// errorScreen is a widget that draws an error in the middle of the context -func errorScreen(s string) ui.Drawable { - errstyle := config.Ui.GetStyle(config.STYLE_ERROR) - text := ui.NewText(s, errstyle).Strategy(ui.TEXT_CENTER) - grid := ui.NewGrid().Rows([]ui.GridSpec{ - {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, - {Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, - {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, - }).Columns([]ui.GridSpec{ - {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, - }) - grid.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(0, 0) - grid.AddChild(text).At(1, 0) - grid.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(2, 0) - return grid -} - -func (aerc *Aerc) isExKey(event *tcell.EventKey, exKey config.KeyStroke) bool { - if event.Key() == tcell.KeyRune { - // Compare runes if it's a KeyRune - return event.Modifiers() == exKey.Modifiers && event.Rune() == exKey.Rune - } - return event.Modifiers() == exKey.Modifiers && event.Key() == exKey.Key -} - -// CmdFallbackSearch checks cmds for the first executable availabe in PATH. An error is -// returned if none are found -func (aerc *Aerc) CmdFallbackSearch(cmds []string) (string, error) { - var tried []string - for _, cmd := range cmds { - if cmd == "" { - continue - } - params := strings.Split(cmd, " ") - _, err := exec.LookPath(params[0]) - if err != nil { - tried = append(tried, cmd) - warn := fmt.Sprintf("cmd '%s' not found in PATH, using fallback", cmd) - aerc.PushWarning(warn) - continue - } - return cmd, nil - } - return "", fmt.Errorf("no command found in PATH: %s", tried) -} |