diff --git a/AsciiArt/AsciiArt.cs b/AsciiArt/AsciiArt.cs index 12460b6..73b4270 100644 --- a/AsciiArt/AsciiArt.cs +++ b/AsciiArt/AsciiArt.cs @@ -53,22 +53,6 @@ public class AsciiArt { NCurses.WindowRefresh (window); } - public static string logo38c3_80x24 { get { - return -@" - - .;lllllllllc, .;c,:oddoc;. 'coddoc' ,clllllllll:. - kMMMMWKOdc;'xx cWMN' 'lkXMMO .dNMM0l' kMN: :MMMMMX0xl:':K - ,;'..':ok0kc ;NMMN' .cNKl .NXx;. NMMW. ';,...;lxOOo. - l0NMMM0c. .:o0lldl, xMo,. dKKo ;kXWMMNd' - 'WMMWo::oOXX0' :kO0kl;;lkK0k, OMMMMWK, 0MMMOc;lxKNKc - .,;;clodkXMc :MKo. .l0W. cMMMW0o. ;0WN, .',;:lodxKM0 - cOkkxdollc::l: .XOddddxxxxxxkO cNMx ,0MMXo 'kOkxddolc:::l - .lxkkkkxoc,. ':odxkkkxoc' ,lc;lxo:. :dxkkkxdl;. - -"; - }} - public static string lorem_ipsum { get { return @" diff --git a/CursesWrapper.cs b/CursesWrapper.cs deleted file mode 100755 index d3d8308..0000000 --- a/CursesWrapper.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Diagnostics.Contracts; -using System.IO.Pipes; -using Microsoft.VisualBasic; -using Mindmagma.Curses; -using Newtonsoft.Json.Linq; - -namespace SCI.CursesWrapper; - -public class Helper { - /// - /// Create a centered NCurses window on the specified screen - /// - /// Parent screen to attach window to - /// Width of columns of window - /// Height of rows of window - /// If true, shows default borders around window - /// nint - Pointer of new window - /// Throws exception if window is outside of screen bounds - public static nint CreateCenteredWindow (nint screen, int width, int height, bool borders = true) { - NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth); - - int originY = (screenHeight / 2) - (height / 2); - int originX = (screenWidth / 2) - (width / 2); - - if (originX + width > screenWidth) throw new ArgumentOutOfRangeException ("width"); - if (originY + height > screenHeight) throw new ArgumentOutOfRangeException ("height"); - - if (originX < 0) throw new ArgumentOutOfRangeException ("originX"); - if (originY < 0) throw new ArgumentOutOfRangeException ("originY"); - - nint window = NCurses.NewWindow (height, width, originY, originX); - if (borders) NCurses.Box (window, (char) 0, (char) 0); - - return window; - } - - /// - /// Prints text horizontally centered to window (on a specific line); - /// - /// Parent window to print into - /// Text to display - /// Y posititon of the text - public static void PrintHCenteredText (nint window, string text, int y = 0) { - NCurses.GetMaxYX (window, out int _, out int windowWidth); - - var x = windowWidth / 2 - text.Length / 2; - NCurses.MoveWindowAddString (window, y, x, text); - } -} - -public class MessageBox { - public enum MessageBoxButtons { - OK, - YesNo, - RetryCancel, - RetryIgnoreCancel - } - - /// - /// Shows a message box with specified text and returns the selected button from zero index - /// - /// Parent screen to show message box on - /// Text to show - /// Type of buttons to show - /// Button pressed by user, zero indexed - public static int Show (nint screen, string text, MessageBoxButtons buttons = MessageBoxButtons.OK) { - NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth); - - // Set static box width, if width is larger than screen, reduce to screen size, - // then calculate the message box and inner pad origin X coords - var boxWidth = 50; - if (boxWidth + 3 >= screenWidth) boxWidth = screenWidth - 3; - int originX = (screenWidth / 2) - (boxWidth / 2); - var padOriginX = originX + 2; // Add one for the border and for a single character padding - - // Calculate sizes for the inner pad element - var padWidth = boxWidth - 4; // Add four to the total box width due to the two character padding on both sides - var totalTextLines = (int) Math.Ceiling ((double) text.Length / padWidth); // Get total amount of lines required to hold all text limited by box width - var padHeight = totalTextLines; - if (padHeight + 8 > screenHeight) { // If height required to show all text lines plus padding is larger than screen, reduce size of message box - padHeight = screenHeight - 8; - } - - // Now calculate the height and Y origin of the text box and inner pad - var boxHeight = padHeight + 6; // Add five to the pad height due to the title bar and two character padding top and bottom - if (boxHeight + 2 > screenHeight) boxHeight = screenHeight - 2; - int originY = (screenHeight / 2) - (boxHeight / 2); - var padOriginY = originY + 3; // Add one for the border, one for the title and one for a single character padding - - //if (text.Length > (totalTextRows * textCols)) text = text.Substring (0, (totalTextRows * textCols) - 1) + "$"; - - // Create message box and inner Pad - var msgBoxWindow = Helper.CreateCenteredWindow (screen, boxWidth, boxHeight, true); - var msgTextPad = NCurses.NewPad (padHeight, padWidth); - - NCurses.Refresh (); - - // Title, currently not in use - Helper.PrintHCenteredText (msgBoxWindow, "Info", 1); - NCurses.WindowRefresh (msgBoxWindow); - - NCurses.WindowAddString (msgTextPad, text); - - NCurses.PadRefresh (msgTextPad, 0, 0, padOriginY, padOriginX, padOriginY + padHeight, padOriginX + padWidth); - - return InputHandler (msgBoxWindow, buttons); - } - - private static List GetStringsFromButtonEnum (MessageBoxButtons buttons) { - List buttonStrings; - - switch (buttons) { - case MessageBoxButtons.OK: - buttonStrings = new List () { "OK" }; - break; - case MessageBoxButtons.YesNo: - buttonStrings = new List () { "Yes", "No" }; - break; - case MessageBoxButtons.RetryCancel: - buttonStrings = new List () { "Retry", "Cancel" }; - break; - case MessageBoxButtons.RetryIgnoreCancel: - buttonStrings = new List () { "Retry", "Ignore", "Cancel" }; - break; - default: - throw new Exception ("Unknown button layout for message box"); - } - - return buttonStrings; - } - - /// - /// Draws buttons specified by the buttons type - /// - /// Window to draw buttons on - /// Buttons to display - private static void DrawButtons (nint window, MessageBoxButtons buttons, int active = 0) { - NCurses.GetMaxYX (window, out int windowHeight, out int windowWidth); - - var buttonStrings = GetStringsFromButtonEnum (buttons); - var totalStringLength = buttonStrings.Count - 1; - foreach (var str in buttonStrings) totalStringLength += str.Length + 2; - - NCurses.WindowMove (window, windowHeight - 2, (windowWidth / 2) - (totalStringLength / 2)); - - var index = 0; - foreach (var str in buttonStrings) { - if (index == active) { - NCurses.WindowAddString (window, $"[{str}]"); - } else { - NCurses.WindowAddString (window, $" {str} "); - } - - NCurses.WindowAddChar (window, ' '); - - index ++; - } - - NCurses.WindowRefresh (window); - } - - /// - /// Handle the user selection of the buttons - /// - /// Window to draw buttons on - /// Buttons to display - /// User choice of button, zero indexed - private static int InputHandler (nint window, MessageBoxButtons buttons) { - DrawButtons (window, buttons); - - int index = 0; - int maxIndex = GetStringsFromButtonEnum (buttons).Count - 1; - - int chr = 0; - while ((chr = NCurses.GetChar ()) != 10) { - if (chr == 260) { // Left - index ++; - if (index > maxIndex) index = 0; - DrawButtons (window, buttons, index); - } else if (chr == 261) { // Right - index --; - if (index < 0) index = maxIndex; - DrawButtons (window, buttons, index); - } - } - - return index; - } -} diff --git a/Fahrplan.cs b/Fahrplan.cs index 7bcf0d1..39b5d50 100644 --- a/Fahrplan.cs +++ b/Fahrplan.cs @@ -1,3 +1,5 @@ +#nullable disable + // Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); using Newtonsoft.Json; diff --git a/Playground.cs b/Playground.cs deleted file mode 100644 index b67fe6c..0000000 --- a/Playground.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Fahrplan; -using Mindmagma.Curses; -using Newtonsoft.Json; -using SCI.CursesWrapper; - -namespace ANSI_Fahrplan; - -public class Playground { - public static void Run (string [] args) { - Console.WriteLine ("Program initializing Curses..."); - - // var jsonString = File.ReadAllText ("schedule.json"); - // var schedule = JsonConvert.DeserializeObject (jsonString); - - // foreach (var day in schedule.schedule.conference.days) { - // Console.WriteLine ($"Day: {day.date}, {day.index}"); - // foreach (var room in day.rooms) { - // Console.WriteLine ($" \\__ Room: {room.Key}"); - // foreach (var ev in room.Value) { - // if (ev is null) continue; - // Console.WriteLine ($" \\__ Event: {ev.title}"); - // } - // } - // } - - var screen = NCurses.InitScreen (); - NCurses.NoEcho (); - NCurses.CBreak (); - NCurses.SetCursor (0); - - var hasColors = NCurses.HasColors (); - if (hasColors) { - NCurses.StartColor (); - NCurses.InitPair (1, CursesColor.WHITE, CursesColor.BLUE); - //ColorPairs.InitColors (); - } - - var win1 = NCurses.NewWindow (10, 20, 10, 10); - NCurses.Box (win1, (char) 0, (char) 0); - NCurses.WindowBackground (win1, ColorSchemes.Default ()); - - NCurses.MoveWindowAddString (win1, 1, 1, "Hello world!"); - - // Draw windows - NCurses.Refresh (); - NCurses.WindowRefresh (win1); - - NCurses.GetChar (); - NCurses.EndWin (); - - /*var window = CreateWindowCentered (screen, 40, 12); - NCurses.WindowBackground (window, NCurses.ColorPair (1)); - - NCurses.WindowAttributeOn (window, NCurses.ColorPair (1));*/ - - //NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth); - //NCurses.MoveAddString (screenHeight - 1, 0, "NCurses Example"); - - /*var menuItems = new List () { - new TopMenu.MenuItem () { Label = "Test 1" }, - new TopMenu.MenuItem () { Label = "Test 2" }, - new TopMenu.MenuItem () { Label = "Test 3" }, - }; - var menu = new TopMenu (screen) { MenuItems = menuItems }; - - menu.Render ();*/ - - var msgBoxResponse = MessageBox.Show (screen, "Hello World, this is a message box text, yippee!! Let's make this text even longer, wooowiieeeee!", MessageBox.MessageBoxButtons.YesNo); - NCurses.AddString ($"Input was {msgBoxResponse}"); - NCurses.Refresh (); - - var inputTask = Task.Run (() => InputRoutine ()); - while (inputTask.Status == TaskStatus.WaitingToRun); - - while (inputTask.Status == TaskStatus.Running) { - NCurses.Refresh (); - //NCurses.WindowRefresh (window); - NCurses.Nap (1000 / 30); - } - - //NCurses.AttributeOff (NCurses.ColorPair (1)); - NCurses.EndWin (); - } - - static void InputRoutine () { - int chr = 0; - while (chr != CursesKey.ESC) { - chr = NCurses.GetChar (); - - /*if (chr == 265) { - menu.Render (0); - } else if (chr == 266) { - menu.Render (1); - } else if (chr == 267) { - menu.Render (2); - } else if (chr > 0) { - menu.Render (); - - NCurses.MoveAddString (2, 10, $"You pressed the key {chr} "); - }*/ - - if (chr > 0) NCurses.MoveAddString (2, 10, $"You pressed the key {chr} "); - } - - /*NCurses.WindowBorder (window, ' ', ' ', ' ',' ',' ',' ',' ',' '); - NCurses.WindowRefresh (window); - NCurses.DeleteWindow (window);*/ - - NCurses.MoveAddString (10, 10, "Input routine exited"); - NCurses.Refresh (); - - return; - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 610231b..f01ac1a 100755 --- a/Program.cs +++ b/Program.cs @@ -43,27 +43,20 @@ class Program { // -- Intro screen -- // var introScreen = new IntroScreen (contentWindow); - introScreen.Draw (); - - // Close intro screen on any keypress - introScreen.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { - ((Window) sender).Destroy (); - }; - introScreen.RegisterInputHandler (inputHandler); introScreen.SetWindowActive (); // Wait until intro screen is closed while (introScreen.WindowId > -1) Thread.Sleep (50); // TODO; Unjank this + introScreen = null; + + // -- Create footer bar -- // + var footerWindow = new FooterWindow (screen); + footerWindow.DrawFooter ("Fahrplan", "LLLeft", "RRRRRight"); // -- Create menu bar -- // var topMenu = new TopMenu (screen, inputHandler, CreateMenuItems (contentWindow)); - - // Testing - var testwin = contentWindow.CreateInnerWindow (); - testwin.BackgroundColorId = ColorSchemes.TextInputField (); - NCurses.WindowAddString (testwin, "Hello World."); - contentWindow.Draw (); + topMenu.ActivateItem (topMenu.MenuItems [1]); // Wait until the input handler routine stops while (inputHandler.IsListening ()) Thread.Sleep (50); diff --git a/Screens/HelpScreen.cs b/Screens/HelpScreen.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Screens/IntroScreen.cs b/Screens/IntroScreen.cs index c157085..f760b88 100644 --- a/Screens/IntroScreen.cs +++ b/Screens/IntroScreen.cs @@ -1,4 +1,3 @@ -using Mindmagma.Curses; using SCI.CursesWrapper; namespace ANSI_Fahrplan.Screens; @@ -28,5 +27,10 @@ public class IntroScreen : Window { "38C3 Fahrplan in your terminal!"; AsciiArt.ShowCentered (WindowId, asciiArtPlusText); + + // Close intro screen on any keypress + OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { + ((Window) sender).Destroy (); + }; } } \ No newline at end of file diff --git a/Screens/ListDisplay.cs b/Screens/ListDisplay.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Screens/Screen.cs b/Screens/Screen.cs deleted file mode 100644 index fe92673..0000000 --- a/Screens/Screen.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Mindmagma.Curses; - -namespace ANSI_Fahrplan.Screens; - -public abstract class Screen { - private nint _rootWindow; - public nint RootWindow { get { return _rootWindow; }} - - public Screen (nint rootWindow) { - _rootWindow = rootWindow; - - PrepareWindow (); - } - - private void PrepareWindow () { - NCurses.NoEcho (); - NCurses.CBreak (); - NCurses.SetCursor (0); - - //NCurses.ClearWindow (RootWindow); - NCurses.WindowMove (RootWindow, 0, 0); - } - - public abstract void Show (); - - public abstract void Hide (); -} \ No newline at end of file diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index acc7fe7..0000000 --- a/TODO.txt +++ /dev/null @@ -1,13 +0,0 @@ -- [x] Create JSON parser for Fahrplan -- [] Add networking functions to pull up-to-date Fahrplan from web -- [] User interface - - [] Add landing page - - [] Add schedule overview by - - [] Day (List view, go left and right to change days) - - [] Room () - - [] Category - - [] Speakers - - [] Today/Currently running - - [] Timeline? - - [] Add search interface - - [] Add about page \ No newline at end of file diff --git a/UiElements/FooterWindow.cs b/UiElements/FooterWindow.cs new file mode 100644 index 0000000..379ce75 --- /dev/null +++ b/UiElements/FooterWindow.cs @@ -0,0 +1,64 @@ +using Mindmagma.Curses; + +namespace SCI.CursesWrapper.UiElements; + +public class FooterWindow : Window { + /// + /// Class constructor + /// + /// Root NCurses screen ID + /// Initial title to show + public FooterWindow (nint screen, string defaultTitle = "") : + base (0, CursesWrapper.GetHeight (screen) - 1, CursesWrapper.GetWidth (screen), 1) + { + var colorSchemeNormal = ColorSchemes.TopMenuNormal (); + BackgroundColorId = colorSchemeNormal; + NCurses.WindowAttributeOn (WindowId, colorSchemeNormal); + + DrawFooter (defaultTitle); + } + + /// + /// Draw the footer window and insert the specified text + /// + /// + /// + /// + public void DrawFooter (string title, string left = "", string right = "") { + Clear (); + + // TODO: Describe the last-character-in-window bug(fix) here + var pad = + (WindowSize.Width / 2) - + (title.Length / 2) + - 1; + + var finalString = + " " + left.PadRight (pad) + + title + + (string.IsNullOrEmpty (right)? + "" : + right.PadLeft (pad) + ); + + // String too long, leave out left and right text + if (finalString.Length > WindowSize.Width - 1) { + finalString = " ".PadLeft (pad) + title; + } + + // String still too long, leave out the padding and left align title + if (finalString.Length > WindowSize.Width - 1) { + finalString = " " + title; + } + + // String still too long, cut to size + if (finalString.Length > WindowSize.Width - 1) { + finalString = title.Substring (0, WindowSize.Width - 1); + } + + try { + NCurses.MoveWindowAddString (this, 0, 0, finalString); + Draw (); + } catch (Exception) {}; + } +} \ No newline at end of file diff --git a/UiElements/InputBox.cs b/UiElements/InputBox.cs deleted file mode 100644 index c0f48e1..0000000 --- a/UiElements/InputBox.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Mindmagma.Curses; - -namespace SCI.CursesWrapper.UiElements; - -public class InputBox { - public delegate void InputCompleted (string input); - - public void RequestInput (nint screen, InputHandler inputHandler, InputCompleted callback, string question) { - const int boxPaddingTop = 1; - const int boxPaddingSides = 2; - const int boxPaddingBottom = 2; - - const int boxMarginTopBottom = 1; - const int boxMarginSides = 2; - - NCurses.GetMaxYX (screen, out int height, out int width); - - // Contents inside the question box may not be larger than these - int maxInnerWidth = width - (boxPaddingSides * 2) - (boxMarginSides * 2); - int maxInnerHeight = height - boxPaddingTop - boxPaddingBottom - boxMarginTopBottom; - - using var strReader = new StringReader (question); - string? inStr; - int innerWidth = 1; - int innerHeight = 1; - while ((inStr = strReader.ReadLine ()) is not null) { - if (inStr.Length > innerWidth) innerWidth = inStr.Length; - - if (innerWidth > maxInnerWidth) { - innerWidth = maxInnerWidth; - innerHeight += (int) Math.Ceiling (inStr.Length / (double) maxInnerWidth); - } else { - innerHeight ++; - } - } - - int boxWidth = innerWidth + (boxPaddingSides * 2); - int boxHeight = innerHeight + boxPaddingTop + boxPaddingBottom; - int boxAnchorX = (width / 2) - (boxWidth / 2); - int boxAnchorY = (height / 2) - (boxHeight / 2); - - var boxWin = NCurses.SubWindow (screen, boxHeight, boxWidth, boxAnchorY, boxAnchorX); - NCurses.Box (boxWin, (char) 0, (char) 0); - - NCurses.Refresh (); - NCurses.WindowRefresh (boxWin); - - int questionTextWinAnchorX = boxAnchorX + boxPaddingSides; - int questionTextWinAnchorY = boxAnchorY + boxPaddingTop; - - var questionTextWin = NCurses.SubWindow (boxWin, innerHeight, innerWidth, questionTextWinAnchorY, questionTextWinAnchorX); - NCurses.WindowAddString (questionTextWin, question.Trim ()); - - NCurses.WindowRefresh (questionTextWin); - - int inputFieldWinX = questionTextWinAnchorX; - int inputFieldWinY = boxAnchorY + boxHeight - boxMarginTopBottom - 1; - - var inputFieldWin = NCurses.SubWindow (boxWin, 1, innerWidth, inputFieldWinY, inputFieldWinX); - NCurses.WindowBackground (inputFieldWin, ColorSchemes.TextInputField ()); - NCurses.WindowAttributeSet (inputFieldWin, ColorSchemes.TextInputField ()); - - NCurses.WindowRefresh (inputFieldWin); - - NCurses.MoveWindow (inputFieldWin, inputFieldWinY, inputFieldWinX); - NCurses.SetCursor (1); - NCurses.Echo (); - - var userInput = ""; - InputHandler.KeypressEventHandler handlerFunction = (object sender, NCursesKeyPressEventArgs e) => { - if (e.KeyCode == '\n' || e.KeyCode == CursesKey.ENTER) { - callback (userInput); - } - - if (e.KeyCode == '\n' || e.KeyCode == CursesKey.ENTER || e.KeyCode == CursesKey.ESC) { - NCurses.SetCursor (0); - NCurses.NoEcho (); - - //inputHandler.DisableRawEventHandler (screen); - - NCurses.MoveWindow (screen, 0, 0); - NCurses.DeleteWindow (inputFieldWin); - NCurses.DeleteWindow (questionTextWin); - NCurses.DeleteWindow (boxWin); - NCurses.Refresh (); - } else if (e.KeyCode == CursesKey.BACKSPACE) { - userInput = userInput.Substring (0, userInput.Length - 2); - } else if (char.IsAscii ((char) e.KeyCode)) { - userInput += (char) e.KeyCode; - } - - NCurses.MoveWindow (inputFieldWin, inputFieldWinY, inputFieldWinX); - }; - - //inputHandler.EnableRawEventHandler (handlerFunction, inputFieldWin); - } -} \ No newline at end of file diff --git a/UiElements/MenuItem.cs b/UiElements/MenuItem.cs index 3136c9d..90221bc 100644 --- a/UiElements/MenuItem.cs +++ b/UiElements/MenuItem.cs @@ -55,18 +55,27 @@ public class MenuItem { if (OnItemActivated is null) return; if (ParentTopMenuWindow is null) return; - var eventArgs = new MenuItemActivatedEventArgs (ParentTopMenuWindow, this); - if (Key.Length > 1 && Key.StartsWith ("F")) { // Handle F keys if (e.KeyCode == CursesKey.KEY_F (int.Parse (Key.Substring (1)))) { - OnItemActivated (this, eventArgs); + ActivateItem (); } } else if (Key.Length == 1) { // Handle letters and numbers if (e.KeyCode == Key [0]) { - OnItemActivated (this, eventArgs); + ActivateItem (); } } else throw new NotImplementedException ("Currently only F-keys and letters work for Top Menu actions"); } + + /// + /// Call event handler assigned to this item being activated + /// + public void ActivateItem () { + if (OnItemActivated is null) return; + if (ParentTopMenuWindow is null) return; + + var eventArgs = new MenuItemActivatedEventArgs (ParentTopMenuWindow, this); + OnItemActivated (this, eventArgs); + } } public class MenuItemActivatedEventArgs : EventArgs { diff --git a/UiElements/TopMenu.cs b/UiElements/TopMenu.cs index 7bd41ab..a478ae8 100644 --- a/UiElements/TopMenu.cs +++ b/UiElements/TopMenu.cs @@ -35,6 +35,14 @@ public class TopMenu : Window { DrawMenu (); } + /// + /// Finalizer, clean up event handlers + /// + ~TopMenu () { + UnregisterItemEventHandlers (); + Destroy (); + } + /// /// Register key event handlers for each menu item /// @@ -71,6 +79,22 @@ public class TopMenu : Window { DrawMenu (); } + /// + /// + /// + /// + public void ActivateItem (MenuItem item) { + foreach (var myItem in MenuItems) { + if (myItem == item) { + ItemActivatedHandler (myItem, EventArgs.Empty); + myItem.ActivateItem (); + return; + } + } + + throw new Exception ("This item does not exist"); + } + /// /// Draw the top menu with currently configured menu items /// diff --git a/UiElements/UiElement.cs b/UiElements/UiElement.cs deleted file mode 100644 index bb22ece..0000000 --- a/UiElements/UiElement.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ANSI_Fahrplan.UiElements; - -public abstract class UiElement { - private nint _screen; - public nint Screen { get { return _screen; }} - - protected nint contentWindow { get; set; } - - public UiElement (nint screen) { - _screen = screen; - } -} \ No newline at end of file