1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
|
package main
import (
"context"
"errors"
"fmt"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
"git.sr.ht/~rjarry/go-opt/v2"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"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/hooks"
"git.sr.ht/~rjarry/aerc/lib/ipc"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pinentry"
"git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
_ "git.sr.ht/~rjarry/aerc/commands/account"
_ "git.sr.ht/~rjarry/aerc/commands/compose"
_ "git.sr.ht/~rjarry/aerc/commands/msg"
_ "git.sr.ht/~rjarry/aerc/commands/msgview"
_ "git.sr.ht/~rjarry/aerc/commands/patch"
)
func execCommand(
cmdline string,
acct *config.AccountConfig, msg *models.MessageInfo,
) error {
cmdline, cmd, err := commands.ResolveCommand(cmdline, acct, msg)
if err != nil {
return err
}
err = commands.ExecuteCommand(cmd, cmdline)
if errors.As(err, new(commands.ErrorExit)) {
ui.Exit()
return nil
}
return err
}
func getCompletions(ctx context.Context, cmdline string) ([]opt.Completion, string) {
// complete template terms
if options, prefix, ok := commands.GetTemplateCompletion(cmdline); ok {
sort.Strings(options)
completions := make([]opt.Completion, 0, len(options))
for _, o := range options {
completions = append(completions, opt.Completion{
Value: o,
Description: "Template",
})
}
return completions, prefix
}
args := opt.LexArgs(cmdline)
if args.Count() < 2 && args.TrailingSpace() == "" {
// complete command names
var completions []opt.Completion
for _, cmd := range commands.ActiveCommands() {
for _, alias := range cmd.Aliases() {
if strings.HasPrefix(alias, cmdline) {
completions = append(completions, opt.Completion{
Value: alias + " ",
Description: cmd.Description(),
})
}
}
}
sort.Slice(completions, func(i, j int) bool {
return completions[i].Value < completions[j].Value
})
return completions, ""
}
// complete command arguments
_, cmd, err := commands.ExpandAbbreviations(args.Arg(0))
if err != nil {
return nil, cmdline
}
return commands.GetCompletions(cmd, args)
}
// set at build time
var (
Version string
Date string
)
func buildInfo() string {
info := Version
if soVersion, hasNotmuch := lib.NotmuchVersion(); hasNotmuch {
info += fmt.Sprintf(" +notmuch-%s", soVersion)
}
info += fmt.Sprintf(" (%s %s %s %s)",
runtime.Version(), runtime.GOARCH, runtime.GOOS, Date)
return info
}
type Opts struct {
Help bool `opt:"-h,--help" action:"ShowHelp"`
Version bool `opt:"-v,--version" action:"ShowVersion"`
Accounts []string `opt:"-a,--account" action:"ParseAccounts" metavar:"<name>"`
ConfAerc string `opt:"-C,--aerc-conf" metavar:"<file>"`
ConfAccounts string `opt:"-A,--accounts-conf" metavar:"<file>"`
ConfBinds string `opt:"-B,--binds-conf" metavar:"<file>"`
NoIPC bool `opt:"-I,--no-ipc"`
Command []string `opt:"..." required:"false" metavar:"mailto:<address> | mbox:<file> | :<command...>"`
}
func (o *Opts) ShowHelp(arg string) error {
fmt.Println("Usage: " + opt.NewCmdSpec(os.Args[0], o).Usage())
fmt.Print(`
Aerc is an email client for your terminal.
Options:
-h, --help Show this help message and exit.
-v, --version Print version information.
-a <name>, --account <name>
Load only the named account, as opposed to all configured
accounts. It can also be a comma separated list of names.
This option may be specified multiple times. The account
order will be preserved.
-C <file>, --aerc-conf <file>
Path to configuration file to be used instead of the default.
-A <file>, --accounts-conf <file>
Path to configuration file to be used instead of the default.
-B <file>, --binds-conf <file>
Path to configuration file to be used instead of the default.
-I, --no-ipc Run any commands in this aerc instance, and don't create a
socket for other aerc instances to communicate with this one.
mailto:<address> Open the composer with the address(es) in the To field.
If aerc is already running, the composer is started in
this instance, otherwise aerc will be started.
mbox:<file> Open the specified mbox file as a virtual temporary account.
:<command...> Run an aerc command as you would in Ex-Mode.
`)
os.Exit(0)
return nil
}
func (o *Opts) ShowVersion(arg string) error {
fmt.Println("aerc " + log.BuildInfo)
os.Exit(0)
return nil
}
func (o *Opts) ParseAccounts(arg string) error {
o.Accounts = append(o.Accounts, strings.Split(arg, ",")...)
return nil
}
func die(format string, args ...any) {
fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...)
os.Exit(1)
}
func main() {
defer log.PanicHandler()
log.BuildInfo = buildInfo()
var opts Opts
args := opt.QuoteArgs(os.Args...)
err := opt.ArgsToStruct(args, &opts)
if err != nil {
die("%s", err)
}
switch {
case len(opts.Command) == 0:
break
case strings.HasPrefix(opts.Command[0], ":"):
case strings.HasPrefix(opts.Command[0], "mailto:"):
case strings.HasPrefix(opts.Command[0], "mbox:"):
break
default:
die("unknown argument: %s", opts.Command[0])
}
err = config.LoadConfigFromFile(
nil, opts.Accounts, opts.ConfAerc, opts.ConfBinds, opts.ConfAccounts,
)
if err != nil {
die("%s", err)
}
noIPC := opts.NoIPC || config.General.DisableIPC
if len(opts.Command) > 0 && !noIPC &&
!(config.General.DisableIPCMailto && strings.HasPrefix(opts.Command[0], "mailto:")) &&
!(config.General.DisableIPCMbox && strings.HasPrefix(opts.Command[0], "mbox:")) {
response, err := ipc.ConnectAndExec(opts.Command)
if err == nil {
if response.Error != "" {
fmt.Printf("response: %s\n", response.Error)
}
return // other aerc instance takes over
}
// continue with setting up a new aerc instance and retry after init
}
log.Infof("Starting up version %s", log.BuildInfo)
deferLoop := make(chan struct{})
c := crypto.New()
err = c.Init()
if err != nil {
log.Warnf("failed to initialise crypto interface: %v", err)
}
defer c.Close()
app.Init(c, execCommand, getCompletions, &commands.CmdHistory, deferLoop)
err = ui.Initialize(app.Drawable())
if err != nil {
panic(err)
}
defer ui.Close()
log.UICleanup = func() {
ui.Close()
}
close(deferLoop)
config.EnablePinentry = pinentry.Enable
config.DisablePinentry = pinentry.Disable
config.SetPinentryEnv = pinentry.SetCmdEnv
startup, startupDone := context.WithCancel(context.Background())
if !noIPC {
as, err := ipc.StartServer(app.IPCHandler(), startup)
if err != nil {
log.Warnf("Failed to start Unix server: %v", err)
} else {
defer as.Close()
}
}
// set the aerc version so that we can use it in the template funcs
templates.SetVersion(Version)
templates.SetExecPath(config.SearchDirs)
endStartup := func() {
startupDone()
if len(opts.Command) == 0 {
return
}
// Retry execution. Since IPC has already failed, we know no
// other aerc instance is running (or IPC was explicitly
// disabled); run the command directly.
err := app.Command(opts.Command)
if err != nil {
// no other aerc instance is running, so let
// this one stay running but show the error
errMsg := fmt.Sprintf("Startup command (%s) failed: %s\n",
strings.Join(opts.Command, " "), err)
log.Errorf(errMsg)
app.PushError(errMsg)
}
}
go func() {
defer log.PanicHandler()
err := hooks.RunHook(&hooks.AercStartup{Version: Version})
if err != nil {
msg := fmt.Sprintf("aerc-startup hook: %s", err)
app.PushError(msg)
}
}()
defer func(start time.Time) {
err := hooks.RunHook(
&hooks.AercShutdown{Lifetime: time.Since(start)},
)
if err != nil {
log.Errorf("aerc-shutdown hook: %s", err)
}
}(time.Now())
var once sync.Once
loop:
for {
select {
case event := <-ui.Events:
ui.HandleEvent(event)
case msg := <-types.WorkerMessages:
app.HandleMessage(msg)
// XXX: The app may not be 100% ready at this point.
// The issue is that there is no real way to tell when
// it will be ready. And in some cases, it may never be.
// At least, we can be confident that accepting IPC
// commands will not crash the whole process.
once.Do(endStartup)
case callback := <-ui.Callbacks:
callback()
case <-ui.Redraw:
ui.Render()
case <-ui.SuspendQueue:
err = ui.Suspend()
if err != nil {
app.PushError(fmt.Sprintf("suspend: %s", err))
}
case <-ui.Quit:
err = app.CloseBackends()
if err != nil {
log.Warnf("failed to close backends: %v", err)
}
break loop
}
}
}
|