├── .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