diff --git a/.gitignore b/.gitignore index c66d6df..0db9923 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ obj/ riderModule.iml /_ReSharper.Caches/ NeonShell.sln.DotSettings.user -publish \ No newline at end of file +publish +global.json \ No newline at end of file diff --git a/Commands/CdCommand.cs b/Commands/CdCommand.cs index 6769ff7..46f567a 100644 --- a/Commands/CdCommand.cs +++ b/Commands/CdCommand.cs @@ -30,4 +30,4 @@ public void Execute(ShellContext context, string[] args) AnsiConsole.MarkupLine($"[[[red]-[/]]] - No such directory: {fullPath}"); } } -} \ No newline at end of file +} diff --git a/Program.cs b/Program.cs index dfd0017..fa9c71a 100644 --- a/Program.cs +++ b/Program.cs @@ -2,9 +2,8 @@ using Spectre.Console; using NShell.Shell; using NShell.Shell.Commands; -using NShell.Shell.History; -using NShell.Shell.Keyboard; using NShell.Shell.Plugins; +using NShell.Shell.Readline; using static NShell.Animation.GlitchOutput; public class Program @@ -37,7 +36,6 @@ public static async Task Main(string[] args) AnsiConsole.Markup("[bold cyan][[*]] - Booting NShell...[/]\n"); ShellContext context = new(); - HistoryManager history = new(); PluginLoader plugins = new(); AnsiConsole.Markup("[bold cyan][[*]] - Loading command(s)...[/]\n"); CommandParser parser = new(); @@ -45,13 +43,33 @@ public static async Task Main(string[] args) plugins.LoadPlugins(); AppDomain.CurrentDomain.ProcessExit += (_, _) => { - history.Save(); + ReadLine.History.Save(); }; await GlitchedPrint("[+] - System Online", TimeSpan.FromMilliseconds(20)); - Console.WriteLine(); + string inputBuffer; - KeyboardHandler.Handler(history, context, parser); - + while (true) + { + Environment.SetEnvironmentVariable("LS_COLORS", context.GetLsColors()); + context.SetTheme(context.CurrentTheme); + AnsiConsole.Markup(context.GetPrompt()); + ReadLine.History.ResetIndex(); + ReadLine.Handler.UpdateInitCursorPos(Console.CursorLeft); + + inputBuffer = ReadLine.GetText(); + + if (string.IsNullOrWhiteSpace(inputBuffer)) continue; + ReadLine.History.Add(inputBuffer); + + try + { + parser.TryExecute(inputBuffer, context); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[[[red]-[/]]] - Shell crash: [yellow]{ex.Message}[/]"); + } + } } } diff --git a/Shell/Readline/KeyAction.cs b/Shell/Readline/KeyAction.cs new file mode 100644 index 0000000..ba5427c --- /dev/null +++ b/Shell/Readline/KeyAction.cs @@ -0,0 +1,126 @@ +using System.Diagnostics; + +namespace NShell.Shell.Readline; + +/// +/// KeyHandler is a class that handle all about key input. +/// define different key handling methods +/// +public partial class KeyHandler +{ + public void HandleNormalChar() + { + _inputBuffer.Insert(_currentCursorPos4Input, _currentKey.KeyChar); + _inputLength++; + Console.Write((string?)_inputBuffer.ToString(_currentCursorPos4Input, _inputLength - _currentCursorPos4Input)); + _currentCursorPos4Input++; + _currentCursorPos4Console++; + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + } + + public void HandleBackspaceChar() + { + if (IsStartOfLine()) + return; + _inputBuffer = _inputBuffer.Remove(_currentCursorPos4Input - 1, 1); + _inputLength--; + _currentCursorPos4Input--; + _currentCursorPos4Console--; + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + Console.Write(_inputBuffer.ToString(_currentCursorPos4Input, _inputLength - _currentCursorPos4Input) + " "); + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + } + + public void HandleDeleteChar() + { + if (IsEndOfLine()) + return; + _inputBuffer.Remove(_currentCursorPos4Input, 1); + _inputLength--; + Console.Write(_inputBuffer.ToString(_currentCursorPos4Input, _inputLength - _currentCursorPos4Input) + " "); + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + } + + public void HandleTab() + { + // TODO: Implement tab completion + } + + public void PreviousHistory() + { + var prev = _history.GetPrevious(); + if (prev != null) + { + Console.Write(new string('\b', _inputLength) + new string(' ', _inputLength) + new string('\b', _inputLength)); + _inputBuffer.Clear(); + _inputLength = prev.Length; + _inputBuffer.Append(prev); + Console.Write((object?)_inputBuffer); + _currentCursorPos4Input = _inputLength; + _currentCursorPos4Console = _initCursorPos4Console + _inputLength; + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + } + } + + public void NextHistory() + { + var next = _history.GetNext(); + if (next != null) + { + Console.Write(new string('\b', _inputLength) + new string(' ', _inputLength) + new string('\b', _inputLength)); + _inputBuffer.Clear(); + _inputLength = next.Length; + _inputBuffer.Append(next); + Console.Write((object?)_inputBuffer); + _currentCursorPos4Input = _inputLength; + _currentCursorPos4Console = _initCursorPos4Console + _inputLength; + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + } + } + + private void MoveCursorToEnd() + { + _currentCursorPos4Input = _inputLength; + _currentCursorPos4Console = _initCursorPos4Console + _inputLength; + Console.SetCursorPosition(_currentCursorPos4Console,Console.CursorTop); + } + + private void MoveCursorToStart() + { + _currentCursorPos4Input = 0; + _currentCursorPos4Console = _initCursorPos4Console; + Console.SetCursorPosition(_currentCursorPos4Console, Console.CursorTop); + } + + private void MoveCursorWordLeft() + { + Debug.Write("MoveCursorWordLeft"); + } + + private void MoveCursorWordRight() + { + Debug.Write("MoveCursorWordRight"); + } + + private void MoveCursorLeft() + { + if (_currentCursorPos4Input == 0) + return; + _currentCursorPos4Console--; + _currentCursorPos4Input--; + Console.SetCursorPosition(_currentCursorPos4Console,Console.CursorTop); + } + + private void MoveCursorRight() + { + if (_currentCursorPos4Input == _inputLength) + return; + _currentCursorPos4Console++; + _currentCursorPos4Input++; + Console.SetCursorPosition(_currentCursorPos4Console,Console.CursorTop); + } + + private bool IsEndOfLine() => _currentCursorPos4Input == _inputLength; + + private bool IsStartOfLine() => _currentCursorPos4Input == 0; +} diff --git a/Shell/Readline/KeyHandler.cs b/Shell/Readline/KeyHandler.cs new file mode 100644 index 0000000..3a15b02 --- /dev/null +++ b/Shell/Readline/KeyHandler.cs @@ -0,0 +1,115 @@ +using System.Text; +using NShell.Shell.History; +using Spectre.Console; + +namespace NShell.Shell.Readline; + +/// +/// KeyHandler deals with keybindings and inputs. +/// defining key behaviors, managing input buffers, and cursor position +/// +public partial class KeyHandler +{ + // a dictionary that stores key bindings + private Dictionary _keyBindings; + // cursor position + private int _currentCursorPos4Console; + private int _currentCursorPos4Input; + private int _initCursorPos4Console; + // length of the string entered + private int _inputLength; + private ConsoleKeyInfo _currentKey; + private StringBuilder _inputBuffer; + private readonly HistoryManager _history; + public string InputBuffer { get => _inputBuffer.ToString(); } + + public void LoadKeyBindings() + { + foreach (var binding in _keyBindings) + { + AnsiConsole.MarkupLine($"[green]+[/] - Loaded key binding: [yellow]{binding.Key}[/] -> [blue]{binding.Value}[/]"); + } + } + + // it can add custom key bindings to facilitate subsequent expansion + public void AddCustomKeyBinding(string key, Action handler) + { + if (_keyBindings.ContainsKey(key)) + { + _keyBindings.Remove(key); + } + _keyBindings.Add(key, handler); + } + + // it can remove custom key bindings + public void RemoveCustomKeyBinding(string key) + { + if (_keyBindings.ContainsKey(key)) + { + _keyBindings.Remove(key); + } + } + + public KeyHandler(HistoryManager history) + { + _history = history; + _inputBuffer = new(); + _keyBindings = new(); + _initCursorPos4Console = Console.CursorLeft; + _currentCursorPos4Console = _initCursorPos4Console; + _currentCursorPos4Input = 0; + _inputLength = 0; + + _keyBindings["Backspace"] = HandleBackspaceChar; + _keyBindings["Delete"] = HandleDeleteChar; + _keyBindings["Home"] = MoveCursorToStart; + _keyBindings["ControlA"] = MoveCursorToStart; + _keyBindings["End"] = MoveCursorToEnd; + _keyBindings["ControlE"] = MoveCursorToEnd; + _keyBindings["LeftArrow"] = MoveCursorLeft; + _keyBindings["RightArrow"] = MoveCursorRight; + _keyBindings["UpArrow"] = PreviousHistory; + _keyBindings["DownArrow"] = NextHistory; + + _keyBindings["Tab"] = HandleTab; + _keyBindings["ControlLeftArrow"] = MoveCursorWordLeft; + _keyBindings["ControlRightArrow"] = MoveCursorWordRight; + } + + private string BuildKeyInput() + { + string result = ""; + if (_currentKey.Modifiers.HasFlag(ConsoleModifiers.Control)) result += "Control"; + if (_currentKey.Modifiers.HasFlag(ConsoleModifiers.Alt)) result += "Alt"; + if (_currentKey.Modifiers.HasFlag(ConsoleModifiers.Shift)) result += "Shift"; + result += _currentKey.Key.ToString(); + return result; + } + + // TODO: need to improve when input complex key (like Control+A, Alt+Shift+A) + public void HandleInput(ConsoleKeyInfo keyInfo) + { + _currentKey = keyInfo; + _keyBindings.TryGetValue(BuildKeyInput(),out var handleAction); + handleAction??=HandleNormalChar; + handleAction.Invoke(); + } + + // clear the input buffer and reset the cursor position + public void ResetInput() + { + _inputBuffer.Clear(); + _currentCursorPos4Console = _initCursorPos4Console; + _currentCursorPos4Input = 0; + _inputLength = 0; + } + + // update the initial cursor position when the prompt is being updated + public void UpdateInitCursorPos(int initCursorPos) + { + _initCursorPos4Console = initCursorPos; + _currentCursorPos4Console = _initCursorPos4Console; + _currentCursorPos4Input = 0; + _inputLength = 0; + } +} diff --git a/Shell/Readline/ReadLine.cs b/Shell/Readline/ReadLine.cs new file mode 100644 index 0000000..8ef84ff --- /dev/null +++ b/Shell/Readline/ReadLine.cs @@ -0,0 +1,33 @@ +using NShell.Shell.History; + +namespace NShell.Shell.Readline; + +/// +/// ReadLine is a simple readline library +/// base on the tonerdo/readline project +/// +public class ReadLine +{ + public static KeyHandler Handler { get; } + public static HistoryManager History { get; } + + static ReadLine() + { + History = new HistoryManager(); + Handler = new KeyHandler(History); + } + + public static string GetText() + { + ConsoleKeyInfo key = Console.ReadKey(intercept: true); + while (key.Key != ConsoleKey.Enter) + { + Handler.HandleInput(key); + key = Console.ReadKey(intercept: true); + } + string input = Handler.InputBuffer; + Handler.ResetInput(); + Console.WriteLine(); + return input; + } +} diff --git a/Shell/ShellContext.cs b/Shell/ShellContext.cs index 1eaabe1..6392fec 100644 --- a/Shell/ShellContext.cs +++ b/Shell/ShellContext.cs @@ -122,7 +122,5 @@ public bool SetTheme(string themeName) } return true; } - - } }