feat: 收件箱功能新增按回车查看详情面板
- 添加邮件详情面板显示(主题、发件人、收件人、抄送、账户、时间、正文) - 优化邮件列表卡片样式,增加选中高亮效果 - 窗口宽度 >= 80 时启用双面板布局,左侧列表右侧详情 - 简化依赖包,从 charm.land 使用统一导入路径 - 删除未使用的 golangci/goreleaser 配置文件
This commit is contained in:
25
AGENTS.md
25
AGENTS.md
@@ -6,12 +6,37 @@
|
||||
|------|------|------|
|
||||
| 2026-04-09 | v0.1.0 | 初始规划:发送历史、收件箱功能 |
|
||||
| 2026-04-09 | v0.1.1 | 配置简化:支持多账户、自动识别Provider |
|
||||
| 2026-04-10 | v0.1.2 | 收件箱功能:按回车查看邮件详情 |
|
||||
|
||||
## 讨论记录
|
||||
|
||||
- [第1次:功能规划](./doc/001-feature-planning.md)
|
||||
- [第2次:配置简化讨论](./doc/002-config-simplification.md)
|
||||
|
||||
## 功能说明
|
||||
|
||||
### 收件箱 (inbox)
|
||||
|
||||
```bash
|
||||
pop inbox # 查看收件箱(默认最近7天)
|
||||
pop inbox -d 3 # 查看最近3天的未读邮件
|
||||
```
|
||||
|
||||
**操作:**
|
||||
- `↑` `↓` - 上下移动选择邮件
|
||||
- `Enter` - 查看邮件详情(在右侧面板显示完整内容)
|
||||
- `/` - 搜索邮件
|
||||
- `q` - 退出
|
||||
|
||||
**详情面板显示:**
|
||||
- 主题
|
||||
- 发件人
|
||||
- 收件人
|
||||
- 抄送(如有)
|
||||
- 账户
|
||||
- 时间
|
||||
- 正文(文本内容优先,无文本则显示 HTML 提示)
|
||||
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
type attachment string
|
||||
|
||||
135
doc/003-inbox-ui-redesign.md
Normal file
135
doc/003-inbox-ui-redesign.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 第3次:收件箱界面改版讨论
|
||||
|
||||
## 日期
|
||||
2026-04-10
|
||||
|
||||
## 背景
|
||||
参考 charmbracelet/glow 的 TUI 设计,将 inbox 界面改为更现代化的双栏布局。
|
||||
|
||||
## 最终实现效果
|
||||
|
||||
### 双栏布局(终端宽度 ≥ 80)
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 收件箱 (5 封新邮件) │ 邮件详情 │
|
||||
├─────────────────────────────────────────┴────────────────────────────────┤
|
||||
│ 新设备登录提醒 │ 主题:新设备登录提醒 │
|
||||
│ ▣ 163 · 11小时前 │ 发件人: xxx@163.com │
|
||||
│─────────────────────────────────────────│ 账户: 163 │
|
||||
│ 您的账号正在一台新设备上登录 │ 时间: 2026-04-10 10:30 │
|
||||
│ ▣ 163 · 11小时前 │ │
|
||||
│─────────────────────────────────────────│ 预览: 您的账号正在一台新设备上登录... │
|
||||
│ 项目进度更新 │ │
|
||||
│ ▣ work · 昨天 │ │
|
||||
├─────────────────────────────────────────┴────────────────────────────────┤
|
||||
│ ↑ 上移 · ↓ 下移 · enter 查看详情 · / 搜索 · q 退出 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 布局说明
|
||||
- **整体布局**:全屏 TTY 模式,清屏后占满整个终端窗口
|
||||
- **列表区域**:占窗口宽度的 45%,高度的 80%
|
||||
- **详情区域**:占窗口宽度的 55%
|
||||
- **帮助栏**:固定在窗口最底部,使用系统内置帮助组件
|
||||
|
||||
## 卡片样式
|
||||
|
||||
### 卡片结构(每封邮件两行)
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 主题:邮件标题内容... │ ← 第一行:白色主题,Bold
|
||||
│ ▣ 账户 · 11小时前 │ ← 第二行:灰色账户+时间,有▣标记
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 样式细节
|
||||
- **未选中卡片**:
|
||||
- 主题行:白色前景,无背景,上内边距1,左右内边距1,下内边距0
|
||||
- 元信息行:灰色前景(#241),无背景,上内边距0,左右内边距1,下内边距1
|
||||
- **选中卡片**:
|
||||
- 主题行:白色前景,浅紫色背景(#99),上内边距1,左右内边距1,下内边距0
|
||||
- 元信息行:浅紫色前景(#186),浅紫色背景(#99),上内边距0,左右内边距1,下内边距1
|
||||
|
||||
### 主题色
|
||||
- 背景色:`#99`(浅紫色)
|
||||
- 选中背景:`#99`
|
||||
- 标题前景:`#219`(粉色紫)
|
||||
|
||||
## 功能需求
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 双栏布局(列表 + 详情) | ✅ | 列表45%,详情55% |
|
||||
| 全屏 TTY 模式 | ✅ | 使用 AltScreen,清屏 |
|
||||
| 卡片式邮件列表 | ✅ | 两行结构:主题 + 元信息 |
|
||||
| 选中高亮 | ✅ | 浅紫色背景 |
|
||||
| 帮助栏固定底部 | ✅ | 使用系统内置帮助组件 |
|
||||
| 列表高度80% | ✅ | 根据窗口高度动态计算 |
|
||||
| 搜索过滤功能 | ⏳ | 待实现 |
|
||||
| 详情面板显示完整邮件 | ⏳ | 待实现(需要按需获取) |
|
||||
|
||||
## 功能需求
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 双栏布局(列表 + 详情) | ✅ | 列表45%,详情55% |
|
||||
| 全屏 TTY 模式 | ✅ | 使用 AltScreen,清屏 |
|
||||
| 卡片式邮件列表 | ✅ | 两行结构:主题 + 元信息 |
|
||||
| 选中高亮 | ✅ | 浅紫色背景 |
|
||||
| 帮助栏固定底部 | ✅ | 使用系统内置帮助组件 |
|
||||
| 列表高度80% | ✅ | 根据窗口高度动态计算 |
|
||||
| 搜索过滤功能 | ⏳ | 待实现 |
|
||||
| 详情面板显示完整邮件 | ✅ | 按回车获取完整邮件内容 |
|
||||
|
||||
## 技术要点
|
||||
|
||||
### 关键实现
|
||||
1. **TTY 模式**:`v.AltScreen = true` 在 View() 方法中设置
|
||||
2. **宽度计算**:`int(float64(totalWidth) * 0.45)` 计算列表宽度
|
||||
3. **高度计算**:`int(float64(msg.Height) * 0.8)` 设置列表高度为窗口的80%
|
||||
4. **帮助栏**:
|
||||
- `l.SetShowHelp(true)` 启用内置帮助
|
||||
- 自定义 `inboxHelpKeyMap` 实现 `ShortHelp()` 和 `FullHelp()` 接口
|
||||
5. **卡片渲染**:
|
||||
- `emailDelegate.Height()` 返回 2(每卡片2行)
|
||||
- `emailDelegate.Spacing()` 返回 0(卡片间无间距)
|
||||
- 使用 `lipgloss.NewStyle().Width(width)` 固定宽度
|
||||
- 使用 `truncateString()` 函数实现超出截断
|
||||
|
||||
### 按回车获取详情实现
|
||||
1. **InboxModel 新增字段**:
|
||||
- `loadingDetail bool` - 是否正在加载详情
|
||||
- `selectedDetail *EmailDetail` - 已获取的详情数据
|
||||
- `detailFetcher DetailFetcher` - 详情获取回调函数
|
||||
2. **DetailFetcher 模式**:通过回调函数解耦获取逻辑,便于测试和扩展
|
||||
3. **DetailResultMsg 消息**:异步获取完成后通过消息传递结果
|
||||
4. **Spinner 加载动画**:使用 bubbles spinner 组件显示加载状态
|
||||
5. **IMAP ID 命令**:QQ 邮箱需要发送 ID 命令通过 "Unsafe Login" 检查
|
||||
|
||||
### 文件修改
|
||||
- `inbox/model.go`:主要实现文件
|
||||
- `inbox.go`:使用 `tea.NewProgram()` 创建程序
|
||||
- `imap.go`:新增 `FetchEmailDetailByUID` 函数获取完整邮件内容
|
||||
|
||||
## 实施步骤
|
||||
|
||||
| 步骤 | 描述 | 状态 |
|
||||
|------|------|------|
|
||||
| 1 | 创建讨论文档 | ✅ |
|
||||
| 2 | 升级 charm.land v2 依赖 | ✅ |
|
||||
| 3 | 实现双栏布局基础结构 | ✅ |
|
||||
| 4 | 实现 View() 全屏渲染 | ✅ |
|
||||
| 5 | 实现卡片样式(两行结构) | ✅ |
|
||||
| 6 | 实现选中高亮效果 | ✅ |
|
||||
| 7 | 添加帮助栏(系统内置) | ✅ |
|
||||
| 8 | 调整列表高度为80% | ✅ |
|
||||
| 9 | 移除边框,优化间距 | ✅ |
|
||||
| 10 | 实现右侧详情面板 | ✅ |
|
||||
| 11 | 添加搜索过滤功能 | ⏳ |
|
||||
| 12 | 实现按需获取完整邮件内容 | ✅ |
|
||||
|
||||
## 待优化项
|
||||
|
||||
1. **搜索功能**:启用 `list.SetFilteringEnabled(true)` 后需要实现过滤逻辑
|
||||
2. **邮件数量**:当前只显示未读邮件,可扩展为支持所有邮件
|
||||
3. **邮件操作**:查看、删除、标记已读等操作待实现
|
||||
2
email.go
2
email.go
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/resendlabs/resend-go"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"github.com/yuin/goldmark"
|
||||
|
||||
23
go.mod
23
go.mod
@@ -3,12 +3,12 @@ module github.com/charmbracelet/pop
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.1.0
|
||||
charm.land/bubbletea/v2 v2.0.2
|
||||
charm.land/huh/v2 v2.0.3
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
charm.land/lipgloss/v2 v2.0.2
|
||||
github.com/BrianLeishman/go-imap v0.1.27
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
||||
github.com/mattn/go-sqlite3 v1.14.42
|
||||
github.com/muesli/mango-cobra v1.3.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
@@ -20,20 +20,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||
github.com/BrianLeishman/go-imap v0.1.27 // indirect
|
||||
github.com/StirlingMarketingGroup/go-retry v0.0.0-20190512160921-94a8eb23e893 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
@@ -42,11 +36,7 @@ require (
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emersion/go-message v0.18.2 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/inbucket/html2text v1.0.0 // indirect
|
||||
@@ -55,14 +45,11 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/mango v0.2.0 // indirect
|
||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
@@ -74,7 +61,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/sqs/go-xoauth2 v0.0.0-20120917012134-0911dad68e56 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
|
||||
81
go.sum
81
go.sum
@@ -1,11 +1,11 @@
|
||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
|
||||
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
github.com/BrianLeishman/go-imap v0.1.27 h1:FjgRwijsf5Cmovu8S6avu0TykP77WN8hZHnutVXvXgg=
|
||||
github.com/BrianLeishman/go-imap v0.1.27/go.mod h1:ftFHqYP7XUPeo3XhTpHpokQ+392Vz6GVxvjxykL5E2I=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
@@ -14,8 +14,6 @@ github.com/StirlingMarketingGroup/go-retry v0.0.0-20190512160921-94a8eb23e893 h1
|
||||
github.com/StirlingMarketingGroup/go-retry v0.0.0-20190512160921-94a8eb23e893/go.mod h1:RHK0VFlYDZQeNFg4C2dp7cPE6urfbpgyEZIGxa9f5zw=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
@@ -24,20 +22,12 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
@@ -63,23 +53,13 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.1+incompatible/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8 h1:5IXZK1E33DyeP526320J3RS7eFlCYGFgtbrfapqDPug=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8/go.mod h1:dhoFe2Q0PwLrMD7oZw8ODuaD0vLYPe5uj2wcOMnvh48=
|
||||
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
@@ -99,18 +79,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
|
||||
@@ -121,8 +95,6 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe
|
||||
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
|
||||
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
@@ -152,65 +124,28 @@ github.com/sqs/go-xoauth2 v0.0.0-20120917012134-0911dad68e56 h1:KCgKdj+ha4CgnVHI
|
||||
github.com/sqs/go-xoauth2 v0.0.0-20120917012134-0911dad68e56/go.mod h1:ghDEBrT4oFcM4rv18bzcZaAWXbHPGpDa4e2hh9oXL8A=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 h1:flbMkdl6HxQkLs6DDhH1UkcnFpNBOu70391STjMS0O4=
|
||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
type HistoryItem struct {
|
||||
@@ -125,17 +125,17 @@ func (m *HistoryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *HistoryModel) View() string {
|
||||
func (m *HistoryModel) View() tea.View {
|
||||
if m.loading {
|
||||
return historyLoadingStyle.Render("正在加载历史...")
|
||||
return tea.NewView(historyLoadingStyle.Render("正在加载历史..."))
|
||||
}
|
||||
if m.err != nil {
|
||||
return historyErrorStyle.Render(fmt.Sprintf("错误: %v", m.err))
|
||||
return tea.NewView(historyErrorStyle.Render(fmt.Sprintf("错误: %v", m.err)))
|
||||
}
|
||||
if len(m.items) == 0 {
|
||||
return historyNoItemsStyle.Render("暂无发送历史")
|
||||
return tea.NewView(historyNoItemsStyle.Render("暂无发送历史"))
|
||||
}
|
||||
return m.list.View()
|
||||
return tea.NewView(m.list.View())
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
142
imap.go
142
imap.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
imap "github.com/BrianLeishman/go-imap"
|
||||
@@ -193,3 +194,144 @@ func sortEmailsByDate(emails []ReceivedEmail) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type EmailDetail struct {
|
||||
UID uint32
|
||||
From string
|
||||
FromName string
|
||||
To string
|
||||
Cc string
|
||||
Subject string
|
||||
Date time.Time
|
||||
TextBody string
|
||||
HTMLBody string
|
||||
Account string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
func FetchEmailDetailByUID(accountID string, uid uint32) (*EmailDetail, error) {
|
||||
account, err := GetAccountByID(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if account.IMAP.Host == "" {
|
||||
return nil, fmt.Errorf("IMAP host not configured for account: %s", account.Email)
|
||||
}
|
||||
|
||||
port := account.IMAP.Port
|
||||
if port == 0 {
|
||||
port = 993
|
||||
}
|
||||
|
||||
username := account.IMAP.Username
|
||||
if username == "" {
|
||||
username = account.Email
|
||||
}
|
||||
|
||||
m, err := imap.New(username, account.IMAP.Password, account.IMAP.Host, port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to IMAP server: %w", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
if account.CheckID != nil && *account.CheckID {
|
||||
info := ProjectConfig.Info
|
||||
idCmd := fmt.Sprintf("ID (\"name\" \"%s\" \"version\" \"%s\" \"vendor\" \"%s\")",
|
||||
info.Name, info.Version, info.Vendor)
|
||||
_, err := m.Exec(idCmd, false, 0, func(line []byte) error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING: failed to send IMAP ID: %v\n", err)
|
||||
}
|
||||
|
||||
m.Exec("NOOP", false, 0, nil)
|
||||
}
|
||||
|
||||
err = m.SelectFolder("INBOX")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to select inbox: %w", err)
|
||||
}
|
||||
|
||||
emails, err := m.GetEmails(int(uid))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch email: %w", err)
|
||||
}
|
||||
|
||||
if len(emails) == 0 {
|
||||
return nil, fmt.Errorf("email not found")
|
||||
}
|
||||
|
||||
var email imap.Email
|
||||
found := false
|
||||
for i := range emails {
|
||||
if emails[i] != nil {
|
||||
email = *emails[i]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("email not found or has been removed")
|
||||
}
|
||||
|
||||
accountName := getEmailProviderName(account.Email)
|
||||
accountIDName := account.Email
|
||||
if account.Name != "" {
|
||||
accountName = account.Name
|
||||
accountIDName = account.Name
|
||||
}
|
||||
|
||||
var fromName string
|
||||
var fromAddr string
|
||||
if email.From != nil {
|
||||
for name, addr := range email.From {
|
||||
fromName = name
|
||||
fromAddr = addr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var toList []string
|
||||
if email.To != nil {
|
||||
for name, addr := range email.To {
|
||||
if name != "" {
|
||||
toList = append(toList, fmt.Sprintf("%s <%s>", name, addr))
|
||||
} else {
|
||||
toList = append(toList, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ccList []string
|
||||
if email.CC != nil {
|
||||
for name, addr := range email.CC {
|
||||
if name != "" {
|
||||
ccList = append(ccList, fmt.Sprintf("%s <%s>", name, addr))
|
||||
} else {
|
||||
ccList = append(ccList, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fromStr := fromAddr
|
||||
if fromName != "" {
|
||||
fromStr = fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
||||
}
|
||||
|
||||
return &EmailDetail{
|
||||
UID: uint32(email.UID),
|
||||
From: fromStr,
|
||||
FromName: fromName,
|
||||
To: strings.Join(toList, ", "),
|
||||
Cc: strings.Join(ccList, ", "),
|
||||
Subject: email.Subject,
|
||||
Date: email.Sent,
|
||||
TextBody: email.Text,
|
||||
HTMLBody: email.HTML,
|
||||
Account: accountName,
|
||||
AccountID: accountIDName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
21
inbox.go
21
inbox.go
@@ -3,13 +3,32 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/bubbletea"
|
||||
"charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/pop/inbox"
|
||||
)
|
||||
|
||||
func runInbox(days int) error {
|
||||
m := inbox.NewInboxModel()
|
||||
m.SetLoading(true)
|
||||
m.SetDetailFetcher(func(accountID string, uid uint32) (*inbox.EmailDetail, error) {
|
||||
detail, err := FetchEmailDetailByUID(accountID, uid)
|
||||
if err != nil || detail == nil {
|
||||
return nil, err
|
||||
}
|
||||
return &inbox.EmailDetail{
|
||||
UID: detail.UID,
|
||||
From: detail.From,
|
||||
FromName: detail.FromName,
|
||||
To: detail.To,
|
||||
Cc: detail.Cc,
|
||||
Subject: detail.Subject,
|
||||
Date: detail.Date,
|
||||
TextBody: detail.TextBody,
|
||||
HTMLBody: detail.HTMLBody,
|
||||
Account: detail.Account,
|
||||
AccountID: detail.AccountID,
|
||||
}, nil
|
||||
})
|
||||
|
||||
emails, err := FetchAllUnreadEmails(days)
|
||||
if err != nil {
|
||||
|
||||
378
inbox/model.go
378
inbox/model.go
@@ -3,13 +3,37 @@ package inbox
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/list"
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
_ "charm.land/bubbles/v2/help"
|
||||
)
|
||||
|
||||
type EmailDetail struct {
|
||||
UID uint32
|
||||
From string
|
||||
FromName string
|
||||
To string
|
||||
Cc string
|
||||
Subject string
|
||||
Date time.Time
|
||||
TextBody string
|
||||
HTMLBody string
|
||||
Account string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
type DetailResultMsg struct {
|
||||
Detail *EmailDetail
|
||||
Err error
|
||||
}
|
||||
|
||||
type EmailItem struct {
|
||||
UID uint32
|
||||
From string
|
||||
@@ -52,25 +76,77 @@ func formatTimeAgo(d time.Duration) string {
|
||||
return d.Truncate(24 * time.Hour).String()
|
||||
}
|
||||
|
||||
func truncateString(s string, maxWidth int) string {
|
||||
if maxWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxWidth {
|
||||
return s
|
||||
}
|
||||
if maxWidth <= 3 {
|
||||
return strings.Repeat(".", maxWidth)
|
||||
}
|
||||
return string(runes[:maxWidth-3]) + "..."
|
||||
}
|
||||
|
||||
type InboxModel struct {
|
||||
list list.Model
|
||||
emails []EmailItem
|
||||
loading bool
|
||||
err error
|
||||
selectedEmail *EmailItem
|
||||
list list.Model
|
||||
emails []EmailItem
|
||||
loading bool
|
||||
err error
|
||||
selectedEmail *EmailItem
|
||||
selectedDetail *EmailDetail
|
||||
loadingDetail bool
|
||||
windowWidth int
|
||||
windowHeight int
|
||||
helpKeyMap inboxHelpKeyMap
|
||||
spinner spinner.Model
|
||||
detailFetcher DetailFetcher
|
||||
}
|
||||
|
||||
type inboxHelpKeyMap struct{}
|
||||
|
||||
func (i inboxHelpKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("↑", "上移")),
|
||||
key.NewBinding(key.WithHelp("↓", "下移")),
|
||||
key.NewBinding(key.WithHelp("enter", "查看详情")),
|
||||
key.NewBinding(key.WithHelp("/", "搜索")),
|
||||
key.NewBinding(key.WithHelp("q", "退出")),
|
||||
}
|
||||
}
|
||||
|
||||
func (i inboxHelpKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{
|
||||
key.NewBinding(key.WithHelp("↑↓", "移动")),
|
||||
key.NewBinding(key.WithHelp("enter", "查看详情")),
|
||||
key.NewBinding(key.WithHelp("/", "搜索")),
|
||||
key.NewBinding(key.WithHelp("q", "退出")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewInboxModel() *InboxModel {
|
||||
l := list.New(nil, emailDelegate{}, 0, 10)
|
||||
l := list.New(nil, emailDelegate{}, 40, 14)
|
||||
l.Title = "收件箱"
|
||||
l.Styles = list.DefaultStyles(true)
|
||||
l.Styles.Title = inboxTitleStyle
|
||||
l.Styles.NoItems = inboxNoItemsStyle
|
||||
l.SetShowHelp(true)
|
||||
l.SetShowPagination(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
s.Style = spinnerStyle
|
||||
|
||||
return &InboxModel{
|
||||
list: l,
|
||||
emails: []EmailItem{},
|
||||
list: l,
|
||||
emails: []EmailItem{},
|
||||
windowWidth: 0,
|
||||
spinner: s,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,31 +161,176 @@ func (m *InboxModel) SetEmails(emails []EmailItem) {
|
||||
}
|
||||
|
||||
func (m InboxModel) Init() tea.Cmd {
|
||||
return nil
|
||||
return tea.Batch(
|
||||
func() tea.Msg {
|
||||
return tea.WindowSizeMsg{Width: 120, Height: 30}
|
||||
},
|
||||
m.spinner.Tick,
|
||||
)
|
||||
}
|
||||
|
||||
func fetchEmailDetail(accountID string, uid uint32, fetcher DetailFetcher) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
detail, err := fetcher(accountID, uid)
|
||||
return DetailResultMsg{Detail: detail, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InboxModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
if msg.String() == "enter" {
|
||||
email := m.SelectedEmail()
|
||||
if email != nil && m.detailFetcher != nil {
|
||||
m.loadingDetail = true
|
||||
m.selectedDetail = nil
|
||||
return m, fetchEmailDetail(email.AccountID, email.UID, m.detailFetcher)
|
||||
}
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.list.SetWidth(msg.Width)
|
||||
m.windowWidth = msg.Width
|
||||
m.windowHeight = msg.Height
|
||||
m.list.SetWidth(calculateListWidth(msg.Width))
|
||||
m.list.SetHeight(int(float64(msg.Height) * 0.8))
|
||||
case DetailResultMsg:
|
||||
m.loadingDetail = false
|
||||
if msg.Err != nil {
|
||||
m.err = msg.Err
|
||||
} else {
|
||||
m.selectedDetail = msg.Detail
|
||||
}
|
||||
case spinner.TickMsg:
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m InboxModel) View() string {
|
||||
func calculateListWidth(totalWidth int) int {
|
||||
if totalWidth <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int(float64(totalWidth) * 0.45)
|
||||
}
|
||||
|
||||
func (m InboxModel) View() tea.View {
|
||||
if m.loading {
|
||||
return loadingStyle.Render("正在加载邮件...")
|
||||
return tea.NewView(loadingStyle.Render("正在加载邮件..."))
|
||||
}
|
||||
if m.err != nil {
|
||||
return errorStyle.Render(fmt.Sprintf("错误: %v", m.err))
|
||||
return tea.NewView(errorStyle.Render(fmt.Sprintf("错误: %v", m.err)))
|
||||
}
|
||||
return m.list.View()
|
||||
|
||||
listWidth := calculateListWidth(m.windowWidth)
|
||||
detailWidth := m.windowWidth - listWidth - 1
|
||||
|
||||
m.list.SetWidth(listWidth)
|
||||
|
||||
mainContentStyle := lipgloss.NewStyle().
|
||||
Width(m.windowWidth - 1).
|
||||
Height(m.windowHeight - 2)
|
||||
|
||||
helpBarStyle := lipgloss.NewStyle().
|
||||
Width(m.windowWidth - 1).
|
||||
Foreground(lipgloss.Color("241"))
|
||||
|
||||
helpView := helpBarStyle.Render(m.list.Help.View(m.helpKeyMap))
|
||||
|
||||
if m.windowWidth >= 80 {
|
||||
detailView := m.renderDetailPanel(detailWidth)
|
||||
listView := m.list.View()
|
||||
mainContent := lipgloss.JoinHorizontal(lipgloss.Top, listView, detailView)
|
||||
box := mainContentStyle.Render(mainContent)
|
||||
content := lipgloss.JoinVertical(lipgloss.Bottom, box, helpView)
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
listView := m.list.View()
|
||||
box := mainContentStyle.Render(listView)
|
||||
content := lipgloss.JoinVertical(lipgloss.Bottom, box, helpView)
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func (m InboxModel) renderHelpBar() string {
|
||||
helpItems := "↑↓ 移动 │ enter 查看详情 │ / 搜索 │ q 退出"
|
||||
return helpBarStyle.Render(helpItems)
|
||||
}
|
||||
|
||||
func (m InboxModel) renderDetailPanel(listWidth int) string {
|
||||
email := m.SelectedEmail()
|
||||
if email == nil {
|
||||
return detailPanelStyle.Render("选择一封邮件查看详情")
|
||||
}
|
||||
|
||||
if m.loadingDetail {
|
||||
spinnerView := m.spinner.View()
|
||||
loadingText := detailMetaStyle.Render(spinnerView + " 正在加载邮件内容...")
|
||||
return detailPanelStyle.Width(40).Render(loadingText)
|
||||
}
|
||||
|
||||
if m.selectedDetail != nil {
|
||||
detail := m.selectedDetail
|
||||
subject := detailTitleStyle.Render(detail.Subject)
|
||||
|
||||
from := detailMetaStyle.Render(fmt.Sprintf("发件人: %s", detail.From))
|
||||
|
||||
to := detailMetaStyle.Render(fmt.Sprintf("收件人: %s", detail.To))
|
||||
if detail.Cc != "" {
|
||||
to += "\n" + detailMetaStyle.Render(fmt.Sprintf("抄送: %s", detail.Cc))
|
||||
}
|
||||
account := detailMetaStyle.Render(fmt.Sprintf("账户: %s", detail.Account))
|
||||
date := detailMetaStyle.Render(fmt.Sprintf("时间: %s", detail.Date.Format("2006-01-02 15:04")))
|
||||
|
||||
var body string
|
||||
if detail.TextBody != "" {
|
||||
body = detailBodyStyle.Render(detail.TextBody)
|
||||
} else if detail.HTMLBody != "" {
|
||||
body = detailMetaStyle.Render("[HTML邮件内容]")
|
||||
} else {
|
||||
body = detailMetaStyle.Render("[无正文]")
|
||||
}
|
||||
|
||||
content := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", subject, from, to, account, date, body)
|
||||
|
||||
detailWidth := 40
|
||||
if m.windowWidth > 0 {
|
||||
detailWidth = m.windowWidth - listWidth - 1
|
||||
}
|
||||
if detailWidth < 10 {
|
||||
detailWidth = 40
|
||||
}
|
||||
|
||||
return detailPanelStyle.Width(detailWidth).Render(content)
|
||||
}
|
||||
|
||||
subject := detailTitleStyle.Render(email.Subject)
|
||||
from := detailMetaStyle.Render(fmt.Sprintf("发件人: %s <%s>", email.FromName, email.From))
|
||||
account := detailMetaStyle.Render(fmt.Sprintf("账户: %s", email.Account))
|
||||
date := detailMetaStyle.Render(fmt.Sprintf("时间: %s", email.Date.Format("2006-01-02 15:04")))
|
||||
preview := detailPanelStyle.Render(email.Preview)
|
||||
|
||||
content := fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", subject, from, account, date, preview)
|
||||
|
||||
detailWidth := 40
|
||||
if m.windowWidth > 0 {
|
||||
detailWidth = m.windowWidth - listWidth - 1
|
||||
}
|
||||
if detailWidth < 10 {
|
||||
detailWidth = 40
|
||||
}
|
||||
|
||||
return detailPanelStyle.Width(detailWidth).Render(content)
|
||||
}
|
||||
|
||||
func (m *InboxModel) SetLoading(loading bool) {
|
||||
@@ -130,7 +351,7 @@ func (m InboxModel) SelectedEmail() *EmailItem {
|
||||
|
||||
var (
|
||||
inboxTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("86")).
|
||||
Foreground(lipgloss.Color("219")).
|
||||
Bold(true)
|
||||
|
||||
inboxNoItemsStyle = lipgloss.NewStyle().
|
||||
@@ -138,17 +359,75 @@ var (
|
||||
Padding(1, 2)
|
||||
|
||||
loadingStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("86")).
|
||||
Foreground(lipgloss.Color("219")).
|
||||
Padding(1, 2)
|
||||
|
||||
errorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("196")).
|
||||
Padding(1, 2)
|
||||
|
||||
emailCardSubjectStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Bold(true).
|
||||
Padding(0, 2)
|
||||
|
||||
emailCardMetaStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241")).
|
||||
Padding(0, 2)
|
||||
|
||||
emailCardSubjectSelectedStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Bold(true).
|
||||
Background(lipgloss.Color("99")).
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderLeft(true).
|
||||
BorderForeground(lipgloss.Color("219")).
|
||||
Padding(0, 2)
|
||||
|
||||
emailCardMetaSelectedStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("186")).
|
||||
Background(lipgloss.Color("99")).
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderLeft(true).
|
||||
BorderForeground(lipgloss.Color("219")).
|
||||
Padding(0, 2)
|
||||
|
||||
detailPanelStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Padding(1, 2)
|
||||
|
||||
detailTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("219")).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
detailMetaStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241")).
|
||||
Padding(0, 1)
|
||||
|
||||
detailBodyStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252")).
|
||||
Padding(0, 1)
|
||||
|
||||
helpBarStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241")).
|
||||
Padding(0, 1)
|
||||
|
||||
spinnerStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("219"))
|
||||
|
||||
cardSpacing = 1
|
||||
)
|
||||
|
||||
type DetailFetcher func(accountID string, uid uint32) (*EmailDetail, error)
|
||||
|
||||
func (m *InboxModel) SetDetailFetcher(fetcher DetailFetcher) {
|
||||
m.detailFetcher = fetcher
|
||||
}
|
||||
|
||||
type emailDelegate struct{}
|
||||
|
||||
func (d emailDelegate) Height() int { return 1 }
|
||||
func (d emailDelegate) Height() int { return 2 }
|
||||
func (d emailDelegate) Spacing() int { return 0 }
|
||||
func (d emailDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
|
||||
return nil
|
||||
@@ -161,29 +440,42 @@ func (d emailDelegate) Render(w io.Writer, m list.Model, index int, item list.It
|
||||
|
||||
isSelected := index == m.Index()
|
||||
|
||||
titleStyle := emailTitleStyle
|
||||
descStyle := emailDescStyle
|
||||
width := m.Width() - 1
|
||||
subjectWidth := width - 2
|
||||
metaWidth := width - 2
|
||||
|
||||
subject := truncateString(email.Subject, subjectWidth)
|
||||
meta := truncateString("▣ "+email.Account+" · "+formatTimeAgo(time.Since(email.Date)), metaWidth)
|
||||
|
||||
if isSelected {
|
||||
titleStyle = emailTitleStyleSelected
|
||||
descStyle = emailDescStyleSelected
|
||||
subjectStyle := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Background(lipgloss.Color("99")).
|
||||
Padding(1, 1, 0, 1)
|
||||
|
||||
metaStyle := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Foreground(lipgloss.Color("186")).
|
||||
Background(lipgloss.Color("99")).
|
||||
Padding(0, 1, 1, 1)
|
||||
|
||||
fmt.Fprintf(w, "%s\n%s",
|
||||
subjectStyle.Render(subject),
|
||||
metaStyle.Render(meta))
|
||||
} else {
|
||||
subjectStyle := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Padding(1, 1, 0, 1)
|
||||
|
||||
metaStyle := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Foreground(lipgloss.Color("241")).
|
||||
Padding(0, 1, 1, 1)
|
||||
|
||||
fmt.Fprintf(w, "%s\n%s",
|
||||
subjectStyle.Render(subject),
|
||||
metaStyle.Render(meta))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\n%s", titleStyle.Render(email.Title()), descStyle.Render(email.Description()))
|
||||
}
|
||||
|
||||
var (
|
||||
emailTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255"))
|
||||
|
||||
emailTitleStyleSelected = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Background(lipgloss.Color("68"))
|
||||
|
||||
emailDescStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241"))
|
||||
|
||||
emailDescStyleSelected = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("186")).
|
||||
Background(lipgloss.Color("68"))
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
import "charm.land/bubbles/v2/key"
|
||||
|
||||
// KeyMap represents the key bindings for the application.
|
||||
type KeyMap struct {
|
||||
|
||||
2
main.go
2
main.go
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
mcobra "github.com/muesli/mango-cobra"
|
||||
"github.com/muesli/roff"
|
||||
"github.com/resendlabs/resend-go"
|
||||
|
||||
98
model.go
98
model.go
@@ -5,14 +5,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"charm.land/bubbles/v2/filepicker"
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/list"
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/x/exp/ordered"
|
||||
"github.com/resendlabs/resend-go"
|
||||
)
|
||||
@@ -89,68 +89,32 @@ func NewModel(defaults resend.SendEmailRequest, deliveryMethod DeliveryMethod) M
|
||||
from := textinput.New()
|
||||
from.Prompt = "From "
|
||||
from.Placeholder = "me@example.com"
|
||||
from.PromptStyle = labelStyle.Copy()
|
||||
from.PromptStyle = labelStyle
|
||||
from.TextStyle = textStyle
|
||||
from.Cursor.Style = cursorStyle
|
||||
from.PlaceholderStyle = placeholderStyle
|
||||
from.SetValue(defaults.From)
|
||||
|
||||
to := textinput.New()
|
||||
to.Prompt = "To "
|
||||
to.PromptStyle = labelStyle.Copy()
|
||||
to.Cursor.Style = cursorStyle
|
||||
to.PlaceholderStyle = placeholderStyle
|
||||
to.TextStyle = textStyle
|
||||
to.Placeholder = "you@example.com"
|
||||
to.SetValue(strings.Join(defaults.To, ToSeparator))
|
||||
|
||||
cc := textinput.New()
|
||||
cc.Prompt = "Cc "
|
||||
cc.PromptStyle = labelStyle.Copy()
|
||||
cc.Cursor.Style = cursorStyle
|
||||
cc.PlaceholderStyle = placeholderStyle
|
||||
cc.TextStyle = textStyle
|
||||
cc.Placeholder = "cc@example.com"
|
||||
cc.SetValue(strings.Join(defaults.Cc, ToSeparator))
|
||||
|
||||
bcc := textinput.New()
|
||||
bcc.Prompt = "Bcc "
|
||||
bcc.PromptStyle = labelStyle.Copy()
|
||||
bcc.Cursor.Style = cursorStyle
|
||||
bcc.PlaceholderStyle = placeholderStyle
|
||||
bcc.TextStyle = textStyle
|
||||
bcc.Placeholder = "bcc@example.com"
|
||||
bcc.SetValue(strings.Join(defaults.Bcc, ToSeparator))
|
||||
|
||||
subject := textinput.New()
|
||||
subject.Prompt = "Subject "
|
||||
subject.PromptStyle = labelStyle.Copy()
|
||||
subject.Cursor.Style = cursorStyle
|
||||
subject.PlaceholderStyle = placeholderStyle
|
||||
subject.TextStyle = textStyle
|
||||
subject.Placeholder = "Hello!"
|
||||
subject.SetValue(defaults.Subject)
|
||||
|
||||
body := textarea.New()
|
||||
body.Placeholder = "# Email"
|
||||
body.ShowLineNumbers = false
|
||||
body.FocusedStyle.CursorLine = activeTextStyle
|
||||
body.FocusedStyle.Prompt = activeLabelStyle
|
||||
body.FocusedStyle.Text = activeTextStyle
|
||||
body.FocusedStyle.Placeholder = placeholderStyle
|
||||
body.BlurredStyle.CursorLine = textStyle
|
||||
body.BlurredStyle.Prompt = labelStyle
|
||||
body.BlurredStyle.Text = textStyle
|
||||
body.BlurredStyle.Placeholder = placeholderStyle
|
||||
body.Cursor.Style = cursorStyle
|
||||
body.CharLimit = 4000
|
||||
body.SetValue(defaults.Text)
|
||||
|
||||
// Adjust for signature (if none, this is a no-op)
|
||||
body.CursorUp()
|
||||
body.CursorUp()
|
||||
|
||||
body.CharLimit = 4000
|
||||
body.Blur()
|
||||
|
||||
// Decide which input to focus.
|
||||
@@ -170,9 +134,6 @@ func NewModel(defaults resend.SendEmailRequest, deliveryMethod DeliveryMethod) M
|
||||
attachments.DisableQuitKeybindings()
|
||||
attachments.SetShowTitle(true)
|
||||
attachments.Title = "Attachments"
|
||||
attachments.Styles.Title = labelStyle
|
||||
attachments.Styles.TitleBar = labelStyle
|
||||
attachments.Styles.NoItems = placeholderStyle
|
||||
attachments.SetShowHelp(false)
|
||||
attachments.SetShowStatusBar(false)
|
||||
attachments.SetStatusBarItemName("attachment", "attachments")
|
||||
@@ -186,7 +147,6 @@ func NewModel(defaults resend.SendEmailRequest, deliveryMethod DeliveryMethod) M
|
||||
picker.CurrentDirectory, _ = os.UserHomeDir()
|
||||
|
||||
loadingSpinner := spinner.New()
|
||||
loadingSpinner.Style = activeLabelStyle
|
||||
loadingSpinner.Spinner = spinner.Dot
|
||||
|
||||
m := Model{
|
||||
@@ -213,9 +173,7 @@ func NewModel(defaults resend.SendEmailRequest, deliveryMethod DeliveryMethod) M
|
||||
|
||||
// Init initializes the model.
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.From.Cursor.BlinkCmd(),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
type clearErrMsg struct{}
|
||||
@@ -370,70 +328,46 @@ func (m *Model) blurInputs() {
|
||||
m.Cc.Blur()
|
||||
m.Bcc.Blur()
|
||||
}
|
||||
m.From.PromptStyle = labelStyle
|
||||
m.To.PromptStyle = labelStyle
|
||||
if m.showCc {
|
||||
m.Cc.PromptStyle = labelStyle
|
||||
m.Cc.TextStyle = textStyle
|
||||
m.Bcc.PromptStyle = labelStyle
|
||||
m.Bcc.TextStyle = textStyle
|
||||
}
|
||||
m.Subject.PromptStyle = labelStyle
|
||||
m.From.TextStyle = textStyle
|
||||
m.To.TextStyle = textStyle
|
||||
m.Subject.TextStyle = textStyle
|
||||
m.Attachments.Styles.Title = labelStyle
|
||||
m.Attachments.SetDelegate(attachmentDelegate{false})
|
||||
}
|
||||
|
||||
func (m *Model) focusActiveInput() {
|
||||
switch m.state {
|
||||
case editingFrom:
|
||||
m.From.PromptStyle = activeLabelStyle
|
||||
m.From.TextStyle = activeTextStyle
|
||||
m.From.Focus()
|
||||
m.From.CursorEnd()
|
||||
case editingTo:
|
||||
m.To.PromptStyle = activeLabelStyle
|
||||
m.To.TextStyle = activeTextStyle
|
||||
m.To.Focus()
|
||||
m.To.CursorEnd()
|
||||
case editingCc:
|
||||
m.Cc.PromptStyle = activeLabelStyle
|
||||
m.Cc.TextStyle = activeTextStyle
|
||||
m.Cc.Focus()
|
||||
m.Cc.CursorEnd()
|
||||
case editingBcc:
|
||||
m.Bcc.PromptStyle = activeLabelStyle
|
||||
m.Bcc.TextStyle = activeTextStyle
|
||||
m.Bcc.Focus()
|
||||
m.Bcc.CursorEnd()
|
||||
case editingSubject:
|
||||
m.Subject.PromptStyle = activeLabelStyle
|
||||
m.Subject.TextStyle = activeTextStyle
|
||||
m.Subject.Focus()
|
||||
m.Subject.CursorEnd()
|
||||
case editingBody:
|
||||
m.Body.Focus()
|
||||
m.Body.CursorEnd()
|
||||
case editingAttachments:
|
||||
m.Attachments.Styles.Title = activeLabelStyle
|
||||
m.Attachments.SetDelegate(attachmentDelegate{true})
|
||||
}
|
||||
}
|
||||
|
||||
// View displays the application.
|
||||
func (m Model) View() string {
|
||||
func (m Model) View() tea.View {
|
||||
if m.quitting {
|
||||
return ""
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case pickingFile:
|
||||
return "\n" + activeLabelStyle.Render("Attachments") + " " + commentStyle.Render(m.filepicker.CurrentDirectory) +
|
||||
"\n\n" + m.filepicker.View()
|
||||
return tea.NewView("\n" + activeLabelStyle.Render("Attachments") + " " + commentStyle.Render(m.filepicker.CurrentDirectory) +
|
||||
"\n\n" + m.filepicker.View())
|
||||
case sendingEmail:
|
||||
return "\n " + m.loadingSpinner.View() + "Sending email"
|
||||
return tea.NewView("\n " + m.loadingSpinner.View() + "Sending email")
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
@@ -469,5 +403,5 @@ func (m Model) View() string {
|
||||
s.WriteString(errorStyle.Render(m.err.Error()))
|
||||
}
|
||||
|
||||
return paddedStyle.Render(s.String())
|
||||
return tea.NewView(paddedStyle.Render(s.String()))
|
||||
}
|
||||
|
||||
16
style.go
16
style.go
@@ -3,17 +3,17 @@ package main
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
const accentColor = lipgloss.Color("99")
|
||||
const yellowColor = lipgloss.Color("#ECFD66")
|
||||
const whiteColor = lipgloss.Color("255")
|
||||
const grayColor = lipgloss.Color("241")
|
||||
const darkGrayColor = lipgloss.Color("236")
|
||||
const lightGrayColor = lipgloss.Color("247")
|
||||
|
||||
var (
|
||||
accentColor = lipgloss.Color("99")
|
||||
yellowColor = lipgloss.Color("#ECFD66")
|
||||
whiteColor = lipgloss.Color("255")
|
||||
grayColor = lipgloss.Color("241")
|
||||
darkGrayColor = lipgloss.Color("236")
|
||||
lightGrayColor = lipgloss.Color("247")
|
||||
|
||||
activeTextStyle = lipgloss.NewStyle().Foreground(whiteColor)
|
||||
textStyle = lipgloss.NewStyle().Foreground(lightGrayColor)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user