├── nativelib
├── .gitignore
├── CMakeLists.txt
└── nativelib.cpp
├── flat
├── NativeMethods.txt
├── lib
│ ├── greetings.dll
│ ├── libgreetings.so
│ └── libgreetings.dylib
├── hello.cs
├── webapi.settings.json
├── apphost.settings.json
├── cswin32sandbox.cs
├── cat.run.json
├── pinvoke.run.json
├── spaceinvaders.run.json
├── envvars.cs
├── apphost.cs
├── webapi.run.json
├── webapi.cs
├── filepath.cs
├── usersecrets.cs
├── apphost.run.json
├── playwright.cs
├── cat.cs
├── wait.cs
├── pinvoke.cs
├── spaceinvaders.cs
└── The Tell-Tale Heart.txt
├── razorapp
├── wwwroot
│ ├── favicon.png
│ └── app.css
├── Home.razor
├── razorapp.settings.json
├── _Imports.razor
├── razorapp.cs
├── razorapp.run.json
└── App.razor
├── .devcontainer
└── devcontainer.json
├── global.json
├── helloblazor
├── wwwroot
│ └── app.css
├── helloblazor.settings.json
├── helloblazor.cs
├── pages
│ └── Index.razor
└── helloblazor.run.json
├── classlibproject
├── ClassLib
│ ├── Greeter.cs
│ └── ClassLib.csproj
└── samples
│ └── hello.cs
├── nuget.config
├── README.md
├── .editorconfig
├── LICENSE
├── THIRDPARTYNOTICES
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
└── verify.cs
/nativelib/.gitignore:
--------------------------------------------------------------------------------
1 | /out/
2 |
--------------------------------------------------------------------------------
/flat/NativeMethods.txt:
--------------------------------------------------------------------------------
1 | GetTickCount
2 | GetTickCount64
--------------------------------------------------------------------------------
/flat/lib/greetings.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/flat/lib/greetings.dll
--------------------------------------------------------------------------------
/flat/lib/libgreetings.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/flat/lib/libgreetings.so
--------------------------------------------------------------------------------
/flat/lib/libgreetings.dylib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/flat/lib/libgreetings.dylib
--------------------------------------------------------------------------------
/razorapp/wwwroot/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/razorapp/wwwroot/favicon.png
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": ".NET 10 dev container",
3 | "image": "mcr.microsoft.com/dotnet/sdk:10.0"
4 | }
--------------------------------------------------------------------------------
/flat/hello.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | var name = args.Length > 0 ? args[0] : "World";
4 | Console.WriteLine($"Hello, {name}!");
5 |
--------------------------------------------------------------------------------
/razorapp/Home.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 |
3 | Home
4 |
5 |
Hello, world!
6 |
7 | Welcome to your new app.
8 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "10.0.100",
4 | "rollForward": "latestFeature",
5 | "allowPrerelease": true
6 | }
7 | }
--------------------------------------------------------------------------------
/helloblazor/wwwroot/app.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
3 | }
4 |
5 | a, .btn-link {
6 | color: #006bb7;
7 | }
--------------------------------------------------------------------------------
/classlibproject/ClassLib/Greeter.cs:
--------------------------------------------------------------------------------
1 | namespace ClassLib;
2 |
3 | public class Greeter
4 | {
5 | public string Greet(string name)
6 | {
7 | return $"Hello, {name}!";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/flat/webapi.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/razorapp/razorapp.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/helloblazor/helloblazor.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/classlibproject/samples/hello.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 | #:project ../ClassLib
3 |
4 | var greeter = new ClassLib.Greeter();
5 | var greeting = greeter.Greet(args.Length > 0 ? args[0] : "World");
6 | Console.WriteLine(greeting);
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # runfile Playground
2 |
3 | Playground of file-based C# apps for `dotnet run file.cs` feature added in .NET 10.
4 |
5 | Requires [.NET SDK version 10.0.100](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) or later.
6 |
--------------------------------------------------------------------------------
/flat/apphost.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Aspire.Hosting.Dcp": "Debug",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 | dotnet_diagnostic.ASPIRECSHARPAPPS001.severity = none
3 | dotnet_diagnostic.CA1050.severity = none
4 | dotnet_diagnostic.SP0001.severity = none
5 | dotnet_diagnostic.IL2026.severity = none
6 | csharp_prefer_braces = true:error
7 |
--------------------------------------------------------------------------------
/classlibproject/ClassLib/ClassLib.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/flat/cswin32sandbox.cs:
--------------------------------------------------------------------------------
1 | #:property TargetFramework=net10.0-windows
2 | #:package Microsoft.Windows.CsWin32@0.3.242
3 | #:package Humanizer@3.0.0-rc.30
4 |
5 | using Windows.Win32;
6 | using Humanizer;
7 |
8 | var ticks = PInvoke.GetTickCount64();
9 | Console.WriteLine($"System has been running for {TimeSpan.FromMilliseconds((long)ticks).Humanize(precision: 4)}.");
10 |
--------------------------------------------------------------------------------
/flat/cat.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "default": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true
7 | },
8 | "verify": {
9 | "commandName": "Project",
10 | "dotnetRunMessages": true,
11 | "commandLineArgs": "apphost.cs"
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/flat/pinvoke.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "default": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true
7 | },
8 | "verify": {
9 | "commandName": "Project",
10 | "dotnetRunMessages": true,
11 | "commandLineArgs": "--console"
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/razorapp/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using System.Net.Http.Json
3 | @using Microsoft.AspNetCore.Components.Forms
4 | @using Microsoft.AspNetCore.Components.Routing
5 | @using Microsoft.AspNetCore.Components.Web
6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode
7 | @using Microsoft.AspNetCore.Components.Web.Virtualization
8 | @using Microsoft.JSInterop
9 | @using razorapp
10 |
--------------------------------------------------------------------------------
/flat/spaceinvaders.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "default": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true
7 | },
8 | "verify": {
9 | "commandName": "Project",
10 | "dotnetRunMessages": true,
11 | "commandLineArgs": "--demo"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/helloblazor/helloblazor.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | #:sdk Microsoft.NET.Sdk.Web
4 | #:property PublishAot=false
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | builder.Services.AddRazorComponents();
9 |
10 | var app = builder.Build();
11 |
12 | app.UseHttpsRedirection();
13 | app.UseAntiforgery();
14 |
15 | app.MapStaticAssets();
16 | app.MapRazorComponents();
17 |
18 | app.Run();
19 |
--------------------------------------------------------------------------------
/flat/envvars.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 | #:property DefineConstants=$(DefineConstants);CUSTOM_CONSTANT;$(EnvVarConstant)
3 |
4 | #if DEBUG
5 | Console.WriteLine("Hello from Debug!");
6 | #endif
7 |
8 | #if RELEASE
9 | Console.WriteLine("Hello from Release!");
10 | #endif
11 |
12 | #if CUSTOM_CONSTANT
13 | Console.WriteLine("CUSTOM_CONSTANT is enabled!");
14 | #endif
15 |
16 | #if ENV_VAR_CONSTANT
17 | Console.WriteLine("EnvVarConstant is enabled!");
18 | #endif
19 |
--------------------------------------------------------------------------------
/nativelib/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.10)
2 | project(greetings VERSION 1.0.0 LANGUAGES CXX)
3 |
4 | # Set C++ standard
5 | set(CMAKE_CXX_STANDARD 11)
6 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
7 | set(CMAKE_CXX_EXTENSIONS OFF)
8 |
9 | # Library target
10 | add_library(greetings SHARED nativelib.cpp)
11 |
12 | # Installation rules
13 | install(TARGETS greetings
14 | EXPORT greetingsTargets
15 | LIBRARY DESTINATION lib
16 | ARCHIVE DESTINATION lib
17 | RUNTIME DESTINATION bin
18 | )
19 |
--------------------------------------------------------------------------------
/helloblazor/pages/Index.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Hello, world!
16 |
17 | Welcome to your new app.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/flat/apphost.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env aspire
2 |
3 | #:sdk Aspire.AppHost.Sdk@13.0.0
4 | #:package Aspire.Hosting.Redis@13.0.0
5 |
6 | var builder = DistributedApplication.CreateBuilder(args);
7 |
8 | var webapi = builder.AddCSharpApp("webapi", "./webapi.cs");
9 |
10 | builder.AddCSharpApp("razorapp", "../razorapp/razorapp.cs")
11 | .WithReference(webapi).WaitFor(webapi);
12 |
13 | if (!string.Equals(builder.Configuration["DOTNET_LAUNCH_PROFILE"], "verify", StringComparison.OrdinalIgnoreCase))
14 | {
15 | var redis = builder.AddRedis("redis");
16 | webapi.WithReference(redis).WaitFor(redis);
17 | }
18 |
19 | builder.Build().Run();
20 |
--------------------------------------------------------------------------------
/flat/webapi.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "http://localhost:5202",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": true,
17 | "applicationUrl": "https://localhost:7276;http://localhost:5202",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/razorapp/razorapp.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | #:sdk Microsoft.NET.Sdk.Web
4 | #:property BlazorDisableThrowNavigationException=true
5 | #:property PublishAot=false
6 |
7 | using razorapp;
8 |
9 | var builder = WebApplication.CreateBuilder(args);
10 |
11 | builder.Services.AddRazorComponents();
12 |
13 | var app = builder.Build();
14 |
15 | if (!app.Environment.IsDevelopment())
16 | {
17 | app.UseExceptionHandler("/Error", createScopeForErrors: true);
18 | app.UseHsts();
19 | }
20 | app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
21 |
22 | app.UseHttpsRedirection();
23 | app.UseAntiforgery();
24 |
25 | app.MapStaticAssets();
26 | app.MapRazorComponents();
27 |
28 | app.Run();
29 |
--------------------------------------------------------------------------------
/razorapp/razorapp.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "http://razorapp.dev.localhost:5149",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": true,
17 | "applicationUrl": "https://razorapp.dev.localhost:7293;http://razorapp.dev.localhost:5149",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/helloblazor/helloblazor.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "http://helloblazor.dev.localhost:5150",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": true,
17 | "applicationUrl": "https://helloblazor.dev.localhost:7301;http://helloblazor.dev.localhost:5150",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/razorapp/App.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/flat/webapi.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | #:sdk Microsoft.NET.Sdk.Web
4 | #:package Microsoft.AspNetCore.OpenApi@10.0.0
5 |
6 | using System.Text.Json.Serialization;
7 |
8 | var builder = WebApplication.CreateBuilder(args);
9 |
10 | builder.Services.ConfigureHttpJsonOptions(options =>
11 | {
12 | options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
13 | });
14 |
15 | builder.Services.AddOpenApi();
16 |
17 | var app = builder.Build();
18 |
19 | if (app.Environment.IsDevelopment())
20 | {
21 | app.MapOpenApi();
22 | }
23 |
24 | app.MapGet("/", () => new HelloResponse { Message = "Hello, World!" })
25 | .WithName("HelloWorld");
26 |
27 | app.Run();
28 |
29 | class HelloResponse
30 | {
31 | public string Message { get; set; } = "Hello, World!";
32 | }
33 |
34 | [JsonSerializable(typeof(HelloResponse))]
35 | partial class AppJsonSerializerContext : JsonSerializerContext
36 | {
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/nativelib/nativelib.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #ifdef _MSC_VER
6 | #include
7 | #define EXPORT __declspec(dllexport)
8 |
9 | char* char_alloc_for_dotnet(size_t size)
10 | {
11 | return static_cast(::CoTaskMemAlloc(size));
12 | }
13 |
14 | #else
15 | #define EXPORT __attribute__((visibility("default")))
16 |
17 | char* char_alloc_for_dotnet(size_t size)
18 | {
19 | return static_cast(::malloc(size));
20 | }
21 |
22 | #endif
23 |
24 | extern "C" EXPORT
25 | char const* Greetings(char const* name)
26 | {
27 | char const* greeting_format = "Hello, %s!";
28 | if (name == nullptr)
29 | name = "World";
30 |
31 | // This represents more space than is needed.
32 | size_t greeting_length = std::strlen(greeting_format) + std::strlen(name) + 1; // +1 for null terminator
33 |
34 | char* greeting = char_alloc_for_dotnet(greeting_length);
35 | if (greeting == nullptr)
36 | return nullptr; // Allocation failed
37 |
38 | std::snprintf(greeting, greeting_length, greeting_format, name);
39 | return greeting;
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Damian Edwards
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/flat/filepath.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | Console.WriteLine("From [CallerFilePath] attribute:");
4 | Console.WriteLine($" - Entry-point path: {Path.EntryPointFilePath()}");
5 | Console.WriteLine($" - Entry-point directory: {Path.EntryPointFileDirectoryPath()}");
6 |
7 | Console.WriteLine("From AppContext data:");
8 | Console.WriteLine($" - Entry-point path: {AppContext.EntryPointFilePath()}");
9 | Console.WriteLine($" - Entry-point directory: {AppContext.EntryPointFileDirectoryPath()}");
10 |
11 | static class PathEntryPointExtensions
12 | {
13 | extension(Path)
14 | {
15 | public static string EntryPointFilePath() => EntryPointImpl();
16 |
17 | public static string EntryPointFileDirectoryPath() => Path.GetDirectoryName(EntryPointImpl()) ?? "";
18 |
19 | private static string EntryPointImpl([System.Runtime.CompilerServices.CallerFilePath] string filePath = "") => filePath;
20 | }
21 | }
22 |
23 | static class AppContextExtensions
24 | {
25 | extension(AppContext)
26 | {
27 | public static string? EntryPointFilePath() => AppContext.GetData("EntryPointFilePath") as string;
28 | public static string? EntryPointFileDirectoryPath() => AppContext.GetData("EntryPointFileDirectoryPath") as string;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/flat/usersecrets.cs:
--------------------------------------------------------------------------------
1 | #:package Microsoft.Extensions.Configuration.UserSecrets@10.0.0
2 |
3 | using System.Reflection;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.Configuration.UserSecrets;
6 |
7 | var key = args.Length > 0 ? args[0] : "MySecret";
8 |
9 | var configuration = new ConfigurationBuilder()
10 | .AddUserSecrets()
11 | .Build();
12 |
13 | Console.WriteLine(configuration[key] ?? $"""
14 | Value for secret '{key}' was not found. Run the following command to set one:
15 | dotnet user-secrets set {key} "This is a secret!" --file {Path.GetFileName((string)AppContext.GetData("EntryPointFilePath")!)}
16 | """);
17 |
18 | static class UserSecretsExtensions
19 | {
20 | extension(IConfigurationBuilder builder)
21 | {
22 | ///
23 | /// Adds user secrets configuration source to based on the user secrets ID returned by .
24 | ///
25 | public IConfigurationBuilder AddUserSecrets()
26 | {
27 | return builder.AddUserSecrets(GetDefaultUserSecretsId());
28 | }
29 |
30 | private static string GetDefaultUserSecretsId()
31 | {
32 | var userSecretsId = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.UserSecretsId;
33 | if (userSecretsId is not null)
34 | {
35 | // Use the UserSecretsId from the attribute
36 | return userSecretsId;
37 | }
38 |
39 | return "global";
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/flat/apphost.run.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "https": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "https://apphost.dev.localhost:17123;http://apphost.dev.localhost:15234",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development",
11 | "DOTNET_ENVIRONMENT": "Development",
12 | "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21456",
13 | "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22678"
14 | }
15 | },
16 | "http": {
17 | "commandName": "Project",
18 | "dotnetRunMessages": true,
19 | "launchBrowser": true,
20 | "applicationUrl": "http://apphost.dev.localhost:15234",
21 | "environmentVariables": {
22 | "ASPNETCORE_ENVIRONMENT": "Development",
23 | "DOTNET_ENVIRONMENT": "Development",
24 | "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19456",
25 | "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20678"
26 | }
27 | },
28 | "verify": {
29 | "commandName": "Project",
30 | "dotnetRunMessages": true,
31 | "applicationUrl": "https://apphost-verify.dev.localhost:17123;http://apphost-verify.dev.localhost:15234",
32 | "environmentVariables": {
33 | "ASPNETCORE_ENVIRONMENT": "Development",
34 | "DOTNET_ENVIRONMENT": "Development",
35 | "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21456",
36 | "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22678"
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/THIRDPARTYNOTICES:
--------------------------------------------------------------------------------
1 | # THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
2 |
3 | This project incorporates material from the project(s) listed below (collectively, "Third Party Code").
4 | This Third Party Code is licensed to you under their original license terms set forth below.
5 |
6 | ================================================================================
7 |
8 | 1. dotnet-stop
9 | https://github.com/devlooped/dotnet-stop
10 |
11 | --------------------------------------------------------------------------------
12 |
13 | The MIT License (MIT)
14 |
15 | Copyright (c) Daniel Cazzulino and Contributors
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 |
24 | The above copyright notice and this permission notice shall be included in all
25 | copies or substantial portions of the Software.
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33 | SOFTWARE.
34 |
35 | ================================================================================
36 |
--------------------------------------------------------------------------------
/flat/playwright.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 | #:package Microsoft.NET.Test.Sdk@18.0.0
3 | #:package TUnit.Playwright@1.1.10
4 | #:property PublishAot=false
5 |
6 | using TUnit.Playwright;
7 | using System.Diagnostics;
8 | using System.Text.RegularExpressions;
9 |
10 | public partial class Tests : PageTest
11 | {
12 | [Test]
13 | public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage()
14 | {
15 | await Page.GotoAsync("https://playwright.dev");
16 |
17 | // Expect a title "to contain" a substring.
18 | await Expect(Page).ToHaveTitleAsync(PlaywrightRegex());
19 |
20 | // create a locator
21 | var getStarted = Page.Locator("text=Get Started");
22 |
23 | // Expect an attribute "to be strictly equal" to the value.
24 | await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
25 |
26 | // Click the get started link.
27 | await getStarted.ClickAsync();
28 |
29 | // Expects the URL to contain intro.
30 | await Expect(Page).ToHaveURLAsync(IntroRegex());
31 | }
32 |
33 | [GeneratedRegex("Playwright")]
34 | public static partial Regex PlaywrightRegex();
35 |
36 | [GeneratedRegex(".*intro")]
37 | public static partial Regex IntroRegex();
38 | }
39 |
40 | public partial class GlobalHooks
41 | {
42 | [Before(TestSession)]
43 | public static void InstallPlaywright()
44 | {
45 | Console.WriteLine("Installing Playwright browsers...");
46 |
47 | if (Debugger.IsAttached)
48 | {
49 | Environment.SetEnvironmentVariable("PWDEBUG", "1");
50 | }
51 |
52 | // Install Playwright browsers and dependencies
53 | // This handles cross-platform installation automatically
54 | var exitCode = Microsoft.Playwright.Program.Main(["install", "--with-deps"]);
55 |
56 | if (exitCode != 0)
57 | {
58 | Console.WriteLine($"Warning: Playwright install exited with code {exitCode}");
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/flat/cat.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 | #:package System.CommandLine@2.0.0
3 |
4 | using System.CommandLine;
5 | using System.IO;
6 |
7 | var fileArg = new Argument(name: "file")
8 | {
9 | Description = "The file to read and display on the console.",
10 | Arity = ArgumentArity.ExactlyOne
11 | };
12 | fileArg.AcceptExistingOnly();
13 |
14 | var rootCommand = new RootCommand("C# implementation of the Unix 'cat' command")
15 | {
16 | fileArg
17 | };
18 |
19 | rootCommand.SetAction((parseResult, ct) =>
20 | {
21 | var result = 0;
22 | var fileInfo = parseResult.GetValue(fileArg);
23 |
24 | if (fileInfo?.Exists != true)
25 | {
26 | var color = Console.ForegroundColor;
27 | Console.ForegroundColor = ConsoleColor.Red;
28 | Console.WriteLine("File not found or no file specified.");
29 | Console.ForegroundColor = color;
30 | result = 1;
31 | }
32 | else
33 | {
34 | var linesRead = FileHelpers.PrintFile(fileInfo, ct);
35 | }
36 |
37 | return Task.FromResult(result);
38 | });
39 |
40 | var parseResult = rootCommand.Parse(args);
41 | return await parseResult.InvokeAsync();
42 |
43 | public static class FileHelpers
44 | {
45 | public static long PrintFile(FileInfo file, CancellationToken cancellationToken)
46 | {
47 | Span buffer = stackalloc char[256];
48 |
49 | using var readStream = File.OpenRead(file.FullName);
50 | using var reader = new StreamReader(readStream);
51 | long linesRead = 0;
52 |
53 | while (!reader.EndOfStream)
54 | {
55 | var charsRead = reader.ReadBlock(buffer);
56 | char? lastChar = null;
57 |
58 | for (var i = 0; i < charsRead; i++)
59 | {
60 | var c = buffer[i];
61 | Console.Write(c);
62 |
63 | if (c == '\n' && (Environment.NewLine == "\n" || lastChar == '\r'))
64 | {
65 | linesRead++;
66 | }
67 |
68 | lastChar = c;
69 | }
70 | }
71 |
72 | return linesRead;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/flat/wait.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | #:package Humanizer@2.14.1
4 |
5 | using System.Text.RegularExpressions;
6 | using Humanizer;
7 |
8 | var delay = TimeSpan.FromSeconds(1);
9 |
10 | if (args.Length > 0)
11 | {
12 | var arg = string.Join(' ', args);
13 | if (int.TryParse(arg, out var ms))
14 | {
15 | delay = TimeSpan.FromMilliseconds(ms);
16 | }
17 | else if (TimeSpan.TryParse(arg, out var ts))
18 | {
19 | delay = ts;
20 | }
21 | else if (TryParseDuration(arg, out var ts2))
22 | {
23 | delay = ts2;
24 | }
25 | else
26 | {
27 | Console.Error.WriteLine($"Invalid argument: {arg}");
28 | return 1;
29 | }
30 | }
31 |
32 | if (delay < TimeSpan.Zero)
33 | {
34 | Console.Error.WriteLine("Negative wait value is not allowed.");
35 | return 2;
36 | }
37 | else if (delay == TimeSpan.Zero)
38 | {
39 | Console.WriteLine("Wait value is zero, exiting immediately.");
40 | }
41 | else
42 | {
43 | Console.WriteLine($"Waiting for {delay.Humanize(5)}…");
44 | await Task.Delay(delay);
45 | }
46 |
47 | return 0;
48 |
49 | static bool TryParseDuration(string input, out TimeSpan result)
50 | {
51 | var tokens = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
52 |
53 | result = TimeSpan.Zero;
54 |
55 | foreach (var token in tokens)
56 | {
57 | var match = RegexParsers.Duration().Match(token);
58 |
59 | if (!match.Success)
60 | {
61 | return false;
62 | }
63 |
64 | var value = int.Parse(match.Groups["value"].Value);
65 | var unit = match.Groups["unit"].Value.ToLowerInvariant();
66 | var now = DateTime.Now;
67 |
68 | result += unit switch
69 | {
70 | "y" or "year" => now.AddYears(value) - now,
71 | "mo" or "month" => now.AddMonths(value) - now,
72 | "w" or "week" => TimeSpan.FromDays(value * 7),
73 | "d" or "day" => TimeSpan.FromDays(value),
74 | "h" or "hr" or "hour" => TimeSpan.FromHours(value),
75 | "m" or "min" or "minute" => TimeSpan.FromMinutes(value),
76 | "s" or "sec" or "second" => TimeSpan.FromSeconds(value),
77 | "ms" or "millisecond" => TimeSpan.FromMilliseconds(value),
78 | "microsecond" => TimeSpan.FromMicroseconds(value),
79 | _ => throw new InvalidOperationException($"Unknown time unit: {unit}")
80 | };
81 | }
82 |
83 | return true;
84 | }
85 |
86 | internal static partial class RegexParsers
87 | {
88 | [GeneratedRegex(@"^(?\d+)(?!\.\d)\s*(?ms|y|year|mo|month|w|wk|week|d|day|h|hr|hour|m|min|minute|s|sec|second|millisecond|microsecond)s?$", RegexOptions.IgnoreCase, "en-US")]
89 | public static partial Regex Duration();
90 | }
--------------------------------------------------------------------------------
/razorapp/wwwroot/app.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
3 | }
4 |
5 | a, .btn-link {
6 | color: #006bb7;
7 | }
8 |
9 | .btn-primary {
10 | color: #fff;
11 | background-color: #1b6ec2;
12 | border-color: #1861ac;
13 | }
14 |
15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
17 | }
18 |
19 | .content {
20 | padding-top: 1.1rem;
21 | }
22 |
23 | h1:focus {
24 | outline: none;
25 | }
26 |
27 | .valid.modified:not([type=checkbox]) {
28 | outline: 1px solid #26b050;
29 | }
30 |
31 | .invalid {
32 | outline: 1px solid #e50000;
33 | }
34 |
35 | .validation-message {
36 | color: #e50000;
37 | }
38 |
39 | .blazor-error-boundary {
40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
41 | padding: 1rem 1rem 1rem 3.7rem;
42 | color: white;
43 | }
44 |
45 | .blazor-error-boundary::after {
46 | content: "An error has occurred."
47 | }
48 |
49 | .darker-border-checkbox.form-check-input {
50 | border-color: #929292;
51 | }
52 |
53 | .form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
54 | color: var(--bs-secondary-color);
55 | text-align: end;
56 | }
57 |
58 | .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
59 | text-align: start;
60 | }
--------------------------------------------------------------------------------
/flat/pinvoke.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 | #:property AllowUnsafeBlocks=True
3 |
4 | using System.Reflection;
5 | using System.Runtime.InteropServices;
6 |
7 | if (args.Contains("--debug"))
8 | {
9 | NativeMethods.Debug = true;
10 | Console.WriteLine("Debug mode enabled.");
11 | }
12 |
13 | NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, NativeMethods.DllImportResolver);
14 |
15 | var greeting = NativeMethods.Greetings("run-file");
16 |
17 | if (OperatingSystem.IsWindows() && !args.Contains("--console"))
18 | {
19 | NativeMethods.MessageBoxW(IntPtr.Zero, greeting, "Attention!", 0);
20 | }
21 | else
22 | {
23 | Console.WriteLine(greeting);
24 | }
25 |
26 | internal partial class NativeMethods
27 | {
28 | public static bool Debug { get; set; } = false;
29 |
30 | public static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
31 | {
32 |
33 | // Try to load the native library from the current directory.
34 | var libraryPath = Path.Join(AppContext.GetData("EntryPointFileDirectoryPath")?.ToString() ?? Environment.CurrentDirectory, GetLibraryName(libraryName));
35 | if (Debug)
36 | {
37 | Console.WriteLine($"Attempting to load library: {Path.NormalizeSeparators(libraryPath)}");
38 | }
39 | return NativeLibrary.TryLoad(libraryPath, out var handle)
40 | ? handle
41 | : IntPtr.Zero; // Fallback to the default import resolver.
42 | }
43 |
44 | private static string GetLibraryName(string libraryName) => OperatingSystem.IsWindows() ? libraryName : PrefixFileName("lib", libraryName);
45 |
46 | private static string PrefixFileName(string prefix, string path)
47 | {
48 | var dir = Path.GetDirectoryName(path);
49 | var fileName = $"{prefix}{Path.GetFileName(path)}";
50 | var ext = OperatingSystem.IsWindows()
51 | ? ".dll"
52 | : OperatingSystem.IsLinux()
53 | ? ".so"
54 | : OperatingSystem.IsMacOS()
55 | ? ".dylib"
56 | : throw new PlatformNotSupportedException("Unsupported platform for native library loading.");
57 | var result = Path.Join(dir ?? string.Empty, $"{fileName}{ext}");
58 |
59 | if (Debug)
60 | {
61 | Console.WriteLine($"Current dir: {Environment.CurrentDirectory}");
62 | Console.WriteLine($"Library dir: {dir}");
63 | Console.WriteLine($"Library file name: {fileName}");
64 | Console.WriteLine($"Prefixed result: {result}");
65 | }
66 |
67 | return result;
68 | }
69 |
70 | [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
71 | public static partial int MessageBoxW(IntPtr hWnd, string lpText, string lpCaption, uint uType);
72 |
73 | [LibraryImport("lib/greetings", StringMarshalling = StringMarshalling.Utf8)]
74 | public static partial string Greetings(string name);
75 | }
76 |
77 | static class PathEntryPointExtensions
78 | {
79 | extension(Path)
80 | {
81 | public static string NormalizeSeparators(string path) => path.Replace(
82 | Path.DirectorySeparatorChar switch
83 | {
84 | '/' => '\\',
85 | '\\' => '/',
86 | _ => throw new InvalidOperationException("Unsupported directory separator character.")
87 | },
88 | Path.DirectorySeparatorChar);
89 | }
90 | }
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build-and-verify:
14 | permissions:
15 | contents: write
16 | pull-requests: write
17 |
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | os: [ubuntu-latest, windows-latest, macos-latest]
22 |
23 | runs-on: ${{ matrix.os }}
24 |
25 | env:
26 | DOTNET_CLI_TELEMETRY_OPTOUT: 1
27 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
28 | DOTNET_NOLOGO: true
29 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false
30 | DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false
31 | DOTNET_MULTILEVEL_LOOKUP: 0
32 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: true
33 |
34 | steps:
35 | - name: Checkout code
36 | uses: actions/checkout@v4
37 | with:
38 | fetch-depth: 0
39 | token: ${{ secrets.GITHUB_TOKEN }}
40 |
41 | - name: Check for nativelib changes
42 | id: check-nativelib
43 | run: |
44 | NEED_BUILD=false
45 |
46 | # Check if nativelib source files changed
47 | if [ "${{ github.event_name }}" == "pull_request" ]; then
48 | if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q '^nativelib/'; then
49 | NEED_BUILD=true
50 | fi
51 | else
52 | if git diff --name-only HEAD~1 HEAD | grep -q '^nativelib/'; then
53 | NEED_BUILD=true
54 | fi
55 | fi
56 |
57 | # Check if output files are missing
58 | if [ "${{ runner.os }}" == "Windows" ] && [ ! -f "flat/lib/greetings.dll" ]; then
59 | NEED_BUILD=true
60 | fi
61 | if [ "${{ runner.os }}" == "Linux" ] && [ ! -f "flat/lib/libgreetings.so" ]; then
62 | NEED_BUILD=true
63 | fi
64 | if [ "${{ runner.os }}" == "macOS" ] && [ ! -f "flat/lib/libgreetings.dylib" ]; then
65 | NEED_BUILD=true
66 | fi
67 |
68 | if [ "$NEED_BUILD" == "true" ]; then
69 | echo "changed=true" >> $GITHUB_OUTPUT
70 | else
71 | echo "changed=false" >> $GITHUB_OUTPUT
72 | fi
73 | shell: bash
74 |
75 | - name: Setup .NET SDK
76 | uses: actions/setup-dotnet@v4
77 | with:
78 | global-json-file: global.json
79 |
80 | - name: Install HTTPS development certificate
81 | run: dotnet dev-certs https --verbose
82 | continue-on-error: true
83 |
84 | - name: Install CMake (Ubuntu)
85 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Linux'
86 | run: |
87 | sudo apt-get update
88 | sudo apt-get install -y cmake build-essential
89 |
90 | - name: Install CMake (macOS)
91 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'macOS'
92 | run: |
93 | brew install cmake
94 |
95 | - name: Install CMake (Windows)
96 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Windows'
97 | run: |
98 | choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y
99 |
100 | - name: Build nativelib
101 | if: steps.check-nativelib.outputs.changed == 'true'
102 | run: |
103 | cmake -S nativelib -B nativelib/build -DCMAKE_BUILD_TYPE=Release
104 | cmake --build nativelib/build --config Release
105 |
106 | - name: Copy nativelib output to flat/lib (Windows)
107 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Windows'
108 | run: |
109 | mkdir -p flat/lib
110 | cp nativelib/build/Release/greetings.dll flat/lib/
111 | shell: bash
112 |
113 | - name: Copy nativelib output to flat/lib (Linux)
114 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Linux'
115 | run: |
116 | mkdir -p flat/lib
117 | cp nativelib/build/libgreetings.so flat/lib/
118 |
119 | - name: Copy nativelib output to flat/lib (macOS)
120 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'macOS'
121 | run: |
122 | mkdir -p flat/lib
123 | cp nativelib/build/libgreetings.dylib flat/lib/
124 |
125 | - name: Commit and push nativelib artifacts
126 | if: steps.check-nativelib.outputs.changed == 'true' && github.event_name == 'pull_request'
127 | run: |
128 | git config user.name "github-actions[bot]"
129 | git config user.email "github-actions[bot]@users.noreply.github.com"
130 | git add flat/lib/
131 | if ! git diff --staged --quiet; then
132 | git commit -m "Update nativelib artifacts for ${{ runner.os }}"
133 | git push origin HEAD:${{ github.head_ref }}
134 | fi
135 | shell: bash
136 |
137 | - name: Run verification script
138 | run: dotnet verify.cs --parallel --timeout 400
139 | working-directory: ${{ github.workspace }}
140 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from `dotnet new gitignore`
5 |
6 | # dotenv files
7 | .env
8 |
9 | # User-specific files
10 | *.rsuser
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # User-specific files (MonoDevelop/Xamarin Studio)
17 | *.userprefs
18 |
19 | # Mono auto generated files
20 | mono_crash.*
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Ww][Ii][Nn]32/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # Tye
69 | .tye/
70 |
71 | # ASP.NET Scaffolding
72 | ScaffoldingReadMe.txt
73 |
74 | # StyleCop
75 | StyleCopReport.xml
76 |
77 | # Files built by Visual Studio
78 | *_i.c
79 | *_p.c
80 | *_h.h
81 | *.ilk
82 | *.meta
83 | *.obj
84 | *.iobj
85 | *.pch
86 | *.pdb
87 | *.ipdb
88 | *.pgc
89 | *.pgd
90 | *.rsp
91 | *.sbr
92 | *.tlb
93 | *.tli
94 | *.tlh
95 | *.tmp
96 | *.tmp_proj
97 | *_wpftmp.csproj
98 | *.log
99 | *.tlog
100 | *.vspscc
101 | *.vssscc
102 | .builds
103 | *.pidb
104 | *.svclog
105 | *.scc
106 |
107 | # Chutzpah Test files
108 | _Chutzpah*
109 |
110 | # Visual C++ cache files
111 | ipch/
112 | *.aps
113 | *.ncb
114 | *.opendb
115 | *.opensdf
116 | *.sdf
117 | *.cachefile
118 | *.VC.db
119 | *.VC.VC.opendb
120 |
121 | # Visual Studio profiler
122 | *.psess
123 | *.vsp
124 | *.vspx
125 | *.sap
126 |
127 | # Visual Studio Trace Files
128 | *.e2e
129 |
130 | # TFS 2012 Local Workspace
131 | $tf/
132 |
133 | # Guidance Automation Toolkit
134 | *.gpState
135 |
136 | # ReSharper is a .NET coding add-in
137 | _ReSharper*/
138 | *.[Rr]e[Ss]harper
139 | *.DotSettings.user
140 |
141 | # TeamCity is a build add-in
142 | _TeamCity*
143 |
144 | # DotCover is a Code Coverage Tool
145 | *.dotCover
146 |
147 | # AxoCover is a Code Coverage Tool
148 | .axoCover/*
149 | !.axoCover/settings.json
150 |
151 | # Coverlet is a free, cross platform Code Coverage Tool
152 | coverage*.json
153 | coverage*.xml
154 | coverage*.info
155 |
156 | # Visual Studio code coverage results
157 | *.coverage
158 | *.coveragexml
159 |
160 | # NCrunch
161 | _NCrunch_*
162 | .*crunch*.local.xml
163 | nCrunchTemp_*
164 |
165 | # MightyMoose
166 | *.mm.*
167 | AutoTest.Net/
168 |
169 | # Web workbench (sass)
170 | .sass-cache/
171 |
172 | # Installshield output folder
173 | [Ee]xpress/
174 |
175 | # DocProject is a documentation generator add-in
176 | DocProject/buildhelp/
177 | DocProject/Help/*.HxT
178 | DocProject/Help/*.HxC
179 | DocProject/Help/*.hhc
180 | DocProject/Help/*.hhk
181 | DocProject/Help/*.hhp
182 | DocProject/Help/Html2
183 | DocProject/Help/html
184 |
185 | # Click-Once directory
186 | publish/
187 |
188 | # Publish Web Output
189 | *.[Pp]ublish.xml
190 | *.azurePubxml
191 | # Note: Comment the next line if you want to checkin your web deploy settings,
192 | # but database connection strings (with potential passwords) will be unencrypted
193 | *.pubxml
194 | *.publishproj
195 |
196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
197 | # checkin your Azure Web App publish settings, but sensitive information contained
198 | # in these scripts will be unencrypted
199 | PublishScripts/
200 |
201 | # NuGet Packages
202 | *.nupkg
203 | # NuGet Symbol Packages
204 | *.snupkg
205 | # The packages folder can be ignored because of Package Restore
206 | **/[Pp]ackages/*
207 | # except build/, which is used as an MSBuild target.
208 | !**/[Pp]ackages/build/
209 | # Uncomment if necessary however generally it will be regenerated when needed
210 | #!**/[Pp]ackages/repositories.config
211 | # NuGet v3's project.json files produces more ignorable files
212 | *.nuget.props
213 | *.nuget.targets
214 |
215 | # Microsoft Azure Build Output
216 | csx/
217 | *.build.csdef
218 |
219 | # Microsoft Azure Emulator
220 | ecf/
221 | rcf/
222 |
223 | # Windows Store app package directories and files
224 | AppPackages/
225 | BundleArtifacts/
226 | Package.StoreAssociation.xml
227 | _pkginfo.txt
228 | *.appx
229 | *.appxbundle
230 | *.appxupload
231 |
232 | # Visual Studio cache files
233 | # files ending in .cache can be ignored
234 | *.[Cc]ache
235 | # but keep track of directories ending in .cache
236 | !?*.[Cc]ache/
237 |
238 | # Others
239 | ClientBin/
240 | ~$*
241 | *~
242 | *.dbmdl
243 | *.dbproj.schemaview
244 | *.jfm
245 | *.pfx
246 | *.publishsettings
247 | orleans.codegen.cs
248 |
249 | # Including strong name files can present a security risk
250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251 | #*.snk
252 |
253 | # Since there are multiple workflows, uncomment next line to ignore bower_components
254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255 | #bower_components/
256 |
257 | # RIA/Silverlight projects
258 | Generated_Code/
259 |
260 | # Backup & report files from converting an old project file
261 | # to a newer Visual Studio version. Backup files are not needed,
262 | # because we have git ;-)
263 | _UpgradeReport_Files/
264 | Backup*/
265 | UpgradeLog*.XML
266 | UpgradeLog*.htm
267 | ServiceFabricBackup/
268 | *.rptproj.bak
269 |
270 | # SQL Server files
271 | *.mdf
272 | *.ldf
273 | *.ndf
274 |
275 | # Business Intelligence projects
276 | *.rdl.data
277 | *.bim.layout
278 | *.bim_*.settings
279 | *.rptproj.rsuser
280 | *- [Bb]ackup.rdl
281 | *- [Bb]ackup ([0-9]).rdl
282 | *- [Bb]ackup ([0-9][0-9]).rdl
283 |
284 | # Microsoft Fakes
285 | FakesAssemblies/
286 |
287 | # GhostDoc plugin setting file
288 | *.GhostDoc.xml
289 |
290 | # Node.js Tools for Visual Studio
291 | .ntvs_analysis.dat
292 | node_modules/
293 |
294 | # Visual Studio 6 build log
295 | *.plg
296 |
297 | # Visual Studio 6 workspace options file
298 | *.opt
299 |
300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
301 | *.vbw
302 |
303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
304 | *.vbp
305 |
306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
307 | *.dsw
308 | *.dsp
309 |
310 | # Visual Studio 6 technical files
311 | *.ncb
312 | *.aps
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # CodeRush personal settings
330 | .cr/personal
331 |
332 | # Python Tools for Visual Studio (PTVS)
333 | __pycache__/
334 | *.pyc
335 |
336 | # Cake - Uncomment if you are using it
337 | # tools/**
338 | # !tools/packages.config
339 |
340 | # Tabs Studio
341 | *.tss
342 |
343 | # Telerik's JustMock configuration file
344 | *.jmconfig
345 |
346 | # BizTalk build output
347 | *.btp.cs
348 | *.btm.cs
349 | *.odx.cs
350 | *.xsd.cs
351 |
352 | # OpenCover UI analysis results
353 | OpenCover/
354 |
355 | # Azure Stream Analytics local run output
356 | ASALocalRun/
357 |
358 | # MSBuild Binary and Structured Log
359 | *.binlog
360 |
361 | # NVidia Nsight GPU debugger configuration file
362 | *.nvuser
363 |
364 | # MFractors (Xamarin productivity tool) working folder
365 | .mfractor/
366 |
367 | # Local History for Visual Studio
368 | .localhistory/
369 |
370 | # Visual Studio History (VSHistory) files
371 | .vshistory/
372 |
373 | # BeatPulse healthcheck temp database
374 | healthchecksdb
375 |
376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
377 | MigrationBackup/
378 |
379 | # Ionide (cross platform F# VS Code tools) working folder
380 | .ionide/
381 |
382 | # Fody - auto-generated XML schema
383 | FodyWeavers.xsd
384 |
385 | # VS Code files for those working on multiple tools
386 | .vscode/*
387 | !.vscode/settings.json
388 | !.vscode/tasks.json
389 | !.vscode/launch.json
390 | !.vscode/extensions.json
391 | *.code-workspace
392 |
393 | # Local History for Visual Studio Code
394 | .history/
395 |
396 | # Windows Installer files from build outputs
397 | *.cab
398 | *.msi
399 | *.msix
400 | *.msm
401 | *.msp
402 |
403 | # JetBrains Rider
404 | *.sln.iml
405 | .idea/
406 |
407 | ##
408 | ## Visual studio for Mac
409 | ##
410 |
411 |
412 | # globs
413 | Makefile.in
414 | *.userprefs
415 | *.usertasks
416 | config.make
417 | config.status
418 | aclocal.m4
419 | install-sh
420 | autom4te.cache/
421 | *.tar.gz
422 | tarballs/
423 | test-results/
424 |
425 | # Mac bundle stuff
426 | *.dmg
427 | *.app
428 |
429 | # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
430 | # General
431 | .DS_Store
432 | .AppleDouble
433 | .LSOverride
434 |
435 | # Icon must end with two \r
436 | Icon
437 |
438 |
439 | # Thumbnails
440 | ._*
441 |
442 | # Files that might appear in the root of a volume
443 | .DocumentRevisions-V100
444 | .fseventsd
445 | .Spotlight-V100
446 | .TemporaryItems
447 | .Trashes
448 | .VolumeIcon.icns
449 | .com.apple.timemachine.donotpresent
450 |
451 | # Directories potentially created on remote AFP share
452 | .AppleDB
453 | .AppleDesktop
454 | Network Trash Folder
455 | Temporary Items
456 | .apdisk
457 |
458 | # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
459 | # Windows thumbnail cache files
460 | Thumbs.db
461 | ehthumbs.db
462 | ehthumbs_vista.db
463 |
464 | # Dump file
465 | *.stackdump
466 |
467 | # Folder config file
468 | [Dd]esktop.ini
469 |
470 | # Recycle Bin used on file shares
471 | $RECYCLE.BIN/
472 |
473 | # Windows Installer files
474 | *.cab
475 | *.msi
476 | *.msix
477 | *.msm
478 | *.msp
479 |
480 | # Windows shortcuts
481 | *.lnk
482 |
483 | # Vim temporary swap files
484 | *.swp
485 |
--------------------------------------------------------------------------------
/flat/spaceinvaders.cs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env dotnet
2 |
3 | // Check for --demo mode
4 | var demoMode = args.Contains("--demo", StringComparer.OrdinalIgnoreCase);
5 |
6 | if (demoMode)
7 | {
8 | // In demo mode, just create the game, render the initial screen, and exit
9 | var game = new Game();
10 | game.RenderInitialScreen();
11 | return 0;
12 | }
13 | else
14 | {
15 | Console.Clear();
16 | new Game().Run();
17 | return 0;
18 | }
19 |
20 | class Game
21 | {
22 | private readonly int width;
23 | private readonly int height;
24 | private Player player;
25 | private readonly List enemies;
26 | private readonly List bullets;
27 | private int enemyDirection; // 1 for right, -1 for left
28 | private bool gameOver;
29 | private bool playerWon;
30 | private int frameCount = 0;
31 |
32 | public Game(int width = 40, int height = 20)
33 | {
34 | this.width = width;
35 | this.height = height;
36 | // Initialize player at bottom center.
37 | player = new Player(width / 2, height - 1);
38 | bullets = [];
39 | enemies = [];
40 | // Create a grid of enemies.
41 | int rows = 3;
42 | int cols = 8;
43 | int startX = 5;
44 | int startY = 0;
45 | for (int r = 0; r < rows; r++)
46 | {
47 | for (int c = 0; c < cols; c++)
48 | {
49 | enemies.Add(new Enemy(startX + c, startY + r));
50 | }
51 | }
52 | enemyDirection = 1;
53 | gameOver = false;
54 | playerWon = false;
55 | }
56 |
57 | public void RenderInitialScreen()
58 | {
59 | // Render the initial game state for demo/verification purposes
60 | // Don't use Console.Clear() or Console.CursorVisible as they fail when redirected
61 | RenderToConsole();
62 | Console.WriteLine("\nDemo mode - initial game state rendered successfully.");
63 | }
64 |
65 | private void RenderToConsole()
66 | {
67 | // Create a buffer for the inner game area.
68 | char[,] buffer = new char[height, width];
69 | for (int r = 0; r < height; r++)
70 | {
71 | for (int c = 0; c < width; c++)
72 | {
73 | buffer[r, c] = ' ';
74 | }
75 | }
76 |
77 | char enemyChar = 'X';
78 | char playerChar = '^';
79 |
80 | // Draw player.
81 | if (player.Y >= 0 && player.Y < height && player.X >= 0 && player.X < width)
82 | {
83 | buffer[player.Y, player.X] = playerChar;
84 | }
85 | // Draw enemies.
86 | foreach (var enemy in enemies)
87 | {
88 | if (enemy.Y >= 0 && enemy.Y < height && enemy.X >= 0 && enemy.X < width)
89 | {
90 | buffer[enemy.Y, enemy.X] = enemyChar;
91 | }
92 | }
93 |
94 | // Draw the border.
95 | string topBorder = "+" + new string('-', width) + "+";
96 | Console.WriteLine(topBorder);
97 | for (int r = 0; r < height; r++)
98 | {
99 | Console.Write("|");
100 | for (int c = 0; c < width; c++)
101 | {
102 | Console.Write(buffer[r, c]);
103 | }
104 | Console.WriteLine("|");
105 | }
106 | string bottomBorder = "+" + new string('-', width) + "+";
107 | Console.WriteLine(bottomBorder);
108 | }
109 |
110 | public void Run()
111 | {
112 | Console.CursorVisible = false;
113 | while (!gameOver)
114 | {
115 | HandleInput();
116 | UpdateGame();
117 | Render();
118 | Thread.Sleep(100);
119 | }
120 | Console.CursorVisible = true;
121 | Console.Clear();
122 | // Display end message with color.
123 | if (playerWon)
124 | {
125 | Console.ForegroundColor = ConsoleColor.Green;
126 | Console.WriteLine("You win! All enemies defeated.");
127 | }
128 | else
129 | {
130 | Console.ForegroundColor = ConsoleColor.Red;
131 | Console.WriteLine("Game Over! An enemy reached the bottom.");
132 | }
133 | Console.ResetColor();
134 | }
135 |
136 | private void HandleInput()
137 | {
138 | if (Console.KeyAvailable)
139 | {
140 | var keyInfo = Console.ReadKey(intercept: true);
141 | if (keyInfo.Key == ConsoleKey.LeftArrow)
142 | {
143 | player = player.MoveLeft();
144 | }
145 | else if (keyInfo.Key == ConsoleKey.RightArrow)
146 | {
147 | player = player.MoveRight(width);
148 | }
149 | else if (keyInfo.Key == ConsoleKey.Spacebar)
150 | {
151 | // Fire a bullet from just above the player.
152 | bullets.Add(new Bullet(player.X, player.Y - 1));
153 | }
154 | // Flush any additional key presses.
155 | while (Console.KeyAvailable)
156 | {
157 | Console.ReadKey(true);
158 | }
159 | }
160 | }
161 |
162 | private void UpdateGame()
163 | {
164 | // Update bullets: move them upward.
165 | for (int i = bullets.Count - 1; i >= 0; i--)
166 | {
167 | bullets[i] = bullets[i].MoveUp();
168 | if (bullets[i].Y < 0)
169 | {
170 | bullets.RemoveAt(i);
171 | }
172 | }
173 |
174 | // Determine enemy boundaries.
175 | bool hitEdge = false;
176 | int minX = int.MaxValue, maxX = int.MinValue;
177 | foreach (var enemy in enemies)
178 | {
179 | if (enemy.X < minX)
180 | {
181 | minX = enemy.X;
182 | }
183 |
184 | if (enemy.X > maxX)
185 | {
186 | maxX = enemy.X;
187 | }
188 | }
189 | if ((enemyDirection == 1 && maxX >= width - 1) ||
190 | (enemyDirection == -1 && minX <= 0))
191 | {
192 | hitEdge = true;
193 | }
194 |
195 | // Update enemies.
196 | if (hitEdge)
197 | {
198 | for (int i = 0; i < enemies.Count; i++)
199 | {
200 | enemies[i] = new Enemy(enemies[i].X, enemies[i].Y + 1);
201 | }
202 | enemyDirection *= -1;
203 | }
204 | else
205 | {
206 | for (int i = 0; i < enemies.Count; i++)
207 | {
208 | enemies[i] = new Enemy(enemies[i].X + enemyDirection, enemies[i].Y);
209 | }
210 | }
211 |
212 | // Collision detection: bullet vs enemy.
213 | for (int i = bullets.Count - 1; i >= 0; i--)
214 | {
215 | for (int j = enemies.Count - 1; j >= 0; j--)
216 | {
217 | if (bullets[i].X == enemies[j].X && bullets[i].Y == enemies[j].Y)
218 | {
219 | bullets.RemoveAt(i);
220 | enemies.RemoveAt(j);
221 | break;
222 | }
223 | }
224 | }
225 |
226 | // Check win condition.
227 | if (enemies.Count == 0)
228 | {
229 | playerWon = true;
230 | gameOver = true;
231 | }
232 | else
233 | {
234 | // Lose condition: any enemy reaches the player's row.
235 | foreach (var enemy in enemies)
236 | {
237 | if (enemy.Y >= player.Y)
238 | {
239 | gameOver = true;
240 | break;
241 | }
242 | }
243 | }
244 | }
245 |
246 | private void Render()
247 | {
248 | // Move cursor to top-left.
249 | Console.SetCursorPosition(0, 0);
250 | // Create a buffer for the inner game area.
251 | char[,] buffer = new char[height, width];
252 | for (int r = 0; r < height; r++)
253 | {
254 | for (int c = 0; c < width; c++)
255 | {
256 | buffer[r, c] = ' ';
257 | }
258 | }
259 |
260 | // Determine animated characters.
261 | char enemyChar = (frameCount % 2 == 0) ? 'X' : 'x';
262 | char playerChar = (frameCount % 2 == 0) ? '^' : 'A';
263 | char bulletChar = (frameCount % 2 == 0) ? '|' : '!';
264 |
265 | // Draw bullets.
266 | foreach (var bullet in bullets)
267 | {
268 | if (bullet.Y >= 0 && bullet.Y < height && bullet.X >= 0 && bullet.X < width)
269 | {
270 | buffer[bullet.Y, bullet.X] = bulletChar;
271 | }
272 | }
273 | // Draw player.
274 | if (player.Y >= 0 && player.Y < height && player.X >= 0 && player.X < width)
275 | {
276 | buffer[player.Y, player.X] = playerChar;
277 | }
278 | // Draw enemies.
279 | foreach (var enemy in enemies)
280 | {
281 | if (enemy.Y >= 0 && enemy.Y < height && enemy.X >= 0 && enemy.X < width)
282 | {
283 | buffer[enemy.Y, enemy.X] = enemyChar;
284 | }
285 | }
286 |
287 | // Draw the border.
288 | // Top border.
289 | string topBorder = "+" + new string('-', width) + "+";
290 | Console.WriteLine(topBorder);
291 | // For each row, draw left border, row content, then right border.
292 | for (int r = 0; r < height; r++)
293 | {
294 | Console.Write("|");
295 | for (int c = 0; c < width; c++)
296 | {
297 | char ch = buffer[r, c];
298 | // Set color based on entity.
299 | Console.ForegroundColor = ch switch
300 | {
301 | '^' or 'A' => ConsoleColor.Green,
302 | 'X' or 'x' => ConsoleColor.Red,
303 | '|' or '!' => ConsoleColor.Yellow,
304 | _ => ConsoleColor.Gray,
305 | };
306 | Console.Write(ch);
307 | }
308 | Console.ResetColor();
309 | Console.WriteLine("|");
310 | }
311 | // Bottom border.
312 | string bottomBorder = "+" + new string('-', width) + "+";
313 | Console.WriteLine(bottomBorder);
314 |
315 | frameCount++;
316 | }
317 |
318 | public record Player(int X, int Y)
319 | {
320 | // Return a new Player with updated X when moving.
321 | public Player MoveLeft() => this with { X = X > 0 ? X - 1 : 0 };
322 | public Player MoveRight(int maxWidth) => this with { X = X < maxWidth - 1 ? X + 1 : maxWidth - 1 };
323 | }
324 |
325 | public record Enemy(int X, int Y);
326 |
327 | public record Bullet(int X, int Y)
328 | {
329 | // Move the bullet upward.
330 | public Bullet MoveUp() => this with { Y = Y - 1 };
331 | }
332 | }
333 |
--------------------------------------------------------------------------------
/flat/The Tell-Tale Heart.txt:
--------------------------------------------------------------------------------
1 | THE TELL-TALE HEART.
2 | Author: Edgar Allan Poe
3 |
4 |
5 | True!—nervous—very, very dreadfully nervous I had been and am;
6 | but why will you say that I am mad? The disease had sharpened my
7 | senses—not destroyed—not dulled them. Above all was the sense of
8 | hearing acute. I heard all things in the heaven and in the earth.
9 | I heard many things in hell. How, then, am I mad? Hearken! and
10 | observe how healthily—how calmly I can tell you the whole story.
11 |
12 | It is impossible to say how first the idea entered my brain; but
13 | once conceived, it haunted me day and night. Object there was
14 | none. Passion there was none. I loved the old man. He had never
15 | wronged me. He had never given me insult. For his gold I had no
16 | desire. I think it was his eye! yes, it was this! He had the eye
17 | of a vulture—a pale blue eye, with a film over it. Whenever it
18 | fell upon me, my blood ran cold; and so by degrees—very
19 | gradually—I made up my mind to take the life of the old man, and
20 | thus rid myself of the eye forever.
21 |
22 | Now this is the point. You fancy me mad. Madmen know nothing. But
23 | you should have seen me. You should have seen how wisely I
24 | proceeded—with what caution—with what foresight—with what
25 | dissimulation I went to work! I was never kinder to the old man
26 | than during the whole week before I killed him. And every night,
27 | about midnight, I turned the latch of his door and opened it—oh,
28 | so gently! And then, when I had made an opening sufficient for my
29 | head, I put in a dark lantern, all closed, closed, that no light
30 | shone out, and then I thrust in my head. Oh, you would have
31 | laughed to see how cunningly I thrust it in! I moved it
32 | slowly—very, very slowly, so that I might not disturb the old
33 | man’s sleep. It took me an hour to place my whole head within the
34 | opening so far that I could see him as he lay upon his bed.
35 | Ha!—would a madman have been so wise as this? And then, when my
36 | head was well in the room, I undid the lantern cautiously—oh, so
37 | cautiously—cautiously (for the hinges creaked)—I undid it just so
38 | much that a single thin ray fell upon the vulture eye. And this I
39 | did for seven long nights—every night just at midnight—but I
40 | found the eye always closed; and so it was impossible to do the
41 | work; for it was not the old man who vexed me, but his Evil Eye.
42 | And every morning, when the day broke, I went boldly into the
43 | chamber, and spoke courageously to him, calling him by name in a
44 | hearty tone, and inquiring how he has passed the night. So you
45 | see he would have been a very profound old man, indeed, to
46 | suspect that every night, just at twelve, I looked in upon him
47 | while he slept.
48 |
49 | Upon the eighth night I was more than usually cautious in opening
50 | the door. A watch’s minute hand moves more quickly than did mine.
51 | Never before that night had I felt the extent of my own powers—of
52 | my sagacity. I could scarcely contain my feelings of triumph. To
53 | think that there I was, opening the door, little by little, and
54 | he not even to dream of my secret deeds or thoughts. I fairly
55 | chuckled at the idea; and perhaps he heard me; for he moved on
56 | the bed suddenly, as if startled. Now you may think that I drew
57 | back—but no. His room was as black as pitch with the thick
58 | darkness, (for the shutters were close fastened, through fear of
59 | robbers,) and so I knew that he could not see the opening of the
60 | door, and I kept pushing it on steadily, steadily.
61 |
62 | I had my head in, and was about to open the lantern, when my
63 | thumb slipped upon the tin fastening, and the old man sprang up
64 | in bed, crying out—“Who’s there?”
65 |
66 | I kept quite still and said nothing. For a whole hour I did not
67 | move a muscle, and in the meantime I did not hear him lie down.
68 | He was still sitting up in the bed listening;—just as I have
69 | done, night after night, hearkening to the death watches in the
70 | wall.
71 |
72 | Presently I heard a slight groan, and I knew it was the groan of
73 | mortal terror. It was not a groan of pain or of grief—oh, no!—it
74 | was the low stifled sound that arises from the bottom of the soul
75 | when overcharged with awe. I knew the sound well. Many a night,
76 | just at midnight, when all the world slept, it has welled up from
77 | my own bosom, deepening, with its dreadful echo, the terrors that
78 | distracted me. I say I knew it well. I knew what the old man
79 | felt, and pitied him, although I chuckled at heart. I knew that
80 | he had been lying awake ever since the first slight noise, when
81 | he had turned in the bed. His fears had been ever since growing
82 | upon him. He had been trying to fancy them causeless, but could
83 | not. He had been saying to himself—“It is nothing but the wind in
84 | the chimney—it is only a mouse crossing the floor,” or “It is
85 | merely a cricket which has made a single chirp.” Yes, he had been
86 | trying to comfort himself with these suppositions: but he had
87 | found all in vain. All in vain; because Death, in approaching him
88 | had stalked with his black shadow before him, and enveloped the
89 | victim. And it was the mournful influence of the unperceived
90 | shadow that caused him to feel—although he neither saw nor
91 | heard—to feel the presence of my head within the room.
92 |
93 | When I had waited a long time, very patiently, without hearing
94 | him lie down, I resolved to open a little—a very, very little
95 | crevice in the lantern. So I opened it—you cannot imagine how
96 | stealthily, stealthily—until, at length a simple dim ray, like
97 | the thread of the spider, shot from out the crevice and fell full
98 | upon the vulture eye.
99 |
100 | It was open—wide, wide open—and I grew furious as I gazed upon
101 | it. I saw it with perfect distinctness—all a dull blue, with a
102 | hideous veil over it that chilled the very marrow in my bones;
103 | but I could see nothing else of the old man’s face or person: for
104 | I had directed the ray as if by instinct, precisely upon the
105 | damned spot.
106 |
107 | And have I not told you that what you mistake for madness is but
108 | over-acuteness of the sense?—now, I say, there came to my ears a
109 | low, dull, quick sound, such as a watch makes when enveloped in
110 | cotton. I knew that sound well, too. It was the beating of the
111 | old man’s heart. It increased my fury, as the beating of a drum
112 | stimulates the soldier into courage.
113 |
114 | But even yet I refrained and kept still. I scarcely breathed. I
115 | held the lantern motionless. I tried how steadily I could
116 | maintain the ray upon the eye. Meantime the hellish tattoo of the
117 | heart increased. It grew quicker and quicker, and louder and
118 | louder every instant. The old man’s terror must have been
119 | extreme! It grew louder, I say, louder every moment!—do you mark
120 | me well? I have told you that I am nervous: so I am. And now at
121 | the dead hour of the night, amid the dreadful silence of that old
122 | house, so strange a noise as this excited me to uncontrollable
123 | terror. Yet, for some minutes longer I refrained and stood still.
124 | But the beating grew louder, louder! I thought the heart must
125 | burst. And now a new anxiety seized me—the sound would be heard
126 | by a neighbour! The old man’s hour had come! With a loud yell, I
127 | threw open the lantern and leaped into the room. He shrieked
128 | once—once only. In an instant I dragged him to the floor, and
129 | pulled the heavy bed over him. I then smiled gaily, to find the
130 | deed so far done. But, for many minutes, the heart beat on with a
131 | muffled sound. This, however, did not vex me; it would not be
132 | heard through the wall. At length it ceased. The old man was
133 | dead. I removed the bed and examined the corpse. Yes, he was
134 | stone, stone dead. I placed my hand upon the heart and held it
135 | there many minutes. There was no pulsation. He was stone dead.
136 | His eye would trouble me no more.
137 |
138 | If still you think me mad, you will think so no longer when I
139 | describe the wise precautions I took for the concealment of the
140 | body. The night waned, and I worked hastily, but in silence.
141 | First of all I dismembered the corpse. I cut off the head and the
142 | arms and the legs.
143 |
144 | I then took up three planks from the flooring of the chamber, and
145 | deposited all between the scantlings. I then replaced the boards
146 | so cleverly, so cunningly, that no human eye—not even his—could
147 | have detected any thing wrong. There was nothing to wash out—no
148 | stain of any kind—no blood-spot whatever. I had been too wary for
149 | that. A tub had caught all—ha! ha!
150 |
151 | When I had made an end of these labors, it was four o’clock—still
152 | dark as midnight. As the bell sounded the hour, there came a
153 | knocking at the street door. I went down to open it with a light
154 | heart,—for what had I now to fear? There entered three men, who
155 | introduced themselves, with perfect suavity, as officers of the
156 | police. A shriek had been heard by a neighbour during the night;
157 | suspicion of foul play had been aroused; information had been
158 | lodged at the police office, and they (the officers) had been
159 | deputed to search the premises.
160 |
161 | I smiled,—for what had I to fear? I bade the gentlemen welcome.
162 | The shriek, I said, was my own in a dream. The old man, I
163 | mentioned, was absent in the country. I took my visitors all over
164 | the house. I bade them search—search well. I led them, at length,
165 | to his chamber. I showed them his treasures, secure, undisturbed.
166 | In the enthusiasm of my confidence, I brought chairs into the
167 | room, and desired them here to rest from their fatigues, while I
168 | myself, in the wild audacity of my perfect triumph, placed my own
169 | seat upon the very spot beneath which reposed the corpse of the
170 | victim.
171 |
172 | The officers were satisfied. My manner had convinced them. I was
173 | singularly at ease. They sat, and while I answered cheerily, they
174 | chatted of familiar things. But, ere long, I felt myself getting
175 | pale and wished them gone. My head ached, and I fancied a ringing
176 | in my ears: but still they sat and still chatted. The ringing
177 | became more distinct:—it continued and became more distinct: I
178 | talked more freely to get rid of the feeling: but it continued
179 | and gained definiteness—until, at length, I found that the noise
180 | was not within my ears.
181 |
182 | No doubt I now grew _very_ pale;—but I talked more fluently, and
183 | with a heightened voice. Yet the sound increased—and what could I
184 | do? It was a low, dull, quick sound—much such a sound as a watch
185 | makes when enveloped in cotton. I gasped for breath—and yet the
186 | officers heard it not. I talked more quickly—more vehemently; but
187 | the noise steadily increased. I arose and argued about trifles,
188 | in a high key and with violent gesticulations; but the noise
189 | steadily increased. Why would they not be gone? I paced the floor
190 | to and fro with heavy strides, as if excited to fury by the
191 | observations of the men—but the noise steadily increased. Oh God!
192 | what could I do? I foamed—I raved—I swore! I swung the chair upon
193 | which I had been sitting, and grated it upon the boards, but the
194 | noise arose over all and continually increased. It grew
195 | louder—louder—louder! And still the men chatted pleasantly, and
196 | smiled. Was it possible they heard not? Almighty God!—no, no!
197 | They heard!—they suspected!—they knew!—they were making a mockery
198 | of my horror!—this I thought, and this I think. But anything was
199 | better than this agony! Anything was more tolerable than this
200 | derision! I could bear those hypocritical smiles no longer! I
201 | felt that I must scream or die! and now—again!—hark! louder!
202 | louder! louder! _louder!_
203 |
204 | “Villains!” I shrieked, “dissemble no more! I admit the
205 | deed!—tear up the planks!—here, here!—It is the beating of his
206 | hideous heart!”
--------------------------------------------------------------------------------
/verify.cs:
--------------------------------------------------------------------------------
1 | #!/bin/usr/env dotnet
2 | #:package Spectre.Console@0.54.0
3 | #:property AllowUnsafeBlocks=true
4 |
5 | using System.Diagnostics;
6 | using System.Runtime.InteropServices;
7 | using System.Text.Json;
8 | using Spectre.Console;
9 |
10 | // Parse command-line arguments
11 | var useParallel = args.Contains("--parallel", StringComparer.OrdinalIgnoreCase);
12 | var timeoutSeconds = 10; // Default timeout
13 | for (int i = 0; i < args.Length; i++)
14 | {
15 | if (args[i].Equals("--timeout", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
16 | {
17 | if (int.TryParse(args[i + 1], out var parsed))
18 | {
19 | timeoutSeconds = parsed;
20 | }
21 | }
22 | }
23 |
24 | // Get the path of this script file
25 | var scriptPath = AppContext.GetData("EntryPointFilePath")?.ToString();
26 | if (string.IsNullOrEmpty(scriptPath))
27 | {
28 | AnsiConsole.MarkupLine("[red]Error: Could not determine script path[/]");
29 | return 1;
30 | }
31 |
32 | var scriptDir = Path.GetDirectoryName(scriptPath) ?? Environment.CurrentDirectory;
33 | AnsiConsole.MarkupLine($"[cyan]Scanning for .cs files from:[/] {scriptDir}");
34 | AnsiConsole.MarkupLine($"[cyan]Execution mode:[/] {(useParallel ? "Parallel" : "Sequential")}");
35 | AnsiConsole.MarkupLine($"[cyan]Timeout:[/] {timeoutSeconds}s");
36 | AnsiConsole.WriteLine();
37 |
38 | // Find all .cs files in subdirectories
39 | var csFiles = FindExecutableCsFiles(scriptDir);
40 |
41 | if (csFiles.Count == 0)
42 | {
43 | AnsiConsole.MarkupLine("[yellow]No executable .cs files found[/]");
44 | return 0;
45 | }
46 |
47 | AnsiConsole.MarkupLine($"[green]Found {csFiles.Count} .cs file(s) to verify[/]");
48 | AnsiConsole.WriteLine();
49 |
50 | // Run verification
51 | var results = useParallel
52 | ? await VerifyFilesParallel(csFiles, timeoutSeconds)
53 | : await VerifyFilesSequential(csFiles, timeoutSeconds);
54 |
55 | // Display results in a table
56 | DisplayResults(results);
57 |
58 | // Return exit code based on results
59 | var failedCount = results.Count(r => !r.Success);
60 | return failedCount > 0 ? 1 : 0;
61 |
62 | // --- Helper Methods ---
63 |
64 | bool HasVerifyLaunchProfile(string csFilePath)
65 | {
66 | try
67 | {
68 | var directory = Path.GetDirectoryName(csFilePath);
69 | var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(csFilePath);
70 | var runJsonPath = Path.Combine(directory ?? ".", $"{fileNameWithoutExtension}.run.json");
71 |
72 | if (!File.Exists(runJsonPath))
73 | {
74 | return false;
75 | }
76 |
77 | var jsonContent = File.ReadAllText(runJsonPath);
78 | using var doc = JsonDocument.Parse(jsonContent, new JsonDocumentOptions { AllowTrailingCommas = true });
79 |
80 | if (doc.RootElement.TryGetProperty("profiles", out var profiles))
81 | {
82 | return profiles.TryGetProperty("verify", out _);
83 | }
84 | }
85 | catch
86 | {
87 | // If we can't read or parse the file, just proceed without the launch profile
88 | }
89 |
90 | return false;
91 | }
92 |
93 | bool ShouldSkipFile(string csFilePath)
94 | {
95 | try
96 | {
97 | // Read the file header (up to first code line)
98 | foreach (var line in File.ReadLines(csFilePath))
99 | {
100 | var trimmed = line.Trim();
101 |
102 | // Stop at first code line (non-comment, non-directive, non-blank)
103 | if (!string.IsNullOrWhiteSpace(trimmed) &&
104 | !trimmed.StartsWith("#") &&
105 | !trimmed.StartsWith("//") &&
106 | !trimmed.StartsWith("/*"))
107 | {
108 | break;
109 | }
110 |
111 | // Check for TargetFramework property directive
112 | if (trimmed.StartsWith("#:property TargetFramework=", StringComparison.OrdinalIgnoreCase))
113 | {
114 | var tfm = trimmed.Substring("#:property TargetFramework=".Length).Trim();
115 |
116 | // Check if TFM is OS-specific and doesn't match current OS
117 | if (tfm.Contains("-windows", StringComparison.OrdinalIgnoreCase) && !OperatingSystem.IsWindows())
118 | {
119 | return true;
120 | }
121 | if (tfm.Contains("-linux", StringComparison.OrdinalIgnoreCase) && !OperatingSystem.IsLinux())
122 | {
123 | return true;
124 | }
125 | if (tfm.Contains("-macos", StringComparison.OrdinalIgnoreCase) && !OperatingSystem.IsMacOS())
126 | {
127 | return true;
128 | }
129 | }
130 | }
131 | }
132 | catch
133 | {
134 | // If we can't read the file, don't skip it
135 | }
136 |
137 | return false;
138 | }
139 |
140 | List FindExecutableCsFiles(string rootDir)
141 | {
142 | var files = new List();
143 |
144 | foreach (var dir in Directory.GetDirectories(rootDir))
145 | {
146 | // Skip if directory contains a .csproj file
147 | if (Directory.GetFiles(dir, "*.csproj").Length > 0)
148 | {
149 | continue;
150 | }
151 |
152 | // Add all .cs files in this directory (that shouldn't be skipped)
153 | foreach (var file in Directory.GetFiles(dir, "*.cs"))
154 | {
155 | if (!ShouldSkipFile(file))
156 | {
157 | files.Add(file);
158 | }
159 | }
160 |
161 | // Recursively search subdirectories
162 | files.AddRange(FindExecutableCsFiles(dir));
163 | }
164 |
165 | return files;
166 | }
167 |
168 | async Task> VerifyFilesSequential(List files, int timeoutSeconds)
169 | {
170 | var results = new List();
171 |
172 | await AnsiConsole.Progress()
173 | .Columns(
174 | new TaskDescriptionColumn(),
175 | new ProgressBarColumn(),
176 | new SpinnerColumn())
177 | .StartAsync(async ctx =>
178 | {
179 | var task = ctx.AddTask($"[green]Verifying files (0/{files.Count})[/]", maxValue: files.Count);
180 |
181 | foreach (var file in files)
182 | {
183 | var result = await VerifyFile(file, timeoutSeconds);
184 | results.Add(result);
185 | task.Increment(1);
186 | task.Description = $"[green]Verifying files ({results.Count}/{files.Count})[/]";
187 | }
188 | });
189 |
190 | return results;
191 | }
192 |
193 | async Task> VerifyFilesParallel(List files, int timeoutSeconds)
194 | {
195 | var results = new List();
196 | var lockObj = new Lock();
197 |
198 | await AnsiConsole.Progress()
199 | .Columns(
200 | new TaskDescriptionColumn(),
201 | new ProgressBarColumn(),
202 | new SpinnerColumn())
203 | .StartAsync(async ctx =>
204 | {
205 | var progressTask = ctx.AddTask($"[green]Verifying files (0/{files.Count})[/]", maxValue: files.Count);
206 |
207 | var tasks = files.Select(async file =>
208 | {
209 | var result = await VerifyFile(file, timeoutSeconds);
210 | lock (lockObj)
211 | {
212 | results.Add(result);
213 | progressTask.Increment(1);
214 | progressTask.Description = $"[green]Verifying files ({results.Count}/{files.Count})[/]";
215 | }
216 | });
217 |
218 | await Task.WhenAll(tasks);
219 | });
220 |
221 | return results;
222 | }
223 |
224 | async Task VerifyFile(string filePath, int timeoutSeconds)
225 | {
226 | var result = new VerificationResult
227 | {
228 | FilePath = filePath,
229 | FileName = Path.GetFileName(filePath)
230 | };
231 |
232 | var startInfo = new ProcessStartInfo
233 | {
234 | FileName = "dotnet",
235 | RedirectStandardOutput = true,
236 | RedirectStandardError = true,
237 | RedirectStandardInput = true,
238 | UseShellExecute = false,
239 | CreateNoWindow = true,
240 | WorkingDirectory = Path.GetDirectoryName(filePath) ?? Environment.CurrentDirectory
241 | };
242 |
243 | // Add arguments using ArgumentList to avoid escaping issues
244 | startInfo.ArgumentList.Add(filePath);
245 |
246 | // Check for .run.json with verify launch profile
247 | var hasVerifyProfile = HasVerifyLaunchProfile(filePath);
248 | if (hasVerifyProfile)
249 | {
250 | startInfo.ArgumentList.Add("--launch-profile");
251 | startInfo.ArgumentList.Add("verify");
252 | result.UsedVerifyProfile = true;
253 | }
254 |
255 | startInfo.Environment["VERIFY_MODE"] = "1";
256 |
257 | if (OperatingSystem.IsWindows())
258 | {
259 | startInfo.CreateNewProcessGroup = true;
260 | }
261 |
262 | var process = new Process { StartInfo = startInfo };
263 | var output = new List();
264 | var error = new List();
265 | var shutdownMessageDetected = false;
266 | var shutdownTcs = new TaskCompletionSource();
267 |
268 | process.OutputDataReceived += (s, e) =>
269 | {
270 | if (e.Data != null)
271 | {
272 | lock (output)
273 | {
274 | output.Add(e.Data);
275 | if (e.Data.Contains("Press Ctrl+C to shut down.") && !shutdownMessageDetected)
276 | {
277 | shutdownMessageDetected = true;
278 | shutdownTcs.TrySetResult(true);
279 | }
280 | }
281 | }
282 | };
283 |
284 | process.ErrorDataReceived += (s, e) =>
285 | {
286 | if (e.Data != null)
287 | {
288 | lock (error)
289 | {
290 | error.Add(e.Data);
291 | }
292 | }
293 | };
294 |
295 | var stopwatch = Stopwatch.StartNew();
296 |
297 | try
298 | {
299 | process.Start();
300 | process.BeginOutputReadLine();
301 | process.BeginErrorReadLine();
302 |
303 | // Wait for either: process exit, shutdown message detected, or timeout
304 | var timeout = TimeSpan.FromSeconds(timeoutSeconds);
305 | var timeoutTask = Task.Delay(timeout);
306 | var processTask = Task.Run(process.WaitForExit);
307 |
308 | var completedTask = await Task.WhenAny(processTask, shutdownTcs.Task, timeoutTask);
309 |
310 | if (completedTask == shutdownTcs.Task)
311 | {
312 | // Shutdown message detected - send shutdown signal
313 | try
314 | {
315 | process.Stop();
316 |
317 | if (!process.HasExited)
318 | {
319 | process.Kill();
320 | }
321 |
322 | stopwatch.Stop();
323 | result.Success = true;
324 | result.ExitCode = 0;
325 | result.Duration = stopwatch.Elapsed;
326 |
327 | result.FullOutput = string.Join("\n", output);
328 | result.FullError = string.Join("\n", error);
329 | result.HasStderr = error.Count > 0;
330 | result.Message = result.HasStderr ? "App started & stopped successfully" : "App started & stopped successfully";
331 | }
332 | catch
333 | {
334 | process.Kill();
335 | stopwatch.Stop();
336 | result.Success = false;
337 | result.Duration = stopwatch.Elapsed;
338 | result.Message = "Failed to gracefully stop app";
339 |
340 | result.FullOutput = string.Join("\n", output);
341 | result.FullError = string.Join("\n", error);
342 | }
343 | }
344 | else if (completedTask == timeoutTask)
345 | {
346 | // Process timed out without shutdown message
347 | var allOutput = string.Join("\n", output);
348 | var allError = string.Join("\n", error);
349 |
350 | result.FullOutput = allOutput;
351 | result.FullError = allError;
352 |
353 | process.Kill();
354 | stopwatch.Stop();
355 | result.Success = false;
356 | result.Duration = stopwatch.Elapsed;
357 | result.Message = $"Timeout ({timeout.TotalSeconds}s)";
358 | }
359 | else
360 | {
361 | // Process completed normally
362 | stopwatch.Stop();
363 | result.ExitCode = process.ExitCode;
364 | result.Duration = stopwatch.Elapsed;
365 | result.Success = process.ExitCode == 0;
366 |
367 | result.FullOutput = string.Join("\n", output);
368 | result.FullError = string.Join("\n", error);
369 | result.HasStderr = error.Count > 0;
370 |
371 | if (result.Success)
372 | {
373 | result.Message = "Completed successfully";
374 | }
375 | else if (error.Count > 0)
376 | {
377 | result.Message = string.Join("; ", error.Take(2));
378 | }
379 | else
380 | {
381 | result.Message = $"Exit code: {process.ExitCode}";
382 | }
383 | }
384 | }
385 | catch (Exception ex)
386 | {
387 | stopwatch.Stop();
388 | result.Success = false;
389 | result.Duration = stopwatch.Elapsed;
390 | result.Message = $"Error: {ex.Message}";
391 | }
392 | finally
393 | {
394 | if (!process.HasExited)
395 | {
396 | try { process.Kill(); } catch { }
397 | }
398 | process.Dispose();
399 | }
400 |
401 | return result;
402 | }
403 |
404 | void DisplayResults(List results)
405 | {
406 | var table = new Table();
407 | table.Border(TableBorder.Rounded);
408 | table.AddColumn("[bold]File[/]");
409 | table.AddColumn("[bold]Status[/]");
410 | table.AddColumn("[bold]Duration[/]");
411 | table.AddColumn("[bold]Message[/]");
412 |
413 | foreach (var result in results.OrderBy(r => r.FileName))
414 | {
415 | var statusText = result.Success
416 | ? (result.HasStderr ? "[green]✓ Pass (stderr)[/]" : "[green]✓ Pass[/]")
417 | : $"[red]✗ Fail ({result.ExitCode})[/]";
418 |
419 | var durationText = $"{result.Duration.TotalSeconds:F2}s";
420 |
421 | var fileNameDisplay = result.UsedVerifyProfile
422 | ? $"{result.FileName} [dim](verify profile)[/]"
423 | : result.FileName;
424 |
425 | // For failed apps, show full error output; for successful apps, show brief message
426 | string messageText;
427 | if (!result.Success)
428 | {
429 | // Combine error and output for failed cases
430 | var fullMessage = !string.IsNullOrEmpty(result.FullError)
431 | ? result.FullError
432 | : result.FullOutput;
433 |
434 | messageText = !string.IsNullOrEmpty(fullMessage)
435 | ? fullMessage
436 | : result.Message;
437 | }
438 | else
439 | {
440 | messageText = result.Message.Length > 60
441 | ? result.Message.Substring(0, 57) + "..."
442 | : result.Message;
443 | }
444 |
445 | table.AddRow(
446 | fileNameDisplay,
447 | statusText,
448 | durationText,
449 | messageText.EscapeMarkup()
450 | );
451 | }
452 |
453 | AnsiConsole.Write(table);
454 | AnsiConsole.WriteLine();
455 |
456 | // Summary
457 | var totalCount = results.Count;
458 | var passCount = results.Count(r => r.Success);
459 | var failCount = totalCount - passCount;
460 |
461 | var summaryTable = new Table();
462 | summaryTable.Border(TableBorder.None);
463 | summaryTable.HideHeaders();
464 | summaryTable.AddColumn("");
465 | summaryTable.AddColumn("");
466 |
467 | summaryTable.AddRow("[bold]Total:[/]", totalCount.ToString());
468 | summaryTable.AddRow("[green]Passed:[/]", passCount.ToString());
469 | if (failCount > 0)
470 | {
471 | summaryTable.AddRow("[red]Failed:[/]", failCount.ToString());
472 | }
473 |
474 | AnsiConsole.Write(summaryTable);
475 |
476 | if (failCount == 0)
477 | {
478 | AnsiConsole.WriteLine();
479 | AnsiConsole.MarkupLine("[green bold]All tests passed! ✓[/]");
480 | }
481 | }
482 |
483 | class VerificationResult
484 | {
485 | public string FilePath { get; set; } = "";
486 | public string FileName { get; set; } = "";
487 | public bool Success { get; set; }
488 | public int ExitCode { get; set; }
489 | public TimeSpan Duration { get; set; }
490 | public string Message { get; set; } = "";
491 | public string FullOutput { get; set; } = "";
492 | public string FullError { get; set; } = "";
493 | public bool HasStderr { get; set; }
494 | public bool UsedVerifyProfile { get; set; }
495 | }
496 |
497 | internal static partial class ProcessExtensions
498 | {
499 | // Code in this class adapted from https://github.com/devlooped/dotnet-stop
500 | // See THIRDPARTYNOTICES for license information.
501 | extension(Process process)
502 | {
503 | public int Stop(TimeSpan? timeout = null, bool quiet = true)
504 | {
505 | timeout ??= TimeSpan.FromSeconds(1);
506 | if (OperatingSystem.IsWindows())
507 | {
508 | return process.StopWindowsProcess(timeout.Value, quiet);
509 | }
510 | else
511 | {
512 | return process.StopUnixProcess(timeout.Value, quiet);
513 | }
514 | }
515 |
516 | int StopUnixProcess(TimeSpan timeout, bool quiet)
517 | {
518 | if (!quiet)
519 | {
520 | AnsiConsole.MarkupLine($"[yellow]Shutting down {process.ProcessName}:{process.Id}...[/]");
521 | }
522 |
523 | var killProcess = new ProcessStartInfo("kill")
524 | {
525 | UseShellExecute = true
526 | };
527 | killProcess.ArgumentList.Add("-s");
528 | killProcess.ArgumentList.Add("SIGINT");
529 | killProcess.ArgumentList.Add(process.Id.ToString());
530 | Process.Start(killProcess)?.WaitForExit();
531 |
532 | if (timeout != TimeSpan.Zero)
533 | {
534 | if (process.WaitForExit(timeout))
535 | {
536 | return 0;
537 | }
538 |
539 | if (!quiet)
540 | {
541 | AnsiConsole.MarkupLine($"[red]Timed out waiting for process {process.ProcessName}:{process.Id} to exit[/]");
542 | }
543 |
544 | return -1;
545 | }
546 | else
547 | {
548 | process.WaitForExit();
549 | return 0;
550 | }
551 | }
552 |
553 | int StopWindowsProcess(TimeSpan timeout, bool quiet)
554 | {
555 | if (!quiet)
556 | {
557 | AnsiConsole.MarkupLine($"[yellow]Shutting down {process.ProcessName}:{process.Id}...[/]");
558 | }
559 |
560 | // Send Ctrl+Break to the process group
561 | GenerateConsoleCtrlEvent(1, (uint)process.Id);
562 |
563 | if (timeout != TimeSpan.Zero)
564 | {
565 | if (process.WaitForExit(timeout))
566 | {
567 | return 0;
568 | }
569 |
570 | if (!quiet)
571 | {
572 | AnsiConsole.MarkupLine($"[red]Timed out waiting for process {process.ProcessName}:{process.Id} to exit[/]");
573 | }
574 |
575 | return -1;
576 | }
577 | else
578 | {
579 | process.WaitForExit();
580 | return 0;
581 | }
582 | }
583 | }
584 |
585 | [LibraryImport("kernel32.dll")]
586 | [return: MarshalAs(UnmanagedType.Bool)]
587 | internal static partial bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
588 | }
589 |
--------------------------------------------------------------------------------