~sircmpwn/aerc

7a489cb0011a34a68d3e77d0174076857cc37902 — Drew DeVault 2 years ago b3a6686
Add Unix socket for communicating with aerc
4 files changed, 139 insertions(+), 0 deletions(-)

M aerc.go
A lib/socket.go
M widgets/aerc.go
M widgets/compose.go
M aerc.go => aerc.go +10 -0
@@ 18,6 18,7 @@ import (
	"git.sr.ht/~sircmpwn/aerc/commands/msgview"
	"git.sr.ht/~sircmpwn/aerc/commands/terminal"
	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib"
	libui "git.sr.ht/~sircmpwn/aerc/lib/ui"
	"git.sr.ht/~sircmpwn/aerc/widgets"
)


@@ 149,6 150,15 @@ func main() {
	}
	defer ui.Close()

	logger.Println("Starting Unix server")
	as, err := lib.StartServer(logger)
	if err != nil {
		logger.Printf("Failed to start Unix server: %v (non-fatal)", err)
	} else {
		defer as.Close()
		as.OnMailto = aerc.Mailto
	}

	for !ui.ShouldExit() {
		for aerc.Tick() {
			// Continue updating our internal state

A lib/socket.go => lib/socket.go +82 -0
@@ 0,0 1,82 @@
package lib

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"net/url"
	"path"
	"strings"
	"sync/atomic"
	"time"

	"github.com/kyoh86/xdg"
)

type AercServer struct {
	logger   *log.Logger
	listener net.Listener
	OnMailto func(addr *url.URL) error
}

func StartServer(logger *log.Logger) (*AercServer, error) {
	sockpath := path.Join(xdg.RuntimeDir(), "aerc.sock")
	l, err := net.Listen("unix", sockpath)
	if err != nil {
		return nil, err
	}
	as := &AercServer{
		logger:   logger,
		listener: l,
	}
	// TODO: stash clients and close them on exit... bleh racey
	go func() {
		for {
			conn, err := l.Accept()
			if err != nil {
				// TODO: Something more useful, in some cases, on wednesdays,
				// after 2 PM, I guess?
				as.logger.Println("Closing Unix server: %v", err)
				return
			}
			go as.handleClient(conn)
		}
	}()
	return as, nil
}

func (as *AercServer) Close() {
	as.listener.Close()
}

var lastId int64 = 0 // access via atomic

func (as *AercServer) handleClient(conn net.Conn) {
	clientId := atomic.AddInt64(&lastId, 1)
	as.logger.Printf("Accepted Unix connection %d", clientId)
	scanner := bufio.NewScanner(conn)
	conn.SetDeadline(time.Now().Add(1 * time.Minute))
	for scanner.Scan() {
		conn.SetDeadline(time.Now().Add(1 * time.Minute))
		msg := scanner.Text()
		if !strings.ContainsRune(msg, ':') {
			conn.Write([]byte("error: invalid command\n"))
		}
		as.logger.Printf("unix:%d: got message %s", clientId, msg)
		prefix := msg[:strings.IndexRune(msg, ':')]
		switch prefix {
		case "mailto":
			mailto, err := url.Parse(msg)
			if err != nil {
				conn.Write([]byte(fmt.Sprintf("error: %v\n", err)))
				break
			}
			if as.OnMailto != nil {
				err = as.OnMailto(mailto)
			}
			conn.Write([]byte(fmt.Sprintf("result: %v\n", err)))
		}
	}
	as.logger.Printf("Closed Unix connection %d", clientId)
}

M widgets/aerc.go => widgets/aerc.go +40 -0
@@ 1,7 1,10 @@
package widgets

import (
	"errors"
	"log"
	"net/url"
	"strings"
	"time"

	"github.com/gdamore/tcell"


@@ 302,3 305,40 @@ func (aerc *Aerc) BeginExCommand() {
	aerc.statusbar.Push(exline)
	aerc.focus(exline)
}

func (aerc *Aerc) Mailto(addr *url.URL) error {
	acct := aerc.SelectedAccount()
	if acct == nil {
		return errors.New("No account selected")
	}
	defaults := make(map[string]string)
	defaults["To"] = addr.Opaque
	headerMap := map[string]string{
		"cc":          "Cc",
		"in-reply-to": "In-Reply-To",
		"subject":     "Subject",
	}
	for key, vals := range addr.Query() {
		if header, ok := headerMap[strings.ToLower(key)]; ok {
			defaults[header] = strings.Join(vals, ",")
		}
	}
	composer := NewComposer(aerc.Config(),
		acct.AccountConfig(), acct.Worker()).Defaults(defaults)
	composer.FocusSubject()
	title := "New email"
	if subj, ok := defaults["Subject"]; ok {
		title = subj
		composer.FocusTerminal()
	}
	tab := aerc.NewTab(composer, title)
	composer.OnSubjectChange(func(subject string) {
		if subject == "" {
			tab.Name = "New email"
		} else {
			tab.Name = subject
		}
		tab.Content.Invalidate()
	})
	return nil
}

M widgets/compose.go => widgets/compose.go +7 -0
@@ 138,6 138,13 @@ func (c *Composer) FocusTerminal() *Composer {
	return c
}

func (c *Composer) FocusSubject() *Composer {
	c.focusable[c.focused].Focus(false)
	c.focused = 2
	c.focusable[c.focused].Focus(true)
	return c
}

func (c *Composer) OnSubjectChange(fn func(subject string)) {
	c.headers.subject.OnChange(func() {
		fn(c.headers.subject.input.String())