-
-
Notifications
You must be signed in to change notification settings - Fork 60
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.
- Before you begin
- Recipe 1 — Add a new setting
- Recipe 2 — Add a new live engine
- Recipe 3 — Add a new shared project
- Recipe 4 — Add a new command to Commandog
- Testing your change
- See also
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..editorconfigstyle violations are build errors locally. Follow Code Conventions exactly. -
Nullableis disabled project-wide,ImplicitUsingsis enabled, andLangVersionispreview(Directory.Build.props). -
Centralized packages. All NuGet versions live in
Directory.Packages.propswith 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.
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.
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).
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.
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.
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.<Area><br/>typed accessor"]
Acc --> Key["Memory.Manage.Constant.<Area><br/>key constant"]
Acc --> Mgr["SettingManager"]
Mgr --> File["%AppData%/Sucrose/Setting/<br/><Area>.json"]
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.
Create src/Live/Sucrose.Live.<Name>/Sucrose.Live.<Name>.csproj with:
- SDK
Microsoft.NET.Sdk -
OutputType=WinExe,UseWPF=true,UseWindowsForms=true StartupObject=$(AssemblyName).App
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.
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).
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.
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.
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>).
Add both the new .csproj and the new .shproj to src/Sucrose.slnx, each with the three platform mappings (x86, x64, ARM64).
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.
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/).
Create src/Shared/Sucrose.Shared.<Name>/ containing:
-
Sucrose.Shared.<Name>.shproj— copy an existing.shproj(e.g.Sucrose.Shared.Core.shproj) and generate a freshProjectGuid. It imports its own.projitemswithLabel="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.shprojProjectGuid) <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/.projitemsuse 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.
Add the .projitems import to every consuming .csproj:
<Import Project="..\..\Shared\Sucrose.Shared.<Name>\Sucrose.Shared.<Name>.projitems" Label="Shared" />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.
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.)
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:
- Add a member to the
CommandTypeenum (src/Shared/Sucrose.Shared.Dependency/Enum/CommandType.cs). This enum is the contract shared across processes. - Add a matching
caseto the switch insrc/Project/Sucrose.Commandog/Helper/Arguments.cs, reading typed values viaParse.ArgumentValue<T>(int/bool/enum/string). - Build and emit the command from the caller. The Launcher tray actions build their strings in
src/Shared/Sucrose.Shared.Launcher/Command/*.csand callProcessor.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.
There is no test suite, so verification is manual:
-
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
-
Fix any
.editorconfigerrors — they fail the build (EnforceCodeStyleInBuild=true). -
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). -
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.
Getting Started
- Installation
- System Requirements
- Quick Start
- Portal Interface Tour
- Updating Sucrose
- Uninstalling Sucrose
Wallpaper Types
Using Sucrose
- Managing Library
- Using Store
- Customizing Wallpaper
- Multi-Monitor
- Wallpaper Cycling
- Choosing Engines
- Performance Rules
- Theme, Tray & Startup
- Discord Rich Presence
Settings Reference
- Settings Overview
- Settings: General
- Settings: Personal
- Settings: Performance
- Settings: Wallpaper
- Settings: System
- Settings: Other
- Settings: All Keys
Creating Wallpapers
- Create Overview
- Create: Step By Step
- Create: Package Format
- Create: Customization Controls
- Create: JS Bridge
- Create: Audio API
- Create: System API
- Create: Property Listener & Filters
- Create: Web Architecture
- Create: Compatibility
- Create: Example Wallpapers
- Create: Sharing & Publishing
Engine Reference
- Engines Overview
- Engine: MpvPlayer
- Engine: VlcPlayer
- Engine: WebView
- Engine: CefSharp
- Engine: Nebula
- Engine: Vexana
- Engine: Xavier
- Engine: Aurora
- Engine Comparison
Automation & Command Line
Architecture & Internals
- Architecture Overview
- Lifecycle
- Commandog Dispatcher
- Single-Instance Mutexes
- IPC
- Backgroundog Service
- Crash Reporting
- Update Internals
- Property Service
- Undo Internals
Data, Files & Diagnostics
Building & Contributing
- Building From Source
- Repository Layout
- Shared Item Projects
- Code Conventions
- Preprocessor Symbols
- Publish Pipeline
- Bundle Installer Internals
- Extending Sucrose
- Contributing
- Translating with Localizer
- Localization Coverage
- Security Policy
- Privacy & Telemetry
Help & Support