[Phase L4] 파일탐색기·QuickLook·단위변환 단축 3종 완료
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
This commit is contained in:
185
src/AxCopilot/Handlers/FileBrowserHandler.cs
Normal file
185
src/AxCopilot/Handlers/FileBrowserHandler.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user