summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--config/aerc.conf.in8
-rw-r--r--config/config.go26
-rw-r--r--doc/aerc-config.5.scd8
-rw-r--r--lib/ui/grid.go14
-rw-r--r--widgets/msgviewer.go101
5 files changed, 116 insertions, 41 deletions
diff --git a/config/aerc.conf.in b/config/aerc.conf.in
index 41f4ce6..d490831 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -61,6 +61,14 @@ alternatives=text/plain,text/html
 # Default: false
 show-headers=false
 
+#
+# Layout of headers when viewing a message. To display multiple headers in the
+# same row, separate them with a pipe, e.g. "From|To". Rows will be hidden if
+# none of their specified headers are present in the message.
+#
+# Default: From|To,Cc|Bcc,Date,Subject
+header-layout=From|To,Cc|Bcc,Date,Subject
+
 [compose]
 #
 # Specifies the command to run the editor with. It will be shown in an embedded
diff --git a/config/config.go b/config/config.go
index aab3905..9e081fd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -79,7 +79,8 @@ type FilterConfig struct {
 type ViewerConfig struct {
 	Pager        string
 	Alternatives []string
-	ShowHeaders  bool `ini:"show-headers"`
+	ShowHeaders  bool       `ini:"show-headers"`
+	HeaderLayout [][]string `ini:"-"`
 }
 
 type AercConfig struct {
@@ -261,6 +262,8 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
 			switch key {
 			case "alternatives":
 				config.Viewer.Alternatives = strings.Split(val, ",")
+			case "header-layout":
+				config.Viewer.HeaderLayout = parseLayout(val)
 			}
 		}
 	}
@@ -323,6 +326,18 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
 			EmptyDirlist:      "(no folders)",
 			MouseEnabled:      false,
 		},
+
+		Viewer: ViewerConfig{
+			Pager:        "less -R",
+			Alternatives: []string{"text/plain", "text/html"},
+			ShowHeaders:  false,
+			HeaderLayout: [][]string{
+				{"From", "To"},
+				{"Cc", "Bcc"},
+				{"Date"},
+				{"Subject"},
+			},
+		},
 	}
 	// These bindings are not configurable
 	config.Bindings.AccountWizard.ExKey = KeyStroke{
@@ -431,3 +446,12 @@ func checkConfigPerms(filename string) error {
 	}
 	return nil
 }
+
+func parseLayout(layout string) [][]string {
+	rows := strings.Split(layout, ",")
+	l := make([][]string, len(rows))
+	for i, r := range rows {
+		l[i] = strings.Split(r, "|")
+	}
+	return l
+}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 95e9087..3d39ef6 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -119,6 +119,14 @@ These options are configured in the *[viewer]* section of aerc.conf.
 
 	Default: text/plain,text/html
 
+*header-layout*
+	Defines the default headers to display when viewing a message. To display
+	multiple headers in the same row, separate them with a pipe, e.g. "From|To".
+	Rows will be hidden if none of their specified headers are present in the
+	message.
+
+	Default: From|To,Cc|Bcc,Date,Subject
+
 *show-headers*
 	Default setting to determine whether to show full headers or only parsed
 	ones in message viewer.
diff --git a/lib/ui/grid.go b/lib/ui/grid.go
index 3f5dd60..96da1cb 100644
--- a/lib/ui/grid.go
+++ b/lib/ui/grid.go
@@ -54,6 +54,20 @@ func NewGrid() *Grid {
 	return &Grid{invalid: true}
 }
 
+// MakeGrid creates a grid with the specified number of columns and rows. Each
+// cell has a size of 1.
+func MakeGrid(numRows, numCols, rowStrategy, colStrategy int) *Grid {
+	rows := make([]GridSpec, numRows)
+	for i := 0; i < numRows; i++ {
+		rows[i] = GridSpec{rowStrategy, 1}
+	}
+	cols := make([]GridSpec, numCols)
+	for i := 0; i < numCols; i++ {
+		cols[i] = GridSpec{colStrategy, 1}
+	}
+	return NewGrid().Rows(rows).Columns(cols)
+}
+
 func (cell *GridCell) At(row, col int) *GridCell {
 	cell.Row = row
 	cell.Column = col
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index f15fbae..19de4b8 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -45,53 +45,26 @@ type PartSwitcher struct {
 
 func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
 	store *lib.MessageStore, msg *models.MessageInfo) *MessageViewer {
+	header, headerHeight := createHeader(msg, conf.Viewer.HeaderLayout)
 
 	grid := ui.NewGrid().Rows([]ui.GridSpec{
-		{ui.SIZE_EXACT, 4}, // TODO: Based on number of header rows
+		{ui.SIZE_EXACT, headerHeight},
 		{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: models.FormatAddresses(msg.Envelope.From),
-		}).At(0, 0)
-	headers.AddChild(
-		&HeaderView{
-			Name:  "To",
-			Value: models.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
+		return &MessageViewer{
+			err:  err,
+			grid: grid,
+			msg:  msg,
+		}
 	}
 
-	grid.AddChild(headers).At(0, 0)
+	grid.AddChild(header).At(0, 0)
 	grid.AddChild(switcher).At(1, 0)
 
 	return &MessageViewer{
@@ -102,12 +75,60 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
 		store:    store,
 		switcher: switcher,
 	}
+}
 
-handle_error:
-	return &MessageViewer{
-		err:  err,
-		grid: grid,
-		msg:  msg,
+func createHeader(msg *models.MessageInfo, layout [][]string) (grid *ui.Grid, height int) {
+	presentHeaders := presentHeaders(msg, layout)
+	rowCount := len(presentHeaders) + 1 // extra row for spacer
+	grid = ui.MakeGrid(rowCount, 1, ui.SIZE_EXACT, ui.SIZE_WEIGHT)
+	for i, cols := range presentHeaders {
+		r := ui.MakeGrid(1, len(cols), ui.SIZE_EXACT, ui.SIZE_WEIGHT)
+		for j, col := range cols {
+			r.AddChild(
+				&HeaderView{
+					Name:  col,
+					Value: fmtHeader(msg, col),
+				}).At(0, j)
+		}
+		grid.AddChild(r).At(i, 0)
+	}
+	grid.AddChild(ui.NewFill(' ')).At(rowCount-1, 0)
+	return grid, rowCount
+}
+
+// presentHeaders returns a filtered header layout, removing rows whose headers
+// do not appear in the provided message.
+func presentHeaders(msg *models.MessageInfo, layout [][]string) [][]string {
+	headers := msg.RFC822Headers
+	result := make([][]string, 0, len(layout))
+	for _, row := range layout {
+		// To preserve layout alignment, only hide rows if all columns are empty
+		for _, col := range row {
+			if headers.Get(col) != "" {
+				result = append(result, row)
+				break
+			}
+		}
+	}
+	return result
+}
+
+func fmtHeader(msg *models.MessageInfo, header string) string {
+	switch header {
+	case "From":
+		return models.FormatAddresses(msg.Envelope.From)
+	case "To":
+		return models.FormatAddresses(msg.Envelope.To)
+	case "Cc":
+		return models.FormatAddresses(msg.Envelope.Cc)
+	case "Bcc":
+		return models.FormatAddresses(msg.Envelope.Bcc)
+	case "Date":
+		return msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM")
+	case "Subject":
+		return msg.Envelope.Subject
+	default:
+		return msg.RFC822Headers.Get(header)
 	}
 }