diff --git a/.github/workflows/makedark.yml b/.github/workflows/makedark.yml new file mode 100644 index 0000000..80a97fa --- /dev/null +++ b/.github/workflows/makedark.yml @@ -0,0 +1,29 @@ +name: MakeDark + +on: + release: + types: [created] + push: + branches: + - master + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 8.0.x + + - name: Setup Windows SDK + uses: GuillaumeFalourd/setup-windows10-sdk-action@v1.5 + + - name: Install dependencies + run: dotnet restore src\MakeDark.csproj + + - name: Build + run: dotnet build src\MakeDark.csproj --configuration Release --no-restore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..262d668 --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Rr]eport/ + +# Visual Studio cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.* +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axocover/ + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_/ +.*crunch*.local.xml +nCrunchTemp_*/ + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# Note: This signature is weak since it does not require VisualStudioVersion=12.0 and it may match on files not in solution folders +*__[Rr]e[Ss]harper.user +*_*.suo +*_*.user +*_*.aps +*_*.ncb +*_*.opensdf +*_*.sdf +*_*.cachefile +*_*.VC.db +*_*.bak +*__history/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git +_reportMaxUpgradeOrder.numbers.html +Backup*/Web.config* +UpgradeLog*.XML +UpgradeLog*.htm +UpgradeLog*.html +UpgradeLog*.txt + +# Windows Presentation Foundation (WPF) specific +*.g.xaml +*.g.cs + +# XAML generated resource dictionaries +*.baml + +# Backup files when performing XAML Binding debugging +*.backupid-*.BindingExpression diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0463556 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lemutec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MakeDark.sln b/MakeDark.sln new file mode 100644 index 0000000..36a5bd5 --- /dev/null +++ b/MakeDark.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34902.65 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakeDark", "src\MakeDark.csproj", "{BEBBDDC5-A479-470B-AA88-CDBDF70C8022}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BEBBDDC5-A479-470B-AA88-CDBDF70C8022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEBBDDC5-A479-470B-AA88-CDBDF70C8022}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEBBDDC5-A479-470B-AA88-CDBDF70C8022}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEBBDDC5-A479-470B-AA88-CDBDF70C8022}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E503C867-4D7D-4414-A048-1E90CF07CCD2} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..751fb82 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +[![Actions](https://github.com/lemutec/MakeDark/actions/workflows/makedark.yml/badge.svg)](https://github.com/lemutec/MakeDark/actions/workflows/makedark.yml) [![Platform](https://img.shields.io/badge/platform-Windows-blue?logo=windowsxp&color=1E9BFA)](https://dotnet.microsoft.com/zh-cn/download/dotnet/latest/runtime) + +# MakeDark + +Make your windows application launch as dark mode. + +## Usage + +Drag&drop the `*.lnk` or `*.exe` file to `makedark.exe`. + +And then the `*.lnk` file will be edited to launch using MakeDark. + +## Effect + +image-20240810143110413 diff --git a/assets/image-20240810143110413.png b/assets/image-20240810143110413.png new file mode 100644 index 0000000..9c2fb5d Binary files /dev/null and b/assets/image-20240810143110413.png differ diff --git a/src/Favicon.ico b/src/Favicon.ico new file mode 100644 index 0000000..074053e Binary files /dev/null and b/src/Favicon.ico differ diff --git a/src/Favicon.png b/src/Favicon.png new file mode 100644 index 0000000..4798823 Binary files /dev/null and b/src/Favicon.png differ diff --git a/src/Favicon.svg b/src/Favicon.svg new file mode 100644 index 0000000..41e0127 --- /dev/null +++ b/src/Favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/MakeDark.csproj b/src/MakeDark.csproj new file mode 100644 index 0000000..6d13e80 --- /dev/null +++ b/src/MakeDark.csproj @@ -0,0 +1,42 @@ + + + + WinExe + net8.0-windows + enable + enable + true + 12.0 + true + false + true + false + Favicon.ico + makedark + 0.1.0 + 0.1.0 + $(VersionPrefix)0.1.0 + Lemutec + + + + + + + + + + + + + tlbimp + 0 + 1 + f935dc20-1cf0-11d0-adb9-00c04fd58a0b + 0 + false + true + + + + diff --git a/src/NativeMethods.cs b/src/NativeMethods.cs new file mode 100644 index 0000000..b6f226b --- /dev/null +++ b/src/NativeMethods.cs @@ -0,0 +1,107 @@ +using System.Runtime.InteropServices; + +namespace MakeDark; + +internal static class NativeMethods +{ + [DllImport("user32.dll", SetLastError = true)] + public static extern nint FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindow(nint hWnd); + + [DllImport("dwmapi.dll", PreserveSig = true)] + public static extern int DwmSetWindowAttribute(nint hwnd, DwmWindowAttribute attr, ref int attrValue, int attrSize); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation); + + public static bool IsWindows10Version1809OrAbove() + { + RTL_OSVERSIONINFOEX versionInfo = new() + { + dwOSVersionInfoSize = (uint)Marshal.SizeOf(), + }; + + if (RtlGetVersion(ref versionInfo) == 0) + { + // Windows 10 1809 + return versionInfo.dwMajorVersion >= 10 && versionInfo.dwBuildNumber >= 17763; + } + + return false; + } + + public static bool EnableDarkModeForWindow(nint hWnd, bool enable = true) + { + if (IsWindows10Version1809OrAbove()) + { + int darkMode = enable ? 1 : 0; + int hr = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int)); + return hr >= 0; + } + return true; + } + + public static bool SetRoundedCorners(nint hWnd, bool enable = true) + { + if (IsWindows10Version1809OrAbove()) + { + int preference = enable ? (int)DwmWindowCornerPreference.DWMWCP_ROUND : (int)DwmWindowCornerPreference.DWMWCP_DONOTROUND; + int hr = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.WindowCornerPreference, ref preference, sizeof(int)); + return hr >= 0; + } + return true; + } + + public enum DwmWindowAttribute : uint + { + NCRenderingEnabled = 1, + NCRenderingPolicy, + TransitionsForceDisabled, + AllowNCPaint, + CaptionButtonBounds, + NonClientRtlLayout, + ForceIconicRepresentation, + Flip3DPolicy, + ExtendedFrameBounds, + HasIconicBitmap, + DisallowPeek, + ExcludedFromPeek, + Cloak, + Cloaked, + FreezeRepresentation, + PassiveUpdateMode, + UseHostBackdropBrush, + UseImmersiveDarkMode = 20, + WindowCornerPreference = 33, + BorderColor, + CaptionColor, + TextColor, + VisibleFrameBorderThickness, + SystemBackdropType, + Last, + } + + public enum DwmWindowCornerPreference : uint + { + DWMWCP_DEFAULT = 0, + DWMWCP_DONOTROUND = 1, + DWMWCP_ROUND = 2, + DWMWCP_ROUNDSMALL = 3 + } + + [StructLayout(LayoutKind.Sequential)] + public struct RTL_OSVERSIONINFOEX + { + public uint dwOSVersionInfoSize; + public uint dwMajorVersion; + public uint dwMinorVersion; + public uint dwBuildNumber; + public uint dwPlatformId; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szCSDVersion; + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..c1da129 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,71 @@ +using Lnk; +using System.Diagnostics; + +namespace MakeDark; + +internal static class Program +{ + public static void Main(string[] args) + { + if (args.Length <= 0) + { + return; + } + + string fileName = args[0]; + + if (!File.Exists(fileName)) + { + return; + } + + if (Path.GetExtension(fileName)?.ToLower().Equals(".lnk") ?? false) + { + LnkFile? src = ShortcutHelper.Open(fileName); + + if (src != null) + { + if (src.LocalPath == Environment.ProcessPath) + { + // Done. + return; + } + + LnkFile2 tar = new() + { + SourceFile = src.SourceFile, + LocalPath = Environment.ProcessPath, + WorkingDirectory = src.WorkingDirectory, + Arguments = ArgumentExtension.ToArguments([src.LocalPath, src.Arguments]), + Description = "Make Dark Mode Launcher Lnk", + IconLocation = src.LocalPath + }; + _ = ShortcutHelper.Create(tar); + } + } + else + { + using Process process = new() + { + StartInfo = new ProcessStartInfo() + { + FileName = args[0], + Arguments = string.Join(" ", args[1..].Select(a => $"\"{a}\"")) + } + }; + + process.Start(); + process.WaitForInputIdle(); + + SpinWait.SpinUntil(() => process.HasExited || process.MainWindowHandle != IntPtr.Zero); + + if (process.HasExited) + { + return; + } + + NativeMethods.EnableDarkModeForWindow(process.MainWindowHandle); + NativeMethods.SetRoundedCorners(process.MainWindowHandle); + } + } +} diff --git a/src/Properties/PublishProfiles/FolderProfile.pubxml b/src/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..c21f27f --- /dev/null +++ b/src/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Release\net8.0-windows\publish\win-x64\ + FileSystem + <_TargetId>Folder + net8.0-windows + false + win-x64 + true + false + + \ No newline at end of file diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 0000000..f0e834b --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MakeDark": { + "commandName": "Project", + "commandLineArgs": "C:\\Users\\ema\\Desktop\\LocalSend.lnk" + } + } +} \ No newline at end of file diff --git a/src/ShortcutHelper.cs b/src/ShortcutHelper.cs new file mode 100644 index 0000000..098fd57 --- /dev/null +++ b/src/ShortcutHelper.cs @@ -0,0 +1,133 @@ +using Lnk; + +namespace MakeDark; + +internal static class ShortcutHelper +{ + public static LnkFile? Open(string fileName) + { + if (!File.Exists(fileName)) + { + return null!; + } + + byte[] raw = File.ReadAllBytes(fileName); + + if (raw[0] == 0x4c) + { + LnkFile lnkObj = new(raw, fileName); + return lnkObj; + } + return null!; + } + + public static bool Create(LnkFile2 lnkFile) + { + if (string.IsNullOrWhiteSpace(lnkFile.SourceFile)) + { + return false; + } + + FileInfo lnkFileInfo = new(lnkFile.SourceFile); + + if (!Directory.Exists(lnkFileInfo.DirectoryName)) + { + _ = Directory.CreateDirectory(lnkFileInfo.DirectoryName!); + } + +#if false + IWshRuntimeLibrary.WshShell shell = new(); + IWshRuntimeLibrary.IWshShortcut shortcut = (IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(lnkFile.SourceFile); + shortcut.TargetPath = lnkFile.LocalPath; + shortcut.WorkingDirectory = lnkFile.WorkingDirectory; + shortcut.WindowStyle = 1; + shortcut.Arguments = lnkFile.Arguments; + shortcut.Description = lnkFile.Description; + shortcut.IconLocation = lnkFile.IconLocation; + shortcut.Save(); +#else + dynamic shell = null!; + dynamic shortcut = null!; + + try + { + shell = Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("72C24DD5-D70A-438B-8A42-98424B88AFB8"))!)!; + shortcut = shell.CreateShortcut(lnkFile.SourceFile); + shortcut.TargetPath = lnkFile.LocalPath; + shortcut.WorkingDirectory = lnkFile.WorkingDirectory; + shortcut.WindowStyle = 1; + shortcut.Arguments = lnkFile.Arguments; + shortcut.Description = lnkFile.Description; + shortcut.IconLocation = lnkFile.IconLocation; + shortcut.Save(); + } + finally + { + if (shortcut != null) + { + _ = System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shortcut); + } + if (shell != null) + { + _ = System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shell); + } + } +#endif + return true; + } +} + +public sealed class LnkFile2 +{ + public string? SourceFile { get; set; } + public string? LocalPath { get; set; } + public string? WorkingDirectory { get; set; } + public string? Arguments { get; set; } + public string? Description { get; set; } + public string? IconLocation { get; set; } + public int WindowStyle { get; set; } = 1; +} + +public static class ArgumentExtension +{ + public static string[] ParseArguments(string commandLine) + { + List args = []; + string currentArg = string.Empty; + bool inQuotes = false; + + for (int i = 0; i < commandLine.Length; i++) + { + char c = commandLine[i]; + + if (c == '"') + { + inQuotes = !inQuotes; + } + else if (c == ' ' && !inQuotes) + { + if (currentArg != string.Empty) + { + args.Add(currentArg); + currentArg = string.Empty; + } + } + else + { + currentArg += c; + } + } + + if (currentArg != string.Empty) + { + args.Add(currentArg); + } + + return [.. args]; + } + + public static string ToArguments(IEnumerable args) + { + return string.Join(" ", args?.Select(arg => (arg?.Contains(' ') ?? false) ? $"\"{arg}\"" : arg) ?? []); + } +}