diff --git a/benchmarks/prototypes/FileSystem.prototype-fast.format.ps1xml b/benchmarks/prototypes/FileSystem.prototype-fast.format.ps1xml
new file mode 100644
index 0000000..06a44a1
--- /dev/null
+++ b/benchmarks/prototypes/FileSystem.prototype-fast.format.ps1xml
@@ -0,0 +1,32 @@
+
+
+
+
+ gly.FileSystem.Prototype.FastTable
+
+ System.IO.FileInfo
+ System.IO.DirectoryInfo
+
+
+
+ 7
+ 25
+ 10Right
+
+
+
+
+
+ Mode
+ LastWriteTime
+ Length
+
+ $global:GlyPrototypeRenderer.Render($_)
+
+
+
+
+
+
+
+
diff --git a/benchmarks/prototypes/FileSystem.prototype.format.ps1xml b/benchmarks/prototypes/FileSystem.prototype.format.ps1xml
new file mode 100644
index 0000000..8138687
--- /dev/null
+++ b/benchmarks/prototypes/FileSystem.prototype.format.ps1xml
@@ -0,0 +1,40 @@
+
+
+
+
+ gly.FileSystem.Prototype.Table
+
+ System.IO.FileInfo
+ System.IO.DirectoryInfo
+
+
+
+ 7
+ 25
+ 10Right
+
+
+
+
+
+ Mode
+ LastWriteTime
+
+
+ $module = Get-Module -Name gly
+ & $module {
+ param($inputObject)
+ Format-GlyFileSize -InputObject $inputObject
+ } $_
+
+
+
+ $global:GlyPrototypeRenderer.Render($_)
+
+
+
+
+
+
+
+
diff --git a/benchmarks/prototypes/GlyDisplayNamePrototype.cs b/benchmarks/prototypes/GlyDisplayNamePrototype.cs
new file mode 100644
index 0000000..df6cc7f
--- /dev/null
+++ b/benchmarks/prototypes/GlyDisplayNamePrototype.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+
+public sealed class GlyPrototypeRule
+{
+ private readonly Regex[] wildcardPatterns;
+
+ public GlyPrototypeRule(
+ string token,
+ string kind,
+ string name,
+ string[] extensions,
+ string[] globs,
+ FileAttributes[] attributes)
+ {
+ Token = token;
+ Kind = string.IsNullOrEmpty(kind) ? null : kind;
+ Name = string.IsNullOrEmpty(name) ? null : name;
+ Extensions = extensions;
+ Attributes = attributes;
+
+ var patterns = new List();
+ var exactNames = new List();
+ foreach (string glob in globs)
+ {
+ if (glob.Contains('*') || glob.Contains('?'))
+ {
+ string pattern = "^" + Regex.Escape(glob).Replace("\\*", ".*").Replace("\\?", ".") + "$";
+ patterns.Add(new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled));
+ }
+ else
+ {
+ exactNames.Add(glob);
+ }
+ }
+
+ ExactNames = exactNames.ToArray();
+ wildcardPatterns = patterns.ToArray();
+ }
+
+ public string Token { get; }
+ public string Kind { get; }
+ public string Name { get; }
+ public string[] Extensions { get; }
+ public string[] ExactNames { get; }
+ public FileAttributes[] Attributes { get; }
+
+ public bool Matches(string kind, string name, FileAttributes attributes)
+ {
+ if (Kind != null && !string.Equals(Kind, kind, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (Name != null && !string.Equals(Name, name, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (Extensions.Length > 0)
+ {
+ bool matched = false;
+ foreach (string extension in Extensions)
+ {
+ if (name.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
+ {
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched)
+ {
+ return false;
+ }
+ }
+
+ if (ExactNames.Length > 0 || wildcardPatterns.Length > 0)
+ {
+ bool matched = false;
+ foreach (string exactName in ExactNames)
+ {
+ if (string.Equals(name, exactName, StringComparison.OrdinalIgnoreCase))
+ {
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched)
+ {
+ foreach (Regex pattern in wildcardPatterns)
+ {
+ if (pattern.IsMatch(name))
+ {
+ matched = true;
+ break;
+ }
+ }
+ }
+
+ if (!matched)
+ {
+ return false;
+ }
+ }
+
+ foreach (FileAttributes required in Attributes)
+ {
+ if ((attributes & required) == 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
+
+public sealed class GlyDisplayNamePrototype
+{
+ private readonly GlyPrototypeRule[] rules;
+ private readonly IReadOnlyDictionary glyphs;
+ private readonly string fileStyle;
+ private readonly string directoryStyle;
+ private readonly string hiddenStyle;
+ private readonly string readOnlyStyle;
+
+ public GlyDisplayNamePrototype(
+ GlyPrototypeRule[] rules,
+ IReadOnlyDictionary glyphs,
+ IReadOnlyDictionary palette)
+ {
+ this.rules = rules;
+ this.glyphs = glyphs;
+ fileStyle = ToAnsiStyle(palette["File"], false);
+ directoryStyle = ToAnsiStyle(palette["Directory"], true);
+ hiddenStyle = ToAnsiStyle(palette["Hidden"], false);
+ readOnlyStyle = ToAnsiStyle(palette["ReadOnly"], false);
+ }
+
+ public string Render(FileSystemInfo item)
+ {
+ FileAttributes attributes = item.Attributes;
+ if ((attributes & FileAttributes.ReparsePoint) != 0)
+ {
+ throw new NotSupportedException("This prototype does not render symbolic links.");
+ }
+
+ string name = item.Name;
+ string kind = item is DirectoryInfo ? "Directory" : "File";
+ string token = "Default";
+ for (int index = rules.Length - 1; index >= 0; index--)
+ {
+ if (rules[index].Matches(kind, name, attributes))
+ {
+ token = rules[index].Token;
+ break;
+ }
+ }
+
+ if (!glyphs.TryGetValue(token, out string glyph))
+ {
+ glyph = glyphs["Default"];
+ }
+
+ string displayName = string.IsNullOrEmpty(glyph) ? name : glyph + " " + name;
+ string style = (attributes & FileAttributes.Hidden) != 0 ? hiddenStyle
+ : (attributes & FileAttributes.ReadOnly) != 0 ? readOnlyStyle
+ : kind == "Directory" ? directoryStyle
+ : fileStyle;
+
+ return style + displayName + "\u001b[0m";
+ }
+
+ private static string ToAnsiStyle(string hexColor, bool bold)
+ {
+ int red = Convert.ToInt32(hexColor.Substring(1, 2), 16);
+ int green = Convert.ToInt32(hexColor.Substring(3, 2), 16);
+ int blue = Convert.ToInt32(hexColor.Substring(5, 2), 16);
+ string codes = bold ? "1;38;2" : "38;2";
+ return "\u001b[" + codes + ";" + red + ";" + green + ";" + blue + "m";
+ }
+}
diff --git a/benchmarks/prototypes/Measure-GlyPrototype.ps1 b/benchmarks/prototypes/Measure-GlyPrototype.ps1
new file mode 100644
index 0000000..418971f
--- /dev/null
+++ b/benchmarks/prototypes/Measure-GlyPrototype.ps1
@@ -0,0 +1,159 @@
+[CmdletBinding()]
+param(
+ [int] $ItemCount = 3000,
+ [int] $Iterations = 3
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+function Measure-Case {
+ param(
+ [string] $Name,
+ [scriptblock] $Action
+ )
+
+ & $Action | Out-Null
+ $samples = for ($iteration = 0; $iteration -lt $Iterations; $iteration++) {
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ [GC]::Collect()
+
+ $timer = [Diagnostics.Stopwatch]::StartNew()
+ & $Action | Out-Null
+ $timer.Stop()
+ $timer.Elapsed.TotalMilliseconds
+ }
+
+ $ordered = @($samples | Sort-Object)
+ [pscustomobject]@{
+ Scenario = $Name
+ MedianMs = [Math]::Round($ordered[[int] [Math]::Floor($ordered.Count / 2)], 2)
+ MinMs = [Math]::Round($ordered[0], 2)
+ MaxMs = [Math]::Round($ordered[-1], 2)
+ }
+}
+
+$dataPath = Join-Path ([IO.Path]::GetTempPath()) ('gly-prototype-{0:N}' -f [guid]::NewGuid())
+if (Test-Path -LiteralPath $dataPath) {
+ throw "Temporary benchmark path already exists: $dataPath"
+}
+
+New-Item -ItemType Directory -Path $dataPath | Out-Null
+try {
+ $extensions = @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.ts', '.d.ts', '.json', '.md', '.tar.gz', '.txt', '.unknown')
+ for ($index = 0; $index -lt $ItemCount; $index++) {
+ if (($index % 50) -eq 0) {
+ New-Item -ItemType Directory -Path (Join-Path $dataPath ('dir-{0:D5}' -f $index)) | Out-Null
+ continue
+ }
+
+ $name = 'file-{0:D5}{1}' -f $index, $extensions[$index % $extensions.Count]
+ $path = Join-Path $dataPath $name
+ [IO.File]::WriteAllText($path, "gly benchmark item $index")
+ if (($index % 40) -eq 0) {
+ $item = Get-Item -LiteralPath $path
+ $item.Attributes = $item.Attributes -bor [IO.FileAttributes]::Hidden
+ }
+ }
+
+ [IO.File]::WriteAllText((Join-Path $dataPath '.gitignore'), "bin/`nobj/`n")
+ [IO.File]::WriteAllText((Join-Path $dataPath 'Dockerfile'), "FROM scratch`n")
+
+ Import-Module (Join-Path $PSScriptRoot '../../src/gly.psd1') -Force
+ Set-GlyConfiguration -StyleRenderer Ansi -ShowGlyphs $true -ShowColors $true -RespectNoColor $false | Out-Null
+ $items = @(Get-ChildItem -LiteralPath $dataPath -Force)
+ $module = Get-Module gly
+
+ $baselineTable = $items | Format-Table -View gly.FileSystem.Table | Out-String -Width 4096
+ $setupTimer = [Diagnostics.Stopwatch]::StartNew()
+ $state = & $module {
+ [pscustomobject]@{
+ Catalog = @(Get-GlyBuiltInSelectorCatalog)
+ Glyphs = $script:GlyGlyphSets['NerdFonts'].Map
+ Palette = $script:GlyThemes['DefaultDark'].Palette
+ }
+ }
+
+ $timer = [Diagnostics.Stopwatch]::StartNew()
+ Add-Type -Path (Join-Path $PSScriptRoot 'GlyDisplayNamePrototype.cs')
+ $timer.Stop()
+ $compileMs = [Math]::Round($timer.Elapsed.TotalMilliseconds, 2)
+
+ $rules = [GlyPrototypeRule[]] @(
+ foreach ($definition in $state.Catalog) {
+ $selector = $definition.Selector
+ $kind = if ($null -eq $selector.Kind) { $null } else { $selector.Kind.ToString() }
+ [GlyPrototypeRule]::new(
+ $definition.Token,
+ $kind,
+ $selector.Name,
+ [string[]] $selector.NormalizedExtension,
+ [string[]] $selector.Glob,
+ [IO.FileAttributes[]] $selector.Attributes
+ )
+ }
+ )
+
+ $glyphs = [Collections.Generic.Dictionary[string, string]]::new([StringComparer]::Ordinal)
+ foreach ($key in $state.Glyphs.Keys) {
+ $glyphs.Add([string] $key, [string] $state.Glyphs[$key])
+ }
+
+ $palette = [Collections.Generic.Dictionary[string, string]]::new([StringComparer]::Ordinal)
+ foreach ($key in $state.Palette.Keys) {
+ $palette.Add([string] $key, [string] $state.Palette[$key])
+ }
+
+ $global:GlyPrototypeRenderer = [GlyDisplayNamePrototype]::new($rules, $glyphs, $palette)
+ $setupTimer.Stop()
+ $setupMs = [Math]::Round($setupTimer.Elapsed.TotalMilliseconds, 2)
+
+ $mismatches = 0
+ foreach ($item in $items) {
+ $powerShellName = Get-GlyFileSystemDisplayName -InputObject $item
+ $prototypeName = $global:GlyPrototypeRenderer.Render($item)
+ if ($powerShellName -cne $prototypeName) {
+ $mismatches++
+ if ($mismatches -le 3) {
+ Write-Warning "Mismatch for $($item.Name): PowerShell '$powerShellName'; C# '$prototypeName'"
+ }
+ }
+ }
+ if ($mismatches -gt 0) {
+ throw "C# prototype produced $mismatches different display names."
+ }
+
+ Update-FormatData -PrependPath (Join-Path $PSScriptRoot 'FileSystem.prototype.format.ps1xml')
+ $prototypeTable = $items | Format-Table -View gly.FileSystem.Prototype.Table | Out-String -Width 4096
+ if ($baselineTable -cne $prototypeTable) {
+ throw 'C# prototype table differs from the PowerShell table.'
+ }
+
+ Update-FormatData -PrependPath (Join-Path $PSScriptRoot 'FileSystem.prototype-fast.format.ps1xml')
+ $fastTable = $items | Format-Table -View gly.FileSystem.Prototype.FastTable | Out-String -Width 4096
+ if ($baselineTable -cne $fastTable) {
+ throw 'C# prototype table with native Length differs from the PowerShell table.'
+ }
+
+ $results = @(
+ Measure-Case 'Read Mode property' { foreach ($item in $items) { [void] $item.Mode } }
+ Measure-Case 'Direct Length property' { foreach ($item in $items) { if ($item -is [IO.FileInfo]) { [void] $item.Length } } }
+ Measure-Case 'Size via module bridge' { foreach ($item in $items) { $currentModule = Get-Module gly; [void] (& $currentModule { param($inputObject) Format-GlyFileSize $inputObject } $item) } }
+ Measure-Case 'PowerShell display name' { foreach ($item in $items) { [void] (Get-GlyFileSystemDisplayName $item) } }
+ Measure-Case 'C# display name' { foreach ($item in $items) { [void] $global:GlyPrototypeRenderer.Render($item) } }
+ Measure-Case 'Native explicit table' { $items | Format-Table Mode, LastWriteTime, Length, Name | Out-String -Width 4096 | Out-Null }
+ Measure-Case 'PowerShell gly table' { $items | Format-Table -View gly.FileSystem.Table | Out-String -Width 4096 | Out-Null }
+ Measure-Case 'C# prototype table' { $items | Format-Table -View gly.FileSystem.Prototype.Table | Out-String -Width 4096 | Out-Null }
+ Measure-Case 'C# with native Length' { $items | Format-Table -View gly.FileSystem.Prototype.FastTable | Out-String -Width 4096 | Out-Null }
+ )
+
+ "Items: $($items.Count); prototype setup: $setupMs ms (C# compile: $compileMs ms); output mismatches: $mismatches"
+ $results | Format-Table -AutoSize
+}
+finally {
+ Remove-Variable GlyPrototypeRenderer -Scope Global -ErrorAction SilentlyContinue
+ if (Test-Path -LiteralPath $dataPath) {
+ Remove-Item -LiteralPath $dataPath -Recurse -Force
+ }
+}
diff --git a/benchmarks/prototypes/README.md b/benchmarks/prototypes/README.md
new file mode 100644
index 0000000..c989e82
--- /dev/null
+++ b/benchmarks/prototypes/README.md
@@ -0,0 +1,11 @@
+# C# display-name prototype
+
+Run the isolated comparison with PowerShell 7+:
+
+```powershell
+pwsh -NoProfile -File ./benchmarks/prototypes/Measure-GlyPrototype.ps1 -ItemCount 3000 -Iterations 5
+```
+
+The script creates disposable file-system data, compiles `GlyDisplayNamePrototype.cs` in memory, and checks every display name and the complete table output against the current module before timing either implementation. It measures the cost of the existing size-formatting bridge separately. The second prototype table uses the native `Length` property and is equivalent only with the default raw-size setting.
+
+This is an experiment, not a module integration. It handles the built-in NerdFonts glyph set, the DefaultDark theme, ANSI output, and ordinary files and directories. Symbolic links, user-registered rules, other themes and renderers, runtime configuration changes, and binary-size formatting still need implementation and verification. Runtime C# compilation is included in the reported setup time; a packaged assembly would avoid that compilation step.