{"id":15092952,"url":"https://github.com/kizari/testudo","last_synced_at":"2026-01-06T06:11:04.158Z","repository":{"id":245730306,"uuid":"818780465","full_name":"Kizari/Testudo","owner":"Kizari","description":"A work-in-progress experimental cross-platform library for creating lightweight desktop Blazor applications.","archived":false,"fork":false,"pushed_at":"2024-09-15T18:48:52.000Z","size":199,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-14T05:22:37.111Z","etag":null,"topics":["blazor","desktop"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kizari.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-22T21:04:14.000Z","updated_at":"2024-10-04T13:56:17.000Z","dependencies_parsed_at":"2024-06-23T18:57:51.610Z","dependency_job_id":"eacd40bd-148b-4011-8f9b-92eb52068e3c","html_url":"https://github.com/Kizari/Testudo","commit_stats":{"total_commits":5,"total_committers":2,"mean_commits":2.5,"dds":0.4,"last_synced_commit":"0a96d7610e28ac11d403745e32156fb774a53bee"},"previous_names":["kizari/testudo"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kizari%2FTestudo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kizari%2FTestudo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kizari%2FTestudo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kizari%2FTestudo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kizari","download_url":"https://codeload.github.com/Kizari/Testudo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219848590,"owners_count":16556334,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blazor","desktop"],"created_at":"2024-09-25T11:02:00.300Z","updated_at":"2026-01-06T06:11:04.116Z","avatar_url":"https://github.com/Kizari.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Testudo\n\nA work-in-progress experimental cross-platform library for creating lightweight desktop Blazor applications.\n\n## Table of contents\n\n* [Features](#Features)\n* [Getting started](#Getting-started)\n  * [Creating the project](#Creating-the-project)\n  * [Setting up dependency injection](#Setting-up-dependency-injection)\n  * [Creating the Blazor application](#Creating-the-Blazor-application)\n  * [Launching the application window](#Launching-the-application-window)\n\n## Features\n\n* Supports Linux only (Windows and macOS planned for future)\n* Supports background applications that need to still run while the main window is not shown\n* Supports multiple windows\n* Per-window dependency injection scopes (overrides Blazor's per-component scoping model)\n* Integrates with `Microsoft.Extensions.DependencyInjection`\n* Integrates with `Microsoft.Extensions.Hosting`\n\n## Getting started\n\nThe following guide explains the basics of creating a Testudo application.\n\n### Creating the project\n\n1. Create a new .NET console application project as a base.\n2. Add a folder called `wwwroot` to the root of the project, this will house the web content.\n3. Open the `.csproj` file for editing.\n4. Replace the project SDK with `Microsoft.NET.Sdk.Razor` as this is needed for using Razor components.\n5. Add a package reference to Testudo.\n6. Set everything in the `wwwroot` folder as an embedded resource. Testudo delivers embedded files to the web view at runtime so the application can be compiled into a single file.\n\nThe final `.csproj` should look like this:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk.Razor\"\u003e\n\n    \u003cPropertyGroup\u003e\n        \u003cOutputType\u003eExe\u003c/OutputType\u003e\n        \u003cTargetFramework\u003enet8.0\u003c/TargetFramework\u003e\n        \u003cImplicitUsings\u003eenable\u003c/ImplicitUsings\u003e\n        \u003cNullable\u003eenable\u003c/Nullable\u003e\n    \u003c/PropertyGroup\u003e\n    \n    \u003cItemGroup\u003e\n        \u003cPackageReference Include=\"Testudo\" Version=\"0.1.0\" /\u003e\n    \u003c/ItemGroup\u003e\n    \n    \u003cItemGroup\u003e\n        \u003cEmbeddedResource Include=\"wwwroot\\**\" /\u003e\n    \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\n### Setting up dependency injection\n\nTestudo operates via `Microsoft.Extensions.DependencyInjection` and provides extension methods to help you easily add Testudo to your service container. Ensure you have a reference to the NuGet package if you don't already have it as a dependency in your project.\n\n```xml\n    \u003cItemGroup\u003e\n        \u003cPackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"8.0.0\" /\u003e\n        \u003cPackageReference Include=\"Testudo\" Version=\"0.1.0\" /\u003e\n    \u003c/ItemGroup\u003e\n```\n\nNow you can create the service container and add Testudo as follows. Logging has also been enabled via `Microsoft.Extensions.Logging` in this example for later, but this is not necessary for Testudo to function.\n\n```csharp\nvar services = new ServiceCollection()\n    .AddLogging()\n    .AddTestudo();\n```\n\nSince Blazor creates a new scope for every Razor component, the default `IServiceProvider` implementation must be replaced with Testudo's implementation. With this implementation, scopes are created per-window so that components within the same window/webview can share state.\n\n```csharp\nvar provider = new TestudoServiceProviderFactory()\n    .CreateServiceProvider(services);\n```\n\nIf you are using `Microsoft.Extensions.Hosting`, you would add the `TestudoServiceProviderFactory` to your `IHostBuilder` instead.\n\n```csharp\nHost.CreateDefaultBuilder(args)\n    .UseServiceProviderFactory(new TestudoServiceProviderFactory())\n    .ConfigureServices((_, services) =\u003e services\n        .AddTestudo()\n    )\n);\n```\n\n### Creating the Blazor application\n\nFirstly, create a file in `wwwroot` named `index.html` and give it the standard Blazor markup.\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"utf-8\"/\u003e\n    \u003cmeta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover\"\n          name=\"viewport\"/\u003e\n    \u003ctitle\u003eMyProject\u003c/title\u003e\n    \u003cbase href=\"/\"/\u003e\n    \u003clink href=\"MyProject.styles.css\" rel=\"stylesheet\"/\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n\n\u003capp\u003eLoading...\u003c/app\u003e\n\n\u003cscript src=\"_framework/blazor.webview.js\"\u003e\u003c/script\u003e\n\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\nNote that `_framework/blazor.webview.js` is used here instead of the usual non-webview script.\n\nNext, create an `_Imports.razor` file in the project root for global `.razor` using statements. Add the common Blazor includes here, and any others you like.\n\n```csharp\n@using Microsoft.AspNetCore.Components.Routing\n@using Microsoft.AspNetCore.Components.Web\n```\n\nCreate an `App.razor` file with the standard router setup.\n\n```html\n\u003cRouter AppAssembly=\"typeof(Program).Assembly\"\u003e\n    \u003cFound Context=\"routeData\"\u003e\n        \u003cRouteView RouteData=\"routeData\" DefaultLayout=\"typeof(MainLayout)\"/\u003e\n    \u003c/Found\u003e\n    \u003cNotFound\u003e\n        \u003cPageTitle\u003eNot found\u003c/PageTitle\u003e\n        \u003cLayoutView Layout=\"@typeof(MainLayout)\"\u003e\n            \u003cp role=\"alert\"\u003eSorry, there's nothing at this address.\u003c/p\u003e\n        \u003c/LayoutView\u003e\n    \u003c/NotFound\u003e\n\u003c/Router\u003e\n```\n\nCreate a `MainLayout.razor` file. This will be a little different to the typical implementation, since Testudo isn't able to throw exceptions the usual way that Blazor does. You can use whatever method you like inside the `\u003cErrorContent\u003e` component to handle the exception, but this example will use `Microsoft.Extensions.Logging` to log the exception to the logger configured in the service container, then display some text in the web view.\n\n```html\n@inject ILogger\u003cMainLayout\u003e Logger\n\n\u003cErrorBoundary\u003e\n    \u003cChildContent\u003e\n        @Body\n    \u003c/ChildContent\u003e\n    \u003cErrorContent Context=\"exception\"\u003e\n        @{\n            Logger.LogError(exception, \"An unhandled exception occurred in the Blazor application\");\n            \u003cp\u003eUnhandled exception! Please review the logs to see what went wrong.\u003c/p\u003e\n        }\n    \u003c/ErrorContent\u003e\n\u003c/ErrorBoundary\u003e\n```\n\nFinally, you can create any pages you like. This example will just use a `HelloWorld.razor` component as follows.\n\n```html\n@page \"/\"\n\n\u003ch1\u003eHello World!\u003c/h1\u003e\n```\n\n### Launching the application window\n\nTo show your Blazor application in a desktop window, simply resolve the `IWindowManager` service and call `OpenWindow`.\n\n```csharp\nvar windowManager = provider.GetRequiredService\u003cIWindowManager\u003e();\nwindowManager.OpenWindow\u003cApp\u003e(new TestudoWindowConfiguration(\"/\"));\n```\n\nNote how the type parameter given to `OpenWindow` is that of the Blazor application's root component. You can open a window with any component you like, but typically the main window would use something like this. Also note that the constructor parameter passed to `TestudoWindowConfiguration` matches the `@page` route declared in `HelloWorld.razor`. This tells the window to navigate to the `HelloWorld.razor` page when the window opens.\n\n### Running the program\n\nFinally, you can resolve the native application from the service container and call `Run` to begin the main program loop. The loop will terminate when the service container cleans up and disposes the service. You can also dispose it manually if you wish to terminate the application programmatically.\n\n```csharp\nvar application = provider.GetRequiredService\u003cITestudoApplication\u003e();\napplication.Run();\n```\n\nBeware that `ITestudoApplication.Run` will not return until the main program loop ends.\n\nThe final `Program.cs` may look something like this.\n\n```csharp\n// Configure the dependency injection container\nvar services = new ServiceCollection()\n    .AddLogging()\n    .AddTestudo();\n    \n// Create the dependency injection container\nvar provider = new TestudoServiceProviderFactory()\n    .CreateServiceProvider(services);\n\n// Launch the main window\nvar windowManager = provider.GetRequiredService\u003cIWindowManager\u003e();\nwindowManager.OpenWindow\u003cApp\u003e(new TestudoWindowConfiguration(\"/\"));\n\n// Run the application\nvar application = provider.GetRequiredService\u003cITestudoApplication\u003e();\napplication.Run();\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkizari%2Ftestudo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkizari%2Ftestudo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkizari%2Ftestudo/lists"}