pat/cli/read.go
Martin Hebnes Pedersen fe9b152a7a Refactor CLI message composer
This change refactors the command line message composer to unify the
various composition methods (interactive, non-interactive, reply,
forward, and template-based) into a single function.

The detection of non-interactive sessions was also changed to use a more
reliable check on `os.Stdin` instead of inspecting arguments.

A message preview and confirmation prompt has been added to the
interactive mode, allowing the user to review or edit the message before
it's posted to the outbox.
2025-07-15 19:35:46 +02:00

176 lines
4.1 KiB
Go

// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.
package cli
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/bndr/gotabulate"
"github.com/la5nta/pat/app"
"github.com/la5nta/wl2k-go/fbb"
"github.com/la5nta/wl2k-go/mailbox"
)
var mailboxes = []string{"in", "out", "sent", "archive"}
func ReadHandle(ctx context.Context, app *app.App, _ []string) {
cancel := exitOnContextCancellation(ctx)
defer cancel()
w := os.Stdout
for {
// Query user for mailbox to list
printMailboxes(w)
fmt.Fprintf(w, "\nChoose mailbox [n]: ")
mailboxIdx, ok := readInt()
if !ok {
break
} else if mailboxIdx+1 > len(mailboxes) {
fmt.Fprintln(w, "Invalid mailbox number")
continue
}
for {
// Fetch messages
msgs, err := mailbox.LoadMessageDir(filepath.Join(app.Mailbox().MBoxPath, mailboxes[mailboxIdx]))
if err != nil {
log.Fatal(err)
} else if len(msgs) == 0 {
fmt.Fprintf(w, "(empty)\n")
break
}
// Print messages (sorted by date)
sort.Sort(fbb.ByDate(msgs))
printMessages(w, msgs)
// Query user for message to print
fmt.Fprintf(w, "Choose message [n]: ")
msgIdx, ok := readInt()
if !ok {
break
} else if msgIdx+1 > len(msgs) {
fmt.Fprintf(w, "invalid message number\n")
continue
}
printMsg(w, msgs[msgIdx])
// Mark as read?
if mailbox.IsUnread(msgs[msgIdx]) {
fmt.Fprintf(w, "Mark as read? [Y/n]: ")
ans := readLine()
if ans == "" || strings.EqualFold(ans, "y") {
mailbox.SetUnread(msgs[msgIdx], false)
}
}
L:
for {
fmt.Fprintf(w, "Action [C,r,ra,f,e,d,q,?]: ")
switch ans := readLine(); ans {
case "C", "c", "":
break L
case "d":
fmt.Fprint(w, "Delete message? [y/N]: ")
if ans := readLine(); strings.EqualFold(ans, "y") {
msg := msgs[msgIdx]
mbox := mailboxes[mailboxIdx]
path := filepath.Join(app.Mailbox().MBoxPath, mbox, msg.MID()+mailbox.Ext)
if err := os.Remove(path); err != nil {
log.Printf("Failed to delete message %s from %s: %v", msg.MID(), mbox, err)
} else {
fmt.Fprintln(w, "Message deleted.")
}
break L
}
case "r":
composeMessage(app, composerFlags{from: app.Options().MyCall, inReplyTo: msgs[msgIdx].MID()}, true)
case "ra":
composeMessage(app, composerFlags{from: app.Options().MyCall, inReplyTo: msgs[msgIdx].MID(), replyAll: true}, true)
case "f":
composeMessage(app, composerFlags{from: app.Options().MyCall, forward: msgs[msgIdx].MID()}, true)
case "e":
ExtractMessageHandle(ctx, app, []string{msgs[msgIdx].MID()})
case "q":
return
case "?":
fallthrough
default:
fmt.Fprintln(w, "c - continue")
fmt.Fprintln(w, "r - reply")
fmt.Fprintln(w, "ra - reply all")
fmt.Fprintln(w, "f - forward")
fmt.Fprintln(w, "e - extract (attachments)")
fmt.Fprintln(w, "d - delete")
fmt.Fprintln(w, "q - quit")
}
}
}
}
}
func readInt() (int, bool) {
str := readLine()
if str == "" {
return 0, false
}
i, _ := strconv.Atoi(str)
return i, true
}
func printMsg(w io.Writer, msg *fbb.Message) {
fmt.Fprintf(w, "========================================\n")
fmt.Fprintln(w, msg)
fmt.Fprintf(w, "========================================\n\n")
}
func printMailboxes(w io.Writer) {
for i, mbox := range mailboxes {
fmt.Fprintf(w, "%d:%s\t", i, mbox)
}
}
func printMessages(w io.Writer, msgs []*fbb.Message) {
rows := make([][]string, len(msgs))
for i, msg := range msgs {
var to string
if len(msg.To()) > 0 {
to = msg.To()[0].Addr
}
if len(msg.To()) > 1 {
to += ", ..."
}
var flags string
if mailbox.IsUnread(msg) {
flags += "N" // New
}
rows[i] = []string{
fmt.Sprintf("%2d", i),
flags,
msg.Subject(),
msg.From().Addr,
msg.Date().String(),
to,
}
}
t := gotabulate.Create(rows)
t.SetHeaders([]string{"i", "Flags", "Subject", "From", "Date", "To"})
t.SetAlign("left")
t.SetWrapStrings(true)
t.SetMaxCellSize(60)
fmt.Fprintln(w, t.Render("simple"))
}