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 bool DisableUtf8 { get; set; }
public string? AdminModeIcon { get; set; }
public string? ClipboardSingleIcon { 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.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
<ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TerminalUI;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models;
namespace FileTime.ConsoleUI.App;
@@ -139,6 +140,14 @@ public class App : IApplication
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();
}

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()
{
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>
{
Margin = 5,
@@ -88,52 +101,53 @@ public class Dialogs
Background = SpecialColor.None,
Content = new Grid<IRootViewModel>
{
RowDefinitionsObject = "Auto Auto",
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,
d => d.DialogService.ReadInput.Value != null,
v => v.IsVisible);
d => d.DialogService.LastMessageBox.Value != null,
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();
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();
}
okButton.Focus();
}
};
return root;
}
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",
ChildInitializer =
@@ -150,8 +164,17 @@ public class Dialogs
))
.WithExtension(new GridPositionExtension(0, 1))
}
}
};
root.Bind(
root,
d => d.DialogService.ReadInput.Value != null,
v => v.IsVisible);
return root;
}
private IView<IPreviewElement> ReadInputPreviewItemTemplate()
{
var grid = new Grid<IPreviewElement>
@@ -367,6 +390,64 @@ public class Dialogs
_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;
}
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",
Extensions = {new GridPositionExtension(2, 0)},
Orientation = Orientation.Horizontal,
ChildInitializer =
{
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 ??
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "C" : "\ud83d\udccb"),
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "C " : "\ud83d\udccb"),
AsciiOnly = false
}.Setup(t => t.Bind(
t,
@@ -152,7 +162,7 @@ public class MainWindow
new TextBlock<IRootViewModel>
{
Text = _consoleApplicationConfiguration.Value.ClipboardMultipleIcon ??
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "CC" : "\ud83d\udccb+"),
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "CC " : "\ud83d\udccb+"),
AsciiOnly = false
}.Setup(t => t.Bind(
t,

View File

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

View File

@@ -16,7 +16,12 @@ public class CustomLoggerSink : ILogEventSink
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();
if (logEvent.Exception is not null)
@@ -25,4 +30,5 @@ public class CustomLoggerSink : ILogEventSink
_dialogService.Value.ShowToastMessage(message);
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,4 +24,5 @@ public interface IConsoleDriver
Size GetWindowSize();
void Clear();
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 (!_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
? position with {Y = position.Y + delta}

View File

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

View File

@@ -6,4 +6,5 @@ public interface IEventLoop
void AddToPermanentQueue(Action action);
void AddInitializer(Action action);
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.EnterRestrictedMode();
});
_eventLoop.AddFinalizer(() =>
{
_applicationContext.ConsoleDriver.ExitRestrictedMode();
});
}
public void RequestRerender(IView view) => RequestRerender();