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

b3b00 / InteractiveCLI / 25244566544

02 May 2026 05:15AM UTC coverage: 90.748% (-4.1%) from 94.849%
25244566544

Pull #14

github

web-flow
Merge 5f98ccd0e into 68b55b5e0
Pull Request #14: Textarea navigation

368 of 426 branches covered (86.38%)

Branch coverage included in aggregate %.

252 of 306 new or added lines in 2 files covered. (82.35%)

2 existing lines in 1 file now uncovered.

1064 of 1152 relevant lines covered (92.36%)

44.4 hits per line

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

91.54
/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; }
130✔
11

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

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

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

21
    private readonly IConsole _console;
22

23
    public Prompt(string invalidInputMessage = null, IConsole console = null)
105✔
24
    {
105✔
25
        InvalidInputMessage = invalidInputMessage;
105✔
26
        _console = console ?? new SystemConsole();
105!
27
    }
105✔
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
        int.TryParse(answer, out var value);
8✔
183
        return value;
8✔
184
    }
8✔
185

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

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

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

279
            _console.WriteError(errorMessage ?? (InvalidInputMessage ?? "Invalid answer."));
11✔
280
            //return new Result<T>() { Ok = false };
281
        }
11✔
282
    }
58✔
283

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

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

312
        (bool ok, string errorMessage) CompoundValidator(string s)
313
        {
11✔
314
            if (validator != null)
11✔
315
            {
3✔
316
                var validation = validator(s);
3✔
317
                if (DoubleValidator(s) && validation.ok)
3!
318
                {
2✔
319
                    return (true,null);
2✔
320
                }
321

322
                return (false, validation.errorMessage);
1✔
323
            }
324

325
            return (DoubleValidator(s),null);
8✔
326
        }
11✔
327

328
        var answer = AskText(label, CompoundValidator);
7✔
329
        return double.Parse(answer, System.Globalization.CultureInfo.InvariantCulture);
7✔
330
    }
7✔
331

332
    public bool AskBool(string label, string[] trueValues, string[] falseValues, Func<string,(bool ok, string errorMessage)>? validator = null)
333
    {
9✔
334
        Func<string, (bool ok, bool value)> tryparse = s =>
9✔
335
        {
24✔
336
            if (trueValues.Contains(s))
24✔
337
            {
9✔
338
                return (true, true);
9✔
339
            }
9✔
340

9✔
341
            if (falseValues.Contains(s))
15✔
342
            {
12✔
343
                return (true, false);
12✔
344
            }
9✔
345

9✔
346
            return (false, false);
3✔
347
        };
33✔
348

349
        (bool ok, string errorMessage) CompoundValidator(string s)
350
        {
351
            var (ok, _tried) = tryparse(s);
352
            if (validator != null && ok)
353
            {
354
                var validation = validator(s);
355
                if (validation.ok && ok)
356
                {
357
                    return (true, null);
358
                }
359

360
                return (false, validation.errorMessage);
361
            }
362

363
            return (ok,null);
364
        }
365

366
        StringBuilder prompt = new StringBuilder();
9✔
367
        prompt.Append(label);
9✔
368
        prompt.Append($"[{string.Join(", ", trueValues)}] or [{string.Join(", ", falseValues)}]");
9✔
369
        string promptValue = AskText(prompt.ToString(), CompoundValidator);
9✔
370

371
        var (isOk, value) = tryparse(promptValue.ToString());
9✔
372
        if (isOk)
9!
373
        {
9✔
374
            return value;
9✔
375
        }
376

377

378
        return false;
×
379
    }
9✔
380

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

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

418
            if (callbacks != null && callbacks.Length > 0)
9✔
419
            {
1✔
420
                foreach (var callback in callbacks)
5✔
421
                {
1✔
422
                    callback(password.ToString());
1✔
423
                }
1✔
424
            }
1✔
425

426

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

464
    }
10✔
465

466
    public string? Select(string label, Func<string, bool, int, string> formatter = null, string[] choices = null, bool isIndexed = false)
467
    {
11✔
468
        interactiveCLI.SelectPrompt select = new interactiveCLI.SelectPrompt(label, choices, formatter, isIndexed, _console);
11✔
469
        var choice = select.Select();
11✔
470
        return choice;
11✔
471
    }
11✔
472

473

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

503
            int originalCursorSize = _console.CursorSize;
11✔
504
            _console.CursorSize = 25;
11✔
505

506
            VirtualConsole vConsole = new VirtualConsole(_console, maxLines);
11✔
507

508
            while (true)
211✔
509
            {
211✔
510
                var key = _console.ReadKey(true);
211✔
511
                
512
                // Finish input with Ctrl+Enter
513
                if (key.Key == finishKey && key.Modifiers == ConsoleModifiers.Control)
211✔
514
                {
10✔
515
                    _console.CursorSize = originalCursorSize;
10✔
516
                    vConsole.Finish();
10✔
517
                    break;
10✔
518
                }
519
                // Cancel with Escape
520
                else if (key.Key == ConsoleKey.Escape)
201✔
521
                {
1✔
522
                    _console.CursorSize = originalCursorSize;
1✔
523
                    vConsole.Finish();
1✔
524
                    return new Result<string>()
1✔
525
                    {
1✔
526
                        IsApplicable = false,
1✔
527
                        Ok = false
1✔
528
                    };
1✔
529
                }
530
                // Toggle Insert/Overwrite mode
531
                else if (key.Key == ConsoleKey.Insert)
200!
UNCOV
532
                {
×
NEW
533
                    vConsole.IsInsertMode = !vConsole.IsInsertMode;
×
NEW
534
                    _console.CursorSize = vConsole.IsInsertMode ? 25 : 100;
×
UNCOV
535
                }
×
536
                // Arrow keys
537
                else if (key.Key == ConsoleKey.LeftArrow) vConsole.MoveLeft();
211✔
538
                else if (key.Key == ConsoleKey.RightArrow) vConsole.MoveRight();
196✔
539
                else if (key.Key == ConsoleKey.UpArrow) vConsole.MoveUp();
183✔
540
                else if (key.Key == ConsoleKey.DownArrow) vConsole.MoveDown();
181!
541
                // Home and End keys
542
                else if (key.Key == ConsoleKey.Home) vConsole.MoveHome(key.Modifiers == ConsoleModifiers.Control);
181!
543
                else if (key.Key == ConsoleKey.End) vConsole.MoveEnd(key.Modifiers == ConsoleModifiers.Control);
181!
544
                // New line with Enter
545
                else if (key.Key == ConsoleKey.Enter) vConsole.Enter();
198✔
546
                // Backspace
547
                else if (key.Key == ConsoleKey.Backspace) vConsole.Backspace();
173✔
548
                // Delete
549
                else if (key.Key == ConsoleKey.Delete) vConsole.Delete();
155!
550
                // Regular character input
551
                else if (!char.IsControl(key.KeyChar)) vConsole.InsertChar(key.KeyChar);
310✔
552
            }
200✔
553

554
            var result = string.Join("\n", vConsole.Lines.Select(l => l.ToString()));
35✔
555

556
            // Validation
557
            if (validator != null)
10✔
558
            {
4✔
559
                var validation = validator(result);
4✔
560
                if (validation.ok)
4✔
561
                {
3✔
562
                    if (callbacks != null && callbacks.Length > 0)
3!
563
                    {
3✔
564
                        foreach (var callback in callbacks)
21✔
565
                        {
6✔
566
                            callback(result);
6✔
567
                        }
6✔
568
                    }
3✔
569
                    
570
                    return new Result<string>()
3✔
571
                    {
3✔
572
                        Value = result,
3✔
573
                        Ok = true,
3✔
574
                        IsApplicable = true
3✔
575
                    };
3✔
576
                }
577
                else
578
                {
1✔
579
                    var errorMessage = validation.errorMessage ?? InvalidInputMessage ?? "Invalid input.";
1!
580
                    _console.WriteError(errorMessage);
1✔
581
                    continue;
1✔
582
                }
583
            }
584
            else
585
            {
6✔
586
                if (callbacks != null && callbacks.Length > 0)
6!
587
                {
×
588
                    foreach (var callback in callbacks)
×
589
                    {
×
590
                        callback(result);
×
591
                    }
×
592
                }
×
593
                
594
                return new Result<string>()
6✔
595
                {
6✔
596
                    Value = result,
6✔
597
                    Ok = true,
6✔
598
                    IsApplicable = true
6✔
599
                };
6✔
600
            }
601
        }
602
    }
10✔
603

604

605

606
    public T AskForm<T>()
607
    {
1✔
608
        FormBuilder<T> formBuilder = new FormBuilder<T>();
1✔
609
        T formBackingData = (T)Activator.CreateInstance(typeof(T));
1✔
610
        var form = formBuilder.Build(formBackingData, this);
1✔
611
        formBackingData = form.Ask();
1✔
612
        return formBackingData;
1✔
613
    }
1✔
614
}
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