변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
278 lines
12 KiB
C#
278 lines
12 KiB
C#
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L20-4: Unix 파일 권한 계산기 핸들러. "perm" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: perm → 주요 권한 목록
|
|
/// perm 755 → rwxr-xr-x 상세 설명
|
|
/// perm 644 → rw-r--r-- 상세 설명
|
|
/// perm rwxr-xr-x → 기호 → 숫자 변환
|
|
/// perm rw-r--r-- → 기호 → 숫자 변환
|
|
/// perm +x 644 → 실행 비트 추가
|
|
/// perm -x 755 → 실행 비트 제거
|
|
/// perm +w 444 → 쓰기 비트 추가
|
|
/// perm umask 022 → umask 적용 결과
|
|
/// perm common → 자주 쓰는 권한 목록
|
|
/// Enter → 값 복사.
|
|
/// </summary>
|
|
public class PermHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "perm";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"Perm",
|
|
"Unix 파일 권한 계산기 — chmod 숫자↔기호·umask·권한 설명",
|
|
"1.0",
|
|
"AX");
|
|
|
|
private record CommonPerm(string Octal, string Symbol, string Description, string UseCase);
|
|
|
|
private static readonly CommonPerm[] Common =
|
|
[
|
|
new("777", "rwxrwxrwx", "모든 사용자 완전 권한", "임시 스크립트 (보안 주의)"),
|
|
new("755", "rwxr-xr-x", "소유자 완전·그룹/기타 읽기+실행", "실행 파일, 디렉토리"),
|
|
new("750", "rwxr-x---", "소유자 완전·그룹 읽기+실행·기타 없음", "그룹 공유 스크립트"),
|
|
new("700", "rwx------", "소유자만 완전 권한", "개인 스크립트"),
|
|
new("644", "rw-r--r--", "소유자 읽기+쓰기·그룹/기타 읽기만", "일반 파일"),
|
|
new("640", "rw-r-----", "소유자 읽기+쓰기·그룹 읽기·기타 없음", "설정 파일"),
|
|
new("600", "rw-------", "소유자만 읽기+쓰기", "개인 설정·SSH 키"),
|
|
new("444", "r--r--r--", "모든 사용자 읽기만", "공유 읽기 전용"),
|
|
new("400", "r--------", "소유자만 읽기", "SSL 인증서·개인 키"),
|
|
new("666", "rw-rw-rw-", "모든 사용자 읽기+쓰기 (실행 없음)", "임시 파일"),
|
|
new("664", "rw-rw-r--", "소유자+그룹 읽기+쓰기·기타 읽기", "그룹 협업 파일"),
|
|
new("660", "rw-rw----", "소유자+그룹 읽기+쓰기·기타 없음", "그룹 협업 파일"),
|
|
new("775", "rwxrwxr-x", "소유자+그룹 완전·기타 읽기+실행", "그룹 협업 디렉토리"),
|
|
new("770", "rwxrwx---", "소유자+그룹 완전·기타 없음", "그룹 전용 디렉토리"),
|
|
];
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim();
|
|
var items = new List<LauncherItem>();
|
|
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
items.Add(new LauncherItem("Unix 파일 권한 계산기",
|
|
"perm 755 / perm rwxr-xr-x / perm +x 644 / perm umask 022 / perm common",
|
|
null, null, Symbol: "\uE8A5"));
|
|
foreach (var p in Common.Take(6))
|
|
items.Add(new LauncherItem($"{p.Octal} {p.Symbol}",
|
|
$"{p.Description} ({p.UseCase})",
|
|
null, ("copy", p.Octal), Symbol: "\uE8A5"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var sub = parts[0].ToLowerInvariant();
|
|
|
|
// common
|
|
if (sub is "common" or "list" or "목록")
|
|
{
|
|
items.Add(new LauncherItem("자주 쓰는 파일 권한 목록", "", null, null, Symbol: "\uE8A5"));
|
|
foreach (var p in Common)
|
|
items.Add(new LauncherItem($"{p.Octal} {p.Symbol}",
|
|
$"{p.Description} · {p.UseCase}",
|
|
null, ("copy", p.Octal), Symbol: "\uE8A5"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// umask
|
|
if (sub == "umask" && parts.Length >= 2)
|
|
{
|
|
if (TryParseOctal(parts[1], out var umaskVal))
|
|
items.AddRange(BuildUmask(umaskVal));
|
|
else
|
|
items.Add(ErrorItem("umask 값은 3자리 8진수입니다 (예: 022)"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// +x / -x / +w / -w / +r / -r (비트 수정)
|
|
if (sub.Length == 2 && sub[0] is '+' or '-' && sub[1] is 'r' or 'w' or 'x')
|
|
{
|
|
if (parts.Length >= 2 && TryParseOctal(parts[1], out var baseVal))
|
|
items.AddRange(BuildBitModify(sub[0] == '+', sub[1], baseVal));
|
|
else
|
|
items.Add(ErrorItem($"예: perm {sub} 644"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 기호 표기 (rwxr-xr-x)
|
|
if (sub.Length == 9 && sub.All(c => c is 'r' or 'w' or 'x' or '-'))
|
|
{
|
|
items.AddRange(BuildFromSymbol(sub));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 8진수 표기 (755, 644...)
|
|
if (TryParseOctal(sub, out var octal))
|
|
{
|
|
items.AddRange(BuildFromOctal(octal));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
items.Add(new LauncherItem($"인식할 수 없는 입력: '{sub}'",
|
|
"perm 755 / perm rwxr-xr-x / perm +x 644 / perm umask 022",
|
|
null, null, Symbol: "\uE783"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is ("copy", string text))
|
|
{
|
|
try
|
|
{
|
|
System.Windows.Application.Current.Dispatcher.Invoke(
|
|
() => Clipboard.SetText(text));
|
|
NotificationService.Notify("Perm", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 빌더 ────────────────────────────────────────────────────────────────
|
|
|
|
private static IEnumerable<LauncherItem> BuildFromOctal(int octal)
|
|
{
|
|
var sym = OctalToSymbol(octal);
|
|
var desc = DescribeOctal(octal);
|
|
var preset = Common.FirstOrDefault(p => p.Octal == octal.ToString("D3"));
|
|
|
|
yield return new LauncherItem($"{octal:D3} → {sym}",
|
|
preset?.Description ?? desc,
|
|
null, ("copy", octal.ToString("D3")), Symbol: "\uE8A5");
|
|
|
|
yield return CopyItem("8진수", octal.ToString("D3"));
|
|
yield return CopyItem("기호 표기", sym);
|
|
yield return CopyItem("chmod 명령", $"chmod {octal:D3} <파일>");
|
|
|
|
var parts = OctalToParts(octal);
|
|
yield return new LauncherItem("소유자 (Owner)",
|
|
$"{OctetToSymbol(parts[0])} ({BitsDesc(parts[0])})",
|
|
null, null, Symbol: "\uE8A5");
|
|
yield return new LauncherItem("그룹 (Group)",
|
|
$"{OctetToSymbol(parts[1])} ({BitsDesc(parts[1])})",
|
|
null, null, Symbol: "\uE8A5");
|
|
yield return new LauncherItem("기타 (Others)",
|
|
$"{OctetToSymbol(parts[2])} ({BitsDesc(parts[2])})",
|
|
null, null, Symbol: "\uE8A5");
|
|
|
|
if (preset != null)
|
|
yield return new LauncherItem("용도", preset.UseCase, null, null, Symbol: "\uE8A5");
|
|
|
|
// 관련 권한 제안
|
|
yield return new LauncherItem("── 관련 권한 ──", "", null, null, Symbol: "\uE8A5");
|
|
var related = Common.Where(p => Math.Abs(int.Parse(p.Octal) - octal) <= 11)
|
|
.Take(4).ToList();
|
|
foreach (var r in related)
|
|
yield return new LauncherItem($"{r.Octal} {r.Symbol}", r.Description,
|
|
null, ("copy", r.Octal), Symbol: "\uE8A5");
|
|
}
|
|
|
|
private static IEnumerable<LauncherItem> BuildFromSymbol(string sym)
|
|
{
|
|
var octal = SymbolToOctal(sym);
|
|
return BuildFromOctal(octal);
|
|
}
|
|
|
|
private static IEnumerable<LauncherItem> BuildUmask(int umask)
|
|
{
|
|
var fileDefault = 0666;
|
|
var dirDefault = 0777;
|
|
var fileResult = fileDefault & ~umask;
|
|
var dirResult = dirDefault & ~umask;
|
|
|
|
yield return new LauncherItem($"umask {umask:D3} → 파일: {fileResult:D3} 디렉토리: {dirResult:D3}",
|
|
"umask 적용 결과", null, null, Symbol: "\uE8A5");
|
|
yield return CopyItem("umask 값", umask.ToString("D3"));
|
|
yield return CopyItem("기본 파일 권한", fileResult.ToString("D3"));
|
|
yield return CopyItem("기본 디렉토리 권한", dirResult.ToString("D3"));
|
|
yield return CopyItem("파일 기호", OctalToSymbol(fileResult));
|
|
yield return CopyItem("디렉토리 기호", OctalToSymbol(dirResult));
|
|
yield return new LauncherItem("설명",
|
|
$"umask {umask:D3}: 파일={OctalToSymbol(fileResult)}, 디렉토리={OctalToSymbol(dirResult)}",
|
|
null, null, Symbol: "\uE8A5");
|
|
}
|
|
|
|
private static IEnumerable<LauncherItem> BuildBitModify(bool add, char bit, int baseOctal)
|
|
{
|
|
var mask = bit switch
|
|
{
|
|
'r' => 0444,
|
|
'w' => 0222,
|
|
'x' => 0111,
|
|
_ => 0
|
|
};
|
|
var result = add ? (baseOctal | mask) : (baseOctal & ~mask);
|
|
result &= 0777;
|
|
|
|
var label = add ? $"+{bit}" : $"-{bit}";
|
|
yield return new LauncherItem($"{baseOctal:D3} {label} → {result:D3} ({OctalToSymbol(result)})",
|
|
"Enter 복사", null, ("copy", result.ToString("D3")), Symbol: "\uE8A5");
|
|
yield return CopyItem("변경 전", baseOctal.ToString("D3"));
|
|
yield return CopyItem("변경 후", result.ToString("D3"));
|
|
yield return CopyItem("기호", OctalToSymbol(result));
|
|
yield return CopyItem("chmod", $"chmod {label} <파일> (또는 chmod {result:D3} <파일>)");
|
|
}
|
|
|
|
// ── 변환 헬퍼 ────────────────────────────────────────────────────────────
|
|
|
|
private static bool TryParseOctal(string s, out int val)
|
|
{
|
|
val = 0;
|
|
s = s.TrimStart('0');
|
|
if (string.IsNullOrEmpty(s)) { val = 0; return true; }
|
|
if (s.Length > 4) return false;
|
|
try { val = Convert.ToInt32(s, 8); return val <= 0777; }
|
|
catch { return false; }
|
|
}
|
|
|
|
private static int[] OctalToParts(int octal) =>
|
|
[(octal >> 6) & 7, (octal >> 3) & 7, octal & 7];
|
|
|
|
private static string OctetToSymbol(int v) =>
|
|
$"{((v & 4) != 0 ? 'r' : '-')}{((v & 2) != 0 ? 'w' : '-')}{((v & 1) != 0 ? 'x' : '-')}";
|
|
|
|
private static string OctalToSymbol(int octal)
|
|
{
|
|
var p = OctalToParts(octal);
|
|
return OctetToSymbol(p[0]) + OctetToSymbol(p[1]) + OctetToSymbol(p[2]);
|
|
}
|
|
|
|
private static int SymbolToOctal(string sym)
|
|
{
|
|
int TriBit(int off) =>
|
|
((sym[off] == 'r' ? 4 : 0) |
|
|
(sym[off + 1] == 'w' ? 2 : 0) |
|
|
(sym[off + 2] == 'x' ? 1 : 0));
|
|
return (TriBit(0) << 6) | (TriBit(3) << 3) | TriBit(6);
|
|
}
|
|
|
|
private static string BitsDesc(int v)
|
|
{
|
|
var parts = new List<string>();
|
|
if ((v & 4) != 0) parts.Add("읽기");
|
|
if ((v & 2) != 0) parts.Add("쓰기");
|
|
if ((v & 1) != 0) parts.Add("실행");
|
|
return parts.Count == 0 ? "없음" : string.Join("·", parts);
|
|
}
|
|
|
|
private static string DescribeOctal(int octal)
|
|
{
|
|
var p = OctalToParts(octal);
|
|
return $"소유자:{BitsDesc(p[0])} 그룹:{BitsDesc(p[1])} 기타:{BitsDesc(p[2])}";
|
|
}
|
|
|
|
private static LauncherItem CopyItem(string label, string value) =>
|
|
new(label, value, null, ("copy", value), Symbol: "\uE8A5");
|
|
|
|
private static LauncherItem ErrorItem(string msg) =>
|
|
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
|
}
|