Files

474 lines
16 KiB
C#

//
// Copyright (c) Antmicro
//
// Full license details are defined in the 'LICENSE' file.
//
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TermSharp.Misc;
using TermSharp.Rows;
using Xwt.Drawing;
namespace TermSharp.Vt100
{
public sealed partial class Decoder
{
public Decoder(Terminal terminal, Action<byte> responseCallback, IDecoderLogger logger)
{
this.terminal = terminal;
this.responseCallback = responseCallback;
this.logger = logger;
commands = new Dictionary<char, Action>();
graphicRendition = new GraphicRendition(this);
savedGraphicRendition = graphicRendition.Clone();
InitializeCommands();
cursor = new Cursor(this);
CharReceivedBlinkDisabledRounds = 1;
receiveState = ReceiveState.Default;
}
public void Feed(string textElement)
{
if(textElement == "\0")
{
return;
}
if(receiveState == ReceiveState.IgnoreNextChar)
{
receiveState = ReceiveState.Default;
return;
}
terminal.Cursor.StayOnForNBlinks(CharReceivedBlinkDisabledRounds);
if(textElement.Length == 1)
{
var c = textElement[0];
if(receiveState == ReceiveState.EatUpToBell)
{
if((ControlByte)c == ControlByte.Bell)
{
receiveState = ReceiveState.Default;
}
}
else if(receiveState == ReceiveState.AnsiCode)
{
HandleAnsiCode(c);
}
else if(receiveState == ReceiveState.SystemCommandNumber)
{
HandleSystemCommandCode(c);
}
else if(receiveState == ReceiveState.Image)
{
HandleReceiveImage(c);
}
else if(ControlByte.Backspace == (ControlByte)c)
{
currentParams = new int?[] { 1 };
CursorLeft();
}
else if(ControlByte.Escape == (ControlByte)c)
{
receiveState = ReceiveState.AnsiCode;
}
else if(ControlByte.LineFeed == (ControlByte)c)
{
HandleLineFeed();
}
else if(ControlByte.CarriageReturn == (ControlByte)c)
{
cursor.Position = cursor.Position.WithX(0);
}
else if(ControlByte.Bell == (ControlByte)c)
{
var bellReceived = BellReceived;
if(bellReceived != null)
{
bellReceived();
}
}
else if(ControlByte.HorizontalTab == (ControlByte)c)
{
HandleRegularCharacter(" ");
}
else if(ControlByte.LockShiftG0 == (ControlByte)c || ControlByte.LockShiftG1 == (ControlByte)c)
{
//ignore, as we do not support character set switching
}
else if(char.IsControl(c))
{
if(c > 0 && c < 32)
{
Feed("^");
Feed(((char)(c + 64)).ToString());
}
else if(c != 0 && c != 127) // intentionally do nothing for NULL/DEL characters
{
logger.Log(string.Format("Unimplemented control character 0x{0:X}.", (int)c));
}
}
else
{
HandleRegularCharacter(textElement);
}
}
else
{
HandleRegularCharacter(textElement);
}
terminal.Redraw();
}
public int CharReceivedBlinkDisabledRounds { get; set; }
public event Action BellReceived;
private void InsertCharacterAtCursor(string textElement)
{
var row = terminal.GetScreenRow(terminal.Cursor.Position.Y, true);
if(row is ImageRow)
{
logger.Log($"Tried to insert character at the top of the image");
return;
}
if(!(row is MonospaceTextRow textRow))
{
throw new InvalidOperationException($"MonospaceTextRow expected but {row.GetType().Name} type found.");
}
if(textRow.PutCharacterAt(terminal.Cursor.Position.X, textElement, graphicRendition.EffectiveForeground, graphicRendition.EffectiveBackground))
{
terminal.Refresh();
}
}
private void HandleRegularCharacter(string textElement)
{
var oldPosition = terminal.Cursor.Position;
if(cursorAtTheEndOfLine)
{
terminal.Cursor.Position = terminal.Cursor.Position.ShiftedByX(1);
cursorAtTheEndOfLine = false;
}
InsertCharacterAtCursor(textElement);
var maximalColumn = terminal.GetScreenRow(terminal.Cursor.Position.Y).MaximalColumn;
if(terminal.Cursor.Position.X % (maximalColumn + 1) != maximalColumn)
{
terminal.Cursor.Position = terminal.Cursor.Position.ShiftedByX(1);
}
else
{
cursorAtTheEndOfLine = true;
}
}
private void HandleAnsiCode(char c)
{
if(ControlByte.OperatingSystemCommand == (ControlByte)c)
{
receiveState = ReceiveState.SystemCommandNumber;
systemCommandNumber = new StringBuilder();
return;
}
if(csiCodeData == null)
{
if(ControlByte.ControlSequenceIntroducer != (ControlByte)c)
{
HandleNonCsiCode(c);
receiveState = ReceiveState.Default;
privateModeCode = false;
return;
}
csiCodeData = new StringBuilder();
return;
}
if(ControlByte.Escape == (ControlByte)c)
{
logger.Log("Escape character within ANSI code.");
receiveState = ReceiveState.Default;
privateModeCode = false;
csiCodeData = null;
return;
}
if(char.IsLetter(c))
{
if(commands.ContainsKey(c))
{
// let's extract parameters
var splitted = csiCodeData.ToString().Split(';');
var parsed = new List<int?>();
foreach(var s in splitted)
{
if(string.IsNullOrEmpty(s))
{
parsed.Add(null);
}
else if(int.TryParse(s, out var i))
{
parsed.Add(i);
}
else
{
logger.Log($"Broken ANSI code data for command '{c}': '{csiCodeData}'");
parsed = null;
break;
}
}
// parsed is set to null when broken ANSI code is detected
if(parsed != null)
{
currentParams = parsed.ToArray();
commands[c]();
}
receiveState = ReceiveState.Default;
privateModeCode = false;
csiCodeData = null;
}
else
{
logger.Log(string.Format("Unimplemented ANSI code {0}, data {1}.", c, csiCodeData));
receiveState = ReceiveState.Default;
csiCodeData = null;
privateModeCode = false;
}
}
else
{
if(c != '?')
{
csiCodeData.Append(c);
}
else
{
privateModeCode = true;
}
}
}
private void HandleNonCsiCode(char c)
{
switch(c)
{
case 'c':
HandleTerminalReset();
break;
case '(':
case ')':
case '*':
case '+':
// G0-G3 character set, we ignore this, at least for now
receiveState = ReceiveState.IgnoreNextChar;
break;
case '7':
SaveCursorPosition();
break;
case '8':
RestoreCursorPosition();
break;
default:
logger.Log(string.Format("Unimplemented non-CSI code '{0}'.", c));
break;
}
}
private void HandleTerminalReset()
{
terminal.Cursor.Enabled = true;
graphicRendition.Reset();
var screenRows = terminal.ScreenRowCount;
for(var i = 0; i < screenRows; i++)
{
terminal.AppendRow(new MonospaceTextRow(string.Empty));
}
terminal.Cursor.Position = new IntegerPosition();
}
private void HandleLineFeed()
{
var oldY = cursor.Position.Y;
cursor.Position = cursor.Position.ShiftedByY(1);
if(oldY == cursor.Position.Y)
{
terminal.AppendRow(new MonospaceTextRow(string.Empty), true);
cursor.Position = cursor.Position.ShiftedByY(1);
}
}
private void HandleSystemCommandCode(char c)
{
if(char.IsDigit(c))
{
systemCommandNumber.Append(c);
return;
}
if(c == ';')
{
if(!int.TryParse(systemCommandNumber.ToString(), out var codeNumber))
{
logger.Log($"Couldn't parse the system command number: '{(systemCommandNumber.ToString())}'");
receiveState = ReceiveState.Default;
return;
}
if(codeNumber == InlineImageCode)
{
receiveState = ReceiveState.Image;
base64ImageBuilder = new StringBuilder();
}
else
{
logger.Log($"Not supported System Command Code 0x{0:X}. Ignoring the rest of the control code", codeNumber);
receiveState = ReceiveState.EatUpToBell;
}
systemCommandNumber = null;
return;
}
logger.Log($"Unexpected character '{c}' in System Command Number.");
receiveState = ReceiveState.Default;
}
private void HandleReceiveImage(char c)
{
if(ControlByte.Bell != (ControlByte)c)
{
base64ImageBuilder.Append(c);
}
else
{
if(!Vt100ITermFileEscapeCodeHandler.TryParse(base64ImageBuilder.ToString(), out var handler))
{
logger.Log(handler.Error);
base64ImageBuilder = null;
receiveState = ReceiveState.Default;
return;
}
DrawImage(handler.Image);
base64ImageBuilder = null;
receiveState = ReceiveState.Default;
}
}
private void DrawImage(Image image)
{
var imageRow = new ImageRow(image);
terminal.AppendRow(imageRow, true);
cursor.Position = cursor.Position.ShiftedByY(imageRow.SublineCount);
HandleLineFeed();
}
private const int InlineImageCode = 1337;
private ReceiveState receiveState;
private GraphicRendition graphicRendition;
private GraphicRendition savedGraphicRendition;
private IntegerPosition savedCursorPosition;
private int?[] currentParams;
private bool privateModeCode;
private bool cursorAtTheEndOfLine;
private StringBuilder csiCodeData;
private StringBuilder systemCommandNumber;
private StringBuilder base64ImageBuilder;
private readonly Terminal terminal;
private readonly Cursor cursor;
private readonly Action<byte> responseCallback;
private readonly IDecoderLogger logger;
private enum ReceiveState
{
Default,
IgnoreNextChar,
AnsiCode,
SystemCommandNumber,
Image,
EatUpToBell
}
private sealed class Cursor
{
public Cursor(Decoder parent)
{
this.parent = parent;
}
public int CurrentRowNumber
{
get
{
return parent.terminal.Cursor.Position.Y + 1;
}
}
public IntegerPosition Position
{
get
{
var terminalPosition = parent.terminal.Cursor.Position;
var resultY = 0;
for(var i = 0; i < terminalPosition.Y; i++)
{
resultY += parent.terminal.GetScreenRow(i).SublineCount;
}
// it can happen that the first row is partially hidden
// our vt100 cursor should be counted from the first completely displayed subrow of the first row
double hiddenHeight;
var firstRow = parent.terminal.GetFirstScreenRow(out hiddenHeight);
resultY -= (int)Math.Ceiling(hiddenHeight / firstRow.LineHeight);
// it can happen that normal cursor is not in vt100 cursor range which gives us negative result here
// in such case we report Y = 0
if(resultY < 0)
{
resultY = 0;
}
var charsInRow = parent.terminal.GetScreenRow(terminalPosition.Y).MaximalColumn + 1;
var resultX = terminalPosition.X % charsInRow;
resultY += terminalPosition.X / charsInRow;
return new IntegerPosition(resultX + 1, resultY + 1);
}
set
{
parent.cursorAtTheEndOfLine = false;
double hiddenPart;
var firstRow = parent.terminal.GetFirstScreenRow(out hiddenPart);
var maxX = firstRow.MaximalColumn + 1;
var maxY = (int)Math.Floor(parent.terminal.ScreenSize / firstRow.LineHeight);
value = new IntegerPosition(Math.Min(value.X, maxX), Math.Min(value.Y, maxY));
value = new IntegerPosition(Math.Max(value.X, 1), Math.Max(value.Y, 1));
var resultY = 0;
var vt100Y = value.Y;
// in the case of first row we only count its visible part
vt100Y -= firstRow.SublineCount - (int)Math.Ceiling(hiddenPart / firstRow.LineHeight);
while(vt100Y > 0)
{
resultY++;
if(resultY >= parent.terminal.ScreenRowCount)
{
// append dummy row to calculate proper position
parent.terminal.AppendRow(new MonospaceTextRow(""));
}
vt100Y -= parent.terminal.GetScreenRow(resultY).SublineCount;
}
var row = parent.terminal.GetScreenRow(resultY);
var resultX = (row.SublineCount - 1 + vt100Y) * (row.MaximalColumn + 1) + value.X - 1;
parent.terminal.Cursor.Position = new IntegerPosition(resultX, resultY);
}
}
private readonly Decoder parent;
}
}
}