using System.Runtime.InteropServices;
using System.Text;
namespace Sunfish.Cli.Tui;
/// <summary>Terminal ANSI/VT: pantalla alternativa, colores y tamano. Sin bibliotecas externas.</summary>
internal static partial class Terminal
{
public const string Reset = "\e[0m";
public const string Bold = "\e[1m";
public const string Dim = "\e[2m";
public const string Reverse = "\e[7m";
/// <summary>El terminal acepta color de 24 bits (Windows Terminal lo anuncia con WT_SESSION).</summary>
public static bool TrueColor { get; } =
(Environment.GetEnvironmentVariable("COLORTERM") ?? "") is "truecolor" or "24bit" ||
Environment.GetEnvironmentVariable("WT_SESSION") is not null;
public static int Width => Math.Max(40, SafeSize(() => Console.WindowWidth, 80));
public static int Height => Math.Max(12, SafeSize(() => Console.WindowHeight, 25));
private static int SafeSize(Func<int> get, int fallback)
{
try
{
int value = get();
return value > 0 ? value : fallback;
}
catch (IOException)
{
return fallback;
}
}
public static void Enter()
{
EnableVirtualTerminal();
Console.Write("\e[?1049h\e[?25l\e[H\e[2J");
}
public static void Leave() => Console.Write("\e[0m\e[?25h\e[?1049l");
/// <summary>Escribe el fotograma entero de una vez (sin parpadeo).</summary>
public static void Draw(string frame) => Console.Write(frame);
public static string Fg(int r, int g, int b) => TrueColor ? $"\e[38;2;{r};{g};{b}m" : $"\e[38;5;{To256(r, g, b)}m";
public static string Bg(int r, int g, int b) => TrueColor ? $"\e[48;2;{r};{g};{b}m" : $"\e[48;5;{To256(r, g, b)}m";
public static string Fg16(int code) => $"\e[{code}m";
/// <summary>Color mas cercano del cubo 6x6x6 de la paleta de 256 colores.</summary>
internal static int To256(int r, int g, int b)
{
static int Level(int v) => v < 48 ? 0 : v < 115 ? 1 : (v - 35) / 40;
return 16 + 36 * Level(r) + 6 * Level(g) + Level(b);
}
/// <summary>
/// Como <see cref="Fit"/> pero con texto que ya lleva secuencias de color: solo cuentan los
/// caracteres visibles y las secuencias se conservan.
/// </summary>
public static string FitVisible(string text, int width)
{
var sb = new StringBuilder();
int visible = 0;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (c == '\e')
{
int end = i + 1;
if (end < text.Length && text[end] == '[')
{
end++;
while (end < text.Length && !char.IsAsciiLetter(text[end]))
end++;
}
sb.Append(text, i, Math.Min(end, text.Length - 1) - i + 1);
i = end;
continue;
}
if (visible == width)
break;
if (visible == width - 1 && HasMoreVisible(text, i + 1))
{
sb.Append('…');
visible++;
break;
}
sb.Append(char.IsControl(c) ? '?' : c);
visible++;
}
if (visible < width)
sb.Append(Reset).Append(' ', width - visible);
return sb.ToString();
}
private static bool HasMoreVisible(string text, int from)
{
for (int i = from; i < text.Length; i++)
{
if (text[i] == '\e')
{
i++;
if (i < text.Length && text[i] == '[')
while (i < text.Length && !char.IsAsciiLetter(text[i]))
i++;
continue;
}
return true;
}
return false;
}
/// <summary>Texto recortado o rellenado a un ancho exacto (contando caracteres).</summary>
public static string Fit(string text, int width)
{
if (width <= 0)
return "";
var sb = new StringBuilder();
foreach (char c in text)
sb.Append(char.IsControl(c) ? '?' : c);
string clean = sb.ToString();
if (clean.Length > width)
return width > 1 ? clean[..(width - 1)] + "…" : clean[..width];
return clean.PadRight(width);
}
private static void EnableVirtualTerminal()
{
if (!OperatingSystem.IsWindows())
return;
const int StdOutputHandle = -11;
const uint EnableVirtualTerminalProcessing = 0x0004;
nint handle = GetStdHandle(StdOutputHandle);
if (GetConsoleMode(handle, out uint mode))
SetConsoleMode(handle, mode | EnableVirtualTerminalProcessing);
}
[LibraryImport("kernel32.dll")]
private static partial nint GetStdHandle(int nStdHandle);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetConsoleMode(nint hConsoleHandle, out uint lpMode);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
}