1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// プレビュー方式の解決とレンダラ選択。
// 設定ルール → PreviewKind に落とし込み、各レンダラ(内蔵/外部委譲)へ振り分ける。
// M0 では種別の解決のみを実装。実描画は各サブモジュールで段階的に実装する
// (M2: image / M3: markdown / 以降: code・command)。
pub mod code;
pub mod command;
pub mod gitdiff;
pub mod image;
pub mod markdown;
pub mod pdf;
pub mod svg;
pub mod text;
pub mod video;
pub mod window;
use std::path::{Path, PathBuf};
use crate::config::Rule;
/// A resolved preview kind. Determined from the config rule (the first one that matched).
#[derive(Debug, Clone)]
pub enum PreviewKind {
/// Built-in Markdown renderer (decorated by tui-markdown; mermaid fences inside the md are composited via mermaid-text).
Markdown(PathBuf),
/// Built-in Mermaid renderer (standalone .mmd/.mermaid files; draws Unicode box lines via mermaid-text).
Mermaid(PathBuf),
/// Built-in image renderer (ratatui-image / kitty graphics). GIFs land here and are expanded into animation on the app side.
Image(PathBuf),
/// Built-in SVG renderer (rasterizes via resvg/usvg/tiny-skia, then flows into the image path).
Svg(PathBuf),
/// Built-in video thumbnail (extracts one representative frame via ffmpegthumbnailer/ffmpeg, then flows into the image path).
/// Does not play inside the terminal. Missing/failed external tools fall back safely (hint display).
Video(PathBuf),
/// Built-in PDF preview (rasterizes the first page via pdftocairo/pdftoppm/qlmanage/sips, then flows into the image path).
/// Missing tools fall back safely (hint display). First page only.
Pdf(PathBuf),
/// Built-in code highlighting (syntect).
Code(PathBuf),
/// Built-in plain-text display (extension not registered but judged to be text).
Text(PathBuf),
/// Git diff preview (opened with Enter in the Git view; unified display, Zed-style coloring).
/// Not produced from config rules; `open_git_diff` sets it directly.
GitDiff(PathBuf),
/// External command delegation. Expands {path}/{out} and runs a child process.
Command {
path: PathBuf,
/// Command string with the template not yet expanded (e.g. "mpv {path}").
template: String,
/// How to treat the output: if Some("image"), display the produced file full-screen as an image.
render_as: Option<String>,
/// If true, open in a separate process and do not block the TUI (videos, etc.).
detached: bool,
},
/// Matches no rule / unsupported. Displays `[can not preview: <ext>]` full-screen.
CanNotPreview { ext: String },
}
impl PreviewKind {
/// Determine the kind from the matched rule. Prefer builtin, falling back to command.
/// If neither is present, or the builtin name is unknown, fall back to the safe CanNotPreview.
pub fn from_rule(rule: &Rule, path: &Path) -> Self {
let p = path.to_path_buf();
if let Some(builtin) = rule.builtin.as_deref() {
return match builtin {
"markdown" => PreviewKind::Markdown(p),
"mermaid" => PreviewKind::Mermaid(p),
"image" => PreviewKind::Image(p),
"svg" => PreviewKind::Svg(p),
"video" => PreviewKind::Video(p),
"pdf" => PreviewKind::Pdf(p),
"code" => PreviewKind::Code(p),
"text" => PreviewKind::Text(p),
_ => PreviewKind::can_not_preview(path),
};
}
if let Some(template) = rule.command.as_deref() {
return PreviewKind::Command {
path: p,
template: template.to_string(),
render_as: rule.render_as.clone(),
detached: rule.detached,
};
}
PreviewKind::can_not_preview(path)
}
/// Unsupported fallback. Displays safely with the extension attached.
pub fn can_not_preview(path: &Path) -> Self {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_string();
PreviewKind::CanNotPreview { ext }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_not_preview_captures_extension_or_empty() {
// 拡張子つき: ext を保持する。
match PreviewKind::can_not_preview(Path::new("/x/foo.xyz")) {
PreviewKind::CanNotPreview { ext } => assert_eq!(ext, "xyz"),
other => panic!("CanNotPreview を期待: {other:?}"),
}
// 拡張子なし: 空文字(クラッシュしない)。
match PreviewKind::can_not_preview(Path::new("/x/Makefile")) {
PreviewKind::CanNotPreview { ext } => assert_eq!(ext, "", "拡張子なしは空"),
other => panic!("CanNotPreview を期待: {other:?}"),
}
}
}