diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..21bd91c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/FileTime.ConsoleUI/bin/Debug/net6.0/FileTime.ConsoleUI.dll", + "args": [], + "cwd": "${workspaceFolder}/src/FileTime.ConsoleUI", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d4fa6c2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,59 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish singlefile linux", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj", + "-p:PublishSingleFile=true", + "-c", + "Release", + "-r", + "linux-x64", + "--self-contained", + "true" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/FileTime.App.Core/Clipboard/Clipboard.cs b/src/FileTime.App.Core/Clipboard/Clipboard.cs new file mode 100644 index 0000000..bad4665 --- /dev/null +++ b/src/FileTime.App.Core/Clipboard/Clipboard.cs @@ -0,0 +1,50 @@ +using FileTime.Core.Command; +using FileTime.Core.Providers; + +namespace FileTime.App.Core.Clipboard +{ + public class Clipboard : IClipboard + { + private readonly List _content; + public IReadOnlyList Content { get; } + public Type? CommandType { get; private set; } + + public Clipboard() + { + _content = new List(); + Content = _content.AsReadOnly(); + } + + public void AddContent(IContentProvider contentProvider, string path) + { + foreach (var content in _content) + { + if (content.ContentProvider == contentProvider && content.Path == path) return; + } + + _content.Add(new ClipboardItem(contentProvider, path)); + } + + public void RemoveContent(IContentProvider contentProvider, string path) + { + for (var i = 0; i < _content.Count; i++) + { + if (_content[i].ContentProvider == contentProvider && _content[i].Path == path) + { + _content.RemoveAt(i--); + } + } + } + + public void Clear() + { + _content.Clear(); + CommandType = null; + } + + public void SetCommand() where T : ITransportationCommand + { + CommandType = typeof(T); + } + } +} \ No newline at end of file diff --git a/src/FileTime.App.Core/Clipboard/ClipboardItem.cs b/src/FileTime.App.Core/Clipboard/ClipboardItem.cs new file mode 100644 index 0000000..0ddd07a --- /dev/null +++ b/src/FileTime.App.Core/Clipboard/ClipboardItem.cs @@ -0,0 +1,17 @@ +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.App.Core.Clipboard +{ + public class ClipboardItem : IAbsolutePath + { + public IContentProvider ContentProvider { get; } + public string Path { get; } + + public ClipboardItem(IContentProvider contentProvider, string path) + { + ContentProvider = contentProvider; + Path = path; + } + } +} \ No newline at end of file diff --git a/src/FileTime.App.Core/Clipboard/IClipboard.cs b/src/FileTime.App.Core/Clipboard/IClipboard.cs new file mode 100644 index 0000000..96b09ee --- /dev/null +++ b/src/FileTime.App.Core/Clipboard/IClipboard.cs @@ -0,0 +1,16 @@ +using FileTime.Core.Command; +using FileTime.Core.Providers; + +namespace FileTime.App.Core.Clipboard +{ + public interface IClipboard + { + IReadOnlyList Content { get; } + Type? CommandType { get; } + + void AddContent(IContentProvider contentProvider, string path); + void Clear(); + void RemoveContent(IContentProvider contentProvider, string path); + void SetCommand() where T : ITransportationCommand; + } +} \ No newline at end of file diff --git a/src/FileTime.App.Core/FileTime.App.Core.csproj b/src/FileTime.App.Core/FileTime.App.Core.csproj new file mode 100644 index 0000000..9a2d8d9 --- /dev/null +++ b/src/FileTime.App.Core/FileTime.App.Core.csproj @@ -0,0 +1,13 @@ + + + + + + + + net6.0 + enable + enable + + + diff --git a/src/FileTime.App.Core/Pane/PaneItem.cs b/src/FileTime.App.Core/Pane/PaneItem.cs new file mode 100644 index 0000000..85de585 --- /dev/null +++ b/src/FileTime.App.Core/Pane/PaneItem.cs @@ -0,0 +1,17 @@ +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.App.Core.Pane +{ + public class PaneItem : IAbsolutePath + { + public IContentProvider ContentProvider { get; } + public string Path { get; } + + public PaneItem(IContentProvider contentProvider, string path) + { + ContentProvider = contentProvider; + Path = path; + } + } +} \ No newline at end of file diff --git a/src/FileTime.App.Core/Pane/PaneState.cs b/src/FileTime.App.Core/Pane/PaneState.cs new file mode 100644 index 0000000..37c3e95 --- /dev/null +++ b/src/FileTime.App.Core/Pane/PaneState.cs @@ -0,0 +1,73 @@ +using System.Collections.ObjectModel; +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.App.Core.Pane +{ + public class PaneState + { + private readonly Dictionary> _selectedItems; + private readonly Dictionary> _selectedItemsReadOnly; + public IReadOnlyDictionary> SelectedItems { get; } + + public FileTime.Core.Components.Pane Pane { get; } + + public PaneState(FileTime.Core.Components.Pane pane) + { + Pane = pane; + + _selectedItems = new Dictionary>(); + _selectedItemsReadOnly = new Dictionary>(); + SelectedItems = new ReadOnlyDictionary>(_selectedItemsReadOnly); + } + + public void AddSelectedItem(IContentProvider contentProvider, IContainer container, string path) + { + if (!_selectedItems.ContainsKey(container)) + { + var val = new List(); + _selectedItems.Add(container, val); + _selectedItemsReadOnly.Add(container, val.AsReadOnly()); + } + + foreach (var content in _selectedItems[container]) + { + if (content.ContentProvider == contentProvider && content.Path == path) return; + } + + _selectedItems[container].Add(new PaneItem(contentProvider, path)); + } + + public void RemoveSelectedItem(IContentProvider contentProvider, IContainer container, string path) + { + if (_selectedItems.ContainsKey(container)) + { + var selectedItems = _selectedItems[container]; + for (var i = 0; i < selectedItems.Count; i++) + { + if (selectedItems[i].ContentProvider == contentProvider && selectedItems[i].Path == path) + { + selectedItems.RemoveAt(i--); + } + } + } + } + + public bool ContainsSelectedItem(IContentProvider contentProvider, IContainer container, string path) + { + if (!_selectedItems.ContainsKey(container)) return false; + + foreach (var content in _selectedItems[container]) + { + if (content.ContentProvider == contentProvider && content.Path == path) return true; + } + + return false; + } + + public IReadOnlyList GetCurrentSelectedItems() => + SelectedItems.ContainsKey(Pane.CurrentLocation) + ? SelectedItems[Pane.CurrentLocation] + : new List().AsReadOnly(); + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/Application.CommandHandlers.cs b/src/FileTime.ConsoleUI.App/Application.CommandHandlers.cs new file mode 100644 index 0000000..925b5ce --- /dev/null +++ b/src/FileTime.ConsoleUI.App/Application.CommandHandlers.cs @@ -0,0 +1,254 @@ +using FileTime.ConsoleUI.App.UI.Color; +using FileTime.Core.Command; +using FileTime.Core.Models; + +namespace FileTime.ConsoleUI.App +{ + public partial class Application + { + private void ClosePane() + { + var currentPaneIndex = _panes.IndexOf(_selectedPane!); + RemovePane(_selectedPane!); + + if (_panes.Count > 0) + { + _selectedPane = _panes[currentPaneIndex == 0 ? 0 : currentPaneIndex - 1]; + } + else + { + _selectedPane = null; + IsRunning = false; + } + } + + private void MoveCursorUp() => _selectedPane!.SelectPreviousItem(); + private void MoveCursorDown() => _selectedPane!.SelectNextItem(); + private void GoUp() => _selectedPane!.GoUp(); + private void Open() => _selectedPane!.Open(); + + private void MoveCursorUpPage() => _selectedPane!.SelectPreviousItem(_renderers[_selectedPane].PageSize); + private void MoveCursorDownPage() => _selectedPane!.SelectNextItem(_renderers[_selectedPane].PageSize); + private void MoveCursorToTop() => _selectedPane!.SelectFirstItem(); + private void MoveCursorToBottom() => _selectedPane!.SelectLastItem(); + + private void ToggleHidden() + { + const string hiddenFilterName = "filter_showhiddenelements"; + + IContainer containerToOpen = _selectedPane!.CurrentLocation; + + if (_selectedPane.CurrentLocation is VirtualContainer oldVirtualContainer) + { + containerToOpen = oldVirtualContainer.HasWithName(hiddenFilterName) + ? oldVirtualContainer.ExceptWithName(hiddenFilterName) + : GenerateHiddenFilterVirtualContainer(_selectedPane.CurrentLocation); + } + else + { + containerToOpen = GenerateHiddenFilterVirtualContainer(_selectedPane.CurrentLocation); + } + + _selectedPane.OpenContainer(containerToOpen); + + static VirtualContainer GenerateHiddenFilterVirtualContainer(IContainer container) + { + return new VirtualContainer( + container, + new List, IEnumerable>>() + { + container => container.Where(c => !c.IsHidden) + }, + new List, IEnumerable>>() + { + element => element.Where(e => !e.IsHidden) + }, + true, + true, + hiddenFilterName + ); + } + } + + public void Select() + { + if (_selectedPane!.CurrentSelectedItem != null) + { + var currentSelectedItem = _selectedPane.CurrentSelectedItem; + if (_paneStates[_selectedPane].ContainsSelectedItem(currentSelectedItem.Provider, _selectedPane.CurrentLocation, currentSelectedItem.FullName!)) + { + _paneStates[_selectedPane].RemoveSelectedItem(currentSelectedItem.Provider, _selectedPane.CurrentLocation, currentSelectedItem.FullName!); + } + else + { + _paneStates[_selectedPane].AddSelectedItem(currentSelectedItem.Provider, _selectedPane.CurrentLocation, currentSelectedItem.FullName!); + } + + _selectedPane.SelectNextItem(); + } + } + + public void Copy() + { + _clipboard.Clear(); + _clipboard.SetCommand(); + + if (_paneStates[_selectedPane!].GetCurrentSelectedItems().Count > 0) + { + foreach (var selectedItem in _paneStates[_selectedPane!].GetCurrentSelectedItems()) + { + _clipboard.AddContent(selectedItem.ContentProvider, selectedItem.Path); + } + } + else + { + _clipboard.AddContent(_selectedPane!.CurrentSelectedItem!.Provider, _selectedPane.CurrentSelectedItem.FullName!); + } + } + + public void Cut() + { + _clipboard.Clear(); + _clipboard.SetCommand(); + } + + public void PasteMerge() + { + Paste(TransportMode.Merge); + } + public void PasteOverwrite() + { + Paste(TransportMode.Overwrite); + } + + public void PasteSkip() + { + Paste(TransportMode.Skip); + } + + private void Paste(TransportMode transportMode) + { + if (_clipboard.CommandType != null) + { + var command = (ITransportationCommand)Activator.CreateInstance(_clipboard.CommandType!)!; + command.TransportMode = transportMode; + + command.Sources.Clear(); + + foreach (var item in _clipboard.Content) + { + command.Sources.Add(item); + } + + command.Target = _selectedPane.CurrentLocation is VirtualContainer virtualContainer + ? virtualContainer.BaseContainer + : _selectedPane.CurrentLocation; + + _commandExecutor.ExecuteCommand(command); + + _clipboard.Clear(); + } + } + + private void CreateContainer() + { + if (_selectedPane?.CurrentLocation != null) + { + _coloredConsoleRenderer.ResetColor(); + MoveToIOLine(2); + _coloredConsoleRenderer.Write("New container name: "); + var newContainerName = _consoleReader.ReadText(validator: Validator); + + if (!string.IsNullOrWhiteSpace(newContainerName)) + { + _selectedPane.CurrentLocation.CreateContainer(newContainerName); + } + } + + void Validator(string newPath) + { + if (_selectedPane!.CurrentLocation.IsExists(newPath)) + { + _coloredConsoleRenderer.ForegroundColor = AnsiColor.From8bit(1); + } + else + { + _coloredConsoleRenderer.ResetColor(); + } + } + } + + private void HardDelete() + { + IList itemsToDelete = null; + + if (_paneStates[_selectedPane!].GetCurrentSelectedItems().Count > 0) + { + var delete = true; + + if (_paneStates[_selectedPane!].GetCurrentSelectedItems().Count == 1 + && _paneStates[_selectedPane!].GetCurrentSelectedItems()[0] is IContainer container + && container.Items.Count > 0) + { + delete = AskForApprove($"The container '{container.Name}' is not empty."); + } + + if (delete) + { + itemsToDelete = _paneStates[_selectedPane].GetCurrentSelectedItems().Cast().ToList(); + } + } + else if (_selectedPane?.CurrentSelectedItem != null) + { + bool delete = true; + if (_selectedPane?.CurrentSelectedItem is IContainer container && container.Items.Count > 0) + { + delete = AskForApprove($"The container '{container.Name}' is not empty."); + } + + if (delete) + { + itemsToDelete = new List() + { + new AbsolutePath(_selectedPane.CurrentSelectedItem.Provider, _selectedPane.CurrentSelectedItem.FullName!) + }; + } + } + + if (itemsToDelete != null) + { + var deleteCommand = new DeleteCommand(); + + foreach (var itemToDelete in itemsToDelete) + { + deleteCommand.ItemsToDelete.Add(itemToDelete); + } + + _commandExecutor.ExecuteCommand(deleteCommand); + _clipboard.Clear(); + } + + bool AskForApprove(string name) + { + MoveToIOLine(2); + _coloredConsoleRenderer.Write(name + " Proceed to delete? (Y/N)"); + + while (true) + { + var key = Console.ReadKey(true); + + if (key.Key == ConsoleKey.Y) + { + break; + } + else if (key.Key == ConsoleKey.N) + { + return false; + } + } + + return true; + } + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/Application.cs b/src/FileTime.ConsoleUI.App/Application.cs new file mode 100644 index 0000000..d6d5db0 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/Application.cs @@ -0,0 +1,271 @@ +using FileTime.Core.Models; +using FileTime.ConsoleUI.App.UI; +using FileTime.ConsoleUI.App.Command; +using FileTime.Core.Components; +using FileTime.Core.Extensions; +using FileTime.App.Core.Clipboard; + +using Microsoft.Extensions.DependencyInjection; +using FileTime.App.Core.Pane; +using FileTime.ConsoleUI.App.UI.Color; +using FileTime.Core.Command; + +namespace FileTime.ConsoleUI.App +{ + public partial class Application + { + private readonly List _panes = new(); + private readonly Dictionary _renderers = new(); + private readonly Dictionary _paneStates = new(); + private Pane? _selectedPane; + + private readonly List _commandBindings = new(); + private readonly IServiceProvider _serviceProvider; + private readonly IClipboard _clipboard; + private readonly IColoredConsoleRenderer _coloredConsoleRenderer; + private readonly CommandExecutor _commandExecutor; + private readonly ConsoleReader _consoleReader; + private readonly List _previousKeys = new(); + + public bool IsRunning { get; private set; } = true; + + public Application( + IServiceProvider serviceProvider, + IClipboard clipboard, + IColoredConsoleRenderer coloredConsoleRenderer, + CommandExecutor commandExecutor, + ConsoleReader consoleReader) + { + _serviceProvider = serviceProvider; + _clipboard = clipboard; + _coloredConsoleRenderer = coloredConsoleRenderer; + _commandExecutor = commandExecutor; + _consoleReader = consoleReader; + InitCommandBindings(); + } + + public void SetContainer(IContainer currentPath) + { + _selectedPane = CreatePane(currentPath); + } + + private Pane CreatePane(IContainer container) + { + var pane = new Pane(container); + _panes.Add(pane); + + var paneState = new PaneState(pane); + _paneStates.Add(pane, paneState); + + var renderer = _serviceProvider.GetService()!; + renderer.Init(pane, paneState); + _renderers.Add(pane, renderer); + + return pane; + } + + private void RemovePane(Pane pane) + { + _panes.Remove(pane); + _renderers.Remove(pane); + _paneStates.Remove(pane); + } + + private void InitCommandBindings() + { + var commandBindings = new List() + { + new CommandBinding("close pane", Commands.ClosePane, new[] { new ConsoleKeyInfo('q', ConsoleKey.Q, false, false, false) }, ClosePane), + new CommandBinding("cursor up", Commands.MoveCursorUp, new[] { new ConsoleKeyInfo('↑', ConsoleKey.UpArrow, false, false, false) }, MoveCursorUp), + new CommandBinding("cursor down", Commands.MoveCursorDown, new[] { new ConsoleKeyInfo('↓', ConsoleKey.DownArrow, false, false, false) }, MoveCursorDown), + new CommandBinding("cursor page up", Commands.MoveCursorUpPage, new[] { new ConsoleKeyInfo(' ', ConsoleKey.PageUp, false, false, false) }, MoveCursorUpPage), + new CommandBinding("cursor page down", Commands.MoveCursorDownPage, new[] { new ConsoleKeyInfo(' ', ConsoleKey.PageDown, false, false, false) }, MoveCursorDownPage), + new CommandBinding("go up", Commands.GoUp, new[] { new ConsoleKeyInfo('←', ConsoleKey.LeftArrow, false, false, false) }, GoUp), + new CommandBinding("open", Commands.Open, new[] { new ConsoleKeyInfo('→', ConsoleKey.RightArrow, false, false, false) }, Open), + new CommandBinding( + "go to top", + Commands.GoToTop, + new[] + { + new ConsoleKeyInfo('g', ConsoleKey.G, false, false, false), + new ConsoleKeyInfo('g', ConsoleKey.G, false, false, false) + }, + MoveCursorToTop), + new CommandBinding( + "go to bottom", + Commands.GoToBottom, + new[] + { + new ConsoleKeyInfo('G', ConsoleKey.G, true, false, false) + }, + MoveCursorToBottom), + new CommandBinding( + "toggle hidden", + Commands.ToggleHidden, + new[] + { + new ConsoleKeyInfo('z', ConsoleKey.Z, false, false, false), + new ConsoleKeyInfo('h', ConsoleKey.H, false, false, false) + }, + ToggleHidden), + new CommandBinding("select", Commands.Select, new[] { new ConsoleKeyInfo(' ', ConsoleKey.Spacebar, false, false, false) }, Select), + new CommandBinding( + "copy", + Commands.Copy, + new[] + { + new ConsoleKeyInfo('y', ConsoleKey.Y, false, false, false), + new ConsoleKeyInfo('y', ConsoleKey.Y, false, false, false) + }, + Copy), + new CommandBinding( + "cut", + Commands.Cut, + new[] + { + new ConsoleKeyInfo('d', ConsoleKey.D, false, false, false), + new ConsoleKeyInfo('d', ConsoleKey.D, false, false, false) + }, + Cut), + new CommandBinding( + "paste (merge)", + Commands.Paste, + new[] + { + new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false), + new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false) + }, + PasteMerge), + new CommandBinding( + "paste (overwrite)", + Commands.Paste, + new[] + { + new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false), + new ConsoleKeyInfo('o', ConsoleKey.O, false, false, false) + }, + PasteOverwrite), + new CommandBinding( + "paste (skip)", + Commands.Paste, + new[] + { + new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false), + new ConsoleKeyInfo('s', ConsoleKey.S, false, false, false) + }, + PasteSkip), + new CommandBinding( + "create container", + Commands.CreateContainer, + new[] + { + new ConsoleKeyInfo('c', ConsoleKey.C, false, false, false), + new ConsoleKeyInfo('c', ConsoleKey.C, false, false, false) + }, + CreateContainer), + new CommandBinding( + "delete", + Commands.CreateContainer, + new[] + { + new ConsoleKeyInfo('d', ConsoleKey.D, false, false, false), + new ConsoleKeyInfo('D', ConsoleKey.D, true, false, false) + }, + HardDelete), + }; + + _commandBindings.AddRange(commandBindings); + } + + public void PrintUI() + { + if (_selectedPane != null) + { + _renderers[_selectedPane].PrintUI(); + } + } + + public bool ProcessKey(ConsoleKeyInfo keyinfo) + { + var key = keyinfo.Key; + _previousKeys.Add(keyinfo); + + CommandBinding? selectedCommandBinding = _commandBindings.Find(c => AreKeysEqual(c.Keys, _previousKeys)); + + if (keyinfo.Key == ConsoleKey.Escape) + { + _previousKeys.Clear(); + return true; + } + else if (_previousKeys.Count == 2 && selectedCommandBinding == null) + { + HandleNoCommandFound(); + return false; + } + else if (selectedCommandBinding != null) + { + selectedCommandBinding.Invoke(); + _previousKeys.Clear(); + } + else + { + Console.ResetColor(); + int commandToDisplay = 0; + var possibleCommands = _commandBindings.Where(c => AreKeysEqual(c.Keys[0], keyinfo)).ToList(); + + if (possibleCommands.Count == 0) + { + HandleNoCommandFound(); + } + else + { + foreach (var commandBinding in possibleCommands) + { + Console.SetCursorPosition(10, Console.WindowHeight - 1 - possibleCommands.Count + commandToDisplay++); + _coloredConsoleRenderer.Write( + $"{{0,-{Console.WindowWidth - 10}}}", + string.Concat(commandBinding.Keys.Select(k => DisplayKey(k).Map(s => s.Length > 1 ? $" {s} " : s))) + ": " + commandBinding.Name + ); + } + } + return false; + } + return true; + + void HandleNoCommandFound() + { + Console.SetCursorPosition(10, Console.WindowHeight - 2); + _coloredConsoleRenderer.Write( + $"{{0,-{Console.WindowWidth - 10}}}", + "No command found for key(s): " + string.Format("{0,-20}", string.Concat(_previousKeys.Select(k => DisplayKey(k).Map(s => s.Length > 1 ? $" {s} " : s)))) + ); + _previousKeys.Clear(); + } + } + + private static string DisplayKey(ConsoleKeyInfo keyInfo) => + string.IsNullOrWhiteSpace(keyInfo.KeyChar.ToString()) || keyInfo.KeyChar == '\0' + ? keyInfo.Key.ToString() + : keyInfo.KeyChar.ToString(); + + private static bool AreKeysEqual(IReadOnlyList collection1, IReadOnlyList collection2) + { + if (collection1.Count != collection2.Count) return false; + + for (var i = 0; i < collection1.Count; i++) + { + if (!AreKeysEqual(collection1[i], collection2[i])) return false; + } + + return true; + } + + private static bool AreKeysEqual(ConsoleKeyInfo keyInfo1, ConsoleKeyInfo keyInfo2) => + keyInfo1.Key == keyInfo2.Key && keyInfo1.Modifiers == keyInfo2.Modifiers; + + private static void MoveToIOLine(int left = 0) + { + Console.SetCursorPosition(left, Console.WindowHeight - 2); + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/Command/CommandBinding.cs b/src/FileTime.ConsoleUI.App/Command/CommandBinding.cs new file mode 100644 index 0000000..f97e533 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/Command/CommandBinding.cs @@ -0,0 +1,22 @@ +namespace FileTime.ConsoleUI.App.Command +{ + public class CommandBinding + { + private readonly Action _commandHandler; + + public string Name { get; } + + public ConsoleKeyInfo[] Keys { get; } + public Commands Command { get; } + + public CommandBinding(string name, Commands command, ConsoleKeyInfo[] keys, Action commandHandler) + { + Name = name; + Command = command; + Keys = keys; + _commandHandler = commandHandler; + } + + public void Invoke() => _commandHandler(); + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/Command/Commands.cs b/src/FileTime.ConsoleUI.App/Command/Commands.cs new file mode 100644 index 0000000..b28dc4c --- /dev/null +++ b/src/FileTime.ConsoleUI.App/Command/Commands.cs @@ -0,0 +1,22 @@ +namespace FileTime.ConsoleUI.App.Command +{ + public enum Commands + { + ClosePane, + Copy, + Cut, + GoUp, + MoveCursorDown, + MoveCursorUp, + Open, + Paste, + Select, + ToggleHidden, + CreateContainer, + CreateElement, + MoveCursorUpPage, + MoveCursorDownPage, + GoToTop, + GoToBottom, + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj b/src/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj new file mode 100644 index 0000000..61abf8d --- /dev/null +++ b/src/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + net6.0 + enable + enable + + \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Color/AnsiColor.cs b/src/FileTime.ConsoleUI.App/UI/Color/AnsiColor.cs new file mode 100644 index 0000000..802a00a --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Color/AnsiColor.cs @@ -0,0 +1,25 @@ +namespace FileTime.ConsoleUI.App.UI.Color +{ + public class AnsiColor : IConsoleColor + { + public string? Color { get; } + public string? Prefix { get; } + public string? Postfix { get; } + public AnsiColor() { } + + public AnsiColor(string color, string preFix = "5;", string postFix = "m") + { + Color = color; + Prefix = preFix; + Postfix = postFix; + } + + public static AnsiColor FromRgb(int r, int g, int b) => new($"{r};{g};{b}", "5;", "m"); + public static AnsiColor From8bit(byte color) => new($"{color}", "5;", "m"); + + public override string? ToString() + { + return Color == null ? null : Prefix + Color + Postfix; + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Color/BasicColor.cs b/src/FileTime.ConsoleUI.App/UI/Color/BasicColor.cs new file mode 100644 index 0000000..b5e667a --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Color/BasicColor.cs @@ -0,0 +1,14 @@ +namespace FileTime.ConsoleUI.App.UI.Color +{ + public class BasicColor : IConsoleColor + { + public BasicColor() { } + + public BasicColor(ConsoleColor color) + { + Color = color; + } + + public ConsoleColor? Color { get; } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Color/ColoredConsoleRenderer.cs b/src/FileTime.ConsoleUI.App/UI/Color/ColoredConsoleRenderer.cs new file mode 100644 index 0000000..66e7fd9 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Color/ColoredConsoleRenderer.cs @@ -0,0 +1,78 @@ +namespace FileTime.ConsoleUI.App.UI.Color +{ + public class ColoredConsoleRenderer : IColoredConsoleRenderer + { + private readonly IStyles _styles; + + public IConsoleColor? BackgroundColor { get; set; } + public IConsoleColor? ForegroundColor { get; set; } + + public ColoredConsoleRenderer(IStyles styles) + { + _styles = styles; + BackgroundColor = _styles.DefaultBackground; + ForegroundColor = _styles.DefaultForeground; + } + + public void Write(string text) => DoWrite(text); + public void Write(char c) => DoWrite(c.ToString()); + public void Write(string format, params object[] param) => DoWrite(string.Format(format, param)); + + private void DoWrite(string text) + { + if (BackgroundColor is AnsiColor ansiBackground && ForegroundColor is AnsiColor ansiForeground) + { + var formatting = "\u001b[0m"; + + if (ansiBackground.Color != null) + { + formatting += "\u001b[48;" + ansiBackground.ToString(); + } + + if (ansiForeground.Color != null) + { + formatting += "\u001b[38;" + ansiForeground.ToString(); + } + + Console.Write(formatting + text); + } + else if (BackgroundColor is BasicColor basicBackground && ForegroundColor is BasicColor basicForeground) + { + Console.BackgroundColor = basicBackground.Color ?? Console.BackgroundColor; + Console.ForegroundColor = basicForeground.Color ?? Console.ForegroundColor; + + Console.Write(text); + } + else if (BackgroundColor == null && ForegroundColor == null) + { + Console.Write(text); + } + else if (BackgroundColor == null || ForegroundColor == null) + { + throw new Exception($"Either both of {nameof(BackgroundColor)} and {nameof(ForegroundColor)} must be null or neither of them."); + } + else if (BackgroundColor.GetType() != ForegroundColor.GetType()) + { + throw new Exception($"Type of {nameof(BackgroundColor)} and {nameof(ForegroundColor)} must be the same."); + } + else + { + throw new Exception($"Unsupported color type: {BackgroundColor.GetType()}"); + } + } + + public void ResetColor() + { + BackgroundColor = _styles.DefaultBackground; + ForegroundColor = _styles.DefaultForeground; + + Console.ResetColor(); + } + + public void Clear() + { + Console.SetCursorPosition(0, 0); + Write(new string(' ', Console.WindowHeight * Console.WindowWidth)); + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Color/IColoredConsoleRenderer.cs b/src/FileTime.ConsoleUI.App/UI/Color/IColoredConsoleRenderer.cs new file mode 100644 index 0000000..69fcf97 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Color/IColoredConsoleRenderer.cs @@ -0,0 +1,14 @@ +namespace FileTime.ConsoleUI.App.UI.Color +{ + public interface IColoredConsoleRenderer + { + IConsoleColor? BackgroundColor { get; set; } + IConsoleColor? ForegroundColor { get; set; } + + void Clear(); + void ResetColor(); + void Write(string text); + void Write(char c); + void Write(string format, params object[] param); + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Color/IConsoleColor.cs b/src/FileTime.ConsoleUI.App/UI/Color/IConsoleColor.cs new file mode 100644 index 0000000..5330e20 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Color/IConsoleColor.cs @@ -0,0 +1,4 @@ +namespace FileTime.ConsoleUI.App.UI.Color +{ + public interface IConsoleColor { } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/ConsoleReader.cs b/src/FileTime.ConsoleUI.App/UI/ConsoleReader.cs new file mode 100644 index 0000000..46cc49c --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/ConsoleReader.cs @@ -0,0 +1,89 @@ +using FileTime.ConsoleUI.App.UI.Color; + +namespace FileTime.ConsoleUI.App.UI +{ + public class ConsoleReader + { + private readonly IColoredConsoleRenderer _coloredConsoleRenderer; + + public ConsoleReader(IColoredConsoleRenderer coloredConsoleRenderer) + { + _coloredConsoleRenderer = coloredConsoleRenderer; + } + public string ReadText(int? maxLength = null, Action? validator = null) + { + var cursorVisible = false; + try + { + cursorVisible = Console.CursorVisible; + } + catch { } + + Console.CursorVisible = true; + + var currentConsoleLeft = Console.CursorLeft; + var currentConsoleTop = Console.CursorTop; + + maxLength ??= Console.WindowWidth - currentConsoleLeft; + + var input = ""; + var position = 0; + + _coloredConsoleRenderer.Write($"{{0,-{maxLength}}}", input); + + var key = Console.ReadKey(true); + while (key.Key != ConsoleKey.Enter) + { + if (key.Key == ConsoleKey.Escape) + { + input = null; + break; + } + else if (key.Key == ConsoleKey.Backspace) + { + if (position != 0) + { + input = input.Length > 0 + ? input[..(position - 1)] + input[position..] + : input; + + position--; + } + } + else if (key.Key == ConsoleKey.LeftArrow) + { + if (position > 0) + position--; + } + else if (key.Key == ConsoleKey.RightArrow) + { + if (position < input.Length) + position++; + } + else if (key.KeyChar != '\0') + { + var newInput = input[..position] + key.KeyChar; + + if (position < input.Length) + { + newInput += input[position..]; + } + + input = newInput; + + position++; + } + validator?.Invoke(input); + + Console.SetCursorPosition(currentConsoleLeft, currentConsoleTop); + _coloredConsoleRenderer.Write($"{{0,-{maxLength}}}", input); + + Console.SetCursorPosition(currentConsoleLeft + position, currentConsoleTop); + key = Console.ReadKey(); + } + + Console.CursorVisible = cursorVisible; + return input; + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/IStyles.cs b/src/FileTime.ConsoleUI.App/UI/IStyles.cs new file mode 100644 index 0000000..8f179af --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/IStyles.cs @@ -0,0 +1,18 @@ +using FileTime.ConsoleUI.App.UI.Color; + +namespace FileTime.ConsoleUI.App.UI +{ + public interface IStyles + { + IConsoleColor? DefaultBackground { get; } + IConsoleColor? DefaultForeground { get; } + IConsoleColor? ContainerBackground { get; } + IConsoleColor? ContainerForeground { get; } + IConsoleColor? ElementBackground { get; } + IConsoleColor? ElementForeground { get; } + IConsoleColor? ElementSpecialBackground { get; } + IConsoleColor? ElementSpecialForeground { get; } + IConsoleColor? SelectedItemBackground { get; } + IConsoleColor? SelectedItemForeground { get; } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/PrintMode.cs b/src/FileTime.ConsoleUI.App/UI/PrintMode.cs new file mode 100644 index 0000000..1088f18 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/PrintMode.cs @@ -0,0 +1,9 @@ +namespace FileTime.ConsoleUI.UI.App +{ + public enum PrintMode + { + Previous, + Current, + Next + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Render.cs b/src/FileTime.ConsoleUI.App/UI/Render.cs new file mode 100644 index 0000000..93d20ca --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Render.cs @@ -0,0 +1,281 @@ +using FileTime.App.Core.Pane; +using FileTime.ConsoleUI.App.UI.Color; +using FileTime.ConsoleUI.UI.App; +using FileTime.Core.Components; +using FileTime.Core.Extensions; +using FileTime.Core.Models; + +namespace FileTime.ConsoleUI.App.UI +{ + public class Render + { + private const int _contentPaddingTop = 1; + private const int _contentPaddingBottom = 2; + private readonly int _contentRowCount; + private readonly IStyles _appStyle; + private readonly IColoredConsoleRenderer _coloredRenderer; + + private int _currentDisplayStartY; + + private const int ITEMPADDINGLEFT = 1; + private const int ITEMPADDINGRIGHT = 2; + + private readonly string _paddingLeft; + private readonly string _paddingRight; + + public Pane Pane { get; private set; } + public PaneState PaneState { get; private set; } + + public int PageSize => Console.WindowHeight - _contentPaddingTop - _contentPaddingBottom; + public Render(IColoredConsoleRenderer coloredRenderer, IStyles appStyle) + { + _coloredRenderer = coloredRenderer; + _appStyle = appStyle; + + _paddingLeft = new string(' ', ITEMPADDINGLEFT); + _paddingRight = new string(' ', ITEMPADDINGRIGHT); + _contentRowCount = Console.WindowHeight - _contentPaddingTop - _contentPaddingBottom; + } + + public void Init(Pane pane, PaneState paneState) + { + if (pane == null) throw new Exception($"{nameof(pane)} can not be null"); + if (paneState == null) throw new Exception($"{nameof(paneState)} can not be null"); + + Pane = pane; + Pane.CurrentLocationChanged += (o, e) => _currentDisplayStartY = 0; + + PaneState = paneState; + } + + public void PrintUI() + { + if (Pane != null) + { + PrintPrompt(); + PrintPanes(); + } + } + + private void PrintPanes() + { + var previousColumnWidth = (int)Math.Floor(Console.WindowWidth * 0.15) - 1; + var currentColumnWidth = (int)Math.Floor(Console.WindowWidth * 0.4) - 1; + var nextColumnWidth = Console.WindowWidth - currentColumnWidth - previousColumnWidth - 2; + var currentVirtualContainer = Pane!.CurrentLocation as VirtualContainer; + + if (Pane.CurrentLocation.GetParent() is var parentContainer && parentContainer is not null) + { + parentContainer.Refresh(); + + PrintColumn( + currentVirtualContainer != null + ? currentVirtualContainer.CloneVirtualChainFor(parentContainer, v => v.IsTransitive) + : parentContainer, + currentVirtualContainer != null + ? currentVirtualContainer.GetRealContainer() + : Pane.CurrentLocation, + PrintMode.Previous, + 0, + _contentPaddingTop, + previousColumnWidth, + _contentRowCount); + } + else + { + CleanColumn( + 0, + _contentPaddingTop, + previousColumnWidth, + _contentRowCount); + } + + Pane.CurrentLocation.Refresh(); + + CheckAndSetCurrentDisplayStartY(); + PrintColumn( + Pane.CurrentLocation, + Pane.CurrentSelectedItem, + PrintMode.Current, + previousColumnWidth + 1, + _contentPaddingTop, + currentColumnWidth, + _contentRowCount); + + if (Pane.CurrentSelectedItem is IContainer selectedContainer) + { + selectedContainer.Refresh(); + + selectedContainer = currentVirtualContainer != null + ? currentVirtualContainer.CloneVirtualChainFor(selectedContainer, v => v.IsTransitive) + : selectedContainer; + + PrintColumn( + selectedContainer, + selectedContainer.Items.Count > 0 ? selectedContainer.Items[0] : null, + PrintMode.Next, + previousColumnWidth + currentColumnWidth + 2, + _contentPaddingTop, + nextColumnWidth, + _contentRowCount); + } + else + { + CleanColumn( + previousColumnWidth + currentColumnWidth + 2, + _contentPaddingTop, + nextColumnWidth, + _contentRowCount); + } + } + + private void PrintPrompt() + { + Console.SetCursorPosition(0, 0); + _coloredRenderer.ResetColor(); + _coloredRenderer.ForegroundColor = AnsiColor.From8bit(2); + _coloredRenderer.Write(Environment.UserName + "@" + Environment.MachineName); + + _coloredRenderer.ResetColor(); + _coloredRenderer.Write(' '); + + _coloredRenderer.ForegroundColor = AnsiColor.From8bit(4); + var path = Pane!.CurrentLocation.FullName + "/"; + _coloredRenderer.Write(path); + + if (Pane.CurrentSelectedItem?.Name != null) + { + _coloredRenderer.ResetColor(); + _coloredRenderer.Write($"{{0,-{300 - path.Length}}}", Pane.CurrentSelectedItem.Name); + } + } + + private void PrintColumn(IContainer currentContainer, IItem? currentItem, PrintMode printMode, int startX, int startY, int elementWidth, int availableRows) + { + var allItem = currentContainer.Containers.Cast().Concat(currentContainer.Elements).ToList(); + var printedItemsCount = 0; + var currentY = 0; + if (allItem.Count > 0) + { + var currentIndex = allItem.FindIndex(i => i == currentItem); + + var skipElements = printMode switch + { + PrintMode.Previous => (currentIndex - (availableRows / 2)).Map(r => r < 0 ? 0 : r), + PrintMode.Current => _currentDisplayStartY, + PrintMode.Next => 0, + _ => 0 + }; + + var maxTextWidth = elementWidth - ITEMPADDINGLEFT - ITEMPADDINGRIGHT; + + var itemsToPrint = currentContainer.Items.Skip(skipElements).Take(availableRows).ToList(); + printedItemsCount = itemsToPrint.Count; + foreach (var item in itemsToPrint) + { + Console.SetCursorPosition(startX, startY + currentY++); + var namePart = item.Name.Length > maxTextWidth + ? string.Concat(item.Name.AsSpan(0, maxTextWidth - 1), "~") + : item.Name; + + var attributePart = ""; + + var container = item as IContainer; + var element = item as IElement; + + IConsoleColor? backgroundColor = null; + IConsoleColor? foregroundColor = null; + + if (container != null) + { + backgroundColor = _appStyle.ContainerBackground; + foregroundColor = _appStyle.ContainerForeground; + } + else if (element != null) + { + if (element.IsSpecial) + { + backgroundColor = _appStyle.ElementSpecialBackground; + foregroundColor = _appStyle.ElementSpecialForeground; + } + else + { + backgroundColor = _appStyle.ElementBackground; + foregroundColor = _appStyle.ElementForeground; + } + } + + var isSelected = PaneState.ContainsSelectedItem(item.Provider, currentContainer, item.FullName!); + if (isSelected) + { + backgroundColor = _appStyle.SelectedItemBackground; + foregroundColor = _appStyle.SelectedItemForeground; + } + + if (item == currentItem) + { + (backgroundColor, foregroundColor) = (foregroundColor, backgroundColor); + } + + _coloredRenderer.BackgroundColor = backgroundColor; + _coloredRenderer.ForegroundColor = foregroundColor; + + attributePart = container != null ? "" + container.Items.Count : element!.GetPrimaryAttributeText(); + + var text = string.Format($"{{0,-{elementWidth}}}", _paddingLeft + (isSelected ? " " : "") + namePart + _paddingRight); + text = string.Concat(text.AsSpan(0, text.Length - attributePart.Length - 1), " ", attributePart); + + _coloredRenderer.Write(text); + + _coloredRenderer.ResetColor(); + } + } + else + { + _coloredRenderer.BackgroundColor = new BasicColor(ConsoleColor.Red); + _coloredRenderer.ForegroundColor = new BasicColor(ConsoleColor.White); + Console.SetCursorPosition(startX, startY + currentY++); + + _coloredRenderer.Write($"{{0,-{elementWidth}}}", _paddingLeft + "" + _paddingRight); + } + + var padding = new string(' ', elementWidth); + _coloredRenderer.ResetColor(); + for (var i = 0; i < availableRows - printedItemsCount + 1; i++) + { + Console.SetCursorPosition(startX, startY + currentY++); + _coloredRenderer.Write(padding); + } + } + + private void CleanColumn(int startX, int startY, int elementWidth, int availableRows) + { + _coloredRenderer.ResetColor(); + + var currentY = 0; + var placeholder = new string(' ', elementWidth); + for (var i = 0; i < availableRows; i++) + { + Console.SetCursorPosition(startX, startY + currentY++); + _coloredRenderer.Write(placeholder); + } + } + + private void CheckAndSetCurrentDisplayStartY() + { + const int padding = 5; + + while (Pane.CurrentSelectedIndex < _currentDisplayStartY + padding + && _currentDisplayStartY > 0) + { + _currentDisplayStartY--; + } + + while (Pane.CurrentSelectedIndex > _currentDisplayStartY + _contentRowCount - padding + && _currentDisplayStartY < Pane.CurrentLocation.Items.Count - _contentRowCount) + { + _currentDisplayStartY++; + } + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI.App/UI/Styles.cs b/src/FileTime.ConsoleUI.App/UI/Styles.cs new file mode 100644 index 0000000..98c6f41 --- /dev/null +++ b/src/FileTime.ConsoleUI.App/UI/Styles.cs @@ -0,0 +1,39 @@ +using FileTime.ConsoleUI.App.UI.Color; + +namespace FileTime.ConsoleUI.App.UI +{ + public class Styles : IStyles + { + public IConsoleColor? DefaultBackground { get; } + public IConsoleColor? DefaultForeground { get; } + public IConsoleColor? ContainerBackground { get; } + public IConsoleColor? ContainerForeground { get; } + public IConsoleColor? ElementBackground { get; } + public IConsoleColor? ElementForeground { get; } + public IConsoleColor? ElementSpecialBackground { get; } + public IConsoleColor? ElementSpecialForeground { get; } + public IConsoleColor? SelectedItemBackground { get; } + public IConsoleColor? SelectedItemForeground { get; } + + public Styles(bool useAnsiColors) + { + if (useAnsiColors) + { + ContainerForeground = AnsiColor.From8bit(4); + ElementForeground = AnsiColor.From8bit(14); + ElementSpecialForeground = AnsiColor.From8bit(2); + SelectedItemForeground = AnsiColor.From8bit(3); + + DefaultForeground = ElementForeground; + SelectedItemBackground = ElementSpecialBackground = ContainerBackground = DefaultBackground = ElementBackground = AnsiColor.From8bit(0); + } + else + { + ContainerBackground = new BasicColor(Console.BackgroundColor); + ContainerForeground = new BasicColor(ConsoleColor.Blue); + ElementBackground = new BasicColor(Console.BackgroundColor); + ElementForeground = new BasicColor(Console.ForegroundColor); + } + } + } +} \ No newline at end of file diff --git a/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj b/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj new file mode 100644 index 0000000..5278e2e --- /dev/null +++ b/src/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + Exe + net6.0 + enable + enable + + \ No newline at end of file diff --git a/src/FileTime.ConsoleUI/Program.cs b/src/FileTime.ConsoleUI/Program.cs new file mode 100644 index 0000000..b514676 --- /dev/null +++ b/src/FileTime.ConsoleUI/Program.cs @@ -0,0 +1,110 @@ +using FileTime.App.Core.Clipboard; +using FileTime.ConsoleUI.App; +using FileTime.ConsoleUI.App.UI; +using FileTime.ConsoleUI.App.UI.Color; +using FileTime.Core.Command; +using FileTime.Core.Models; +using FileTime.Core.Providers; +using FileTime.Core.StateManagement; +using FileTime.Providers.Local; +using Microsoft.Extensions.DependencyInjection; + +namespace FileTime.ConsoleUI +{ + public static class Program + { + public static void Main() + { + /* Console.Clear(); + for (var x = 0; x < 16; x++) + { + for (var y = 0; y < 16; y++) + { + var i = x * 16 + y; + Console.Write("\u001b[48;5;{0}m{0,4}", i); + Console.ResetColor(); + Console.Write(' '); + } + Console.WriteLine("\n"); + } + return; */ + + /* var colors = new int[][] + { + new int[] {0,43,54}, + new int[] {255,0,0}, + new int[] {0,255,0}, + new int[] {0,0,255}, + }; + + foreach (var color in colors) + { + Console.Write($"\u001b[0m\u001b[48;2;{color[0]};{color[1]};{color[2]}mTESZT "); + Console.WriteLine($"\u001b[0m\u001b[38;2;{color[0]};{color[1]};{color[2]}mTESZT"); + } + + Console.WriteLine("\u001b[0m\u001b[48;5;0;38;5;14mASD"); + return; */ + + var serviceProvider = CreateServiceProvider(); + + var coloredConsoleRenderer = serviceProvider.GetService()!; + var localContentProvider = serviceProvider.GetService()!; + + var currentPossibleDirectory = localContentProvider.GetByPath(Environment.CurrentDirectory.Replace(Path.DirectorySeparatorChar, Constants.SeparatorChar)); + + if (currentPossibleDirectory is IContainer container) + { + coloredConsoleRenderer.Clear(); + Console.CursorVisible = false; + + var app = serviceProvider.GetService()!; + app.SetContainer(container); + app.PrintUI(); + + while (app.IsRunning) + { + if (app.ProcessKey(Console.ReadKey(true))) + { + app.PrintUI(); + } + } + + Console.SetCursorPosition(0, Console.WindowHeight - 1); + + Console.CursorVisible = true; + } + else + { + Console.WriteLine("Current working directory is not a directory???"); + } + } + + private static ServiceProvider CreateServiceProvider() + { + return new ServiceCollection() + .AddSingleton() + .AddSingleton(new Styles(true)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(sp => sp.GetService() ?? throw new Exception($"No {nameof(LocalContentProvider)} instance found")) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddTransient() + .RegisterCommandHandlers() + .BuildServiceProvider(); + } + + private static IServiceCollection RegisterCommandHandlers(this IServiceCollection serviceCollection) + { + foreach (var commandHandler in Startup.GetCommandHandlers()) + { + serviceCollection.AddTransient(typeof(ICommandHandler), commandHandler); + } + + return serviceCollection; + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/CommandExecutor.cs b/src/FileTime.Core/Command/CommandExecutor.cs new file mode 100644 index 0000000..a62dd52 --- /dev/null +++ b/src/FileTime.Core/Command/CommandExecutor.cs @@ -0,0 +1,24 @@ +namespace FileTime.Core.Command +{ + public class CommandExecutor + { + private readonly List _commandHandlers; + + public CommandExecutor(IEnumerable commandHandlers) + { + _commandHandlers = commandHandlers.ToList(); + } + + public void ExecuteCommand(ICommand command) + { + if (command is IExecutableCommand executableCommand) + { + executableCommand.Execute(); + } + else + { + _commandHandlers.Find(c => c.CanHandle(command))?.Execute(command); + } + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/CopyCommand.cs b/src/FileTime.Core/Command/CopyCommand.cs new file mode 100644 index 0000000..a7c24b8 --- /dev/null +++ b/src/FileTime.Core/Command/CopyCommand.cs @@ -0,0 +1,62 @@ +using FileTime.Core.Models; +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command +{ + public class CopyCommand : ITransportationCommand + { + public IList Sources { get; } = new List(); + + public IContainer? Target { get; set; } + + public TransportMode TransportMode { get; set; } = TransportMode.Merge; + + public PointInTime SimulateCommand(PointInTime delta) + { + throw new NotImplementedException(); + } + + public void Execute(Action copy) + { + DoCopy(Sources, Target, TransportMode, copy); + } + + private void DoCopy(IEnumerable sources, IContainer target, TransportMode transportMode, Action copy) + { + foreach (var source in sources) + { + var item = source.ContentProvider.GetByPath(source.Path); + + if (item is IContainer container) + { + var targetContainer = target.Containers.FirstOrDefault(d => d.Name == container.Name) ?? (target.CreateContainer(container.Name)!); + + var childDirectories = container.Containers.Select(d => new AbsolutePath(item.Provider, d.FullName!)); + var childFiles = container.Elements.Select(f => new AbsolutePath(item.Provider, f.FullName!)); + + DoCopy(childDirectories.Concat(childFiles), targetContainer, transportMode, copy); + } + else if (item is IElement element) + { + var targetName = element.Name; + + if (transportMode == TransportMode.Merge) + { + for (var i = 0; target.IsExists(targetName); i++) + { + targetName = element.Name + (i == 0 ? "_" : $"_{i}"); + } + } + else if (transportMode == TransportMode.Skip && target.IsExists(targetName)) + { + continue; + } + + var targetPath = target.FullName + Constants.SeparatorChar + targetName; + + copy(new AbsolutePath(source.ContentProvider, element.FullName!), new AbsolutePath(target.Provider, targetPath)); + } + } + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/CreateContainerCommand.cs b/src/FileTime.Core/Command/CreateContainerCommand.cs new file mode 100644 index 0000000..90a2f48 --- /dev/null +++ b/src/FileTime.Core/Command/CreateContainerCommand.cs @@ -0,0 +1,12 @@ +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command +{ + public class CreateContainerCommand : ICommand + { + public PointInTime SimulateCommand(PointInTime delta) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/CreateElementCommand.cs b/src/FileTime.Core/Command/CreateElementCommand.cs new file mode 100644 index 0000000..e851181 --- /dev/null +++ b/src/FileTime.Core/Command/CreateElementCommand.cs @@ -0,0 +1,12 @@ +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command +{ + public class CreateElementCommand : ICommand + { + public PointInTime SimulateCommand(PointInTime delta) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/DeleteCommand.cs b/src/FileTime.Core/Command/DeleteCommand.cs new file mode 100644 index 0000000..412b3ec --- /dev/null +++ b/src/FileTime.Core/Command/DeleteCommand.cs @@ -0,0 +1,41 @@ +using FileTime.Core.Models; +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command +{ + public class DeleteCommand : IExecutableCommand + { + public IList ItemsToDelete { get; } = new List(); + + public PointInTime SimulateCommand(PointInTime delta) + { + throw new NotImplementedException(); + } + + public void Execute() + { + foreach (var item in ItemsToDelete) + { + DoDelete(item.ContentProvider.GetByPath(item.Path)!); + } + } + + private void DoDelete(IItem item) + { + if (item is IContainer container) + { + foreach (var child in container.Items) + { + DoDelete(child); + child.Delete(); + } + + item.Delete(); + } + else if(item is IElement element) + { + element.Delete(); + } + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/ICommand.cs b/src/FileTime.Core/Command/ICommand.cs new file mode 100644 index 0000000..355fc54 --- /dev/null +++ b/src/FileTime.Core/Command/ICommand.cs @@ -0,0 +1,9 @@ +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command +{ + public interface ICommand + { + PointInTime SimulateCommand(PointInTime moment); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/ICommandHandler.cs b/src/FileTime.Core/Command/ICommandHandler.cs new file mode 100644 index 0000000..74dc6ef --- /dev/null +++ b/src/FileTime.Core/Command/ICommandHandler.cs @@ -0,0 +1,8 @@ +namespace FileTime.Core.Command +{ + public interface ICommandHandler + { + bool CanHandle(object command); + void Execute(object command); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/IExecutableCommand.cs b/src/FileTime.Core/Command/IExecutableCommand.cs new file mode 100644 index 0000000..e9a74bd --- /dev/null +++ b/src/FileTime.Core/Command/IExecutableCommand.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Command +{ + public interface IExecutableCommand : ICommand + { + void Execute(); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/ITransportationCommand.cs b/src/FileTime.Core/Command/ITransportationCommand.cs new file mode 100644 index 0000000..f065c12 --- /dev/null +++ b/src/FileTime.Core/Command/ITransportationCommand.cs @@ -0,0 +1,11 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Command +{ + public interface ITransportationCommand : ICommand + { + IList Sources { get; } + IContainer Target { get; set;} + TransportMode TransportMode { get; set; } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/MoveCommand.cs b/src/FileTime.Core/Command/MoveCommand.cs new file mode 100644 index 0000000..2b6f2a4 --- /dev/null +++ b/src/FileTime.Core/Command/MoveCommand.cs @@ -0,0 +1,18 @@ +using FileTime.Core.Models; +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command +{ + public class MoveCommand : ITransportationCommand + { + public IList Sources { get; } = new List(); + + public IContainer? Target { get; set; } + public TransportMode TransportMode { get; set; } = TransportMode.Merge; + + public PointInTime SimulateCommand(PointInTime delta) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Command/TransportMode.cs b/src/FileTime.Core/Command/TransportMode.cs new file mode 100644 index 0000000..45b11cf --- /dev/null +++ b/src/FileTime.Core/Command/TransportMode.cs @@ -0,0 +1,9 @@ +namespace FileTime.Core.Command +{ + public enum TransportMode + { + Merge, + Overwrite, + Skip + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Components/Pane.cs b/src/FileTime.Core/Components/Pane.cs new file mode 100644 index 0000000..500e9d1 --- /dev/null +++ b/src/FileTime.Core/Components/Pane.cs @@ -0,0 +1,168 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Components +{ + public class Pane + { + private IItem? currentSelectedItem; + private IContainer currentLocation; + + public IContainer CurrentLocation + { + get => currentLocation; + private set + { + if (currentLocation != value) + { + if (currentLocation != null) + { + currentLocation.Refreshed -= HandleCurrentLocationRefresh; + } + + currentLocation = value; + CurrentLocationChanged?.Invoke(this, EventArgs.Empty); + CurrentSelectedItem = CurrentLocation.Items.Count > 0 ? CurrentLocation.Items[0] : null; + currentLocation.Refreshed += HandleCurrentLocationRefresh; + } + } + } + public IItem? CurrentSelectedItem + { + get => currentSelectedItem; + private set + { + if (currentSelectedItem != value && (currentLocation.Items.Contains(value) || value == null)) + { + currentSelectedItem = value; + CurrentSelectedIndex = GetItemIndex(value); + } + } + } + public int CurrentSelectedIndex { get; private set; } + + public event EventHandler CurrentLocationChanged; + + public Pane(IContainer currentPath) + { + CurrentLocation = currentPath; + CurrentSelectedItem = CurrentLocation.Items.Count > 0 ? CurrentLocation.Items[0] : null; + } + + private void HandleCurrentLocationRefresh(object? sender, EventArgs e) + { + var currentSelectedName = CurrentSelectedItem?.FullName; + if (currentSelectedName != null) + { + CurrentSelectedItem = CurrentLocation.Items.FirstOrDefault(i => i.FullName == currentSelectedName) ?? currentLocation.Items.FirstOrDefault(); + } + } + + public void SelectFirstItem() + { + if (CurrentLocation.Items.Count > 0) + { + CurrentSelectedItem = CurrentLocation.Items[0]; + } + } + + public void SelectLastItem() + { + if (CurrentLocation.Items.Count > 0) + { + CurrentSelectedItem = CurrentLocation.Items[CurrentLocation.Items.Count - 1]; + } + } + + public void SelectPreviousItem(int skip = 0) + { + var possibleItemsToSelect = CurrentLocation.Items.Take(CurrentSelectedIndex).Reverse().Skip(skip).ToList(); + + if (possibleItemsToSelect.Count == 0) possibleItemsToSelect = CurrentLocation.Items.ToList(); + SelectItem(possibleItemsToSelect); + } + + public void SelectNextItem(int skip = 0) + { + var possibleItemsToSelect = CurrentLocation.Items.Skip(CurrentSelectedIndex + 1 + skip).ToList(); + + if (possibleItemsToSelect.Count == 0) possibleItemsToSelect = CurrentLocation.Items.Reverse().ToList(); + SelectItem(possibleItemsToSelect); + } + + private void SelectItem(IEnumerable currentPossibleItems) + { + if (!currentPossibleItems.Any()) return; + + if (CurrentSelectedItem != null) + { + CurrentLocation.Refresh(); + + IItem? newSelectedItem = null; + foreach (var item in currentPossibleItems) + { + if (CurrentLocation.Items.FirstOrDefault(i => i.Name == item.Name) is var possibleNewSelectedItem + && possibleNewSelectedItem is not null) + { + newSelectedItem = possibleNewSelectedItem; + break; + } + } + + CurrentSelectedItem = newSelectedItem ?? (CurrentLocation.Items.Count > 0 ? CurrentLocation.Items[0] : null); + } + else + { + CurrentSelectedItem = CurrentLocation.Items.Count > 0 ? CurrentLocation.Items[0] : null; + } + } + + public void GoUp() + { + var lastCurrentLocation = CurrentLocation; + var parent = CurrentLocation.GetParent(); + + if (parent is not null) + { + if (lastCurrentLocation is VirtualContainer lastCurrentVirtualContainer) + { + CurrentLocation = lastCurrentVirtualContainer.CloneVirtualChainFor(parent, v => v.IsPermanent); + CurrentSelectedItem = lastCurrentVirtualContainer.GetRealContainer(); + } + else + { + CurrentLocation = parent; + CurrentSelectedItem = lastCurrentLocation; + } + } + } + + public void Open() + { + if (currentSelectedItem is IContainer childContainer) + { + if (CurrentLocation is VirtualContainer currentVirtuakContainer) + { + CurrentLocation = currentVirtuakContainer.CloneVirtualChainFor(childContainer, v => v.IsPermanent); + } + else + { + CurrentLocation = childContainer; + } + } + } + + public void OpenContainer(IContainer container) => CurrentLocation = container; + + private int GetItemIndex(IItem? item) + { + if (item == null) return -1; + + for (var i = 0; i < CurrentLocation.Items.Count; i++) + { + if (CurrentLocation.Items[i] == item) return i; + } + + return -1; + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Extensions/GeneralExtensions.cs b/src/FileTime.Core/Extensions/GeneralExtensions.cs new file mode 100644 index 0000000..3bdada7 --- /dev/null +++ b/src/FileTime.Core/Extensions/GeneralExtensions.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Extensions +{ + public static class GeneralExtensions + { + public static TResult Map(this T obj, Func map) => map(obj); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/FileTime.Core.csproj b/src/FileTime.Core/FileTime.Core.csproj new file mode 100644 index 0000000..bafd05b --- /dev/null +++ b/src/FileTime.Core/FileTime.Core.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/src/FileTime.Core/Models/AbsolutePath.cs b/src/FileTime.Core/Models/AbsolutePath.cs new file mode 100644 index 0000000..7b6ae2e --- /dev/null +++ b/src/FileTime.Core/Models/AbsolutePath.cs @@ -0,0 +1,17 @@ +using FileTime.Core.Providers; + +namespace FileTime.Core.Models +{ + public class AbsolutePath : IAbsolutePath + { + public IContentProvider ContentProvider { get; } + + public string Path { get; } + + public AbsolutePath(IContentProvider contentProvider, string path) + { + ContentProvider = contentProvider; + Path = path; + } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Models/Constants.cs b/src/FileTime.Core/Models/Constants.cs new file mode 100644 index 0000000..7244ea6 --- /dev/null +++ b/src/FileTime.Core/Models/Constants.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Models +{ + public static class Constants + { + public const char SeparatorChar = '/'; + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Models/IAbsolutePath.cs b/src/FileTime.Core/Models/IAbsolutePath.cs new file mode 100644 index 0000000..fc88672 --- /dev/null +++ b/src/FileTime.Core/Models/IAbsolutePath.cs @@ -0,0 +1,10 @@ +using FileTime.Core.Providers; + +namespace FileTime.Core.Models +{ + public interface IAbsolutePath + { + IContentProvider ContentProvider { get; } + string Path { get; } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Models/IContainer.cs b/src/FileTime.Core/Models/IContainer.cs new file mode 100644 index 0000000..59f42cf --- /dev/null +++ b/src/FileTime.Core/Models/IContainer.cs @@ -0,0 +1,19 @@ +namespace FileTime.Core.Models +{ + public interface IContainer : IItem + { + IReadOnlyList Items { get; } + IReadOnlyList Containers { get; } + IReadOnlyList Elements { get; } + + void Refresh(); + IContainer? GetParent(); + IItem? GetByPath(string path); + IContainer CreateContainer(string name); + IElement CreateElement(string name); + + bool IsExists(string name); + + event EventHandler? Refreshed; + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Models/IElement.cs b/src/FileTime.Core/Models/IElement.cs new file mode 100644 index 0000000..e8fb630 --- /dev/null +++ b/src/FileTime.Core/Models/IElement.cs @@ -0,0 +1,8 @@ +namespace FileTime.Core.Models +{ + public interface IElement : IItem + { + bool IsSpecial { get; } + string GetPrimaryAttributeText(); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Models/IItem.cs b/src/FileTime.Core/Models/IItem.cs new file mode 100644 index 0000000..6c38bfe --- /dev/null +++ b/src/FileTime.Core/Models/IItem.cs @@ -0,0 +1,13 @@ +using FileTime.Core.Providers; + +namespace FileTime.Core.Models +{ + public interface IItem + { + string Name { get; } + string? FullName { get; } + bool IsHidden { get; } + IContentProvider Provider { get; } + void Delete(); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Models/VirtualContainer.cs b/src/FileTime.Core/Models/VirtualContainer.cs new file mode 100644 index 0000000..229f4a3 --- /dev/null +++ b/src/FileTime.Core/Models/VirtualContainer.cs @@ -0,0 +1,122 @@ +using FileTime.Core.Providers; + +namespace FileTime.Core.Models +{ + public class VirtualContainer : IContainer + { + private readonly List, IEnumerable>> _containerTransformators; + private readonly List, IEnumerable>> _elementTransformators; + + public IContainer BaseContainer { get; } + + public bool IsPermanent { get; } + public bool IsTransitive { get; } + public string? VirtualContainerName { get; } + public IReadOnlyList Items { get; private set; } + + public IReadOnlyList Containers { get; private set; } + + public IReadOnlyList Elements { get; private set; } + + public string Name => BaseContainer.Name; + + public string? FullName => BaseContainer.FullName; + + public bool IsHidden => BaseContainer.IsHidden; + + public IContentProvider Provider => BaseContainer.Provider; + + public event EventHandler? Refreshed + { + add => BaseContainer.Refreshed += value; + remove => BaseContainer.Refreshed -= value; + } + + public VirtualContainer( + IContainer baseContainer, + List, IEnumerable>> containerTransformators, + List, IEnumerable>> elementTransformators, + bool isPermanent = false, + bool isTransitive = false, + string? virtualContainerName = null) + { + BaseContainer = baseContainer; + _containerTransformators = containerTransformators; + _elementTransformators = elementTransformators; + + InitItems(); + IsPermanent = isPermanent; + IsTransitive = isTransitive; + VirtualContainerName = virtualContainerName; + } + + private void InitItems() + { + Containers = _containerTransformators.Aggregate(BaseContainer.Containers.AsEnumerable(), (a, t) => t(a)).ToList().AsReadOnly(); + Elements = _elementTransformators.Aggregate(BaseContainer.Elements.AsEnumerable(), (a, t) => t(a)).ToList().AsReadOnly(); + + Items = Containers.Cast().Concat(Elements).ToList().AsReadOnly(); + } + + public IItem? GetByPath(string path) => BaseContainer.GetByPath(path); + + public IContainer? GetParent() => BaseContainer.GetParent(); + + public void Refresh() + { + BaseContainer.Refresh(); + InitItems(); + } + + public IContainer GetRealContainer() => + BaseContainer is VirtualContainer virtualContainer ? virtualContainer.GetRealContainer() : BaseContainer; + + public bool HasWithName(string name) => + VirtualContainerName == name + || (BaseContainer is VirtualContainer virtualContainer + && virtualContainer.HasWithName(name)); + + public IContainer ExceptWithName(string name) + { + if (BaseContainer is VirtualContainer virtualBaseContainer && virtualBaseContainer.VirtualContainerName == name) + { + return new VirtualContainer( + virtualBaseContainer.ExceptWithName(name), + _containerTransformators, + _elementTransformators, + IsPermanent, + IsTransitive, + VirtualContainerName); + } + else if (VirtualContainerName == name) + { + return BaseContainer; + } + + return this; + } + + public IContainer CloneVirtualChainFor(IContainer container, Func predicate) + { + var baseContainer = BaseContainer is VirtualContainer baseVirtualContainer + ? baseVirtualContainer.CloneVirtualChainFor(container, predicate) + : container; + + return predicate(this) + ? new VirtualContainer( + baseContainer, + _containerTransformators, + _elementTransformators, + IsPermanent, + IsTransitive, + VirtualContainerName) + : baseContainer; + } + + public IContainer CreateContainer(string name) => BaseContainer.CreateContainer(name); + public IElement CreateElement(string name) => BaseContainer.CreateElement(name); + public bool IsExists(string name) => BaseContainer.IsExists(name); + + public void Delete() => BaseContainer.Delete(); + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Providers/IContentProvider.cs b/src/FileTime.Core/Providers/IContentProvider.cs new file mode 100644 index 0000000..954ef41 --- /dev/null +++ b/src/FileTime.Core/Providers/IContentProvider.cs @@ -0,0 +1,9 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Providers +{ + public interface IContentProvider : IContainer + { + IReadOnlyList RootContainers { get; } + } +} \ No newline at end of file diff --git a/src/FileTime.Core/StateManagement/ElementCreationStates.cs b/src/FileTime.Core/StateManagement/ElementCreationStates.cs new file mode 100644 index 0000000..b2b6d4b --- /dev/null +++ b/src/FileTime.Core/StateManagement/ElementCreationStates.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.StateManagement +{ + public class ElementCreationStates + { + + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Timeline/ContainerSnapshot.cs b/src/FileTime.Core/Timeline/ContainerSnapshot.cs new file mode 100644 index 0000000..32685e9 --- /dev/null +++ b/src/FileTime.Core/Timeline/ContainerSnapshot.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Timeline +{ + public class ContainerSnapshot + { + + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Timeline/ElementSnapshot.cs b/src/FileTime.Core/Timeline/ElementSnapshot.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/FileTime.Core/Timeline/PointInTime.cs b/src/FileTime.Core/Timeline/PointInTime.cs new file mode 100644 index 0000000..61aacd8 --- /dev/null +++ b/src/FileTime.Core/Timeline/PointInTime.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; +using FileTime.Core.Providers; + +namespace FileTime.Core.Timeline +{ + public class PointInTime + { + private readonly Dictionary snapshots = new(); + + public IReadOnlyDictionary Snapshots => new Lazy>(() => new ReadOnlyDictionary(snapshots)).Value; + } +} \ No newline at end of file diff --git a/src/FileTime.Core/Timeline/RootSnapshot.cs b/src/FileTime.Core/Timeline/RootSnapshot.cs new file mode 100644 index 0000000..46a0dae --- /dev/null +++ b/src/FileTime.Core/Timeline/RootSnapshot.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Timeline +{ + public class RootSnapshot + { + + } +} \ No newline at end of file diff --git a/src/FileTime.sln b/src/FileTime.sln new file mode 100644 index 0000000..d0b752e --- /dev/null +++ b/src/FileTime.sln @@ -0,0 +1,55 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Core", "FileTime.Core\FileTime.Core.csproj", "{F5C58BDC-BDCE-47B8-9371-70DB815E9B0E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI", "FileTime.ConsoleUI\FileTime.ConsoleUI.csproj", "{EC1F7FC4-5600-4953-A85A-534CA43601C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.App", "FileTime.ConsoleUI.App\FileTime.ConsoleUI.App.csproj", "{A88EB44A-EE33-4A91-8C61-B33B31C9DF07}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{517D96CE-A956-4638-A93D-465D34DE22B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Providers.Local", "Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj", "{AAE01ED7-2E8B-40A2-AD0E-95BDA7C99272}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.Core", "FileTime.App.Core\FileTime.App.Core.csproj", "{2C0F630D-FD5D-4554-B8DD-F11BF4EB49C5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F5C58BDC-BDCE-47B8-9371-70DB815E9B0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5C58BDC-BDCE-47B8-9371-70DB815E9B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5C58BDC-BDCE-47B8-9371-70DB815E9B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5C58BDC-BDCE-47B8-9371-70DB815E9B0E}.Release|Any CPU.Build.0 = Release|Any CPU + {EC1F7FC4-5600-4953-A85A-534CA43601C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC1F7FC4-5600-4953-A85A-534CA43601C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC1F7FC4-5600-4953-A85A-534CA43601C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC1F7FC4-5600-4953-A85A-534CA43601C3}.Release|Any CPU.Build.0 = Release|Any CPU + {D9FC4BC8-4E7D-491A-AA67-EABE52CF2999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9FC4BC8-4E7D-491A-AA67-EABE52CF2999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9FC4BC8-4E7D-491A-AA67-EABE52CF2999}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9FC4BC8-4E7D-491A-AA67-EABE52CF2999}.Release|Any CPU.Build.0 = Release|Any CPU + {A88EB44A-EE33-4A91-8C61-B33B31C9DF07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A88EB44A-EE33-4A91-8C61-B33B31C9DF07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A88EB44A-EE33-4A91-8C61-B33B31C9DF07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A88EB44A-EE33-4A91-8C61-B33B31C9DF07}.Release|Any CPU.Build.0 = Release|Any CPU + {AAE01ED7-2E8B-40A2-AD0E-95BDA7C99272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAE01ED7-2E8B-40A2-AD0E-95BDA7C99272}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAE01ED7-2E8B-40A2-AD0E-95BDA7C99272}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAE01ED7-2E8B-40A2-AD0E-95BDA7C99272}.Release|Any CPU.Build.0 = Release|Any CPU + {2C0F630D-FD5D-4554-B8DD-F11BF4EB49C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C0F630D-FD5D-4554-B8DD-F11BF4EB49C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C0F630D-FD5D-4554-B8DD-F11BF4EB49C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C0F630D-FD5D-4554-B8DD-F11BF4EB49C5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {AAE01ED7-2E8B-40A2-AD0E-95BDA7C99272} = {517D96CE-A956-4638-A93D-465D34DE22B1} + EndGlobalSection +EndGlobal diff --git a/src/Providers/FileTime.Providers.Local/CommandHandlers/CopyCommandHandler.cs b/src/Providers/FileTime.Providers.Local/CommandHandlers/CopyCommandHandler.cs new file mode 100644 index 0000000..10eb5b1 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/CommandHandlers/CopyCommandHandler.cs @@ -0,0 +1,58 @@ +using FileTime.Core.Command; +using FileTime.Core.Models; +using FileTime.Core.StateManagement; + +namespace FileTime.Providers.Local.CommandHandlers +{ + public class CopyCommandHandler : ICommandHandler + { + private readonly List _copyOperations = new(); + private readonly ElementCreationStates _elementCreationStates; + + public CopyCommandHandler(ElementCreationStates elementCreationStates) + { + _elementCreationStates = elementCreationStates; + } + + public bool CanHandle(object command) + { + if (command is not CopyCommand copyCommand) return false; + + if (copyCommand.Target != null && copyCommand.Target is not LocalFolder) return false; + + if (copyCommand.Sources.Any(s => s.ContentProvider is not LocalContentProvider)) return false; + + return true; + } + + public void Execute(object command) + { + if (command is not CopyCommand copyCommand) throw new ArgumentException($"Can not execute command of type '{command.GetType()}'."); + + var thread = new Thread(() => copyCommand.Execute(CopyElement)); + thread.Start(); + + _copyOperations.Add(thread); + } + + public void CopyElement(IAbsolutePath sourcePath, IAbsolutePath targetPath) + { + using var sourceStream = File.OpenRead(sourcePath.Path); + using var sourceReader = new BinaryReader(sourceStream); + + using var targetStream = File.OpenWrite(targetPath.Path); + using var targetWriter = new BinaryWriter(targetStream); + + var bufferSize = 1024 * 1024; + byte[] dataRead; + + do + { + dataRead = sourceReader.ReadBytes(bufferSize); + targetWriter.Write(dataRead); + targetWriter.Flush(); + } + while (dataRead.Length > 0); + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/Extensions/FormatExtensions.cs b/src/Providers/FileTime.Providers.Local/Extensions/FormatExtensions.cs new file mode 100644 index 0000000..5bba837 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/Extensions/FormatExtensions.cs @@ -0,0 +1,25 @@ +namespace FileTime.Providers.Local.Extensions +{ + public static class FormatExtensions + { + private const long OneKiloByte = 1024; + private const long OneMegaByte = OneKiloByte * 1024; + private const long OneGigaByte = OneMegaByte * 1024; + private const long OneTerraByte = OneGigaByte * 1024; + + public static string ToSizeString(this long fileSize, int precision = 1) + { + var fileSizeD = (decimal)fileSize; + var (size, suffix) = fileSize switch + { + > OneTerraByte => (fileSizeD / OneTerraByte, "T"), + > OneGigaByte => (fileSizeD / OneGigaByte, "G"), + > OneMegaByte => (fileSizeD / OneMegaByte, "M"), + > OneKiloByte => (fileSizeD / OneKiloByte, "K"), + _ => (fileSizeD, "B") + }; + + return string.Format("{0:N" + precision + "}", size).TrimEnd('0').Replace(',', '.').TrimEnd('.') + " " + suffix; + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj b/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj new file mode 100644 index 0000000..08780b0 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj @@ -0,0 +1,13 @@ + + + + + + + + + net6.0 + enable + enable + + \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs new file mode 100644 index 0000000..960a189 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -0,0 +1,61 @@ +using System.Runtime.InteropServices; +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Providers.Local +{ + public class LocalContentProvider : IContentProvider + { + public IReadOnlyList RootContainers { get; } + + public IReadOnlyList Items => RootContainers; + + public IReadOnlyList Containers => RootContainers; + + public IReadOnlyList Elements { get; } = new List(); + + public string Name { get; } = "local"; + + public string? FullName { get; } + public bool IsHidden => false; + + public IContentProvider Provider => this; + + public event EventHandler? Refreshed; + + public LocalContentProvider() + { + var rootDirectories = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? new DirectoryInfo("/").GetDirectories() + : Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d)); + + FullName = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "" : null; + + RootContainers = rootDirectories.Select(d => new LocalFolder(d, this, this)).OrderBy(d => d.Name).ToList().AsReadOnly(); + } + + public IItem? GetByPath(string path) + { + var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar); + var rootContainer = RootContainers.FirstOrDefault(c => c.Name == pathParts[0]); + + if (rootContainer == null) return null; + + return rootContainer.GetByPath(string.Join(Constants.SeparatorChar, pathParts.Skip(1))); + } + + public void Refresh() + { + } + + public IContainer? GetParent() + { + return null; + } + public IContainer CreateContainer(string name) => throw new NotSupportedException(); + public IElement CreateElement(string name) => throw new NotSupportedException(); + public bool IsExists(string name) => Items.Any(i => i.Name == name); + + public void Delete() => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalFile.cs b/src/Providers/FileTime.Providers.Local/LocalFile.cs new file mode 100644 index 0000000..eeb6f6b --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalFile.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; +using FileTime.Core.Models; +using FileTime.Core.Providers; +using FileTime.Providers.Local.Extensions; +using Mono.Unix; + +namespace FileTime.Providers.Local +{ + public class LocalFile : IElement + { + private readonly FileInfo _file; + + public string Name { get; } + + public string FullName { get; } + + public IContentProvider Provider { get; } + + public bool IsHidden => (_file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + public bool IsSpecial => + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && (new UnixFileInfo(_file.FullName).FileAccessPermissions & FileAccessPermissions.UserExecute) == FileAccessPermissions.UserExecute; + + public LocalFile(FileInfo file, IContentProvider contentProvider) + { + _file = file; + + Name = file.Name; + FullName = file.FullName; + Provider = contentProvider; + } + + public string GetPrimaryAttributeText() => _file.Length.ToSizeString(); + + public void Delete() + { + _file.Delete(); + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalFolder.cs b/src/Providers/FileTime.Providers.Local/LocalFolder.cs new file mode 100644 index 0000000..30fd042 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalFolder.cs @@ -0,0 +1,125 @@ +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Providers.Local +{ + public class LocalFolder : IContainer + { + private IReadOnlyList? _items; + private IReadOnlyList? _containers; + private IReadOnlyList? _elements; + private readonly IContainer? _parent; + + public bool IsHidden => (Directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + public DirectoryInfo Directory { get; } + public IContentProvider Provider { get; } + + public IReadOnlyList Items + { + get + { + if (_items == null) Refresh(); + return _items!; + } + + private set => _items = value; + } + + public IReadOnlyList Containers + { + get + { + if (_containers == null) Refresh(); + return _containers!; + } + + private set => _containers = value; + } + + public IReadOnlyList Elements + { + get + { + if (_elements == null) Refresh(); + return _elements!; + } + + private set => _elements = value; + } + + public string Name { get; } + + public string FullName { get; } + + public event EventHandler? Refreshed; + + public LocalFolder(DirectoryInfo directory, IContentProvider contentProvider, IContainer? parent) + { + Directory = directory; + _parent = parent; + + Name = directory.Name; + FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name; + Provider = contentProvider; + } + + public IContainer? GetParent() => _parent; + + public void Refresh() + { + _containers = new List(); + _elements = new List(); + + try + { + _containers = Directory.GetDirectories().Select(d => new LocalFolder(d, Provider, this)).OrderBy(d => d.Name).ToList().AsReadOnly(); + _elements = Directory.GetFiles().Select(f => new LocalFile(f, Provider)).OrderBy(f => f.Name).ToList().AsReadOnly(); + } + catch { } + + _items = _containers.Cast().Concat(_elements).ToList().AsReadOnly(); + Refreshed?.Invoke(this, EventArgs.Empty); + } + + public IItem? GetByPath(string path) + { + var paths = path.Split(Constants.SeparatorChar); + + var item = Items.FirstOrDefault(i => i.Name == paths[0]); + + if (paths.Length == 1) + { + return item; + } + + if (item is IContainer container) + { + return container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1))); + } + + return null; + } + public IContainer CreateContainer(string name) + { + Directory.CreateSubdirectory(name); + Refresh(); + + return _containers!.FirstOrDefault(c => c.Name == name)!; + } + + public IElement CreateElement(string name) + { + using (File.Create(Path.Combine(Directory.FullName, name))) { } + Refresh(); + + return _elements!.FirstOrDefault(e => e.Name == name)!; + } + + public bool IsExists(string name) => Items.Any(i => i.Name == name); + + public void Delete() + { + Directory.Delete(true); + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/Startup.cs b/src/Providers/FileTime.Providers.Local/Startup.cs new file mode 100644 index 0000000..c20c2b4 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/Startup.cs @@ -0,0 +1,14 @@ +using FileTime.Providers.Local.CommandHandlers; + +namespace FileTime.Providers.Local +{ + public static class Startup + { + public static Type[] GetCommandHandlers() + { + return new Type[]{ + typeof(CopyCommandHandler) + }; + } + } +} \ No newline at end of file