Compare commits
4 commits
70b0eedb9c
...
1752562279
| Author | SHA1 | Date | |
|---|---|---|---|
| 1752562279 | |||
| 3af666978c | |||
| 9ba845e5f4 | |||
| 9c0ca254f5 |
4 changed files with 73 additions and 6 deletions
|
|
@ -48,6 +48,7 @@ go install github.com/nemunaire/mqv@latest
|
|||
| `↑` / `↓` | Navigate messages |
|
||||
| `Enter` | Open message |
|
||||
| `r` | Refresh queue |
|
||||
| `F` | Flush queue (`postqueue -f`) |
|
||||
| `q` | Quit |
|
||||
|
||||
Subjects are fetched lazily in parallel via `postcat` as the list loads; a progress bar tracks completion.
|
||||
|
|
@ -59,6 +60,8 @@ Subjects are fetched lazily in parallel via `postcat` as the list loads; a progr
|
|||
| `↑↓` / `Space` / `PgUp` / `PgDn` | Scroll |
|
||||
| `H` | Toggle full headers / short headers |
|
||||
| `s` | Save raw EML to `~/QUEUEID.eml` |
|
||||
| `D` | Delete message (`postsuper -d`) |
|
||||
| `F` | Requeue message (`postsuper -r`) |
|
||||
| `v` | Browse MIME parts |
|
||||
| `q` | Back to queue list |
|
||||
|
||||
|
|
|
|||
48
model.go
48
model.go
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ type Model struct {
|
|||
saveNotice string
|
||||
width int
|
||||
height int
|
||||
messageSaving bool
|
||||
messageSaving bool
|
||||
// stateParts fields
|
||||
parts []MessagePart
|
||||
partsCursor int
|
||||
|
|
@ -209,6 +210,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.list.SetItems(nil)
|
||||
_ = m.progress.SetPercent(0)
|
||||
return m, loadQueueCmd()
|
||||
case "F":
|
||||
m.entries = nil
|
||||
m.entryIndex = nil
|
||||
m.loadingTotal = 0
|
||||
m.loadingDone = 0
|
||||
m.list.SetItems(nil)
|
||||
_ = m.progress.SetPercent(0)
|
||||
return m, flushQueueCmd()
|
||||
case "enter":
|
||||
if item, ok := m.list.SelectedItem().(queueItem); ok {
|
||||
return m, loadMessageCmd(item.entry.ID)
|
||||
|
|
@ -253,6 +262,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.refreshViewport()
|
||||
m.viewport.GotoTop()
|
||||
return m, nil
|
||||
case "D":
|
||||
id := m.currentID
|
||||
m.state = stateList
|
||||
m.saveNotice = ""
|
||||
return m, deleteMessageCmd(id)
|
||||
case "F":
|
||||
return m, requeueMessageCmd(m.currentID)
|
||||
case "v":
|
||||
m.parts = extractParts(m.currentRaw)
|
||||
m.partsCursor = 0
|
||||
|
|
@ -366,14 +382,24 @@ func (m Model) View() string {
|
|||
return m.list.View() + "\n" + m.renderBottom()
|
||||
|
||||
case stateMessage:
|
||||
header := titleStyle.Render(fmt.Sprintf("Message: %s", m.currentID))
|
||||
title := titleStyle.Render(fmt.Sprintf("Message: %s", m.currentID))
|
||||
header := title
|
||||
if idx, ok := m.entryIndex[m.currentID]; ok {
|
||||
if reason := m.entries[idx].Reason; reason != "" {
|
||||
gap := m.width - lipgloss.Width(title) - lipgloss.Width(reason) - 2
|
||||
if gap < 1 {
|
||||
gap = 1
|
||||
}
|
||||
header = title + strings.Repeat(" ", gap) + reasonStyle.Render(reason)
|
||||
}
|
||||
}
|
||||
scrollPct := int(m.viewport.ScrollPercent() * 100)
|
||||
headersHint := "H: full headers"
|
||||
if m.showFullHeaders {
|
||||
headersHint = "H: short headers"
|
||||
}
|
||||
status := statusBarStyle.Render(
|
||||
fmt.Sprintf(" ↑↓/SPC/PgUp/Dn: scroll │ s: save EML │ v: parts │ %s │ q: back │ %d%% ", headersHint, scrollPct),
|
||||
fmt.Sprintf(" ↑↓/SPC/PgUp/Dn: scroll │ s: save EML │ D: delete │ F: requeue │ v: parts │ %s │ q: back │ %d%% ", headersHint, scrollPct),
|
||||
)
|
||||
notice := ""
|
||||
if m.messageSaving {
|
||||
|
|
@ -452,7 +478,17 @@ func (m Model) renderBottom() string {
|
|||
label := fmt.Sprintf(" Fetching subjects: %d / %d ", m.loadingDone, m.loadingTotal)
|
||||
return dimStyle.Render(label) + "\n " + m.progress.View()
|
||||
}
|
||||
return statusBarStyle.Render(
|
||||
fmt.Sprintf(" %d message(s) │ Enter: open │ r: refresh │ q: quit ", len(m.list.Items())),
|
||||
)
|
||||
left := fmt.Sprintf(" %d message(s) │ Enter: open │ r: refresh │ F: flush │ q: quit ", len(m.list.Items()))
|
||||
reason := ""
|
||||
if item, ok := m.list.SelectedItem().(queueItem); ok {
|
||||
reason = item.entry.Reason
|
||||
}
|
||||
if reason == "" {
|
||||
return statusBarStyle.Render(left)
|
||||
}
|
||||
right := " " + reason + " "
|
||||
rightWidth := lipgloss.Width(right)
|
||||
leftPart := statusBarStyle.Width(m.width - rightWidth).Render(left)
|
||||
rightPart := lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Background(lipgloss.Color("241")).Render(right)
|
||||
return leftPart + rightPart
|
||||
}
|
||||
|
|
|
|||
27
msgs.go
27
msgs.go
|
|
@ -1,6 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
|
|
@ -34,6 +36,31 @@ func loadQueueCmd() tea.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func flushQueueCmd() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
exec.Command("postqueue", "-f").Run()
|
||||
entries, err := loadQueue()
|
||||
if err != nil {
|
||||
return queueErrMsg{err}
|
||||
}
|
||||
return queueParsedMsg{entries}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteMessageCmd(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
exec.Command("postsuper", "-d", id).Run()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func requeueMessageCmd(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
exec.Command("postsuper", "-r", id).Run()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func fetchSubjectCmd(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
raw, err := fetchHeaders(id)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,5 @@ var (
|
|||
partSepStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
attachStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214"))
|
||||
headerKeyStyle = lipgloss.NewStyle().Bold(true)
|
||||
reasonStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue