feat: From in inbox view (#268)

Drew Smirnoff created

Change summary

fetcher/fetcher.go | 14 +++++++++++-
tui/inbox.go       | 49 ++++++++++++++++++++++++++++++++++++++---------
2 files changed, 51 insertions(+), 12 deletions(-)

Detailed changes

fetcher/fetcher.go ๐Ÿ”—

@@ -59,6 +59,16 @@ type Folder struct {
 	Attributes []string
 }
 
+// formatAddress returns "Name <email>" when a PersonalName is present,
+// otherwise just "email".
+func formatAddress(addr *imap.Address) string {
+	email := addr.Address()
+	if addr.PersonalName != "" {
+		return addr.PersonalName + " <" + email + ">"
+	}
+	return email
+}
+
 func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
 	mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
 	if err != nil {
@@ -275,7 +285,7 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 
 			var fromAddr string
 			if len(msg.Envelope.From) > 0 {
-				fromAddr = msg.Envelope.From[0].Address()
+				fromAddr = formatAddress(msg.Envelope.From[0])
 			}
 
 			var toAddrList []string
@@ -1079,7 +1089,7 @@ func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email,
 
 		var fromAddr string
 		if len(msg.Envelope.From) > 0 {
-			fromAddr = msg.Envelope.From[0].Address()
+			fromAddr = formatAddress(msg.Envelope.From[0])
 		}
 
 		var toAddrList []string

tui/inbox.go ๐Ÿ”—

@@ -23,7 +23,8 @@ var (
 	tabBarStyle     = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1)
 )
 
-var dateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
+var dateStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
+var senderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Bold(true)
 
 type item struct {
 	title, desc   string
@@ -49,11 +50,14 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 		return
 	}
 
-	str := fmt.Sprintf("%d. %s", index+1, i.title)
+	prefix := fmt.Sprintf("%d. ", index+1)
+	sender := parseSenderName(i.desc)
+	styledSender := senderStyle.Render(sender)
+	separator := " ยท "
 
-	// For "ALL" view, show account indicator
+	// For "ALL" view, show account indicator instead of number
 	if i.accountEmail != "" {
-		str = fmt.Sprintf("%d. [%s] %s", index+1, truncateEmail(i.accountEmail), i.title)
+		prefix = fmt.Sprintf("%d. [%s] ", index+1, truncateEmail(i.accountEmail))
 	}
 
 	// Format and right-align date
@@ -61,25 +65,37 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 	styledDate := dateStyle.Render(dateStr)
 	dateWidth := lipgloss.Width(styledDate)
 
-	// Truncate the left part to fit within the available width
 	listWidth := m.Width()
 	isSelected := index == m.Index()
 	cursorWidth := 0
 	if isSelected {
 		cursorWidth = 2 // "> " prefix
 	}
+
+	// Available width for the whole left side (prefix + sender + separator + subject)
 	maxLeft := listWidth - dateWidth - 2 - cursorWidth // 2 for spacing
 	if maxLeft < 10 {
 		maxLeft = 10
 	}
-	if lipgloss.Width(str) > maxLeft {
-		// Truncate with ellipsis
-		for lipgloss.Width(str) > maxLeft-1 && len(str) > 0 {
-			str = str[:len(str)-1]
+
+	prefixWidth := lipgloss.Width(prefix)
+	senderWidth := lipgloss.Width(styledSender)
+	sepWidth := len(separator)
+	subjectBudget := maxLeft - prefixWidth - senderWidth - sepWidth
+
+	subject := i.title
+	if subjectBudget < 4 {
+		subjectBudget = 4
+	}
+	if lipgloss.Width(subject) > subjectBudget {
+		for lipgloss.Width(subject) > subjectBudget-1 && len(subject) > 0 {
+			subject = subject[:len(subject)-1]
 		}
-		str += "โ€ฆ"
+		subject += "โ€ฆ"
 	}
 
+	str := prefix + styledSender + separator + subject
+
 	// Pad to push date to the right
 	padding := listWidth - lipgloss.Width(str) - dateWidth - cursorWidth
 	if padding < 1 {
@@ -134,6 +150,19 @@ func formatRelativeDate(t time.Time) string {
 	}
 }
 
+// parseSenderName extracts the display name from a "Name <email>" string,
+// falling back to the local part of the email address.
+func parseSenderName(from string) string {
+	if idx := strings.Index(from, " <"); idx > 0 {
+		return strings.TrimSpace(from[:idx])
+	}
+	// No display name โ€” use local part of email
+	if idx := strings.Index(from, "@"); idx > 0 {
+		return from[:idx]
+	}
+	return from
+}
+
 // truncateEmail shortens an email for display
 func truncateEmail(email string) string {
 	parts := strings.Split(email, "@")