├── .github ├── debug_ssh_start.ps1 └── workflows │ └── build.yaml ├── .gitmodules ├── Directory.Build.props ├── Directory.Build.targets ├── EasyWindowsTerminalControl.WinUI ├── DepPropHelper.cs ├── EasyWindowsTerminalControl.WinUI.csproj └── NativeMethods.txt ├── EasyWindowsTerminalControl.sln ├── EasyWindowsTerminalControl ├── EasyTerminalControl.cs ├── EasyWindowsTerminalControl.csproj ├── Internals │ ├── DepPropHelper.cs │ ├── Process.cs │ ├── ProcessFactory.cs │ ├── PseudoConsole.cs │ ├── PseudoConsoleApi.cs │ └── PseudoConsolePipe.cs ├── NativeMethods.txt ├── ReadDelimitedTermPTY.cs └── TermPTY.cs ├── Microsoft.Terminal.WinUI3 ├── HwndHost.WPfImports.cs ├── HwndHost.cs ├── Microsoft.Terminal.WinUI3.csproj ├── NativeMethods.txt ├── TerminalContainer.cs ├── TerminalControl.xaml ├── TerminalControl.xaml.cs ├── UWPHelpers.cs ├── WindowsMessages.cs └── build │ └── CI.Microsoft.Terminal.WinUI3.Unofficial.targets ├── NuGet.config ├── README.md ├── TermExample.WinUI ├── App.xaml ├── App.xaml.cs ├── Assets │ ├── LockScreenLogo.scale-200.png │ ├── SplashScreen.scale-200.png │ ├── Square150x150Logo.scale-200.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── StoreLogo.png │ └── Wide310x150Logo.scale-200.png ├── MainWindow.xaml ├── Package.appxmanifest ├── Properties │ ├── PublishProfiles │ │ ├── win-arm64.pubxml │ │ ├── win-x64.pubxml │ │ └── win-x86.pubxml │ └── launchSettings.json ├── TermExample.WinUI.csproj └── app.manifest ├── TermExample ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── ProcessOutput.xaml ├── ProcessOutput.xaml.cs ├── Screenshot.png ├── TermExample.args.json ├── TermExample.csproj └── nugetImage.png ├── build.ps1 └── version-increment.ps1 /.github/debug_ssh_start.ps1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -version latest; 2 | $ErrorActionPreference = "Stop"; 3 | $VerbosePreference="Continue"; 4 | 5 | 6 | $publicKey=(curl https://github.com/$($env:GITHUB_REPOSITORY_OWNER).keys) + " gh" 7 | Function InstallSshServer(){ 8 | [CmdletBinding()] 9 | param( 10 | [Parameter(Mandatory=$true)] 11 | [String] $publicKey 12 | ) 13 | Add-WindowsCapability -Online -Name OpenSSH.Server 14 | echo "PubkeyAuthentication yes`nPasswordAuthentication no`nSubsystem sftp sftp-server.exe`nMatch Group administrators`n`tAuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys`n" | out-file -Encoding ASCII $env:programData/ssh/sshd_config 15 | ssh-keygen -A 16 | echo "$publicKey`n" | out-file -Encoding ASCII $env:programData/ssh/administrators_authorized_keys 17 | icacls.exe "$env:programData\ssh\administrators_authorized_keys" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F" 18 | cat $env:programData/ssh/administrators_authorized_keys 19 | } 20 | Function DownloadStartCloudflareServer(){ 21 | [CmdletBinding()] 22 | param( 23 | [Parameter(Mandatory=$true)] 24 | [String] $LocalHostnameAndPort, #ie 127.0.0.1:22 25 | [Parameter(Mandatory=$false)] 26 | [String] $SaveToFilename="cloudflared.exe" #can include path 27 | ) 28 | if (([System.IO.File]::Exists($SaveToFilename)) -eq $false) { 29 | Invoke-WebRequest -URI https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe -OutFile $SaveToFilename 30 | } 31 | $myargs = "tunnel --no-autoupdate --url tcp://$LocalHostnameAndPort --logfile cfd.log" 32 | #$scriptBlock = [Scriptblock]::Create("Start-Process -NoNewWindow -Wait `"$SaveToFilename`" $myargs ") 33 | $myjob = Start-Process -PassThru -NoNewWindow `"$SaveToFilename`" -ArgumentList $myargs 34 | 35 | #Start-Job -Name CFD -ScriptBlock $scriptBlock 36 | #$myjob= Receive-Job -Name CFD 37 | return $myjob 38 | } 39 | Function InstallSSHStartCF(){ 40 | [CmdletBinding()] 41 | param( 42 | [Parameter(Mandatory=$true)] 43 | [String] $publicKey, 44 | [Parameter(Mandatory=$false)] 45 | [String] $SaveToFilename="cloudflared.exe" #can include path 46 | ) 47 | InstallSshServer $publicKey 48 | $server = DownloadStartCloudflareServer("127.0.0.1:22") 49 | $scriptBlock = [Scriptblock]::Create("Start-Process -NoNewWindow -Wait `"sshd.exe`" ") 50 | 51 | Start-Job -Name SSHD -ScriptBlock $scriptBlock 52 | return $server 53 | } 54 | InstallSSHStartCF $publicKey 55 | while ($true) { 56 | Start-Sleep -Seconds 30 57 | cat cfd.log 58 | } 59 | #Wait-Job SSHD 60 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - trash 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | strategy: 12 | matrix: 13 | configuration: [Debug, Release] 14 | defaults: 15 | run: 16 | shell: pwsh 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: recursive 21 | 22 | 23 | - name: Setup dotnet 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: | 27 | 6 28 | 8 29 | - name: Get .NET information 30 | run: dotnet --info 31 | 32 | - name: Build 33 | run: ./build.ps1 -Configuration ${{ matrix.configuration }} 34 | 35 | - name: Debug Session 36 | if: ${{ failure() && vars.DEBUG_FAIL == '1' && env.WLB_BUILD_TRACE == '1' }} 37 | run: .github/debug_ssh_start.ps1 38 | 39 | - uses: actions/upload-artifact@v4 40 | with: 41 | name: NugetPackages-${{ matrix.configuration }} 42 | path: publish/*.nupkg 43 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/Terminal"] 2 | path = external/Terminal 3 | url = https://github.com/microsoft/terminal 4 | shallow = true 5 | sparse-checkout = src/cascadia/WpfTerminalControl 6 | fetchRecurseSubmodules = false 7 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | true 7 | 8 | 9 | MitchCapper 10 | https://github.com/MitchCapper/EasyWindowsTerminalControl 11 | https://github.com/MitchCapper/EasyWindowsTerminalControl 12 | MIT 13 | Copyright © MitchCapper $([System.DateTime]::Now.Year) 14 | terminal,console,windows,winui,wpf 15 | 16 | 17 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | false 7 | true 8 | true 9 | 10 | 11 | false 12 | true 13 | false 14 | 15 | 16 | 17 | 19 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl.WinUI/DepPropHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | using Microsoft.UI.Xaml; 6 | using Microsoft.UI.Xaml.Controls; 7 | 8 | 9 | namespace EasyWindowsTerminalControl.Internals { 10 | 11 | internal class DepPropHelper where CONTROL_TYPE : UserControl { 12 | protected DepPropHelper() => throw new Exception("Should not be instanced"); 13 | public static DependencyProperty GenerateWriteOnlyProperty(Expression> PropToSet) { 14 | 15 | var me = PropToSet.Body as MemberExpression; 16 | if (me == null) 17 | throw new ArgumentException(nameof(PropToSet)); 18 | var propName = me.Member.Name; 19 | var prop = typeof(CONTROL_TYPE).GetProperty(me.Member.Name, BindingFlags.Instance | BindingFlags.Public); 20 | 21 | if (prop == null) 22 | throw new ArgumentException(nameof(PropToSet)); 23 | 24 | return DependencyProperty.Register(propName, typeof(PROP_TYPE), typeof(CONTROL_TYPE), new PropertyMetadata(null, (target, value) => CoerceReadOnlyHandle(prop.SetMethod, target, value))); 25 | } 26 | private static object CoerceReadOnlyHandle(MethodInfo SetMethod, DependencyObject target, object value) { 27 | SetMethod.Invoke(target, new object[] { value }); 28 | return null; 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl.WinUI/EasyWindowsTerminalControl.WinUI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0-windows10.0.19041.0 4 | 10.0.17763.0 5 | EasyWindowsTerminalControl.WinUI 6 | win-x86;win-x64;win-arm64 7 | true 8 | AnyCPU;x64 9 | 1.0.18-beta.1 10 | High performance WinUI3 native terminal/shell control. For WPF version see EasyWindowsTerminalControl alt package. It features full 24-bit color support with ANSI/VT escape sequences (and colors), hardware / GPU accelerated rendering, mouse support, and true console interaction. Support for command shells and key sequences, user interaction and programatic control. See GH for more details. 11 | README.md 12 | MitchCapper 13 | true 14 | ../Publish 15 | https://github.com/MitchCapper/EasyWindowsTerminalControl 16 | 17 | 18 | true 19 | $(NoWarn);NU5128 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | all 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl.WinUI/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | GetConsoleMode 2 | SetConsoleMode 3 | CreatePipe 4 | CreateProcess 5 | DeleteProcThreadAttributeList 6 | UpdateProcThreadAttribute 7 | InitializeProcThreadAttributeList 8 | DeleteProcThreadAttributeList 9 | ResizePseudoConsole 10 | COORD 11 | STARTUPINFOEXW 12 | PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 13 | SetConsoleCtrlHandler 14 | CTRL_CLOSE_EVENT 15 | CreatePseudoConsole 16 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33417.168 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TermExample", "TermExample\TermExample.csproj", "{640EBCC2-B7B8-416D-B6B4-D484D1D404BB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyWindowsTerminalControl", "EasyWindowsTerminalControl\EasyWindowsTerminalControl.csproj", "{8DBC2368-E247-4930-B51D-23F2EB59C524}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Terminal.WinUI3", "Microsoft.Terminal.WinUI3\Microsoft.Terminal.WinUI3.csproj", "{EAE4A2C3-203E-43DD-BADE-63611B1489DF}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyWindowsTerminalControl.WinUI", "EasyWindowsTerminalControl.WinUI\EasyWindowsTerminalControl.WinUI.csproj", "{0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6}" 13 | ProjectSection(ProjectDependencies) = postProject 14 | {EAE4A2C3-203E-43DD-BADE-63611B1489DF} = {EAE4A2C3-203E-43DD-BADE-63611B1489DF} 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TermExample.WinUI", "TermExample.WinUI\TermExample.WinUI.csproj", "{63F66283-5142-4B8E-BF28-FF434AF08B0B}" 18 | ProjectSection(ProjectDependencies) = postProject 19 | {0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6} = {0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6} 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|x64 = Debug|x64 25 | Release|x64 = Release|x64 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {640EBCC2-B7B8-416D-B6B4-D484D1D404BB}.Debug|x64.ActiveCfg = Debug|x64 29 | {640EBCC2-B7B8-416D-B6B4-D484D1D404BB}.Debug|x64.Build.0 = Debug|x64 30 | {640EBCC2-B7B8-416D-B6B4-D484D1D404BB}.Release|x64.ActiveCfg = Release|x64 31 | {640EBCC2-B7B8-416D-B6B4-D484D1D404BB}.Release|x64.Build.0 = Release|x64 32 | {8DBC2368-E247-4930-B51D-23F2EB59C524}.Debug|x64.ActiveCfg = Debug|x64 33 | {8DBC2368-E247-4930-B51D-23F2EB59C524}.Debug|x64.Build.0 = Debug|x64 34 | {8DBC2368-E247-4930-B51D-23F2EB59C524}.Release|x64.ActiveCfg = Release|x64 35 | {8DBC2368-E247-4930-B51D-23F2EB59C524}.Release|x64.Build.0 = Release|x64 36 | {EAE4A2C3-203E-43DD-BADE-63611B1489DF}.Debug|x64.ActiveCfg = Debug|x64 37 | {EAE4A2C3-203E-43DD-BADE-63611B1489DF}.Debug|x64.Build.0 = Debug|x64 38 | {EAE4A2C3-203E-43DD-BADE-63611B1489DF}.Release|x64.ActiveCfg = Release|Any CPU 39 | {EAE4A2C3-203E-43DD-BADE-63611B1489DF}.Release|x64.Build.0 = Release|Any CPU 40 | {0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6}.Debug|x64.ActiveCfg = Debug|x64 41 | {0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6}.Debug|x64.Build.0 = Debug|x64 42 | {0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6}.Release|x64.ActiveCfg = Release|Any CPU 43 | {0DC813BE-F9B4-43BC-9F5B-E4A50FD86CB6}.Release|x64.Build.0 = Release|Any CPU 44 | {63F66283-5142-4B8E-BF28-FF434AF08B0B}.Debug|x64.ActiveCfg = Debug|x64 45 | {63F66283-5142-4B8E-BF28-FF434AF08B0B}.Debug|x64.Build.0 = Debug|x64 46 | {63F66283-5142-4B8E-BF28-FF434AF08B0B}.Debug|x64.Deploy.0 = Debug|x64 47 | {63F66283-5142-4B8E-BF28-FF434AF08B0B}.Release|x64.ActiveCfg = Release|x64 48 | {63F66283-5142-4B8E-BF28-FF434AF08B0B}.Release|x64.Build.0 = Release|x64 49 | {63F66283-5142-4B8E-BF28-FF434AF08B0B}.Release|x64.Deploy.0 = Release|x64 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {94BA1E45-9BBB-4F9C-8041-98596F25CA05} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/EasyTerminalControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Threading.Tasks; 4 | #if WPF 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Input; 8 | using System.Windows.Media; 9 | #else 10 | using System.Drawing; 11 | using Microsoft.UI.Xaml; 12 | using Microsoft.UI.Xaml.Controls; 13 | using Microsoft.Terminal.WinUI3; 14 | using FontFamily = Microsoft.UI.Xaml.Media.FontFamily; 15 | using Microsoft.UI.Xaml.Input; 16 | #endif 17 | using EasyWindowsTerminalControl.Internals; 18 | using Microsoft.Terminal.Wpf; 19 | 20 | using System.Diagnostics; 21 | 22 | 23 | namespace EasyWindowsTerminalControl { 24 | public class EasyTerminalControl : UserControl { 25 | /// 26 | /// Converts Color to COLOREF, note that COLOREF does not support alpha channels so it is ignored 27 | /// 28 | /// 29 | /// 30 | public static uint ColorToVal(Color color) => BitConverter.ToUInt32(new byte[] { color.R, color.G, color.B, 0 }, 0); 31 | public EasyTerminalControl() { 32 | InitializeComponent(); 33 | SetKBCaptureOptions(); 34 | #if WPF 35 | #else 36 | this.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, OnVisibleChanged); 37 | #endif 38 | } 39 | #if WPF 40 | #else 41 | private void OnVisibleChanged(DependencyObject sender, DependencyProperty dp) { 42 | if (Terminal != null) 43 | Terminal.Visibility = Visibility; 44 | } 45 | #endif 46 | [Flags] 47 | [System.ComponentModel.TypeConverter(typeof(System.ComponentModel.EnumConverter))] 48 | public enum INPUT_CAPTURE { None = 1 << 0, TabKey = 1 << 1, DirectionKeys = 1 << 2 }; 49 | 50 | 51 | 52 | private static void InputCaptureChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { 53 | var cntrl = target as EasyTerminalControl; 54 | cntrl.SetKBCaptureOptions(); 55 | } 56 | private void SetKBCaptureOptions() { 57 | #if WPF 58 | KeyboardNavigation.SetTabNavigation(this, InputCapture.HasFlag(INPUT_CAPTURE.TabKey) ? KeyboardNavigationMode.Contained : KeyboardNavigationMode.Continue); 59 | KeyboardNavigation.SetDirectionalNavigation(this, InputCapture.HasFlag(INPUT_CAPTURE.DirectionKeys) ? KeyboardNavigationMode.Contained : KeyboardNavigationMode.Continue); 60 | #endif 61 | } 62 | /// 63 | /// Helper property for setting KeyboardNavigation.Set*Navigation commands to prevent arrow keys or tabs from causing us to leave the control (aka pass through to conpty) 64 | /// 65 | public INPUT_CAPTURE InputCapture { 66 | get => (INPUT_CAPTURE)GetValue(InputCaptureProperty); 67 | set => SetValue(InputCaptureProperty, value); 68 | } 69 | 70 | [Description("Write only, sets the terminal theme"), Category("Common")] 71 | public TerminalTheme? Theme { set => SetTheme(_Theme = value); private get => _Theme; } 72 | private TerminalTheme? _Theme; 73 | private void SetTheme(TerminalTheme? v) { if (v != null) Terminal?.SetTheme(v.Value, FontFamilyWhenSettingTheme.Source, (short)FontSizeWhenSettingTheme); } 74 | 75 | 76 | 77 | [Description("Write only, When true user cannot give input through the Terminal UI (can still write to the Term from code behind using Term.WriteToTerm)"), Category("Common")] 78 | public bool? IsReadOnly { set => SetReadOnly(_IsReadOnly = value); private get => _IsReadOnly; } 79 | private bool? _IsReadOnly; 80 | private void SetReadOnly(bool? v) { if (v != null) ConPTYTerm?.SetReadOnly(v.Value, false); }//no cursor auto update if user wants that they can use the separate dependency property for the cursor visibility 81 | 82 | [Description("Write only, if the type cursor shows on the Terminal UI"), Category("Common")] 83 | public bool? IsCursorVisible { set => SetCursor(_IsCursorVisible = value); private get => _IsCursorVisible; } 84 | private bool? _IsCursorVisible; 85 | private void SetCursor(bool? v) { if (v != null) ConPTYTerm?.SetCursorVisibility(v.Value); } 86 | 87 | [Description("Direct access to the UI terminal control itself that handles rendering")] 88 | public TerminalControl Terminal { 89 | #if WPF 90 | get => (TerminalControl)GetValue(TerminalPropertyKey.DependencyProperty); 91 | set => SetValue(TerminalPropertyKey, value); 92 | #else 93 | get => (TerminalControl)GetValue(TerminalProperty); 94 | set => SetValue(TerminalProperty, value); 95 | #endif 96 | } 97 | 98 | private static void OnTermChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { 99 | var cntrl = (target as EasyTerminalControl); 100 | var newTerm = e.NewValue as TermPTY; 101 | if (newTerm != null) { 102 | if (cntrl.Terminal.IsLoaded) 103 | cntrl.Terminal_Loaded(cntrl.Terminal, null); 104 | 105 | if (newTerm.TermProcIsStarted) 106 | cntrl.Term_TermReady(newTerm, null); 107 | else 108 | newTerm.TermReady += cntrl.Term_TermReady; 109 | } 110 | } 111 | /// 112 | /// Update the Term if you want to set to an existing 113 | /// 114 | [Description("The backend TermPTY connection allows changing the application the control is connected to")] 115 | public TermPTY ConPTYTerm { 116 | get => (TermPTY)GetValue(ConPTYTermProperty); 117 | set => SetValue(ConPTYTermProperty, value); 118 | } 119 | 120 | 121 | public TermPTY DisconnectConPTYTerm() { 122 | if (Terminal != null) 123 | Terminal.Connection = null; 124 | if (ConPTYTerm != null) 125 | ConPTYTerm.TermReady -= Term_TermReady; 126 | var ret = ConPTYTerm; 127 | ConPTYTerm = null; 128 | return ret; 129 | } 130 | 131 | public string StartupCommandLine { 132 | get => (string)GetValue(StartupCommandLineProperty); 133 | set => SetValue(StartupCommandLineProperty, value); 134 | } 135 | 136 | public bool LogConPTYOutput { 137 | get => (bool)GetValue(LogConPTYOutputProperty); 138 | set => SetValue(LogConPTYOutputProperty, value); 139 | } 140 | /// 141 | /// Sets if the GUI Terminal control communicates to ConPTY using extended key events (handles certain control sequences better) 142 | /// https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md 143 | /// 144 | public bool Win32InputMode { 145 | get => (bool)GetValue(Win32InputModeProperty); 146 | set => SetValue(Win32InputModeProperty, value); 147 | } 148 | 149 | public FontFamily FontFamilyWhenSettingTheme { 150 | get => (FontFamily)GetValue(FontFamilyWhenSettingThemeProperty); 151 | set => SetValue(FontFamilyWhenSettingThemeProperty, value); 152 | } 153 | 154 | public int FontSizeWhenSettingTheme { 155 | get => (int)GetValue(FontSizeWhenSettingThemeProperty); 156 | set => SetValue(FontSizeWhenSettingThemeProperty, value); 157 | } 158 | private void InitializeComponent() { 159 | Terminal = new(); 160 | ConPTYTerm = new(); 161 | Terminal.AutoResize = true; 162 | Terminal.Loaded += Terminal_Loaded; 163 | var grid = new Grid() { }; 164 | grid.Children.Add(Terminal); 165 | this.Content = grid; 166 | #if WPF 167 | Focusable = true; 168 | Terminal.Focusable = true; 169 | this.GotFocus += (_, _) => Terminal.Focus(); 170 | #else 171 | this.GotFocus += (_, _) => Terminal.Focus(FocusState.Pointer); 172 | IsTabStop = true; 173 | //FocusManager.GotFocus += (o,e) => Debug.WriteLine($"FocusManager: {e.NewFocusedElement}"); 174 | #endif 175 | 176 | } 177 | 178 | #if WPF 179 | void MainThreadRun(Action action) => Dispatcher.Invoke(action); 180 | #else 181 | async void MainThreadRun(Action action) => await UWPHelpers.Enqueue(this.DispatcherQueue, action); 182 | #endif 183 | 184 | private void Term_TermReady(object sender, EventArgs e) { 185 | MainThreadRun(() => { 186 | Terminal.Connection = ConPTYTerm; 187 | ConPTYTerm.Win32DirectInputMode(Win32InputMode); 188 | ConPTYTerm.Resize(Terminal.Columns, Terminal.Rows);//fix the size being partially off on first load 189 | }); 190 | } 191 | /// 192 | /// Restarts the command we are running in a brand new term and disposes of the old one 193 | /// 194 | /// Optional term to use, note if useTerm.TermProcIsStarted this function will not do verry much 195 | /// True if the old term should be killed off 196 | public async Task RestartTerm(TermPTY useTerm = null, bool disposeOld = true) { 197 | var oldTerm = ConPTYTerm; 198 | DisconnectConPTYTerm(); 199 | if (disposeOld) { 200 | try { 201 | oldTerm?.CloseStdinToApp(); 202 | } catch { } 203 | try { 204 | oldTerm?.StopExternalTermOnly(); 205 | } catch { } 206 | } 207 | 208 | ConPTYTerm = useTerm ?? new TermPTY(); //setting the term to a new value will automatically initalize everyhting 209 | } 210 | private void StartTerm(int column_width, int row_height) { 211 | if (ConPTYTerm?.TermProcIsStarted != false) 212 | return; 213 | 214 | MainThreadRun(() => { 215 | var cmd = StartupCommandLine;//thread safety for dp 216 | var term = ConPTYTerm; 217 | var logOutput = LogConPTYOutput; 218 | Task.Run(() => term.Start(cmd, column_width, row_height, logOutput)); 219 | }); 220 | } 221 | private async void Terminal_Loaded(object sender, RoutedEventArgs e) { 222 | await TermInit(); 223 | //Terminal.Focus(); 224 | } 225 | 226 | private async Task TermInit() { 227 | StartTerm(Terminal.Columns, Terminal.Rows); 228 | SetTheme(Theme); 229 | SetCursor(IsCursorVisible); 230 | SetReadOnly(IsReadOnly); 231 | 232 | await Task.Delay(1000); 233 | SetCursor(IsCursorVisible); 234 | } 235 | 236 | #region Depdendency Properties 237 | public static readonly DependencyProperty InputCaptureProperty = DependencyProperty.Register(nameof(InputCapture), typeof(INPUT_CAPTURE), typeof(EasyTerminalControl), new 238 | PropertyMetadata(INPUT_CAPTURE.TabKey | INPUT_CAPTURE.DirectionKeys, InputCaptureChanged)); 239 | 240 | public static readonly DependencyProperty ThemeProperty = PropHelper.GenerateWriteOnlyProperty((c) => c.Theme); 241 | #if WPF 242 | protected static readonly DependencyPropertyKey TerminalPropertyKey = DependencyProperty.RegisterReadOnly(nameof(Terminal), typeof(TerminalControl), typeof(EasyTerminalControl), new PropertyMetadata()); 243 | public static readonly DependencyProperty TerminalProperty = TerminalPropertyKey.DependencyProperty; 244 | #else 245 | public static readonly DependencyProperty TerminalProperty = DependencyProperty.Register(nameof(Terminal), typeof(TerminalControl), typeof(EasyTerminalControl), new PropertyMetadata(null)); 246 | #endif 247 | public static readonly DependencyProperty ConPTYTermProperty = DependencyProperty.Register(nameof(ConPTYTerm), typeof(TermPTY), typeof(EasyTerminalControl), new(null, OnTermChanged)); 248 | public static readonly DependencyProperty StartupCommandLineProperty = DependencyProperty.Register(nameof(StartupCommandLine), typeof(string), typeof(EasyTerminalControl), new PropertyMetadata("powershell.exe")); 249 | 250 | public static readonly DependencyProperty LogConPTYOutputProperty = DependencyProperty.Register(nameof(LogConPTYOutput), typeof(bool), typeof(EasyTerminalControl), new PropertyMetadata(false)); 251 | public static readonly DependencyProperty Win32InputModeProperty = DependencyProperty.Register(nameof(Win32InputMode), typeof(bool), typeof(EasyTerminalControl), new PropertyMetadata(true)); 252 | public static readonly DependencyProperty IsReadOnlyProperty = PropHelper.GenerateWriteOnlyProperty((c) => c.IsReadOnly); 253 | public static readonly DependencyProperty IsCursorVisibleProperty = PropHelper.GenerateWriteOnlyProperty((c) => c.IsCursorVisible); 254 | 255 | public static readonly DependencyProperty FontFamilyWhenSettingThemeProperty = DependencyProperty.Register(nameof(FontFamilyWhenSettingTheme), typeof(FontFamily), typeof(EasyTerminalControl), new PropertyMetadata(new FontFamily("Cascadia Code"))); 256 | 257 | public static readonly DependencyProperty FontSizeWhenSettingThemeProperty = DependencyProperty.Register(nameof(FontSizeWhenSettingTheme), typeof(int), typeof(EasyTerminalControl), new PropertyMetadata(12)); 258 | 259 | private class PropHelper : DepPropHelper { } 260 | 261 | #endregion 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/EasyWindowsTerminalControl.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-windows;net8.0-windows 5 | x64 6 | x64 7 | true 8 | 1.0.36 9 | README.md 10 | ../Publish 11 | true 12 | 13 | 14 | 15 | $(DefineConstants);WPF 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/Internals/DepPropHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | 7 | namespace EasyWindowsTerminalControl.Internals { 8 | 9 | internal class DepPropHelper where CONTROL_TYPE : UserControl { 10 | protected DepPropHelper() => throw new Exception("Should not be instanced"); 11 | public static DependencyProperty GenerateWriteOnlyProperty(Expression> PropToSet) { 12 | 13 | var me = PropToSet.Body as MemberExpression; 14 | if (me == null) 15 | throw new ArgumentException(nameof(PropToSet)); 16 | var propName = me.Member.Name; 17 | var prop = typeof(CONTROL_TYPE).GetProperty(me.Member.Name, BindingFlags.Instance | BindingFlags.Public); 18 | 19 | if (prop == null) 20 | throw new ArgumentException(nameof(PropToSet)); 21 | 22 | return DependencyProperty.Register(propName, typeof(PROP_TYPE), typeof(CONTROL_TYPE), new FrameworkPropertyMetadata(null, (target, value) => CoerceReadOnlyHandle(prop.SetMethod, target, value))); 23 | } 24 | private static object CoerceReadOnlyHandle(MethodInfo SetMethod, DependencyObject target, object value) { 25 | SetMethod.Invoke(target, new object[] { value }); 26 | return null; 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/Internals/Process.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.Win32; 3 | using Windows.Win32.System.Threading; 4 | 5 | namespace EasyWindowsTerminalControl.Internals { 6 | /// 7 | /// Represents an instance of a process. 8 | /// 9 | internal sealed class Process : IDisposable { 10 | public Process(STARTUPINFOEXW startupInfo, PROCESS_INFORMATION processInfo) { 11 | 12 | StartupInfo = startupInfo; 13 | ProcessInfo = processInfo; 14 | } 15 | 16 | public STARTUPINFOEXW StartupInfo { get; } 17 | public PROCESS_INFORMATION ProcessInfo { get; } 18 | 19 | #region IDisposable Support 20 | 21 | private bool disposedValue = false; // To detect redundant calls 22 | 23 | void Dispose(bool disposing) { 24 | if (!disposedValue) { 25 | if (disposing) { 26 | // dispose managed state (managed objects). 27 | } 28 | 29 | // dispose unmanaged state 30 | 31 | // Free the attribute list 32 | if (StartupInfo.lpAttributeList != default) { 33 | PInvoke.DeleteProcThreadAttributeList(StartupInfo.lpAttributeList); 34 | } 35 | // Close process and thread handles 36 | if (ProcessInfo.hProcess != IntPtr.Zero) { 37 | PInvoke.CloseHandle(ProcessInfo.hProcess); 38 | } 39 | if (ProcessInfo.hThread != IntPtr.Zero) { 40 | PInvoke.CloseHandle(ProcessInfo.hThread); 41 | } 42 | 43 | disposedValue = true; 44 | } 45 | } 46 | 47 | ~Process() { 48 | Dispose(false); 49 | } 50 | 51 | public void Dispose() { 52 | Dispose(true); 53 | GC.SuppressFinalize(this); 54 | } 55 | 56 | #endregion 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/Internals/ProcessFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.InteropServices; 4 | using Windows.Win32; 5 | using Windows.Win32.System.Threading; 6 | using Windows.Win32.Security; 7 | using Windows.Win32.Foundation; 8 | 9 | namespace EasyWindowsTerminalControl.Internals { 10 | public interface IProcess : IDisposable { 11 | void WaitForExit(); 12 | bool HasExited { get; } 13 | void Kill(bool EntireProcessTree = false); 14 | } 15 | public interface IProcessFactory { 16 | IProcess Start(string command, nuint attributes, PseudoConsole console); 17 | } 18 | /// 19 | /// Support for starting and configuring processes. 20 | /// 21 | public static class ProcessFactory { 22 | public class WrappedProcess : IDisposable, IProcess { 23 | internal WrappedProcess(Process process) { _process = process; } 24 | internal Process _process; 25 | public int Pid => (int)_process.ProcessInfo.dwProcessId; 26 | public System.Diagnostics.Process Process => _Process ??= System.Diagnostics.Process.GetProcessById(Pid); 27 | 28 | public bool HasExited => Process.HasExited; 29 | public void WaitForExit() => Process.WaitForExit(); 30 | public void Kill(bool EntireProcessTree = false) => Process.Kill(EntireProcessTree); 31 | 32 | private System.Diagnostics.Process _Process; 33 | private bool IsDisposed; 34 | 35 | protected virtual void Dispose(bool disposing) { 36 | if (!IsDisposed) { 37 | if (disposing) { 38 | _process.Dispose(); 39 | } 40 | IsDisposed = true; 41 | } 42 | } 43 | 44 | public void Dispose() { 45 | Dispose(disposing: true); 46 | GC.SuppressFinalize(this); 47 | } 48 | } 49 | /// 50 | /// Start and configure a process. The return value represents the process and should be disposed. 51 | /// 52 | public static WrappedProcess Start(string command, nuint attributes, PseudoConsole console) { 53 | var startupInfo = ConfigureProcessThread(console.Handle, attributes); 54 | var processInfo = RunProcess(ref startupInfo, command); 55 | return new(new Process(startupInfo, processInfo)); 56 | } 57 | 58 | unsafe private static STARTUPINFOEXW ConfigureProcessThread(PseudoConsole.ConPtyClosePseudoConsoleSafeHandle hPC, nuint attributes) { 59 | // this method implements the behavior described in https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-for-creation-of-the-child-process 60 | 61 | nuint lpSize = 0; 62 | var success = PInvoke.InitializeProcThreadAttributeList( 63 | lpAttributeList: default, 64 | dwAttributeCount: 1, 65 | dwFlags: 0, 66 | lpSize: &lpSize 67 | ); 68 | if (success || lpSize == 0) // we're not expecting `success` here, we just want to get the calculated lpSize 69 | { 70 | throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not calculate the number of bytes for the attribute list."); 71 | } 72 | 73 | var startupInfo = new STARTUPINFOEXW(); 74 | startupInfo.StartupInfo.cb = (uint)Marshal.SizeOf(); 75 | startupInfo.lpAttributeList = new LPPROC_THREAD_ATTRIBUTE_LIST((void*)Marshal.AllocHGlobal((int)lpSize)); 76 | 77 | success = PInvoke.InitializeProcThreadAttributeList( 78 | lpAttributeList: startupInfo.lpAttributeList, 79 | dwAttributeCount: 1, 80 | dwFlags: 0, 81 | lpSize: &lpSize 82 | ); 83 | if (!success) { 84 | throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set up attribute list."); 85 | } 86 | 87 | success = PInvoke.UpdateProcThreadAttribute( 88 | lpAttributeList: startupInfo.lpAttributeList, 89 | dwFlags: 0, 90 | attributes, 91 | (void*)hPC.DangerousGetHandle(), 92 | (nuint)IntPtr.Size, 93 | null, 94 | (nuint*)null 95 | ); 96 | if (!success) { 97 | throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set pseudoconsole thread attribute."); 98 | } 99 | 100 | return startupInfo; 101 | } 102 | 103 | unsafe private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEXW sInfoEx, string commandLine) { 104 | uint securityAttributeSize = (uint)Marshal.SizeOf(); 105 | var pSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize }; 106 | var tSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize }; 107 | var info = sInfoEx; 108 | Span spanChar = (commandLine + '\0').ToCharArray(); 109 | 110 | var success = PInvoke.CreateProcess(null, ref spanChar, pSec, tSec, false, PROCESS_CREATION_FLAGS.EXTENDED_STARTUPINFO_PRESENT, null, null, info.StartupInfo, out var pInfo); 111 | 112 | if (!success) 113 | throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not create process."); 114 | 115 | return pInfo; 116 | 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/Internals/PseudoConsole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32.SafeHandles; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using Windows.Win32; 6 | using Windows.Win32.System.Console; 7 | 8 | namespace EasyWindowsTerminalControl.Internals { 9 | /// 10 | /// Utility functions around the new Pseudo Console APIs. 11 | /// 12 | public class PseudoConsole : IDisposable { 13 | private bool disposed; 14 | public bool IsDisposed => disposed; 15 | internal ConPtyClosePseudoConsoleSafeHandle Handle { get; } 16 | 17 | /// 18 | /// Required for any 3rd parties trying to implement their own process creation 19 | /// 20 | public IntPtr GetDangerousHandle => Handle.DangerousGetHandle(); 21 | 22 | private PseudoConsole(ConPtyClosePseudoConsoleSafeHandle handle) { 23 | Handle = handle; 24 | } 25 | public void Resize(int width, int height) { 26 | PseudoConsoleApi.ResizePseudoConsole(Handle.DangerousGetHandle(), new COORD { X = (short)width, Y = (short)height }); 27 | } 28 | internal class ConPtyClosePseudoConsoleSafeHandle : ClosePseudoConsoleSafeHandle { 29 | public ConPtyClosePseudoConsoleSafeHandle(IntPtr preexistingHandle, bool ownsHandle = true) : base(preexistingHandle, ownsHandle) { 30 | } 31 | protected override bool ReleaseHandle() { 32 | PseudoConsoleApi.ClosePseudoConsole(handle); 33 | return true; 34 | } 35 | } 36 | public static PseudoConsole Create(SafeFileHandle inputReadSide, SafeFileHandle outputWriteSide, int width, int height) { 37 | if (width == 0 || height == 0){ 38 | Debug.WriteLine($"PseudoConsole Create called with 0 width height"); 39 | width = 80; 40 | height=30; 41 | } 42 | var createResult = PseudoConsoleApi.CreatePseudoConsole( 43 | new COORD { X = (short)width, Y = (short)height }, 44 | inputReadSide, outputWriteSide, 45 | 0, out IntPtr hPC); 46 | if (createResult != 0) { 47 | throw new Win32Exception(createResult); 48 | //throw new Win32Exception(createResult, "Could not create pseudo console."); 49 | } 50 | return new PseudoConsole(new ConPtyClosePseudoConsoleSafeHandle(hPC)); 51 | } 52 | 53 | private void Dispose(bool disposing) { 54 | if (!disposed) { 55 | if (disposing) { 56 | Handle.Dispose(); 57 | } 58 | 59 | // TODO: set large fields to null 60 | disposed = true; 61 | } 62 | } 63 | 64 | public void Dispose() { 65 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 66 | Dispose(disposing: true); 67 | GC.SuppressFinalize(this); 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/Internals/PseudoConsoleApi.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32.SafeHandles; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using Windows.Win32.System.Console; 5 | namespace EasyWindowsTerminalControl.Internals { 6 | /// 7 | /// PInvoke signatures for Win32's PseudoConsole API. 8 | /// 9 | public static class PseudoConsoleApi { 10 | 11 | [DllImport("conpty.dll", SetLastError = true)] 12 | internal static extern int CreatePseudoConsole(COORD size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out IntPtr phPC); 13 | [DllImport("conpty.dll", SetLastError = true)] 14 | internal static extern int ClosePseudoConsole(IntPtr hPC); 15 | [DllImport("conpty.dll", SetLastError = true)] 16 | internal static extern int ResizePseudoConsole(IntPtr hPC, COORD size); 17 | 18 | 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/Internals/PseudoConsolePipe.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32.SafeHandles; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.InteropServices; 5 | using Windows.Win32; 6 | 7 | namespace EasyWindowsTerminalControl.Internals { 8 | /// 9 | /// A pipe used to talk to the pseudoconsole, as described in: 10 | /// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session 11 | /// 12 | /// 13 | /// We'll have two instances of this class, one for input and one for output. 14 | /// 15 | public class PseudoConsolePipe : IDisposable { 16 | public readonly SafeFileHandle ReadSide; 17 | public readonly SafeFileHandle WriteSide; 18 | 19 | public PseudoConsolePipe() { 20 | if (!PInvoke.CreatePipe(out ReadSide, out WriteSide, null, 0)) { 21 | throw new Win32Exception(Marshal.GetLastWin32Error(), "failed to create pipe"); 22 | } 23 | } 24 | 25 | #region IDisposable 26 | 27 | void Dispose(bool disposing) { 28 | if (disposing) { 29 | ReadSide?.Dispose(); 30 | WriteSide?.Dispose(); 31 | } 32 | } 33 | 34 | public void Dispose() { 35 | Dispose(true); 36 | GC.SuppressFinalize(this); 37 | } 38 | 39 | #endregion 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | GetConsoleMode 2 | SetConsoleMode 3 | CreatePipe 4 | CreateProcess 5 | DeleteProcThreadAttributeList 6 | UpdateProcThreadAttribute 7 | InitializeProcThreadAttributeList 8 | DeleteProcThreadAttributeList 9 | ResizePseudoConsole 10 | COORD 11 | STARTUPINFOEXW 12 | PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 13 | SetConsoleCtrlHandler 14 | CTRL_CLOSE_EVENT 15 | CreatePseudoConsole 16 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/ReadDelimitedTermPTY.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyWindowsTerminalControl { 4 | /// 5 | /// terminal that will only output text after a specific delimiter is hit and will remove the delmiter 6 | /// 7 | public class ReadDelimitedTermPTY : TermPTY { 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// Maximum time to buffer output waiting for a delimiter since the last delimiter was seen. Once this time passes the entire buffer is sent on the next output. 15 | public ReadDelimitedTermPTY(int READ_BUFFER_SIZE = 1024 * 16, bool USE_BINARY_WRITER=false, ReadOnlySpan delimiter=default, TimeSpan MaxWaitTimeoutForDelimiter = default) : base(READ_BUFFER_SIZE,USE_BINARY_WRITER) { 16 | if (delimiter != default) 17 | SetReadOutputDelimiter(delimiter, MaxWaitTimeoutForDelimiter); 18 | } 19 | 20 | override protected Span HandleRead(ref ReadState state) { 21 | var sendSpan = Span.Empty; 22 | curBufferOffset += state.readChars; 23 | var working = state.entireBuffer.Slice(lastDelimEndOffset, curBufferOffset - lastDelimEndOffset); 24 | var delimPos = working.LastIndexOf(delimiter); 25 | if (delimPos != -1) { 26 | sendSpan = working.Slice(0, delimPos); 27 | lastDelimEndOffset += delimPos + delimiter.Length; 28 | if (delimiterTimeout != default) 29 | lastDelimiterSeen = DateTime.Now; 30 | } 31 | state.curBuffer = state.entireBuffer.Slice(curBufferOffset); 32 | if (state.curBuffer.Length == 0) { 33 | if (lastDelimEndOffset == 0) {//this means the buffer is full so just send it all 34 | sendSpan = state.entireBuffer; 35 | curBufferOffset = lastDelimEndOffset = 0; 36 | } else {//shift everything left 37 | var toCopyBlocks = state.entireBuffer.Length - lastDelimEndOffset; 38 | var copyWindow = lastDelimEndOffset < toCopyBlocks ? lastDelimEndOffset : toCopyBlocks; 39 | var copyPos = lastDelimEndOffset; 40 | while (toCopyBlocks > 0) { 41 | var dstPos = copyPos - lastDelimEndOffset; 42 | var copyAmount = (copyPos + copyWindow) > state.entireBuffer.Length ? state.entireBuffer.Length - copyPos : copyWindow; 43 | state.entireBuffer.Slice(copyPos, copyAmount).CopyTo(state.entireBuffer.Slice(dstPos, copyAmount)); 44 | copyPos += copyAmount; 45 | toCopyBlocks -= copyAmount; 46 | } 47 | curBufferOffset = state.entireBuffer.Length - lastDelimEndOffset; 48 | lastDelimEndOffset = 0; 49 | } 50 | state.curBuffer = state.entireBuffer.Slice(curBufferOffset); 51 | } 52 | if (sendSpan.IsEmpty && delimiterTimeout != default && lastDelimiterSeen != default){ 53 | if ((DateTime.Now - lastDelimiterSeen) > delimiterTimeout){ 54 | sendSpan = state.entireBuffer; 55 | curBufferOffset = lastDelimEndOffset = 0; 56 | lastDelimiterSeen = DateTime.Now; 57 | } 58 | } 59 | return sendSpan; 60 | } 61 | 62 | protected int curBufferOffset = 0; //where in the entirebuffer does the current buffer to read into start 63 | protected int lastDelimEndOffset = 0; //where in the entirebuffer did the last delimiter end should always be <= curBufferOffset, the data between here and curBufferOffset is what is still valid data needing to be sent. 64 | 65 | 66 | /// 67 | /// Will only send data to the UI Terminal after each delimiter is hit. Caution as the terminal will not get any updates until the delimiter is hit. Pass a 0 length span to disable waiting for a delimiter. Note if the buffer is completely filled before the delimiter is hit the buffer will be pasesd on. 68 | /// Note: Delimiter itself is not ever passed 69 | /// 70 | /// 71 | /// Maximum time to buffer output waiting for a delimiter since the last delimiter was seen. Once this time passes the entire buffer is sent on the next output. 72 | public void SetReadOutputDelimiter(ReadOnlySpan delimiter, TimeSpan MaxWaitTimeoutForDelimiter = default) { 73 | this.delimiter = delimiter.ToArray(); 74 | delimiterTimeout = MaxWaitTimeoutForDelimiter; 75 | } 76 | protected char[] delimiter; 77 | protected TimeSpan delimiterTimeout; 78 | protected DateTime lastDelimiterSeen; 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /EasyWindowsTerminalControl/TermPTY.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Terminal.Wpf; 2 | using Microsoft.Win32.SafeHandles; 3 | using System; 4 | using System.IO; 5 | using Windows.Win32; 6 | using System.Threading.Tasks; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using EasyWindowsTerminalControl.Internals; 10 | 11 | namespace EasyWindowsTerminalControl { 12 | /// 13 | /// Class for managing communication with the underlying console, and communicating with its pseudoconsole. 14 | /// 15 | public class TermPTY : ITerminalConnection { 16 | protected class InternalProcessFactory : IProcessFactory { 17 | public IProcess Start(string command, nuint attributes, PseudoConsole console) => ProcessFactory.Start(command, attributes, console); 18 | } 19 | #if WPF 20 | private static bool IsDesignMode = System.ComponentModel.DesignerProperties.GetIsInDesignMode(new System.Windows.DependencyObject()); 21 | #else 22 | private static bool IsDesignMode = false; // no designer no need to detect:) 23 | #endif 24 | private SafeFileHandle _consoleInputPipeWriteHandle; 25 | private StreamWriter _consoleInputWriter; 26 | private BinaryWriter _consoleInputWriterB; 27 | public TermPTY(int READ_BUFFER_SIZE = 1024 * 16, bool USE_BINARY_WRITER = false, IProcessFactory ProcessFactory = null) { 28 | this.READ_BUFFER_SIZE = READ_BUFFER_SIZE; 29 | this.USE_BINARY_WRITER = USE_BINARY_WRITER; 30 | } 31 | private bool USE_BINARY_WRITER; 32 | 33 | public StringBuilder ConsoleOutputLog { get; private set; } 34 | private static Regex NewlineReduce = new(@"\n\s*?\n\s*?[\s]+", RegexOptions.Singleline); 35 | public static Regex colorStrip = new(@"((\x1B\[\??[0-9;]*[a-zA-Z])|\uFEFF|\u200B|\x1B\]0;|[\a\b])", RegexOptions.Compiled | RegexOptions.ExplicitCapture); //also strips BOM, bells, backspaces etc 36 | public string GetConsoleText(bool stripVTCodes = true) => NewlineReduce.Replace((stripVTCodes ? StripColors(ConsoleOutputLog.ToString()) : ConsoleOutputLog.ToString()).Replace("\r", ""), "\n\n").Trim(); 37 | 38 | public static string StripColors(String str) { 39 | return colorStrip.Replace(str, ""); 40 | } 41 | 42 | /// 43 | /// A stream of VT-100-enabled output from the console. 44 | /// 45 | public FileStream ConsoleOutStream { get; private set; } 46 | 47 | /// 48 | /// Fired once the console has been hooked up and is ready to receive input. 49 | /// 50 | public event EventHandler TermReady; 51 | public event EventHandler TerminalOutput;//how we send data to the UI terminal 52 | public bool TermProcIsStarted { get; private set; } 53 | 54 | 55 | /// 56 | /// Start the pseudoconsole and run the process as shown in 57 | /// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#creating-the-pseudoconsole 58 | /// 59 | /// the command to run, e.g. cmd.exe 60 | /// The height (in characters) to start the pseudoconsole with. Defaults to 80. 61 | /// The width (in characters) to start the pseudoconsole with. Defaults to 30. 62 | /// Whether to log the output of the console to a file. Defaults to false. 63 | /// While not recommended, you can provide your own process factory for more granular control over process creation 64 | public void Start(string command, int consoleWidth = 80, int consoleHeight = 30, bool logOutput = false, IProcessFactory factory = null) { 65 | if (Process != null) 66 | throw new Exception("Called Start on ConPTY term after already started"); 67 | factory ??= new InternalProcessFactory(); 68 | if (IsDesignMode) { 69 | TermProcIsStarted = true; 70 | TermReady?.Invoke(this, EventArgs.Empty); 71 | return; 72 | } 73 | if (logOutput) 74 | ConsoleOutputLog = new(); 75 | using (var inputPipe = new PseudoConsolePipe()) 76 | using (var outputPipe = new PseudoConsolePipe()) 77 | using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, consoleWidth, consoleHeight)) 78 | using (var process = factory.Start(command, PInvoke.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pseudoConsole)) { 79 | Process = process; 80 | TheConsole = pseudoConsole; 81 | // copy all pseudoconsole output to a FileStream and expose it to the rest of the app 82 | ConsoleOutStream = new FileStream(outputPipe.ReadSide, FileAccess.Read); 83 | TermProcIsStarted = true; 84 | 85 | TermReady?.Invoke(this, EventArgs.Empty); 86 | 87 | // Store input pipe handle, and a writer for later reuse 88 | _consoleInputPipeWriteHandle = inputPipe.WriteSide; 89 | var st = new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write); 90 | if (!USE_BINARY_WRITER) 91 | _consoleInputWriter = new StreamWriter(st) { AutoFlush = true }; 92 | else 93 | _consoleInputWriterB = new BinaryWriter(st); 94 | // free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar) 95 | ReadOutputLoop(); 96 | OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe, _consoleInputWriter)); 97 | 98 | process.WaitForExit(); 99 | WriteToUITerminal("Session Terminated"); 100 | 101 | TheConsole.Dispose(); 102 | } 103 | } 104 | public IProcess Process { get; protected set; } 105 | PseudoConsole TheConsole; 106 | /// 107 | /// Sends the given string to the anonymous pipe that writes to the active pseudoconsole. 108 | /// 109 | /// A string of characters to write to the console. Supports VT-100 codes. 110 | public void WriteToTerm(ReadOnlySpan input) { 111 | if (IsDesignMode) 112 | return; 113 | if (TheConsole.IsDisposed) 114 | return; 115 | if (_consoleInputWriter == null && _consoleInputWriterB == null) 116 | throw new InvalidOperationException("There is no writer attached to a pseudoconsole. Have you called Start on this instance yet?"); 117 | if (!USE_BINARY_WRITER) 118 | _consoleInputWriter.Write(input); 119 | else 120 | WriteToTermBinary(Encoding.UTF8.GetBytes(input.ToString())); 121 | } 122 | public void WriteToTermBinary(ReadOnlySpan input) { 123 | if (!USE_BINARY_WRITER) { 124 | WriteToTerm(Encoding.UTF8.GetString(input)); 125 | return; 126 | } 127 | _consoleInputWriterB.Write(input); 128 | _consoleInputWriterB.Flush(); 129 | } 130 | /// 131 | /// Close the input stream to the process (will send EOF if attempted to be read). 132 | /// 133 | public void CloseStdinToApp() { 134 | _consoleInputWriter?.Close(); 135 | _consoleInputWriter?.Dispose(); 136 | _consoleInputWriterB?.Close(); 137 | _consoleInputWriterB?.Dispose(); 138 | _consoleInputWriter = null; 139 | _consoleInputWriterB = null; 140 | } 141 | public void StopExternalTermOnly() { 142 | if (Process?.HasExited != false) return; 143 | Process.Kill(); 144 | } 145 | /// 146 | /// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button). 147 | /// Intended for resource cleanup logic. 148 | /// 149 | private static void OnClose(Action handler) { 150 | PInvoke.SetConsoleCtrlHandler(eventType => { 151 | if (eventType == PInvoke.CTRL_CLOSE_EVENT) { 152 | handler(); 153 | } 154 | return false; 155 | }, true); 156 | } 157 | 158 | private void DisposeResources(params IDisposable[] disposables) { 159 | foreach (var disposable in disposables) { 160 | disposable.Dispose(); 161 | } 162 | } 163 | 164 | public void Start() { 165 | if (IsDesignMode) { 166 | WriteToUITerminal("MyShell DesignMode:> Your command window content here\r\n"); 167 | return; 168 | } 169 | 170 | Task.Run(ReadOutputLoop); 171 | } 172 | 173 | /// 174 | /// Note if you change the span to a 0 length then no input will be passed on 175 | /// 176 | /// 177 | public delegate void InterceptDelegate(ref Span str); 178 | 179 | public InterceptDelegate InterceptOutputToUITerminal; 180 | public InterceptDelegate InterceptInputToTermApp; 181 | /// 182 | /// This simulates output from the program itself to the terminal window, ANSI sequences can be sent here as well 183 | /// 184 | /// 185 | public void WriteToUITerminal(ReadOnlySpan str) { 186 | //Debug.WriteLine($"Term.cs WriteToUITerminal got: {str.ToString().Replace("\n","\n\t").Trim()}"); 187 | TerminalOutput?.Invoke(this, new TerminalOutputEventArgs(str.ToString())); 188 | } 189 | public bool ReadLoopStarted = false; 190 | 191 | /// 192 | /// Sets if the GUI Terminal control communicates to ConPTY using extended key events (handles certain control sequences better) 193 | /// https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md 194 | /// 195 | /// 196 | public void Win32DirectInputMode(bool enable) { 197 | var decSet = enable ? "h" : "l"; 198 | var str = $"\x1b[?9001{decSet}"; 199 | WriteToUITerminal(str); 200 | } 201 | int READ_BUFFER_SIZE; 202 | protected ref struct ReadState { 203 | public Span entireBuffer; 204 | public Span curBuffer; 205 | public int readChars; 206 | } 207 | protected virtual void ReadOutputLoop() { 208 | if (ReadLoopStarted) 209 | return; 210 | ReadLoopStarted = true; 211 | // We have a few ways to handle the buffer with a delimiter but given the size of the buffer and the fairly cheap cost of copying, the ability to let the span be modified before passing it on, we will just copy any parts before the next delimiter to the start of the buffer when reaching the end. 212 | using (StreamReader reader = new StreamReader(ConsoleOutStream)) { 213 | ReadState state = new() { entireBuffer = new char[READ_BUFFER_SIZE] }; 214 | 215 | state.curBuffer = state.entireBuffer.Slice(0); 216 | 217 | while ((state.readChars = reader.Read(state.curBuffer)) != 0) { 218 | // Debug.WriteLine($"Read: {read}"); 219 | 220 | var sendSpan = HandleRead(ref state); 221 | 222 | if (!sendSpan.IsEmpty) { 223 | InterceptOutputToUITerminal?.Invoke(ref sendSpan); 224 | if (sendSpan.Length > 0) { 225 | var str = sendSpan.ToString(); 226 | WriteToUITerminal(str); 227 | ConsoleOutputLog?.Append(str); 228 | } 229 | } 230 | } 231 | } 232 | } 233 | /// 234 | /// return the span (if any) to send to client, and the curBuffer 235 | /// 236 | /// 237 | /// 238 | protected virtual Span HandleRead(ref ReadState state) { 239 | return state.curBuffer.Slice(0, state.readChars); 240 | } 241 | 242 | 243 | /// 244 | /// When set to true and input from the UI WPF Terminal Control will be ignored, will still invoke the intercept event. You can still call WriteToTerm to write to the app yourself. 245 | /// 246 | /// Enable / Disable readonly mode 247 | /// Will hide/show the cursor depending on readonly setting 248 | public void SetReadOnly(bool readOnly = true, bool updateCursor = true) { 249 | _ReadOnly = readOnly; 250 | if (updateCursor) 251 | SetCursorVisibility(!readOnly); 252 | } 253 | protected bool _ReadOnly; 254 | 255 | void ITerminalConnection.WriteInput(string data) { 256 | Span span = data.ToCharArray(); 257 | InterceptInputToTermApp?.Invoke(ref span); 258 | if (span.Length > 0 && !_ReadOnly) 259 | WriteToTerm(span); 260 | } 261 | 262 | void ITerminalConnection.Resize(uint row_height, uint column_width) { 263 | TheConsole?.Resize((int)column_width, (int)row_height); 264 | } 265 | public void Resize(int column_width, int row_height) { 266 | TheConsole?.Resize(column_width, row_height); 267 | } 268 | public void SetCursorVisibility(bool visible) => WriteToUITerminal("\x1b[?25" + (visible ? 'h' : 'l')); 269 | /// 270 | /// 271 | /// 272 | /// Means near all parameters of the term are reset to defaults rather than just clearing the screen 273 | public void ClearUITerminal(bool fullReset = false) => WriteToUITerminal(fullReset ? "\x001bc\x1b]104\x1b\\" : "\x1b[H\x1b[2J\u001b[3J"); 274 | 275 | void ITerminalConnection.Close() { 276 | TheConsole?.Dispose(); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/HwndHost.WPfImports.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows.Interop; 7 | using Microsoft.UI.Xaml.Automation.Peers; 8 | using Windows.System; 9 | using WinUIEx.Messaging; 10 | 11 | namespace Microsoft.Terminal.WinUI3.WPFImports { 12 | public struct DpiScale { 13 | /// Initializes a new instance of the structure. 14 | /// The DPI scale on the X axis. 15 | /// The DPI scale on the Y axis. 16 | // Token: 0x0600174F RID: 5967 RVA: 0x0048792C File Offset: 0x0048792C 17 | public DpiScale(double dpiScaleX, double dpiScaleY) { 18 | _dpiScaleX = dpiScaleX; 19 | _dpiScaleY = dpiScaleY; 20 | } 21 | 22 | /// Gets the DPI scale on the X axis. 23 | /// The DPI scale for the X axis. 24 | // Token: 0x1700066A RID: 1642 25 | // (get) Token: 0x06001750 RID: 5968 RVA: 0x0048793C File Offset: 0x0048793C 26 | public double DpiScaleX => _dpiScaleX; 27 | 28 | /// Gets the DPI scale on the Yaxis. 29 | /// The DPI scale for the Y axis. 30 | // Token: 0x1700066B RID: 1643 31 | // (get) Token: 0x06001751 RID: 5969 RVA: 0x00487944 File Offset: 0x00487944 32 | public double DpiScaleY => _dpiScaleY; 33 | 34 | /// Get or sets the PixelsPerDip at which the text should be rendered. 35 | /// The current value. 36 | // Token: 0x1700066C RID: 1644 37 | // (get) Token: 0x06001752 RID: 5970 RVA: 0x0048794C File Offset: 0x0048794C 38 | public double PixelsPerDip => _dpiScaleY; 39 | 40 | /// Gets the DPI along X axis. 41 | /// The DPI along the X axis. 42 | // Token: 0x1700066D RID: 1645 43 | // (get) Token: 0x06001753 RID: 5971 RVA: 0x00487954 File Offset: 0x00487954 44 | public double PixelsPerInchX => 96.0 * _dpiScaleX; 45 | 46 | /// Gets the DPI along Y axis. 47 | /// The DPI along the Y axis. 48 | // Token: 0x1700066E RID: 1646 49 | // (get) Token: 0x06001754 RID: 5972 RVA: 0x00487968 File Offset: 0x00487968 50 | public double PixelsPerInchY => 96.0 * _dpiScaleY; 51 | 52 | // Token: 0x06001755 RID: 5973 RVA: 0x0048797C File Offset: 0x0048797C 53 | internal bool Equals(DpiScale other) { 54 | if (_dpiScaleX == other._dpiScaleX) { 55 | return _dpiScaleY == other._dpiScaleY; 56 | } 57 | return false; 58 | } 59 | 60 | // Token: 0x04000F55 RID: 3925 61 | private readonly double _dpiScaleX; 62 | 63 | // Token: 0x04000F56 RID: 3926 64 | private readonly double _dpiScaleY; 65 | } 66 | public sealed class DpiChangedEventArgs : EventArgs { 67 | // Token: 0x06001738 RID: 5944 RVA: 0x004877EC File Offset: 0x004877EC 68 | public DpiChangedEventArgs(DpiScale oldDpi, DpiScale newDpi, object source) 69 | : base() { 70 | OldDpi = oldDpi; 71 | NewDpi = newDpi; 72 | } 73 | 74 | /// Gets the DPI scale information before a DPI change. 75 | /// Information about the previous DPI scale. 76 | // Token: 0x17000660 RID: 1632 77 | // (get) Token: 0x06001739 RID: 5945 RVA: 0x00487808 File Offset: 0x00487808 78 | // (set) Token: 0x0600173A RID: 5946 RVA: 0x00487810 File Offset: 0x00487810 79 | public DpiScale OldDpi { get; private set; } 80 | 81 | /// Gets the scale information after a DPI change. 82 | /// The new DPI scale information. 83 | // Token: 0x17000661 RID: 1633 84 | // (get) Token: 0x0600173B RID: 5947 RVA: 0x0048781C File Offset: 0x0048781C 85 | // (set) Token: 0x0600173C RID: 5948 RVA: 0x00487824 File Offset: 0x00487824 86 | public DpiScale NewDpi { get; private set; } 87 | } 88 | public delegate void DpiChangedEventHandler(object sender, DpiChangedEventArgs e); 89 | public interface IKeyboardInputSite { 90 | /// Unregisters a child keyboard input sink from this site. 91 | // Token: 0x060010B4 RID: 4276 92 | void Unregister(); 93 | 94 | /// Gets the keyboard sink associated with this site. 95 | /// The current site's interface. 96 | // Token: 0x170004E0 RID: 1248 97 | // (get) Token: 0x060010B5 RID: 4277 98 | IKeyboardInputSink Sink { get; } 99 | 100 | /// Called by a contained component when it has reached its last tab stop and has no further items to tab to. 101 | /// Specifies whether focus should be set to the first or the last tab stop. 102 | /// If this method returns , the site has shifted focus to another component. If this method returns , focus is still within the calling component. The component should "wrap around" and set focus to its first contained tab stop. 103 | // Token: 0x060010B6 RID: 4278 104 | bool OnNoMoreTabStops(TraversalRequest request); 105 | } 106 | public interface IKeyboardInputSink { 107 | /// Registers the interface of a contained component. 108 | /// The sink of the contained component. 109 | /// The site of the contained component. 110 | // Token: 0x060010AC RID: 4268 111 | IKeyboardInputSite RegisterKeyboardInputSink(IKeyboardInputSink sink); 112 | 113 | /// Processes keyboard input at the keydown message level. 114 | /// The message and associated data. Do not modify this structure. It is passed by reference for performance reasons only. 115 | /// Modifier keys. 116 | /// 117 | /// if the message was handled by the method implementation; otherwise, . 118 | // Token: 0x060010AD RID: 4269 119 | bool TranslateAccelerator(ref Message msg, VirtualKeyModifiers modifiers); 120 | 121 | /// Sets focus on either the first tab stop or the last tab stop of the sink. 122 | /// Specifies whether focus should be set to the first or the last tab stop. 123 | /// 124 | /// if the focus has been set as requested; , if there are no tab stops. 125 | // Token: 0x060010AE RID: 4270 126 | bool TabInto(TraversalRequest request); 127 | 128 | /// Gets or sets a reference to the component's container's interface. 129 | /// A reference to the container's interface. 130 | // Token: 0x170004DF RID: 1247 131 | // (get) Token: 0x060010AF RID: 4271 132 | // (set) Token: 0x060010B0 RID: 4272 133 | IKeyboardInputSite KeyboardInputSite { get; set; } 134 | 135 | /// Called when one of the mnemonics (access keys) for this sink is invoked. 136 | /// The message for the mnemonic and associated data. Do not modify this message structure. It is passed by reference for performance reasons only. 137 | /// Modifier keys. 138 | /// 139 | /// if the message was handled; otherwise, . 140 | // Token: 0x060010B1 RID: 4273 141 | bool OnMnemonic(ref Message msg, VirtualKeyModifiers modifiers); 142 | 143 | /// Processes WM_CHAR, WM_SYSCHAR, WM_DEADCHAR, and WM_SYSDEADCHAR input messages before is called. 144 | /// The message and associated data. Do not modify this structure. It is passed by reference for performance reasons only. 145 | /// Modifier keys. 146 | /// 147 | /// if the message was processed and should not be called; otherwise, . 148 | // Token: 0x060010B2 RID: 4274 149 | bool TranslateChar(ref Message msg, VirtualKeyModifiers modifiers); 150 | 151 | /// Gets a value that indicates whether the sink or one of its contained components has focus. 152 | /// 153 | /// if the sink or one of its contained components has focus; otherwise, . 154 | // Token: 0x060010B3 RID: 4275 155 | bool HasFocusWithin(); 156 | } 157 | public class TraversalRequest { 158 | /// Initializes a new instance of the class. 159 | /// The intended direction of the focus traversal, as a value of the enumeration. 160 | // Token: 0x0600127B RID: 4731 RVA: 0x001253A8 File Offset: 0x001253A8 161 | public TraversalRequest(bool isBackward) { 162 | 163 | _focusNavigationDirection = isBackward; 164 | } 165 | 166 | /// Gets or sets a value that indicates whether focus traversal has reached the end of child elements that can have focus. 167 | /// 168 | /// if this traversal has reached the end of child elements that can have focus; otherwise, . The default is . 169 | // Token: 0x1700052A RID: 1322 170 | // (get) Token: 0x0600127C RID: 4732 RVA: 0x001253F8 File Offset: 0x001253F8 171 | // (set) Token: 0x0600127D RID: 4733 RVA: 0x00125400 File Offset: 0x00125400 172 | public bool Wrapped { 173 | get { 174 | return _wrapped; 175 | } 176 | set { 177 | _wrapped = value; 178 | } 179 | } 180 | 181 | /// Gets the traversal direction. 182 | /// One of the traversal direction enumeration values. 183 | // Token: 0x1700052B RID: 1323 184 | // (get) Token: 0x0600127E RID: 4734 RVA: 0x0012540C File Offset: 0x0012540C 185 | public bool FocusNavigationDirection => _focusNavigationDirection; 186 | 187 | // Token: 0x0400116B RID: 4459 188 | private bool _wrapped; 189 | 190 | // Token: 0x0400116C RID: 4460 191 | private bool _focusNavigationDirection; 192 | } 193 | internal class HwndHostAutomationPeer : FrameworkElementAutomationPeer { 194 | // Token: 0x060049D3 RID: 18899 RVA: 0x008C5954 File Offset: 0x008C5954 195 | public HwndHostAutomationPeer(HwndHost owner) 196 | : base(owner) { 197 | //base.IsInteropPeer = true; 198 | } 199 | 200 | // Token: 0x060049D4 RID: 18900 RVA: 0x008C5964 File Offset: 0x008C5964 201 | protected override string GetClassNameCore() { 202 | return "HwndHost"; 203 | } 204 | 205 | // Token: 0x060049D5 RID: 18901 RVA: 0x008C596C File Offset: 0x008C596C 206 | protected override AutomationControlType GetAutomationControlTypeCore() { 207 | return AutomationControlType.Pane; 208 | } 209 | 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/Microsoft.Terminal.WinUI3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0-windows10.0.19041.0 4 | 10.0.17763.0 5 | Microsoft.Terminal.WinUI3 6 | win-x86;win-x64;win-arm64 7 | true 8 | AnyCPU;x64 9 | CI.$(AssemblyName).Unofficial 10 | CollectNativePackContents 11 | MitchCapper 12 | https://github.com/MitchCapper/EasyWindowsTerminalControl 13 | 1.0.18-beta.1 14 | This is an unofficial WinUI3 recreation of the official Microsoft.Terminal.WPF package. Highly recommend using this with the "EasyWindowsTerminalControl.WinUI". 15 | ../Publish 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | MSBuild:Compile 40 | 41 | 42 | 43 | 44 | true 45 | 46 | 47 | 48 | Always 49 | runtimes 50 | 51 | 52 | 53 | 54 | MSBuild:Compile 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | true 71 | runtimes\win-x86\native\ 72 | 73 | 74 | true 75 | runtimes\win-x64\native\ 76 | 77 | 78 | true 79 | runtimes\win-arm64\native\ 80 | 81 | 82 | true 83 | lib\net8.0-windows10.0.19041\Microsoft.Terminal.WinUI3 84 | 85 | 86 | 87 | 88 | true 89 | build 90 | 91 | 92 | 93 | true 94 | buildTransitive 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | EnumChildWindows 2 | GetClassName 3 | GetFocus 4 | GetDpiForWindow 5 | WM_* 6 | SetWindowPos 7 | ShowWindowAsync 8 | EnableWindow 9 | SetParent 10 | GetParent 11 | GetWindowLong 12 | GetWindowThreadProcessId 13 | GetCurrentThreadId 14 | IsWindow 15 | GetWindowRect 16 | WINDOWPOS 17 | IsChild 18 | SystemParametersInfo 19 | IsChild 20 | WINDOW_STYLE 21 | ShowWindow 22 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/TerminalContainer.cs: -------------------------------------------------------------------------------- 1 | using Windows.Win32.Foundation; 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.Runtime.InteropServices; 7 | using Microsoft.UI.Xaml; 8 | using WinUIEx.Messaging; 9 | using Microsoft.UI.Xaml.Input; 10 | using System.Windows.Interop; 11 | using Microsoft.UI.Xaml.Automation.Peers; 12 | using Microsoft.Terminal.WinUI3.WPFImports; 13 | using Microsoft.Terminal.Wpf; 14 | 15 | namespace Microsoft.Terminal.WinUI3 { 16 | 17 | 18 | /// 19 | /// The container class that hosts the native hwnd terminal. 20 | /// 21 | /// 22 | /// This class is only left public since xaml cannot work with internal classes. 23 | /// 24 | internal class TerminalContainer : HwndHost { 25 | private ITerminalConnection connection; 26 | private HWND hwnd; 27 | private IntPtr terminal; 28 | private DispatcherTimer blinkTimer; 29 | private NativeMethods.ScrollCallback scrollCallback; 30 | private NativeMethods.WriteCallback writeCallback; 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | public TerminalContainer() { 36 | 37 | this.MessageHook += this.TerminalContainer_MessageHook; 38 | this.GettingFocus += TerminalContainer_GettingFocus; 39 | this.IsTabStop=false; 40 | 41 | var blinkTime = NativeMethods.GetCaretBlinkTime(); 42 | 43 | if (blinkTime == uint.MaxValue) { 44 | return; 45 | } 46 | 47 | this.blinkTimer = new DispatcherTimer(); 48 | this.blinkTimer.Interval = TimeSpan.FromMilliseconds(blinkTime); 49 | this.blinkTimer.Tick += (_, __) => { 50 | if (this.terminal != IntPtr.Zero) { 51 | NativeMethods.TerminalBlinkCursor(this.terminal); 52 | } 53 | }; 54 | } 55 | internal void PassFocus(){ 56 | NativeMethods.SetFocus(this.hwnd); 57 | } 58 | private void TerminalContainer_GettingFocus(UIElement sender, GettingFocusEventArgs args) { 59 | args.Handled=true; 60 | Debug.WriteLine($"TerminalContainer_GettingFocus setting to hwnd"); 61 | PassFocus(); 62 | } 63 | 64 | /// 65 | /// Event that is fired when the terminal buffer scrolls from text output. 66 | /// 67 | internal event EventHandler<(int viewTop, int viewHeight, int bufferSize)> TerminalScrolled; 68 | 69 | /// 70 | /// Event that is fired when the user engages in a mouse scroll over the terminal hwnd. 71 | /// 72 | internal event EventHandler UserScrolled; 73 | 74 | /// 75 | /// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control 76 | /// on user action. 77 | /// 78 | internal bool AutoResize { get; set; } = true; 79 | 80 | /// 81 | /// Gets or sets the size of the parent user control that hosts the terminal hwnd. 82 | /// 83 | /// Control size is in device independent units, but for simplicity all sizes should be scaled. 84 | internal Size TerminalControlSize { get; set; } 85 | 86 | /// 87 | /// Gets or sets the size of the terminal renderer. 88 | /// 89 | internal Size TerminalRendererSize { get; set; } 90 | 91 | /// 92 | /// Gets the current character rows available to the terminal. 93 | /// 94 | internal int Rows { get; private set; } 95 | 96 | /// 97 | /// Gets the current character columns available to the terminal. 98 | /// 99 | internal int Columns { get; private set; } 100 | 101 | /// 102 | /// Gets the window handle of the terminal. 103 | /// 104 | internal IntPtr Hwnd => this.hwnd; 105 | 106 | /// 107 | /// Sets the connection to the terminal backend. 108 | /// 109 | internal ITerminalConnection Connection { 110 | private get { 111 | return this.connection; 112 | } 113 | 114 | set { 115 | if (this.connection != null) { 116 | this.connection.TerminalOutput -= this.Connection_TerminalOutput; 117 | } 118 | 119 | this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x001bc\x1b]104\x1b\\")); // reset console/clear screen - https://github.com/microsoft/terminal/pull/15062#issuecomment-1505654110 120 | var wasNull = this.connection == null; 121 | this.connection = value; 122 | if (this.connection != null) { 123 | if (wasNull) { 124 | this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25h")); // show cursor 125 | } 126 | 127 | this.connection.TerminalOutput += this.Connection_TerminalOutput; 128 | this.connection.Start(); 129 | } else { 130 | this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25l")); // hide cursor 131 | } 132 | } 133 | } 134 | 135 | /// 136 | /// Manually invoke a scroll of the terminal buffer. 137 | /// 138 | /// The top line to show in the terminal. 139 | internal void UserScroll(int viewTop) { 140 | NativeMethods.TerminalUserScroll(this.terminal, viewTop); 141 | } 142 | 143 | /// 144 | /// Sets the theme for the terminal. This includes font family, size, color, as well as background and foreground colors. 145 | /// 146 | /// The color theme for the terminal to use. 147 | /// The font family to use in the terminal. 148 | /// The font size to use in the terminal. 149 | internal void SetTheme(TerminalTheme theme, string fontFamily, short fontSize) { 150 | var dpiScale = curDPI; 151 | 152 | NativeMethods.TerminalSetTheme(this.terminal, theme, fontFamily, fontSize, (int)dpiScale.PixelsPerInchX); 153 | 154 | // Validate before resizing that we have a non-zero size. 155 | if (!this.RenderSize.IsEmpty && !this.TerminalControlSize.IsEmpty 156 | && this.TerminalControlSize.Width != 0 && this.TerminalControlSize.Height != 0) { 157 | this.Resize(this.TerminalControlSize); 158 | } 159 | } 160 | 161 | /// 162 | /// Gets the selected text from the terminal renderer and clears the selection. 163 | /// 164 | /// The selected text, empty if no text is selected. 165 | internal string GetSelectedText() { 166 | if (NativeMethods.TerminalIsSelectionActive(this.terminal)) { 167 | return NativeMethods.TerminalGetSelection(this.terminal); 168 | } 169 | 170 | return string.Empty; 171 | } 172 | 173 | /// 174 | /// Triggers a resize of the terminal with the given size, redrawing the rendered text. 175 | /// 176 | /// Size of the rendering window. 177 | internal void Resize(Size renderSize) { 178 | if (renderSize.Width == 0 || renderSize.Height == 0) { 179 | throw new ArgumentException("Terminal column or row count cannot be 0.", nameof(renderSize)); 180 | } 181 | 182 | NativeMethods.TerminalTriggerResize( 183 | this.terminal, 184 | (int)renderSize.Width, 185 | (int)renderSize.Height, 186 | out NativeMethods.TilSize dimensions); 187 | 188 | this.Rows = dimensions.Y; 189 | this.Columns = dimensions.X; 190 | this.TerminalRendererSize = renderSize; 191 | 192 | this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); 193 | } 194 | 195 | /// 196 | /// Resizes the terminal using row and column count as the new size. 197 | /// 198 | /// Number of rows to show. 199 | /// Number of columns to show. 200 | internal void Resize(uint rows, uint columns) { 201 | if (rows == 0) { 202 | throw new ArgumentException("Terminal row count cannot be 0.", nameof(rows)); 203 | } 204 | 205 | if (columns == 0) { 206 | throw new ArgumentException("Terminal column count cannot be 0.", nameof(columns)); 207 | } 208 | 209 | NativeMethods.TilSize dimensions = new NativeMethods.TilSize { 210 | X = (int)columns, 211 | Y = (int)rows, 212 | }; 213 | 214 | NativeMethods.TerminalTriggerResizeWithDimension(this.terminal, dimensions, out var dimensionsInPixels); 215 | 216 | this.Columns = dimensions.X; 217 | this.Rows = dimensions.Y; 218 | 219 | this.TerminalRendererSize = new Size { 220 | Width = dimensionsInPixels.X, 221 | Height = dimensionsInPixels.Y, 222 | }; 223 | 224 | this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); 225 | } 226 | 227 | /// 228 | /// Calculates the rows and columns that would fit in the given size. 229 | /// 230 | /// DPI scaled size. 231 | /// Amount of rows and columns that would fit the given size. 232 | internal (int columns, int rows) CalculateRowsAndColumns(Size size) { 233 | NativeMethods.TerminalCalculateResize(this.terminal, (int)size.Width, (int)size.Height, out NativeMethods.TilSize dimensions); 234 | 235 | return (dimensions.X, dimensions.Y); 236 | } 237 | 238 | /// 239 | /// Triggers the terminal resize event if more space is available in the terminal control. 240 | /// 241 | internal void RaiseResizedIfDrawSpaceIncreased() { 242 | var (columns, rows) = this.CalculateRowsAndColumns(this.TerminalControlSize); 243 | 244 | if (this.Columns < columns || this.Rows < rows) { 245 | this.connection?.Resize((uint)rows, (uint)columns); 246 | } 247 | } 248 | 249 | /// 250 | /// WPF's HwndHost likes to mark the WM_GETOBJECT message as handled to 251 | /// force the usage of the WPF automation peer. We explicitly mark it as 252 | /// not handled and don't return an automation peer in "OnCreateAutomationPeer" below. 253 | /// This forces the message to go down to the HwndTerminal where we return terminal's UiaProvider. 254 | /// 255 | /// 256 | protected override void WndProc(WindowMessageEventArgs e) { 257 | if ((WindowsMessages)e.Message.MessageId == WindowsMessages.GETOBJECT) { 258 | e.Handled = false; 259 | return; 260 | } 261 | 262 | base.WndProc(e); 263 | } 264 | 265 | /// 266 | protected override AutomationPeer OnCreateAutomationPeer() { 267 | return null; 268 | } 269 | 270 | /// 271 | protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) { 272 | if (this.terminal != IntPtr.Zero) { 273 | NativeMethods.TerminalDpiChanged(this.terminal, (int)(NativeMethods.USER_DEFAULT_SCREEN_DPI * newDpi.DpiScaleX)); 274 | } 275 | } 276 | 277 | /// 278 | protected override HWND BuildWindowCore(HWND hwndParent) { 279 | var dpiScale = curDPI; 280 | NativeMethods.CreateTerminal(hwndParent, out var hostedHwnd, out this.terminal); 281 | this.hwnd =new( hostedHwnd); 282 | 283 | this.scrollCallback = this.OnScroll; 284 | this.writeCallback = this.OnWrite; 285 | 286 | NativeMethods.TerminalRegisterScrollCallback(this.terminal, this.scrollCallback); 287 | NativeMethods.TerminalRegisterWriteCallback(this.terminal, this.writeCallback); 288 | 289 | // If the saved DPI scale isn't the default scale, we push it to the terminal. 290 | if (dpiScale.PixelsPerInchX != NativeMethods.USER_DEFAULT_SCREEN_DPI) { 291 | NativeMethods.TerminalDpiChanged(this.terminal, (int)dpiScale.PixelsPerInchX); 292 | } 293 | 294 | if (NativeMethods.GetFocus() == this.hwnd) { 295 | this.blinkTimer?.Start(); 296 | } else { 297 | NativeMethods.TerminalSetCursorVisible(this.terminal, false); 298 | } 299 | 300 | return this.hwnd; 301 | } 302 | 303 | /// 304 | protected override void DestroyWindowCore(HWND hwnd) { 305 | NativeMethods.DestroyTerminal(this.terminal); 306 | this.terminal = IntPtr.Zero; 307 | } 308 | 309 | private static void UnpackKeyMessage(IntPtr wParam, IntPtr lParam, out ushort vkey, out ushort scanCode, out ushort flags) { 310 | ulong scanCodeAndFlags = ((ulong)lParam >> 16) & 0xFFFF; 311 | scanCode = (ushort)(scanCodeAndFlags & 0x00FFu); 312 | flags = (ushort)(scanCodeAndFlags & 0xFF00u); 313 | vkey = (ushort)wParam; 314 | } 315 | 316 | private static void UnpackCharMessage(IntPtr wParam, IntPtr lParam, out char character, out ushort scanCode, out ushort flags) { 317 | UnpackKeyMessage(wParam, lParam, out ushort vKey, out scanCode, out flags); 318 | character = (char)vKey; 319 | } 320 | 321 | 322 | private void TerminalContainer_MessageHook(object sender, WindowMessageEventArgs e) { 323 | var msg = e.Message; 324 | var hwnd = msg.Hwnd; 325 | var wParam = (nint)msg.WParam; 326 | var lParam = msg.LParam; 327 | if (hwnd == this.hwnd) { 328 | switch ((WindowsMessages)e.Message.MessageId) { 329 | case WindowsMessages.SETFOCUS: 330 | NativeMethods.TerminalSetFocus(this.terminal); 331 | this.blinkTimer?.Start(); 332 | break; 333 | case WindowsMessages.KILLFOCUS: 334 | NativeMethods.TerminalKillFocus(this.terminal); 335 | this.blinkTimer?.Stop(); 336 | NativeMethods.TerminalSetCursorVisible(this.terminal, false); 337 | break; 338 | case WindowsMessages.MOUSEACTIVATE: 339 | this.Focus(FocusState.Pointer); 340 | NativeMethods.SetFocus(this.hwnd); 341 | break; 342 | case WindowsMessages.SYSKEYDOWN: // fallthrough 343 | case WindowsMessages.KeyDown: { 344 | // WM_KEYDOWN lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown 345 | NativeMethods.TerminalSetCursorVisible(this.terminal, true); 346 | 347 | UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); 348 | NativeMethods.TerminalSendKeyEvent(this.terminal, vkey, scanCode, flags, true); 349 | this.blinkTimer?.Start(); 350 | break; 351 | } 352 | 353 | case WindowsMessages.SYSKEYUP: // fallthrough 354 | case WindowsMessages.KeyUp: { 355 | // WM_KEYUP lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup 356 | UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); 357 | NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, scanCode, flags, false); 358 | break; 359 | } 360 | 361 | case WindowsMessages.Char: { 362 | // WM_CHAR lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char 363 | UnpackCharMessage(wParam, lParam, out char character, out ushort scanCode, out ushort flags); 364 | NativeMethods.TerminalSendCharEvent(this.terminal, character, scanCode, flags); 365 | break; 366 | } 367 | 368 | case WindowsMessages.WINDOWPOSCHANGED: 369 | var windowpos = (NativeMethods.WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(NativeMethods.WINDOWPOS)); 370 | if (((NativeMethods.SetWindowPosFlags)windowpos.flags).HasFlag(NativeMethods.SetWindowPosFlags.SWP_NOSIZE) 371 | || (windowpos.cx == 0 && windowpos.cy == 0)) { 372 | break; 373 | } 374 | 375 | NativeMethods.TilSize dimensions; 376 | 377 | if (this.AutoResize) { 378 | NativeMethods.TerminalTriggerResize(this.terminal, windowpos.cx, windowpos.cy, out dimensions); 379 | 380 | this.Columns = dimensions.X; 381 | this.Rows = dimensions.Y; 382 | 383 | this.TerminalRendererSize = new Size { 384 | Width = windowpos.cx, 385 | Height = windowpos.cy, 386 | }; 387 | } else { 388 | // Calculate the new columns and rows that fit the total control size and alert the control to redraw the margins. 389 | NativeMethods.TerminalCalculateResize(this.terminal, (int)this.TerminalControlSize.Width, (int)this.TerminalControlSize.Height, out dimensions); 390 | } 391 | 392 | this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); 393 | break; 394 | 395 | case WindowsMessages.MOUSEWHEEL: 396 | var delta = (short)(((long)wParam) >> 16); 397 | this.UserScrolled?.Invoke(this, delta); 398 | break; 399 | } 400 | } 401 | 402 | //e.re IntPtr.Zero; 403 | } 404 | 405 | private void Connection_TerminalOutput(object sender, TerminalOutputEventArgs e) { 406 | if (this.terminal == IntPtr.Zero || string.IsNullOrEmpty(e.Data)) { 407 | return; 408 | } 409 | 410 | NativeMethods.TerminalSendOutput(this.terminal, e.Data); 411 | } 412 | 413 | private void OnScroll(int viewTop, int viewHeight, int bufferSize) { 414 | this.TerminalScrolled?.Invoke(this, (viewTop, viewHeight, bufferSize)); 415 | } 416 | 417 | private void OnWrite(string data) { 418 | this.Connection?.WriteInput(data); 419 | } 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/TerminalControl.xaml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/TerminalControl.xaml.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // 5 | using System; 6 | using System.Drawing; 7 | using System.Threading; 8 | 9 | 10 | using Microsoft.UI.Xaml; 11 | using Microsoft.UI.Xaml.Controls; 12 | using Microsoft.UI.Xaml.Media; 13 | 14 | using UIColor = Windows.UI.Color; 15 | using Microsoft.UI.Xaml.Input; 16 | using Microsoft.UI.Xaml.Automation.Peers; 17 | using Windows.Win32.UI.WindowsAndMessaging; 18 | using Windows.Win32; 19 | using Microsoft.Terminal.WinUI3; 20 | using System.Diagnostics; 21 | namespace Microsoft.Terminal.Wpf { 22 | 23 | 24 | using Task = System.Threading.Tasks.Task; 25 | 26 | /// 27 | /// A basic terminal control. This control can receive and render standard VT100 sequences. 28 | /// 29 | public partial class TerminalControl : UserControl { 30 | private int accumulatedDelta = 0; 31 | public TerminalControl() { 32 | 33 | this.InitializeComponent(); 34 | Init(); 35 | 36 | 37 | } 38 | 39 | private async void TerminalControl_GettingFocus(UIElement sender, GettingFocusEventArgs args) { 40 | args.Cancel=true; 41 | termContainer.PassFocus(); 42 | } 43 | 44 | private static int? _ScrollLines; 45 | private static unsafe int ScrollLines { 46 | get { 47 | if (_ScrollLines == null) { 48 | uint scrollLines; 49 | _ScrollLines = PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETWHEELSCROLLLINES, 0, &scrollLines, 0); 50 | _ScrollLines = (int)scrollLines; 51 | } 52 | return _ScrollLines.Value; 53 | } 54 | } 55 | 56 | 57 | private void Init() { 58 | 59 | this.termContainer.TerminalScrolled += this.TermControl_TerminalScrolled; 60 | this.termContainer.UserScrolled += this.TermControl_UserScrolled; 61 | this.scrollbar.Scroll += this.Scrollbar_Scroll; 62 | 63 | this.SizeChanged += this.TerminalControl_SizeChanged; 64 | 65 | this.PointerWheelChanged += MouseWheelChanged; 66 | 67 | this.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, OnVisibleChanged); 68 | IsTabStop=true; 69 | this.GettingFocus += TerminalControl_GettingFocus; 70 | } 71 | 72 | private void OnVisibleChanged(DependencyObject sender, DependencyProperty dp) { 73 | termContainer.Visibility = Visibility; 74 | } 75 | 76 | private void MouseWheelChanged(object sender, PointerRoutedEventArgs e) { 77 | var delta = e.GetCurrentPoint(this)?.Properties.MouseWheelDelta; 78 | if (delta == null) 79 | return; 80 | this.TermControl_UserScrolled(sender, delta.Value); 81 | } 82 | 83 | /// 84 | /// Gets the current character rows available to the terminal. 85 | /// 86 | public int Rows => this.termContainer.Rows; 87 | 88 | /// 89 | /// Gets the current character columns available to the terminal. 90 | /// 91 | public int Columns => this.termContainer.Columns; 92 | 93 | /// 94 | /// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control 95 | /// on user action. 96 | /// 97 | public bool AutoResize { 98 | get => this.termContainer.AutoResize; 99 | set => this.termContainer.AutoResize = value; 100 | } 101 | 102 | /// 103 | /// Sets the connection to a terminal backend. 104 | /// 105 | public ITerminalConnection Connection { 106 | set => this.termContainer.Connection = value; 107 | } 108 | 109 | /// 110 | /// Gets size of the terminal renderer. 111 | /// 112 | private Size TerminalRendererSize { 113 | get => this.termContainer.TerminalRendererSize; 114 | } 115 | private UIColor FromDrawingColor(Color color) => UIColor.FromArgb(color.A, color.R, color.G, color.B); 116 | /// 117 | /// Sets the theme for the terminal. This includes font family, size, color, as well as background and foreground colors. 118 | /// 119 | /// The color theme to use in the terminal. 120 | /// The font family to use in the terminal. 121 | /// The font size to use in the terminal. 122 | /// Color for the control background when the terminal window is smaller than the hosting WPF window. 123 | public void SetTheme(TerminalTheme theme, string fontFamily, short fontSize, Color externalBackground = default) { 124 | 125 | if (termContainer.Hwnd == 0) 126 | return; 127 | this.termContainer.SetTheme(theme, fontFamily, fontSize); 128 | 129 | // DefaultBackground uses Win32 COLORREF syntax which is BGR instead of RGB. 130 | byte b = Convert.ToByte((theme.DefaultBackground >> 16) & 0xff); 131 | byte g = Convert.ToByte((theme.DefaultBackground >> 8) & 0xff); 132 | byte r = Convert.ToByte(theme.DefaultBackground & 0xff); 133 | 134 | // Set the background color for the control only if one is provided. 135 | // This is only shown when the terminal renderer is smaller than the enclosing WPF window. 136 | if (externalBackground != default) { 137 | this.Background = new SolidColorBrush(FromDrawingColor(externalBackground)); 138 | } 139 | } 140 | 141 | /// 142 | /// Gets the selected text in the terminal, clearing the selection. Otherwise returns an empty string. 143 | /// 144 | /// Selected text, empty string if no content is selected. 145 | public string GetSelectedText() { 146 | return this.termContainer.GetSelectedText(); 147 | } 148 | 149 | /// 150 | /// Resizes the terminal to the specified rows and columns. 151 | /// 152 | /// Number of rows to display. 153 | /// Number of columns to display. 154 | /// Cancellation token for this task. 155 | /// A representing the asynchronous operation. 156 | public async Task ResizeAsync(uint rows, uint columns, CancellationToken cancellationToken) { 157 | this.termContainer.Resize(rows, columns); 158 | 159 | #pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs 160 | await RunAsync( 161 | new Action(delegate { 162 | this.terminalGrid.Margin = this.CalculateMargins(); 163 | }) 164 | ); 165 | #pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs 166 | } 167 | public Task RunAsync(Action action) => UWPHelpers.Enqueue(this.DispatcherQueue, action); 168 | public double DPIScale => termContainer.curDPI.DpiScaleX; 169 | /// 170 | /// Resizes the terminal to the specified dimensions. 171 | /// 172 | /// Rendering size for the terminal in device independent units. 173 | /// A tuple of (int, int) representing the number of rows and columns in the terminal. 174 | public (int rows, int columns) TriggerResize(Size rendersize) { 175 | rendersize.Width = (int)(DPIScale * rendersize.Width); 176 | rendersize.Height = (int)(DPIScale * rendersize.Height); 177 | 178 | if (rendersize.Width == 0 || rendersize.Height == 0) { 179 | return (0, 0); 180 | } 181 | 182 | this.termContainer.Resize(rendersize); 183 | 184 | return (this.Rows, this.Columns); 185 | } 186 | 187 | /// 188 | protected override AutomationPeer OnCreateAutomationPeer() { 189 | var peer = FrameworkElementAutomationPeer.FromElement(this); 190 | if (peer == null) { 191 | // Provide our own automation peer here that just sets IsContentElement/IsControlElement to false 192 | // (aka AccessibilityView = Raw). This makes it not pop up in the UIA tree. 193 | peer = new TermControlAutomationPeer(this); 194 | } 195 | 196 | return peer; 197 | } 198 | 199 | 200 | 201 | /// 202 | private void TerminalControl_SizeChanged(object sender, SizeChangedEventArgs sizeInfo) { 203 | 204 | var newSizeWidth = (sizeInfo.NewSize.Width - this.scrollbar.ActualWidth) * DPIScale; 205 | newSizeWidth = newSizeWidth < 0 ? 0 : newSizeWidth; 206 | 207 | var newSizeHeight = sizeInfo.NewSize.Height * DPIScale; 208 | newSizeHeight = newSizeHeight < 0 ? 0 : newSizeHeight; 209 | 210 | this.termContainer.TerminalControlSize = new Size { 211 | Width = (int)newSizeWidth, 212 | Height = (int)newSizeHeight, 213 | }; 214 | 215 | if (!this.AutoResize) { 216 | // Renderer will not resize on control resize. We have to manually calculate the margin to fill in the space. 217 | terminalGrid.Margin = CalculateMargins(new Size((int)sizeInfo.NewSize.Width, (int)sizeInfo.NewSize.Height)); 218 | 219 | // Margins stop resize events, therefore we have to manually check if more space is available and raise 220 | // a resize event if needed. 221 | this.termContainer.RaiseResizedIfDrawSpaceIncreased(); 222 | } 223 | 224 | //base.OnRenderSizeChanged(sizeInfo); 225 | } 226 | 227 | 228 | /// 229 | /// Calculates the margins that should surround the terminal renderer, if any. 230 | /// 231 | /// New size of the control. Uses the control's current size if not provided. 232 | /// The new terminal control margin thickness in device independent units. 233 | private Thickness CalculateMargins(Size controlSize = default) { 234 | double width = 0, height = 0; 235 | 236 | if (controlSize == default) { 237 | controlSize = new Size { 238 | Width = (int)this.terminalUserControl.ActualWidth, 239 | Height = (int)this.terminalUserControl.ActualHeight, 240 | }; 241 | } 242 | 243 | // During initialization, the terminal renderer size will be 0 and the terminal renderer 244 | // draws on all available space. Therefore no margins are needed until resized. 245 | if (this.TerminalRendererSize.Width != 0) { 246 | width = controlSize.Width - (this.TerminalRendererSize.Width / DPIScale); 247 | } 248 | 249 | if (this.TerminalRendererSize.Height != 0) { 250 | height = controlSize.Height - (this.TerminalRendererSize.Height / DPIScale); 251 | } 252 | 253 | width -= this.scrollbar.ActualWidth; 254 | 255 | // Prevent negative margin size. 256 | width = width < 0 ? 0 : width; 257 | height = height < 0 ? 0 : height; 258 | 259 | return new Thickness(0, 0, width, height); 260 | } 261 | 262 | 263 | private async void TermControl_UserScrolled(object sender, int delta) { 264 | 265 | var lineDelta = 120 / ScrollLines; 266 | this.accumulatedDelta += delta; 267 | 268 | if (this.accumulatedDelta < lineDelta && this.accumulatedDelta > -lineDelta) { 269 | return; 270 | } 271 | 272 | await RunAsync(() => { 273 | var lines = -this.accumulatedDelta / lineDelta; 274 | this.scrollbar.Value += lines; 275 | this.accumulatedDelta = 0; 276 | 277 | this.termContainer.UserScroll((int)this.scrollbar.Value); 278 | }); 279 | } 280 | 281 | private async void TermControl_TerminalScrolled(object sender, (int viewTop, int viewHeight, int bufferSize) e) { 282 | await RunAsync(() => { 283 | this.scrollbar.Minimum = 0; 284 | this.scrollbar.Maximum = e.bufferSize - e.viewHeight; 285 | this.scrollbar.Value = e.viewTop; 286 | this.scrollbar.ViewportSize = e.viewHeight; 287 | }); 288 | } 289 | private void Scrollbar_Scroll(object sender, UI.Xaml.Controls.Primitives.ScrollEventArgs e) { 290 | var viewTop = (int)e.NewValue; 291 | this.termContainer.UserScroll(viewTop); 292 | } 293 | 294 | private class TermControlAutomationPeer : FrameworkElementAutomationPeer { 295 | public TermControlAutomationPeer(UserControl owner) 296 | : base(owner) { 297 | } 298 | 299 | protected override bool IsContentElementCore() { 300 | return false; 301 | } 302 | 303 | protected override bool IsControlElementCore() { 304 | return false; 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/UWPHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.UI.Dispatching; 6 | using Windows.Win32; 7 | using Windows.Win32.Foundation; 8 | namespace Microsoft.Terminal.WinUI3 { 9 | public static class UWPHelpers { 10 | internal class WindowChildEnumerator { 11 | protected List all = new(); 12 | protected BOOL Callback(HWND hWnd, LPARAM lparam) { 13 | all.Add(hWnd); 14 | return true; 15 | } 16 | public WindowChildEnumerator(HWND parent) => PInvoke.EnumChildWindows(parent, Callback, 0); 17 | 18 | public IEnumerable Result => all; 19 | } 20 | internal static unsafe string GetClassName(HWND hwnd) { 21 | var className = stackalloc char[256]; 22 | var count = PInvoke.GetClassName(hwnd, className, 256); 23 | return new string(className, 0, count); 24 | } 25 | 26 | /// 27 | /// Gets the window that actually handles all window messages for that Winui3 Window 28 | /// 29 | /// 30 | /// 31 | public static IntPtr GetInputHwnd(IntPtr rootHwnd) { 32 | return new WindowChildEnumerator(new(rootHwnd)).Result.FirstOrDefault(win => GetClassName(win) == "InputSiteWindowClass"); 33 | } 34 | public static Task Enqueue(this DispatcherQueue dispatcher, Action action, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal) { 35 | try { 36 | if (dispatcher.HasThreadAccess) { 37 | action(); 38 | return Task.CompletedTask; 39 | } 40 | var tcs = new TaskCompletionSource(); 41 | dispatcher.TryEnqueue(priority, () => { 42 | try { 43 | action(); 44 | tcs.SetResult(null); 45 | } catch (Exception ex) { 46 | tcs.SetException(ex); 47 | } 48 | }); 49 | return tcs.Task; 50 | } catch (Exception ex) { 51 | return Task.FromException(ex); 52 | } 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/WindowsMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.Win32; 3 | 4 | namespace WinUIEx.Messaging; 5 | 6 | [Flags] 7 | public enum WindowsMessages : uint //making this public so we can use it 8 | { 9 | DDE_FIRST = PInvoke.WM_DDE_FIRST, 10 | DDE_INITIATE = PInvoke.WM_DDE_INITIATE, 11 | DDE_TERMINATE = PInvoke.WM_DDE_TERMINATE, 12 | DDE_ADVISE = PInvoke.WM_DDE_ADVISE, 13 | DDE_UNADVISE = PInvoke.WM_DDE_UNADVISE, 14 | DDE_ACK = PInvoke.WM_DDE_ACK, 15 | DDE_DATA = PInvoke.WM_DDE_DATA, 16 | DDE_REQUEST = PInvoke.WM_DDE_REQUEST, 17 | DDE_POKE = PInvoke.WM_DDE_POKE, 18 | DDE_EXECUTE = PInvoke.WM_DDE_EXECUTE, 19 | DDE_LAST = PInvoke.WM_DDE_LAST, 20 | IME_REPORT = PInvoke.WM_IME_REPORT, 21 | WNT_CONVERTREQUESTEX = PInvoke.WM_WNT_CONVERTREQUESTEX, 22 | CONVERTREQUEST = PInvoke.WM_CONVERTREQUEST, 23 | CONVERTRESULT = PInvoke.WM_CONVERTRESULT, 24 | INTERIM = PInvoke.WM_INTERIM, 25 | IMEKEYDOWN = PInvoke.WM_IMEKEYDOWN, 26 | IMEKEYUP = PInvoke.WM_IMEKEYUP, 27 | CTLCOLOR = PInvoke.WM_CTLCOLOR, 28 | MOUSEHOVER = PInvoke.WM_MOUSEHOVER, 29 | MOUSELEAVE = PInvoke.WM_MOUSELEAVE, 30 | CHOOSEFONT_GETLOGFONT = PInvoke.WM_CHOOSEFONT_GETLOGFONT, 31 | CHOOSEFONT_SETLOGFONT = PInvoke.WM_CHOOSEFONT_SETLOGFONT, 32 | CHOOSEFONT_SETFLAGS = PInvoke.WM_CHOOSEFONT_SETFLAGS, 33 | PSD_FULLPAGERECT = PInvoke.WM_PSD_FULLPAGERECT, 34 | PSD_MINMARGINRECT = PInvoke.WM_PSD_MINMARGINRECT, 35 | PSD_MARGINRECT = PInvoke.WM_PSD_MARGINRECT, 36 | PSD_GREEKTEXTRECT = PInvoke.WM_PSD_GREEKTEXTRECT, 37 | PSD_ENVSTAMPRECT = PInvoke.WM_PSD_ENVSTAMPRECT, 38 | PSD_YAFULLPAGERECT = PInvoke.WM_PSD_YAFULLPAGERECT, 39 | CPL_LAUNCH = PInvoke.WM_CPL_LAUNCH, 40 | CPL_LAUNCHED = PInvoke.WM_CPL_LAUNCHED, 41 | TABLET_DEFBASE = PInvoke.WM_TABLET_DEFBASE, 42 | TABLET_MAXOFFSET = PInvoke.WM_TABLET_MAXOFFSET, 43 | TABLET_ADDED = PInvoke.WM_TABLET_ADDED, 44 | TABLET_DELETED = PInvoke.WM_TABLET_DELETED, 45 | TABLET_FLICK = PInvoke.WM_TABLET_FLICK, 46 | TABLET_QUERYSYSTEMGESTURESTATUS = PInvoke.WM_TABLET_QUERYSYSTEMGESTURESTATUS, 47 | CONTEXTMENU = PInvoke.WM_CONTEXTMENU, 48 | UNICHAR = PInvoke.WM_UNICHAR, 49 | PRINTCLIENT = PInvoke.WM_PRINTCLIENT, 50 | NOTIFY = PInvoke.WM_NOTIFY, 51 | DEVICECHANGE = PInvoke.WM_DEVICECHANGE, 52 | NULL = PInvoke.WM_NULL, 53 | CREATE = PInvoke.WM_CREATE, 54 | DESTROY = PInvoke.WM_DESTROY, 55 | MOVE = PInvoke.WM_MOVE, 56 | SIZE = PInvoke.WM_SIZE, 57 | Activate = PInvoke.WM_ACTIVATE, 58 | SETFOCUS = PInvoke.WM_SETFOCUS, 59 | KILLFOCUS = PInvoke.WM_KILLFOCUS, 60 | ENABLE = PInvoke.WM_ENABLE, 61 | SETREDRAW = PInvoke.WM_SETREDRAW, 62 | SETTEXT = PInvoke.WM_SETTEXT, 63 | GETTEXT = PInvoke.WM_GETTEXT, 64 | GETTEXTLENGTH = PInvoke.WM_GETTEXTLENGTH, 65 | PAINT = PInvoke.WM_PAINT, 66 | Close = PInvoke.WM_CLOSE, 67 | QUERYENDSESSION = PInvoke.WM_QUERYENDSESSION, 68 | QUERYOPEN = PInvoke.WM_QUERYOPEN, 69 | ENDSESSION = PInvoke.WM_ENDSESSION, 70 | QUIT = PInvoke.WM_QUIT, 71 | EarseBackground = PInvoke.WM_ERASEBKGND, 72 | SYSCOLORCHANGE = PInvoke.WM_SYSCOLORCHANGE, 73 | SHOWWINDOW = PInvoke.WM_SHOWWINDOW, 74 | WININICHANGE = PInvoke.WM_WININICHANGE, 75 | SETTINGCHANGE = PInvoke.WM_SETTINGCHANGE, 76 | DEVMODECHANGE = PInvoke.WM_DEVMODECHANGE, 77 | ACTIVATEAPP = PInvoke.WM_ACTIVATEAPP, 78 | FONTCHANGE = PInvoke.WM_FONTCHANGE, 79 | TIMECHANGE = PInvoke.WM_TIMECHANGE, 80 | CANCELMODE = PInvoke.WM_CANCELMODE, 81 | SETCURSOR = PInvoke.WM_SETCURSOR, 82 | MOUSEACTIVATE = PInvoke.WM_MOUSEACTIVATE, 83 | CHILDACTIVATE = PInvoke.WM_CHILDACTIVATE, 84 | QUEUESYNC = PInvoke.WM_QUEUESYNC, 85 | GETMINMAXINFO = PInvoke.WM_GETMINMAXINFO, 86 | PAINTICON = PInvoke.WM_PAINTICON, 87 | ICONERASEBKGND = PInvoke.WM_ICONERASEBKGND, 88 | NEXTDLGCTL = PInvoke.WM_NEXTDLGCTL, 89 | SPOOLERSTATUS = PInvoke.WM_SPOOLERSTATUS, 90 | DRAWITEM = PInvoke.WM_DRAWITEM, 91 | MEASUREITEM = PInvoke.WM_MEASUREITEM, 92 | DELETEITEM = PInvoke.WM_DELETEITEM, 93 | VKEYTOITEM = PInvoke.WM_VKEYTOITEM, 94 | CHARTOITEM = PInvoke.WM_CHARTOITEM, 95 | SETFONT = PInvoke.WM_SETFONT, 96 | GETFONT = PInvoke.WM_GETFONT, 97 | SETHOTKEY = PInvoke.WM_SETHOTKEY, 98 | GETHOTKEY = PInvoke.WM_GETHOTKEY, 99 | QUERYDRAGICON = PInvoke.WM_QUERYDRAGICON, 100 | COMPAREITEM = PInvoke.WM_COMPAREITEM, 101 | GETOBJECT = PInvoke.WM_GETOBJECT, 102 | COMPACTING = PInvoke.WM_COMPACTING, 103 | COMMNOTIFY = PInvoke.WM_COMMNOTIFY, 104 | WINDOWPOSCHANGING = PInvoke.WM_WINDOWPOSCHANGING, 105 | WINDOWPOSCHANGED = PInvoke.WM_WINDOWPOSCHANGED, 106 | POWER = PInvoke.WM_POWER, 107 | COPYDATA = PInvoke.WM_COPYDATA, 108 | CANCELJOURNAL = PInvoke.WM_CANCELJOURNAL, 109 | INPUTLANGCHANGEREQUEST = PInvoke.WM_INPUTLANGCHANGEREQUEST, 110 | INPUTLANGCHANGE = PInvoke.WM_INPUTLANGCHANGE, 111 | TCARD = PInvoke.WM_TCARD, 112 | HELP = PInvoke.WM_HELP, 113 | USERCHANGED = PInvoke.WM_USERCHANGED, 114 | NOTIFYFORMAT = PInvoke.WM_NOTIFYFORMAT, 115 | STYLECHANGING = PInvoke.WM_STYLECHANGING, 116 | STYLECHANGED = PInvoke.WM_STYLECHANGED, 117 | DISPLAYCHANGE = PInvoke.WM_DISPLAYCHANGE, 118 | GetIcon = PInvoke.WM_GETICON, 119 | SetIcon = PInvoke.WM_SETICON, 120 | NCCREATE = PInvoke.WM_NCCREATE, 121 | NCDESTROY = PInvoke.WM_NCDESTROY, 122 | NCCALCSIZE = PInvoke.WM_NCCALCSIZE, 123 | NCHITTEST = PInvoke.WM_NCHITTEST, 124 | NCPAINT = PInvoke.WM_NCPAINT, 125 | NCACTIVATE = PInvoke.WM_NCACTIVATE, 126 | GETDLGCODE = PInvoke.WM_GETDLGCODE, 127 | SYNCPAINT = PInvoke.WM_SYNCPAINT, 128 | NCMOUSEMOVE = PInvoke.WM_NCMOUSEMOVE, 129 | NCLBUTTONDOWN = PInvoke.WM_NCLBUTTONDOWN, 130 | NCLBUTTONUP = PInvoke.WM_NCLBUTTONUP, 131 | NCLBUTTONDBLCLK = PInvoke.WM_NCLBUTTONDBLCLK, 132 | NCRBUTTONDOWN = PInvoke.WM_NCRBUTTONDOWN, 133 | NCRBUTTONUP = PInvoke.WM_NCRBUTTONUP, 134 | NCRBUTTONDBLCLK = PInvoke.WM_NCRBUTTONDBLCLK, 135 | NCMBUTTONDOWN = PInvoke.WM_NCMBUTTONDOWN, 136 | NCMBUTTONUP = PInvoke.WM_NCMBUTTONUP, 137 | NCMBUTTONDBLCLK = PInvoke.WM_NCMBUTTONDBLCLK, 138 | NCXBUTTONDOWN = PInvoke.WM_NCXBUTTONDOWN, 139 | NCXBUTTONUP = PInvoke.WM_NCXBUTTONUP, 140 | NCXBUTTONDBLCLK = PInvoke.WM_NCXBUTTONDBLCLK, 141 | INPUT_DEVICE_CHANGE = PInvoke.WM_INPUT_DEVICE_CHANGE, 142 | INPUT = PInvoke.WM_INPUT, 143 | KEYFIRST = PInvoke.WM_KEYFIRST, 144 | KeyDown = PInvoke.WM_KEYDOWN, 145 | KeyUp = PInvoke.WM_KEYUP, 146 | Char = PInvoke.WM_CHAR, 147 | DEADCHAR = PInvoke.WM_DEADCHAR, 148 | SYSKEYDOWN = PInvoke.WM_SYSKEYDOWN, 149 | SYSKEYUP = PInvoke.WM_SYSKEYUP, 150 | SYSCHAR = PInvoke.WM_SYSCHAR, 151 | SYSDEADCHAR = PInvoke.WM_SYSDEADCHAR, 152 | KEYLAST = PInvoke.WM_KEYLAST, 153 | IME_STARTCOMPOSITION = PInvoke.WM_IME_STARTCOMPOSITION, 154 | IME_ENDCOMPOSITION = PInvoke.WM_IME_ENDCOMPOSITION, 155 | IME_COMPOSITION = PInvoke.WM_IME_COMPOSITION, 156 | IME_KEYLAST = PInvoke.WM_IME_KEYLAST, 157 | INITDIALOG = PInvoke.WM_INITDIALOG, 158 | Command = PInvoke.WM_COMMAND, 159 | SysCommand = PInvoke.WM_SYSCOMMAND, 160 | TIMER = PInvoke.WM_TIMER, 161 | HSCROLL = PInvoke.WM_HSCROLL, 162 | VSCROLL = PInvoke.WM_VSCROLL, 163 | INITMENU = PInvoke.WM_INITMENU, 164 | INITMENUPOPUP = PInvoke.WM_INITMENUPOPUP, 165 | GESTURE = PInvoke.WM_GESTURE, 166 | GESTURENOTIFY = PInvoke.WM_GESTURENOTIFY, 167 | MENUSELECT = PInvoke.WM_MENUSELECT, 168 | MENUCHAR = PInvoke.WM_MENUCHAR, 169 | ENTERIDLE = PInvoke.WM_ENTERIDLE, 170 | MENURBUTTONUP = PInvoke.WM_MENURBUTTONUP, 171 | MENUDRAG = PInvoke.WM_MENUDRAG, 172 | MENUGETOBJECT = PInvoke.WM_MENUGETOBJECT, 173 | UNINITMENUPOPUP = PInvoke.WM_UNINITMENUPOPUP, 174 | MENUCOMMAND = PInvoke.WM_MENUCOMMAND, 175 | CHANGEUISTATE = PInvoke.WM_CHANGEUISTATE, 176 | UPDATEUISTATE = PInvoke.WM_UPDATEUISTATE, 177 | QUERYUISTATE = PInvoke.WM_QUERYUISTATE, 178 | CTLCOLORMSGBOX = PInvoke.WM_CTLCOLORMSGBOX, 179 | CTLCOLOREDIT = PInvoke.WM_CTLCOLOREDIT, 180 | CTLCOLORLISTBOX = PInvoke.WM_CTLCOLORLISTBOX, 181 | CTLCOLORBTN = PInvoke.WM_CTLCOLORBTN, 182 | CTLCOLORDLG = PInvoke.WM_CTLCOLORDLG, 183 | CTLCOLORSCROLLBAR = PInvoke.WM_CTLCOLORSCROLLBAR, 184 | CTLCOLORSTATIC = PInvoke.WM_CTLCOLORSTATIC, 185 | MOUSEFIRST = PInvoke.WM_MOUSEFIRST, 186 | MOUSEMOVE = PInvoke.WM_MOUSEMOVE, 187 | LBUTTONDOWN = PInvoke.WM_LBUTTONDOWN, 188 | MouseLeftButtonUp = PInvoke.WM_LBUTTONUP, 189 | LBUTTONDBLCLK = PInvoke.WM_LBUTTONDBLCLK, 190 | RBUTTONDOWN = PInvoke.WM_RBUTTONDOWN, 191 | MouseRightButtonUp = PInvoke.WM_RBUTTONUP, 192 | RBUTTONDBLCLK = PInvoke.WM_RBUTTONDBLCLK, 193 | MBUTTONDOWN = PInvoke.WM_MBUTTONDOWN, 194 | MBUTTONUP = PInvoke.WM_MBUTTONUP, 195 | MBUTTONDBLCLK = PInvoke.WM_MBUTTONDBLCLK, 196 | MOUSEWHEEL = PInvoke.WM_MOUSEWHEEL, 197 | XBUTTONDOWN = PInvoke.WM_XBUTTONDOWN, 198 | XBUTTONUP = PInvoke.WM_XBUTTONUP, 199 | XBUTTONDBLCLK = PInvoke.WM_XBUTTONDBLCLK, 200 | MOUSEHWHEEL = PInvoke.WM_MOUSEHWHEEL, 201 | MOUSELAST = PInvoke.WM_MOUSELAST, 202 | PARENTNOTIFY = PInvoke.WM_PARENTNOTIFY, 203 | ENTERMENULOOP = PInvoke.WM_ENTERMENULOOP, 204 | EXITMENULOOP = PInvoke.WM_EXITMENULOOP, 205 | NEXTMENU = PInvoke.WM_NEXTMENU, 206 | SIZING = PInvoke.WM_SIZING, 207 | CAPTURECHANGED = PInvoke.WM_CAPTURECHANGED, 208 | MOVING = PInvoke.WM_MOVING, 209 | POWERBROADCAST = PInvoke.WM_POWERBROADCAST, 210 | MDICREATE = PInvoke.WM_MDICREATE, 211 | MDIDESTROY = PInvoke.WM_MDIDESTROY, 212 | MDIACTIVATE = PInvoke.WM_MDIACTIVATE, 213 | MDIRESTORE = PInvoke.WM_MDIRESTORE, 214 | MDINEXT = PInvoke.WM_MDINEXT, 215 | MDIMAXIMIZE = PInvoke.WM_MDIMAXIMIZE, 216 | MDITILE = PInvoke.WM_MDITILE, 217 | MDICASCADE = PInvoke.WM_MDICASCADE, 218 | MDIICONARRANGE = PInvoke.WM_MDIICONARRANGE, 219 | MDIGETACTIVE = PInvoke.WM_MDIGETACTIVE, 220 | MDISETMENU = PInvoke.WM_MDISETMENU, 221 | ENTERSIZEMOVE = PInvoke.WM_ENTERSIZEMOVE, 222 | EXITSIZEMOVE = PInvoke.WM_EXITSIZEMOVE, 223 | DROPFILES = PInvoke.WM_DROPFILES, 224 | MDIREFRESHMENU = PInvoke.WM_MDIREFRESHMENU, 225 | POINTERDEVICECHANGE = PInvoke.WM_POINTERDEVICECHANGE, 226 | POINTERDEVICEINRANGE = PInvoke.WM_POINTERDEVICEINRANGE, 227 | POINTERDEVICEOUTOFRANGE = PInvoke.WM_POINTERDEVICEOUTOFRANGE, 228 | TOUCH = PInvoke.WM_TOUCH, 229 | NCPOINTERUPDATE = PInvoke.WM_NCPOINTERUPDATE, 230 | NCPOINTERDOWN = PInvoke.WM_NCPOINTERDOWN, 231 | NCPOINTERUP = PInvoke.WM_NCPOINTERUP, 232 | POINTERUPDATE = PInvoke.WM_POINTERUPDATE, 233 | POINTERDOWN = PInvoke.WM_POINTERDOWN, 234 | POINTERUP = PInvoke.WM_POINTERUP, 235 | POINTERENTER = PInvoke.WM_POINTERENTER, 236 | POINTERLEAVE = PInvoke.WM_POINTERLEAVE, 237 | POINTERACTIVATE = PInvoke.WM_POINTERACTIVATE, 238 | POINTERCAPTURECHANGED = PInvoke.WM_POINTERCAPTURECHANGED, 239 | TOUCHHITTESTING = PInvoke.WM_TOUCHHITTESTING, 240 | POINTERWHEEL = PInvoke.WM_POINTERWHEEL, 241 | POINTERHWHEEL = PInvoke.WM_POINTERHWHEEL, 242 | POINTERROUTEDTO = PInvoke.WM_POINTERROUTEDTO, 243 | POINTERROUTEDAWAY = PInvoke.WM_POINTERROUTEDAWAY, 244 | POINTERROUTEDRELEASED = PInvoke.WM_POINTERROUTEDRELEASED, 245 | IME_SETCONTEXT = PInvoke.WM_IME_SETCONTEXT, 246 | IME_NOTIFY = PInvoke.WM_IME_NOTIFY, 247 | IME_CONTROL = PInvoke.WM_IME_CONTROL, 248 | IME_COMPOSITIONFULL = PInvoke.WM_IME_COMPOSITIONFULL, 249 | IME_SELECT = PInvoke.WM_IME_SELECT, 250 | IME_CHAR = PInvoke.WM_IME_CHAR, 251 | IME_REQUEST = PInvoke.WM_IME_REQUEST, 252 | IME_KEYDOWN = PInvoke.WM_IME_KEYDOWN, 253 | IME_KEYUP = PInvoke.WM_IME_KEYUP, 254 | NCMOUSEHOVER = PInvoke.WM_NCMOUSEHOVER, 255 | NCMOUSELEAVE = PInvoke.WM_NCMOUSELEAVE, 256 | WTSSESSION_CHANGE = PInvoke.WM_WTSSESSION_CHANGE, 257 | TABLET_FIRST = PInvoke.WM_TABLET_FIRST, 258 | TABLET_LAST = PInvoke.WM_TABLET_LAST, 259 | DPICHANGED = PInvoke.WM_DPICHANGED, 260 | DPICHANGED_BEFOREPARENT = PInvoke.WM_DPICHANGED_BEFOREPARENT, 261 | DPICHANGED_AFTERPARENT = PInvoke.WM_DPICHANGED_AFTERPARENT, 262 | GETDPISCALEDSIZE = PInvoke.WM_GETDPISCALEDSIZE, 263 | CUT = PInvoke.WM_CUT, 264 | COPY = PInvoke.WM_COPY, 265 | PASTE = PInvoke.WM_PASTE, 266 | CLEAR = PInvoke.WM_CLEAR, 267 | UNDO = PInvoke.WM_UNDO, 268 | RENDERFORMAT = PInvoke.WM_RENDERFORMAT, 269 | RENDERALLFORMATS = PInvoke.WM_RENDERALLFORMATS, 270 | DESTROYCLIPBOARD = PInvoke.WM_DESTROYCLIPBOARD, 271 | DRAWCLIPBOARD = PInvoke.WM_DRAWCLIPBOARD, 272 | PAINTCLIPBOARD = PInvoke.WM_PAINTCLIPBOARD, 273 | VSCROLLCLIPBOARD = PInvoke.WM_VSCROLLCLIPBOARD, 274 | SIZECLIPBOARD = PInvoke.WM_SIZECLIPBOARD, 275 | ASKCBFORMATNAME = PInvoke.WM_ASKCBFORMATNAME, 276 | CHANGECBCHAIN = PInvoke.WM_CHANGECBCHAIN, 277 | HSCROLLCLIPBOARD = PInvoke.WM_HSCROLLCLIPBOARD, 278 | QUERYNEWPALETTE = PInvoke.WM_QUERYNEWPALETTE, 279 | PALETTEISCHANGING = PInvoke.WM_PALETTEISCHANGING, 280 | PALETTECHANGED = PInvoke.WM_PALETTECHANGED, 281 | HOTKEY = PInvoke.WM_HOTKEY, 282 | PRINT = PInvoke.WM_PRINT, 283 | APPCOMMAND = PInvoke.WM_APPCOMMAND, 284 | THEMECHANGED = PInvoke.WM_THEMECHANGED, 285 | CLIPBOARDUPDATE = PInvoke.WM_CLIPBOARDUPDATE, 286 | DWMCOMPOSITIONCHANGED = PInvoke.WM_DWMCOMPOSITIONCHANGED, 287 | DWMNCRENDERINGCHANGED = PInvoke.WM_DWMNCRENDERINGCHANGED, 288 | DWMCOLORIZATIONCOLORCHANGED = PInvoke.WM_DWMCOLORIZATIONCOLORCHANGED, 289 | DWMWINDOWMAXIMIZEDCHANGE = PInvoke.WM_DWMWINDOWMAXIMIZEDCHANGE, 290 | DwmSendIconICThumbnail = PInvoke.WM_DWMSENDICONICTHUMBNAIL, 291 | DwmSendIconICLivePreviewBitmap = PInvoke.WM_DWMSENDICONICLIVEPREVIEWBITMAP, 292 | GETTITLEBARINFOEX = PInvoke.WM_GETTITLEBARINFOEX, 293 | HANDHELDFIRST = PInvoke.WM_HANDHELDFIRST, 294 | HANDHELDLAST = PInvoke.WM_HANDHELDLAST, 295 | AFXFIRST = PInvoke.WM_AFXFIRST, 296 | AFXLAST = PInvoke.WM_AFXLAST, 297 | PENWINFIRST = PInvoke.WM_PENWINFIRST, 298 | PENWINLAST = PInvoke.WM_PENWINLAST, 299 | APP = PInvoke.WM_APP, 300 | USER = PInvoke.WM_USER, 301 | TOOLTIPDISMISS = PInvoke.WM_TOOLTIPDISMISS, 302 | FI_FILENAME = PInvoke.WM_FI_FILENAME, 303 | CODEC_ONEPASS_CBR = PInvoke.WM_CODEC_ONEPASS_CBR, 304 | CODEC_ONEPASS_VBR = PInvoke.WM_CODEC_ONEPASS_VBR, 305 | CODEC_TWOPASS_CBR = PInvoke.WM_CODEC_TWOPASS_CBR, 306 | CODEC_TWOPASS_VBR_UNCONSTRAINED = PInvoke.WM_CODEC_TWOPASS_VBR_UNCONSTRAINED, 307 | CODEC_TWOPASS_VBR_PEAKCONSTRAINED = PInvoke.WM_CODEC_TWOPASS_VBR_PEAKCONSTRAINED, 308 | CAP_START = PInvoke.WM_CAP_START, 309 | CAP_UNICODE_START = PInvoke.WM_CAP_UNICODE_START, 310 | CAP_GET_CAPSTREAMPTR = PInvoke.WM_CAP_GET_CAPSTREAMPTR, 311 | CAP_SET_CALLBACK_ERRORW = PInvoke.WM_CAP_SET_CALLBACK_ERRORW, 312 | CAP_SET_CALLBACK_STATUSW = PInvoke.WM_CAP_SET_CALLBACK_STATUSW, 313 | CAP_SET_CALLBACK_ERRORA = PInvoke.WM_CAP_SET_CALLBACK_ERRORA, 314 | CAP_SET_CALLBACK_STATUSA = PInvoke.WM_CAP_SET_CALLBACK_STATUSA, 315 | CAP_SET_CALLBACK_ERROR = PInvoke.WM_CAP_SET_CALLBACK_ERROR, 316 | CAP_SET_CALLBACK_STATUS = PInvoke.WM_CAP_SET_CALLBACK_STATUS, 317 | CAP_SET_CALLBACK_YIELD = PInvoke.WM_CAP_SET_CALLBACK_YIELD, 318 | CAP_SET_CALLBACK_FRAME = PInvoke.WM_CAP_SET_CALLBACK_FRAME, 319 | CAP_SET_CALLBACK_VIDEOSTREAM = PInvoke.WM_CAP_SET_CALLBACK_VIDEOSTREAM, 320 | CAP_SET_CALLBACK_WAVESTREAM = PInvoke.WM_CAP_SET_CALLBACK_WAVESTREAM, 321 | CAP_GET_USER_DATA = PInvoke.WM_CAP_GET_USER_DATA, 322 | CAP_SET_USER_DATA = PInvoke.WM_CAP_SET_USER_DATA, 323 | CAP_DRIVER_CONNECT = PInvoke.WM_CAP_DRIVER_CONNECT, 324 | CAP_DRIVER_DISCONNECT = PInvoke.WM_CAP_DRIVER_DISCONNECT, 325 | CAP_DRIVER_GET_NAMEA = PInvoke.WM_CAP_DRIVER_GET_NAMEA, 326 | CAP_DRIVER_GET_VERSIONA = PInvoke.WM_CAP_DRIVER_GET_VERSIONA, 327 | CAP_DRIVER_GET_NAMEW = PInvoke.WM_CAP_DRIVER_GET_NAMEW, 328 | CAP_DRIVER_GET_VERSIONW = PInvoke.WM_CAP_DRIVER_GET_VERSIONW, 329 | CAP_DRIVER_GET_NAME = PInvoke.WM_CAP_DRIVER_GET_NAME, 330 | CAP_DRIVER_GET_VERSION = PInvoke.WM_CAP_DRIVER_GET_VERSION, 331 | CAP_DRIVER_GET_CAPS = PInvoke.WM_CAP_DRIVER_GET_CAPS, 332 | CAP_FILE_SET_CAPTURE_FILEA = PInvoke.WM_CAP_FILE_SET_CAPTURE_FILEA, 333 | CAP_FILE_GET_CAPTURE_FILEA = PInvoke.WM_CAP_FILE_GET_CAPTURE_FILEA, 334 | CAP_FILE_SAVEASA = PInvoke.WM_CAP_FILE_SAVEASA, 335 | CAP_FILE_SAVEDIBA = PInvoke.WM_CAP_FILE_SAVEDIBA, 336 | CAP_FILE_SET_CAPTURE_FILEW = PInvoke.WM_CAP_FILE_SET_CAPTURE_FILEW, 337 | CAP_FILE_GET_CAPTURE_FILEW = PInvoke.WM_CAP_FILE_GET_CAPTURE_FILEW, 338 | CAP_FILE_SAVEASW = PInvoke.WM_CAP_FILE_SAVEASW, 339 | CAP_FILE_SAVEDIBW = PInvoke.WM_CAP_FILE_SAVEDIBW, 340 | CAP_FILE_SET_CAPTURE_FILE = PInvoke.WM_CAP_FILE_SET_CAPTURE_FILE, 341 | CAP_FILE_GET_CAPTURE_FILE = PInvoke.WM_CAP_FILE_GET_CAPTURE_FILE, 342 | CAP_FILE_SAVEAS = PInvoke.WM_CAP_FILE_SAVEAS, 343 | CAP_FILE_SAVEDIB = PInvoke.WM_CAP_FILE_SAVEDIB, 344 | CAP_FILE_ALLOCATE = PInvoke.WM_CAP_FILE_ALLOCATE, 345 | CAP_FILE_SET_INFOCHUNK = PInvoke.WM_CAP_FILE_SET_INFOCHUNK, 346 | CAP_EDIT_COPY = PInvoke.WM_CAP_EDIT_COPY, 347 | CAP_SET_AUDIOFORMAT = PInvoke.WM_CAP_SET_AUDIOFORMAT, 348 | CAP_GET_AUDIOFORMAT = PInvoke.WM_CAP_GET_AUDIOFORMAT, 349 | CAP_DLG_VIDEOFORMAT = PInvoke.WM_CAP_DLG_VIDEOFORMAT, 350 | CAP_DLG_VIDEOSOURCE = PInvoke.WM_CAP_DLG_VIDEOSOURCE, 351 | CAP_DLG_VIDEODISPLAY = PInvoke.WM_CAP_DLG_VIDEODISPLAY, 352 | CAP_GET_VIDEOFORMAT = PInvoke.WM_CAP_GET_VIDEOFORMAT, 353 | CAP_SET_VIDEOFORMAT = PInvoke.WM_CAP_SET_VIDEOFORMAT, 354 | CAP_DLG_VIDEOCOMPRESSION = PInvoke.WM_CAP_DLG_VIDEOCOMPRESSION, 355 | CAP_SET_PREVIEW = PInvoke.WM_CAP_SET_PREVIEW, 356 | CAP_SET_OVERLAY = PInvoke.WM_CAP_SET_OVERLAY, 357 | CAP_SET_PREVIEWRATE = PInvoke.WM_CAP_SET_PREVIEWRATE, 358 | CAP_SET_SCALE = PInvoke.WM_CAP_SET_SCALE, 359 | CAP_GET_STATUS = PInvoke.WM_CAP_GET_STATUS, 360 | CAP_SET_SCROLL = PInvoke.WM_CAP_SET_SCROLL, 361 | CAP_GRAB_FRAME = PInvoke.WM_CAP_GRAB_FRAME, 362 | CAP_GRAB_FRAME_NOSTOP = PInvoke.WM_CAP_GRAB_FRAME_NOSTOP, 363 | CAP_SEQUENCE = PInvoke.WM_CAP_SEQUENCE, 364 | CAP_SEQUENCE_NOFILE = PInvoke.WM_CAP_SEQUENCE_NOFILE, 365 | CAP_SET_SEQUENCE_SETUP = PInvoke.WM_CAP_SET_SEQUENCE_SETUP, 366 | CAP_GET_SEQUENCE_SETUP = PInvoke.WM_CAP_GET_SEQUENCE_SETUP, 367 | CAP_SET_MCI_DEVICEA = PInvoke.WM_CAP_SET_MCI_DEVICEA, 368 | CAP_GET_MCI_DEVICEA = PInvoke.WM_CAP_GET_MCI_DEVICEA, 369 | CAP_SET_MCI_DEVICEW = PInvoke.WM_CAP_SET_MCI_DEVICEW, 370 | CAP_GET_MCI_DEVICEW = PInvoke.WM_CAP_GET_MCI_DEVICEW, 371 | CAP_SET_MCI_DEVICE = PInvoke.WM_CAP_SET_MCI_DEVICE, 372 | CAP_GET_MCI_DEVICE = PInvoke.WM_CAP_GET_MCI_DEVICE, 373 | CAP_STOP = PInvoke.WM_CAP_STOP, 374 | CAP_ABORT = PInvoke.WM_CAP_ABORT, 375 | CAP_SINGLE_FRAME_OPEN = PInvoke.WM_CAP_SINGLE_FRAME_OPEN, 376 | CAP_SINGLE_FRAME_CLOSE = PInvoke.WM_CAP_SINGLE_FRAME_CLOSE, 377 | CAP_SINGLE_FRAME = PInvoke.WM_CAP_SINGLE_FRAME, 378 | CAP_PAL_OPENA = PInvoke.WM_CAP_PAL_OPENA, 379 | CAP_PAL_SAVEA = PInvoke.WM_CAP_PAL_SAVEA, 380 | CAP_PAL_OPENW = PInvoke.WM_CAP_PAL_OPENW, 381 | CAP_PAL_SAVEW = PInvoke.WM_CAP_PAL_SAVEW, 382 | CAP_PAL_OPEN = PInvoke.WM_CAP_PAL_OPEN, 383 | CAP_PAL_SAVE = PInvoke.WM_CAP_PAL_SAVE, 384 | CAP_PAL_PASTE = PInvoke.WM_CAP_PAL_PASTE, 385 | CAP_PAL_AUTOCREATE = PInvoke.WM_CAP_PAL_AUTOCREATE, 386 | CAP_PAL_MANUALCREATE = PInvoke.WM_CAP_PAL_MANUALCREATE, 387 | CAP_SET_CALLBACK_CAPCONTROL = PInvoke.WM_CAP_SET_CALLBACK_CAPCONTROL, 388 | CAP_UNICODE_END = PInvoke.WM_CAP_UNICODE_END, 389 | CAP_END = PInvoke.WM_CAP_END, 390 | SampleExtension_ContentType_Size = PInvoke.WM_SampleExtension_ContentType_Size, 391 | SampleExtension_PixelAspectRatio_Size = PInvoke.WM_SampleExtension_PixelAspectRatio_Size, 392 | SampleExtension_Timecode_Size = PInvoke.WM_SampleExtension_Timecode_Size, 393 | SampleExtension_SampleDuration_Size = PInvoke.WM_SampleExtension_SampleDuration_Size, 394 | SampleExtension_ChromaLocation_Size = PInvoke.WM_SampleExtension_ChromaLocation_Size, 395 | SampleExtension_ColorSpaceInfo_Size = PInvoke.WM_SampleExtension_ColorSpaceInfo_Size, 396 | CT_REPEAT_FIRST_FIELD = PInvoke.WM_CT_REPEAT_FIRST_FIELD, 397 | CT_BOTTOM_FIELD_FIRST = PInvoke.WM_CT_BOTTOM_FIELD_FIRST, 398 | CT_TOP_FIELD_FIRST = PInvoke.WM_CT_TOP_FIELD_FIRST, 399 | CT_INTERLACED = PInvoke.WM_CT_INTERLACED, 400 | CL_INTERLACED420 = PInvoke.WM_CL_INTERLACED420, 401 | CL_PROGRESSIVE420 = PInvoke.WM_CL_PROGRESSIVE420, 402 | MAX_VIDEO_STREAMS = PInvoke.WM_MAX_VIDEO_STREAMS, 403 | MAX_STREAMS = PInvoke.WM_MAX_STREAMS, 404 | ADSPROP_NOTIFY_PAGEINIT = PInvoke.WM_ADSPROP_NOTIFY_PAGEINIT, 405 | ADSPROP_NOTIFY_PAGEHWND = PInvoke.WM_ADSPROP_NOTIFY_PAGEHWND, 406 | ADSPROP_NOTIFY_CHANGE = PInvoke.WM_ADSPROP_NOTIFY_CHANGE, 407 | ADSPROP_NOTIFY_APPLY = PInvoke.WM_ADSPROP_NOTIFY_APPLY, 408 | ADSPROP_NOTIFY_SETFOCUS = PInvoke.WM_ADSPROP_NOTIFY_SETFOCUS, 409 | ADSPROP_NOTIFY_FOREGROUND = PInvoke.WM_ADSPROP_NOTIFY_FOREGROUND, 410 | ADSPROP_NOTIFY_EXIT = PInvoke.WM_ADSPROP_NOTIFY_EXIT, 411 | ADSPROP_NOTIFY_ERROR = PInvoke.WM_ADSPROP_NOTIFY_ERROR, 412 | RASDIALEVENT = PInvoke.WM_RASDIALEVENT 413 | } 414 | -------------------------------------------------------------------------------- /Microsoft.Terminal.WinUI3/build/CI.Microsoft.Terminal.WinUI3.Unofficial.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.Terminal.WinUI3\TerminalControl.xbf 5 | PreserveNewest 6 | 7 | 8 | Microsoft.Terminal.WinUI3\TerminalControl.xaml 9 | PreserveNewest 10 | 11 | 12 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyWindowsTerminalControl 2 | [![Build](https://github.com/mitchcapper/EasyWindowsTerminalControl/actions/workflows/build.yaml/badge.svg)](https://github.com/mitchcapper/EasyWindowsTerminalControl/actions/workflows/build.yaml) 3 | [![EasyWindowsTerminalControl](https://img.shields.io/nuget/v/EasyWindowsTerminalControl.svg?style=flat&label=EasyWindowsTerminalControl)](https://www.nuget.org/packages/EasyWindowsTerminalControl/) 4 | [![EasyWindowsTerminalControl.WinUI](https://img.shields.io/nuget/v/EasyWindowsTerminalControl.WinUI.svg?style=flat&label=EasyWindowsTerminalControl.WinUI)](https://www.nuget.org/packages/EasyWindowsTerminalControl.WinUI/) 5 | 6 | A high performance full-feature WPF / (winui3* beta) terminal control that uses the [Official Windows Terminal](https://github.com/microsoft/terminal) console host for the backend driver. 7 | 8 | ![](TermExample/Screenshot.png) 9 | 10 | It features full 24-bit color support with ANSI/VT escape sequences (and colors), hardware / GPU accelerated rendering, mouse support, and true console interaction. This is not just some isolated window hosted in c#, you have full api control for input, output, sizing, and the terminal theming. The control can start any console application you want (or any default terminal like powershell, pwsh, cmd.exe). It provides XAML properties for easy settings, and events for things like input/output interception. The control can also passively log the terminal session and provide both the raw terminal sequence (including all VT codes) and the standard UTF-8 human readable text. 11 | 12 | The control features the ability to detach and re-attach live terminal instances between controls (even between windows). It also includes a basic support for the XAML designer (won't show theming). 13 | 14 | Technically you could use the control simply as ANSI render frontend if you needed to display say log data or a prior saved console session in its original form. 15 | 16 | 17 | 18 | - [Disclaimer](#disclaimer) 19 | - [Usage](#usage) 20 | - [WinUI3 Support](#winui3-support) 21 | - [Control Properties](#control-properties) 22 | - [Methods](#methods) 23 | - [EasyTerminalControl](#easyterminalcontrol) 24 | - [TermPTY](#termpty) 25 | - [Theming](#theming) 26 | - [Limitations](#limitations) 27 | - [Advanced Usage / Internals](#advanced-usage--internals) 28 | - [ReadDelimitedTermPTY](#readdelimitedtermpty) 29 | 30 | 31 | 32 | ### Disclaimer 33 | While the core of this control uses the Windows Terminal core control conpty/the wpf control are not yet publicly packaged. This relies on beta packages so the low level API we use might change in the future. See [Issue #6999 · microsoft/terminal](https://github.com/microsoft/terminal/issues/6999) for details. 34 | 35 | ## Usage 36 | `dotnet add package EasyWindowsTerminalControl` 37 | 38 | ```xml 39 | 40 | ``` 41 | 42 | ### WinUI3 Support 43 | This control does support WinUI3 with a special package `EasyWindowsTerminalControl.WinUI`. This is very alpha and very un-official. Windows terminal does not have anything like a WinUI3 control and the dependency `CI.Microsoft.Terminal.WinUI3.Unofficial` is done in house to replicate the WPF package as closely as possible. The biggest WinUI3 issue is the lack of HwndHost. To make that package we created our own based on the original WPF version. Otherwise the `EasyWindowsTerminalControl.WinUI` package replicates our `EasyWindowsTerminalControl` nearly identically for usage. As this does use near identical implementations as the WPF version it has similar limitations as well. 44 | 45 | ### Control Properties 46 | The control has all the standard UserControl properties but it has the following unique properties that support binding. Note some of these properties are "write-only" meaning they are not meant to be read as there are external ways the properties could be changed we would be unaware of. An example is the `IsCursorVisible` property where VT codes could change this (infact setting this property just sends VT codes itself). Binding to a write only property means you won't get the change when it changes however you can always call control.IsCursorVisible=X and it will always update the terminal to that state. 47 | - **StartupCommandLine** *(string, "powershell.exe")* - The full command line to launch for the application to run. Can contain any arguments as well. 48 | - **IsReadOnly** *(bool?, write only, null)* - Ignores all input from the Terminal GUI, this includes things like resize notifications so may have unintended affects. It is actually a helper to setting it on the underlying ConPTYTerm. 49 | - **IsCursorVisible** *(bool?, write only, null)* - Controls if the cursor is showing. Note apps emitting VT codes can re-enable it. 50 | - **Theme** *(TerminalTheme?, write only)* - Sets the default terminal theme, background, foreground, cursor style, etc. 51 | - **LogConPTYOutput** *(bool, false)* - If the underlying TermPTY instance should be told to log the application output to its buffer (can call ConPTYTerm.GetConsoleText() to retrieve) 52 | - **Win32InputMode** *(bool, true)* - Standard VT sequences can't quite transmit all the key data that INPUT_RECORD could. This uses a 'key record' sequence to transmit all the original information that conpty knows how to translate into VT codes. 53 | - **InputCapture** *(INPUT_CAPTURE flags enum)* - Controls what specialized keys should be captured by the control (tab, arrow keys). 54 | - **FontSizeWhenSettingTheme** *(int, 12)* - The font size for terminal text, if you set this after the control is initialized it won't take effect until you call `SetTheme` directly. 55 | - **FontFamilyWhenSettingTheme** *(FontFamily, "Cascadia Code")* - Font to use for terminal text, similar behavior to font-size above in terms of changes after initialized 56 | - **ConPTYTerm** - Allows you to change the TermPTY 57 | - **Terminal** *(write only)* 58 | 59 | ### Methods 60 | #### EasyTerminalControl 61 | - `DisconnectConPTYTerm()` - Mostly useful if you plan to connect another ConPTYTerm to the frontend 62 | - `RestartTerm(TermPTY useTerm = null, bool disposeOld=true)` - Restarts the process in a clean term with a new TermPTY instance. Can set `useTerm` to use your own TermPTY instance rather than ours. Any properties (ie StartupCommandLine) that have been changed the new version is used. 63 | 64 | #### TermPTY 65 | - `TermPTY(int READ_BUFFER_SIZE = 1024 * 16, bool USE_BINARY_WRITER = false, IProcessFactory ProcessFactory=null)` - The read buffer is the maximum that can be read at once, rarely would it need to be changed. By default data is transmitted as UTF8 text however conpty does support binary reading and writing. No matter what mode you use, you can use the `WriteToTerm` or `WriteToTermBinary` functions and it will automatically transform them to bytes/text automatically. Of course if you are working with binary data but don't have binary writing on you will run into problems when it attempts to call Encoding.UTF8.GetString on those bytes. IProcessFactory interface is responsible for creating the actual process. If you need more control you can implement this but please review the default implementation first. There are some [specific things](https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-for-creation-of-the-child-process) that must be set or else the console interaction may not work properly. 66 | - `ClearUITerminal(bool fullReset = false)` - Clears the screen using the proper VT sequences 67 | - `WriteToUITerminal(ReadOnlySpan str)` - This simulates output to the Terminal UI 68 | - `WriteToTerm(ReadOnlySpan input)` - Write to the conpty directly as if input came from the Terminal UI 69 | - `WriteToTermBinary(ReadOnlySpan input)` - Write to the conpty directly taking a byte array. Note if USE_BINARY_WRITER is not enabled this will just be converted to a UTF8 string. 70 | - `InterceptOutputToUITerminal`/`InterceptInputToTermApp` both of these take a delegate: `void InterceptDelegate(ref Span str)` they allow you to manipulate the data that will be sent. You can return a whole new span if desired. 71 | 72 | ### Theming 73 | The terminal can take a Theme object setting the default foreground/background/highlight colors and then the standard 16 vt100 colors. The colors are (COLORREF)[https://learn.microsoft.com/en-us/windows/win32/gdi/colorref] values. There is a helper EasyTerminalControl.ColorToVal to convert Color values to COLORREF format. Note there is no transparency support currently with the control. As the control has its own HWND there are methods to set the entire layers transparency (note that would affect the foreground and background text). 74 | 75 | ## Limitations 76 | Sadly nothing is free. The way the Windows Terminal team is able to provide such great performance and functionality without the normal WPF limitations is by doing all their own render handling with native C++, DirectX, and their atlas engine. This means the UI control itself uses Hwnd hosting (similar to xaml islands but for native controls). Most of this work is completely hidden from you and it behaves like any normal WPF control (resizing, backend api call / property / function calls) except for airspace. The primary limitation with airspace is in terms of trying render WPF content on top of the terminal itself. Context menus and such will work but normal WPF controls cannot appear above the terminal part itself. If you have used the WebView2 control this the same sort of limitation (see their bug [#286](https://github.com/MicrosoftEdge/WebView2Feedback/issues/286)). For more details on airspace in general see the Microsoft documentation on [layered rendering](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/technology-regions-overview). There are some ways around the stock limitations (ie you can specify arbitrary shapes at runtime that punch through certain sections of some layers), but that is beyond the scope of this project. 77 | 78 | ## Advanced Usage / Internals 79 | Behind the scenes there are two main parts to the control. There is the [Pseudo Console (conpty.dll)](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) this is what hooks directly to the executable on one side and our library on the other (the `TermPTY` class in particular). Then there is the Terminal GUI control `TerminalControl` that handles the actual rendering (the massive work done by the Windows Terminal Team). The Terminal GUI not only renders output but also takes user input and converts it into the proper VT codes for sending to the backend tty. The interactions between these two is managed by our BasicTerminalControl. 80 | 81 | As we use our own class `TermPTY` to control conpty it allows us to do interesting things. Normally TermPTY is fairly passive input and output are passed transparently between the Terminal GUI and the backend conpty/application. You can directly write ANSI/VT/text to either the Terminal GUI or the backend conpty. For example, while normally input to the application is what the user types if you had an AI helper it could directly inject input into the application without having to simulate keystrokes. Similarly your application could interpret program output and then change the ANSI sequences sent to the Terminal GUI to do things like provided colorized output for applications that don't support it. NOTE: There are strict limitations here, normally these two components are tightly integrated together. If you change the output of complex programs to the Terminal GUI you may get strange results (think about sequences sent to change the cursor position and then that cursor not being where its expected). 82 | 83 | ### ReadDelimitedTermPTY 84 | There is an additional ConPTY class called `ReadDelimitedTermPTY`. This is a special ConPTY version it only shows the output from the program every time a specific delimiter found in the output (and only the output before the delimiter). It has some special logic to try and efficiently reuse the same buffer as a ring buffer so that even if the delimiter is only partially output in one write from the program it will catch it will still match when the rest is written on the next write. There is an example of it in the TermExample app with the Process Output Sample button and window. 85 | -------------------------------------------------------------------------------- /TermExample.WinUI/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /TermExample.WinUI/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices.WindowsRuntime; 6 | using Microsoft.UI.Xaml; 7 | using Microsoft.UI.Xaml.Controls; 8 | using Microsoft.UI.Xaml.Controls.Primitives; 9 | using Microsoft.UI.Xaml.Data; 10 | using Microsoft.UI.Xaml.Input; 11 | using Microsoft.UI.Xaml.Media; 12 | using Microsoft.UI.Xaml.Navigation; 13 | using Microsoft.UI.Xaml.Shapes; 14 | using Windows.ApplicationModel; 15 | using Windows.ApplicationModel.Activation; 16 | using Windows.Foundation; 17 | using Windows.Foundation.Collections; 18 | 19 | // To learn more about WinUI, the WinUI project structure, 20 | // and more about our project templates, see: http://aka.ms/winui-project-info. 21 | 22 | namespace TermExample.WinUI { 23 | /// 24 | /// Provides application-specific behavior to supplement the default Application class. 25 | /// 26 | public partial class App : Application { 27 | /// 28 | /// Initializes the singleton application object. This is the first line of authored code 29 | /// executed, and as such is the logical equivalent of main() or WinMain(). 30 | /// 31 | public App() { 32 | this.InitializeComponent(); 33 | } 34 | 35 | /// 36 | /// Invoked when the application is launched. 37 | /// 38 | /// Details about the launch request and process. 39 | protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { 40 | m_window = new MainWindow(); 41 | m_window.Activate(); 42 | } 43 | 44 | private Window m_window; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/StoreLogo.png -------------------------------------------------------------------------------- /TermExample.WinUI/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/EasyWindowsTerminalControl/65df6c77c648c42b8bc972051f6a5a4c1f537a2d/TermExample.WinUI/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /TermExample.WinUI/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |