diff --git a/.gitignore b/.gitignore index c304b65..89a2bca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,223 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +src/**/build/ +tests/**/build/ +bld/ [Bb]in/ [Oo]bj/ -.vscode -.DS_Store -*.user -project.lock.json +packages/ + +# Visual Studo 2015 cache/options directory .vs/ + +# Jetbrains Rider cache/options directory +.idea/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# Visual Studio preference files +*.DotSettings.user +*.csproj.user +*.vcxproj.filters + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +#*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.[Cc]ache +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.publishsettings +node_modules/ +bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +**/node_modules +**/node_modules/* +**/Images/ActualOutput +**/Images/ReferenceOutput + +# ASP.NET 5 +project.lock.json + +#BenchmarkDotNet +**/BenchmarkDotNet.Artifacts/ + +# Build process +*.csproj.bak + +# Advanced Installer +*-cache/ +*-SetupFiles/ + +# GitHub +.github/* \ No newline at end of file diff --git a/Artifacts/ReadLine.2.1.4.nupkg b/Artifacts/ReadLine.2.1.4.nupkg new file mode 100644 index 0000000..72cf285 Binary files /dev/null and b/Artifacts/ReadLine.2.1.4.nupkg differ diff --git a/ReadLine.Demo/AutoCompleteHandler.cs b/ReadLine.Demo/AutoCompleteHandler.cs new file mode 100644 index 0000000..8fa9a11 --- /dev/null +++ b/ReadLine.Demo/AutoCompleteHandler.cs @@ -0,0 +1,28 @@ +namespace ReadLine.Demo +{ + internal class AutoCompletionHandler : IAutoCompleteHandler + { + public char[] Separators { get; set; } = + { + ' ', + '.', + '/', + '\\', + ':' + }; + + + public string[] GetSuggestions(string text, int index) + { + return text.StartsWith("git ") + ? new[] + { + "init", + "clone", + "pull", + "push" + } + : null; + } + } +} diff --git a/ReadLine.Demo/Program.cs b/ReadLine.Demo/Program.cs new file mode 100644 index 0000000..5be4213 --- /dev/null +++ b/ReadLine.Demo/Program.cs @@ -0,0 +1,33 @@ +using System; + + +namespace ReadLine.Demo +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("ReadLine Library Demo"); + Console.WriteLine("---------------------"); + Console.WriteLine(); + + string[] history = + { + "ls -a", + "dotnet run", + "git init" + }; + + IReadLine readLine = ReadLine.Instance; + readLine.AddHistory(history); + + readLine.AutoCompletionHandler = new AutoCompletionHandler(); + + var input = readLine.Read("(prompt)> "); + Console.WriteLine(input); + + input = readLine.ReadPassword("Enter Password> "); + Console.WriteLine(input); + } + } +} \ No newline at end of file diff --git a/src/ReadLine.Demo/ReadLine.Demo.csproj b/ReadLine.Demo/ReadLine.Demo.csproj old mode 100755 new mode 100644 similarity index 86% rename from src/ReadLine.Demo/ReadLine.Demo.csproj rename to ReadLine.Demo/ReadLine.Demo.csproj index ea2bcbb..039c0da --- a/src/ReadLine.Demo/ReadLine.Demo.csproj +++ b/ReadLine.Demo/ReadLine.Demo.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp2.2 portable ReadLine.Demo Exe diff --git a/ReadLine.Tests/Abstractions/Console2.cs b/ReadLine.Tests/Abstractions/Console2.cs new file mode 100644 index 0000000..ad000e4 --- /dev/null +++ b/ReadLine.Tests/Abstractions/Console2.cs @@ -0,0 +1,49 @@ +using ReadLine.Abstractions; + + +namespace ReadLine.Tests.Abstractions +{ + internal class Console2 : IConsole + { + public bool PasswordMode { get; set; } + + public int CursorLeft { get; private set; } + + public int CursorTop { get; private set; } + + public int BufferWidth { get; private set; } + + public int BufferHeight { get; private set; } + + + public Console2() + { + CursorLeft = 0; + CursorTop = 0; + BufferWidth = 100; + BufferHeight = 100; + } + + public void SetBufferSize(int width, int height) + { + BufferWidth = width; + BufferHeight = height; + } + + public void SetCursorPosition(int left, int top) + { + CursorLeft = left; + CursorTop = top; + } + + public void Write(string value) + { + CursorLeft += value.Length; + } + + public void WriteLine(string value) + { + CursorLeft += value.Length; + } + } +} \ No newline at end of file diff --git a/ReadLine.Tests/AutoCompleteHandler.cs b/ReadLine.Tests/AutoCompleteHandler.cs new file mode 100644 index 0000000..bee1fc4 --- /dev/null +++ b/ReadLine.Tests/AutoCompleteHandler.cs @@ -0,0 +1,8 @@ +namespace ReadLine.Tests +{ + internal class AutoCompleteHandler : IAutoCompleteHandler + { + public char[] Separators { get; set; } = { ' ', '.', '/', '\\', ':' }; + public string[] GetSuggestions(string text, int index) => new[] { "World", "Angel", "Love" }; + } +} \ No newline at end of file diff --git a/test/ReadLine.Tests/CharExtensions.cs b/ReadLine.Tests/CharExtensions.cs similarity index 89% rename from test/ReadLine.Tests/CharExtensions.cs rename to ReadLine.Tests/CharExtensions.cs index 45ea98d..2888008 100644 --- a/test/ReadLine.Tests/CharExtensions.cs +++ b/ReadLine.Tests/CharExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; + namespace ReadLine.Tests { public static class CharExtensions @@ -10,6 +11,7 @@ public static class CharExtensions public const char CtrlA = '\u0001'; public const char CtrlB = '\u0002'; + public const char CtrlC = '\u0003'; public const char CtrlD = '\u0004'; public const char CtrlE = '\u0005'; public const char CtrlF = '\u0006'; @@ -22,12 +24,13 @@ public static class CharExtensions public const char CtrlU = '\u0015'; public const char CtrlW = '\u0017'; - private static readonly Dictionary> specialKeyCharMap = new Dictionary>() + private static readonly Dictionary> SpecialKeyCharMap = new Dictionary>() { {ExclamationPoint, Tuple.Create(ConsoleKey.D0, NoModifiers())}, {Space, Tuple.Create(ConsoleKey.Spacebar, NoModifiers())}, {CtrlA, Tuple.Create(ConsoleKey.A, ConsoleModifiers.Control)}, {CtrlB, Tuple.Create(ConsoleKey.B, ConsoleModifiers.Control)}, + {CtrlC, Tuple.Create(ConsoleKey.C, ConsoleModifiers.Control)}, {CtrlD, Tuple.Create(ConsoleKey.D, ConsoleModifiers.Control)}, {CtrlE, Tuple.Create(ConsoleKey.E, ConsoleModifiers.Control)}, {CtrlF, Tuple.Create(ConsoleKey.F, ConsoleModifiers.Control)}, @@ -55,12 +58,12 @@ public static ConsoleKeyInfo ToConsoleKeyInfo(this char c) private static Tuple ParseKeyInfo(this char c) { { - var success = Enum.TryParse(c.ToString().ToUpper(), out ConsoleKey result); + var success = Enum.TryParse(c.ToString().ToUpper(), out var result); if (success) {return Tuple.Create(result, NoModifiers());} } { - var success = specialKeyCharMap.TryGetValue(c, out Tuple result); + var success = SpecialKeyCharMap.TryGetValue(c, out var result); if (success) { return result; } } @@ -68,9 +71,6 @@ private static Tuple ParseKeyInfo(this char c) return Tuple.Create(default(ConsoleKey), NoModifiers()); } - private static ConsoleModifiers NoModifiers() - { - return (ConsoleModifiers)0; - } + private static ConsoleModifiers NoModifiers() => 0; } -} +} \ No newline at end of file diff --git a/test/ReadLine.Tests/ConsoleKeyInfoExtensions.cs b/ReadLine.Tests/ConsoleKeyInfoExtensions.cs similarity index 96% rename from test/ReadLine.Tests/ConsoleKeyInfoExtensions.cs rename to ReadLine.Tests/ConsoleKeyInfoExtensions.cs index 0bb0f18..e2497e9 100644 --- a/test/ReadLine.Tests/ConsoleKeyInfoExtensions.cs +++ b/ReadLine.Tests/ConsoleKeyInfoExtensions.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; + namespace ReadLine.Tests { @@ -23,6 +23,7 @@ public static class ConsoleKeyInfoExtensions public static readonly ConsoleKeyInfo Space = CharExtensions.Space.ToConsoleKeyInfo(); public static readonly ConsoleKeyInfo CtrlA = CharExtensions.CtrlA.ToConsoleKeyInfo(); public static readonly ConsoleKeyInfo CtrlB = CharExtensions.CtrlB.ToConsoleKeyInfo(); + public static readonly ConsoleKeyInfo CtrlC = CharExtensions.CtrlC.ToConsoleKeyInfo(); public static readonly ConsoleKeyInfo CtrlD = CharExtensions.CtrlD.ToConsoleKeyInfo(); public static readonly ConsoleKeyInfo CtrlE = CharExtensions.CtrlE.ToConsoleKeyInfo(); public static readonly ConsoleKeyInfo CtrlF = CharExtensions.CtrlF.ToConsoleKeyInfo(); diff --git a/test/ReadLine.Tests/KeyHandlerTests.cs b/ReadLine.Tests/KeyHandlerTests.cs similarity index 69% rename from test/ReadLine.Tests/KeyHandlerTests.cs rename to ReadLine.Tests/KeyHandlerTests.cs index 63fe29a..4a48dad 100644 --- a/test/ReadLine.Tests/KeyHandlerTests.cs +++ b/ReadLine.Tests/KeyHandlerTests.cs @@ -1,29 +1,29 @@ using System; using System.Collections.Generic; using System.Linq; - -using Xunit; - +using NUnit.Framework; +using ReadLine.Abstractions; using ReadLine.Tests.Abstractions; -using Internal.ReadLine; - using static ReadLine.Tests.ConsoleKeyInfoExtensions; + namespace ReadLine.Tests { + [TestFixture] public class KeyHandlerTests { private KeyHandler _keyHandler; private List _history; private AutoCompleteHandler _autoCompleteHandler; private string[] _completions; - private Internal.ReadLine.Abstractions.IConsole _console; + private IConsole _console; - public KeyHandlerTests() + [SetUp] + public void Initialize() { _autoCompleteHandler = new AutoCompleteHandler(); _completions = _autoCompleteHandler.GetSuggestions("", 0); - _history = new List(new string[] { "dotnet run", "git init", "clear" }); + _history = new List(new[] { "dotnet run", "git init", "clear" }); _console = new Console2(); _keyHandler = new KeyHandler(_console, _history, null); @@ -33,59 +33,65 @@ public KeyHandlerTests() .ForEach(_keyHandler.Handle); } - [Fact] + [Test] public void TestWriteChar() { - Assert.Equal("Hello", _keyHandler.Text); + Assert.AreEqual("Hello", _keyHandler.Text); " World".Select(c => c.ToConsoleKeyInfo()) .ToList() .ForEach(_keyHandler.Handle); - Assert.Equal("Hello World", _keyHandler.Text); + Assert.AreEqual("Hello World", _keyHandler.Text); } - [Fact] + [Test] public void TestBackspace() { _keyHandler.Handle(Backspace); - Assert.Equal("Hell", _keyHandler.Text); + Assert.AreEqual("Hell", _keyHandler.Text); } - [Fact] + [Test] public void TestDelete() { new List() { LeftArrow, Delete } .ForEach(_keyHandler.Handle); - Assert.Equal("Hell", _keyHandler.Text); + Assert.AreEqual("Hell", _keyHandler.Text); } - [Fact] + [Test] public void TestDelete_EndOfLine() { _keyHandler.Handle(Delete); - Assert.Equal("Hello", _keyHandler.Text); + Assert.AreEqual("Hello", _keyHandler.Text); + } + + [Test] + public void TestControlC() { + //_keyHandler.Handle(CtrlC); + Assert.Ignore("Unable to test."); } - [Fact] + [Test] public void TestControlH() { _keyHandler.Handle(CtrlH); - Assert.Equal("Hell", _keyHandler.Text); + Assert.AreEqual("Hell", _keyHandler.Text); } - [Fact] + [Test] public void TestControlT() { var initialCursorCol = _console.CursorLeft; _keyHandler.Handle(CtrlT); - Assert.Equal("Helol", _keyHandler.Text); - Assert.Equal(initialCursorCol, _console.CursorLeft); + Assert.AreEqual("Helol", _keyHandler.Text); + Assert.AreEqual(initialCursorCol, _console.CursorLeft); } - [Fact] + [Test] public void TestControlT_LeftOnce_CursorMovesToEnd() { var initialCursorCol = _console.CursorLeft; @@ -93,11 +99,11 @@ public void TestControlT_LeftOnce_CursorMovesToEnd() new List() { LeftArrow, CtrlT } .ForEach(_keyHandler.Handle); - Assert.Equal("Helol", _keyHandler.Text); - Assert.Equal(initialCursorCol, _console.CursorLeft); + Assert.AreEqual("Helol", _keyHandler.Text); + Assert.AreEqual(initialCursorCol, _console.CursorLeft); } - [Fact] + [Test] public void TestControlT_CursorInMiddleOfLine() { Enumerable @@ -109,11 +115,11 @@ public void TestControlT_CursorInMiddleOfLine() _keyHandler.Handle(CtrlT); - Assert.Equal("Hlelo", _keyHandler.Text); - Assert.Equal(initialCursorCol + 1, _console.CursorLeft); + Assert.AreEqual("Hlelo", _keyHandler.Text); + Assert.AreEqual(initialCursorCol + 1, _console.CursorLeft); } - [Fact] + [Test] public void TestControlT_CursorAtBeginningOfLine_HasNoEffect() { _keyHandler.Handle(CtrlA); @@ -122,47 +128,47 @@ public void TestControlT_CursorAtBeginningOfLine_HasNoEffect() _keyHandler.Handle(CtrlT); - Assert.Equal("Hello", _keyHandler.Text); - Assert.Equal(initialCursorCol, _console.CursorLeft); + Assert.AreEqual("Hello", _keyHandler.Text); + Assert.AreEqual(initialCursorCol, _console.CursorLeft); } - [Fact] + [Test] public void TestHome() { new List() { Home, 'S'.ToConsoleKeyInfo() } .ForEach(_keyHandler.Handle); - Assert.Equal("SHello", _keyHandler.Text); + Assert.AreEqual("SHello", _keyHandler.Text); } - [Fact] + [Test] public void TestControlA() { new List() { CtrlA, 'S'.ToConsoleKeyInfo() } .ForEach(_keyHandler.Handle); - Assert.Equal("SHello", _keyHandler.Text); + Assert.AreEqual("SHello", _keyHandler.Text); } - [Fact] + [Test] public void TestEnd() { new List() { Home, End, ExclamationPoint } .ForEach(_keyHandler.Handle); - Assert.Equal("Hello!", _keyHandler.Text); + Assert.AreEqual("Hello!", _keyHandler.Text); } - [Fact] + [Test] public void TestControlE() { new List() { CtrlA, CtrlE, ExclamationPoint } .ForEach(_keyHandler.Handle); - Assert.Equal("Hello!", _keyHandler.Text); + Assert.AreEqual("Hello!", _keyHandler.Text); } - [Fact] + [Test] public void TestLeftArrow() { " N".Select(c => c.ToConsoleKeyInfo()) @@ -170,10 +176,10 @@ public void TestLeftArrow() .ToList() .ForEach(_keyHandler.Handle); - Assert.Equal("Hell No", _keyHandler.Text); + Assert.AreEqual("Hell No", _keyHandler.Text); } - [Fact] + [Test] public void TestControlB() { " N".Select(c => c.ToConsoleKeyInfo()) @@ -181,19 +187,19 @@ public void TestControlB() .ToList() .ForEach(_keyHandler.Handle); - Assert.Equal("Hell No", _keyHandler.Text); + Assert.AreEqual("Hell No", _keyHandler.Text); } - [Fact] + [Test] public void TestRightArrow() { new List() { LeftArrow, RightArrow, ExclamationPoint } .ForEach(_keyHandler.Handle); - Assert.Equal("Hello!", _keyHandler.Text); + Assert.AreEqual("Hello!", _keyHandler.Text); } - [Fact] + [Test] public void TestControlD() { Enumerable.Repeat(LeftArrow, 4) @@ -201,44 +207,44 @@ public void TestControlD() .ToList() .ForEach(_keyHandler.Handle); - Assert.Equal("Hllo", _keyHandler.Text); + Assert.AreEqual("Hllo", _keyHandler.Text); } - [Fact] + [Test] public void TestControlF() { new List() { LeftArrow, CtrlF, ExclamationPoint } .ForEach(_keyHandler.Handle); - Assert.Equal("Hello!", _keyHandler.Text); + Assert.AreEqual("Hello!", _keyHandler.Text); } - [Fact] + [Test] public void TestControlL() { _keyHandler.Handle(CtrlL); - Assert.Equal(string.Empty, _keyHandler.Text); + Assert.AreEqual(string.Empty, _keyHandler.Text); } - [Fact] + [Test] public void TestUpArrow() { _history.AsEnumerable().Reverse().ToList().ForEach((history) => { _keyHandler.Handle(UpArrow); - Assert.Equal(history, _keyHandler.Text); + Assert.AreEqual(history, _keyHandler.Text); }); } - [Fact] + [Test] public void TestControlP() { _history.AsEnumerable().Reverse().ToList().ForEach((history) => { _keyHandler.Handle(CtrlP); - Assert.Equal(history, _keyHandler.Text); + Assert.AreEqual(history, _keyHandler.Text); }); } - [Fact] + [Test] public void TestDownArrow() { Enumerable.Repeat(UpArrow, _history.Count) @@ -246,12 +252,12 @@ public void TestDownArrow() .ForEach(_keyHandler.Handle); _history.ForEach( history => { - Assert.Equal(history, _keyHandler.Text); + Assert.AreEqual(history, _keyHandler.Text); _keyHandler.Handle(DownArrow); }); } - [Fact] + [Test] public void TestControlN() { Enumerable.Repeat(UpArrow, _history.Count) @@ -259,40 +265,40 @@ public void TestControlN() .ForEach(_keyHandler.Handle); _history.ForEach( history => { - Assert.Equal(history, _keyHandler.Text); + Assert.AreEqual(history, _keyHandler.Text); _keyHandler.Handle(CtrlN); }); } - [Fact] + [Test] public void TestControlU() { _keyHandler.Handle(LeftArrow); _keyHandler.Handle(CtrlU); - Assert.Equal("o", _keyHandler.Text); + Assert.AreEqual("o", _keyHandler.Text); _keyHandler.Handle(End); _keyHandler.Handle(CtrlU); - Assert.Equal(string.Empty, _keyHandler.Text); + Assert.AreEqual(string.Empty, _keyHandler.Text); } - [Fact] + [Test] public void TestControlK() { _keyHandler.Handle(LeftArrow); _keyHandler.Handle(CtrlK); - Assert.Equal("Hell", _keyHandler.Text); + Assert.AreEqual("Hell", _keyHandler.Text); _keyHandler.Handle(Home); _keyHandler.Handle(CtrlK); - Assert.Equal(string.Empty, _keyHandler.Text); + Assert.AreEqual(string.Empty, _keyHandler.Text); } - [Fact] + [Test] public void TestControlW() { " World".Select(c => c.ToConsoleKeyInfo()) @@ -300,20 +306,20 @@ public void TestControlW() .ToList() .ForEach(_keyHandler.Handle); - Assert.Equal("Hello ", _keyHandler.Text); + Assert.AreEqual("Hello ", _keyHandler.Text); _keyHandler.Handle(Backspace); _keyHandler.Handle(CtrlW); - Assert.Equal(string.Empty, _keyHandler.Text); + Assert.AreEqual(string.Empty, _keyHandler.Text); } - [Fact] + [Test] public void TestTab() { _keyHandler.Handle(Tab); // Nothing happens when no auto complete handler is set - Assert.Equal("Hello", _keyHandler.Text); + Assert.AreEqual("Hello", _keyHandler.Text); _keyHandler = new KeyHandler(new Console2(), _history, _autoCompleteHandler); @@ -321,17 +327,17 @@ public void TestTab() _completions.ToList().ForEach(completion => { _keyHandler.Handle(Tab); - Assert.Equal($"Hi {completion}", _keyHandler.Text); + Assert.AreEqual($"Hi {completion}", _keyHandler.Text); }); } - [Fact] + [Test] public void TestBackwardsTab() { _keyHandler.Handle(Tab); // Nothing happens when no auto complete handler is set - Assert.Equal("Hello", _keyHandler.Text); + Assert.AreEqual("Hello", _keyHandler.Text); _keyHandler = new KeyHandler(new Console2(), _history, _autoCompleteHandler); @@ -342,17 +348,17 @@ public void TestBackwardsTab() _completions.Reverse().ToList().ForEach(completion => { _keyHandler.Handle(ShiftTab); - Assert.Equal($"Hi {completion}", _keyHandler.Text); + Assert.AreEqual($"Hi {completion}", _keyHandler.Text); }); } - [Fact] + [Test] public void MoveCursorThenPreviousHistory() { _keyHandler.Handle(LeftArrow); _keyHandler.Handle(UpArrow); - Assert.Equal("clear", _keyHandler.Text); + Assert.AreEqual("clear", _keyHandler.Text); } } -} +} \ No newline at end of file diff --git a/ReadLine.Tests/ReadLine.Tests.csproj b/ReadLine.Tests/ReadLine.Tests.csproj new file mode 100644 index 0000000..568a4fb --- /dev/null +++ b/ReadLine.Tests/ReadLine.Tests.csproj @@ -0,0 +1,22 @@ + + + + net472 + ReadLine.Tests + ReadLine.Tests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/ReadLine.Tests/ReadLineTests.cs b/ReadLine.Tests/ReadLineTests.cs new file mode 100644 index 0000000..f07e200 --- /dev/null +++ b/ReadLine.Tests/ReadLineTests.cs @@ -0,0 +1,51 @@ +using System.Linq; +using NUnit.Framework; + + +namespace ReadLine.Tests +{ + [TestFixture] + public class ReadLineTests + { + private readonly IReadLine _readLine = ReadLine.Instance; + private static readonly string[] History = { "ls -a", "dotnet run", "git init" }; + + [SetUp] + public void Initialize() { + _readLine.AddHistory(History); + } + + [TearDown] + public void Destruct() { + _readLine.ClearHistory(); + } + + [Test] + public void TestClearHistory() { + _readLine.ClearHistory(); + Assert.AreEqual(0, _readLine.GetHistory().Count); + } + + [Test] + public void TestNoInitialHistory() + { + Assert.AreEqual(3, _readLine.GetHistory().Count); + } + + [Test] + public void TestUpdatesHistory() + { + _readLine.AddHistory("mkdir"); + Assert.AreEqual(4, _readLine.GetHistory().Count); + Assert.AreEqual("mkdir", _readLine.GetHistory().Last()); + } + + [Test] + public void TestGetCorrectHistory() + { + Assert.AreEqual("ls -a", _readLine.GetHistory()[0]); + Assert.AreEqual("dotnet run", _readLine.GetHistory()[1]); + Assert.AreEqual("git init", _readLine.GetHistory()[2]); + } + } +} diff --git a/ReadLine.sln b/ReadLine.sln index 90c6480..489bf7c 100755 --- a/ReadLine.sln +++ b/ReadLine.sln @@ -1,17 +1,12 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +VisualStudioVersion = 15.0.27130.2036 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{06FC0E85-E73C-4DC1-B6FD-A583B7B1DFE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadLine", "ReadLine\ReadLine.csproj", "{9EB290C9-BB52-4B24-9779-5A816AC75E18}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReadLine", "src\ReadLine\ReadLine.csproj", "{9EB290C9-BB52-4B24-9779-5A816AC75E18}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadLine.Demo", "ReadLine.Demo\ReadLine.Demo.csproj", "{4C512FD1-A0E8-4908-B0A4-B0D85947716A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReadLine.Demo", "src\ReadLine.Demo\ReadLine.Demo.csproj", "{4C512FD1-A0E8-4908-B0A4-B0D85947716A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{38C56AEB-DB08-4CBF-AAC0-EBFC2B0EE045}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReadLine.Tests", "test\ReadLine.Tests\ReadLine.Tests.csproj", "{631582D9-EEEA-4F9D-8069-71ECA24ED282}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadLine.Tests", "ReadLine.Tests\ReadLine.Tests.csproj", "{631582D9-EEEA-4F9D-8069-71ECA24ED282}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,50 +17,53 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x64.ActiveCfg = Debug|x64 - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x64.Build.0 = Debug|x64 - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x86.ActiveCfg = Debug|x86 - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x86.Build.0 = Debug|x86 + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x64.ActiveCfg = Debug|Any CPU + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x64.Build.0 = Debug|Any CPU + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x86.ActiveCfg = Debug|Any CPU + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Debug|x86.Build.0 = Debug|Any CPU {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|Any CPU.Build.0 = Release|Any CPU - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x64.ActiveCfg = Release|x64 - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x64.Build.0 = Release|x64 - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x86.ActiveCfg = Release|x86 - {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x86.Build.0 = Release|x86 + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x64.ActiveCfg = Release|Any CPU + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x64.Build.0 = Release|Any CPU + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x86.ActiveCfg = Release|Any CPU + {9EB290C9-BB52-4B24-9779-5A816AC75E18}.Release|x86.Build.0 = Release|Any CPU {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x64.ActiveCfg = Debug|x64 - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x64.Build.0 = Debug|x64 - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x86.ActiveCfg = Debug|x86 - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x86.Build.0 = Debug|x86 + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x64.Build.0 = Debug|Any CPU + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Debug|x86.Build.0 = Debug|Any CPU {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|Any CPU.Build.0 = Release|Any CPU - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x64.ActiveCfg = Release|x64 - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x64.Build.0 = Release|x64 - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x86.ActiveCfg = Release|x86 - {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x86.Build.0 = Release|x86 + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x64.ActiveCfg = Release|Any CPU + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x64.Build.0 = Release|Any CPU + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x86.ActiveCfg = Release|Any CPU + {4C512FD1-A0E8-4908-B0A4-B0D85947716A}.Release|x86.Build.0 = Release|Any CPU {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|Any CPU.Build.0 = Debug|Any CPU - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x64.ActiveCfg = Debug|x64 - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x64.Build.0 = Debug|x64 - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x86.ActiveCfg = Debug|x86 - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x86.Build.0 = Debug|x86 + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x64.ActiveCfg = Debug|Any CPU + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x64.Build.0 = Debug|Any CPU + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x86.ActiveCfg = Debug|Any CPU + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Debug|x86.Build.0 = Debug|Any CPU {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|Any CPU.ActiveCfg = Release|Any CPU {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|Any CPU.Build.0 = Release|Any CPU - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x64.ActiveCfg = Release|x64 - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x64.Build.0 = Release|x64 - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x86.ActiveCfg = Release|x86 - {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x86.Build.0 = Release|x86 + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x64.ActiveCfg = Release|Any CPU + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x64.Build.0 = Release|Any CPU + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x86.ActiveCfg = Release|Any CPU + {631582D9-EEEA-4F9D-8069-71ECA24ED282}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {9EB290C9-BB52-4B24-9779-5A816AC75E18} = {06FC0E85-E73C-4DC1-B6FD-A583B7B1DFE4} {4C512FD1-A0E8-4908-B0A4-B0D85947716A} = {06FC0E85-E73C-4DC1-B6FD-A583B7B1DFE4} {631582D9-EEEA-4F9D-8069-71ECA24ED282} = {38C56AEB-DB08-4CBF-AAC0-EBFC2B0EE045} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {90E42483-9D9A-4800-B1FE-B5044432D28F} + EndGlobalSection EndGlobal diff --git a/src/ReadLine/Abstractions/Console2.cs b/ReadLine/Abstractions/Console2.cs similarity index 88% rename from src/ReadLine/Abstractions/Console2.cs rename to ReadLine/Abstractions/Console2.cs index 8536b8b..9ebbfe4 100644 --- a/src/ReadLine/Abstractions/Console2.cs +++ b/ReadLine/Abstractions/Console2.cs @@ -1,35 +1,44 @@ using System; -namespace Internal.ReadLine.Abstractions + +namespace ReadLine.Abstractions { internal class Console2 : IConsole { + public bool PasswordMode { get; set; } + + public int CursorLeft => Console.CursorLeft; + public int CursorTop => Console.CursorTop; + public int BufferWidth => Console.BufferWidth; + public int BufferHeight => Console.BufferHeight; - public bool PasswordMode { get; set; } public void SetBufferSize(int width, int height) => Console.SetBufferSize(width, height); + public void SetCursorPosition(int left, int top) { if (!PasswordMode) Console.SetCursorPosition(left, top); } + public void Write(string value) { if (PasswordMode) - value = new String(default(char), value.Length); + value = new string(default(char), value.Length); Console.Write(value); } + public void WriteLine(string value) => Console.WriteLine(value); } } \ No newline at end of file diff --git a/src/ReadLine/Abstractions/IConsole.cs b/ReadLine/Abstractions/IConsole.cs similarity index 75% rename from src/ReadLine/Abstractions/IConsole.cs rename to ReadLine/Abstractions/IConsole.cs index 5241515..b6f4e57 100644 --- a/src/ReadLine/Abstractions/IConsole.cs +++ b/ReadLine/Abstractions/IConsole.cs @@ -1,7 +1,8 @@ -namespace Internal.ReadLine.Abstractions +namespace ReadLine.Abstractions { - internal interface IConsole + public interface IConsole { + bool PasswordMode { get; set; } int CursorLeft { get; } int CursorTop { get; } int BufferWidth { get; } diff --git a/src/ReadLine/IAutoCompleteHandler.cs b/ReadLine/IAutoCompleteHandler.cs similarity index 89% rename from src/ReadLine/IAutoCompleteHandler.cs rename to ReadLine/IAutoCompleteHandler.cs index 20b84c5..aa19dd0 100644 --- a/src/ReadLine/IAutoCompleteHandler.cs +++ b/ReadLine/IAutoCompleteHandler.cs @@ -1,4 +1,4 @@ -namespace System +namespace ReadLine { public interface IAutoCompleteHandler { diff --git a/ReadLine/IReadLine.cs b/ReadLine/IReadLine.cs new file mode 100644 index 0000000..e7ab988 --- /dev/null +++ b/ReadLine/IReadLine.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ReadLine { + public interface IReadLine { + IAutoCompleteHandler AutoCompletionHandler { get; set; } + void AddHistory(params string[] text); + List GetHistory(); + void ClearHistory(); + string Read(string prompt = "", string @default = ""); + string ReadPassword(string prompt = ""); + } +} diff --git a/ReadLine/KeyHandler.cs b/ReadLine/KeyHandler.cs new file mode 100644 index 0000000..5fb3824 --- /dev/null +++ b/ReadLine/KeyHandler.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.Text; +using ReadLine.Abstractions; + + +namespace ReadLine +{ + public class KeyHandler + { + private readonly IConsole _console2; + private readonly List _history; + private readonly Dictionary _keyActions; + private readonly StringBuilder _text; + private string[] _completions; + private int _completionsIndex; + private int _completionStart; + private int _cursorLimit; + private int _cursorPos; + private int _historyIndex; + private ConsoleKeyInfo _keyInfo; + + public KeyHandler(IConsole console, List history, IAutoCompleteHandler autoCompleteHandler) + { + _console2 = console; + + _history = history ?? new List(); + _historyIndex = _history.Count; + _text = new StringBuilder(); + _keyActions = new Dictionary + { + ["LeftArrow"] = MoveCursorLeft, + ["Home"] = MoveCursorHome, + ["End"] = MoveCursorEnd, + ["ControlA"] = MoveCursorHome, + ["ControlB"] = MoveCursorLeft, + ["ControlC"] = () => Environment.Exit(0), + ["RightArrow"] = MoveCursorRight, + ["ControlF"] = MoveCursorRight, + ["ControlE"] = MoveCursorEnd, + ["Backspace"] = Backspace, + ["Delete"] = Delete, + ["ControlD"] = Delete, + ["ControlH"] = Backspace, + ["ControlL"] = ClearLine, + ["Escape"] = ClearLine, + ["UpArrow"] = PrevHistory, + ["ControlP"] = PrevHistory, + ["DownArrow"] = NextHistory, + ["ControlN"] = NextHistory, + ["ControlU"] = () => { Backspace(_cursorPos); }, + ["ControlK"] = () => + { + var pos = _cursorPos; + MoveCursorEnd(); + Backspace(_cursorPos - pos); + }, + ["ControlW"] = () => + { + while (!IsStartOfLine() && _text[_cursorPos - 1] != ' ') + Backspace(); + }, + ["ControlT"] = TransposeChars, + ["Tab"] = () => + { + if (IsInAutoCompleteMode()) + { + NextAutoComplete(); + } + else + { + if (autoCompleteHandler == null || !IsEndOfLine()) + return; + + var text = _text.ToString(); + + _completionStart = text.LastIndexOfAny(autoCompleteHandler.Separators); + _completionStart = _completionStart == -1 ? 0 : _completionStart + 1; + + _completions = autoCompleteHandler.GetSuggestions(text, _completionStart); + _completions = _completions?.Length == 0 ? null : _completions; + + if (_completions == null) + return; + + StartAutoComplete(); + } + }, + ["ShiftTab"] = () => + { + if (IsInAutoCompleteMode()) + PreviousAutoComplete(); + } + }; + } + + + public string Text => _text.ToString(); + + + private bool IsStartOfLine() => _cursorPos == 0; + + + private bool IsEndOfLine() => _cursorPos == _cursorLimit; + + + private bool IsStartOfBuffer() => _console2.CursorLeft == 0; + + + private bool IsEndOfBuffer() => _console2.CursorLeft == _console2.BufferWidth - 1; + + + private bool IsInAutoCompleteMode() => _completions != null; + + + private void MoveCursorLeft() => MoveCursorLeft(1); + + private void MoveCursorLeft(int count) + { + if (count > _cursorPos) + count = _cursorPos; + + if (count > _console2.CursorLeft) + _console2.SetCursorPosition(_console2.BufferWidth - 1, _console2.CursorTop - 1); + else + _console2.SetCursorPosition(_console2.CursorLeft - 1, _console2.CursorTop); + + _cursorPos -= count; + } + + + private void MoveCursorHome() + { + while (!IsStartOfLine()) + MoveCursorLeft(); + } + + + private string BuildKeyInput() => _keyInfo.Modifiers != ConsoleModifiers.Control && _keyInfo.Modifiers != ConsoleModifiers.Shift ? _keyInfo.Key.ToString() : _keyInfo.Modifiers + _keyInfo.Key.ToString(); + + + private void MoveCursorRight() + { + if (IsEndOfLine()) + return; + + if (IsEndOfBuffer()) + _console2.SetCursorPosition(0, _console2.CursorTop + 1); + else + _console2.SetCursorPosition(_console2.CursorLeft + 1, _console2.CursorTop); + + _cursorPos++; + } + + + private void MoveCursorEnd() + { + while (!IsEndOfLine()) + MoveCursorRight(); + } + + + private void ClearLine() + { + MoveCursorEnd(); + Backspace(_cursorPos); + } + + + private void WriteNewString(string str) + { + ClearLine(); + WriteString(str); + } + + + private void WriteString(string str) + { + foreach (var character in str) + WriteChar(character); + } + + + private void WriteChar() => WriteChar(_keyInfo.KeyChar); + + + private void WriteChar(char c) + { + if (IsEndOfLine()) + { + _text.Append(c); + _console2.Write(c.ToString()); + _cursorPos++; + } + else + { + var left = _console2.CursorLeft; + var top = _console2.CursorTop; + var str = _text.ToString().Substring(_cursorPos); + _text.Insert(_cursorPos, c); + _console2.Write(c + str); + _console2.SetCursorPosition(left, top); + MoveCursorRight(); + } + + _cursorLimit++; + } + + + private void Backspace() => Backspace(1); + + private void Backspace(int count) + { + if (count > _cursorPos) + count = _cursorPos; + + MoveCursorLeft(count); + var index = _cursorPos; + _text.Remove(index, count); + var replacement = _text.ToString().Substring(index); + var left = _console2.CursorLeft; + var top = _console2.CursorTop; + var spaces = new string(' ', count); + _console2.Write($"{replacement}{spaces}"); + _console2.SetCursorPosition(left, top); + _cursorLimit -= count; + } + + + private void Delete() + { + if (IsEndOfLine()) + return; + + var index = _cursorPos; + _text.Remove(index, 1); + var replacement = _text.ToString().Substring(index); + var left = _console2.CursorLeft; + var top = _console2.CursorTop; + _console2.Write($"{replacement} "); + _console2.SetCursorPosition(left, top); + _cursorLimit--; + } + + + private void TransposeChars() + { + // local helper functions + bool AlmostEndOfLine() => _cursorLimit - _cursorPos == 1; + + int IncrementIf(Func expression, int index) => expression() ? index + 1 : index; + + int DecrementIf(Func expression, int index) => expression() ? index - 1 : index; + + if (IsStartOfLine()) + return; + + var firstIdx = DecrementIf(IsEndOfLine, _cursorPos - 1); + var secondIdx = DecrementIf(IsEndOfLine, _cursorPos); + + var secondChar = _text[secondIdx]; + _text[secondIdx] = _text[firstIdx]; + _text[firstIdx] = secondChar; + + var left = IncrementIf(AlmostEndOfLine, _console2.CursorLeft); + var cursorPosition = IncrementIf(AlmostEndOfLine, _cursorPos); + + WriteNewString(_text.ToString()); + + _console2.SetCursorPosition(left, _console2.CursorTop); + _cursorPos = cursorPosition; + + MoveCursorRight(); + } + + + private void StartAutoComplete() + { + Backspace(_cursorPos - _completionStart); + + _completionsIndex = 0; + + WriteString(_completions[_completionsIndex]); + } + + + private void NextAutoComplete() + { + Backspace(_cursorPos - _completionStart); + + _completionsIndex++; + + if (_completionsIndex == _completions.Length) + _completionsIndex = 0; + + WriteString(_completions[_completionsIndex]); + } + + + private void PreviousAutoComplete() + { + Backspace(_cursorPos - _completionStart); + + _completionsIndex--; + + if (_completionsIndex == -1) + _completionsIndex = _completions.Length - 1; + + WriteString(_completions[_completionsIndex]); + } + + + private void PrevHistory() + { + if (_historyIndex > 0) + { + _historyIndex--; + WriteNewString(_history[_historyIndex]); + } + } + + + private void NextHistory() + { + if (_historyIndex < _history.Count) + { + _historyIndex++; + if (_historyIndex == _history.Count) + ClearLine(); + else + WriteNewString(_history[_historyIndex]); + } + } + + + private void ResetAutoComplete() + { + _completions = null; + _completionsIndex = 0; + } + + + public void Handle(ConsoleKeyInfo keyInfo) + { + _keyInfo = keyInfo; + + // If in auto complete mode and Tab wasn't pressed + if (IsInAutoCompleteMode() && _keyInfo.Key != ConsoleKey.Tab) + ResetAutoComplete(); + + _keyActions.TryGetValue(BuildKeyInput(), out var action); + action = action ?? WriteChar; + action.Invoke(); + } + } +} \ No newline at end of file diff --git a/ReadLine/ReadLine.cs b/ReadLine/ReadLine.cs new file mode 100644 index 0000000..434744a --- /dev/null +++ b/ReadLine/ReadLine.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using ReadLine.Abstractions; + + +namespace ReadLine +{ + public class ReadLine : IReadLine + { + private List _history; + + private ReadLine() { + _history = new List(); + } + + public static ReadLine Instance = new ReadLine(); // Singleton implementation + + public IAutoCompleteHandler AutoCompletionHandler { get; set; } + + + public void AddHistory(params string[] text) => _history.AddRange(text); + + + public List GetHistory() => _history; + + + public void ClearHistory() => _history = new List(); + + + public string Read(string prompt = "", string @default = "") + { + Console.Write(prompt); + var keyHandler = new KeyHandler(new Console2(), _history, AutoCompletionHandler); + var text = GetText(keyHandler); + + if (string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(@default)) + text = @default; + else + _history.Add(text); + + return text; + } + + + public string ReadPassword(string prompt = "") + { + Console.Write(prompt); + var keyHandler = new KeyHandler(new Console2 + { + PasswordMode = true + }, null, null); + return GetText(keyHandler); + } + + + private static string GetText(KeyHandler keyHandler) + { + Console.TreatControlCAsInput = true; + + var keyInfo = Console.ReadKey(true); + while (keyInfo.Key != ConsoleKey.Enter) + { + keyHandler.Handle(keyInfo); + keyInfo = Console.ReadKey(true); + } + + Console.WriteLine(); + + return keyHandler.Text; + } + } +} \ No newline at end of file diff --git a/ReadLine/ReadLine.csproj b/ReadLine/ReadLine.csproj new file mode 100644 index 0000000..53097b4 --- /dev/null +++ b/ReadLine/ReadLine.csproj @@ -0,0 +1,34 @@ + + + + ReadLine + net45;net461;net472;net48;netcoreapp2.2;netcoreapp3.0;netstandard2.0 + ReadLine + ReadLine + readline gnu console shell cui + https://github.com/tonerdo/readline + GIT + https://github.com/Latency/ReadLine + true + snKey.snk + true + Updated to support .NET Standard v2.0 / Core 2.2 & 3.0 / Framework v4.5 - v4.8 + 2.1.4 + A GNU-Readline like library for .NET + artifacts + MIT + + + + + + + + + + + + + + + diff --git a/ReadLine/snKey.snk b/ReadLine/snKey.snk new file mode 100644 index 0000000..31d343f Binary files /dev/null and b/ReadLine/snKey.snk differ diff --git a/build.ps1 b/build.ps1 index 9334024..7afc25a 100644 --- a/build.ps1 +++ b/build.ps1 @@ -3,6 +3,6 @@ param( ) dotnet restore -dotnet build ".\src\ReadLine" -c $p1 -dotnet build ".\src\ReadLine.Demo" -c $p1 -dotnet build ".\test\ReadLine.Tests" -c $p1 +dotnet build ".\ReadLine" -c $p1 +dotnet build ".\ReadLine.Demo" -c $p1 +dotnet build ".\ReadLine.Tests" -c $p1 diff --git a/build.sh b/build.sh index f697f1d..1968d73 100755 --- a/build.sh +++ b/build.sh @@ -9,6 +9,6 @@ if [[ $1 ]]; then fi dotnet restore -dotnet build ./src/ReadLine -c $CONFIGURATION -dotnet build ./src/ReadLine.Demo -c $CONFIGURATION -dotnet build ./test/ReadLine.Tests -c $CONFIGURATION +dotnet build ./ReadLine -c $CONFIGURATION +dotnet build ./ReadLine.Demo -c $CONFIGURATION +dotnet build ./ReadLine.Tests -c $CONFIGURATION diff --git a/src/ReadLine.Demo/Program.cs b/src/ReadLine.Demo/Program.cs deleted file mode 100755 index 29b484e..0000000 --- a/src/ReadLine.Demo/Program.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace ConsoleApplication -{ - public class Program - { - public static void Main(string[] args) - { - Console.WriteLine("ReadLine Library Demo"); - Console.WriteLine("---------------------"); - Console.WriteLine(); - - string[] history = new string[] { "ls -a", "dotnet run", "git init" }; - ReadLine.AddHistory(history); - - ReadLine.AutoCompletionHandler = new AutoCompletionHandler(); - - string input = ReadLine.Read("(prompt)> "); - Console.WriteLine(input); - - input = ReadLine.ReadPassword("Enter Password> "); - Console.WriteLine(input); - } - } - - class AutoCompletionHandler : IAutoCompleteHandler - { - public char[] Separators { get; set; } = new char[] { ' ', '.', '/', '\\', ':' }; - public string[] GetSuggestions(string text, int index) - { - if (text.StartsWith("git ")) - return new string[] { "init", "clone", "pull", "push" }; - else - return null; - } - } -} diff --git a/src/ReadLine/KeyHandler.cs b/src/ReadLine/KeyHandler.cs deleted file mode 100644 index 046d11d..0000000 --- a/src/ReadLine/KeyHandler.cs +++ /dev/null @@ -1,344 +0,0 @@ -using Internal.ReadLine.Abstractions; - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Internal.ReadLine -{ - internal class KeyHandler - { - private int _cursorPos; - private int _cursorLimit; - private StringBuilder _text; - private List _history; - private int _historyIndex; - private ConsoleKeyInfo _keyInfo; - private Dictionary _keyActions; - private string[] _completions; - private int _completionStart; - private int _completionsIndex; - private IConsole Console2; - - private bool IsStartOfLine() => _cursorPos == 0; - - private bool IsEndOfLine() => _cursorPos == _cursorLimit; - - private bool IsStartOfBuffer() => Console2.CursorLeft == 0; - - private bool IsEndOfBuffer() => Console2.CursorLeft == Console2.BufferWidth - 1; - private bool IsInAutoCompleteMode() => _completions != null; - - private void MoveCursorLeft() - { - if (IsStartOfLine()) - return; - - if (IsStartOfBuffer()) - Console2.SetCursorPosition(Console2.BufferWidth - 1, Console2.CursorTop - 1); - else - Console2.SetCursorPosition(Console2.CursorLeft - 1, Console2.CursorTop); - - _cursorPos--; - } - - private void MoveCursorHome() - { - while (!IsStartOfLine()) - MoveCursorLeft(); - } - - private string BuildKeyInput() - { - return (_keyInfo.Modifiers != ConsoleModifiers.Control && _keyInfo.Modifiers != ConsoleModifiers.Shift) ? - _keyInfo.Key.ToString() : _keyInfo.Modifiers.ToString() + _keyInfo.Key.ToString(); - } - - private void MoveCursorRight() - { - if (IsEndOfLine()) - return; - - if (IsEndOfBuffer()) - Console2.SetCursorPosition(0, Console2.CursorTop + 1); - else - Console2.SetCursorPosition(Console2.CursorLeft + 1, Console2.CursorTop); - - _cursorPos++; - } - - private void MoveCursorEnd() - { - while (!IsEndOfLine()) - MoveCursorRight(); - } - - private void ClearLine() - { - MoveCursorEnd(); - while (!IsStartOfLine()) - Backspace(); - } - - private void WriteNewString(string str) - { - ClearLine(); - foreach (char character in str) - WriteChar(character); - } - - private void WriteString(string str) - { - foreach (char character in str) - WriteChar(character); - } - - private void WriteChar() => WriteChar(_keyInfo.KeyChar); - - private void WriteChar(char c) - { - if (IsEndOfLine()) - { - _text.Append(c); - Console2.Write(c.ToString()); - _cursorPos++; - } - else - { - int left = Console2.CursorLeft; - int top = Console2.CursorTop; - string str = _text.ToString().Substring(_cursorPos); - _text.Insert(_cursorPos, c); - Console2.Write(c.ToString() + str); - Console2.SetCursorPosition(left, top); - MoveCursorRight(); - } - - _cursorLimit++; - } - - private void Backspace() - { - if (IsStartOfLine()) - return; - - MoveCursorLeft(); - int index = _cursorPos; - _text.Remove(index, 1); - string replacement = _text.ToString().Substring(index); - int left = Console2.CursorLeft; - int top = Console2.CursorTop; - Console2.Write(string.Format("{0} ", replacement)); - Console2.SetCursorPosition(left, top); - _cursorLimit--; - } - - private void Delete() - { - if (IsEndOfLine()) - return; - - int index = _cursorPos; - _text.Remove(index, 1); - string replacement = _text.ToString().Substring(index); - int left = Console2.CursorLeft; - int top = Console2.CursorTop; - Console2.Write(string.Format("{0} ", replacement)); - Console2.SetCursorPosition(left, top); - _cursorLimit--; - } - - private void TransposeChars() - { - // local helper functions - bool almostEndOfLine() => (_cursorLimit - _cursorPos) == 1; - int incrementIf(Func expression, int index) => expression() ? index + 1 : index; - int decrementIf(Func expression, int index) => expression() ? index - 1 : index; - - if (IsStartOfLine()) { return; } - - var firstIdx = decrementIf(IsEndOfLine, _cursorPos - 1); - var secondIdx = decrementIf(IsEndOfLine, _cursorPos); - - var secondChar = _text[secondIdx]; - _text[secondIdx] = _text[firstIdx]; - _text[firstIdx] = secondChar; - - var left = incrementIf(almostEndOfLine, Console2.CursorLeft); - var cursorPosition = incrementIf(almostEndOfLine, _cursorPos); - - WriteNewString(_text.ToString()); - - Console2.SetCursorPosition(left, Console2.CursorTop); - _cursorPos = cursorPosition; - - MoveCursorRight(); - } - - private void StartAutoComplete() - { - while (_cursorPos > _completionStart) - Backspace(); - - _completionsIndex = 0; - - WriteString(_completions[_completionsIndex]); - } - - private void NextAutoComplete() - { - while (_cursorPos > _completionStart) - Backspace(); - - _completionsIndex++; - - if (_completionsIndex == _completions.Length) - _completionsIndex = 0; - - WriteString(_completions[_completionsIndex]); - } - - private void PreviousAutoComplete() - { - while (_cursorPos > _completionStart) - Backspace(); - - _completionsIndex--; - - if (_completionsIndex == -1) - _completionsIndex = _completions.Length - 1; - - WriteString(_completions[_completionsIndex]); - } - - private void PrevHistory() - { - if (_historyIndex > 0) - { - _historyIndex--; - WriteNewString(_history[_historyIndex]); - } - } - - private void NextHistory() - { - if (_historyIndex < _history.Count) - { - _historyIndex++; - if (_historyIndex == _history.Count) - ClearLine(); - else - WriteNewString(_history[_historyIndex]); - } - } - - private void ResetAutoComplete() - { - _completions = null; - _completionsIndex = 0; - } - - public string Text - { - get - { - return _text.ToString(); - } - } - - public KeyHandler(IConsole console, List history, IAutoCompleteHandler autoCompleteHandler) - { - Console2 = console; - - _history = history ?? new List(); - _historyIndex = _history.Count; - _text = new StringBuilder(); - _keyActions = new Dictionary(); - - _keyActions["LeftArrow"] = MoveCursorLeft; - _keyActions["Home"] = MoveCursorHome; - _keyActions["End"] = MoveCursorEnd; - _keyActions["ControlA"] = MoveCursorHome; - _keyActions["ControlB"] = MoveCursorLeft; - _keyActions["RightArrow"] = MoveCursorRight; - _keyActions["ControlF"] = MoveCursorRight; - _keyActions["ControlE"] = MoveCursorEnd; - _keyActions["Backspace"] = Backspace; - _keyActions["Delete"] = Delete; - _keyActions["ControlD"] = Delete; - _keyActions["ControlH"] = Backspace; - _keyActions["ControlL"] = ClearLine; - _keyActions["Escape"] = ClearLine; - _keyActions["UpArrow"] = PrevHistory; - _keyActions["ControlP"] = PrevHistory; - _keyActions["DownArrow"] = NextHistory; - _keyActions["ControlN"] = NextHistory; - _keyActions["ControlU"] = () => - { - while (!IsStartOfLine()) - Backspace(); - }; - _keyActions["ControlK"] = () => - { - int pos = _cursorPos; - MoveCursorEnd(); - while (_cursorPos > pos) - Backspace(); - }; - _keyActions["ControlW"] = () => - { - while (!IsStartOfLine() && _text[_cursorPos - 1] != ' ') - Backspace(); - }; - _keyActions["ControlT"] = TransposeChars; - - _keyActions["Tab"] = () => - { - if (IsInAutoCompleteMode()) - { - NextAutoComplete(); - } - else - { - if (autoCompleteHandler == null || !IsEndOfLine()) - return; - - string text = _text.ToString(); - - _completionStart = text.LastIndexOfAny(autoCompleteHandler.Separators); - _completionStart = _completionStart == -1 ? 0 : _completionStart + 1; - - _completions = autoCompleteHandler.GetSuggestions(text, _completionStart); - _completions = _completions?.Length == 0 ? null : _completions; - - if (_completions == null) - return; - - StartAutoComplete(); - } - }; - - _keyActions["ShiftTab"] = () => - { - if (IsInAutoCompleteMode()) - { - PreviousAutoComplete(); - } - }; - } - - public void Handle(ConsoleKeyInfo keyInfo) - { - _keyInfo = keyInfo; - - // If in auto complete mode and Tab wasn't pressed - if (IsInAutoCompleteMode() && _keyInfo.Key != ConsoleKey.Tab) - ResetAutoComplete(); - - Action action; - _keyActions.TryGetValue(BuildKeyInput(), out action); - action = action ?? WriteChar; - action.Invoke(); - } - } -} diff --git a/src/ReadLine/Properties/AssemblyInfo.cs b/src/ReadLine/Properties/AssemblyInfo.cs deleted file mode 100644 index 8d7f478..0000000 --- a/src/ReadLine/Properties/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("ReadLine.Tests")] \ No newline at end of file diff --git a/src/ReadLine/ReadLine.cs b/src/ReadLine/ReadLine.cs deleted file mode 100755 index 157cf66..0000000 --- a/src/ReadLine/ReadLine.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Internal.ReadLine; -using Internal.ReadLine.Abstractions; - -using System.Collections.Generic; - -namespace System -{ - public static class ReadLine - { - private static List _history; - - static ReadLine() - { - _history = new List(); - } - - public static void AddHistory(params string[] text) => _history.AddRange(text); - public static List GetHistory() => _history; - public static void ClearHistory() => _history = new List(); - public static bool HistoryEnabled { get; set; } - public static IAutoCompleteHandler AutoCompletionHandler { private get; set; } - - public static string Read(string prompt = "", string @default = "") - { - Console.Write(prompt); - KeyHandler keyHandler = new KeyHandler(new Console2(), _history, AutoCompletionHandler); - string text = GetText(keyHandler); - - if (String.IsNullOrWhiteSpace(text) && !String.IsNullOrWhiteSpace(@default)) - { - text = @default; - } - else - { - if (HistoryEnabled) - _history.Add(text); - } - - return text; - } - - public static string ReadPassword(string prompt = "") - { - Console.Write(prompt); - KeyHandler keyHandler = new KeyHandler(new Console2() { PasswordMode = true }, null, null); - return GetText(keyHandler); - } - - private static string GetText(KeyHandler keyHandler) - { - ConsoleKeyInfo keyInfo = Console.ReadKey(true); - while (keyInfo.Key != ConsoleKey.Enter) - { - keyHandler.Handle(keyInfo); - keyInfo = Console.ReadKey(true); - } - - Console.WriteLine(); - return keyHandler.Text; - } - } -} diff --git a/src/ReadLine/ReadLine.csproj b/src/ReadLine/ReadLine.csproj deleted file mode 100755 index d63699b..0000000 --- a/src/ReadLine/ReadLine.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - ReadLine - A GNU-Readline like library for .NET/.NET Core - 2.0.1 - Toni Solarin-Sodara - netstandard2.0 - portable - ReadLine - ReadLine - $(AssemblyVersion) - readline;gnu;console;shell;cui - https://github.com/tonerdo/readline - https://github.com/tonerdo/readline/blob/master/LICENSE - git - https://github.com/tonerdo/readline - - - diff --git a/test.ps1 b/test.ps1 index e19f256..0233801 100644 --- a/test.ps1 +++ b/test.ps1 @@ -1,2 +1,2 @@ -dotnet test .\test\ReadLine.Tests\ReadLine.Tests.csproj +dotnet test .\ReadLine.Tests\ReadLine.Tests.csproj if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/test.sh b/test.sh index 6d26b09..e9e4824 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -dotnet test ./test/ReadLine.Tests +dotnet test ./ReadLine.Tests diff --git a/test/ReadLine.Tests/Abstractions/Console2.cs b/test/ReadLine.Tests/Abstractions/Console2.cs deleted file mode 100644 index d87f23d..0000000 --- a/test/ReadLine.Tests/Abstractions/Console2.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Internal.ReadLine.Abstractions; - -namespace ReadLine.Tests.Abstractions -{ - internal class Console2 : IConsole - { - public int CursorLeft => _cursorLeft; - - public int CursorTop => _cursorTop; - - public int BufferWidth => _bufferWidth; - - public int BufferHeight => _bufferHeight; - - private int _cursorLeft; - private int _cursorTop; - private int _bufferWidth; - private int _bufferHeight; - - public Console2() - { - _cursorLeft = 0; - _cursorTop = 0; - _bufferWidth = 100; - _bufferHeight = 100; - } - - public void SetBufferSize(int width, int height) - { - _bufferWidth = width; - _bufferHeight = height; - } - - public void SetCursorPosition(int left, int top) - { - _cursorLeft = left; - _cursorTop = top; - } - - public void Write(string value) - { - _cursorLeft += value.Length; - } - - public void WriteLine(string value) - { - _cursorLeft += value.Length; - } - } -} \ No newline at end of file diff --git a/test/ReadLine.Tests/AutoCompleteHandler.cs b/test/ReadLine.Tests/AutoCompleteHandler.cs deleted file mode 100644 index 3d65e89..0000000 --- a/test/ReadLine.Tests/AutoCompleteHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace ReadLine.Tests -{ - class AutoCompleteHandler : IAutoCompleteHandler - { - public char[] Separators { get; set; } = new char[] { ' ', '.', '/', '\\', ':' }; - public string[] GetSuggestions(string text, int index) => new string[] { "World", "Angel", "Love" }; - } -} \ No newline at end of file diff --git a/test/ReadLine.Tests/ReadLine.Tests.csproj b/test/ReadLine.Tests/ReadLine.Tests.csproj deleted file mode 100755 index e1e1f05..0000000 --- a/test/ReadLine.Tests/ReadLine.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netcoreapp2.0 - portable - ReadLine.Tests - ReadLine.Tests - - - - - - - - - - - - - diff --git a/test/ReadLine.Tests/ReadLineTests.cs b/test/ReadLine.Tests/ReadLineTests.cs deleted file mode 100755 index 7debc6a..0000000 --- a/test/ReadLine.Tests/ReadLineTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Linq; -using Xunit; - -using static System.ReadLine; - -namespace ReadLine.Tests -{ - public class ReadLineTests : IDisposable - { - public ReadLineTests() - { - string[] history = new string[] { "ls -a", "dotnet run", "git init" }; - AddHistory(history); - } - - [Fact] - public void TestNoInitialHistory() - { - Assert.Equal(3, GetHistory().Count); - } - - [Fact] - public void TestUpdatesHistory() - { - AddHistory("mkdir"); - Assert.Equal(4, GetHistory().Count); - Assert.Equal("mkdir", GetHistory().Last()); - } - - [Fact] - public void TestGetCorrectHistory() - { - Assert.Equal("ls -a", GetHistory()[0]); - Assert.Equal("dotnet run", GetHistory()[1]); - Assert.Equal("git init", GetHistory()[2]); - } - - public void Dispose() - { - // If all above tests pass - // clear history works - ClearHistory(); - } - } -}