Skip to content

Commit 37d3229

Browse files
Johnny A. dos Santosvslee
authored andcommitted
Add interactive option (#486)
1 parent a0e5fff commit 37d3229

File tree

6 files changed

+193
-49
lines changed

6 files changed

+193
-49
lines changed

ExchangeSharpConsole/ExchangeSharpConsole.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
@@ -15,6 +15,7 @@
1515

1616
<ItemGroup>
1717
<PackageReference Include="CommandLineParser" Version="2.6.0" />
18+
<PackageReference Include="ReadLine" Version="2.0.1" />
1819
</ItemGroup>
1920

2021
<ItemGroup>

ExchangeSharpConsole/Options/BaseOption.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,14 @@ protected async Task RunWebSocket(string exchangeName, Func<IExchangeAPI, Task<I
129129
Console.WriteLine("Connecting web socket to {0}...", api.Name);
130130

131131
IWebSocket socket = null;
132+
var tcs = new TaskCompletionSource<bool>();
132133

133134
// ReSharper disable once AccessToModifiedClosure
134-
var disposable = KeepSessionAlive(() => socket?.Dispose());
135+
var disposable = KeepSessionAlive(() =>
136+
{
137+
socket?.Dispose();
138+
tcs.TrySetResult(true);
139+
});
135140

136141
try
137142
{
@@ -148,10 +153,11 @@ protected async Task RunWebSocket(string exchangeName, Func<IExchangeAPI, Task<I
148153

149154
// ReSharper disable once AccessToDisposedClosure
150155
disposable.Dispose();
151-
WaitInteractively();
152156

153157
return Task.CompletedTask;
154158
};
159+
160+
await tcs.Task;
155161
}
156162
catch
157163
{
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using CommandLine;
8+
9+
namespace ExchangeSharpConsole.Options
10+
{
11+
[Verb("interactive", HelpText = "Enables an interactive session.")]
12+
public class InteractiveOption : BaseOption
13+
{
14+
internal static readonly string HistoryFilePath =
15+
Path.Combine(
16+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
17+
".exchange-sharp-history"
18+
);
19+
20+
/// <summary>
21+
/// UTF-8 No BOM
22+
/// </summary>
23+
internal static readonly Encoding HistoryFileEncoding = new UTF8Encoding(false, true);
24+
25+
internal const int HistoryMax = 100;
26+
27+
public override async Task RunCommand()
28+
{
29+
ReadLine.HistoryEnabled = true;
30+
ReadLine.AutoCompletionHandler = new AutoCompleter();
31+
Console.TreatControlCAsInput = true;
32+
var program = Program.Instance;
33+
34+
LoadHistory();
35+
36+
try
37+
{
38+
await RunInteractiveSession(program);
39+
Console.WriteLine();
40+
}
41+
finally
42+
{
43+
SaveHistory();
44+
}
45+
}
46+
47+
private void LoadHistory()
48+
{
49+
if (!File.Exists(HistoryFilePath))
50+
return;
51+
52+
var lines = File.ReadLines(HistoryFilePath, HistoryFileEncoding)
53+
.TakeLast(HistoryMax)
54+
.ToArray();
55+
56+
ReadLine.AddHistory(lines);
57+
}
58+
59+
private void SaveHistory()
60+
{
61+
var lines = ReadLine.GetHistory()
62+
.TakeLast(HistoryMax)
63+
.ToArray();
64+
65+
using var sw = File.CreateText(HistoryFilePath);
66+
67+
foreach (var line in lines)
68+
{
69+
sw.WriteLine(line);
70+
}
71+
}
72+
73+
private static async Task RunInteractiveSession(Program program)
74+
{
75+
while (true)
76+
{
77+
var command = ReadLine.Read("ExchangeSharp> ", "help");
78+
79+
if (command.Equals("exit", StringComparison.OrdinalIgnoreCase))
80+
break;
81+
82+
var (error, help) = program.ParseArguments(
83+
command.Split(' '),
84+
out var options
85+
);
86+
87+
if (error || help)
88+
continue;
89+
90+
await program.Run(options, exitOnError: false);
91+
}
92+
}
93+
94+
public class AutoCompleter : IAutoCompleteHandler
95+
{
96+
private readonly string[] options;
97+
98+
public AutoCompleter()
99+
{
100+
var optionsList = Program.Instance.CommandOptions
101+
.Where(t => typeof(InteractiveOption) != t)
102+
.Select(t => t.GetCustomAttribute<VerbAttribute>(true))
103+
.Where(v => !v.Hidden)
104+
.Select(v => v.Name)
105+
.ToList();
106+
107+
optionsList.Add("help");
108+
optionsList.Add("exit");
109+
110+
options = optionsList
111+
.OrderBy(o => o)
112+
.ToArray();
113+
}
114+
115+
public string[] GetSuggestions(string text, int index)
116+
{
117+
if (string.IsNullOrWhiteSpace(text))
118+
return options;
119+
120+
return options
121+
.Where(o => o.StartsWith(text, StringComparison.OrdinalIgnoreCase))
122+
.ToArray();
123+
}
124+
125+
public char[] Separators { get; set; } = {' ', '.', '/', '\"', '\''};
126+
}
127+
}
128+
}

ExchangeSharpConsole/Program.Main.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ public partial class Program
99
internal const int ExitCodeOk = 0;
1010
internal const int ExitCodeErrorParsing = -1;
1111

12+
public static Program Instance { get; } = new Program();
13+
1214
public static async Task<int> Main(string[] args)
1315
{
14-
var program = new Program();
16+
var program = Instance;
1517
var (error, help) = program.ParseArguments(args, out var options);
1618

1719
if (help)

ExchangeSharpConsole/Program.cs

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,36 @@ namespace ExchangeSharpConsole
1010
{
1111
public partial class Program
1212
{
13-
private readonly Parser parser;
13+
internal readonly Parser Parser;
14+
15+
public Type[] CommandOptions { get; } =
16+
{
17+
typeof(BuyOption),
18+
typeof(CancelOrderOption),
19+
typeof(CandlesOption),
20+
typeof(ConvertOption),
21+
typeof(ExampleOption),
22+
typeof(ExportOption),
23+
typeof(InteractiveOption),
24+
typeof(KeysOption),
25+
typeof(MarketSymbolsMetadataOption),
26+
typeof(MarketSymbolsOption),
27+
typeof(OrderDetailsOption),
28+
typeof(OrderHistoryOption),
29+
typeof(SellOption),
30+
typeof(StatsOption),
31+
typeof(SupportedExchangesOption),
32+
typeof(TestOption),
33+
typeof(TickerOption),
34+
typeof(TradeHistoryOption),
35+
typeof(WebSocketsOrderbookOption),
36+
typeof(WebSocketsTickersOption),
37+
typeof(WebSocketsTradesOption)
38+
};
1439

1540
public Program()
1641
{
17-
parser = new Parser(c =>
42+
Parser = new Parser(c =>
1843
{
1944
c.AutoHelp = true;
2045
c.AutoVersion = true;
@@ -27,36 +52,14 @@ public Program()
2752
});
2853
}
2954

30-
private (bool error, bool help) ParseArguments(string[] args, out List<BaseOption> options)
55+
internal (bool error, bool help) ParseArguments(string[] args, out List<BaseOption> options)
3156
{
3257
var error = false;
3358
var help = false;
3459
var optionList = new List<BaseOption>();
3560

36-
parser
37-
.ParseArguments(
38-
args,
39-
typeof(BuyOption),
40-
typeof(CancelOrderOption),
41-
typeof(CandlesOption),
42-
typeof(ConvertOption),
43-
typeof(ExampleOption),
44-
typeof(ExportOption),
45-
typeof(KeysOption),
46-
typeof(MarketSymbolsMetadataOption),
47-
typeof(MarketSymbolsOption),
48-
typeof(OrderDetailsOption),
49-
typeof(OrderHistoryOption),
50-
typeof(SellOption),
51-
typeof(StatsOption),
52-
typeof(SupportedExchangesOption),
53-
typeof(TestOption),
54-
typeof(TickerOption),
55-
typeof(TradeHistoryOption),
56-
typeof(WebSocketsOrderbookOption),
57-
typeof(WebSocketsTickersOption),
58-
typeof(WebSocketsTradesOption)
59-
)
61+
Parser
62+
.ParseArguments(args, CommandOptions)
6063
.WithParsed(opt => optionList.Add((BaseOption) opt))
6164
.WithNotParsed(errs => (error, help) = ValidateParseErrors(errs));
6265

@@ -88,7 +91,7 @@ public Program()
8891
return (error, help);
8992
}
9093

91-
private async Task Run(List<BaseOption> actions)
94+
internal async Task Run(List<BaseOption> actions, bool exitOnError = true)
9295
{
9396
foreach (var action in actions)
9497
{
@@ -103,7 +106,8 @@ private async Task Run(List<BaseOption> actions)
103106
catch (Exception e)
104107
{
105108
Console.Error.WriteLine(e.Message);
106-
Environment.Exit(ExitCodeError);
109+
if (exitOnError)
110+
Environment.Exit(ExitCodeError);
107111
return;
108112
}
109113
}

ExchangeSharpConsole/Utilities/ConsoleSessionKeeper.cs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Diagnostics;
3+
using System.Text;
34
using System.Threading;
45

56
namespace ExchangeSharpConsole.Utilities
@@ -9,16 +10,19 @@ public class ConsoleSessionKeeper : IDisposable
910
private readonly Action callback;
1011
private readonly Thread threadCheckKey;
1112
private bool shouldStop;
13+
private readonly bool previousConsoleCtrlCConfig;
1214

1315
public ConsoleSessionKeeper(Action callback = null)
1416
{
1517
this.callback = callback;
18+
previousConsoleCtrlCConfig = Console.TreatControlCAsInput;
19+
Console.TreatControlCAsInput = false;
1620

1721
Console.WriteLine("Press CTRL-C or Q to quit");
1822

1923
threadCheckKey = new Thread(CheckKeyCombination)
2024
{
21-
Name = "console-waiter",
25+
Name = $"console-waiter-{callback?.Method.Name}",
2226
IsBackground = false
2327
};
2428

@@ -29,33 +33,30 @@ public ConsoleSessionKeeper(Action callback = null)
2933

3034
private void CheckKeyCombination()
3135
{
32-
ConsoleKeyInfo cki;
33-
do
36+
using var stdin = Console.OpenStandardInput();
37+
var charArr = new byte[2];
38+
39+
while (stdin.Read(charArr, 0, 2) > 0)
3440
{
35-
while (Console.KeyAvailable == false)
36-
{
37-
if (shouldStop)
38-
{
39-
return;
40-
}
41+
if (shouldStop)
42+
return;
4143

42-
Thread.Sleep(100);
43-
Thread.Yield();
44-
}
44+
var c = Encoding.UTF8.GetChars(charArr)[0];
45+
if (c == 'q' || c == 'Q')
46+
break;
47+
}
4548

46-
cki = Console.ReadKey(true);
47-
} while (!(cki.Key == ConsoleKey.Q || cki.Key == ConsoleKey.Escape));
49+
if (shouldStop)
50+
return;
4851

4952
Debug.WriteLine("Q pressed.");
50-
callback?.Invoke();
5153
Dispose();
5254
}
5355

5456
private void OnConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs args)
5557
{
5658
Debug.WriteLine("CTRL-C pressed.");
5759
args.Cancel = true;
58-
callback?.Invoke();
5960
Dispose();
6061
}
6162

@@ -64,7 +65,9 @@ public void Dispose()
6465
if (shouldStop)
6566
return;
6667

68+
callback?.Invoke();
6769
Console.CancelKeyPress -= OnConsoleOnCancelKeyPress;
70+
Console.TreatControlCAsInput = previousConsoleCtrlCConfig;
6871
// this does not work on .net core
6972
// threadCheckKey.Abort();
7073
shouldStop = true;

0 commit comments

Comments
 (0)