mirror of
https://github.com/Stone-Red-Code/EchoHub.git
synced 2026-09-04 09:06:07 +02:00
Add Terminal.Gui fork as submodule with transparent color support (Color.Transparent emitting CSI 39m/49m default bg/fg sequences). Switch EchoHub.Client from NuGet to project reference. Update Transparent theme to use real terminal transparency.
556 lines
15 KiB
C#
556 lines
15 KiB
C#
using System.Text.Json;
|
|
using Terminal.Gui.Drawing;
|
|
using Terminal.Gui.Configuration;
|
|
using Attribute = Terminal.Gui.Drawing.Attribute;
|
|
|
|
namespace EchoHub.Client.Themes;
|
|
|
|
public static class ThemeManager
|
|
{
|
|
private static readonly string ThemeDir = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes");
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
private static readonly Theme DefaultTheme = new()
|
|
{
|
|
Name = "Default",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "Gray",
|
|
Background = "Black",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkGray"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "Gray",
|
|
Background = "Black",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkGray"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Gray"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkGray"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme ClassicTheme = new()
|
|
{
|
|
Name = "Classic",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Blue",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Cyan"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "White",
|
|
FocusBackground = "Black"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Cyan"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkGray"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme LightTheme = new()
|
|
{
|
|
Name = "Light",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "Black",
|
|
Background = "White",
|
|
FocusForeground = "White",
|
|
FocusBackground = "Blue"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Blue",
|
|
FocusForeground = "BrightYellow",
|
|
FocusBackground = "Blue"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "Black",
|
|
Background = "White",
|
|
FocusForeground = "White",
|
|
FocusBackground = "Blue"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Blue",
|
|
FocusForeground = "White",
|
|
FocusBackground = "Blue"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme HackerTheme = new()
|
|
{
|
|
Name = "Hacker",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "BrightGreen",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Green"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "BrightGreen",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightGreen"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "BrightGreen",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Green"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightGreen",
|
|
Background = "Black",
|
|
FocusForeground = "BrightGreen",
|
|
FocusBackground = "Black"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme SolarizedTheme = new()
|
|
{
|
|
Name = "Solarized",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "Cyan",
|
|
Background = "Black",
|
|
FocusForeground = "BrightYellow",
|
|
FocusBackground = "DarkGray"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "BrightCyan",
|
|
Background = "DarkGray",
|
|
FocusForeground = "BrightYellow",
|
|
FocusBackground = "Black"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "Cyan",
|
|
Background = "DarkGray",
|
|
FocusForeground = "BrightYellow",
|
|
FocusBackground = "Black"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightCyan",
|
|
Background = "DarkGray",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkGray"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme DraculaTheme = new()
|
|
{
|
|
Name = "Dracula",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Magenta"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "BrightMagenta",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightMagenta"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "Magenta"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightMagenta",
|
|
Background = "Black",
|
|
FocusForeground = "BrightMagenta",
|
|
FocusBackground = "Black"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme MonokaiTheme = new()
|
|
{
|
|
Name = "Monokai",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightYellow"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightYellow"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightYellow"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "BrightYellow",
|
|
FocusBackground = "Black"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme NordTheme = new()
|
|
{
|
|
Name = "Nord",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkBlue",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "Blue"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Blue",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkBlue"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Blue",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkBlue"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightCyan",
|
|
Background = "DarkBlue",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkBlue"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme GruvboxTheme = new()
|
|
{
|
|
Name = "Gruvbox",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "DarkYellow"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "DarkYellow"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "DarkGray",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "DarkYellow"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "DarkYellow",
|
|
Background = "Black",
|
|
FocusForeground = "DarkYellow",
|
|
FocusBackground = "Black"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme OceanTheme = new()
|
|
{
|
|
Name = "Ocean",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "BrightCyan",
|
|
Background = "DarkBlue",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkCyan"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkCyan",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkBlue"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkCyan",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkBlue"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightCyan",
|
|
Background = "DarkCyan",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "DarkCyan"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme HighContrastTheme = new()
|
|
{
|
|
Name = "HighContrast",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightYellow"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightYellow"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "BrightYellow"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "BrightYellow",
|
|
Background = "Black",
|
|
FocusForeground = "BrightYellow",
|
|
FocusBackground = "Black"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme RosePineTheme = new()
|
|
{
|
|
Name = "RosePine",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Black",
|
|
FocusForeground = "Black",
|
|
FocusBackground = "DarkMagenta"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "Magenta",
|
|
Background = "Black",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkMagenta"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "White",
|
|
FocusBackground = "DarkMagenta"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "Magenta",
|
|
Background = "Black",
|
|
FocusForeground = "Magenta",
|
|
FocusBackground = "Black"
|
|
}
|
|
};
|
|
|
|
private static readonly Theme TransparentTheme = new()
|
|
{
|
|
Name = "Transparent",
|
|
Base = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Transparent",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "Transparent"
|
|
},
|
|
Menu = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "Transparent",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "Transparent"
|
|
},
|
|
Dialog = new ThemeColors
|
|
{
|
|
Foreground = "White",
|
|
Background = "DarkGray",
|
|
FocusForeground = "BrightCyan",
|
|
FocusBackground = "Black"
|
|
},
|
|
Status = new ThemeColors
|
|
{
|
|
Foreground = "Gray",
|
|
Background = "Transparent",
|
|
FocusForeground = "Gray",
|
|
FocusBackground = "Transparent"
|
|
}
|
|
};
|
|
|
|
private static readonly List<Theme> BuiltInThemes =
|
|
[
|
|
DefaultTheme,
|
|
TransparentTheme,
|
|
ClassicTheme,
|
|
LightTheme,
|
|
HackerTheme,
|
|
SolarizedTheme,
|
|
DraculaTheme,
|
|
MonokaiTheme,
|
|
NordTheme,
|
|
GruvboxTheme,
|
|
OceanTheme,
|
|
HighContrastTheme,
|
|
RosePineTheme
|
|
];
|
|
|
|
public static List<Theme> GetAvailableThemes()
|
|
{
|
|
var themes = new List<Theme>(BuiltInThemes);
|
|
|
|
try
|
|
{
|
|
if (Directory.Exists(ThemeDir))
|
|
{
|
|
foreach (var file in Directory.GetFiles(ThemeDir, "*.json"))
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(file);
|
|
var theme = JsonSerializer.Deserialize<Theme>(json, JsonOptions);
|
|
if (theme is not null && !string.IsNullOrWhiteSpace(theme.Name))
|
|
{
|
|
// Skip if a built-in theme already has this name
|
|
if (!themes.Exists(t => string.Equals(t.Name, theme.Name, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
themes.Add(theme);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Skip malformed theme files
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// If we can't read the theme directory, just return built-ins
|
|
}
|
|
|
|
return themes;
|
|
}
|
|
|
|
public static Theme GetTheme(string name)
|
|
{
|
|
var themes = GetAvailableThemes();
|
|
return themes.Find(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
?? DefaultTheme;
|
|
}
|
|
|
|
public static void ApplyTheme(Theme theme)
|
|
{
|
|
SchemeManager.AddScheme("Base", BuildColorScheme(theme.Base));
|
|
SchemeManager.AddScheme("Menu", BuildColorScheme(theme.Menu));
|
|
SchemeManager.AddScheme("Dialog", BuildColorScheme(theme.Dialog));
|
|
}
|
|
|
|
public static void SaveTheme(Theme theme)
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(ThemeDir);
|
|
var fileName = $"{theme.Name}.json";
|
|
var filePath = Path.Combine(ThemeDir, fileName);
|
|
var json = JsonSerializer.Serialize(theme, JsonOptions);
|
|
File.WriteAllText(filePath, json);
|
|
}
|
|
catch
|
|
{
|
|
// Silently fail — theme save is best-effort
|
|
}
|
|
}
|
|
|
|
private static Scheme BuildColorScheme(ThemeColors colors)
|
|
{
|
|
var normal = new Attribute(ParseColor(colors.Foreground), ParseColor(colors.Background));
|
|
var focus = new Attribute(ParseColor(colors.FocusForeground), ParseColor(colors.FocusBackground));
|
|
|
|
return new Scheme
|
|
{
|
|
Normal = normal,
|
|
Focus = focus,
|
|
HotNormal = normal,
|
|
HotFocus = focus,
|
|
Disabled = normal
|
|
};
|
|
}
|
|
|
|
private static Color ParseColor(string colorName)
|
|
{
|
|
if (Color.TryParse(colorName, out var color))
|
|
return color ?? Color.White;
|
|
|
|
return Color.White;
|
|
}
|
|
}
|