diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj
index 316f89f..22168c5 100644
--- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj
+++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj
@@ -11,8 +11,4 @@
-
-
-
-
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs
index dee940b..e338921 100644
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs
+++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs
@@ -1,8 +1,5 @@
using FileTime.App.Core.Services;
-using FileTime.ConsoleUI.App.Extensions;
using FileTime.ConsoleUI.App.KeyInputHandling;
-using FileTime.Core.Models;
-using Terminal.Gui;
namespace FileTime.ConsoleUI.App;
@@ -10,7 +7,7 @@ public class App : IApplication
{
private readonly ILifecycleService _lifecycleService;
private readonly IConsoleAppState _consoleAppState;
- private readonly IAppKeyService _appKeyService;
+ //private readonly IAppKeyService _appKeyService;
private readonly MainWindow _mainWindow;
private readonly IKeyInputHandlerService _keyInputHandlerService;
@@ -18,13 +15,13 @@ public class App : IApplication
ILifecycleService lifecycleService,
IKeyInputHandlerService keyInputHandlerService,
IConsoleAppState consoleAppState,
- IAppKeyService appKeyService,
+ //IAppKeyService appKeyService,
MainWindow mainWindow)
{
_lifecycleService = lifecycleService;
_keyInputHandlerService = keyInputHandlerService;
_consoleAppState = consoleAppState;
- _appKeyService = appKeyService;
+ //_appKeyService = appKeyService;
_mainWindow = mainWindow;
}
@@ -34,24 +31,5 @@ public class App : IApplication
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
_mainWindow.Initialize();
-
- Application.Init();
- var asd = Application.Top.ColorScheme;
-
- foreach (var element in _mainWindow.GetElements())
- {
- Application.Top.Add(element);
- }
-
- Application.RootKeyEvent += e =>
- {
- if (e.ToGeneralKeyEventArgs(_appKeyService) is not { } args) return false;
- _keyInputHandlerService.HandleKeyInput(args);
-
- return args.Handled;
- };
-
- Application.Run();
- Application.Shutdown();
}
}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemRenderer.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemRenderer.cs
deleted file mode 100644
index 7620731..0000000
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemRenderer.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System.Collections;
-using System.Collections.ObjectModel;
-using DeclarativeProperty;
-using FileTime.App.Core.ViewModels;
-using Terminal.Gui;
-
-namespace FileTime.ConsoleUI.App.Controls;
-
-public class ItemRenderer : IListDataSource
-{
- private readonly IDeclarativeProperty?> _source;
-
- public ItemRenderer(
- IDeclarativeProperty?> source,
- Action update
- )
- {
- _source = source;
- source.Subscribe((_, _) =>
- {
- update();
- });
- }
-
- public void Render(ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0)
- {
- var itemViewModel = _source.Value?[item];
- container.Move(col, line);
- driver.AddStr(itemViewModel?.DisplayNameText ?? string.Empty);
- }
-
- public bool IsMarked(int item) => false;
-
- public void SetMark(int item, bool value)
- {
- }
-
- public IList ToList() => _source.Value!;
-
- public int Count => _source.Value?.Count ?? 0;
- public int Length => 20;
-}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/KeyEventArgsExtensions.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/KeyEventArgsExtensions.cs
deleted file mode 100644
index ccdcdb3..0000000
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/KeyEventArgsExtensions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using FileTime.App.Core.Models;
-using FileTime.App.Core.Services;
-using Terminal.Gui;
-
-namespace FileTime.ConsoleUI.App.Extensions;
-
-public static class KeyEventArgsExtensions
-{
- public static GeneralKeyEventArgs? ToGeneralKeyEventArgs(this KeyEvent args, IAppKeyService appKeyService)
- {
- var maybeKey = appKeyService.MapKey(args.Key);
- if (maybeKey is not { } key1) return null;
- return new GeneralKeyEventArgs
- {
- Key = key1
- };
- }
-}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/UIExtensions.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/UIExtensions.cs
deleted file mode 100644
index d25e2ca..0000000
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/UIExtensions.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Terminal.Gui;
-
-namespace FileTime.ConsoleUI.App.Extensions;
-
-public static class UiExtensions
-{
- public static void Update(this ListView listView)
- {
- listView.EnsureSelectedItemVisible();
- listView.SetNeedsDisplay();
- }
-}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj b/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj
index 1cea262..93c5f85 100644
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj
+++ b/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj
@@ -13,12 +13,16 @@
-
+
+
+
+
+
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs
deleted file mode 100644
index dc0294f..0000000
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System.Collections.ObjectModel;
-using FileTime.App.Core.Models;
-using FileTime.App.Core.Services;
-using Terminal.Gui;
-
-namespace FileTime.ConsoleUI.App.KeyInputHandling;
-
-public sealed class ConsoleAppKeyService : IAppKeyService
-{
- private static readonly Dictionary KeyMapping;
-
- //TODO: write test for this. Test if every enum value is present in the dictionary.
- public static ReadOnlyDictionary KeyMappingReadOnly { get; }
-
- static ConsoleAppKeyService()
- {
- KeyMapping = new Dictionary
- {
- {Key.A, Keys.A},
- {Key.B, Keys.B},
- {Key.C, Keys.C},
- {Key.D, Keys.D},
- {Key.E, Keys.E},
- {Key.F, Keys.F},
- {Key.G, Keys.G},
- {Key.H, Keys.H},
- {Key.I, Keys.I},
- {Key.J, Keys.J},
- {Key.K, Keys.K},
- {Key.L, Keys.L},
- {Key.M, Keys.M},
- {Key.N, Keys.N},
- {Key.O, Keys.O},
- {Key.P, Keys.P},
- {Key.Q, Keys.Q},
- {Key.R, Keys.R},
- {Key.S, Keys.S},
- {Key.T, Keys.T},
- {Key.U, Keys.U},
- {Key.V, Keys.V},
- {Key.W, Keys.W},
- {Key.X, Keys.X},
- {Key.Y, Keys.Y},
- {Key.Z, Keys.Z},
- {Key.F1, Keys.F1},
- {Key.F2, Keys.F2},
- {Key.F3, Keys.F3},
- {Key.F4, Keys.F4},
- {Key.F5, Keys.F5},
- {Key.F6, Keys.F6},
- {Key.F7, Keys.F7},
- {Key.F8, Keys.F8},
- {Key.F9, Keys.F9},
- {Key.F10, Keys.F10},
- {Key.F11, Keys.F11},
- {Key.F12, Keys.F12},
- {Key.D0, Keys.Num0},
- {Key.D1, Keys.Num1},
- {Key.D2, Keys.Num2},
- {Key.D3, Keys.Num3},
- {Key.D4, Keys.Num4},
- {Key.D5, Keys.Num5},
- {Key.D6, Keys.Num6},
- {Key.D7, Keys.Num7},
- {Key.D8, Keys.Num8},
- {Key.D9, Keys.Num9},
- {Key.CursorUp, Keys.Up},
- {Key.CursorDown, Keys.Down},
- {Key.CursorLeft, Keys.Left},
- {Key.CursorRight, Keys.Right},
- {Key.Enter, Keys.Enter},
- {Key.Esc, Keys.Escape},
- {Key.Backspace, Keys.Backspace},
- {Key.Space, Keys.Space},
- {Key.PageUp, Keys.PageUp},
- {Key.PageDown, Keys.PageDown},
- {Key.Tab, Keys.Tab},
- {(Key)44, Keys.Comma},
- {(Key)63, Keys.Question},
- {(Key)123456, Keys.LWin},
- {(Key)123457, Keys.RWin},
- };
-
- KeyMappingReadOnly = new(KeyMapping);
- }
-
- public Keys? MapKey(Key key)
- {
- if (!KeyMapping.TryGetValue(key, out var mappedKey)) return null;
- return mappedKey;
- }
-}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs
index f3c2da1..302f38b 100644
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs
+++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs
@@ -1,14 +1,16 @@
-using DeclarativeProperty;
-using FileTime.ConsoleUI.App.Controls;
+using System.Collections.ObjectModel;
+using DeclarativeProperty;
+using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Extensions;
-using Terminal.Gui;
+using TerminalUI;
+using TerminalUI.Controls;
+using TerminalUI.Extensions;
namespace FileTime.ConsoleUI.App;
public class MainWindow
{
private readonly IConsoleAppState _consoleAppState;
- private View[] _views;
private const int ParentColumnWidth = 20;
public MainWindow(IConsoleAppState consoleAppState)
@@ -16,67 +18,16 @@ public class MainWindow
_consoleAppState = consoleAppState;
}
- public void Initialize() =>
- _views = new View[]
- {
- GetSelectedItemsView(),
- GetParentsChildren(),
- GetSelectedsChildren()
- };
-
- private ListView GetSelectedItemsView()
+ public void Initialize()
{
- ListView selectedItemsView = new() {X = ParentColumnWidth, Y = 1, Width = Dim.Percent(60) - 20, Height = Dim.Fill()};
- var selectedsItems = _consoleAppState
- .SelectedTab
- .Map(t => t.CurrentItems)
- .Switch();
+ ListView selectedItemsView = new();
+ selectedItemsView.DataContext = _consoleAppState;
- var selectedItem = _consoleAppState.SelectedTab
- .Map(t => t.CurrentSelectedItem)
- .Switch();
-
- DeclarativePropertyHelpers.CombineLatest(
- selectedItem,
- selectedsItems,
- (selected, items) => Task.FromResult(items.IndexOf(selected)))
- .Subscribe((index, _) =>
- {
- if (index == -1) return;
- selectedItemsView.SelectedItem = index;
- selectedItemsView.Update();
- });
-
- var renderer = new ItemRenderer(selectedsItems, selectedItemsView.Update);
- selectedItemsView.Source = renderer;
- return selectedItemsView;
+ selectedItemsView.Bind(
+ selectedItemsView,
+ appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
+ v => v.ItemsSource);
+
+ selectedItemsView.Render();
}
-
- private ListView GetParentsChildren()
- {
- ListView parentsChildrenView = new() {X = 0, Y = 1, Width = ParentColumnWidth, Height = Dim.Fill()};
- var parentsChildren = _consoleAppState
- .SelectedTab
- .Map(t => t.ParentsChildren)
- .Switch();
-
- var renderer = new ItemRenderer(parentsChildren, parentsChildrenView.Update);
- parentsChildrenView.Source = renderer;
- return parentsChildrenView;
- }
-
- private ListView GetSelectedsChildren()
- {
- ListView selectedsChildrenView = new() {X = Pos.Percent(60), Y = 1, Width = Dim.Percent(40), Height = Dim.Fill()};
- var selectedsChildren = _consoleAppState
- .SelectedTab
- .Map(t => t.SelectedsChildren)
- .Switch();
-
- var renderer = new ItemRenderer(selectedsChildren, selectedsChildrenView.Update);
- selectedsChildrenView.Source = renderer;
- return selectedsChildrenView;
- }
-
- public IEnumerable GetElements() => _views;
}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/SystemClipboardService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/SystemClipboardService.cs
deleted file mode 100644
index d4777b1..0000000
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/SystemClipboardService.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using FileTime.App.Core.Services;
-using FileTime.Core.Models;
-using Terminal.Gui;
-
-namespace FileTime.ConsoleUI.App.Services;
-
-public class SystemClipboardService : ISystemClipboardService
-{
- public Task CopyToClipboardAsync(string text)
- {
- Clipboard.TrySetClipboardData(text);
- return Task.CompletedTask;
- }
-
- public Task> GetFilesAsync() => throw new NotImplementedException();
-
- public Task SetFilesAsync(IEnumerable files) => throw new NotImplementedException();
-}
\ No newline at end of file
diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs
index 1990aad..69fa664 100644
--- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs
+++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs
@@ -5,7 +5,6 @@ using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Terminal.Gui;
namespace FileTime.ConsoleUI.App;
@@ -19,8 +18,6 @@ public static class Startup
services.TryAddSingleton(sp => sp.GetRequiredService());
services.TryAddSingleton();
services.TryAddSingleton();
- services.TryAddSingleton, ConsoleAppKeyService>();
- services.TryAddSingleton();
services.AddSingleton();
return services;
diff --git a/src/FileTime.sln b/src/FileTime.sln
index 3305bbf..fff18c0 100644
--- a/src/FileTime.sln
+++ b/src/FileTime.sln
@@ -117,6 +117,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.ContainerSizeS
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.App.Abstractions", "ConsoleApp\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj", "{81F44BBB-6F89-41B4-89F1-4A3204843DB5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\TerminalUI\TerminalUI.csproj", "{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -315,6 +317,10 @@ Global
{81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -369,6 +375,7 @@ Global
{E5FD38ED-6E4B-42AA-850B-470B939B836B} = {A5291117-3001-498B-AC8B-E14F71F72570}
{826AFD32-E36B-48BA-BC1E-1476B393CF24} = {A5291117-3001-498B-AC8B-E14F71F72570}
{81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
+ {2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs
index bf5aa8c..919afd5 100644
--- a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs
+++ b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs
@@ -91,6 +91,6 @@ public static class DeclarativePropertyExtensions
Action? setValueHook = null)
=> new CombineLatestProperty(prop1, prop2, func, setValueHook);
- public static IDeclarativeProperty Switch(this IDeclarativeProperty> from)
+ public static IDeclarativeProperty Switch(this IDeclarativeProperty?> from)
=> new SwitchProperty(from);
}
\ No newline at end of file
diff --git a/src/Library/DeclarativeProperty/SwitchProperty.cs b/src/Library/DeclarativeProperty/SwitchProperty.cs
index 51bdb7c..da861d8 100644
--- a/src/Library/DeclarativeProperty/SwitchProperty.cs
+++ b/src/Library/DeclarativeProperty/SwitchProperty.cs
@@ -4,13 +4,13 @@ public sealed class SwitchProperty : DeclarativePropertyBase
{
private IDisposable? _innerSubscription;
- public SwitchProperty(IDeclarativeProperty?> from) : base(from.Value is null ? default : from.Value.Value)
+ public SwitchProperty(IDeclarativeProperty?> from) : base(from.Value is null ? default : from.Value.Value)
{
AddDisposable(from.Subscribe(HandleStreamChange));
_innerSubscription = from.Value?.Subscribe(HandleInnerValueChange);
}
- private async Task HandleStreamChange(IDeclarativeProperty? next, CancellationToken token)
+ private async Task HandleStreamChange(IDeclarativeProperty? next, CancellationToken token)
{
_innerSubscription?.Dispose();
_innerSubscription = next?.Subscribe(HandleInnerValueChange);
diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs
new file mode 100644
index 0000000..b50bd5f
--- /dev/null
+++ b/src/Library/TerminalUI/Binding.cs
@@ -0,0 +1,49 @@
+using System.ComponentModel;
+using System.Reflection;
+using TerminalUI.Controls;
+using TerminalUI.Traits;
+
+namespace TerminalUI;
+
+public class Binding : IDisposable
+{
+ private readonly Func _dataContextMapper;
+ private IView _view;
+ private object? _propertySource;
+ private PropertyInfo _targetProperty;
+
+ public Binding(
+ IView view,
+ Func dataContextMapper,
+ object? propertySource,
+ PropertyInfo targetProperty
+ )
+ {
+ _view = view;
+ _dataContextMapper = dataContextMapper;
+ _propertySource = propertySource;
+ _targetProperty = targetProperty;
+ view.PropertyChanged += View_PropertyChanged;
+ _targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext));
+
+ if(propertySource is IDisposableCollection disposableCollection)
+ disposableCollection.AddDisposable(this);
+
+ view.AddDisposable(this);
+ }
+
+ private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(IView.DataContext)) return;
+
+ _targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext));
+ }
+
+ public void Dispose()
+ {
+ _view.PropertyChanged -= View_PropertyChanged;
+ _view = null!;
+ _propertySource = null!;
+ _targetProperty = null!;
+ }
+}
\ No newline at end of file
diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs
new file mode 100644
index 0000000..67e9a6c
--- /dev/null
+++ b/src/Library/TerminalUI/Controls/IView.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel;
+using TerminalUI.Traits;
+
+namespace TerminalUI.Controls;
+
+public interface IView : INotifyPropertyChanged, IDisposableCollection
+{
+ T? DataContext { get; set; }
+ void Render();
+
+ TChild CreateChild()
+ where TChild : IView, new();
+
+ TChild CreateChild(Func dataContextMapper)
+ where TChild : IView, new();
+}
\ No newline at end of file
diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs
new file mode 100644
index 0000000..71b5d28
--- /dev/null
+++ b/src/Library/TerminalUI/Controls/ListView.cs
@@ -0,0 +1,85 @@
+using System.Buffers;
+using System.Collections.ObjectModel;
+using DeclarativeProperty;
+
+namespace TerminalUI.Controls;
+
+public class ListView : View
+{
+ private static readonly ArrayPool> ListViewItemPool = ArrayPool>.Shared;
+
+ private readonly List _itemsDisposables = new();
+ private Func>? _getItems;
+ private object? _itemsSource;
+ private ListViewItem[]? _listViewItems;
+ private int _listViewItemLength;
+
+ public object? ItemsSource
+ {
+ get => _itemsSource;
+ set
+ {
+ if (_itemsSource == value) return;
+ _itemsSource = value;
+
+ foreach (var disposable in _itemsDisposables)
+ {
+ disposable.Dispose();
+ }
+
+ _itemsDisposables.Clear();
+
+ if (_itemsSource is IDeclarativeProperty> observableDeclarativeProperty)
+ _getItems = () => observableDeclarativeProperty.Value;
+ else if (_itemsSource is IDeclarativeProperty> readOnlyObservableDeclarativeProperty)
+ _getItems = () => readOnlyObservableDeclarativeProperty.Value;
+ else if (_itemsSource is IDeclarativeProperty> enumerableDeclarativeProperty)
+ _getItems = () => enumerableDeclarativeProperty.Value;
+ else if (_itemsSource is ICollection collection)
+ _getItems = () => collection;
+ else if (_itemsSource is TItem[] array)
+ _getItems = () => array;
+ else if (_itemsSource is IEnumerable enumerable)
+ _getItems = () => enumerable.ToArray();
+
+ if (_listViewItems is not null)
+ {
+ ListViewItemPool.Return(_listViewItems);
+ _listViewItems = null;
+ }
+
+ OnPropertyChanged();
+ }
+ }
+
+ public override void Render()
+ {
+ if (_getItems is null) return;
+ var items = _getItems().ToList();
+
+ Span> listViewItems = null;
+
+ if (_listViewItems is null || _listViewItems.Length != items.Count)
+ {
+ var newListViewItems = ListViewItemPool.Rent(items.Count);
+ for (var i = 0; i < items.Count; i++)
+ {
+ var dataContext = items[i];
+ newListViewItems[i] = CreateChild, TItem>(_ => dataContext);
+ }
+
+ _listViewItems = newListViewItems;
+ _listViewItemLength = items.Count;
+ listViewItems = newListViewItems[..items.Count];
+ }
+ else
+ {
+ listViewItems = _listViewItems[.._listViewItemLength];
+ }
+
+ foreach (var item in listViewItems)
+ {
+ item.Render();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Library/TerminalUI/Controls/ListViewItem.cs b/src/Library/TerminalUI/Controls/ListViewItem.cs
new file mode 100644
index 0000000..307a2a7
--- /dev/null
+++ b/src/Library/TerminalUI/Controls/ListViewItem.cs
@@ -0,0 +1,9 @@
+namespace TerminalUI.Controls;
+
+public class ListViewItem : View
+{
+ public override void Render()
+ {
+ Console.WriteLine(DataContext?.ToString());
+ }
+}
\ No newline at end of file
diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs
new file mode 100644
index 0000000..ab93bb7
--- /dev/null
+++ b/src/Library/TerminalUI/Controls/View.cs
@@ -0,0 +1,73 @@
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace TerminalUI.Controls;
+
+public abstract class View : IView
+{
+ private readonly ConcurrentBag _disposables = new();
+ public T? DataContext { get; set; }
+ public abstract void Render();
+
+ public TChild CreateChild() where TChild : IView, new()
+ {
+ var child = new TChild
+ {
+ DataContext = DataContext
+ };
+ var mapper = new DataContextMapper(this, d => child.DataContext = d);
+ AddDisposable(mapper);
+ child.AddDisposable(mapper);
+
+ return child;
+ }
+
+ public TChild CreateChild(Func dataContextMapper)
+ where TChild : IView, new()
+ {
+ var child = new TChild
+ {
+ DataContext = dataContextMapper(DataContext)
+ };
+ var mapper = new DataContextMapper(this, d => child.DataContext = dataContextMapper(d));
+ AddDisposable(mapper);
+ child.AddDisposable(mapper);
+
+ return child;
+ }
+
+ public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ protected bool SetField(ref TProp field, TProp value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this); // Violates rule
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ foreach (var disposable in _disposables)
+ {
+ disposable.Dispose();
+ }
+
+ _disposables.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Library/TerminalUI/DataContextMapper.cs b/src/Library/TerminalUI/DataContextMapper.cs
new file mode 100644
index 0000000..8031a56
--- /dev/null
+++ b/src/Library/TerminalUI/DataContextMapper.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using TerminalUI.Controls;
+
+namespace TerminalUI;
+
+public class DataContextMapper : IDisposable
+{
+ private readonly IView _source;
+ private readonly Action _setter;
+
+ public DataContextMapper(IView source, Action setter)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ _source = source;
+ _setter = setter;
+ source.PropertyChanged += SourceOnPropertyChanged;
+ }
+
+ private void SourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(IView