diff --git a/Pansies.sln b/Pansies.sln
new file mode 100644
index 0000000..719cbb1
--- /dev/null
+++ b/Pansies.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pansies", "Pansies.csproj", "{9037884B-464E-7D76-D4CA-093F85B25B46}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9037884B-464E-7D76-D4CA-093F85B25B46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9037884B-464E-7D76-D4CA-093F85B25B46}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9037884B-464E-7D76-D4CA-093F85B25B46}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9037884B-464E-7D76-D4CA-093F85B25B46}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B791020B-78E4-4968-B1D3-1EB8DB93C7C8}
+ EndGlobalSection
+EndGlobal
diff --git a/Source/Assembly/ColorSpaceConfiguration.cs b/Source/Assembly/ColorSpaceConfiguration.cs
new file mode 100644
index 0000000..eea8a56
--- /dev/null
+++ b/Source/Assembly/ColorSpaceConfiguration.cs
@@ -0,0 +1,26 @@
+using PoshCode.Pansies.ColorSpaces;
+using PoshCode.Pansies.ColorSpaces.Conversions;
+
+namespace PoshCode.Pansies
+{
+ ///
+ /// Provides helpers for configuring color space defaults exposed to PowerShell consumers.
+ ///
+ public static class ColorSpaceConfiguration
+ {
+ public static IXyz GetWhiteReference()
+ {
+ return XyzConverter.GetWhiteReference();
+ }
+
+ public static void SetWhiteReference(IXyz whiteReference)
+ {
+ XyzConverter.SetWhiteReference(whiteReference);
+ }
+
+ public static void ResetWhiteReference()
+ {
+ XyzConverter.ResetWhiteReference();
+ }
+ }
+}
diff --git a/Source/Assembly/ColorSpaces/Conversions/XyzConverter.cs b/Source/Assembly/ColorSpaces/Conversions/XyzConverter.cs
index 9748039..a5848f9 100644
--- a/Source/Assembly/ColorSpaces/Conversions/XyzConverter.cs
+++ b/Source/Assembly/ColorSpaces/Conversions/XyzConverter.cs
@@ -4,17 +4,70 @@ namespace PoshCode.Pansies.ColorSpaces.Conversions
{
internal static class XyzConverter
{
+ private static readonly object WhiteReferenceLock = new object();
+ private static readonly Xyz DefaultWhiteReference = new Xyz
+ {
+ X = 95.047,
+ Y = 100.000,
+ Z = 108.883
+ };
+
#region Constants/Helper methods for Xyz related spaces
- internal static IXyz WhiteReference { get; private set; } // TODO: Hard-coded!
+ internal static IXyz WhiteReference { get; private set; }
internal const double Epsilon = 0.008856; // Intent is 216/24389
internal const double Kappa = 903.3; // Intent is 24389/27
+
static XyzConverter()
{
- WhiteReference = new Xyz
+ ResetWhiteReference();
+ }
+
+ public static IXyz GetWhiteReference()
+ {
+ lock (WhiteReferenceLock)
+ {
+ return Clone(WhiteReference ?? DefaultWhiteReference);
+ }
+ }
+
+ public static void SetWhiteReference(IXyz whiteReference)
+ {
+ if (whiteReference is null)
+ {
+ throw new ArgumentNullException(nameof(whiteReference));
+ }
+
+ lock (WhiteReferenceLock)
+ {
+ WhiteReference = Clone(whiteReference);
+ }
+ }
+
+ public static void ResetWhiteReference()
+ {
+ lock (WhiteReferenceLock)
+ {
+ WhiteReference = Clone(DefaultWhiteReference);
+ }
+ }
+
+ private static Xyz Clone(IXyz source)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (source is Xyz xyz)
+ {
+ return new Xyz(xyz.X, xyz.Y, xyz.Z);
+ }
+
+ return new Xyz
{
- X = 95.047,
- Y = 100.000,
- Z = 108.883
+ X = source.X,
+ Y = source.Y,
+ Z = source.Z
};
}
@@ -72,4 +125,4 @@ private static double PivotRgb(double n)
return (n > 0.04045 ? Math.Pow((n + 0.055) / 1.055, 2.4) : n / 12.92) * 100.0;
}
}
-}
\ No newline at end of file
+}
diff --git a/Source/Assembly/Commands/ExpandVariableCommand.cs b/Source/Assembly/Commands/ExpandVariableCommand.cs
new file mode 100644
index 0000000..55b83e2
--- /dev/null
+++ b/Source/Assembly/Commands/ExpandVariableCommand.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management.Automation;
+using System.Management.Automation.Language;
+using System.Text;
+
+namespace PoshCode.Pansies.Commands
+{
+ [Cmdlet("Expand", "Variable", DefaultParameterSetName = ParameterSetContent)]
+ [OutputType(typeof(string))]
+ public sealed class ExpandVariableCommand : PSCmdlet
+ {
+ private const string ParameterSetPath = "Path";
+ private const string ParameterSetContent = "Content";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, ParameterSetName = ParameterSetPath)]
+ [Alias("PSPath")]
+ public string Path { get; set; } = string.Empty;
+
+ [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSetContent)]
+ public string Content { get; set; } = string.Empty;
+
+ [Parameter]
+ public SwitchParameter Unescaped { get; set; }
+
+ ///
+ /// Specifies which drives to use for variable expansion. Each drive represents a different source or type of variable:
+ ///
+ /// - bgBackground color variables.
+ /// - emojiEmoji variables.
+ /// - escEscape sequence variables.
+ /// - extraExtra or custom variables.
+ /// - fgForeground color variables.
+ /// - nfNerd Font icon variables.
+ /// - variableStandard PowerShell variables.
+ ///
+ /// By specifying one or more drives, you control which sources are used during variable expansion.
+ ///
+ [Parameter]
+ public string[] Drive { get; set; } = new[] { "bg", "emoji", "esc", "extra", "fg", "nf", "variable" };
+
+ [Parameter(ParameterSetName = ParameterSetPath)]
+ public SwitchParameter InPlace { get; set; }
+
+ [Parameter(ParameterSetName = ParameterSetPath)]
+ public SwitchParameter Passthru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (ParameterSetName == ParameterSetContent)
+ {
+ var result = ExpandVariable(Content ?? string.Empty, "Content");
+
+ if (result != null)
+ {
+ WriteObject(result);
+ }
+
+ return;
+ }
+
+ var resolvedPaths = GetResolvedProviderPathFromPSPath(Path, out var providerInfo);
+
+ foreach (var resolvedPath in resolvedPaths)
+ {
+ var fullName = BuildProviderQualifiedPath(providerInfo, resolvedPath);
+ var value = GetVariableValue(fullName);
+ var replacement = ExpandVariable(value?.ToString() ?? string.Empty, fullName);
+
+ if (replacement is null)
+ {
+ continue;
+ }
+
+ var isVariableProvider = string.Equals(providerInfo.Name, "Variable", StringComparison.OrdinalIgnoreCase);
+ var passthruPath = fullName;
+
+ if (isVariableProvider && fullName.IndexOf(':') < 0)
+ {
+ passthruPath = providerInfo.Name + ":" + fullName;
+ }
+
+ if (InPlace)
+ {
+ if (isVariableProvider)
+ {
+ var variableName = passthruPath;
+
+ var providerSeparator = variableName.IndexOf(':');
+
+ if (providerSeparator >= 0)
+ {
+ variableName = variableName.Substring(providerSeparator + 1);
+ }
+
+ var variable = SessionState.PSVariable.Get(variableName);
+
+ if (variable != null)
+ {
+ variable.Value = replacement;
+ SessionState.PSVariable.Set(variable);
+ }
+ else
+ {
+ SessionState.PSVariable.Set(variableName, replacement);
+ }
+ }
+ else
+ {
+ SessionState.InvokeProvider.Item.Set(passthruPath, replacement);
+ }
+
+ if (Passthru)
+ {
+ if (isVariableProvider)
+ {
+ WriteObject(replacement);
+ }
+ else
+ {
+ WriteObject(SessionState.InvokeProvider.Item.Get(passthruPath), true);
+ }
+ }
+ }
+ else
+ {
+ WriteObject(replacement);
+ }
+ }
+ }
+
+ private static string BuildProviderQualifiedPath(ProviderInfo providerInfo, string path)
+ {
+ if (providerInfo.Name == "Variable" || providerInfo.Name == "FileSystem" || path.Contains(':'))
+ {
+ return path;
+ }
+
+ return $"{providerInfo.Name}:{path}";
+ }
+
+ private string ExpandVariable(string code, string source)
+ {
+ var replacements = new List();
+ var ast = Parser.ParseInput(code, source, out _, out var errors);
+
+ if (errors.Length > 0)
+ {
+ WriteError(new ErrorRecord(
+ new ParseException($"{errors.Length} Parse Errors in {source}, cannot expand."),
+ "ParseErrors",
+ ErrorCategory.InvalidOperation,
+ source));
+
+ return null;
+ }
+
+ var drives = new HashSet(Drive, StringComparer.OrdinalIgnoreCase);
+
+ var variables = ast
+ .FindAll(node => node is VariableExpressionAst, searchNestedScriptBlocks: true)
+ .OfType()
+ .Where(variable => ShouldExpand(variable, drives));
+
+ foreach (var variable in variables)
+ {
+ try
+ {
+ var replacement = GetVariableValue(variable.VariablePath.UserPath)?.ToString() ?? string.Empty;
+
+ if (!Unescaped)
+ {
+ replacement = replacement.ToPsEscapedString();
+ }
+
+ if (variable.Parent is ExpandableStringExpressionAst)
+ {
+ replacements.Add(new TextReplacement(replacement, variable.Extent));
+ }
+ else
+ {
+ replacements.Add(new TextReplacement("\"" + replacement + "\"", variable.Extent));
+ }
+ }
+ catch
+ {
+ WriteWarning($"VariableNotFound: '{variable.VariablePath.UserPath}' at {source}:{variable.Extent.StartLineNumber}:{variable.Extent.StartColumnNumber}");
+ }
+ }
+
+ var builder = new StringBuilder(code);
+
+ foreach (var replacement in replacements.OrderByDescending(r => r.StartOffset))
+ {
+ builder.Remove(replacement.StartOffset, replacement.Length)
+ .Insert(replacement.StartOffset, replacement.Text);
+ }
+
+ return builder.ToString();
+ }
+
+ private bool ShouldExpand(VariableExpressionAst variable, HashSet drives)
+ {
+ if (!variable.VariablePath.IsDriveQualified)
+ {
+ return drives.Contains("variable");
+ }
+
+ return drives.Contains(variable.VariablePath.DriveName ?? "variable");
+ }
+
+ private sealed class TextReplacement
+ {
+ public TextReplacement(string text, IScriptExtent extent)
+ {
+ Text = text;
+ StartOffset = extent.StartOffset;
+ EndOffset = extent.EndOffset;
+ }
+
+ public string Text { get; }
+
+ public int StartOffset { get; }
+
+ public int EndOffset { get; }
+
+ public int Length => EndOffset - StartOffset;
+ }
+
+ private sealed class ParseException : Exception
+ {
+ public ParseException(string message)
+ : base(message)
+ {
+ }
+ }
+ }
+}
diff --git a/Source/Assembly/Commands/RestoreCursorPosition.cs b/Source/Assembly/Commands/RestoreCursorPosition.cs
new file mode 100644
index 0000000..83b46b7
--- /dev/null
+++ b/Source/Assembly/Commands/RestoreCursorPosition.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Management.Automation;
+
+namespace PoshCode.Pansies.Commands
+{
+ [Cmdlet("Restore", "CursorPosition")]
+ public sealed class RestoreCursorPositionCommand : Cmdlet
+ {
+ protected override void EndProcessing()
+ {
+ Console.Write("\u001b8");
+ }
+ }
+}
diff --git a/Source/Assembly/Commands/SaveCursorPosition.cs b/Source/Assembly/Commands/SaveCursorPosition.cs
new file mode 100644
index 0000000..3c18821
--- /dev/null
+++ b/Source/Assembly/Commands/SaveCursorPosition.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Management.Automation;
+
+namespace PoshCode.Pansies.Commands
+{
+ [Cmdlet("Save", "CursorPosition")]
+ public sealed class SaveCursorPositionCommand : Cmdlet
+ {
+ protected override void EndProcessing()
+ {
+ Console.Write("\u001b[s");
+ }
+ }
+}
diff --git a/Source/Assembly/Commands/SetCursorPosition.cs b/Source/Assembly/Commands/SetCursorPosition.cs
new file mode 100644
index 0000000..1f8ea91
--- /dev/null
+++ b/Source/Assembly/Commands/SetCursorPosition.cs
@@ -0,0 +1,24 @@
+using System.Management.Automation;
+
+namespace PoshCode.Pansies.Commands
+{
+ [Cmdlet("Set", "CursorPosition")]
+ [OutputType(typeof(string))]
+ public sealed class SetCursorPositionCommand : Cmdlet
+ {
+ [Parameter(Position = 0)]
+ public int? Line { get; set; }
+
+ [Parameter(Position = 1)]
+ public int? Column { get; set; }
+
+ [Parameter]
+ public SwitchParameter Absolute { get; set; }
+
+ protected override void EndProcessing()
+ {
+ var position = new Position(Line, Column, Absolute);
+ WriteObject(position.ToString());
+ }
+ }
+}
diff --git a/Source/Assembly/IPsMetadataSerializable.cs b/Source/Assembly/IPsMetadataSerializable.cs
new file mode 100644
index 0000000..432f2e3
--- /dev/null
+++ b/Source/Assembly/IPsMetadataSerializable.cs
@@ -0,0 +1,11 @@
+namespace PoshCode
+{
+ // Two rules for IPsMetadataSerializable implementations:
+ // 1. Provide a public parameterless constructor
+ // 2. Ensure FromPsMetadata can interpret the value produced by ToPsMetadata
+ public interface IPsMetadataSerializable
+ {
+ string ToPsMetadata();
+ void FromPsMetadata(string metadata);
+ }
+}
diff --git a/Source/Assembly/Position.cs b/Source/Assembly/Position.cs
new file mode 100644
index 0000000..5c4d1ba
--- /dev/null
+++ b/Source/Assembly/Position.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Globalization;
+
+namespace PoshCode.Pansies
+{
+ public class Position : IPsMetadataSerializable
+ {
+ public bool Absolute { get; set; }
+
+ public int? Line { get; set; }
+
+ public int? Column { get; set; }
+
+ public Position()
+ {
+ }
+
+ public Position(int? line, int? column, bool absolute = false)
+ {
+ Line = line;
+ Column = column;
+ Absolute = absolute;
+ }
+
+ public Position(string metadata)
+ {
+ FromPsMetadata(metadata);
+ }
+
+ public override string ToString()
+ {
+ if (Line is null && Column is null)
+ {
+ return string.Empty;
+ }
+
+ if (Absolute)
+ {
+ if (Line is null)
+ {
+ return "\u001b" + $"[{Column}G";
+ }
+
+ if (Column is null)
+ {
+ return "\u001b" + $"[{Line}d";
+ }
+
+ return "\u001b" + $"[{Line};{Column}H";
+ }
+
+ if (Column != null)
+ {
+ if (Column > 0)
+ {
+ return "\u001b" + $"[{Column}C";
+ }
+
+ if (Column < 0)
+ {
+ return "\u001b" + $"[{-Column}D";
+ }
+ }
+
+ if (Line != null)
+ {
+ if (Line > 0)
+ {
+ return "\u001b" + $"[{Line}B";
+ }
+
+ if (Line < 0)
+ {
+ return "\u001b" + $"[{-Line}A";
+ }
+ }
+
+ return string.Empty;
+ }
+
+ public string ToPsMetadata()
+ {
+ return $"{Line};{Column}{(Absolute ? ";1" : string.Empty)}";
+ }
+
+ public void FromPsMetadata(string metadata)
+ {
+ if (string.IsNullOrEmpty(metadata))
+ {
+ throw new ArgumentNullException(nameof(metadata));
+ }
+
+ var data = metadata.Split(';');
+
+ if (data.Length >= 3)
+ {
+ Absolute = data[2] == "1";
+ }
+ else
+ {
+ Absolute = false;
+ }
+
+ if (data.Length >= 1 && data[0].Length > 0)
+ {
+ Line = int.Parse(data[0], NumberFormatInfo.InvariantInfo);
+ }
+ else
+ {
+ Line = null;
+ }
+
+ if (data.Length >= 2 && data[1].Length > 0)
+ {
+ Column = int.Parse(data[1], NumberFormatInfo.InvariantInfo);
+ }
+ else
+ {
+ Column = null;
+ }
+ }
+ }
+}
diff --git a/Source/Assembly/RgbColor.cs b/Source/Assembly/RgbColor.cs
index 0cea479..a821ca1 100644
--- a/Source/Assembly/RgbColor.cs
+++ b/Source/Assembly/RgbColor.cs
@@ -16,12 +16,13 @@ public partial class RgbColor : Rgb, IEquatable
private static ConsolePalette _consolePalette;
private static XTermPalette _xTermPalette;
private static X11Palette _x11Palette;
+ private static readonly ColorMode DefaultColorMode = GetRecommendedColorMode();
public static ConsolePalette ConsolePalette
{
get
{
- if(null == _consolePalette)
+ if (null == _consolePalette)
{
_consolePalette = new ConsolePalette();
}
@@ -71,10 +72,7 @@ public static X11Palette X11Palette
#region private ctors (to be removed?)
private RgbColor(byte xTerm256Index)
{
- // TODO: Need a SetXTermColor to set the actual RGB values
- _mode = ColorMode.XTerm256;
- index = xTerm256Index;
- Initialize(XTermPalette[index]);
+ SetXTermColor(xTerm256Index);
}
private RgbColor(int rgb)
@@ -192,8 +190,7 @@ public void FromPsMetadata(string metadata)
try
{
index = ParseXtermIndex(color.Substring(2));
- _mode = ColorMode.XTerm256;
- RGB = XTermPalette[index].RGB;
+ SetXTermColor(index);
return;
}
catch (Exception ex)
@@ -208,8 +205,7 @@ public void FromPsMetadata(string metadata)
try
{
index = ParseXtermIndex(color);
- _mode = ColorMode.XTerm256;
- RGB = XTermPalette[index].RGB;
+ SetXTermColor(index);
return;
}
catch { }
@@ -287,10 +283,14 @@ private static int ParseXtermIndex(string xTermIndex)
public static RgbColor FromXTermIndex(string xTermIndex)
{
- // handle #rrggbb hex strings like CSS colors ....
+ if (string.IsNullOrEmpty(xTermIndex))
+ {
+ throw new ArgumentException("xTermIndex cannot be null or empty", nameof(xTermIndex));
+ }
+
if (xTermIndex[0] == 'x' || xTermIndex[0] == 'X')
{
- if (xTermIndex[1] == 't' || xTermIndex[1] == 'T')
+ if (s.StartsWith("xt", StringComparison.OrdinalIgnoreCase))
{
xTermIndex = xTermIndex.Substring(2);
}
@@ -299,12 +299,9 @@ public static RgbColor FromXTermIndex(string xTermIndex)
xTermIndex = xTermIndex.Substring(1);
}
}
+
var result = ParseXtermIndex(xTermIndex);
- return new RgbColor
- {
- _mode = ColorMode.XTerm256,
- index = result
- };
+ return CreateFromXTermIndex(result);
}
@@ -386,6 +383,107 @@ public static RgbColor ConvertFrom(object inputData)
return new RgbColor();
}
+ private void SetXTermColor(int xTermIndex)
+ {
+ if (xTermIndex < 0 || xTermIndex > 255)
+ {
+ throw new ArgumentOutOfRangeException(nameof(xTermIndex), "xTerm index must be between 0 and 255");
+ }
+
+ _mode = ColorMode.XTerm256;
+ index = xTermIndex;
+ Initialize(XTermPalette[index]);
+ }
+
+ private static RgbColor CreateFromXTermIndex(int xTermIndex)
+ {
+ var color = new RgbColor();
+ color.SetXTermColor(xTermIndex);
+ return color;
+ }
+
+ public static ColorMode GetRecommendedColorMode(IDictionary environment = null, OperatingSystem os = null)
+ {
+ if (environment == null)
+ {
+ environment = GetEnvironmentVariables();
+ }
+
+ if (os == null)
+ {
+ os = Environment.OSVersion;
+ }
+
+ var isWindows = os.Platform == PlatformID.Win32NT;
+
+ if (isWindows)
+ {
+ if (os.Version.Major >= 10)
+ {
+ return ColorMode.Rgb24Bit;
+ }
+
+ return ColorMode.ConsoleColor;
+ }
+
+ var colorTerm = GetEnvironmentValue(environment, "COLORTERM");
+ if (!string.IsNullOrEmpty(colorTerm))
+ {
+ if (colorTerm.IndexOf("truecolor", StringComparison.OrdinalIgnoreCase) >= 0 ||
+ colorTerm.IndexOf("24bit", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ return ColorMode.Rgb24Bit;
+ }
+ }
+
+ var term = GetEnvironmentValue(environment, "TERM");
+ if (!string.IsNullOrEmpty(term))
+ {
+ if (term.IndexOf("truecolor", StringComparison.OrdinalIgnoreCase) >= 0 ||
+ term.IndexOf("direct", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ return ColorMode.Rgb24Bit;
+ }
+
+ if (term.IndexOf("256color", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ return ColorMode.XTerm256;
+ }
+
+ if (term.IndexOf("color", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ return ColorMode.ConsoleColor;
+ }
+ }
+
+ return ColorMode.ConsoleColor;
+ }
+
+ private static IDictionary GetEnvironmentVariables()
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
+ {
+ if (entry.Key is string key)
+ {
+ result[key] = entry.Value?.ToString();
+ }
+ }
+
+ return result;
+ }
+
+ private static string GetEnvironmentValue(IDictionary environment, string key)
+ {
+ if (environment != null && environment.TryGetValue(key, out var value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+
private void SetConsoleColor(ConsoleColor color)
{
_mode = ColorMode.ConsoleColor;
@@ -403,8 +501,7 @@ private void SetX11Color(X11ColorName color)
///
/// The default ColorMode for the console
///
- // TODO: Detect from platform. Should default to RGB on Windows 10, use TERM on others
- public static ColorMode ColorMode { get; set; } = ColorMode.Automatic;
+ public static ColorMode ColorMode { get; set; } = DefaultColorMode;
public ColorMode Mode { get => _mode; set => _mode = value; }
@@ -519,7 +616,7 @@ public string ToVtEscapeSequence(bool background = false, ColorMode? mode = null
{
mode = RgbColor.ColorMode;
}
- else if(_mode != ColorMode.Automatic)
+ else if (_mode != ColorMode.Automatic)
{
mode = _mode;
}
@@ -532,63 +629,63 @@ public string ToVtEscapeSequence(bool background = false, ColorMode? mode = null
switch (mode.Value)
{
case ColorMode.ConsoleColor:
- {
- switch (this.ConsoleColor)
{
- case ConsoleColor.Black:
- return background ? "\u001B[40m" : "\u001B[30m";
- case ConsoleColor.Blue:
- return background ? "\u001B[104m" : "\u001B[94m";
- case ConsoleColor.Cyan:
- return background ? "\u001B[106m" : "\u001B[96m";
- case ConsoleColor.DarkBlue:
- return background ? "\u001B[44m" : "\u001B[34m";
- case ConsoleColor.DarkCyan:
- return background ? "\u001B[46m" : "\u001B[36m";
- case ConsoleColor.DarkGray:
- return background ? "\u001B[100m" : "\u001B[90m";
- case ConsoleColor.DarkGreen:
- return background ? "\u001B[42m" : "\u001B[32m";
- case ConsoleColor.DarkMagenta:
- return background ? "\u001B[45m" : "\u001B[35m";
- case ConsoleColor.DarkRed:
- return background ? "\u001B[41m" : "\u001B[31m";
- case ConsoleColor.DarkYellow:
- return background ? "\u001B[43m" : "\u001B[33m";
- case ConsoleColor.Gray:
- return background ? "\u001B[47m" : "\u001B[37m";
- case ConsoleColor.Green:
- return background ? "\u001B[102m" : "\u001B[92m";
- case ConsoleColor.Magenta:
- return background ? "\u001B[105m" : "\u001B[95m";
- case ConsoleColor.Red:
- return background ? "\u001B[101m" : "\u001B[91m";
- case ConsoleColor.White:
- return background ? "\u001B[107m" : "\u001B[97m";
- case ConsoleColor.Yellow:
- return background ? "\u001B[103m" : "\u001B[93m";
- default:
- return background ? "\u001B[49m" : "\u001B[39m";
+ switch (this.ConsoleColor)
+ {
+ case ConsoleColor.Black:
+ return background ? "\u001B[40m" : "\u001B[30m";
+ case ConsoleColor.Blue:
+ return background ? "\u001B[104m" : "\u001B[94m";
+ case ConsoleColor.Cyan:
+ return background ? "\u001B[106m" : "\u001B[96m";
+ case ConsoleColor.DarkBlue:
+ return background ? "\u001B[44m" : "\u001B[34m";
+ case ConsoleColor.DarkCyan:
+ return background ? "\u001B[46m" : "\u001B[36m";
+ case ConsoleColor.DarkGray:
+ return background ? "\u001B[100m" : "\u001B[90m";
+ case ConsoleColor.DarkGreen:
+ return background ? "\u001B[42m" : "\u001B[32m";
+ case ConsoleColor.DarkMagenta:
+ return background ? "\u001B[45m" : "\u001B[35m";
+ case ConsoleColor.DarkRed:
+ return background ? "\u001B[41m" : "\u001B[31m";
+ case ConsoleColor.DarkYellow:
+ return background ? "\u001B[43m" : "\u001B[33m";
+ case ConsoleColor.Gray:
+ return background ? "\u001B[47m" : "\u001B[37m";
+ case ConsoleColor.Green:
+ return background ? "\u001B[102m" : "\u001B[92m";
+ case ConsoleColor.Magenta:
+ return background ? "\u001B[105m" : "\u001B[95m";
+ case ConsoleColor.Red:
+ return background ? "\u001B[101m" : "\u001B[91m";
+ case ConsoleColor.White:
+ return background ? "\u001B[107m" : "\u001B[97m";
+ case ConsoleColor.Yellow:
+ return background ? "\u001B[103m" : "\u001B[93m";
+ default:
+ return background ? "\u001B[49m" : "\u001B[39m";
+ }
}
- }
case ColorMode.XTerm256:
- {
- var format = string.Format(background ? "\u001B[48;5;{0}m" : "\u001B[38;5;{0}m", XTerm256Index);
- return format;
- }
+ {
+ var format = string.Format(background ? "\u001B[48;5;{0}m" : "\u001B[38;5;{0}m", XTerm256Index);
+ return format;
+ }
case ColorMode.Rgb24Bit:
default:
- {
- if (RGB < 0)
{
- return string.Empty;
- }
+ if (RGB < 0)
+ {
+ return string.Empty;
+ }
- var format = string.Format(background ? "\u001B[48;2;{0:n0};{1:n0};{2:n0}m" : "\u001B[38;2;{0:n0};{1:n0};{2:n0}m", R, G, B);
- return format;
- }
+ var format = string.Format(background ? "\u001B[48;2;{0:n0};{1:n0};{2:n0}m" : "\u001B[38;2;{0:n0};{1:n0};{2:n0}m", R, G, B);
+ return format;
+ }
}
}
@@ -669,3 +766,4 @@ public static string VtEscapeSequence(int color, bool background)
}
}
}
+
diff --git a/Source/Assembly/StringExtensions.cs b/Source/Assembly/StringExtensions.cs
new file mode 100644
index 0000000..ce19ac3
--- /dev/null
+++ b/Source/Assembly/StringExtensions.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace PoshCode.Pansies
+{
+ public static class StringExtensions
+ {
+ public static IEnumerable ToUtf32(this string value)
+ {
+ for (var i = 0; i < value.Length; i++)
+ {
+ if (char.IsHighSurrogate(value[i]))
+ {
+ if (value.Length <= i + 1 || !char.IsLowSurrogate(value[i + 1]))
+ {
+ throw new InvalidDataException("High surrogate must be followed by a low surrogate.");
+ }
+
+ yield return char.ConvertToUtf32(value[i], value[++i]);
+ }
+ else
+ {
+ yield return value[i];
+ }
+ }
+ }
+
+ public static string ToPsEscapedString(this string value)
+ {
+ var builder = new StringBuilder();
+
+ for (var i = 0; i < value.Length; i++)
+ {
+ if (char.IsHighSurrogate(value[i]))
+ {
+ if (value.Length <= i + 1 || !char.IsLowSurrogate(value[i + 1]))
+ {
+ throw new InvalidDataException("High surrogate must be followed by a low surrogate.");
+ }
+
+ builder.AppendFormat("`u{{{0:x6}}}", char.ConvertToUtf32(value[i], value[++i]));
+ continue;
+ }
+
+ var current = value[i];
+
+ switch (current)
+ {
+ case '\u001b':
+ builder.Append("`e");
+ break;
+ case '`':
+ builder.Append("``");
+ break;
+ case '$':
+ builder.Append("`$");
+ break;
+ case '"':
+ builder.Append("`\"");
+ break;
+ default:
+ if (current < 32 || current > 126)
+ {
+ builder.AppendFormat("`u{{{0:x4}}}", (int)current);
+ }
+ else
+ {
+ builder.Append(current);
+ }
+ break;
+ }
+ }
+
+ return builder.ToString();
+ }
+ }
+}
diff --git a/Source/Pansies.deps.json b/Source/Pansies.deps.json
new file mode 100644
index 0000000..12a1502
--- /dev/null
+++ b/Source/Pansies.deps.json
@@ -0,0 +1,93 @@
+{
+ "runtimeTarget": {
+ "name": ".NETStandard,Version=v2.0/",
+ "signature": ""
+ },
+ "compilationOptions": {},
+ "targets": {
+ ".NETStandard,Version=v2.0": {},
+ ".NETStandard,Version=v2.0/": {
+ "Pansies/1.0.0": {
+ "dependencies": {
+ "CodeOwls.PowerShell.Paths": "1.0.0",
+ "CodeOwls.PowerShell.Provider": "1.0.0"
+ },
+ "runtime": {
+ "Pansies.dll": {}
+ }
+ },
+ "System.Security.AccessControl/5.0.0": {
+ "dependencies": {
+ "System.Security.Principal.Windows": "5.0.0"
+ },
+ "runtime": {
+ "lib/netstandard2.0/System.Security.AccessControl.dll": {
+ "assemblyVersion": "5.0.0.0",
+ "fileVersion": "5.0.20.51904"
+ }
+ }
+ },
+ "System.Security.Principal.Windows/5.0.0": {
+ "runtime": {
+ "lib/netstandard2.0/System.Security.Principal.Windows.dll": {
+ "assemblyVersion": "5.0.0.0",
+ "fileVersion": "5.0.20.51904"
+ }
+ }
+ },
+ "CodeOwls.PowerShell.Paths/1.0.0": {
+ "dependencies": {
+ "System.Security.AccessControl": "5.0.0"
+ },
+ "runtime": {
+ "CodeOwls.PowerShell.Paths.dll": {
+ "assemblyVersion": "1.0.0.0",
+ "fileVersion": "1.0.0.0"
+ }
+ }
+ },
+ "CodeOwls.PowerShell.Provider/1.0.0": {
+ "dependencies": {
+ "CodeOwls.PowerShell.Paths": "1.0.0"
+ },
+ "runtime": {
+ "CodeOwls.PowerShell.Provider.dll": {
+ "assemblyVersion": "1.0.0.0",
+ "fileVersion": "1.0.0.0"
+ }
+ }
+ }
+ }
+ },
+ "libraries": {
+ "Pansies/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "System.Security.AccessControl/5.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
+ "path": "system.security.accesscontrol/5.0.0",
+ "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
+ },
+ "System.Security.Principal.Windows/5.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
+ "path": "system.security.principal.windows/5.0.0",
+ "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
+ },
+ "CodeOwls.PowerShell.Paths/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "CodeOwls.PowerShell.Provider/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/Pansies.psd1 b/Source/Pansies.psd1
index ce5e126..6d30d54 100644
--- a/Source/Pansies.psd1
+++ b/Source/Pansies.psd1
@@ -1,82 +1,93 @@
@{
-# Script module or binary module file associated with this manifest.
-RootModule = 'Pansies.psm1'
+ # Script module or binary module file associated with this manifest.
+ RootModule = 'Pansies.psm1'
-# Version number of this module.
-ModuleVersion = '2.11.0'
+ # Version number of this module.
+ ModuleVersion = '2.11.0'
-# Supported PSEditions
-# CompatiblePSEditions = @()
+ # Supported PSEditions
+ # CompatiblePSEditions = @()
-# ID used to uniquely identify this module
-GUID = '6c376de1-1baf-4d52-9666-d46f6933bc16'
+ # ID used to uniquely identify this module
+ GUID = '6c376de1-1baf-4d52-9666-d46f6933bc16'
-# Author of this module
-Author = 'Joel Bennett'
+ # Author of this module
+ Author = 'Joel Bennett'
-# Company or vendor of this module
-CompanyName = 'HuddledMasses.org'
+ # Company or vendor of this module
+ CompanyName = 'HuddledMasses.org'
-# Copyright statement for this module
-Copyright = '(c) 2017 Joel Bennett. All rights reserved.'
+ # Copyright statement for this module
+ Copyright = '(c) 2017 Joel Bennett. All rights reserved.'
-# Description of the functionality provided by this module
-Description = 'A PowerShell module for handling color and cursor positioning via ANSI escape sequences'
+ # Description of the functionality provided by this module
+ Description = 'A PowerShell module for handling color and cursor positioning via ANSI escape sequences'
-# Assemblies that must be loaded prior to importing this module
-# RequiredAssemblies =
+ # Assemblies that must be loaded prior to importing this module
+ # RequiredAssemblies =
-# Script files (.ps1) that are run in the caller's environment prior to importing this module.
-# ScriptsToProcess = @()
+ # Script files (.ps1) that are run in the caller's environment prior to importing this module.
+ # ScriptsToProcess = @()
-# Type files (.ps1xml) to be loaded when importing this module
-# TypesToProcess = @()
+ # Type files (.ps1xml) to be loaded when importing this module
+ # TypesToProcess = @()
-# Format files (.ps1xml) to be loaded when importing this module
-FormatsToProcess = @("Pansies.format.ps1xml")
+ # Format files (.ps1xml) to be loaded when importing this module
+ FormatsToProcess = @('Pansies.format.ps1xml')
-# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
-NestedModules = @( "lib/Pansies.dll" )
+ # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+ NestedModules = @( 'lib/Pansies.dll' )
-# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
-FunctionsToExport = @()
+ # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+ FunctionsToExport = @()
-# A default Prefix for for Cmdlets to export
-# DefaultCommandPrefix = "Pansies"
+ # A default Prefix for for Cmdlets to export
+ # DefaultCommandPrefix = "Pansies"
-# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
-CmdletsToExport = @('New-Text', 'New-Hyperlink', 'Write-Host', 'Get-Gradient', 'Get-Complement', 'Get-ColorWheel')
+ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+ CmdletsToExport = @(
+ 'New-Text',
+ 'New-Hyperlink',
+ 'Write-Host',
+ 'Get-Gradient',
+ 'Get-Complement',
+ 'Get-ColorWheel',
+ 'Set-CursorPosition',
+ 'Save-CursorPosition',
+ 'Restore-CursorPosition',
+ 'Expand-Variable'
+ )
-# Variables to export from this module
-VariablesToExport = 'RgbColorCompleter'
+ # Variables to export from this module
+ VariablesToExport = 'RgbColorCompleter'
-# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
-AliasesToExport = 'Text', 'Url'
+ # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+ AliasesToExport = 'Text', 'Url'
-# List of all files packaged with this module
-# FileList = @()
+ # List of all files packaged with this module
+ # FileList = @()
-# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
-PrivateData = @{
- PSData = @{
- # ModuleBuilder will set the pre-release value
- Prerelease = "dev"
+ # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+ PrivateData = @{
+ PSData = @{
+ # ModuleBuilder will set the pre-release value
+ Prerelease = 'dev'
- # Tags applied to this module. These help with module discovery in online galleries.
- Tags = @("ANSI", "EscapeSequences", "VirtualTerminal", "Color")
+ # Tags applied to this module. These help with module discovery in online galleries.
+ Tags = @('ANSI', 'EscapeSequences', 'VirtualTerminal', 'Color')
- # A URL to the license for this module.
- LicenseUri = 'https://github.com/PoshCode/Pansies/blob/master/LICENSE'
+ # A URL to the license for this module.
+ LicenseUri = 'https://github.com/PoshCode/Pansies/blob/master/LICENSE'
- # A URL to the main website for this project.
- ProjectUri = 'https://github.com/PoshCode/Pansies'
+ # A URL to the main website for this project.
+ ProjectUri = 'https://github.com/PoshCode/Pansies'
- # A URL to an icon representing this module.
- IconUri = 'https://github.com/PoshCode/Pansies/blob/resources/Pansies_32.gif?raw=true'
+ # A URL to an icon representing this module.
+ IconUri = 'https://github.com/PoshCode/Pansies/blob/resources/Pansies_32.gif?raw=true'
- # ReleaseNotes of this module
- ReleaseNotes = '
+ # ReleaseNotes of this module
+ ReleaseNotes = '
2.11.0 Added a ColorCompleterAttribute (on PowerShell 7 only)
On Windows PowerShell, you can still use [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])]
Additionally, on module load, will register the ArgumentCompleter for all commands with RGBColor parameters
@@ -84,16 +95,16 @@ PrivateData = @{
Updated the NerdFont characters and code points to deal with the migration of the MDI characters in 2.3.0+
'
- } # End of PSData hashtable
+ } # End of PSData hashtable
-} # End of PrivateData hashtable
+ } # End of PrivateData hashtable
-# HelpInfo URI of this module
-# HelpInfoURI = ''
+ # HelpInfo URI of this module
+ # HelpInfoURI = ''
# Minimum version of the Windows PowerShell engine required by this module
- PowerShellVersion = '5.1'
- CompatiblePSEditions = @('Core','Desktop')
+ PowerShellVersion = '5.1'
+ CompatiblePSEditions = @('Core', 'Desktop')
}
diff --git a/Tests/Commands.Tests.ps1 b/Tests/Commands.Tests.ps1
new file mode 100644
index 0000000..783e958
--- /dev/null
+++ b/Tests/Commands.Tests.ps1
@@ -0,0 +1,283 @@
+<#
+ Pester tests validating new cursor commands, position helper, and Expand-Variable functionality.
+#>
+
+BeforeAll {
+ $scriptPath = if ($PSCommandPath) {
+ $PSCommandPath
+ } elseif ($MyInvocation.MyCommand.Path) {
+ $MyInvocation.MyCommand.Path
+ } else {
+ $null
+ }
+ if (-not $scriptPath) {
+ throw 'Unable to determine script path for test execution.'
+ }
+
+ $testsFolder = Split-Path -Parent $scriptPath
+ $moduleRoot = Split-Path -Parent $testsFolder
+ $script:PublishPath = Join-Path $moduleRoot ('.pester-publish-' + [Guid]::NewGuid().ToString('N'))
+
+ New-Item -ItemType Directory -Path $script:PublishPath | Out-Null
+
+ Push-Location $moduleRoot
+ try {
+ $publishResult = dotnet publish -c Release -o $script:PublishPath 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ throw "dotnet publish failed:`n$publishResult"
+ }
+ } finally {
+ Pop-Location
+ }
+
+ $path = Join-Path $script:PublishPath 'Pansies.dll'
+ Import-Module $path -Force
+ Set-Variable -Name ModuleAssemblyPath -Scope Script -Value $path
+}
+
+AfterAll {
+ Remove-Module Pansies -Force -ErrorAction SilentlyContinue
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+
+ if ($script:PublishPath -and (Test-Path -LiteralPath $script:PublishPath)) {
+ Remove-Item -LiteralPath $script:PublishPath -Recurse -Force -ErrorAction SilentlyContinue
+ }
+}
+
+Describe 'Position helper' {
+ It 'formats absolute positions with row and column' {
+ $position = [PoshCode.Pansies.Position]::new(3, 4, $true)
+ $position.ToString() | Should -Be (([char]0x1b) + '[3;4H')
+ }
+
+ It 'formats absolute positions with only column' {
+ $position = [PoshCode.Pansies.Position]::new($null, 12, $true)
+ $position.ToString() | Should -Be (([char]0x1b) + '[12G')
+ }
+
+ It 'formats relative movement' {
+ $position = [PoshCode.Pansies.Position]::new(-2, 5, $false)
+ $position.ToString() | Should -Be (([char]0x1b) + '[5C') # column takes precedence when positive
+ }
+
+ It 'formats relative movement vertically' {
+ $position = [PoshCode.Pansies.Position]::new(-3, $null, $false)
+ $position.ToString() | Should -Be (([char]0x1b) + '[3A')
+ }
+
+ It 'round trips metadata' {
+ $original = [PoshCode.Pansies.Position]::new(7, 9, $true)
+ $metadata = $original.ToPsMetadata()
+ $metadata | Should -Be '7;9;1'
+
+ $roundTrip = [PoshCode.Pansies.Position]::new($metadata)
+ $roundTrip.Absolute | Should -BeTrue
+ $roundTrip.Line | Should -Be 7
+ $roundTrip.Column | Should -Be 9
+ }
+
+ It 'throws when metadata is null' {
+ $position = [PoshCode.Pansies.Position]::new()
+ try {
+ $position.FromPsMetadata($null)
+ $true | Should -BeFalse -Because 'Expected a MethodInvocationException wrapping ArgumentNullException'
+ } catch [System.Management.Automation.MethodInvocationException] {
+ $_.Exception.InnerException | Should -BeOfType ([System.ArgumentNullException])
+ }
+ }
+}
+
+Describe 'Set-CursorPosition cmdlet' {
+ It 'emits absolute escape sequence' {
+ $result = Set-CursorPosition -Line 2 -Column 5 -Absolute
+ $result | Should -Be (([char]0x1b) + '[2;5H')
+ }
+
+ It 'emits relative escape sequence when Line is negative' {
+ $result = Set-CursorPosition -Line -4
+ $result | Should -Be (([char]0x1b) + '[4A')
+ }
+
+ It 'emits relative escape sequence when Column is positive' {
+ $result = Set-CursorPosition -Column 6
+ $result | Should -Be (([char]0x1b) + '[6C')
+ }
+}
+
+Describe 'Save/Restore-CursorPosition cmdlets' {
+ It 'writes ESC[s to the console' {
+ $writer = New-Object System.IO.StringWriter
+ $original = [Console]::Out
+ try {
+ [Console]::SetOut($writer)
+ Save-CursorPosition
+ } finally {
+ [Console]::SetOut($original)
+ }
+
+ $writer.ToString() | Should -Be (([char]0x1b) + '[s')
+ }
+
+ It 'writes ESC8 to the console' {
+ $writer = New-Object System.IO.StringWriter
+ $original = [Console]::Out
+ try {
+ [Console]::SetOut($writer)
+ Restore-CursorPosition
+ } finally {
+ [Console]::SetOut($original)
+ }
+
+ $writer.ToString() | Should -Be (([char]0x1b) + '8')
+ }
+}
+
+Describe 'Expand-Variable cmdlet' {
+ BeforeEach {
+ Set-Variable -Name foo -Value 'rainbow' -Scope Script
+ }
+
+ AfterEach {
+ Remove-Variable -Name foo -Scope Script -ErrorAction SilentlyContinue
+ Remove-Variable -Name bar -Scope Script -ErrorAction SilentlyContinue
+ }
+
+ It 'replaces variables outside expandable strings with quoted value' {
+ $result = Expand-Variable -Content 'Write-Host $variable:foo' -Drive variable
+ $result | Should -Be 'Write-Host "rainbow"'
+ }
+
+ It 'replaces variables within expandable strings without extra quotes' {
+ $result = Expand-Variable -Content '"Color: $variable:foo"' -Drive variable
+ $result | Should -Be '"Color: rainbow"'
+ }
+
+ It 'escapes characters by default' {
+ Set-Variable -Name foo -Value 'He said "Go!"' -Scope Script
+ $result = Expand-Variable -Content 'Write-Host $variable:foo' -Drive variable
+ $result | Should -Be 'Write-Host "He said `"Go!`""'
+ }
+
+ It 'supports unescaped output when requested' {
+ Set-Variable -Name foo -Value "Line1`nLine2" -Scope Script
+ $result = Expand-Variable -Content 'Write-Host $variable:foo' -Drive variable -Unescaped
+ $expected = "Write-Host `"Line1`nLine2`""
+ $result | Should -Be $expected
+ }
+
+ It 'updates variables in place when requested' {
+ Set-Variable -Name bar -Value 'azure' -Scope Script
+ Set-Variable -Name foo -Value 'Write-Host $variable:bar' -Scope Script
+
+ $result = Expand-Variable -Path 'variable:foo' -Drive variable -InPlace -Passthru
+
+ $result | Should -Be 'Write-Host "azure"'
+ (Get-Variable -Name foo -Scope Script).Value | Should -Be 'Write-Host "azure"'
+ }
+}
+
+Describe 'XyzConverter white reference' {
+ AfterEach {
+ [PoshCode.Pansies.ColorSpaceConfiguration]::ResetWhiteReference()
+ }
+
+ It 'allows customizing the default white reference point' {
+ $custom = [PoshCode.Pansies.ColorSpaces.Xyz]::new(96.5, 101.2, 110.3)
+ [PoshCode.Pansies.ColorSpaceConfiguration]::SetWhiteReference($custom)
+
+ $updated = [PoshCode.Pansies.ColorSpaceConfiguration]::GetWhiteReference()
+ $updated.X | Should -Be 96.5
+ $updated.Y | Should -Be 101.2
+ $updated.Z | Should -Be 110.3
+ }
+
+ It 'clones the input value when setting the white reference' {
+ $custom = [PoshCode.Pansies.ColorSpaces.Xyz]::new(90.1, 92.2, 93.3)
+ [PoshCode.Pansies.ColorSpaceConfiguration]::SetWhiteReference($custom)
+ $custom.X = 0
+
+ $stored = [PoshCode.Pansies.ColorSpaceConfiguration]::GetWhiteReference()
+ $stored.X | Should -Be 90.1
+ }
+
+ It 'returns a copy of the white reference when reading' {
+ $original = [PoshCode.Pansies.ColorSpaceConfiguration]::GetWhiteReference()
+ $copy = [PoshCode.Pansies.ColorSpaceConfiguration]::GetWhiteReference()
+ $copy.X = 0
+
+ $current = [PoshCode.Pansies.ColorSpaceConfiguration]::GetWhiteReference()
+ $current.X | Should -Be $original.X
+ }
+}
+
+Describe 'RgbColor XTerm helpers' {
+ It 'creates full RGB values when converting from an xterm index' {
+ $expected = ([PoshCode.Pansies.RgbColor]::XTermPalette)[42]
+ $color = [PoshCode.Pansies.RgbColor]::FromXTermIndex('42')
+
+ $color.RGB | Should -Be $expected.RGB
+ $color.Mode | Should -Be ([PoshCode.Pansies.ColorMode]::XTerm256)
+ }
+
+ It 'parses prefixed xterm indexes' {
+ $expected = ([PoshCode.Pansies.RgbColor]::XTermPalette)[5]
+ $color = [PoshCode.Pansies.RgbColor]::FromXTermIndex('xt5')
+
+ $color.RGB | Should -Be $expected.RGB
+ $color.Mode | Should -Be ([PoshCode.Pansies.ColorMode]::XTerm256)
+ }
+
+ It 'populates RGB values when converting from a byte' {
+ $expected = ([PoshCode.Pansies.RgbColor]::XTermPalette)[12]
+ $color = [PoshCode.Pansies.RgbColor]::ConvertFrom([byte]12)
+
+ $color.RGB | Should -Be $expected.RGB
+ $color.Mode | Should -Be ([PoshCode.Pansies.ColorMode]::XTerm256)
+ }
+}
+
+Describe 'RgbColor color mode detection' {
+ It 'initializes ColorMode to the recommended default' {
+ $expected = [PoshCode.Pansies.RgbColor]::GetRecommendedColorMode()
+ [PoshCode.Pansies.RgbColor]::ColorMode | Should -Be $expected
+ }
+
+ It 'prefers truecolor when COLORTERM advertises support' {
+ $env = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([System.StringComparer]::OrdinalIgnoreCase)
+ $env['COLORTERM'] = 'truecolor'
+ $os = New-Object System.OperatingSystem ([System.PlatformID]::Unix), (New-Object System.Version 5, 4)
+
+ [PoshCode.Pansies.RgbColor]::GetRecommendedColorMode($env, $os) | Should -Be ([PoshCode.Pansies.ColorMode]::Rgb24Bit)
+ }
+
+ It 'selects xterm256 when TERM advertises 256 colors' {
+ $env = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([System.StringComparer]::OrdinalIgnoreCase)
+ $env['TERM'] = 'xterm-256color'
+ $os = New-Object System.OperatingSystem ([System.PlatformID]::Unix), (New-Object System.Version 6, 2)
+
+ [PoshCode.Pansies.RgbColor]::GetRecommendedColorMode($env, $os) | Should -Be ([PoshCode.Pansies.ColorMode]::XTerm256)
+ }
+
+ It 'falls back to console colors when hints are absent' {
+ $env = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([System.StringComparer]::OrdinalIgnoreCase)
+ $env['TERM'] = 'vt100'
+ $os = New-Object System.OperatingSystem ([System.PlatformID]::Unix), (New-Object System.Version 4, 19)
+
+ [PoshCode.Pansies.RgbColor]::GetRecommendedColorMode($env, $os) | Should -Be ([PoshCode.Pansies.ColorMode]::ConsoleColor)
+ }
+
+ It 'prefers RGB24 for Windows 10 and newer' {
+ $env = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([System.StringComparer]::OrdinalIgnoreCase)
+ $os = New-Object System.OperatingSystem ([System.PlatformID]::Win32NT), (New-Object System.Version 10, 0)
+
+ [PoshCode.Pansies.RgbColor]::GetRecommendedColorMode($env, $os) | Should -Be ([PoshCode.Pansies.ColorMode]::Rgb24Bit)
+ }
+
+ It 'uses console colors for legacy Windows releases' {
+ $env = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([System.StringComparer]::OrdinalIgnoreCase)
+ $os = New-Object System.OperatingSystem ([System.PlatformID]::Win32NT), (New-Object System.Version 6, 1)
+
+ [PoshCode.Pansies.RgbColor]::GetRecommendedColorMode($env, $os) | Should -Be ([PoshCode.Pansies.ColorMode]::ConsoleColor)
+ }
+}