Add command line.

This commit is contained in:
Kolosov Alexandr 2023-08-29 09:40:28 +05:00
parent 440fd4d5d4
commit ee490fb84e
18 changed files with 294 additions and 180 deletions

View File

@ -5,7 +5,7 @@ namespace TUI.Controls;
public class Copyright : IControl<string>
{
public void Render(string author, Position position)
public void Render(string author, Position position, int? height = 0)
{
const string icon = "© ";
Console.SetCursorPosition(position.Left - icon.Width(), position.Top);

View File

@ -6,39 +6,42 @@ namespace TUI.Controls;
public class Dashboard : IControl<string>
{
public void Render(string title, Position position)
public bool IsFocused { get; set; }
public void Render(string title, Position position, int? height = 0)
{
Console.SetCursorPosition(position.Left, position.Top);
RenderTopLine(title);
RenderTopLine(title, IsFocused);
var marginTop = Theme.BorderWidth + Theme.Padding + position.Top;
var dashboardHeight = Console.WindowHeight - Theme.BorderWidth;
var marginTop = position.Top;
for (var top = marginTop; top < dashboardHeight; top++)
{
RenderMiddleLine();
}
var dashboardHeight = height == 0 ? Console.WindowHeight - marginTop : height + Theme.Padding * 2;
RenderBottomLine();
for (var top = marginTop;
top < dashboardHeight + marginTop - Theme.BorderWidth * 2 - Theme.Padding * 2;
top++)
RenderMiddleLine(IsFocused);
RenderBottomLine(IsFocused);
}
private static void RenderMiddleLine()
private static void RenderMiddleLine(bool isFocused)
{
Console.Write("│".Primary());
Console.Write("│".Primary(isFocused));
Console.Write(new string(' ', Console.WindowWidth - Theme.BorderWidth * 2));
Console.WriteLine("│".Primary());
Console.WriteLine("│".Primary(isFocused));
}
private static void RenderBottomLine()
private static void RenderBottomLine(bool isFocused)
{
var lineWidth = Console.WindowWidth - Theme.BorderWidth * 2;
Console.Write("└".Primary());
Console.Write('─'.Repeat(lineWidth).Primary());
Console.WriteLine("┘".Primary());
Console.Write("└".Primary(isFocused));
Console.Write('─'.Repeat(lineWidth).Primary(isFocused));
Console.WriteLine("┘".Primary(isFocused));
}
private static void RenderTopLine(string title)
private static void RenderTopLine(string title, bool isFocused)
{
var lineWidth =
(Console.WindowWidth - title.Width() - Theme.BorderWidth * 2 - Theme.Padding * 2) /
@ -48,13 +51,10 @@ public class Dashboard : IControl<string>
topLine.Append("┌");
topLine.Append('─'.Repeat(lineWidth));
topLine.AppendFormat("{0}{1}{0}", ' '.Repeat(Theme.Padding), title);
if (title.Width() % 2 == 1)
{
topLine.Append('─');
}
if (title.Width() % 2 == 1) topLine.Append('─');
topLine.Append('─'.Repeat(lineWidth));
topLine.Append("┐");
Console.WriteLine(topLine.ToString().Primary());
Console.WriteLine(topLine.ToString().Primary(isFocused));
}
}

View File

@ -7,33 +7,43 @@ namespace TUI.Controls;
public class Display
{
private bool _headerInDisplay = true;
public readonly Header Header;
public readonly CommandLine CommandLine;
public readonly Copyright Copyright;
public readonly DependencyDashboard DependencyDashboard;
public readonly Header Header;
private bool _commandLineInDisplay;
private Project _currentProject;
private bool _headerInDisplay = true;
public Display()
{
Header = new Header();
Copyright = new Copyright();
CommandLine = new CommandLine();
DependencyDashboard = new DependencyDashboard();
Render();
}
public string FocusedElement { get; set; } = "";
public void OpenDeps(Project project)
{
_currentProject = project;
var dashboardPosition = new Position(0, Header.Height);
DependencyDashboard.IsFocused = true;
CommandLine.IsFocused = false;
DependencyDashboard.Render(_currentProject, dashboardPosition);
}
private void ResizeDependencies(bool full)
private void ResizeDependencies()
{
var dashboardPosition = new Position(0, full ? 0 : Header.Height);
var topPosition = 0;
topPosition += _commandLineInDisplay ? CommandLine.Height : 0;
topPosition += _headerInDisplay ? Header.Height : 0;
var dashboardPosition = new Position(0, topPosition);
DependencyDashboard.Render(_currentProject, dashboardPosition);
}
@ -58,7 +68,7 @@ public class Display
Header.Render(headerPosition);
}
ResizeDependencies(!_headerInDisplay);
ResizeDependencies();
}
public void Next()
@ -70,4 +80,18 @@ public class Display
{
DependencyDashboard.Previous();
}
public void OpenCommandLine()
{
var commandLinePosition = new Position(0, _headerInDisplay ? Header.Height : 0);
CommandLine.IsFocused = true;
DependencyDashboard.IsFocused = false;
FocusedElement = nameof(CommandLine);
CommandLine.Render(commandLinePosition);
_commandLineInDisplay = true;
ResizeDependencies();
Console.SetCursorPosition(commandLinePosition.Left + Theme.Padding + Theme.BorderWidth + 2,
commandLinePosition.Top + Theme.BorderWidth);
Console.CursorVisible = true;
}
}

View File

@ -7,5 +7,7 @@ public interface IControl
public interface IControl<in TProps>
{
void Render(TProps props, Position position);
// bool IsFocused { get; }
void Render(TProps props, Position position, int? height);
}

View File

@ -11,23 +11,20 @@ public record TableProps(IEnumerable<string> HeaderCells,
public class Table : IControl<TableProps>
{
private readonly Dictionary<int, string> _rows = new();
private Position _position;
private int _selectedRowId;
public void Render(TableProps props, Position position)
public void Render(TableProps props, Position position, int? height = 0)
{
_position = position;
Console.SetCursorPosition(_position.Left, _position.Top);
Console.Write(' '.Repeat(props.TitleWidth));
foreach (var headerCell in props.HeaderCells)
{
Console.Write(' '.Repeat(props.ColumnWidth - headerCell.Width()) + headerCell);
}
}
private readonly Dictionary<int, string> _rows = new();
public void RenderRow(int rowId, string rowText, string? bgColor = default)
{
var padRight = ' '.Repeat(Console.WindowWidth - rowText.Width() - Theme.BorderWidth * 2);
@ -39,10 +36,7 @@ public class Table : IControl<TableProps>
public void Next()
{
if (_selectedRowId >= _rows.Count)
{
return;
}
if (_selectedRowId >= _rows.Count) return;
RemoveHoverFromCurrentRow();
RenderRow(++_selectedRowId, _rows[_selectedRowId], Palette.HoverColor);
@ -50,23 +44,14 @@ public class Table : IControl<TableProps>
private void RemoveHoverFromCurrentRow()
{
if (_rows.TryGetValue(_selectedRowId, out var row))
{
RenderRow(_selectedRowId, row);
}
if (_rows.TryGetValue(_selectedRowId, out var row)) RenderRow(_selectedRowId, row);
}
public void Previous()
{
if (_selectedRowId == 0)
{
Next();
}
if (_selectedRowId == 0) Next();
if (_selectedRowId == 1)
{
return;
}
if (_selectedRowId == 1) return;
RemoveHoverFromCurrentRow();
RenderRow(--_selectedRowId, _rows[_selectedRowId], Palette.HoverColor);

View File

@ -15,13 +15,20 @@ public class DependencyDashboard : IControl<Project>
private const int TitleWidth = 25;
private const int ColumnWidth = 10;
private readonly static Dictionary<string, Package> Packages = new();
private readonly Dashboard _dashboard = new();
private readonly Table _table = new();
public void Render(Project project, Position position)
public bool IsFocused
{
var dashboard = new Dashboard();
dashboard.Render(project.Icon, position);
get => _dashboard.IsFocused;
set => _dashboard.IsFocused = value;
}
public void Render(Project project, Position position, int? height = 0)
{
_dashboard.Render(project.Icon + " Dependencies", position);
var header = project.Dependencies.Select(GetConventionVersion).ToArray();
var rows = project.Sources.Select(GetTitle).ToArray();
@ -77,10 +84,7 @@ public class DependencyDashboard : IControl<Project>
{
var currentVersion = package.ParseVersion(dependency.Name);
if (currentVersion == null)
{
return Icons.NotFound;
}
if (currentVersion == null) return Icons.NotFound;
var conventionVersion = dependency.Version?.ToVersion();
return PaintingVersion(currentVersion, conventionVersion);
@ -90,30 +94,20 @@ public class DependencyDashboard : IControl<Project>
{
var textVersion = current.ToString();
if (current > convention)
{
return textVersion.Info();
}
if (current > convention) return textVersion.Info();
if (current < convention)
{
return current.Major == convention.Major ? textVersion.Primary() : textVersion.Warning();
}
return textVersion.Hint();
}
private readonly static Dictionary<string, Package> Packages = new();
private static Package DownloadPackage(SourceDto sourceDto)
{
if (Packages.TryGetValue(sourceDto.Repo, out var downloadPackage))
{
return downloadPackage;
}
var endpoint = sourceDto.Tags.Have("gitlab") ? GetGitlabEndpoint(sourceDto) : sourceDto.Repo;
if (Packages.TryGetValue(endpoint, out var downloadPackage)) return downloadPackage;
using HttpClient client = new();
var endpoint = sourceDto.Tags.Have("gitlab") ? GetGitlabEndpoint(sourceDto) : sourceDto.Repo;
var json = client.GetStringAsync(endpoint).GetAwaiter().GetResult();
var package = JsonSerializer.Deserialize<Package>(json);
Packages.Add(endpoint, package);
@ -135,10 +129,7 @@ public class DependencyDashboard : IControl<Project>
private static string RenderCurrentVersion(string version)
{
var versionWidth = version.Width();
if (versionWidth == 0)
{
return ' '.Repeat(ColumnWidth - 1) + Icons.NotFound.Hint();
}
if (versionWidth == 0) return ' '.Repeat(ColumnWidth - 1) + Icons.NotFound.Hint();
return ' '.Repeat(ColumnWidth - versionWidth) + version;
}
@ -188,20 +179,21 @@ public class DependencyDashboard : IControl<Project>
private static string GetApplicationType(SourceDto sourceDto)
{
foreach (var application in Icons.Applications)
{
if (sourceDto.Tags.Have(application.Value))
return application.Key;
}
return Icons.Undefined;
}
private static string GetGitApplication(SourceDto sourceDto) => sourceDto.Repo switch
private static string GetGitApplication(SourceDto sourceDto)
{
{ } url when url.Contains("gitlab") => Icons.GitLab,
{ } url when url.Contains("github") => Icons.GitHub,
_ => Icons.Git
};
return sourceDto.Repo switch
{
{ } url when url.Contains("gitlab") => Icons.GitLab,
{ } url when url.Contains("github") => Icons.GitHub,
_ => Icons.Git
};
}
public void Next()
{

View File

@ -0,0 +1,53 @@
󰘳 1 󱓟 releases  select prev
󰘳 2 󰧑 competencies  select next
󰘳 3  dependencies 󰬌 toggle head
󰘳 4 󰬘 quit
󰎔 too new  Auth  package
 so good 󰞉 WWW 󰡨 image
 be nice 󰚩 SEO  site
󰬟 too old  VCS  api
󰦖 wait 󰲽 build 󱓞 release
󱔢 reopen 󱞈 testing 󰶯 hotfix
󱞇 in progress 󰦕 done  publish
󱞇 review 󱄊 cancel
  releases 󱓟  Fact  2023
  releases 󱓟  Fact
  releases 󱓟  Planned
  releases 󱓟  Planned  2024-12-31 xfirm
  competencies 󰧑
  growth zone 󰶼
  dependencies 
┌──────────────────────────────────────────────────────────────────────── Planned release ─────────────────────────────────────────────────────────────────┐
│ 2024-12-31 4/11 xfirm [############# ] //green - done, blue - test, pink review/build, orange WIP
│ 2024-12-31 xfirm 4/11 [###############.......] //green - done, blue - test, pink review/build
│ 2024-12-31 xfirm 4/11 [############..........] //green - done, blue - test, pink review/build
│ 2024-12-31 xfirm 4/11 [############..........] //green - done, blue - test, pink review/build
│ 2024-12-31 xfirm 4/11 [############..........] //green - done, blue - test, pink review/build
│ 2024-12-31 xfirm 4/11 [############..........] //green - done, blue - test, pink review/build
│ 2024-12-31 xfirm 4/11 [############..........] //green - done, blue - test, pink review/build
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────── checkperson-site ────────────────────────────────────────────────────────────────┐
│ 󰨑 󰦖  ELK-3628 Create new menu. 󰵮 Petrov A. 󰙨 Ivanov I.
│ 󰨑 󰦕  XF-12 Change input hover. 󰵮 Ivanov I. 󰙨 Petrov A., Petrov B.
│ 󰨑 󱞇  ELK-3628 Crete new project with menu, profile, issues and # 󰵮 Ivanov I. 󰙨 Petrov A., Petrov B.
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────── Actual releases 2023 ─────────────────────────────────────────────────────────┐
│ Jan Feb Mar Apl May Jun Jul Aug Sep Oct Nov Dec
│ ├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ LK 󱓞 󱓞  󱓞 󱓞  󱓞 󱓞  󱓞 󱓞  󱓞 󱓞  󱓞 󱓞  󱓞 󱓞 󱓞  󱓞 󱓞  󱓞 󱓞  󱓞 󱓞  󱓞 󱓞
│ CP 󱓞  󱓞  󱓞  󱓞    󱓞  󱓞  󱓞  󱓞  󱓞  󱓞  󱓞
│ RR 󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯  󰶯 󰶯
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@ -17,25 +17,16 @@ public class Package
public Version? ParseVersion(string? dependencyName)
{
if (dependencyName == null)
{
return null;
}
if (dependencyName == null) return null;
JsonNode? version = null;
var lowerDependencyName = dependencyName.ToLower();
Dependencies?.TryGetPropertyValue(lowerDependencyName, out version);
if (version == null)
{
Engines?.TryGetPropertyValue(lowerDependencyName, out version);
}
if (version == null)
{
DevDependencies?.TryGetPropertyValue(lowerDependencyName, out version);
}
if (version == null) Engines?.TryGetPropertyValue(lowerDependencyName, out version);
if (version == null) DevDependencies?.TryGetPropertyValue(lowerDependencyName, out version);
return version?.GetValue<string>().ToVersion();
}

View File

@ -7,6 +7,9 @@ namespace TUI.Domain;
[YamlSerializable]
public class Settings
{
[YamlMember]
public Project[] Projects { get; set; }
public static Settings Init()
{
var deserializer = new DeserializerBuilder()
@ -16,7 +19,4 @@ public class Settings
using var sr = new StreamReader("settings.yaml");
return deserializer.Deserialize<Settings>(sr.ReadToEnd());
}
[YamlMember]
public Project[] Projects { get; set; }
}

View File

@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.RegularExpressions;
@ -17,18 +18,15 @@ public static class Extensions
public static string RemoveColors(this string text)
{
return Regex.Replace(text, @"\S\[(\d{0,3}[;m][_]?){0,5}", "");
return Regex.Replace(text, @"\S\[(\d{0,3}[;m]_?){0,5}", "");
}
public static int Width(this string text)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
if (string.IsNullOrEmpty(text)) return 0;
var clearText = text.RemoveColors();
var stringInfo = new System.Globalization.StringInfo(clearText);
var stringInfo = new StringInfo(clearText);
return stringInfo.LengthInTextElements;
}

View File

@ -1,5 +1,5 @@
using TUI.Controls;
using Settings = TUI.Domain.Settings;
using TUI.Domain;
Console.Clear();
@ -10,23 +10,53 @@ var settings = Settings.Init();
var display = new Display();
display.OpenDeps(settings.Projects[0]);
var hotKey = ConsoleKey.NoName;
var key = new ConsoleKeyInfo('1', ConsoleKey.NoName, false, false, false);
var waitCommand = true;
do
{
switch (hotKey)
if (key.Key == ConsoleKey.Q && !display.CommandLine.IsFocused)
{
case ConsoleKey.DownArrow:
display.Next();
break;
case ConsoleKey.UpArrow:
display.Previous();
break;
case ConsoleKey.E:
display.ToggleHeader();
break;
waitCommand = false;
continue;
}
hotKey = Console.ReadKey(intercept: true).Key;
} while (hotKey != ConsoleKey.Q);
if (display.CommandLine.IsFocused)
{
switch (key.Key)
{
case ConsoleKey.Escape:
display.CommandLine.IsFocused = false;
break;
default:
Console.Write(key.KeyChar);
break;
}
}
else
{
switch (key.KeyChar)
{
case ':':
display.OpenCommandLine();
break;
}
switch (key.Key)
{
case ConsoleKey.DownArrow:
display.Next();
break;
case ConsoleKey.UpArrow:
display.Previous();
break;
case ConsoleKey.E:
display.ToggleHeader();
break;
}
}
key = Console.ReadKey(true);
} while (waitCommand);
Console.Clear();

View File

@ -13,7 +13,7 @@ public class DependencyDto
[DataMember]
[YamlMember]
public string? Name { get; set; }
[DataMember]
[YamlMember]
public string? Icon

View File

@ -1,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyVersion>0.1.0</AssemblyVersion>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyVersion>0.1.0</AssemblyVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Pastel" Version="4.1.0" />
<PackageReference Include="YamlDotNet" Version="13.1.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Pastel" Version="4.1.0"/>
<PackageReference Include="YamlDotNet" Version="13.1.1"/>
</ItemGroup>
<ItemGroup>
<None Update="settings.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="settings.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using TUI.Controls;
namespace TUI.UserInterface;
public class CommandLine : Dashboard
{
public const int Height = 3;
public void Render(Position position)
{
base.Render("Command", position, Height);
Console.SetCursorPosition(position.Left + Theme.BorderWidth + Theme.Padding, position.Top + Theme.BorderWidth);
Console.Write(">");
}
}

View File

@ -9,15 +9,6 @@ public class Header : IControl
public const int Height = 6;
public const int MaxHeaderBlocksWidth = 16;
private readonly Dictionary<string, string> _hotKeys = new()
{
{ "", "select prev" },
{ "", "select next" },
{ "󰬌", "toggle head" },
{ "󰬘", "quit" },
};
private readonly Dictionary<string, string> _hints = new()
{
{ "󰎔", "too new".Info() },
@ -26,22 +17,28 @@ public class Header : IControl
{ "󰬟", "too old".Warning() }
};
private readonly Dictionary<string, string> _hotKeys = new()
{
{ "", "select prev" },
{ "", "select next" },
{ "󰬌", "toggle head" },
{ "󰬘", "quit" }
};
private readonly Dictionary<string, string> _tags = new()
{
{ Icons.Auth, "Auth" },
{ Icons.NetworkPublic, "WWW" },
{ Icons.SEO, "SEO" },
{ Icons.GitLab, "VCS" },
{ Icons.GitLab, "VCS" }
};
public void Render(Position position)
{
Console.SetCursorPosition(position.Left, position.Top);
for (var i = 1; i <= Height; i++)
{
Console.WriteLine(new string(' ', Console.WindowWidth - LogoWidth));
}
for (var i = 1; i <= Height; i++) Console.WriteLine(new string(' ', Console.WindowWidth - LogoWidth));
RenderBlock(0, _hints);
RenderBlock(1, _tags);

View File

@ -5,6 +5,14 @@ namespace TUI.UserInterface;
public static class Icons
{
public readonly static Dictionary<string, string> Applications = new()
{
{ NpmPackage, "package" },
{ DockerImage, "image" },
{ Site, "site" },
{ Api, "api" }
};
public static string GitLab => GetIcon("", "E24329");
public static string GitHub => GetIcon("", "ADBAC7");
public static string Git => GetIcon("", "F14E32");
@ -19,14 +27,8 @@ public static class Icons
public static string Auth => GetIcon("", "FFD700");
public static string NotFound => GetIcon("");
public readonly static Dictionary<string, string> Applications = new()
private static string GetIcon(string icon, string? activeColor = null)
{
{ NpmPackage, "package" },
{ DockerImage, "image" },
{ Site, "site" },
{ Api, "api" },
};
private static string GetIcon(string icon, string? activeColor = null) =>
icon.Pastel(activeColor ?? Palette.HintColor);
return icon.Pastel(activeColor ?? Palette.HintColor);
}
}

View File

@ -12,10 +12,35 @@ public static class Palette
public const string WarningColor = "EC9706";
public const string InfoColor = "25799F";
public static string Primary(this string currentText) => currentText.Pastel(PrimaryColor);
public static string Hint(this string currentText) => currentText.Pastel(HintColor);
public static string Disable(this string currentText) => currentText.RemoveColors().Pastel(HintColor);
public static string Warning(this string currentText) => currentText.Pastel(WarningColor);
public static string Error(this string currentText) => currentText.Pastel(ErrorColor);
public static string Info(this string currentText) => currentText.Pastel(InfoColor);
public static string Primary(this string currentText, bool isFocused = true)
{
return isFocused
? currentText.Pastel(PrimaryColor)
: Hint(currentText);
}
public static string Hint(this string currentText)
{
return currentText.Pastel(HintColor);
}
public static string Disable(this string currentText)
{
return currentText.RemoveColors().Pastel(HintColor);
}
public static string Warning(this string currentText)
{
return currentText.Pastel(WarningColor);
}
public static string Error(this string currentText)
{
return currentText.Pastel(ErrorColor);
}
public static string Info(this string currentText)
{
return currentText.Pastel(InfoColor);
}
}

View File

@ -11,6 +11,9 @@ public static class Panel
private const int TagCount = 5;
private const int TagWidth = 2;
private static int _marginTop;
public static void RenderRows(SourceDto[] sources, int selectedRowNumber)
{
for (var index = 0; index < sources.Length; index++)
@ -28,20 +31,18 @@ public static class Panel
}
for (var index = 0; index < sources.Length; index++)
{
Console.SetCursorPosition(TitleWidth,
6 + index + _marginTop + BorderWidth + Theme.Padding);
// var source = sources[index];
// var package = DownloadPackage(source);
// var resultText = package.Dependencies.React;
// resultText = new string(' ', ColumnWidth - resultText.Width()) + resultText;
// if (selectedRowNumber == index + 1)
// {
// resultText = resultText.PastelBg("292928");
// }
//
// Console.Write(resultText);
}
// var source = sources[index];
// var package = DownloadPackage(source);
// var resultText = package.Dependencies.React;
// resultText = new string(' ', ColumnWidth - resultText.Width()) + resultText;
// if (selectedRowNumber == index + 1)
// {
// resultText = resultText.PastelBg("292928");
// }
//
// Console.Write(resultText);
// for (var index = 0; index < sources.Length; index++)
// {
// var loading = true;
@ -74,9 +75,6 @@ public static class Panel
// }
}
private static int _marginTop;
// private static Package DownloadPackage(Source source)
// {
// if (Packages.TryGetValue(source.Repo, out var downloadPackage))