Skip to main content

fresh_core/
file_explorer.rs

1use crate::api::OverlayColorSpec;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use ts_rs::TS;
5
6/// Decoration metadata for a file explorer entry.
7#[derive(Debug, Clone, Serialize, Deserialize, TS)]
8#[serde(deny_unknown_fields)]
9#[ts(export)]
10pub struct FileExplorerDecoration {
11    /// File path to decorate
12    #[ts(type = "string")]
13    pub path: PathBuf,
14    /// Symbol to display (e.g., "●", "M", "A")
15    pub symbol: String,
16    /// Color as RGB array or theme key string (e.g., "ui.file_status_added_fg")
17    pub color: OverlayColorSpec,
18    /// Priority for display when multiple decorations exist (higher wins)
19    #[serde(default)]
20    pub priority: i32,
21}
22
23fn default_leading_slot_min_width() -> usize {
24    2
25}
26
27/// Tooltip content shown when hovering a trailing file-explorer slot.
28#[derive(Debug, Clone, Serialize, Deserialize, TS)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30#[ts(export)]
31pub struct FileExplorerTooltip {
32    /// Tooltip title shown in the popup border.
33    pub title: String,
34    /// Body lines shown inside the popup.
35    pub lines: Vec<String>,
36}
37
38/// Leading-slot content for a file explorer row.
39#[derive(Debug, Clone, Serialize, Deserialize, TS)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41#[ts(export)]
42pub struct FileExplorerLeadingSlot {
43    /// Text shown in the leading slot (for example, an icon glyph).
44    pub text: String,
45    /// Foreground colour for the leading slot.
46    pub color: OverlayColorSpec,
47    /// Minimum display width reserved for the leading slot.
48    #[serde(default = "default_leading_slot_min_width")]
49    pub min_width: usize,
50}
51
52/// Trailing-slot content for a file explorer row.
53#[derive(Debug, Clone, Serialize, Deserialize, TS)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55#[ts(export)]
56pub struct FileExplorerTrailingSlot {
57    /// Text shown in the trailing slot (for example, a badge glyph).
58    pub text: String,
59    /// Foreground colour for the trailing slot.
60    pub color: OverlayColorSpec,
61    /// Optional tooltip shown when hovering the trailing slot.
62    #[serde(default)]
63    pub tooltip: Option<FileExplorerTooltip>,
64}
65
66/// Additive slot override for a file explorer entry.
67///
68/// Any field left as `None` falls back to the editor's compatibility providers,
69/// so plugins can override just the piece they care about.
70#[derive(Debug, Clone, Serialize, Deserialize, TS)]
71#[serde(rename_all = "camelCase", deny_unknown_fields)]
72#[ts(export)]
73pub struct FileExplorerSlotEntry {
74    /// File or directory path to override.
75    #[ts(type = "string")]
76    pub path: PathBuf,
77    /// Optional leading-slot override.
78    #[serde(default)]
79    pub leading: Option<FileExplorerLeadingSlot>,
80    /// Explicitly suppress the compatibility leading slot for this path.
81    #[serde(default)]
82    pub suppress_leading: bool,
83    /// Optional trailing-slot override.
84    #[serde(default)]
85    pub trailing: Option<FileExplorerTrailingSlot>,
86    /// Explicitly suppress the compatibility trailing slot for this path.
87    #[serde(default)]
88    pub suppress_trailing: bool,
89    /// Optional filename colour override.
90    #[serde(default)]
91    pub name_color: Option<OverlayColorSpec>,
92    /// Explicitly suppress compatibility filename colouring for this path.
93    #[serde(default)]
94    pub suppress_name_color: bool,
95    /// Priority for display when multiple overrides exist (higher wins).
96    #[serde(default)]
97    pub priority: i32,
98}
99
100#[cfg(feature = "plugins")]
101impl<'js> rquickjs::FromJs<'js> for FileExplorerDecoration {
102    fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result<Self> {
103        rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
104            from: "object",
105            to: "FileExplorerDecoration",
106            message: Some(e.to_string()),
107        })
108    }
109}
110
111#[cfg(feature = "plugins")]
112impl<'js> rquickjs::FromJs<'js> for FileExplorerSlotEntry {
113    fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result<Self> {
114        rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
115            from: "object",
116            to: "FileExplorerSlotEntry",
117            message: Some(e.to_string()),
118        })
119    }
120}
121
122#[cfg(all(test, feature = "plugins"))]
123mod tests {
124    use super::*;
125    use rquickjs::{Context, FromJs, Runtime, Value};
126
127    /// `FileExplorerDecoration::from_js` reads every decoration field, not
128    /// just returning a defaulted stub. Uses non-zero priority and a theme
129    /// key colour to tie down the full conversion.
130    #[test]
131    fn from_js_decodes_all_visible_fields() {
132        let rt = Runtime::new().unwrap();
133        let ctx = Context::full(&rt).unwrap();
134        ctx.with(|ctx| {
135            let v: Value = ctx
136                .eval::<Value, _>(
137                    b"({path: '/tmp/a.rs', symbol: 'M', \
138                       color: 'ui.file_status_added_fg', priority: 7})"
139                        .as_slice(),
140                )
141                .unwrap();
142            let got = FileExplorerDecoration::from_js(&ctx, v).unwrap();
143            assert_eq!(got.path, PathBuf::from("/tmp/a.rs"));
144            assert_eq!(got.symbol, "M");
145            assert_eq!(got.priority, 7);
146            assert_eq!(got.color.as_theme_key(), Some("ui.file_status_added_fg"));
147        });
148    }
149
150    #[test]
151    fn slot_entry_from_js_decodes_suppression_flags() {
152        let rt = Runtime::new().unwrap();
153        let ctx = Context::full(&rt).unwrap();
154        ctx.with(|ctx| {
155            let v: Value = ctx
156                .eval::<Value, _>(
157                    br#"({
158                        path: '/tmp/a.rs',
159                        suppressLeading: true,
160                        suppressTrailing: true,
161                        suppressNameColor: true,
162                        priority: 5
163                    })"#,
164                )
165                .unwrap();
166            let got = FileExplorerSlotEntry::from_js(&ctx, v).unwrap();
167            assert_eq!(got.path, PathBuf::from("/tmp/a.rs"));
168            assert!(got.suppress_leading);
169            assert!(got.suppress_trailing);
170            assert!(got.suppress_name_color);
171            assert_eq!(got.priority, 5);
172        });
173    }
174}