about summary refs log tree commit diff stats
path: root/util.c
Commit message (Expand)AuthorAgeFilesLines
* added a comment to spawn 1.1Anselm R. Garbe2006-08-281-0/+1
* eliminated sentinel warningAnselm R. Garbe2006-08-231-1/+1
* removed badmalloc (thx for the pointer to Uriel)Anselm R. Garbe2006-08-221-10/+2
* small renamings of two static functionsAnselm R.Garbe2006-08-211-3/+3
* implemented restack behavior (floats are on top in tiled mode)Anselm R.Garbe2006-08-141-0/+9
* applied grabbing-- and shell_minimalarg@10ksloc.org2006-08-071-1/+1
* using execl now, argv changed, using cmd and const char defs directly in the ...arg@10ksloc.org2006-08-041-4/+7
* small stylistic fixarg@10ksloc.org2006-08-041-1/+2
* made fullscreen apps working fine in floating mode (there is no sane way to m...arg@10ksloc.org2006-08-021-1/+0
* some cleanups/fixes inspired by Jukka Salmi's feedbackarg@10ksloc.org2006-07-211-4/+2
* cleaned up codearg@10ksloc.org2006-07-201-0/+3
* using EXIT_stuff in exit() nowAnselm R. Garbe2006-07-181-3/+3
* ordered variables in structs and source files alphabeticallyAnselm R. Garbe2006-07-171-2/+2
* proceeded with cleaning up, sorting functions, etcAnselm R. Garbe2006-07-151-11/+13
* rearranged several stuffAnselm R. Garbe2006-07-151-1/+1
* removed a bunch of lines through swap removalAnselm R. Garbe2006-07-141-8/+0
* searching for a better way to discard enter notifiesAnselm R. Garbe2006-07-141-28/+0
* implemented tagging a clientAnselm R. Garbe2006-07-131-1/+2
* changed default colorsAnselm R. Garbe2006-07-131-1/+1
* added logo+descriptionAnselm R. Garbe2006-07-131-1/+1
* new stuff (some warning elimination)Anselm R. Garbe2006-07-131-2/+2
* removed unnecessary crapAnselm R. Garbe2006-07-131-49/+2
* new stuff, fixed several issuesAnselm R. Garbe2006-07-121-2/+3
* several changes, new stuffAnselm R. Garbe2006-07-111-1/+1
* removed unnecessary sel stuffAnselm R. Garbe2006-07-111-38/+0
* added bar event timerAnselm R. Garbe2006-07-111-17/+8
* added gridsel to gridwmAnselm R. Garbe2006-07-111-0/+38
* fixed several stuff (gridwm gets better and better)Anselm R. Garbe2006-07-111-6/+7
* implemented pipe_spawnAnselm R. Garbe2006-07-111-4/+52
* added several other stuffAnselm R. Garbe2006-07-101-0/+23
* several new changes, made gridmenu workingAnselm R. Garbe2006-07-101-0/+59
* added new stuffAnselm R. Garbe2006-07-101-0/+18
ame.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #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 (
	"fmt"
	"log"

	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"

	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/lib/format"
	"git.sr.ht/~sircmpwn/aerc/lib/ui"
	"git.sr.ht/~sircmpwn/aerc/models"
)

type MessageList struct {
	ui.Invalidatable
	conf          *config.AercConfig
	logger        *log.Logger
	height        int
	scroll        int
	nmsgs         int
	spinner       *Spinner
	store         *lib.MessageStore
	isInitalizing bool
	aerc          *Aerc
}

func NewMessageList(conf *config.AercConfig, logger *log.Logger, aerc *Aerc) *MessageList {
	ml := &MessageList{
		conf:          conf,
		logger:        logger,
		spinner:       NewSpinner(&conf.Ui),
		isInitalizing: true,
		aerc:          aerc,
	}
	ml.spinner.OnInvalidate(func(_ ui.Drawable) {
		ml.Invalidate()
	})
	// TODO: stop spinner, probably
	ml.spinner.Start()
	return ml
}

func (ml *MessageList) Invalidate() {
	ml.DoInvalidate(ml)
}

func (ml *MessageList) Draw(ctx *ui.Context) {
	ml.height = ctx.Height()
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)

	store := ml.Store()
	if store == nil {
		if ml.isInitalizing {
			ml.spinner.Draw(ctx)
			return
		} else {
			ml.spinner.Stop()
			ml.drawEmptyMessage(ctx)
			return
		}
	}

	var (
		needsHeaders []uint32
		row          int = 0
	)
	uids := store.Uids()

	for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
		uid := uids[i]
		msg := store.Messages[uid]

		if row >= ctx.Height() {
			break
		}

		if msg == nil {
			needsHeaders = append(needsHeaders, uid)
			ml.spinner.Draw(ctx.Subcontext(0, row, ctx.Width(), 1))
			row += 1
			continue
		}

		style := tcell.StyleDefault

		// current row
		if row == ml.store.SelectedIndex()-ml.scroll {
			style = style.Reverse(true)
		}
		// deleted message
		if _, ok := store.Deleted[msg.Uid]; ok {
			style = style.Foreground(tcell.ColorGray)
		}
		// unread message
		seen := false
		for _, flag := range msg.Flags {
			if flag == models.SeenFlag {
				seen = true
			}
		}
		if !seen {
			style = style.Bold(true)
		}

		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
		uiConfig := ml.conf.GetUiConfig(map[int]string{
			config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
			config.UI_CONTEXT_FOLDER:  ml.aerc.SelectedAccount().Directories().Selected(),
			config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject,
		})

		fmtStr, args, err := format.ParseMessageFormat(
			ml.aerc.SelectedAccount().acct.From,
			uiConfig.IndexFormat,
			uiConfig.TimestampFormat, "", i, msg, store.IsMarked(uid))
		if err != nil {
			ctx.Printf(0, row, style, "%v", err)
		} else {
			line := fmt.Sprintf(fmtStr, args...)
			line = runewidth.Truncate(line, ctx.Width(), "…")
			ctx.Printf(0, row, style, "%s", line)
		}

		row += 1
	}

	if len(uids) == 0 {
		ml.drawEmptyMessage(ctx)
	}

	if len(needsHeaders) != 0 {
		store.FetchHeaders(needsHeaders, nil)
		ml.spinner.Start()
	} else {
		ml.spinner.Stop()
	}
}

func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		switch event.Buttons() {
		case tcell.Button1:
			if ml.aerc == nil {
				return
			}
			selectedMsg, ok := ml.Clicked(localX, localY)
			if ok {
				ml.Select(selectedMsg)
				acct := ml.aerc.SelectedAccount()
				if acct.Messages().Empty() {
					return
				}
				store := acct.Messages().Store()
				msg := acct.Messages().Selected()
				if msg == nil {
					return
				}
				viewer := NewMessageViewer(acct, ml.aerc.Config(), store, msg)
				ml.aerc.NewTab(viewer, msg.Envelope.Subject)
			}
		case tcell.WheelDown:
			ml.store.Next()
			ml.Scroll()
		case tcell.WheelUp:
			ml.store.Prev()
			ml.Scroll()
		}
	}
}

func (ml *MessageList) Clicked(x, y int) (int, bool) {
	store := ml.Store()
	if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs {
		return 0, false
	}
	return y + ml.scroll, true
}

func (ml *MessageList) Height() int {
	return ml.height
}

func (ml *MessageList) storeUpdate(store *lib.MessageStore) {
	if ml.Store() != store {
		return
	}
	uids := store.Uids()

	if len(uids) > 0 {
		// When new messages come in, advance the cursor accordingly
		// Note that this assumes new messages are appended to the top, which
		// isn't necessarily true once we implement SORT... ideally we'd look
		// for the previously selected UID.
		if len(uids) > ml.nmsgs && ml.nmsgs != 0 {
			for i := 0; i < len(uids)-ml.nmsgs; i++ {
				ml.Store().Next()
			}
		}
		if len(uids) < ml.nmsgs && ml.nmsgs != 0 {
			for i := 0; i < ml.nmsgs-len(uids); i++ {
				ml.Store().Prev()
			}
		}
		ml.nmsgs = len(uids)
	}

	ml.Scroll()
	ml.Invalidate()
}

func (ml *MessageList) SetStore(store *lib.MessageStore) {
	if ml.Store() != store {
		ml.scroll = 0
	}
	ml.store = store
	if store != nil {
		ml.spinner.Stop()
		ml.nmsgs = len(store.Uids())
		store.OnUpdate(ml.storeUpdate)
	} else {
		ml.spinner.Start()
	}
	ml.Invalidate()
}

func (ml *MessageList) SetInitDone() {
	ml.isInitalizing = false
}

func (ml *MessageList) Store() *lib.MessageStore {
	return ml.store
}

func (ml *MessageList) Empty() bool {
	store := ml.Store()
	return store == nil || len(store.Uids()) == 0
}

func (ml *MessageList) Selected() *models.MessageInfo {
	store := ml.Store()
	uids := store.Uids()
	return store.Messages[uids[len(uids)-ml.store.SelectedIndex()-1]]
}

func (ml *MessageList) Select(index int) {
	store := ml.Store()
	store.Select(index)
	ml.Scroll()
}

func (ml *MessageList) Scroll() {
	store := ml.Store()

	if store == nil || len(store.Uids()) == 0 {
		return
	}
	if ml.Height() != 0 {
		// I'm too lazy to do the math right now
		for store.SelectedIndex()-ml.scroll >= ml.Height() {
			ml.scroll += 1
		}
		for store.SelectedIndex()-ml.scroll < 0 {
			ml.scroll -= 1
		}
	}
	ml.Invalidate()
}

func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
	msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage
	ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
		tcell.StyleDefault, "%s", msg)
}