public SecureString ReadSecureLine(CancellationToken cancellationToken) { Console.TreatControlCAsInput = true; int previousInputLength = 0; SecureString secureString = new(); try { bool enterPressed = false; while (!enterPressed && !cancellationToken.IsCancellationRequested) { ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); if (keyInfo.IsCtrlC()) { throw new PipelineStoppedException(); } switch (keyInfo.Key) { case ConsoleKey.Enter: // Stop the while loop so we can realign the cursor // and then return the entered string enterPressed = true; continue; case ConsoleKey.Tab: break; case ConsoleKey.Backspace: if (secureString.Length > 0) { secureString.RemoveAt(secureString.Length - 1); } break; default: if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) { secureString.AppendChar(keyInfo.KeyChar); } break; } // Re-render the secure string characters int currentInputLength = secureString.Length; int consoleWidth = Console.WindowWidth; if (currentInputLength > previousInputLength) { Console.Write('*'); } else if (previousInputLength > 0 && currentInputLength < previousInputLength) { int row = ConsoleProxy.GetCursorTop(cancellationToken); int col = ConsoleProxy.GetCursorLeft(cancellationToken); // Back up the cursor before clearing the character col--; if (col < 0) { col = consoleWidth - 1; row--; } Console.SetCursorPosition(col, row); Console.Write(' '); Console.SetCursorPosition(col, row); } previousInputLength = currentInputLength; } } finally { Console.TreatControlCAsInput = false; } return(secureString); }
public override string ReadLine(CancellationToken cancellationToken) { string inputBeforeCompletion = null; string inputAfterCompletion = null; CommandCompletion currentCompletion = null; int historyIndex = -1; IReadOnlyList <PSObject> currentHistory = null; StringBuilder inputLine = new(); int initialCursorCol = Console.CursorLeft; int initialCursorRow = Console.CursorTop; int currentCursorIndex = 0; Console.TreatControlCAsInput = true; try { while (!cancellationToken.IsCancellationRequested) { ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); // Do final position calculation after the key has been pressed // because the window could have been resized before then int promptStartCol = initialCursorCol; int promptStartRow = initialCursorRow; int consoleWidth = Console.WindowWidth; switch (keyInfo.Key) { case ConsoleKey.Tab: if (currentCompletion is null) { inputBeforeCompletion = inputLine.ToString(); inputAfterCompletion = null; // TODO: This logic should be moved to AstOperations or similar! if (_psesHost.DebugContext.IsStopped) { PSCommand command = new PSCommand() .AddCommand("TabExpansion2") .AddParameter("InputScript", inputBeforeCompletion) .AddParameter("CursorColumn", currentCursorIndex) .AddParameter("Options", null); currentCompletion = _psesHost.InvokePSCommand <CommandCompletion>(command, executionOptions: null, cancellationToken).FirstOrDefault(); } else { currentCompletion = _psesHost.InvokePSDelegate( "Legacy readline inline command completion", executionOptions: null, (pwsh, _) => CommandCompletion.CompleteInput(inputAfterCompletion, currentCursorIndex, options: null, pwsh), cancellationToken); if (currentCompletion.CompletionMatches.Count > 0) { int replacementEndIndex = currentCompletion.ReplacementIndex + currentCompletion.ReplacementLength; inputAfterCompletion = inputLine.ToString( replacementEndIndex, inputLine.Length - replacementEndIndex); } else { currentCompletion = null; } } } CompletionResult completion = currentCompletion?.GetNextResult( !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); if (completion is not null) { currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, completion.CompletionText + inputAfterCompletion, currentCursorIndex, insertIndex: currentCompletion.ReplacementIndex, replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); } continue; case ConsoleKey.LeftArrow: currentCompletion = null; if (currentCursorIndex > 0) { currentCursorIndex = MoveCursorToIndex( promptStartCol, promptStartRow, consoleWidth, currentCursorIndex - 1); } continue; case ConsoleKey.Home: currentCompletion = null; currentCursorIndex = MoveCursorToIndex( promptStartCol, promptStartRow, consoleWidth, 0); continue; case ConsoleKey.RightArrow: currentCompletion = null; if (currentCursorIndex < inputLine.Length) { currentCursorIndex = MoveCursorToIndex( promptStartCol, promptStartRow, consoleWidth, currentCursorIndex + 1); } continue; case ConsoleKey.End: currentCompletion = null; currentCursorIndex = MoveCursorToIndex( promptStartCol, promptStartRow, consoleWidth, inputLine.Length); continue; case ConsoleKey.UpArrow: currentCompletion = null; // TODO: Ctrl+Up should allow navigation in multi-line input if (currentHistory is null) { historyIndex = -1; PSCommand command = new PSCommand() .AddCommand("Get-History"); currentHistory = _psesHost.InvokePSCommand <PSObject>(command, executionOptions: null, cancellationToken); if (currentHistory is not null) { historyIndex = currentHistory.Count; } } if (currentHistory?.Count > 0 && historyIndex > 0) { historyIndex--; currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, (string)currentHistory[historyIndex].Properties["CommandLine"].Value, currentCursorIndex, insertIndex: 0, replaceLength: inputLine.Length); } continue; case ConsoleKey.DownArrow: currentCompletion = null; // The down arrow shouldn't cause history to be loaded, // it's only for navigating an active history array if (historyIndex > -1 && historyIndex < currentHistory.Count && currentHistory?.Count > 0) { historyIndex++; if (historyIndex < currentHistory.Count) { currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, (string)currentHistory[historyIndex].Properties["CommandLine"].Value, currentCursorIndex, insertIndex: 0, replaceLength: inputLine.Length); } else if (historyIndex == currentHistory.Count) { currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, string.Empty, currentCursorIndex, insertIndex: 0, replaceLength: inputLine.Length); } } continue; case ConsoleKey.Escape: currentCompletion = null; historyIndex = currentHistory != null ? currentHistory.Count : -1; currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, string.Empty, currentCursorIndex, insertIndex: 0, replaceLength: inputLine.Length); continue; case ConsoleKey.Backspace: currentCompletion = null; if (currentCursorIndex > 0) { currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, string.Empty, currentCursorIndex, insertIndex: currentCursorIndex - 1, replaceLength: 1, finalCursorIndex: currentCursorIndex - 1); } continue; case ConsoleKey.Delete: currentCompletion = null; if (currentCursorIndex < inputLine.Length) { currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, string.Empty, currentCursorIndex, replaceLength: 1, finalCursorIndex: currentCursorIndex); } continue; case ConsoleKey.Enter: string completedInput = inputLine.ToString(); currentCompletion = null; currentHistory = null; // TODO: Add line continuation support: // - When shift+enter is pressed, or // - When the parse indicates incomplete input //if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) //{ // // TODO: Start a new line! // continue; //} //Parser.ParseInput( // completedInput, // out Token[] tokens, // out ParseError[] parseErrors); //if (parseErrors.Any(e => e.IncompleteInput)) //{ // // TODO: Start a new line! // continue; //} return(completedInput); default: if (keyInfo.IsCtrlC()) { throw new PipelineStoppedException(); } // Normal character input if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) { currentCompletion = null; currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, keyInfo.KeyChar.ToString(), // TODO: Determine whether this should take culture into account currentCursorIndex, finalCursorIndex: currentCursorIndex + 1); } continue; } } } catch (OperationCanceledException) { // We've broken out of the loop } finally { Console.TreatControlCAsInput = false; } // If we break out of the loop without returning (because of the Enter key) // then the readline has been aborted in some way and we should return nothing return(null); }