Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ README.preview.html
docs/readme-preview.html
release/
runtime/
windows/app/**/bin/
windows/app/**/obj/
windows/dist/
1 change: 1 addition & 0 deletions windows/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### 新增

- 新增自包含的 Windows 图形化 EXE 管理器,提供壁纸库浏览与预览、一键应用、透出调节、启动/暂停/恢复、托盘控制和可选开机启动;发布文件内置 Dream Skin 引擎与 Node.js,最终用户无需手动运行脚本。
- Windows 系统托盘新增「壁纸透出」滑杆;100% 时视频保持原始画面,整页主题蒙层降为零,并随当前主题持久化;拖动滑杆不会重新传输整段视频。
- Windows 主题仓库与托盘支持本地 MP4/WebM 动态壁纸;视频静音循环、页面隐藏时暂停,并可像图片主题一样保存和切换。
- 视频采用上限 128 MB、单块 512 KiB 的 CDP 分块传输,在 renderer 内组装为 Blob URL,避免把完整视频塞进 early bootstrap payload;换主题、暂停和清理时会释放播放器与 Blob URL。
Expand Down
20 changes: 20 additions & 0 deletions windows/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@

Codex Dream Skin loads an external theme into the official Codex Windows desktop app through loopback CDP. The native sidebar, project picker, task content, and composer remain interactive. The tool does not modify WindowsApps, `app.asar`, or the app signature.

## Graphical EXE manager

`windows/app/CodexDreamSkin.Manager` is a complete Windows desktop manager. Its release artifact is a
self-contained, single-file `CodexDreamSkinManager.exe` with the tested Dream Skin engine and Node.js
runtime embedded, so end users do not need Node.js, the .NET SDK, or manual PowerShell commands.

The manager browses and previews PNG, JPEG, WebP, MP4, and WebM libraries; starts, reapplies, pauses,
restores, and switches Dream Skin; controls wallpaper reveal; exposes tray actions; and offers optional
per-user startup.

Build it with:

```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\app\build-manager.ps1
```

The build runs the Windows regression suite, embeds a trusted local Node.js 22+ runtime, publishes a
self-contained `win-x64` executable, and runs a post-publish self-test. Output is written to
`windows/dist`.

## Requirements

- The official `OpenAI.Codex` app installed from Microsoft Store and registered for the current user.
Expand Down
22 changes: 22 additions & 0 deletions windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@

Codex Dream Skin 通过本机回环 CDP 给官方 Codex Windows 桌面应用加载外部主题。它保留原生侧栏、项目选择、任务内容和输入框,不修改 WindowsApps、`app.asar` 或应用签名。

## 图形化 EXE 管理器

`windows/app/CodexDreamSkin.Manager` 提供完整的 Windows 图形化管理器。发布版是自包含的
单文件 `CodexDreamSkinManager.exe`,内置经过验证的 Dream Skin 引擎和 Node.js 运行时;最终
用户不需要安装 Node.js、.NET SDK 或手动运行 PowerShell。

管理器支持:

- 浏览、搜索并预览桌面壁纸库中的 PNG、JPEG、WebP、MP4 和 WebM。
- 一键启动或重新应用 Dream Skin、切换壁纸、暂停/恢复以及还原官方外观。
- 调整「壁纸透出」程度,100% 时视频保持原始画面且整页主题蒙层为零。
- 系统托盘控制和可选的当前用户开机启动。

开发者可使用下列命令构建单文件版本:

```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\app\build-manager.ps1
```

构建脚本会运行 Windows 回归测试、嵌入本机可信的 Node.js 22+ 运行时、发布自包含
`win-x64` EXE,并执行发布后自检。输出位于 `windows/dist`。

## 运行要求

- 从 Microsoft Store 安装且已注册到当前用户的官方 `OpenAI.Codex` 应用。
Expand Down
32 changes: 32 additions & 0 deletions windows/app/CodexDreamSkin.Manager/AutoStartManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Win32;

namespace CodexDreamSkin.Manager;

internal static class AutoStartManager
{
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string ValueName = "CodexDreamSkinManager";

public static bool IsEnabled()
{
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, false);
return key?.GetValue(ValueName) is string value &&
value.Contains(Environment.ProcessPath ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}

public static void SetEnabled(bool enabled)
{
using var key = Registry.CurrentUser.CreateSubKey(RunKeyPath, true)
?? throw new InvalidOperationException("无法打开当前用户启动项。");
if (enabled)
{
var executable = Environment.ProcessPath
?? throw new InvalidOperationException("无法确定管理器 EXE 路径。");
key.SetValue(ValueName, $"\"{executable}\" --minimized", RegistryValueKind.String);
}
else
{
key.DeleteValue(ValueName, false);
}
}
}
50 changes: 50 additions & 0 deletions windows/app/CodexDreamSkin.Manager/CodexDreamSkin.Manager.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>CodexDreamSkinManager</AssemblyName>
<RootNamespace>CodexDreamSkin.Manager</RootNamespace>
<Version>1.0.0</Version>
<Product>Codex Dream Skin Manager</Product>
<Description>One-click wallpaper manager for the official Codex Windows app.</Description>
<Authors>CCDawn</Authors>
<Copyright>Copyright © CCDawn</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<PublishTrimmed>false</PublishTrimmed>
<DebugType>embedded</DebugType>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="..\..\assets\dream-reference.jpg" LogicalName="DreamSkin.Engine.assets.dream-reference.jpg" />
<EmbeddedResource Include="..\..\assets\dream-skin.css" LogicalName="DreamSkin.Engine.assets.dream-skin.css" />
<EmbeddedResource Include="..\..\assets\renderer-inject.js" LogicalName="DreamSkin.Engine.assets.renderer-inject.js" />
<EmbeddedResource Include="..\..\assets\theme.json" LogicalName="DreamSkin.Engine.assets.theme.json" />
<EmbeddedResource Include="..\..\scripts\common-windows.ps1" LogicalName="DreamSkin.Engine.scripts.common-windows.ps1" />
<EmbeddedResource Include="..\..\scripts\config-utf8.ps1" LogicalName="DreamSkin.Engine.scripts.config-utf8.ps1" />
<EmbeddedResource Include="..\..\scripts\image-metadata.mjs" LogicalName="DreamSkin.Engine.scripts.image-metadata.mjs" />
<EmbeddedResource Include="..\..\scripts\injector.mjs" LogicalName="DreamSkin.Engine.scripts.injector.mjs" />
<EmbeddedResource Include="..\..\scripts\install-dream-skin.ps1" LogicalName="DreamSkin.Engine.scripts.install-dream-skin.ps1" />
<EmbeddedResource Include="..\..\scripts\manager-command.ps1" LogicalName="DreamSkin.Engine.scripts.manager-command.ps1" />
<EmbeddedResource Include="..\..\scripts\restore-dream-skin.ps1" LogicalName="DreamSkin.Engine.scripts.restore-dream-skin.ps1" />
<EmbeddedResource Include="..\..\scripts\start-dream-skin.ps1" LogicalName="DreamSkin.Engine.scripts.start-dream-skin.ps1" />
<EmbeddedResource Include="..\..\scripts\theme-windows.ps1" LogicalName="DreamSkin.Engine.scripts.theme-windows.ps1" />
<EmbeddedResource Include="..\..\scripts\tray-dream-skin.ps1" LogicalName="DreamSkin.Engine.scripts.tray-dream-skin.ps1" />
<EmbeddedResource Include="..\..\scripts\verify-dream-skin.ps1" LogicalName="DreamSkin.Engine.scripts.verify-dream-skin.ps1" />
<EmbeddedResource Include="$(NodeExe)" LogicalName="DreamSkin.Runtime.node.exe" Condition="Exists('$(NodeExe)')" />
<EmbeddedResource Include="$(NodeLicense)" LogicalName="DreamSkin.Runtime.NODE-LICENSE.txt" Condition="Exists('$(NodeLicense)')" />
</ItemGroup>

<Target Name="ValidateBundledRuntime" BeforeTargets="BeforeBuild">
<Error Condition="!Exists('$(NodeExe)')" Text="Set /p:NodeExe to a trusted Node.js 22+ node.exe." />
<Error Condition="!Exists('$(NodeLicense)')" Text="Set /p:NodeLicense to the matching Node.js LICENSE file." />
</Target>
</Project>
167 changes: 167 additions & 0 deletions windows/app/CodexDreamSkin.Manager/DreamSkinService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System.Diagnostics;
using System.Text.Json;

namespace CodexDreamSkin.Manager;

internal sealed class DreamSkinService
{
private readonly RuntimeProvisioner _runtime;
private readonly PowerShellRunner _runner;

public DreamSkinService(RuntimeProvisioner runtime)
{
_runtime = runtime;
_runner = new PowerShellRunner(runtime);
}

public async Task StartAsync(CancellationToken cancellationToken = default)
{
var result = await _runner.RunScriptAsync(
_runtime.StartScript,
new[] { "-Port", "9335", "-PromptRestart" },
cancellationToken);
result.ThrowIfFailed("启动 Dream Skin");
}

public async Task ApplyWallpaperAsync(string path, CancellationToken cancellationToken = default)
{
if (!File.Exists(path) || !WallpaperCatalog.IsSupported(path))
{
throw new InvalidOperationException("请选择有效的 PNG、JPEG、WebP、MP4 或 WebM 文件。");
}

var result = await RunManagerCommandAsync(
new[] { "-Action", "SetWallpaper", "-Path", path },
cancellationToken);
result.ThrowIfFailed("切换壁纸");

var status = GetStatus();
if (!status.WatcherRunning)
{
await StartAsync(cancellationToken);
}
}

public async Task SetRevealAsync(int percent, CancellationToken cancellationToken = default)
{
if (percent is < 0 or > 100)
{
throw new ArgumentOutOfRangeException(nameof(percent));
}
var result = await RunManagerCommandAsync(
new[] { "-Action", "SetReveal", "-Percent", percent.ToString() },
cancellationToken);
result.ThrowIfFailed("调整壁纸透出程度");
}

public async Task SetPausedAsync(bool paused, CancellationToken cancellationToken = default)
{
var result = await RunManagerCommandAsync(
new[] { "-Action", paused ? "Pause" : "Resume" },
cancellationToken);
result.ThrowIfFailed(paused ? "暂停皮肤" : "恢复皮肤");
}

public async Task RestoreAsync(CancellationToken cancellationToken = default)
{
var result = await _runner.RunScriptAsync(
_runtime.RestoreScript,
new[] { "-Port", "9335", "-RestoreBaseTheme", "-PromptRestart" },
cancellationToken);
result.ThrowIfFailed("恢复 Codex 官方外观");
}

public DreamSkinStatus GetStatus()
{
var statePath = Path.Combine(_runtime.StateRoot, "state.json");
var themePath = Path.Combine(_runtime.StateRoot, "active-theme", "theme.json");
var paused = File.Exists(Path.Combine(_runtime.StateRoot, "paused"));
var watcherRunning = false;
string? activeTheme = null;
string? mediaPath = null;
WallpaperKind? mediaKind = null;
var reveal = 100;

try
{
if (File.Exists(statePath))
{
using var state = JsonDocument.Parse(File.ReadAllText(statePath));
if (state.RootElement.TryGetProperty("injectorPid", out var pidElement) &&
pidElement.TryGetInt32(out var pid))
{
try
{
using var process = Process.GetProcessById(pid);
watcherRunning = !process.HasExited &&
process.ProcessName.Equals("node", StringComparison.OrdinalIgnoreCase);
}
catch
{
watcherRunning = false;
}
}
}

if (File.Exists(themePath))
{
using var theme = JsonDocument.Parse(File.ReadAllText(themePath));
if (theme.RootElement.TryGetProperty("name", out var nameElement))
{
activeTheme = nameElement.GetString();
}
if (theme.RootElement.TryGetProperty("image", out var imageElement))
{
var image = imageElement.GetString();
if (!string.IsNullOrWhiteSpace(image))
{
mediaPath = Path.Combine(Path.GetDirectoryName(themePath)!, image);
mediaKind = WallpaperCatalog.GetKind(mediaPath);
}
}
if (theme.RootElement.TryGetProperty("media", out var mediaElement) &&
mediaElement.TryGetProperty("opacity", out var opacityElement) &&
opacityElement.TryGetDouble(out var opacity))
{
reveal = Math.Clamp((int)Math.Round(opacity * 100), 0, 100);
}
}
}
catch
{
// Status rendering stays available even if an external process is replacing state atomically.
}

var codexRunning = IsAnyProcessRunning("ChatGPT") ||
IsAnyProcessRunning("Codex");
return new DreamSkinStatus(
watcherRunning,
codexRunning,
paused,
activeTheme,
mediaPath,
mediaKind,
reveal);
}

private Task<ProcessResult> RunManagerCommandAsync(
IEnumerable<string> arguments,
CancellationToken cancellationToken) =>
_runner.RunScriptAsync(_runtime.ManagerCommandScript, arguments, cancellationToken);

private static bool IsAnyProcessRunning(string processName)
{
var processes = Process.GetProcessesByName(processName);
try
{
return processes.Any(process => !process.HasExited);
}
finally
{
foreach (var process in processes)
{
process.Dispose();
}
}
}
}
1 change: 1 addition & 0 deletions windows/app/CodexDreamSkin.Manager/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using System.IO;
Loading
Loading