Skip to content

Extending Sucrose

Taiizor edited this page Jun 19, 2026 · 4 revisions

Extending Sucrose

Audience: developers / contributors. This page collects practical, pattern-based recipes for extending the Sucrose codebase: adding a setting, adding a live engine, and adding a shared project. Every recipe follows the conventions already used throughout the repository (namespace-alias acronyms, Shared Item Projects, per-executable preprocessor symbols, centralized package management). Before you start, read Code Conventions, Shared Item Projects, and Preprocessor Symbols — they describe the building blocks every recipe below relies on.

Contents


Before you begin

A few facts that shape every change in this repository:

  • There is no automated test suite. No test projects exist in any of the three solutions, so there is nothing to run to validate behavior automatically — you must build and exercise the app locally. See Testing your change.
  • CI does not build your change. The only CI compile is CodeQL, which builds seven src/Library/ projects only (Pipe, Memory, Signal, Manager, Resources, Transmission, XamlAnimatedGif). It does not build the executables, the engines, the Portal, the shared projects, or the full solution. A PR that breaks an engine or the Portal will pass CI — reviewers must build locally. See Contributing.
  • EnforceCodeStyleInBuild=true. .editorconfig style violations are build errors locally. Follow Code Conventions exactly.
  • Nullable is disabled project-wide, ImplicitUsings is enabled, and LangVersion is preview (Directory.Build.props).
  • Centralized packages. All NuGet versions live in Directory.Packages.props with transitive pinning. Reference a package by <PackageReference Include="X" /> with no version in the project file; add/update the version centrally instead.

The repository's physical src/ folder names do not match the logical layer names — see Repository Layout for the mapping you will need throughout these recipes.


Recipe 1 — Add a new setting

Settings in Sucrose are static properties backed by a key constant and a default value, persisted as JSON under %AppData%\Sucrose\ (see Settings Persistence). The same three-layer pattern is used everywhere.

1. Add the key constant

Add a const string for the setting's storage key to the matching area class under Sucrose.Memory.Manage.Constant.<Area> (e.g. src/Library/Sucrose.Memory/Manage/Constant/General.cs). Setting keys always live in Sucrose.Memory.Manage.Constant.* (aliased SMMCG and friends).

2. Add the typed accessor

Add a static property to the matching Sucrose.Manager.Manage.<Area> class (e.g. src/Library/Sucrose.Manager/Manage/General.cs). The property reads (and an optional setter writes) through the area's SettingManager, passing the constant key plus an inline default. Use the existing patterns verbatim:

// String setting with a computed default
public static string Culture => SMMI.GeneralSettingManager.GetSetting(SMMCG.Culture, SHC.CurrentUITwoLetterISOLanguageName);

// Bounded int — GetSettingStable + SHS.Clamp(value, min, max)
public static int RunStartup => SHS.Clamp(SMMI.GeneralSettingManager.GetSettingStable(SMMCG.RunStartup, 0), 0, 10);

// Bool setting with a literal default
public static bool TelemetryData => SMMI.GeneralSettingManager.GetSetting(SMMCG.TelemetryData, true);
Concern What to use
String / bool / enum value SMMI.<Area>SettingManager.GetSetting(<key>, <default>)
Bounded integer SHS.Clamp(SMMI.<Area>SettingManager.GetSettingStable(<key>, <default>), <min>, <max>)
Setting key constants Sucrose.Memory.Manage.Constant.<Area> (alias SMMCG, etc.)
Manager instances Sucrose.Manager.Manage.Internal (alias SMMI)

The Manager area classes are grouped by domain: General, System, Objectionable, Library, Portal, Engine, Backgroundog, Aurora, Cycling, Donate, Hook, Update, Warehouse, Internal. Pick the class that matches the setting's purpose.

3. Surface it in the Portal

Add a control bound to the new accessor in the relevant Portal settings page under Sucrose.Portal.Views.Pages.SettingPage.* and its ViewModel. The settings pages map one-to-one to the documented settings areas — see Settings Overview for the page→JSON-file map and Settings All Keys for the master key table to keep consistent.

4. Follow the alias convention

Every Sucrose/Skylark namespace import must be aliased to its acronym (e.g. using SMMI = Sucrose.Manager.Manage.Internal;). Never use fully-qualified names or bare using imports for Sucrose namespaces. See Code Conventions.

flowchart LR
    Ctrl["Portal control"] --> VM["ViewModel"]
    VM --> Acc["Manager.Manage.&lt;Area&gt;<br/>typed accessor"]
    Acc --> Key["Memory.Manage.Constant.&lt;Area&gt;<br/>key constant"]
    Acc --> Mgr["SettingManager"]
    Mgr --> File["%AppData%/Sucrose/Setting/<br/>&lt;Area&gt;.json"]
Loading

Recipe 2 — Add a new live engine

A live engine is a standalone WPF executable (Sucrose.Live.<Name>.exe) that renders one or more wallpaper types. Adding one touches the project file, the preprocessor symbols, a new engine-specific shared project, the cross-process enums, and the solution. The existing eight engines (WebView, CefSharp, MpvPlayer, VlcPlayer, Aurora, Nebula, Vexana, Xavier) are your templates — see Engines Overview.

1. Create the executable project

Create src/Live/Sucrose.Live.<Name>/Sucrose.Live.<Name>.csproj with:

  • SDK Microsoft.NET.Sdk
  • OutputType=WinExe, UseWPF=true, UseWindowsForms=true
  • StartupObject=$(AssemblyName).App

2. Define its preprocessor symbols

Every engine defines ENGINE plus exactly one LIVE_<NAME> symbol so shared code can branch per engine:

<DefineConstants>$(DefineConstants);ENGINE;LIVE_<NAME></DefineConstants>

The existing engine symbols are LIVE_WEBVIEW, LIVE_CEFSHARP, LIVE_MPVPLAYER, LIVE_VLCPLAYER, LIVE_AURORA, LIVE_NEBULA, LIVE_VEXANA, LIVE_XAVIER (full list in Preprocessor Symbols). Add your new LIVE_<NAME> to that set.

3. Set per-platform output paths

Engines nest their output under $(BaseOutputPath)\Live\<Name>\<arch>:

<OutputPath Condition="'$(PlatformTarget)' == 'x86'">$(BaseOutputPath)\Live\<Name>\x86</OutputPath>
<OutputPath Condition="'$(PlatformTarget)' == 'x64'">$(BaseOutputPath)\Live\<Name>\x64</OutputPath>
<OutputPath Condition="'$(PlatformTarget)' == 'arm64'">$(BaseOutputPath)\Live\<Name>\ARM64</OutputPath>

Release builds point the apphost at the shared private runtime via AppHostDotnetRoot/AppHostRelativeDotNet = ..\Sucrose.Runtime (see Publish Pipeline).

4. Reference libraries and import shared projects

Add ProjectReferences to the needed src/Library/ class libraries, then import the .projitems of the shared projects you need with Label="Shared" (typically Zip, Live, Space, Theme, Watchdog, Dependency, the base Sucrose.Shared.Engine, plus your new Sucrose.Shared.Engine.<Name>):

<Import Project="..\..\Shared\Engine\Sucrose.Shared.Engine\Sucrose.Shared.Engine.projitems" Label="Shared" />
<Import Project="..\..\Shared\Engine\Sucrose.Shared.Engine.<Name>\Sucrose.Shared.Engine.<Name>.projitems" Label="Shared" />

Always import the .projitems, never the .shproj. See Shared Item Projects.

5. Create the engine-specific shared project

Create src/Shared/Engine/Sucrose.Shared.Engine.<Name>/ with a .shproj + .projitems pair (a fresh ProjectGuid/SharedGUID and Import_RootNamespace=Sucrose.Shared.Engine.<Name>). This holds the engine-specific View/*.xaml, Helper/, Event/, and Manage/Internal.cs. Copy an existing engine shared project as a starting point — see Recipe 3 for the shared-project mechanics.

6. Register in the enums and dispatch logic

Add the engine to the relevant cross-process enums in Sucrose.Shared.Dependency.Enum (EngineType / WallpaperType) and to the per-type engine dispatch. These enums are the central contract surface; the type→engine selection and launch path is described in Engines Overview and IPC (Run.cs → Commandog ✔Live✖<engine.exe>).

7. Add everything to the solution

Add both the new .csproj and the new .shproj to src/Sucrose.slnx, each with the three platform mappings (x86, x64, ARM64).

8. Include it in the publish pipeline

The publish script .build/Sucrose.ps1 enumerates a fixed list of 18 projects ($script:Projects). To ship the engine, add it there so it is published into the app payload. See Publish Pipeline.


Recipe 3 — Add a new shared project

A Shared Item Project is a .shproj + .projitems pair whose source files are compiled into every executable that imports it — it is not a class library and produces no .dll of its own. See Shared Item Projects for the full model and the distinction from class libraries (src/Library/).

1. Create the folder and files

Create src/Shared/Sucrose.Shared.<Name>/ containing:

  • Sucrose.Shared.<Name>.shproj — copy an existing .shproj (e.g. Sucrose.Shared.Core.shproj) and generate a fresh ProjectGuid. It imports its own .projitems with Label="Shared" plus the VS CodeSharing targets:

    <PropertyGroup Label="Globals">
      <ProjectGuid>NEW-GUID-HERE</ProjectGuid>
      <MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
    </PropertyGroup>
    ...
    <Import Project="Sucrose.Shared.<Name>.projitems" Label="Shared" />
    <Import Project="$(MSBuildExtensionsPath32)\...\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
  • Sucrose.Shared.<Name>.projitems — the explicit file list. Set:

    • <HasSharedItems>true</HasSharedItems>
    • <SharedGUID>...</SharedGUID> (must match the .shproj ProjectGuid)
    • <Import_RootNamespace>Sucrose.Shared.<Name></Import_RootNamespace>
    • one <Compile Include="$(MSBuildThisFileDirectory)...cs" /> per source file (XAML uses <Page> with <Generator>MSBuild:Compile</Generator> and code-behind uses <DependentUpon>)

Note: .shproj/.projitems use the old MSBuild 2003 XML schema (xmlns=".../developer/msbuild/2003"), unlike the SDK-style .csproj. SDK-style globbing does not apply — adding a file later means editing the <Compile Include=...> list by hand.

2. Import it from each consumer

Add the .projitems import to every consuming .csproj:

<Import Project="..\..\Shared\Sucrose.Shared.<Name>\Sucrose.Shared.<Name>.projitems" Label="Shared" />

3. Organize files by role

Inside the shared project, follow the directory convention: Enum/, Helper/, Manage/ (with Manage/Manager/ and Manage/Readonly/), Struct/, Extension/, plus Services/ (for pipe/signal classes) and Event/ / View/ for engine shared projects. See Code Conventions.

4. Register in the solution

Add the .shproj to the /Shared/ folder of src/Sucrose.slnx so the IDE lists it. (Build inclusion still happens via the .projitems import in each .csproj, not via the solution.)


Recipe 4 — Add a new command to Commandog

Commandog is the central command dispatcher. Its commands use a marker-delimited wire format (✔<Command>✖<Value0>✖<Value1>…, U+2714 start / U+2716 separator), not normal --flag value syntax. This is an internal IPC protocol, not a stable public CLI — see Command Reference for the full format and disclaimer.

To add a command:

  1. Add a member to the CommandType enum (src/Shared/Sucrose.Shared.Dependency/Enum/CommandType.cs). This enum is the contract shared across processes.
  2. Add a matching case to the switch in src/Project/Sucrose.Commandog/Helper/Arguments.cs, reading typed values via Parse.ArgumentValue<T> (int/bool/enum/string).
  3. Build and emit the command from the caller. The Launcher tray actions build their strings in src/Shared/Sucrose.Shared.Launcher/Command/*.cs and call Processor.Run(Commandog, …). Follow that pattern.

Because command names are parsed case-insensitively via Enum.TryParse<CommandType>(Name, ignoreCase: true, …), the enum member name is the command name.


Testing your change

There is no test suite, so verification is manual:

  1. Restore and build the affected project(s) for your target platform:

    dotnet restore src/Sucrose.slnx
    dotnet build src/Sucrose.slnx -c Release -p:PlatformTarget=x64

    or a single project (faster while iterating):

    dotnet build src/Portal/Sucrose.Portal/Sucrose.Portal.csproj -c Release -p:PlatformTarget=x64
  2. Fix any .editorconfig errors — they fail the build (EnforceCodeStyleInBuild=true).

  3. Exercise the change in the running app. Build output goes to src/Sucrose/. For engine/Portal/settings changes, run the relevant executable and confirm behavior across the platforms you touched (X86/X64/ARM64).

  4. Remember CI won't catch you. Since CodeQL only builds 7 Library projects, a broken engine/Portal/shared-project build passes CI — your local build is the real gate. See Building From Source and Contributing.

For deeper background on the build, see Building From Source and Publish Pipeline.


See also

Home

Getting Started

Wallpaper Types

Using Sucrose

Settings Reference

Creating Wallpapers

Engine Reference

Automation & Command Line

Architecture & Internals

Data, Files & Diagnostics

Building & Contributing

Help & Support

Clone this wiki locally