diff --git a/src/DebiaNetApp/.editorconfig b/Trash/DebiaNetApp/.editorconfig similarity index 100% rename from src/DebiaNetApp/.editorconfig rename to Trash/DebiaNetApp/.editorconfig diff --git a/src/DebiaNetApp/.gitignore b/Trash/DebiaNetApp/.gitignore similarity index 100% rename from src/DebiaNetApp/.gitignore rename to Trash/DebiaNetApp/.gitignore diff --git a/src/DebiaNetApp/Commands/DockerMenuCommand.cs b/Trash/DebiaNetApp/Commands/DockerMenuCommand.cs similarity index 93% rename from src/DebiaNetApp/Commands/DockerMenuCommand.cs rename to Trash/DebiaNetApp/Commands/DockerMenuCommand.cs index bc41e71..0e61490 100644 --- a/src/DebiaNetApp/Commands/DockerMenuCommand.cs +++ b/Trash/DebiaNetApp/Commands/DockerMenuCommand.cs @@ -12,6 +12,12 @@ internal sealed class DockerMenuCommand : AsyncCommand { public override async Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) { + if (!DependencyChecker.IsDockerInstalled()) + { + Ui.Error(Resources.Error_DockerNotInstalled); + return ExitCodes.MissingDependency; + } + var selections = new SelectorMenuItem[] { new JsonCliMenuItem(Resources.Menu_Docker_ListRunningContainers, $"{Emoji.Known.Scroll} ") diff --git a/src/DebiaNetApp/Commands/MainMenuCommand.cs b/Trash/DebiaNetApp/Commands/MainMenuCommand.cs similarity index 100% rename from src/DebiaNetApp/Commands/MainMenuCommand.cs rename to Trash/DebiaNetApp/Commands/MainMenuCommand.cs diff --git a/src/DebiaNetApp/DebiaNetApp.csproj b/Trash/DebiaNetApp/DebiaNetApp.csproj similarity index 68% rename from src/DebiaNetApp/DebiaNetApp.csproj rename to Trash/DebiaNetApp/DebiaNetApp.csproj index e71d386..55f2457 100644 --- a/src/DebiaNetApp/DebiaNetApp.csproj +++ b/Trash/DebiaNetApp/DebiaNetApp.csproj @@ -6,6 +6,16 @@ enable enable true + Webmaster442 + https://github.com/webmaster442/DebiaNet + https://github.com/webmaster442/DebiaNet + git + True + debianet + True + + $([System.DateTime]::UtcNow.ToString("yyyy")).$([System.DateTime]::UtcNow.ToString("MM")).$([System.DateTime]::UtcNow.ToString("dd")).0 + diff --git a/src/DebiaNetApp/Dto/DockerContainer.cs b/Trash/DebiaNetApp/Dto/DockerContainer.cs similarity index 100% rename from src/DebiaNetApp/Dto/DockerContainer.cs rename to Trash/DebiaNetApp/Dto/DockerContainer.cs diff --git a/src/DebiaNetApp/Dto/DockerImage.cs b/Trash/DebiaNetApp/Dto/DockerImage.cs similarity index 100% rename from src/DebiaNetApp/Dto/DockerImage.cs rename to Trash/DebiaNetApp/Dto/DockerImage.cs diff --git a/src/DebiaNetApp/Dto/DockerPlatform.cs b/Trash/DebiaNetApp/Dto/DockerPlatform.cs similarity index 100% rename from src/DebiaNetApp/Dto/DockerPlatform.cs rename to Trash/DebiaNetApp/Dto/DockerPlatform.cs diff --git a/src/DebiaNetApp/Dto/DockerPs.cs b/Trash/DebiaNetApp/Dto/DockerPs.cs similarity index 100% rename from src/DebiaNetApp/Dto/DockerPs.cs rename to Trash/DebiaNetApp/Dto/DockerPs.cs diff --git a/Trash/DebiaNetApp/ExitCodes.cs b/Trash/DebiaNetApp/ExitCodes.cs new file mode 100644 index 0000000..bed6dcf --- /dev/null +++ b/Trash/DebiaNetApp/ExitCodes.cs @@ -0,0 +1,9 @@ +namespace DebiaNetApp; + +internal static class ExitCodes +{ + public const int NotLinux = ushort.MaxValue; + public const int SudoUser = 1; + public const int Success = 0; + public const int MissingDependency = 2; +} \ No newline at end of file diff --git a/src/DebiaNetApp/Infrastructure/CliCommand.cs b/Trash/DebiaNetApp/Infrastructure/CliCommand.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/CliCommand.cs rename to Trash/DebiaNetApp/Infrastructure/CliCommand.cs diff --git a/src/DebiaNetApp/Infrastructure/CliMenuItem.cs b/Trash/DebiaNetApp/Infrastructure/CliMenuItem.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/CliMenuItem.cs rename to Trash/DebiaNetApp/Infrastructure/CliMenuItem.cs diff --git a/src/DebiaNetApp/Infrastructure/DebugInterceptor.cs b/Trash/DebiaNetApp/Infrastructure/DebugInterceptor.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/DebugInterceptor.cs rename to Trash/DebiaNetApp/Infrastructure/DebugInterceptor.cs diff --git a/src/DebiaNetApp/Infrastructure/DelegateMenuItem.cs b/Trash/DebiaNetApp/Infrastructure/DelegateMenuItem.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/DelegateMenuItem.cs rename to Trash/DebiaNetApp/Infrastructure/DelegateMenuItem.cs diff --git a/Trash/DebiaNetApp/Infrastructure/DependencyChecker.cs b/Trash/DebiaNetApp/Infrastructure/DependencyChecker.cs new file mode 100644 index 0000000..d035d61 --- /dev/null +++ b/Trash/DebiaNetApp/Infrastructure/DependencyChecker.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; + +namespace DebiaNetApp.Infrastructure; + +internal static class DependencyChecker +{ + public static bool IsInstalled(string appBinary, params string[] arguments) + { + try + { + using var process = new Process(); + process.StartInfo.FileName = appBinary; + for (int i = 0; i < arguments.Length; i++) + { + process.StartInfo.ArgumentList.Add(arguments[i]); + } + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + process.Start(); + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + public static bool IsDockerInstalled() + => IsInstalled("docker", "--version"); + + public static bool IsAptAvailable() + => IsInstalled("apt-get", "--version"); +} diff --git a/src/DebiaNetApp/Infrastructure/JsonCliMenuItem.cs b/Trash/DebiaNetApp/Infrastructure/JsonCliMenuItem.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/JsonCliMenuItem.cs rename to Trash/DebiaNetApp/Infrastructure/JsonCliMenuItem.cs diff --git a/src/DebiaNetApp/Infrastructure/Linux.cs b/Trash/DebiaNetApp/Infrastructure/Linux.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/Linux.cs rename to Trash/DebiaNetApp/Infrastructure/Linux.cs diff --git a/src/DebiaNetApp/Infrastructure/SelectorMenuItem.cs b/Trash/DebiaNetApp/Infrastructure/SelectorMenuItem.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/SelectorMenuItem.cs rename to Trash/DebiaNetApp/Infrastructure/SelectorMenuItem.cs diff --git a/src/DebiaNetApp/Infrastructure/Ui.cs b/Trash/DebiaNetApp/Infrastructure/Ui.cs similarity index 100% rename from src/DebiaNetApp/Infrastructure/Ui.cs rename to Trash/DebiaNetApp/Infrastructure/Ui.cs diff --git a/src/DebiaNetApp/Program.cs b/Trash/DebiaNetApp/Program.cs similarity index 84% rename from src/DebiaNetApp/Program.cs rename to Trash/DebiaNetApp/Program.cs index c637d70..61c00b9 100644 --- a/src/DebiaNetApp/Program.cs +++ b/Trash/DebiaNetApp/Program.cs @@ -17,7 +17,13 @@ if (Linux.IsRunningWithElevatedPriviliges()) { Ui.Error(Resources.Error_SudoUser); - return ExitCodes.NotLinux; + return ExitCodes.SudoUser; +} + +if (!DependencyChecker.IsAptAvailable()) +{ + Ui.Error(Resources.Error_NotAptDistro); + return ExitCodes.MissingDependency; } var app = new CommandApp(); diff --git a/src/DebiaNetApp/Properties/Resources.Designer.cs b/Trash/DebiaNetApp/Properties/Resources.Designer.cs similarity index 93% rename from src/DebiaNetApp/Properties/Resources.Designer.cs rename to Trash/DebiaNetApp/Properties/Resources.Designer.cs index 0ee05aa..6ffa6cd 100644 --- a/src/DebiaNetApp/Properties/Resources.Designer.cs +++ b/Trash/DebiaNetApp/Properties/Resources.Designer.cs @@ -60,6 +60,24 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to This function of the app requires docker to be installed.. + /// + public static string Error_DockerNotInstalled { + get { + return ResourceManager.GetString("Error_DockerNotInstalled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your distribution doesn't use apt as package manager. This app only supports apt based distributions. + /// + public static string Error_NotAptDistro { + get { + return ResourceManager.GetString("Error_NotAptDistro", resourceCulture); + } + } + /// /// Looks up a localized string similar to This application is only supported on Linux systems.. /// diff --git a/src/DebiaNetApp/Properties/Resources.resx b/Trash/DebiaNetApp/Properties/Resources.resx similarity index 96% rename from src/DebiaNetApp/Properties/Resources.resx rename to Trash/DebiaNetApp/Properties/Resources.resx index 4ada2bc..8c42f75 100644 --- a/src/DebiaNetApp/Properties/Resources.resx +++ b/Trash/DebiaNetApp/Properties/Resources.resx @@ -186,4 +186,10 @@ Value + + This function of the app requires docker to be installed. + + + Your distribution doesn't use apt as package manager. This app only supports apt based distributions + \ No newline at end of file diff --git a/src/DebiaNetApp/Tasks.cs b/Trash/DebiaNetApp/Tasks.cs similarity index 100% rename from src/DebiaNetApp/Tasks.cs rename to Trash/DebiaNetApp/Tasks.cs diff --git a/changelog.md b/changelog.md index ffb8170..1813045 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,18 @@ # Changelog +## 2026.06.21 +- New DebiaNet app +- Removed .NET tools from base install, can be installed via the DebiaNet app + +## 2026.04.04 - Version 26.04 +- Based on Debian +- Added byobu to default install +- Added just to default install +- Added jq to install +- Added Infer# to install +- .NET 8.0.25 +- .NET 9.0.14 +- .NET 10.0.5 + ## 2025.12.27 - Initial release - Initial 'beta' release for testing \ No newline at end of file diff --git a/readme.md b/readme.md index 3995a1a..6adddac 100644 --- a/readme.md +++ b/readme.md @@ -31,37 +31,21 @@ The complete list of changes can be found here: [changelog](changelog.md) - [DevToys](https://devtoys.app/) - [Dive](https://github.com/wagoodman/dive) - [docker](https://www.docker.com/) -- [FastFetch](https://github.com/fastfetch-cli/fastfetch) - [git](https://git-scm.com/) with [git-lfs](https://git-lfs.com/) - [htop](https://htop.dev/) -- [lazygit](https://github.com/jesseduffield/lazygit) +- [just](https://github.com/casey/just) - [openssl](https://www.openssl.org/) - [ripgrep](https://github.com/BurntSushi/ripgrep) - [strace](https://strace.io/) - [tmux](https://github.com/tmux/tmux/wiki) -- [Visual studio remote shell](https://learn.microsoft.com/en-us/visualstudio/debugger/remote-debugging?view=visualstudio) - [wget](https://www.gnu.org/software/wget/) +- [jq](https://jqlang.org/) +- [byobu](https://www.byobu.org/) -**.NET tools from microsoft** - -- [dotnet-counters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters) -- [dotnet-coverage](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/dotnet-coverage) -- [dotnet-ef](https://learn.microsoft.com/en-us/ef/core/cli/dotnet) -- [dotnet-gcdump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-gcdump) -- [dotnet-monitor](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-monitor) -- [dotnet-stack](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-stack) -- [dotnet-symbol](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-symbol) -- [dotnet-trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace) -- [powershell](https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell-on-linux) -- [sngen](https://github.com/microsoft/slngen) -- [upgrade-assistant](https://learn.microsoft.com/en-us/dotnet/core/porting/upgrade-assistant-overview) -- [DocFx](https://dotnet.github.io/docfx/index.html) - -**3rd party .NET tools** +**Apps** -- [csharprepl](https://github.com/waf/CSharpRepl) -- [ilspycmd](github.com/icsharpcode/ILSpy) -- [roslynator](https://github.com/dotnet/roslynator?tab=readme-ov-file#command-line-tool) +- [Visual studio remote shell](https://learn.microsoft.com/en-us/visualstudio/debugger/remote-debugging?view=visualstudio) +- [Infer#](https://github.com/microsoft/infersharp) ## Installation @@ -87,11 +71,10 @@ wsl --set-default-version 2 1. Install debian: `wsl --install Debian` 2. Install user: `user` with password: `pass` -3. Run install.sh: `./src/install.sh` this will do most of the installing of software -4. Run branding.sh: `sudo ./src/branding.sh` to do branding -5. Run install-app.sh: `sudo ./src/install-app.sh` to install the debianet app. -5. exit: `exit` -6. do a shutdown: `wsl --shutdown` -7. export: `wsl --export Debian --format tar.xz debiannet.wsl` -8. unregister debian: `wsl --unregister Debian` -9. reinstall debianet.wsl \ No newline at end of file +3. Run commands: + +```bash +cd src +./install.sh +./copy-files.sh +``` \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..3b15c69 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e +# but not directories ending in .e2e +!*.e2e/ + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/src/App/.editorconfig b/src/App/.editorconfig new file mode 100644 index 0000000..af3d700 --- /dev/null +++ b/src/App/.editorconfig @@ -0,0 +1,389 @@ +root = true + +# All files +[*] +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 + +# Xml project files +[*.{csproj,fsproj,vbproj,proj,slnx}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +[*.json] +indent_size = 2 + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +tab_width = 4 + +# New line preferences +insert_final_newline = false + +#### .NET Coding Conventions #### +[*.{cs,vb}] + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### +[*.cs] + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_anonymous_function = true:suggestion +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### +[*.{cs,vb}] + +# Naming rules + +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + diff --git a/src/App/Abstractions/Application.cs b/src/App/Abstractions/Application.cs new file mode 100644 index 0000000..b0e5997 --- /dev/null +++ b/src/App/Abstractions/Application.cs @@ -0,0 +1,185 @@ +using Debianet.Properties; +using Debianet.Ui; + +using Spectre.Console; + +namespace Debianet.Abstractions; + +internal sealed class Application : IApplication +{ + private sealed class ConsoleCancellationTokenSource : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + + public ConsoleCancellationTokenSource() + { + _cancellationTokenSource = new CancellationTokenSource(); + Console.CancelKeyPress += OnCancelKeyPress; + } + + public CancellationToken Token + => _cancellationTokenSource.Token; + + private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + _cancellationTokenSource.Cancel(); + e.Cancel = true; + } + + public void Dispose() + { + Console.CancelKeyPress -= OnCancelKeyPress; + _cancellationTokenSource.Dispose(); + } + } + + private readonly Stack _menuStack; + private readonly ITerminal _terminal; + private readonly Menu _mainMenu; + private readonly Dictionary _icons; + private Menu? _currentMenu; + + public Application(ITerminal terminal, MenuRegistry menuRegistry) + { + _menuStack = new Stack(); + _terminal = terminal; + _mainMenu = menuRegistry.Main; + _icons = new Dictionary + { + { 'a', "🅰" }, + { 'b', "🅱" }, + { 'c', "🅲" }, + { 'd', "🅳" }, + { 'e', "🅴" }, + { 'f', "🅵" }, + { 'g', "🅶" }, + { 'h', "🅷" }, + { 'i', "🅸" }, + { 'j', "🅹" }, + { 'k', "🅺" }, + { 'l', "🅻" }, + { 'm', "🅼" }, + { 'n', "🅽" }, + { 'o', "🅾" }, + { 'p', "🅿" }, + { 'q', "🆀" }, + { 'r', "🆁" }, + { 's', "🆂" }, + { 't', "🆃" }, + { 'u', "🆄" }, + { 'v', "🆅" }, + { 'w', "🆆" }, + { 'x', "🆇" }, + { 'y', "🆈" }, + { 'z', "🆉" } + }; + } + + private Menu GetMenu() + { + return _menuStack.Count > 0 + ? _menuStack.Pop() + : _mainMenu; + } + + public void SwitchMenu(Menu menu) + { + if (_currentMenu != null) + { + _menuStack.Push(_currentMenu); + } + _menuStack.Push(menu); + } + + public async Task Run() + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + + while (true) + { + _currentMenu = GetMenu(); + _terminal.SwitchToAlternateBuffer(); + _terminal.Clear(); + try + { + _currentMenu.BeforeSelection(); + if (_currentMenu is MultiSelectMenu multiSelectMenu) + await DoMultiSelectionMenu(multiSelectMenu); + else + await DoSingleSelectionMenu(_currentMenu); + } + catch (Exception ex) + { + _terminal.DisplayException(ex); + } + } + } + + private async Task DoMultiSelectionMenu(MultiSelectMenu menu) + { + static List OnCancel() + => []; + + var prompt = new MultiSelectionPrompt() + .Title(menu.Title) + .UseConverter(MenuitemConverter) + .AddCancelResult(OnCancel) + .PageSize(GetPageSize()) + .InstructionsText(Resources.App_MultiSelectionInstructions) + .AddChoices(menu.Items); + + List selections = AnsiConsole.Prompt(prompt); + + using (var tokenSource = new ConsoleCancellationTokenSource()) + { + _terminal.SwitchToMainBuffer(); + _terminal.Clear(); + await menu.ProcessSelectedItems(selections, tokenSource.Token); + } + } + + private async Task DoSingleSelectionMenu(Menu menu) + { + static MenuItemBase OnCancel() + { + return new DelegateMenuItem() + { + Action = () => { }, + Text = "Cancel" + }; + } + + var prompt = new SelectionPrompt() + .Title(menu.Title) + .UseConverter(MenuitemConverter) + .AddCancelResult(OnCancel) + .PageSize(GetPageSize()) + .AddChoices(menu.Items); + + prompt.SearchEnabled = true; + var selection = AnsiConsole.Prompt(prompt); + + using (var tokenSource = new ConsoleCancellationTokenSource()) + { + _terminal.SwitchToMainBuffer(); + _terminal.Clear(); + await selection.Execute(this, tokenSource.Token); + } + } + + private static int GetPageSize() + { + int size = Console.WindowHeight - Console.CursorTop - 2; + return size < 1 ? 1 : size; + } + + private string MenuitemConverter(MenuItemBase item) + { + if (item.Icon != null) + { + return $"{item.Icon} {item.Text}"; + } + var firstChar = char.ToLower(item.Text[0]); + return $"{_icons[firstChar]} {item.Text}"; + } +} diff --git a/src/App/Abstractions/DockerClient.cs b/src/App/Abstractions/DockerClient.cs new file mode 100644 index 0000000..79ffd8b --- /dev/null +++ b/src/App/Abstractions/DockerClient.cs @@ -0,0 +1,53 @@ +using System.Net.Sockets; + +namespace Debianet.Abstractions; + +internal sealed class DockerClient : IDisposable, IDockerClient +{ + private const string SockerPath = "/var/run/docker.sock"; + private readonly SocketsHttpHandler _handler; + private readonly HttpClient _client; + private bool _disposed; + + public static bool IsDockerInstalled() + => File.Exists(SockerPath); + + public DockerClient() + { + _handler = new SocketsHttpHandler + { + ConnectCallback = async (context, cancellationToken) => + { + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + + await socket.ConnectAsync( + new UnixDomainSocketEndPoint(SockerPath), + cancellationToken); + + return new NetworkStream(socket, ownsSocket: true); + } + }; + _client = new HttpClient(_handler); + _client.BaseAddress = new Uri("http://localhost/v1.54/"); + } + + public async Task IsAccessible() + { + try + { + var response = await _client.GetAsync("_ping"); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) + { + return false; + } + } + + public void Dispose() + { + _client.Dispose(); + _handler.Dispose(); + _disposed = true; + } +} diff --git a/src/App/Abstractions/IApplication.cs b/src/App/Abstractions/IApplication.cs new file mode 100644 index 0000000..3a17c3a --- /dev/null +++ b/src/App/Abstractions/IApplication.cs @@ -0,0 +1,8 @@ +using Debianet.Ui; + +namespace Debianet.Abstractions; + +internal interface IApplication +{ + void SwitchMenu(Menu menu); +} diff --git a/src/App/Abstractions/IDockerClient.cs b/src/App/Abstractions/IDockerClient.cs new file mode 100644 index 0000000..a77b178 --- /dev/null +++ b/src/App/Abstractions/IDockerClient.cs @@ -0,0 +1,6 @@ +namespace Debianet.Abstractions; + +internal interface IDockerClient +{ + Task IsAccessible(); +} \ No newline at end of file diff --git a/src/App/Abstractions/ITerminal.cs b/src/App/Abstractions/ITerminal.cs new file mode 100644 index 0000000..a7bafde --- /dev/null +++ b/src/App/Abstractions/ITerminal.cs @@ -0,0 +1,24 @@ +using Debianet.Ui; + +using Spectre.Console.Rendering; + +namespace Debianet.Abstractions; + +internal interface ITerminal +{ + void SwitchToAlternateBuffer(); + void SwitchToMainBuffer(); + void Clear(); + void DisplayException(Exception ex); + void StandardOutput(string stdOut); + void StandardError(string stdErr); + void FigletText(string text); + void Info(FormattableString formattableString); + void Warning(FormattableString formattableString); + void Success(FormattableString formattableString); + void Error(FormattableString formattableString); + void WaitKey(); + void Line(); + void ShowMessageBox(MessageBox messageBox); + void ShowDialog(string dialogTitle, IRenderable content); +} diff --git a/src/App/Abstractions/Icons.cs b/src/App/Abstractions/Icons.cs new file mode 100644 index 0000000..cc72e5f --- /dev/null +++ b/src/App/Abstractions/Icons.cs @@ -0,0 +1,14 @@ +namespace Debianet.Abstractions; + +internal static class Icons +{ + public const string SubMenu = "↪️"; + public const string Back = "🔙"; + public const string Package = "📦"; + public const string Door = "🚪"; + public const string Info = "ℹ️"; + public const string Refresh = "🔄"; + public const string Trash = "🗑️"; + public const string Text = "📄"; + public const string Computer = "💻"; +} diff --git a/src/App/Abstractions/Palete.cs b/src/App/Abstractions/Palete.cs new file mode 100644 index 0000000..4fc7034 --- /dev/null +++ b/src/App/Abstractions/Palete.cs @@ -0,0 +1,21 @@ +using Spectre.Console; + +namespace Debianet.Abstractions; + +internal sealed class Palete +{ + public required Color Accent { get; init; } + public required Color Warning { get; init; } + public required Color Error { get; init; } + public required Color Info { get; init; } + public required Color Success { get; init; } + + public static readonly Palete Dracula = new() + { + Accent = Color.FromHex("#F8F8F2"), + Error = Color.FromHex("#FF5555"), + Warning = Color.FromHex("#F1FA8C"), + Info = Color.FromHex("#BD93F9"), + Success = Color.FromHex("#50FA7B") + }; +} diff --git a/src/App/Abstractions/StringBuilderBuffer.cs b/src/App/Abstractions/StringBuilderBuffer.cs new file mode 100644 index 0000000..0683d41 --- /dev/null +++ b/src/App/Abstractions/StringBuilderBuffer.cs @@ -0,0 +1,41 @@ +using System.Text; + +namespace Debianet.Abstractions; + +internal static class StringBuilderBuffer +{ + private static readonly StringBuilder _buffer = new StringBuilder(4096); + + public static StringBuilder GetBuffer() + { + return _buffer; + } + + public static string GetStringAndClear(int lines) + { + if (lines < 0) + { + string result = _buffer.ToString(); + _buffer.Clear(); + return result; + } + + for (int i=0; i<_buffer.Length; i++) + { + if (_buffer[i] == '\n') + { + lines--; + if (lines == 0) + { + string result = _buffer.ToString(0, i); + _buffer.Clear(); + return result; + } + } + } + + string remaining = _buffer.ToString(); + _buffer.Clear(); + return remaining; + } +} diff --git a/src/App/Abstractions/SystemInfoCollector.cs b/src/App/Abstractions/SystemInfoCollector.cs new file mode 100644 index 0000000..469995f --- /dev/null +++ b/src/App/Abstractions/SystemInfoCollector.cs @@ -0,0 +1,48 @@ +using System.Text.Json; + +using CliWrap; + +using Debianet.Dto; +using Debianet.Properties; + +namespace Debianet.Abstractions; + +internal static class SystemInfoCollector +{ + private static async Task RunAndGetStdout(string command, string[] arguments, int lines = -1) + { + if (command.StartsWith('$')) + { + command = Environment.GetEnvironmentVariable(command[1..]) ?? command; + } + + await Cli.Wrap(command) + .WithArguments(arguments) + .WithStandardOutputPipe(PipeTarget.ToStringBuilder(StringBuilderBuffer.GetBuffer())) + .ExecuteAsync(); + + return StringBuilderBuffer.GetStringAndClear(lines); + } + + + + public static async IAsyncEnumerable<(string property, string value)> CollectSystemInfoAsync() + { + yield return (Resources.SysInfo_HostName, Environment.MachineName); + yield return (Resources.SysInfo_Cpu, await GetCpuData()); + yield return (Resources.SysInfo_KernelVersion, await RunAndGetStdout("uname", ["-s", "-r"], 1)); + yield return (Resources.SysInfo_Uptime, await RunAndGetStdout("uptime", ["-p"], 1)); + yield return (Resources.SysInfo_Shell, await RunAndGetStdout("$SHELL", ["--version"], 1)); + yield return (Resources.SysInfo_Memory, await RunAndGetStdout("free", ["-h"])); + yield return (Resources.SysInfo_Disks, await RunAndGetStdout("df", ["-h", "-x", "tmpfs", "-x", "overlay", "-x", "devtmpfs", "--output=target,size,avail,pcent"])); + } + + private static async Task GetCpuData() + { + var lscpuJson = await RunAndGetStdout("lscpu", ["-J"]); + LsCpuResult? result = JsonSerializer.Deserialize(lscpuJson, JsonSerializerOptions.Web); + var cpuModel = result?.Lscpu.FirstOrDefault(x => x.Field == LsCpuResult.ModelName)?.Data; + return cpuModel != null ? cpuModel : "n/a"; + } +} + diff --git a/src/App/Abstractions/Terminal.cs b/src/App/Abstractions/Terminal.cs new file mode 100644 index 0000000..ccb49cf --- /dev/null +++ b/src/App/Abstractions/Terminal.cs @@ -0,0 +1,102 @@ +using Debianet.Ui; + +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace Debianet.Abstractions; + +internal sealed class Terminal : ITerminal +{ + private readonly Palete _palete; + + public Terminal(Palete? palete = null) + { + _palete = palete ?? Palete.Dracula; + } + + public void Clear() + => AnsiConsole.Clear(); + + public void DisplayException(Exception ex) + => AnsiConsole.WriteException(ex); + + public void Info(FormattableString formattableString) + => AnsiConsole.MarkupLine($"[#{_palete.Info.ToHex()}]{formattableString}[/]"); + + public void Warning(FormattableString formattableString) + => AnsiConsole.MarkupLine($"[#{_palete.Warning.ToHex()}]{formattableString}[/]"); + + public void Success(FormattableString formattableString) + => AnsiConsole.MarkupLine($"[#{_palete.Success.ToHex()}]{formattableString}[/]"); + + public void Error(FormattableString formattableString) + => AnsiConsole.MarkupLine($"[#{_palete.Error.ToHex()}]{formattableString}[/]"); + + public void StandardError(string stdErr) + => AnsiConsole.MarkupLine($"[#{_palete.Error.ToHex()}]{stdErr}[/]"); + + public void StandardOutput(string stdOut) + => AnsiConsole.WriteLine(stdOut); + + public void SwitchToAlternateBuffer() + => AnsiConsole.WriteLine("\e[?1049h"); + + public void SwitchToMainBuffer() + => AnsiConsole.WriteLine("\e[?1049l"); + + public void WaitKey() + { + AnsiConsole.MarkupLine($"[#{_palete.Accent.ToHex()}]Press any key to continue...[/]"); + Console.ReadKey(); + } + + public void Line() + => AnsiConsole.Write(new Rule()); + + public void FigletText(string text) + { + var figlet = new FigletText(text) + .Color(_palete.Info) + .Justify(Justify.Left); + + AnsiConsole.Write(figlet); + } + + public void ShowMessageBox(MessageBox messageBox) + { + SwitchToAlternateBuffer(); + Clear(); + + int topPad = (Console.WindowHeight / 2) - 4; + + Console.SetCursorPosition(0, topPad); + + var panel = new Panel(messageBox.Message) + .Header($"| {messageBox.Title} |", Justify.Center) + .HeavyBorder() + .BorderColor(_palete.Accent) + .Expand(); + + AnsiConsole.Write(panel); + + WaitKey(); + SwitchToMainBuffer(); + } + + public void ShowDialog(string dialogTitle, IRenderable content) + { + SwitchToAlternateBuffer(); + Clear(); + + var panel = new Panel(content) + .Header($"| {dialogTitle} |", Justify.Center) + .HeavyBorder() + .BorderColor(_palete.Accent) + .Expand(); + + AnsiConsole.Write(panel); + + WaitKey(); + SwitchToMainBuffer(); + } +} \ No newline at end of file diff --git a/src/App/AppVersionProvider.cs b/src/App/AppVersionProvider.cs new file mode 100644 index 0000000..e9a10d2 --- /dev/null +++ b/src/App/AppVersionProvider.cs @@ -0,0 +1,12 @@ +namespace Debianet; + +internal static class AppVersionProvider +{ + public static Version GetAppVersion() + { + Version? version = typeof(AppVersionProvider) + .Assembly.GetName().Version; + + return version ?? new Version(0, 0, 0, 0); + } +} diff --git a/src/App/Debianet.csproj b/src/App/Debianet.csproj new file mode 100644 index 0000000..95c9e3e --- /dev/null +++ b/src/App/Debianet.csproj @@ -0,0 +1,53 @@ + + + + Exe + net10.0 + enable + enable + true + $([System.DateTime]::UtcNow.ToString("yyyy")).$([System.DateTime]::UtcNow.ToString("MM")).$([System.DateTime]::UtcNow.ToString("dd")).0 + false + ..\bin\$(Configuration)\ + True + Webmaster442 + https://github.com/webmaster442/DebiaNet + debianet-128.png + https://github.com/webmaster442/DebiaNet + True + debianet + + + + + + + + + True + \ + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + True + True + Resources.resx + + + + diff --git a/src/App/Debianet.slnx b/src/App/Debianet.slnx new file mode 100644 index 0000000..a76b2dc --- /dev/null +++ b/src/App/Debianet.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/App/Dto/LsCpuResult.cs b/src/App/Dto/LsCpuResult.cs new file mode 100644 index 0000000..8fcea73 --- /dev/null +++ b/src/App/Dto/LsCpuResult.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace Debianet.Dto; + +public class LsCpuResult +{ + [JsonPropertyName("lscpu")] + public required FieldData[] Lscpu { get; init; } + + public class FieldData + { + [JsonPropertyName("field")] + public required string Field { get; init; } + + [JsonPropertyName("data")] + public required string Data { get; init; } + + public override string ToString() + { + return $"{Field}: {Data}"; + } + } + + public const string ModelName = "Model name:"; +} diff --git a/src/DebiaNetApp/ExitCodes.cs b/src/App/ExitCodes.cs similarity index 85% rename from src/DebiaNetApp/ExitCodes.cs rename to src/App/ExitCodes.cs index a5ae340..fdb0853 100644 --- a/src/DebiaNetApp/ExitCodes.cs +++ b/src/App/ExitCodes.cs @@ -1,4 +1,4 @@ -namespace DebiaNetApp; +namespace Debianet; internal static class ExitCodes { diff --git a/src/App/GlobalArgHandler.cs b/src/App/GlobalArgHandler.cs new file mode 100644 index 0000000..7448dae --- /dev/null +++ b/src/App/GlobalArgHandler.cs @@ -0,0 +1,27 @@ +using Spectre.Console; + +namespace Debianet; + +internal static class GlobalArgHandler +{ + public static void HandleGlobalArgs(string[] args) + { + HashSet argsSet = new HashSet(args, StringComparer.InvariantCultureIgnoreCase); + + if (argsSet.Contains("--wd") || argsSet.Contains("--wait-debugger")) + { + AnsiConsole.WriteLine("Waiting for debugger to attach..."); + while (!System.Diagnostics.Debugger.IsAttached) + { + Thread.Sleep(100); + } + AnsiConsole.WriteLine("Debugger attached."); + } + + if (argsSet.Contains("--version") || argsSet.Contains("-v")) + { + AnsiConsole.WriteLine(AppVersionProvider.GetAppVersion().ToString()); + Environment.Exit(0); + } + } +} diff --git a/src/App/MenuRegistry.cs b/src/App/MenuRegistry.cs new file mode 100644 index 0000000..5749b3a --- /dev/null +++ b/src/App/MenuRegistry.cs @@ -0,0 +1,22 @@ +using Debianet.Abstractions; +using Debianet.Menus; + +namespace Debianet; + +internal class MenuRegistry +{ + public MenuRegistry(ITerminal terminal, DockerClient dockerClient) + { + Main = new MainMenu(this, terminal); + Dotnet = new DotnetMenu(this, terminal); + DotnetToolInstaller = new DotnetToolsInstallerMenu(this, terminal); + System = new SystemMenu(this, terminal); + Docker = new DockerMenu(this, dockerClient); + } + + public MainMenu Main { get; } + public DotnetMenu Dotnet { get; } + public DotnetToolsInstallerMenu DotnetToolInstaller { get; } + public SystemMenu System { get; } + public DockerMenu Docker { get; } +} diff --git a/src/App/Menus/DockerMenu.cs b/src/App/Menus/DockerMenu.cs new file mode 100644 index 0000000..d56bdf1 --- /dev/null +++ b/src/App/Menus/DockerMenu.cs @@ -0,0 +1,34 @@ +using Debianet.Abstractions; +using Debianet.Properties; +using Debianet.Ui; + +namespace Debianet.Menus; + +internal class DockerMenu : Menu +{ + private readonly ITerminal _terminal; + private readonly IDockerClient _dockerClient; + + public DockerMenu(MenuRegistry menuRegistry, ITerminal terminal, IDockerClient dockerClient) + : base(menuRegistry) + { + _terminal = terminal; + _dockerClient = dockerClient; + } + + public override string Title + => Resources.DockerMenu_Title; + + public override IEnumerable Items + { + get + { + yield return new ReplaceMenuMenuItem + { + Icon = Icons.Back, + Text = Resources.Menu_Item_Back, + Submenu = MenuRegistry.Main + }; + } + } +} diff --git a/src/App/Menus/DotnetMenu.cs b/src/App/Menus/DotnetMenu.cs new file mode 100644 index 0000000..4ae2308 --- /dev/null +++ b/src/App/Menus/DotnetMenu.cs @@ -0,0 +1,86 @@ +using Debianet.Abstractions; +using Debianet.Properties; +using Debianet.Ui; + +using Spectre.Console; + +namespace Debianet.Menus; + +internal class DotnetMenu : Menu +{ + private readonly ITerminal _terminal; + + public DotnetMenu(MenuRegistry menuRegistry, ITerminal terminal) + : base(menuRegistry) + { + _terminal = terminal; + } + + public override string Title + => Resources.DotnetMenu_Title; + + public override void BeforeSelection() + => _terminal.FigletText(".NET"); + + public override IEnumerable Items + { + get + { + yield return new ReplaceMenuMenuItem + { + Icon = Icons.Back, + Text = Resources.Menu_Item_Back, + Submenu = MenuRegistry.Main + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Info, + Text = Resources.DotnetMenu_Item_ListRuntimes, + Program = "dotnet", + Arguments = ["--list-runtimes"] + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Info, + Text = Resources.DotnetMenu_Item_ListSdks, + Program = "dotnet", + Arguments = ["--list-sdks"] + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Info, + Text = Resources.DotnetMenu_Item_ListWorkloads, + Program = "dotnet", + Arguments = ["workload", "list"] + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Info, + Text = Resources.DotnetMenu_Item_ListTools, + Program = "dotnet", + Arguments = ["tool", "list", "-g"] + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Refresh, + Text = Resources.DotnetMenu_Item_UpdateWorkloads, + Program = "dotnet", + Arguments = ["workload", "update"] + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Refresh, + Text = Resources.DotnetMenu_Item_UpdateTools, + Program = "dotnet", + Arguments = ["tool", "update", "-g", "--all"] + }; + yield return new RunCommandMenuItem(_terminal) + { + Icon = Icons.Trash, + Text = Resources.DotnetMenu_Item_ClearNugetCache, + Program = "dotnet", + Arguments = ["nuget", "locals", "all", "--clear"] + }; + } + } +} diff --git a/src/App/Menus/DotnetToolsInstallerMenu.cs b/src/App/Menus/DotnetToolsInstallerMenu.cs new file mode 100644 index 0000000..1d53dd3 --- /dev/null +++ b/src/App/Menus/DotnetToolsInstallerMenu.cs @@ -0,0 +1,89 @@ +using CliWrap; + +using Debianet.Abstractions; +using Debianet.Properties; +using Debianet.Ui; + +using Spectre.Console; + +namespace Debianet.Menus; + +internal class DotnetToolsInstallerMenu : MultiSelectMenu +{ + private readonly ITerminal _terminal; + + public DotnetToolsInstallerMenu(MenuRegistry menuRegistry, ITerminal terminal) + : base(menuRegistry) + { + _terminal = terminal; + } + + public override string Title + => Resources.DotnetToolsInstall_Title; + + private MenuItem CreateToolInstaller(string toolName) + { + return new MenuItem + { + Text = toolName, + Data = toolName, + Icon = Icons.Package + }; + } + + public override void BeforeSelection() + => _terminal.FigletText(".NET tools"); + + public override IEnumerable Items + { + get + { + yield return CreateToolInstaller("dotnet-counters"); + yield return CreateToolInstaller("dotnet-coverage"); + yield return CreateToolInstaller("dotnet-ef"); + yield return CreateToolInstaller("dotnet-counters"); + yield return CreateToolInstaller("dotnet-gcdump"); + yield return CreateToolInstaller("dotnet-monitor"); + yield return CreateToolInstaller("dotnet-stack"); + yield return CreateToolInstaller("dotnet-symbol"); + yield return CreateToolInstaller("dotnet-trace"); + yield return CreateToolInstaller("Microsoft.VisualStudio.SlnGen.Tool"); + yield return CreateToolInstaller("PowerShell"); + yield return CreateToolInstaller("upgrade-assistant"); + yield return CreateToolInstaller("docfx"); + yield return CreateToolInstaller("csharprepl"); + yield return CreateToolInstaller("ilspycmd"); + yield return CreateToolInstaller("roslynator.dotnet.cli"); + yield return CreateToolInstaller("CsProj"); + } + } + + public override async Task ProcessSelectedItems(IReadOnlyList selectedItems, CancellationToken cancellationToken) + { + if (selectedItems.Count == 0) + return; + + int counter = 0; + var items = selectedItems.OfType().ToArray(); + TimeSpan totalTime = TimeSpan.Zero; + + foreach (var item in items) + { + _terminal.Info($"{counter}/{items.Length}: Installing {item.Data}..."); + _terminal.Line(); + + var result = await Cli.Wrap("dotnet") + .WithArguments(["tool", "install", "-g", item.Data]) + .WithStandardOutputPipe(PipeTarget.ToDelegate(_terminal.StandardOutput)) + .WithStandardErrorPipe(PipeTarget.ToDelegate(_terminal.StandardError)) + .ExecuteAsync(cancellationToken); + + _terminal.Line(); + ++counter; + totalTime += result.RunTime; + } + _terminal.Info($"Run time: {totalTime.FormatTime()}"); + _terminal.WaitKey(); + + } +} diff --git a/src/App/Menus/MainMenu.cs b/src/App/Menus/MainMenu.cs new file mode 100644 index 0000000..3fb8a37 --- /dev/null +++ b/src/App/Menus/MainMenu.cs @@ -0,0 +1,126 @@ +using Debianet.Abstractions; +using Debianet.Properties; +using Debianet.Ui; + +using Spectre.Console; + +namespace Debianet.Menus; + +internal class MainMenu : Menu +{ + private readonly ITerminal _terminal; + private readonly IDockerClient _dockerClient; + + public MainMenu(MenuRegistry menuRegistry, ITerminal terminal, IDockerClient dockerClient) + : base(menuRegistry) + { + _terminal = terminal; + _dockerClient = dockerClient; + } + + public override string Title + => Resources.MainMenu_Title; + + public override void BeforeSelection() + { + string text = $$""" + @ + @@@@@@@@@@@@@ + @@@@ @@@@ + @ @@ @@@ @@@@@ @@@ @@@@@@@@@@ @@@@@@@@@@@@@@ + %@ @@@@ @@ @@@@@@ @@@ @@@ @@@ + @@ @@ @@@ @@@ @@@ @@@ @@@ + @ @ @ @@@ @@@@ @@@ @@@ @@@ + @ @ @@ @@@ @@@@ @@@ @@@@@@@@@@ @@@ + @@ @@ @ @@@ @@@@ @@@ @@@ @@@ + @@ :@* @@@ @@@ @@@ @@@ @@@ + @@ @@@ @@@@@@ @@@ @@@ + @@ @@@ @@@@@ @@@@@@@@@@ @@@ + @ + @@ + @@ App Version: {{AppVersionProvider.GetAppVersion()}} + + """; + + _terminal.Info($"{text}"); + } + + public override IEnumerable Items + { + get + { + yield return new ReplaceMenuMenuItem + { + Icon = Icons.SubMenu, + Text = Resources.MainMenu_Item_Dotnet, + Submenu = MenuRegistry.Dotnet + }; + yield return new ReplaceMenuMenuItem + { + Icon = Icons.SubMenu, + Text = Resources.MainMenu_DotnetToolsInstall, + Submenu = MenuRegistry.DotnetToolInstaller + }; + yield return new ReplaceMenuMenuItem + { + Icon = Icons.SubMenu, + Text = Resources.MainMenu_System, + Submenu = MenuRegistry.System, + }; + + if (DockerClient.IsDockerInstalled()) + { + yield return new ReplaceMenuMenuItem + { + Icon = Icons.SubMenu, + Text = Resources.MainMenu_Docker, + Submenu = new DockerMenu(MenuRegistry, _terminal, _dockerClient), + }; + } + + yield return new DelegateTaskMenuItem + { + Icon = Icons.Computer, + Task = SytemInfo, + Text = Resources.MainMenu_SystemInfo, + }; + yield return new DelegateMenuItem + { + Text = Resources.MainMenu_Changelog, + Icon = Icons.Text, + Action = DisplayChangeLog + }; + yield return new DelegateMenuItem() + { + Text = Resources.MainMenu_Item_Exit, + Icon = Icons.Door, + Action = Exit + }; + } + } + + private async Task SytemInfo(CancellationToken token) + { + var grid = new Grid(); + grid.AddColumn(); + grid.AddColumn(); + await foreach (var (property, value) in SystemInfoCollector.CollectSystemInfoAsync()) + { + grid.AddRow(property, value); + } + _terminal.ShowDialog(Resources.Dialog_Sysinfo, grid); + } + + private void DisplayChangeLog() + { + var changelog = ResourceHandler.GetChangeLog(); + var viewer = new TextViewer(changelog, Resources.TextView_Changelog); + viewer.Show(); + } + + private void Exit() + { + _terminal.Clear(); + Environment.Exit(0); + } +} diff --git a/src/App/Menus/SystemMenu.cs b/src/App/Menus/SystemMenu.cs new file mode 100644 index 0000000..009b8c5 --- /dev/null +++ b/src/App/Menus/SystemMenu.cs @@ -0,0 +1,58 @@ +using Debianet.Abstractions; +using Debianet.Properties; +using Debianet.Ui; + +namespace Debianet.Menus; + +internal sealed class SystemMenu : Menu +{ + private readonly ITerminal _terminal; + + public SystemMenu(MenuRegistry menuRegistry, ITerminal terminal) : base(menuRegistry) + { + _terminal = terminal; + } + + public override string Title + => Resources.SystemMenu_Title; + + public override IEnumerable Items + { + get + { + yield return new ReplaceMenuMenuItem + { + Icon = Icons.Back, + Text = Resources.Menu_Item_Back, + Submenu = MenuRegistry.Main + }; + yield return new RunCommandsMenuItem(_terminal) + { + Text = Resources.SystemMenu_AptClean, + Commands = + [ + ("sudo", ["apt", "autoremove"]), + ("sudo", ["apt", "clean"]) + ] + }; + yield return new RunCommandsMenuItem(_terminal) + { + Text = Resources.SystemMenu_Update, + Commands = + [ + ("sudo", ["apt", "update"]), + ("sudo", ["apt", "upgrade", "-y"]) + ] + }; + yield return new RunCommandsMenuItem(_terminal) + { + Text = Resources.SystemMenu_InstallCpp, + Commands = + [ + ("sudo", ["apt", "update"]), + ("sudo", ["apt", "install", "-y", "build-essential", "cmake", "gcc", "g++", "libtool"]) + ] + }; + } + } +} \ No newline at end of file diff --git a/src/App/Program.cs b/src/App/Program.cs new file mode 100644 index 0000000..5b5bbb0 --- /dev/null +++ b/src/App/Program.cs @@ -0,0 +1,20 @@ +using Debianet; +using Debianet.Abstractions; + +var terminal = new Terminal(); +using var dockerClient = new DockerClient(); + +GlobalArgHandler.HandleGlobalArgs(args); + +var startupChecks = new StartupChecks(terminal); +if (startupChecks.TryCheckExit(out int exitCode)) +{ + return exitCode; +} + +var registry = new MenuRegistry(terminal, dockerClient); +var application = new Application(terminal, registry); + +await application.Run(); + +return ExitCodes.Success; \ No newline at end of file diff --git a/src/App/Properties/Resources.Designer.cs b/src/App/Properties/Resources.Designer.cs new file mode 100644 index 0000000..a81bd11 --- /dev/null +++ b/src/App/Properties/Resources.Designer.cs @@ -0,0 +1,387 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Debianet.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Debianet.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to (Press <space> to select, <enter> to accept, <esc> to cancel). + /// + internal static string App_MultiSelectionInstructions { + get { + return ResourceManager.GetString("App_MultiSelectionInstructions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System Information. + /// + internal static string Dialog_Sysinfo { + get { + return ResourceManager.GetString("Dialog_Sysinfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Docker. + /// + internal static string DockerMenu_Title { + get { + return ResourceManager.GetString("DockerMenu_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clear all local NuGet caches.... + /// + internal static string DotnetMenu_Item_ClearNugetCache { + get { + return ResourceManager.GetString("DotnetMenu_Item_ClearNugetCache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List installed .NET runtimes.... + /// + internal static string DotnetMenu_Item_ListRuntimes { + get { + return ResourceManager.GetString("DotnetMenu_Item_ListRuntimes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List installed .NET SDKs.... + /// + internal static string DotnetMenu_Item_ListSdks { + get { + return ResourceManager.GetString("DotnetMenu_Item_ListSdks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List installed global tools.... + /// + internal static string DotnetMenu_Item_ListTools { + get { + return ResourceManager.GetString("DotnetMenu_Item_ListTools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List installed .NET workloads.... + /// + internal static string DotnetMenu_Item_ListWorkloads { + get { + return ResourceManager.GetString("DotnetMenu_Item_ListWorkloads", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update installed global tools.... + /// + internal static string DotnetMenu_Item_UpdateTools { + get { + return ResourceManager.GetString("DotnetMenu_Item_UpdateTools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update installed workloads.... + /// + internal static string DotnetMenu_Item_UpdateWorkloads { + get { + return ResourceManager.GetString("DotnetMenu_Item_UpdateWorkloads", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to .NET commands. + /// + internal static string DotnetMenu_Title { + get { + return ResourceManager.GetString("DotnetMenu_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to .NET tool installer. + /// + internal static string DotnetToolsInstall_Title { + get { + return ResourceManager.GetString("DotnetToolsInstall_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View Changelog.... + /// + internal static string MainMenu_Changelog { + get { + return ResourceManager.GetString("MainMenu_Changelog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Docker.... + /// + internal static string MainMenu_Docker { + get { + return ResourceManager.GetString("MainMenu_Docker", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install .NET Tools.... + /// + internal static string MainMenu_DotnetToolsInstall { + get { + return ResourceManager.GetString("MainMenu_DotnetToolsInstall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to .NET related commands.... + /// + internal static string MainMenu_Item_Dotnet { + get { + return ResourceManager.GetString("MainMenu_Item_Dotnet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exit. + /// + internal static string MainMenu_Item_Exit { + get { + return ResourceManager.GetString("MainMenu_Item_Exit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System.... + /// + internal static string MainMenu_System { + get { + return ResourceManager.GetString("MainMenu_System", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System Info.... + /// + internal static string MainMenu_SystemInfo { + get { + return ResourceManager.GetString("MainMenu_SystemInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Debianet main menu. + /// + internal static string MainMenu_Title { + get { + return ResourceManager.GetString("MainMenu_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Back to main menu. + /// + internal static string Menu_Item_Back { + get { + return ResourceManager.GetString("Menu_Item_Back", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error. + /// + internal static string MessageBox_Error { + get { + return ResourceManager.GetString("MessageBox_Error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This application is only supported on Linux systems.. + /// + internal static string StartupCheck_NotLinux { + get { + return ResourceManager.GetString("StartupCheck_NotLinux", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please run the application as a normal user.. + /// + internal static string StartupCheck_RootUser { + get { + return ResourceManager.GetString("StartupCheck_RootUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Processor. + /// + internal static string SysInfo_Cpu { + get { + return ResourceManager.GetString("SysInfo_Cpu", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disks. + /// + internal static string SysInfo_Disks { + get { + return ResourceManager.GetString("SysInfo_Disks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Host name. + /// + internal static string SysInfo_HostName { + get { + return ResourceManager.GetString("SysInfo_HostName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kernel Version. + /// + internal static string SysInfo_KernelVersion { + get { + return ResourceManager.GetString("SysInfo_KernelVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Memory. + /// + internal static string SysInfo_Memory { + get { + return ResourceManager.GetString("SysInfo_Memory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shell. + /// + internal static string SysInfo_Shell { + get { + return ResourceManager.GetString("SysInfo.Shell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uptime. + /// + internal static string SysInfo_Uptime { + get { + return ResourceManager.GetString("SysInfo_Uptime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clean apt cache.... + /// + internal static string SystemMenu_AptClean { + get { + return ResourceManager.GetString("SystemMenu_AptClean", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install c/c++ dev tools. + /// + internal static string SystemMenu_InstallCpp { + get { + return ResourceManager.GetString("SystemMenu_InstallCpp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System. + /// + internal static string SystemMenu_Title { + get { + return ResourceManager.GetString("SystemMenu_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System update.... + /// + internal static string SystemMenu_Update { + get { + return ResourceManager.GetString("SystemMenu_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Changelog. + /// + internal static string TextView_Changelog { + get { + return ResourceManager.GetString("TextView_Changelog", resourceCulture); + } + } + } +} diff --git a/src/App/Properties/Resources.resx b/src/App/Properties/Resources.resx new file mode 100644 index 0000000..7c66626 --- /dev/null +++ b/src/App/Properties/Resources.resx @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + (Press <space> to select, <enter> to accept, <esc> to cancel) + + + System Information + + + Back to main menu + + + Clear all local NuGet caches... + + + List installed .NET runtimes... + + + List installed .NET SDKs... + + + List installed global tools... + + + List installed .NET workloads... + + + Update installed global tools... + + + Update installed workloads... + + + .NET commands + + + .NET tool installer + + + View Changelog... + + + Install .NET Tools... + + + .NET related commands... + + + Exit + + + System Info... + + + Debianet main menu + + + Error + + + This application is only supported on Linux systems. + + + Please run the application as a normal user. + + + Shell + + + Processor + + + Disks + + + Host name + + + Kernel Version + + + Memory + + + Uptime + + + System + + + Changelog + + + Clean apt cache... + + + System update... + + + Install c/c++ dev tools + + + System... + + + Docker + + + Docker... + + \ No newline at end of file diff --git a/src/App/ResourceHandler.cs b/src/App/ResourceHandler.cs new file mode 100644 index 0000000..3f7d468 --- /dev/null +++ b/src/App/ResourceHandler.cs @@ -0,0 +1,22 @@ +namespace Debianet; + +internal static class ResourceHandler +{ + private static StreamReader GetEmbeddedStream(string resourceName) + { + return new StreamReader(typeof(ResourceHandler).Assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Resource '{resourceName}' not found.")); + } + + public static string GetChangeLog() + { + using var reader = GetEmbeddedStream("Debianet.Properties.changelog.md"); + return reader.ReadToEnd(); + } + + public static string GetReadme() + { + using var reader = GetEmbeddedStream("Debianet.Properties.readme.md"); + return reader.ReadToEnd(); + } +} diff --git a/src/App/StartupChecks.cs b/src/App/StartupChecks.cs new file mode 100644 index 0000000..13e426a --- /dev/null +++ b/src/App/StartupChecks.cs @@ -0,0 +1,61 @@ +using System.Runtime.InteropServices; + +using Debianet.Abstractions; +using Debianet.Properties; +using Debianet.Ui; + +namespace Debianet; + +internal sealed partial class StartupChecks +{ + private readonly Terminal _terminal; + + public StartupChecks(Terminal terminal) + { + _terminal = terminal; + } + + [LibraryImport("libc", EntryPoint = "geteuid")] + private static partial uint GetEUid(); + + private static bool IsRunningWithElevatedPriviliges() + { + try + { + uint euid = GetEUid(); + return euid == 0; + } + catch + { + return string.Equals(Environment.UserName, "root", StringComparison.Ordinal); + } + } + + public bool TryCheckExit(out int exitCode) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _terminal.ShowMessageBox(new MessageBox + { + Title = Resources.MessageBox_Error, + Message = Resources.StartupCheck_NotLinux + }); + exitCode = ExitCodes.NotLinux; + return true; + } + + if (IsRunningWithElevatedPriviliges()) + { + _terminal.ShowMessageBox(new MessageBox + { + Title = Resources.MessageBox_Error, + Message = Resources.StartupCheck_RootUser + }); + exitCode = ExitCodes.SudoUser; + return true; + } + + exitCode = ExitCodes.Success; + return false; + } +} diff --git a/src/App/Ui/DelegateMenuItem.cs b/src/App/Ui/DelegateMenuItem.cs new file mode 100644 index 0000000..2452a8f --- /dev/null +++ b/src/App/Ui/DelegateMenuItem.cs @@ -0,0 +1,14 @@ +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal sealed class DelegateMenuItem : MenuItemBase +{ + public required Action Action { get; init; } + + public override Task Execute(IApplication app, CancellationToken cancellationToken) + { + Action(); + return Task.CompletedTask; + } +} diff --git a/src/App/Ui/DelegateTaskMenuItem.cs b/src/App/Ui/DelegateTaskMenuItem.cs new file mode 100644 index 0000000..ce26fff --- /dev/null +++ b/src/App/Ui/DelegateTaskMenuItem.cs @@ -0,0 +1,13 @@ +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal sealed class DelegateTaskMenuItem : MenuItemBase +{ + public required Func Task { get; init; } + + public override async Task Execute(IApplication app, CancellationToken cancellationToken) + { + await Task(cancellationToken); + } +} \ No newline at end of file diff --git a/src/App/Ui/Extensions.cs b/src/App/Ui/Extensions.cs new file mode 100644 index 0000000..a731754 --- /dev/null +++ b/src/App/Ui/Extensions.cs @@ -0,0 +1,21 @@ +namespace Debianet.Ui; + +internal static class Extensions +{ + public static string FormatTime(this TimeSpan timeSpan) + { + if (timeSpan.TotalMilliseconds < 1000) + { + return $"{timeSpan.TotalMilliseconds:F0} ms"; + } + else if (timeSpan.TotalSeconds < 60) + { + return $"{timeSpan.TotalSeconds:F2} s"; + } + else if (timeSpan.TotalMinutes < 60) + { + return $"{(int)timeSpan.TotalMinutes} minutes {timeSpan.Seconds:D2} seconds"; + } + return $"{(int)timeSpan.TotalHours} hours {timeSpan.Minutes:D2} minutes {timeSpan.Seconds:D2} seconds"; + } +} diff --git a/src/App/Ui/Menu.cs b/src/App/Ui/Menu.cs new file mode 100644 index 0000000..00832c2 --- /dev/null +++ b/src/App/Ui/Menu.cs @@ -0,0 +1,20 @@ +namespace Debianet.Ui; + +internal abstract class Menu +{ + public MenuRegistry MenuRegistry { get; } + + public Menu(MenuRegistry menuRegistry) + { + MenuRegistry = menuRegistry; + } + + public abstract string Title { get; } + + public abstract IEnumerable Items { get; } + + public virtual void BeforeSelection() + { + + } +} diff --git a/src/App/Ui/MenuItem.cs b/src/App/Ui/MenuItem.cs new file mode 100644 index 0000000..7d0235a --- /dev/null +++ b/src/App/Ui/MenuItem.cs @@ -0,0 +1,11 @@ +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal class MenuItem : MenuItemBase +{ + public string Data { get; set; } = ""; + + public override Task Execute(IApplication app, CancellationToken cancellationToken) + => Task.CompletedTask; +} \ No newline at end of file diff --git a/src/App/Ui/MenuItemBase.cs b/src/App/Ui/MenuItemBase.cs new file mode 100644 index 0000000..d1aef25 --- /dev/null +++ b/src/App/Ui/MenuItemBase.cs @@ -0,0 +1,11 @@ +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal abstract class MenuItemBase +{ + public string? Icon { get; init; } + public required string Text { get; init; } + public abstract Task Execute(IApplication app, CancellationToken cancellationToken); + public virtual bool CanExecute() => true; +} diff --git a/src/App/Ui/MessageBox.cs b/src/App/Ui/MessageBox.cs new file mode 100644 index 0000000..ece27c9 --- /dev/null +++ b/src/App/Ui/MessageBox.cs @@ -0,0 +1,7 @@ +namespace Debianet.Ui; + +internal sealed class MessageBox +{ + public required string Title { get; set; } + public required string Message { get; set; } +} diff --git a/src/App/Ui/MultiSelectMenu.cs b/src/App/Ui/MultiSelectMenu.cs new file mode 100644 index 0000000..ecee726 --- /dev/null +++ b/src/App/Ui/MultiSelectMenu.cs @@ -0,0 +1,11 @@ +namespace Debianet.Ui; + +internal abstract class MultiSelectMenu : Menu +{ + protected MultiSelectMenu(MenuRegistry menuRegistry) + : base(menuRegistry) + { + } + + public abstract Task ProcessSelectedItems(IReadOnlyList selectedItems, CancellationToken cancellationToken); +} diff --git a/src/App/Ui/ReplaceMenuMenuItem.cs b/src/App/Ui/ReplaceMenuMenuItem.cs new file mode 100644 index 0000000..49aa6b4 --- /dev/null +++ b/src/App/Ui/ReplaceMenuMenuItem.cs @@ -0,0 +1,13 @@ +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal class ReplaceMenuMenuItem : MenuItemBase +{ + public required Menu Submenu { get; init; } + + public override async Task Execute(IApplication app, CancellationToken cancellationToken) + { + app.SwitchMenu(Submenu); + } +} diff --git a/src/App/Ui/RunCommandMenuItem.cs b/src/App/Ui/RunCommandMenuItem.cs new file mode 100644 index 0000000..81e3f3d --- /dev/null +++ b/src/App/Ui/RunCommandMenuItem.cs @@ -0,0 +1,28 @@ +using CliWrap; + +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal sealed class RunCommandMenuItem(ITerminal terminal) : MenuItemBase +{ + public required string Program { get; init; } + + public required string[] Arguments { get; init; } + + public override async Task Execute(IApplication app, CancellationToken cancellationToken) + { + terminal.Info($"Executing {Program} {string.Join(' ', Arguments)} ..."); + terminal.Line(); + + var result = await Cli.Wrap(Program) + .WithArguments(Arguments) + .WithStandardOutputPipe(PipeTarget.ToDelegate(terminal.StandardOutput)) + .WithStandardErrorPipe(PipeTarget.ToDelegate(terminal.StandardError)) + .ExecuteAsync(cancellationToken); + + terminal.Line(); + terminal.Info($"Exit code: {result.ExitCode}, Run time: {result.RunTime.FormatTime()}"); + terminal.WaitKey(); + } +} diff --git a/src/App/Ui/RunCommandsMenuItem.cs b/src/App/Ui/RunCommandsMenuItem.cs new file mode 100644 index 0000000..b11f535 --- /dev/null +++ b/src/App/Ui/RunCommandsMenuItem.cs @@ -0,0 +1,37 @@ +using CliWrap; + +using Debianet.Abstractions; + +namespace Debianet.Ui; + +internal sealed class RunCommandsMenuItem(ITerminal terminal) : MenuItemBase +{ + public required IEnumerable<(string program, string[] arguments)> Commands { get; init; } + + public override async Task Execute(IApplication app, CancellationToken cancellationToken) + { + TimeSpan totalTime = TimeSpan.Zero; + foreach (var (program, arguments) in Commands) + { + terminal.Info($"Executing {program} {string.Join(' ', arguments)} ..."); + terminal.Line(); + + var result = await Cli.Wrap(program) + .WithArguments(arguments) + .WithStandardOutputPipe(PipeTarget.ToDelegate(terminal.StandardOutput)) + .WithStandardErrorPipe(PipeTarget.ToDelegate(terminal.StandardError)) + .ExecuteAsync(cancellationToken); + + if (result.ExitCode != 0) + { + terminal.Error($"Command {program} {string.Join(' ', arguments)} failed with exit code {result.ExitCode}"); + break; + } + + totalTime += result.RunTime; + terminal.Line(); + } + terminal.Info($"Total run time: {totalTime.FormatTime()}"); + terminal.WaitKey(); + } +} diff --git a/src/App/Ui/TextViewer.cs b/src/App/Ui/TextViewer.cs new file mode 100644 index 0000000..1a4f022 --- /dev/null +++ b/src/App/Ui/TextViewer.cs @@ -0,0 +1,238 @@ +using System.Text; + +using Debianet.Abstractions; + +using Spectre.Console; + +namespace Debianet.Ui; + +internal sealed class TextViewer +{ + private const int TabSize = 8; + + private readonly Palete _palete; + private readonly IReadOnlyList _lines; + private readonly string _title; + + public TextViewer(string text, string? title = null, Palete? palete = null) + { + _lines = SplitIntoLines(text ?? string.Empty); + _title = title ?? string.Empty; + _palete = palete ?? Palete.Dracula; + } + + public void Show() + { + // Move to the alternate screen buffer so the original terminal content is preserved. + AnsiConsole.WriteLine("\e[?1049h"); + + try + { + Console.CursorVisible = false; + Console.Out.Write("\e[?7l\e[2J\e[H"); // disable line wrapping, clear, home + + Loop(); + } + finally + { + // Restore the main screen buffer first so the terminal is usable even on failure. + AnsiConsole.WriteLine("\e[?1049l"); + Console.Out.Write("\e[?7h"); // re-enable line wrapping + Console.CursorVisible = true; + } + } + + private void Loop() + { + int top = 0; + int renderedWidth = -1; + int renderedHeight = -1; + List visibleLines = []; + bool redraw = true; + + while (true) + { + int width = Math.Max(1, Console.WindowWidth); + int height = Math.Max(2, Console.WindowHeight); + int viewport = height - 1; // the last row is reserved for the status bar + + if (width != renderedWidth) + { + visibleLines = WrapLines(_lines, width); + renderedWidth = width; + redraw = true; + } + + if (height != renderedHeight) + { + renderedHeight = height; + redraw = true; + } + + int maxTop = Math.Max(0, visibleLines.Count - viewport); + top = Math.Clamp(top, 0, maxTop); + + if (redraw) + { + Render(visibleLines, top, viewport, width, height); + redraw = false; + } + + ConsoleKeyInfo key = Console.ReadKey(intercept: true); + switch (key.Key) + { + case ConsoleKey.Escape: + case ConsoleKey.Q: + return; + case ConsoleKey.UpArrow: + case ConsoleKey.K: + redraw = TryScroll(ref top, -1, maxTop); + break; + case ConsoleKey.DownArrow: + case ConsoleKey.J: + redraw = TryScroll(ref top, 1, maxTop); + break; + case ConsoleKey.PageUp: + redraw = TryScroll(ref top, -viewport, maxTop); + break; + case ConsoleKey.PageDown: + case ConsoleKey.Spacebar: + redraw = TryScroll(ref top, viewport, maxTop); + break; + case ConsoleKey.Home: + redraw = TryScroll(ref top, int.MinValue, maxTop); + break; + case ConsoleKey.End: + redraw = TryScroll(ref top, int.MaxValue, maxTop); + break; + } + } + } + + private static bool TryScroll(ref int top, int delta, int maxTop) + { + int target = delta switch + { + int.MinValue => 0, + int.MaxValue => maxTop, + _ => top + delta, + }; + + target = Math.Clamp(target, 0, maxTop); + if (target == top) + { + return false; + } + + top = target; + return true; + } + + private void Render(IReadOnlyList visibleLines, int top, int viewport, int width, int height) + { + StringBuilder frame = new(); + + for (int row = 0; row < viewport; row++) + { + frame.Append("\e[").Append(row + 1).Append(";1H"); // move the caret to the start of the row + + int index = top + row; + if (index < visibleLines.Count) + { + string line = visibleLines[index]; + frame.Append(line.Length > width ? line[..width] : line); + } + + frame.Append("\e[K"); // clear leftovers from the previous frame + } + + frame.Append("\e[").Append(height).Append(";1H"); // park the caret on the status row + Console.Out.Write(frame.ToString()); + + string status = BuildStatusBar(visibleLines.Count, top, viewport, width); + AnsiConsole.Markup($"[#{_palete.Accent.ToHex()} on #{_palete.Info.ToHex()}]{Markup.Escape(status)}[/]"); + } + + private string BuildStatusBar(int totalLines, int top, int viewport, int width) + { + string position; + if (totalLines == 0) + { + position = "(empty)"; + } + else + { + int first = top + 1; + int last = Math.Min(totalLines, top + viewport); + int maxTop = Math.Max(0, totalLines - viewport); + int percent = maxTop == 0 ? 100 : (int)Math.Round((100.0 * top) / maxTop); + position = $"Lines {first}-{last}/{totalLines} ({percent}%)"; + } + + string left = string.IsNullOrWhiteSpace(_title) ? position : $"{_title} | {position}"; + const string help = "Up/Down scroll PgUp/PgDn page Home/End jump Esc quit"; + + int gap = width - left.Length - help.Length; + string status = gap >= 2 ? $"{left}{new string(' ', gap)}{help}" : left; + + return status.Length > width ? status[..width] : status.PadRight(width); + } + + private static List WrapLines(IReadOnlyList lines, int width) + { + List wrapped = []; + + foreach (string raw in lines) + { + string line = ExpandAndSanitize(raw); + if (line.Length == 0) + { + wrapped.Add(string.Empty); + continue; + } + + for (int start = 0; start < line.Length; start += width) + { + int length = Math.Min(width, line.Length - start); + wrapped.Add(line.Substring(start, length)); + } + } + + return wrapped; + } + + private static string ExpandAndSanitize(string line) + { + StringBuilder builder = new(line.Length); + int column = 0; + + foreach (char c in line) + { + if (c == '\t') + { + int spaces = TabSize - (column % TabSize); + builder.Append(' ', spaces); + column += spaces; + } + else if (c < ' ' || c == '\u007F') + { + // Show control characters in caret notation so an embedded escape + // sequence cannot corrupt the rendered layout. + builder.Append('^').Append(c == '\u007F' ? '?' : (char)(c + '@')); + column += 2; + } + else + { + builder.Append(c); + column++; + } + } + + return builder.ToString(); + } + + private static List SplitIntoLines(string text) + => text.Length == 0 + ? [] + : [.. text.ReplaceLineEndings("\n").Split('\n')]; +} diff --git a/src/branding/os-release.conf b/src/Root/etc/os-release.conf similarity index 100% rename from src/branding/os-release.conf rename to src/Root/etc/os-release.conf diff --git a/src/docker-override.conf b/src/Root/etc/systemd/system/docker.service.d/docker-override.conf similarity index 100% rename from src/docker-override.conf rename to src/Root/etc/systemd/system/docker.service.d/docker-override.conf diff --git a/src/branding/wsl-distribution.conf b/src/Root/etc/wsl-distribution.conf similarity index 100% rename from src/branding/wsl-distribution.conf rename to src/Root/etc/wsl-distribution.conf diff --git a/src/branding/debianet.ico b/src/Root/usr/lib/wsl/debianet.ico similarity index 100% rename from src/branding/debianet.ico rename to src/Root/usr/lib/wsl/debianet.ico diff --git a/src/branding/oobe.sh b/src/Root/usr/lib/wsl/oobe.sh similarity index 100% rename from src/branding/oobe.sh rename to src/Root/usr/lib/wsl/oobe.sh diff --git a/src/branding/terminal-debianet.json b/src/Root/usr/lib/wsl/terminal-debianet.json similarity index 100% rename from src/branding/terminal-debianet.json rename to src/Root/usr/lib/wsl/terminal-debianet.json diff --git a/src/branding.sh b/src/branding.sh deleted file mode 100644 index 324f097..0000000 --- a/src/branding.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -sudo rm /etc/wsl-distribution.conf -sudo cp ./branding/wsl-distribution.conf /etc/wsl-distribution.conf - -sudo rm /usr/lib/wsl/debian_logo.ico -sudo cp ./branding/debianet.ico /usr/lib/wsl/debianet.ico - -sudo rm /etc/os-release -sudo cp ./branding/os-release.conf /etc/os-release - -sudo rm /usr/lib/wsl/oobe.sh -sudo cp ./branding/oobe.sh /usr/lib/wsl/oobe.sh - -sudo cp ./branding/terminal-debianet.json /usr/lib/wsl/terminal-debianet.json - -sudo chmod 755 /etc/wsl-distribution.conf -sudo chmod 755 /usr/lib/wsl/oobe.sh -sudo chmod 755 /usr/lib/wsl/debianet.ico -sudo chmod 755 /usr/lib/wsl/terminal-debianet.json -sudo chmod 755 /etc/os-release diff --git a/src/branding/DebiaNet_2.png b/src/branding/DebiaNet_2.png new file mode 100644 index 0000000..370e807 --- /dev/null +++ b/src/branding/DebiaNet_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63a188a104e59016ec8a39538e44f5b0ac9091631203a92e96b2a0fdc276a8be +size 35866 diff --git a/src/branding/debianet2.svg b/src/branding/debianet2.svg new file mode 100644 index 0000000..41d5dab --- /dev/null +++ b/src/branding/debianet2.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + NET + diff --git a/src/branding/repository-open-graph.png b/src/branding/repository-open-graph.png new file mode 100644 index 0000000..b1cc8d1 --- /dev/null +++ b/src/branding/repository-open-graph.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d268ec7f6cd9acc1a9c9db815154d9f8d9484664d2fca65dacbaa6a85ab7d3c8 +size 36185 diff --git a/src/branding/repository-open-graph.svg b/src/branding/repository-open-graph.svg new file mode 100644 index 0000000..8a0e407 --- /dev/null +++ b/src/branding/repository-open-graph.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + NET + + + diff --git a/src/copy-files.sh b/src/copy-files.sh new file mode 100644 index 0000000..101c7a6 --- /dev/null +++ b/src/copy-files.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +sudo rm /etc/wsl-distribution.conf +sudo rm /usr/lib/wsl/debian_logo.ico +sudo rm /usr/lib/wsl/oobe.sh +sudo mv /etc/os-release /etc/os_release.bak + +chmod 755 -R ./Root +cd /Root +cp -R -f ./etc /etc +cp -R -f ./usr /usr \ No newline at end of file diff --git a/src/dev-app.sh b/src/dev-app.sh new file mode 100644 index 0000000..cf23d7d --- /dev/null +++ b/src/dev-app.sh @@ -0,0 +1,3 @@ +#!/bin/bash +dotnet build ./App/Debianet.csproj +./bin/Debug/Debianet diff --git a/src/install-app.sh b/src/install-app.sh deleted file mode 100644 index 0212ba0..0000000 --- a/src/install-app.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -sudo rm /usr/bin/debianet -sudo rm -r /opt/debianet-app/DebiaNetApp - -cd DebiaNetApp -sudo dotnet publish -r linux-x64 -c Release -o ./publish DebiaNetApp.csproj -sudo mkdir -p /opt/debianet-app -sudo cp -r ./publish/* /opt/debianet-app - -sudo rm -f /usr/bin/debianet -sudo ln -s /opt/debianet-app/DebiaNetApp /usr/bin/debianet -sudo chmod +x /usr/bin/debianet - -dotnet nuget locals all --clear -sudo dotnet nuget locals all --clear diff --git a/src/install.sh b/src/install.sh index 47f26f5..490ea00 100644 --- a/src/install.sh +++ b/src/install.sh @@ -18,12 +18,11 @@ sudo apt install -y \ binutils \ ca-certificates \ curl \ - fastfetch \ git \ git-lfs \ gpg \ htop \ - lazygit \ + just \ libxml2 \ mc \ openssh-server \ @@ -31,7 +30,9 @@ sudo apt install -y \ strace \ tmux \ unzip \ - wget + wget \ + jq \ + byobu # prerpare docker install sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc @@ -61,13 +62,6 @@ sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin d # configure docker user sudo usermod -aG docker $USER -# enable docker tcp port for vs debug connecting -sudo mkdir -p /etc/systemd/system/docker.service.d/ -sudo cp docker-override.conf /etc/systemd/system/docker.service.d/override.conf - -# update dotnet workloads -sudo dotnet workload update - # install devtoys.cli wget https://github.com/DevToys-app/DevToys/releases/download/v2.0.8.0/devtoys.cli_linux_x64.deb -O devtoys.cli.deb sudo dpkg -i devtoys.cli.deb @@ -76,35 +70,28 @@ rm devtoys.cli.deb # install VS Remote debugger curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l ~/vsdbg +# install infer# +mkdir -p ~/infersharp +curl -L https://github.com/microsoft/infersharp/releases/download/v1.5/infersharp-linux64-v1.5.tar.gz -o infersharp.tar.gz +tar -xvzf infersharp.tar.gz -C ~/infersharp +rm infersharp.tar.gz + # install ripgrep wget https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep_15.1.0-1_amd64.deb -O ripgrep.deb sudo dpkg -i ripgrep.deb rm ripgrep.deb +# install Fresh +curl https://raw.githubusercontent.com/sinelaw/fresh/refs/heads/master/scripts/install.sh | sh + # install dive DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') curl -fOL "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb" sudo apt install ./dive_${DIVE_VERSION}_linux_amd64.deb rm ./dive_${DIVE_VERSION}_linux_amd64.deb -# install .NET Global tools -dotnet tool install --global dotnet-counters -dotnet tool install --global dotnet-coverage -dotnet tool install --global dotnet-ef -dotnet tool install --global dotnet-gcdump -dotnet tool install --global dotnet-monitor -dotnet tool install --global dotnet-stack -dotnet tool install --global dotnet-symbol -dotnet tool install --global dotnet-trace -dotnet tool install --global Microsoft.VisualStudio.SlnGen.Tool -dotnet tool install --global PowerShell -dotnet tool install --global upgrade-assistant -dotnet tool install --global docfx - -# install third party dotnet tools -dotnet tool install --global csharprepl -dotnet tool install --global ilspycmd -dotnet tool install --global roslynator.dotnet.cli +# update dotnet workloads +sudo dotnet workload update # set path for dotnet tools echo 'export PATH="$PATH:$HOME/.dotnet/tools"' >> ~/.bashrc