Hello again! It’s been a while since my last post. I was playing around with color themes for PuTTY and the Windows Terminal, and I thought I’d share some of my recent findings on unifying the terminal experience in Windows. This is a continuation of a previous post I wrote some time ago. The previous post unfortunately did not survive the gamedev.net upgrade. I’ll detail how I have been iterating color palettes in a future post.
Unifying Windows 10 Terminal Settings
I spend most of my time at work and at home inside a terminal. I was very excited to use the new Windows 10’s terminal. It gave me reason to customize my terminal experience to match my workflow.
Unfortunately, this is apparently difficult.
It became apparent that over time, I could not reliably expect a certain look or behavior whenever I opened any terminal window. Programs would install and use their own shortcuts with their their own defaults. Windows updates would clear out my user defined settings. Launching the same program in a different way would launch an older version of my settings.
If I wanted to adjust these settings, I would have to hunt down and apply the change to every shortcut, every subkey. This is absolutely unacceptable.
And so, I set off to answer the question: How could I configure Windows to use only one set of settings? I believe I have this mostly worked out, but it’s not perfect. Let’s begin.
About the Terminal Settings in Windows
The Windows terminal settings can be stored in a number of different places, which makes unifying them all a huge pain. Understanding Windows Console Host Settings explains in great detail how this works. Here’s a quick rundown of where these settings are stored:
HKCU:\Console, the default console settings. These can be accessed from the “defaults” menu item from any terminal’s application menu.
HKCU:\Console\*, per–executable overrides using the executable path as subkey. If the terminal was launched from the run dialog, then these settings can be changed from the “properties” menu item from the terminal’s application menu.
Shortcut files (*.lnk), where each shortcut may have an embedded `CONSOLE_PROPS` data block with its own terminal settings. If the terminal was launched from a shortcut, then these settings can be changed from the “properties” menu item from the terminal’s application menu. These settings can also be changed from the shortcut’s file properties dialog.
The good news is that according to the article, Microsoft plans to release a tool to manage console settings in the future. Until then, this can be used to fix the settings in the meantime.
Now, on to unifying all the settings. I advise to proceed with caution as making permission changes may cause unexpected errors. Make backups of your registry settings and shortcuts before proceeding and proceed at your own risk.
Normalizing All the Terminal Settings
The items listed above describe where we can find terminal settings, and with them we can apply a fix and write rules to prevent them from diverging in the future:
Deleting the subkeys and denying your Windows user the ability to create keys in `HKCU:\Console`. This will prevent terminal programs launched from the run dialog from diverging from the master terminal settings.
Removing the `CONSOLE_PROPS` from shortcut files. This causes those programs to use the default settings in `HKCU:\Console`, but inspecting the fonts/colors tab on the shortcut’s properties causes them to be re–added.
Denying your Windows User write access to shortcut files prevents `CONSOLE_PROPS` from being re–added.
Disabling Registry–Level Settings
Removing the console subkeys is easy, but preventing them from being created in the future is a little trickier. This can be done by denying the “Create Subkey” permission on `HKCU:\Console`. Note: This may break some installers that expect to create a subkey here.
On the advanced security settings for `HKCU:\Console` properties, disable inheritance.
Add a permission entry for your Windows user that allows all advanced permissions except “Create subkey”.
Add a permission entry for your Windows user that denies “Create subkey”.
Check your Windows user on “Effective Access” and confirm that your Windows user cannot create a subkey.
Check the “Administrator” user on the effective access tab and confirm that administrators can create subkeys.
The MSDN blog post mentions that launching a terminal from the run dialog should use the registry to store its settings, but I haven’t been able to reproduce. It is possible that the permission step is not needed.
Disabling Shortcut–Level Settings
This is the hard part. First, a bit about how shortcuts are indexed. Windows Search uses a few locations to source your Start Menu:
%APPDATA%\Microsoft\Windows\Start Menu
%APPDATA%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar
C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu
C:\ProgramData\Microsoft\Windows\Start Menu
The shortcuts presented in the start menu are grouped by their `Target` shortcut property. If the search index or task bar found more than one shortcut with the same `Target`, then the first is selected, possibly in the order specified above. If they differ, then you will see two entries in your start menu or your task bar. The comparison is case-insensitive with variables unexpanded: `%SYSTEMROOT%\example.exe` is different from `C:\WINDOWS\example.exe`, and `C:\WINDOWS\example.exe` is the same as `C:\windows\example.exe`. All of them are different from `C:\Windows\sample.exe /ABC`.
Unlike the start menu, the shortcuts in the task bar are grouped by their `Target` property without arguments and with environment variables expanded.
My previous post recommended denying all authenticated users write access to the shortcut, which caused the indexer to skip the shortcut entirely after grouping, effectively removing the shortcut from your start menu search altogether. In this post, we’ll take a different approach.
First, a C# shim to handle COM interactions:
# space.wtfbox.win32.ShellLink.cs
using System;
using System.Runtime.InteropServices;
namespace space.wtfbox.win32 {
[ ComImport()
, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
, Guid("0000010B-0000-0000-C000-000000000046") ]
public interface IPersistFile {
int GetClassID(out Guid pClassID);
[PreserveSig()]
int IsDirty();
int Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, int dwMode);
int Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fRemember);
int SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
int GetCurFile(out IntPtr ppszFileName);
}
[ ComImport()
, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
, Guid("45E2B4AE-B1C3-11D0-B92F-00A0C90312E1") ]
public interface IShellLinkDataList {
void AddDataBlock(IntPtr pDataBlock);
[PreserveSig()]
int CopyDataBlock(uint dwSig, out IntPtr ppDataBlock);
void RemoveDataBlock(uint dwSig);
void GetFlags(out int dwFlags);
void SetFlags(uint dwFlags);
}
// custom interface
public class ShellLink : IDisposable {
public ShellLink(object lnk) {
_handle = (IPersistFile)lnk;
}
public ShellLink(object lnk, string path, int mode) {
_handle = (IPersistFile)lnk;
Load(path,mode);
}
public void Dispose() {
Marshal.ReleaseComObject(_handle);
}
public void Load(string path, int mode){
_handle.Load(path, mode);
}
public void RemoveDataBlock(uint signature) {
(_handle as IShellLinkDataList).RemoveDataBlock(signature);
}
public bool HasDataBlock(uint signature) {
IntPtr block;
int hResult = (_handle as IShellLinkDataList).CopyDataBlock(signature,out block);
if ( hResult != 0 ) {
return false;
}
Marshal.FreeHGlobal(block);
return true;
}
public int Save() {
return _handle.Save(null,true);
}
private IPersistFile _handle;
public const uint CONSOLE_PROPS = 0xA0000002;
}
}
And an accompanying PowerShell cmdlet:
# Unify-ConsoleProps.ps
[CmdLetBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory=$false, ValueFromPipeline=$true)]
[IO.FileInfo[]] $InputArray,
[IO.DirectoryInfo[]] $Paths,
[switch] $CommonPaths,
[switch] $FixRegistry,
[switch] $AdjustPermissions
)
# load dependencies
try {
[space.wtfbox.win32.ShellLink] | out-null;
}
catch {
write-verbose "Compiling helper assembly";
add-type -TypeDefinition (get-content -raw "$PSScriptRoot\wtfbox.space.win32.ShellLink.cs") | out-null;
}
# check arguments
if ( $CommonPaths ) {
$Paths += (
"$env:appdata\Microsoft\Windows\Start Menu",
"$env:appdata\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar",
"$home\desktop"
);
}
if ( $AdjustPermissions ) {
$user = [Security.Principal.NTAccount][Security.Principal.WindowsIdentity]::GetCurrent().Name;
$newRule = new-object Security.AccessControl.FileSystemAccessRule $user, "ReadAndExecute,Synchronize", "Allow";
$oldRule = new-object Security.AccessControl.FileSystemAccessRule $user, "FullControl", "Allow";
}
if ( $FixRegistry ) {
write-verbose "Removing console subkeys from registry";
get-childitem HKCU:\Console | select -expand PSPath | remove-item;
}
# prepare arguments
[IO.FileInfo[]]$InputArray = (
$InputArray + ($Paths |% {
get-childitem $_ -recurse -include *.lnk;
})
) | sort -unique;
# process shortcuts
$SL = [space.wtfbox.win32.ShellLink];
$lnk = new-object space.wtfbox.win32.ShellLink (new-object -ComObject lnkfile);
try {
$InputArray |% {
$path = $_.FullName;
try {
# load and check for CONSOLE_PROPS
$lnk.Load($path,2);
if ( -not $lnk.HasDataBlock($SL::CONSOLE_PROPS) ) {
write-verbose "CONSOLE_PROPS not found in $path";
return;
}
if ( $pscmdlet.ShouldProcess($path, "Remove CONSOLE_PROPS") ) {
# remove CONSOLE_PROPS and save
$lnk.RemoveDataBlock($SL::CONSOLE_PROPS);
$hr = $lnk.Save();
}
if ( $AdjustPermissions -and $pscmdlet.ShouldProcess($path, "Adjust ACL permissions") ) {
# disable inheritance, convert parent permissions
$acl = get-acl $path;
$acl.SetAccessRuleProtection($true, $true);
$acl.RemoveAccessRule($oldRule);
$acl.AddAccessRule($newRule);
set-acl $path $acl;
}
}
catch {
"Failed to process $($path): $_" | write-error;
}
}
}
finally {
$lnk.Dispose();
}
Then run the command as an administrator:
Unify-ConsoleProps -FixRegistry -CommonPaths -AdjustPermissions -Verbose -ErrorAction Inquire
Afterwards, all custom settings under `HKCU:\Console` will be removed, all shortcuts will be stripped of their `CONSOLE_PROPS` block, and your Windows user will be denied created them. Every terminal hence forth will be using the settings under `HKCU:\Console` as intended.
Conclusion
Because this is not perfectly future proof, this will need to be run after a major Windows update or a program is installed with shortcuts having CONSOLE_PROPS embedded. At least this is only needed until Microsoft releases the terminal settings tool.
Hopefully that day will come soon.
↧