Hosts file editor (#20462)

This commit is contained in:
Davide Giacometti 2022-10-13 13:05:43 +02:00 committed by GitHub
parent ab41b61e84
commit b2e1337d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 3858 additions and 13 deletions

View File

@ -65,6 +65,10 @@
"modules\\FileExplorerPreview\\PowerToys.SvgThumbnailProvider.dll",
"modules\\FileExplorerPreview\\PowerToys.SvgThumbnailProvider.comhost.dll",
"modules\\Hosts\\PowerToys.HostsModuleInterface.dll",
"modules\\Hosts\\PowerToys.Hosts.dll",
"modules\\Hosts\\PowerToys.Hosts.exe",
"modules\\ImageResizer\\PowerToys.ImageResizer.exe",
"modules\\ImageResizer\\PowerToys.ImageResizer.dll",
"modules\\ImageResizer\\PowerToys.ImageResizerExt.dll",
@ -217,6 +221,7 @@
"modules\\FileExplorerPreview\\Microsoft.Web.WebView2.WinForms.dll",
"modules\\FileExplorerPreview\\Microsoft.Web.WebView2.Wpf.dll",
"modules\\FileExplorerPreview\\WebView2Loader.dll",
"modules\\Hosts\\Microsoft.Graphics.Canvas.Interop.dll",
"modules\\launcher\\e_sqlite3.dll",
"modules\\launcher\\LazyCache.dll",
"modules\\launcher\\SQLitePCLRaw.batteries_v2.dll",

View File

@ -200,6 +200,7 @@ steps:
**\PreviewPaneUnitTests.dll
**\UnitTests-SvgThumbnailProvider.dll
**\UnitTests-SvgPreviewHandler.dll
**\Hosts.Tests.dll
!**\obj\**
!**\ref\**

View File

@ -449,6 +449,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeasureToolUI", "src\module
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerAccentKeyboardService", "src\modules\poweraccent\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj", "{C97D9A5D-206C-454E-997E-009E227D7F02}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\Hosts\Hosts.csproj", "{31D1C81D-765F-4446-AA62-E743F6325049}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.Tests", "src\modules\Hosts\Hosts.Tests\Hosts.Tests.csproj", "{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HostsModuleInterface", "src\modules\Hosts\HostsModuleInterface\HostsModuleInterface.vcxproj", "{B41B888C-7DB8-4747-B262-4062E05A230D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@ -1804,6 +1812,42 @@ Global
{C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x64.Build.0 = Release|x64
{C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x86.ActiveCfg = Release|x64
{C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x86.Build.0 = Release|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Debug|ARM64.ActiveCfg = Debug|ARM64
{31D1C81D-765F-4446-AA62-E743F6325049}.Debug|ARM64.Build.0 = Debug|ARM64
{31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x64.ActiveCfg = Debug|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x64.Build.0 = Debug|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x86.ActiveCfg = Debug|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x86.Build.0 = Debug|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Release|ARM64.ActiveCfg = Release|ARM64
{31D1C81D-765F-4446-AA62-E743F6325049}.Release|ARM64.Build.0 = Release|ARM64
{31D1C81D-765F-4446-AA62-E743F6325049}.Release|x64.ActiveCfg = Release|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Release|x64.Build.0 = Release|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Release|x86.ActiveCfg = Release|x64
{31D1C81D-765F-4446-AA62-E743F6325049}.Release|x86.Build.0 = Release|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|ARM64.Build.0 = Debug|ARM64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x64.ActiveCfg = Debug|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x64.Build.0 = Debug|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x86.ActiveCfg = Debug|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x86.Build.0 = Debug|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|ARM64.ActiveCfg = Release|ARM64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|ARM64.Build.0 = Release|ARM64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x64.ActiveCfg = Release|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x64.Build.0 = Release|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x86.ActiveCfg = Release|x64
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x86.Build.0 = Release|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|ARM64.ActiveCfg = Debug|ARM64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|ARM64.Build.0 = Debug|ARM64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x64.ActiveCfg = Debug|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x64.Build.0 = Debug|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x86.ActiveCfg = Debug|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x86.Build.0 = Debug|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Release|ARM64.ActiveCfg = Release|ARM64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Release|ARM64.Build.0 = Release|ARM64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x64.ActiveCfg = Release|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x64.Build.0 = Release|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x86.ActiveCfg = Release|x64
{B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1956,6 +2000,10 @@ Global
{92C39820-9F84-4529-BC7D-22AAE514D63B} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{515554D1-D004-4F7F-A107-2211FC0F6B2C} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{C97D9A5D-206C-454E-997E-009E227D7F02} = {0F14491C-6369-4C45-AAA8-135814E66E6B}
{31D1C81D-765F-4446-AA62-E743F6325049} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{B41B888C-7DB8-4747-B262-4062E05A230D} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@ -15,6 +15,7 @@
<?define MouseUtilsProjectName="MouseUtils"?>
<?define AlwaysOnTopProjectName="AlwaysOnTop"?>
<?define MeasureToolProjectName="MeasureTool"?>
<?define HostsProjectName="Hosts"?>
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
<?if $(var.Platform) = x64?>
@ -121,13 +122,17 @@
<?define MeasureToolFiles=Ijwhost.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Windows.ApplicationModel.DynamicDependency.Projection.dll;Microsoft.Windows.ApplicationModel.Resources.Projection.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Power.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WinUI.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.MeasureToolCore.dll;PowerToys.MeasureToolUI.deps.json;PowerToys.MeasureToolUI.dll;PowerToys.MeasureToolUI.exe;PowerToys.MeasureToolUI.runtimeconfig.json;resources.pri;System.CodeDom.dll;System.Management.dll;WinRT.Runtime.dll;WinUIEx.dll?>
<?define PowerRenameMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>
<?define HostsFiles=ColorCode.Core.dll;ColorCode.WinUI.dll;CommunityToolkit.Common.dll;CommunityToolkit.Mvvm.dll;CommunityToolkit.WinUI.dll;CommunityToolkit.WinUI.UI.Controls.Core.dll;CommunityToolkit.WinUI.UI.Controls.DataGrid.dll;CommunityToolkit.WinUI.UI.Controls.Input.dll;CommunityToolkit.WinUI.UI.Controls.Layout.dll;CommunityToolkit.WinUI.UI.Controls.Markdown.dll;CommunityToolkit.WinUI.UI.Controls.Media.dll;CommunityToolkit.WinUI.UI.Controls.Primitives.dll;CommunityToolkit.WinUI.UI.dll;ControlzEx.dll;Ijwhost.dll;Microsoft.Extensions.Configuration.Abstractions.dll;Microsoft.Extensions.Configuration.Binder.dll;Microsoft.Extensions.Configuration.CommandLine.dll;Microsoft.Extensions.Configuration.dll;Microsoft.Extensions.Configuration.EnvironmentVariables.dll;Microsoft.Extensions.Configuration.FileExtensions.dll;Microsoft.Extensions.Configuration.Json.dll;Microsoft.Extensions.Configuration.UserSecrets.dll;Microsoft.Extensions.DependencyInjection.Abstractions.dll;Microsoft.Extensions.DependencyInjection.dll;Microsoft.Extensions.FileProviders.Abstractions.dll;Microsoft.Extensions.FileProviders.Physical.dll;Microsoft.Extensions.FileSystemGlobbing.dll;Microsoft.Extensions.Hosting.Abstractions.dll;Microsoft.Extensions.Hosting.dll;Microsoft.Extensions.Logging.Abstractions.dll;Microsoft.Extensions.Logging.Configuration.dll;Microsoft.Extensions.Logging.Console.dll;Microsoft.Extensions.Logging.Debug.dll;Microsoft.Extensions.Logging.dll;Microsoft.Extensions.Logging.EventLog.dll;Microsoft.Extensions.Logging.EventSource.dll;Microsoft.Extensions.Options.ConfigurationExtensions.dll;Microsoft.Extensions.Options.dll;Microsoft.Extensions.Primitives.dll;Microsoft.Graphics.Canvas.Interop.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Windows.ApplicationModel.DynamicDependency.Projection.dll;Microsoft.Windows.ApplicationModel.Resources.Projection.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Power.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WinUI.dll;Microsoft.Xaml.Behaviors.dll;Microsoft.Xaml.Interactions.dll;Microsoft.Xaml.Interactivity.dll;PowerToys.Common.UI.dll;PowerToys.Hosts.deps.json;PowerToys.Hosts.dll;PowerToys.Hosts.exe;PowerToys.Hosts.runtimeconfig.json;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.Settings.UI.Lib.dll;resources.pri;System.IO.Abstractions.dll;System.Management.dll;System.Text.Json.dll;WinRT.Runtime.dll;WinUIEx.dll;icon.ico?>
<?define PowerRenameMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>
<?define WinAppSDKFiles=CoreMessagingXP.dll;DWriteCore.dll;DwmSceneI.dll;MRM.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.ui.xaml.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;PushNotificationsLongRunningTask.ProxyStub.dll;WinUIEdit.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;dcompi.dll;dwmcorei.dll;marshal.dll;wuceffectsi.dll?>
<?define PowerToysInteropFiles=concrt140.dll;msvcp140.dll;msvcp140_1.dll;msvcp140_2.dll;msvcp140_atomic_wait.dll;msvcp140_codecvt_ids.dll;PowerToys.Interop.dll;vcamp140.dll;vccorlib140.dll;vcomp140.dll;vcruntime140.dll;vcruntime140_1.dll?>
<?define MeasureToolMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>
<?define HostsMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>
<?define PowerAccentFiles=ControlzEx.dll;GongSolutions.WPF.DragDrop.dll;Ijwhost.dll;MahApps.Metro.dll;Microsoft.Xaml.Behaviors.dll;PowerAccent.Core.dll;PowerAccent.deps.json;PowerAccent.dll;PowerAccent.exe;PowerAccent.runtimeconfig.json;PowerToys.PowerAccentModuleInterface.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.PowerAccent.deps.json;PowerToys.PowerAccent.dll;PowerToys.PowerAccent.exe;PowerToys.PowerAccent.runtimeconfig.json;PowerToys.Settings.UI.Lib.dll;System.IO.Abstractions.dll;System.Management.dll;System.Text.Json.dll;Vanara.Core.dll;Vanara.PInvoke.ComCtl32.dll;Vanara.PInvoke.Cryptography.dll;Vanara.PInvoke.Gdi32.dll;Vanara.PInvoke.Kernel32.dll;Vanara.PInvoke.Ole.dll;Vanara.PInvoke.Rpc.dll;Vanara.PInvoke.Security.dll;Vanara.PInvoke.Shared.dll;Vanara.PInvoke.Shell32.dll;Vanara.PInvoke.ShlwApi.dll;Vanara.PInvoke.User32.dll;PowerToys.PowerAccentKeyboardService.dll;Microsoft.Windows.SDK.NET.dll;WinRT.Runtime.dll?>
@ -543,7 +548,14 @@
<Directory Id="MeasureToolMicrosoftUIXamlAssetsInstallFolder" Name="Assets" />
</Directory>
</Directory>
<!-- Hosts -->
<Directory Id="HostsInstallFolder" Name="$(var.HostsProjectName)">
<Directory Id="HostsMicrosoftUIXamlInstallFolder" Name="Microsoft.UI.Xaml">
<Directory Id="HostsMicrosoftUIXamlAssetsInstallFolder" Name="Assets" />
</Directory>
</Directory>
<!-- Launcher -->
<Directory Id="LauncherInstallFolder" Name="launcher">
<Directory Id="LauncherImagesFolder" Name="Images" />
@ -1079,6 +1091,28 @@
<?endforeach?>
</DirectoryRef>
<!-- Hosts -->
<DirectoryRef Id="HostsInstallFolder" FileSource="$(var.BinDir)modules\$(var.HostsProjectName)">
<Component Id="Module_HostsInterface" Win64="yes">
<File Source="$(var.BinDir)modules\$(var.HostsProjectName)\PowerToys.HostsModuleInterface.dll" />
</Component>
<?foreach File in $(var.HostsFiles)?>
<Component Id="H_$(var.File)" Win64="yes">
<File Id="H_$(var.File)" Source="$(var.BinDir)modules\$(var.HostsProjectName)\$(var.File)" />
</Component>
<?endforeach?>
</DirectoryRef>
<DirectoryRef Id="HostsMicrosoftUIXamlAssetsInstallFolder" FileSource="$(var.BinDir)modules\$(var.HostsProjectName)\Microsoft.UI.Xaml\Assets">
<?foreach File in $(var.HostsMicrosoftUIXamlAssetsInstallFiles)?>
<Component Id="HostsMicrosoftUIXamlAssets_$(var.File)" Win64="yes">
<File Id="HostsMicrosoftUIXamlAssetsFile_$(var.File)" Source="$(var.BinDir)modules\$(var.HostsProjectName)\Microsoft.UI.Xaml\Assets\$(var.File)" />
</Component>
<?endforeach?>
</DirectoryRef>
<!-- SettingsV2 components -->
<DirectoryRef Id="SettingsV2InstallFolder" FileSource="$(var.BinDir)Settings\">
<?foreach File in $(var.SettingsV2Files)?>
@ -1227,6 +1261,13 @@
<?foreach File in $(var.MeasureToolMicrosoftUIXamlAssetsInstallFiles)?>
<ComponentRef Id="MeasureToolMicrosoftUIXamlAssets_$(var.File)" />
<?endforeach?>
<ComponentRef Id="Module_HostsInterface"/>
<?foreach File in $(var.HostsFiles)?>
<ComponentRef Id="H_$(var.File)" />
<?endforeach?>
<?foreach File in $(var.HostsMicrosoftUIXamlAssetsInstallFiles)?>
<ComponentRef Id="HostsMicrosoftUIXamlAssets_$(var.File)" />
<?endforeach?>
<?foreach File in $(var.SettingsV2Files)?>
<ComponentRef Id="SV2C_$(var.File)" />
@ -1580,7 +1621,7 @@
<!-- Localization languages shipped with WinAppSDK. We should ship these as well. -->
<?define WinAppSDKLocLanguageList = af-ZA;ar-SA;az-Latn-AZ;bg-BG;bs-Latn-BA;ca-ES;cs-CZ;cy-GB;da-DK;de-DE;el-GR;en-GB;en-us;es-ES;es-MX;et-EE;eu-ES;fa-IR;fi-FI;fr-CA;fr-FR;gl-ES;he-IL;hi-IN;hr-HR;hu-HU;id-ID;is-IS;it-IT;ja-JP;ka-GE;kk-KZ;ko-KR;lt-LT;lv-LV;ms-MY;nb-NO;nl-NL;nn-NO;pl-PL;pt-BR;pt-PT;ro-RO;ru-RU;sk-SK;sl-SI;sq-AL;sr-Cyrl-RS;sr-Latn-RS;sv-SE;th-TH;tr-TR;uk-UA;vi-VN;zh-CN;zh-TW?>
<Fragment>
<?foreach ParentDirectory in SettingsV2InstallFolder;PowerRenameInstallFolder;MeasureToolInstallFolder?>
<?foreach ParentDirectory in SettingsV2InstallFolder;PowerRenameInstallFolder;MeasureToolInstallFolder;HostsInstallFolder?>
<DirectoryRef Id="$(var.ParentDirectory)">
<?foreach Language in $(var.WinAppSDKLocLanguageList)?>
<?if $(var.Language) = af-ZA?>
@ -1899,6 +1940,13 @@
<File Id="MeasureTool_WinAppSDKLoc_$(var.IdSafeLanguage)_XamlMui_File" Source="$(var.BinDir)modules\$(var.MeasureToolProjectName)\$(var.Language)\Microsoft.ui.xaml.dll.mui" />
<File Id="MeasureTool_WinAppSDKLoc_$(var.IdSafeLanguage)_XamlPhoneMui_File" Source="$(var.BinDir)modules\$(var.MeasureToolProjectName)\$(var.Language)\Microsoft.UI.Xaml.Phone.dll.mui" />
</Component>
<Component
Id="Hosts_WinAppSDKLoc_$(var.IdSafeLanguage)_Component"
Directory="WinAppSDKLoc$(var.IdSafeLanguage)HostsInstallFolder"
Guid="$(var.CompGUIDPrefix)04">
<File Id="Hosts_WinAppSDKLoc_$(var.IdSafeLanguage)_XamlMui_File" Source="$(var.BinDir)modules\$(var.HostsProjectName)\$(var.Language)\Microsoft.ui.xaml.dll.mui" />
<File Id="Hosts_WinAppSDKLoc_$(var.IdSafeLanguage)_XamlPhoneMui_File" Source="$(var.BinDir)modules\$(var.HostsProjectName)\$(var.Language)\Microsoft.UI.Xaml.Phone.dll.mui" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
<?endforeach?>

View File

@ -1049,7 +1049,7 @@ UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder, winAppSDKFilesSrcDir, settingsDir, powerRenameDir, measureToolDir;
std::wstring installationFolder, winAppSDKFilesSrcDir, settingsDir, powerRenameDir, measureToolDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "CreateWinAppSDKHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@ -1058,6 +1058,7 @@ UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
ExitOnFailure(hr, "Failed to get installation folder");
winAppSDKFilesSrcDir = installationFolder + L"dll\\WinAppSDK\\";
hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
settingsDir = installationFolder + L"Settings\\";
powerRenameDir = installationFolder + L"modules\\PowerRename\\";
measureToolDir = installationFolder + L"modules\\MeasureTool\\";
@ -1065,6 +1066,7 @@ UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
for (auto file : winAppSdkFiles)
{
std::error_code ec;
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (hostsFileEditorDir + file).c_str(), ec);
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (settingsDir + file).c_str(), ec);
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (powerRenameDir + file).c_str(), ec);
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (measureToolDir + file).c_str(), ec);
@ -1088,7 +1090,7 @@ UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall)
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder, interopFilesSrcDir, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir,
imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir;
imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "CreatePTInteropHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@ -1101,6 +1103,7 @@ UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall)
powerOCRDir = installationFolder + L"modules\\PowerOCR\\";
launcherDir = installationFolder + L"modules\\launcher\\";
fancyZonesDir = installationFolder + L"modules\\FancyZones\\";
hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
imageResizerDir = installationFolder + L"modules\\ImageResizer\\";
settingsDir = installationFolder + L"Settings\\";
awakeDir = installationFolder + L"modules\\Awake\\";
@ -1114,6 +1117,7 @@ UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall)
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (powerOCRDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (launcherDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (fancyZonesDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (hostsFileEditorDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (imageResizerDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (settingsDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (awakeDir + file).c_str(), ec);
@ -1138,7 +1142,7 @@ UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder, settingsDir, powerRenameDir, measureToolDir;
std::wstring installationFolder, settingsDir, powerRenameDir, measureToolDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "DeleteWinAppSDKHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@ -1146,6 +1150,7 @@ UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get installation folder");
hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
settingsDir = installationFolder + L"Settings\\";
powerRenameDir = installationFolder + L"modules\\PowerRename\\";
measureToolDir = installationFolder + L"modules\\MeasureTool\\";
@ -1154,6 +1159,7 @@ UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
for (auto file : winAppSdkFiles)
{
DeleteFile((hostsFileEditorDir + file).c_str());
DeleteFile((settingsDir + file).c_str());
DeleteFile((powerRenameDir + file).c_str());
DeleteFile((measureToolDir + file).c_str());
@ -1178,7 +1184,7 @@ UINT __stdcall DeletePTInteropHardlinksCA(MSIHANDLE hInstall)
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder, interopFilesSrcDir, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir,
imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir;
imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "DeletePTInteropHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@ -1190,6 +1196,7 @@ UINT __stdcall DeletePTInteropHardlinksCA(MSIHANDLE hInstall)
powerOCRDir = installationFolder + L"modules\\PowerOCR\\";
launcherDir = installationFolder + L"modules\\launcher\\";
fancyZonesDir = installationFolder + L"modules\\FancyZones\\";
hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
imageResizerDir = installationFolder + L"modules\\ImageResizer\\";
settingsDir = installationFolder + L"Settings\\";
awakeDir = installationFolder + L"modules\\Awake\\";
@ -1204,6 +1211,7 @@ UINT __stdcall DeletePTInteropHardlinksCA(MSIHANDLE hInstall)
DeleteFile((powerOCRDir + file).c_str());
DeleteFile((launcherDir + file).c_str());
DeleteFile((fancyZonesDir + file).c_str());
DeleteFile((hostsFileEditorDir + file).c_str());
DeleteFile((imageResizerDir + file).c_str());
DeleteFile((settingsDir + file).c_str());
DeleteFile((awakeDir + file).c_str());

View File

@ -23,6 +23,7 @@ namespace Common.UI
FileExplorer,
ShortcutGuide,
VideoConference,
Hosts,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@ -53,6 +54,8 @@ namespace Common.UI
return "ShortcutGuide";
case SettingsWindow.VideoConference:
return "VideoConference";
case SettingsWindow.Hosts:
return "Hosts";
default:
{
return string.Empty;

View File

@ -35,6 +35,8 @@ struct LogSettings
inline const static std::string alwaysOnTopLoggerName = "always-on-top";
inline const static std::string powerOcrLoggerName = "TextExtractor";
inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt";
inline const static std::string hostsLoggerName = "hosts";
inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.txt";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();

View File

@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Hosts.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Hosts.Tests
{
[TestClass]
public class EntryTest
{
[DataTestMethod]
[DataRow("\t\t10.1.1.1\t\thost\t\t", "10.1.1.1", "host", "", true)]
[DataRow(" 10.1.1.1 host ", "10.1.1.1", "host", "", true)]
[DataRow("10.1.1.1 host", "10.1.1.1", "host", "", true)]
[DataRow("\t\t#\t\t10.1.1.1\thost\t\t", "10.1.1.1", "host", "", false)]
[DataRow(" # 10.1.1.1 host ", "10.1.1.1", "host", "", false)]
[DataRow("#10.1.1.1 host", "10.1.1.1", "host", "", false)]
[DataRow("\t\t10.1.1.1\t\thost\t\t#\t\tcomment\t\t", "10.1.1.1", "host", "comment", true)]
[DataRow(" 10.1.1.1 host # comment ", "10.1.1.1", "host", "comment", true)]
[DataRow("10.1.1.1 host#comment", "10.1.1.1", "host", "comment", true)]
[DataRow("\t\t#\t\t10.1.1.1\thost\t\t#\t\tcomment\t\t", "10.1.1.1", "host", "comment", false)]
[DataRow(" # 10.1.1.1 host # comment ", "10.1.1.1", "host", "comment", false)]
[DataRow("#10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
[DataRow("# #10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
[DataRow("# #\t10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
[DataRow("# # \t10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
public void Valid_Entry_SingleHost(string line, string address, string host, string comment, bool active)
{
var entry = new Entry(line);
Assert.AreEqual(entry.Address, address);
Assert.AreEqual(entry.Hosts, host);
Assert.AreEqual(entry.Comment, comment);
Assert.AreEqual(entry.Active, active);
Assert.IsTrue(entry.Valid);
}
[DataTestMethod]
[DataRow("\t\t10.1.1.1\t\thost host.local\t\t", "10.1.1.1", "host host.local", "", true)]
[DataRow(" 10.1.1.1 host host.local ", "10.1.1.1", "host host.local", "", true)]
[DataRow("10.1.1.1 host host.local", "10.1.1.1", "host host.local", "", true)]
[DataRow("\t\t#\t\t10.1.1.1\thost\t\thost.local\t\t", "10.1.1.1", "host host.local", "", false)]
[DataRow(" # 10.1.1.1 host host.local ", "10.1.1.1", "host host.local", "", false)]
[DataRow("#10.1.1.1 host host.local", "10.1.1.1", "host host.local", "", false)]
[DataRow("\t\t10.1.1.1\t\thost\t\thost.local\t\t#\t\tcomment\t\t", "10.1.1.1", "host host.local", "comment", true)]
[DataRow(" 10.1.1.1 host host.local # comment ", "10.1.1.1", "host host.local", "comment", true)]
[DataRow("10.1.1.1 host host.local#comment", "10.1.1.1", "host host.local", "comment", true)]
[DataRow("\t\t#\t\t10.1.1.1\thost\t\thost.local\t\t#\t\tcomment\t\t", "10.1.1.1", "host host.local", "comment", false)]
[DataRow(" # 10.1.1.1 host host.local # comment ", "10.1.1.1", "host host.local", "comment", false)]
[DataRow("#10.1.1.1 host host.local#comment", "10.1.1.1", "host host.local", "comment", false)]
public void Valid_Entry_MultipleHosts(string line, string address, string host, string comment, bool active)
{
var entry = new Entry(line);
Assert.AreEqual(entry.Address, address);
Assert.AreEqual(entry.Hosts, host);
Assert.AreEqual(entry.Comment, comment);
Assert.AreEqual(entry.Active, active);
Assert.IsTrue(entry.Valid);
}
[DataTestMethod]
[DataRow("\t\t10.1.1.1\t\t")]
[DataRow(" 10.1.1.1 ")]
[DataRow("10.1.1.1")]
[DataRow("\t\thost\t\t")]
[DataRow(" host ")]
[DataRow("host")]
[DataRow("\t\t10\t\thost")]
[DataRow(" 10 host ")]
[DataRow("10 host")]
[DataRow("\t\thost\t\t10.1.1.1")]
[DataRow(" host 10.1.1.1")]
[DataRow("host 10.1.1.1")]
public void Not_Valid_Entry(string line)
{
var entry = new Entry(line);
Assert.IsFalse(entry.Valid);
}
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
<RuntimeIdentifiers>win10-x64;win10-arm64</RuntimeIdentifiers>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\Hosts\Hosts.Tests\</OutputPath>
<IntermediateOutputPath>$(SolutionDir)$(Platform)\$(Configuration)\obj\$(AssemblyName)\</IntermediateOutputPath>
<RootNamespace>Hosts.Tests</RootNamespace>
<AssemblyName>PowerToys.Hosts.Tests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
<PackageReference Include="System.IO.Abstractions" Version="12.2.5" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="12.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hosts\Hosts.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,255 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
using Hosts.Helpers;
using Hosts.Models;
using Hosts.Settings;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Settings.UI.Library.Enumerations;
namespace Hosts.Tests
{
[TestClass]
public class HostsServiceTest
{
private static Mock<IElevationHelper> _elevationHelper;
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
_elevationHelper = new Mock<IElevationHelper>();
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
}
[TestMethod]
public void Hosts_Exists()
{
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
var result = service.Exists();
Assert.IsTrue(result);
}
[TestMethod]
public void Hosts_Not_Exists()
{
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
})
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var result = service.Exists();
Assert.IsFalse(result);
}
[TestMethod]
public async Task Host_Added()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var contentResult =
@" 10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
# 10.1.1.30 host30 host30.local # new entry
";
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (_, entries) = await service.ReadAsync();
entries.Add(new Entry("10.1.1.30", "host30 host30.local", "new entry", false));
await service.WriteAsync(string.Empty, entries);
var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult);
}
[TestMethod]
public async Task Host_Deleted()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var contentResult =
@"10.1.1.2 host2 host2.local # another comment
";
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (_, entries) = await service.ReadAsync();
entries.RemoveAt(0);
await service.WriteAsync(string.Empty, entries);
var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult);
}
[TestMethod]
public async Task Host_Updated()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var contentResult =
@"# 10.1.1.10 host host.local host1.local # updated comment
10.1.1.2 host2 host2.local # another comment
";
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (_, entries) = await service.ReadAsync();
var entry = entries[0];
entry.Address = "10.1.1.10";
entry.Hosts = "host host.local host1.local";
entry.Comment = "updated comment";
entry.Active = false;
await service.WriteAsync(string.Empty, entries);
var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult);
}
[TestMethod]
public async Task Empty_Hosts()
{
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, string.Empty);
}
[TestMethod]
public async Task AdditionalLines_Top()
{
var content =
@"# header
10.1.1.1 host host.local # comment
# comment
10.1.1.2 host2 host2.local # another comment
# footer
";
var contentResult =
@"# header
# comment
# footer
10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(AdditionalLinesPosition.Top);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (additionalLines, entries) = await service.ReadAsync();
await service.WriteAsync(additionalLines, entries);
var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult);
}
[TestMethod]
public async Task AdditionalLines_Bottom()
{
var content =
@"# header
10.1.1.1 host host.local # comment
# comment
10.1.1.2 host2 host2.local # another comment
# footer
";
var contentResult =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
# header
# comment
# footer
";
var fileSystem = new MockFileSystem
{
FileSystemWatcher = new TestFileSystemWatcherFactory(),
};
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(AdditionalLinesPosition.Bottom);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (additionalLines, entries) = await service.ReadAsync();
await service.WriteAsync(additionalLines, entries);
var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult);
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.IO;
using System.IO.Abstractions;
namespace Hosts.Tests
{
public class TestFileSystemWatcher : FileSystemWatcherBase
{
public override bool IncludeSubdirectories { get; set; }
public override bool EnableRaisingEvents { get; set; }
public override string Filter { get; set; }
public override int InternalBufferSize { get; set; }
public override NotifyFilters NotifyFilter { get; set; }
public override string Path { get; set; }
public override ISite Site { get; set; }
public override ISynchronizeInvoke SynchronizingObject { get; set; }
public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType) => default(WaitForChangedResult);
public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int timeout) => default(WaitForChangedResult);
public TestFileSystemWatcher(string path) => Path = path;
public TestFileSystemWatcher(string path, string filter)
{
Path = path;
Filter = filter;
}
public override void BeginInit()
{
}
public override void EndInit()
{
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO.Abstractions;
namespace Hosts.Tests
{
public class TestFileSystemWatcherFactory : IFileSystemWatcherFactory
{
public IFileSystemWatcher CreateNew() => new TestFileSystemWatcher(null);
public IFileSystemWatcher CreateNew(string path) => new TestFileSystemWatcher(path);
public IFileSystemWatcher CreateNew(string path, string filter) => new TestFileSystemWatcher(path, filter);
public IFileSystemWatcher FromPath(string path) => new TestFileSystemWatcher(path);
}
}

View File

@ -0,0 +1,115 @@
<Application
x:Class="Hosts.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Hosts">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="SubtleButtonBackground" Color="{ThemeResource SubtleFillColorTransparent}" />
<SolidColorBrush x:Key="SubtleButtonBackgroundPointerOver" Color="{ThemeResource SubtleFillColorSecondary}" />
<SolidColorBrush x:Key="SubtleButtonBackgroundPressed" Color="{ThemeResource SubtleFillColorTertiary}" />
<SolidColorBrush x:Key="SubtleButtonBackgroundDisabled" Color="{ThemeResource ControlFillColorDisabled}" />
<SolidColorBrush x:Key="SubtleButtonForeground" Color="{ThemeResource TextFillColorPrimary}" />
<SolidColorBrush x:Key="SubtleButtonForegroundPointerOver" Color="{ThemeResource TextFillColorPrimary}" />
<SolidColorBrush x:Key="SubtleButtonForegroundPressed" Color="{ThemeResource TextFillColorSecondary}" />
<SolidColorBrush x:Key="SubtleButtonForegroundDisabled" Color="{ThemeResource TextFillColorDisabled}" />
<Style x:Key="SubtleButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource SubtleButtonBackground}" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="Foreground" Value="{ThemeResource SubtleButtonForeground}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</ContentPresenter.BackgroundTransition>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO.Abstractions;
using System.Threading;
using Hosts.Helpers;
using Hosts.Settings;
using Hosts.ViewModels;
using Hosts.Views;
using ManagedCommon;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
namespace Hosts
{
public partial class App : Application
{
private Window _window;
public IHost Host
{
get;
}
public static T GetService<T>()
where T : class
{
if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
}
public App()
{
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
// Core Services
services.AddSingleton<IFileSystem, FileSystem>();
services.AddSingleton<IHostsService, HostsService>();
services.AddSingleton<IUserSettings, UserSettings>();
services.AddSingleton<IElevationHelper, ElevationHelper>();
// Views and ViewModels
services.AddTransient<MainPage>();
services.AddTransient<MainViewModel>();
}).
Build();
UnhandledException += App_UnhandledException;
new Thread(() =>
{
// Delete old backups only if running elevated
if (!GetService<IElevationHelper>().IsElevated)
{
return;
}
try
{
GetService<IHostsService>().CleanupBackup();
}
catch (Exception ex)
{
Logger.LogError("Failed to delete backup", ex);
}
}).Start();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
{
if (int.TryParse(cmdArgs[cmdArgs.Length - 1], out int powerToysRunnerPid))
{
Logger.LogInfo($"Hosts started from the PowerToys Runner. Runner pid={powerToysRunnerPid}");
var dispatcher = DispatcherQueue.GetForCurrentThread();
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting Hosts");
dispatcher.TryEnqueue(App.Current.Exit);
});
}
}
else
{
Logger.LogInfo($"Hosts started detached from PowerToys Runner.");
}
_window = new MainWindow();
_window.Activate();
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Security.Principal;
namespace Hosts.Helpers
{
public class ElevationHelper : IElevationHelper
{
private readonly bool _isElevated;
public bool IsElevated => _isElevated;
public ElevationHelper()
{
_isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
}
}
}

View File

@ -0,0 +1,238 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Hosts.Models;
using Hosts.Settings;
using Settings.UI.Library.Enumerations;
namespace Hosts.Helpers
{
public class HostsService : IHostsService, IDisposable
{
private const string _backupSuffix = $"_PowerToysBackup_";
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
private readonly IFileSystem _fileSystem;
private readonly IUserSettings _userSettings;
private readonly IElevationHelper _elevationHelper;
private readonly IFileSystemWatcher _fileSystemWatcher;
private readonly string _hostsFilePath;
private bool _backupDone;
private bool _disposed;
public string HostsFilePath => _hostsFilePath;
public event EventHandler FileChanged;
public HostsService(
IFileSystem fileSystem,
IUserSettings userSettings,
IElevationHelper elevationHelper)
{
_fileSystem = fileSystem;
_userSettings = userSettings;
_elevationHelper = elevationHelper;
_hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts");
_fileSystemWatcher = _fileSystem.FileSystemWatcher.CreateNew();
_fileSystemWatcher.Path = _fileSystem.Path.GetDirectoryName(HostsFilePath);
_fileSystemWatcher.Filter = _fileSystem.Path.GetFileName(HostsFilePath);
_fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite;
_fileSystemWatcher.Changed += FileSystemWatcher_Changed;
_fileSystemWatcher.EnableRaisingEvents = true;
}
public bool Exists()
{
return _fileSystem.File.Exists(HostsFilePath);
}
public async Task<(string Unparsed, List<Entry> Entries)> ReadAsync()
{
var entries = new List<Entry>();
var unparsedBuilder = new StringBuilder();
if (!Exists())
{
return (unparsedBuilder.ToString(), entries);
}
var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var entry = new Entry(line);
if (entry.Valid)
{
entries.Add(entry);
}
else
{
if (unparsedBuilder.Length > 0)
{
unparsedBuilder.Append(Environment.NewLine);
}
unparsedBuilder.Append(line);
}
}
return (unparsedBuilder.ToString(), entries);
}
public async Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries)
{
if (!_elevationHelper.IsElevated)
{
return false;
}
var lines = new List<string>();
if (entries.Any())
{
var addressPadding = entries.Max(e => e.Address.Length) + 1;
var hostsPadding = entries.Max(e => e.Hosts.Length) + 1;
var anyDisabled = entries.Any(e => !e.Active);
foreach (var e in entries)
{
var lineBuilder = new StringBuilder();
if (!e.Valid)
{
lineBuilder.Append(e.GetLine());
}
else
{
if (!e.Active)
{
lineBuilder.Append('#').Append(' ');
}
else if (anyDisabled)
{
lineBuilder.Append(' ').Append(' ');
}
lineBuilder.Append(e.Address.PadRight(addressPadding));
lineBuilder.Append(string.Join(' ', e.Hosts).PadRight(hostsPadding));
if (e.Comment != string.Empty)
{
lineBuilder.Append('#').Append(' ');
lineBuilder.Append(e.Comment);
}
lines.Add(lineBuilder.ToString().TrimEnd());
}
}
}
if (!string.IsNullOrWhiteSpace(additionalLines))
{
if (_userSettings.AdditionalLinesPosition == AdditionalLinesPosition.Top)
{
lines.Insert(0, additionalLines);
}
else if (_userSettings.AdditionalLinesPosition == AdditionalLinesPosition.Bottom)
{
lines.Add(additionalLines);
}
}
try
{
await _asyncLock.WaitAsync();
_fileSystemWatcher.EnableRaisingEvents = false;
if (!_backupDone && Exists())
{
_fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture));
_backupDone = true;
}
await _fileSystem.File.WriteAllLinesAsync(HostsFilePath, lines);
}
catch (Exception ex)
{
Logger.LogError("Failed to write hosts file", ex);
return false;
}
finally
{
_fileSystemWatcher.EnableRaisingEvents = true;
_asyncLock.Release();
}
return true;
}
public async Task<bool> PingAsync(string address)
{
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync(address, 4000); // 4000 is the default ping timeout for ping.exe
return reply.Status == IPStatus.Success;
}
catch
{
return false;
}
}
public void CleanupBackup()
{
Directory.GetFiles(Path.GetDirectoryName(HostsFilePath), $"*{_backupSuffix}*")
.Select(f => new FileInfo(f))
.Where(f => f.CreationTime < DateTime.Now.AddDays(-15))
.ToList()
.ForEach(f => f.Delete());
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{
_fileSystemWatcher.EnableRaisingEvents = false;
FileChanged?.Invoke(this, EventArgs.Empty);
_fileSystemWatcher.EnableRaisingEvents = true;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_asyncLock.Dispose();
_disposed = true;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Hosts.Helpers
{
public interface IElevationHelper
{
bool IsElevated { get; }
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Hosts.Models;
namespace Hosts.Helpers
{
public interface IHostsService : IDisposable
{
string HostsFilePath { get; }
event EventHandler FileChanged;
Task<(string Unparsed, List<Entry> Entries)> ReadAsync();
Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries);
Task<bool> PingAsync(string address);
void CleanupBackup();
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using interop;
namespace Hosts.Helpers
{
// TODO: use centralized logger https://github.com/microsoft/PowerToys/issues/19650
public static class Logger
{
private static readonly IFileSystem _fileSystem = new FileSystem();
private static readonly string ApplicationLogPath = Path.Combine(Constants.AppDataPath(), "Hosts\\Logs");
static Logger()
{
if (!_fileSystem.Directory.Exists(ApplicationLogPath))
{
_fileSystem.Directory.CreateDirectory(ApplicationLogPath);
}
// Using InvariantCulture since this is used for a log file name
var logFilePath = _fileSystem.Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
}
public static void LogError(string message)
{
Log(message, "ERROR");
}
public static void LogError(string message, Exception ex)
{
Log(
message + Environment.NewLine +
ex?.Message + Environment.NewLine +
"Inner exception: " + Environment.NewLine +
ex?.InnerException?.Message + Environment.NewLine +
"Stack trace: " + Environment.NewLine +
ex?.StackTrace,
"ERROR");
}
public static void LogWarning(string message)
{
Log(message, "WARNING");
}
public static void LogInfo(string message)
{
Log(message, "INFO");
}
private static void Log(string message, string type)
{
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
Trace.Indent();
Trace.WriteLine(GetCallerInfo());
Trace.WriteLine(message);
Trace.Unindent();
}
private static string GetCallerInfo()
{
StackTrace stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(3)?.GetMethod();
var className = methodName?.DeclaringType.Name;
return "[Method]: " + methodName?.Name + " [Class]: " + className;
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Hosts.Helpers
{
using System;
using System.Runtime.InteropServices;
internal class NativeMethods
{
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
[DllImport("user32.dll")]
internal static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Hosts.Helpers
{
public static class StringHelper
{
public static string GetHostsWithComment(string hosts, string comment)
{
return string.IsNullOrWhiteSpace(comment) ? hosts : string.Concat(hosts, " - ", comment);
}
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.RegularExpressions;
namespace Hosts.Helpers
{
public static class ValidationHelper
{
/// <summary>
/// Determines whether the address is a valid IPv4
/// </summary>
public static bool ValidIPv4(string address)
{
if (string.IsNullOrWhiteSpace(address))
{
return false;
}
var regex = new Regex("^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
return regex.IsMatch(address);
}
/// <summary>
/// Determines whether the address is a valid IPv6
/// </summary>
public static bool ValidIPv6(string address)
{
if (string.IsNullOrWhiteSpace(address))
{
return false;
}
var regex = new Regex("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$");
return regex.IsMatch(address);
}
/// <summary>
/// Determines whether the hosts are valid
/// </summary>
public static bool ValidHosts(string hosts)
{
if (string.IsNullOrWhiteSpace(hosts))
{
return false;
}
foreach (var host in hosts.Split(' '))
{
if (System.Uri.CheckHostName(host) == System.UriHostNameType.Unknown)
{
return false;
}
}
return true;
}
}
}

View File

@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Hosts</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<RuntimeIdentifiers>win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\$(AssemblyName)</OutputPath>
<IntermediateOutputPath>$(SolutionDir)$(Platform)\$(Configuration)\obj\$(AssemblyName)</IntermediateOutputPath>
<RootNamespace>Hosts</RootNamespace>
<AssemblyName>PowerToys.Hosts</AssemblyName>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22000.197" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.8" />
<PackageReference Include="System.IO.Abstractions" Version="12.2.5" />
<PackageReference Include="WinUIEx" Version="1.8.0" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Views\MainPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@
<winuiex:WindowEx
x:Uid="Window"
x:Class="Hosts.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Hosts"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:Hosts.Views"
xmlns:winuiex="using:WinUIEx"
Width="680"
MinWidth="480"
MinHeight="320"
mc:Ignorable="d">
<winuiex:WindowEx.Backdrop>
<winuiex:MicaSystemBackdrop />
</winuiex:WindowEx.Backdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="titleBar">
<StackPanel
Margin="16,8,8,8"
VerticalAlignment="Top"
Orientation="Horizontal">
<TextBlock FontFamily="{ThemeResource SymbolThemeFontFamily}" Text="&#xEA3F;" />
<TextBlock
Margin="12,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Hosts File Editor" />
</StackPanel>
</Grid>
<views:MainPage Grid.Row="1" />
</Grid>
</winuiex:WindowEx>

View File

@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Hosts.Helpers;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinUIEx;
namespace Hosts
{
public sealed partial class MainWindow : WindowEx
{
public MainWindow()
{
InitializeComponent();
if (AppWindowTitleBar.IsCustomizationSupported())
{
SetTitleBar();
}
else
{
titleBar.Visibility = Visibility.Collapsed;
// Set window icon
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.SetIcon("icon.ico");
if (ThemeHelpers.GetAppTheme() == AppTheme.Dark)
{
ThemeHelpers.SetImmersiveDarkMode(hWnd, true);
}
}
BringToForeground();
}
private void SetTitleBar()
{
AppWindow window = this.GetAppWindow();
window.TitleBar.ExtendsContentIntoTitleBar = true;
window.TitleBar.ButtonBackgroundColor = Colors.Transparent;
SetTitleBar(titleBar);
}
private void BringToForeground()
{
var handle = this.GetWindowHandle();
var fgHandle = NativeMethods.GetForegroundWindow();
var threadId1 = NativeMethods.GetWindowThreadProcessId(handle, System.IntPtr.Zero);
var threadId2 = NativeMethods.GetWindowThreadProcessId(fgHandle, System.IntPtr.Zero);
if (threadId1 != threadId2)
{
NativeMethods.AttachThreadInput(threadId1, threadId2, true);
NativeMethods.SetForegroundWindow(handle);
NativeMethods.AttachThreadInput(threadId1, threadId2, false);
}
else
{
NativeMethods.SetForegroundWindow(handle);
}
}
}
}

View File

@ -0,0 +1,149 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using Hosts.Helpers;
namespace Hosts.Models
{
public partial class Entry : ObservableObject
{
private string _line;
private string _address;
public string Address
{
get => _address;
set
{
SetProperty(ref _address, value);
OnPropertyChanged(nameof(Valid));
}
}
private string _hosts;
public string Hosts
{
get => _hosts;
set
{
SetProperty(ref _hosts, value);
OnPropertyChanged(nameof(Valid));
}
}
[ObservableProperty]
private string _comment;
[ObservableProperty]
private bool _active;
[ObservableProperty]
private bool? _ping;
[ObservableProperty]
private bool _pinging;
public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address));
public Entry()
{
}
public Entry(string line)
{
_line = line.Trim();
Parse();
}
public Entry(string address, string hosts, string comment, bool active)
{
Address = address.Trim();
Hosts = hosts.Trim();
Comment = comment.Trim();
Active = active;
}
public void Parse()
{
Active = !_line.StartsWith("#", StringComparison.InvariantCultureIgnoreCase);
var lineSplit = _line.TrimStart(' ', '#').Split('#');
if (lineSplit.Length == 0)
{
return;
}
var addressHost = lineSplit[0];
var addressHostSplit = addressHost.Split(' ', '\t');
var hostsBuilder = new StringBuilder();
var commentBuilder = new StringBuilder();
for (var i = 0; i < addressHostSplit.Length; i++)
{
var element = addressHostSplit[i].Trim();
if (string.IsNullOrWhiteSpace(element))
{
continue;
}
if (Address == null)
{
if (IPAddress.TryParse(element, out var _) && (element.Contains(':') || element.Contains('.')))
{
Address = element;
}
}
else
{
if (hostsBuilder.Length > 0)
{
hostsBuilder.Append(' ');
}
hostsBuilder.Append(element);
}
}
Hosts = hostsBuilder.ToString();
for (var i = 1; i < lineSplit.Length; i++)
{
if (commentBuilder.Length > 0)
{
commentBuilder.Append('#');
}
commentBuilder.Append(lineSplit[i]);
}
Comment = commentBuilder.ToString().Trim();
}
public Entry Clone()
{
return new Entry
{
_line = _line,
Address = Address,
Hosts = Hosts,
Comment = Comment,
Active = Active,
};
}
public string GetLine()
{
return _line;
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using Hosts.Helpers;
using Microsoft.UI.Dispatching;
using Microsoft.Windows.AppLifecycle;
namespace Hosts
{
public static class Program
{
[STAThread]
public static void Main(string[] args)
{
WinRT.ComWrappersSupport.InitializeComWrappers();
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_Hosts_Instance");
if (instanceKey.IsCurrent)
{
Microsoft.UI.Xaml.Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
});
}
else
{
Logger.LogWarning("Another instance of Hosts running. Exiting Hosts");
}
return;
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Settings.UI.Library.Enumerations;
namespace Hosts.Settings
{
public interface IUserSettings
{
public bool ShowStartupWarning { get; }
public AdditionalLinesPosition AdditionalLinesPosition { get; }
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO.Abstractions;
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Settings.UI.Library.Enumerations;
namespace Hosts.Settings
{
public class UserSettings : IUserSettings
{
private const string HostsModuleName = "Hosts";
private const int MaxNumberOfRetry = 5;
private readonly ISettingsUtils _settingsUtils;
private readonly IFileSystemWatcher _watcher;
private readonly object _loadingSettingsLock = new object();
public bool ShowStartupWarning { get; private set; }
public AdditionalLinesPosition AdditionalLinesPosition { get; private set; }
public UserSettings()
{
_settingsUtils = new SettingsUtils();
ShowStartupWarning = true;
AdditionalLinesPosition = AdditionalLinesPosition.Top;
LoadSettingsFromJson();
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
}
private void LoadSettingsFromJson()
{
lock (_loadingSettingsLock)
{
var retry = true;
var retryCount = 0;
while (retry)
{
try
{
retryCount++;
if (!_settingsUtils.SettingsExists(HostsModuleName))
{
Logger.LogInfo("Hosts settings.json was missing, creating a new one");
var defaultSettings = new HostsSettings();
defaultSettings.Save(_settingsUtils);
}
var settings = _settingsUtils.GetSettingsOrDefault<HostsSettings>(HostsModuleName);
if (settings != null)
{
ShowStartupWarning = settings.Properties.ShowStartupWarning;
AdditionalLinesPosition = settings.Properties.AdditionalLinesPosition;
}
retry = false;
}
catch (Exception ex)
{
if (retryCount > MaxNumberOfRetry)
{
retry = false;
}
Logger.LogError("Failed to read changed settings", ex);
Thread.Sleep(500);
}
}
}
}
}
}

View File

@ -0,0 +1,261 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Active.Header" xml:space="preserve">
<value>Active</value>
</data>
<data name="ActiveToggle.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Active</value>
</data>
<data name="AddBtn" xml:space="preserve">
<value>Add</value>
</data>
<data name="AddEntry.Text" xml:space="preserve">
<value>New entry</value>
</data>
<data name="AddEntryBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>New entry</value>
</data>
<data name="AdditionalLinesBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Additional lines</value>
</data>
<data name="AdditionalLinesBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Additional lines</value>
</data>
<data name="AdditionalLinesDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="AdditionalLinesDialog.PrimaryButtonText" xml:space="preserve">
<value>Save</value>
</data>
<data name="AdditionalLinesDialog.Title" xml:space="preserve">
<value>Additional lines</value>
</data>
<data name="AddNewEntryDialog_Title" xml:space="preserve">
<value>Add new entry</value>
</data>
<data name="Address.Header" xml:space="preserve">
<value>Address</value>
<comment>"Address" refers to the IP address of the entry</comment>
</data>
<data name="AddressFilter.Header" xml:space="preserve">
<value>Address</value>
<comment>"Address" refers to the IP address of the entry</comment>
</data>
<data name="ClearFiltersBtn.Content" xml:space="preserve">
<value>Clear filters</value>
</data>
<data name="ClearFiltersBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Clear filters</value>
</data>
<data name="Comment.Header" xml:space="preserve">
<value>Comment</value>
<comment>"Comment" refers to the comment of the entry</comment>
</data>
<data name="CommentFilter.Header" xml:space="preserve">
<value>Comment</value>
<comment>"Comment" refers to the comment of the entry</comment>
</data>
<data name="Delete.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="DeleteDialog.CloseButtonText" xml:space="preserve">
<value>No</value>
</data>
<data name="DeleteDialog.PrimaryButtonText" xml:space="preserve">
<value>Yes</value>
</data>
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
<value>Are you sure you want to delete this entry?</value>
</data>
<data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Entries</value>
</data>
<data name="EntryDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="FileChanged.Message" xml:space="preserve">
<value>Hosts file has changed by another application. Do you want to reload it?</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="FileSaveError.Message" xml:space="preserve">
<value>Failed to save hosts file.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Filters</value>
</data>
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Filters</value>
</data>
<data name="Hosts.Header" xml:space="preserve">
<value>Hosts</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="Hosts.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Seperate multiple hosts by space (e.g. server server.local).</value>
<comment>Do not localize "server" and "server.local"</comment>
</data>
<data name="HostsFilter.Header" xml:space="preserve">
<value>Hosts</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="MoveDown.Text" xml:space="preserve">
<value>Move down</value>
</data>
<data name="MoveUp.Text" xml:space="preserve">
<value>Move up</value>
</data>
<data name="Ping.Text" xml:space="preserve">
<value>Ping</value>
<comment>"Ping" refers to the command-line utility, do not loc</comment>
</data>
<data name="Reload.Content" xml:space="preserve">
<value>Reload</value>
</data>
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Settings</value>
</data>
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Settings</value>
</data>
<data name="UpdateBtn" xml:space="preserve">
<value>Update</value>
</data>
<data name="UpdateEntry_Title" xml:space="preserve">
<value>Update the entry</value>
</data>
<data name="WarningDialog_AcceptBtn" xml:space="preserve">
<value>Accept</value>
</data>
<data name="WarningDialog_QuitBtn" xml:space="preserve">
<value>Quit</value>
</data>
<data name="WarningDialog_Text" xml:space="preserve">
<value>Altering hosts file has direct real world impact of how this computer resolves domain names.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="WarningDialog_Title" xml:space="preserve">
<value>Warning</value>
</data>
<data name="Window.Title" xml:space="preserve">
<value>Hosts File Editor</value>
<comment>"Hosts File Editor" is the name of the utility. "Hosts" refers to the system hosts file, do not loc</comment>
</data>
</root>

View File

@ -0,0 +1,242 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Common.UI;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using Hosts.Helpers;
using Hosts.Models;
using Hosts.Settings;
using Microsoft.UI.Dispatching;
namespace Hosts.ViewModels
{
public partial class MainViewModel : ObservableObject, IDisposable
{
private readonly IHostsService _hostsService;
private readonly IUserSettings _userSettings;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private bool _disposed;
[ObservableProperty]
private Entry _selected;
[ObservableProperty]
private bool _error;
[ObservableProperty]
private bool _fileChanged;
[ObservableProperty]
private string _addressFilter;
[ObservableProperty]
private string _hostsFilter;
[ObservableProperty]
private string _commentFilter;
[ObservableProperty]
private bool _filtered;
[ObservableProperty]
private string _additionalLines;
private ObservableCollection<Entry> _entries;
public ObservableCollection<Entry> Entries
{
get
{
if (_filtered)
{
var filter = _entries.AsEnumerable();
if (!string.IsNullOrWhiteSpace(_addressFilter))
{
filter = filter.Where(e => e.Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(_hostsFilter))
{
filter = filter.Where(e => e.Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(_commentFilter))
{
filter = filter.Where(e => e.Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
}
return new ObservableCollection<Entry>(filter);
}
else
{
return _entries;
}
}
set
{
_entries = value;
OnPropertyChanged(nameof(Entries));
}
}
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
public ICommand ApplyFiltersCommand => new RelayCommand(ApplyFilters);
public ICommand ClearFiltersCommand => new RelayCommand(ClearFilters);
public ICommand OpenSettingsCommand => new RelayCommand(OpenSettings);
public MainViewModel(
IHostsService hostService,
IUserSettings userSettings)
{
_hostsService = hostService;
_userSettings = userSettings;
_hostsService.FileChanged += (s, e) =>
{
_dispatcherQueue.TryEnqueue(() => FileChanged = true);
};
}
public void Add(Entry entry)
{
entry.PropertyChanged += Entry_PropertyChanged;
_entries.Add(entry);
}
public void Update(int index, Entry entry)
{
var existingEntry = _entries.ElementAt(index);
existingEntry.Address = entry.Address;
existingEntry.Comment = entry.Comment;
existingEntry.Hosts = entry.Hosts;
existingEntry.Active = entry.Active;
}
public void DeleteSelected()
{
_entries.Remove(Selected);
if (Filtered)
{
OnPropertyChanged(nameof(Entries));
}
}
public void UpdateAdditionalLines(string lines)
{
_additionalLines = lines;
Task.Run(async () =>
{
var error = !await _hostsService.WriteAsync(_additionalLines, _entries);
await _dispatcherQueue.EnqueueAsync(() => Error = error);
});
}
public void ReadHosts()
{
FileChanged = false;
Task.Run(async () =>
{
(_additionalLines, var entries) = await _hostsService.ReadAsync();
await _dispatcherQueue.EnqueueAsync(() =>
{
Entries = new ObservableCollection<Entry>(entries);
foreach (var e in _entries)
{
e.PropertyChanged += Entry_PropertyChanged;
}
_entries.CollectionChanged += Entries_CollectionChanged;
});
});
}
public void ApplyFilters()
{
if (_entries != null)
{
Filtered = !string.IsNullOrWhiteSpace(_addressFilter) || !string.IsNullOrWhiteSpace(_hostsFilter) || !string.IsNullOrWhiteSpace(_commentFilter);
OnPropertyChanged(nameof(Entries));
}
}
public void ClearFilters()
{
AddressFilter = null;
HostsFilter = null;
CommentFilter = null;
}
public async Task PingSelectedAsync()
{
var selected = _selected;
selected.Ping = null;
selected.Pinging = true;
selected.Ping = await _hostsService.PingAsync(_selected.Address);
selected.Pinging = false;
}
public void OpenSettings()
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Ping should't trigger a file save
if (e.PropertyName == nameof(Entry.Ping) || e.PropertyName == nameof(Entry.Pinging))
{
return;
}
Task.Run(async () =>
{
var error = !await _hostsService.WriteAsync(_additionalLines, _entries);
await _dispatcherQueue.EnqueueAsync(() => Error = error);
});
}
private void Entries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Task.Run(async () =>
{
var error = !await _hostsService.WriteAsync(_additionalLines, _entries);
await _dispatcherQueue.EnqueueAsync(() => Error = error);
});
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_hostsService?.Dispose();
_disposed = true;
}
}
}
}
}

View File

@ -0,0 +1,366 @@
<Page
x:Class="Hosts.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Hosts.Helpers"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:local="using:Hosts.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Hosts.Models"
x:Name="Page"
Loaded="Page_Loaded"
mc:Ignorable="d">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ReadHostsCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Page.Resources>
<converters:StringVisibilityConverter
x:Key="StringVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="64" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<Button
x:Uid="AddEntryBtn"
Height="36"
Margin="16,0,0,0"
Command="{x:Bind NewDialogCommand}">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock
x:Name="Icon"
Margin="0,0,0,0"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Text="&#xE109;" />
<TextBlock x:Uid="AddEntry" />
</StackPanel>
</Button>
<StackPanel
Padding="0,0,16,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
x:Uid="AdditionalLinesBtn"
Height="36"
Command="{x:Bind AdditionalLinesDialogCommand}"
Style="{StaticResource SubtleButtonStyle}">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="14"
Glyph="&#xe8a5;" />
</Button>
<Button
x:Uid="FilterBtn"
Height="36"
Style="{StaticResource SubtleButtonStyle}">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="14"
Glyph="&#xe71c;" />
<Button.Flyout>
<Flyout>
<StackPanel
Width="320"
HorizontalAlignment="Stretch"
Spacing="12">
<AutoSuggestBox
x:Uid="AddressFilter"
PlaceholderText=""
QueryIcon="Find"
Text="{x:Bind ViewModel.AddressFilter, Mode=TwoWay}">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="TextChanged">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
</AutoSuggestBox>
<AutoSuggestBox
x:Uid="HostsFilter"
QueryIcon="Find"
Text="{x:Bind ViewModel.HostsFilter, Mode=TwoWay}">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="TextChanged">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
</AutoSuggestBox>
<AutoSuggestBox
x:Uid="CommentFilter"
QueryIcon="Find"
Text="{x:Bind ViewModel.CommentFilter, Mode=TwoWay}">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="TextChanged">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
</AutoSuggestBox>
<Button
x:Uid="ClearFiltersBtn"
Margin="0,6,0,0"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.ClearFiltersCommand}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button
x:Uid="SettingsBtn"
Height="36"
Command="{x:Bind ViewModel.OpenSettingsCommand}"
Style="{StaticResource SubtleButtonStyle}">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="14"
Glyph="&#xe713;" />
</Button>
</StackPanel>
</Grid>
<StackPanel
Grid.Row="2"
Orientation="Vertical"
Visibility="Visible">
<InfoBar
x:Uid="FileSaveError"
CornerRadius="0"
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
Severity="Error" />
<InfoBar
x:Uid="FileChanged"
IsOpen="{x:Bind ViewModel.FileChanged, Mode=TwoWay}"
Severity="Informational">
<InfoBar.ActionButton>
<Button x:Uid="Reload" Command="{x:Bind ViewModel.ReadHostsCommand}" />
</InfoBar.ActionButton>
</InfoBar>
</StackPanel>
<!--
https://github.com/microsoft/microsoft-ui-xaml/issues/7690
AllowDrop="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
CanDragItems="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
CanReorderItems="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
-->
<ListView
x:Uid="Entries"
x:Name="Entries"
Grid.Row="1"
Margin="16,8,16,16"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
IsItemClickEnabled="True"
ItemClick="Entries_ItemClick"
ItemsSource="{Binding Entries, Mode=TwoWay}"
SelectedItem="{Binding Selected, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Entry">
<Grid
AutomationProperties.Name="{x:Bind Address, Mode=OneWay}"
Background="Transparent"
IsRightTapEnabled="True"
RightTapped="Grid_RightTapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<FlyoutBase.AttachedFlyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="Ping"
Click="Ping_Click"
Icon="TwoBars" />
<MenuFlyoutItem
x:Uid="MoveUp"
Click="ReorderButtonUp_Click"
IsEnabled="{Binding DataContext.Filtered, ElementName=Page, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE74A;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
x:Uid="MoveDown"
Click="ReorderButtonDown_Click"
IsEnabled="{Binding DataContext.Filtered, ElementName=Page, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE74B;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="Delete"
Click="Delete_Click"
Icon="Delete" />
</MenuFlyout>
</FlyoutBase.AttachedFlyout>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Bind Address, Mode=OneWay}"
TextWrapping="Wrap" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind helpers:StringHelper.GetHostsWithComment(Hosts, Comment), Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<ProgressRing
Grid.Column="2"
Width="20"
Height="20"
Margin="0,0,8,0"
IsActive="{x:Bind Pinging, Mode=OneWay}" />
<FontIcon
x:Name="PingIcon"
Grid.Column="2"
Margin="0,0,8,0"
FontSize="18"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Visibility="Collapsed">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior
Binding="{x:Bind Ping, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<ic:ChangePropertyAction
PropertyName="Glyph"
TargetObject="{Binding ElementName=PingIcon}"
Value="&#xe8fb;" />
<ic:ChangePropertyAction
PropertyName="Visibility"
TargetObject="{Binding ElementName=PingIcon}"
Value="Visible" />
<ic:ChangePropertyAction
PropertyName="Foreground"
TargetObject="{Binding ElementName=PingIcon}"
Value="{StaticResource SystemFillColorSuccessBrush}" />
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior
Binding="{x:Bind Ping, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<ic:ChangePropertyAction
PropertyName="Glyph"
TargetObject="{Binding ElementName=PingIcon}"
Value="&#xe894;" />
<ic:ChangePropertyAction
PropertyName="Visibility"
TargetObject="{Binding ElementName=PingIcon}"
Value="Visible" />
<ic:ChangePropertyAction
PropertyName="Foreground"
TargetObject="{Binding ElementName=PingIcon}"
Value="{StaticResource SystemFillColorCriticalBrush}" />
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior
Binding="{x:Bind Ping, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<ic:ChangePropertyAction
PropertyName="Visibility"
TargetObject="{Binding ElementName=PingIcon}"
Value="Collapsed" />
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</FontIcon>
<ToggleSwitch
x:Uid="ActiveToggle"
Grid.Column="3"
Width="40"
MinWidth="0"
HorizontalAlignment="Right"
IsOn="{x:Bind Active, Mode=TwoWay}"
OffContent=""
OnContent="" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentDialog
x:Name="EntryDialog"
x:Uid="EntryDialog"
IsPrimaryButtonEnabled="{Binding Valid, Mode=TwoWay}"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<ContentDialog.DataContext>
<models:Entry />
</ContentDialog.DataContext>
<StackPanel
MinWidth="480"
Margin="0,12,0,0"
HorizontalAlignment="Stretch"
Spacing="24">
<TextBox
x:Uid="Address"
IsSpellCheckEnabled="False"
Text="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBox
x:Uid="Hosts"
IsSpellCheckEnabled="False"
Text="{Binding Hosts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBox
x:Uid="Comment"
IsSpellCheckEnabled="False"
Text="{Binding Comment, Mode=TwoWay}" />
<ToggleSwitch
x:Uid="Active"
IsOn="{Binding Active, Mode=TwoWay}"
OffContent=""
OnContent="" />
</StackPanel>
</ContentDialog>
<ContentDialog
x:Name="DeleteDialog"
x:Uid="DeleteDialog"
PrimaryButtonCommand="{x:Bind DeleteCommand}"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<TextBlock x:Uid="DeleteDialogAreYouSure" />
</ContentDialog>
<ContentDialog
x:Name="AdditionalLinesDialog"
x:Uid="AdditionalLinesDialog"
PrimaryButtonCommand="{x:Bind UpdateAdditionalLinesCommand}"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<TextBox
x:Name="AdditionalLines"
Height="300"
MinWidth="480"
Margin="0,12,0,0"
HorizontalAlignment="Stretch"
AcceptsReturn="True"
TextWrapping="Wrap" />
</ContentDialog>
</Grid>
</Page>

View File

@ -0,0 +1,185 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Hosts.Models;
using Hosts.Settings;
using Hosts.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Windows.ApplicationModel.Resources;
namespace Hosts.Views
{
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel { get; private set; }
public ICommand NewDialogCommand => new AsyncRelayCommand(OpenNewDialogAsync);
public ICommand AdditionalLinesDialogCommand => new AsyncRelayCommand(OpenAdditionalLinesDialogAsync);
public ICommand AddCommand => new RelayCommand(Add);
public ICommand UpdateCommand => new RelayCommand(Update);
public ICommand DeleteCommand => new RelayCommand(Delete);
public ICommand UpdateAdditionalLinesCommand => new RelayCommand(UpdateAdditionalLines);
public ICommand ExitCommand => new RelayCommand(() => { Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().TryEnqueue(Application.Current.Exit); });
public MainPage()
{
InitializeComponent();
ViewModel = App.GetService<MainViewModel>();
DataContext = ViewModel;
}
private async Task OpenNewDialogAsync()
{
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
EntryDialog.Title = resourceLoader.GetString("AddNewEntryDialog_Title");
EntryDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
EntryDialog.PrimaryButtonCommand = AddCommand;
EntryDialog.DataContext = new Entry(string.Empty, string.Empty, string.Empty, true);
await EntryDialog.ShowAsync();
}
private async Task OpenAdditionalLinesDialogAsync()
{
AdditionalLines.Text = ViewModel.AdditionalLines;
await AdditionalLinesDialog.ShowAsync();
}
private async void Entries_ItemClick(object sender, ItemClickEventArgs e)
{
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
ViewModel.Selected = e.ClickedItem as Entry;
EntryDialog.Title = resourceLoader.GetString("UpdateEntry_Title");
EntryDialog.PrimaryButtonText = resourceLoader.GetString("UpdateBtn");
EntryDialog.PrimaryButtonCommand = UpdateCommand;
var clone = ViewModel.Selected.Clone();
EntryDialog.DataContext = clone;
await EntryDialog.ShowAsync();
}
private void Add()
{
ViewModel.Add(EntryDialog.DataContext as Entry);
}
private void Update()
{
ViewModel.Update(Entries.SelectedIndex, EntryDialog.DataContext as Entry);
}
private void Delete()
{
ViewModel.DeleteSelected();
}
private void UpdateAdditionalLines()
{
ViewModel.UpdateAdditionalLines(AdditionalLines.Text);
}
private void Grid_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
var owner = sender as FrameworkElement;
if (owner != null)
{
var flyoutBase = FlyoutBase.GetAttachedFlyout(owner);
flyoutBase.ShowAt(owner, new FlyoutShowOptions
{
Position = e.GetPosition(owner),
});
}
}
private async void Delete_Click(object sender, RoutedEventArgs e)
{
var menuFlyoutItem = sender as MenuFlyoutItem;
if (menuFlyoutItem != null)
{
var selectedEntry = menuFlyoutItem.DataContext as Entry;
ViewModel.Selected = selectedEntry;
DeleteDialog.Title = selectedEntry.Address;
await DeleteDialog.ShowAsync();
}
}
private async void Ping_Click(object sender, RoutedEventArgs e)
{
var menuFlyoutItem = sender as MenuFlyoutItem;
if (menuFlyoutItem != null)
{
ViewModel.Selected = menuFlyoutItem.DataContext as Entry;
await ViewModel.PingSelectedAsync();
}
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
var userSettings = App.GetService<IUserSettings>();
if (userSettings.ShowStartupWarning)
{
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
var dialog = new ContentDialog();
dialog.XamlRoot = XamlRoot;
dialog.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style;
dialog.Title = resourceLoader.GetString("WarningDialog_Title");
dialog.Content = new TextBlock
{
Text = resourceLoader.GetString("WarningDialog_Text"),
TextWrapping = TextWrapping.Wrap,
};
dialog.PrimaryButtonText = resourceLoader.GetString("WarningDialog_AcceptBtn");
dialog.PrimaryButtonStyle = Application.Current.Resources["AccentButtonStyle"] as Style;
dialog.CloseButtonText = resourceLoader.GetString("WarningDialog_QuitBtn");
dialog.CloseButtonCommand = ExitCommand;
await dialog.ShowAsync();
}
}
private void ReorderButtonUp_Click(object sender, RoutedEventArgs e)
{
var menuFlyoutItem = sender as MenuFlyoutItem;
if (menuFlyoutItem != null)
{
var entry = menuFlyoutItem.DataContext as Entry;
var index = ViewModel.Entries.IndexOf(entry);
if (index > 0)
{
ViewModel.Entries.Move(index, index - 1);
}
}
}
private void ReorderButtonDown_Click(object sender, RoutedEventArgs e)
{
var menuFlyoutItem = sender as MenuFlyoutItem;
if (menuFlyoutItem != null)
{
var entry = menuFlyoutItem.DataContext as Entry;
var index = ViewModel.Entries.IndexOf(entry);
if (index < ViewModel.Entries.Count - 1)
{
ViewModel.Entries.Move(index, index + 1);
}
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Hosts.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
</Target>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{B41B888C-7DB8-4747-B262-4062E05A230D}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>HostsModuleInterface</RootNamespace>
<ProjectName>HostsModuleInterface</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\Hosts\</OutDir>
<TargetName>PowerToys.HostsModuleInterface</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(CIBuild)'!='true'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<None Include="resource.base.h" />
<ClInclude Include="Generated Files\resource.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="trace.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(CIBuild)'!='true'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="dllmain.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="HostsModuleInterface.base.rc" />
<ResourceCompile Include="Generated Files\HostsModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<None Include="Resource.resx" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{875a08c6-f610-4667-bd0f-80171ed96072}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="HostsModuleInterface.base.rc">
<Filter>Resource Files</Filter>
</None>
<None Include="Resource.resx">
<Filter>Resource Files</Filter>
</None>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Generated Files\resource.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.base.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files\HostsModuleInterface.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

Binary file not shown.

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Hosts_Name" xml:space="preserve">
<value>Hosts File Editor</value>
<comment>"Hosts" refer to the system hosts file, do not loc</comment>
</data>
</root>

View File

@ -0,0 +1,199 @@
#include "pch.h"
#include "trace.h"
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <interface/powertoy_module_interface.h>
#include "Generated Files/resource.h"
#include <shellapi.h>
#include <common/utils/resources.h>
#include <common/utils/winapi_error.h>
#include <common/SettingsAPI/settings_objects.h>
#include <string>
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
namespace
{
// Name of the powertoy module.
inline const std::wstring ModuleKey = L"Hosts";
}
class HostsModuleInterface : public PowertoyModuleIface
{
private:
bool m_enabled = false;
std::wstring app_name;
//contains the non localized key of the powertoy
std::wstring app_key;
HANDLE m_hProcess;
bool is_process_running()
{
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void bring_process_to_front()
{
auto enum_windows = [](HWND hwnd, LPARAM param) -> BOOL {
HANDLE process_handle = (HANDLE)param;
DWORD window_process_id = 0;
GetWindowThreadProcessId(hwnd, &window_process_id);
if (GetProcessId(process_handle) == window_process_id)
{
SetForegroundWindow(hwnd);
return FALSE;
}
return TRUE;
};
EnumWindows(enum_windows, (LPARAM)m_hProcess);
}
void launch_process(bool runas)
{
Logger::trace(L"Starting Hosts process");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = L"modules\\Hosts\\PowerToys.Hosts.exe";
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (runas)
{
sei.lpVerb = L"runas";
}
if (ShellExecuteExW(&sei))
{
Logger::trace("Successfully started the Hosts process");
}
else
{
Logger::error(L"Hosts failed to start. {}", get_last_error_or_default(GetLastError()));
}
m_hProcess = sei.hProcess;
}
public:
HostsModuleInterface()
{
app_name = GET_RESOURCE_STRING(IDS_HOSTS_NAME);
app_key = ModuleKey;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::hostsLoggerName);
}
~HostsModuleInterface()
{
m_enabled = false;
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
Logger::trace("HostsModuleInterface::destroy()");
delete this;
}
// Return the localized display name of the powertoy
virtual const wchar_t* get_name() override
{
return app_name.c_str();
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
return false;
}
virtual void call_custom_action(const wchar_t* action) override
{
try
{
PowerToysSettings::CustomActionObject action_object =
PowerToysSettings::CustomActionObject::from_json_string(action);
if (is_process_running())
{
bring_process_to_front();
}
else if (action_object.get_name() == L"Launch")
{
launch_process(false);
}
else if (action_object.get_name() == L"LaunchAdministrator")
{
launch_process(true);
}
}
catch (std::exception&)
{
Logger::error(L"Failed to parse action. {}", action);
}
}
virtual void set_config(const wchar_t* config) override
{
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual void enable()
{
Logger::trace("HostsModuleInterface::enable()");
m_enabled = true;
};
virtual void disable()
{
Logger::trace("HostsModuleInterface::disable()");
if (m_enabled)
{
TerminateProcess(m_hProcess, 1);
}
m_enabled = false;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new HostsModuleInterface();
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.220418.1" targetFramework="native" />
</packages>

View File

@ -0,0 +1 @@
#include "pch.h"

View File

@ -0,0 +1,5 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <ProjectTelemetry.h>

View File

@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by AlwaysOnTopModuleInterface.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Hosts Module"
#define INTERNAL_NAME "PowerToys.HostsModuleInterface"
#define ORIGINAL_FILENAME "PowerToys.HostsModuleInterface.dll"
// Non-localizable
//////////////////////////////

View File

@ -0,0 +1,19 @@
#include "pch.h"
#include "trace.h"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider() noexcept
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider() noexcept
{
TraceLoggingUnregister(g_hProvider);
}

View File

@ -0,0 +1,8 @@
#pragma once
class Trace
{
public:
static void RegisterProvider() noexcept;
static void UnregisterProvider() noexcept;
};

View File

@ -155,8 +155,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"modules/MouseUtils/PowerToys.MousePointerCrosshairs.dll",
L"modules/PowerAccent/PowerToys.PowerAccentModuleInterface.dll",
L"modules/PowerOCR/PowerToys.PowerOCRModuleInterface.dll",
L"modules/MeasureTool/PowerToys.MeasureToolModuleInterface.dll",
L"modules/Hosts/PowerToys.HostsModuleInterface.dll",
};
const auto VCM_PATH = L"modules/VideoConference/PowerToys.VideoConferenceModule.dll";
if (const auto mf = LoadLibraryA("mf.dll"))

View File

@ -569,6 +569,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "ShortcutGuide";
case ESettingsWindowNames::VideoConference:
return "VideoConference";
case ESettingsWindowNames::Hosts:
return "Hosts";
default:
{
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast<int>(value));
@ -628,6 +630,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::VideoConference;
}
else if (value == "Hosts")
{
return ESettingsWindowNames::Hosts;
}
else
{
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));

View File

@ -15,7 +15,8 @@ enum class ESettingsWindowNames
PowerRename,
FileExplorer,
ShortcutGuide,
VideoConference
VideoConference,
Hosts
};
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);

View File

@ -287,6 +287,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool hosts = true;
[JsonPropertyName("Hosts")]
public bool Hosts
{
get => hosts;
set
{
if (hosts != value)
{
LogTelemetryEvent(value);
hosts = value;
}
}
}
public string ToJsonString()
{
return JsonSerializer.Serialize(this);

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Settings.UI.Library.Enumerations
{
public enum AdditionalLinesPosition
{
Top = 0,
Bottom = 1,
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Settings.UI.Library.Enumerations;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class HostsProperties
{
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool ShowStartupWarning { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool LaunchAdministrator { get; set; }
public AdditionalLinesPosition AdditionalLinesPosition { get; set; }
public HostsProperties()
{
ShowStartupWarning = true;
LaunchAdministrator = true;
AdditionalLinesPosition = AdditionalLinesPosition.Top;
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class HostsSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "Hosts";
[JsonPropertyName("properties")]
public HostsProperties Properties { get; set; }
public HostsSettings()
{
Properties = new HostsProperties();
Version = "1.0";
Name = ModuleName;
}
public virtual void Save(ISettingsUtils settingsUtils)
{
// Save settings to file
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
if (settingsUtils == null)
{
throw new ArgumentNullException(nameof(settingsUtils));
}
settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
}
public string GetModuleName() => Name;
public bool UpgradeSettingsConfiguration() => false;
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using Settings.UI.Library.Enumerations;
namespace Settings.UI.Library.ViewModels
{
public class HostsViewModel : Observable
{
private bool _isElevated;
private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; }
private HostsSettings Settings { get; set; }
private Func<string, int> SendConfigMSG { get; }
public ButtonClickCommand LaunchEventHandler => new ButtonClickCommand(Launch);
public bool IsEnabled
{
get => GeneralSettingsConfig.Enabled.Hosts;
set
{
if (value != GeneralSettingsConfig.Enabled.Hosts)
{
// Set the status in the general settings configuration
GeneralSettingsConfig.Enabled.Hosts = value;
OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(snd.ToString());
OnPropertyChanged(nameof(IsEnabled));
}
}
}
public bool LaunchAdministratorEnabled => IsEnabled && !_isElevated;
public bool ShowStartupWarning
{
get => Settings.Properties.ShowStartupWarning;
set
{
if (value != Settings.Properties.ShowStartupWarning)
{
Settings.Properties.ShowStartupWarning = value;
NotifyPropertyChanged();
}
}
}
public bool LaunchAdministrator
{
get => Settings.Properties.LaunchAdministrator;
set
{
if (value != Settings.Properties.LaunchAdministrator)
{
Settings.Properties.LaunchAdministrator = value;
NotifyPropertyChanged();
}
}
}
public int AdditionalLinesPosition
{
get => (int)Settings.Properties.AdditionalLinesPosition;
set
{
if (value != (int)Settings.Properties.AdditionalLinesPosition)
{
Settings.Properties.AdditionalLinesPosition = (AdditionalLinesPosition)value;
NotifyPropertyChanged();
}
}
}
public HostsViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<HostsSettings> moduleSettingsRepository, Func<string, int> ipcMSGCallBackFunc, bool isElevated)
{
SettingsUtils = settingsUtils;
GeneralSettingsConfig = settingsRepository.SettingsConfig;
Settings = moduleSettingsRepository.SettingsConfig;
SendConfigMSG = ipcMSGCallBackFunc;
_isElevated = isElevated;
}
public void Launch()
{
var actionName = "Launch";
if (!_isElevated && LaunchAdministrator)
{
actionName = "LaunchAdministrator";
}
SendConfigMSG("{\"action\":{\"Hosts\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
SettingsUtils.SaveSettings(Settings.ToJsonString(), HostsSettings.ModuleName);
}
}
}

View File

@ -129,6 +129,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "TextExtractor": StartupPage = typeof(Views.PowerOcrPage); break;
case "VideoConference": StartupPage = typeof(Views.VideoConferencePage); break;
case "MeasureTool": StartupPage = typeof(Views.MeasureToolPage); break;
case "Hosts": StartupPage = typeof(Views.HostsPage); break;
default: Debug.Assert(false, "Unexpected SettingsWindow argument value"); break;
}
}

View File

@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
TextExtractor,
VideoConference,
MeasureTool,
Hosts,
WhatsNew,
}
}

View File

@ -0,0 +1,28 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeHosts"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:toolkitcontrols="using:CommunityToolkit.WinUI.UI.Controls">
<controls:OOBEPageControl x:Uid="Oobe_Hosts"
HeroImage="ms-appx:///Assets/Modules/OOBE/AlwaysOnTop.png">
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,24,0,0">
<Button x:Uid="Launch_Hosts" Style="{StaticResource AccentButtonStyle}" Click="Launch_Hosts_Click" />
<Button x:Uid="OOBE_Settings"
Click="Launch_Settings_Click" />
<HyperlinkButton NavigateUri="https://aka.ms/PowerToysOverview_HostsFileEditor" Style="{StaticResource TextButtonStyle}">
<TextBlock x:Uid="LearnMore_Hosts"
TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
</StackPanel>
</controls:OOBEPageControl.PageContent>
</controls:OOBEPageControl>
</Page>

View File

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class OobeHosts : Page
{
public OobePowerToysModule ViewModel { get; }
public OobeHosts()
{
InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Hosts]);
DataContext = ViewModel;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
}
private void Launch_Hosts_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
bool launchAdmin = SettingsRepository<HostsSettings>.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator;
var actionName = "Launch";
if (!App.IsElevated && launchAdmin)
{
actionName = "LaunchAdministrator";
}
ShellPage.SendDefaultIPCMessage("{\"action\":{\"Hosts\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
private void Launch_Settings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
OobeShellPage.OpenMainWindowCallback(typeof(HostsPage));
}
ViewModel.LogOpeningSettingsEvent();
}
}
}

View File

@ -75,6 +75,13 @@
ShowAsMonochrome="False" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_Hosts" Tag="Hosts">
<NavigationViewItem.Icon>
<BitmapIcon
UriSource="ms-appx:///Assets/FluentIcons/FluentIconsAlwaysOnTop.png"
ShowAsMonochrome="False" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_ImageResizer" Tag="ImageResizer">
<NavigationViewItem.Icon>
<BitmapIcon

View File

@ -139,6 +139,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
IsNew = true,
});
Modules.Insert((int)PowerToysModules.Hosts, new OobePowerToysModule()
{
ModuleName = "Hosts",
IsNew = true,
});
Modules.Insert((int)PowerToysModules.WhatsNew, new OobePowerToysModule()
{
ModuleName = "WhatsNew",
@ -192,6 +198,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
case "VideoConference": NavigationFrame.Navigate(typeof(OobeVideoConference)); break;
case "MouseUtils": NavigationFrame.Navigate(typeof(OobeMouseUtils)); break;
case "MeasureTool": NavigationFrame.Navigate(typeof(OobeMeasureTool)); break;
case "Hosts": NavigationFrame.Navigate(typeof(OobeHosts)); break;
}
}
}

View File

@ -35,6 +35,10 @@
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<None Remove="OOBE\Views\OobeHosts.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
@ -68,6 +72,9 @@
<None Update="icon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Page Update="OOBE\Views\OobeHosts.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="OOBE\Views\OobePowerOCR.xaml">

View File

@ -2554,4 +2554,72 @@ Activate by holding the key for the character you want to add an accent to, then
<data name="QuickAccent_SelectedLanguage_Italian.Content" xml:space="preserve">
<value>Italian</value>
</data>
<data name="Hosts.ModuleDescription" xml:space="preserve">
<value>Quick and simple utility for managing hosts file.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="Hosts.ModuleTitle" xml:space="preserve">
<value>Hosts File Editor</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="Shell_Hosts.Content" xml:space="preserve">
<value>Hosts File Editor</value>
<comment>Products name: Navigation view item name for Hosts File Editor</comment>
</data>
<data name="Hosts_EnableToggleControl_HeaderText.Header" xml:space="preserve">
<value>Enable Hosts File Editor</value>
<comment>"Hosts File Editor" is the name of the utility</comment>
</data>
<data name="Hosts_Toggle_ShowStartupWarning.Header" xml:space="preserve">
<value>Show a warning at startup</value>
</data>
<data name="Hosts_Activation_GroupSettings.Header" xml:space="preserve">
<value>Activation</value>
</data>
<data name="Hosts_LaunchButtonControl.Description" xml:space="preserve">
<value>Manage your hosts file</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="Hosts_LaunchButtonControl.Header" xml:space="preserve">
<value>Launch Host File Editor</value>
<comment>"Host File Editor" is a product name</comment>
</data>
<data name="Hosts_LaunchButton_Accessible.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Launch Host File Editor</value>
<comment>"Host File Editor" is a product name</comment>
</data>
<data name="Hosts_AdditionalLinesPosition.Header" xml:space="preserve">
<value>Additional lines position</value>
</data>
<data name="Hosts_AdditionalLinesPosition_Bottom.Content" xml:space="preserve">
<value>Bottom</value>
</data>
<data name="Hosts_AdditionalLinesPosition_Top.Content" xml:space="preserve">
<value>Top</value>
</data>
<data name="Hosts_File_GroupSettings.Header" xml:space="preserve">
<value>File</value>
</data>
<data name="Launch_Hosts.Content" xml:space="preserve">
<value>Launch Hosts File Editor</value>
<comment>"Hosts File Editor" is the name of the utility</comment>
</data>
<data name="LearnMore_Hosts.Text" xml:space="preserve">
<value>Learn more about Hosts File Editor</value>
<comment>"Hosts File Editor" is the name of the utility</comment>
</data>
<data name="Oobe_Hosts.Description" xml:space="preserve">
<value>Hosts File Editor is a quick and simple utility for managing hosts file.</value>
<comment>"Hosts File Editor" is the name of the utility</comment>
</data>
<data name="Oobe_Hosts.Title" xml:space="preserve">
<value>Hosts File Editor</value>
<comment>"Hosts File Editor" is the name of the utility</comment>
</data>
<data name="Hosts_Toggle_LaunchAdministrator.Description" xml:space="preserve">
<value>Needs to be launched as administrator in order to make changes to the hosts file</value>
</data>
<data name="Hosts_Toggle_LaunchAdministrator.Header" xml:space="preserve">
<value>Launch as administrator</value>
</data>
</root>

View File

@ -0,0 +1,79 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.Views.HostsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<controls:SettingsPageControl x:Uid="Hosts" IsTabStop="False"
ModuleImageSource="ms-appx:///Assets/Modules/AlwaysOnTop.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel Orientation="Vertical">
<controls:Setting x:Uid="Hosts_EnableToggleControl_HeaderText">
<controls:Setting.Icon>
<BitmapIcon UriSource="ms-appx:///Assets/FluentIcons/FluentIconsAlwaysOnTop.png" ShowAsMonochrome="False" />
</controls:Setting.Icon>
<controls:Setting.ActionContent>
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" x:Uid="ToggleSwitch"/>
</controls:Setting.ActionContent>
</controls:Setting>
<controls:SettingsGroup x:Uid="Hosts_Activation_GroupSettings"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<Button x:Uid="Hosts_LaunchButton_Accessible"
Style="{StaticResource SettingButtonStyle}"
Command="{x:Bind ViewModel.LaunchEventHandler}">
<controls:Setting x:Uid="Hosts_LaunchButtonControl"
Style="{StaticResource ExpanderHeaderSettingStyle}"
Icon="&#xEA37;">
<controls:Setting.ActionContent>
<FontIcon Glyph="&#xE8A7;"
FontSize="18"
FontFamily="{ThemeResource SymbolThemeFontFamily}" />
</controls:Setting.ActionContent>
</controls:Setting>
</Button>
<controls:Setting x:Uid="Hosts_Toggle_LaunchAdministrator"
Icon="&#xE7EF;"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.LaunchAdministratorEnabled}">
<controls:Setting.ActionContent>
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind Mode=TwoWay, Path=ViewModel.LaunchAdministrator}" />
</controls:Setting.ActionContent>
</controls:Setting>
<controls:Setting x:Uid="Hosts_Toggle_ShowStartupWarning"
Icon="&#xE7BA;"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<controls:Setting.ActionContent>
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind Mode=TwoWay, Path=ViewModel.ShowStartupWarning}" />
</controls:Setting.ActionContent>
</controls:Setting>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="Hosts_File_GroupSettings"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<controls:Setting x:Uid="Hosts_AdditionalLinesPosition" Icon="&#xE8A5;" >
<controls:Setting.ActionContent>
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind Path=ViewModel.AdditionalLinesPosition, Mode=TwoWay}" >
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Top" />
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Bottom" />
</ComboBox>
</controls:Setting.ActionContent>
</controls:Setting>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink x:Uid="LearnMore_Hosts" Link="https://aka.ms/PowerToysOverview_HostsFileEditor"/>
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</Page>

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Controls;
using Settings.UI.Library.ViewModels;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class HostsPage : Page
{
private HostsViewModel ViewModel { get; }
public HostsPage()
{
InitializeComponent();
var settingsUtils = new SettingsUtils();
ViewModel = new HostsViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<HostsSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated);
}
}
}

View File

@ -76,6 +76,12 @@
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_Hosts" helpers:NavHelper.NavigateTo="views:HostsPage" >
<NavigationViewItem.Icon>
<BitmapIcon ShowAsMonochrome="False" UriSource="ms-appx:///Assets/FluentIcons/FluentIconsSettings.png" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_ImageResizer" helpers:NavHelper.NavigateTo="views:ImageResizerPage">
<NavigationViewItem.Icon>
<BitmapIcon ShowAsMonochrome="False" UriSource="ms-appx:///Assets/FluentIcons/FluentIconsImageResizer.png" />
@ -137,9 +143,6 @@
<BitmapIcon ShowAsMonochrome="False" UriSource="ms-appx:///Assets/FluentIcons/FluentIconsVideoConferenceMute.png" />
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>
<StackPanel Orientation="Vertical">

View File

@ -20,5 +20,6 @@ std::vector<std::wstring> processes =
L"PowerToys.ImageResizer.exe",
L"PowerToys.Update.exe",
L"PowerToys.ActionRunner.exe",
L"PowerToys.AlwaysOnTop.exe"
L"PowerToys.AlwaysOnTop.exe",
L"PowerToys.Hosts.exe"
};