summary refs log blame commit diff stats
path: root/doc/uml/141058.diagram
blob: 285ba67bc652d59389ed186a4d6164cfad9ca172 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16















                                                    

                               







































































































































































































                                                                  
format 70

classinstance 128258 class_ref 169218 // Main
  name ""   xyz 64 4 2000 life_line_z 2000
classinstance 128386 class_ref 149378 // FM
  name ""   xyz 185 32 2000 life_line_z 2000
classinstance 128898 class_ref 128258 // DefaultUI
  name ""   mortal  xyz 289 80 2000 life_line_z 2000
classinstance 132226 class_ref 149122 // Environment
  name ""   xyz 421 84 2000 life_line_z 2000
classinstance 133122 class_ref 156034 // Widget
  name ""   xyz 544 107 2000 life_line_z 2000
classinstance 133762 class_ref 149250 // curses
  name ""   xyz 632 4 2000 life_line_z 2000
classinstance 134530 class_ref 148866 // Command
  name ""   xyz 727 4 2000 life_line_z 2000
note 136962 "This is outdated."
  xyzwh 352 23 2000 145 35
durationcanvas 128514 classinstance_ref 128258 // :Main
  xyzwh 83 82 2010 11 40
end
durationcanvas 128642 classinstance_ref 128386 // :FM
  xyzwh 204 82 2010 11 25
end
durationcanvas 129026 classinstance_ref 128258 // :Main
  xyzwh 83 130 2010 11 34
end
durationcanvas 129154 classinstance_ref 128898 // :DefaultUI
  xyzwh 325 130 2010 11 58
  overlappingdurationcanvas 135426
    xyzwh 331 142 2020 11 40
    overlappingdurationcanvas 135682
      xyzwh 337 151 2030 11 25
    end
  end
end
durationcanvas 129410 classinstance_ref 128258 // :Main
  xyzwh 83 180 2010 11 35
end
durationcanvas 129538 classinstance_ref 128386 // :FM
  xyzwh 204 172 2010 11 468
  overlappingdurationcanvas 136450
    xyzwh 210 537 2020 11 25
  end
end
durationcanvas 129794 classinstance_ref 128258 // :Main
  xyzwh 83 655 2010 11 27
end
durationcanvas 129922 classinstance_ref 128898 // :DefaultUI
  xyzwh 325 655 2010 11 27
end
durationcanvas 130178 classinstance_ref 128898 // :DefaultUI
  xyzwh 325 265 2010 11 26
end
durationcanvas 130434 classinstance_ref 128898 // :DefaultUI
  xyzwh 325 311 2010 11 26
end
durationcanvas 130690 classinstance_ref 128898 // :DefaultUI
  xyzwh 325 381 2010 11 43
end
durationcanvas 131074 classinstance_ref 128898 // :DefaultUI
  xyzwh 325 467 2010 11 53
  overlappingdurationcanvas 134914
    xyzwh 331 489 2020 11 25
  end
end
durationcanvas 132354 classinstance_ref 132226 // :Environment
  xyzwh 469 606 2010 11 32
end
durationcanvas 132866 classinstance_ref 132226 // :Environment
  xyzwh 469 184 2010 11 27
end
durationcanvas 133250 classinstance_ref 133122 // :Widget
  xyzwh 571 280 2010 11 25
end
durationcanvas 133506 classinstance_ref 133122 // :Widget
  xyzwh 571 323 2010 11 25
end
durationcanvas 133890 classinstance_ref 133762 // :curses
  xyzwh 658 389 2010 11 31
end
durationcanvas 135170 classinstance_ref 133122 // :Widget
  xyzwh 571 501 2010 11 27
end
durationcanvas 135938 classinstance_ref 134530 // :Command
  xyzwh 767 506 2010 11 72
end
durationcanvas 136706 classinstance_ref 133122 // :Widget
  xyzwh 571 563 2010 11 34
end
msg 128770 synchronous
  from durationcanvas_ref 128514
  to durationcanvas_ref 128642
  yz 82 2015 msg operation_ref 141826 // "initialize()"
  show_full_operations_definition default drawing_language default
  label_xy 117 64
msg 129282 synchronous
  from durationcanvas_ref 129026
  to durationcanvas_ref 129154
  yz 130 2015 msg operation_ref 171138 // "initialize()"
  show_full_operations_definition default drawing_language default
  label_xy 124 111
msg 129666 synchronous
  from durationcanvas_ref 129410
  to durationcanvas_ref 129538
  yz 180 2015 msg operation_ref 141954 // "loop()"
  show_full_operations_definition default drawing_language default
  label_xy 129 162
msg 130050 synchronous
  from durationcanvas_ref 129794
  to durationcanvas_ref 129922
  yz 655 2015 msg operation_ref 134914 // "destroy()"
  show_full_operations_definition default drawing_language default
  label_xy 119 636
msg 130306 synchronous
  from durationcanvas_ref 129538
  to durationcanvas_ref 130178
  yz 265 2015 msg operation_ref 134530 // "draw()"
  show_full_operations_definition default drawing_language default
  label_xy 245 247
msg 130562 synchronous
  from durationcanvas_ref 129538
  to durationcanvas_ref 130434
  yz 311 2015 msg operation_ref 149378 // "finalize()"
  show_full_operations_definition default drawing_language default
  label_xy 240 293
msg 130818 synchronous
  from durationcanvas_ref 129538
  to durationcanvas_ref 130690
  yz 382 2015 msg operation_ref 148738 // "get_next_key()"
  show_full_operations_definition default drawing_language default
  label_xy 229 361
msg 130946 return
  from durationcanvas_ref 130690
  to durationcanvas_ref 129538
  yz 412 2020 unspecifiedmsg
  show_full_operations_definition default drawing_language default
msg 131202 synchronous
  from durationcanvas_ref 129538
  to durationcanvas_ref 131074
  yz 467 2015 msg operation_ref 148610 // "handle_key()"
  show_full_operations_definition default drawing_language default
  label_xy 234 449
msg 132482 synchronous
  from durationcanvas_ref 129538
  to durationcanvas_ref 132354
  yz 606 2015 msg operation_ref 171522 // "garbage_collect()"
  show_full_operations_definition default drawing_language default
  label_xy 260 587
msg 132994 synchronous
  from durationcanvas_ref 129538
  to durationcanvas_ref 132866
  yz 185 2020 msg operation_ref 171650 // "enter_dir()"
  show_full_operations_definition default drawing_language default
  label_xy 222 164
msg 133378 synchronous
  from durationcanvas_ref 130178
  to durationcanvas_ref 133250
  yz 280 2015 msg operation_ref 134530 // "draw()"
  show_full_operations_definition default drawing_language default
  label_xy 407 262
msg 133634 synchronous
  from durationcanvas_ref 130434
  to durationcanvas_ref 133506
  yz 323 2015 msg operation_ref 149378 // "finalize()"
  show_full_operations_definition default drawing_language default
  label_xy 405 303
msg 134018 synchronous
  from durationcanvas_ref 130690
  to durationcanvas_ref 133890
  yz 391 2015 msg operation_ref 171778 // "getch()"
  show_full_operations_definition default drawing_language default
  label_xy 713 361
msg 134402 return
  from durationcanvas_ref 133890
  to durationcanvas_ref 130690
  yz 408 2020 unspecifiedmsg
  show_full_operations_definition default drawing_language default
reflexivemsg 135042 synchronous
  to durationcanvas_ref 134914
  yz 489 2025 msg operation_ref 148482 // "handle_mouse()"
  show_full_operations_definition default drawing_language default
  label_xy 345 462
msg 135298 synchronous
  from durationcanvas_ref 134914
  to durationcanvas_ref 135170
  yz 502 2030 msg operation_ref 134786 // "click()"
  show_full_operations_definition default drawing_language default
  label_xy 474 484
reflexivemsg 135554 synchronous
  to durationcanvas_ref 135426
  yz 142 2025 msg operation_ref 148866 // "setup()"
  show_full_operations_definition default drawing_language default
  label_xy 340 120
reflexivemsg 135810 synchronous
  to durationcanvas_ref 135682
  yz 151 2035 msg operation_ref 149890 // "add_obj()"
  show_full_operations_definition default drawing_language default
  label_xy 372 143
msg 136066 synchronous
  from durationcanvas_ref 135170
  to durationcanvas_ref 135938
  yz 506 2015 msg operation_ref 164226 // "execute()"
  show_full_operations_definition default drawing_language default
  label_xy 593 488
msg 136578 synchronous
  from durationcanvas_ref 135938
  to durationcanvas_ref 136450
  yz 537 2025 explicitmsg "<command>"
  show_full_operations_definition default drawing_language default
  label_xy 222 520
msg 136834 synchronous
  from durationcanvas_ref 135938
  to durationcanvas_ref 136706
  yz 567 2030 explicitmsg "<command>"
  show_full_operations_definition default drawing_language default
  label_xy 581 554
end
: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
package widgets

import (
	"bufio"
	"fmt"
	"io"
	"os/exec"
	"regexp"
	"strings"

	"github.com/danwakefield/fnmatch"
	"github.com/emersion/go-imap"
	"github.com/emersion/go-message"
	_ "github.com/emersion/go-message/charset"
	"github.com/emersion/go-message/mail"
	"github.com/gdamore/tcell"
	"github.com/google/shlex"
	"github.com/mattn/go-runewidth"

	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/lib/ui"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

var ansi = regexp.MustCompile("^\x1B\\[[0-?]*[ -/]*[@-~]")

type MessageViewer struct {
	ui.Invalidatable
	acct     *AccountView
	conf     *config.AercConfig
	err      error
	grid     *ui.Grid
	msg      *types.MessageInfo
	switcher *PartSwitcher
	store    *lib.MessageStore
}

type PartSwitcher struct {
	ui.Invalidatable
	parts       []*PartViewer
	selected    int
	showHeaders bool
}

func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
	store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer {

	grid := ui.NewGrid().Rows([]ui.GridSpec{
		{ui.SIZE_EXACT, 4}, // TODO: Based on number of header rows
		{ui.SIZE_WEIGHT, 1},
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, 1},
	})

	// TODO: let user specify additional headers to show by default
	headers := ui.NewGrid().Rows([]ui.GridSpec{
		{ui.SIZE_EXACT, 1},
		{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: lib.FormatAddresses(msg.Envelope.From),
		}).At(0, 0)
	headers.AddChild(
		&HeaderView{
			Name:  "To",
			Value: lib.FormatAddresses(msg.Envelope.To),
		}).At(0, 1)
	headers.AddChild(
		&HeaderView{
			Name:  "Date",
			Value: msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM"),
		}).At(1, 0).Span(1, 2)
	headers.AddChild(
		&HeaderView{
			Name:  "Subject",
			Value: msg.Envelope.Subject,
		}).At(2, 0).Span(1, 2)
	headers.AddChild(ui.NewFill(' ')).At(3, 0).Span(1, 2)

	switcher := &PartSwitcher{}
	err := createSwitcher(switcher, conf, store, msg, conf.Viewer.ShowHeaders)
	if err != nil {
		goto handle_error
	}

	grid.AddChild(headers).At(0, 0)
	grid.AddChild(switcher).At(1, 0)

	return &MessageViewer{
		acct:     acct,
		conf:     conf,
		grid:     grid,
		msg:      msg,
		store:    store,
		switcher: switcher,
	}

handle_error:
	return &MessageViewer{
		err:  err,
		grid: grid,
		msg:  msg,
	}
}

func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
	msg *types.MessageInfo, body *imap.BodyStructure,
	showHeaders bool, index []int) ([]*PartViewer, error) {

	var parts []*PartViewer
	for i, part := range body.Parts {
		curindex := append(index, i+1)
		if part.MIMEType == "multipart" {
			// Multipart meta-parts are faked
			pv := &PartViewer{part: part}
			parts = append(parts, pv)
			subParts, err := enumerateParts(
				conf, store, msg, part, showHeaders, curindex)
			if err != nil {
				return nil, err
			}
			parts = append(parts, subParts...)
			continue
		}
		pv, err := NewPartViewer(conf, store, msg, part, showHeaders, curindex)
		if err != nil {
			return nil, err
		}
		parts = append(parts, pv)
	}
	return parts, nil
}

func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig,
	store *lib.MessageStore, msg *types.MessageInfo, showHeaders bool) error {
	var err error
	switcher.showHeaders = showHeaders

	if len(msg.BodyStructure.Parts) == 0 {
		pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure,
			showHeaders, []int{1})
		if err != nil {
			return err
		}
		switcher.parts = []*PartViewer{pv}
		pv.OnInvalidate(func(_ ui.Drawable) {
			switcher.Invalidate()
		})
	} else {
		switcher.parts, err = enumerateParts(conf, store,
			msg, msg.BodyStructure, showHeaders, []int{})
		if err != nil {
			return err
		}
		selectedPriority := -1
		for i, pv := range switcher.parts {
			pv.OnInvalidate(func(_ ui.Drawable) {
				switcher.Invalidate()
			})
			// Switch to user's preferred mimetype
			if switcher.selected == -1 && pv.part.MIMEType != "multipart" {
				switcher.selected = i
			} else if selectedPriority == -1 {
				for idx, m := range conf.Viewer.Alternatives {
					if m != pv.part.MIMEType+"/"+pv.part.MIMESubType {
						continue
					}
					priority := len(conf.Viewer.Alternatives) - idx
					if priority > selectedPriority {
						selectedPriority = priority
						switcher.selected = i
					}
				}
			}
		}
	}
	return nil
}

func (mv *MessageViewer) Draw(ctx *ui.Context) {
	if mv.err != nil {
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
		return
	}
	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) Store() *lib.MessageStore {
	return mv.store
}

func (mv *MessageViewer) SelectedAccount() *AccountView {
	return mv.acct
}

func (mv *MessageViewer) SelectedMessage() *types.MessageInfo {
	return mv.msg
}

func (mv *MessageViewer) ToggleHeaders() {
	switcher := mv.switcher
	err := createSwitcher(
		switcher, mv.conf, mv.store, mv.msg, !switcher.showHeaders)
	if err != nil {
		mv.acct.Logger().Printf(
			"warning: error during create switcher - %v", err)
	}
	switcher.Invalidate()
}

func (mv *MessageViewer) CurrentPart() *PartInfo {
	switcher := mv.switcher
	part := switcher.parts[switcher.selected]

	return &PartInfo{
		Index: part.index,
		Msg:   part.msg,
		Part:  part.part,
		Store: part.store,
	}
}

func (mv *MessageViewer) PreviousPart() {
	switcher := mv.switcher
	for {
		switcher.selected--
		if switcher.selected < 0 {
			switcher.selected = len(switcher.parts) - 1
		}
		if switcher.parts[switcher.selected].part.MIMEType != "multipart" {
			break
		}
	}
	mv.Invalidate()
}

func (mv *MessageViewer) NextPart() {
	switcher := mv.switcher
	for {
		switcher.selected++
		if switcher.selected >= len(switcher.parts) {
			switcher.selected = 0
		}
		if switcher.parts[switcher.selected].part.MIMEType != "multipart" {
			break
		}
	}
	mv.Invalidate()
}

func (ps *PartSwitcher) Invalidate() {
	ps.DoInvalidate(ps)
}

func (ps *PartSwitcher) Focus(focus bool) {
	if ps.parts[ps.selected].term != nil {
		ps.parts[ps.selected].term.Focus(focus)
	}
}

func (ps *PartSwitcher) Event(event tcell.Event) bool {
	if ps.parts[ps.selected].term != nil {
		return ps.parts[ps.selected].term.Event(event)
	}
	return false
}

func (ps *PartSwitcher) Draw(ctx *ui.Context) {
	height := len(ps.parts)
	if height == 1 {
		ps.parts[ps.selected].Draw(ctx)
		return
	}
	// TODO: cap height and add scrolling for messages with many parts
	y := ctx.Height() - height
	for i, part := range ps.parts {
		style := tcell.StyleDefault.Reverse(ps.selected == i)
		ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
		name := fmt.Sprintf("%s/%s",
			strings.ToLower(part.part.MIMEType),
			strings.ToLower(part.part.MIMESubType))
		if filename, ok := part.part.DispositionParams["filename"]; ok {
			name += fmt.Sprintf(" (%s)", filename)
		}
		ctx.Printf(len(part.index)*2, y+i, style, "%s", name)
	}
	ps.parts[ps.selected].Draw(ctx.Subcontext(
		0, 0, ctx.Width(), ctx.Height()-height))
}

func (mv *MessageViewer) Event(event tcell.Event) bool {
	return mv.switcher.Event(event)
}

func (mv *MessageViewer) Focus(focus bool) {
	mv.switcher.Focus(focus)
}

type PartViewer struct {
	ui.Invalidatable
	err         error
	fetched     bool
	filter      *exec.Cmd
	index       []int
	msg         *types.MessageInfo
	pager       *exec.Cmd
	pagerin     io.WriteCloser
	part        *imap.BodyStructure
	showHeaders bool
	sink        io.WriteCloser
	source      io.Reader
	store       *lib.MessageStore
	term        *Terminal
}

type PartInfo struct {
	Index []int
	Msg   *types.MessageInfo
	Part  *imap.BodyStructure
	Store *lib.MessageStore
}

func NewPartViewer(conf *config.AercConfig,
	store *lib.MessageStore, msg *types.MessageInfo,
	part *imap.BodyStructure, showHeaders bool,
	index []int) (*PartViewer, error) {

	var (
		filter  *exec.Cmd
		pager   *exec.Cmd
		pipe    io.WriteCloser
		pagerin io.WriteCloser
		term    *Terminal
	)
	cmd, err := shlex.Split(conf.Viewer.Pager)
	if err != nil {
		return nil, err
	}

	pager = exec.Command(cmd[0], cmd[1:]...)

	for _, f := range conf.Filters {
		mime := strings.ToLower(part.MIMEType) +
			"/" + strings.ToLower(part.MIMESubType)
		switch f.FilterType {
		case config.FILTER_MIMETYPE:
			if fnmatch.Match(f.Filter, mime, 0) {
				filter = exec.Command("sh", "-c", f.Command)
			}
		case config.FILTER_HEADER:
			var header string
			switch f.Header {
			case "subject":
				header = msg.Envelope.Subject
			case "from":
				header = lib.FormatAddresses(msg.Envelope.From)
			case "to":
				header = lib.FormatAddresses(msg.Envelope.To)
			case "cc":
				header = lib.FormatAddresses(msg.Envelope.Cc)
			}
			if f.Regex.Match([]byte(header)) {
				filter = exec.Command("sh", "-c", f.Command)
			}
		}
		if filter != nil {
			break
		}
	}
	if filter != nil {
		if pipe, err = filter.StdinPipe(); err != nil {
			return nil, err
		}
		if pagerin, _ = pager.StdinPipe(); err != nil {
			return nil, err
		}
		if term, err = NewTerminal(pager); err != nil {
			return nil, err
		}
	}

	pv := &PartViewer{
		filter:      filter,
		index:       index,
		msg:         msg,
		pager:       pager,
		pagerin:     pagerin,
		part:        part,
		showHeaders: showHeaders,
		sink:        pipe,
		store:       store,
		term:        term,
	}

	if term != nil {
		term.OnStart = func() {
			pv.attemptCopy()
		}
		term.OnInvalidate(func(_ ui.Drawable) {
			pv.Invalidate()
		})
	}

	return pv, nil
}

func (pv *PartViewer) SetSource(reader io.Reader) {
	pv.source = reader
	pv.attemptCopy()
}

func (pv *PartViewer) attemptCopy() {
	if pv.source != nil && pv.pager.Process != nil {
		header := message.Header{}
		header.SetText("Content-Transfer-Encoding", pv.part.Encoding)
		header.SetContentType(pv.part.MIMEType, pv.part.Params)
		header.SetText("Content-Description", pv.part.Description)
		if pv.filter != nil {
			stdout, _ := pv.filter.StdoutPipe()
			stderr, _ := pv.filter.StderrPipe()
			pv.filter.Start()
			ch := make(chan interface{})
			go func() {
				_, err := io.Copy(pv.pagerin, stdout)
				if err != nil {
					pv.err = err
					pv.Invalidate()
				}
				stdout.Close()
				ch <- nil
			}()
			go func() {
				_, err := io.Copy(pv.pagerin, stderr)
				if err != nil {
					pv.err = err
					pv.Invalidate()
				}
				stderr.Close()
				ch <- nil
			}()
			go func() {
				<-ch
				<-ch
				pv.pagerin.Close()
			}()
		}
		go func() {
			if pv.showHeaders && pv.msg.RFC822Headers != nil {
				fields := pv.msg.RFC822Headers.Fields()
				for fields.Next() {
					field := fmt.Sprintf(
						"%s: %s\n", fields.Key(), fields.Value())
					pv.sink.Write([]byte(field))
				}
				pv.sink.Write([]byte{'\n'})
			}

			entity, err := message.New(header, pv.source)
			if err != nil {
				pv.err = err
				pv.Invalidate()
				return
			}
			reader := mail.NewReader(entity)
			part, err := reader.NextPart()
			if err != nil {
				pv.err = err
				pv.Invalidate()
				return
			}
			if pv.part.MIMEType == "text" {
				scanner := bufio.NewScanner(part.Body)
				for scanner.Scan() {
					text := scanner.Text()
					text = ansi.ReplaceAllString(text, "")
					io.WriteString(pv.sink, text+"\n")
				}
			} else {
				io.Copy(pv.sink, part.Body)
			}
			pv.sink.Close()
		}()
	}
}

func (pv *PartViewer) Invalidate() {
	pv.DoInvalidate(pv)
}

func (pv *PartViewer) Draw(ctx *ui.Context) {
	if pv.filter == nil {
		// TODO: Let them download it directly or something
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed),
			"No filter configured for this mimetype")
		return
	}
	if !pv.fetched {
		pv.store.FetchBodyPart(pv.msg.Uid, pv.index, pv.SetSource)
		pv.fetched = true
	}
	if pv.err != nil {
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
		return
	}
	pv.term.Draw(ctx)
}

type HeaderView struct {
	ui.Invalidatable
	Name  string
	Value string
}

func (hv *HeaderView) Draw(ctx *ui.Context) {
	name := hv.Name
	size := runewidth.StringWidth(name)
	lim := ctx.Width() - size - 1
	value := runewidth.Truncate(" "+hv.Value, lim, "…")
	var (
		hstyle tcell.Style
		vstyle tcell.Style
	)
	// TODO: Make this more robust and less dumb
	if hv.Name == "PGP" {
		vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
		hstyle = tcell.StyleDefault.Bold(true)
	} else {
		vstyle = tcell.StyleDefault
		hstyle = tcell.StyleDefault.Bold(true)
	}
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
	ctx.Printf(0, 0, hstyle, name)
	ctx.Printf(size, 0, vstyle, value)
}

func (hv *HeaderView) Invalidate() {
	hv.DoInvalidate(hv)
}