Multiprocotol Terminalprogram (BAT)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MultiTerm/MultiTerm.Core/ViewModel/ConsoleViewModel.cs

342 lines
13 KiB

using Common.AppSettings;
using Common.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using MultiTerm.Core.Model;
using MultiTerm.Core.Types;
using MultiTerm.Protocols.Model;
using System.Collections.ObjectModel;
using System.Text;
namespace MultiTerm.Core.ViewModel;
public partial class ConsoleViewModel : TerminalViewModel
{
public override TerminalViewType ViewType => TerminalViewType.Console;
private readonly ILogger logger;
private Encoding currentEncoding;
private List<char>? listOfPrevCharacters;
/* settings */
private readonly Encoding defaultEncoding = Encoding.ASCII; // default encoding instance
private const EncodingType defaultEncodingType = EncodingType.ASCII; // default encoding type
private const int DataMaxLength = 20000; // limit of maximum shown characters
/// <summary>
/// Newline Sequence string inserted on every newline in the <see cref="Data"/>.
/// </summary>
public static readonly string NewlineSequence = Environment.NewLine;
#region Observable Properties
/// <summary>
/// Holds Console content.
/// </summary>
[ObservableProperty]
private string data = string.Empty;
/// <summary>
/// Encoding type of the sent and received messages.
/// </summary>
[ObservableProperty]
private EncodingType selectedEncodingType = defaultEncodingType;
/// <summary>
/// Holds message that is to be sent next. E.g. currently being typed in.
/// </summary>
[ObservableProperty]
private string sendableMessage = string.Empty;
#endregion
public ConsoleViewModel(IAppSettingsProvider appSettings, IMessenger messenger, ILogger logger) : base(appSettings, messenger)
{
this.logger = logger;
this.currentEncoding = this.GetEncoding(this.SelectedEncodingType);
}
public override void DisplayNewReceivedData(IEnumerable<ExtendedByte> newData)
{
// extract bytes
var bytes = newData.Select(b => b.Byte).ToArray();
string newDataDecoded = currentEncoding.GetString(bytes);
// replace newlines
newDataDecoded = ReplaceNewlineCharacters(newDataDecoded, ref this.listOfPrevCharacters, this.DataDisplayNewlineSeparatorType, currentEncoding);
// calculate new lnegth
int newTotalLength = this.Data.Length + newDataDecoded.Length;
// trim current data at the beginning if new length is too long
if (newTotalLength > DataMaxLength)
{
// trim overshoot from start of string
this.Data = this.Data[(newTotalLength - DataMaxLength)..];
}
// append data
this.Data += newDataDecoded;
}
/// <summary>
/// Command to send a prepared message.
/// </summary>
[RelayCommand]
public void Send()
{
// encode
var encodedBytes = this.currentEncoding.GetBytes(this.SendableMessage);
// send
if (this.SendToCommunicationProtocol(encodedBytes))
{
// add to history
this.AddToSendHistory(new StringHistoryElement(this.SendableMessage));
// clear if sending was successful
this.SendableMessage = string.Empty;
}
}
protected override void HandleInsertElementFromHistory(object? element)
{
// null element received => insert empty
if (element == null)
{
this.SendableMessage = string.Empty;
return;
}
// otherwise check type
if (element is not StringHistoryElement elementString)
{
throw new Exception($"'{HandleInsertElementFromHistory}()' in {nameof(ConsoleViewModel)} got wrong type of element. Got Type: {element.GetType()}");
}
// overwrite sendable message with history element
this.SendableMessage = elementString.Text;
}
/// <summary>
/// Command to clear the data that was received.
/// </summary>
[RelayCommand]
public void ClearData()
{
this.Data = string.Empty;
}
/// <summary>
/// Handles changing of encoding type.
/// </summary>
/// <param name="value">new value</param>
partial void OnSelectedEncodingTypeChanged(EncodingType value)
{
this.currentEncoding = this.GetEncoding(value);
}
#region Newline Insertion
/// <summary>
/// Returns if the <paramref name="character"/> is a known newline characters.
/// </summary>
/// <param name="character">character to test if it is a known newline characters</param>
/// <returns>true if the character is a newline character</returns>
private static bool IsKnownNewlineCharacter(char character)
{
// must be extended if new newline characters shall be detected!
const string knownNewlineCharacters = "\n\r";
return knownNewlineCharacters.Contains(character);
}
/// <summary>
/// Searches <paramref name="text"/> for known Newline characters (using <see cref="IsKnownNewlineCharacter(char)"/>. Removes all known Newline characters from the text.
/// If the newline sequence according to <paramref name="newlineSeparatorType"/> is found, the configured <see cref="NewlineSequence"/> is introduced.
/// </summary>
/// <param name="text">text to search for newline characters</param>
/// <param name="previousCharacters">list of previous characters, managed by this method. to identify newline sequences over multiple calls of this method</param>
/// <param name="newlineSeparatorType">separator type</param>
/// <param name="encoding">encoding to use</param>
/// <returns>string with replaced newline characters</returns>
private static string ReplaceNewlineCharacters(
string text,
ref List<char>? previousCharacters,
NewlineSeparatorType newlineSeparatorType,
Encoding encoding)
{
StringBuilder stringBuilder = new();
// go through characters
foreach (char character in text)
{
// add to collection if the character is not a known newline character
if (IsKnownNewlineCharacter(character) == false)
{
stringBuilder.Append(character);
}
// check if a newline should be introduced after this character
switch (ShouldIntroduceNewlineAfterThisCharacter(character, previousCharacters, newlineSeparatorType, encoding))
{
case ShouldIntroduceNewlineAfterThisCharacterResult.NoNewline:
// a decision could be made, previousCharacters can be cleared
if (previousCharacters != null) { previousCharacters = null; }
// nothing to do
break;
case ShouldIntroduceNewlineAfterThisCharacterResult.IntroduceNewline:
// a decision could be made, previousCharacters can be cleared
if (previousCharacters != null) { previousCharacters = null; }
// append line in string
stringBuilder.Append(NewlineSequence);
break;
case ShouldIntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters:
// first time that more characters are required => create new list
previousCharacters ??= new List<char>();
// add current character to list
previousCharacters.Add(character);
break;
default:
throw new Exception($"'{nameof(ReplaceNewlineCharacters)}()' failed because of error when checking if a newline should be introduced.");
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Result type for <see cref="ShouldIntroduceNewlineAfterThisCharacter"/>
/// </summary>
public enum ShouldIntroduceNewlineAfterThisCharacterResult
{
/// <summary>
/// No newline is required.
/// </summary>
NoNewline,
/// <summary>
/// A newline shall be introduced after this byte.
/// </summary>
IntroduceNewline,
/// <summary>
/// Following characters are required to finalize result wether a newline shall be introduced or not.
/// </summary>
RequiresMoreCharacters
}
/// <summary>
/// Function to check wether a newline shall be introduced after the given <paramref name="character"/>.
/// Since some newline sequences will require multiple characters in correct order, a more complex handling is required, which is possible using this function.
/// </summary>
/// <param name="character">the current character in the collection</param>
/// <param name="previousCharacters">list of previous character, newest at the last position of the list, null if not required</param>
/// <param name="newlineSeparatorType">separator type</param>
/// <param name="encoding">encoding to be used to check if the characters are equal</param>
/// <returns>enum result of type <see cref="ShouldIntroduceNewlineAfterThisCharacterResult"/></returns>
/// <exception cref="NotImplementedException">if the handling for the <paramref name="newlineSeparatorType"/> is not implemented</exception>
private static ShouldIntroduceNewlineAfterThisCharacterResult ShouldIntroduceNewlineAfterThisCharacter(
char character,
List<char>? previousCharacters,
NewlineSeparatorType newlineSeparatorType,
Encoding encoding)
{
var result = ShouldIntroduceNewlineAfterThisCharacterResult.NoNewline;
switch (newlineSeparatorType)
{
case NewlineSeparatorType.None:
break;
case NewlineSeparatorType.CR:
if (EncodedCharacterEqualsAsciiCharacter(character, '\r', encoding))
{
result = ShouldIntroduceNewlineAfterThisCharacterResult.IntroduceNewline;
}
break;
case NewlineSeparatorType.LF:
if (EncodedCharacterEqualsAsciiCharacter(character, '\n', encoding))
{
result = ShouldIntroduceNewlineAfterThisCharacterResult.IntroduceNewline;
}
break;
case NewlineSeparatorType.CR_LF:
if (EncodedCharacterEqualsAsciiCharacter(character, '\r', encoding))
{
result = ShouldIntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters;
}
if (EncodedCharacterEqualsAsciiCharacter(character, '\n', encoding))
{
if (previousCharacters != null && EncodedCharacterEqualsAsciiCharacter(previousCharacters.Last(), '\r', encoding))
{
result = ShouldIntroduceNewlineAfterThisCharacterResult.IntroduceNewline;
}
}
break;
default:
throw new NotImplementedException($"'{nameof(ShouldIntroduceNewlineAfterThisCharacter)}()' does not implement handling for {nameof(NewlineSeparatorType)} {newlineSeparatorType}");
}
return result;
}
#endregion
#region Unused Methods
public override void DisplayNewSentData(IEnumerable<ExtendedByte> newData)
{
// no display of sent data
}
protected override void ActOnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType newType)
{
// No change when data display newline separator type changed
}
#endregion
#region Helpers
/// <summary>
/// Returns <see cref="Encoding"/> instance of <paramref name="encodingType"/>.
/// If the <paramref name="encodingType"/> is not supported or any issue arrived, the <see cref="SelectedEncodingType"/> is set to ASCII and ASCII <see cref="Encoding"/> is returned.
/// </summary>
/// <param name="encodingType">type of encoding</param>
/// <returns>encoding instance</returns>
private Encoding GetEncoding(EncodingType encodingType)
{
Encoding? encoding;
try
{
// Encoding Type value corresponds to codepage number, therefore can be get by cast to int
encoding = Encoding.GetEncoding(Convert.ToInt32(encodingType));
}
catch (Exception)
{
// log exception and set encoding to null
this.logger.LogWarn($"'{nameof(GetEncoding)}()' failed to resolve encoding type {encodingType} (codepage {Convert.ToInt32(encodingType)}). Reverted to default.", nameof(ConsoleViewModel));
encoding = null;
}
// failed to get encoding or got null encoding
if (encoding == null)
{
// set selected encoding type to default and return default encoding
this.SelectedEncodingType = defaultEncodingType;
encoding = defaultEncoding;
}
return encoding!;
}
/// <summary>
/// Returns wether the <paramref name="encodedChar"/> equals the <paramref name="asciiChar"/>.
/// <paramref name="encodedChar"/> must be encoded with <paramref name="encoding"/>.
/// </summary>
/// <returns>true if equal</returns>
private static bool EncodedCharacterEqualsAsciiCharacter(char encodedChar, char asciiChar, Encoding encoding)
{
return encodedChar.ToString() == encoding.GetString(Encoding.ASCII.GetBytes(new char[] { asciiChar }));
}
#endregion
}