Terminal UI V2, advanced binding
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
20
src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs
Normal file
20
src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
|
public interface ITheme
|
||||||
|
{
|
||||||
|
IColor? ItemBackgroundColor { get; }
|
||||||
|
IColor? AlternativeItemBackgroundColor { get; }
|
||||||
|
IColor? SelectedItemBackgroundColor { get; }
|
||||||
|
IColor? MarkedItemBackgroundColor { get; }
|
||||||
|
IColor? MarkedAlternativeItemBackgroundColor { get; }
|
||||||
|
IColor? MarkedSelectedItemBackgroundColor { get; }
|
||||||
|
IColor? DefaultForegroundColor { get; }
|
||||||
|
IColor? DefaultBackgroundColor { get; }
|
||||||
|
IColor? AlternativeItemForegroundColor { get; }
|
||||||
|
IColor? SelectedItemForegroundColor { get; }
|
||||||
|
IColor? MarkedItemForegroundColor { get; }
|
||||||
|
IColor? MarkedAlternativeItemForegroundColor { get; }
|
||||||
|
IColor? MarkedSelectedItemForegroundColor { get; }
|
||||||
|
}
|
||||||
@@ -1,32 +1,42 @@
|
|||||||
using FileTime.App.Core.Services;
|
using FileTime.App.Core.Models;
|
||||||
|
using FileTime.App.Core.Services;
|
||||||
using FileTime.ConsoleUI.App.KeyInputHandling;
|
using FileTime.ConsoleUI.App.KeyInputHandling;
|
||||||
using TerminalUI;
|
using TerminalUI;
|
||||||
|
using TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
public class App : IApplication
|
public class App : IApplication
|
||||||
{
|
{
|
||||||
private readonly ILifecycleService _lifecycleService;
|
private readonly ILifecycleService _lifecycleService;
|
||||||
|
|
||||||
private readonly IConsoleAppState _consoleAppState;
|
private readonly IConsoleAppState _consoleAppState;
|
||||||
//private readonly IAppKeyService<Key> _appKeyService;
|
|
||||||
|
private readonly IAppKeyService<ConsoleKey> _appKeyService;
|
||||||
private readonly MainWindow _mainWindow;
|
private readonly MainWindow _mainWindow;
|
||||||
private readonly IApplicationContext _applicationContext;
|
private readonly IApplicationContext _applicationContext;
|
||||||
|
private readonly IConsoleDriver _consoleDriver;
|
||||||
private readonly IKeyInputHandlerService _keyInputHandlerService;
|
private readonly IKeyInputHandlerService _keyInputHandlerService;
|
||||||
|
private readonly Thread _renderThread;
|
||||||
|
|
||||||
public App(
|
public App(
|
||||||
ILifecycleService lifecycleService,
|
ILifecycleService lifecycleService,
|
||||||
IKeyInputHandlerService keyInputHandlerService,
|
IKeyInputHandlerService keyInputHandlerService,
|
||||||
IConsoleAppState consoleAppState,
|
IConsoleAppState consoleAppState,
|
||||||
//IAppKeyService<Key> appKeyService,
|
IAppKeyService<ConsoleKey> appKeyService,
|
||||||
MainWindow mainWindow,
|
MainWindow mainWindow,
|
||||||
IApplicationContext applicationContext)
|
IApplicationContext applicationContext,
|
||||||
|
IConsoleDriver consoleDriver)
|
||||||
{
|
{
|
||||||
_lifecycleService = lifecycleService;
|
_lifecycleService = lifecycleService;
|
||||||
_keyInputHandlerService = keyInputHandlerService;
|
_keyInputHandlerService = keyInputHandlerService;
|
||||||
_consoleAppState = consoleAppState;
|
_consoleAppState = consoleAppState;
|
||||||
//_appKeyService = appKeyService;
|
_appKeyService = appKeyService;
|
||||||
_mainWindow = mainWindow;
|
_mainWindow = mainWindow;
|
||||||
_applicationContext = applicationContext;
|
_applicationContext = applicationContext;
|
||||||
|
_consoleDriver = consoleDriver;
|
||||||
|
|
||||||
|
_renderThread = new Thread(Render);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run()
|
public void Run()
|
||||||
@@ -34,7 +44,31 @@ public class App : IApplication
|
|||||||
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
|
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
|
||||||
|
|
||||||
_mainWindow.Initialize();
|
_mainWindow.Initialize();
|
||||||
|
foreach (var rootView in _mainWindow.RootViews())
|
||||||
|
{
|
||||||
|
_applicationContext.EventLoop.AddViewToRender(rootView);
|
||||||
|
}
|
||||||
|
|
||||||
_applicationContext.EventLoop.Run();
|
_applicationContext.IsRunning = true;
|
||||||
|
_renderThread.Start();
|
||||||
|
|
||||||
|
while (_applicationContext.IsRunning)
|
||||||
|
{
|
||||||
|
if (_consoleDriver.CanRead())
|
||||||
|
{
|
||||||
|
var key = _consoleDriver.ReadKey();
|
||||||
|
if (_appKeyService.MapKey(key.Key) is { } mappedKey)
|
||||||
|
{
|
||||||
|
var keyEventArgs = new GeneralKeyEventArgs
|
||||||
|
{
|
||||||
|
Key = mappedKey
|
||||||
|
};
|
||||||
|
_keyInputHandlerService.HandleKeyInput(keyEventArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Render() => _applicationContext.EventLoop.Run();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using FileTime.App.Core.Models;
|
||||||
|
using FileTime.App.Core.Services;
|
||||||
|
|
||||||
|
namespace FileTime.ConsoleUI.App.KeyInputHandling;
|
||||||
|
|
||||||
|
public class ConsoleAppKeyService : IAppKeyService<ConsoleKey>
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<ConsoleKey, Keys> KeyMapping;
|
||||||
|
|
||||||
|
//TODO: write test for this. Test if every enum value is present in the dictionary.
|
||||||
|
public static ReadOnlyDictionary<ConsoleKey, Keys> KeyMappingReadOnly { get; }
|
||||||
|
|
||||||
|
static ConsoleAppKeyService()
|
||||||
|
{
|
||||||
|
KeyMapping = new Dictionary<ConsoleKey, Keys>
|
||||||
|
{
|
||||||
|
{ConsoleKey.A, Keys.A},
|
||||||
|
{ConsoleKey.B, Keys.B},
|
||||||
|
{ConsoleKey.C, Keys.C},
|
||||||
|
{ConsoleKey.D, Keys.D},
|
||||||
|
{ConsoleKey.E, Keys.E},
|
||||||
|
{ConsoleKey.F, Keys.F},
|
||||||
|
{ConsoleKey.G, Keys.G},
|
||||||
|
{ConsoleKey.H, Keys.H},
|
||||||
|
{ConsoleKey.I, Keys.I},
|
||||||
|
{ConsoleKey.J, Keys.J},
|
||||||
|
{ConsoleKey.K, Keys.K},
|
||||||
|
{ConsoleKey.L, Keys.L},
|
||||||
|
{ConsoleKey.M, Keys.M},
|
||||||
|
{ConsoleKey.N, Keys.N},
|
||||||
|
{ConsoleKey.O, Keys.O},
|
||||||
|
{ConsoleKey.P, Keys.P},
|
||||||
|
{ConsoleKey.Q, Keys.Q},
|
||||||
|
{ConsoleKey.R, Keys.R},
|
||||||
|
{ConsoleKey.S, Keys.S},
|
||||||
|
{ConsoleKey.T, Keys.T},
|
||||||
|
{ConsoleKey.U, Keys.U},
|
||||||
|
{ConsoleKey.V, Keys.V},
|
||||||
|
{ConsoleKey.W, Keys.W},
|
||||||
|
{ConsoleKey.X, Keys.X},
|
||||||
|
{ConsoleKey.Y, Keys.Y},
|
||||||
|
{ConsoleKey.Z, Keys.Z},
|
||||||
|
{ConsoleKey.F1, Keys.F1},
|
||||||
|
{ConsoleKey.F2, Keys.F2},
|
||||||
|
{ConsoleKey.F3, Keys.F3},
|
||||||
|
{ConsoleKey.F4, Keys.F4},
|
||||||
|
{ConsoleKey.F5, Keys.F5},
|
||||||
|
{ConsoleKey.F6, Keys.F6},
|
||||||
|
{ConsoleKey.F7, Keys.F7},
|
||||||
|
{ConsoleKey.F8, Keys.F8},
|
||||||
|
{ConsoleKey.F9, Keys.F9},
|
||||||
|
{ConsoleKey.F10, Keys.F10},
|
||||||
|
{ConsoleKey.F11, Keys.F11},
|
||||||
|
{ConsoleKey.F12, Keys.F12},
|
||||||
|
{ConsoleKey.D0, Keys.Num0},
|
||||||
|
{ConsoleKey.D1, Keys.Num1},
|
||||||
|
{ConsoleKey.D2, Keys.Num2},
|
||||||
|
{ConsoleKey.D3, Keys.Num3},
|
||||||
|
{ConsoleKey.D4, Keys.Num4},
|
||||||
|
{ConsoleKey.D5, Keys.Num5},
|
||||||
|
{ConsoleKey.D6, Keys.Num6},
|
||||||
|
{ConsoleKey.D7, Keys.Num7},
|
||||||
|
{ConsoleKey.D8, Keys.Num8},
|
||||||
|
{ConsoleKey.D9, Keys.Num9},
|
||||||
|
{ConsoleKey.UpArrow, Keys.Up},
|
||||||
|
{ConsoleKey.DownArrow, Keys.Down},
|
||||||
|
{ConsoleKey.LeftArrow, Keys.Left},
|
||||||
|
{ConsoleKey.RightArrow, Keys.Right},
|
||||||
|
{ConsoleKey.Enter, Keys.Enter},
|
||||||
|
{ConsoleKey.Escape, Keys.Escape},
|
||||||
|
{ConsoleKey.Backspace, Keys.Backspace},
|
||||||
|
{ConsoleKey.Spacebar, Keys.Space},
|
||||||
|
{ConsoleKey.PageUp, Keys.PageUp},
|
||||||
|
{ConsoleKey.PageDown, Keys.PageDown},
|
||||||
|
{ConsoleKey.OemComma, Keys.Comma},
|
||||||
|
{(ConsoleKey)0xA1, Keys.Question},
|
||||||
|
{ConsoleKey.Tab, Keys.Tab},
|
||||||
|
{ConsoleKey.LeftWindows, Keys.LWin},
|
||||||
|
{ConsoleKey.RightWindows, Keys.RWin},
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyMappingReadOnly = new(KeyMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Keys? MapKey(ConsoleKey key)
|
||||||
|
{
|
||||||
|
if (!KeyMapping.TryGetValue(key, out var mappedKey)) return null;
|
||||||
|
return mappedKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using DeclarativeProperty;
|
using DeclarativeProperty;
|
||||||
|
using FileTime.App.Core.Models.Enums;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
using TerminalUI;
|
using TerminalUI;
|
||||||
using TerminalUI.Controls;
|
using TerminalUI.Controls;
|
||||||
using TerminalUI.Extensions;
|
using TerminalUI.Extensions;
|
||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
@@ -11,22 +14,28 @@ public class MainWindow
|
|||||||
{
|
{
|
||||||
private readonly IConsoleAppState _consoleAppState;
|
private readonly IConsoleAppState _consoleAppState;
|
||||||
private readonly IApplicationContext _applicationContext;
|
private readonly IApplicationContext _applicationContext;
|
||||||
private const int ParentColumnWidth = 20;
|
private readonly ITheme _theme;
|
||||||
|
private ListView<IAppState, IItemViewModel> _selectedItemsView;
|
||||||
|
|
||||||
public MainWindow(IConsoleAppState consoleAppState, IApplicationContext applicationContext)
|
public MainWindow(
|
||||||
|
IConsoleAppState consoleAppState,
|
||||||
|
IApplicationContext applicationContext,
|
||||||
|
ITheme theme)
|
||||||
{
|
{
|
||||||
_consoleAppState = consoleAppState;
|
_consoleAppState = consoleAppState;
|
||||||
_applicationContext = applicationContext;
|
_applicationContext = applicationContext;
|
||||||
|
_theme = theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
ListView<IAppState, IItemViewModel> selectedItemsView = new()
|
_selectedItemsView = new()
|
||||||
{
|
{
|
||||||
|
DataContext = _consoleAppState,
|
||||||
ApplicationContext = _applicationContext
|
ApplicationContext = _applicationContext
|
||||||
};
|
};
|
||||||
selectedItemsView.DataContext = _consoleAppState;
|
|
||||||
selectedItemsView.ItemTemplate = item =>
|
_selectedItemsView.ItemTemplate = item =>
|
||||||
{
|
{
|
||||||
var textBlock = item.CreateChild<TextBlock<IItemViewModel>>();
|
var textBlock = item.CreateChild<TextBlock<IItemViewModel>>();
|
||||||
textBlock.Bind(
|
textBlock.Bind(
|
||||||
@@ -34,15 +43,44 @@ public class MainWindow
|
|||||||
dc => dc == null ? string.Empty : dc.DisplayNameText,
|
dc => dc == null ? string.Empty : dc.DisplayNameText,
|
||||||
tb => tb.Text
|
tb => tb.Text
|
||||||
);
|
);
|
||||||
|
textBlock.Bind(
|
||||||
|
textBlock,
|
||||||
|
dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value),
|
||||||
|
tb => tb.Foreground
|
||||||
|
);
|
||||||
|
|
||||||
return textBlock;
|
return textBlock;
|
||||||
};
|
};
|
||||||
|
|
||||||
selectedItemsView.Bind(
|
_selectedItemsView.Bind(
|
||||||
selectedItemsView,
|
_selectedItemsView,
|
||||||
appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
|
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
|
||||||
v => v.ItemsSource);
|
v => v.ItemsSource);
|
||||||
|
|
||||||
selectedItemsView.RequestRerender();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IView> RootViews() => new IView[] {_selectedItemsView};
|
||||||
|
|
||||||
|
private IColor? ToForegroundColor(ItemViewMode viewMode)
|
||||||
|
=> viewMode switch
|
||||||
|
{
|
||||||
|
ItemViewMode.Default => _theme.DefaultForegroundColor,
|
||||||
|
ItemViewMode.Alternative => _theme.AlternativeItemForegroundColor,
|
||||||
|
ItemViewMode.Selected => _theme.SelectedItemForegroundColor,
|
||||||
|
ItemViewMode.Marked => _theme.MarkedItemForegroundColor,
|
||||||
|
ItemViewMode.MarkedSelected => _theme.MarkedSelectedItemForegroundColor,
|
||||||
|
ItemViewMode.MarkedAlternative => _theme.MarkedAlternativeItemForegroundColor,
|
||||||
|
_ => throw new NotImplementedException()
|
||||||
|
};
|
||||||
|
|
||||||
|
private IColor? ToBackgroundColor(ItemViewMode viewMode)
|
||||||
|
=> viewMode switch
|
||||||
|
{
|
||||||
|
ItemViewMode.Default => _theme.DefaultBackgroundColor,
|
||||||
|
ItemViewMode.Alternative => _theme.AlternativeItemBackgroundColor,
|
||||||
|
ItemViewMode.Selected => _theme.SelectedItemBackgroundColor,
|
||||||
|
ItemViewMode.Marked => _theme.MarkedItemBackgroundColor,
|
||||||
|
ItemViewMode.MarkedSelected => _theme.MarkedSelectedItemBackgroundColor,
|
||||||
|
ItemViewMode.MarkedAlternative => _theme.MarkedAlternativeItemBackgroundColor,
|
||||||
|
_ => throw new NotImplementedException()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using FileTime.App.Core.Services;
|
||||||
|
using FileTime.Core.Models;
|
||||||
|
|
||||||
|
namespace FileTime.ConsoleUI.App.Services;
|
||||||
|
|
||||||
|
public class ConsoleSystemClipboardService : ISystemClipboardService
|
||||||
|
{
|
||||||
|
public Task CopyToClipboardAsync(string text) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<IEnumerable<FullName>> GetFilesAsync() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task SetFilesAsync(IEnumerable<FullName> files) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using FileTime.Core.Interactions;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using TerminalUI;
|
using TerminalUI;
|
||||||
|
using TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
@@ -19,9 +20,16 @@ public static class Startup
|
|||||||
services.TryAddSingleton<IAppState>(sp => sp.GetRequiredService<IConsoleAppState>());
|
services.TryAddSingleton<IAppState>(sp => sp.GetRequiredService<IConsoleAppState>());
|
||||||
services.TryAddSingleton<IUserCommunicationService, ConsoleUserCommunicationService>();
|
services.TryAddSingleton<IUserCommunicationService, ConsoleUserCommunicationService>();
|
||||||
services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
|
services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
|
||||||
|
services.TryAddSingleton<IAppKeyService<ConsoleKey>, ConsoleAppKeyService>();
|
||||||
|
services.TryAddSingleton<ISystemClipboardService, ConsoleSystemClipboardService>();
|
||||||
services.AddSingleton<CustomLoggerSink>();
|
services.AddSingleton<CustomLoggerSink>();
|
||||||
|
|
||||||
services.TryAddSingleton<IApplicationContext, ApplicationContext>();
|
services.TryAddSingleton<IApplicationContext>(sp
|
||||||
|
=> new ApplicationContext
|
||||||
|
{
|
||||||
|
ConsoleDriver = sp.GetRequiredService<IConsoleDriver>()
|
||||||
|
}
|
||||||
|
);
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
53
src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs
Normal file
53
src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using FileTime.ConsoleUI.App;
|
||||||
|
using TerminalUI.Models;
|
||||||
|
using ConsoleColor = TerminalUI.Models.ConsoleColor;
|
||||||
|
|
||||||
|
namespace FileTime.ConsoleUI.Styles;
|
||||||
|
|
||||||
|
public record Theme(
|
||||||
|
IColor? ItemBackgroundColor,
|
||||||
|
IColor? AlternativeItemBackgroundColor,
|
||||||
|
IColor? SelectedItemBackgroundColor,
|
||||||
|
IColor? MarkedItemBackgroundColor,
|
||||||
|
IColor? MarkedAlternativeItemBackgroundColor,
|
||||||
|
IColor? MarkedSelectedItemBackgroundColor,
|
||||||
|
IColor? DefaultForegroundColor,
|
||||||
|
IColor? DefaultBackgroundColor,
|
||||||
|
IColor? AlternativeItemForegroundColor,
|
||||||
|
IColor? SelectedItemForegroundColor,
|
||||||
|
IColor? MarkedItemForegroundColor,
|
||||||
|
IColor? MarkedAlternativeItemForegroundColor,
|
||||||
|
IColor? MarkedSelectedItemForegroundColor) : ITheme;
|
||||||
|
|
||||||
|
public static class DefaultThemes
|
||||||
|
{
|
||||||
|
public static Theme Color256Theme => new(
|
||||||
|
ItemBackgroundColor: Color256Colors.Backgrounds.Black,
|
||||||
|
AlternativeItemBackgroundColor: Color256Colors.Backgrounds.Black,
|
||||||
|
SelectedItemBackgroundColor: Color256Colors.Backgrounds.Black,
|
||||||
|
MarkedItemBackgroundColor: Color256Colors.Backgrounds.Black,
|
||||||
|
MarkedAlternativeItemBackgroundColor: Color256Colors.Backgrounds.Black,
|
||||||
|
MarkedSelectedItemBackgroundColor: Color256Colors.Backgrounds.Black,
|
||||||
|
DefaultForegroundColor: null,
|
||||||
|
DefaultBackgroundColor: null,
|
||||||
|
AlternativeItemForegroundColor: null,
|
||||||
|
SelectedItemForegroundColor: Color256Colors.Foregrounds.Black,
|
||||||
|
MarkedItemForegroundColor: Color256Colors.Foregrounds.White,
|
||||||
|
MarkedAlternativeItemForegroundColor: Color256Colors.Foregrounds.White,
|
||||||
|
MarkedSelectedItemForegroundColor: Color256Colors.Foregrounds.Cyan);
|
||||||
|
|
||||||
|
public static Theme ConsoleColorTheme => new(
|
||||||
|
ItemBackgroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
AlternativeItemBackgroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
SelectedItemBackgroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
MarkedItemBackgroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
MarkedAlternativeItemBackgroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
DefaultForegroundColor: null,
|
||||||
|
DefaultBackgroundColor: null,
|
||||||
|
AlternativeItemForegroundColor: null,
|
||||||
|
SelectedItemForegroundColor: ConsoleColors.Foregrounds.Black,
|
||||||
|
MarkedItemForegroundColor: ConsoleColors.Foregrounds.White,
|
||||||
|
MarkedAlternativeItemForegroundColor: ConsoleColors.Foregrounds.White,
|
||||||
|
MarkedSelectedItemForegroundColor: ConsoleColors.Foregrounds.Cyan);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -18,9 +18,9 @@ public static class DI
|
|||||||
{
|
{
|
||||||
public static IServiceProvider ServiceProvider { get; private set; } = null!;
|
public static IServiceProvider ServiceProvider { get; private set; } = null!;
|
||||||
|
|
||||||
public static void Initialize(IConfigurationRoot configuration)
|
public static void Initialize(IConfigurationRoot configuration, IServiceCollection serviceCollection)
|
||||||
=> ServiceProvider = DependencyInjection
|
=> ServiceProvider = DependencyInjection
|
||||||
.RegisterDefaultServices(configuration: configuration)
|
.RegisterDefaultServices(configuration: configuration, serviceCollection: serviceCollection)
|
||||||
.AddConsoleServices()
|
.AddConsoleServices()
|
||||||
.AddLocalProviderServices()
|
.AddLocalProviderServices()
|
||||||
.AddServerCoreServices()
|
.AddServerCoreServices()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.FrequencyNavigation\FileTime.App.FrequencyNavigation.csproj" />
|
<ProjectReference Include="..\..\AppCommon\FileTime.App.FrequencyNavigation\FileTime.App.FrequencyNavigation.csproj" />
|
||||||
<ProjectReference Include="..\..\Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj" />
|
<ProjectReference Include="..\..\Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj" />
|
||||||
<ProjectReference Include="..\FileTime.ConsoleUI.App\FileTime.ConsoleUI.App.csproj" />
|
<ProjectReference Include="..\FileTime.ConsoleUI.App\FileTime.ConsoleUI.App.csproj" />
|
||||||
|
<ProjectReference Include="..\FileTime.ConsoleUI.Styles\FileTime.ConsoleUI.Styles.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Terminal.Gui" Version="1.13.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,20 +2,52 @@
|
|||||||
using FileTime.App.Core.Configuration;
|
using FileTime.App.Core.Configuration;
|
||||||
using FileTime.ConsoleUI;
|
using FileTime.ConsoleUI;
|
||||||
using FileTime.ConsoleUI.App;
|
using FileTime.ConsoleUI.App;
|
||||||
|
using FileTime.ConsoleUI.Styles;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
(AppDataRoot, EnvironmentName) = Init.InitDevelopment();
|
IConsoleDriver driver = new WindowsDriver();
|
||||||
var configuration = new ConfigurationBuilder()
|
driver.Init();
|
||||||
.AddInMemoryCollection(MainConfiguration.Configuration)
|
ITheme theme;
|
||||||
|
if (driver.GetCursorPosition() is not {PosX: 0, PosY: 0})
|
||||||
|
{
|
||||||
|
driver = new DotnetDriver();
|
||||||
|
driver.Init();
|
||||||
|
theme = DefaultThemes.ConsoleColorTheme;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
theme = DefaultThemes.Color256Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.SetCursorVisible(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(AppDataRoot, EnvironmentName) = Init.InitDevelopment();
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(MainConfiguration.Configuration)
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||||
#endif
|
#endif
|
||||||
.Build();
|
.Build();
|
||||||
DI.Initialize(configuration);
|
|
||||||
|
|
||||||
var app = DI.ServiceProvider.GetRequiredService<IApplication>();
|
var serviceCollection = new ServiceCollection();
|
||||||
app.Run();
|
serviceCollection.TryAddSingleton<IConsoleDriver>(driver);
|
||||||
|
serviceCollection.TryAddSingleton<ITheme>(theme);
|
||||||
|
|
||||||
|
DI.Initialize(configuration, serviceCollection);
|
||||||
|
|
||||||
|
var app = DI.ServiceProvider.GetRequiredService<IApplication>();
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
driver.SetCursorVisible(true);
|
||||||
|
driver.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public partial class Program
|
public partial class Program
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\Termi
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\CircularBuffer\CircularBuffer.csproj", "{AF4FE804-12D9-46E2-A584-BFF6D4509766}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\CircularBuffer\CircularBuffer.csproj", "{AF4FE804-12D9-46E2-A584-BFF6D4509766}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles", "ConsoleApp\FileTime.ConsoleUI.Styles\FileTime.ConsoleUI.Styles.csproj", "{CCB6F86A-7E80-448E-B543-DF9DB337C42A}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -327,6 +329,10 @@ Global
|
|||||||
{AF4FE804-12D9-46E2-A584-BFF6D4509766}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{AF4FE804-12D9-46E2-A584-BFF6D4509766}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{AF4FE804-12D9-46E2-A584-BFF6D4509766}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{AF4FE804-12D9-46E2-A584-BFF6D4509766}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{AF4FE804-12D9-46E2-A584-BFF6D4509766}.Release|Any CPU.Build.0 = Release|Any CPU
|
{AF4FE804-12D9-46E2-A584-BFF6D4509766}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -383,6 +389,7 @@ Global
|
|||||||
{81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
|
{81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
|
||||||
{2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
{2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
||||||
{AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
{AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
||||||
|
{CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
|||||||
private readonly List<Action<IDeclarativeProperty<T>, T>> _unsubscribeTriggers = new();
|
private readonly List<Action<IDeclarativeProperty<T>, T>> _unsubscribeTriggers = new();
|
||||||
private readonly List<IDisposable> _triggerDisposables = new();
|
private readonly List<IDisposable> _triggerDisposables = new();
|
||||||
private readonly object _triggerLock = new();
|
private readonly object _triggerLock = new();
|
||||||
|
private readonly object _subscriberLock = new();
|
||||||
|
|
||||||
private T? _value;
|
private T? _value;
|
||||||
|
|
||||||
@@ -35,7 +36,12 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
|||||||
|
|
||||||
protected async Task NotifySubscribersAsync(T? newValue, CancellationToken cancellationToken = default)
|
protected async Task NotifySubscribersAsync(T? newValue, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var subscribers = _subscribers.ToList();
|
List<Func<T?, CancellationToken, Task>> subscribers;
|
||||||
|
lock (_subscriberLock)
|
||||||
|
{
|
||||||
|
subscribers = _subscribers.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var handler in subscribers)
|
foreach (var handler in subscribers)
|
||||||
{
|
{
|
||||||
await handler(newValue, cancellationToken);
|
await handler(newValue, cancellationToken);
|
||||||
@@ -50,13 +56,23 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
|||||||
|
|
||||||
public IDisposable Subscribe(Func<T?, CancellationToken, Task> onChange)
|
public IDisposable Subscribe(Func<T?, CancellationToken, Task> onChange)
|
||||||
{
|
{
|
||||||
_subscribers.Add(onChange);
|
lock (_subscriberLock)
|
||||||
|
{
|
||||||
|
_subscribers.Add(onChange);
|
||||||
|
}
|
||||||
|
|
||||||
onChange(_value, default);
|
onChange(_value, default);
|
||||||
|
|
||||||
return new Unsubscriber<T>(this, onChange);
|
return new Unsubscriber<T>(this, onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Unsubscribe(Func<T, CancellationToken, Task> onChange) => _subscribers.Remove(onChange);
|
public void Unsubscribe(Func<T, CancellationToken, Task> onChange)
|
||||||
|
{
|
||||||
|
lock (_subscriberLock)
|
||||||
|
{
|
||||||
|
_subscribers.Remove(onChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IDeclarativeProperty<T> RegisterTrigger(
|
public IDeclarativeProperty<T> RegisterTrigger(
|
||||||
Func<IDeclarativeProperty<T>, T, IDisposable?> triggerSubscribe,
|
Func<IDeclarativeProperty<T>, T, IDisposable?> triggerSubscribe,
|
||||||
@@ -139,7 +155,10 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
|||||||
disposable.Dispose();
|
disposable.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscribers.Clear();
|
lock (_subscriberLock)
|
||||||
|
{
|
||||||
|
_subscribers.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
protected void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
namespace TerminalUI;
|
using TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
|
namespace TerminalUI;
|
||||||
|
|
||||||
public class ApplicationContext : IApplicationContext
|
public class ApplicationContext : IApplicationContext
|
||||||
{
|
{
|
||||||
|
public required IConsoleDriver ConsoleDriver { get; init; }
|
||||||
public IEventLoop EventLoop { get; init; }
|
public IEventLoop EventLoop { get; init; }
|
||||||
public bool IsRunning { get; set; }
|
public bool IsRunning { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,15 @@ public class Binding<TDataContext, TResult> : IDisposable
|
|||||||
private IView<TDataContext> _dataSourceView;
|
private IView<TDataContext> _dataSourceView;
|
||||||
private object? _propertySource;
|
private object? _propertySource;
|
||||||
private PropertyInfo _targetProperty;
|
private PropertyInfo _targetProperty;
|
||||||
private readonly List<string> _rerenderProperties;
|
private IDisposableCollection? _propertySourceDisposableCollection;
|
||||||
private readonly IDisposableCollection? _propertySourceDisposableCollection;
|
private PropertyTrackTreeItem? _propertyTrackTreeItem;
|
||||||
private INotifyPropertyChanged? _dataSourceLastDataContext;
|
private IPropertyChangeTracker? _propertyChangeTracker;
|
||||||
|
|
||||||
public Binding(
|
public Binding(
|
||||||
IView<TDataContext> dataSourceView,
|
IView<TDataContext> dataSourceView,
|
||||||
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
||||||
object? propertySource,
|
object? propertySource,
|
||||||
PropertyInfo targetProperty,
|
PropertyInfo targetProperty
|
||||||
IEnumerable<string>? rerenderProperties = null
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(dataSourceView);
|
ArgumentNullException.ThrowIfNull(dataSourceView);
|
||||||
@@ -31,70 +30,148 @@ public class Binding<TDataContext, TResult> : IDisposable
|
|||||||
_dataContextMapper = dataContextExpression.Compile();
|
_dataContextMapper = dataContextExpression.Compile();
|
||||||
_propertySource = propertySource;
|
_propertySource = propertySource;
|
||||||
_targetProperty = targetProperty;
|
_targetProperty = targetProperty;
|
||||||
_rerenderProperties = rerenderProperties?.ToList() ?? new List<string>();
|
|
||||||
|
|
||||||
FindReactiveProperties(dataContextExpression);
|
InitTrackingTree(dataContextExpression);
|
||||||
|
|
||||||
|
UpdateTrackers();
|
||||||
|
|
||||||
dataSourceView.PropertyChanged += View_PropertyChanged;
|
dataSourceView.PropertyChanged += View_PropertyChanged;
|
||||||
var initialValue = _dataContextMapper(_dataSourceView.DataContext);
|
UpdateTargetProperty();
|
||||||
_targetProperty.SetValue(_propertySource, initialValue);
|
|
||||||
|
|
||||||
|
AddToSourceDisposables(propertySource);
|
||||||
|
|
||||||
|
dataSourceView.AddDisposable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToSourceDisposables(object? propertySource)
|
||||||
|
{
|
||||||
if (propertySource is IDisposableCollection propertySourceDisposableCollection)
|
if (propertySource is IDisposableCollection propertySourceDisposableCollection)
|
||||||
{
|
{
|
||||||
propertySourceDisposableCollection.AddDisposable(this);
|
propertySourceDisposableCollection.AddDisposable(this);
|
||||||
_propertySourceDisposableCollection = propertySourceDisposableCollection;
|
_propertySourceDisposableCollection = propertySourceDisposableCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dataSourceView.DataContext is INotifyPropertyChanged dataSourcePropertyChanged)
|
|
||||||
{
|
|
||||||
_dataSourceLastDataContext = dataSourcePropertyChanged;
|
|
||||||
dataSourcePropertyChanged.PropertyChanged += DataContext_PropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSourceView.AddDisposable(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FindReactiveProperties(Expression expression)
|
private void InitTrackingTree(Expression<Func<TDataContext?, TResult>> dataContextExpression)
|
||||||
|
{
|
||||||
|
var properties = new List<string>();
|
||||||
|
FindReactiveProperties(dataContextExpression, properties);
|
||||||
|
|
||||||
|
if (properties.Count > 0)
|
||||||
|
{
|
||||||
|
var rootItem = new PropertyTrackTreeItem();
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var pathParts = property.Split('.');
|
||||||
|
var currentItem = rootItem;
|
||||||
|
for (var i = 0; i < pathParts.Length; i++)
|
||||||
|
{
|
||||||
|
if (!currentItem.Children.TryGetValue(pathParts[i], out var child))
|
||||||
|
{
|
||||||
|
child = new PropertyTrackTreeItem();
|
||||||
|
currentItem.Children.Add(pathParts[i], child);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentItem = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_propertyTrackTreeItem = rootItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? FindReactiveProperties(Expression expression, List<string> properties)
|
||||||
{
|
{
|
||||||
if (expression is LambdaExpression lambdaExpression)
|
if (expression is LambdaExpression lambdaExpression)
|
||||||
{
|
{
|
||||||
FindReactiveProperties(lambdaExpression.Body);
|
SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties));
|
||||||
}
|
}
|
||||||
else if (expression is ConditionalExpression conditionalExpression)
|
else if (expression is ConditionalExpression conditionalExpression)
|
||||||
{
|
{
|
||||||
FindReactiveProperties(conditionalExpression.IfFalse);
|
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties));
|
||||||
FindReactiveProperties(conditionalExpression.IfTrue);
|
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties));
|
||||||
}
|
}
|
||||||
else if (expression is MemberExpression {Member: PropertyInfo dataContextPropertyInfo})
|
else if (expression is MemberExpression memberExpression)
|
||||||
{
|
{
|
||||||
_rerenderProperties.Add(dataContextPropertyInfo.Name);
|
if (memberExpression.Expression is not null)
|
||||||
|
{
|
||||||
|
FindReactiveProperties(memberExpression.Expression, properties);
|
||||||
|
|
||||||
|
if (FindReactiveProperties(memberExpression.Expression, properties) is { } path
|
||||||
|
&& memberExpression.Member is PropertyInfo dataContextPropertyInfo)
|
||||||
|
{
|
||||||
|
path += "." + memberExpression.Member.Name;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (expression is MethodCallExpression methodCallExpression)
|
||||||
|
{
|
||||||
|
if (methodCallExpression.Object is
|
||||||
|
{
|
||||||
|
NodeType:
|
||||||
|
not ExpressionType.Parameter
|
||||||
|
and not ExpressionType.Constant
|
||||||
|
} methodObject)
|
||||||
|
{
|
||||||
|
SavePropertyPath(FindReactiveProperties(methodObject, properties));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var argument in methodCallExpression.Arguments)
|
||||||
|
{
|
||||||
|
SavePropertyPath(FindReactiveProperties(argument, properties));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (expression is BinaryExpression binaryExpression)
|
||||||
|
{
|
||||||
|
SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties));
|
||||||
|
SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties));
|
||||||
|
}
|
||||||
|
else if (expression is UnaryExpression unaryExpression)
|
||||||
|
{
|
||||||
|
SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties));
|
||||||
|
}
|
||||||
|
else if (expression is ParameterExpression parameterExpression)
|
||||||
|
{
|
||||||
|
if (parameterExpression.Type == typeof(TDataContext))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
void SavePropertyPath(string? path)
|
||||||
|
{
|
||||||
|
if (path is null) return;
|
||||||
|
path = path.TrimStart('.');
|
||||||
|
properties.Add(path);
|
||||||
}
|
}
|
||||||
//TODO: Handle other expression types
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
|
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
|
||||||
|
|
||||||
if (_dataSourceLastDataContext is not null)
|
UpdateTrackers();
|
||||||
{
|
|
||||||
_dataSourceLastDataContext.PropertyChanged -= DataContext_PropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dataSourceView.DataContext is INotifyPropertyChanged dataSourcePropertyChanged)
|
|
||||||
{
|
|
||||||
_dataSourceLastDataContext = dataSourcePropertyChanged;
|
|
||||||
dataSourcePropertyChanged.PropertyChanged += DataContext_PropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTargetProperty();
|
UpdateTargetProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DataContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void UpdateTrackers()
|
||||||
{
|
{
|
||||||
if (e.PropertyName == null
|
if (_propertyChangeTracker is not null)
|
||||||
|| !_rerenderProperties.Contains(e.PropertyName)) return;
|
{
|
||||||
UpdateTargetProperty();
|
_propertyChangeTracker.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_propertyTrackTreeItem is not null)
|
||||||
|
{
|
||||||
|
_propertyChangeTracker = PropertyChangeHelper.TraverseDataContext(
|
||||||
|
_propertyTrackTreeItem,
|
||||||
|
_dataSourceView.DataContext,
|
||||||
|
UpdateTargetProperty
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTargetProperty()
|
private void UpdateTargetProperty()
|
||||||
|
|||||||
41
src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs
Normal file
41
src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using TerminalUI.Models;
|
||||||
|
using ConsoleColor = TerminalUI.Models.ConsoleColor;
|
||||||
|
|
||||||
|
namespace TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
|
public class DotnetDriver : IConsoleDriver
|
||||||
|
{
|
||||||
|
public virtual void Init() => Console.Clear();
|
||||||
|
|
||||||
|
public void SetCursorPosition(Position position) => Console.SetCursorPosition(position.PosX, position.PosY);
|
||||||
|
|
||||||
|
public void ResetColor() => Console.ResetColor();
|
||||||
|
|
||||||
|
public Position GetCursorPosition()
|
||||||
|
{
|
||||||
|
var (x, y) = Console.GetCursorPosition();
|
||||||
|
return new(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(string text) => Console.Write(text);
|
||||||
|
|
||||||
|
public void Write(char text) => Console.Write(text);
|
||||||
|
|
||||||
|
public virtual void Dispose() {}
|
||||||
|
|
||||||
|
public bool CanRead() => Console.KeyAvailable;
|
||||||
|
public ConsoleKeyInfo ReadKey() => Console.ReadKey(true);
|
||||||
|
|
||||||
|
public void SetCursorVisible(bool cursorVisible) => Console.CursorVisible = cursorVisible;
|
||||||
|
public virtual void SetForegroundColor(IColor foreground)
|
||||||
|
{
|
||||||
|
if (foreground is not ConsoleColor consoleColor) throw new NotSupportedException();
|
||||||
|
Console.ForegroundColor = consoleColor.Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void SetBackgroundColor(IColor background)
|
||||||
|
{
|
||||||
|
if (background is not ConsoleColor consoleColor) throw new NotSupportedException();
|
||||||
|
Console.ForegroundColor = consoleColor.Color;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs
Normal file
19
src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
|
namespace TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
|
public interface IConsoleDriver
|
||||||
|
{
|
||||||
|
void Init();
|
||||||
|
void Dispose();
|
||||||
|
void SetCursorPosition(Position position);
|
||||||
|
void ResetColor();
|
||||||
|
Position GetCursorPosition();
|
||||||
|
void Write(string text);
|
||||||
|
void Write(char text);
|
||||||
|
bool CanRead();
|
||||||
|
ConsoleKeyInfo ReadKey();
|
||||||
|
void SetCursorVisible(bool cursorVisible);
|
||||||
|
void SetForegroundColor(IColor foreground);
|
||||||
|
void SetBackgroundColor(IColor background);
|
||||||
|
}
|
||||||
16
src/Library/TerminalUI/ConsoleDrivers/WindowsDriver.cs
Normal file
16
src/Library/TerminalUI/ConsoleDrivers/WindowsDriver.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using TerminalUI.Traits;
|
using TerminalUI.Models;
|
||||||
|
using TerminalUI.Traits;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ public abstract class ContentView<T>: View<T>, IContentRenderer
|
|||||||
ContentRendererMethod = DefaultContentRender;
|
ContentRendererMethod = DefaultContentRender;
|
||||||
}
|
}
|
||||||
public IView? Content { get; set; }
|
public IView? Content { get; set; }
|
||||||
public Action ContentRendererMethod { get; set; }
|
public Action<Position> ContentRendererMethod { get; set; }
|
||||||
|
|
||||||
private void DefaultContentRender() => Content?.Render();
|
private void DefaultContentRender(Position position) => Content?.Render(position);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using TerminalUI.Models;
|
||||||
using TerminalUI.Traits;
|
using TerminalUI.Traits;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
@@ -6,12 +7,10 @@ namespace TerminalUI.Controls;
|
|||||||
public interface IView : INotifyPropertyChanged, IDisposableCollection
|
public interface IView : INotifyPropertyChanged, IDisposableCollection
|
||||||
{
|
{
|
||||||
object? DataContext { get; set; }
|
object? DataContext { get; set; }
|
||||||
Action RenderMethod { get; set; }
|
Action<Position> RenderMethod { get; set; }
|
||||||
IApplicationContext ApplicationContext { get; init;}
|
IApplicationContext? ApplicationContext { get; init;}
|
||||||
event Action<IView> Disposed;
|
event Action<IView> Disposed;
|
||||||
event Action<IView> RenderRequested;
|
void Render(Position position);
|
||||||
void Render();
|
|
||||||
void RequestRerender();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IView<T> : IView
|
public interface IView<T> : IView
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using DeclarativeProperty;
|
using DeclarativeProperty;
|
||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
|||||||
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared;
|
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared;
|
||||||
|
|
||||||
private readonly List<IDisposable> _itemsDisposables = new();
|
private readonly List<IDisposable> _itemsDisposables = new();
|
||||||
private Func<IEnumerable<TItem>>? _getItems;
|
private Func<IEnumerable<TItem>?>? _getItems;
|
||||||
private object? _itemsSource;
|
private object? _itemsSource;
|
||||||
private ListViewItem<TItem>[]? _listViewItems;
|
private ListViewItem<TItem>[]? _listViewItems;
|
||||||
private int _listViewItemLength;
|
private int _listViewItemLength;
|
||||||
@@ -30,11 +31,20 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
|||||||
_itemsDisposables.Clear();
|
_itemsDisposables.Clear();
|
||||||
|
|
||||||
if (_itemsSource is IDeclarativeProperty<ObservableCollection<TItem>> observableDeclarativeProperty)
|
if (_itemsSource is IDeclarativeProperty<ObservableCollection<TItem>> observableDeclarativeProperty)
|
||||||
|
{
|
||||||
|
observableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||||
_getItems = () => observableDeclarativeProperty.Value;
|
_getItems = () => observableDeclarativeProperty.Value;
|
||||||
|
}
|
||||||
else if (_itemsSource is IDeclarativeProperty<ReadOnlyObservableCollection<TItem>> readOnlyObservableDeclarativeProperty)
|
else if (_itemsSource is IDeclarativeProperty<ReadOnlyObservableCollection<TItem>> readOnlyObservableDeclarativeProperty)
|
||||||
|
{
|
||||||
|
readOnlyObservableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||||
_getItems = () => readOnlyObservableDeclarativeProperty.Value;
|
_getItems = () => readOnlyObservableDeclarativeProperty.Value;
|
||||||
|
}
|
||||||
else if (_itemsSource is IDeclarativeProperty<IEnumerable<TItem>> enumerableDeclarativeProperty)
|
else if (_itemsSource is IDeclarativeProperty<IEnumerable<TItem>> enumerableDeclarativeProperty)
|
||||||
|
{
|
||||||
|
enumerableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||||
_getItems = () => enumerableDeclarativeProperty.Value;
|
_getItems = () => enumerableDeclarativeProperty.Value;
|
||||||
|
}
|
||||||
else if (_itemsSource is ICollection<TItem> collection)
|
else if (_itemsSource is ICollection<TItem> collection)
|
||||||
_getItems = () => collection;
|
_getItems = () => collection;
|
||||||
else if (_itemsSource is TItem[] array)
|
else if (_itemsSource is TItem[] array)
|
||||||
@@ -54,18 +64,20 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
|||||||
|
|
||||||
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
|
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
|
||||||
|
|
||||||
protected override void DefaultRenderer()
|
protected override void DefaultRenderer(Position position)
|
||||||
{
|
{
|
||||||
var listViewItems = InstantiateItemViews();
|
var listViewItems = InstantiateItemViews();
|
||||||
|
var deltaY = 0;
|
||||||
foreach (var item in listViewItems)
|
foreach (var item in listViewItems)
|
||||||
{
|
{
|
||||||
item.Render();
|
item.Render(position with {PosY = position.PosY + deltaY++});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Span<ListViewItem<TItem>> InstantiateItemViews()
|
private Span<ListViewItem<TItem>> InstantiateItemViews()
|
||||||
{
|
{
|
||||||
if (_getItems is null)
|
var items = _getItems?.Invoke()?.ToList();
|
||||||
|
if (items is null)
|
||||||
{
|
{
|
||||||
if (_listViewItemLength != 0)
|
if (_listViewItemLength != 0)
|
||||||
{
|
{
|
||||||
@@ -74,11 +86,10 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
|||||||
|
|
||||||
return _listViewItems;
|
return _listViewItems;
|
||||||
}
|
}
|
||||||
var items = _getItems().ToList();
|
|
||||||
|
|
||||||
Span<ListViewItem<TItem>> listViewItems;
|
Span<ListViewItem<TItem>> listViewItems;
|
||||||
|
|
||||||
if (_listViewItems is null || _listViewItems.Length != items.Count)
|
if (_listViewItems is null || _listViewItemLength != items.Count)
|
||||||
{
|
{
|
||||||
var newListViewItems = ListViewItemPool.Rent(items.Count);
|
var newListViewItems = ListViewItemPool.Rent(items.Count);
|
||||||
for (var i = 0; i < items.Count; i++)
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using TerminalUI.Traits;
|
using TerminalUI.Models;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
|
|
||||||
public class ListViewItem<T> : ContentView<T>
|
public class ListViewItem<T> : ContentView<T>
|
||||||
{
|
{
|
||||||
protected override void DefaultRenderer()
|
protected override void DefaultRenderer(Position position)
|
||||||
{
|
{
|
||||||
if (ContentRendererMethod is null)
|
if (ContentRendererMethod is null)
|
||||||
{
|
{
|
||||||
@@ -16,6 +16,6 @@ public class ListViewItem<T> : ContentView<T>
|
|||||||
+ DataContext?.GetType().Name);
|
+ DataContext?.GetType().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentRendererMethod();
|
ContentRendererMethod(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using TerminalUI.Extensions;
|
using TerminalUI.Extensions;
|
||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
|
|
||||||
public partial class TextBlock<T> : View<T>
|
public partial class TextBlock<T> : View<T>
|
||||||
{
|
{
|
||||||
|
private record RenderContext(Position Position, string? Text, IColor? Foreground, IColor? Background);
|
||||||
|
|
||||||
|
private RenderContext? _renderContext;
|
||||||
|
|
||||||
[Notify] private string? _text = string.Empty;
|
[Notify] private string? _text = string.Empty;
|
||||||
|
[Notify] private IColor? _foreground;
|
||||||
|
[Notify] private IColor? _background;
|
||||||
|
|
||||||
public TextBlock()
|
public TextBlock()
|
||||||
{
|
{
|
||||||
@@ -16,8 +23,35 @@ public partial class TextBlock<T> : View<T>
|
|||||||
);
|
);
|
||||||
|
|
||||||
RerenderProperties.Add(nameof(Text));
|
RerenderProperties.Add(nameof(Text));
|
||||||
|
RerenderProperties.Add(nameof(Foreground));
|
||||||
|
RerenderProperties.Add(nameof(Background));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DefaultRenderer()
|
protected override void DefaultRenderer(Position position)
|
||||||
=> Console.Write(Text);
|
{
|
||||||
|
var driver = ApplicationContext!.ConsoleDriver;
|
||||||
|
var renderContext = new RenderContext(position, Text, _foreground, _background);
|
||||||
|
if (!NeedsRerender(renderContext)) return;
|
||||||
|
|
||||||
|
_renderContext = renderContext;
|
||||||
|
|
||||||
|
if (Text is null) return;
|
||||||
|
|
||||||
|
driver.SetCursorPosition(position);
|
||||||
|
driver.ResetColor();
|
||||||
|
if (Foreground is { } foreground)
|
||||||
|
{
|
||||||
|
driver.SetForegroundColor(foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Background is { } background)
|
||||||
|
{
|
||||||
|
driver.SetBackgroundColor(background);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.Write(Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool NeedsRerender(RenderContext renderContext)
|
||||||
|
=> _renderContext is null || _renderContext != renderContext;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
|
|
||||||
@@ -9,10 +10,9 @@ public abstract partial class View<T> : IView<T>
|
|||||||
{
|
{
|
||||||
private readonly List<IDisposable> _disposables = new();
|
private readonly List<IDisposable> _disposables = new();
|
||||||
[Notify] private T? _dataContext;
|
[Notify] private T? _dataContext;
|
||||||
public Action RenderMethod { get; set; }
|
public Action<Position> RenderMethod { get; set; }
|
||||||
public IApplicationContext ApplicationContext { get; init; }
|
public IApplicationContext? ApplicationContext { get; init; }
|
||||||
public event Action<IView>? Disposed;
|
public event Action<IView>? Disposed;
|
||||||
public event Action<IView>? RenderRequested;
|
|
||||||
protected List<string> RerenderProperties { get; } = new();
|
protected List<string> RerenderProperties { get; } = new();
|
||||||
|
|
||||||
protected View()
|
protected View()
|
||||||
@@ -29,13 +29,13 @@ public abstract partial class View<T> : IView<T>
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
RenderRequested?.Invoke(this);
|
ApplicationContext?.EventLoop.RequestRerender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void DefaultRenderer();
|
protected abstract void DefaultRenderer(Position position);
|
||||||
|
|
||||||
public void Render()
|
public void Render(Position position)
|
||||||
{
|
{
|
||||||
if (RenderMethod is null)
|
if (RenderMethod is null)
|
||||||
{
|
{
|
||||||
@@ -47,11 +47,9 @@ public abstract partial class View<T> : IView<T>
|
|||||||
+ DataContext?.GetType().Name);
|
+ DataContext?.GetType().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderMethod();
|
RenderMethod(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RequestRerender() => RenderRequested?.Invoke(this);
|
|
||||||
|
|
||||||
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
|
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
|
||||||
{
|
{
|
||||||
var child = new TChild
|
var child = new TChild
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Buffers;
|
using TerminalUI.Controls;
|
||||||
using System.Collections.Concurrent;
|
using TerminalUI.Models;
|
||||||
using TerminalUI.Controls;
|
|
||||||
|
|
||||||
namespace TerminalUI;
|
namespace TerminalUI;
|
||||||
|
|
||||||
@@ -8,10 +7,8 @@ public class EventLoop : IEventLoop
|
|||||||
{
|
{
|
||||||
private readonly IApplicationContext _applicationContext;
|
private readonly IApplicationContext _applicationContext;
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private readonly ArrayPool<IView> _viewPool = ArrayPool<IView>.Shared;
|
private readonly List<IView> _viewsToRender = new();
|
||||||
|
private bool _rerenderRequested;
|
||||||
private readonly ConcurrentBag<IView> _viewsToRenderInstantly = new();
|
|
||||||
private readonly LinkedList<IView> _viewsToRender = new();
|
|
||||||
|
|
||||||
public EventLoop(IApplicationContext applicationContext)
|
public EventLoop(IApplicationContext applicationContext)
|
||||||
{
|
{
|
||||||
@@ -21,6 +18,7 @@ public class EventLoop : IEventLoop
|
|||||||
public void Run()
|
public void Run()
|
||||||
{
|
{
|
||||||
_applicationContext.IsRunning = true;
|
_applicationContext.IsRunning = true;
|
||||||
|
_rerenderRequested = true;
|
||||||
while (_applicationContext.IsRunning)
|
while (_applicationContext.IsRunning)
|
||||||
{
|
{
|
||||||
Render();
|
Render();
|
||||||
@@ -28,103 +26,36 @@ public class EventLoop : IEventLoop
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RequestRerender()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_rerenderRequested = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Render()
|
public void Render()
|
||||||
{
|
{
|
||||||
IView[]? viewsToRenderCopy = null;
|
List<IView> viewsToRender;
|
||||||
IView[]? viewsAlreadyRendered = null;
|
lock (_lock)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
int viewsToRenderCopyCount;
|
if (!_rerenderRequested) return;
|
||||||
IView[]? viewsToRenderInstantly;
|
_rerenderRequested = false;
|
||||||
|
|
||||||
lock (_lock)
|
viewsToRender = _viewsToRender.ToList();
|
||||||
{
|
|
||||||
CleanViewsToRender();
|
|
||||||
|
|
||||||
viewsToRenderCopyCount = _viewsToRender.Count;
|
|
||||||
viewsToRenderCopy = _viewPool.Rent(_viewsToRender.Count);
|
|
||||||
_viewsToRender.CopyTo(viewsToRenderCopy, 0);
|
|
||||||
|
|
||||||
viewsToRenderInstantly = _viewsToRenderInstantly.ToArray();
|
|
||||||
_viewsToRenderInstantly.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
viewsAlreadyRendered = _viewPool.Rent(viewsToRenderCopy.Length + viewsToRenderInstantly.Length);
|
|
||||||
var viewsAlreadyRenderedIndex = 0;
|
|
||||||
|
|
||||||
foreach (var view in viewsToRenderInstantly)
|
|
||||||
{
|
|
||||||
if (Contains(viewsAlreadyRendered, view, viewsAlreadyRenderedIndex)) continue;
|
|
||||||
|
|
||||||
view.Render();
|
|
||||||
viewsAlreadyRendered[viewsAlreadyRenderedIndex++] = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < viewsToRenderCopyCount; i++)
|
|
||||||
{
|
|
||||||
var view = viewsToRenderCopy[i];
|
|
||||||
if (Contains(viewsAlreadyRendered, view, viewsAlreadyRenderedIndex)) continue;
|
|
||||||
|
|
||||||
view.Render();
|
|
||||||
viewsAlreadyRendered[viewsAlreadyRenderedIndex++] = view;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (viewsToRenderCopy is not null)
|
|
||||||
_viewPool.Return(viewsToRenderCopy);
|
|
||||||
|
|
||||||
if (viewsAlreadyRendered is not null)
|
|
||||||
_viewPool.Return(viewsAlreadyRendered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanViewsToRender()
|
|
||||||
{
|
|
||||||
IView[]? viewsAlreadyProcessed = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
viewsAlreadyProcessed = _viewPool.Rent(_viewsToRender.Count);
|
|
||||||
var viewsAlreadyProcessedIndex = 0;
|
|
||||||
|
|
||||||
var currentItem = _viewsToRender.First;
|
|
||||||
for (var i = 0; i < _viewsToRender.Count && currentItem is not null; i++)
|
|
||||||
{
|
|
||||||
if (Contains(viewsAlreadyProcessed, currentItem.Value, viewsAlreadyProcessedIndex))
|
|
||||||
{
|
|
||||||
var itemToRemove = currentItem;
|
|
||||||
currentItem = currentItem.Next;
|
|
||||||
_viewsToRender.Remove(itemToRemove);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewsAlreadyProcessed[viewsAlreadyProcessedIndex++] = currentItem.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (viewsAlreadyProcessed is not null)
|
|
||||||
{
|
|
||||||
_viewPool.Return(viewsAlreadyProcessed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool Contains(IView[] views, IView view, int max)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < max; i++)
|
|
||||||
{
|
|
||||||
if (views[i] == view) return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
foreach (var view in viewsToRender)
|
||||||
|
{
|
||||||
|
view.Render(new Position(0, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddViewToRender(IView view)
|
public void AddViewToRender(IView view)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_viewsToRender.AddLast(view);
|
_viewsToRender.Add(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,7 @@ public static class Binding
|
|||||||
this TView targetView,
|
this TView targetView,
|
||||||
IView<TDataContext> dataSourceView,
|
IView<TDataContext> dataSourceView,
|
||||||
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
||||||
Expression<Func<TView, TResult>> propertyExpression,
|
Expression<Func<TView, TResult>> propertyExpression)
|
||||||
IEnumerable<string>? rerenderProperties = null)
|
|
||||||
{
|
{
|
||||||
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
|
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
|
||||||
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
|
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
|
||||||
@@ -20,8 +19,7 @@ public static class Binding
|
|||||||
dataSourceView,
|
dataSourceView,
|
||||||
dataContextExpression,
|
dataContextExpression,
|
||||||
targetView,
|
targetView,
|
||||||
propertyInfo,
|
propertyInfo
|
||||||
rerenderProperties
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
namespace TerminalUI;
|
using TerminalUI.ConsoleDrivers;
|
||||||
|
|
||||||
|
namespace TerminalUI;
|
||||||
|
|
||||||
public interface IApplicationContext
|
public interface IApplicationContext
|
||||||
{
|
{
|
||||||
IEventLoop EventLoop { get; init; }
|
IEventLoop EventLoop { get; init; }
|
||||||
bool IsRunning { get; set; }
|
bool IsRunning { get; set; }
|
||||||
|
IConsoleDriver ConsoleDriver { get; init; }
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,5 @@ public interface IEventLoop
|
|||||||
void Render();
|
void Render();
|
||||||
void AddViewToRender(IView view);
|
void AddViewToRender(IView view);
|
||||||
void Run();
|
void Run();
|
||||||
|
void RequestRerender();
|
||||||
}
|
}
|
||||||
14
src/Library/TerminalUI/Models/Color256.cs
Normal file
14
src/Library/TerminalUI/Models/Color256.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public record struct Color256(byte Color, ColorType Type) : IColor
|
||||||
|
{
|
||||||
|
public string ToConsoleColor()
|
||||||
|
=> Type switch
|
||||||
|
{
|
||||||
|
ColorType.Foreground => $"\x1b[38;5;{Color}m",
|
||||||
|
ColorType.Background => $"\x1b[48;5;{Color}m",
|
||||||
|
_ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType))
|
||||||
|
};
|
||||||
|
}
|
||||||
14
src/Library/TerminalUI/Models/ColorRGB.cs
Normal file
14
src/Library/TerminalUI/Models/ColorRGB.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor
|
||||||
|
{
|
||||||
|
public string ToConsoleColor()
|
||||||
|
=> Type switch
|
||||||
|
{
|
||||||
|
ColorType.Foreground => $"\x1b[38;2;{R};{G};{B};m",
|
||||||
|
ColorType.Background => $"\x1b[48;2;{R};{G};{B};m",
|
||||||
|
_ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType))
|
||||||
|
};
|
||||||
|
}
|
||||||
8
src/Library/TerminalUI/Models/ColorType.cs
Normal file
8
src/Library/TerminalUI/Models/ColorType.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public enum ColorType
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Foreground,
|
||||||
|
Background
|
||||||
|
}
|
||||||
83
src/Library/TerminalUI/Models/Colors.cs
Normal file
83
src/Library/TerminalUI/Models/Colors.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public static class Color256Colors
|
||||||
|
{
|
||||||
|
public static class Backgrounds
|
||||||
|
{
|
||||||
|
public static readonly Color256 Black = new(0, ColorType.Background);
|
||||||
|
public static readonly Color256 Blue = new(9, ColorType.Background);
|
||||||
|
public static readonly Color256 Cyan = new(11, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkBlue = new(1, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkCyan = new(3, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkGray = new(8, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkGreen = new(2, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkMagenta = new(5, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkRed = new(4, ColorType.Background);
|
||||||
|
public static readonly Color256 DarkYellow = new(6, ColorType.Background);
|
||||||
|
public static readonly Color256 Gray = new(7, ColorType.Background);
|
||||||
|
public static readonly Color256 Green = new(10, ColorType.Background);
|
||||||
|
public static readonly Color256 Magenta = new(13, ColorType.Background);
|
||||||
|
public static readonly Color256 Red = new(12, ColorType.Background);
|
||||||
|
public static readonly Color256 White = new(15, ColorType.Background);
|
||||||
|
}
|
||||||
|
public static class Foregrounds
|
||||||
|
{
|
||||||
|
public static readonly Color256 Black = new(0, ColorType.Foreground);
|
||||||
|
public static readonly Color256 Blue = new(9, ColorType.Foreground);
|
||||||
|
public static readonly Color256 Cyan = new(11, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkBlue = new(1, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkCyan = new(3, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkGray = new(8, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkGreen = new(2, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkMagenta = new(5, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkRed = new(4, ColorType.Foreground);
|
||||||
|
public static readonly Color256 DarkYellow = new(6, ColorType.Foreground);
|
||||||
|
public static readonly Color256 Gray = new(7, ColorType.Foreground);
|
||||||
|
public static readonly Color256 Green = new(10, ColorType.Foreground);
|
||||||
|
public static readonly Color256 Magenta = new(13, ColorType.Foreground);
|
||||||
|
public static readonly Color256 Red = new(12, ColorType.Foreground);
|
||||||
|
public static readonly Color256 White = new(15, ColorType.Foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ConsoleColors
|
||||||
|
{
|
||||||
|
public static class Backgrounds
|
||||||
|
{
|
||||||
|
public static readonly ConsoleColor Black = new(System.ConsoleColor.Black, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Blue = new(System.ConsoleColor.Blue, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Cyan = new(System.ConsoleColor.Cyan, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkBlue = new(System.ConsoleColor.DarkBlue, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkCyan = new(System.ConsoleColor.DarkCyan, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkGray = new(System.ConsoleColor.DarkGray, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkGreen = new(System.ConsoleColor.DarkGreen, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkMagenta = new(System.ConsoleColor.DarkMagenta, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkRed = new(System.ConsoleColor.DarkRed, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor DarkYellow = new(System.ConsoleColor.DarkYellow, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Gray = new(System.ConsoleColor.Gray, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Green = new(System.ConsoleColor.Green, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Magenta = new(System.ConsoleColor.Magenta, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Red = new(System.ConsoleColor.Red, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor White = new(System.ConsoleColor.White, ColorType.Background);
|
||||||
|
public static readonly ConsoleColor Yellow = new(System.ConsoleColor.Yellow, ColorType.Background);
|
||||||
|
}
|
||||||
|
public static class Foregrounds
|
||||||
|
{
|
||||||
|
public static readonly ConsoleColor Black = new(System.ConsoleColor.Black, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Blue = new(System.ConsoleColor.Blue, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Cyan = new(System.ConsoleColor.Cyan, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkBlue = new(System.ConsoleColor.DarkBlue, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkCyan = new(System.ConsoleColor.DarkCyan, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkGray = new(System.ConsoleColor.DarkGray, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkGreen = new(System.ConsoleColor.DarkGreen, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkMagenta = new(System.ConsoleColor.DarkMagenta, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkRed = new(System.ConsoleColor.DarkRed, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor DarkYellow = new(System.ConsoleColor.DarkYellow, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Gray = new(System.ConsoleColor.Gray, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Green = new(System.ConsoleColor.Green, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Magenta = new(System.ConsoleColor.Magenta, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Red = new(System.ConsoleColor.Red, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor White = new(System.ConsoleColor.White, ColorType.Foreground);
|
||||||
|
public static readonly ConsoleColor Yellow = new(System.ConsoleColor.Yellow, ColorType.Foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/Library/TerminalUI/Models/ConsoleColor.cs
Normal file
6
src/Library/TerminalUI/Models/ConsoleColor.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor
|
||||||
|
{
|
||||||
|
public string ToConsoleColor() => throw new NotImplementedException();
|
||||||
|
}
|
||||||
7
src/Library/TerminalUI/Models/IColor.cs
Normal file
7
src/Library/TerminalUI/Models/IColor.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public interface IColor
|
||||||
|
{
|
||||||
|
ColorType Type { get; }
|
||||||
|
string ToConsoleColor();
|
||||||
|
}
|
||||||
3
src/Library/TerminalUI/Models/Position.cs
Normal file
3
src/Library/TerminalUI/Models/Position.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace TerminalUI.Models;
|
||||||
|
|
||||||
|
public record struct Position(int PosX, int PosY);
|
||||||
113
src/Library/TerminalUI/PropertyChangeTracker.cs
Normal file
113
src/Library/TerminalUI/PropertyChangeTracker.cs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace TerminalUI;
|
||||||
|
|
||||||
|
internal interface IPropertyChangeTracker : IDisposable
|
||||||
|
{
|
||||||
|
Dictionary<string, IPropertyChangeTracker> Children { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
|
||||||
|
{
|
||||||
|
public Dictionary<string, IPropertyChangeTracker> Children { get; } = new();
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var propertyChangeTracker in Children.Values)
|
||||||
|
{
|
||||||
|
propertyChangeTracker.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||||
|
{
|
||||||
|
private readonly PropertyTrackTreeItem _propertyTrackTreeItem;
|
||||||
|
private readonly INotifyPropertyChanged _target;
|
||||||
|
private readonly IEnumerable<string> _propertiesToListen;
|
||||||
|
private readonly Action _updateBinding;
|
||||||
|
|
||||||
|
public PropertyChangeTracker(
|
||||||
|
PropertyTrackTreeItem propertyTrackTreeItem,
|
||||||
|
INotifyPropertyChanged target,
|
||||||
|
IEnumerable<string> propertiesToListen,
|
||||||
|
Action updateBinding)
|
||||||
|
{
|
||||||
|
_propertyTrackTreeItem = propertyTrackTreeItem;
|
||||||
|
_target = target;
|
||||||
|
_propertiesToListen = propertiesToListen;
|
||||||
|
_updateBinding = updateBinding;
|
||||||
|
target.PropertyChanged += Target_PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Target_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var propertyName = e.PropertyName;
|
||||||
|
if (propertyName is null || !_propertiesToListen.Contains(propertyName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateBinding();
|
||||||
|
Children.Remove(propertyName);
|
||||||
|
|
||||||
|
var newChild = PropertyChangeHelper.TraverseDataContext(
|
||||||
|
_propertyTrackTreeItem.Children[propertyName],
|
||||||
|
_target.GetType().GetProperty(propertyName)?.GetValue(_target),
|
||||||
|
_updateBinding
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newChild is not null)
|
||||||
|
{
|
||||||
|
Children.Add(propertyName, newChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_target.PropertyChanged -= Target_PropertyChanged;
|
||||||
|
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PropertyTrackTreeItem
|
||||||
|
{
|
||||||
|
public Dictionary<string, PropertyTrackTreeItem> Children { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class PropertyChangeHelper
|
||||||
|
{
|
||||||
|
internal static IPropertyChangeTracker? TraverseDataContext(
|
||||||
|
PropertyTrackTreeItem propertyTrackTreeItem,
|
||||||
|
object? obj,
|
||||||
|
Action updateBinding
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (obj is null) return null;
|
||||||
|
|
||||||
|
IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged
|
||||||
|
? new PropertyChangeTracker(propertyTrackTreeItem, notifyPropertyChanged, propertyTrackTreeItem.Children.Keys, updateBinding)
|
||||||
|
: new NonSubscriberPropertyChangeTracker();
|
||||||
|
|
||||||
|
foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children)
|
||||||
|
{
|
||||||
|
var childTracker = TraverseDataContext(
|
||||||
|
trackerTreeItem,
|
||||||
|
obj.GetType().GetProperty(propertyName)?.GetValue(obj),
|
||||||
|
updateBinding
|
||||||
|
);
|
||||||
|
|
||||||
|
if (childTracker is not null)
|
||||||
|
{
|
||||||
|
tracker.Children.Add(propertyName, childTracker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
using TerminalUI.Controls;
|
using TerminalUI.Controls;
|
||||||
|
using TerminalUI.Models;
|
||||||
|
|
||||||
namespace TerminalUI.Traits;
|
namespace TerminalUI.Traits;
|
||||||
|
|
||||||
public interface IContentRenderer
|
public interface IContentRenderer
|
||||||
{
|
{
|
||||||
IView? Content { get; set; }
|
IView? Content { get; set; }
|
||||||
Action ContentRendererMethod { get; set; }
|
Action<Position> ContentRendererMethod { get; set; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user