From 27f301fe48625290fcc72e33c5f4bfe268a59287 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Wed, 22 Oct 2025 17:31:32 -0500 Subject: [PATCH] Get way further --- .gitignore | 3 +- .onedev-buildspec.yml | 25 +- App.axaml | 1 + CrossLangDevStudio.csproj | 4 + CrossLangFile.cs | 222 +++++++++- CrossLangShell.cs | 98 ++++- Messages/PublishCloseMessage.cs | 4 + Messages/PublishMessage.cs | 7 + Models/ConsolePackageManger.cs | 72 ++++ Models/TemplatePackageManager.cs | 70 +++ Models/ToolPackageManager.cs | 72 ++++ Models/WebAppPackageManager.cs | 72 ++++ Packaging/Linux/build.sh | 20 +- Packaging/Linux/debian/control-arm64 | 8 + Packaging/Linux/push.sh | 18 + Packaging/Windows/build.sh | 4 +- Packaging/Windows/script.nsh | 2 +- ViewModels/AddProjectReferenceViewModel.cs | 92 ++++ ViewModels/CRVMViewerViewModel.cs | 103 +++++ ViewModels/FileEditorViewModel.cs | 87 +++- ViewModels/ISavable.cs | 5 + ViewModels/MainWindowViewModel.cs | 451 +++++++++++++++++++- ViewModels/NewProjectDialogViewModel.cs | 18 +- ViewModels/PackageManagerViewModel.cs | 4 +- ViewModels/ProjectConfigurationViewModel.cs | 35 +- ViewModels/PublishDialogViewModel.cs | 279 ++++++++++++ Views/AddProjectReferenceView.axaml | 43 ++ Views/AddProjectReferenceView.axaml.cs | 14 + Views/CRVMViewerView.axaml | 75 ++++ Views/CRVMViewerView.axaml.cs | 14 + Views/FileEditorView.axaml | 6 +- Views/FileEditorView.axaml.cs | 17 + Views/MainWindow.axaml | 26 +- Views/MainWindow.axaml.cs | 48 ++- Views/NewProjectDialog.axaml | 16 +- Views/PackageManagerView.axaml | 8 +- Views/ProjectConfigurationView.axaml | 8 +- Views/PublishDialogView.axaml | 79 ++++ Views/PublishDialogView.axaml.cs | 21 + 39 files changed, 2028 insertions(+), 123 deletions(-) create mode 100644 Messages/PublishCloseMessage.cs create mode 100644 Messages/PublishMessage.cs create mode 100644 Models/ConsolePackageManger.cs create mode 100644 Models/TemplatePackageManager.cs create mode 100644 Models/ToolPackageManager.cs create mode 100644 Models/WebAppPackageManager.cs create mode 100644 Packaging/Linux/debian/control-arm64 create mode 100644 ViewModels/AddProjectReferenceViewModel.cs create mode 100644 ViewModels/CRVMViewerViewModel.cs create mode 100644 ViewModels/PublishDialogViewModel.cs create mode 100644 Views/AddProjectReferenceView.axaml create mode 100644 Views/AddProjectReferenceView.axaml.cs create mode 100644 Views/CRVMViewerView.axaml create mode 100644 Views/CRVMViewerView.axaml.cs create mode 100644 Views/PublishDialogView.axaml create mode 100644 Views/PublishDialogView.axaml.cs diff --git a/.gitignore b/.gitignore index de79242..9f78f99 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ bin obj out build -Installing \ No newline at end of file +Installing +artifacts \ No newline at end of file diff --git a/.onedev-buildspec.yml b/.onedev-buildspec.yml index 5f2f5ef..28718b4 100644 --- a/.onedev-buildspec.yml +++ b/.onedev-buildspec.yml @@ -17,13 +17,32 @@ jobs: commands: | apt update -y apt install -y nsis cmake g++-mingw-w64-i686-win32 make + mkdir artifacts cd Packaging/Windows bash build.sh + cd ../Linux + bash build.sh + bash publish.sh + envVars: + - name: GITEA_AUTH + value: '@secret:GITEA_AUTH@' useTTY: true condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL - - !PublishArtifactStep - name: Publish Windows Artifact - artifacts: Packaging/Windows/build/package/CrossLangDevStudio-Installer.exe + - !SCPCommandStep + name: Copy Files + privateKeySecret: TRUENAS_SSH + source: artifacts + target: mike@@10.137.42.30:/mnt/storage24tb/Files/Public/CrossLangDevStudio/@build_number@ + options: -r + condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL + - !SSHCommandStep + name: Link latest + remoteMachine: 10.137.42.30 + userName: mike + privateKeySecret: TRUENAS_SSH + commands: | + cd /mnt/storage24tb/Files/Public/CrossLangDevStudio + ln -s @build_number@ latest condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL triggers: - !BranchUpdateTrigger diff --git a/App.axaml b/App.axaml index b2ca1cf..8c59a5d 100644 --- a/App.axaml +++ b/App.axaml @@ -16,6 +16,7 @@ + diff --git a/CrossLangDevStudio.csproj b/CrossLangDevStudio.csproj index dcf008b..244bc4d 100644 --- a/CrossLangDevStudio.csproj +++ b/CrossLangDevStudio.csproj @@ -16,6 +16,7 @@ + @@ -25,8 +26,11 @@ None All + + + diff --git a/CrossLangFile.cs b/CrossLangFile.cs index 18463ee..3a4d336 100644 --- a/CrossLangFile.cs +++ b/CrossLangFile.cs @@ -16,6 +16,24 @@ using Newtonsoft.Json.Linq; namespace CrossLangDevStudio; +public static class CrossLangFileExtensions +{ + public static void Skip(this Stream strm, long bytes) + { + byte[] data = new byte[1024]; + if (strm.CanSeek) + strm.Position += bytes; + else + { + do + { + int read = strm.Read(data, 0, (int)Math.Min(bytes, data.LongLength)); + if (read == 0) return; + bytes -= read; + } while (bytes != 0); + } + } +} public class CrossLangFile { @@ -68,7 +86,7 @@ public class CrossLangFile return Strings[(int)index]; } - + uint count = ReadInt(); for (uint i = 0; i < count; i++) @@ -77,6 +95,42 @@ public class CrossLangFile uint sectionSize = ReadInt(); switch (section) { + case "FUNS": + { + uint chks = ReadInt(); + for (uint j = 0; j < chks; j++) + { + uint fnParts = ReadInt(); + + string[] parts = new string[fnParts]; + for (uint k = 0; k < fnParts; k++) + { + parts[k] = GetString(); + } + uint fid = ReadInt(); + Functions.Add(new CrossLangFunction(parts, fid)); + } + } + break; + case "CHKS": + { + uint chks = ReadInt(); + for (uint j = 0; j < chks; j++) + { + uint fnParts = ReadInt(); + + string[] parts = new string[fnParts]; + for (uint k = 0; k < fnParts; k++) + { + parts[k] = GetString(); + } + + uint len = ReadInt(); + strm.Skip(len); + Chunks.Add(parts); + } + } + break; case "STRS": { uint theCount = ReadInt(); @@ -109,8 +163,69 @@ public class CrossLangFile Dependencies.Add(new CrossLangDependency() { Name = name, Version = version }); } break; + case "CLSS": + { + uint chks = ReadInt(); + for (uint j = 0; j < chks; j++) + { + string docs = GetString(); + uint namePartLen = ReadInt(); + string[] nameParts = new string[namePartLen]; + for (uint k = 0; k < namePartLen; k++) + { + nameParts[k] = GetString(); + } + string name = string.Join(".", nameParts); + uint inhPartLen = ReadInt(); + string[] inhParts = new string[inhPartLen]; + for (uint k = 0; k < inhPartLen; k++) + { + inhParts[k] = GetString(); + } + string inh = string.Join(".", inhParts); + uint entCount = ReadInt(); + CrossLangClassEntry[] entries = new CrossLangClassEntry[entCount]; + for (uint k = 0; k < entCount; k++) + { + /* + Ensure(stream,main_header,1); + uint8_t sig = main_header[0]; + ent.isAbstract = (sig & 0b00001000) != 0; + ent.isFunction = (sig & 0b00000100) == 0; + ent.modifier = (TClassModifier)(sig & 3); + ent.documentation = GetString(stream); + ent.name = GetString(stream); + uint32_t arglen = EnsureInt(stream); + for(uint32_t l = 0; l < arglen; l++) + ent.args.push_back(GetString(stream)); + ent.chunkId = EnsureInt(stream); + cls.entry.push_back(ent); + */ + int sig = strm.ReadByte(); + + if (sig == -1) throw new EndOfStreamException("Could not read flags byte"); + + bool isAbstract = ((byte)sig & 0b00001000) != 0; + bool isFunction = ((byte)sig & 0b00000100) == 0; + CrossLangClassEntryModifier modifier = (CrossLangClassEntryModifier)(sig & 3); + string ent_doc = GetString(); + string ent_name = GetString(); + uint arglen = ReadInt(); + string[] ent_args = new string[arglen]; + for (uint l = 0; l < arglen; l++) + { + ent_args[l] = GetString(); + } + uint chunkId = ReadInt(); + + entries[k] = new CrossLangClassEntry(isAbstract, isFunction, modifier, ent_doc, ent_name, ent_args, chunkId); + } + Classes.Add(new CrossLangClass(docs, nameParts, name, inhParts, inh, entries)); + } + } + break; default: - strm.Position += sectionSize; + strm.Skip(sectionSize); break; } } @@ -125,7 +240,7 @@ public class CrossLangFile public CrossLangInfo InfoObject => JsonConvert.DeserializeObject(Info) ?? new CrossLangInfo(); - public Bitmap GetIcon(int height=128) + public Bitmap GetIcon(int height = 128) { using (Stream strm = (Icon > -1 && Icon < Resources.Count) ? new MemoryStream(Resources[Icon], false) : AssetLoader.Open(new Uri("avares://CrossLangDevStudio/Assets/crosslang.png"))) { @@ -133,25 +248,85 @@ public class CrossLangFile } } - internal string GetTemplatePrettyName() + internal string GetPrettyName() { var info = InfoObject; if (info is not null) { - if (!string.IsNullOrWhiteSpace(info.TemplateNamePretty)) return info.TemplateNamePretty; - if (!string.IsNullOrWhiteSpace(info.TemplateName)) return info.TemplateName; + if (!string.IsNullOrWhiteSpace(info.ShortNamePretty)) return info.ShortNamePretty; + if (!string.IsNullOrWhiteSpace(info.ShortName)) return info.ShortName; } return this.Name; } public List Strings { get; } = new List(); - public List Resources { get; } = new List(); + public List Resources { get; } = new List(); public List Dependencies { get; } = new List(); + public List Functions { get; } = new List(); + public List Classes { get; } = new List(); + public List Chunks { get; } = new List(); + +} +public class CrossLangClass(string doc, string[] nameParts, string name, string[] inhParts, string inh, CrossLangClassEntry[] ents) +{ + public string Documentation => doc; + + public string[] NameParts => nameParts; + public string Name => name; + + public string[] InheritParts => inhParts; + + public string Inherit => inh; + + public CrossLangClassEntry[] Entries => ents; +} +public enum CrossLangClassEntryModifier +{ + Private, + Protected, + Public, + Static +} +public class CrossLangClassEntry(bool isAbstract, bool isFunction, CrossLangClassEntryModifier modifier, string documentation, string name, string[] args, uint chunkId) +{ + public bool IsAbstract => isAbstract; + + public bool IsFunction => isFunction; + + public CrossLangClassEntryModifier Modifier => modifier; + + public string Documentation => documentation; + public string Name => name; + + public string[] Arguments => args; + + public uint ChunkId => chunkId; + + + /* + Ensure(stream,main_header,1); + uint8_t sig = main_header[0]; + ent.isAbstract = (sig & 0b00001000) != 0; + ent.isFunction = (sig & 0b00000100) == 0; + ent.modifier = (TClassModifier)(sig & 3); + ent.documentation = GetString(stream); + ent.name = GetString(stream); + uint32_t arglen = EnsureInt(stream); + for(uint32_t l = 0; l < arglen; l++) + ent.args.push_back(GetString(stream)); + ent.chunkId = EnsureInt(stream); + cls.entry.push_back(ent); + */ } +public class CrossLangFunction(string[] nameParts, uint chunkId) +{ + public string[] NameParts => nameParts; + public uint ChunkId => chunkId; +} public class CrossLangConfig { @@ -190,10 +365,13 @@ public class CrossLangInfo [JsonProperty("description")] public string? Description { get; set; } //optional but tells people about the package - [JsonProperty("template_name")] - public string? TemplateName { get; set; } - [JsonProperty("template_name_pretty")] - public string? TemplateNamePretty { get; set; } + [JsonProperty("short_name")] + public string? ShortName { get; set; } + + public string? ToolName { get; set; } + + [JsonProperty("short_name_pretty")] + public string? ShortNamePretty { get; set; } [JsonProperty("template_project_dependencies")] public JToken? TemplateDependencies { get; set; } @@ -201,9 +379,9 @@ public class CrossLangInfo [JsonProperty("template_info")] public JToken? TemplateInfo { get; set; } [JsonProperty("template_extra_text_ftles")] - public JToken? TemplateExtraTextFiles { get; set; } + public JToken? TemplateExtraTextFiles { get; set; } [JsonProperty("template_ignored_files")] - public JToken? TemplateIgnoredFiles { get; set; } + public JToken? TemplateIgnoredFiles { get; set; } } public class CrossLangDependency @@ -229,9 +407,9 @@ public class CrossLangVersionConverter : Newtonsoft.Json.JsonConverter, IComparable { public void CopyTo(byte[] data, int offset = 0) @@ -350,10 +528,10 @@ public struct CrossLangVersion : IComparable, IComparable { if (offset + 5 > data.Length) return; - Major = data[offset+0]; - Minor = data[offset+1]; - Patch = data[offset+2]; - ushort build = (ushort)(data[offset+3] << 8 | data[offset+4]); + Major = data[offset + 0]; + Minor = data[offset + 1]; + Patch = data[offset + 2]; + ushort build = (ushort)(data[offset + 3] << 8 | data[offset + 4]); Build = (ushort)(build >> 2); Stage = (CrossLangVersionStage)(build & 3); } @@ -363,7 +541,7 @@ public struct CrossLangVersion : IComparable, IComparable public ushort Build { get; set; } = 0; - public static CrossLangVersion OnePointZeroProd => new CrossLangVersion(){Major=1,Minor=0,Patch=0,Build=0,Stage= CrossLangVersionStage.Production}; + public static CrossLangVersion OnePointZeroProd => new CrossLangVersion() { Major = 1, Minor = 0, Patch = 0, Build = 0, Stage = CrossLangVersionStage.Production }; public CrossLangVersionStage Stage { get; set; } = CrossLangVersionStage.Development; @@ -386,7 +564,7 @@ public struct CrossLangVersion : IComparable, IComparable public override string ToString() { - string stage="dev"; + string stage = "dev"; switch (Stage) { case CrossLangVersionStage.Development: diff --git a/CrossLangShell.cs b/CrossLangShell.cs index 1ff5aaa..fd57608 100644 --- a/CrossLangShell.cs +++ b/CrossLangShell.cs @@ -27,7 +27,7 @@ class CrossLangShell return ["https://cpkg.tesseslanguage.com/"]; } public static string[] ProjectTypes { get; } = ["console", "lib", "webapp", "template", "compile_tool", "tool", "archive"]; - public static string[] ProjectProperTypes { get; } = ["Console Application","Library","Web Application","Template","Compile Tool","Tool","Archive"]; + public static string[] ProjectProperTypes { get; } = ["Console Application", "Library", "Web Application", "Template", "Compile Tool", "Tool", "Archive"]; private static void EscapeBashElement(StringBuilder builder, string val) { builder.Append("\""); @@ -53,27 +53,27 @@ class CrossLangShell private static string EscapeBashCommand(string name, string[] args) { StringBuilder builder = new StringBuilder(); - EscapeBashElement(builder,name); + EscapeBashElement(builder, name); foreach (var item in args) { builder.Append(' '); - EscapeBashElement(builder,item); + EscapeBashElement(builder, item); } builder.Append(" ; read -p \"Press Enter to close.\""); return builder.ToString(); } - private static string EscapeMacBashCommand(string workDir, bool keepOpen,string name, string[] args) + private static string EscapeMacBashCommand(string workDir, bool keepOpen, string name, string[] args) { StringBuilder builder = new StringBuilder(); builder.Append("cd "); EscapeBashElement(builder, workDir); builder.Append(" ; "); - EscapeBashElement(builder,name); + EscapeBashElement(builder, name); foreach (var item in args) { builder.Append(' '); - EscapeBashElement(builder,item); + EscapeBashElement(builder, item); } if (keepOpen) builder.Append(" ; read -p \"Press Enter to close.\""); @@ -96,7 +96,7 @@ class CrossLangShell settings = JsonConvert.DeserializeObject(File.ReadAllText(settingsFile)); } } - + return settings is null ? settings = new CrossLangSettings() : settings; } } @@ -158,18 +158,18 @@ class CrossLangShell if (execDir is not null) { var dir = Path.GetDirectoryName(execDir); - if(dir is not null) + if (dir is not null) paths.Add(dir); } foreach (var item in paths) + { + var path = Path.Combine(item, exec); + if (File.Exists(path)) return path; + foreach (var ext in (Environment.GetEnvironmentVariable("PATHEXT") ?? "").Split(Path.PathSeparator)) { - var path = Path.Combine(item, exec); - if (File.Exists(path)) return path; - foreach (var ext in (Environment.GetEnvironmentVariable("PATHEXT") ?? "").Split(Path.PathSeparator)) - { - if (File.Exists(path + ext)) return path + ext; - } + if (File.Exists(path + ext)) return path + ext; } + } return ""; } @@ -188,6 +188,10 @@ class CrossLangShell return !string.IsNullOrWhiteSpace(CrossLangPath); } } + public static void PushProjectInFolder(string folder) + { + OpenTerminal(false, folder, CrossLangPath, "upload-package"); + } public static void BuildProjectInFolder(string folder) { OpenTerminal(false, folder, CrossLangPath, "build"); @@ -252,7 +256,7 @@ class CrossLangShell } process.Start(); } - if (File.Exists(xfce_terminal)) + else if (File.Exists(xfce_terminal)) { using Process process = new Process(); process.StartInfo.WorkingDirectory = workingDirectory; @@ -435,11 +439,40 @@ class CrossLangShell } } + public static async Task RunCommandAsync(string workingDirectory, string commandName, params string[] args) + { + using Process process = new(); + process.StartInfo.WorkingDirectory = workingDirectory; + process.StartInfo.FileName = GetRealPath(commandName); + process.StartInfo.CreateNoWindow = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + foreach (var arg in args) + process.StartInfo.ArgumentList.Add(arg); + + if (process.Start()) await process.WaitForExitAsync(); + } private static string GetCrossLangDirectory() { + string path = CrossLangPath; if (string.IsNullOrWhiteSpace(path)) return ""; + using Process process0 = new Process(); + process0.StartInfo.ArgumentList.Add("configdir"); + process0.StartInfo.FileName = path; + process0.StartInfo.CreateNoWindow = true; + process0.StartInfo.RedirectStandardOutput = true; + process0.StartInfo.UseShellExecute = false; + process0.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + process0.StartInfo.RedirectStandardInput = true; + if (process0.Start()) + { + process0.StandardInput.WriteLine("y"); + process0.WaitForExit(); + var text = process0.StandardOutput.ReadToEnd(); + return (text ?? "").Replace("\n", "").Replace("\r", ""); + } using Process process = new Process(); process.StartInfo.ArgumentList.Add("configdir"); process.StartInfo.FileName = path; @@ -457,6 +490,29 @@ class CrossLangShell return ""; } + public async Task GetReferenceAsync() + { + await RunCommandAsync(Environment.CurrentDirectory, "crosslang", "docs", "--reference"); + string path = CrossLangPath; + if (string.IsNullOrWhiteSpace(path)) return ""; + using Process process = new Process(); + process.StartInfo.ArgumentList.Add("docs"); + process.StartInfo.ArgumentList.Add("--reference-path"); + process.StartInfo.FileName = path; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + if (process.Start()) + { + + process.WaitForExit(); + var text = process.StandardOutput.ReadToEnd(); + return (text ?? "").Replace("\n", "").Replace("\r", ""); + } + return ""; + } + internal static bool CreateProject(string templateName, string path) { string cpath = CrossLangPath; @@ -478,6 +534,18 @@ class CrossLangShell } return false; } + + public static async Task PublishAsync(string project, string identifier, string prefix, string outputDir) + { + List args = new List(); + args.Add("publish"); + args.Add($"--runtime-package-prefix={prefix}"); + if (!string.IsNullOrWhiteSpace(outputDir)) + args.Add($"--out={outputDir}"); + args.Add(identifier); + + await RunCommandAsync(project,"crosslang",args.ToArray()); + } } public class CrossLangSettings diff --git a/Messages/PublishCloseMessage.cs b/Messages/PublishCloseMessage.cs new file mode 100644 index 0000000..de24984 --- /dev/null +++ b/Messages/PublishCloseMessage.cs @@ -0,0 +1,4 @@ +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging.Messages; +namespace CrossLangDevStudio.Messages; +public class PublishCloseMessage : AsyncRequestMessage; \ No newline at end of file diff --git a/Messages/PublishMessage.cs b/Messages/PublishMessage.cs new file mode 100644 index 0000000..228c735 --- /dev/null +++ b/Messages/PublishMessage.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging.Messages; +namespace CrossLangDevStudio.Messages; +public class PublishMessage(string path) : AsyncRequestMessage +{ + public string Path => path; +} \ No newline at end of file diff --git a/Models/ConsolePackageManger.cs b/Models/ConsolePackageManger.cs new file mode 100644 index 0000000..bb4b525 --- /dev/null +++ b/Models/ConsolePackageManger.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace CrossLangDevStudio.Models; + +public class ConsolePackageManager : IPackageManager +{ + public string Filter => "console"; + + private IEnumerable GetFiles() + { + var path = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "ConsoleApps"); + if (Directory.Exists(path)) + foreach (var dir in Directory.EnumerateDirectories(path)) + { + var file = Path.Combine(dir, $"{Path.GetFileName(dir)}.crvm"); + if (File.Exists(file)) yield return file; + } + } + + public IEnumerable GetInstalledPackages() + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + + } + catch (Exception) + { + continue; + } + yield return new CrossLangDependency { Name = file.Name, Version = file.Version }; + } + + } + + public async Task InstallPackageAsync(string name, CrossLangVersion version) + { + await CrossLangShell.RunCommandAsync(Environment.CurrentDirectory, "crosslang", ["install-console", name, $"--version={version.ToString()}"]); + + } + + public void UninstallPackage(string name) + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + if (name == file.Name) + { + var dir = Path.GetDirectoryName(item); + if(dir is not null) + Directory.Delete(dir,true); + return; + } + } + catch (Exception) + { + continue; + } + } + } +} diff --git a/Models/TemplatePackageManager.cs b/Models/TemplatePackageManager.cs new file mode 100644 index 0000000..c483219 --- /dev/null +++ b/Models/TemplatePackageManager.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace CrossLangDevStudio.Models; + +public class TemplatePackageManager : IPackageManager +{ + public string Filter => "template"; + + private IEnumerable GetFiles() + { + var path = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "Templates"); + if (Directory.Exists(path)) + foreach (var dir in Directory.EnumerateDirectories(path)) + { + var file = Path.Combine(dir, $"{Path.GetFileName(dir)}.crvm"); + if (File.Exists(file)) yield return file; + } + } + + public IEnumerable GetInstalledPackages() + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + + } + catch (Exception) + { + continue; + } + yield return new CrossLangDependency { Name = file.Name, Version = file.Version }; + } + + } + + public async Task InstallPackageAsync(string name, CrossLangVersion version) + { + await CrossLangShell.RunCommandAsync(Environment.CurrentDirectory, "crosslang", ["install-template", name, $"--version={version.ToString()}"]); + + } + + public void UninstallPackage(string name) + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + if (name == file.Name) + { + File.Delete(item); + return; + } + } + catch (Exception) + { + continue; + } + } + } +} diff --git a/Models/ToolPackageManager.cs b/Models/ToolPackageManager.cs new file mode 100644 index 0000000..58a1d5f --- /dev/null +++ b/Models/ToolPackageManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace CrossLangDevStudio.Models; + +public class ToolPackageManager : IPackageManager +{ + public string Filter => "tool"; + + private IEnumerable GetFiles() + { + var path = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "Tools"); + if (Directory.Exists(path)) + foreach (var dir in Directory.EnumerateDirectories(path)) + { + var file = Path.Combine(dir, $"{Path.GetFileName(dir)}.crvm"); + if (File.Exists(file)) yield return file; + } + } + + public IEnumerable GetInstalledPackages() + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + + } + catch (Exception) + { + continue; + } + yield return new CrossLangDependency { Name = file.Name, Version = file.Version }; + } + + } + + public async Task InstallPackageAsync(string name, CrossLangVersion version) + { + await CrossLangShell.RunCommandAsync(Environment.CurrentDirectory, "crosslang", ["install-tool", name, $"--version={version.ToString()}"]); + + } + + public void UninstallPackage(string name) + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + if (name == file.Name) + { + var dir = Path.GetDirectoryName(item); + if(dir is not null) + Directory.Delete(dir,true); + return; + } + } + catch (Exception) + { + continue; + } + } + } +} diff --git a/Models/WebAppPackageManager.cs b/Models/WebAppPackageManager.cs new file mode 100644 index 0000000..6505037 --- /dev/null +++ b/Models/WebAppPackageManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace CrossLangDevStudio.Models; + +public class WebAppPackageManager : IPackageManager +{ + public string Filter => "webapp"; + + private IEnumerable GetFiles() + { + var path = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "WebApps"); + if (Directory.Exists(path)) + foreach (var dir in Directory.EnumerateDirectories(path)) + { + var file = Path.Combine(dir, $"{Path.GetFileName(dir)}.crvm"); + if (File.Exists(file)) yield return file; + } + } + + public IEnumerable GetInstalledPackages() + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + + } + catch (Exception) + { + continue; + } + yield return new CrossLangDependency { Name = file.Name, Version = file.Version }; + } + + } + + public async Task InstallPackageAsync(string name, CrossLangVersion version) + { + await CrossLangShell.RunCommandAsync(Environment.CurrentDirectory, "crosslang", ["install-webapp", name, $"--version={version.ToString()}"]); + + } + + public void UninstallPackage(string name) + { + foreach (var item in GetFiles()) + { + var file = new CrossLangFile(); + try + { + file.Load(item); + if (name == file.Name) + { + var dir = Path.GetDirectoryName(item); + if(dir is not null) + Directory.Delete(dir,true); + return; + } + } + catch (Exception) + { + continue; + } + } + } +} diff --git a/Packaging/Linux/build.sh b/Packaging/Linux/build.sh index bd63f93..3e1922b 100644 --- a/Packaging/Linux/build.sh +++ b/Packaging/Linux/build.sh @@ -11,5 +11,23 @@ cp ../../debian/control-amd64 DEBIAN/control cp ../../crosslang-devstudio.desktop usr/share/applications/ cp ../../crossdev usr/bin/crossdev chmod 755 usr/bin/crossdev -cd .. +cd opt +tar cvzf ../../../../../artifacts/crosslang-devstudio-x86_64.tar.gz . +cd ../.. dpkg-deb --build crosslang-devstudio_1.0.0_amd64/ + + +mkdir crosslang-devstudio_1.0.0_arm64 +cd crosslang-devstudio_1.0.0_arm64 +dotnet publish -c Release -r linux-arm64 -o opt/CrossLangDevStudio -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained ../../../../CrossLangDevStudio.csproj +mkdir DEBIAN +mkdir -p usr/share/applications +mkdir -p usr/bin +cp ../../debian/control-arm64 DEBIAN/control +cp ../../crosslang-devstudio.desktop usr/share/applications/ +cp ../../crossdev usr/bin/crossdev +chmod 755 usr/bin/crossdev +cd opt +tar cvzf ../../../../../artifacts/crosslang-devstudio-aarch64.tar.gz . +cd ../.. +dpkg-deb --build crosslang-devstudio_1.0.0_arm64/ \ No newline at end of file diff --git a/Packaging/Linux/debian/control-arm64 b/Packaging/Linux/debian/control-arm64 new file mode 100644 index 0000000..d1fe100 --- /dev/null +++ b/Packaging/Linux/debian/control-arm64 @@ -0,0 +1,8 @@ +Package: crosslang-devstudio +Version: 1.0.0 +Architecture: arm64 +Essential: no +Priority: optional +Depends: crosslang +Maintainer: Mike Nolan +Description: IDE for CrossLang diff --git a/Packaging/Linux/push.sh b/Packaging/Linux/push.sh index 2cbe2f8..f7c31e4 100644 --- a/Packaging/Linux/push.sh +++ b/Packaging/Linux/push.sh @@ -4,3 +4,21 @@ curl --user tesses50:$GITEA_AUTH -X DELETE \ curl --user tesses50:$GITEA_AUTH \ --upload-file build/crosslang-devstudio_1.0.0_amd64.deb \ https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/jammy/main/upload + +curl --user tesses50:$GITEA_AUTH -X DELETE \ + https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/jammy/main/crosslang-devstudio/1.0.0/arm64 +curl --user tesses50:$GITEA_AUTH \ + --upload-file build/crosslang-devstudio_1.0.0_arm64.deb \ + https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/jammy/main/upload + +curl --user tesses50:$GITEA_AUTH -X DELETE \ + https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/plucky/main/crosslang-devstudio/1.0.0/amd64 +curl --user tesses50:$GITEA_AUTH \ + --upload-file build/crosslang-devstudio_1.0.0_amd64.deb \ + https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/plucky/main/upload + +curl --user tesses50:$GITEA_AUTH -X DELETE \ + https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/plucky/main/crosslang-devstudio/1.0.0/arm64 +curl --user tesses50:$GITEA_AUTH \ + --upload-file build/crosslang-devstudio_1.0.0_arm64.deb \ + https://git.tesseslanguage.com/api/packages/tesses50/debian/pool/plucky/main/upload diff --git a/Packaging/Windows/build.sh b/Packaging/Windows/build.sh index a972c2a..3eb0c23 100644 --- a/Packaging/Windows/build.sh +++ b/Packaging/Windows/build.sh @@ -17,4 +17,6 @@ cd package dotnet publish -c Release -r win-x86 -o bin -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained ../../../../CrossLangDevStudio.csproj cp ../../script.nsh . cp ../../license.txt . -makensis script.nsh \ No newline at end of file +makensis script.nsh +mv crosslang-devstudio-win32.exe ../../../../artifacts/ +zip -r ../../../../artifacts/crosslang-devstudio-win32.zip bin share \ No newline at end of file diff --git a/Packaging/Windows/script.nsh b/Packaging/Windows/script.nsh index 4c971cf..14deaf5 100644 --- a/Packaging/Windows/script.nsh +++ b/Packaging/Windows/script.nsh @@ -29,7 +29,7 @@ LicenseData "license.txt" # This will be in the installer/uninstaller's title bar Name "${COMPANYNAME} - ${APPNAME}" Icon "crosslang.ico" -outFile "CrossLangDevStudio-Installer.exe" +outFile "crosslang-devstudio-win32.exe" !include LogicLib.nsh diff --git a/ViewModels/AddProjectReferenceViewModel.cs b/ViewModels/AddProjectReferenceViewModel.cs new file mode 100644 index 0000000..3e7b35d --- /dev/null +++ b/ViewModels/AddProjectReferenceViewModel.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; +using MsBox.Avalonia; +using Newtonsoft.Json; + +namespace CrossLangDevStudio.ViewModels; + + +public partial class AddProjectReferenceViewModel : ViewModelBase +{ + public AddProjectReferenceViewModel(string projectDir) + { + ProjectDirectory = projectDir; + var data = File.ReadAllText(Path.Combine(projectDir, "cross.json")); + var jsonData = JsonConvert.DeserializeObject(data) ?? new CrossLangConfig(); + foreach (var item in jsonData.ProjectDependencies) + References.Add(new ProjectReferenceEntry(this, item)); + } + public string ProjectDirectory { get; init; } + public ObservableCollection References { get; } = new ObservableCollection(); + + public void Remove(ProjectReferenceEntry projectReferenceEntry) + { + References.Remove(projectReferenceEntry); + var data = File.ReadAllText(Path.Combine(ProjectDirectory, "cross.json")); + var jsonData = JsonConvert.DeserializeObject(data) ?? new CrossLangConfig(); + jsonData.ProjectDependencies.Remove(projectReferenceEntry.Path); + File.WriteAllText(Path.Combine(ProjectDirectory, "cross.json"), JsonConvert.SerializeObject(jsonData, Formatting.Indented, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore })); + + } + + [RelayCommand] + private async Task AddProjectAsync() + { + var window = await WeakReferenceMessenger.Default.Send(); + var dir = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions() { Title = "Add Project Dependency" }); + if (dir.Count != 0) + { + string? path = dir[0].TryGetLocalPath(); + if (!string.IsNullOrWhiteSpace(path)) + { + var jsonFile = Path.Combine(path, "cross.json"); + if (!File.Exists(jsonFile)) + { + await MessageBoxManager.GetMessageBoxStandard("Error", $"Could not find cross.json in folder {path}", MsBox.Avalonia.Enums.ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error).ShowAsync(); + return; + } + string projectPath = Path.GetRelativePath(this.ProjectDirectory, path); + var abs = Path.GetFullPath(projectPath, ProjectDirectory); + + foreach (var item in References) + { + if (Path.GetFullPath(item.Path, ProjectDirectory) == abs) + { + await MessageBoxManager.GetMessageBoxStandard("Error", $"Project {path} already exists in your project.", MsBox.Avalonia.Enums.ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error).ShowAsync(); + + return; + } + } + var data = File.ReadAllText(Path.Combine(ProjectDirectory, "cross.json")); + var jsonData = JsonConvert.DeserializeObject(data) ?? new CrossLangConfig(); + jsonData.ProjectDependencies.Add(projectPath); + File.WriteAllText(Path.Combine(ProjectDirectory, "cross.json"), JsonConvert.SerializeObject(jsonData, Formatting.Indented, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore })); + References.Add(new ProjectReferenceEntry(this, projectPath)); + } + + } + } +} + +public partial class ProjectReferenceEntry(AddProjectReferenceViewModel parent,string path) : ObservableObject +{ + public string Path => path; + + [RelayCommand] + private void Remove() + { + parent.Remove(this); + } +} \ No newline at end of file diff --git a/ViewModels/CRVMViewerViewModel.cs b/ViewModels/CRVMViewerViewModel.cs new file mode 100644 index 0000000..6f917d9 --- /dev/null +++ b/ViewModels/CRVMViewerViewModel.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; + +namespace CrossLangDevStudio.ViewModels; + +public class CRVMViewerViewModel : ViewModelBase +{ + public string FilePath { get; set; } + public CRVMViewerViewModel(string path) + { + FilePath = path; + var clf = new CrossLangFile(); + using (var strm = File.OpenRead(path)) + { + + clf.Load(strm); + + } + + + foreach (var fn in clf.Functions) + { + string docs = $"/^{fn.NameParts[0]}^/"; + string name = string.Join(".", fn.NameParts.Skip(1)); + string args = string.Join(", ", clf.Chunks[(int)fn.ChunkId]); + Functions.Add(new Function(docs, name, args)); + } + foreach (var cls in clf.Classes) + { + string docs = $"/^{cls.Documentation}^/"; + ObservableCollection entries = new ObservableCollection(); + + foreach (var item in cls.Entries) + { + string modifiers = $"\t{item.Modifier.ToString().ToLower()} "; + if (item.IsFunction && item.IsAbstract) + modifiers += "abstract "; + + Class.ClassEntry ce = new Class.ClassEntry($"\t/^{item.Documentation}^/", item.Name, modifiers, item.IsFunction, item.IsFunction ? string.Join(", ", item.Arguments) : ""); + entries.Add(ce); + } + + Classes.Add(new Class(docs, cls.Name, cls.Inherit, entries)); + } + } + + public ObservableCollection Functions { get; set; } = new ObservableCollection(); + + public ObservableCollection Classes { get; set; } = new ObservableCollection(); + + public class Function(string docs, string name, string args) : ObservableObject + { + public string Name => name; + + public string Documentation => docs; + + public string Args => args; + } + + public class Class(string docs, string name, string inherits, ObservableCollection entries) : ObservableObject + { + public string Documentation => docs; + + public string Name => name; + + public string Inherits => inherits == "ClassObject" ? "" : inherits; + + public string InheritsColon => inherits == "ClassObject" ? "" : " : "; + + public ObservableCollection Entries => entries; + + public class ClassEntry(string docs,string name, string modifiers, bool isFunction, string args) + { + public string Documentation => docs; + + public string Name => name; + public string Modifiers => modifiers; + + public IBrush NameColor => new SolidColorBrush(isFunction ? 0xFFDCDCAA : 0xFF9CDCFE); + + public IBrush LeftParenColor => new SolidColorBrush(isFunction ? 0xFFC586C0 : 0xFFCCCCCC); + + public string LeftParen => isFunction ? "(" : ";"; + + public string RightParen => isFunction ? ")" : ""; + + + public string Args => args; + } + } +} \ No newline at end of file diff --git a/ViewModels/FileEditorViewModel.cs b/ViewModels/FileEditorViewModel.cs index 6e3c52a..4628d45 100644 --- a/ViewModels/FileEditorViewModel.cs +++ b/ViewModels/FileEditorViewModel.cs @@ -5,13 +5,22 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Platform.Storage; - +using AvaloniaEdit; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using CrossLangDevStudio.Messages; using CrossLangDevStudio.Views; - +using AvaloniaEdit.CodeCompletion; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Folding; +using AvaloniaEdit.Rendering; +using TextMateSharp.Grammars; +using AvaloniaEdit.TextMate; +using Avalonia.Input; +using System.Collections.Generic; +using Avalonia.Media; namespace CrossLangDevStudio.ViewModels; public partial class FileEditorViewModel : ViewModelBase, ISavable @@ -19,7 +28,7 @@ public partial class FileEditorViewModel : ViewModelBase, ISavable TabItemViewModel tab; public string FilePath { get; } bool _modified = false; - private bool Modified + public bool Modified { get => _modified; set @@ -36,30 +45,72 @@ public partial class FileEditorViewModel : ViewModelBase, ISavable } } } - - private string _text = ""; - public string Text - { - get => _text; - set - { - Modified = true; - this.SetProperty(ref _text, value, nameof(Text)); - } - } - public FileEditorViewModel(string path,TabItemViewModel tab) + TextEditor textEdit = new TextEditor(); + + public TextEditor TextEditor => textEdit; + + + + public FileEditorViewModel(string path, TabItemViewModel tab) { this.tab = tab; FilePath = path; - _text = File.ReadAllText(path); + textEdit.ShowLineNumbers = true; + textEdit.FontSize = 24; + string ext = Path.GetExtension(FilePath); + if (ext == ".tcross") + { + ext = ".go"; + } + { + + var _registryOptions = new RegistryOptions(ThemeName.Dark); + + var _textMateInstallation = textEdit.InstallTextMate(_registryOptions); + + //definition. + + + + //Here we are getting the language by the extension and right after that we are initializing grammar with this language. + //And that's all 😀, you are ready to use AvaloniaEdit with syntax highlighting! + try + { + //AssetLoader.Open(new Uri("avares://CrossLangDevStudio/Assets/crosslang.png") + + _textMateInstallation.SetGrammar(_registryOptions.GetScopeByLanguageId(_registryOptions.GetLanguageByExtension(ext).Id)); + } + catch (Exception) + { + + } + } + + + textEdit.Text = File.ReadAllText(path); + textEdit.TextChanged += (sender, e) => + { + Modified = true; + }; } + public void Save() { - File.WriteAllText(FilePath, Text); + + File.WriteAllText(FilePath, textEdit.Text); Modified = false; + } -} \ No newline at end of file + + public void SaveRecovery(string path) + { + + File.WriteAllText(path, textEdit.Text); + + + } +} diff --git a/ViewModels/ISavable.cs b/ViewModels/ISavable.cs index 0335067..e8f84b3 100644 --- a/ViewModels/ISavable.cs +++ b/ViewModels/ISavable.cs @@ -1,5 +1,10 @@ namespace CrossLangDevStudio.ViewModels; + interface ISavable { + string FilePath { get; } + bool Modified { get; set; } void Save(); + + void SaveRecovery(string path); } \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index e1b5e55..fe3412a 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -2,15 +2,21 @@ using System.Collections.ObjectModel; using System.IO; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform.Storage; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using CrossLangDevStudio.Messages; using CrossLangDevStudio.Models; using CrossLangDevStudio.Views; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using Newtonsoft.Json; +using Tabalonia.Events; namespace CrossLangDevStudio.ViewModels; @@ -26,6 +32,7 @@ public partial class MainWindowViewModel : ViewModelBase public ObservableCollection ProjectFiles { get; } = new ObservableCollection(); public EventHandler? Closed { get; } = null; + public EventHandler? ClosingTab { get; } public TabItemViewModel WelcomePage { @@ -49,16 +56,120 @@ public partial class MainWindowViewModel : ViewModelBase value?.Click?.Invoke(); } } + private Avalonia.Threading.DispatcherTimer timer=new (); + [RelayCommand] + private void Welcome() + { + foreach (var item in TabItems) + { + if (item.Body is WelcomeViewModel) + { + SelectedTab = item; + return; + } + } + AddTab(WelcomePage); + } + + [RelayCommand] + private async Task WebsiteAsync() + { + var win = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); + if (win is not null) + { + await win.Launcher.LaunchUriAsync(new Uri("https://crosslang.tesseslanguage.com/")); + } + } + + [RelayCommand] + private async Task VideoDocumentationAsync() + { + var win = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); + if (win is not null) + { + await win.Launcher.LaunchUriAsync(new Uri("https://peertube.site.tesses.net/c/crosslang")); + } + } + [RelayCommand] + private async Task DocumentationAsync() + { + var win = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); + if (win is not null) + { + await win.Launcher.LaunchUriAsync(new Uri("https://crosslang.tesseslanguage.com/language-features/index.html")); + } + } + + private async Task CloseTabAsync(TabClosingEventArgs e, ISavable savable) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Save?", $"Do you want to save {Path.GetFileName(savable.FilePath)}.?", + ButtonEnum.YesNoCancel, Icon.Question); + var msg = await WeakReferenceMessenger.Default.Send(); + if (msg is not null) + { + switch (await box.ShowWindowDialogAsync(msg)) + { + case ButtonResult.Yes: + savable.Save(); + break; + case ButtonResult.Cancel: + e.Cancel = true; + break; + } + } + } + + public bool HasUnsaved + { + get + { + foreach (var item in TabItems) + { + if (item.Body is ISavable s && s.Modified) return true; + } + return false; + } + } + public MainWindowViewModel(string[] args) { + this.ClosingTab = (sender, e) => + { + + if (e.Item is TabItemViewModel vm) + { + if (vm.Body is ISavable savable) + { + if (savable.Modified) + { + //thanks https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704372304 + using (var source = new CancellationTokenSource()) + { + CloseTabAsync(e, savable).ContinueWith(e => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext()); + Dispatcher.UIThread.MainLoop(source.Token); + } + + + } + } + } + }; + timer.Interval = TimeSpan.FromMinutes(1); + timer.Tick += (sender, e) => + { + SaveRecovery(); + }; + if (args.Length == 1) { LoadProject(args[0]); } if (string.IsNullOrWhiteSpace(CurrentProject)) - this.TabItems.Add(WelcomePage); + this.TabItems.Add(WelcomePage); + } @@ -74,7 +185,7 @@ public partial class MainWindowViewModel : ViewModelBase var newTabItem = new NewTabViewModel(this, tab); tab.Body = newTabItem; } - + return tab; @@ -82,12 +193,19 @@ public partial class MainWindowViewModel : ViewModelBase internal void LoadProject(string obj) { + timer.Stop(); //we don't want autorecovery timer ticking when we change projects if (!File.Exists(Path.Combine(obj, "cross.json"))) { return; } CrossLangShell.EnsureRecent(obj); CurrentProject = obj.TrimEnd(Path.DirectorySeparatorChar); + //we don't care that it isn't awaited due to only Refreshing here (you can referesh in GUI) + //and the timer starting after async call inside the async function + //so I think I know what I am doing +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + InitRecoveryAsync(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Refresh(CurrentProject); } [RelayCommand] @@ -102,7 +220,7 @@ public partial class MainWindowViewModel : ViewModelBase private void Refresh(string obj) { - + for (int i = 0; i < TabItems.Count; i++) @@ -122,11 +240,11 @@ public partial class MainWindowViewModel : ViewModelBase OpenProjectConfig(); }), - new ProjectFileNode("Packages", ()=>{ + new ProjectFileNode("Project Packages", ()=>{ OpenProjectPackages(); }), - new ProjectFileNode("Project Dependencies",()=>{ - + new ProjectFileNode("Project Dependencies (folders)",()=>{ + OpenProjectDependencies(); }) ]) @@ -155,7 +273,227 @@ public partial class MainWindowViewModel : ViewModelBase ProjectFiles.Clear(); ProjectFiles.Add(new ProjectFileNode(Path.GetFileName(obj), entries)); } + private int proj_id = -1; + private async Task InitRecoveryAsync() + { + int i = 0; + var dir = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "DevStudio", "Recovery"); + foreach (var item in Directory.EnumerateDirectories(dir)) + { + if (int.TryParse(Path.GetFileName(item), out int id)) + { + if (id >= i) i = id + 1; + var file = $"{item}.json"; + if (File.Exists(file)) + { + var cfg = JsonConvert.DeserializeObject(File.ReadAllText(file)); + if (cfg is not null) + { + if (cfg.ProjectDirectory == CurrentProject) + { + proj_id = id; + + if (cfg.HasRecovery) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Autorecovery", $"Found a autorecovery from {cfg.Date} do you want to load it?", + ButtonEnum.YesNo, Icon.Question); + var msg = await WeakReferenceMessenger.Default.Send(); + if (msg is not null && await box.ShowWindowDialogAsync(msg) == ButtonResult.Yes) + { + CopyDir(item, CurrentProject); + + + } + cfg.HasRecovery = false; + File.WriteAllText(file, JsonConvert.SerializeObject(cfg)); + } + + //WeakReferenceMessenger.Default.Send + + timer.Start(); + + return; + } + } + } + } + } + + proj_id = i; + + + timer.Start(); + } + + private void CopyDir(string src, string dest) + { + foreach (var dir in Directory.EnumerateDirectories(src)) + { + var destDir = Path.Combine(dest, Path.GetFileName(dir)); + Directory.CreateDirectory(destDir); + CopyDir(dir, destDir); + } + + foreach (var file in Directory.EnumerateFiles(src)) + { + var destFile = Path.Combine(dest, Path.GetFileName(file)); + + File.Copy(file, destFile, true); + } + } + + private void SaveRecovery() + { + if (proj_id == -1 || !Directory.Exists(CurrentProject)) return; + var id = proj_id; + var dir = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "DevStudio", "Recovery", $"{id}_tmp"); + var dirdest = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "DevStudio", "Recovery", $"{id}"); + Directory.CreateDirectory(dir); + bool hasAnything = false; + + foreach (var item in TabItems) + { + if (item.Body is ISavable savable && savable.Modified) + { + if (savable.FilePath.StartsWith(CurrentProject)) + { + var path = Path.GetRelativePath(CurrentProject, savable.FilePath); + string? dirPath = Path.GetDirectoryName(path); + if (dirPath is not null) + Directory.CreateDirectory(Path.Combine(dir, dirPath)); + + savable.SaveRecovery(Path.Combine(dir, path)); + hasAnything = true; + } + } + } + + if (Directory.Exists(dirdest)) + { + Directory.Delete(dirdest, true); + } + if (hasAnything) + { + Directory.Move(dir, dirdest); + File.WriteAllText($"{dirdest}.json", JsonConvert.SerializeObject(new RecoveryConfig { Date = DateTime.Now, ProjectDirectory = CurrentProject, HasRecovery=true })); + } + else + { + Directory.Delete(dir, true); + + File.WriteAllText($"{dirdest}.json", JsonConvert.SerializeObject(new RecoveryConfig { Date = DateTime.Now, ProjectDirectory = CurrentProject, HasRecovery=false })); + } + Directory.CreateDirectory(dirdest); + } + [RelayCommand] + private void InstallTool() + { + foreach (var item in TabItems) + { + if (item.Body is PackageManagerViewModel vm) + { + if (vm.Packages is ToolPackageManager) + { + SelectedTab = item; + return; + } + } + } + AddTab(new TabItemViewModel() + { + Header = "Tool Packages", + Body = new PackageManagerViewModel(this, new ToolPackageManager()) + }); + } + [RelayCommand] + private void InstallTemplate() + { + foreach (var item in TabItems) + { + if (item.Body is PackageManagerViewModel vm) + { + if (vm.Packages is TemplatePackageManager) + { + SelectedTab = item; + return; + } + } + } + AddTab(new TabItemViewModel() + { + Header = "Template Packages", + Body = new PackageManagerViewModel(this, new TemplatePackageManager()) + }); + } + [RelayCommand] + private void InstallConsole() + { + foreach (var item in TabItems) + { + if (item.Body is PackageManagerViewModel vm) + { + if (vm.Packages is ConsolePackageManager) + { + SelectedTab = item; + return; + } + } + } + AddTab(new TabItemViewModel() + { + Header = "Console Packages", + Body = new PackageManagerViewModel(this, new ConsolePackageManager()) + }); + } + + [RelayCommand] + private void InstallWebApp() + { + foreach (var item in TabItems) + { + if (item.Body is PackageManagerViewModel vm) + { + if (vm.Packages is WebAppPackageManager) + { + SelectedTab = item; + return; + } + } + } + AddTab(new TabItemViewModel() + { + Header = "Web Application Packages", + Body = new PackageManagerViewModel(this, new WebAppPackageManager()) + }); + } + private void OpenProjectDependencies() + { + if (Directory.Exists(CurrentProject)) + { + var config = Path.Combine(CurrentProject, "cross.json"); + if (File.Exists(config)) + { + foreach (var item in TabItems) + { + if (item.Body is AddProjectReferenceViewModel vm) + { + if (vm.ProjectDirectory == CurrentProject) + { + SelectedTab = item; + return; + } + } + } + AddTab(new TabItemViewModel() + { + Header = "Project Dependencies (folders)", + Body = new AddProjectReferenceViewModel(CurrentProject) + }); + } + } + } private void OpenProjectPackages() { if (Directory.Exists(CurrentProject)) @@ -185,7 +523,7 @@ public partial class MainWindowViewModel : ViewModelBase private void OpenProjectConfig() { - + if (Directory.Exists(CurrentProject)) { var config = Path.Combine(CurrentProject, "cross.json"); @@ -200,12 +538,23 @@ public partial class MainWindowViewModel : ViewModelBase TabItemViewModel vm = new TabItemViewModel(); vm.Header = "Project Configuration"; var pcm = new ProjectConfigurationViewModel(config, vm); - + vm.Body = pcm; AddTab(vm); } } + [RelayCommand] + private async Task ReferenceAsync() + { + await CrossLangShell.RunCommandAsync(Environment.CurrentDirectory, "crosslang", "docs", "--reference"); + var refFile = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "Reference.crvm"); + Console.WriteLine(refFile); + if(File.Exists(refFile)) + { + OpenFile(refFile); + } + } static string[] FILE_EXTS = new string[]{ ".tcross", @@ -232,12 +581,17 @@ public partial class MainWindowViewModel : ViewModelBase public void OpenFile(string path) { bool isValid = false; + string ext = Path.GetExtension(path).ToLower(); foreach (var item in FILE_EXTS) { - if (Path.GetExtension(path).ToLower() == item) + if (ext == item) isValid = true; } - if (!isValid) return; + if (!isValid) + { + OpenOtherFile(path, ext); + return; + } foreach (var item in TabItems) { @@ -255,10 +609,39 @@ public partial class MainWindowViewModel : ViewModelBase Header = Path.GetFileName(path), }; - tab.Body = new FileEditorViewModel(path,tab); + tab.Body = new FileEditorViewModel(path, tab); AddTab(tab); } + private void OpenOtherFile(string path, string ext) + { + if (ext == ".crvm") + { + foreach (var item in TabItems) + { + if (item.Body is CRVMViewerViewModel model) + { + if (model.FilePath == path) + { + SelectedTab = item; + return; + } + } + } + try + { + var tab = new TabItemViewModel() + { + Header = Path.GetFileName(path), + Body = new CRVMViewerViewModel(path) + }; + + AddTab(tab); + }catch(Exception ex) { Console.WriteLine(ex); } + + } + } + [ObservableProperty] private TabItemViewModel? _selectedTab = null; private void AddTab(TabItemViewModel item) @@ -337,13 +720,57 @@ public partial class MainWindowViewModel : ViewModelBase SelectedTab?.Save(); } [RelayCommand] - private void SaveAll() + public void SaveAll() { foreach (var tab in TabItems) { tab.Save(); } } + + [RelayCommand] + private void AddPackage() + { + if (Directory.Exists(CurrentProject)) + { + var conf = Path.Combine(CurrentProject, "cross.json"); + if (File.Exists(conf)) + { + OpenProjectPackages(); + } + } + } + [RelayCommand] + private async Task PublishAsync() + { + SaveAll(); + if(Directory.Exists(CurrentProject)) + await WeakReferenceMessenger.Default.Send(new PublishMessage(CurrentProject)); + } + [RelayCommand] + private async Task PushPackageAsync() + { + SaveAll(); + if (Directory.Exists(CurrentProject)) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Push Package Confirmation", $"Are you sure you want to push the package?", + ButtonEnum.YesNo, Icon.Question); + var msg = await WeakReferenceMessenger.Default.Send(); + if (msg is not null && await box.ShowWindowDialogAsync(msg) == ButtonResult.Yes) + + CrossLangShell.PushProjectInFolder(CurrentProject); + } + } +} + +public class RecoveryConfig +{ + public DateTime Date { get; set; } + + public string ProjectDirectory { get; set; } = ""; + + public bool HasRecovery { get; set; } } public partial class NewTabViewModel : ViewModelBase diff --git a/ViewModels/NewProjectDialogViewModel.cs b/ViewModels/NewProjectDialogViewModel.cs index 6abf493..418c58f 100644 --- a/ViewModels/NewProjectDialogViewModel.cs +++ b/ViewModels/NewProjectDialogViewModel.cs @@ -1,8 +1,10 @@ using System; using System.Collections.ObjectModel; using System.IO; +using System.Threading.Tasks; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; +using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; @@ -85,6 +87,20 @@ public partial class NewProjectDialogViewModel : ViewModelBase } } } + [RelayCommand] + public async Task BrowseAsync() + { + var window = await WeakReferenceMessenger.Default.Send(); + + + var res = await window.StorageProvider.OpenFolderPickerAsync(new Avalonia.Platform.Storage.FolderPickerOpenOptions() { Title = "Pick Project Parent Folder" }); + if (res.Count != 0) + { + string? path = res[0].TryGetLocalPath(); + if(!string.IsNullOrWhiteSpace(path)) + this.ParentDirectory = path; + } + } [RelayCommand] private void OK() @@ -111,7 +127,7 @@ public partial class CrossLangTemplate(string name,CrossLangFile file) : Observa { [ObservableProperty] - private string _name = file.GetTemplatePrettyName(); + private string _name = file.GetPrettyName(); [ObservableProperty] private IImage _icon = file.GetIcon(64); diff --git a/ViewModels/PackageManagerViewModel.cs b/ViewModels/PackageManagerViewModel.cs index f58f10d..72aff3a 100644 --- a/ViewModels/PackageManagerViewModel.cs +++ b/ViewModels/PackageManagerViewModel.cs @@ -49,12 +49,12 @@ public partial class PackageManagerViewModel : ViewModelBase foreach(var svr in CrossLangShell.GetServers()) this.Servers.Add(svr); } - + // [RelayCommand] private async Task SearchPackagesAsync() { var svr = Servers[ServerIndex].TrimEnd('/'); - var body = await Main.Client.GetStringAsync($"{svr}/api/v1/search?q={HttpUtility.UrlEncode(Search)}&filter={HttpUtility.UrlEncode(Packages.Filter)}&offset={Page-1}&limit=20"); + var body = await Main.Client.GetStringAsync($"{svr}/api/v1/search?q={HttpUtility.UrlEncode(Search)}&type={HttpUtility.UrlEncode(Packages.Filter)}&offset={Page-1}&limit=20"); var res = JsonConvert.DeserializeObject(body); PackageList.Clear(); if (res is not null) diff --git a/ViewModels/ProjectConfigurationViewModel.cs b/ViewModels/ProjectConfigurationViewModel.cs index b295207..fa4f81e 100644 --- a/ViewModels/ProjectConfigurationViewModel.cs +++ b/ViewModels/ProjectConfigurationViewModel.cs @@ -29,7 +29,7 @@ public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable TabItemViewModel tab; public string FilePath { get; } bool _modified = false; - private bool Modified + public bool Modified { get => _modified; set @@ -56,8 +56,8 @@ public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable string _license; - string _templatename; - string _templatenamepretty; + string _shortname; + string _shortnamepretty; string _description; @@ -130,22 +130,22 @@ public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable } } - public string TemplateName + public string ShortName { - get => _templatename; + get => _shortname; set { Modified = true; - SetProperty(ref _templatename, value, nameof(TemplateName)); + SetProperty(ref _shortname, value, nameof(ShortName)); } } - public string TemplateNamePretty + public string ShortNamePretty { - get => _templatenamepretty; + get => _shortnamepretty; set { Modified = true; - SetProperty(ref _templatenamepretty, value, nameof(TemplateNamePretty)); + SetProperty(ref _shortnamepretty, value, nameof(ShortNamePretty)); } } public string Description @@ -183,8 +183,8 @@ public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable _homepage = config?.Info?.HomePage ?? ""; _maintainer = config?.Info?.Maintainer ?? ""; _license = config?.Info?.License ?? ""; - _templatename = config?.Info?.TemplateName ?? ""; - _templatenamepretty = config?.Info?.TemplateNamePretty ?? ""; + _shortname = config?.Info?.ShortName ?? ""; + _shortnamepretty = config?.Info?.ShortNamePretty ?? ""; _type = Array.IndexOf(CrossLangShell.ProjectTypes, config?.Info?.Type ?? "console"); _type = _type == -1 ? 0 : _type; _description = config?.Info?.Description ?? ""; @@ -193,6 +193,12 @@ public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable [RelayCommand] public void Save() + { + SaveRecovery(FilePath); + Modified = false; + } + + public void SaveRecovery(string path) { var config = JsonConvert.DeserializeObject(File.ReadAllText(FilePath)) ?? new CrossLangConfig(); config.Name = Name; @@ -207,13 +213,12 @@ public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable config.Info.License = string.IsNullOrWhiteSpace(License) ? null : License; config.Info.Maintainer = string.IsNullOrWhiteSpace(Maintainer) ? null : Maintainer; config.Info.Repoository = string.IsNullOrWhiteSpace(Repository) ? null : Repository; - config.Info.TemplateName = string.IsNullOrWhiteSpace(TemplateName) ? null : TemplateName; - config.Info.TemplateNamePretty = string.IsNullOrWhiteSpace(TemplateNamePretty) ? null : TemplateNamePretty; + config.Info.ShortName = string.IsNullOrWhiteSpace(ShortName) ? null : ShortName; + config.Info.ShortNamePretty = string.IsNullOrWhiteSpace(ShortNamePretty) ? null : ShortNamePretty; config.Info.Type = CrossLangShell.ProjectTypes[Type]; - File.WriteAllText(FilePath, JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings() + File.WriteAllText(path, JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore })); - Modified = false; } } \ No newline at end of file diff --git a/ViewModels/PublishDialogViewModel.cs b/ViewModels/PublishDialogViewModel.cs new file mode 100644 index 0000000..510cc26 --- /dev/null +++ b/ViewModels/PublishDialogViewModel.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using System.Web; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; +using Newtonsoft.Json; +using ICSharpCode.SharpZipLib.Zip; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.GZip; +using ICSharpCode.SharpZipLib.BZip2; +using System.Text; +using Avalonia.Platform.Storage; + +namespace CrossLangDevStudio.ViewModels; + +public partial class PublishDialogViewModel : ViewModelBase +{ + public string Path { get; } + public PublishDialogViewModel(string path) + { + Path = path; + foreach (var item in CrossLangShell.GetServers()) + { + Servers.Add(item); + } + Targets.Add(new Target("copy", "Just Copy")); + Targets.Add(new Target("crvm", "Merge")); + Targets.Add(new Target("cmake", "CMake")); + Targets.Add(new Target("header", "C++ Header")); + this.TargetIndex = 0; + } + [ObservableProperty] + private string _prefix = "Tesses.CrossLang.Runtime"; + + public ObservableCollection Servers { get; } = new ObservableCollection(); + [ObservableProperty] + private int _serverIndex = 0; + public ObservableCollection Targets { get; } = new ObservableCollection(); + [ObservableProperty] + private int _targetIndex = 0; + + [ObservableProperty] + private string _output = ""; + int _ot=0; + public int OutputType + { + get => _ot; + set { + _ot = value; + switch(value) + { + case 0: + OutputLabel = "Directory To Publish To (Optional)"; + break; + case 1: + OutputLabel = "Zip Path"; + break; + case 2: + case 3: + case 4: + OutputLabel = "Tar Path"; + break; + } + } + } + + [ObservableProperty] + private string _outputLabel = "Directory To Publish To (Optional)"; + + [RelayCommand] + private async Task BrowseAsync() + { + var window = await WeakReferenceMessenger.Default.Send(); + + if (OutputType == 0) + { + var res = await window.StorageProvider.OpenFolderPickerAsync(new Avalonia.Platform.Storage.FolderPickerOpenOptions() { Title = "Pick publish folder" }); + if (res.Count != 0) + { + string? path = res[0].TryGetLocalPath(); + if (!string.IsNullOrWhiteSpace(path)) + this.Output = path; + } + } + else + { + string ext = "zip"; + switch(OutputType) + { + case 2: + ext = "tar"; + break; + case 3: + ext = "tar.gz"; + break; + case 4: + ext = "tar.bz2"; + break; + } + + var res = await window.StorageProvider.SaveFilePickerAsync(new Avalonia.Platform.Storage.FilePickerSaveOptions() { Title = $"Save {ext}", DefaultExtension = $".{ext}" , FileTypeChoices = [new FilePickerFileType(ext.ToUpper()){ Patterns =[$".{ext}"]}]}); + if (res is not null) + { + string? path = res.TryGetLocalPath(); + if (!string.IsNullOrWhiteSpace(path)) + this.Output = path; + } + } + } + + [RelayCommand] + private async Task LoadAsync() + { + Targets.Clear(); + Targets.Add(new Target("copy", "Just Copy")); + Targets.Add(new Target("crvm", "Merge")); + Targets.Add(new Target("cmake", "CMake")); + Targets.Add(new Target("header", "C++ Header")); + this.TargetIndex = 0; + if (ServerIndex >= 0 && ServerIndex < this.Servers.Count) + { + string url = $"{this.Servers[ServerIndex].TrimEnd('/')}/api/v1/search?type=archive"; + if (!string.IsNullOrWhiteSpace(Prefix)) + { + url = $"{url}&q={HttpUtility.UrlEncode(Prefix.TrimEnd('.'))}."; + } + var window = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); + if (window is not null && window.DataContext is MainWindowViewModel mwvm) + { + int page = 0; + bool hadany = false; + do + { + var body = await mwvm.Client.GetStringAsync($"{url}&offset={page}"); + page++; + var res = JsonConvert.DeserializeObject(body); + if (res is not null) + { + hadany = res.Packages.Count > 0; + foreach (var item in res.Packages) + { + string name = item.Name; + if (!string.IsNullOrWhiteSpace(Prefix)) + { + int prefixLen = Prefix.TrimEnd('.').Length + 1; + name = name.Substring(prefixLen); + } + + Targets.Add(new Target(name, name)); + } + } + } while (hadany); + } + } + + } + + [RelayCommand] + private async Task PublishAsync() + { + + string ident = Targets[TargetIndex].Real; + await CrossLangShell.PublishAsync(Path, ident, Prefix, OutputType == 0 ? Output : ""); + + void Tar(Stream strm) + { + + string dir = System.IO.Path.Combine(Path, "publish", ident); + + + using (var tarArchive = TarArchive.CreateOutputTarArchive(strm, Encoding.UTF8)) + { + tarArchive.RootPath = System.IO.Path.DirectorySeparatorChar == '/' ? dir : dir.Replace('\\', '/'); + if (tarArchive.RootPath.EndsWith("/")) + tarArchive.RootPath = tarArchive.RootPath.Remove(tarArchive.RootPath.Length - 1); + + DirectoryContents(tarArchive, dir); + } + + + + + + /* + TarEntry ent = TarEntry.CreateEntryFromFile(); + ent.TarHeader.Mode = 0755; + ent.TarHeader.UserId = 1000; + ent.TarHeader.GroupId = 1000; + */ + + } + + if (OutputType > 0) + { + switch (OutputType) + { + case 1: + { + FastZip z = new FastZip(); + z.CreateZip(Output, System.IO.Path.Combine(Path, "publish", ident), true, null); + } + break; + case 2: + { + using (var strm = File.Create(Output)) + Tar(strm); + + } + break; + case 3: + { + using (var strm = File.Create(Output)) + { + using (var gz = new GZipOutputStream(strm)) + Tar(gz); + } + } + break; + case 4: + { + using (var strm = File.Create(Output)) + { + using (var bzip2 = new BZip2OutputStream(strm)) + Tar(bzip2); + } + } + break; + } + } + _ = WeakReferenceMessenger.Default.Send(new PublishCloseMessage()); + } + + [RelayCommand] + private void Cancel() + { + WeakReferenceMessenger.Default.Send(new PublishCloseMessage()); + } + + private void DirectoryContents(TarArchive tarArchive, string src) + { + TarEntry tarEntry = TarEntry.CreateEntryFromFile(src); + tarArchive.WriteEntry(tarEntry, false); + + // Write each file to the tar. + string[] filenames = Directory.GetFiles(src); + foreach (string filename in filenames) + { + tarEntry = TarEntry.CreateEntryFromFile(filename); + + tarEntry.TarHeader.UserId = 1000; + tarEntry.TarHeader.GroupId = 1000; + tarArchive.WriteEntry(tarEntry, true); + + } + string[] directories = Directory.GetDirectories(src); + foreach (string directory in directories) + DirectoryContents(tarArchive, directory); + } +} + +public class Target(string real, string pretty) +{ + public string Real => real; + + public string Pretty => pretty; + + public override string ToString() + { + return Pretty; + } +} \ No newline at end of file diff --git a/Views/AddProjectReferenceView.axaml b/Views/AddProjectReferenceView.axaml new file mode 100644 index 0000000..ffad591 --- /dev/null +++ b/Views/AddProjectReferenceView.axaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + +