From 845a37050f4f9aa8d6e2c6fecad4d2ce46df121f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Sun, 20 Aug 2023 07:37:28 +0200 Subject: [PATCH] Console Binary file preview --- .../ViewModels/CommandPaletteViewModel.cs | 1 - .../ItemPreview/IElementPreviewViewModel.cs | 1 + .../ItemPreview/ElementPreviewViewModel.cs | 2 + .../FuzzyPanelViewModel.cs | 13 +- .../ConsoleApplicationConfiguration.cs | 2 + .../IConsoleAppState.cs | 2 + .../IRootViewModel.cs | 1 + .../Preview/ItemPreviewType.cs | 7 + .../UserCommand/NextPreviewUserCommand.cs | 17 +++ .../UserCommand/PreviousPreviewUserCommand.cs | 17 +++ .../FileTime.ConsoleUI.App/ConsoleAppState.cs | 3 + .../Controls/CommandPalette.cs | 4 +- .../Controls/ItemPreviews.cs | 38 +++++- .../FileTime.ConsoleUI.App/MainWindow.cs | 36 ++++- .../FileTime.ConsoleUI.App/RootViewModel.cs | 17 ++- .../FileTime.ConsoleUI.App/Startup.cs | 13 ++ .../UserCommand/ConsoleUserCommandHandler.cs | 80 +++++++++++ .../FileTime.ConsoleUI.csproj | 4 + .../FileTime.ConsoleUI/appsettings.json | 4 + .../Mocks/MockRenderEngine.cs | 4 + src/Library/TerminalUI/Controls/BinaryView.cs | 126 ++++++++++++++++++ src/Library/TerminalUI/Controls/TextBlock.cs | 17 ++- src/Library/TerminalUI/Controls/View.cs | 5 +- 23 files changed, 391 insertions(+), 23 deletions(-) create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Preview/ItemPreviewType.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/NextPreviewUserCommand.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/PreviousPreviewUserCommand.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App/UserCommand/ConsoleUserCommandHandler.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI/appsettings.json create mode 100644 src/Library/TerminalUI/Controls/BinaryView.cs diff --git a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs index dd72b29..f065700 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs @@ -22,7 +22,6 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel logger) - : base((a, b) => a.Identifier == b.Identifier) { _commandPaletteService = commandPaletteService; _identifiableUserCommandService = identifiableUserCommandService; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ItemPreview/IElementPreviewViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ItemPreview/IElementPreviewViewModel.cs index 43864bc..d94a864 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ItemPreview/IElementPreviewViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ItemPreview/IElementPreviewViewModel.cs @@ -6,5 +6,6 @@ public interface IElementPreviewViewModel : IItemPreviewViewModel { ItemPreviewMode Mode { get; } string TextContent { get; } + byte[] BinaryContent { get; } string TextEncoding { get; } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs index e2d97c2..585bde5 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs @@ -26,6 +26,7 @@ public partial class ElementPreviewViewModel : IElementPreviewViewModel, IAsyncI public ItemPreviewMode Mode { get; private set; } [Property] private string? _textContent; + [Property] private byte[]? _binaryContent; [Property] private string? _textEncoding; public string Name => PreviewName; @@ -35,6 +36,7 @@ public partial class ElementPreviewViewModel : IElementPreviewViewModel, IAsyncI try { var content = await element.Provider.GetContentAsync(element, MaxTextPreviewSize); + BinaryContent = content; if (content is null) { diff --git a/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs b/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs index ea435c9..81ae21e 100644 --- a/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs +++ b/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs @@ -29,13 +29,20 @@ public abstract partial class FuzzyPanelViewModel : IFuzzyPanelViewModel< _searchText = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchText))); - UpdateFilteredMatchesInternal(); + UpdateFilteredMatches(); + if (string.IsNullOrWhiteSpace(value)) + { + SelectedItem = null; + } + else + { + UpdateSelectedItem(); + } } } - private void UpdateFilteredMatchesInternal() + private void UpdateSelectedItem() { - UpdateFilteredMatches(); if (SelectedItem != null && FilteredMatches.Contains(SelectedItem)) return; SelectedItem = FilteredMatches.Count > 0 diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs index d6c4a25..efbec09 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs @@ -4,4 +4,6 @@ public class ConsoleApplicationConfiguration { public string? ConsoleDriver { get; set; } public bool DisableUtf8 { get; set; } + public string? ClipboardSingleIcon { get; set; } + public string? ClipboardMultipleIcon { get; set; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs index 9647d2b..2c1e305 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using FileTime.App.Core.ViewModels; +using FileTime.ConsoleUI.App.Preview; namespace FileTime.ConsoleUI.App; @@ -7,4 +8,5 @@ public interface IConsoleAppState : IAppState { string ErrorText { get; set; } ObservableCollection PopupTexts { get; } + ItemPreviewType? PreviewType { get; set; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs index ecaca3a..57c8b27 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs @@ -22,5 +22,6 @@ public interface IRootViewModel IDeclarativeProperty VolumeSizeInfo { get; } IFrequencyNavigationViewModel FrequencyNavigation { get; } IItemPreviewService ItemPreviewService { get; } + IClipboardService ClipboardService { get; } event Action? FocusReadInputElement; } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Preview/ItemPreviewType.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Preview/ItemPreviewType.cs new file mode 100644 index 0000000..1d7b6eb --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Preview/ItemPreviewType.cs @@ -0,0 +1,7 @@ +namespace FileTime.ConsoleUI.App.Preview; + +public enum ItemPreviewType +{ + Text, + Binary +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/NextPreviewUserCommand.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/NextPreviewUserCommand.cs new file mode 100644 index 0000000..571bd7f --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/NextPreviewUserCommand.cs @@ -0,0 +1,17 @@ +using FileTime.App.Core.UserCommand; + +namespace FileTime.ConsoleUI.App.UserCommand; + +public class NextPreviewUserCommand : IIdentifiableUserCommand +{ + public const string CommandId = "console_next_preview"; + + public static NextPreviewUserCommand Instance = new(); + + private NextPreviewUserCommand() + { + } + + public string UserCommandID => CommandId; + public string Title => "Next preview"; +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/PreviousPreviewUserCommand.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/PreviousPreviewUserCommand.cs new file mode 100644 index 0000000..1952828 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/UserCommand/PreviousPreviewUserCommand.cs @@ -0,0 +1,17 @@ +using FileTime.App.Core.UserCommand; + +namespace FileTime.ConsoleUI.App.UserCommand; + +public class PreviousPreviewUserCommand : IIdentifiableUserCommand +{ + public const string CommandId = "console_previous_preview"; + + public static PreviousPreviewUserCommand Instance = new(); + + private PreviousPreviewUserCommand() + { + } + + public string UserCommandID => CommandId; + public string Title => "Previous preview"; +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs index 05dcc8d..b25e2d8 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using FileTime.App.Core.ViewModels; +using FileTime.ConsoleUI.App.Preview; using PropertyChanged.SourceGenerator; namespace FileTime.ConsoleUI.App; @@ -9,4 +10,6 @@ public partial class ConsoleAppState : AppStateBase, IConsoleAppState [Notify] private string? _errorText; //TODO: make it thread safe public ObservableCollection PopupTexts { get; } = new(); + + [Notify] private ItemPreviewType? _previewType = ItemPreviewType.Binary; } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs index b9a59b2..f3258a8 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs @@ -105,13 +105,13 @@ public class CommandPalette item.Bind( item.Parent, - d => d.CommandPalette.SelectedItem == item.DataContext ? _theme.ListViewItemTheme.SelectedBackgroundColor : null, + d => d.CommandPalette.SelectedItem.Identifier == item.DataContext.Identifier ? _theme.ListViewItemTheme.SelectedBackgroundColor : null, t => t.Background ); item.Bind( item.Parent, - d => d.CommandPalette.SelectedItem == item.DataContext ? _theme.ListViewItemTheme.SelectedForegroundColor : null, + d => d.CommandPalette.SelectedItem.Identifier == item.DataContext.Identifier ? _theme.ListViewItemTheme.SelectedForegroundColor : null, t => t.Foreground ); diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs index 43343ea..cac5643 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs @@ -1,5 +1,6 @@ using FileTime.App.Core.Models; using FileTime.App.Core.ViewModels.ItemPreview; +using FileTime.ConsoleUI.App.Preview; using FileTime.ConsoleUI.App.Styling; using TerminalUI.Controls; using TerminalUI.Extensions; @@ -11,10 +12,12 @@ namespace FileTime.ConsoleUI.App.Controls; public class ItemPreviews { private readonly ITheme _theme; + private readonly IConsoleAppState _appState; - public ItemPreviews(ITheme theme) + public ItemPreviews(ITheme theme, IConsoleAppState appState) { _theme = theme; + _appState = appState; } public IView View() @@ -75,11 +78,34 @@ public class ItemPreviews ChildInitializer = { new TextBlock() - .Setup(t => t.Bind( - t, - dc => dc.TextContent, - t => t.Text, - fallbackValue: string.Empty)), + .Setup(t => + { + t.Bind( + t, + dc => dc.TextContent, + t => t.Text, + fallbackValue: string.Empty); + + t.Bind( + t, + dc => _appState.PreviewType, + t => t.IsVisible, + v => v is null or ItemPreviewType.Text); + }), + new BinaryView() + .Setup(b => + { + b.Bind( + b, + dc => dc.BinaryContent, + b => b.Data); + + b.Bind( + b, + dc => _appState.PreviewType, + t => t.IsVisible, + v => v == ItemPreviewType.Binary); + }), new TextBlock { Margin = "0 1 0 0", diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs index beb9fad..38e89a5 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,11 +1,13 @@ using System.Globalization; using FileTime.App.Core.Models.Enums; using FileTime.App.Core.ViewModels; +using FileTime.ConsoleUI.App.Configuration; using FileTime.ConsoleUI.App.Controls; using FileTime.ConsoleUI.App.Styling; using FileTime.Core.Enums; using FileTime.Core.Models; using Humanizer.Bytes; +using Microsoft.Extensions.Options; using TerminalUI; using TerminalUI.Color; using TerminalUI.Controls; @@ -36,6 +38,7 @@ public class MainWindow private readonly Dialogs _dialogs; private readonly Timeline _timeline; private readonly ItemPreviews _itemPreviews; + private readonly IOptions _consoleApplicationConfiguration; private readonly Lazy _root; @@ -47,7 +50,8 @@ public class MainWindow FrequencyNavigation frequencyNavigation, Dialogs dialogs, Timeline timeline, - ItemPreviews itemPreviews) + ItemPreviews itemPreviews, + IOptions consoleApplicationConfiguration) { _rootViewModel = rootViewModel; _applicationContext = applicationContext; @@ -57,6 +61,7 @@ public class MainWindow _dialogs = dialogs; _timeline = timeline; _itemPreviews = itemPreviews; + _consoleApplicationConfiguration = consoleApplicationConfiguration; _root = new Lazy(Initialize); } @@ -92,7 +97,7 @@ public class MainWindow { new Grid { - ColumnDefinitionsObject = "Auto * Auto", + ColumnDefinitionsObject = "Auto * Auto Auto", ChildInitializer = { new StackPanel @@ -130,8 +135,32 @@ public class MainWindow root => root.AppState.SelectedTab.Value.CurrentLocation.Value.FullName.Path, tb => tb.Text )), + new StackPanel + { + Margin = "2 0 0 0", + Extensions = {new GridPositionExtension(2, 0)}, + ChildInitializer = + { + new TextBlock + { + Text = _consoleApplicationConfiguration.Value.ClipboardSingleIcon ?? "C", + AsciiOnly = false + }.Setup(t => t.Bind( + t, + dc => dc.ClipboardService.Content.Count == 1, + t => t.IsVisible)), + new TextBlock + { + Text = _consoleApplicationConfiguration.Value.ClipboardMultipleIcon ?? "CC", + AsciiOnly = false + }.Setup(t => t.Bind( + t, + dc => dc.ClipboardService.Content.Count > 1, + t => t.IsVisible)) + } + }, TabControl() - .WithExtension(new GridPositionExtension(2, 0)) + .WithExtension(new GridPositionExtension(3, 0)) } }, new Grid @@ -375,6 +404,7 @@ public class MainWindow { var tabList = new ListView { + Margin = "1 0 0 0", Orientation = Orientation.Horizontal, ItemTemplate = item => { diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs index 15e99c5..d49b780 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs @@ -4,13 +4,15 @@ using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels.Timeline; using FileTime.App.FrequencyNavigation.ViewModels; +using FileTime.ConsoleUI.App.Preview; using FileTime.ConsoleUI.App.Services; using FileTime.Core.Interactions; using FileTime.Core.Models; +using PropertyChanged.SourceGenerator; namespace FileTime.ConsoleUI.App; -public class RootViewModel : IRootViewModel +public partial class RootViewModel : IRootViewModel { public string UserName => Environment.UserName; public string MachineName => Environment.MachineName; @@ -19,6 +21,7 @@ public class RootViewModel : IRootViewModel public ICommandPaletteViewModel CommandPalette { get; } public IFrequencyNavigationViewModel FrequencyNavigation { get; } public IItemPreviewService ItemPreviewService { get; } + public IClipboardService ClipboardService { get; } public IDialogService DialogService { get; } public ITimelineViewModel TimelineViewModel { get; } public IDeclarativeProperty VolumeSizeInfo { get;} @@ -32,7 +35,8 @@ public class RootViewModel : IRootViewModel IDialogService dialogService, ITimelineViewModel timelineViewModel, IFrequencyNavigationViewModel frequencyNavigation, - IItemPreviewService itemPreviewService) + IItemPreviewService itemPreviewService, + IClipboardService clipboardService) { AppState = appState; PossibleCommands = possibleCommands; @@ -41,6 +45,7 @@ public class RootViewModel : IRootViewModel TimelineViewModel = timelineViewModel; FrequencyNavigation = frequencyNavigation; ItemPreviewService = itemPreviewService; + ClipboardService = clipboardService; DialogService.ReadInput.PropertyChanged += (o, e) => { @@ -53,6 +58,14 @@ public class RootViewModel : IRootViewModel } }; + itemPreviewService.ItemPreview.PropertyChanged += (o, e) => + { + if (e.PropertyName == nameof(itemPreviewService.ItemPreview.Value)) + { + appState.PreviewType = null; + } + }; + VolumeSizeInfo = appState.SelectedTab .Map(t => t?.CurrentLocation) .Switch() diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs index 750006e..7f78ff1 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -5,6 +5,7 @@ using FileTime.ConsoleUI.App.Configuration; using FileTime.ConsoleUI.App.Controls; using FileTime.ConsoleUI.App.KeyInputHandling; using FileTime.ConsoleUI.App.Services; +using FileTime.ConsoleUI.App.UserCommand; using FileTime.Core.Interactions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +13,16 @@ using Microsoft.Extensions.DependencyInjection.Extensions; namespace FileTime.ConsoleUI.App; +public class StartupHandler : IStartupHandler +{ + public StartupHandler(IIdentifiableUserCommandService identifiableUserCommandService) + { + identifiableUserCommandService.AddIdentifiableUserCommand(NextPreviewUserCommand.Instance); + identifiableUserCommandService.AddIdentifiableUserCommand(PreviousPreviewUserCommand.Instance); + } + public Task InitAsync() => Task.CompletedTask; +} + public static class Startup { public static IServiceCollection AddConsoleServices(this IServiceCollection services, IConfigurationRoot configuration) @@ -27,6 +38,8 @@ public static class Startup services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); services.Configure(configuration); return services; diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/UserCommand/ConsoleUserCommandHandler.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/UserCommand/ConsoleUserCommandHandler.cs new file mode 100644 index 0000000..a80b8c2 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/UserCommand/ConsoleUserCommandHandler.cs @@ -0,0 +1,80 @@ +using FileTime.App.Core.Services; +using FileTime.App.Core.Services.UserCommandHandler; +using FileTime.App.Core.ViewModels.ItemPreview; +using FileTime.ConsoleUI.App.Preview; + +namespace FileTime.ConsoleUI.App.UserCommand; + +public class ConsoleUserCommandHandler : AggregatedUserCommandHandler +{ + private static readonly ItemPreviewType[] ElementPreviewOrder = {ItemPreviewType.Text, ItemPreviewType.Binary}; + + private readonly IConsoleAppState _consoleAppState; + private readonly IItemPreviewService _itemPreviewService; + + public ConsoleUserCommandHandler( + IConsoleAppState consoleAppState, + IItemPreviewService itemPreviewService + ) + { + _consoleAppState = consoleAppState; + _itemPreviewService = itemPreviewService; + AddCommandHandler(new IUserCommandHandler[] + { + new TypeUserCommandHandler(NextPreview), + new TypeUserCommandHandler(PreviousPreview), + }); + } + + private Task NextPreview() + { + if (_itemPreviewService.ItemPreview.Value is not IElementPreviewViewModel) return Task.CompletedTask; + + var previewOrder = ElementPreviewOrder; + if (previewOrder.Length < 2) return Task.CompletedTask; + + if (_consoleAppState.PreviewType == null) + { + _consoleAppState.PreviewType = previewOrder.Length > 1 ? previewOrder[1] : null; + return Task.CompletedTask; + } + + var currentPreviewType = _consoleAppState.PreviewType.Value; + int i; + for (i = 0; i < previewOrder.Length; i++) + { + if (previewOrder[i] == currentPreviewType) break; + } + + i++; + + _consoleAppState.PreviewType = i >= previewOrder.Length ? previewOrder[0] : previewOrder[i]; + return Task.CompletedTask; + } + + private Task PreviousPreview() + { + if (_itemPreviewService.ItemPreview.Value is not IElementPreviewViewModel) return Task.CompletedTask; + + var previewOrder = ElementPreviewOrder; + if (previewOrder.Length < 2) return Task.CompletedTask; + + if (_consoleAppState.PreviewType == null) + { + _consoleAppState.PreviewType = previewOrder.Length > 1 ? previewOrder[^1] : null; + return Task.CompletedTask; + } + + var currentPreviewType = _consoleAppState.PreviewType.Value; + int i; + for (i = previewOrder.Length - 1; i > -1; i--) + { + if (previewOrder[i] == currentPreviewType) break; + } + + i--; + + _consoleAppState.PreviewType = i > -1 ? previewOrder[^1] : previewOrder[i]; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj b/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj index 0b2e74b..c51e521 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj @@ -35,4 +35,8 @@ + + + + diff --git a/src/ConsoleApp/FileTime.ConsoleUI/appsettings.json b/src/ConsoleApp/FileTime.ConsoleUI/appsettings.json new file mode 100644 index 0000000..0f45925 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI/appsettings.json @@ -0,0 +1,4 @@ +{ + "ClipboardSingleIcon": "\udb80\udd4c", + "ClipboardMultipleIcon": "\udb84\ude68" +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs b/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs index fae4268..50aa71c 100644 --- a/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs +++ b/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs @@ -8,6 +8,10 @@ public class MockRenderEngine : IRenderEngine { } + public void VisibilityChanged(IView view, bool newVisibility) + { + } + public void VisibilityChanged(IView view) { } diff --git a/src/Library/TerminalUI/Controls/BinaryView.cs b/src/Library/TerminalUI/Controls/BinaryView.cs new file mode 100644 index 0000000..f0bfb22 --- /dev/null +++ b/src/Library/TerminalUI/Controls/BinaryView.cs @@ -0,0 +1,126 @@ +using System.Reflection.Metadata; +using PropertyChanged.SourceGenerator; +using TerminalUI.Color; +using TerminalUI.Models; +using TerminalUI.Traits; + +namespace TerminalUI.Controls; + +public partial class BinaryView : View, T>, IDisplayView +{ + private record RenderState( + byte[]? Data, + int BytesPerLine, + Position Position, + Size Size, + IColor? Foreground, + IColor? Background); + + private RenderState? _lastRenderState; + + [Notify] private byte[]? _data; + [Notify] private int _bytesPerLine = 16; + + public BinaryView() + { + RerenderProperties.Add(nameof(Data)); + RerenderProperties.Add(nameof(BytesPerLine)); + } + + protected override Size CalculateSize() + { + if (_data is null) return new(0, 0); + if (_data.Length < _bytesPerLine) return new Size(_data.Length, 1); + + var completeLines = (int) Math.Floor((double) _data.Length / _bytesPerLine); + var remaining = completeLines * _bytesPerLine - completeLines; + + var lines = remaining > 0 ? completeLines + 1 : completeLines; + + return new Size(_bytesPerLine * 3 - 1, lines); + } + + protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size) + { + if (size.Width < 2 || size.Height == 0) return false; + + var data = _data; + var bytesPerLine = _bytesPerLine; + if (size.Width < _bytesPerLine * 3 - 1) + { + bytesPerLine = (int) Math.Floor((double) (size.Width + 1) / 3); + } + + var foreground = Foreground ?? renderContext.Foreground; + var background = Background ?? renderContext.Background; + + var renderState = new RenderState( + data, + bytesPerLine, + position, + size, + foreground, + background); + + var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState); + _lastRenderState = renderState; + + if (data is null) return false; + + var lineI = 0; + var textSize = size with {Height = 1}; + for (var i = 0; i < data.Length; i += bytesPerLine, lineI++) + { + if (lineI > size.Height) break; + RenderLine( + renderContext, + data, + i, + i + bytesPerLine, + position with {Y = position.Y + lineI}, + textSize, + skipRender); + } + + return true; + } + + private void RenderLine( + in RenderContext renderContext, + byte[] data, + int startIndex, + int maxEndIndex, + Position position, + Size size, + bool updateCellsOnly + ) + { + Span text = stackalloc char[(maxEndIndex - startIndex) * 3 - 1]; + var textI = 0; + for (var i = startIndex; i < maxEndIndex && i < data.Length; i++, textI += 3) + { + var b = data[i]; + var b1 = (byte) (b >> 4); + var b2 = (byte) (b & 0x0F); + + var c1 = b1 < 10 ? (char) (b1 + '0') : (char) (b1 - 10 + 'A'); + var c2 = b2 < 10 ? (char) (b2 + '0') : (char) (b2 - 10 + 'A'); + text[textI] = c1; + text[textI + 1] = c2; + + if (textI + 2 < text.Length) + text[textI + 2] = ' '; + } + + RenderText( + text, + renderContext, + position, + size, + updateCellsOnly + ); + } + + private bool NeedsRerender(RenderState renderState) + => _lastRenderState is null || _lastRenderState != renderState; +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index 1be3291..a80d70f 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -17,7 +17,8 @@ public sealed partial class TextBlock : View, T>, IDisplayView string? Text, IColor? Foreground, IColor? Background, - ITextFormat? TextFormat); + ITextFormat? TextFormat, + bool AsciiOnly); private RenderState? _lastRenderState; private string[]? _textLines; @@ -26,6 +27,7 @@ public sealed partial class TextBlock : View, T>, IDisplayView [Notify] private TextAlignment _textAlignment = TextAlignment.Left; [Notify] private ITextFormat? _textFormat; [Notify] private int _textStartIndex; + [Notify] private bool _asciiOnly = true; public TextBlock() { @@ -33,6 +35,7 @@ public sealed partial class TextBlock : View, T>, IDisplayView RerenderProperties.Add(nameof(TextAlignment)); RerenderProperties.Add(nameof(TextFormat)); RerenderProperties.Add(nameof(TextStartIndex)); + RerenderProperties.Add(nameof(AsciiOnly)); ((INotifyPropertyChanged) this).PropertyChanged += (o, e) => { @@ -51,19 +54,23 @@ public sealed partial class TextBlock : View, T>, IDisplayView var foreground = Foreground ?? renderContext.Foreground; var background = Background ?? renderContext.Background; + var text = Text; + var textLines = _textLines; + var asciiOnly = _asciiOnly; + var renderState = new RenderState( position, size, - Text, + text, foreground, background, - _textFormat); + _textFormat, + asciiOnly); var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState); _lastRenderState = renderState; - var textLines = _textLines; var textStartIndex = _textStartIndex; if (textLines is null) { @@ -81,7 +88,7 @@ public sealed partial class TextBlock : View, T>, IDisplayView _textStartIndex = textLines.Length - size.Height; } - RenderText(textLines, renderContext, position, size, skipRender, TransformText); + RenderText(textLines, renderContext, position, size, skipRender, TransformText, asciiOnly); return !skipRender; } diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index 1efbf9f..61a7038 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; @@ -220,10 +220,13 @@ public abstract partial class View : IView where TConcrete : Vi private static void UpdateCells(bool[,] renderContextUpdatedCells, Position position, int sizeWidth, int sizeHeight) { + var xLen = renderContextUpdatedCells.GetLength(0); + var yLen = renderContextUpdatedCells.GetLength(1); for (var x = 0; x < sizeWidth; x++) { for (var y = 0; y < sizeHeight; y++) { + if (position.X + x >= xLen || position.Y + y >= yLen) continue; renderContextUpdatedCells[position.X + x, position.Y + y] = true; } }