Console MessageBox, admin mode

This commit is contained in:
2023-08-28 13:49:54 +02:00
parent bb44ca0308
commit bc865011d3
18 changed files with 313 additions and 88 deletions

View File

@@ -4,6 +4,7 @@ public class ConsoleApplicationConfiguration
{ {
public string? ConsoleDriver { get; set; } public string? ConsoleDriver { get; set; }
public bool DisableUtf8 { get; set; } public bool DisableUtf8 { get; set; }
public string? AdminModeIcon { get; set; }
public string? ClipboardSingleIcon { get; set; } public string? ClipboardSingleIcon { get; set; }
public string? ClipboardMultipleIcon { get; set; } public string? ClipboardMultipleIcon { get; set; }
} }

View File

@@ -12,6 +12,7 @@
<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="..\..\AppCommon\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" /> <ProjectReference Include="..\..\AppCommon\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
<ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" /> <ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -7,6 +7,7 @@ using FileTime.App.FrequencyNavigation.ViewModels;
using FileTime.ConsoleUI.App.Services; using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Providers.LocalAdmin;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -23,5 +24,6 @@ public interface IRootViewModel
IFrequencyNavigationViewModel FrequencyNavigation { get; } IFrequencyNavigationViewModel FrequencyNavigation { get; }
IItemPreviewService ItemPreviewService { get; } IItemPreviewService ItemPreviewService { get; }
IClipboardService ClipboardService { get; } IClipboardService ClipboardService { get; }
IAdminElevationManager AdminElevationManager { get; }
event Action<IInputElement>? FocusReadInputElement; event Action<IInputElement>? FocusReadInputElement;
} }

View File

@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using TerminalUI; using TerminalUI;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
using TerminalUI.Models;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -139,6 +140,14 @@ public class App : IApplication
Thread.Sleep(10); Thread.Sleep(10);
} }
_consoleDriver.ExitRestrictedMode();
_consoleDriver.Clear();
var size = _consoleDriver.GetWindowSize();
var shutdownText = "Shutting down...";
_consoleDriver.SetCursorPosition(new Position(size.Width / 2 - shutdownText.Length / 2, size.Height / 2));
_consoleDriver.Write(shutdownText);
Task.Run(async () => await _lifecycleService.ExitAsync()).Wait(); Task.Run(async () => await _lifecycleService.ExitAsync()).Wait();
} }

View File

@@ -51,36 +51,49 @@ public class Dialogs
}; };
} }
private void UpdateReadInputsFocus()
{
foreach (var readInputsChild in _readInputs.Children)
{
if (readInputsChild.DataContext == _inputElementToFocus)
{
if (FindFocusable(readInputsChild) is { } focusable)
{
focusable.Focus();
_inputElementToFocus = null;
break;
}
}
}
IFocusable? FindFocusable(IView view)
{
if (view is IFocusable focusable) return focusable;
foreach (var viewVisualChild in view.VisualChildren)
{
if (FindFocusable(viewVisualChild) is { } focusableChild)
return focusableChild;
}
return null;
}
}
public IView<IRootViewModel> View() public IView<IRootViewModel> View()
{ {
var root = new Grid<IRootViewModel>()
{
Margin = 5,
ChildInitializer =
{
ReadInputs(),
MessageBox()
}
};
return root;
}
private IView<IRootViewModel> MessageBox()
{
var okButton = new Button<IRootViewModel>
{
Margin = "0 0 5 0",
Content = new TextBlock<IRootViewModel>()
.Setup(t => t.Bind(
t,
dc => dc.DialogService.LastMessageBox.Value.OkText,
t => t.Text)),
}.WithClickHandler(b => b.DataContext?.DialogService.LastMessageBox.Value?.Ok());
var cancelButton =
new Button<IRootViewModel>
{
Margin = "0 0 5 0",
Content = new TextBlock<IRootViewModel>()
.Setup(t => t.Bind(
t,
dc => dc.DialogService.LastMessageBox.Value.CancelText,
t => t.Text)),
}
.Setup(b => b.Bind(
b,
dc => dc.DialogService.LastMessageBox.Value.ShowCancel,
b => b.IsVisible))
.WithClickHandler(b => b.DataContext?.DialogService.LastMessageBox.Value?.Cancel());
var root = new Border<IRootViewModel> var root = new Border<IRootViewModel>
{ {
Margin = 5, Margin = 5,
@@ -88,52 +101,53 @@ public class Dialogs
Background = SpecialColor.None, Background = SpecialColor.None,
Content = new Grid<IRootViewModel> Content = new Grid<IRootViewModel>
{ {
RowDefinitionsObject = "Auto Auto",
ChildInitializer = ChildInitializer =
{ {
ReadInputs() new TextBlock<IRootViewModel>()
.Setup(t => t.Bind(
t,
dc => dc.DialogService.LastMessageBox.Value.Text,
t => t.Text)),
new StackPanel<IRootViewModel>
{
Orientation = Orientation.Horizontal,
Extensions = {new GridPositionExtension(0, 1)},
ChildInitializer =
{
okButton,
cancelButton
}
}
} }
} }
}; };
root.Bind( root.Bind(
root, root,
d => d.DialogService.ReadInput.Value != null, d => d.DialogService.LastMessageBox.Value != null,
v => v.IsVisible); v => v.IsVisible,
fallbackValue: false);
((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) => ((INotifyPropertyChanged) root).PropertyChanged += (_, e) =>
{ {
if (e.PropertyName == nameof(ItemsControl<object, object>.Children)) if (e.PropertyName == nameof(IView.IsVisible))
{ {
_readInputChildHandlerUnSubscriber?.Invoke(); okButton.Focus();
if (_readInputs.Children.Count > 0)
{
UpdateReadInputsFocus();
}
else
{
_inputElementToFocus = null;
}
if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler;
_readInputChildHandlerUnSubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; };
}
void NotifyCollectionChangedEventHandler(
object? sender,
NotifyCollectionChangedEventArgs e)
{
UpdateReadInputsFocus();
}
} }
}; };
return root; return root;
} }
private IView<IRootViewModel> ReadInputs() private IView<IRootViewModel> ReadInputs()
=> new Grid<IRootViewModel> {
var root = new Border<IRootViewModel>
{
Margin = 5,
BorderThickness = 1,
Background = SpecialColor.None,
Content = new Grid<IRootViewModel>
{ {
RowDefinitionsObject = "Auto Auto", RowDefinitionsObject = "Auto Auto",
ChildInitializer = ChildInitializer =
@@ -150,8 +164,17 @@ public class Dialogs
)) ))
.WithExtension(new GridPositionExtension(0, 1)) .WithExtension(new GridPositionExtension(0, 1))
} }
}
}; };
root.Bind(
root,
d => d.DialogService.ReadInput.Value != null,
v => v.IsVisible);
return root;
}
private IView<IPreviewElement> ReadInputPreviewItemTemplate() private IView<IPreviewElement> ReadInputPreviewItemTemplate()
{ {
var grid = new Grid<IPreviewElement> var grid = new Grid<IPreviewElement>
@@ -367,6 +390,64 @@ public class Dialogs
_readInputs = readInputs; _readInputs = readInputs;
((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(ItemsControl<object, object>.Children))
{
_readInputChildHandlerUnSubscriber?.Invoke();
if (_readInputs.Children.Count > 0)
{
UpdateReadInputsFocus();
}
else
{
_inputElementToFocus = null;
}
if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler;
_readInputChildHandlerUnSubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; };
}
void NotifyCollectionChangedEventHandler(
object? sender,
NotifyCollectionChangedEventArgs e)
{
UpdateReadInputsFocus();
}
}
};
return readInputs; return readInputs;
} }
private void UpdateReadInputsFocus()
{
foreach (var readInputsChild in _readInputs.Children)
{
if (readInputsChild.DataContext == _inputElementToFocus)
{
if (FindFocusable(readInputsChild) is { } focusable)
{
focusable.Focus();
_inputElementToFocus = null;
break;
}
}
}
IFocusable? FindFocusable(IView view)
{
if (view is IFocusable focusable) return focusable;
foreach (var viewVisualChild in view.VisualChildren)
{
if (FindFocusable(viewVisualChild) is { } focusableChild)
return focusableChild;
}
return null;
}
}
} }

View File

@@ -138,12 +138,22 @@ public class MainWindow
{ {
Margin = "2 0 0 0", Margin = "2 0 0 0",
Extensions = {new GridPositionExtension(2, 0)}, Extensions = {new GridPositionExtension(2, 0)},
Orientation = Orientation.Horizontal,
ChildInitializer = ChildInitializer =
{ {
new TextBlock<IRootViewModel> new TextBlock<IRootViewModel>
{
Text = _consoleApplicationConfiguration.Value.AdminModeIcon ??
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "A+ " : "\ud83d\udd11"),
AsciiOnly = false
}.Setup(t => t.Bind(
t,
dc => dc.AdminElevationManager.IsAdminInstanceRunning,
t => t.IsVisible)),
new TextBlock<IRootViewModel>
{ {
Text = _consoleApplicationConfiguration.Value.ClipboardSingleIcon ?? Text = _consoleApplicationConfiguration.Value.ClipboardSingleIcon ??
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "C" : "\ud83d\udccb"), (_consoleApplicationConfiguration.Value.DisableUtf8 ? "C " : "\ud83d\udccb"),
AsciiOnly = false AsciiOnly = false
}.Setup(t => t.Bind( }.Setup(t => t.Bind(
t, t,
@@ -152,7 +162,7 @@ public class MainWindow
new TextBlock<IRootViewModel> new TextBlock<IRootViewModel>
{ {
Text = _consoleApplicationConfiguration.Value.ClipboardMultipleIcon ?? Text = _consoleApplicationConfiguration.Value.ClipboardMultipleIcon ??
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "CC" : "\ud83d\udccb+"), (_consoleApplicationConfiguration.Value.DisableUtf8 ? "CC " : "\ud83d\udccb+"),
AsciiOnly = false AsciiOnly = false
}.Setup(t => t.Bind( }.Setup(t => t.Bind(
t, t,

View File

@@ -7,6 +7,7 @@ using FileTime.App.FrequencyNavigation.ViewModels;
using FileTime.ConsoleUI.App.Services; using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Providers.LocalAdmin;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -20,6 +21,7 @@ public partial class RootViewModel : IRootViewModel
public IFrequencyNavigationViewModel FrequencyNavigation { get; } public IFrequencyNavigationViewModel FrequencyNavigation { get; }
public IItemPreviewService ItemPreviewService { get; } public IItemPreviewService ItemPreviewService { get; }
public IClipboardService ClipboardService { get; } public IClipboardService ClipboardService { get; }
public IAdminElevationManager AdminElevationManager { get; }
public IDialogService DialogService { get; } public IDialogService DialogService { get; }
public ITimelineViewModel TimelineViewModel { get; } public ITimelineViewModel TimelineViewModel { get; }
public IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get;} public IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get;}
@@ -34,7 +36,8 @@ public partial class RootViewModel : IRootViewModel
ITimelineViewModel timelineViewModel, ITimelineViewModel timelineViewModel,
IFrequencyNavigationViewModel frequencyNavigation, IFrequencyNavigationViewModel frequencyNavigation,
IItemPreviewService itemPreviewService, IItemPreviewService itemPreviewService,
IClipboardService clipboardService) IClipboardService clipboardService,
IAdminElevationManager adminElevationManager)
{ {
AppState = appState; AppState = appState;
PossibleCommands = possibleCommands; PossibleCommands = possibleCommands;
@@ -44,6 +47,7 @@ public partial class RootViewModel : IRootViewModel
FrequencyNavigation = frequencyNavigation; FrequencyNavigation = frequencyNavigation;
ItemPreviewService = itemPreviewService; ItemPreviewService = itemPreviewService;
ClipboardService = clipboardService; ClipboardService = clipboardService;
AdminElevationManager = adminElevationManager;
DialogService.ReadInput.PropertyChanged += (o, e) => DialogService.ReadInput.PropertyChanged += (o, e) =>
{ {

View File

@@ -16,7 +16,12 @@ public class CustomLoggerSink : ILogEventSink
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
if (logEvent.Level >= LogEventLevel.Error) if (logEvent.Level >= LogEventLevel.Error
&& logEvent.Properties.TryGetValue("SourceContext", out var sourceContext))
{
var s = sourceContext.ToString();
if (s != "\"Microsoft.AspNetCore.SignalR.Client.HubConnection\"")
{ {
var message = logEvent.RenderMessage(); var message = logEvent.RenderMessage();
if (logEvent.Exception is not null) if (logEvent.Exception is not null)
@@ -25,4 +30,5 @@ public class CustomLoggerSink : ILogEventSink
_dialogService.Value.ShowToastMessage(message); _dialogService.Value.ShowToastMessage(message);
} }
} }
}
} }

View File

@@ -1,4 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using FileTime.App.Core; using FileTime.App.Core;
using FileTime.App.Core.Configuration; using FileTime.App.Core.Configuration;
using FileTime.ConsoleUI; using FileTime.ConsoleUI;
@@ -19,6 +21,7 @@ Console.OutputEncoding = System.Text.Encoding.UTF8;
IConsoleDriver? driver = null; IConsoleDriver? driver = null;
#if DEBUG #if DEBUG
(AppDataRoot, EnvironmentName) = Init.InitDevelopment(); (AppDataRoot, EnvironmentName) = Init.InitDevelopment();
#endif #endif
if (AppDataRoot is null) if (AppDataRoot is null)
@@ -49,6 +52,7 @@ try
} }
finally finally
{ {
driver?.Clear();
driver?.SetCursorVisible(true); driver?.SetCursorVisible(true);
driver?.Dispose(); driver?.Dispose();
} }

View File

@@ -1,4 +1,5 @@
{ {
"AdminModeIcon": "\uf0e7",
"ClipboardSingleIcon": "\udb80\udd4c", "ClipboardSingleIcon": "\udb80\udd4c",
"ClipboardMultipleIcon": "\udb84\ude68" "ClipboardMultipleIcon": "\udb84\ude68"
} }

View File

@@ -57,9 +57,9 @@ public static class Program
Log.Logger.Information("Early app starting..."); Log.Logger.Information("Early app starting...");
AppDomain.CurrentDomain.FirstChanceException -= OnFirstChanceException; AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException;
AppDomain.CurrentDomain.UnhandledException -= OnAppDomainUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException;
TaskScheduler.UnobservedTaskException -= OnTaskSchedulerUnobservedTaskException; TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedTaskException;
try try
{ {
BuildAvaloniaApp() BuildAvaloniaApp()

View File

@@ -11,6 +11,7 @@ public class DotnetDriver : IConsoleDriver
public int ThreadId { get; set; } public int ThreadId { get; set; }
public void EnterRestrictedMode() => CheckThreadId = true; public void EnterRestrictedMode() => CheckThreadId = true;
public void ExitRestrictedMode() => CheckThreadId = false;
public virtual bool Init() public virtual bool Init()
{ {

View File

@@ -24,4 +24,5 @@ public interface IConsoleDriver
Size GetWindowSize(); Size GetWindowSize();
void Clear(); void Clear();
void EnterRestrictedMode(); void EnterRestrictedMode();
void ExitRestrictedMode();
} }

View File

@@ -0,0 +1,94 @@
using GeneralInputKey;
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models;
using TerminalUI.Traits;
namespace TerminalUI.Controls;
public partial class Button<T> : ContentView<Button<T>, T>, IFocusable
{
private record RenderState(IColor? Foreground, IColor? Background);
private Position? _cursorRenderPosition;
private RenderState? _lastRenderState;
[Notify] private Position? _cursorPosition;
private List<Action<Button<T>>> _clickHandlers = new();
public Button()
{
RerenderProperties.Add(nameof(CursorPosition));
}
public void Focus()
=> ApplicationContext?.FocusManager.SetFocus(this);
public void UnFocus()
=> ApplicationContext?.FocusManager.UnFocus(this);
public void SetCursorPosition(IConsoleDriver consoleDriver)
{
if (_cursorRenderPosition is null) return;
consoleDriver.SetCursorPosition(_cursorRenderPosition.Value);
}
protected override Size CalculateSize()
{
if (Content is null || !Content.IsVisible) return new Size(0, 0);
return Content.GetRequestedSize();
}
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{
if (ContentRendererMethod is null)
{
throw new NullReferenceException(
nameof(ContentRendererMethod)
+ " is null, cannot render content of "
+ Content?.GetType().Name
+ " with DataContext of "
+ DataContext?.GetType().Name);
}
var backgroundColor = Background ?? renderContext.Background;
var foregroundColor = Foreground ?? renderContext.Foreground;
var renderState = new RenderState(foregroundColor, backgroundColor);
var forceRerender = !renderContext.ForceRerender && !NeedsRerender(renderState);
_lastRenderState = renderState;
var childRenderContext = renderContext with
{
Background = backgroundColor,
Foreground = foregroundColor,
ForceRerender = forceRerender
};
_cursorRenderPosition = position;
return ContentRendererMethod(childRenderContext, position, size);
}
private bool NeedsRerender(RenderState renderState) => renderState != _lastRenderState;
public override void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
{
if (keyEventArgs.Key != Keys.Enter) return;
keyEventArgs.Handled = true;
foreach (var clickHandler in _clickHandlers)
{
clickHandler(this);
}
}
public Button<T> WithClickHandler(Action<Button<T>> handler)
{
_clickHandlers.Add(handler);
return this;
}
}

View File

@@ -125,8 +125,7 @@ public sealed partial class ItemsControl<TDataContext, TItem>
{ {
if (!child.IsVisible) continue; if (!child.IsVisible) continue;
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found"); if (!_requestedSizes.TryGetValue(child, out var childSize)) continue;
var childPosition = Orientation == Orientation.Vertical var childPosition = Orientation == Orientation.Vertical
? position with {Y = position.Y + delta} ? position with {Y = position.Y + delta}

View File

@@ -8,6 +8,7 @@ public class EventLoop : IEventLoop
private readonly ILogger<EventLoop> _logger; private readonly ILogger<EventLoop> _logger;
private readonly List<Action> _initializers = new(); private readonly List<Action> _initializers = new();
private readonly List<Action> _permanentQueue = new(); private readonly List<Action> _permanentQueue = new();
private readonly List<Action> _finalizers = new();
public int ThreadId { get; set; } = -1; public int ThreadId { get; set; } = -1;
@@ -21,6 +22,7 @@ public class EventLoop : IEventLoop
public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action); public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action);
public void AddInitializer(Action action) => _initializers.Add(action); public void AddInitializer(Action action) => _initializers.Add(action);
public void AddFinalizer(Action action) => _finalizers.Add(action);
public void Run() public void Run()
{ {
@@ -35,6 +37,10 @@ public class EventLoop : IEventLoop
ProcessQueues(); ProcessQueues();
Thread.Sleep(10); Thread.Sleep(10);
} }
foreach (var finalizer in _finalizers)
{
finalizer();
}
ThreadId = -1; ThreadId = -1;
} }

View File

@@ -6,4 +6,5 @@ public interface IEventLoop
void AddToPermanentQueue(Action action); void AddToPermanentQueue(Action action);
void AddInitializer(Action action); void AddInitializer(Action action);
int ThreadId { get; set; } int ThreadId { get; set; }
void AddFinalizer(Action action);
} }

View File

@@ -33,6 +33,10 @@ public class RenderEngine : IRenderEngine
_applicationContext.ConsoleDriver.ThreadId = _eventLoop.ThreadId; _applicationContext.ConsoleDriver.ThreadId = _eventLoop.ThreadId;
_applicationContext.ConsoleDriver.EnterRestrictedMode(); _applicationContext.ConsoleDriver.EnterRestrictedMode();
}); });
_eventLoop.AddFinalizer(() =>
{
_applicationContext.ConsoleDriver.ExitRestrictedMode();
});
} }
public void RequestRerender(IView view) => RequestRerender(); public void RequestRerender(IView view) => RequestRerender();