Skip to content

Commit

Permalink
Formatting: handle XTPUSHSGR/XTPOPSGR sequences
Browse files Browse the repository at this point in the history
The RawUI's LengthInBufferCells needs to treat VT control sequences as
zero-width for the purpose of determining string length.

Previously, it only handled SGR (Select Graphics Rendition) sequences
(which do things like set fg color, bg color, etc.).

I'm currently adding support for some new control sequences in the
microsoft/terminal project: XTPUSHSGR and XTPOPSGR.

Initial WIP PR [here](microsoft/terminal#1978).

Summarized descriptions of XTPUSHSGR and XTPOPSGR from XTerm's
[ctlseqs](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
documentation:

```
CSI # {
CSI Ps ; Ps # {
          Push video attributes onto stack (XTPUSHSGR), xterm.

CSI # }   Pop video attributes from stack (XTPOPSGR), xterm.  Popping
          restores the video-attributes which were saved using XTPUSHSGR
          to their previous state.
CSI # p
CSI Ps ; Ps # p
          Push video attributes onto stack (XTPUSHSGR), xterm.  This is
          an alias for CSI # {.

CSI # q   Pop video attributes from stack (XTPOPSGR), xterm.  This is an
          alias for CSI # }.
```

The scenario enabled by these sequences is composability (see [Issue
1796](microsoft/terminal#1796)).

I'd like to support these sequences in PowerShell, similarly to SGR
sequences, to enable better SGR content composability.
  • Loading branch information
jazzdelightsme committed Jul 23, 2019
1 parent b768d87 commit 5a09517
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 19 deletions.
75 changes: 56 additions & 19 deletions src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2666,38 +2666,75 @@ internal static void SetConsoleTextAttribute(ConsoleHandle consoleHandle, WORD a
// Return the length of a VT100 control sequence character in str starting
// at the given offset.
//
// This code only handles the most common formatting sequences, which are
// all of the pattern:
// ESC '[' digits+ (';' digits)* 'm'
// This code only handles the following formatting sequences, corresponding to
// the patterns:
// CSI params? 'm' // SGR: Select Graphics Rendition
// CSI params? '#' [{}pq] // XTPUSHSGR ('{'), XTPOPSGR ('}'), or their aliases ('p' and 'q')
//
// There are many other VT100 escape sequences, but this simple pattern
// is sufficient for our formatting system. We won't handle cursor movements
// or other attempts at animation.
// Where:
// CSI: ESC '[' OR \x009b // (C0 or C1 CSI)
// params: digit+ (';' params)?
//
// Note that offset is adjusted past the escape sequence.
// There are many other VT100 escape sequences, but these text attribute sequences
// (color-related, underline, etc.) are sufficient for our formatting system. We
// won't handle cursor movements or other attempts at animation.
//
// Note that offset is adjusted past the escape sequence, or at least one
// character forward if there is no escape sequence at the specified position.
internal static int ControlSequenceLength(string str, ref int offset)
{
var start = offset;
if (str[offset++] != (char)0x1B)

// First, check for the CSI:
if ((str[offset] == (char) 0x1b) && (str.Length > (offset + 1)) && (str[offset + 1] == '[')) // C0 CSI
{
offset += 2;
}
else if (str[offset] == (char) 0x9b) // C1 CSI
{
offset += 1;
}
else
{
// We need to skip over at least the current character.
offset += 1;
return 0;
}

if (offset >= str.Length || str[offset] != '[')
if (offset >= str.Length)
return 0;

offset += 1;
while (offset < str.Length)
{
var c = str[offset++];
if (c == 'm')
break;
// Next, handle possible numeric arguments:
char c;
do {
c = str[offset++];
} while ((offset < str.Length) && (char.IsDigit(c) || c == ';'));

if (char.IsDigit(c) || c == ';')
continue;
// Finally, handle the command characters for the specific sequences we
// handle:
if (c == 'm')
{
// SGR: Select Graphics Rendition
return offset - start;
}
else if (c == '#')
{
// Maybe XTPUSHSGR or XTPOPSGR, but we need to read another char. Offset
// is already positioned on the next char (or past the end).
if (offset >= str.Length)
return 0;

return 0;
c = str[offset++];
if ((c == '{') || // XTPUSHSGR
(c == '}') || // XTPOPSGR
(c == 'p') || // alias for XTPUSHSGR
(c == 'q')) // alias for XTPOPSGR
{
return offset - start;
}
}

return offset - start;
return 0;
}

/// <summary>
Expand Down
19 changes: 19 additions & 0 deletions test/powershell/Host/ConsoleHost.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ public enum ShowWindowCommands : int
Describe "Console host api tests" -Tag CI {
Context "String escape sequences" {
$esc = [char]0x1b
$csi = [char]0x9b
$testCases =
@{InputObject = "abc"; Length = 3; Name = "No escapes"},
@{InputObject = "${esc} [31mabc"; Length = 9; Name = "Malformed escape - extra space"},
Expand All @@ -807,11 +808,29 @@ Describe "Console host api tests" -Tag CI {
{
@{InputObject = "$esc[31mabc"; Length = 3; Name = "Escape at start"}
@{InputObject = "$esc[31mabc$esc[0m"; Length = 3; Name = "Escape at start and end"}
@{InputObject = "${csi}31mabc"; Length = 3; Name = "C1 CSI at start"}
@{InputObject = "${csi}31mabc${csi}0m"; Length = 3; Name = "C1 CSI at start and end"}
@{InputObject = "abc${csi}m"; Length = 3; Name = "C1 CSI, no params"}
@{InputObject = "abc${csi}#{"; Length = 3; Name = "C1 CSI, XTPUSHSGR"}
@{InputObject = "abc${csi}#}"; Length = 3; Name = "C1 CSI, XTPOPSGR"}
@{InputObject = "abc${csi}#p"; Length = 3; Name = "C1 CSI, XTPUSHSGR (alias)"}
@{InputObject = "abc${csi}#q"; Length = 3; Name = "C1 CSI, XTPOPSGR (alias)"}
@{InputObject = "abc${esc}[0#p"; Length = 3; Name = "XTPUSHSGR, with param"}
@{InputObject = "${esc}[0;1#qabc"; Length = 3; Name = "XTPOPSGR, with multiple params"}
}
else
{
@{InputObject = "$esc[31mabc"; Length = 8; Name = "Escape at start - no virtual term support"}
@{InputObject = "$esc[31mabc$esc[0m"; Length = 12; Name = "Escape at start and end - no virtual term support"}
@{InputObject = "${csi}31mabc"; Length = 7; Name = "C1 CSI at start - no virtual term support - no virtual term support"}
@{InputObject = "${csi}31mabc${csi}0m"; Length = 10; Name = "C1 CSI at start and end - no virtual term support"}
@{InputObject = "abc${csi}m"; Length = 5; Name = "C1 CSI, no params - no virtual term support"}
@{InputObject = "abc${csi}#{"; Length = 6; Name = "C1 CSI, XTPUSHSGR - no virtual term support"}
@{InputObject = "abc${csi}#}"; Length = 6; Name = "C1 CSI, XTPOPSGR - no virtual term support"}
@{InputObject = "abc${csi}#p"; Length = 6; Name = "C1 CSI, XTPUSHSGR (alias) - no virtual term support"}
@{InputObject = "abc${csi}#q"; Length = 6; Name = "C1 CSI, XTPOPSGR (alias) - no virtual term support"}
@{InputObject = "abc${esc}[0#p"; Length = 8; Name = "XTPUSHSGR, with param - no virtual term support"}
@{InputObject = "${esc}[0;1#qabc"; Length = 10; Name = "XTPOPSGR, with multiple params - no virtual term support"}
}

It "Should properly calculate buffer cell width of '<Name>'" -TestCases $testCases {
Expand Down

0 comments on commit 5a09517

Please sign in to comment.