FileBrowserHandler (185줄) — L4-1 인라인 파일 탐색기:
- Handlers/FileBrowserHandler.cs: Prefix=null, 경로 패턴 감지(C:\, D:\, \, ~\)
- 폴더/파일 나열: 상위폴더(..) + 하위폴더 40개 + 파일 30개
- 확장자별 MDL2 아이콘, 파일 크기 포맷(B/KB/MB/GB), 필터링 지원
- FileBrowserEntry(Path, IsFolder) record 정의
- App.xaml.cs: Phase L4 섹션에 FileBrowserHandler 등록
CommandResolver (18줄 추가) — 경로 쿼리 우선 처리:
- 퍼지 검색 전 IsPathQuery() 감지 → 파일탐색기 단독 결과 반환(항목 수 제한 없음)
- FileBrowserEntry 실행 라우팅 → ExecuteNullPrefixAsync 위임
LauncherWindow.Keyboard.cs (41줄 추가) — 키보드 탐색:
- Key.Right: FileBrowserEntry {IsFolder:true} 선택 시 해당 경로로 InputText 업데이트
- Key.Left: 경로 쿼리 상태에서 상위 폴더로 이동(Path.GetDirectoryName)
- 기존 → 키 액션모드 진입 로직 유지
QuickLookWindow (L4-2 F3 미리보기 강화):
- XAML: 줄번호 열(LineNumBg+LineNumbers), PDF 패널(빨강 배지), Office 패널(파랑 배지) 추가
- Code-behind: PDF(PdfPig), Word(OpenXml), Excel(OpenXml) 미리보기 구현
- ApplyCodeStyle(): 언어별 배경 색조(C#=파랑, Python=녹색, JS=앰버 등)
- 빌드: 경고 0, 오류 0
186 lines
7.3 KiB
C#
186 lines
7.3 KiB
C#
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L4-1: 인라인 파일 탐색기 핸들러.
|
|
/// 입력이 파일시스템 경로처럼 보이면 (예: C:\, D:\Users\) 해당 폴더의 내용을 런처 목록으로 표시합니다.
|
|
/// → 키로 하위 폴더 진입, ← 키 또는 Backspace로 상위 폴더 이동.
|
|
/// prefix=null: 일반 쿼리 파이프라인에서 경로 감지 후 동작.
|
|
/// </summary>
|
|
public class FileBrowserHandler : IActionHandler
|
|
{
|
|
public string? Prefix => null; // 경로 패턴 직접 감지
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"FileBrowser",
|
|
"파일 탐색기 — 경로 입력 후 → 키로 탐색",
|
|
"1.0",
|
|
"AX");
|
|
|
|
// C:\, D:\path, \\server\share, ~\ 패턴 감지
|
|
private static readonly Regex PathPattern = new(
|
|
@"^([A-Za-z]:\\|\\\\|~\\|~\/|\/)",
|
|
RegexOptions.Compiled);
|
|
|
|
/// <summary>쿼리가 파일시스템 경로처럼 보이는지 빠르게 판별합니다.</summary>
|
|
public static bool IsPathQuery(string query)
|
|
=> !string.IsNullOrEmpty(query) && PathPattern.IsMatch(query.Trim());
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = ExpandPath(query.Trim());
|
|
|
|
// 경로가 아닌 쿼리는 빈 결과 반환 (다른 핸들러가 처리)
|
|
if (!IsPathQuery(query.Trim()))
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
|
|
|
|
// 입력이 존재하는 디렉터리이면 그 내용 표시
|
|
if (Directory.Exists(q))
|
|
return Task.FromResult(ListDirectory(q));
|
|
|
|
// 부분 경로: 마지막 세그먼트를 필터로 사용
|
|
var parent = Path.GetDirectoryName(q);
|
|
var filter = Path.GetFileName(q).ToLowerInvariant();
|
|
|
|
if (parent != null && Directory.Exists(parent))
|
|
return Task.FromResult(ListDirectory(parent, filter));
|
|
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
|
{
|
|
new LauncherItem("경로를 찾을 수 없습니다", q, null, null, Symbol: Symbols.Error)
|
|
});
|
|
}
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is FileBrowserEntry { IsFolder: true } dir)
|
|
{
|
|
// 폴더: 탐색기로 열기
|
|
System.Diagnostics.Process.Start(
|
|
new System.Diagnostics.ProcessStartInfo("explorer.exe", dir.Path)
|
|
{ UseShellExecute = true });
|
|
}
|
|
else if (item.Data is FileBrowserEntry { IsFolder: false } file)
|
|
{
|
|
// 파일: 기본 앱으로 열기
|
|
System.Diagnostics.Process.Start(
|
|
new System.Diagnostics.ProcessStartInfo(file.Path)
|
|
{ UseShellExecute = true });
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ─── 디렉터리 내용 나열 ─────────────────────────────────────────────────────
|
|
|
|
private static IEnumerable<LauncherItem> ListDirectory(string dir, string filter = "")
|
|
{
|
|
var items = new List<LauncherItem>();
|
|
|
|
// 상위 폴더 항목 (루트가 아닐 때)
|
|
var parent = Path.GetDirectoryName(dir.TrimEnd('\\', '/'));
|
|
if (!string.IsNullOrEmpty(parent))
|
|
{
|
|
items.Add(new LauncherItem(
|
|
".. (상위 폴더)",
|
|
parent,
|
|
null,
|
|
new FileBrowserEntry(parent, true),
|
|
Symbol: "\uE74A")); // Back 아이콘
|
|
}
|
|
|
|
try
|
|
{
|
|
// 폴더 먼저
|
|
var dirs = Directory.GetDirectories(dir)
|
|
.Where(d => string.IsNullOrEmpty(filter) ||
|
|
Path.GetFileName(d).Contains(filter, StringComparison.OrdinalIgnoreCase))
|
|
.OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase)
|
|
.Take(40);
|
|
|
|
foreach (var d in dirs)
|
|
{
|
|
var name = Path.GetFileName(d);
|
|
items.Add(new LauncherItem(
|
|
name,
|
|
d,
|
|
null,
|
|
new FileBrowserEntry(d, true),
|
|
Symbol: Symbols.Folder));
|
|
}
|
|
|
|
// 파일
|
|
var files = Directory.GetFiles(dir)
|
|
.Where(f => string.IsNullOrEmpty(filter) ||
|
|
Path.GetFileName(f).Contains(filter, StringComparison.OrdinalIgnoreCase))
|
|
.OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)
|
|
.Take(30);
|
|
|
|
foreach (var f in files)
|
|
{
|
|
var name = Path.GetFileName(f);
|
|
var ext = Path.GetExtension(f).ToLowerInvariant();
|
|
var size = FormatSize(new FileInfo(f).Length);
|
|
items.Add(new LauncherItem(
|
|
name,
|
|
$"{size} · {ext.TrimStart('.')} 파일",
|
|
null,
|
|
new FileBrowserEntry(f, false),
|
|
Symbol: ExtToSymbol(ext)));
|
|
}
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
items.Add(new LauncherItem("접근 권한 없음", dir, null, null, Symbol: Symbols.Error));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
items.Add(new LauncherItem("읽기 오류", ex.Message, null, null, Symbol: Symbols.Error));
|
|
}
|
|
|
|
if (items.Count == 0 || (items.Count == 1 && items[0].Symbol == "\uE74A"))
|
|
items.Add(new LauncherItem("(빈 폴더)", dir, null, null, Symbol: Symbols.Folder));
|
|
|
|
return items;
|
|
}
|
|
|
|
// ─── 헬퍼 ─────────────────────────────────────────────────────────────────
|
|
|
|
private static string ExpandPath(string path)
|
|
{
|
|
if (path.StartsWith("~")) path = "%USERPROFILE%" + path[1..];
|
|
return Environment.ExpandEnvironmentVariables(path);
|
|
}
|
|
|
|
private static string FormatSize(long bytes) => bytes switch
|
|
{
|
|
< 1_024L => $"{bytes} B",
|
|
< 1_024L * 1_024 => $"{bytes / 1_024.0:F1} KB",
|
|
< 1_024L * 1_024 * 1_024 => $"{bytes / 1_048_576.0:F1} MB",
|
|
_ => $"{bytes / 1_073_741_824.0:F1} GB",
|
|
};
|
|
|
|
private static string ExtToSymbol(string ext) => ext switch
|
|
{
|
|
".exe" or ".msi" => Symbols.App,
|
|
".pdf" => "\uEA90",
|
|
".docx" or ".doc" => "\uE8A5",
|
|
".xlsx" or ".xls" => "\uE9F9",
|
|
".pptx" or ".ppt" => "\uE8A5",
|
|
".zip" or ".7z" or ".rar" => "\uED25",
|
|
".mp4" or ".avi" or ".mkv" => "\uE714",
|
|
".mp3" or ".wav" or ".flac" => "\uE767",
|
|
".png" or ".jpg" or ".jpeg" or ".gif" => "\uEB9F",
|
|
".txt" or ".md" or ".log" => "\uE8A5",
|
|
".cs" or ".py" or ".js" or ".ts" => "\uE8A5",
|
|
".lnk" => "\uE71B",
|
|
_ => "\uE7C3",
|
|
};
|
|
}
|
|
|
|
/// <summary>파일 탐색기 핸들러에서 사용하는 항목 데이터</summary>
|
|
public record FileBrowserEntry(string Path, bool IsFolder);
|