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.
342 lines
13 KiB
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
|
|
}
|
|
|