Files
AX-Copilot-Codex/src/AxCopilot/Handlers/RecentFilesHandler.cs

142 lines
4.8 KiB
C#

using System.IO;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 최근 파일 핸들러. "recent" 프리픽스로 사용합니다.
/// Windows Recent 폴더(%APPDATA%\Microsoft\Windows\Recent)의 .lnk 파일을
/// 최근 수정 순으로 나열합니다.
/// 예: recent → 최근 20개 파일 목록
/// recent 보고서 → 이름에 "보고서" 포함 파일 필터
/// </summary>
public class RecentFilesHandler : IActionHandler
{
public string? Prefix => "recent";
public PluginMetadata Metadata => new(
"RecentFiles",
"최근 파일 — recent 뒤에 검색어 입력",
"1.0",
"AX");
private static readonly string RecentFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
@"Microsoft\Windows\Recent");
// 간단한 캐시: 10초간 유효
private static (DateTime At, List<(string Name, string LinkPath, DateTime Modified)> Files)? _cache;
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(10);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
// 힌트만 표시
}
var files = GetRecentFiles();
IEnumerable<(string Name, string LinkPath, DateTime Modified)> filtered = files;
if (!string.IsNullOrWhiteSpace(q))
{
filtered = files.Where(f =>
f.Name.Contains(q, StringComparison.OrdinalIgnoreCase));
}
var items = filtered.Take(20).Select(f => new LauncherItem(
f.Name,
$"{f.Modified:yyyy-MM-dd HH:mm} · Enter로 열기",
null,
f.LinkPath,
Symbol: GetSymbol(f.Name))).ToList();
if (!items.Any())
{
items.Add(new LauncherItem(
string.IsNullOrWhiteSpace(q) ? "최근 파일 없음" : "검색 결과 없음",
string.IsNullOrWhiteSpace(q)
? "Windows Recent 폴더가 비어 있습니다"
: $"'{q}' 파일을 최근 목록에서 찾을 수 없습니다",
null, null, Symbol: Symbols.Info));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string linkPath && File.Exists(linkPath))
{
try
{
System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(linkPath)
{ UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"최근 파일 열기 실패: {ex.Message}");
}
}
return Task.CompletedTask;
}
// ─── 내부 ─────────────────────────────────────────────────────────────────
private static List<(string Name, string LinkPath, DateTime Modified)> GetRecentFiles()
{
// 캐시 유효 확인
if (_cache.HasValue && (DateTime.Now - _cache.Value.At) < CacheTtl)
return _cache.Value.Files;
var result = new List<(string, string, DateTime)>();
try
{
if (!Directory.Exists(RecentFolder))
return result;
var lnkFiles = Directory
.GetFiles(RecentFolder, "*.lnk")
.Select(p => (Path: p, Info: new FileInfo(p)))
.OrderByDescending(f => f.Info.LastWriteTime)
.Take(100)
.ToList();
foreach (var (path, info) in lnkFiles)
{
var name = Path.GetFileNameWithoutExtension(info.Name);
result.Add((name, path, info.LastWriteTime));
}
}
catch (Exception ex)
{
LogService.Warn($"최근 파일 목록 읽기 실패: {ex.Message}");
}
_cache = (DateTime.Now, result);
return result;
}
private static string GetSymbol(string name)
{
var ext = Path.GetExtension(name).ToLowerInvariant();
return ext switch
{
".exe" or ".msi" => Symbols.App,
".xlsx" or ".xls" or ".csv" => Symbols.File,
".docx" or ".doc" => Symbols.File,
".pptx" or ".ppt" => Symbols.File,
".pdf" => Symbols.File,
".txt" or ".md" or ".log" => Symbols.Text,
".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".bmp" => Symbols.Picture,
_ => Symbols.File
};
}
}