aerc

Working clone of aerc-mail.org
git clone git://git.danielmoch.com/aerc.git
Log | Files | Refs | README | LICENSE

commit fa04a1e036a418258451466d99de34a9546a9965
parent 2958579ee75359dacfabb6d1b36664468011113c
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sat, 30 Mar 2019 14:12:04 -0400

Add basic message viewer mockup

Diffstat:
Maerc.go | 2+-
Mcommands/account/pipe.go | 3+--
Mcommands/term.go | 5++---
Mcommands/terminal/close.go | 6+++---
Acontrib/hldiff.py | 27+++++++++++++++++++++++++++
Mlib/ui/text.go | 10++++++++++
Mwidgets/aerc.go | 18++++++------------
Awidgets/msgviewer.go | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dwidgets/termhost.go | 52----------------------------------------------------
9 files changed, 257 insertions(+), 73 deletions(-)

diff --git a/aerc.go b/aerc.go @@ -24,7 +24,7 @@ func getCommands(selected libui.Drawable) []*commands.Commands { account.AccountCommands, commands.GlobalCommands, } - case *widgets.TermHost: + case *widgets.Terminal: return []*commands.Commands{ terminal.TerminalCommands, commands.GlobalCommands, diff --git a/commands/account/pipe.go b/commands/account/pipe.go @@ -41,12 +41,11 @@ func Pipe(aerc *widgets.Aerc, args []string) error { Color(tcell.ColorDefault, tcell.ColorRed) return } - host := widgets.NewTermHost(term, aerc.Config()) name := msg.Subject() if len(name) > 12 { name = name[:12] } - aerc.NewTab(host, args[1] + " <" + name) + aerc.NewTab(term, args[1] + " <" + name) term.OnClose = func(err error) { if err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). diff --git a/commands/term.go b/commands/term.go @@ -26,8 +26,7 @@ func Term(aerc *widgets.Aerc, args []string) error { if err != nil { return err } - host := widgets.NewTermHost(term, aerc.Config()) - tab := aerc.NewTab(host, args[1]) + tab := aerc.NewTab(term, args[1]) term.OnTitle = func(title string) { if title == "" { title = args[1] @@ -36,7 +35,7 @@ func Term(aerc *widgets.Aerc, args []string) error { tab.Content.Invalidate() } term.OnClose = func(err error) { - aerc.RemoveTab(host) + aerc.RemoveTab(term) if err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) diff --git a/commands/terminal/close.go b/commands/terminal/close.go @@ -14,11 +14,11 @@ func CommandClose(aerc *widgets.Aerc, args []string) error { if len(args) != 1 { return errors.New("Usage: close") } - thost, ok := aerc.SelectedTab().(*widgets.TermHost) + term, ok := aerc.SelectedTab().(*widgets.Terminal) if !ok { return errors.New("Error: not a terminal") } - thost.Terminal().Close(nil) - aerc.RemoveTab(thost) + term.Close(nil) + aerc.RemoveTab(term) return nil } diff --git a/contrib/hldiff.py b/contrib/hldiff.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +from colorama import Fore, Style +import sys +import re + +patch = sys.stdin.read().replace("\r\n", "\n") +stat_re = re.compile(r'(\+*)(\-*)') + +hit_diff = False +for line in patch.split("\n"): + if line.startswith("diff "): + hit_diff = True + print(line) + continue + if hit_diff: + if line.startswith("-"): + print(f"{Fore.RED}{line}{Style.RESET_ALL}") + elif line.startswith("+"): + print(f"{Fore.GREEN}{line}{Style.RESET_ALL}") + else: + print(line) + else: + if line.startswith(" ") and "|" in line and ("+" in line or "-" in line): + line = stat_re.sub( + f'{Fore.GREEN}\\1{Fore.RED}\\2{Style.RESET_ALL}', + line) + print(line) diff --git a/lib/ui/text.go b/lib/ui/text.go @@ -16,6 +16,7 @@ type Text struct { strategy uint fg tcell.Color bg tcell.Color + bold bool reverse bool onInvalidate func(d Drawable) } @@ -40,6 +41,12 @@ func (t *Text) Strategy(strategy uint) *Text { return t } +func (t *Text) Bold(bold bool) *Text { + t.bold = bold + t.Invalidate() + return t +} + func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text { t.fg = fg t.bg = bg @@ -63,6 +70,9 @@ func (t *Text) Draw(ctx *Context) { x = ctx.Width() - size } style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg) + if t.bold { + style = style.Bold(true) + } if t.reverse { style = style.Reverse(true) } diff --git a/widgets/aerc.go b/widgets/aerc.go @@ -39,19 +39,11 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, {libui.SIZE_WEIGHT, 1}, {libui.SIZE_EXACT, 1}, }).Columns([]libui.GridSpec{ - {libui.SIZE_EXACT, conf.Ui.SidebarWidth}, {libui.SIZE_WEIGHT, 1}, }) - grid.AddChild(statusbar).At(2, 1) - // Minor hack - grid.AddChild(libui.NewBordered( - libui.NewFill(' '), libui.BORDER_RIGHT)).At(2, 0) - - grid.AddChild(libui.NewText("aerc"). - Strategy(libui.TEXT_CENTER). - Reverse(true)) - grid.AddChild(tabs.TabStrip).At(0, 1) - grid.AddChild(tabs.TabContent).At(1, 0).Span(1, 2) + grid.AddChild(tabs.TabStrip) + grid.AddChild(tabs.TabContent).At(1, 0) + grid.AddChild(statusbar).At(2, 0) aerc := &Aerc{ accounts: make(map[string]*AccountView), @@ -70,6 +62,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, tabs.Add(view, acct.Name) } + tabs.Add(NewMessageViewer(), "[PATCH todo.sr.ht v2 …") + return aerc } @@ -99,7 +93,7 @@ func (aerc *Aerc) getBindings() *config.KeyBindings { switch aerc.SelectedTab().(type) { case *AccountView: return aerc.conf.Bindings.MessageList - case *TermHost: + case *Terminal: return aerc.conf.Bindings.Terminal default: return aerc.conf.Bindings.Global diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go @@ -0,0 +1,207 @@ +package widgets + +import ( + "bytes" + "io" + "os/exec" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" + + "git.sr.ht/~sircmpwn/aerc2/lib/ui" +) + +type MessageViewer struct { + grid *ui.Grid + term *Terminal +} + +var testMsg = `Makes the following changes to the Event type: + +* make 'user' and 'ticket' nullable since some events require it +* add 'by_user' and 'from_ticket' to enable mentions +* remove 'assinged_user' which is no longer used + +Ticket: https://todo.sr.ht/~sircmpwn/todo.sr.ht/156 +--- + tests/test_comments.py | 23 ++- + .../versions/75ff2f7624fd_new_event_fields.py | 142 ++++++++++++++++++ + todosrht/templates/events.html | 18 ++- + todosrht/templates/ticket.html | 31 +++- + todosrht/tickets.py | 14 +- + todosrht/types/event.py | 16 +- + 6 files changed, 207 insertions(+), 37 deletions(-) + create mode 100644 todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py + +diff --git a/tests/test_comments.py b/tests/test_comments.py +index 4b3161d..b85d751 100644 +--- a/tests/test_comments.py ++++ b/tests/test_comments.py +@@ -253,20 +253,25 @@ def test_notifications_and_events(mailbox): + # Check correct events are generated + comment_events = {e for e in ticket.events + if e.event_type == EventType.comment} +- user_events = {e for e in ticket.events ++ u1_events = {e for e in u1.events ++ if e.event_type == EventType.user_mentioned} ++ u2_events = {e for e in u2.events + if e.event_type == EventType.user_mentioned} + + assert len(comment_events) == 1 +- assert len(user_events) == 2 ++ assert len(u1_events) == 1 ++ assert len(u2_events) == 1 + +- u1_mention = next(e for e in user_events if e.user == u1) +- u2_mention = next(e for e in user_events if e.user == u2) ++ u1_mention = u1_events.pop() ++ u2_mention = u2_events.pop() + + assert u1_mention.comment == comment +- assert u1_mention.ticket == ticket ++ assert u1_mention.from_ticket == ticket ++ assert u1_mention.by_user == commenter + + assert u2_mention.comment == comment +- assert u2_mention.ticket == ticket ++ assert u2_mention.from_ticket == ticket ++ assert u2_mention.by_user == commenter + + assert len(t1.events) == 1 + assert len(t2.events) == 1 +@@ -276,10 +281,12 @@ def test_notifications_and_events(mailbox): + t2_mention = t2.events[0] + + assert t1_mention.comment == comment +- assert t1_mention.user == commenter ++ assert t1_mention.from_ticket == ticket ++ assert t1_mention.by_user == commenter + + assert t2_mention.comment == comment +- assert t2_mention.user == commenter ++ assert t2_mention.from_ticket == ticket ++ assert t2_mention.by_user == commenter + + def test_ticket_mention_pattern(): + def match(text): +diff --git a/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py +b/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py +new file mode 100644 +index 0000000..1c55bfe +--- /dev/null ++++ b/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py +@@ -0,0 +1,142 @@ ++"""Add new event fields and migrate data. ++ ++Also makes Event.ticket_id and Event.user_id nullable since some these fields ++can be empty for mention events. ++ ++Revision ID: 75ff2f7624fd ++Revises: c7146cb70d6b ++Create Date: 2019-03-28 16:26:18.714300 ++ ++""" ++ ++# revision identifiers, used by Alembic. ++revision = "75ff2f7624fd" ++down_revision = "c7146cb70d6b" +` + +func NewMessageViewer() *MessageViewer { + grid := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 3}, + {ui.SIZE_WEIGHT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + + headers := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 1}, + {ui.SIZE_EXACT, 1}, + {ui.SIZE_EXACT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + {ui.SIZE_WEIGHT, 1}, + }) + headers.AddChild( + &HeaderView{ + Name: "From", + Value: "Ivan Habunek <ivan@habunek.com>", + }).At(0, 0) + headers.AddChild( + &HeaderView{ + Name: "To", + Value: "~sircmpwn/sr.ht-dev@lists.sr.ht", + }).At(0, 1) + headers.AddChild( + &HeaderView{ + Name: "Subject", + Value: "[PATCH todo.sr.ht v2 1/3 Alter Event fields " + + "and migrate data]", + }).At(1, 0).Span(1, 2) + headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2) + + cmd := exec.Command("sh", "-c", "./contrib/hldiff.py | less -R") + pipe, _ := cmd.StdinPipe() + term, _ := NewTerminal(cmd) + term.OnStart = func() { + go func() { + reader := bytes.NewBufferString(testMsg) + io.Copy(pipe, reader) + pipe.Close() + }() + } + term.Focus(true) + + grid.AddChild(headers).At(0, 0) + grid.AddChild(term).At(1, 0) + return &MessageViewer{grid, term} +} + +func (mv *MessageViewer) Draw(ctx *ui.Context) { + mv.grid.Draw(ctx) +} + +func (mv *MessageViewer) Invalidate() { + mv.grid.Invalidate() +} + +func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) { + mv.grid.OnInvalidate(func(_ ui.Drawable) { + fn(mv) + }) +} + +func (mv *MessageViewer) Event(event tcell.Event) bool { + return mv.term.Event(event) +} + +func (mv *MessageViewer) Focus(focus bool) { + mv.term.Focus(focus) +} + +type HeaderView struct { + onInvalidate func(d ui.Drawable) + + Name string + Value string +} + +func (hv *HeaderView) Draw(ctx *ui.Context) { + size := runewidth.StringWidth(" " + hv.Name + " ") + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + style := tcell.StyleDefault.Reverse(true) + ctx.Printf(0, 0, style, " "+hv.Name+" ") + style = tcell.StyleDefault + ctx.Printf(size, 0, style, " "+hv.Value) +} + +func (hv *HeaderView) Invalidate() { + if hv.onInvalidate != nil { + hv.onInvalidate(hv) + } +} + +func (hv *HeaderView) OnInvalidate(fn func(d ui.Drawable)) { + hv.onInvalidate = fn +} diff --git a/widgets/termhost.go b/widgets/termhost.go @@ -1,52 +0,0 @@ -package widgets - -import ( - "github.com/gdamore/tcell" - - "git.sr.ht/~sircmpwn/aerc2/config" - "git.sr.ht/~sircmpwn/aerc2/lib/ui" -) - -type TermHost struct { - grid *ui.Grid - term *Terminal -} - -// Thin wrapper around terminal which puts it in a grid and passes through -// input events. A bit of a hack tbh -func NewTermHost(term *Terminal, conf *config.AercConfig) *TermHost { - grid := ui.NewGrid().Rows([]ui.GridSpec{ - {ui.SIZE_WEIGHT, 1}, - }).Columns([]ui.GridSpec{ - {ui.SIZE_EXACT, conf.Ui.SidebarWidth}, - {ui.SIZE_WEIGHT, 1}, - }) - grid.AddChild(term).At(0, 1) - return &TermHost{grid, term} -} - -func (th *TermHost) Draw(ctx *ui.Context) { - th.grid.Draw(ctx) -} - -func (th TermHost) Invalidate() { - th.grid.Invalidate() -} - -func (th *TermHost) OnInvalidate(fn func(d ui.Drawable)) { - th.grid.OnInvalidate(func(_ ui.Drawable) { - fn(th) - }) -} - -func (th *TermHost) Event(event tcell.Event) bool { - return th.term.Event(event) -} - -func (th *TermHost) Focus(focus bool) { - th.term.Focus(focus) -} - -func (th *TermHost) Terminal() *Terminal { - return th.term -}