• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

b3b00 / InteractiveCLI / 24891330001

24 Apr 2026 01:12PM UTC coverage: 94.584% (+8.2%) from 86.401%
24891330001

push

github

b3b00
unit tests, test coverage and doc

312 of 344 branches covered (90.7%)

Branch coverage included in aggregate %.

20 of 28 new or added lines in 3 files covered. (71.43%)

1 existing line in 1 file now uncovered.

858 of 893 relevant lines covered (96.08%)

16.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.14
/src/interactiveCLI/Prompt.cs
1
using System.Text;
2
using interactiveCLI;
3
using interactiveCLI.forms;
4

5

6
namespace interactiveCLI;
7

8
public class Result<T>
9
{
10
    public T Value { get; set; }
122✔
11

12
    public bool Ok { get; set; }
112✔
13

14
    public bool IsApplicable { get; set; } = true;
137✔
15
}
16

17
public class Prompt
18
{
19
    public string InvalidInputMessage { get; set; }
173✔
20

21
    private readonly IConsole _console;
22

23
    public Prompt(string invalidInputMessage = null, IConsole console = null)
102✔
24
    {
102✔
25
        InvalidInputMessage = invalidInputMessage;
102✔
26
        _console = console ?? new SystemConsole();
102!
27
    }
102✔
28

29
    public string AskText(string label, Func<string,(bool ok, string errorMessage)> validator = null, string pattern = null,
30
        Predicate<(int, char)>? charValidator = null)
31
    {
56✔
32
        _console.WriteLine();
56✔
33
        string answer = null;
56✔
34
        if (!string.IsNullOrWhiteSpace(pattern) && pattern.Contains("_"))
56✔
35
        {
8✔
36
            answer = ReadPatternCopilot(pattern, charValidator);
8✔
37
        }
8✔
38
        else
39
        {
48✔
40
            answer = _console.ReadLine();
48✔
41
        }
48✔
42

43
        while (true)
68✔
44
        {
68✔
45
            var errorMessage = InvalidInputMessage ?? "Invalid answer.";
68✔
46
            if (validator != null)
68✔
47
            {
53✔
48
                var validation = validator(answer);
53✔
49
                if (validation.ok)
53✔
50
                {
41✔
51
                    return answer;
41✔
52
                }
53
                else
54
                {
12✔
55
                    if (!string.IsNullOrWhiteSpace(validation.errorMessage))
12✔
56
                    {
4✔
57
                        errorMessage = validation.errorMessage;
4✔
58
                    }
4✔
59
                    errorMessage ??= validation.errorMessage;
12!
60
                }
12✔
61
            }
12✔
62
            else
63
            {
15✔
64
                return answer;
15✔
65
            }
66

67
            _console.WriteError(errorMessage);
12✔
68
            answer = AskText(label, validator, pattern);
12✔
69
        }
12✔
70
    }
56✔
71

72
    /// <summary>
73
    /// This method has been generated with Github Copilot.
74
    /// It displays a pattern that he user must fill. (ex __/__/____ for a date following the format dd/MM/yyyy)
75
    /// </summary>
76
    /// <param name="pattern">A pattern where `_` are free space and other chars are constant.</param>
77
    /// <returns>the entered string</returns>
78
    public string ReadPatternCopilot(string pattern, Predicate<(int position, char c)>? isAllowed = null)
79
    {
11✔
80
        char[] buffer = pattern.ToCharArray();
11✔
81
        int[] editableIndexes = new int[pattern.Count(c => c == '_')];
84✔
82
        int idx = 0;
11✔
83
        for (int i = 0; i < pattern.Length; i++)
168✔
84
            if (pattern[i] == '_')
73✔
85
                editableIndexes[idx++] = i;
58✔
86

87
        int current = 0;
11✔
88
        _console.Write(pattern);
11✔
89
        _console.SetCursorPosition(editableIndexes[0], _console.CursorTop);
11✔
90

91
        while (true)
78✔
92
        {
78✔
93
            var key = _console.ReadKey(true);
78✔
94

95
            if (key.Key == ConsoleKey.Escape)
78✔
96
            {
2✔
97
                _console.WriteLine("ESC");
2✔
98
                return null;
2✔
99
            }
100

101
            if (key.Key == ConsoleKey.Backspace)
76✔
102
            {
3✔
103
                if (current > 0)
3✔
104
                {
2✔
105
                    if (current != editableIndexes.Length)
2✔
106
                    {
1✔
107
                        current--;
1✔
108
                        buffer[editableIndexes[current]] = '_';
1✔
109
                        _console.SetCursorPosition(editableIndexes[current], _console.CursorTop);
1✔
110
                        _console.Write('_');
1✔
111
                        _console.SetCursorPosition(editableIndexes[current], _console.CursorTop);
1✔
112
                    }
1✔
113
                    else
114
                    {
1✔
115
                        int index = editableIndexes[current - 1];
1✔
116
                        buffer[index] = '_';
1✔
117
                        _console.SetCursorPosition(index, _console.CursorTop);
1✔
118
                        _console.Write('_');
1✔
119
                        _console.SetCursorPosition(index, _console.CursorTop);
1✔
120
                        current--;
1✔
121
                    }
1✔
122
                }
2✔
123
            }
3✔
124
            else if (key.Key == ConsoleKey.LeftArrow && current > 0)
73✔
125
            {
6✔
126
                current--;
6✔
127
                _console.SetCursorPosition(editableIndexes[current], _console.CursorTop);
6✔
128
            }
6✔
129
            else if (key.Key == ConsoleKey.RightArrow && current < editableIndexes.Length)
67✔
130
            {
1✔
131
                current++;
1✔
132
                if (current < editableIndexes.Length)
1✔
133
                    _console.SetCursorPosition(editableIndexes[current], _console.CursorTop);
1✔
134
            }
1✔
135
            else if (key.Key == ConsoleKey.Enter)
66✔
136
            {
9✔
137
                break;
9✔
138
            }
139
            else if (!char.IsControl(key.KeyChar) && current < editableIndexes.Length)
57!
140
            {
57✔
141
                int pos = editableIndexes[current];
57✔
142
                if (isAllowed == null || isAllowed((pos, key.KeyChar)))
57✔
143
                {
54✔
144
                    buffer[pos] = key.KeyChar;
54✔
145
                    _console.SetCursorPosition(pos, _console.CursorTop);
54✔
146
                    _console.Write(key.KeyChar);
54✔
147
                    current++;
54✔
148
                    if (current < editableIndexes.Length)
54✔
149
                        _console.SetCursorPosition(editableIndexes[current], _console.CursorTop);
42✔
150
                }
54✔
151
            }
57✔
152
        }
67✔
153

154
        _console.WriteLine();
9✔
155
        return new string(buffer);
9✔
156
    }
11✔
157

158

159
    public int AskInt(string label, Func<string,(bool ok, string errorMessage)>? validator = null)
160
    {
8✔
161
        bool IntValidator(string s)
162
        {
16✔
163
            return int.TryParse(s, out var x);
16✔
164
        }
16✔
165

166
        (bool ok, string errorMessage) CompoundValidator(string s)
167
        {
16✔
168
            if (validator != null)
16✔
169
            {
3✔
170
                var validation = validator(s);
3✔
171
                if (IntValidator(s) && validation.ok)
3!
172
                {
2✔
173
                    return (true, null);
2✔
174
                }
175
                return (false,validation.errorMessage);
1✔
176
            }
177

178
            return (IntValidator(s),null);
13✔
179
        }
16✔
180

181
        var answer = AskText(label, CompoundValidator);
8✔
182
        while (true)
8✔
183
        {
8✔
184
            if (int.TryParse(answer, out var value))
8!
185
            {
8✔
186
                return value;
8✔
187
            }
UNCOV
188
        }
×
189
    }
8✔
190

191
    public Result<T> Ask<T>(string label, string pattern = null,string[] possibleValues = null, Func<string,(bool ok, string errorMessage)>? validator = null,
192
        Func<string, T>? converter = null, Func<string[]> dataSource = null, Predicate<(int, char)>? charValidator = null,
193
        Func<bool> condition=null, bool isIndexed=false, params Action<T>[] callbacks)
194
    {
58✔
195
        if (condition != null && !condition())
58✔
196
        {
6✔
197
            return new  Result<T> { IsApplicable = false };
6✔
198
        } 
199
        
200
        while (true)
63✔
201
        {
63✔
202
            _console.Write(label);
63✔
203
            string input = null;
63✔
204
            if ((possibleValues != null && possibleValues.Length >= 2) || (dataSource !=null))
63✔
205
            {
9✔
206
                if ((possibleValues == null || possibleValues.Length < 2) && dataSource != null)
9✔
207
                {
7✔
208
                    possibleValues = dataSource();
7✔
209
                }
7✔
210
                input = Select(label, choices: possibleValues, isIndexed:isIndexed);
9✔
211
            }
9✔
212
            else if (!string.IsNullOrEmpty(pattern))
54✔
213
            {
4✔
214
                input = AskText(label,validator,pattern,charValidator:charValidator);
4✔
215
            }
4✔
216
            else if ((typeof(T) == typeof(bool) || typeof(T) == typeof(Boolean)) 
50✔
217
                     && (charValidator == null && validator == null && converter == null))
50✔
218
            {
2✔
219
                input = Check<T>(label, validator);
2✔
220
            }
2✔
221
            else
222
            {
48✔
223
                input = _console.ReadLine();
48✔
224
            }
48✔
225

226
            var typeConverter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(T));
63✔
227
            bool isValid = false;
63✔
228
            string errorMessage = null;
63✔
229
            try
230
            {
63✔
231
                if (validator != null)
63✔
232
                {
28✔
233
                    var validation = validator(input);
28✔
234
                    isValid = validation.ok;
28✔
235
                    errorMessage = validation.errorMessage;
28✔
236
                }
28✔
237
                else
238
                {
35✔
239
                    if (typeConverter != null)
35✔
240
                    {
35✔
241
                        isValid = typeConverter.CanConvertFrom(typeof(string));
35✔
242
                    }
35✔
243
                }
35✔
244
            }
63✔
245
            catch (ArgumentException e)
×
246
            {
×
247
                _console.WriteError(e.Message);
×
248
                isValid = false;
×
249
            }
×
250

251
            if (isValid)
63✔
252
            {
54✔
253
                if (converter != null)
54✔
254
                {
11✔
255
                    return new Result<T>()
11✔
256
                        { Ok = true, Value = converter(input) };
11✔
257
                }
258
                else
259
                {
43✔
260
                    try
261
                    {
43✔
262
                        if (typeConverter != null)
43!
263
                        {
43✔
264
                            T convertedValue = (T)typeConverter.ConvertFrom(input);
43✔
265
                            if (callbacks != null && callbacks.Length > 0)
41✔
266
                            {
4✔
267
                                foreach (var callback in callbacks)
22✔
268
                                {
5✔
269
                                    callback(convertedValue);
5✔
270
                                }
5✔
271
                            }
4✔
272
                            
273
                            return new Result<T>()
41✔
274
                                { Ok = true, Value = convertedValue };
41✔
275
                        }
276
                    }
×
277
                    catch
2✔
278
                    {
2✔
279
                        isValid = false;
2✔
280
                    }
2✔
281
                }
2✔
282
            }
2✔
283

284
            _console.WriteError(errorMessage ?? (InvalidInputMessage ?? "Invalid answer."));
11✔
285
            //return new Result<T>() { Ok = false };
286
        }
11✔
287
    }
58✔
288

289
    private string Check<T>(string label, Func<string, (bool ok, string errorMessage)>? validator)
290
    {
2✔
291
        bool isChecked = false;
2✔
292
        var position = _console.GetCursorPosition();
2✔
293
        _console.Write("❌");
2✔
294
        var key = _console.ReadKey(true);
2✔
295
        while (key.Key !=  ConsoleKey.Enter)
5✔
296
        {
3✔
297
            if (key.Key == ConsoleKey.Spacebar)
3✔
298
            {
3✔
299
                isChecked = !isChecked;
3✔
300
                _console.SetCursorPosition(position.Left, position.Top);
3✔
301
                _console.Write(isChecked ? "✔️" : "❌");
3✔
302
                key = _console.ReadKey(true);
3✔
303
            }
3✔
304
        }
3✔
305
        _console.WriteLine();
2✔
306
        return isChecked.ToString();
2✔
307
    }
2✔
308

309
    public double AskDouble(string label, Func<string,(bool ok, string errorMessage)>? validator = null)
310
    {
7✔
311
        bool DoubleValidator(string s)
312
        {
11✔
313
            return double.TryParse(s, System.Globalization.NumberStyles.Float,
11✔
314
                System.Globalization.CultureInfo.InvariantCulture, out _);
11✔
315
        }
11✔
316

317
        (bool ok, string errorMessage) CompoundValidator(string s)
318
        {
11✔
319
            if (validator != null)
11✔
320
            {
3✔
321
                var validation = validator(s);
3✔
322
                if (DoubleValidator(s) && validation.ok)
3!
323
                {
2✔
324
                    return (true,null);
2✔
325
                }
326

327
                return (false, validation.errorMessage);
1✔
328
            }
329

330
            return (DoubleValidator(s),null);
8✔
331
        }
11✔
332

333
        var answer = AskText(label, CompoundValidator);
7✔
334
        return double.Parse(answer, System.Globalization.CultureInfo.InvariantCulture);
7✔
335
    }
7✔
336

337
    public bool AskBool(string label, string[] trueValues, string[] falseValues, Func<string,(bool ok, string errorMessage)>? validator = null)
338
    {
9✔
339
        Func<string, (bool ok, bool value)> tryparse = s =>
9✔
340
        {
24✔
341
            if (trueValues.Contains(s))
24✔
342
            {
9✔
343
                return (true, true);
9✔
344
            }
9✔
345

9✔
346
            if (falseValues.Contains(s))
15✔
347
            {
12✔
348
                return (true, false);
12✔
349
            }
9✔
350

9✔
351
            return (false, false);
3✔
352
        };
33✔
353

354
        (bool ok, string errorMessage) CompoundValidator(string s)
355
        {
356
            var (ok, _tried) = tryparse(s);
357
            if (validator != null && ok)
358
            {
359
                var validation = validator(s);
360
                if (validation.ok && ok)
361
                {
362
                    return (true, null);
363
                }
364

365
                return (false, validation.errorMessage);
366
            }
367

368
            return (ok,null);
369
        }
370

371
        StringBuilder prompt = new StringBuilder();
9✔
372
        prompt.Append(label);
9✔
373
        prompt.Append($"[{string.Join(", ", trueValues)}] or [{string.Join(", ", falseValues)}]");
9✔
374
        string promptValue = AskText(prompt.ToString(), CompoundValidator);
9✔
375

376
        var (isOk, value) = tryparse(promptValue.ToString());
9✔
377
        if (isOk)
9!
378
        {
9✔
379
            return value;
9✔
380
        }
381

382

383
        return false;
×
384
    }
9✔
385

386
    public Result<string> AskPassword(string label, char hiddenChar = '*', Func<string,(bool ok, string errorMessage)>? validator = null, Func<bool> condition = null, params Action<string>[] callbacks)
387
    {
10✔
388
        if (condition != null && !condition())
10✔
389
        {
1✔
390
            return new Result<string>()
1✔
391
            {
1✔
392
                IsApplicable = false
1✔
393
            };
1✔
394
        }
395

396
        while (true)
9✔
397
        {
9✔
398
            _console.Write(label);
9✔
399
            var password = new StringBuilder();
9✔
400
            while (true)
48✔
401
            {
48✔
402
                var key = _console.ReadKey(true);
48✔
403
                if (key.Key == ConsoleKey.Enter)
48✔
404
                {
9✔
405
                    _console.WriteLine();
9✔
406
                    break;
9✔
407
                }
408
                else if (key.Key == ConsoleKey.Backspace)
39✔
409
                {
2✔
410
                    if (password.Length > 0)
2✔
411
                    {
1✔
412
                        password.Length--;
1✔
413
                        _console.Write("\b \b");
1✔
414
                    }
1✔
415
                }
2✔
416
                else if (!char.IsControl(key.KeyChar))
37✔
417
                {
37✔
418
                    password.Append(key.KeyChar);
37✔
419
                    _console.Write(hiddenChar);
37✔
420
                }
37✔
421
            }
39✔
422

423
            if (callbacks != null && callbacks.Length > 0)
9✔
424
            {
1✔
425
                foreach (var callback in callbacks)
5✔
426
                {
1✔
427
                    callback(password.ToString());
1✔
428
                }
1✔
429
            }
1✔
430

431

432
            if (validator != null)
9✔
433
            {
1✔
434
                var errorMessage = InvalidInputMessage ?? "Invalid answer.";
1✔
435
                if (validator != null)
1!
436
                {
1✔
437
                    var validation = validator(password.ToString());
1✔
438
                    if (validation.ok)
1!
439
                    {
×
440
                        return new Result<string>()
×
441
                        {
×
442
                            Ok = true,
×
443
                            Value = password.ToString(),
×
444
                            IsApplicable = true
×
445
                        };
×
446
                    }
447
                    else
448
                    {
1✔
449
                        errorMessage = validation.errorMessage ?? errorMessage;
1!
450
                        _console.WriteError(errorMessage);
1✔
451
                        return new Result<string>()
1✔
452
                        {
1✔
453
                            Ok = false,
1✔
454
                        };
1✔
455
                    }
456
                }
457
            }
×
458
            else
459
            {
8✔
460
                return new Result<string>()
8✔
461
                {
8✔
462
                    Ok = true,
8✔
463
                    Value = password.ToString(),
8✔
464
                    IsApplicable = true
8✔
465
                };
8✔
466
            }
467
        }
×
468

469
    }
10✔
470

471
    public string? Select(string label, Func<string, bool, int, string> formatter = null, string[] choices = null, bool isIndexed = false)
472
    {
11✔
473
        interactiveCLI.SelectPrompt select = new interactiveCLI.SelectPrompt(label, choices, formatter, isIndexed, _console);
11✔
474
        var choice = select.Select();
11✔
475
        return choice;
11✔
476
    }
11✔
477

478

479
    /// <summary>
480
    /// Prompts the user for multi-line text input, similar to an HTML textarea.
481
    /// Press Enter to create new lines, and Ctrl+Enter or Escape to finish input.
482
    /// </summary>
483
    /// <param name="label">The prompt label to display</param>
484
    /// <param name="maxLines">Maximum number of lines allowed (0 for unlimited)</param>
485
    /// <param name="finishKey">Key combination to finish input (default: Ctrl+Enter)</param>
486
    /// <param name="validator">Optional validator function</param>
487
    /// <param name="condition"></param>
488
    /// <param name="callbacks"></param>
489
    /// <returns>The multi-line text input</returns>
490
    ///
491
    public Result<string> AskMultiLineText(string label,
492
        int maxLines = 0,
493
        ConsoleKey finishKey = ConsoleKey.Enter,
494
        Func<string, (bool ok, string errorMessage)>? validator = null,
495
        Func<bool>? condition=null,
496
        params Action<string>[] callbacks)
497
    {
7✔
498
        if (condition != null && !condition())
7!
NEW
499
        {
×
NEW
500
            return new  Result<string> { IsApplicable = false };
×
501
        } 
502
        
503
        while (true)
7✔
504
        {
7✔
505
            _console.WriteLine(label);
7✔
506
            //Console.WriteLine("(Press Ctrl+Enter to finish, Escape to cancel)");
507

508
            var lines = new List<string>();
7✔
509
            var currentLine = new StringBuilder();
7✔
510
            int startTop = _console.CursorTop;
7✔
511

512
            while (true)
166✔
513
            {
166✔
514
                var key = _console.ReadKey(true);
166✔
515
                // Finish input with Ctrl+Enter
516
                if (key.Key == finishKey && key.Modifiers == ConsoleModifiers.Control)
166✔
517
                {
6✔
518
                    if (currentLine.Length > 0)
6✔
519
                    {
6✔
520
                        lines.Add(currentLine.ToString());
6✔
521
                    }
6✔
522
                    _console.WriteLine();
6✔
523
                    break;
6✔
524
                }
525
                // Cancel with Escape
526
                else if (key.Key == ConsoleKey.Escape)
160✔
527
                {
1✔
528
                    _console.WriteLine();
1✔
529
                    return new Result<string>()
1✔
530
                    {
1✔
531
                        IsApplicable = false,
1✔
532
                        Ok = false
1✔
533
                    };
1✔
534
                }
535
                // New line with Enter
536
                else if (key.Key == ConsoleKey.Enter)
159✔
537
                {
14✔
538
                    if (maxLines > 0 && lines.Count >= maxLines - 1)
14✔
539
                    {
1✔
540
                        continue; // Don't allow more lines than maxLines
1✔
541
                    }
542

543
                    lines.Add(currentLine.ToString());
13✔
544
                    currentLine.Clear();
13✔
545
                    _console.WriteLine();
13✔
546
                }
13✔
547
                // Backspace
548
                else if (key.Key == ConsoleKey.Backspace)
145✔
549
                {
9✔
550
                    if (currentLine.Length > 0)
9✔
551
                    {
8✔
552
                        currentLine.Length--;
8✔
553
                        _console.Write("\b \b");
8✔
554
                    }
8✔
555
                    else if (lines.Count > 0)
1✔
556
                    {
1✔
557
                        // Move to previous line
558
                        currentLine = new StringBuilder(lines[^1]);
1✔
559
                        lines.RemoveAt(lines.Count - 1);
1✔
560
                        _console.SetCursorPosition(0, _console.CursorTop - 1);
1✔
561
                        _console.Write(currentLine.ToString());
1✔
562
                    }
1✔
563
                }
9✔
564
                // Regular character input
565
                else if (!char.IsControl(key.KeyChar))
136✔
566
                {
120✔
567
                    currentLine.Append(key.KeyChar);
120✔
568
                    _console.Write(key.KeyChar);
120✔
569
                }
120✔
570
            }
158✔
571

572
            var result = string.Join("\n", lines);
6✔
573

574
            // Validation
575
            if (validator != null)
6✔
576
            {
3✔
577
                var validation = validator(result);
3✔
578
                if (validation.ok)
3✔
579
                {
2✔
580
                    if (callbacks != null && callbacks.Length > 0)
2!
581
                    {
2✔
582
                        foreach (var callback in callbacks)
14✔
583
                        {
4✔
584
                            callback(result);
4✔
585
                        }
4✔
586
                    }
2✔
587
                    
588
                    return new Result<string>()
2✔
589
                    {
2✔
590
                        Value = result,
2✔
591
                        Ok = true,
2✔
592
                        IsApplicable = true
2✔
593
                    };
2✔
594
                }
595
                else
596
                {
1✔
597
                    var errorMessage = validation.errorMessage ?? InvalidInputMessage ?? "Invalid input.";
1!
598
                    _console.WriteLine(errorMessage);
1✔
599
                    _console.WriteLine();
1✔
600
                    return new Result<string>()
1✔
601
                    {
1✔
602
                        Ok = false,                        
1✔
603
                    };
1✔
604
                    continue;
605
                }
606
            }
607
            else
608
            {
3✔
609
                if (callbacks != null && callbacks.Length > 0)
3!
NEW
610
                {
×
NEW
611
                    foreach (var callback in callbacks)
×
NEW
612
                    {
×
NEW
613
                        callback(result);
×
NEW
614
                    }
×
NEW
615
                }
×
616
                
617
                return new Result<string>()
3✔
618
                {
3✔
619
                    Value = result,
3✔
620
                    Ok = true,
3✔
621
                    IsApplicable = true
3✔
622
                };
3✔
623
            }
624
        }
625
    }
7✔
626

627

628

629
    public T AskForm<T>()
630
    {
1✔
631
        FormBuilder<T> formBuilder = new FormBuilder<T>();
1✔
632
        T formBackingData = (T)Activator.CreateInstance(typeof(T));
1✔
633
        var form = formBuilder.Build(formBackingData, this);
1✔
634
        formBackingData = form.Ask();
1✔
635
        return formBackingData;
1✔
636
    }
1✔
637
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc