Skip to content

Commit 2456ae1

Browse files
committed
The big one: Windows auto preview could select chafa before the native opener, so Windows users got pixelated symbol previews by default. That is now changed in internal/media/
backend_windows.go:21: Windows now tries sixel, then native external opener, then chafa. Linux order is unchanged.
1 parent f2c3936 commit 2456ae1

15 files changed

Lines changed: 249 additions & 7 deletions

‎PLAN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Implementation is past the local-shell phase and currently sits at a DB-first, l
1212
- SQLite-backed local state with migrations, chat/message/media/draft storage, stats, and FTS-backed search.
1313
- Bubble Tea TUI with modal interaction (`normal`, `insert`, `visual`, `command`, `search`), chat list, message viewport, optional info pane, composer, filters, and help.
1414
- Local draft persistence, local outgoing message persistence, normal/visual message yanking, text clipboard integration, image clipboard paste/copy integration, attachment staging, message delete flow, and search routing by pane.
15-
- Media backend detection and in-chat preview behavior with `sixel`, Unix `ueberzug++`, `chafa`, compact audio playback rows, plus native external open/save fallback paths.
15+
- Media backend detection and in-chat preview behavior with `sixel`, Unix `ueberzug++`, `chafa`, compact audio playback rows, plus native external open/save fallback paths. Windows auto preview prefers Sixel and then the native opener before `chafa`, because Chafa is a low-resolution symbol fallback there rather than a high-quality overlay path.
1616
- Real `whatsmeow` session store, QR login, logout, rejected-session cleanup, first-use-state local reset on logout, and `doctor` session status reporting.
1717
- Live WhatsApp connection bootstrap from a paired session, protocol event subscription, inbound chat/message/receipt/media metadata ingestion into SQLite, DB-first UI refreshes, and visible connection state.
1818
- On-demand remote history fetch for the focused chat, using SQLite paging first and then anchored `whatsmeow` history sync requests before the oldest known local message.
@@ -27,7 +27,7 @@ Implementation is past the local-shell phase and currently sits at a DB-first, l
2727
- Chat title quality tracking with source precedence, group/contact metadata refresh, and safe placeholders so group JIDs/phone-like IDs are not treated as real names.
2828
- Direct-chat identity hardening for WhatsApp PN/LID aliases: canonicalize mapped 1:1 chats onto a single chat ID, merge already-split alias rows/messages/drafts/history cursors in SQLite, and preserve the active conversation when a live merge remaps the selected chat.
2929
- Large-history TUI guardrails: historical imports avoid refresh storms, reconnect catch-up sync shows a blocking progress overlay and batches snapshot refreshes until completion, live refreshes are debounced, stale snapshot reloads do not steal chat focus, message rendering is bounded to the visible window, message cursor scrolling behaves like the chat list viewport, duplicate in-flight history requests are suppressed, and `ueberzug++` media/avatar/sticker overlays pause into blank reserved space while scrolling before resyncing.
30-
- Terminal/UI polish for real chat data: full/compat/auto emoji rendering, stable emoji fallback for terminals such as `st`, pywal-backed mode indicators with per-mode hex overrides, focused-pane borders that distinguish chat-list vs message-pane input focus, highlighted unread-count badges in the chat list, a structured keymap-driven help screen, non-redundant mode prompts, configurable help/prompt key hints, search match counts in the status bar, configurable cancellation/search-clear bindings, and native Windows runtime defaults that do not affect Linux behavior.
30+
- Terminal/UI polish for real chat data: full/compat/auto emoji rendering, stable emoji fallback for terminals such as `st` and classic Windows console hosts, pywal-backed mode indicators with per-mode hex overrides, focused-pane borders that distinguish chat-list vs message-pane input focus, highlighted unread-count badges in the chat list, a structured keymap-driven help screen, non-redundant mode prompts, configurable help/prompt key hints, search match counts in the status bar, configurable cancellation/search-clear bindings, platform-specific terminal focus-reporting options, and native Windows runtime defaults that do not affect Linux behavior.
3131
- Demo/dev workflows that exercise the full local UI without a live WhatsApp session.
3232

3333
### In progress

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,15 @@ All TUI action keys are configurable in `config.toml` using flat `key_<mode>_<ac
211211

212212
## Media and previews
213213

214-
`preview_backend = "auto"` chooses the best available inline renderer in this order: Sixel, `ueberzug++` where supported, `chafa`, external opener, none. Images, videos, stickers, and avatars can render inline when a capable backend is available. Videos use generated thumbnails when `ffmpeg` is available. Audio messages render as compact playback rows and use `audio_player_command`, which defaults to `mpv --no-video --no-terminal --really-quiet {path}` on Linux and the native Windows opener on Windows.
214+
`preview_backend = "auto"` chooses the best available renderer. On Linux the order is Sixel, `ueberzug++`, `chafa`, external opener, none. On Windows the order is Sixel, native external opener, `chafa`, none, so Windows does not default to low-resolution symbol previews when the native opener is available. Images, videos, stickers, and avatars can render inline when a capable backend is selected. Videos use generated thumbnails when `ffmpeg` is available. Audio messages render as compact playback rows and use `audio_player_command`, which defaults to `mpv --no-video --no-terminal --really-quiet {path}` on Linux and the native Windows opener on Windows.
215215

216216
Remote media starts as metadata in SQLite. Opening, previewing, saving, or running `vimwhat media open <message-id>` can download it through the paired WhatsApp session when a download descriptor exists.
217217

218218
## Emoji and theming
219219

220-
Emoji rendering defaults to `emoji_mode = "auto"` in `config.toml`. Auto mode preserves full emoji sequences on most UTF-8 terminals, but uses the stable compatibility path for terminals such as `st` that can display emoji glyphs while still misreporting complex emoji cell widths. Set `emoji_mode = "compat"` to force stable degraded rendering, or `emoji_mode = "full"` to force skin tones, ZWJ professions/families, and flags.
220+
Emoji rendering defaults to `emoji_mode = "auto"` in `config.toml`. Auto mode preserves full emoji sequences on most UTF-8 terminals, but uses the stable compatibility path for terminals such as `st` and classic Windows console hosts that can display emoji glyphs while still misreporting complex emoji cell widths. Set `emoji_mode = "compat"` to force stable degraded rendering, or `emoji_mode = "full"` to force skin tones, ZWJ professions/families, and flags.
221+
222+
On Windows, run `vimwhat doctor` if the TUI looks wrong. It reports the terminal environment variables used for media, emoji, and focus-reporting decisions. Older console hosts may not support all optional terminal features; Windows focus reporting is enabled only for known modern hosts or with `VIMWHAT_FORCE_REPORT_FOCUS=1`.
221223

222224
Mode indicator colors default to pywal-derived theme colors:
223225

‎internal/app/app.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2719,6 +2719,7 @@ func printDoctor(env Environment, w io.Writer) {
27192719
fmt.Sprintf("downloads dir: %s", env.Config.DownloadsDir),
27202720
fmt.Sprintf("leader key: %s", env.Config.LeaderKey),
27212721
fmt.Sprintf("emoji mode: %s -> %s (TERM=%s UTF-8=%s)", emojiMode, resolvedEmojiMode, emptyAsAuto(os.Getenv("TERM")), yesNo(config.LocaleLooksUTF8())),
2722+
fmt.Sprintf("terminal env: %s", terminalEnvSummary()),
27222723
fmt.Sprintf("image viewer command: %s", emptyAsAuto(env.Config.ImageViewerCommand)),
27232724
fmt.Sprintf("video player command: %s", emptyAsAuto(env.Config.VideoPlayerCommand)),
27242725
fmt.Sprintf("audio player command: %s", emptyAsAuto(env.Config.AudioPlayerCommand)),
@@ -2755,6 +2756,15 @@ func emptyAsAuto(value string) string {
27552756
return value
27562757
}
27572758

2759+
func terminalEnvSummary() string {
2760+
keys := []string{"TERM", "TERM_PROGRAM", "COLORTERM", "WT_SESSION", "ConEmuANSI", "ANSICON", "VIMWHAT_FORCE_SIXEL", "VIMWHAT_FORCE_REPORT_FOCUS", "VIMWHAT_DISABLE_REPORT_FOCUS"}
2761+
parts := make([]string, 0, len(keys))
2762+
for _, key := range keys {
2763+
parts = append(parts, fmt.Sprintf("%s=%s", key, emptyAsAuto(os.Getenv(key))))
2764+
}
2765+
return strings.Join(parts, " ")
2766+
}
2767+
27582768
func yesNo(value bool) string {
27592769
if value {
27602770
return "yes"

‎internal/config/config.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func ResolveEmojiModeForEnv(mode, term, lcAll, lcCtype, lang string) string {
237237
case EmojiModeCompat:
238238
return EmojiModeCompat
239239
case "", EmojiModeAuto:
240-
if terminalPrefersCompatEmoji(term) || !LocaleLooksUTF8ForEnv(lcAll, lcCtype, lang) {
240+
if terminalPrefersCompatEmoji(term) || platformPrefersCompatEmoji(term) || !LocaleLooksUTF8ForEnv(lcAll, lcCtype, lang) {
241241
return EmojiModeCompat
242242
}
243243
return EmojiModeFull

‎internal/config/emoji_unix.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build !windows
2+
3+
package config
4+
5+
func platformPrefersCompatEmoji(string) bool {
6+
return false
7+
}

‎internal/config/emoji_windows.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build windows
2+
3+
package config
4+
5+
import (
6+
"os"
7+
"strings"
8+
)
9+
10+
func platformPrefersCompatEmoji(term string) bool {
11+
term = strings.ToLower(strings.TrimSpace(term))
12+
if strings.Contains(term, "kitty") {
13+
return false
14+
}
15+
program := strings.ToLower(strings.TrimSpace(os.Getenv("TERM_PROGRAM")))
16+
if strings.Contains(program, "wezterm") {
17+
return false
18+
}
19+
return strings.TrimSpace(os.Getenv("WT_SESSION")) == ""
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build windows
2+
3+
package config
4+
5+
import "testing"
6+
7+
func TestWindowsEmojiAutoUsesCompatInClassicConsole(t *testing.T) {
8+
t.Setenv("TERM_PROGRAM", "")
9+
t.Setenv("WT_SESSION", "")
10+
11+
if got := ResolveEmojiModeForEnv(EmojiModeAuto, "xterm-256color", "", "", "en_US.UTF-8"); got != EmojiModeCompat {
12+
t.Fatalf("ResolveEmojiModeForEnv() = %q, want %q", got, EmojiModeCompat)
13+
}
14+
}
15+
16+
func TestWindowsEmojiAutoAllowsFullInWindowsTerminal(t *testing.T) {
17+
t.Setenv("TERM_PROGRAM", "")
18+
t.Setenv("WT_SESSION", "session")
19+
20+
if got := ResolveEmojiModeForEnv(EmojiModeAuto, "xterm-256color", "", "", "en_US.UTF-8"); got != EmojiModeFull {
21+
t.Fatalf("ResolveEmojiModeForEnv() = %q, want %q", got, EmojiModeFull)
22+
}
23+
}

‎internal/media/backend.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func Detect(requested string) Report {
4040
}
4141

4242
available := detectAvailable()
43-
order := []Backend{BackendSixel, BackendUeberzugPP, BackendChafa, BackendExternal}
43+
order := platformBackendOrder()
4444

4545
for _, backend := range order {
4646
if available[backend] {

‎internal/media/backend_unix.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ func platformExternalOpenerAvailable() bool {
2525
func platformExternalOpenerUnavailableReason() string {
2626
return "xdg-open not found in PATH"
2727
}
28+
29+
func platformBackendOrder() []Backend {
30+
return []Backend{BackendSixel, BackendUeberzugPP, BackendChafa, BackendExternal}
31+
}

‎internal/media/backend_windows.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ func platformExternalOpenerAvailable() bool {
1717
func platformExternalOpenerUnavailableReason() string {
1818
return "rundll32.exe/explorer.exe not found in PATH"
1919
}
20+
21+
func platformBackendOrder() []Backend {
22+
return []Backend{BackendSixel, BackendExternal, BackendChafa, BackendUeberzugPP}
23+
}

0 commit comments

Comments
 (0)