summary refs log blame commit diff stats
path: root/lib/ui/tab.go
blob: f18b0ac2ce1dd2a3c8ac37820b164e39d33879e6 (plain) (tree)
1
2
3
4


          
                                  








































                                                      


                                                                                 



























                                                                             


                                                     
                                        


                                                               
                 
                                                              
         


                                             
                                                    




















                                                                    

                                             




                                                                        
package ui

import (
	"github.com/gdamore/tcell"
)

type Tabs struct {
	Tabs       []*Tab
	TabStrip   *TabStrip
	TabContent *TabContent
	Selected   int

	onInvalidateStrip   func(d Drawable)
	onInvalidateContent func(d Drawable)
}

type Tab struct {
	Content Drawable
	Name    string
	invalid bool
}

type TabStrip Tabs
type TabContent Tabs

func NewTabs() *Tabs {
	tabs := &Tabs{}
	tabs.TabStrip = (*TabStrip)(tabs)
	tabs.TabContent = (*TabContent)(tabs)
	return tabs
}

func (tabs *Tabs) Add(content Drawable, name string) {
	tabs.Tabs = append(tabs.Tabs, &Tab{
		Content: content,
		Name:    name,
	})
	tabs.TabStrip.Invalidate()
	content.OnInvalidate(tabs.invalidateChild)
}

func (tabs *Tabs) invalidateChild(d Drawable) {
	for i, tab := range tabs.Tabs {
		if tab.Content == d {
			if i == tabs.Selected {
				if tabs.onInvalidateContent != nil {
					tabs.onInvalidateContent(tabs.TabContent)
				}
			}
			return
		}
	}
}

func (tabs *Tabs) Remove(content Drawable) {
	for i, tab := range tabs.Tabs {
		if tab.Content == content {
			tabs.Tabs = append(tabs.Tabs[:i], tabs.Tabs[i+1:]...)
			break
		}
	}
	tabs.TabStrip.Invalidate()
}

func (tabs *Tabs) Select(index int) {
	if tabs.Selected != index {
		tabs.Selected = index
		tabs.TabStrip.Invalidate()
		tabs.TabContent.Invalidate()
	}
}

// TODO: Color repository
func (strip *TabStrip) Draw(ctx *Context) {
	x := 0
	for i, tab := range strip.Tabs {
		style := tcell.StyleDefault.
			Background(tcell.ColorWhite).
			Foreground(tcell.ColorBlack)
		if strip.Selected == i {
			style = tcell.StyleDefault.
				Background(tcell.ColorDefault).
				Foreground(tcell.ColorDefault)
		}
		x += ctx.Printf(x, 0, style, " %s ", tab.Name)
	}
	style := tcell.StyleDefault.
		Background(tcell.ColorWhite).
		Foreground(tcell.ColorBlack)
	ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style)
}

func (strip *TabStrip) Invalidate() {
	if strip.onInvalidateStrip != nil {
		strip.onInvalidateStrip(strip)
	}
}

func (strip *TabStrip) OnInvalidate(onInvalidate func(d Drawable)) {
	strip.onInvalidateStrip = onInvalidate
}

func (content *TabContent) Draw(ctx *Context) {
	tab := content.Tabs[content.Selected]
	tab.Content.Draw(ctx)
}

func (content *TabContent) Invalidate() {
	if content.onInvalidateContent != nil {
		content.onInvalidateContent(content)
	}
	tab := content.Tabs[content.Selected]
	tab.Content.Invalidate()
}

func (content *TabContent) OnInvalidate(onInvalidate func(d Drawable)) {
	content.onInvalidateContent = onInvalidate
}
x">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) }