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? 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 /// /// Newline Sequence string inserted on every newline in the . /// public static readonly string NewlineSequence = Environment.NewLine; #region Observable Properties /// /// Holds Console content. /// [ObservableProperty] private string data = string.Empty; /// /// Encoding type of the sent and received messages. /// [ObservableProperty] private EncodingType selectedEncodingType = defaultEncodingType; /// /// Holds message that is to be sent next. E.g. currently being typed in. /// [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 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; } /// /// Command to send a prepared message. /// [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; } /// /// Command to clear the data that was received. /// [RelayCommand] public void ClearData() { this.Data = string.Empty; } /// /// Handles changing of encoding type. /// /// new value partial void OnSelectedEncodingTypeChanged(EncodingType value) { this.currentEncoding = this.GetEncoding(value); } #region Newline Insertion /// /// Returns if the is a known newline characters. /// /// character to test if it is a known newline characters /// true if the character is a newline character 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); } /// /// Searches for known Newline characters (using . Removes all known Newline characters from the text. /// If the newline sequence according to is found, the configured is introduced. /// /// text to search for newline characters /// list of previous characters, managed by this method. to identify newline sequences over multiple calls of this method /// separator type /// encoding to use /// string with replaced newline characters private static string ReplaceNewlineCharacters( string text, ref List? 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(); // 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(); } /// /// Result type for /// public enum ShouldIntroduceNewlineAfterThisCharacterResult { /// /// No newline is required. /// NoNewline, /// /// A newline shall be introduced after this byte. /// IntroduceNewline, /// /// Following characters are required to finalize result wether a newline shall be introduced or not. /// RequiresMoreCharacters } /// /// Function to check wether a newline shall be introduced after the given . /// Since some newline sequences will require multiple characters in correct order, a more complex handling is required, which is possible using this function. /// /// the current character in the collection /// list of previous character, newest at the last position of the list, null if not required /// separator type /// encoding to be used to check if the characters are equal /// enum result of type /// if the handling for the is not implemented private static ShouldIntroduceNewlineAfterThisCharacterResult ShouldIntroduceNewlineAfterThisCharacter( char character, List? 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 newData) { // no display of sent data } protected override void ActOnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType newType) { // No change when data display newline separator type changed } #endregion #region Helpers /// /// Returns instance of . /// If the is not supported or any issue arrived, the is set to ASCII and ASCII is returned. /// /// type of encoding /// encoding instance 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!; } /// /// Returns wether the equals the . /// must be encoded with . /// /// true if equal private static bool EncodedCharacterEqualsAsciiCharacter(char encodedChar, char asciiChar, Encoding encoding) { return encodedChar.ToString() == encoding.GetString(Encoding.ASCII.GetBytes(new char[] { asciiChar })); } #endregion }