Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

# 2026-10-06

- **New built-in theme: `catppuccin-latte`** — a second light palette alongside
`kanagawa-light`, using the official Catppuccin Latte colors
(https://github.com/catppuccin/catppuccin): blue `#1E66F5` primary, lavender
`#8839EF` unread, peach `#DF8E1D` dates, red `#D20F39` errors/read-authors,
green `#40A02B` success, on the `#EFF1F5` base. The reader picks glamour's
`light` style for it, so markdown emails render for a light background too
(`glamourStyleFor`, `internal/ui/styles.go`). Tests: `TestApplyTheme_KnownNames`,
`TestGlamourStyleFor`.

# 2026-10-05

- **The folder window is the newest mail by arrival date, not the highest UIDs (#34)** — `FetchHeaders` picked its `inbox_count` window as the highest `n` UIDs. IMAP MOVE/COPY give a message a fresh, highest UID in the destination mailbox (RFC 9051 per-mailbox UIDs), so every mail bulk-moved *into* a folder — `I` approve on ToScreen, `:reset-toscreen`, `U` undo — landed above all real recent mail, filled the window, and pushed the recent mail below the cutoff where no display sort (`,m`) could bring it back. Only folders with more than `inbox_count` messages were affected, which is why a HEY-style Inbox of a dozen rows never showed it while a 12'000-mail legacy Inbox after its first screening did (reproduced on Hostpoint/Dovecot, so not an Infomaniak quirk; the demo's `moves.log` shows `ToScreen uid=273 → INBOX destUID=1397`). `FetchHeaders`, `FetchLatest` (`:everything`) and the per-folder search cap (`searchFolderCap`, was a literal 100) now narrow the window with one cheap `UID FETCH (UID INTERNALDATE)` and keep the `n` most recently received (`newestUIDsByInternalDate`, `internal/imap/client.go`); INTERNALDATE survives MOVE/COPY on every server tested, and the SORT extension is not used because Infomaniak does not offer it. Folders at or below `inbox_count`, and `inbox_count = 0`, take the old single-round-trip path unchanged, so a daily Inbox sees no new traffic. Verified on the live Hostpoint demo: a 10-row window returned April mail before, October mail after. Tests: `TestMem_FetchHeaders_WindowIsNewestByInternalDateNotUID`, `TestMem_FetchHeaders_FolderWithinLimitUnchanged`, `TestMem_FetchLatest_WindowIsNewestByInternalDateNotUID`, `TestMem_SearchFolder_CapIsNewestByInternalDateNotUID`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Keep your inbox clean without effort.

- **IMAP + SMTP** — direct connection via RFC 6851 MOVE, no local sync daemon required; stays in sync if you use it on mobile or different device [→](https://neomd.ssp.sh/docs/configuration/)
- **RFC 5322 compliant email delivery** — Message-IDs use sender's domain, proper MIME multipart/alternative structure (text/plain before text/html), quoted-printable encoding, and all required headers; ensures deliverability across all providers, spam filter compatibility, and correct email threading [→](https://neomd.ssp.sh/docs/configuration/email-standards/)
- **Themes** — six built-in palettes (`kanagawa` default, `kanagawa-paper`, `kanagawa-light` for daylight terminals, `rose-pine`, `gruvbox`, `osaka-jade`); pick via `[ui].theme = "..."` and override individual colour slots in an optional `[theme]` block [→](https://neomd.ssp.sh/docs/configuration/#theming)
- **Themes** — seven built-in palettes (`kanagawa` default, `kanagawa-paper`, `kanagawa-light` and `catppuccin-latte` for daylight terminals, `rose-pine`, `gruvbox`, `osaka-jade`); pick via `[ui].theme = "..."` and override individual colour slots in an optional `[theme]` block [→](https://neomd.ssp.sh/docs/configuration/#theming)

> [!NOTE]
> Folder switches you've seen before, deletes/archives/moves, and screening are **instant on any provider** — they're served from cache and update the list before the server answers (optimistic updates + a second IMAP connection). What still depends on your IMAP provider's latency: the **first** visit to a folder, **opening an email**, and **search**. On Hostpoint (the provider I use) those cost about 2 round trips at ~33ms each; on Gmail the same round trips run at ~570ms each. See [Benchmark](#benchmark) for full details and how to test your provider.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Keep your inbox clean without effort.

- **IMAP + SMTP** — direct connection via RFC 6851 MOVE, no local sync daemon required; stays in sync if you use it on mobile or different device [→](https://neomd.ssp.sh/docs/configuration/)
- **RFC 5322 compliant email delivery** — Message-IDs use sender's domain, proper MIME multipart/alternative structure (text/plain before text/html), quoted-printable encoding, and all required headers; ensures deliverability across all providers, spam filter compatibility, and correct email threading [→](https://neomd.ssp.sh/docs/configuration/email-standards/)
- **Themes** — six built-in palettes (`kanagawa` default, `kanagawa-paper`, `kanagawa-light` for daylight terminals, `rose-pine`, `gruvbox`, `osaka-jade`); pick via `[ui].theme = "..."` and override individual colour slots in an optional `[theme]` block [→](https://neomd.ssp.sh/docs/configuration/#theming)
- **Themes** — seven built-in palettes (`kanagawa` default, `kanagawa-paper`, `kanagawa-light` and `catppuccin-latte` for daylight terminals, `rose-pine`, `gruvbox`, `osaka-jade`); pick via `[ui].theme = "..."` and override individual colour slots in an optional `[theme]` block [→](https://neomd.ssp.sh/docs/configuration/#theming)

{{< callout type="info" >}}
Folder switches you've seen before, deletes/archives/moves, and screening are **instant on any provider** — they're served from cache and update the list before the server answers (optimistic updates + a second IMAP connection). What still depends on your IMAP provider's latency: the **first** visit to a folder, **opening an email**, and **search**. On Hostpoint (the provider I use) those cost about 2 round trips at ~33ms each; on Gmail the same round trips run at ~570ms each. See [Benchmark](#benchmark) for full details and how to test your provider.
Expand Down
5 changes: 3 additions & 2 deletions docs/content/docs/configuration/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ spam = "spam" #check capitalization of your pre-existing Spam folder, so
# Gmail uses different folder names — see docs/content/gmail.md for the correct mapping.

[ui]
theme = "kanagawa" # kanagawa | kanagawa-paper | kanagawa-light | rose-pine | gruvbox | osaka-jade
theme = "kanagawa" # kanagawa | kanagawa-paper | kanagawa-light | catppuccin-latte | rose-pine | gruvbox | osaka-jade
inbox_count = 200 # how many newest emails neomd loads per folder/reload
auto_screen_on_load = true # screen inbox automatically on every load (default true)
bg_sync_interval = 5 # background sync interval in minutes; 0 = disabled (default 5)
Expand Down Expand Up @@ -450,13 +450,14 @@ For a business account where you want a logo, table layout, and CSS that goldmar

## Theming

Pick from six built-in palettes via `[ui].theme`:
Pick from seven built-in palettes via `[ui].theme`:

| Name | Mode | Source |
|---|---|---|
| `kanagawa` (default) | dark | https://github.com/rebelot/kanagawa.nvim |
| `kanagawa-paper` | dark | https://github.com/thesimonho/kanagawa-paper.nvim |
| `kanagawa-light` | **light** | Lotus palette from kanagawa.nvim, paperwhite (#F2EFE9) background |
| `catppuccin-latte` | **light** | https://github.com/catppuccin/catppuccin — Latte |
| `rose-pine` | dark | https://github.com/rose-pine/rose-pine-theme |
| `gruvbox` | dark | https://github.com/morhetz/gruvbox |
| `osaka-jade` | dark | https://github.com/Justikun/omarchy-osaka-jade-theme |
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ func defaults() *Config {
Spam: "Spam",
},
UI: UIConfig{
Theme: "kanagawa", // built-in: kanagawa | kanagawa-paper | kanagawa-light | rose-pine | gruvbox | osaka-jade
Theme: "kanagawa", // built-in: kanagawa | kanagawa-paper | kanagawa-light | catppuccin-latte | rose-pine | gruvbox | osaka-jade
InboxCount: 200,
BgSyncInterval: 5,
MarkAsReadAfterSecs: 7,
Expand Down
12 changes: 11 additions & 1 deletion internal/ui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ var themes = map[string]config.Theme{
AuthorUnread: "#CC6D00", SubjectUnread: "#4E8CA2",
Error: "#C84053", Success: "#6F894E",
},
"catppuccin-latte": {
// https://github.com/catppuccin/catppuccin — Latte (light)
Bg: "#EFF1F5", Border: "#9CA0B0", Subtle: "#CCD0DA", Selected: "#DCE0E8",
Text: "#4C4F69", Muted: "#6C6F85",
Primary: "#1E66F5", Unread: "#8839EF",
Number: "#1E66F5", Date: "#DF8E1D",
AuthorRead: "#D20F39", SubjectRead: "#179299", SizeCol: "#6C6F85",
AuthorUnread: "#FE640B", SubjectUnread: "#04A5E5",
Error: "#D20F39", Success: "#40A02B",
},
"rose-pine": {
// https://github.com/rose-pine/rose-pine-theme — main variant
Bg: "#191724", Border: "#26233A", Subtle: "#1F1D2E", Selected: "#403D52",
Expand Down Expand Up @@ -128,7 +138,7 @@ func init() {
// `dark`. The legacy "auto" was rarely useful in practice and would now
// be ambiguous, so we collapse it to `dark` for predictability.
func glamourStyleFor(themeName string) string {
if themeName == "kanagawa-light" || themeName == "light" {
if themeName == "kanagawa-light" || themeName == "catppuccin-latte" || themeName == "light" {
return "light"
}
return "dark"
Expand Down
23 changes: 12 additions & 11 deletions internal/ui/styles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestKanagawaDefault(t *testing.T) {
}

func TestApplyTheme_KnownNames(t *testing.T) {
for _, name := range []string{"kanagawa", "kanagawa-paper", "kanagawa-light", "rose-pine", "gruvbox", "osaka-jade"} {
for _, name := range []string{"kanagawa", "kanagawa-paper", "kanagawa-light", "catppuccin-latte", "rose-pine", "gruvbox", "osaka-jade"} {
ApplyTheme(name, config.Theme{})
if colorBg == "" || colorPrimary == "" {
t.Errorf("ApplyTheme(%q): expected colors populated, got empty", name)
Expand Down Expand Up @@ -111,16 +111,17 @@ func TestGlamourStyleFor(t *testing.T) {
// falls back to "notty" which strips colours/wrapping. The mapper must
// always return one of glamour's built-in style names.
cases := map[string]string{
"kanagawa": "dark",
"kanagawa-paper": "dark",
"kanagawa-light": "light",
"rose-pine": "dark",
"gruvbox": "dark",
"osaka-jade": "dark",
"": "dark", // empty config falls through to dark
"unknown-theme": "dark", // unknown names also fall through, never to notty
"light": "light", // legacy literal still respected
"auto": "dark", // legacy "auto" collapses to dark for predictability
"kanagawa": "dark",
"kanagawa-paper": "dark",
"kanagawa-light": "light",
"catppuccin-latte": "light",
"rose-pine": "dark",
"gruvbox": "dark",
"osaka-jade": "dark",
"": "dark", // empty config falls through to dark
"unknown-theme": "dark", // unknown names also fall through, never to notty
"light": "light", // legacy literal still respected
"auto": "dark", // legacy "auto" collapses to dark for predictability
}
for in, want := range cases {
if got := glamourStyleFor(in); got != want {
Expand Down