Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ obj/
riderModule.iml
/_ReSharper.Caches/
NeonShell.sln.DotSettings.user
publish
publish
global.json
2 changes: 1 addition & 1 deletion Commands/CdCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ public void Execute(ShellContext context, string[] args)
AnsiConsole.MarkupLine($"[[[red]-[/]]] - No such directory: {fullPath}");
}
}
}
}
32 changes: 25 additions & 7 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -37,21 +36,40 @@ 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();
AnsiConsole.Markup("[bold cyan][[*]] - Loading plugin(s)...[/]\n");
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}[/]");
}
}
}
}
126 changes: 126 additions & 0 deletions Shell/Readline/KeyAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System.Diagnostics;

namespace NShell.Shell.Readline;

/// <summary>
/// <c>KeyHandler</c> is a class that handle all about key input.
/// define different key handling methods
/// </summary>
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;
}
115 changes: 115 additions & 0 deletions Shell/Readline/KeyHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Text;
using NShell.Shell.History;
using Spectre.Console;

namespace NShell.Shell.Readline;

/// <summary>
/// <c>KeyHandler</c> deals with keybindings and inputs.
/// defining key behaviors, managing input buffers, and cursor position
/// </summary>
public partial class KeyHandler
{
// a dictionary that stores key bindings
private Dictionary<string, Action> _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;
}
}
33 changes: 33 additions & 0 deletions Shell/Readline/ReadLine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using NShell.Shell.History;

namespace NShell.Shell.Readline;

/// <summary>
/// <c>ReadLine</c> is a simple readline library
/// base on the tonerdo/readline project
/// </summary>
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;
}
}
2 changes: 0 additions & 2 deletions Shell/ShellContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
public string Prompt { get; private set; }
public string LSColors { get; private set; }

public ShellContext()

Check warning on line 18 in Shell/ShellContext.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LSColors' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 18 in Shell/ShellContext.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Prompt' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
CurrentDirectory = Directory.GetCurrentDirectory();
SetPromptAndColors("default");
Expand Down Expand Up @@ -122,7 +122,5 @@
}
return true;
}


}
}
Loading