diff --git a/src/ConsoleBuffer/Buffer.cs b/src/ConsoleBuffer/Buffer.cs
index e0c3f04..f3d57a3 100644
--- a/src/ConsoleBuffer/Buffer.cs
+++ b/src/ConsoleBuffer/Buffer.cs
@@ -1,4 +1,4 @@
-namespace ConsoleBuffer
+namespace ConsoleBuffer
{
using System;
using System.Collections.Generic;
@@ -15,22 +15,22 @@ public sealed class Buffer : INotifyPropertyChanged
private short cursorX;
private short cursorY;
private int currentChar;
+ ///
+ /// we store X/Y as 0-offset indexes for convenience. escape codes will pass these around as 1-offset (top left is 1,1)
+ /// and we'll translate that nonsense where we have to.
+ ///
public (short X, short Y) CursorPosition => (this.cursorX, this.cursorY);
public bool CursorVisible { get; private set; }
public bool CursorBlink { get; private set; }
- private short bufferTopVisibleLine
- {
- get
- {
- return (short)Math.Max(0, this.lines.Size - this.Height);
- }
- }
- private short currentLine
+ private int topVisibleLine;
+ private int bottomVisibleLine;
+
+ private int CurrentLine
{
get
{
- return (short)(this.bufferTopVisibleLine + this.CursorPosition.Y);
+ return this.topVisibleLine + this.CursorPosition.Y;
}
}
@@ -44,7 +44,14 @@ public Buffer(short width, short height)
this.Width = width;
this.Height = height;
this.CursorVisible = this.CursorBlink = true;
- this.lines.PushBack(new Line());
+ this.cursorX = this.cursorY = 0;
+
+ for (var y = 0; y < this.Height; ++y)
+ {
+ this.lines.PushBack(new Line());
+ }
+ this.topVisibleLine = 0;
+ this.bottomVisibleLine = this.Height - 1;
}
public void Append(byte[] bytes, int length)
@@ -58,8 +65,20 @@ public void Append(byte[] bytes, int length)
switch (this.parser.Append(this.currentChar))
{
case ParserAppendResult.Render:
- this.lines[this.currentLine].Set(this.cursorX, new Character { Glyph = this.currentChar });
- this.cursorX = (short)Math.Min(this.Width - 1, this.cursorX + 1);
+ this.lines[this.CurrentLine].Set(this.cursorX, new Character { Glyph = this.currentChar });
+ ++this.cursorX;
+ if (this.cursorX == this.Width)
+ {
+ this.cursorX = 0;
+ if (this.cursorY == this.Height - 1)
+ {
+ this.ScrollDown();
+ }
+ else
+ {
+ ++this.cursorY;
+ }
+ }
break;
case ParserAppendResult.Complete:
this.ExecuteParserCommand();
@@ -102,10 +121,13 @@ private void ExecuteParserCommand()
this.OnPropertyChanged("Title");
}
break;
+ case Commands.ControlSequence csiCommand:
+ this.HandleControlSequence(csiCommand);
+ break;
case Commands.Unsupported unsupported:
break;
default:
- throw new InvalidOperationException("Unknown command type passed.");
+ throw new InvalidOperationException($"Unknown command type passed: {this.parser.Command?.GetType()}.");
}
}
@@ -127,9 +149,9 @@ private void HandleControlCharacter(Commands.ControlCharacter.ControlCode code)
break;
case Commands.ControlCharacter.ControlCode.FF: // NB: could clear screen with this if we were so inclined. apparently xterm treats this as LF though, let's emulate.
case Commands.ControlCharacter.ControlCode.LF:
- if (this.currentLine == this.lines.Size - 1)
+ if (this.CurrentLine == this.bottomVisibleLine)
{
- this.lines.PushBack(new Line());
+ this.ScrollDown();
}
this.cursorY = (short)Math.Min(this.Height - 1, this.cursorY + 1);
@@ -145,6 +167,74 @@ private void HandleControlCharacter(Commands.ControlCharacter.ControlCode code)
}
}
+ private void HandleControlSequence(Commands.ControlSequence cmd)
+ {
+ switch (cmd)
+ {
+ case Commands.EraseInDisplay eid:
+ switch (eid.Direction)
+ {
+ case Commands.EraseInDisplay.Parameter.All:
+ for (var y = this.topVisibleLine; y <= this.bottomVisibleLine; ++y)
+ {
+ this.lines[y].Clear();
+ }
+ break;
+ case Commands.EraseInDisplay.Parameter.Above:
+ for (var y = this.topVisibleLine; y < this.CurrentLine; ++y)
+ {
+ this.lines[y].Clear();
+ }
+ break;
+ case Commands.EraseInDisplay.Parameter.Below:
+ for (var y = this.CurrentLine; y < this.bottomVisibleLine; ++y)
+ {
+ this.lines[y].Clear();
+ }
+ break;
+ }
+ break;
+ case Commands.SetMode sm:
+ switch (sm.Setting)
+ {
+ case Commands.SetMode.Parameter.CursorBlink:
+ this.CursorBlink = sm.Set;
+ break;
+ case Commands.SetMode.Parameter.CursorShow:
+ this.CursorVisible = sm.Set;
+ break;
+ }
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown CSI command type {cmd.GetType()}.");
+ }
+ }
+
+ ///
+ /// Scroll the visible buffer down, adding new lines if needed.
+ /// If we're at the bottom of the buffer we will replace lines from the top with new, blank lines.
+ ///
+ private void ScrollDown(int lines = 1)
+ {
+ while (lines > 0)
+ {
+ --lines;
+ if (this.bottomVisibleLine == this.lines.Capacity - 1)
+ {
+ this.lines.PushBack(new Line()); // will force an old line from the buffer;
+ }
+ else
+ {
+ ++this.topVisibleLine;
+ ++this.bottomVisibleLine;
+ if (this.lines.Size <= this.bottomVisibleLine)
+ {
+ this.lines.PushBack(new Line());
+ }
+ }
+ }
+ }
+
///
/// Render character-by-character onto the specified target.
///
@@ -154,7 +244,7 @@ public void Render(IRenderTarget target)
{
for (var y = 0; y < this.Height; ++y)
{
- var renderLine = this.bufferTopVisibleLine + y;
+ var renderLine = this.topVisibleLine + y;
var line = renderLine < this.lines.Size ? this.lines[renderLine] : Line.Empty;
short x = 0;
foreach (var c in line)
diff --git a/src/ConsoleBuffer/Commands/ControlSequence.cs b/src/ConsoleBuffer/Commands/ControlSequence.cs
index 6c5368c..fd1ed26 100644
--- a/src/ConsoleBuffer/Commands/ControlSequence.cs
+++ b/src/ConsoleBuffer/Commands/ControlSequence.cs
@@ -9,20 +9,26 @@ public static Base Create(char command, string bufferData)
{
switch (command)
{
+ case 'h':
+ case 'l':
+ return new SetMode(bufferData, command == 'h');
case 'J':
- var cmd = new EraseInDisplay(bufferData);
- if (!cmd.IsExtended) // currently no support for selective erase in display
- return cmd;
- break;
+ return new EraseInDisplay(bufferData);
}
return new Unsupported($"^[[{bufferData}{command}");
}
- protected bool IsExtended { get; private set; }
+ public bool IsExtended { get; private set; }
protected IList Parameters { get; private set; }
protected ControlSequence(string bufferData) : base(bufferData) { }
protected override void Parse(string bufferData)
{
+ if (bufferData.Length == 0)
+ {
+ this.Parameters = Array.Empty();
+ return;
+ }
+
var startIndex = 0;
if (bufferData[0] == '?')
{
diff --git a/src/ConsoleBuffer/Commands/EraseInDisplay.cs b/src/ConsoleBuffer/Commands/EraseInDisplay.cs
index e3628c9..13147dc 100644
--- a/src/ConsoleBuffer/Commands/EraseInDisplay.cs
+++ b/src/ConsoleBuffer/Commands/EraseInDisplay.cs
@@ -1,7 +1,36 @@
+using System;
+
namespace ConsoleBuffer.Commands
{
public sealed class EraseInDisplay : ControlSequence
{
- public EraseInDisplay(string bufferData) : base(bufferData) { }
+ public enum Parameter
+ {
+ Below = 0,
+ Above,
+ All,
+ // xterm supports '3' for saved lines.
+ Unknown,
+ }
+
+ public Parameter Direction { get; private set; }
+
+ public EraseInDisplay(string bufferData) : base(bufferData)
+ {
+ this.Direction = Parameter.Unknown;
+ if (this.IsExtended)
+ {
+ return; // extended means selective erase which we don't support.
+ }
+ if (this.Parameters.Count == 0)
+ {
+ this.Direction = Parameter.Below;
+ }
+ if (this.Parameters.Count == 1 && uint.TryParse(this.Parameters[0], out var param))
+ {
+ param = Math.Min((uint)Parameter.Unknown, param);
+ this.Direction = (Parameter)param;
+ }
+ }
}
}
diff --git a/src/ConsoleBuffer/Commands/SetMode.cs b/src/ConsoleBuffer/Commands/SetMode.cs
new file mode 100644
index 0000000..4779874
--- /dev/null
+++ b/src/ConsoleBuffer/Commands/SetMode.cs
@@ -0,0 +1,42 @@
+namespace ConsoleBuffer.Commands
+{
+ public sealed class SetMode : ControlSequence
+ {
+ ///
+ /// True if the command was to set (not reset) the parameter.
+ ///
+ public bool Set { get; private set; }
+ public enum Parameter
+ {
+ CursorShow = 0,
+ CursorBlink,
+ Unknown,
+ }
+
+ public Parameter Setting { get; private set; }
+
+ public SetMode(string bufferData, bool set) : base(bufferData)
+ {
+ this.Set = set;
+ this.Setting = Parameter.Unknown;
+
+ if (!this.IsExtended)
+ {
+ return;
+ }
+
+ if (this.Parameters.Count == 1)
+ {
+ switch (this.Parameters[0])
+ {
+ case "12":
+ this.Setting = Parameter.CursorBlink;
+ break;
+ case "25":
+ this.Setting = Parameter.CursorShow;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ConsoleBuffer/Line.cs b/src/ConsoleBuffer/Line.cs
index 874df01..6894f05 100644
--- a/src/ConsoleBuffer/Line.cs
+++ b/src/ConsoleBuffer/Line.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -28,6 +28,11 @@ public void Set(int pos, Character ch)
this.chars[pos] = ch;
}
+ public void Clear()
+ {
+ this.chars.Clear();
+ }
+
private void Extend(int pos)
{
// XXX: not efficient.
@@ -37,14 +42,15 @@ private void Extend(int pos)
}
}
+
public IEnumerator GetEnumerator()
{
- return chars.GetEnumerator();
+ return this.chars.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
- return chars.GetEnumerator();
+ return this.chars.GetEnumerator();
}
public override string ToString()
diff --git a/src/condo/Screen.cs b/src/condo/Screen.cs
index c57a835..b25b695 100644
--- a/src/condo/Screen.cs
+++ b/src/condo/Screen.cs
@@ -1,14 +1,12 @@
-namespace condo
+namespace condo
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
- using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
- using System.Windows.Threading;
using ConsoleBuffer;
public sealed class Screen : FrameworkElement, IRenderTarget
@@ -24,6 +22,7 @@ public sealed class Screen : FrameworkElement, IRenderTarget
private readonly Rect cellRectangle;
private int horizontalCells, verticalCells;
private Character[,] characters;
+ bool cursorInverted;
private volatile int shouldRedraw;
private static readonly TimeSpan MaxRedrawFrequency = TimeSpan.FromMilliseconds(10);
@@ -63,7 +62,6 @@ public Screen(ConsoleWrapper console)
this.Resize();
}
- bool cursorBlunk;
private void RenderFrame(object sender, EventArgs e)
{
if (this.redrawWatch.Elapsed >= MaxRedrawFrequency && this.shouldRedraw != 0)
@@ -74,12 +72,15 @@ private void RenderFrame(object sender, EventArgs e)
this.redrawWatch.Restart();
}
- if (this.cursorBlinkWatch.Elapsed >= BlinkFrequency)
+ if (this.Console.Buffer.CursorVisible)
{
- (var x, var y) = this.Console.Buffer.CursorPosition;
- this.SetCellCharacter(x, y, (char)this.characters[x, y].Glyph, this.cursorBlunk);
- this.cursorBlunk = !this.cursorBlunk;
- this.cursorBlinkWatch.Restart();
+ if (this.cursorBlinkWatch.Elapsed >= BlinkFrequency)
+ {
+ this.cursorInverted = this.Console.Buffer.CursorBlink ? !this.cursorInverted : true;
+ (var x, var y) = this.Console.Buffer.CursorPosition;
+ this.SetCellCharacter(x, y, (char)this.characters[x, y].Glyph, this.cursorInverted);
+ this.cursorBlinkWatch.Restart();
+ }
}
}
diff --git a/test/ConsoleBufferTests/SequenceParserTests.cs b/test/ConsoleBufferTests/SequenceParserTests.cs
index 954086a..5b59df7 100644
--- a/test/ConsoleBufferTests/SequenceParserTests.cs
+++ b/test/ConsoleBufferTests/SequenceParserTests.cs
@@ -55,16 +55,59 @@ public void OSCommands()
const string title = "this is a random title";
var command = $"\x1b]2;{title}\a";
+ var parser = this.EnsureCommandParses(command);
+ var cmd = parser.Command as ConsoleBuffer.Commands.OS;
+ Assert.IsNotNull(cmd);
+ Assert.AreEqual(title, cmd.Title);
+ }
+
+ [TestMethod]
+ [DataRow("", ConsoleBuffer.Commands.EraseInDisplay.Parameter.Below)]
+ [DataRow("0", ConsoleBuffer.Commands.EraseInDisplay.Parameter.Below)]
+ [DataRow("1", ConsoleBuffer.Commands.EraseInDisplay.Parameter.Above)]
+ [DataRow("2", ConsoleBuffer.Commands.EraseInDisplay.Parameter.All)]
+ [DataRow("?2", ConsoleBuffer.Commands.EraseInDisplay.Parameter.Unknown)]
+ [DataRow("-50", ConsoleBuffer.Commands.EraseInDisplay.Parameter.Unknown)]
+ public void EraseInDisplay(string direction, ConsoleBuffer.Commands.EraseInDisplay.Parameter expectedDirection)
+ {
+ var command = $"\x1b[{direction}J";
+ var parser = this.EnsureCommandParses(command);
+ var cmd = parser.Command as ConsoleBuffer.Commands.EraseInDisplay;
+ Assert.IsNotNull(cmd);
+ Assert.AreEqual(expectedDirection, cmd.Direction);
+ }
+
+ [TestMethod]
+ [DataRow("25", ConsoleBuffer.Commands.SetMode.Parameter.Unknown)]
+ [DataRow("?25", ConsoleBuffer.Commands.SetMode.Parameter.CursorShow)]
+ [DataRow("12", ConsoleBuffer.Commands.SetMode.Parameter.Unknown)]
+ [DataRow("?12", ConsoleBuffer.Commands.SetMode.Parameter.CursorBlink)]
+ [DataRow("", ConsoleBuffer.Commands.SetMode.Parameter.Unknown)]
+ [DataRow("?", ConsoleBuffer.Commands.SetMode.Parameter.Unknown)]
+ public void SetMode(string mode, ConsoleBuffer.Commands.SetMode.Parameter expectedSetting)
+ {
+ foreach (var cmdChar in "hl")
+ {
+ var on = cmdChar == 'h';
+ var command = $"\x1b[{mode}{cmdChar}";
+ var parser = this.EnsureCommandParses(command);
+ var cmd = parser.Command as ConsoleBuffer.Commands.SetMode;
+ Assert.IsNotNull(cmd);
+ Assert.AreEqual(on, cmd.Set);
+ Assert.AreEqual(expectedSetting, cmd.Setting);
+ }
+ }
+
+ private SequenceParser EnsureCommandParses(string command)
+ {
var parser = new SequenceParser();
for (var i = 0; i < command.Length - 1; ++i)
{
Assert.AreEqual(ParserAppendResult.Pending, parser.Append(command[i]));
}
Assert.AreEqual(ParserAppendResult.Complete, parser.Append(command[command.Length - 1]));
- Assert.IsInstanceOfType(parser.Command, typeof(ConsoleBuffer.Commands.OS));
- var osCmd = parser.Command as ConsoleBuffer.Commands.OS;
- Assert.AreEqual(title, osCmd.Title);
+ return parser;
}
}
}