ls-plus 0.0.1

Enhanced ls command with modern features - supports both cargo and npm installation
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
// 导入依赖项
use crate::cli::{ColorOption, ConfigAction, FormatOption}; // CLI 相关的枚举类型
use crate::error::{LsPlusError, Result}; // 错误处理类型
use dirs; // 系统目录路径获取
use serde::{Deserialize, Serialize}; // 序列化和反序列化支持
use std::fs; // 文件系统操作
use std::path::PathBuf; // 路径处理

/// 应用程序的主配置结构体
///
/// 这个结构体包含了应用程序的所有配置选项,分为四个主要类别:
/// - display: 显示相关的配置
/// - behavior: 行为相关的配置  
/// - icons: 图标相关的配置
/// - colors: 颜色相关的配置
///
/// 使用 serde 的 Serialize 和 Deserialize 特质来支持从 TOML 文件读取和写入配置。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// 显示相关的配置选项
    pub display: DisplayConfig,
    /// 行为相关的配置选项
    pub behavior: BehaviorConfig,
    /// 图标相关的配置选项
    pub icons: IconConfig,
    /// 颜色相关的配置选项
    pub colors: ColorConfig,
}

/// 显示相关的配置选项
///
/// 包含所有与文件列表显示相关的配置,
/// 如格式、颜色、图标等。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayConfig {
    /// 默认的输出格式
    /// 可选值:default, long, tree, grid, json
    pub default_format: FormatOption,

    /// 颜色模式
    /// 控制何时使用颜色输出
    pub color_mode: ColorOption,

    /// 是否显示文件图标
    /// 为不同类型的文件显示 emoji 图标
    pub show_icons: bool,

    /// 是否使用人类可读的文件大小
    /// 例如:1K, 2M, 3G 而不是字节数
    pub human_readable_sizes: bool,

    /// 是否目录优先显示
    /// 在文件之前先显示所有目录
    pub directories_first: bool,

    /// 是否显示隐藏文件
    /// 默认是否显示以 . 开头的隐藏文件
    pub show_hidden: bool,
}

/// 行为相关的配置选项
///
/// 包含所有与应用程序行为相关的配置,
/// 如排序、深度限制、Git 集成等。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorConfig {
    /// 是否跟踪符号链接
    /// 当为 true 时,显示符号链接指向的实际文件信息
    pub follow_symlinks: bool,

    /// 是否启用 Git 集成
    /// 为文件显示 Git 仓库状态信息
    pub git_integration: bool,

    /// 递归遍历的最大深度
    /// None 表示无限制,Some(n) 表示最大 n 层
    pub max_depth: Option<usize>,

    /// 是否按修改时间排序
    /// 默认按文件名排序,设为 true 则按修改时间排序
    pub sort_by_time: bool,

    /// 是否反向排序
    /// 默认从小到大排序,设为 true 则从大到小排序
    pub reverse_sort: bool,
}

/// 图标相关的配置选项
///
/// 控制文件图标的显示和主题。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IconConfig {
    /// 是否启用图标显示
    /// 为不同类型的文件显示相应的 emoji 图标
    pub enable_icons: bool,

    /// 图标主题
    /// 目前支持 "default" 主题,未来可能支持更多主题
    pub icon_theme: String,
}

/// 颜色相关的配置选项
///
/// 细化控制不同类型信息的颜色显示。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorConfig {
    /// 是否为文件名使用颜色
    /// 根据文件类型显示不同颜色(目录、文件、可执行文件等)
    pub file_colors: bool,

    /// 是否为文件大小使用颜色
    /// 根据文件大小范围显示不同颜色
    pub size_colors: bool,

    /// 是否为时间信息使用颜色
    /// 为修改时间显示颜色
    pub time_colors: bool,

    /// 是否为权限信息使用颜色
    /// 为文件权限显示颜色
    pub permission_colors: bool,
}

/// 为 Config 实现 Default trait
///
/// 提供合理的默认配置值,这些默认值:
/// - 保持与传统 ls 命令的兼容性
/// - 提供良好的用户体验
/// - 适合大多数使用场景
impl Default for Config {
    fn default() -> Self {
        Self {
            display: DisplayConfig {
                default_format: FormatOption::Default, // 使用标准格式
                color_mode: ColorOption::Auto,         // 自动检测是否使用颜色
                show_icons: false,                     // 默认不显示图标
                human_readable_sizes: true,            // 默认使用人类可读的文件大小
                directories_first: true,               // 默认目录优先显示
                show_hidden: false,                    // 默认不显示隐藏文件
            },
            behavior: BehaviorConfig {
                follow_symlinks: false, // 默认不跟踪符号链接
                git_integration: false, // 默认不启用 Git 集成
                max_depth: None,        // 默认无深度限制
                sort_by_time: false,    // 默认按文件名排序
                reverse_sort: false,    // 默认正向排序
            },
            icons: IconConfig {
                enable_icons: false,               // 默认不启用图标
                icon_theme: "default".to_string(), // 使用默认图标主题
            },
            colors: ColorConfig {
                file_colors: true,       // 默认为文件名使用颜色
                size_colors: true,       // 默认为文件大小使用颜色
                time_colors: true,       // 默认为时间信息使用颜色
                permission_colors: true, // 默认为权限信息使用颜色
            },
        }
    }
}

impl Config {
    /// 获取配置目录路径
    ///
    /// 返回应用程序的配置目录路径:
    /// - Linux/macOS: ~/.config/ls-plus/
    /// - Windows: %APPDATA%/ls-plus/
    ///
    /// # 返回值
    /// * `Result<PathBuf>` - 成功时返回配置目录路径,失败时返回错误
    pub fn config_dir() -> Result<PathBuf> {
        dirs::config_dir()
            .map(|dir| dir.join("ls-plus")) // 在系统配置目录下创建 ls-plus 子目录
            .ok_or_else(|| LsPlusError::system_error("Could not determine config directory"))
    }

    /// 获取配置文件路径
    ///
    /// 返回主配置文件 config.toml 的完整路径。
    ///
    /// # 返回值
    /// * `Result<PathBuf>` - 成功时返回配置文件路径
    pub fn config_file() -> Result<PathBuf> {
        Ok(Self::config_dir()?.join("config.toml"))
    }

    /// 从配置文件加载配置
    ///
    /// 尝试从 config.toml 文件加载配置。如果文件不存在,
    /// 则返回默认配置。
    ///
    /// # 返回值
    /// * `Result<Self>` - 成功时返回加载的配置,失败时返回错误
    ///
    /// # 错误
    /// * 配置文件读取失败
    /// * TOML 格式解析失败
    pub fn load() -> Result<Self> {
        let config_file = Self::config_file()?;

        // 如果配置文件不存在,返回默认配置
        if !config_file.exists() {
            return Ok(Self::default());
        }

        // 读取配置文件内容
        let content = fs::read_to_string(&config_file)
            .map_err(|e| LsPlusError::system_error(format!("Failed to read config file: {e}")))?;

        // 解析 TOML 格式的配置内容
        let config: Config = toml::from_str(&content)
            .map_err(|e| LsPlusError::invalid_config(format!("Invalid config format: {e}")))?;

        Ok(config)
    }

    /// 将配置保存到文件
    ///
    /// 将当前配置序列化为 TOML 格式并写入配置文件。
    /// 如果配置目录不存在,会自动创建。
    ///
    /// # 返回值
    /// * `Result<()>` - 成功时返回空结果,失败时返回错误
    ///
    /// # 错误
    /// * 配置目录创建失败
    /// * 配置序列化失败
    /// * 配置文件写入失败
    pub fn save(&self) -> Result<()> {
        let config_dir = Self::config_dir()?;
        let config_file = Self::config_file()?;

        // 如果配置目录不存在,创建它
        if !config_dir.exists() {
            fs::create_dir_all(&config_dir).map_err(|e| {
                LsPlusError::system_error(format!("Failed to create config directory: {e}"))
            })?;
        }

        // 将配置序列化为格式化的 TOML 字符串
        let content = toml::to_string_pretty(self)
            .map_err(|e| LsPlusError::system_error(format!("Failed to serialize config: {e}")))?;

        // 将内容写入配置文件
        fs::write(&config_file, content)
            .map_err(|e| LsPlusError::system_error(format!("Failed to write config file: {e}")))?;

        Ok(())
    }

    /// 设置配置项的值
    ///
    /// 根据键名设置对应的配置值。支持的键名包括:
    ///
    /// ## 显示配置 (display.*)
    /// - `display.default_format`: 默认显示格式 (default|long|tree|grid|json)
    /// - `display.color_mode`: 颜色模式 (always|never|auto)
    /// - `display.show_icons`: 是否显示图标 (true|false)
    /// - `display.human_readable_sizes`: 是否使用人类可读的文件大小 (true|false)
    /// - `display.directories_first`: 是否目录优先显示 (true|false)
    /// - `display.show_hidden`: 是否显示隐藏文件 (true|false)
    ///
    /// ## 行为配置 (behavior.*)
    /// - `behavior.follow_symlinks`: 是否跟踪符号链接 (true|false)
    /// - `behavior.git_integration`: 是否启用Git集成 (true|false)
    /// - `behavior.sort_by_time`: 是否按时间排序 (true|false)
    /// - `behavior.reverse_sort`: 是否反向排序 (true|false)
    ///
    /// # 参数
    /// * `key` - 配置键名,使用点号分隔的层次结构
    /// * `value` - 配置值的字符串表示
    ///
    /// # 返回值
    /// * `Result<()>` - 成功时返回空结果,失败时返回错误
    ///
    /// # 错误
    /// * 未知的配置键名
    /// * 无效的配置值格式
    /// * 布尔值解析失败
    pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
        match key {
            // 显示格式配置
            "display.default_format" => {
                self.display.default_format = match value {
                    "default" => FormatOption::Default, // 标准格式
                    "long" => FormatOption::Long,       // 长格式,显示详细信息
                    "tree" => FormatOption::Tree,       // 树形格式
                    "grid" => FormatOption::Grid,       // 网格格式
                    "json" => FormatOption::Json,       // JSON 格式
                    _ => return Err(LsPlusError::invalid_config("Invalid format option")),
                };
            }
            // 颜色模式配置
            "display.color_mode" => {
                self.display.color_mode = match value {
                    "always" => ColorOption::Always, // 总是使用颜色
                    "never" => ColorOption::Never,   // 从不使用颜色
                    "auto" => ColorOption::Auto,     // 自动检测
                    _ => return Err(LsPlusError::invalid_config("Invalid color option")),
                };
            }
            // 显示图标配置
            "display.show_icons" => {
                self.display.show_icons = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 人类可读文件大小配置
            "display.human_readable_sizes" => {
                self.display.human_readable_sizes = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 目录优先显示配置
            "display.directories_first" => {
                self.display.directories_first = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 显示隐藏文件配置
            "display.show_hidden" => {
                self.display.show_hidden = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 跟踪符号链接配置
            "behavior.follow_symlinks" => {
                self.behavior.follow_symlinks = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // Git 集成配置
            "behavior.git_integration" => {
                self.behavior.git_integration = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 按时间排序配置
            "behavior.sort_by_time" => {
                self.behavior.sort_by_time = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 反向排序配置
            "behavior.reverse_sort" => {
                self.behavior.reverse_sort = value
                    .parse()
                    .map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
            }
            // 未知的配置键名
            _ => {
                return Err(LsPlusError::invalid_config(format!(
                    "Unknown config key: {key}"
                )))
            }
        }
        Ok(())
    }
}

/// 处理配置相关的命令
///
/// 这个函数是配置管理的主入口点,根据传入的动作执行相应的配置操作。
///
/// # 参数
/// * `action` - 要执行的配置动作(显示、设置、重置、编辑)
///
/// # 返回值
/// * `Result<()>` - 成功时返回空结果,失败时返回错误
///
/// # 支持的操作
/// - `Show`: 显示当前配置内容
/// - `Set`: 设置指定配置项的值
/// - `Reset`: 重置所有配置为默认值
/// - `Edit`: 使用系统编辑器打开配置文件
pub async fn handle_config_command(action: ConfigAction) -> Result<()> {
    match action {
        // 显示当前配置
        ConfigAction::Show => {
            let config = Config::load()?;
            // 将配置序列化为格式化的 TOML 字符串
            let config_str = toml::to_string_pretty(&config).map_err(|e| {
                LsPlusError::system_error(format!("Failed to serialize config: {e}"))
            })?;
            println!("{config_str}");
        }
        // 设置配置值
        ConfigAction::Set { key, value } => {
            let mut config = Config::load()?;
            // 设置指定键的值
            config.set_value(&key, &value)?;
            // 保存配置到文件
            config.save()?;
            println!("Configuration updated: {key} = {value}");
        }
        // 重置配置为默认值
        ConfigAction::Reset => {
            let config = Config::default();
            // 保存默认配置到文件,覆盖现有配置
            config.save()?;
            println!("Configuration reset to defaults");
        }
        // 编辑配置文件
        ConfigAction::Edit => {
            let config_file = Config::config_file()?;

            // 如果配置文件不存在,先创建一个包含默认值的配置文件
            if !config_file.exists() {
                let default_config = Config::default();
                default_config.save()?;
            }

            // 尝试使用系统默认编辑器打开配置文件
            // 首先尝试从环境变量 EDITOR 获取编辑器,如果没有则使用 nano
            let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
            let status = std::process::Command::new(&editor)
                .arg(&config_file)
                .status()
                .map_err(|e| LsPlusError::system_error(format!("Failed to open editor: {e}")))?;

            // 检查编辑器是否成功退出
            if !status.success() {
                return Err(LsPlusError::system_error("Editor exited with error"));
            }
        }
    }
    Ok(())
}