diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/PendingProgress.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/PendingProgress.cs index 89901b18126..0c38f35960f 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/PendingProgress.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/PendingProgress.cs @@ -516,7 +516,11 @@ internal override Visit(ProgressNode node, ArrayList unused, int unusedToo) { node.Age = Math.Min(node.Age + 1, Int32.MaxValue - 1); - node.Style = ProgressNode.RenderStyle.FullPlus; + + node.Style = ProgressNode.IsMinimalProgressRenderingEnabled() + ? ProgressNode.RenderStyle.Ansi + : node.Style = ProgressNode.RenderStyle.FullPlus; + return true; } } @@ -582,6 +586,13 @@ internal override } ArrayList result = new ArrayList(); + + if (ProgressNode.IsMinimalProgressRenderingEnabled()) + { + RenderHelper(result, _topLevelNodes, indentation: 0, maxWidth, rawUI); + return (string[])result.ToArray(typeof(string)); + } + string border = StringUtil.Padding(maxWidth); result.Add(border); diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs index 438ea03c669..ca2694bcabc 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs @@ -6,6 +6,9 @@ using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Internal; +using System.Text; + +using Microsoft.PowerShell.Commands.Internal.Format; using Dbg = System.Management.Automation.Diagnostics; @@ -20,7 +23,7 @@ namespace Microsoft.PowerShell ProgressNode : ProgressRecord { /// - /// Indicates the various layouts for rendering a particular node. Each style is progressively less terse. + /// Indicates the various layouts for rendering a particular node. /// internal enum @@ -40,6 +43,11 @@ namespace Microsoft.PowerShell /// The node will be displayed the same as Full, plus, the whole StatusDescription and CurrentOperation will be displayed (in multiple lines if needed). /// FullPlus = 4, + + /// + /// The node will be displayed using ANSI escape sequences. + /// + Ansi = 5, } /// @@ -56,7 +64,11 @@ namespace Microsoft.PowerShell this.PercentComplete = Math.Min(record.PercentComplete, 100); this.SecondsRemaining = record.SecondsRemaining; this.RecordType = record.RecordType; - this.Style = RenderStyle.FullPlus; + + this.Style = IsMinimalProgressRenderingEnabled() + ? RenderStyle.Ansi + : this.Style = RenderStyle.FullPlus; + this.SourceId = sourceId; } @@ -98,6 +110,9 @@ namespace Microsoft.PowerShell case RenderStyle.Minimal: RenderMinimal(strCollection, indentation, maxWidth, rawUI); break; + case RenderStyle.Ansi: + RenderAnsi(strCollection, indentation, maxWidth); + break; case RenderStyle.Invisible: // do nothing break; @@ -336,6 +351,88 @@ private static void RenderFullDescription(string description, string indent, int maxWidth)); } + internal static bool IsMinimalProgressRenderingEnabled() + { + return ExperimentalFeature.IsEnabled(ExperimentalFeature.PSAnsiProgressFeatureName) && PSStyle.Instance.Progress.View == ProgressView.Minimal; + } + + /// + /// Renders a node in the "ANSI" style. + /// + /// + /// List of strings to which the node's rendering will be appended. + /// + /// + /// The indentation level in chars at which the node should be rendered. + /// + /// + /// The maximum number of chars that the rendering is allowed to consume. + /// + private + void + RenderAnsi(ArrayList strCollection, int indentation, int maxWidth) + { + string indent = StringUtil.Padding(indentation); + string secRemain = string.Empty; + if (SecondsRemaining >= 0) + { + secRemain = SecondsRemaining.ToString() + "s"; + } + + int secRemainLength = secRemain.Length + 1; + + // limit progress bar to 120 chars as no need to render full width + if (PSStyle.Instance.Progress.MaxWidth > 0 && maxWidth > PSStyle.Instance.Progress.MaxWidth) + { + maxWidth = PSStyle.Instance.Progress.MaxWidth; + } + + // 4 is for the extra space and square brackets below and one extra space + int barWidth = maxWidth - Activity.Length - indentation - 4; + + var sb = new StringBuilder(); + int padding = maxWidth + PSStyle.Instance.Progress.Style.Length + PSStyle.Instance.Reverse.Length + PSStyle.Instance.ReverseOff.Length; + sb.Append(PSStyle.Instance.Reverse); + + if (StatusDescription.Length > barWidth - secRemainLength) + { + sb.Append(StatusDescription.Substring(0, barWidth - secRemainLength - 1)); + sb.Append(PSObjectHelper.Ellipsis); + } + else + { + sb.Append(StatusDescription); + } + + sb.Append(string.Empty.PadRight(barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength)); + sb.Append(secRemain); + + if (PercentComplete > 0 && PercentComplete < 100) + { + int barLength = PercentComplete * barWidth / 100; + if (barLength >= barWidth) + { + barLength = barWidth - 1; + } + + sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff); + } + else + { + sb.Append(PSStyle.Instance.ReverseOff); + } + + strCollection.Add( + StringUtil.Format( + "{0}{1}{2} [{3}]{4}", + indent, + PSStyle.Instance.Progress.Style, + Activity, + sb.ToString(), + PSStyle.Instance.Reset) + .PadRight(padding)); + } + /// /// The nodes that have this node as their parent. /// @@ -396,6 +493,9 @@ internal int LinesRequiredMethod(PSHostRawUserInterface rawUi, int maxWidth) case RenderStyle.Invisible: return 0; + case RenderStyle.Ansi: + return 1; + default: Dbg.Assert(false, "Unknown RenderStyle value"); break; diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressPane.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressPane.cs index 65f0267d0ed..873cfb483c7 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressPane.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressPane.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Management.Automation; using System.Management.Automation.Host; using Dbg = System.Management.Automation.Diagnostics; @@ -70,65 +71,89 @@ class ProgressPane int rows = tempProgressRegion.GetLength(0); int cols = tempProgressRegion.GetLength(1); + if (ProgressNode.IsMinimalProgressRenderingEnabled()) + { + rows = _content.Length; + cols = PSStyle.Instance.Progress.MaxWidth; + if (cols > _bufSize.Width) + { + cols = _bufSize.Width; + } + } + _savedCursor = _rawui.CursorPosition; _location.X = 0; -#if UNIX - _location.Y = _rawui.CursorPosition.Y; - - // if cursor is not on left edge already move down one line - if (_rawui.CursorPosition.X != 0) + if (!Platform.IsWindows || ProgressNode.IsMinimalProgressRenderingEnabled()) { - _location.Y++; - _rawui.CursorPosition = _location; - } + _location.Y = _rawui.CursorPosition.Y; - // if the cursor is at the bottom, create screen buffer space by scrolling - int scrollRows = rows - ((_rawui.BufferSize.Height - 1) - _location.Y); - if (scrollRows > 0) - { - // Scroll the console screen up by 'scrollRows' - var bottomLocation = _location; - bottomLocation.Y = _rawui.BufferSize.Height; - _rawui.CursorPosition = bottomLocation; - for (int i = 0; i < scrollRows; i++) + // if cursor is not on left edge already move down one line + if (_rawui.CursorPosition.X != 0) { - Console.Out.Write('\n'); + _location.Y++; + _rawui.CursorPosition = _location; } - _location.Y -= scrollRows; - _savedCursor.Y -= scrollRows; - } + // if the cursor is at the bottom, create screen buffer space by scrolling + int scrollRows = rows - ((_rawui.BufferSize.Height - 1) - _location.Y); + if (scrollRows > 0) + { + // Scroll the console screen up by 'scrollRows' + var bottomLocation = _location; + bottomLocation.Y = _rawui.BufferSize.Height - 1; + + _rawui.CursorPosition = bottomLocation; + for (int i = 0; i < scrollRows; i++) + { + Console.Out.Write('\n'); + } + + _location.Y -= scrollRows; + _savedCursor.Y -= scrollRows; + } - // create cleared region to clear progress bar later - _savedRegion = tempProgressRegion; - for (int row = 0; row < rows; row++) - { - for (int col = 0; col < cols; col++) + // create cleared region to clear progress bar later + _savedRegion = tempProgressRegion; + if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSAnsiProgressFeatureName) && PSStyle.Instance.Progress.View != ProgressView.Minimal) { - _savedRegion[row, col].Character = ' '; + for (int row = 0; row < rows; row++) + { + for (int col = 0; col < cols; col++) + { + _savedRegion[row, col].Character = ' '; + } + } } - } - // put cursor back to where output should be - _rawui.CursorPosition = _location; -#else - _location = _rawui.WindowPosition; + // put cursor back to where output should be + _rawui.CursorPosition = _location; + } + else + { + _location = _rawui.WindowPosition; - // We have to show the progress pane in the first column, as the screen buffer at any point might contain - // a CJK double-cell characters, which makes it impractical to try to find a position where the pane would - // not slice a character. Column 0 is the only place where we know for sure we can place the pane. + // We have to show the progress pane in the first column, as the screen buffer at any point might contain + // a CJK double-cell characters, which makes it impractical to try to find a position where the pane would + // not slice a character. Column 0 is the only place where we know for sure we can place the pane. - _location.Y = Math.Min(_location.Y + 2, _bufSize.Height); + _location.Y = Math.Min(_location.Y + 2, _bufSize.Height); - // Save off the current contents of the screen buffer in the region that we will occupy - _savedRegion = - _rawui.GetBufferContents( - new Rectangle(_location.X, _location.Y, _location.X + cols - 1, _location.Y + rows - 1)); -#endif + // Save off the current contents of the screen buffer in the region that we will occupy + _savedRegion = + _rawui.GetBufferContents( + new Rectangle(_location.X, _location.Y, _location.X + cols - 1, _location.Y + rows - 1)); + } - // replace the saved region in the screen buffer with our progress display - _rawui.SetBufferContents(_location, tempProgressRegion); + if (ProgressNode.IsMinimalProgressRenderingEnabled()) + { + WriteContent(); + } + else + { + // replace the saved region in the screen buffer with our progress display + _rawui.SetBufferContents(_location, tempProgressRegion); + } } } @@ -142,12 +167,37 @@ class ProgressPane { if (IsShowing) { - // It would be nice if we knew that the saved region could be kept for the next time Show is called, but alas, - // we have no way of knowing if the screen buffer has changed since we were hidden. By "no good way" I mean that - // detecting a change would be at least as expensive as chucking the savedRegion and rebuilding it. And it would - // be very complicated. + if (ProgressNode.IsMinimalProgressRenderingEnabled()) + { + _rawui.CursorPosition = _location; + int maxWidth = PSStyle.Instance.Progress.MaxWidth; + if (maxWidth > _bufSize.Width) + { + maxWidth = _bufSize.Width; + } + + for (int i = 0; i < _savedRegion.GetLength(1); i++) + { + if (i < _savedRegion.GetLength(1) - 1) + { + Console.Out.WriteLine(string.Empty.PadRight(maxWidth)); + } + else + { + Console.Out.Write(string.Empty.PadRight(maxWidth)); + } + } + } + else + { + // It would be nice if we knew that the saved region could be kept for the next time Show is called, but alas, + // we have no way of knowing if the screen buffer has changed since we were hidden. By "no good way" I mean that + // detecting a change would be at least as expensive as chucking the savedRegion and rebuilding it. And it would + // be very complicated. + + _rawui.SetBufferContents(_location, _savedRegion); + } - _rawui.SetBufferContents(_location, _savedRegion); _savedRegion = null; _rawui.CursorPosition = _savedCursor; } @@ -173,8 +223,8 @@ class ProgressPane int maxWidth = _bufSize.Width; int maxHeight = Math.Max(5, _rawui.WindowSize.Height / 3); - string[] contents = pendingProgress.Render(maxWidth, maxHeight, _rawui); - if (contents == null) + _content = pendingProgress.Render(maxWidth, maxHeight, _rawui); + if (_content == null) { // There's nothing to show. @@ -183,9 +233,21 @@ class ProgressPane return; } - // NTRAID#Windows OS Bugs-1061752-2004/12/15-sburns should read a skin setting here... + BufferCell[,] newRegion; + if (ProgressNode.IsMinimalProgressRenderingEnabled()) + { + // Legacy progress rendering relies on a BufferCell which defines a character, foreground color, and background color + // per cell. This model doesn't work with ANSI escape sequences. However, there is existing logic on rendering that + // relies on the existence of the BufferCell to know if something has been rendered previously. Here we are creating + // an empty BufferCell, but using the second dimension to capture the number of rows so that we can clear that many + // elsewhere in Hide(). + newRegion = new BufferCell[0, _content.Length]; + } + else + { + newRegion = _rawui.NewBufferCellArray(_content, _ui.ProgressForegroundColor, _ui.ProgressBackgroundColor); + } - BufferCell[,] newRegion = _rawui.NewBufferCellArray(contents, _ui.ProgressForegroundColor, _ui.ProgressBackgroundColor); Dbg.Assert(newRegion != null, "NewBufferCellArray has failed!"); if (_progressRegion == null) @@ -223,8 +285,40 @@ class ProgressPane } else { - _rawui.SetBufferContents(_location, _progressRegion); + if (ProgressNode.IsMinimalProgressRenderingEnabled()) + { + WriteContent(); + } + else + { + _rawui.SetBufferContents(_location, _progressRegion); + } + } + } + } + + private void WriteContent() + { + if (_content is not null) + { + Console.CursorVisible = false; + var currentPosition = _rawui.CursorPosition; + _rawui.CursorPosition = _location; + + for (int i = 0; i < _content.Length; i++) + { + if (i < _content.Length - 1) + { + Console.Out.WriteLine(_content[i]); + } + else + { + Console.Out.Write(_content[i]); + } } + + _rawui.CursorPosition = currentPosition; + Console.CursorVisible = true; } } @@ -233,6 +327,7 @@ class ProgressPane private Size _bufSize; private BufferCell[,] _savedRegion; private BufferCell[,] _progressRegion; + private string[] _content; private readonly PSHostRawUserInterface _rawui; private readonly ConsoleHostUserInterface _ui; } diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs index d881f551b31..d9763164679 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs @@ -272,6 +272,10 @@ internal static IEnumerable GetFormatData() "System.Management.Automation.PSStyle+FormattingData", ViewsOf_System_Management_Automation_PSStyleFormattingData()); + yield return new ExtendedTypeDefinition( + "System.Management.Automation.PSStyle+ProgressConfiguration", + ViewsOf_System_Management_Automation_PSStyleProgressConfiguration()); + yield return new ExtendedTypeDefinition( "System.Management.Automation.PSStyle+ForegroundColor", ViewsOf_System_Management_Automation_PSStyleForegroundColor()); @@ -2048,6 +2052,9 @@ private static IEnumerable ViewsOf_System_Management_Autom .AddItemScriptBlock(@"""$($_.Formatting.Warning)$($_.Formatting.Warning.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Warning") .AddItemScriptBlock(@"""$($_.Formatting.Verbose)$($_.Formatting.Verbose.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Verbose") .AddItemScriptBlock(@"""$($_.Formatting.Debug)$($_.Formatting.Debug.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Debug") + .AddItemScriptBlock(@"""$($_.Progress.Style)$($_.Progress.Style.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Progress.Style") + .AddItemScriptBlock(@"""$($_.Progress.MaxWidth)""", label: "Progress.MaxWidth") + .AddItemScriptBlock(@"""$($_.Progress.View)""", label: "Progress.View") .AddItemScriptBlock(@"""$($_.Foreground.Black)$($_.Foreground.Black.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.Black") .AddItemScriptBlock(@"""$($_.Foreground.White)$($_.Foreground.White.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.White") .AddItemScriptBlock(@"""$($_.Foreground.DarkGray)$($_.Foreground.DarkGray.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.DarkGray") @@ -2099,6 +2106,18 @@ private static IEnumerable ViewsOf_System_Management_Autom .EndList()); } + private static IEnumerable ViewsOf_System_Management_Automation_PSStyleProgressConfiguration() + { + yield return new FormatViewDefinition("System.Management.Automation.PSStyle+ProgressConfiguration", + ListControl.Create() + .StartEntry() + .AddItemScriptBlock(@"""$($_.Style)$($_.Style.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Style") + .AddItemProperty(@"MaxWidth") + .AddItemProperty(@"View") + .EndEntry() + .EndList()); + } + private static IEnumerable ViewsOf_System_Management_Automation_PSStyleForegroundColor() { yield return new FormatViewDefinition("System.Management.Automation.PSStyle+ForegroundColor", diff --git a/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs b/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs index 9d6876c44e0..aa00bdf9e18 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs @@ -23,16 +23,28 @@ public enum OutputRendering } #endregion OutputRendering + /// + /// Defines the options for views of progress rendering. + /// + public enum ProgressView + { + /// Render progress using minimal space. + Minimal = 0, + + /// Classic rendering of progress. + Classic = 1, + } + #region PSStyle /// /// Contains configuration for how PowerShell renders text. /// - public class PSStyle + public sealed class PSStyle { /// /// Contains foreground colors. /// - public class ForegroundColor + public sealed class ForegroundColor { /// /// Gets the color black. @@ -147,7 +159,7 @@ public string FromRgb(int rgb) /// /// Contains background colors. /// - public class BackgroundColor + public sealed class BackgroundColor { /// /// Gets the color black. @@ -259,10 +271,31 @@ public string FromRgb(int rgb) } } + /// + /// Contains configuration for the progress bar visualization. + /// + public sealed class ProgressConfiguration + { + /// + /// Gets or sets the style for progress bar. + /// + public string Style { get; set; } = "\x1b[33;1m"; + + /// + /// Gets or sets the max width of the progress bar. + /// + public int MaxWidth { get; set; } = 120; + + /// + /// Gets or sets the view for progress bar. + /// + public ProgressView View { get; set; } = ProgressView.Minimal; + } + /// /// Contains formatting styles for steams and objects. /// - public class FormattingData + public sealed class FormattingData { /// /// Gets or sets the accent style for formatting. @@ -391,6 +424,11 @@ public string FormatHyperlink(string text, Uri link) /// public FormattingData Formatting { get; } + /// + /// Gets the configuration for progress rendering. + /// + public ProgressConfiguration Progress { get; } + /// /// Gets foreground colors. /// @@ -406,6 +444,7 @@ public string FormatHyperlink(string text, Uri link) private PSStyle() { Formatting = new FormattingData(); + Progress = new ProgressConfiguration(); Foreground = new ForegroundColor(); Background = new BackgroundColor(); } diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index f1be0c5acc8..07fb6ed59c2 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -21,6 +21,7 @@ public class ExperimentalFeature #region Const Members internal const string EngineSource = "PSEngine"; + internal const string PSAnsiProgressFeatureName = "PSAnsiProgress"; #endregion @@ -129,6 +130,9 @@ static ExperimentalFeature() new ExperimentalFeature( name: "PSAnsiRendering", description: "Enable $PSStyle variable to control ANSI rendering of strings"), + new ExperimentalFeature( + name: PSAnsiProgressFeatureName, + description: "Enable lightweight progress bar that leverages ANSI codes for rendering"), }; EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures);