5 Commits
15 changed files with 819 additions and 23 deletions
+15 -1
View File
@@ -39,7 +39,13 @@ namespace QuickClaude
"or other personal information from your context into web searches or URLs you fetch, " + "or other personal information from your context into web searches or URLs you fetch, " +
"unless the user specifically asks you to search for them. " + "unless the user specifically asks you to search for them. " +
"Shell commands (Bash or PowerShell) are allowed for other things, but each one first shows the user a confirm box; " + "Shell commands (Bash or PowerShell) are allowed for other things, but each one first shows the user a confirm box; " +
"if they decline, carry on without it."; "if they decline, carry on without it. " +
"When the user asks you to open something, run exactly one PowerShell Start-Process command with one quoted target " +
"and nothing else on the line (no arguments, no second command); those open without a confirm box. " +
"A website: Start-Process \"https://x.com\" (always the full https:// URL). " +
"A folder: Start-Process \"C:\\wallpapers\" (the full path). " +
"A program: Start-Process mspaint (just its name). If QuickClaude answers that the name isn't on the PATH and gives " +
"a Start Menu shortcut, run Start-Process with that shortcut's full path, exactly as given.";
// Run without prompts. Bash and PowerShell stay out so they go through the confirm box hook. // Run without prompts. Bash and PowerShell stay out so they go through the confirm box hook.
// Write and Edit only when the user picked "Edit files" or "Auto"; "Only look" removes them outright. // Write and Edit only when the user picked "Edit files" or "Auto"; "Only look" removes them outright.
@@ -214,6 +220,8 @@ namespace QuickClaude
Finished?.Invoke(error); Finished?.Invoke(error);
} }
// File writes also go through a guard hook ("QuickClaude.exe --guard-write") that blocks the
// Desktop and AppData, where OpenRules lets things open without asking.
// Shell commands go through QuickClaude's own confirm box: a PreToolUse hook that runs // Shell commands go through QuickClaude's own confirm box: a PreToolUse hook that runs
// "QuickClaude.exe --confirm" (exec form, so no shell quoting). The dialog denies after // "QuickClaude.exe --confirm" (exec form, so no shell quoting). The dialog denies after
// 2 minutes; the hook timeout is a bit longer so Claude Code never cuts it off first. // 2 minutes; the hook timeout is a bit longer so Claude Code never cuts it off first.
@@ -231,6 +239,12 @@ namespace QuickClaude
{ {
matcher = "Bash|PowerShell|Monitor", // Monitor also runs shell commands matcher = "Bash|PowerShell|Monitor", // Monitor also runs shell commands
hooks = new[] { new { type = "command", command = exe, args = new[] { "--confirm" }, timeout = 150 } } hooks = new[] { new { type = "command", command = exe, args = new[] { "--confirm" }, timeout = 150 } }
},
new
{
// Never lets Claude write to the Desktop or AppData (see WriteGuard)
matcher = "Write|Edit|MultiEdit|NotebookEdit",
hooks = new[] { new { type = "command", command = exe, args = new[] { "--guard-write" }, timeout = 30 } }
} }
} }
} }
+24 -7
View File
@@ -31,14 +31,31 @@ namespace QuickClaude
// Nothing to show: say nothing, so Claude Code's normal permission rules decide // Nothing to show: say nothing, so Claude Code's normal permission rules decide
if (string.IsNullOrEmpty(command)) return 0; if (string.IsNullOrEmpty(command)) return 0;
new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; string decision, reason;
var win = new ConfirmWindow(toolName, command, cwd); var verdict = OpenRules.Check(command, cwd, out var hint);
win.ShowDialog(); if (verdict == OpenRules.Verdict.Allow)
{
// "open x.com", "open C:\wallpapers", "open mspaint": no confirm box (see OpenRules)
decision = "allow";
reason = "Opening a website, folder or installed program is allowed without asking.";
}
else if (verdict == OpenRules.Verdict.Hint)
{
// The bare name wouldn't resolve, but there's a Start Menu shortcut for it
decision = "deny";
reason = hint;
}
else
{
new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
var win = new ConfirmWindow(toolName, command, cwd);
win.ShowDialog();
string decision = win.Allowed ? "allow" : "deny"; decision = win.Allowed ? "allow" : "deny";
string reason = win.Allowed ? "The user allowed this command." reason = win.Allowed ? "The user allowed this command."
: win.TimedOut ? "The user didn't answer within 2 minutes, so the command was not run. Carry on without it." : win.TimedOut ? "The user didn't answer within 2 minutes, so the command was not run. Carry on without it."
: "The user declined to run this command. Carry on without it."; : "The user declined to run this command. Carry on without it.";
}
var json = JsonSerializer.Serialize(new var json = JsonSerializer.Serialize(new
{ {
+58
View File
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
namespace QuickClaude
{
// The same font choices as wIRC. Saved in settings.json by Name.
// - Windows fonts (Segoe UI, Tahoma, Tw Cen MT) aren't bundled; they're listed only when
// installed (Tw Cen MT only comes with Office).
// - The rounded fonts (Nunito, Quicksand, Varela Round) are SIL OFL licensed and built into
// the exe from the fonts folder, with their license files.
// An unknown or missing font falls back to Consolas.
public static class Fonts
{
public const string Default = "Consolas";
static readonly Uri PackRoot = new Uri("pack://application:,,,/");
public class Choice
{
public string Name;
public FontFamily Family;
}
static List<Choice> choices;
public static List<Choice> All
{
get
{
if (choices != null) return choices;
var installed = new HashSet<string>(
System.Windows.Media.Fonts.SystemFontFamilies.Select(f => f.Source),
StringComparer.OrdinalIgnoreCase);
choices = new List<Choice>
{
new Choice { Name = "Consolas", Family = new FontFamily("Consolas, Courier New") }
};
foreach (var sys in new[] { "Segoe UI", "Tahoma", "Tw Cen MT" })
if (installed.Contains(sys))
choices.Add(new Choice { Name = sys, Family = new FontFamily(sys) });
choices.Add(Bundled("Nunito (rounded)", "Nunito"));
choices.Add(Bundled("Quicksand (rounded)", "Quicksand"));
choices.Add(Bundled("Varela Round (rounded)", "Varela Round"));
return choices;
}
}
static Choice Bundled(string name, string family) =>
new Choice { Name = name, Family = new FontFamily(PackRoot, "./fonts/#" + family + ", Consolas") };
public static Choice Find(string name) =>
All.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)) ?? All[0];
}
}
+205 -14
View File
@@ -20,21 +20,31 @@ namespace QuickClaude
readonly TextBox input = new TextBox(); readonly TextBox input = new TextBox();
readonly TextBlock status = new TextBlock(); readonly TextBlock status = new TextBlock();
readonly TextBlock grip = new TextBlock(); readonly TextBlock grip = new TextBlock();
// Holds the transcript. The transcript inside is always sized to a whole number of lines
// and sits at the bottom, so no line is ever shown cut in half (see FitTranscript).
readonly Border transcriptHost = new Border { ClipToBounds = true };
double lineHeight = 13;
WF.NotifyIcon tray; WF.NotifyIcon tray;
WF.ToolStripMenuItem topItem, blackItem, whiteItem, lookItem, editItem, autoItem, folderItem; WF.ToolStripMenuItem fontMenu, sizeMenu, snapItem, topItem, blackItem, whiteItem, lookItem, editItem, autoItem, folderItem;
bool lastWasTool; bool lastWasTool;
bool quitting; bool quitting;
// Nearly invisible fill so the window still catches the mouse (fully clear pixels click through) // Nearly invisible fill so the window still catches the mouse (fully clear pixels click through)
static readonly Brush HitFill = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); static readonly Brush HitFill = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
internal static readonly FontFamily Mono = new FontFamily("Consolas, Courier New"); // The current font (picked from the tray's Font menu). Still called Mono since
// Matches wIRC at its smallest font slider setting (10sp = 10 WPF units) // ConfirmWindow and DarkMenu read it when they open.
internal const double TextSize = 10; internal static FontFamily Mono = new FontFamily("Consolas, Courier New");
// Picked from the tray's Font size menu, 10 to 18 (same units as wIRC's font size
// slider: 10sp = 10 WPF units). ConfirmWindow and DarkMenu read it when they open.
internal const double MinTextSize = 10, MaxTextSize = 18;
internal static double TextSize = 10;
const double ResizeEdge = 6; const double ResizeEdge = 6;
public MainWindow() public MainWindow()
{ {
settings = Settings.Load(); settings = Settings.Load();
Mono = Fonts.Find(settings.Font).Family;
TextSize = Math.Clamp(Math.Round(settings.FontSize), MinTextSize, MaxTextSize);
Title = "QuickClaude"; Title = "QuickClaude";
WindowStyle = WindowStyle.None; WindowStyle = WindowStyle.None;
@@ -66,7 +76,7 @@ namespace QuickClaude
if (transcript.Text.Length == 0) if (transcript.Text.Length == 0)
transcript.Text = "QuickClaude. Type below and press Enter. Shift+Enter for a new line, Esc stops Claude, Ctrl+Alt+C hides or shows this note.\n"; transcript.Text = "QuickClaude. Type below and press Enter. Shift+Enter for a new line, Esc stops Claude, Ctrl+Alt+C hides or shows this note.\n";
Loaded += (s, e) => { transcript.ScrollToEnd(); input.Focus(); }; Loaded += (s, e) => { MeasureLineHeight(); transcript.ScrollToEnd(); input.Focus(); };
PreviewMouseLeftButtonUp += ClickToInput; PreviewMouseLeftButtonUp += ClickToInput;
SourceInitialized += (s, e) => RegisterHotkey(); SourceInitialized += (s, e) => RegisterHotkey();
LocationChanged += (s, e) => SavePlace(); LocationChanged += (s, e) => SavePlace();
@@ -88,7 +98,7 @@ namespace QuickClaude
rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
// Top row: a faint "clod" label that moves the window, plus a quiet status line // Top row: a solid "clod" label that moves the window, plus a quiet status line
grip.Text = "clod"; grip.Text = "clod";
grip.FontSize = TextSize; grip.FontSize = TextSize;
grip.FontFamily = Mono; grip.FontFamily = Mono;
@@ -113,8 +123,12 @@ namespace QuickClaude
transcript.TextWrapping = TextWrapping.Wrap; transcript.TextWrapping = TextWrapping.Wrap;
transcript.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden; transcript.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
transcript.IsUndoEnabled = false; transcript.IsUndoEnabled = false;
Grid.SetRow(transcript, 1); transcript.VerticalAlignment = VerticalAlignment.Bottom;
rootGrid.Children.Add(transcript); transcriptHost.Background = HitFill;
transcriptHost.Child = transcript;
transcriptHost.SizeChanged += (s, e) => FitTranscript();
Grid.SetRow(transcriptHost, 1);
rootGrid.Children.Add(transcriptHost);
// Input line // Input line
var bottom = new Grid { Margin = new Thickness(0, 6, 0, 0) }; var bottom = new Grid { Margin = new Thickness(0, 6, 0, 0) };
@@ -131,12 +145,83 @@ namespace QuickClaude
Grid.SetRow(bottom, 2); Grid.SetRow(bottom, 2);
rootGrid.Children.Add(bottom); rootGrid.Children.Add(bottom);
// Right-click menus: Copy (and Cut/Paste in the input) plus New conversation; blank areas get just the latter Content = rootGrid;
BuildContextMenus();
}
// Right-click menus: Copy (and Cut/Paste in the input) plus New conversation; blank areas get just the latter.
// Rebuilt when the font changes so they use it too.
void BuildContextMenus()
{
transcript.ContextMenu = DarkMenu.Make(transcript, NewConversation); transcript.ContextMenu = DarkMenu.Make(transcript, NewConversation);
input.ContextMenu = DarkMenu.Make(input, NewConversation); input.ContextMenu = DarkMenu.Make(input, NewConversation);
rootGrid.ContextMenu = DarkMenu.Make(null, NewConversation); ((Grid)Content).ContextMenu = DarkMenu.Make(null, NewConversation);
}
Content = rootGrid; // Height of one line of transcript text at the current font and size. Measured as the
// difference between two lines and one, so it's exactly what the text box steps by.
void MeasureLineHeight()
{
var probe = new TextBlock { FontFamily = Mono, FontSize = TextSize, Text = "Mg" };
probe.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double one = probe.DesiredSize.Height;
probe.Text = "Mg\nMg";
probe.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double two = probe.DesiredSize.Height;
lineHeight = two - one > 1 ? two - one : Math.Max(1, one);
FitTranscript();
}
// Make the transcript exactly as tall as the whole lines that fit, bottom-aligned, and keep
// it scrolled to the end so the top line is whole too. Runs on every size change (window
// resize, the input box growing, font changes).
void FitTranscript()
{
double avail = transcriptHost.ActualHeight;
if (avail <= 0 || lineHeight <= 0) return;
int lines = Math.Max(1, (int)Math.Floor((avail + 0.01) / lineHeight));
transcript.Height = lines * lineHeight;
transcript.ScrollToEnd();
}
// Leftover space under the last whole line, in WPF units
double TranscriptGap() => Math.Max(0, transcriptHost.ActualHeight - (double.IsNaN(transcript.Height) ? 0 : transcript.Height));
void SetFontSize(double size)
{
size = Math.Clamp(size, MinTextSize, MaxTextSize);
settings.FontSize = size;
settings.Save();
TextSize = size;
FontSize = size;
transcript.FontSize = size;
input.FontSize = size;
status.FontSize = size;
grip.FontSize = size;
MeasureLineHeight();
BuildContextMenus();
transcript.ScrollToEnd();
if (sizeMenu != null)
foreach (WF.ToolStripMenuItem item in sizeMenu.DropDownItems)
item.Checked = item.Text == size.ToString();
}
void SetFont(string name)
{
var choice = Fonts.Find(name);
settings.Font = choice.Name;
settings.Save();
Mono = choice.Family;
FontFamily = Mono;
transcript.FontFamily = Mono;
input.FontFamily = Mono;
status.FontFamily = Mono;
grip.FontFamily = Mono;
MeasureLineHeight();
BuildContextMenus();
if (fontMenu != null)
foreach (WF.ToolStripMenuItem item in fontMenu.DropDownItems)
item.Checked = item.Text == choice.Name;
} }
static void StyleBox(TextBox tb) static void StyleBox(TextBox tb)
@@ -178,7 +263,7 @@ namespace QuickClaude
input.CaretBrush = solid; input.CaretBrush = solid;
transcript.CaretBrush = solid; transcript.CaretBrush = solid;
status.Foreground = faint; status.Foreground = faint;
grip.Foreground = faint; grip.Foreground = solid;
if (blackItem != null) { blackItem.Checked = c == Colors.Black; whiteItem.Checked = c == Colors.White; } if (blackItem != null) { blackItem.Checked = c == Colors.Black; whiteItem.Checked = c == Colors.White; }
} }
@@ -354,6 +439,29 @@ namespace QuickClaude
color.DropDownItems.Add(whiteItem); color.DropDownItems.Add(whiteItem);
menu.Items.Add(color); menu.Items.Add(color);
fontMenu = new WF.ToolStripMenuItem("Font");
var current = Fonts.Find(settings.Font).Name;
foreach (var choice in Fonts.All)
{
var name = choice.Name;
fontMenu.DropDownItems.Add(new WF.ToolStripMenuItem(name, null, (s, e) => SetFont(name)) { Checked = name == current });
}
menu.Items.Add(fontMenu);
sizeMenu = new WF.ToolStripMenuItem("Font size");
for (int px = (int)MinTextSize; px <= (int)MaxTextSize; px++)
{
double sz = px;
sizeMenu.DropDownItems.Add(new WF.ToolStripMenuItem(px.ToString(), null, (s, e) => SetFontSize(sz)) { Checked = sz == TextSize });
}
menu.Items.Add(sizeMenu);
snapItem = new WF.ToolStripMenuItem("Snap to desktop grid", null, (s, e) =>
{
settings.SnapToGrid = !settings.SnapToGrid; snapItem.Checked = settings.SnapToGrid; settings.Save();
}) { Checked = settings.SnapToGrid };
menu.Items.Add(snapItem);
topItem = new WF.ToolStripMenuItem("Always on top", null, (s, e) => topItem = new WF.ToolStripMenuItem("Always on top", null, (s, e) =>
{ {
settings.Topmost = !settings.Topmost; Topmost = settings.Topmost; topItem.Checked = settings.Topmost; settings.Save(); settings.Topmost = !settings.Topmost; Topmost = settings.Topmost; topItem.Checked = settings.Topmost; settings.Save();
@@ -418,7 +526,8 @@ namespace QuickClaude
var hit = EdgeHit(hwnd, l); var hit = EdgeHit(hwnd, l);
if (hit != 0) { handled = true; return new IntPtr(hit); } if (hit != 0) { handled = true; return new IntPtr(hit); }
} }
else if (msg == WM_EXITSIZEMOVE) SavePlace(); else if (msg == WM_ENTERSIZEMOVE) GetWindowRect(hwnd, out moveStart);
else if (msg == WM_EXITSIZEMOVE) { AfterMoveOrResize(hwnd); SavePlace(); }
return IntPtr.Zero; return IntPtr.Zero;
}); });
} }
@@ -426,7 +535,89 @@ namespace QuickClaude
// Resize from any edge or corner: report a ~6px border strip as the matching hit code // Resize from any edge or corner: report a ~6px border strip as the matching hit code
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
[StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; }
const int WM_NCHITTEST = 0x0084, WM_EXITSIZEMOVE = 0x0232; const int WM_NCHITTEST = 0x0084, WM_ENTERSIZEMOVE = 0x0231, WM_EXITSIZEMOVE = 0x0232;
RECT moveStart;
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
[DllImport("user32.dll")] static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern bool GetMonitorInfo(IntPtr mon, ref MONITORINFO info);
[DllImport("user32.dll")] static extern bool SystemParametersInfo(uint action, uint param, out int value, uint ini);
[StructLayout(LayoutKind.Sequential)] struct MONITORINFO { public int cbSize; public RECT rcMonitor, rcWork; public uint dwFlags; }
const uint SWP_NOSIZE = 0x1, SWP_NOMOVE = 0x2, SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10;
const uint SPI_ICONHORIZONTALSPACING = 0x000D, SPI_ICONVERTICALSPACING = 0x0018;
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindow(string cls, string name);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string cls, string name);
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, int msg, IntPtr w, IntPtr l);
const int LVM_GETITEMSPACING = 0x1033;
// The desktop icon grid, asked of the desktop's own icon view so it follows the icon size
// and spacing Windows is really using (wIRC asks the same way, so both line up), falling
// back to the system icon spacing. In physical pixels.
static bool DesktopGridSpacing(out int sx, out int sy)
{
sx = sy = 0;
try
{
var view = FindWindowEx(FindWindow("Progman", null), IntPtr.Zero, "SHELLDLL_DefView", null);
var worker = IntPtr.Zero;
while (view == IntPtr.Zero)
{
worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null);
if (worker == IntPtr.Zero) break;
view = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null);
}
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
if (list != IntPtr.Zero)
{
long r = SendMessage(list, LVM_GETITEMSPACING, IntPtr.Zero, IntPtr.Zero).ToInt64();
sx = (int)(r & 0xFFFF); sy = (int)((r >> 16) & 0xFFFF);
if (sx > 8 && sy > 8) return true;
}
}
catch { }
return SystemParametersInfo(SPI_ICONHORIZONTALSPACING, 0, out sx, 0) && sx > 8 &&
SystemParametersInfo(SPI_ICONVERTICALSPACING, 0, out sy, 0) && sy > 8;
}
// After a drag or resize ends: a resize trims the window to whole transcript lines, and a
// plain move snaps the top-left corner to the desktop icon grid (same spacing Windows uses
// for desktop icons, counted from the top-left of the screen's work area).
void AfterMoveOrResize(IntPtr hwnd)
{
if (!GetWindowRect(hwnd, out var r)) return;
bool resized = (r.Right - r.Left) != (moveStart.Right - moveStart.Left) ||
(r.Bottom - r.Top) != (moveStart.Bottom - moveStart.Top);
double scale = PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M22 ?? 1;
if (resized)
{
UpdateLayout();
double gap = TranscriptGap();
int gapPx = (int)Math.Floor(gap * scale);
int h = r.Bottom - r.Top;
int minPx = (int)Math.Ceiling(MinHeight * scale);
if (gapPx > 0 && h - gapPx >= minPx)
{
// Keep whichever edge didn't move: resizing from the top keeps the bottom fixed
bool fromTop = r.Top != moveStart.Top && r.Bottom == moveStart.Bottom;
int top = fromTop ? r.Top + gapPx : r.Top;
SetWindowPos(hwnd, IntPtr.Zero, r.Left, top, r.Right - r.Left, h - gapPx, SWP_NOZORDER | SWP_NOACTIVATE);
}
return;
}
if (!settings.SnapToGrid) return;
if (r.Left == moveStart.Left && r.Top == moveStart.Top) return;
if (!DesktopGridSpacing(out int sx, out int sy)) return;
var info = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
if (!GetMonitorInfo(MonitorFromWindow(hwnd, 2 /* nearest */), ref info)) return;
var w = info.rcWork;
int x = w.Left + (int)Math.Round((r.Left - w.Left) / (double)sx) * sx;
int y = w.Top + (int)Math.Round((r.Top - w.Top) / (double)sy) * sy;
if (x != r.Left || y != r.Top)
SetWindowPos(hwnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17; const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
int EdgeHit(IntPtr hwnd, IntPtr l) int EdgeHit(IntPtr hwnd, IntPtr l)
+147
View File
@@ -0,0 +1,147 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace QuickClaude
{
// Decides which "open ..." shell commands skip the confirm box. Only a command that is a
// single open of ONE target, with no arguments and nothing chained on, qualifies:
// Start-Process "https://x.com" a website (http/https only)
// Start-Process "C:\wallpapers" a folder that exists (no network paths)
// Start-Process mspaint a program: an .exe in Program Files, Windows,
// Start-Process "C:\...\mirc.exe" AppData\Local\Programs or the Store app aliases,
// Start-Process "...\mIRC.lnk" or a shortcut in the Start Menu or on the Desktop
// Scripts (.bat, .ps1 and so on) and programs anywhere else still get the confirm box.
public static class OpenRules
{
public enum Verdict { Ask, Allow, Hint }
// verb, optional "" title (cmd start), optional -FilePath/-Path, then ONE target.
// Quoted targets may contain spaces and ( ) for "Program Files (x86)", but never quotes,
// ` $ % or line breaks (PowerShell and cmd expand those). Unquoted targets are stricter.
static readonly Regex OpenCmd = new Regex(
@"^\s*(?:cmd(?:\.exe)?\s+/c\s+)?(?:Start-Process|saps|start|explorer(?:\.exe)?|Invoke-Item|ii)\s+" +
@"(?:""""\s+)?(?:-(?:FilePath|Path|LiteralPath)\s+)?" +
@"(?:""(?<t>[^""'`$%\r\n;&|<>^]+)""|'(?<t>[^""'`$%\r\n;&|<>^]+)'|(?<t>[^\s""'`$%;&|<>()^]+))\s*$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
static readonly Regex Url = new Regex(@"^https?://[^\s""'`;&|<>$()^%]+$", RegexOptions.IgnoreCase);
static readonly Regex BareName = new Regex(@"^[A-Za-z0-9_.\-]+$");
public static Verdict Check(string command, string cwd, out string hint)
{
hint = null;
if (string.IsNullOrEmpty(command) || command.IndexOfAny(new[] { '\n', '\r' }) >= 0) return Verdict.Ask;
var m = OpenCmd.Match(command);
if (!m.Success) return Verdict.Ask;
var target = m.Groups["t"].Value.Trim();
if (target.Length == 0) return Verdict.Ask;
if (Url.IsMatch(target)) return Verdict.Allow;
try
{
if (Path.IsPathFullyQualified(target))
{
if (target.StartsWith(@"\\")) return Verdict.Ask; // no network paths
var full = Path.GetFullPath(target);
if (Directory.Exists(full)) return Verdict.Allow;
if (File.Exists(full) && TrustedProgram(full)) return Verdict.Allow;
return Verdict.Ask;
}
if (BareName.IsMatch(target))
{
// A bare name could also pick up a file sitting in the work folder; never allow that
if (!string.IsNullOrEmpty(cwd) && Directory.Exists(cwd) &&
(File.Exists(Path.Combine(cwd, target)) || Directory.EnumerateFiles(cwd, target + ".*").Any())) return Verdict.Ask;
if (FindProgram(target) != null) return Verdict.Allow;
var lnk = FindShortcut(target);
if (lnk != null)
{
hint = "\"" + target + "\" isn't on the PATH, so that command wouldn't find it. " +
"Its Start Menu shortcut is " + lnk + ". Run Start-Process \"" + lnk + "\" instead " +
"(that one opens without a confirm box).";
return Verdict.Hint;
}
}
}
catch { }
return Verdict.Ask;
}
static string Env(string name) => Environment.GetEnvironmentVariable(name) ?? "";
static string Special(Environment.SpecialFolder f) => Environment.GetFolderPath(f);
static string[] ProgramDirs => new[]
{
Env("ProgramFiles"), Env("ProgramFiles(x86)"), Env("ProgramW6432"),
Env("WINDIR"),
Path.Combine(Special(Environment.SpecialFolder.LocalApplicationData), "Programs"),
Path.Combine(Special(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "WindowsApps"),
}.Where(d => d.Length > 0).ToArray();
static string[] ShortcutDirs => new[]
{
Path.Combine(Special(Environment.SpecialFolder.CommonApplicationData), "Microsoft", "Windows", "Start Menu"),
Path.Combine(Special(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Start Menu"),
Special(Environment.SpecialFolder.DesktopDirectory),
Special(Environment.SpecialFolder.CommonDesktopDirectory),
}.Where(d => d.Length > 0).ToArray();
static bool Under(string path, string dir)
{
var d = Path.GetFullPath(dir).TrimEnd('\\') + "\\";
return path.StartsWith(d, StringComparison.OrdinalIgnoreCase);
}
static bool TrustedProgram(string full)
{
var ext = Path.GetExtension(full).ToLowerInvariant();
if (ext == ".exe") return ProgramDirs.Any(d => Under(full, d));
if (ext == ".lnk") return ShortcutDirs.Any(d => Under(full, d));
return false;
}
// Where Windows itself would find a bare program name: the Windows folders, the Store app
// aliases, and the App Paths registry (how "mspaint" and friends resolve).
static string FindProgram(string name)
{
var exe = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name : name + ".exe";
var win = Env("WINDIR");
foreach (var dir in new[]
{
Path.Combine(win, "System32"), win,
Path.Combine(Special(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "WindowsApps"),
})
{
var p = Path.Combine(dir, exe);
if (File.Exists(p)) return p;
}
foreach (var hive in new[] { Registry.CurrentUser, Registry.LocalMachine })
{
using var key = hive.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\App Paths\" + exe);
var val = (key?.GetValue(null) as string)?.Trim('"');
if (!string.IsNullOrEmpty(val) && File.Exists(val) && TrustedProgram(Path.GetFullPath(val))) return val;
}
return null;
}
// A Start Menu or Desktop shortcut named like the program ("mirc" finds "mIRC.lnk").
// Exact name first, then the shortest name that starts with it.
static string FindShortcut(string name)
{
var all = ShortcutDirs.Where(Directory.Exists)
.SelectMany(d => { try { return Directory.EnumerateFiles(d, "*.lnk", SearchOption.AllDirectories); } catch { return Enumerable.Empty<string>(); } })
.ToList();
var exact = all.FirstOrDefault(f => string.Equals(Path.GetFileNameWithoutExtension(f), name, StringComparison.OrdinalIgnoreCase));
if (exact != null) return exact;
return all.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(name, StringComparison.OrdinalIgnoreCase))
.OrderBy(f => Path.GetFileNameWithoutExtension(f).Length).FirstOrDefault();
}
}
}
+2
View File
@@ -12,6 +12,8 @@ namespace QuickClaude
{ {
// Run as Claude Code's shell-command confirm hook (separate short-lived process) // Run as Claude Code's shell-command confirm hook (separate short-lived process)
if (Array.IndexOf(args, "--confirm") >= 0) return ConfirmHook.Run(); if (Array.IndexOf(args, "--confirm") >= 0) return ConfirmHook.Run();
// Run as Claude Code's file-write guard hook (see WriteGuard)
if (Array.IndexOf(args, "--guard-write") >= 0) return WriteGuard.Run();
using var mutex = new Mutex(true, "QuickClaude.SingleInstance", out bool first); using var mutex = new Mutex(true, "QuickClaude.SingleInstance", out bool first);
if (!first) return 0; if (!first) return 0;
+5 -1
View File
@@ -10,7 +10,7 @@
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<AssemblyName>QuickClaude</AssemblyName> <AssemblyName>QuickClaude</AssemblyName>
<RootNamespace>QuickClaude</RootNamespace> <RootNamespace>QuickClaude</RootNamespace>
<Version>1.0.0</Version> <Version>1.5.0</Version>
<Product>QuickClaude</Product> <Product>QuickClaude</Product>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
@@ -21,4 +21,8 @@
<DebugType>none</DebugType> <DebugType>none</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Resource Include="fonts\*.ttf" />
</ItemGroup>
</Project> </Project>
+3
View File
@@ -11,6 +11,9 @@ namespace QuickClaude
public double Width { get; set; } = 460; public double Width { get; set; } = 460;
public double Height { get; set; } = 380; public double Height { get; set; } = 380;
public string TextColor { get; set; } = "Black"; public string TextColor { get; set; } = "Black";
public string Font { get; set; } = "Consolas";
public double FontSize { get; set; } = 10;
public bool SnapToGrid { get; set; } = true;
public bool Topmost { get; set; } = false; public bool Topmost { get; set; } = false;
public string WorkDir { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); public string WorkDir { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public string Mode { get; set; } = "default"; public string Mode { get; set; } = "default";
+81
View File
@@ -0,0 +1,81 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace QuickClaude
{
// QuickClaude.exe --guard-write: Claude Code's PreToolUse hook for Write, Edit and NotebookEdit.
// Keeps Claude from writing anywhere OpenRules lets things open without a confirm box that a
// normal (non-admin) account can write to: your Desktop and all of AppData (which holds your
// own Start Menu, AppData\Local\Programs and the Store app aliases). Without this, Claude in
// "Edit files" or "Auto" mode could drop a shortcut or program there and then open it unasked.
// Every other folder is left to the "Claude can" setting as before.
public static class WriteGuard
{
public static int Run()
{
string path = null, cwd = null;
try
{
using var stdin = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
using var doc = JsonDocument.Parse(stdin.ReadToEnd());
var root = doc.RootElement;
cwd = Str(root, "cwd");
if (root.TryGetProperty("tool_input", out var input))
path = Str(input, "file_path") ?? Str(input, "notebook_path") ?? Str(input, "path");
}
catch { }
if (string.IsNullOrEmpty(path)) return 0; // nothing to check: normal rules decide
if (!Blocked(path, cwd)) return 0;
var json = JsonSerializer.Serialize(new
{
hookSpecificOutput = new
{
hookEventName = "PreToolUse",
permissionDecision = "deny",
permissionDecisionReason =
"QuickClaude doesn't let Claude write to the Desktop or AppData folders (programs and shortcuts there " +
"can open without a confirm box). Tell the user, and suggest a different folder if one would do."
}
});
using var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
stdout.Write(json);
return 0;
}
static bool Blocked(string path, string cwd)
{
try
{
var full = Path.IsPathRooted(path) || string.IsNullOrEmpty(cwd)
? Path.GetFullPath(path)
: Path.GetFullPath(Path.Combine(cwd, path));
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var dirs = new[]
{
Path.Combine(home, "AppData"),
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Microsoft", "Windows", "Start Menu"),
}.Where(d => !string.IsNullOrEmpty(d));
return dirs.Any(d =>
{
var root = Path.GetFullPath(d).TrimEnd('\\');
return full.Equals(root, StringComparison.OrdinalIgnoreCase) ||
full.StartsWith(root + "\\", StringComparison.OrdinalIgnoreCase);
});
}
catch { return true; } // a path we can't even parse doesn't get written
}
static string Str(JsonElement e, string name) =>
e.ValueKind == JsonValueKind.Object && e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
}
}
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily), with Reserved Font Name “Quicksand”.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2023 The Varela Round Project Authors (https://github.com/alefalefalef/Varela-Round-Hebrew/), with Reserved Font Names 'Varela' and ‘Varela Round’.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.