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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
// 导入依赖项
use crate::cli::{Cli, FileTypeFilter}; // CLI 类型和文件类型过滤器
use crate::error::{LsPlusError, Result}; // 错误处理类型
use chrono::{DateTime, Local}; // 时间处理
use std::fs; // 标准库文件系统操作
use std::path::{Path, PathBuf}; // 路径处理
use walkdir::WalkDir; // 递归目录遍历

/// 文件条目信息结构体
///
/// 表示文件系统中一个文件或目录的完整信息,包含了显示文件列表
/// 所需的所有元数据。这个结构体是整个应用程序中传递文件信息的
/// 核心数据结构。
#[derive(Debug, Clone)]
pub struct FileEntry {
    /// 文件或目录的完整路径
    pub path: PathBuf,
    /// 文件名(不包含路径)
    pub name: String,
    /// 文件大小(字节数)
    pub size: u64,
    /// 最后修改时间
    pub modified: DateTime<Local>,
    /// 文件权限的字符串表示(如 "rwxr-xr-x")
    pub permissions: String,
    /// 文件类型(文件、目录、符号链接等)
    pub file_type: EntryType,
    /// 是否为隐藏文件(文件名以 . 开头)
    pub is_hidden: bool,
    /// 是否为符号链接
    pub is_symlink: bool,
    /// 如果是符号链接,指向的目标路径
    pub symlink_target: Option<PathBuf>,
}

/// 文件类型枚举
///
/// 定义了文件系统中不同类型的条目。
/// 这个枚举用于区分不同类型的文件系统对象,
/// 以便在显示时采用不同的颜色和图标。
#[derive(Debug, Clone, PartialEq)]
pub enum EntryType {
    /// 普通文件
    File,
    /// 目录文件夹
    Directory,
    /// 符号链接(软链接)
    Symlink,
    /// 其他类型(如设备文件、管道等)
    Other,
}

impl FileEntry {
    /// 从文件路径创建 FileEntry 实例
    ///
    /// 读取指定路径的文件或目录信息,并创建对应的 FileEntry。
    /// 这是创建文件条目的主要方法。
    ///
    /// # 参数
    /// * `path` - 文件或目录的路径
    /// * `follow_links` - 是否跟踪符号链接(true 表示读取链接指向的文件信息)
    ///
    /// # 返回值
    /// * `Result<Self>` - 成功时返回 FileEntry 实例,失败时返回错误
    ///
    /// # 错误
    /// * 文件不存在或无法访问
    /// * 无法读取文件元数据
    /// * 无法读取修改时间
    pub fn from_path(path: &Path, follow_links: bool) -> Result<Self> {
        // 根据 follow_links 参数决定如何读取元数据
        // - 如果跟踪链接,使用 metadata() 读取链接指向的文件信息
        // - 否则使用 symlink_metadata() 读取链接本身的信息
        let metadata = if follow_links {
            fs::metadata(path)
        } else {
            fs::symlink_metadata(path)
        }
        .map_err(|e| {
            LsPlusError::system_error(format!("Failed to read metadata for {path:?}: {e}"))
        })?;

        // 提取文件名(不包含路径部分)
        let name = path
            .file_name()
            .and_then(|n| n.to_str()) // 将 OsStr 转换为 &str
            .unwrap_or("") // 如果转换失败,使用空字符串
            .to_string();

        // 判断是否为隐藏文件(文件名以 . 开头)
        let is_hidden = name.starts_with('.');
        // 判断是否为符号链接
        let is_symlink = metadata.file_type().is_symlink();

        // 根据元数据确定文件类型
        let file_type = if metadata.is_dir() {
            EntryType::Directory // 目录
        } else if is_symlink {
            EntryType::Symlink // 符号链接
        } else if metadata.is_file() {
            EntryType::File // 普通文件
        } else {
            EntryType::Other // 其他类型(设备文件、管道等)
        };

        // 如果是符号链接,尝试读取链接目标
        let symlink_target = if is_symlink {
            fs::read_link(path).ok() // 读取失败时返回 None
        } else {
            None
        };

        // 获取文件的最后修改时间
        let modified = metadata
            .modified()
            .map_err(|e| {
                LsPlusError::system_error(format!("Failed to read modification time: {e}"))
            })?
            .into(); // 转换为 DateTime<Local>

        // 格式化文件权限字符串
        let permissions = format_permissions(&metadata);

        Ok(FileEntry {
            path: path.to_path_buf(),
            name,
            size: metadata.len(),
            modified,
            permissions,
            file_type,
            is_hidden,
            is_symlink,
            symlink_target,
        })
    }

    /// 检查文件是否匹配指定的文件类型过滤器
    ///
    /// 根据传入的过滤器判断当前文件条目是否符合过滤条件。
    /// 这个方法用于实现按文件类型过滤的功能。
    ///
    /// # 参数
    /// * `filter` - 文件类型过滤器
    ///
    /// # 返回值
    /// * `bool` - 如果文件匹配过滤器返回 true,否则返回 false
    pub fn matches_filter(&self, filter: &FileTypeFilter) -> bool {
        match filter {
            // 基本文件类型过滤
            FileTypeFilter::File => self.file_type == EntryType::File, // 普通文件
            FileTypeFilter::Dir => self.file_type == EntryType::Directory, // 目录
            FileTypeFilter::Link => self.file_type == EntryType::Symlink, // 符号链接

            // 特殊文件类型过滤(需要同时是文件并满足特定条件)
            FileTypeFilter::Exec => {
                self.file_type == EntryType::File && self.is_executable() // 可执行文件
            }
            FileTypeFilter::Image => {
                self.file_type == EntryType::File && self.is_image() // 图像文件
            }
            FileTypeFilter::Audio => {
                self.file_type == EntryType::File && self.is_audio() // 音频文件
            }
            FileTypeFilter::Video => {
                self.file_type == EntryType::File && self.is_video() // 视频文件
            }
            FileTypeFilter::Archive => {
                self.file_type == EntryType::File && self.is_archive() // 压缩档案文件
            }
            FileTypeFilter::Document => {
                self.file_type == EntryType::File && self.is_document() // 文档文件
            }
        }
    }

    /// 检查文件是否为可执行文件
    ///
    /// 这是一个简化的实现,主要基于文件扩展名判断。
    /// 在真实的实现中,应该检查文件的执行权限。
    ///
    /// # 返回值
    /// * `bool` - 如果文件可能是可执行的返回 true
    fn is_executable(&self) -> bool {
        // 简化的检查 - 在真实实现中应该检查文件权限
        self.name.ends_with(".exe") ||   // Windows 可执行文件
        self.name.ends_with(".sh") ||    // Shell 脚本
        self.name.ends_with(".bat") ||   // Windows 批处理文件
        !self.name.contains('.') // 无扩展名的文件(可能是 Unix 可执行文件)
    }

    /// 检查文件是否为图像文件
    ///
    /// 基于文件扩展名判断是否为常见的图像格式。
    ///
    /// # 返回值
    /// * `bool` - 如果是图像文件返回 true
    fn is_image(&self) -> bool {
        let extensions = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "ico"];
        self.has_extension(&extensions)
    }

    /// 检查文件是否为音频文件
    ///
    /// 基于文件扩展名判断是否为常见的音频格式。
    ///
    /// # 返回值
    /// * `bool` - 如果是音频文件返回 true
    fn is_audio(&self) -> bool {
        let extensions = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
        self.has_extension(&extensions)
    }

    /// 检查文件是否为视频文件
    ///
    /// 基于文件扩展名判断是否为常见的视频格式。
    ///
    /// # 返回值
    /// * `bool` - 如果是视频文件返回 true
    fn is_video(&self) -> bool {
        let extensions = ["mp4", "avi", "mov", "mkv", "wmv", "flv", "webm", "m4v"];
        self.has_extension(&extensions)
    }

    /// 检查文件是否为压缩档案文件
    ///
    /// 基于文件扩展名判断是否为常见的压缩档案格式。
    ///
    /// # 返回值
    /// * `bool` - 如果是压缩档案文件返回 true
    fn is_archive(&self) -> bool {
        let extensions = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar", "deb", "rpm"];
        self.has_extension(&extensions)
    }

    /// 检查文件是否为文档文件
    ///
    /// 基于文件扩展名判断是否为常见的文档格式。
    ///
    /// # 返回值
    /// * `bool` - 如果是文档文件返回 true
    fn is_document(&self) -> bool {
        let extensions = ["pdf", "doc", "docx", "txt", "md", "rst", "odt", "rtf"];
        self.has_extension(&extensions)
    }

    /// 检查文件是否具有指定的扩展名
    ///
    /// 辅助方法,用于检查文件扩展名是否在指定的列表中。
    /// 检查时不区分大小写。
    ///
    /// # 参数
    /// * `extensions` - 扩展名列表(不包含点号)
    ///
    /// # 返回值
    /// * `bool` - 如果文件扩展名在列表中返回 true
    fn has_extension(&self, extensions: &[&str]) -> bool {
        if let Some(ext) = self.path.extension().and_then(|e| e.to_str()) {
            // 将扩展名转为小写后在列表中查找
            extensions.contains(&ext.to_lowercase().as_str())
        } else {
            false // 文件没有扩展名
        }
    }
}

/// 列出指定目录中的文件和子目录
///
/// 这是获取目录内容的主要函数,支持各种过滤和排序选项。
/// 如果指定的路径是文件而非目录,则返回包含该文件信息的单元素向量。
///
/// # 参数
/// * `path` - 要列出的目录路径
/// * `cli` - CLI 配置,包含排序、过滤、隐藏文件等选项
///
/// # 返回值
/// * `Result<Vec<FileEntry>>` - 成功时返回文件条目列表,失败时返回错误
///
/// # 错误
/// * 路径不存在
/// * 权限不足,无法读取目录
/// * 文件系统错误
///
/// # 功能特性
/// - 根据 CLI 配置应用文件类型过滤
/// - 可选择显示或隐藏以 . 开头的隐藏文件
/// - 支持多种排序方式(文件名、修改时间)
/// - 支持正向或反向排序
/// - 可选择目录优先显示
pub async fn list_directory(path: &Path, cli: &Cli) -> Result<Vec<FileEntry>> {
    // 检查路径是否存在
    if !path.exists() {
        return Err(LsPlusError::path_not_found(path.to_path_buf()));
    }

    // 如果指定的路径是文件而非目录,直接返回该文件的信息
    if !path.is_dir() {
        let entry = FileEntry::from_path(path, cli.follow_links)?;
        return Ok(vec![entry]);
    }

    let mut entries = Vec::new();

    // 读取目录内容,处理可能的权限错误
    let dir_entries = fs::read_dir(path).map_err(|e| match e.kind() {
        std::io::ErrorKind::PermissionDenied => LsPlusError::permission_denied(path.to_path_buf()),
        _ => LsPlusError::system_error(format!("Failed to read directory {path:?}: {e}")),
    })?;

    // 遍历目录中的每个条目
    for entry in dir_entries {
        let entry = entry.map_err(|e| {
            LsPlusError::system_error(format!("Failed to read directory entry: {e}"))
        })?;
        let path = entry.path();

        // 尝试创建 FileEntry,如果失败则跳过该条目
        if let Ok(file_entry) = FileEntry::from_path(&path, cli.follow_links) {
            // 根据 CLI 配置决定是否跳过隐藏文件
            if !cli.all && file_entry.is_hidden {
                continue;
            }

            // 应用文件类型过滤器(如果指定了的话)
            if let Some(ref filter) = cli.file_type {
                if !file_entry.matches_filter(filter) {
                    continue;
                }
            }

            entries.push(file_entry);
        }
    }

    // 根据 CLI 配置对条目进行排序
    sort_entries(&mut entries, cli);

    Ok(entries)
}

/// 递归列出目录内容
///
/// 递归遍历指定目录及其所有子目录,返回所有找到的文件和目录。
/// 主要用于树形视图功能。
///
/// # 参数
/// * `path` - 要递归遍历的目录路径
/// * `cli` - CLI 配置
/// * `max_depth` - 最大递归深度,None 表示无限制
///
/// # 返回值
/// * `Result<Vec<FileEntry>>` - 成功时返回所有文件条目列表
///
/// # 错误
/// * 路径不存在
/// * 遍历过程中发生文件系统错误
pub async fn list_directory_recursive(
    path: &Path,
    cli: &Cli,
    max_depth: Option<usize>,
) -> Result<Vec<FileEntry>> {
    // 检查路径是否存在
    if !path.exists() {
        return Err(LsPlusError::path_not_found(path.to_path_buf()));
    }

    let mut entries = Vec::new();

    // 根据是否指定了深度限制来配置 WalkDir
    let walker = if let Some(depth) = max_depth {
        WalkDir::new(path).max_depth(depth) // 有深度限制
    } else {
        WalkDir::new(path) // 无限制递归
    };

    // 遍历所有找到的条目
    for entry in walker {
        let entry = entry.map_err(|e| LsPlusError::system_error(format!("Walk error: {e}")))?;
        let path = entry.path();

        // 尝试创建 FileEntry
        if let Ok(file_entry) = FileEntry::from_path(path, cli.follow_links) {
            // 根据 CLI 配置决定是否跳过隐藏文件
            if !cli.all && file_entry.is_hidden {
                continue;
            }

            // 应用文件类型过滤器
            if let Some(ref filter) = cli.file_type {
                if !file_entry.matches_filter(filter) {
                    continue;
                }
            }

            entries.push(file_entry);
        }
    }

    // 对条目进行排序
    sort_entries(&mut entries, cli);

    Ok(entries)
}

/// 搜索文件功能
///
/// 目前这是一个简化的实现,直接返回目录列表。
/// 在完整的实现中,这里应该实现模式匹配和文件名搜索功能。
///
/// # 参数
/// * `cli` - CLI 配置,包含搜索路径和过滤条件
///
/// # 返回值
/// * `Result<Vec<FileEntry>>` - 搜索结果列表
///
/// # TODO
/// - 实现模式匹配功能
/// - 支持正则表达式
/// - 支持大小写敏感性控制
pub async fn find_files(cli: &Cli) -> Result<Vec<FileEntry>> {
    // 目前直接返回目录列表
    // 在完整实现中,这里应该实现模式匹配功能
    list_directory(&cli.path, cli).await
}

/// 对文件条目进行排序
///
/// 根据 CLI 配置对文件列表进行排序。支持目录优先、
/// 按时间排序和反向排序。
///
/// # 参数
/// * `entries` - 要排序的文件条目列表(可变引用)
/// * `cli` - CLI 配置,包含排序选项
fn sort_entries(entries: &mut [FileEntry], cli: &Cli) {
    if cli.directories_first {
        // 目录优先排序:先按文件类型分组,然后在组内排序
        entries.sort_by(|a, b| {
            match (&a.file_type, &b.file_type) {
                // 两个都是目录,按正常规则排序
                (EntryType::Directory, EntryType::Directory) => compare_entries(a, b, cli),
                // 目录排在非目录之前
                (EntryType::Directory, _) => std::cmp::Ordering::Less,
                // 非目录排在目录之后
                (_, EntryType::Directory) => std::cmp::Ordering::Greater,
                // 两个都不是目录,按正常规则排序
                _ => compare_entries(a, b, cli),
            }
        });
    } else {
        // 正常排序,不区分文件类型
        entries.sort_by(|a, b| compare_entries(a, b, cli));
    }

    // 如果设置了反向排序,将整个列表反转
    if cli.reverse {
        entries.reverse();
    }
}

/// 比较两个文件条目的排序顺序
///
/// 根据 CLI 配置决定排序规则:按文件名或修改时间排序。
///
/// # 参数
/// * `a` - 第一个文件条目
/// * `b` - 第二个文件条目
/// * `cli` - CLI 配置,包含排序选项
///
/// # 返回值
/// * `std::cmp::Ordering` - 比较结果(Less、Equal、Greater)
fn compare_entries(a: &FileEntry, b: &FileEntry, cli: &Cli) -> std::cmp::Ordering {
    if cli.time {
        // 按修改时间排序
        a.modified.cmp(&b.modified)
    } else {
        // 按文件名排序(不区分大小写)
        a.name.to_lowercase().cmp(&b.name.to_lowercase())
    }
}

/// 格式化文件权限为字符串
///
/// 这是一个简化的实现,只根据文件类型返回固定的权限字符串。
/// 在完整的实现中,应该正确解析并格式化 Unix 文件权限。
///
/// # 参数
/// * `metadata` - 文件元数据
///
/// # 返回值
/// * `String` - 权限的字符串表示(如 "rwxr-xr-x")
///
/// # TODO
/// - 实现真实的 Unix 权限解析
/// - 支持 Windows 文件属性
/// - 显示特殊权限位(setuid、setgid、sticky)
fn format_permissions(metadata: &fs::Metadata) -> String {
    // 简化的权限格式化
    // 在真实实现中应该正确格式化 Unix 权限
    if metadata.is_dir() {
        "drwxr-xr-x".to_string() // 目录的默认权限
    } else if metadata.is_file() {
        "rw-r--r--".to_string() // 文件的默认权限
    } else {
        "lrwxrwxrwx".to_string() // 符号链接的默认权限
    }
}