using NStack; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Terminal.Gui; using Microsoft.DotNet.PlatformAbstractions; using Rune = System.Rune; /// /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios. /// /// /// /// UI Catalog attempts to satisfy the following goals: /// /// /// /// /// /// Be an easy to use showcase for Terminal.Gui concepts and features. /// /// /// /// /// Provide sample code that illustrates how to properly implement said concepts & features. /// /// /// /// /// Make it easy for contributors to add additional samples in a structured way. /// /// /// /// /// /// See the project README for more details (https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog/README.md). /// /// namespace UICatalog { /// /// UI Catalog is a comprehensive sample app and scenario library for /// class UICatalogApp { static void Main (string [] args) { Console.OutputEncoding = Encoding.Default; if (Debugger.IsAttached) { CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); } _scenarios = Scenario.GetScenarios (); _categories = Scenario.GetAllCategories (); _nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName ().Length; if (args.Length > 0 && args.Contains ("-usc")) { _useSystemConsole = true; args = args.Where (val => val != "-usc").ToArray (); } // If a Scenario name has been provided on the commandline // run it and exit when done. if (args.Length > 0) { var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase)); _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ()); Application.UseSystemConsole = _useSystemConsole; Application.Init (); _selectedScenario.Init (_colorScheme); _selectedScenario.Setup (); _selectedScenario.Run (); _selectedScenario.Dispose (); _selectedScenario = null; Application.Shutdown (); return; } _aboutMessage = new StringBuilder (); _aboutMessage.AppendLine (@"A comprehensive sample library for"); _aboutMessage.AppendLine (@""); _aboutMessage.AppendLine (@" _______ _ _ _____ _ "); _aboutMessage.AppendLine (@" |__ __| (_) | | / ____| (_) "); _aboutMessage.AppendLine (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ "); _aboutMessage.AppendLine (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | "); _aboutMessage.AppendLine (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | "); _aboutMessage.AppendLine (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| "); _aboutMessage.AppendLine (@""); _aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui"); Scenario scenario; while ((scenario = RunUICatalogTopLevel ()) != null) { VerifyObjectsWereDisposed (); scenario.Init (_colorScheme); scenario.Setup (); scenario.Run (); scenario.Dispose (); // This call to Application.Shutdown brackets the Application.Init call // made by Scenario.Init() above Application.Shutdown (); VerifyObjectsWereDisposed (); } VerifyObjectsWereDisposed (); } /// /// 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.Top. /// When the Scenario exits, this function exits. /// /// static Scenario RunUICatalogTopLevel () { Application.UseSystemConsole = _useSystemConsole; // Run UI Catalog UI. When it exits, if _selectedScenario is != null then // a Scenario was selected. Otherwise, the user wants to exit UI Catalog. Application.Init (); Application.Run (); Application.Shutdown (); return _selectedScenario; } static List _scenarios; static List _categories; static int _nameColumnWidth; // When a scenario is run, the main app is killed. These items // are therefore cached so that when the scenario exits the // main app UI can be restored to previous state static int _cachedScenarioIndex = 0; static int _cachedCategoryIndex = 0; static StringBuilder _aboutMessage; // If set, holds the scenario the user selected static Scenario _selectedScenario = null; static bool _useSystemConsole = false; static ConsoleDriver.DiagnosticFlags _diagnosticFlags; static bool _isFirstRunning = true; static ColorScheme _colorScheme; /// /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on /// the command line) and each time a Scenario ends. /// class UICatalogTopLevel : Toplevel { public MenuItem miIsMouseDisabled; public FrameView LeftPane; public ListView CategoryListView; public FrameView RightPane; public ListView ScenarioListView; public StatusItem Capslock; public StatusItem Numlock; public StatusItem Scrolllock; public StatusItem DriverName; public StatusItem OS; public UICatalogTopLevel () { ColorScheme = _colorScheme = Colors.Base; MenuBar = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Quit", "Quit UI Catalog", () => RequestStop(), null, null, Key.Q | Key.CtrlMask) }), new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()), new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()), new MenuBarItem ("_Help", new MenuItem [] { new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/articles/overview.html"), null, null, Key.F1), new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2), new MenuItem ("_About...", "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), }), }); Capslock = new StatusItem (Key.CharMask, "Caps", null); Numlock = new StatusItem (Key.CharMask, "Num", null); Scrolllock = new StatusItem (Key.CharMask, "Scroll", null); DriverName = new StatusItem (Key.CharMask, "Driver:", null); OS = new StatusItem (Key.CharMask, "OS:", null); StatusBar = new StatusBar () { Visible = true, }; StatusBar.Items = new StatusItem [] { new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => { if (_selectedScenario is null){ // This causes GetScenarioToRun to return null _selectedScenario = null; RequestStop(); } else { _selectedScenario.RequestStop(); } }), new StatusItem(Key.F10, "~F10~ Status Bar", () => { StatusBar.Visible = !StatusBar.Visible; LeftPane.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); RightPane.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); LayoutSubviews(); SetChildNeedsDisplay(); }), DriverName, OS }; LeftPane = new FrameView ("Categories") { X = 0, Y = 1, // for menu Width = 25, Height = Dim.Fill (1), CanFocus = true, Shortcut = Key.CtrlMask | Key.C }; LeftPane.Title = $"{LeftPane.Title} ({LeftPane.ShortcutTag})"; LeftPane.ShortcutAction = () => LeftPane.SetFocus (); CategoryListView = new ListView (_categories) { X = 0, Y = 0, Width = Dim.Fill (0), Height = Dim.Fill (0), AllowsMarking = false, CanFocus = true, }; CategoryListView.OpenSelectedItem += (a) => { RightPane.SetFocus (); }; CategoryListView.SelectedItemChanged += CategoryListView_SelectedChanged; LeftPane.Add (CategoryListView); RightPane = new FrameView ("Scenarios") { X = 25, Y = 1, // for menu Width = Dim.Fill (), Height = Dim.Fill (1), CanFocus = true, Shortcut = Key.CtrlMask | Key.S }; RightPane.Title = $"{RightPane.Title} ({RightPane.ShortcutTag})"; RightPane.ShortcutAction = () => RightPane.SetFocus (); ScenarioListView = new ListView () { X = 0, Y = 0, Width = Dim.Fill (0), Height = Dim.Fill (0), AllowsMarking = false, CanFocus = true, }; ScenarioListView.OpenSelectedItem += ScenarioListView_OpenSelectedItem; RightPane.Add (ScenarioListView); KeyDown += KeyDownHandler; Add (MenuBar); Add (LeftPane); Add (RightPane); Add (StatusBar); Loaded += LoadedHandler; // Restore previous selections CategoryListView.SelectedItem = _cachedCategoryIndex; ScenarioListView.SelectedItem = _cachedScenarioIndex; } void LoadedHandler () { if (_colorScheme == null) { ColorScheme = _colorScheme = Colors.Base; } miIsMouseDisabled.Checked = Application.IsMouseDisabled; DriverName.Title = $"Driver: {Driver.GetType ().Name}"; OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}"; if (_selectedScenario != null) { _selectedScenario = null; _isFirstRunning = false; } if (!_isFirstRunning) { RightPane.SetFocus (); } Loaded -= LoadedHandler; } /// /// Launches the selected scenario, setting the global _selectedScenario /// /// void ScenarioListView_OpenSelectedItem (EventArgs e) { if (_selectedScenario is null) { // Save selected item state _cachedCategoryIndex = CategoryListView.SelectedItem; _cachedScenarioIndex = ScenarioListView.SelectedItem; // Create new instance of scenario (even though Scenarios contains instances) _selectedScenario = (Scenario)Activator.CreateInstance (ScenarioListView.Source.ToList () [ScenarioListView.SelectedItem].GetType ()); // Tell the main app to stop Application.RequestStop (); } } List CreateDiagnosticMenuItems () { List menuItems = new List (); menuItems.Add (CreateDiagnosticFlagsMenuItems ()); menuItems.Add (new MenuItem [] { null }); menuItems.Add (CreateDisabledEnabledMouseItems ()); menuItems.Add (CreateKeybindingsMenuItems ()); return menuItems; } MenuItem [] CreateDisabledEnabledMouseItems () { List menuItems = new List (); miIsMouseDisabled = new MenuItem (); miIsMouseDisabled.Title = "_Disable Mouse"; miIsMouseDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMouseDisabled.Title.ToString ().Substring (1, 1) [0]; miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; miIsMouseDisabled.Action += () => { miIsMouseDisabled.Checked = Application.IsMouseDisabled = !miIsMouseDisabled.Checked; }; menuItems.Add (miIsMouseDisabled); return menuItems.ToArray (); } MenuItem [] CreateKeybindingsMenuItems () { List menuItems = new List (); var item = new MenuItem (); item.Title = "_Key Bindings"; item.Help = "Change which keys do what"; item.Action += () => { var dlg = new KeyBindingsDialog (); Application.Run (dlg); }; menuItems.Add (null); menuItems.Add (item); return menuItems.ToArray (); } MenuItem [] CreateDiagnosticFlagsMenuItems () { const string OFF = "Diagnostics: _Off"; const string FRAME_RULER = "Diagnostics: Frame _Ruler"; const string FRAME_PADDING = "Diagnostics: _Frame Padding"; var index = 0; List menuItems = new List (); foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { var item = new MenuItem (); item.Title = GetDiagnosticsTitle (diag); item.Shortcut = Key.AltMask + index.ToString () [0]; index++; item.CheckType |= MenuItemCheckStyle.Checked; if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) { item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0; } else { item.Checked = _diagnosticFlags.HasFlag (diag); } item.Action += () => { var t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off); if (item.Title == t && !item.Checked) { _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); item.Checked = true; } else if (item.Title == t && item.Checked) { _diagnosticFlags |= (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); item.Checked = false; } else { var f = GetDiagnosticsEnumValue (item.Title); if (_diagnosticFlags.HasFlag (f)) { SetDiagnosticsFlag (f, false); } else { SetDiagnosticsFlag (f, true); } } foreach (var menuItem in menuItems) { if (menuItem.Title == t) { menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding); } else if (menuItem.Title != t) { menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); } } ConsoleDriver.Diagnostics = _diagnosticFlags; Application.Top.SetNeedsDisplay (); }; menuItems.Add (item); } return menuItems.ToArray (); string GetDiagnosticsTitle (Enum diag) { switch (Enum.GetName (_diagnosticFlags.GetType (), diag)) { case "Off": return OFF; case "FrameRuler": return FRAME_RULER; case "FramePadding": return FRAME_PADDING; } return ""; } Enum GetDiagnosticsEnumValue (ustring title) { switch (title.ToString ()) { case FRAME_RULER: return ConsoleDriver.DiagnosticFlags.FrameRuler; case FRAME_PADDING: return ConsoleDriver.DiagnosticFlags.FramePadding; } return null; } void SetDiagnosticsFlag (Enum diag, bool add) { switch (diag) { case ConsoleDriver.DiagnosticFlags.FrameRuler: if (add) { _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler; } else { _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler; } break; case ConsoleDriver.DiagnosticFlags.FramePadding: if (add) { _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding; } else { _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding; } break; default: _diagnosticFlags = default; break; } } } MenuItem [] CreateColorSchemeMenuItems () { List menuItems = new List (); foreach (var sc in Colors.ColorSchemes) { var item = new MenuItem (); item.Title = $"_{sc.Key}"; item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0]; item.CheckType |= MenuItemCheckStyle.Radio; item.Checked = sc.Value == _colorScheme; item.Action += () => { ColorScheme = _colorScheme = sc.Value; SetNeedsDisplay (); foreach (var menuItem in menuItems) { menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme; } }; menuItems.Add (item); } return menuItems.ToArray (); } void KeyDownHandler (View.KeyEventEventArgs a) { if (a.KeyEvent.IsCapslock) { Capslock.Title = "Caps: On"; StatusBar.SetNeedsDisplay (); } else { Capslock.Title = "Caps: Off"; StatusBar.SetNeedsDisplay (); } if (a.KeyEvent.IsNumlock) { Numlock.Title = "Num: On"; StatusBar.SetNeedsDisplay (); } else { Numlock.Title = "Num: Off"; StatusBar.SetNeedsDisplay (); } if (a.KeyEvent.IsScrolllock) { Scrolllock.Title = "Scroll: On"; StatusBar.SetNeedsDisplay (); } else { Scrolllock.Title = "Scroll: Off"; StatusBar.SetNeedsDisplay (); } } void CategoryListView_SelectedChanged (ListViewItemEventArgs e) { var item = _categories [e.Item]; List newlist; if (e.Item == 0) { // First category is "All" newlist = _scenarios; } else { newlist = _scenarios.Where (s => s.GetCategories ().Contains (item)).ToList (); } ScenarioListView.SetSource (newlist.ToList ()); } } static void VerifyObjectsWereDisposed () { #if DEBUG_IDISPOSABLE // Validate there are no outstanding Responder-based instances // after a scenario was selected to run. This proves the main UI Catalog // 'app' closed cleanly. foreach (var inst in Responder.Instances) { Debug.Assert (inst.WasDisposed); } Responder.Instances.Clear (); // Validate there are no outstanding Application.RunState-based instances // after a scenario was selected to run. This proves the main UI Catalog // 'app' closed cleanly. foreach (var inst in Application.RunState.Instances) { Debug.Assert (inst.WasDisposed); } Application.RunState.Instances.Clear (); #endif } static void OpenUrl (string url) { try { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { url = url.Replace ("&", "^&"); Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { using (var process = new Process { StartInfo = new ProcessStartInfo { FileName = "xdg-open", Arguments = url, RedirectStandardError = true, RedirectStandardOutput = true, CreateNoWindow = true, UseShellExecute = false } }) { process.Start (); } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { Process.Start ("open", url); } } catch { throw; } } } }