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) } }