| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- global using Attribute = Terminal.Gui.Drawing.Attribute;
- global using Color = Terminal.Gui.Drawing.Color;
- global using CM = Terminal.Gui.Configuration.ConfigurationManager;
- global using Terminal.Gui.App;
- global using Terminal.Gui.ViewBase;
- global using Terminal.Gui.Drivers;
- global using Terminal.Gui.Input;
- global using Terminal.Gui.Configuration;
- global using Terminal.Gui.Views;
- global using Terminal.Gui.Drawing;
- global using Terminal.Gui.Text;
- global using Terminal.Gui.FileServices;
- using System.CommandLine;
- using System.CommandLine.Builder;
- using System.CommandLine.Parsing;
- using System.Data;
- using System.Diagnostics;
- using System.Diagnostics.CodeAnalysis;
- using System.Globalization;
- using System.Reflection;
- using System.Reflection.Metadata;
- using System.Text;
- using System.Text.Json;
- using Microsoft.Extensions.Logging;
- using Serilog;
- using Serilog.Core;
- using Serilog.Events;
- using Command = Terminal.Gui.Input.Command;
- using ILogger = Microsoft.Extensions.Logging.ILogger;
- #nullable enable
- namespace UICatalog;
- /// <summary>
- /// UI Catalog is a comprehensive sample library and test app for Terminal.Gui. It provides a simple UI for adding to
- /// the
- /// catalog of scenarios.
- /// </summary>
- /// <remarks>
- /// <para>UI Catalog attempts to satisfy the following goals:</para>
- /// <para>
- /// <list type="number">
- /// <item>
- /// <description>Be an easy-to-use showcase for Terminal.Gui concepts and features.</description>
- /// </item>
- /// <item>
- /// <description>Provide sample code that illustrates how to properly implement said concepts & features.</description>
- /// </item>
- /// <item>
- /// <description>Make it easy for contributors to add additional samples in a structured way.</description>
- /// </item>
- /// </list>
- /// </para>
- /// </remarks>
- public class UICatalog
- {
- private static string? _forceDriver;
- private static string? _uiCatalogDriver;
- private static string? _scenarioDriver;
- public static string LogFilePath { get; set; } = string.Empty;
- public static LoggingLevelSwitch LogLevelSwitch { get; } = new ();
- public const string LOGFILE_LOCATION = "logs";
- public static UICatalogCommandLineOptions Options { get; set; }
- private static int Main (string [] args)
- {
- Console.OutputEncoding = Encoding.Default;
- if (Debugger.IsAttached)
- {
- CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
- }
- UICatalogRunnable.CachedScenarios = Scenario.GetScenarios ();
- UICatalogRunnable.CachedCategories = Scenario.GetAllCategories ();
- // Process command line args
- // If no driver is provided, the default driver is used.
- // Get allowed driver names
- string? [] allowedDrivers = Application.GetDriverTypes ().Item2.ToArray ();
- Option<string> driverOption = new Option<string> ("--driver", "The IDriver to use.")
- .FromAmong (allowedDrivers!);
- driverOption.SetDefaultValue (string.Empty);
- driverOption.AddAlias ("-d");
- driverOption.AddAlias ("--d");
- // Add validator separately (not chained)
- driverOption.AddValidator (result =>
- {
- var value = result.GetValueOrDefault<string> ();
- if (result.Tokens.Count > 0 && !allowedDrivers.Contains (value))
- {
- result.ErrorMessage = $"Invalid driver name '{value}'. Allowed values: {string.Join (", ", allowedDrivers)}";
- }
- });
- // Configuration Management
- Option<bool> disableConfigManagement = new (
- "--disable-cm",
- "Indicates Configuration Management should not be enabled. Only `ConfigLocations.HardCoded` settings will be loaded.");
- disableConfigManagement.AddAlias ("-dcm");
- disableConfigManagement.AddAlias ("--dcm");
- Option<bool> benchmarkFlag = new ("--benchmark", "Enables benchmarking. If a Scenario is specified, just that Scenario will be benchmarked.");
- benchmarkFlag.AddAlias ("-b");
- benchmarkFlag.AddAlias ("--b");
- Option<uint> benchmarkTimeout = new (
- "--timeout",
- () => Scenario.BenchmarkTimeout,
- $"The maximum time in milliseconds to run a benchmark for. Default is {Scenario.BenchmarkTimeout}ms.");
- benchmarkTimeout.AddAlias ("-t");
- benchmarkTimeout.AddAlias ("--t");
- Option<string> resultsFile = new ("--file", "The file to save benchmark results to. If not specified, the results will be displayed in a TableView.");
- resultsFile.AddAlias ("-f");
- resultsFile.AddAlias ("--f");
- // what's the app name?
- LogFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}";
- Option<string> debugLogLevel = new Option<string> ("--debug-log-level", $"The level to use for logging (debug console and {LogFilePath})").FromAmong (
- Enum.GetNames<LogLevel> ()
- );
- debugLogLevel.SetDefaultValue ("Warning");
- debugLogLevel.AddAlias ("-dl");
- debugLogLevel.AddAlias ("--dl");
- Argument<string> scenarioArgument = new Argument<string> (
- "scenario",
- description:
- "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.",
- getDefaultValue: () => "none"
- ).FromAmong (
- UICatalogRunnable.CachedScenarios.Select (s => s.GetName ())
- .Append ("none")
- .ToArray ()
- );
- var rootCommand = new RootCommand ("A comprehensive sample library and test app for Terminal.Gui")
- {
- scenarioArgument, debugLogLevel, benchmarkFlag, benchmarkTimeout, resultsFile, driverOption, disableConfigManagement
- };
- rootCommand.SetHandler (
- context =>
- {
- var options = new UICatalogCommandLineOptions
- {
- Scenario = context.ParseResult.GetValueForArgument (scenarioArgument),
- Driver = context.ParseResult.GetValueForOption (driverOption) ?? string.Empty,
- DontEnableConfigurationManagement = context.ParseResult.GetValueForOption (disableConfigManagement),
- Benchmark = context.ParseResult.GetValueForOption (benchmarkFlag),
- BenchmarkTimeout = context.ParseResult.GetValueForOption (benchmarkTimeout),
- ResultsFile = context.ParseResult.GetValueForOption (resultsFile) ?? string.Empty,
- DebugLogLevel = context.ParseResult.GetValueForOption (debugLogLevel) ?? "Warning"
- /* etc. */
- };
- // See https://github.com/dotnet/command-line-api/issues/796 for the rationale behind this hackery
- Options = options;
- }
- );
- var helpShown = false;
- Parser parser = new CommandLineBuilder (rootCommand)
- .UseHelp (ctx => helpShown = true)
- .Build ();
- parser.Invoke (args);
- if (helpShown)
- {
- return 0;
- }
- var parseResult = parser.Parse (args);
- if (parseResult.Errors.Count > 0)
- {
- foreach (var error in parseResult.Errors)
- {
- Console.Error.WriteLine (error.Message);
- }
- return 1; // Non-zero exit code for error
- }
- Scenario.BenchmarkTimeout = Options.BenchmarkTimeout;
- Logging.Logger = CreateLogger ();
- UICatalogMain (Options);
- Application.ForceDriver = string.Empty;
- return 0;
- }
- public static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel)
- {
- return logLevel switch
- {
- LogLevel.Trace => LogEventLevel.Verbose,
- LogLevel.Debug => LogEventLevel.Debug,
- LogLevel.Information => LogEventLevel.Information,
- LogLevel.Warning => LogEventLevel.Warning,
- LogLevel.Error => LogEventLevel.Error,
- LogLevel.Critical => LogEventLevel.Fatal,
- LogLevel.None => LogEventLevel.Fatal, // Default to Fatal if None is specified
- _ => LogEventLevel.Fatal // Default to Information for any unspecified LogLevel
- };
- }
- private static ILogger CreateLogger ()
- {
- // Configure Serilog to write logs to a file
- LogLevelSwitch.MinimumLevel = LogLevelToLogEventLevel (Enum.Parse<LogLevel> (Options.DebugLogLevel));
- Log.Logger = new LoggerConfiguration ()
- .MinimumLevel.ControlledBy (LogLevelSwitch)
- .Enrich.FromLogContext () // Enables dynamic enrichment
- .WriteTo.Debug ()
- .WriteTo.File (
- LogFilePath,
- rollingInterval: RollingInterval.Day,
- outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
- .CreateLogger ();
- // Create a logger factory compatible with Microsoft.Extensions.Logging
- using ILoggerFactory loggerFactory = LoggerFactory.Create (
- builder =>
- {
- builder
- .AddSerilog (dispose: true) // Integrate Serilog with ILogger
- .SetMinimumLevel (LogLevel.Trace); // Set minimum log level
- });
- // Get an ILogger instance
- return loggerFactory.CreateLogger ("Global Logger");
- }
- /// <summary>
- /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is
- /// killed and the Scenario is run as though it were Application.TopRunnable. When the Scenario exits, this function exits.
- /// </summary>
- /// <returns></returns>
- private static Scenario RunUICatalogRunnable ()
- {
- // Run UI Catalog UI. When it exits, if _selectedScenario is != null then
- // a Scenario was selected. Otherwise, the user wants to quit UI Catalog.
- // If the user specified a driver on the command line then use it,
- // ignoring Config files.
- Application.Init (driverName: _forceDriver);
- _uiCatalogDriver = Application.Driver!.GetName ();
- Application.Run<UICatalogRunnable> ();
- Application.Shutdown ();
- VerifyObjectsWereDisposed ();
- return UICatalogRunnable.CachedSelectedScenario!;
- }
- [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
- private static readonly FileSystemWatcher _currentDirWatcher = new ();
- [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
- private static readonly FileSystemWatcher _homeDirWatcher = new ();
- private static void StartConfigWatcher ()
- {
- // Set up a file system watcher for `./.tui/`
- _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
- string assemblyLocation = Assembly.GetExecutingAssembly ().Location;
- string tuiDir;
- if (!string.IsNullOrEmpty (assemblyLocation))
- {
- var assemblyFile = new FileInfo (assemblyLocation);
- tuiDir = Path.Combine (assemblyFile.Directory!.FullName, ".tui");
- }
- else
- {
- tuiDir = Path.Combine (AppContext.BaseDirectory, ".tui");
- }
- if (!Directory.Exists (tuiDir))
- {
- Directory.CreateDirectory (tuiDir);
- }
- _currentDirWatcher.Path = tuiDir;
- _currentDirWatcher.Filter = "*config.json";
- // Set up a file system watcher for `~/.tui/`
- _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
- var f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
- tuiDir = Path.Combine (f.FullName, ".tui");
- if (!Directory.Exists (tuiDir))
- {
- Directory.CreateDirectory (tuiDir);
- }
- _homeDirWatcher.Path = tuiDir;
- _homeDirWatcher.Filter = "*config.json";
- _currentDirWatcher.Changed += ConfigFileChanged;
- //_currentDirWatcher.Created += ConfigFileChanged;
- _currentDirWatcher.EnableRaisingEvents = true;
- _homeDirWatcher.Changed += ConfigFileChanged;
- //_homeDirWatcher.Created += ConfigFileChanged;
- _homeDirWatcher.EnableRaisingEvents = true;
- ThemeManager.ThemeChanged += ThemeManagerOnThemeChanged;
- }
- private static void ThemeManagerOnThemeChanged (object? sender, EventArgs<string> e)
- {
- CM.Apply ();
- }
- private static void StopConfigWatcher ()
- {
- ThemeManager.ThemeChanged += ThemeManagerOnThemeChanged;
- _currentDirWatcher.EnableRaisingEvents = false;
- _currentDirWatcher.Changed -= ConfigFileChanged;
- _currentDirWatcher.Created -= ConfigFileChanged;
- _homeDirWatcher.EnableRaisingEvents = false;
- _homeDirWatcher.Changed -= ConfigFileChanged;
- _homeDirWatcher.Created -= ConfigFileChanged;
- }
- private static void ConfigFileChanged (object sender, FileSystemEventArgs e)
- {
- if (Application.TopRunnableView == null)
- {
- return;
- }
- Logging.Debug ($"{e.FullPath} {e.ChangeType} - Loading and Applying");
- ConfigurationManager.Load (ConfigLocations.All);
- ConfigurationManager.Apply ();
- }
- private static void UICatalogMain (UICatalogCommandLineOptions options)
- {
- // By setting _forceDriver we ensure that if the user has specified a driver on the command line, it will be used
- // regardless of what's in a config file.
- Application.ForceDriver = _forceDriver = options.Driver;
- // If a Scenario name has been provided on the commandline
- // run it and exit when done.
- if (options.Scenario != "none")
- {
- if (!Options.DontEnableConfigurationManagement)
- {
- ConfigurationManager.Enable (ConfigLocations.All);
- }
- int item = UICatalogRunnable.CachedScenarios!.IndexOf (
- UICatalogRunnable.CachedScenarios!.FirstOrDefault (
- s =>
- s.GetName ()
- .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase)
- )!);
- UICatalogRunnable.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogRunnable.CachedScenarios [item].GetType ())!;
- BenchmarkResults? results = RunScenario (UICatalogRunnable.CachedSelectedScenario, options.Benchmark);
- if (results is { })
- {
- Console.WriteLine (
- JsonSerializer.Serialize (
- results,
- new JsonSerializerOptions
- {
- WriteIndented = true
- }));
- }
- VerifyObjectsWereDisposed ();
- return;
- }
- // Benchmark all Scenarios
- if (options.Benchmark)
- {
- BenchmarkAllScenarios ();
- return;
- }
- #if DEBUG_IDISPOSABLE
- View.EnableDebugIDisposableAsserts = true;
- #endif
- if (!Options.DontEnableConfigurationManagement)
- {
- ConfigurationManager.Enable (ConfigLocations.All);
- StartConfigWatcher ();
- }
- while (RunUICatalogRunnable () is { } scenario)
- {
- #if DEBUG_IDISPOSABLE
- VerifyObjectsWereDisposed ();
- // Measure how long it takes for the app to shut down
- var sw = new Stopwatch ();
- string scenarioName = scenario.GetName ();
- Application.InitializedChanged += ApplicationOnInitializedChanged;
- #endif
- Application.ForceDriver = _forceDriver;
- scenario.Main ();
- scenario.Dispose ();
- // This call to Application.Shutdown brackets the Application.Init call
- // made by Scenario.Init() above
- if (Application.Driver is { })
- {
- Application.Shutdown ();
- }
- VerifyObjectsWereDisposed ();
- #if DEBUG_IDISPOSABLE
- Application.InitializedChanged -= ApplicationOnInitializedChanged;
- void ApplicationOnInitializedChanged (object? sender, EventArgs<bool> e)
- {
- if (e.Value)
- {
- sw.Start ();
- _scenarioDriver = Application.Driver!.GetName ();
- Debug.Assert (_scenarioDriver == _uiCatalogDriver);
- }
- else
- {
- sw.Stop ();
- Logging.Trace ($"Shutdown of {scenarioName} Scenario took {sw.ElapsedMilliseconds}ms");
- }
- }
- #endif
- }
- StopConfigWatcher ();
- VerifyObjectsWereDisposed ();
- }
- private static BenchmarkResults? RunScenario (Scenario scenario, bool benchmark)
- {
- if (benchmark)
- {
- scenario.StartBenchmark ();
- }
- Application.ForceDriver = _forceDriver!;
- scenario.Main ();
- BenchmarkResults? results = null;
- if (benchmark)
- {
- results = scenario.EndBenchmark ();
- }
- scenario.Dispose ();
- if (Application.Driver is { })
- {
- Application.Shutdown ();
- }
- return results;
- }
- private static void BenchmarkAllScenarios ()
- {
- List<BenchmarkResults> resultsList = [];
- var maxScenarios = 5;
- foreach (Scenario s in UICatalogRunnable.CachedScenarios!)
- {
- resultsList.Add (RunScenario (s, true)!);
- maxScenarios--;
- if (maxScenarios == 0)
- {
- // break;
- }
- }
- if (resultsList.Count <= 0)
- {
- return;
- }
- if (!string.IsNullOrEmpty (Options.ResultsFile))
- {
- string output = JsonSerializer.Serialize (
- resultsList,
- new JsonSerializerOptions
- {
- WriteIndented = true
- });
- using StreamWriter file = File.CreateText (Options.ResultsFile);
- file.Write (output);
- file.Close ();
- return;
- }
- Application.Init ();
- var benchmarkWindow = new Window
- {
- Title = "Benchmark Results"
- };
- if (benchmarkWindow.Border is { })
- {
- benchmarkWindow.Border!.Thickness = new (0, 0, 0, 0);
- }
- TableView resultsTableView = new ()
- {
- Width = Dim.Fill (),
- Height = Dim.Fill ()
- };
- // TableView provides many options for table headers. For simplicity we turn all
- // of these off. By enabling FullRowSelect and turning off headers, TableView looks just
- // like a ListView
- resultsTableView.FullRowSelect = true;
- resultsTableView.Style.ShowHeaders = true;
- resultsTableView.Style.ShowHorizontalHeaderOverline = false;
- resultsTableView.Style.ShowHorizontalHeaderUnderline = true;
- resultsTableView.Style.ShowHorizontalBottomline = false;
- resultsTableView.Style.ShowVerticalCellLines = true;
- resultsTableView.Style.ShowVerticalHeaderLines = true;
- /* By default, TableView lays out columns at render time and only
- * measures y rows of data at a time. Where y is the height of the
- * console. This is for the following reasons:
- *
- * - Performance, when tables have a large amount of data
- * - Defensive, prevents a single wide cell value pushing other
- * columns off-screen (requiring horizontal scrolling
- *
- * In the case of UICatalog here, such an approach is overkill so
- * we just measure all the data ourselves and set the appropriate
- * max widths as ColumnStyles
- */
- //int longestName = _scenarios!.Max (s => s.GetName ().Length);
- //resultsTableView.Style.ColumnStyles.Add (
- // 0,
- // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
- // );
- //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 });
- //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem;
- // TableView typically is a grid where nav keys are biased for moving left/right.
- resultsTableView.KeyBindings.Remove (Key.Home);
- resultsTableView.KeyBindings.Add (Key.Home, Command.Start);
- resultsTableView.KeyBindings.Remove (Key.End);
- resultsTableView.KeyBindings.Add (Key.End, Command.End);
- // Ideally, TableView.MultiSelect = false would turn off any keybindings for
- // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
- // a shortcut to About.
- resultsTableView.MultiSelect = false;
- var dt = new DataTable ();
- dt.Columns.Add (new DataColumn ("Scenario", typeof (string)));
- dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan)));
- dt.Columns.Add (new DataColumn ("Refreshed", typeof (int)));
- dt.Columns.Add (new DataColumn ("LaidOut", typeof (int)));
- dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int)));
- dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int)));
- dt.Columns.Add (new DataColumn ("Updated", typeof (int)));
- dt.Columns.Add (new DataColumn ("Iterations", typeof (int)));
- foreach (BenchmarkResults r in resultsList)
- {
- dt.Rows.Add (
- r.Scenario,
- r.Duration,
- r.RefreshedCount,
- r.LaidOutCount,
- r.ClearedContentCount,
- r.DrawCompleteCount,
- r.UpdatedCount,
- r.IterationCount
- );
- }
- BenchmarkResults totalRow = new ()
- {
- Scenario = "TOTAL",
- Duration = new (resultsList.Sum (r => r.Duration.Ticks)),
- RefreshedCount = resultsList.Sum (r => r.RefreshedCount),
- LaidOutCount = resultsList.Sum (r => r.LaidOutCount),
- ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount),
- DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount),
- UpdatedCount = resultsList.Sum (r => r.UpdatedCount),
- IterationCount = resultsList.Sum (r => r.IterationCount)
- };
- dt.Rows.Add (
- totalRow.Scenario,
- totalRow.Duration,
- totalRow.RefreshedCount,
- totalRow.LaidOutCount,
- totalRow.ClearedContentCount,
- totalRow.DrawCompleteCount,
- totalRow.UpdatedCount,
- totalRow.IterationCount
- );
- dt.DefaultView.Sort = "Duration";
- DataTable sortedCopy = dt.DefaultView.ToTable ();
- resultsTableView.Table = new DataTableSource (sortedCopy);
- benchmarkWindow.Add (resultsTableView);
- Application.Run (benchmarkWindow);
- benchmarkWindow.Dispose ();
- Application.Shutdown ();
- }
- private static void VerifyObjectsWereDisposed ()
- {
- #if DEBUG_IDISPOSABLE
- if (!View.EnableDebugIDisposableAsserts)
- {
- View.Instances.Clear ();
- return;
- }
- // Validate there are no outstanding View instances
- // after a scenario was selected to run. This proves the main UI Catalog
- // 'app' closed cleanly.
- foreach (View? inst in View.Instances)
- {
- Debug.Assert (inst.WasDisposed);
- }
- View.Instances.Clear ();
- #endif
- }
- }
|