// // Copyright (c) Antmicro // // Full license details are defined in the 'LICENSE' file. // using System; using System.Collections.Generic; using System.Linq; using System.Text; using TermSharp.Misc; using TermSharp.Rows; using Xwt.Drawing; namespace TermSharp.Vt100 { public partial class Decoder { // all cursor limits are already handled in terminal private void CursorUp() { var delta = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.ShiftedByY(-delta); } private void CursorDown() { var delta = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.ShiftedByY(delta); } private void CursorLeft() { var delta = GetParamOrDefault(0, 1); if(cursorAtTheEndOfLine) { // we ignore one cursor left here delta--; } cursor.Position = cursor.Position.ShiftedByX(-delta); } private void CursorRight() { var delta = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.ShiftedByX(delta); } private void CursorNextLine() { var delta = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.ShiftedByY(delta).WithX(1); } private void CursorPreviousLine() { var delta = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.ShiftedByY(-delta).WithX(1); } private void CursorHorizontalAbsolute() { var column = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.WithX(column); } private void CursorVerticalAbsolute() { var row = GetParamOrDefault(0, 1); cursor.Position = cursor.Position.WithY(row); } private void CursorPosition() { var row = GetParamOrDefault(0, 1); var column = GetParamOrDefault(1, 1); cursor.Position = new IntegerPosition(column, row); } private void EraseDisplay() { var type = GetParamOrDefault(0, 0); var currentPosition = terminal.Cursor.Position; IntegerPosition clearBegin, clearEnd; switch(type) { case 0: clearBegin = currentPosition; clearEnd = terminal.Cursor.MaximalPosition; break; case 1: clearBegin = new IntegerPosition(); clearEnd = currentPosition; break; case 2: clearBegin = new IntegerPosition(); clearEnd = terminal.Cursor.MaximalPosition; cursor.Position = new IntegerPosition(0, 0); break; default: logger.Log("Unimplemented erase display mode."); return; } terminal.EraseScreen(clearBegin, clearEnd, graphicRendition.Background); } private void EraseCharacter() { var amount = GetParamOrDefault(0, 1); if(amount == 0) { amount = 1; } var currentRow = terminal.GetScreenRow(terminal.Cursor.Position.Y); currentRow.Erase(terminal.Cursor.Position.X, terminal.Cursor.Position.X + amount - 1, graphicRendition.Background); } private void EraseInLine() { var type = GetParamOrDefault(0, 0); var currentRow = terminal.GetScreenRow(terminal.Cursor.Position.Y); var screenRowBegin = (terminal.Cursor.Position.X / (currentRow.MaximalColumn + 1)) * (currentRow.MaximalColumn + 1); var screenRowEnd = screenRowBegin + currentRow.MaximalColumn + 1; switch(type) { case 0: currentRow.Erase(terminal.Cursor.Position.X, screenRowEnd, graphicRendition.Background); break; case 1: currentRow.Erase(screenRowBegin, terminal.Cursor.Position.X, graphicRendition.Background); break; case 2: currentRow.Erase(screenRowBegin, screenRowEnd, graphicRendition.Background); break; default: logger.Log("Unimplemented erase line mode."); break; } } private void DeviceStatusReport() { if(GetParamOrDefault(0, 0) != 6) { logger.Log(string.Format("Unsupported device status report with params {0}.", ParamsToString())); return; } var response = new List(); response.AddRange(new[] { (byte)ControlByte.Escape, (byte)ControlByte.ControlSequenceIntroducer }); response.AddRange(Encoding.ASCII.GetBytes(string.Format("{0};{1}R", cursor.Position.Y, cursor.Position.X))); SendResponse(response); } private void SaveCursorPosition() { savedGraphicRendition = graphicRendition.Clone(); savedCursorPosition = cursor.Position; } private void RestoreCursorPosition() { graphicRendition = savedGraphicRendition.Clone(); cursor.Position = savedCursorPosition; } private void DeviceAttributes() { if(privateModeCode || GetParamOrDefault(0, 0) != 0) { logger.Log(string.Format("Unsupported device attributes query with params {0}.", ParamsToString())); return; } SendResponse(new[] { (byte)ControlByte.LineFeed, (byte)ControlByte.Escape, (byte)ControlByte.ControlSequenceIntroducer }.Union(Encoding.ASCII.GetBytes("?1;2c"))); } private void SetMode() { if(privateModeCode) { switch(GetParamOrDefault(0, 0)) { case 5: graphicRendition.Negative = true; return; case 7: // Enable wraparound mode - we do it by default, have no option to turn it off // This is effectively a no-op. return; case 25: terminal.Cursor.Enabled = true; return; } } logger.Log(string.Format("Unimplemented mode set with params {0}.", ParamsToString())); } private void ResetMode() { if(privateModeCode) { switch(GetParamOrDefault(0, 0)) { case 4: // Disable insert mode. We do not handle this mode, so this is a no-op return; case 5: graphicRendition.Negative = false; return; case 25: terminal.Cursor.Enabled = false; return; } } logger.Log(string.Format("Unimplemented mode reset with params {0}.", ParamsToString())); } private int GetParamOrDefault(int index, int defaultValue) { if(currentParams.Length <= index) { return defaultValue; } return currentParams[index] ?? defaultValue; } private string ParamsToString() { return currentParams.Select(x => x == null ? "(default)" : x.Value.ToString()).Aggregate((x, y) => x + "; " + y); } private void SendResponse(IEnumerable response) { foreach(var b in response) { responseCallback(b); } } private void InitializeCommands() { commands.Add('A', CursorUp); commands.Add('B', CursorDown); commands.Add('C', CursorRight); commands.Add('D', CursorLeft); commands.Add('E', CursorNextLine); commands.Add('F', CursorPreviousLine); commands.Add('G', CursorHorizontalAbsolute); commands.Add('H', CursorPosition); commands.Add('f', CursorPosition); commands.Add('J', EraseDisplay); commands.Add('K', EraseInLine); commands.Add('X', EraseCharacter); commands.Add('m', () => graphicRendition.HandleSgr()); commands.Add('n', DeviceStatusReport); commands.Add('s', SaveCursorPosition); commands.Add('u', RestoreCursorPosition); commands.Add('h', SetMode); commands.Add('l', ResetMode); commands.Add('c', DeviceAttributes); commands.Add('d', CursorVerticalAbsolute); commands.Add('`', CursorHorizontalAbsolute); } private readonly Dictionary commands; private sealed class GraphicRendition { public GraphicRendition(Decoder parent) { this.parent = parent; } public void Reset() { Foreground = null; Background = null; Negative = false; Bright = false; } public GraphicRendition Clone() { return (GraphicRendition)MemberwiseClone(); } // Codes based on https://en.wikipedia.org/wiki/ANSI_escape_code public void HandleSgr() { for(var i = 0; i < parent.currentParams.Length; i++) { var parameter = parent.currentParams[i]; if(!parameter.HasValue) { // no parameter is interpreted as 0 // todo: analyze if this change can be made more general, in all ANSI codes parameter = 0; } var value = parameter.Value; Action handler; if(SgrHandlers.TryGetValue(value, out handler)) { handler(this); } else if(value >= 30 && value <= 37) { Foreground = GetConsolePaletteColor(value - 30); } else if(value >= 40 && value <= 47) { Background = GetConsolePaletteColor(value - 40); } else if(value == 38) { i++; Foreground = GetExtendedColor(ref i); } else if(value == 48) { i++; Background = GetExtendedColor(ref i); } else if(value >= 90 && value <= 97) { Foreground = Brighten(GetConsolePaletteColor(value - 90)); } else if(value >= 100 && value <= 107) { Background = Brighten(GetConsolePaletteColor(value - 100)); } else { parent.logger.Log(string.Format("Unimplemented SGR code {0}.", value)); } } } public Color? Foreground { get { return foreground; } set { foreground = value; Refresh(); } } public Color? Background { get { return background; } set { background = value; Refresh(); } } public bool Bright { get { return bright; } set { bright = value; Refresh(); } } public bool Negative { get { return negative; } set { negative = value; Refresh(); } } public Color? EffectiveForeground { get; private set; } public Color? EffectiveBackground { get; private set; } private Color GetConsolePaletteColor(int number) { switch(number) { case 0: return Colors.Black; case 1: return Colors.DarkRed; case 2: return Colors.DarkGreen; case 3: return Colors.Brown; case 4: return Colors.DarkBlue; case 5: return Colors.DarkMagenta; case 6: return Colors.DarkCyan; case 7: return Terminal.DefaultGray; } parent.logger.Log("Unexpected palette color number."); return Colors.Black; } private Color GetExtendedColor(ref int baseParameterIndex) { switch(parent.GetParamOrDefault(baseParameterIndex, 0)) { case 2: baseParameterIndex += 3; return new Color(parent.GetParamOrDefault(baseParameterIndex - 2, 0) / 255.0, parent.GetParamOrDefault(baseParameterIndex - 1, 0) / 255.0, parent.GetParamOrDefault(baseParameterIndex, 0) / 255.0); case 5: baseParameterIndex++; var index = parent.GetParamOrDefault(baseParameterIndex, 0); if(index >= 0 && index <= 15) { var color = GetConsolePaletteColor(index % 8); if(index > 7) { color = Brighten(color); } return color; } else if(index < 0xE7) { index -= 16; var r = ToColorTable((index / 36) % 6); var g = ToColorTable((index / 6) % 6); var b = ToColorTable(index % 6); return new Color(r / 255.0, g / 255.0, b / 255.0); } else { // colors taken from https://jonasjacek.github.io/colors/ var intensity = 0x8 + (index - 0xE8) * 0xA; return new Color(intensity / 255.0, intensity / 255.0, intensity / 255.0); } default: parent.logger.Log("Unimplemented extended color mode."); return Colors.Black; } } private void Refresh() { var currentForeground = foreground ?? parent.terminal.DefaultForeground; var currentBackground = background ?? parent.terminal.DefaultBackground; if(Bright) { currentForeground = Brighten(currentForeground); } if(Negative) { var temporary = currentForeground; currentForeground = currentBackground; currentBackground = temporary; } EffectiveForeground = currentForeground == parent.terminal.DefaultForeground ? default(Color?) : currentForeground; EffectiveBackground = currentBackground == parent.terminal.DefaultBackground ? default(Color?) : currentBackground; } private static Color Brighten(Color value) { Color result; if(!DefinedBrightColors.TryGetValue(value, out result)) { result = value.WithIncreasedLight(0.3); } return result; } private static int ToColorTable(int index) { if(index > 0) { index = 0x5f + (index - 1) * 0x28; } return index; } private Color? foreground; private Color? background; private bool bright; private bool negative; private readonly Decoder parent; private static readonly Dictionary DefinedBrightColors = new Dictionary { { Colors.Black, Colors.DarkGray }, { Colors.DarkRed, Colors.Red }, { Colors.DarkGreen, Colors.Green }, { Colors.Brown, Colors.Yellow }, { Colors.DarkBlue, Colors.Blue }, { Colors.DarkMagenta, Colors.Magenta }, { Colors.DarkCyan, Colors.Cyan }, { Terminal.DefaultGray, Colors.White } }; private static readonly Dictionary> SgrHandlers = new Dictionary> { { 0, x => x.Reset() }, { 1, x => x.Bright = true }, { 7, x => x.Negative = true }, { 10, x => { /* Select primary font. We don't support font switching, so this is always no-op */ }}, { 21, x => x.Bright = false }, { 24, x => { /* Underline off. We don't handle underline yet, so this is always no-op */ }}, { 27, x => x.Negative = false }, { 39, x => x.Foreground = null }, { 49, x => x.Background = null } }; } } }