using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using static System.ConsoleColor; using static System.ConsoleKey; using static System.StringComparison; namespace InnovEnergy.Lib.Utils; public static class AutoComplete { [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] public static String? ChooseFrom(this String prompt, IEnumerable chooseFrom, String initialInput = "", Int32 nPopUpLines = 7) { if (chooseFrom == null || !chooseFrom.Any()) throw new ArgumentException(nameof(chooseFrom)); var chooseFromSorted = chooseFrom.OrderBy(s => s).ToList(); if (chooseFromSorted.Count == 0 || chooseFromSorted.Any(StringUtils.IsNullOrEmpty)) throw new ArgumentException(nameof(chooseFrom)); var acceptedChars = chooseFromSorted .SelectMany(c => c.ToLower()) .ToHashSet(); var filtered = chooseFromSorted; prompt = prompt.TrimEnd() + " "; var input = initialInput; var selectedLine = 0; var defaultBg = Console.BackgroundColor; var maxLen = chooseFromSorted.Max(n => n.Length); prompt.Write(); // TODO: remove handler when done Console.CancelKeyPress += (_, _) => { ClearPopUp(); BackToInput(); }; while (true) { filtered = chooseFromSorted.Any(e => e.Contains(input, InvariantCultureIgnoreCase)) ? chooseFromSorted.Where(e => e.Contains(input, InvariantCultureIgnoreCase)).ToList() : filtered; Debug.Assert(filtered.Count > 0); var nChoices = Math.Min(nPopUpLines, filtered.Count); ShowInput(input, prompt); ShowPopUp(nChoices); BackToInput(); var keyInfo = Console.ReadKey(true); var key = keyInfo.Key; var keyChar = keyInfo.KeyChar; if (key == UpArrow ) selectedLine = (selectedLine - 1).Modulo(nChoices); else if (key == DownArrow) selectedLine = (selectedLine + 1).Modulo(nChoices); else if (key == Escape) { ClearPopUp(); BackToInput(); return null; } else if (key == Backspace && !input.IsNullOrEmpty()) { input = input.Substring(0, input.Length - 1); selectedLine = 0; } else if (key is Enter or Tab) { var selectedElement = filtered[selectedLine]; ClearPopUp(); BackToBeforeInput(); return selectedElement; } else if (acceptedChars.Contains(Char.ToLower(keyChar))) { input += keyInfo.KeyChar; selectedLine = 0; } } void BackToInput() { Console.SetCursorPosition(prompt.Length + input.Length, Console.CursorTop - nPopUpLines - 1); } void BackToBeforeInput() { Console.SetCursorPosition(0, Console.CursorTop - nPopUpLines - 2); } void ClearPopUp() { foreach (var _ in Enumerable.Range(0, nPopUpLines + 1)) "".PadRight(maxLen + prompt.Length).WriteLine(); } void ShowPopUp(Int32 nChoices) { foreach (var i in Enumerable.Range(0, nPopUpLines)) { var line = i < nChoices ? filtered[i] : ""; var hlStart = line.IndexOf(input, InvariantCultureIgnoreCase); var hlEnd = hlStart + input.Length; Console.BackgroundColor = i == selectedLine ? DarkGray : defaultBg; "".PadLeft(prompt.Length).Write(defaultBg, defaultBg); if (hlStart >= 0) DrawLineWithHighlight(line, hlStart, hlEnd); else DrawLineWithoutHighlight(line); // nothing to highlight "".PadRight(maxLen - line.Length).WriteLine(defaultBg, defaultBg); } Console.BackgroundColor = defaultBg; void DrawLineWithHighlight(String line, Int32 hlStart, Int32 hlEnd) { line.Substring(0, hlStart).Write(Cyan); line.Substring(hlStart, input.Length).Write(Yellow); line.Substring(hlEnd).Write(Cyan); } void DrawLineWithoutHighlight(String line) { line.Write(Cyan); } } } private static void ShowInput(String inp, String prompt) { const String instructions = "type to narrow selection..."; Console.CursorLeft = prompt.Length; if (inp.IsNullOrEmpty()) instructions.WriteLine(Gray); else inp.PadRight(Math.Max(instructions.Length, inp.Length + 1)).WriteLine(Cyan); } }