Make arc list and fs list use system's .dir_colors, or the embedded one, to colorize the filenames appropriately.

This commit is contained in:
2025-08-20 08:21:35 +01:00
parent 9fa5a1b62e
commit 39ccc6562e
6 changed files with 805 additions and 5 deletions

View File

@@ -41,6 +41,8 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Remove="dir_colors"/>
<EmbeddedResource Include="dir_colors"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Memory"/>

View File

@@ -0,0 +1,159 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
using System;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
namespace Aaru.Helpers;
public static partial class AnsiColorParser
{
// Standard ANSI 16-color palette (indexes 015)
private static readonly Color[] _basicPalette =
[
Color.FromArgb(0, 0, 0), // 0: black
Color.FromArgb(128, 0, 0), // 1: red
Color.FromArgb(0, 128, 0), // 2: green
Color.FromArgb(128, 128, 0), // 3: yellow
Color.FromArgb(0, 0, 128), // 4: blue
Color.FromArgb(128, 0, 128), // 5: magenta
Color.FromArgb(0, 128, 128), // 6: cyan
Color.FromArgb(192, 192, 192), // 7: white
Color.FromArgb(128, 128, 128), // 8: bright black (gray)
Color.FromArgb(255, 0, 0), // 9: bright red
Color.FromArgb(0, 255, 0), // 10: bright green
Color.FromArgb(255, 255, 0), // 11: bright yellow
Color.FromArgb(0, 0, 255), // 12: bright blue
Color.FromArgb(255, 0, 255), // 13: bright magenta
Color.FromArgb(0, 255, 255), // 14: bright cyan
Color.FromArgb(255, 255, 255) // 15: bright white
];
// Matches ESC [ params m
private static readonly Regex _sequenceRegex = AnsiRegex();
public static Color Parse(string input)
{
Match m = _sequenceRegex.Match(input);
if(!m.Success) throw new ArgumentException("No ANSI SGR sequence found.", nameof(input));
int[] parts = m.Groups["params"]
.Value.Split([
';'
],
StringSplitOptions.RemoveEmptyEntries)
.Select(int.Parse)
.ToArray();
bool isBold = parts.Contains(1);
bool isDim = parts.Contains(2);
// True-color: ESC[38;2;<r>;<g>;<b>m
int idx38 = Array.IndexOf(parts, 38);
switch(idx38)
{
case >= 0 when parts.Length > idx38 + 4 && parts[idx38 + 1] == 2:
{
int r = parts[idx38 + 2], g = parts[idx38 + 3], b = parts[idx38 + 4];
return Color.FromArgb(r, g, b);
}
// 256-color: ESC[38;5;<n>m
case >= 0 when parts.Length > idx38 + 2 && parts[idx38 + 1] == 5:
return ColorFrom256(parts[idx38 + 2]);
}
// 3037 and 9097 color codes
foreach(int code in parts)
{
switch(code)
{
// 3037 => palette[07]
case >= 30 and <= 37:
{
int baseIndex = code - 30;
// Bold takes precedence
if(isBold) return _basicPalette[baseIndex + 8];
return isDim ? DimColor(_basicPalette[baseIndex]) : _basicPalette[baseIndex];
}
// 9097 => palette[815]
case >= 90 and <= 97:
{
int brightIndex = code - 90 + 8;
return isDim ? DimColor(_basicPalette[brightIndex]) : _basicPalette[brightIndex];
}
}
}
// Fallback
return Color.White;
}
private static Color ColorFrom256(int index)
{
switch(index)
{
case < 16:
return _basicPalette[index];
case <= 231:
{
// 6×6×6 color cube
int ci = index - 16;
int r = ci / 36, g = ci / 6 % 6, b = ci % 6;
int[] levels =
[
0, 95, 135, 175, 215, 255
];
return Color.FromArgb(levels[r], levels[g], levels[b]);
}
default:
{
// Grayscale 232255
int gray = 8 + (index - 232) * 10;
return Color.FromArgb(gray, gray, gray);
}
}
}
private static Color DimColor(Color c) =>
// Halve each channel to simulate faint intensity
Color.FromArgb(c.R / 2, c.G / 2, c.B / 2);
[GeneratedRegex(@"\e\[(?<params>[0-9;]*)m", RegexOptions.Compiled)]
private static partial Regex AnsiRegex();
}

View File

@@ -0,0 +1,166 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
#nullable enable
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Aaru.Helpers;
/// <summary>
/// Singleton that parses ~/.dir_colors or falls back to an embedded "dir_colors" resource.
/// Maps file extensions (".txt") → Spectre.Console hex color strings ("#RRGGBB"),
/// and provides a separate property for the directory color.
/// Uses AnsiColorParser.Parse to convert ANSI SGR codes into System.Drawing.Color.
/// </summary>
public sealed class DirColorsParser
{
private static readonly Lazy<DirColorsParser> _instance = new(() => new DirColorsParser());
private DirColorsParser()
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string? directoryHex = null;
// Choose ~/.dir_colors or embedded fallback
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string path = Path.Combine(home, ".dir_colors");
string[] rawLines = File.Exists(path) ? File.ReadAllLines(path) : LoadResourceLines("dir_colors").ToArray();
foreach(string raw in rawLines)
{
string? line = raw?.Trim();
if(string.IsNullOrEmpty(line) || line is ['#', ..]) continue;
// Remove inline comments
int hashIdx = line.IndexOf('#');
if(hashIdx >= 0) line = line[..hashIdx].Trim();
if(string.IsNullOrEmpty(line)) continue;
// Split on '=' or whitespace
string pattern, sgr;
if(line.Contains('='))
{
string[] parts = line.Split('=', 2);
pattern = parts[0].Trim();
sgr = parts[1].Trim();
}
else
{
string[] parts = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
if(parts.Length < 2) continue;
pattern = parts[0];
sgr = parts[1];
}
if(!pattern.StartsWith("DIR", StringComparison.OrdinalIgnoreCase) &&
pattern is not ['.', ..] &&
!pattern.StartsWith("NORM", StringComparison.OrdinalIgnoreCase))
continue; // ( DIR, FILE, LINK, EXECUTABLE, or SUID)
// Build ANSI escape sequence
string ansi = $"\e[{sgr}m";
string hex;
try
{
Color color = AnsiColorParser.Parse(ansi);
hex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}
catch
{
#pragma warning disable ERP022
continue;
#pragma warning restore ERP022
}
// Directory color pattern
if(pattern.Equals("DIR", StringComparison.OrdinalIgnoreCase))
{
directoryHex = hex;
continue;
}
// Directory color pattern
if(pattern.Equals("NORM", StringComparison.OrdinalIgnoreCase))
{
directoryHex = hex;
continue;
}
if(pattern is ['.', ..]) map[pattern] = hex;
}
DirectoryColor = directoryHex;
ExtensionColors = map;
}
public static DirColorsParser Instance => _instance.Value;
/// <summary>
/// The hex color (e.g. "#RRGGBB") used for directories ("DIR" pattern).
/// Null if no directory color was defined.
/// </summary>
public string? DirectoryColor { get; }
/// <summary>
/// The hex color (e.g. "#RRGGBB") used for normal files ("NORM" pattern).
/// Null if no directory color was defined.
/// </summary>
public string NormalColor => "white";
/// <summary>
/// Maps file extensions (including the leading '.') to hex color strings.
/// </summary>
public IReadOnlyDictionary<string, string> ExtensionColors { get; }
private static IEnumerable<string> LoadResourceLines(string resourceFileName)
{
var asm = Assembly.GetExecutingAssembly();
string? resource = asm.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith(resourceFileName, StringComparison.OrdinalIgnoreCase));
if(resource == null) yield break;
using Stream? stream = asm.GetManifestResourceStream(resource);
if(stream == null) yield break;
using var reader = new StreamReader(stream);
while(!reader.EndOfStream) yield return reader.ReadLine()!;
}
}

455
Aaru.Helpers/dir_colors Normal file
View File

@@ -0,0 +1,455 @@
# /***************************************************************************
# Aaru Data Preservation Suite
# ----------------------------------------------------------------------------
#
# Filename : dir_colors
# Author(s) : Natalia Portillo <claunia@claunia.com>
#
# Component : Commands.
#
# --[ Description ] ----------------------------------------------------------
#
# Implements the 'ls' command.
#
# --[ License ] --------------------------------------------------------------
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# ----------------------------------------------------------------------------
# Copyright © 2011-2025 Natalia Portillo
# ****************************************************************************/
#NORMAL 00 # no color code at all
DIR 00;34 # directory
# List any file extensions like '.gz' or '.tar' that you would like ls
# to colorize below. Put the extension, a space, and the color init string.
# (and any comments you want to add after a '#')
# If you use DOS-style suffixes, you may want to uncomment the following:
# Or if you want to colorize scripts even if they do not have the
.app 01;32
.bat 01;32
.btm 01;32
.cmd 01;32 # executables (bright green)
.com 01;32
.exe 01;32
.prg 01;32
# executable bit actually set.
#.sh 01;32
#.csh 01;32
# archives or compressed (bright red)
.7z 01;31
.Z 01;31
.ace 01;31
.arj 01;31
.bz 01;31
.bz2 01;31
.bz3 01;31
.cab 01;31
.cpio 01;31
.crx 01;31
.deb 01;31
.dz 01;31
.ear 01;31
.gem 01;31
.gz 01;31
.hqx 01;31
.jar 01;31
.lrz 01;31
.lz 01;31
.lzfse 01;31
.lzh 01;31
.lzma 01;31
.msi 01;31
.pkg 01;31
.rar 01;31
.rpm 01;31
.rz 01;31
.s7z 01;31
.sar 01;31
.sit 01;31
.sitx 01;31
.sz 01;31
.tar 01;31
.taz 01;31
.tbz 01;31
.tbz2 01;31
.tgz 01;31
.tlz 01;31
.tx 01;31
.txz 01;31
.tz 01;31
.war 01;31
.xpi 01;31
.xz 01;31
.z 01;31
.zip 01;31
.zipx 01;31
.zoo 01;31
.zst 01;31
.zstd 01;31
# disk image formats (yellow)
.2mg 00;33
.86f 00;33
.aaruf 00;33
.aif 00;33
.b5i 00;33
.b5t 00;33
.b6i 00;33
.b6t 00;33
.bwi 00;33
.bwt 00;33
.cdi 00;33
.cptp 00;33
.cqm 00;33
.cue 00;33
.d77 00;33
.d81 00;33
.d88 00;33
.dart 00;33
.dasd 00;33
.dc42 00;33
.dcf 00;33
.dcp 00;33
.dicf 00;33
.dko 00;33
.dmg 00;33
.fdd 00;33
.hdd 00;33
.hdi 00;33
.hdk 00;33
.hdv 00;33
.imd 00;33
.img 00;33
.iso 00;33
.mdf 00;33
.mds 00;33
.mdx 00;33
.nrg 00;33
.qcow 00;33
.scp 00;33
.sub 00;33
.td0 00;33
.toast 00;33
.vdi 00;33
.vhd 00;33
.vhdx 00;33
.vmdk 00;33
.wim 00;33
.xdf 00;33
.xdi 00;33
# video formats
.3g2 00;35
.3gp 00;35
.MOV 00;35
.MP4 00;35
.anx 00;35
.asf 00;35
.ass 00;35
.avi 00;35
.axv 00;35
.flc 00;35
.fli 00;35
.flv 00;35
.m2v 00;35
.m4v 00;35
.mkv 00;35
.mng 00;35
.mov 00;35
.mp4 00;35
.mp4v 00;35
.mpeg 00;35
.mpg 00;35
.nuv 00;35
.ogm 00;35
.ogv 00;35
.ogv 00;35
.ogx 00;35
.ogx 00;35
.qt 00;35
.rm 00;35
.rmvb 00;35
.srt 00;35
.vob 00;35
.vtt 00;35
.webm 00;35
.wmv 00;35
.yuv 00;35
# image formats
.ai 01;35
.bmp 01;35
.cdr 01;35
.cgm 01;35
.dl 01;35
.emf 01;35
.eps 01;35
.flif 01;35
.gif 01;35
.gl 01;35
.ico 01;35
.jp2 01;35
.jpeg 01;35
.jpg 01;35
.jxl 01;35
.pbm 01;35
.pcx 01;35
.pgm 01;35
.png 01;35
.ppm 01;35
.pps 01;35
.ppsx 01;35
.psd 01;35
.qpi 01;35
.svg 01;35
.svgz 01;35
.swf 01;35
.tga 01;35
.tif 01;35
.tiff 01;35
.wmf 01;35
.xbm 01;35
.xcf 01;35
.xpm 01;35
.xspf 01;35
.xwd 01;35
# Document files
.diff 00;31
.djvu 00;31
.doc 00;31
.docx 00;31
.dot 00;31
.dotx 00;31
.epub 00;31
.fla 00;31
.kfx 00;31
.mobi 00;31
.odp 00;31
.ods 00;31
.odt 00;31
.otp 00;31
.ots 00;31
.ott 00;31
.patch 00;31
.pdf 00;31
.pdf 00;31
.ppt 00;31
.pptx 00;31
.prc 00;31
.ps 00;31
.rtf 00;31
.tex 00;31
.txt 00;31
.vcf 00;31
.wps 00;31
.xls 00;31
.xlsx 00;31
# audio formats
.aac 00;36
.au 00;36
.axa 00;36
.flac 00;36
.m4a 00;36
.mid 00;36
.midi 00;36
.mka 00;36
.mp3 00;36
.mpa 00;36
.mpc 00;36
.oga 00;36
.ogg 00;36
.opus 00;36
.ra 00;36
.spx 00;36
.wav 00;36
.wma 00;36
.xspf 00;36
# Documentation
*AUTHORS 01;34
*CHANGELOG 01;34
*CHANGELOG.md 01;34
*CHANGES 01;34
*CHANGES.md 01;34
*CODE_OF_CONDUCT.md 01;34
*CONTRIBUTING 01;34
*CONTRIBUTING.md 01;34
*CONTRIBUTORS 01;34
*COPYING 01;34
*COPYRIGHT 01;34
*ChangeLog 01;34
*Changelog 01;34
*HACKING 01;34
*HISTORY 01;34
*INSTALL 01;34
*INSTALL.md 01;34
*LICENSE 01;34
*LICENSE.MD 01;34
*LICENSE.md 01;34
*NEWS 01;34
*NOTICE 01;34
*PATENTS 01;34
*README 01;34
*README.MD 01;34
*README.md 01;34
*README.rst 01;34
*THANKS 01;34
*TODO 01;34
*VERSION 01;34
*code-of-conduct.md 01;34
*contributing.md 01;34
*readme.md 01;34
.MD 01;34
.info 01;34
.log 01;34
.markdown 01;34
.md 01;34
.mkd 01;34
.org 01;34
.rst 01;34
# Programming
*Makefile 00;32
.C 00;32
.ac 00;32
.am 38;5;246
.asm 00;32
.aspx 00;32
.bash 00;32
.c 00;32
.cc 00;32
.cl 00;32
.class 00;32
.coffee 00;32
.cpp 00;32
.cs 00;32
.csh 00;32
.css 00;32
.csv 00;32
.cxx 00;32
.db 00;32
.editorconfig 38;5;246
.el 00;32
.env 38;5;246
.erb 00;32
.example 38;5;246
.f90 00;32
.fish 00;32
.gitattributes 38;5;246
.gitignore 38;5;246
.gitmodules 38;5;246
.go 00;32
.h 00;32
.haml 00;32
.hpp 00;32
.hs 00;32
.htm 00;32
.html 00;32
.in 38;5;246
.java 00;32
.js 00;32
.json 00;32
.l 00;32
.less 00;32
.lisp 00;32
.lock 00;32
.lua 00;32
.m 00;32
.m4 38;5;246
.man 00;32
.n 00;32
.p 00;32
.php 00;32
.pl 00;32
.pm 00;32
.pod 00;32
.py 00;32
.r 00;32
.razor 00;32
.rb 00;32
.rdf 00;32
.rs 00;32
.rss 00;32
.sass 00;32
.scss 00;32
.sh 00;32
.shtml 00;32
.sql 00;32
.sqlite 00;32
.sv 00;32
.svh 00;32
.tex 00;32
.toml 00;32
.ts 00;32
.v 00;32
.vh 00;32
.vhd 00;32
.vim 00;32
.xml 00;32
.yaml 00;32
.yml 00;32
.zsh 00;32
# Unix
.3des 34
.aes 34
.asc 34
.cer 34
.cfg 34
.conf 34
.csr 34
.desktop 34
.enc 34
.gpg 34
.ini 34
.otf 34
.pgp 34
.pid 34
.ttf 34
# Miscellaneous
.crdownload 37
.torrent 34
# Backup, Temporary, etc.
*# 1;34
*lockfile 38;5;246
*~ 1;34
.BAK 38;5;246
.DAT 38;5;246
.DIST 38;5;246
.OFF 38;5;246
.OLD 38;5;246
.ORIG 38;5;246
.bak 38;5;246
.dat 38;5;246
.dist 38;5;246
.dll 38;5;246
.kate-swp 38;5;246
.o 38;5;246
.off 38;5;246
.old 38;5;246
.org_archive 38;5;246
.orig 38;5;246
.rlib 38;5;246
.sassc 38;5;246
.swo 38;5;246
.swp 38;5;246
.tmp 38;5;246

View File

@@ -32,16 +32,19 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Core;
using Aaru.Helpers;
using Aaru.Localization;
using Aaru.Logging;
using Spectre.Console;
using Spectre.Console.Cli;
using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;
namespace Aaru.Commands.Archive;
@@ -312,6 +315,15 @@ sealed class ArchiveListCommand : Command<ArchiveListCommand.Settings>
continue;
}
string color = DirColorsParser.Instance.NormalColor;
if(stat.Attributes.HasFlag(FileAttributes.Directory))
color = DirColorsParser.Instance.DirectoryColor;
else if(!DirColorsParser.Instance.ExtensionColors
.TryGetValue(Path.GetExtension(fileName) ?? "",
out color))
color = DirColorsParser.Instance.NormalColor;
if(archive.ArchiveFeatures.HasFlag(ArchiveSupportedFeature.SupportsCompression))
{
table.AddRow($"[blue]{stat.CreationTime?.ToShortDateString() ?? ""}[/]",
@@ -319,7 +331,7 @@ sealed class ArchiveListCommand : Command<ArchiveListCommand.Settings>
$"[gold3]{new string(attr)}[/]",
$"[lime]{uncompressedSize}[/]",
$"[teal]{compressedSize}[/]",
$"[green]{Markup.Escape(fileName)}[/]");
$"[{color}]{Markup.Escape(fileName)}[/]");
AaruLogging.Information($"Date: {stat.CreationTime?.ToShortDateString() ?? ""} " +
$"Time: ({stat.CreationTime?.ToLongTimeString() ?? ""}), " +
@@ -334,7 +346,7 @@ sealed class ArchiveListCommand : Command<ArchiveListCommand.Settings>
$"[dodgerblue1]{stat.CreationTime?.ToLongTimeString() ?? ""}[/]",
$"[gold3]{new string(attr)}[/]",
$"[lime]{uncompressedSize}[/]",
$"[green]{Markup.Escape(fileName)}[/]");
$"[{color}]{Markup.Escape(fileName)}[/]");
AaruLogging.Information($"Date: {stat.CreationTime?.ToShortDateString() ?? ""} " +
$"Time: ({stat.CreationTime?.ToLongTimeString() ?? ""}), " +

View File

@@ -33,6 +33,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
@@ -40,11 +41,13 @@ using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Core;
using Aaru.Helpers;
using Aaru.Localization;
using Aaru.Logging;
using JetBrains.Annotations;
using Spectre.Console;
using Spectre.Console.Cli;
using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;
namespace Aaru.Commands.Filesystem;
@@ -379,17 +382,20 @@ sealed class LsCommand : Command<LsCommand.Settings>
table.AddRow($"[gold3]{entry.Value.Attributes.ToAttributeChars()}[/]",
"",
"",
$"[green]{Markup.Escape(entry.Key)}[/]");
$"[{DirColorsParser.Instance.DirectoryColor}]{Markup.Escape(entry.Key)}[/]");
AaruLogging.Information($"{entry.Value.Attributes.ToAttributeChars()} {entry.Key}");
}
else
{
if(!DirColorsParser.Instance.ExtensionColors.TryGetValue(Path.GetExtension(entry.Key) ?? "",
out string color))
color = DirColorsParser.Instance.NormalColor;
table.AddRow($"[gold3]{entry.Value.Attributes.ToAttributeChars()}[/]",
$"[lime]{entry.Value.Length}[/]",
$"[dodgerblue1]{entry.Value.LastWriteTimeUtc:s}[/]",
$"[green]{Markup.Escape(entry.Key)}[/]");
$"[{color}]{Markup.Escape(entry.Key)}[/]");
AaruLogging
.Information($"{entry.Value.Attributes.ToAttributeChars()} {entry.Value.Length} {entry.Value.LastWriteTimeUtc:s} {entry.Key}");