Controls, Startup&Driver improvements

This commit is contained in:
2023-08-09 11:54:32 +02:00
parent 2528487ff6
commit d549733b71
49 changed files with 875 additions and 120 deletions

View File

@@ -0,0 +1,3 @@
namespace FileTime.App.Core.Configuration;
public record ApplicationConfiguration(bool AllowCloseLastTab);

View File

@@ -4,4 +4,5 @@ public static class SectionNames
{
public const string KeybindingSectionName = "KeyBindings";
public const string ProgramsSectionName = "Programs";
public const string ApplicationSectionName = "Application";
}

View File

@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using DeclarativeProperty;
using FileTime.App.CommandPalette.Services;
using FileTime.App.Core.Configuration;
using FileTime.App.Core.Extensions;
using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.UserCommand;
@@ -14,6 +15,7 @@ using FileTime.Core.Timeline;
using FileTime.Providers.Local;
using InitableService;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace FileTime.App.Core.Services.UserCommandHandler;
@@ -29,6 +31,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
private readonly IFrequencyNavigationService _frequencyNavigationService;
private readonly ICommandPaletteService _commandPaletteService;
private readonly ILogger<NavigationUserCommandHandlerService> _logger;
private readonly ApplicationConfiguration _applicationConfiguration;
private ITabViewModel? _selectedTab;
private IDeclarativeProperty<IContainer?>? _currentLocation;
private IDeclarativeProperty<IItemViewModel?>? _currentSelectedItem;
@@ -44,7 +47,8 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
IUserCommunicationService userCommunicationService,
IFrequencyNavigationService frequencyNavigationService,
ICommandPaletteService commandPaletteService,
ILogger<NavigationUserCommandHandlerService> logger) : base(appState)
ILogger<NavigationUserCommandHandlerService> logger,
ApplicationConfiguration applicationConfiguration) : base(appState)
{
_appState = appState;
_serviceProvider = serviceProvider;
@@ -55,6 +59,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
_frequencyNavigationService = frequencyNavigationService;
_commandPaletteService = commandPaletteService;
_logger = logger;
_applicationConfiguration = applicationConfiguration;
SaveSelectedTab(t => _selectedTab = t);
SaveCurrentSelectedItem(i => _currentSelectedItem = i);
@@ -387,7 +392,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
private Task CloseTab()
{
if (_appState.Tabs.Count < 2 || _selectedTab == null) return Task.CompletedTask;
if ((!_applicationConfiguration.AllowCloseLastTab && _appState.Tabs.Count < 2) || _selectedTab == null) return Task.CompletedTask;
var tabToRemove = _selectedTab;
_appState.RemoveTab(tabToRemove!);

View File

@@ -41,20 +41,16 @@ public static class Startup
.AddSingleton<IExitHandler, ContainerRefreshHandler>();
}
private static IServiceCollection AddCommandHandlers(this IServiceCollection serviceCollection)
{
return serviceCollection
private static IServiceCollection AddCommandHandlers(this IServiceCollection serviceCollection) =>
serviceCollection
.AddSingleton<IUserCommandHandler, NavigationUserCommandHandlerService>()
.AddSingleton<IUserCommandHandler, ItemManipulationUserCommandHandlerService>()
.AddSingleton<IUserCommandHandler, ToolUserCommandHandlerService>()
.AddSingleton<IUserCommandHandler, CommandSchedulerUserCommandHandlerService>();
}
internal static IServiceCollection AddConfiguration(this IServiceCollection serviceCollection, IConfigurationRoot configuration)
{
return serviceCollection
internal static IServiceCollection AddConfiguration(this IServiceCollection serviceCollection, IConfigurationRoot configuration) =>
serviceCollection
.Configure<ProgramsConfiguration>(configuration.GetSection(SectionNames.ProgramsSectionName))
.Configure<KeyBindingConfiguration>(configuration.GetSection(SectionNames.KeybindingSectionName))
.AddSingleton<IConfiguration>(configuration);
}
}

View File

@@ -0,0 +1,6 @@
namespace FileTime.ConsoleUI.App.Configuration;
public class ConsoleApplicationConfiguration
{
public string? ConsoleDriver { get; set; }
}

View File

@@ -1,4 +1,5 @@
using TerminalUI.Models;
using TerminalUI.Color;
using TerminalUI.Models;
namespace FileTime.ConsoleUI.App;

View File

@@ -1,5 +1,7 @@
using FileTime.App.Core.Models;
using System.Collections.Specialized;
using FileTime.App.Core.Models;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.KeyInputHandling;
using TerminalUI;
using TerminalUI.ConsoleDrivers;
@@ -16,6 +18,7 @@ public class App : IApplication
private readonly MainWindow _mainWindow;
private readonly IApplicationContext _applicationContext;
private readonly IConsoleDriver _consoleDriver;
private readonly IAppState _appState;
private readonly IKeyInputHandlerService _keyInputHandlerService;
private readonly Thread _renderThread;
@@ -26,7 +29,8 @@ public class App : IApplication
IAppKeyService<ConsoleKey> appKeyService,
MainWindow mainWindow,
IApplicationContext applicationContext,
IConsoleDriver consoleDriver)
IConsoleDriver consoleDriver,
IAppState appState)
{
_lifecycleService = lifecycleService;
_keyInputHandlerService = keyInputHandlerService;
@@ -35,6 +39,7 @@ public class App : IApplication
_mainWindow = mainWindow;
_applicationContext = applicationContext;
_consoleDriver = consoleDriver;
_appState = appState;
_renderThread = new Thread(Render);
}
@@ -43,6 +48,12 @@ public class App : IApplication
{
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
((INotifyCollectionChanged) _appState.Tabs).CollectionChanged += (_, _) =>
{
if(_appState.Tabs.Count == 0)
_applicationContext.IsRunning = false;
};
_mainWindow.Initialize();
foreach (var rootView in _mainWindow.RootViews())
{
@@ -66,6 +77,7 @@ public class App : IApplication
_keyInputHandlerService.HandleKeyInput(keyEventArgs);
}
}
Thread.Sleep(10);
}
}

View File

@@ -1,8 +1,9 @@
using FileTime.App.Core.ViewModels;
using FileTime.App.Core.ViewModels.Timeline;
using PropertyChanged.SourceGenerator;
namespace FileTime.ConsoleUI.App;
public class ConsoleAppState : AppStateBase, IConsoleAppState
public partial class ConsoleAppState : AppStateBase, IConsoleAppState
{
[Notify] private string? _errorText;
}

View File

@@ -12,6 +12,10 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog" Version="3.0.1" />
</ItemGroup>

View File

@@ -0,0 +1,10 @@
namespace FileTime.ConsoleUI.App;
public class MainConsoleConfiguration
{
public static Dictionary<string, string?> Configuration { get; }
static MainConsoleConfiguration()
{
Configuration = new();
}
}

View File

@@ -4,9 +4,12 @@ using DeclarativeProperty;
using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.ViewModels;
using TerminalUI;
using TerminalUI.Color;
using TerminalUI.Controls;
using TerminalUI.Extensions;
using TerminalUI.Models;
using TerminalUI.ViewExtensions;
using ConsoleColor = TerminalUI.Color.ConsoleColor;
namespace FileTime.ConsoleUI.App;
@@ -17,6 +20,8 @@ public class MainWindow
private readonly ITheme _theme;
private ListView<IAppState, IItemViewModel> _selectedItemsView;
private Grid<object> _grid;
public MainWindow(
IConsoleAppState consoleAppState,
IApplicationContext applicationContext,
@@ -56,9 +61,69 @@ public class MainWindow
_selectedItemsView,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
v => v.ItemsSource);
TestGrid();
}
public IEnumerable<IView> RootViews() => new IView[] {_selectedItemsView};
private void TestGrid()
{
var grid = new Grid<object>
{
ApplicationContext = _applicationContext,
ColumnDefinitionsObject = "Auto Auto",
RowDefinitionsObject = "Auto Auto",
ChildInitializer =
{
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Blue, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(0, 0)
},
Width = 2,
Height = 2,
},
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Red, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(0, 1)
},
Width = 3,
Height = 3,
},
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Green, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(1, 0)
},
Width = 4,
Height = 4,
},
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(1, 1)
},
Width = 5,
Height = 5,
}
}
};
_grid = grid;
}
public IEnumerable<IView> RootViews() => new IView[]
{
_grid, _selectedItemsView
};
private IColor? ToForegroundColor(ItemViewMode viewMode)
=> viewMode switch

View File

@@ -1,8 +1,11 @@
using FileTime.App.Core.Services;
using FileTime.App.Core.Configuration;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Configuration;
using FileTime.ConsoleUI.App.KeyInputHandling;
using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using TerminalUI;
@@ -12,7 +15,7 @@ namespace FileTime.ConsoleUI.App;
public static class Startup
{
public static IServiceCollection AddConsoleServices(this IServiceCollection services)
public static IServiceCollection AddConsoleServices(this IServiceCollection services, IConfigurationRoot configuration)
{
services.TryAddSingleton<IApplication, App>();
services.TryAddSingleton<MainWindow>();
@@ -23,6 +26,9 @@ public static class Startup
services.TryAddSingleton<IAppKeyService<ConsoleKey>, ConsoleAppKeyService>();
services.TryAddSingleton<ISystemClipboardService, ConsoleSystemClipboardService>();
services.AddSingleton<CustomLoggerSink>();
services.TryAddSingleton(new ApplicationConfiguration(true));
services.Configure<ConsoleApplicationConfiguration>(configuration);
services.TryAddSingleton<IApplicationContext>(sp
=> new ApplicationContext

View File

@@ -1,6 +1,7 @@
using FileTime.ConsoleUI.App;
using TerminalUI.Color;
using TerminalUI.Models;
using ConsoleColor = TerminalUI.Models.ConsoleColor;
using ConsoleColor = TerminalUI.Color.ConsoleColor;
namespace FileTime.ConsoleUI.Styles;

View File

@@ -18,10 +18,10 @@ public static class DI
{
public static IServiceProvider ServiceProvider { get; private set; } = null!;
public static void Initialize(IConfigurationRoot configuration, IServiceCollection serviceCollection)
public static IServiceProvider Initialize(IConfigurationRoot configuration)
=> ServiceProvider = DependencyInjection
.RegisterDefaultServices(configuration: configuration, serviceCollection: serviceCollection)
.AddConsoleServices()
.RegisterDefaultServices(configuration: configuration)
.AddConsoleServices(configuration)
.AddLocalProviderServices()
.AddServerCoreServices()
.AddFrequencyNavigation()
@@ -31,6 +31,8 @@ public static class DI
.AddCompression()
.SetupLogging()
.AddLogging(loggingBuilder => loggingBuilder.AddSerilog())
.AddConsoleDriver()
.AddTheme()
.BuildServiceProvider();

View File

@@ -23,6 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />

View File

@@ -0,0 +1,26 @@
using System.Text;
using FileTime.App.Core.Configuration;
using FileTime.ConsoleUI.App.Configuration;
namespace FileTime.ConsoleUI;
public static class Help
{
public static void PrintHelp()
{
StringBuilder sb = new();
sb.AppendLine("Options:");
PrintDriverOption(sb);
Console.Write(sb.ToString());
}
public static void PrintDriverOption(StringBuilder sb)
{
sb.AppendLine($"--{SectionNames.ApplicationSectionName}.{nameof(ConsoleApplicationConfiguration.ConsoleDriver)}");
foreach (var driver in Startup.Drivers.Keys)
{
sb.AppendLine("\t" + driver);
}
}
}

View File

@@ -1,52 +1,75 @@
using FileTime.App.Core;
using System.Diagnostics;
using FileTime.App.Core;
using FileTime.App.Core.Configuration;
using FileTime.ConsoleUI;
using FileTime.ConsoleUI.App;
using FileTime.ConsoleUI.Styles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Serilog;
using Serilog.Debugging;
using TerminalUI.ConsoleDrivers;
IConsoleDriver driver = new WindowsDriver();
driver.Init();
ITheme theme;
if (driver.GetCursorPosition() is not {PosX: 0, PosY: 0})
if(args.Contains("--help"))
{
driver = new DotnetDriver();
driver.Init();
theme = DefaultThemes.ConsoleColorTheme;
}
else
{
theme = DefaultThemes.Color256Theme;
Help.PrintHelp();
return;
}
driver.SetCursorVisible(false);
IConsoleDriver? driver = null;
(AppDataRoot, EnvironmentName) = Init.InitDevelopment();
InitLogging();
try
{
(AppDataRoot, EnvironmentName) = Init.InitDevelopment();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(MainConfiguration.Configuration)
#if DEBUG
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
#endif
.Build();
var configuration = CreateConfiguration(args);
var serviceCollection = new ServiceCollection();
serviceCollection.TryAddSingleton<IConsoleDriver>(driver);
serviceCollection.TryAddSingleton<ITheme>(theme);
var serviceProvider = DI.Initialize(configuration);
DI.Initialize(configuration, serviceCollection);
driver = serviceProvider.GetRequiredService<IConsoleDriver>();
Log.Logger.Debug("Using driver {Driver}", driver.GetType().Name);
driver.SetCursorVisible(false);
var app = DI.ServiceProvider.GetRequiredService<IApplication>();
var app = serviceProvider.GetRequiredService<IApplication>();
app.Run();
}
finally
{
driver.SetCursorVisible(true);
driver.Dispose();
driver?.SetCursorVisible(true);
driver?.Dispose();
}
static void InitLogging()
{
SelfLog.Enable(l => Debug.WriteLine(l));
var logFolder = Path.Combine(AppDataRoot, "logs", "bootstrap");
if (!Directory.Exists(logFolder)) Directory.CreateDirectory(logFolder);
Log.Logger = new LoggerConfiguration()
#if DEBUG || VERBOSE_LOGGING
.MinimumLevel.Verbose()
#endif
.Enrich.FromLogContext()
.WriteTo.File(
Path.Combine(logFolder, "appLog.log"),
fileSizeLimitBytes: 10 * 1024 * 1024,
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true)
.CreateBootstrapLogger();
}
static IConfigurationRoot CreateConfiguration(string[] strings)
{
var configurationRoot = new ConfigurationBuilder()
.AddInMemoryCollection(MainConfiguration.Configuration)
.AddInMemoryCollection(MainConsoleConfiguration.Configuration)
#if DEBUG
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
#endif
.AddCommandLine(strings)
.Build();
return configurationRoot;
}
public partial class Program

View File

@@ -0,0 +1,66 @@
using FileTime.ConsoleUI.App;
using FileTime.ConsoleUI.App.Configuration;
using FileTime.ConsoleUI.Styles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using TerminalUI.ConsoleDrivers;
namespace FileTime.ConsoleUI;
public static class Startup
{
public static readonly Dictionary<string, Func<IConsoleDriver>> Drivers = new()
{
["windows"] = () => new XTermDriver(),
["dotnet"] = () => new DotnetDriver()
};
public static IServiceCollection AddConsoleDriver(this IServiceCollection serviceCollection)
{
serviceCollection.TryAddSingleton<IConsoleDriver>(sp =>
{
var appConfig = sp.GetRequiredService<IOptions<ConsoleApplicationConfiguration>>();
IConsoleDriver? driver = null;
if (appConfig.Value.ConsoleDriver is { } consoleDriver
&& Drivers.TryGetValue(consoleDriver, out var driverFactory))
{
driver = driverFactory();
driver.Init();
}
if (driver == null)
{
driver = new XTermDriver();
var asd = driver.GetCursorPosition();
driver.Init();
if (!driver.Init())
{
driver = new DotnetDriver();
driver.Init();
}
}
return driver;
});
return serviceCollection;
}
public static IServiceCollection AddTheme(this IServiceCollection serviceCollection)
{
serviceCollection.TryAddSingleton<ITheme>(sp =>
{
var driver = sp.GetRequiredService<IConsoleDriver>();
return driver switch
{
XTermDriver _ => DefaultThemes.Color256Theme,
DotnetDriver _ => DefaultThemes.ConsoleColorTheme,
_ => throw new ArgumentOutOfRangeException(nameof(driver))
};
});
return serviceCollection;
}
}

View File

@@ -60,6 +60,7 @@ public static class Startup
serviceCollection.TryAddSingleton<ToastMessageSink>();
serviceCollection.TryAddSingleton<IUserCommunicationService>(s => s.GetRequiredService<IDialogService>());
serviceCollection.TryAddSingleton<IAppKeyService<Key>, GuiAppKeyService>();
serviceCollection.TryAddSingleton(new ApplicationConfiguration(false));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel;
namespace TerminalUI.Models;
namespace TerminalUI.Color;
public record struct Color256(byte Color, ColorType Type) : IColor
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel;
namespace TerminalUI.Models;
namespace TerminalUI.Color;
public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor
{

View File

@@ -1,4 +1,4 @@
namespace TerminalUI.Models;
namespace TerminalUI.Color;
public enum ColorType
{

View File

@@ -1,4 +1,4 @@
namespace TerminalUI.Models;
namespace TerminalUI.Color;
public static class Color256Colors
{

View File

@@ -1,4 +1,4 @@
namespace TerminalUI.Models;
namespace TerminalUI.Color;
public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor
{

View File

@@ -1,4 +1,4 @@
namespace TerminalUI.Models;
namespace TerminalUI.Color;
public interface IColor
{

View File

@@ -1,13 +1,18 @@
using TerminalUI.Models;
using ConsoleColor = TerminalUI.Models.ConsoleColor;
using TerminalUI.Color;
using TerminalUI.Models;
using ConsoleColor = TerminalUI.Color.ConsoleColor;
namespace TerminalUI.ConsoleDrivers;
public class DotnetDriver : IConsoleDriver
{
public virtual void Init() => Console.Clear();
public virtual bool Init()
{
Console.Clear();
return true;
}
public void SetCursorPosition(Position position) => Console.SetCursorPosition(position.PosX, position.PosY);
public void SetCursorPosition(Position position) => Console.SetCursorPosition(position.X, position.Y);
public void ResetColor() => Console.ResetColor();
@@ -21,8 +26,8 @@ public class DotnetDriver : IConsoleDriver
public void Write(char text) => Console.Write(text);
public virtual void Dispose() {}
public virtual void Dispose() => Console.Clear();
public bool CanRead() => Console.KeyAvailable;
public ConsoleKeyInfo ReadKey() => Console.ReadKey(true);
@@ -36,6 +41,9 @@ public class DotnetDriver : IConsoleDriver
public virtual void SetBackgroundColor(IColor background)
{
if (background is not ConsoleColor consoleColor) throw new NotSupportedException();
Console.ForegroundColor = consoleColor.Color;
Console.BackgroundColor = consoleColor.Color;
}
public Size GetBufferSize() => new(Console.BufferWidth, Console.BufferHeight);
public void Clear() => Console.Clear();
}

View File

@@ -1,10 +1,11 @@
using TerminalUI.Models;
using TerminalUI.Color;
using TerminalUI.Models;
namespace TerminalUI.ConsoleDrivers;
public interface IConsoleDriver
{
void Init();
bool Init();
void Dispose();
void SetCursorPosition(Position position);
void ResetColor();
@@ -16,4 +17,6 @@ public interface IConsoleDriver
void SetCursorVisible(bool cursorVisible);
void SetForegroundColor(IColor foreground);
void SetBackgroundColor(IColor background);
Size GetBufferSize();
void Clear();
}

View File

@@ -1,16 +0,0 @@
using TerminalUI.Models;
namespace TerminalUI.ConsoleDrivers;
public sealed class WindowsDriver : DotnetDriver
{
public override void Init() => Console.Out.Write("\x1b[?1049h");
public override void Dispose() => Console.Out.Write("\x1b[?1049l");
public override void SetBackgroundColor(IColor background)
=> Write(background.ToConsoleColor());
public override void SetForegroundColor(IColor foreground)
=> Write(foreground.ToConsoleColor());
}

View File

@@ -0,0 +1,52 @@
using TerminalUI.Color;
using TerminalUI.Models;
using ConsoleColor = TerminalUI.Color.ConsoleColor;
namespace TerminalUI.ConsoleDrivers;
public sealed class XTermDriver : DotnetDriver
{
private Position _initialCursorPosition;
public override bool Init()
{
_initialCursorPosition = GetCursorPosition();
Write("\x1b[?1047h");
var isInitSuccessful = _initialCursorPosition == GetCursorPosition();
if (isInitSuccessful)
{
Clear();
}
return isInitSuccessful;
}
public override void Dispose()
{
Write("\x1b[?1047l");
SetCursorPosition(_initialCursorPosition);
}
public override void SetBackgroundColor(IColor background)
{
if (background is ConsoleColor consoleColor)
{
Console.BackgroundColor = consoleColor.Color;
}
else
{
Write(background.ToConsoleColor());
}
}
public override void SetForegroundColor(IColor foreground)
{
if (foreground is ConsoleColor consoleColor)
{
Console.ForegroundColor = consoleColor.Color;
}
else
{
Write(foreground.ToConsoleColor());
}
}
}

View File

@@ -10,7 +10,7 @@ public abstract class ContentView<T>: View<T>, IContentRenderer
ContentRendererMethod = DefaultContentRender;
}
public IView? Content { get; set; }
public Action<Position> ContentRendererMethod { get; set; }
public Action<Position, Size> ContentRendererMethod { get; set; }
private void DefaultContentRender(Position position) => Content?.Render(position);
private void DefaultContentRender(Position position, Size size) => Content?.Render(position, size);
}

View File

@@ -0,0 +1,248 @@
using System.Collections.ObjectModel;
using TerminalUI.Extensions;
using TerminalUI.Models;
using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls;
public class Grid<T> : View<T>
{
private const int ToBeCalculated = -1;
private readonly ObservableCollection<IView> _children = new();
public ReadOnlyObservableCollection<IView> Children { get; }
public GridChildInitializer<T> ChildInitializer { get; }
public ObservableCollection<RowDefinition> RowDefinitions { get; } = new();
public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new();
public object? ColumnDefinitionsObject
{
get => ColumnDefinitions;
set
{
if (value is IEnumerable<ColumnDefinition> columnDefinitions)
{
ColumnDefinitions.Clear();
foreach (var columnDefinition in columnDefinitions)
{
ColumnDefinitions.Add(columnDefinition);
}
}
else if (value is string s)
{
SetColumnDefinitions(s);
}
else
{
throw new NotSupportedException();
}
}
}
public object? RowDefinitionsObject
{
get => RowDefinitions;
set
{
if (value is IEnumerable<RowDefinition> rowDefinitions)
{
RowDefinitions.Clear();
foreach (var rowDefinition in rowDefinitions)
{
RowDefinitions.Add(rowDefinition);
}
}
else if (value is string s)
{
SetRowDefinitions(s);
}
else
{
throw new NotSupportedException();
}
}
}
public Grid()
{
ChildInitializer = new GridChildInitializer<T>(this);
Children = new ReadOnlyObservableCollection<IView>(_children);
_children.CollectionChanged += (o, e) =>
{
if (Attached)
{
if (e.NewItems?.OfType<IView>() is { } newItems)
{
foreach (var newItem in newItems)
{
newItem.Attached = true;
}
}
ApplicationContext?.EventLoop.RequestRerender();
}
};
}
public override Size GetRequestedSize() => throw new NotImplementedException();
protected override void DefaultRenderer(Position position, Size size)
{
//TODO: Optimize it, dont calculate all of these only if there is Auto value(s)
var columns = ColumnDefinitions.Count;
Span<int> allWidth = stackalloc int[columns * RowDefinitions.Count];
Span<int> allHeight = stackalloc int[columns * RowDefinitions.Count];
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
allWidth.SetToMatrix(childSize.Width, x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns);
}
Span<int> columnWidths = stackalloc int[columns];
Span<int> rowHeights = stackalloc int[RowDefinitions.Count];
for (var i = 0; i < columnWidths.Length; i++)
{
if (ColumnDefinitions[i].Type == GridUnitType.Pixel)
{
columnWidths[i] = ColumnDefinitions[i].Value;
}
else if (ColumnDefinitions[i].Type == GridUnitType.Star)
{
columnWidths[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < RowDefinitions.Count; j++)
{
max = Math.Max(max, allWidth.GetFromMatrix(i, j, columns));
}
columnWidths[i] = max;
}
}
for (var i = 0; i < rowHeights.Length; i++)
{
if (RowDefinitions[i].Type == GridUnitType.Pixel)
{
rowHeights[i] = RowDefinitions[i].Value;
}
else if (RowDefinitions[i].Type == GridUnitType.Star)
{
rowHeights[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < columns; j++)
{
max = Math.Max(max, allHeight.GetFromMatrix(j, i, columns));
}
rowHeights[i] = max;
}
}
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
var width = columnWidths[x];
var height = rowHeights[y];
var left = 0;
var top = 0;
for (var i = 0; i < x; i++)
{
left += columnWidths[i];
}
for (var i = 0; i < y; i++)
{
top += rowHeights[i];
}
child.Render(new Position(left, top), new Size(width, height));
}
}
public void SetRowDefinitions(string value)
{
var values = value.Split(' ');
RowDefinitions.Clear();
foreach (var v in values)
{
if (v == "Auto")
{
RowDefinitions.Add(RowDefinition.Auto);
}
else if (v.EndsWith("*"))
{
var starValue = int.Parse(v[0..^1]);
RowDefinitions.Add(RowDefinition.Star(starValue));
}
else if (int.TryParse(v, out var pixelValue))
{
RowDefinitions.Add(RowDefinition.Pixel(pixelValue));
}
else
{
throw new ArgumentException("Invalid row definition: " + v);
}
}
}
public void SetColumnDefinitions(string value)
{
var values = value.Split(' ');
ColumnDefinitions.Clear();
foreach (var v in values)
{
if (v == "Auto")
{
ColumnDefinitions.Add(ColumnDefinition.Auto);
}
else if (v.EndsWith("*"))
{
var starValue = int.Parse(v[0..^1]);
ColumnDefinitions.Add(ColumnDefinition.Star(starValue));
}
else if (int.TryParse(v, out var pixelValue))
{
ColumnDefinitions.Add(ColumnDefinition.Pixel(pixelValue));
}
else
{
throw new ArgumentException("Invalid column definition: " + v);
}
}
}
public override TChild AddChild<TChild>(TChild child)
{
child = base.AddChild(child);
_children.Add(child);
return child;
}
public override TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TDataContext : default
{
child = base.AddChild(child, dataContextMapper);
_children.Add(child);
return child;
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections;
namespace TerminalUI.Controls;
public record ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Func<TSourceDataContext?, TTargetDataContext?> DataContextMapper);
public class GridChildInitializer<T> : IEnumerable<IView>
{
private readonly Grid<T> _grid;
public GridChildInitializer(Grid<T> grid)
{
_grid = grid;
}
public void Add(IView<T> item) => _grid.AddChild(item);
public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item)
=> _grid.AddChild(item.Child, item.DataContextMapper);
public IEnumerator<IView> GetEnumerator() => _grid.Children.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -0,0 +1,23 @@
namespace TerminalUI.Controls;
public enum GridUnitType
{
Auto,
Pixel,
Star
}
public record struct RowDefinition(GridUnitType Type, int Value)
{
public static RowDefinition Auto => new(GridUnitType.Auto, 0);
public static RowDefinition Pixel(int value) => new(GridUnitType.Pixel, value);
public static RowDefinition Star(int value) => new(GridUnitType.Star, value);
}
public record struct ColumnDefinition(GridUnitType Type, int Value)
{
public static ColumnDefinition Auto => new(GridUnitType.Auto, 0);
public static ColumnDefinition Pixel(int value) => new(GridUnitType.Pixel, value);
public static ColumnDefinition Star(int value) => new(GridUnitType.Star, value);
}

View File

@@ -7,10 +7,20 @@ namespace TerminalUI.Controls;
public interface IView : INotifyPropertyChanged, IDisposableCollection
{
object? DataContext { get; set; }
Action<Position> RenderMethod { get; set; }
IApplicationContext? ApplicationContext { get; init;}
int? MinWidth { get; set; }
int? MaxWidth { get; set; }
int? Width { get; set; }
int? MinHeight { get; set; }
int? MaxHeight { get; set; }
int? Height { get; set; }
bool Attached { get; set; }
Size GetRequestedSize();
IApplicationContext? ApplicationContext { get; set; }
List<object> Extensions { get; }
Action<Position, Size> RenderMethod { get; set; }
event Action<IView> Disposed;
void Render(Position position);
void Render(Position position, Size size);
}
public interface IView<T> : IView
@@ -28,4 +38,9 @@ public interface IView<T> : IView
TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new();
TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>;
}

View File

@@ -1,5 +1,6 @@
using System.Buffers;
using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using DeclarativeProperty;
using TerminalUI.Models;
@@ -14,6 +15,23 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
private object? _itemsSource;
private ListViewItem<TItem>[]? _listViewItems;
private int _listViewItemLength;
private int _selectedIndex = 0;
private int _renderStartIndex = 0;
private Size _requestedItemSize = new(0, 0);
public int SelectedIndex
{
get => _selectedIndex;
set
{
if (_selectedIndex != value)
{
_selectedIndex = value;
OnPropertyChanged();
ApplicationContext?.EventLoop.RequestRerender();
}
}
}
public object? ItemsSource
{
@@ -64,13 +82,53 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
protected override void DefaultRenderer(Position position)
public override Size GetRequestedSize()
{
if (_listViewItems is null || _listViewItems.Length == 0)
return new Size(0, 0);
var itemSize = _listViewItems[0].GetRequestedSize();
_requestedItemSize = itemSize;
return itemSize with {Height = itemSize.Height * _listViewItems.Length};
}
protected override void DefaultRenderer(Position position, Size size)
{
var listViewItems = InstantiateItemViews();
var deltaY = 0;
foreach (var item in listViewItems)
if (listViewItems.Length == 0) return;
var requestedItemSize = _requestedItemSize;
var itemsToRender = listViewItems.Length;
var heightNeeded = requestedItemSize.Height * listViewItems.Length;
var renderStartIndex = _renderStartIndex;
if (heightNeeded < size.Height)
{
item.Render(position with {PosY = position.PosY + deltaY++});
var maxItemsToRender = (int) Math.Floor((double) size.Height / requestedItemSize.Height);
if (SelectedIndex < renderStartIndex)
{
renderStartIndex = SelectedIndex - 1;
}
else if (SelectedIndex > renderStartIndex + maxItemsToRender)
{
renderStartIndex = SelectedIndex - maxItemsToRender + 1;
}
if(renderStartIndex < 0)
renderStartIndex = 0;
else if (renderStartIndex + maxItemsToRender > listViewItems.Length)
renderStartIndex = listViewItems.Length - maxItemsToRender;
_renderStartIndex = renderStartIndex;
}
var deltaY = 0;
for (var i = renderStartIndex; i < itemsToRender && i < listViewItems.Length; i++)
{
var item = listViewItems[i];
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize);
deltaY += requestedItemSize.Height;
}
}

View File

@@ -4,7 +4,13 @@ namespace TerminalUI.Controls;
public class ListViewItem<T> : ContentView<T>
{
protected override void DefaultRenderer(Position position)
public override Size GetRequestedSize()
{
if (Content is null) return new Size(0, 0);
return Content.GetRequestedSize();
}
protected override void DefaultRenderer(Position position, Size size)
{
if (ContentRendererMethod is null)
{
@@ -16,6 +22,6 @@ public class ListViewItem<T> : ContentView<T>
+ DataContext?.GetType().Name);
}
ContentRendererMethod(position);
ContentRendererMethod(position, size);
}
}

View File

@@ -0,0 +1,24 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public partial class Rectangle<T> : View<T>
{
[Notify] private IColor? _fill;
public override Size GetRequestedSize() => new(Width ?? 0, Height ?? 0);
protected override void DefaultRenderer(Position position, Size size)
{
var s = new string('█', Width ?? size.Width);
ApplicationContext?.ConsoleDriver.SetBackgroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Background));
ApplicationContext?.ConsoleDriver.SetForegroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground));
var height = Height ?? size.Height;
for (var i = 0; i < height; i++)
{
ApplicationContext?.ConsoleDriver.SetCursorPosition(position with {Y = position.Y + i});
ApplicationContext?.ConsoleDriver.Write(s);
}
}
}

View File

@@ -1,4 +1,5 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Extensions;
using TerminalUI.Models;
@@ -27,7 +28,9 @@ public partial class TextBlock<T> : View<T>
RerenderProperties.Add(nameof(Background));
}
protected override void DefaultRenderer(Position position)
public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1);
protected override void DefaultRenderer(Position position, Size size)
{
var driver = ApplicationContext!.ConsoleDriver;
var renderContext = new RenderContext(position, Text, _foreground, _background);

View File

@@ -10,8 +10,30 @@ public abstract partial class View<T> : IView<T>
{
private readonly List<IDisposable> _disposables = new();
[Notify] private T? _dataContext;
public Action<Position> RenderMethod { get; set; }
public IApplicationContext? ApplicationContext { get; init; }
[Notify] private int? _minWidth;
[Notify] private int? _maxWidth;
[Notify] private int? _width;
[Notify] private int? _minHeight;
[Notify] private int? _maxHeight;
[Notify] private int? _height;
private bool _attached;
public bool Attached
{
get => _attached;
set
{
if (_attached == value) return;
_attached = value;
if (value)
{
AttachChildren();
}
}
}
public List<object> Extensions { get; } = new();
public Action<Position, Size> RenderMethod { get; set; }
public IApplicationContext? ApplicationContext { get; set; }
public event Action<IView>? Disposed;
protected List<string> RerenderProperties { get; } = new();
@@ -20,22 +42,27 @@ public abstract partial class View<T> : IView<T>
RenderMethod = DefaultRenderer;
((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged;
}
public abstract Size GetRequestedSize();
protected virtual void AttachChildren()
{
}
private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is not null
&& (e.PropertyName == nameof(IView.DataContext)
if (e.PropertyName is not null
&& (e.PropertyName == nameof(IView.DataContext)
|| RerenderProperties.Contains(e.PropertyName)
)
)
)
{
ApplicationContext?.EventLoop.RequestRerender();
}
}
protected abstract void DefaultRenderer(Position position);
protected abstract void DefaultRenderer(Position position, Size size);
public void Render(Position position)
public void Render(Position position, Size size)
{
if (RenderMethod is null)
{
@@ -47,16 +74,27 @@ public abstract partial class View<T> : IView<T>
+ DataContext?.GetType().Name);
}
RenderMethod(position);
RenderMethod(position, size);
}
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
{
var child = new TChild
{
DataContext = DataContext,
ApplicationContext = ApplicationContext
};
var child = new TChild();
return AddChild(child);
}
public TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new()
{
var child = new TChild();
return AddChild(child, dataContextMapper);
}
public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T>
{
child.DataContext = DataContext;
child.ApplicationContext = ApplicationContext;
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
AddDisposable(mapper);
child.AddDisposable(mapper);
@@ -64,14 +102,12 @@ public abstract partial class View<T> : IView<T>
return child;
}
public TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new()
public virtual TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>
{
var child = new TChild
{
DataContext = dataContextMapper(DataContext),
ApplicationContext = ApplicationContext
};
child.DataContext = dataContextMapper(DataContext);
child.ApplicationContext = ApplicationContext;
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
AddDisposable(mapper);
child.AddDisposable(mapper);

View File

@@ -45,9 +45,11 @@ public class EventLoop : IEventLoop
viewsToRender = _viewsToRender.ToList();
}
var size =_applicationContext.ConsoleDriver.GetBufferSize();
foreach (var view in viewsToRender)
{
view.Render(new Position(0, 0));
view.Attached = true;
view.Render(new Position(0, 0), size);
}
}

View File

@@ -0,0 +1,7 @@
namespace TerminalUI.Extensions;
public static class SpanExtensions
{
public static T GetFromMatrix<T>(this Span<T> span, int x, int y, int width) => span[y * width + x];
public static void SetToMatrix<T>(this Span<T> span, T value, int x, int y, int width) => span[y * width + x] = value;
}

View File

@@ -0,0 +1,14 @@
using TerminalUI.Controls;
namespace TerminalUI.Extensions;
public static class ViewExtensions
{
public static T? GetExtension<T>(this IView view)
=> (T?) view.Extensions.FirstOrDefault(e => e is T);
public static ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext> WithDataContextMapper<TSourceDataContext, TTargetDataContext>(
this IView<TTargetDataContext> view,
Func<TSourceDataContext?, TTargetDataContext?> dataContextMapper)
=> new(view, dataContextMapper);
}

View File

@@ -1,3 +1,3 @@
namespace TerminalUI.Models;
public record struct Position(int PosX, int PosY);
public record struct Position(int X, int Y);

View File

@@ -0,0 +1,3 @@
namespace TerminalUI.Models;
public record Size(int Width, int Height);

View File

@@ -0,0 +1,12 @@
using TerminalUI.Controls;
using TerminalUI.Models;
namespace TerminalUI.Rendering;
public class RenderingEngine
{
public static void Asd(IView root, Size bufferSize)
{
var rootSize = root.GetRequestedSize();
}
}

View File

@@ -6,5 +6,5 @@ namespace TerminalUI.Traits;
public interface IContentRenderer
{
IView? Content { get; set; }
Action<Position> ContentRendererMethod { get; set; }
Action<Position, Size> ContentRendererMethod { get; set; }
}

View File

@@ -0,0 +1,3 @@
namespace TerminalUI.ViewExtensions;
public record GridPositionExtension(int Row, int Column);

View File

@@ -56,11 +56,11 @@ void HandleStartup(Action action)
IConfigurationRoot CreateConfiguration()
{
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddCommandLine(args);
#if DEBUG
configurationBuilder.AddJsonFile("appsettings.Development.json", optional: true);
configurationBuilder.AddJsonFile("appsettings.Local.json", optional: true);
#endif
configurationBuilder.AddCommandLine(args);
return configurationBuilder.Build();
}

View File

@@ -18,8 +18,8 @@ public class CompressionUserCommandHandler : AggregatedUserCommandHandler
IClipboardService clipboardService)
{
_clipboardService = clipboardService;
_markedItems = appState.SelectedTab.Map(t => t.MarkedItems).Switch();
_selectedItem = appState.SelectedTab.Map(t => t.CurrentSelectedItem).Switch();
_markedItems = appState.SelectedTab.Map(t => t?.MarkedItems).Switch();
_selectedItem = appState.SelectedTab.Map(t => t?.CurrentSelectedItem).Switch();
AddCommandHandler(new[]
{