Merge branch 'main' into chesterliu/dev/text-colorrrr

This commit is contained in:
Chester Liu 2021-09-15 15:48:05 +08:00
commit 5423de51ff
330 changed files with 15262 additions and 3625 deletions

View file

@ -1,6 +1,7 @@
apc
calt
ccmp
cybersecurity
Apc
clickable
clig
@ -33,6 +34,7 @@ kje
liga
lje
locl
lorem
maxed
mkmk
mru
@ -59,6 +61,7 @@ TLDR
tokenizes
tonos
tshe
uiatextrange
UIs
und
unregister

View file

@ -2,11 +2,13 @@ ACCEPTFILES
ACCESSDENIED
alignas
alignof
APPLYTOSUBMENUS
bitfield
bitfields
BUILDBRANCH
BUILDMSG
BUILDNUMBER
BYPOSITION
charconv
CLASSNOTAVAILABLE
cmdletbinding
@ -78,7 +80,11 @@ llu
localtime
lround
LSHIFT
MENUCOMMAND
MENUDATA
MENUINFO
memicmp
mptt
mov
msappx
MULTIPLEUSE
@ -93,6 +99,7 @@ NOCHANGEDIR
NOPROGRESS
NOREDIRECTIONBITMAP
NOREPEAT
NOTIFYBYPOS
NOTIFYICON
NOTIFYICONDATA
ntprivapi
@ -133,12 +140,14 @@ SRWLOCK
STDCPP
STDMETHOD
strchr
strcpy
streambuf
strtoul
Stubless
Subheader
Subpage
syscall
TASKBARCREATED
TBPF
THEMECHANGED
tlg
@ -171,6 +180,7 @@ xlocmes
xlocmon
xlocnum
xloctime
XMax
xmemory
XParse
xpath
@ -179,3 +189,4 @@ xstring
xtree
xutility
YIcon
YMax

View file

@ -25,6 +25,7 @@ DWINRT
enablewttlogging
Intelli
LKG
Lxss
mfcribbon
microsoft
microsoftonline

View file

@ -31,6 +31,7 @@ Kourosh
kowalczyk
leonmsft
Lepilleur
lhecker
lukesampson
Manandhar
mbadolato
@ -66,6 +67,7 @@ sonpham
stakx
thereses
Walisch
Wellons
Wirt
Wojciech
zadjii

View file

@ -169,6 +169,7 @@ brandings
BRK
Browsable
bsearch
Bspace
bstr
BTNFACE
buf
@ -270,10 +271,12 @@ cmder
CMDEXT
Cmdlet
cmdline
cmh
CMOUSEBUTTONS
cmp
cmpeq
cmt
cmw
cmyk
CNL
cnt
@ -406,11 +409,13 @@ csbiex
csharp
CSHORT
CSIDL
Cspace
csproj
Csr
csrmsg
CSRSS
csrutil
css
cstdarg
cstddef
cstdio
@ -509,6 +514,8 @@ DECAWM
DECCKM
DECCOLM
DECDHL
decdld
DECDLD
DECDWL
DECEKBD
DECID
@ -529,6 +536,7 @@ DECRC
DECREQTPARM
DECRLM
DECRQM
DECRQSS
DECRST
DECSASD
DECSC
@ -781,6 +789,7 @@ flyout
fmodern
fmtarg
fmtid
FNV
FOLDERID
FONTCHANGE
fontdlg
@ -789,6 +798,7 @@ FONTENUMPROC
FONTFACE
FONTFAMILY
FONTHEIGHT
FONTINFO
fontlist
FONTOK
FONTSIZE
@ -902,6 +912,7 @@ github
gitlab
gle
globals
GLYPHENTRY
gmail
GMEM
GNUC
@ -950,6 +961,7 @@ hdrstop
HEIGHTSCROLL
hfile
hfont
hfontresource
hglobal
hhh
HHmm
@ -1272,6 +1284,7 @@ locsrc
locstudio
Loewen
LOGFONT
LOGFONTA
LOGFONTW
logissue
lowercased
@ -1458,6 +1471,7 @@ Mul
multiline
munged
munges
murmurhash
mutex
mutexes
muxes
@ -1935,6 +1949,7 @@ realloc
reamapping
rects
redef
redefinable
Redir
redirector
redist
@ -1980,6 +1995,7 @@ rfc
rftp
rgb
rgba
RGBCOLOR
rgbi
rgci
rgfae
@ -2149,6 +2165,7 @@ SIGDN
SINGLEFLAG
SINGLETHREADED
siup
sixel
SIZEBOX
sizeof
SIZESCROLL
@ -2253,6 +2270,7 @@ SWMR
SWP
swprintf
SYMED
symlink
SYNCPAINT
sys
syscalls
@ -2754,6 +2772,7 @@ WTo
wtof
wtoi
WTs
WTSOFTFONT
wtw
wtypes
Wubi
@ -2783,6 +2802,7 @@ xes
xff
XFile
XFORM
xIcon
XManifest
XMath
XMFLOAT
@ -2815,6 +2835,7 @@ YCast
YCENTER
YCount
YDPI
yIcon
yml
YOffset
YPosition

View file

@ -11,6 +11,7 @@ leonerd
fixterms
winui
appshellintegration
mdtauk
cppreference
gfycat
Guake

View file

@ -21,7 +21,7 @@ Write-Host "Checking test results..."
$queryUri = GetQueryTestRunsUri -CollectionUri $CollectionUri -TeamProject $TeamProject -BuildUri $BuildUri -IncludeRunDetails
Write-Host "queryUri = $queryUri"
$testRuns = Invoke-RestMethod -Uri $queryUri -Method Get -Headers $azureDevOpsRestApiHeaders
$testRuns = Invoke-RestMethodWithRetries $queryUri -Headers $azureDevOpsRestApiHeaders
[System.Collections.Generic.List[string]]$failingTests = @()
[System.Collections.Generic.List[string]]$unreliableTests = @()
[System.Collections.Generic.List[string]]$unexpectedResultTest = @()
@ -50,7 +50,7 @@ foreach ($testRun in ($testRuns.value | Sort-Object -Property "completedDate" -D
$totalTestsExecutedCount += $testRun.totalTests
$testRunResultsUri = "$($testRun.url)/results?api-version=5.0"
$testResults = Invoke-RestMethod -Uri "$($testRun.url)/results?api-version=5.0" -Method Get -Headers $azureDevOpsRestApiHeaders
$testResults = Invoke-RestMethodWithRetries "$($testRun.url)/results?api-version=5.0" -Headers $azureDevOpsRestApiHeaders
foreach ($testResult in $testResults.value)
{

View file

@ -20,13 +20,31 @@ function Generate-File-Links
Out-File -FilePath $helixLinkFile -Append -InputObject "<ul>"
foreach($file in $files)
{
Out-File -FilePath $helixLinkFile -Append -InputObject "<li><a href=$($file.Link)>$($file.Name)</a></li>"
$url = Append-HelixAccessTokenToUrl $file.Link "{Your-Helix-Access-Token-Here}"
Out-File -FilePath $helixLinkFile -Append -InputObject "<li>$($url)</li>"
}
Out-File -FilePath $helixLinkFile -Append -InputObject "</ul>"
Out-File -FilePath $helixLinkFile -Append -InputObject "</div>"
}
}
function Append-HelixAccessTokenToUrl
{
Param ([string]$url, [string]$token)
if($token)
{
if($url.Contains("?"))
{
$url = "$($url)&access_token=$($token)"
}
else
{
$url = "$($url)?access_token=$($token)"
}
}
return $url
}
#Create output directory
New-Item $OutputFolder -ItemType Directory
@ -63,7 +81,8 @@ foreach ($testRun in $testRuns.value)
if (-not $workItems.Contains($workItem))
{
$workItems.Add($workItem)
$filesQueryUri = "https://helix.dot.net/api/2019-06-17/jobs/$helixJobId/workitems/$helixWorkItemName/files$accessTokenParam"
$filesQueryUri = "https://helix.dot.net/api/2019-06-17/jobs/$helixJobId/workitems/$helixWorkItemName/files"
$filesQueryUri = Append-HelixAccessTokenToUrl $filesQueryUri $helixAccessToken
$files = Invoke-RestMethodWithRetries $filesQueryUri
$screenShots = $files | where { $_.Name.EndsWith(".jpg") }
@ -102,6 +121,7 @@ foreach ($testRun in $testRuns.value)
Write-Host "Downloading $link to $destination"
$link = Append-HelixAccessTokenToUrl $link $HelixAccessToken
Download-FileWithRetries $link $destination
}
}

View file

@ -23,7 +23,7 @@ Write-Host "queryUri = $queryUri"
# To account for unreliable tests, we'll iterate through all of the tests associated with this build, check to see any tests that were unreliable
# (denoted by being marked as "skipped"), and if so, we'll instead mark those tests with a warning and enumerate all of the attempted runs
# with their pass/fail states as well as any relevant error messages for failed attempts.
$testRuns = Invoke-RestMethod -Uri $queryUri -Method Get -Headers $azureDevOpsRestApiHeaders
$testRuns = Invoke-RestMethodWithRetries $queryUri -Headers $azureDevOpsRestApiHeaders
$timesSeenByRunName = @{}
@ -32,10 +32,10 @@ foreach ($testRun in $testRuns.value)
$testRunResultsUri = "$($testRun.url)/results?api-version=5.0"
Write-Host "Marking test run `"$($testRun.name)`" as in progress so we can change its results to account for unreliable tests."
Invoke-RestMethod -Uri "$($testRun.url)?api-version=5.0" -Method Patch -Body (ConvertTo-Json @{ "state" = "InProgress" }) -Headers $azureDevOpsRestApiHeaders -ContentType "application/json" | Out-Null
Invoke-RestMethod "$($testRun.url)?api-version=5.0" -Method Patch -Body (ConvertTo-Json @{ "state" = "InProgress" }) -Headers $azureDevOpsRestApiHeaders -ContentType "application/json" | Out-Null
Write-Host "Retrieving test results..."
$testResults = Invoke-RestMethod -Uri $testRunResultsUri -Method Get -Headers $azureDevOpsRestApiHeaders
$testResults = Invoke-RestMethodWithRetries $testRunResultsUri -Headers $azureDevOpsRestApiHeaders
foreach ($testResult in $testResults.value)
{
@ -54,7 +54,8 @@ foreach ($testRun in $testRuns.value)
Write-Host " Test $($testResult.testCaseTitle) was detected as unreliable. Updating..."
# The errorMessage field contains a link to the JSON-encoded rerun result data.
$rerunResults = ConvertFrom-Json (New-Object System.Net.WebClient).DownloadString($testResult.errorMessage)
$resultsJson = Download-StringWithRetries "Error results" $testResult.errorMessage
$rerunResults = ConvertFrom-Json $resultsJson
[System.Collections.Generic.List[System.Collections.Hashtable]]$rerunDataList = @()
$attemptCount = 0
$passCount = 0

View file

@ -2,8 +2,9 @@
trigger: none
pr: none
pool:
name: Package ES Standard Build
pool:
name: WinDevPool-L
demands: ImageOverride -equals WinDevVS16-latest
parameters:
- name: branding
@ -70,11 +71,9 @@ jobs:
clean: true
submodules: true
persistCredentials: True
- task: PkgESSetupBuild@10
- task: PkgESSetupBuild@12
displayName: Package ES - Setup Build
inputs:
useDfs: false
productName: OpenConsole
disableOutputRedirect: true
- task: PowerShell@2
displayName: Rationalize Build Platform
@ -153,7 +152,7 @@ jobs:
inputs:
solution: '**\OpenConsole.sln'
vsVersion: 16.0
msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /t:Terminal\CascadiaPackage;Terminal\WindowsTerminalUniversal /p:WindowsTerminalReleaseBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog
msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }};PGOBuildMode=${{ parameters.pgoBuildMode }} /t:Terminal\CascadiaPackage;Terminal\WindowsTerminalUniversal /p:WindowsTerminalReleaseBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
clean: true
@ -195,7 +194,7 @@ jobs:
inputs:
solution: '**\OpenConsole.sln'
vsVersion: 16.0
msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /p:WindowsTerminalReleaseBuild=true /t:Terminal\wpf\PublicTerminalCore
msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }};PGOBuildMode=${{ parameters.pgoBuildMode }} /p:WindowsTerminalReleaseBuild=true /t:Terminal\wpf\PublicTerminalCore
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
- task: PowerShell@2
@ -275,11 +274,9 @@ jobs:
clean: true
submodules: true
persistCredentials: True
- task: PkgESSetupBuild@10
- task: PkgESSetupBuild@12
displayName: Package ES - Setup Build
inputs:
useDfs: false
productName: OpenConsole
disableOutputRedirect: true
- task: DownloadBuildArtifacts@0
displayName: Download Artifacts (*.appx, *.msix)
@ -354,11 +351,9 @@ jobs:
clean: true
submodules: true
persistCredentials: True
- task: PkgESSetupBuild@10
- task: PkgESSetupBuild@12
displayName: Package ES - Setup Build
inputs:
useDfs: false
productName: OpenConsole
disableOutputRedirect: true
- task: DownloadBuildArtifacts@0
displayName: Download x86 PublicTerminalCore
@ -480,7 +475,7 @@ jobs:
mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\
workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed
- task: PkgESVPack@10
- task: PkgESVPack@12
displayName: 'Package ES - VPack'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

View file

@ -8,9 +8,12 @@ jobs:
variables:
BuildConfiguration: AuditMode
BuildPlatform: ${{ parameters.platform }}
pool: "windevbuildagents"
# The public pool is also an option!
# pool: { vmImage: windows-2019 }
pool:
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: WinDevPoolOSS-L
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: WinDevPool-L
demands: ImageOverride -equals WinDevVS16-latest
steps:
- checkout: self

View file

@ -11,9 +11,12 @@ jobs:
variables:
BuildConfiguration: ${{ parameters.configuration }}
BuildPlatform: ${{ parameters.platform }}
pool: "windevbuildagents"
# The public pool is also an option!
# pool: { vmImage: windows-2019 }
pool:
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: WinDevPoolOSS-L
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: WinDevPool-L
demands: ImageOverride -equals WinDevVS16-latest
steps:
- template: build-console-steps.yml

View file

@ -3,7 +3,7 @@ parameters:
platform: ''
additionalBuildArguments: ''
minimumExpectedTestsExecutedCount: 1 # Sanity check for minimum expected tests to be reported
rerunPassesRequiredToAvoidFailure: 0
rerunPassesRequiredToAvoidFailure: 5
jobs:
- job: Build${{ parameters.platform }}${{ parameters.configuration }}
@ -12,9 +12,12 @@ jobs:
BuildConfiguration: ${{ parameters.configuration }}
BuildPlatform: ${{ parameters.platform }}
PGOBuildMode: 'Instrument'
pool: "windevbuildagents"
# The public pool is also an option!
# pool: { vmImage: windows-2019 }
pool:
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: WinDevPoolOSS-L
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: WinDevPool-L
demands: ImageOverride -equals WinDevVS16-latest
steps:
- template: build-console-steps.yml

View file

@ -11,7 +11,7 @@ jobs:
clean: true
- task: PowerShell@2
displayName: 'Code Formattting Check'
displayName: 'Code Formatting Check'
inputs:
targetType: filePath
filePath: '.\build\scripts\Invoke-FormattingCheck.ps1'

View file

@ -22,6 +22,7 @@ jobs:
condition: succeededOrFailed()
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
HelixAccessToken: $(HelixApiAccessToken)
inputs:
targetType: filePath
filePath: build\Helix\UpdateUnreliableTests.ps1
@ -32,6 +33,7 @@ jobs:
condition: succeededOrFailed()
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
HelixAccessToken: $(HelixApiAccessToken)
inputs:
targetType: filePath
filePath: build\Helix\OutputTestResults.ps1

View file

@ -15,6 +15,7 @@ parameters:
# if 'useBuildOutputFromBuildId' is set, we will default to using a build from this pipeline:
useBuildOutputFromPipeline: $(System.DefinitionId)
openHelixTargetQueues: 'windows.10.amd64.client19h1.open.xaml'
closedHelixTargetQueues: 'windows.10.amd64.client19h1.xaml'
jobs:
- job: ${{ parameters.name }}
@ -29,11 +30,11 @@ jobs:
buildConfiguration: ${{ parameters.configuration }}
buildPlatform: ${{ parameters.platform }}
openHelixTargetQueues: ${{ parameters.openHelixTargetQueues }}
closedHelixTargetQueues: ${{ parameters.closedHelixTargetQueues }}
artifactsDir: $(Build.SourcesDirectory)\Artifacts
taefPath: $(Build.SourcesDirectory)\build\Helix\packages\Microsoft.Taef.10.60.210621002\build\Binaries\$(buildPlatform)
helixCommonArgs: '/binaryLogger:$(Build.SourcesDirectory)/${{parameters.name}}.$(buildPlatform).$(buildConfiguration).binlog /p:HelixBuild=$(Build.BuildId).$(buildPlatform).$(buildConfiguration) /p:Platform=$(buildPlatform) /p:Configuration=$(buildConfiguration) /p:HelixType=${{parameters.helixType}} /p:TestSuite=${{parameters.testSuite}} /p:ProjFilesPath=$(Build.ArtifactStagingDirectory) /p:rerunPassesRequiredToAvoidFailure=${{parameters.rerunPassesRequiredToAvoidFailure}}'
steps:
- task: CmdLine@1
displayName: 'Display build machine environment variables'
@ -140,6 +141,7 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Run tests in Helix (open queues)'
condition: and(succeeded(),eq(variables['System.CollectionUri'],'https://dev.azure.com/ms/'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
@ -147,3 +149,15 @@ jobs:
projects: build\Helix\RunTestsInHelix.proj
custom: msbuild
arguments: '$(helixCommonArgs) /p:IsExternal=true /p:Creator=Terminal /p:HelixTargetQueues=$(openHelixTargetQueues)'
- task: DotNetCoreCLI@2
displayName: 'Run tests in Helix (closed queues)'
condition: and(succeeded(),ne(variables['System.CollectionUri'],'https://dev.azure.com/ms/'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
HelixAccessToken: $(HelixApiAccessToken)
inputs:
command: custom
projects: build\Helix\RunTestsInHelix.proj
custom: msbuild
arguments: '$(helixCommonArgs) /p:HelixTargetQueues=$(closedHelixTargetQueues)'

View file

@ -20,11 +20,15 @@ jobs:
inputs:
artifactName: ${{ parameters.pgoArtifact }}
downloadPath: $(artifactsPath)
- task: NuGetToolInstaller@0
displayName: 'Use NuGet 5.2.0'
- task: NuGetAuthenticate@0
inputs:
versionSpec: 5.2.0
nuGetServiceConnections: 'Terminal Public Artifact Feed'
- task: NuGetToolInstaller@0
displayName: 'Use NuGet 5.8.0'
inputs:
versionSpec: 5.8.0
- task: CopyFiles@2
displayName: 'Copy pgd files to NuGet build directory'
@ -58,5 +62,11 @@ jobs:
displayName: 'NuGet push'
inputs:
command: push
publishVstsFeed: Terminal/TerminalDependencies
packagesToPush: $(Build.ArtifactStagingDirectory)/*.nupkg
nuGetFeedType: external
packagesToPush: $(Build.ArtifactStagingDirectory)/*.nupkg
# The actual URL and PAT for this feed is configured at
# https://microsoft.visualstudio.com/Dart/_settings/adminservices
# This is the name of that connection
publishFeedCredentials: 'Terminal Public Artifact Feed'
feedsToUse: config
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config'

View file

@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2021</XesBaseYearForStoreVersion>
<VersionMajor>1</VersionMajor>
<VersionMinor>11</VersionMinor>
<VersionMinor>12</VersionMinor>
<VersionInfoProductName>Windows Terminal</VersionInfoProductName>
</PropertyGroup>
</Project>

View file

@ -154,6 +154,17 @@
"description": "Sets how the background image aligns to the boundaries of the window when unfocused. Possible values: \"center\", \"left\", \"top\", \"right\", \"bottom\", \"topLeft\", \"topRight\", \"bottomLeft\", \"bottomRight\"",
"type": "string"
},
"intenseTextStyle": {
"default": "bright",
"description": "Controls how 'intense' text is rendered. Values are \"bold\", \"bright\", \"all\" and \"none\"",
"enum": [
"none",
"bold",
"bright",
"all"
],
"type": "string"
},
"experimental.retroTerminalEffect": {
"description": "When set to true, enable retro terminal effects when unfocused. This is an experimental feature, and its continued existence is not guaranteed.",
"type": "boolean"
@ -204,6 +215,22 @@
"type": "integer"
}
]
},
"features": {
"description": "Sets the DWrite font features for the given font. For example, { \"ss01\": 1, \"liga\":0 } will enable ss01 and disable ligatures.",
"type": "object",
"patternProperties": {
"^(([A-Za-z0-9]){4})$": { "type": "integer" }
},
"additionalProperties": false
},
"axes": {
"description": "Sets the DWrite font axes for the given font. For example, { \"wght\": 200 } will set the font weight to 200.",
"type": "object",
"patternProperties": {
"^([A-Za-z]{4})$": { "type": "number" }
},
"additionalProperties": false
}
},
"type": "object"
@ -239,7 +266,9 @@
"identifyWindows",
"moveFocus",
"movePane",
"swapPane",
"moveTab",
"multipleActions",
"newTab",
"newWindow",
"nextTab",
@ -250,6 +279,7 @@
"paste",
"prevTab",
"renameTab",
"openSystemMenu",
"openTabRenamer",
"quakeMode",
"resetFontSize",
@ -275,6 +305,7 @@
"toggleReadOnlyMode",
"toggleShaderEffects",
"wt",
"quit",
"unbound"
],
"type": "string"
@ -285,7 +316,10 @@
"right",
"up",
"down",
"previous"
"previous",
"nextInOrder",
"previousInOrder",
"first"
],
"type": "string"
},
@ -493,6 +527,23 @@
"type": "integer",
"default": 0,
"description": "Which tab to switch to, with the first being 0"
}
}
}
],
"required": [ "index" ]
},
"MovePaneAction": {
"description": "Arguments corresponding to a Move Pane Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "movePane" },
"index": {
"type": "integer",
"default": 0,
"description": "Which tab to move the pane to, with the first being 0"
}
}
}
@ -509,24 +560,24 @@
"direction": {
"$ref": "#/definitions/FocusDirection",
"default": "left",
"description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane."
"description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane."
}
}
}
],
"required": [ "direction" ]
},
"MovePaneAction": {
"description": "Arguments corresponding to a Move Pane Action",
"SwapPaneAction": {
"description": "Arguments corresponding to a Swap Pane Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "movePane" },
"action": { "type": "string", "pattern": "swapPane" },
"direction": {
"$ref": "#/definitions/FocusDirection",
"default": "left",
"description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane."
"description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane."
}
}
}
@ -777,6 +828,24 @@
],
"required": [ "direction" ]
},
"MultipleActionsAction": {
"description": "Arguments for the multiple actions command",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "multipleActions" },
"actions" : {
"$ref": "#/definitions/ShortcutAction",
"type": "array",
"minItems": 1,
"description": "A list of other actions."
}
}
}
],
"required": [ "actions" ]
},
"CommandPaletteAction": {
"description": "Arguments for a commandPalette action",
"allOf": [
@ -972,6 +1041,7 @@
{ "$ref": "#/definitions/SwitchToTabAction" },
{ "$ref": "#/definitions/MoveFocusAction" },
{ "$ref": "#/definitions/MovePaneAction" },
{ "$ref": "#/definitions/SwapPaneAction" },
{ "$ref": "#/definitions/ResizePaneAction" },
{ "$ref": "#/definitions/SendInputAction" },
{ "$ref": "#/definitions/SplitPaneAction" },
@ -1163,6 +1233,15 @@
"description": "When set to true, this enables the launch of Windows Terminal at startup. Setting this to false will disable the startup task entry. If the Windows Terminal startup task entry is disabled either by org policy or by user action this setting will have no effect.",
"type": "boolean"
},
"firstWindowPreference": {
"default": "defaultProfile",
"description": "Defines what behavior the terminal takes when it starts. \"defaultProfile\" will have the terminal launch with one tab of the default profile, and \"persistedWindowLayout\" will cause the terminal to save its layout on close and reload it on open.",
"enum": [
"defaultProfile",
"persistedWindowLayout"
],
"type": "string"
},
"launchMode": {
"default": "default",
"description": "Defines whether the terminal will launch as maximized, full screen, or in a window. Setting this to \"focus\" is equivalent to launching the terminal in the \"default\" mode, but with the focus mode enabled. Similar, setting this to \"maximizedFocus\" will result in launching the terminal in a maximized window with the focus mode enabled.",
@ -1182,6 +1261,21 @@
"minimum": 0,
"type": [ "integer", "string" ],
"deprecated": true
},
"minimizeToNotificationArea": {
"default": "false",
"description": "When set to true, minimizing a Terminal window will no longer appear in the taskbar. Instead, a Terminal icon will appear in the notification area through which the user can access their windows.",
"type": "boolean"
},
"alwaysShowNotificationIcon": {
"default": "false",
"description": "When set to true, the Terminal's notification icon will always be shown in the notification area.",
"type": "boolean"
},
"useAcrylicInTabRow": {
"default": "false",
"description": "When set to true, the tab row will have an acrylic background with 50% opacity.",
"type": "boolean"
},
"actions": {
"description": "Properties are specific to each custom action.",

View file

@ -0,0 +1,619 @@
---
author: Mike Griese @zadjii-msft
created on: 2020-11-20
last updated: 2021-08-17
issue id: #1032
---
# Elevation Quality of Life Improvements
## Abstract
For a long time, we've been researching adding support to the Windows Terminal
for running both unelevated and elevated (admin) tabs side-by-side, in the same
window. However, after much research, we've determined that there isn't a safe
way to do this without opening the Terminal up as a potential
escalation-of-privilege vector.
Instead, we'll be adding a number of features to the Terminal to improve the
user experience of working in elevated scenarios. These improvements include:
* A visible indicator that the Terminal window is elevated ([#1939])
* Configuring the Terminal to always run elevated ([#632])
* Configuring a specific profile to always open elevated ([#632])
* Allowing new tabs, panes to be opened elevated directly from an unelevated
window
* Dynamic profile appearance that changes depending on if the Terminal is
elevated or not. ([#1939], [#8311])
## Background
_This section was originally authored in the [Process Model 2.0 Spec]. Please
refer to it there for its original context._
Let's presume that you're a user who wants to be able to open an elevated tab
within an otherwise unelevated Terminal window. We call this scenario "mixed
elevation" - the tabs within the Terminal can be running either unelevated _or_
elevated client applications.
It wouldn't be terribly difficult for the unelevated Terminal to request the
permission of the user to spawn an elevated client application. The user would
see a UAC prompt, they'd accept, and then they'd be able to have an elevated
shell alongside their unelevated tabs.
However, this creates an escalation of privilege vector. Now, there's an
unelevated window which is connected directly to an elevated process. At this
point, **any other unelevated application could send input to the Terminal's
`HWND`**. This would make it possible for another unelevated process to "drive"
the Terminal window, and send commands to the elevated client application.
It was initially theorized that the window/content model architecture would also
help enable "mixed elevation". With mixed elevation, tabs could run at different
integrity levels within the same terminal window. However, after investigation
and research, it has become apparent that this scenario is not possible to do
safely after all. There are numerous technical difficulties involved, and each
with their own security risks. At the end of the day, the team wouldn't be
comfortable shipping a mixed-elevation solution, because there's simply no way
for us to be confident that we haven't introduced an escalation-of-privilege
vector utilizing the Terminal. No matter how small the attack surface might be,
we wouldn't be confident that there are _no_ vectors for an attack.
Some things we considered during this investigation:
* If a user requests a new elevated tab from an otherwise unelevated window, we
could use UAC to create a new, elevated window process, and "move" all the
current tabs to that window process, as well as the new elevated client. Now,
the window process would be elevated, preventing it from input injection, and
it would still contains all the previously existing tabs. The original window
process could now be discarded, as the new elevated window process will
pretend to be the original window.
- However, it is unfortunately not possible with COM to have an elevated
client attach to an unelevated server that's registered at runtime. Even in
a packaged environment, the OS will reject the attempt to `CoCreateInstance`
the content process object. this will prevent elevated windows from
re-connecting to unelevated client processes.
- We could theoretically build an RPC tunnel between content and window
processes, and use the RPC connection to marshal the content process to the
elevated window. However, then _we_ would need to be responsible for
securing access the the RPC endpoint, and we feel even less confident doing
that.
- Attempts were also made to use a window-broker-content architecture, with
the broker process having a static CLSID in the registry, and having the
window and content processes at mixed elevation levels `CoCreateInstance`
that broker. This however _also_ did not work across elevation levels. This
may be due to a lack of Packaged COM support for mixed elevation levels.
It's also possible that the author forgot that packaged WinRT doesn't play
nicely with creating objects in an elevated context. The Terminal has
previously needed to manually manifest all its classes in a SxS manifest for
Unpackaged WinRT to allow the classes to be activated, rather than relying
on the packaged catalog. It's theoretically possible that doing that would
have allowed the broker to be activated across integrity levels.
Even if this approach did end up working, we would still need to be
responsible for securing the elevated windows so that an unelevated attacker
couldn't hijack a content process and trigger unexpected code in the window
process. We didn't feel confident that we could properly secure this channel
either.
We also considered allowing mixed content in windows that were _originally_
elevated. If the window is already elevated, then it can launch new unelevated
processes. We could allow elevated windows to still create unelevated
connections. However, we'd want to indicate per-pane what the elevation state
of each connection is. The user would then need to keep track themselves of
which terminal instances are elevated, and which are not.
This also marks a departure from the current behavior, where everything in an
elevated window would be elevated by default. The user would need to specify for
each thing in the elevated window that they'd want to create it elevated. Or the
Terminal would need to provide some setting like
`"autoElevateEverythingInAnElevatedWindow"`.
We cannot support mixed elevation when starting in a unelevated window.
Therefore, it doesn't make a lot of UX sense to support it in the other
direction. It's a cleaner UX story to just have everything in a single window at
the same elevation level.
## Solution Design
Instead of supporting mixed elevation in the same window, we'll introduce the
following features to the Terminal. These are meant as a way of improving the
quality of life for users who work in mixed-elevation (or even just elevated)
environments.
### Visible indicator for elevated windows
As requested in [#1939], it would be nice if it was easy to visibly identify if
a Terminal window was elevated or not.
One easy way of doing this is by adding a simple UAC shield to the left of the
tabs for elevated windows. This shield could be configured by the theme (see
[#3327]). We could provide the following states:
* Colored (the default)
* Monochrome
* Hidden, to hide the shield even on elevated windows. This is the current
behavior.
![UAC-shield-in-titlebar](UAC-shield-in-titlebar.png)
_figure 1: a monochrome UAC shield in the titlebar of the window, courtesy of @mdtauk_
We could also simplify this to only allow a boolean true/false for displaying
the shield. As we do often with other enums, we could define `true` to be the
same as the default appearance, and `false` to be the hidden option. As always,
the development of the Terminal is an iterative process, where we can
incrementally improve from no setting, to a boolean setting, to a enum-backed
one.
### Configuring a profile to always run elevated
Oftentimes, users might have a particular tool chain that only works when
running elevated. In these scenarios, it would be convenient for the user to be
able to identify that the profile should _always_ run elevated. That way, they
could open the profile from the dropdown menu of an otherwise unelevated window
and have the elevated window open with the profile automatically.
We'll be adding the `"elevate": true|false` setting as a per-profile setting,
with a default value of `false`. When set to `true`, we'll try to auto-elevate
the profile whenever it's launched. We'll check to see if this window is
elevated before creating the connection for this profile. If the window is not
elevated, then we'll create a new window with the requested elevation level to
handle the new connection.
`"elevate": false` will do nothing. If the window is already elevated, then the
profile won't open an un-elevated window.
If the user tries to open an `"elevate": true` profile in a window that's
already elevated, then a new tab/split will open in the existing window, rather
than spawning an additional elevated window.
There are three situations where we're creating new terminal instances: new
tabs, new splits, and new windows. Currently, these are all actions that are
also exposed in the `wt` commandline as subcommands. We can convert from the
commandline arguments into these actions already. Therefore, it shouldn't be too
challenging to convert these actions back into the equal commandline arguments.
For the following examples, let's assume the user is currently in an unelevated
Terminal window.
When the user tries to create a new elevated **tab**, we'll need to create a new
process, elevated, with the following commandline:
```
wt new-tab [args...]
```
When we create this new `wt` instance, it will obey the glomming rules as
specified in [Session Management Spec]. It might end up glomming to another
existing window at that elevation level, or possibly create its own window.
Similarly, for a new elevated **window**, we can make sure to pass the `-w new`
arguments to `wt`. These parameters indicate that we definitely want this
command to run in a new window, regardless of the current glomming settings.
```
wt -w new new-tab [args...]
```
However, creating a new **pane** is a little trickier. Invoking the `wt
split-pane [args...]` is straightforward enough.
<!-- Discussion notes follow:
If the current window doesn't have the same elevation level as the
requested profile, do we always want to just create a new split? If the command
ends up glomming to an existing window, does that even make sense? That invoking
an elevated split in an unelevated window would end up splitting the elevated
window? It's very possible that the user wanted a split in the tab they're
currently in, in the unelevated window, but they don't want a split in the
elevated window.
What if there's not space in the elevated window to create the split (but there
would be in the current window)? That would sure make it seem like nothing
happened, silently.
We could alternatively have cross-elevation splits default to always opening a
new tab. That might mitigate some of the odd behaviors. Until we actually have
support for running commands in existing windows, we'll always need to make a
new window when running elevated. We'll need to make the new window for new tabs
and splits, because there's no way to invoke another existing window.
A third proposal is to pop a warning dialog at the user when they try to open an
elevated split from and unelevated window. This dialog could be something like
> What you requested couldn't be completed. Do you want to:
> A. Make me a new tab instead.
> B. Forget it and cancel. I'll go fix my config.
I'm certainly leaning towards proposal 2 - always create a new tab. This is how
it's implemented in [#8514]. In that PR, this seems to work sensibly.
-->
After discussing with the team, we have decided that the most sensible approach
for handling a cross-elevation `split-pane` is to just create a new tab in the
elevated window. The user can always re-attach the pane as a split with the
`move-pane` command once the new pane in the elevated window.
#### Configure the Terminal to _always_ run elevated
`elevate` is a per-profile property, not a global property. If a user
wants to always have all instances of the Terminal run elevated, they
could set `"elevate": true` in their profile defaults. That would cause _all_
profiles they launch to always spawn as elevated windows.
#### `elevate` in Actions
Additionally, we'll add the `elevate` property to the `NewTerminalArgs` used in
the `newTab`, `splitPane`, and `newWindow` actions. This is similar to how other
properties of profiles can be overridden at launch time. This will allow
windows, tabs and panes to all be created specifically as elevated windows.
In the `NewTerminalArgs`, `elevate` will be an optional boolean, with the
following behavior:
* `null` (_default_): Don't modify the `elevate` property for this profile
* `true`: This launch should act like the profile had `"elevate": true` in its
properties.
* `false`: This launch should act like the profile had `"elevate": false` in its
properties.
We'll also add an iterable command for opening a profile in an
elevated tab, with the following json:
```jsonc
{
// New elevated tab...
"name": { "key": "NewElevatedTabParentCommandName", "icon": "UAC-Shield.png" },
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": "${profile.name}",
"command": { "action": "newTab", "profile": "${profile.name}", "elevated": true }
}
]
},
```
#### Elevation from the dropdown
Currently, the new tab dropdown supports opening a new pane by
<kbd>Alt+click</kbd>ing on a profile. We could similarly add support to open a
tab elevated with <kbd>Ctrl+click</kbd>. This is similar to the behavior of the
Windows taskbar. It supports creating an elevated instance of a program by
<kbd>Ctrl+click</kbd>ing on entries as well.
## Implementation Details
### Starting an elevated process from an unelevated process
It seems that we're able to create an elevated process by passing the `"runas"`
verb to
[`ShellExecute`](https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutea).
So we could use something like
```c++
ShellExecute(nullptr,
L"runas",
L"wt.exe",
L"-w new new-tab [args...]",
nullptr,
SW_SHOWNORMAL);
```
This will ask the shell to perform a UAC prompt before spawning `wt.exe` as an
elevated process.
> 👉 NOTE: This mechanism won't always work on non-Desktop SKUs of Windows. For
> more discussion, see [Elevation on OneCore SKUs](#Elevation-on-OneCore-SKUs).
## Potential Issues
<table>
<tr>
<td><strong>Accessibility</strong></td>
<td>
The set of changes proposed here are not expected to introduce any new
accessibility issues. Users can already create elevated Terminal windows. Making
it easier to create these windows doesn't really change our accessibility story.
</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td>
We won't be doing anything especially unique, so there aren't expected to be any
substantial security risks associated with these changes. Users can already
create elevated Terminal windows, so we're not really introducing any new
functionality, from a security perspective.
We're relying on the inherent security of the `runas` verb of `ShellExecute` to
prevent any sort of unexpected escalation-of-privilege.
<hr>
One security concern is the fact that the `settings.json` file is currently a
totally unsecured file. It's completely writable by any medium-IL process. That
means it's totally possible for a malicious program to change the file. The
malicious program could find a user's "Elevated PowerShell" profile, and change
the commandline to `malicious.exe`. The user might then think that their
"Elevated PowerShell" will run `powershell.exe` elevated, but will actually
auto-elevate this attacker.
If all we expose to the user is the name of the profile in the UAC dialog, then
there's no way for the user to be sure that the program that's about to be
launched is actually what they expect.
To help mitigate this, we should _always_ pass the evaluated `commandline` as a
part of the call to `ShellExecute`. the arguments that are passed to
`ShellExecute` are visible to the user, though they need to click the "More
Details" dropdown to reveal them.
We will need to mitigate this vulnerability regardless of adding support for the
auto-elevation of individual terminal tabs/panes. If a user is launching the
Terminal elevated (i.e. from the Win+X menu in Windows 11), then it's possible
for a malicious program to overwrite the `commandline` of their default profile.
The user may now unknowingly invoke this malicious program while thinking they
are simply launching the Terminal.
To deal with this more broadly, we will display a dialog within the Terminal
window before creating **any** elevated terminal instance. In that dialog, we'll
display the commandline that will be executed, so the user can very easily
confirm the commandline.
This will need to happen for all elevated terminal instances. For an elevated
Windows Terminal window, this means _all_ connections made by the Terminal.
Every time the user opens a new profile or a new commandline in a pane, we'll
need to prompt them first to confirm the commandline. This dialog within the
elevated window will also prevent an attacker from editing the `settings.json`
file while the user already has an elevated Terminal window open and hijacking a
profile.
The dialog options will certainly be annoying to users who don't want to be
taken out of their flow to confirm the commandline that they wish to launch.
There's precedent for a similar warning being implemented by VSCode, with their
[Workspace Trust] feature. They too faced a similar backlash when the feature
first shipped. However, in light of recent global cybersecurity attacks, this is
seen as an acceptable UX degradation in the name of application trust. We don't
want to provide an avenue that's too easy to abuse.
When the user confirms the commandline of this profile as something safe to run,
we'll add it to an elevated-only version of `state.json`. (see [#7972] for more
details). This elevated version of the file will only be accessible by the
elevated Terminal, so an attacker cannot hijack the contents of the file. This
will help mitigate the UX discomfort caused by prompting on every commandline
launched. This should mean that the discomfort is only limited to the first
elevated launch of a particular profile. Subsequent launches (without modifying
the `commandline`) will work as they always have.
The dialog for confirming these commandlines should have a link to the docs for
"Learn more...". Transparency in the face of this dialog should
mitigate some dissatisfaction.
The dialog will _not_ appear if the user does not have a split token - if the
user's PC does not have UAC enabled, then they're _already_ running as an
Administrator. Everything they do is elevated, so they shouldn't be prompted in
this way.
The Settings UI should also expose a way of viewing and removing these cached
entries. This page should only be populated in the elevated version of the
Terminal.
</td>
</tr>
<tr>
<td><strong>Reliability</strong></td>
<td>
No changes to our reliability are expected as a part of this change.
</td>
</tr>
<tr>
<td><strong>Compatibility</strong></td>
<td>
There are no serious compatibility concerns expected with this changelist. The
new `elevate` property will be unset by default, so users will heed to opt-in
to the new auto-elevating behavior.
There is one minor concern regarding introducing the UAC shield on the window.
We're planning on using themes to configure the appearance of the shield. That
means we'll need to ship themes before the user will be able to hide the shield
again.
</td>
</tr>
<tr>
<td><strong>Performance, Power, and Efficiency</strong></td>
<td>
No changes to our performance are expected as a part of this change.
</td>
</tr>
</table>
### Centennial Applications
In the past, we've had a notoriously rough time with the Centennial app
infrastructure and running the Terminal elevated. Notably, we've had to list all
our WinRT classes in our SxS manifest so they could be activated using
unpackaged WinRT while running elevated. Additionally, there are plenty of
issues running the Terminal in an "over the shoulder" elevation (OTS) scenario.
Specifically, we're concerned with the following scenario:
* the current user account has the Terminal installed,
* but they aren't an Administrator,
* the Administrator account doesn't have the Terminal installed.
In that scenario, the user can run into issues launching the Terminal in an
elevated context (even after entering the Admin's credentials in the UAC
prompt).
This spec proposes no new mitigations for dealing with these issues. It may in
fact make them more prevalent, by making elevated contexts more easily
accessible.
Unfortunately, these issues are OS bugs that are largely out of our own control.
We will continue to apply pressure to the centennial app team internally as we
encounter these issues. They are are team best equipped to resolve these issues.
### Default Terminal & auto-elevation
In the future, when we support setting the Terminal as the "default terminal
emulator" on Windows. When that lands, we will use the `profiles.defaults`
settings to create the tab where we'll be hosting the commandline client. If the user has
`"elevate": true` in their `profiles.defaults`, we'd usually try to
auto-elevate the profile. In this scenario, however, we can't do that. The
Terminal is being invoked on behalf of the client app launching, instead of the
Terminal invoking the client application.
**2021-08-17 edit**: Now that "defterm" has shipped, we're a little more aware
of some of the limitations with packaged COM and elevation boundaries. Defterm
cannot be used with elevated processes _at all_ currently (see [#10276]). When
an elevated commandline application is launched, it will always just appear in
`conhost.exe`. Furthermore, An unelevated peasant can't communicate with an
elevated monarch so we can't toss the connection to the elevated monarch and
have them handle it.
The simplest solution here is to just _always_ ignore the `elevate` property for
incoming defterm connections. This is not an ideal solution, and one that we're
willing to revisit if/when [#10276] is ever fixed.
### Elevation on OneCore SKUs
This spec proposes using `ShellExecute` to elevate the Terminal window. However,
not all Windows SKUs have support for `ShellExecute`. Notably, the non-Desktop
SKUs, which are often referred to as "OneCore" SKUs. On these platforms, we
won't be able to use `ShellExecute` to elevate the Terminal. There might not
even be the concept of multiple elevation levels, or different users, depending
on the SKU.
Fortunately, this is a mostly hypothetical concern for the moment. Desktop is
the only publicly supported SKU for the Terminal currently. If the Terminal ever
does become available on those SKUs, we can use these proposals as mitigations.
* If elevation is supported, there must be some other way of elevating a
process. We could always use that mechanism instead.
* If elevation isn't supported (I'm thinking 10X is one of these), then we could
instead display a warning dialog whenever a user tries to open an elevated
profile.
- We could take the warning a step further. We could add another settings
validation step. This would warn the user if they try to mark any profiles
or actions as `"elevate":true`
## Future considerations
* If we wanted to go even further down the visual differentiation route, we
could consider allowing the user to set an entirely different theme ([#3327])
based on the elevation state. Something like `elevatedTheme`, to pick another
theme from the set of themes. This would allow them to force elevated windows
to have a red titlebar, for example.
* Over the course of discussion concerning appearance objects ([#8345]), it
became clear that having separate "elevated" appearances defined for
`profile`s was overly complicated. This is left as a consideration for a
possible future extension that could handle this scenario in a cleaner way.
* Similarly, we're going to leave [#3637] "different profiles when elevated vs
unelevated" for the future. This also plays into the design of "configure the
new tab dropdown" ([#1571]), and reconciling those two designs is out-of-scope
for this particular release.
* Tangentially, we may want to have a separate Terminal icon we ship with the
UAC shield present on it. This would be especially useful for the tray icon.
Since there will be different tray icon instances for elevated and unelevated
windows, having unique icons may help users identify which is which.
### De-elevating a Terminal
the original version of this spec proposed that `"elevated":false` from an
elevated Terminal window should create a new unelevated Terminal instance. The
mechanism for doing this is described in [The Old New Thing: How can I launch an
unelevated process from my elevated process, redux].
This works well when the Terminal is running unpackaged. However, de-elevating a
process does not play well with packaged centennial applications. When asking
the OS to run the packaged application from an elevated context, the system will
still create the child process _elevated_. This means the packaged version of
the Terminal won't be able to create a new unelevated Terminal instance.
From an internal mail thread:
> App model intercepts the `CreateProcess` call and redirects it to a COM
> service. The parent of a packaged app is not the launching app, its some COM
> service. So none of the parent process nonsense will work because the
> parameters you passed to `CreateProcess` arent being used to create the
> process.
If this is fixed in the future, we could theoretically re-introduce de-elevating
a profile. The original spec proposed a `"elevated": bool?` setting, with the
following behaviors:
* `null` (_default_): Don't modify the elevation level when running this profile
* `true`: If the current window is unelevated, try to create a new elevated
window to host this connection.
* `false`: If the current window is elevated, try to create a new unelevated
window to host this connection.
We could always re-introduce this setting, to supercede `elevate`.
### Change profile appearance for elevated windows
In [#3062] and [#8345], we're planning on allowing users to set different
appearances for a profile whether it's focused or not. We could do similar thing
to enable a profile to have a different appearance when elevated. In the
simplest case, this could allow the user to set `"background": "#ff0000"`. This
would make a profile always appear to have a red background when in an elevated
window.
The more specific details of this implementation are left to the spec
[Configuration object for profiles].
In discussion of that spec, we decided that it would be far too complicated to
try and overload the `unfocusedAppearance` machinery for differentiating between
elevated and unelevated versions of the same profile. Already, that would lead
to 4 states: [`appearance`, `unfocusedAppearance`, `elevatedAppearance`,
`elevatedUnfocusedAppearance`]. This would lead to a combinatorial explosion if
we decided in the future that there should also be other states for a profile.
This particular QoL improvement is currently being left as a future
consideration, should someone come up with a clever way of defining
elevated-specific settings.
<!--
Brainstorming notes for future readers:
You could have a profile that layers on an existing profile, with elevated-specific settings:
{
"name": "foo",
"background": "#0000ff",
"commandline": "cmd.exe /k echo I am unelevated"
},
{
"inheritsFrom": "foo",
"background": "#ff0000",
"elevate": true,
"commandline": "cmd.exe /k echo I am ELEVATED"
}
-->
<!-- Footnotes -->
[#632]: https://github.com/microsoft/terminal/issues/632
[#1032]: https://github.com/microsoft/terminal/issues/1032
[#1571]: https://github.com/microsoft/terminal/issues/1571
[#1939]: https://github.com/microsoft/terminal/issues/1939
[#3062]: https://github.com/microsoft/terminal/issues/3062
[#3327]: https://github.com/microsoft/terminal/issues/3327
[#3637]: https://github.com/microsoft/terminal/issues/3637
[#4472]: https://github.com/microsoft/terminal/issues/4472
[#5000]: https://github.com/microsoft/terminal/issues/5000
[#7972]: https://github.com/microsoft/terminal/pull/7972
[#8311]: https://github.com/microsoft/terminal/issues/8311
[#8345]: https://github.com/microsoft/terminal/issues/8345
[#8514]: https://github.com/microsoft/terminal/issues/8514
[#10276]: https://github.com/microsoft/terminal/issues/10276
[Process Model 2.0 Spec]: https://github.com/microsoft/terminal/blob/main/doc/specs/%235000%20-%20Process%20Model%202.0.md
[Configuration object for profiles]: https://github.com/microsoft/terminal/blob/main/doc/specs/Configuration%20object%20for%20profiles.md
[Session Management Spec]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234472%20-%20Windows%20Terminal%20Session%20Management.md
[The Old New Thing: How can I launch an unelevated process from my elevated process, redux]: https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443
[Workspace Trust]: https://code.visualstudio.com/docs/editor/workspace-trust

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View file

@ -0,0 +1,130 @@
---
author: Kayla Cinnamon - cinnamon-msft
created on: 2021-03-04
last updated: 2021-03-09
issue id: 6900
---
# Actions Page
## Abstract
We need to represent actions inside the settings UI. This spec goes through the possible use cases and reasoning for including specific features for actions inside the settings UI.
## Background
### Inspiration
It would be ideal if we could get the settings UI to have parity with the JSON file. This will take some design work if we want every feature possible in relation to actions. There is also the option of not having parity with the JSON file in order to present a simpler UX.
### User Stories
All of these features are possible with the JSON file. This spec will go into discussion of which (possibly all) of these user stories need to be handled by the settings UI.
- Add key bindings to an action that does not already have keys assigned
- Edit key bindings for an action
- Remove key bindings from an action
- Add multiple key bindings for the same action
- Create an iterable action
- Create a nested action
- Choose which actions appear inside the command palette
- See all possible actions, regardless of keys
Commands with properties:
- sendInput has "input"
- closeOtherTabs has "index"
- closeTabsAfter has "index"
- renameTab has "title"*
- setTabColor has "color"*
- newWindow has "commandline", "startingDirectory", "tabTitle", "index", "profile"
- splitPane has "split", "commandline", "startingDirectory", "tabTitle", "index", "profile", "splitMode", "size"
- copy has "singleLine", "copyFormatting"
- scrollUp has "rowsToScroll"
- scrollDown has "rowsToScroll"
- setColorScheme has "colorScheme"
Majority of these commands listed above are intended for the command palette, so they wouldn't make much sense with keys assigned to them anyway.
### Future Considerations
One day we'll have actions that can be invoked by items in the dropdown menu. This setting will have to live somewhere. Also, once we get a status bar, people may want to invoke actions from there.
## Solution Design
### Proposal 1: Keyboard and Command Palette pages
Implement a Keyboard page in place of the Actions page. Also plan for a Command Palette page in the future if it's something that's heavily requested. The Command Palette page would cover the missing use cases listed below.
When users want to add a new key binding, the dropdown will list every action, regardless if it already has keys assigned. This page should show every key binding assigned to an action, even if there are multiple bindings to the same action.
Users will be able to view every possible action from the command palette if they'd like.
Use cases covered:
- Add key bindings to an action that does not already have keys assigned
- Edit key bindings for an action
- Remove key bindings from an action
- Add multiple key bindings for the same action
- See all actions that have keys assigned
Use cases missing:
- Create an iterable action
- Create a nested action
- Choose which actions appear inside the command palette
- See all possible actions, regardless of keys
* **Pros**:
- This allows people to edit their actions in most of their scenarios.
- This gives us some wiggle room to cover majority of the use cases we need and seeing if people want the other use cases that are missing.
* **Cons**:
- Unfortunately we couldn't cover every single use case with this design.
- You can't edit the properties that are on some commands, however the default commands from the command palette include options with properties anyway. For example "decrease font size" has the `delta` property already included.
### Proposal 2: Have everything on one Actions page
Implement an Actions page that allows you to create actions designed for the command palette as well as actions with keys.
Use cases covered:
- Add key bindings to an action that does not already have keys assigned
- Edit key bindings for an action
- Remove key bindings from an action
- Add multiple key bindings for the same action
- See all actions that have keys assigned
- Create an iterable action
- Create a nested action
- Choose which actions appear inside the command palette
- See all possible actions, regardless of keys
I could not come up with a UX design that wasn't too complicated or confusing for this scenario.
**Pros**:
- There is full parity with the JSON file.
**Cons**:
- Could not come up with a simplistic design to represent all of the use cases (which makes the settings UI not as enticing since it promotes ease of use).
## Conclusion
We considered Proposal 2, however the design became cluttered very quickly and we agreed to create two pages and start off with Proposal 1.
## UI/UX Design
![Click edit on key binding](./edit-click.png)
The Add new button is using the secondary color, to align with the button on the Color schemes page.
![Edit key binding](./edit-keys.png)
![Click add new](./add-click.png)
![Add key binding](./add-keys.png)
## Potential Issues
This design is not 1:1 with the JSON file, so actions that don't have keys will not appear on this page. Additionally, you can't add a new action without keys with this current design.
You also cannot specify properties on commands (like the `newTab` command) and these will have to be added through the JSON file. Considering there are only a few of these and we're planning to iterate on this and add a Command Palette page, we were okay with this decision.
## Resources
### Footnotes

View file

@ -29,8 +29,8 @@ Below is the schedule for when milestones will be included in release builds of
| 2021-03-01 | [1.7] in Windows Terminal Preview<br>[1.6] in Windows Terminal | [Windows Terminal Preview 1.7 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-7-release/) |
| 2021-04-14 | [1.8] in Windows Terminal Preview<br>[1.7] in Windows Terminal | [Windows Terminal Preview 1.8 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-8-release/) |
| 2021-05-31 | [1.9] in Windows Terminal Preview<br>[1.8] in Windows Terminal | [Windows Terminal Preview 1.9 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-9-release/) |
| 2021-07-31 | 1.10 in Windows Terminal Preview<br>[1.9] in Windows Terminal | |
| 2021-08-30 | 1.11 in Windows Terminal Preview<br>1.10 in Windows Terminal | |
| 2021-07-14 | [1.10] in Windows Terminal Preview<br>[1.9] in Windows Terminal | [Windows Terminal Preview 1.10 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-10-release/) |
| 2021-08-31 | [1.11] in Windows Terminal Preview<br>[1.10] in Windows Terminal | [Windows Terminal Preview 1.11 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-11-release/) |
| 2021-10-31 | 1.12 in Windows Terminal Preview<br>1.11 in Windows Terminal | |
| 2021-11-30 | 2.0 RC in Windows Terminal Preview<br>2.0 RC in Windows Terminal | |
| 2021-12-31 | [2.0] in Windows Terminal Preview<br>[2.0] in Windows Terminal | |
@ -89,6 +89,8 @@ Feature Notes:
[1.7]: https://github.com/microsoft/terminal/milestone/32
[1.8]: https://github.com/microsoft/terminal/milestone/33
[1.9]: https://github.com/microsoft/terminal/milestone/34
[1.10]: https://github.com/microsoft/terminal/milestone/35
[1.11]: https://github.com/microsoft/terminal/milestone/36
[2.0]: https://github.com/microsoft/terminal/milestone/22
[#1564]: https://github.com/microsoft/terminal/issues/1564
[#6720]: https://github.com/microsoft/terminal/pull/6720

View file

@ -4,7 +4,7 @@ This was originally imported by @Austin-Lamb in December 2020.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.
## What should be done to update this in the future?
@ -12,4 +12,4 @@ That provenance file is automatically read and inventoried by Microsoft systems
2. Take the parts you want, but leave most of it behind since it's HUGE and will bloat the repo to take it all. At the time of this writing, we only use small_vector.hpp and its dependencies as a header-only library.
3. Validate that the license in the root of the repository didn't change and update it if so. It is sitting in a version-specific subdirectory below this readme.
If it changed dramatically, ensure that it is still compatible with our license scheme. Also update the NOTICE file in the root of our repository to declare the third-party usage.
4. Submit the pull.
4. Submit the pull.

View file

@ -4,7 +4,7 @@ This was originally imported by @miniksa in January 2020.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.
## What should be done to update this in the future?

View file

@ -4,7 +4,7 @@ This was originally imported by @miniksa in March 2020.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.
## What should be done to update this in the future?

View file

@ -4,7 +4,7 @@ This was originally imported by @DHowett-MSFT in April 2020.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.
## What should be done to update this in the future?

View file

@ -4,7 +4,7 @@ This was originally imported by @PankajBhojwani in September 2020.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.
## What should be done to update this in the future?

View file

@ -4,7 +4,7 @@ This was originally imported by @miniksa in March 2020.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.
## What should be done to update this in the future?

View file

@ -4,4 +4,4 @@ This manifest anchors our usage of rgb.txt from the X11 distribution.
The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme.
Please update the provenance information in that file when ingesting an updated version of the dependent library.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards.
That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropriate governance standards.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -8,5 +8,5 @@ Please consult the [license](https://raw.githubusercontent.com/microsoft/cascadi
### Fonts Included
* Cascadia Code, Cascadia Mono (2106.17)
* from microsoft/cascadia-code@fb0bce69c1c12f6c298b8bc1c1d181868f5daa9a
* Cascadia Code, Cascadia Mono (2108.26)
* from microsoft/cascadia-code@f91d08f703ee61cf4ae936b9700ca974de2748fe

View file

@ -2,5 +2,5 @@
<packages>
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
<package id="Microsoft.UI.Xaml" version="2.5.0-prerelease.201202003" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.210309.3" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.210825.3" targetFramework="native" />
</packages>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.210309.3" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.210825.3" targetFramework="native" />
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
<package id="Microsoft.UI.Xaml" version="2.5.0-prerelease.201202003" targetFramework="native" />
<package id="Microsoft.VCRTForwarders.140" version="1.0.4" targetFramework="native" />

View file

@ -97,16 +97,18 @@ bool TextAttribute::IsLegacy() const noexcept
// - defaultFgColor: the default foreground color rgb value.
// - defaultBgColor: the default background color rgb value.
// - reverseScreenMode: true if the screen mode is reversed.
// - blinkingIsFaint: true if blinking should be interpreted as faint.
// - blinkingIsFaint: true if blinking should be interpreted as faint. (defaults to false)
// - boldIsBright: true if "bold" should be interpreted as bright. (defaults to true)
// Return Value:
// - the foreground and background colors that should be displayed.
std::pair<COLORREF, COLORREF> TextAttribute::CalculateRgbColors(const std::array<COLORREF, 256>& colorTable,
const COLORREF defaultFgColor,
const COLORREF defaultBgColor,
const bool reverseScreenMode,
const bool blinkingIsFaint) const noexcept
const bool blinkingIsFaint,
const bool boldIsBright) const noexcept
{
auto fg = _foreground.GetColor(colorTable, defaultFgColor, IsBold());
auto fg = _foreground.GetColor(colorTable, defaultFgColor, boldIsBright && IsBold());
auto bg = _background.GetColor(colorTable, defaultBgColor);
if (IsFaint() || (IsBlinking() && blinkingIsFaint))
{

View file

@ -68,7 +68,8 @@ public:
const COLORREF defaultFgColor,
const COLORREF defaultBgColor,
const bool reverseScreenMode = false,
const bool blinkingIsFaint = false) const noexcept;
const bool blinkingIsFaint = false,
const bool boldIsBright = true) const noexcept;
bool IsLeadingByte() const noexcept;
bool IsTrailingByte() const noexcept;

View file

@ -199,21 +199,24 @@ COLORREF TextColor::GetColor(const std::array<COLORREF, 256>& colorTable, const
unsigned long index;
return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast<size_t>(index) + 8) : defaultColor; // 5.
#elif _M_AMD64
// If you look closely this SSE2 algorithm is the exact same as the AVX one.
// If you look closely this SSE2 algorithm is the same as the AVX one.
// The two differences are that we need to:
// * do everything twice, because SSE is limited to 128 bits and not 256.
// * use _mm_packs_epi32 to merge two 128 bits vectors into one in step 3.5.
// _mm_packs_epi32 takes two SSE registers and truncates all 8 DWORDs into 8 WORDs,
// the latter of which fits into a single register (which is then used in the identical step 4).
// * since the result are now 8 WORDs, we need to use _mm_movemask_epi8 (there's no 16-bit variant),
// which unlike AVX's step 4 results in in something like 0b0000110000000000.
// --> the index returned by _BitScanForward must be divided by 2.
const auto haystack1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(colorTable.data() + 0));
const auto haystack2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(colorTable.data() + 4));
const auto needle = _mm_set1_epi32(__builtin_bit_cast(int, defaultColor));
const auto result1 = _mm_cmpeq_epi32(haystack1, needle);
const auto result2 = _mm_cmpeq_epi32(haystack2, needle);
const auto result = _mm_packs_epi32(result1, result2); // 3.5
const auto mask = _mm_movemask_ps(_mm_castsi128_ps(result));
const auto mask = _mm_movemask_epi8(result);
unsigned long index;
return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast<size_t>(index) + 8) : defaultColor;
return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast<size_t>(index / 2) + 8) : defaultColor;
#else
for (size_t i = 0; i < 8; i++)
{

View file

@ -22,6 +22,7 @@ class TextAttributeTests
TEST_METHOD(TestTextAttributeColorGetters);
TEST_METHOD(TestReverseDefaultColors);
TEST_METHOD(TestRoundtripDefaultColors);
TEST_METHOD(TestBoldAsBright);
std::array<COLORREF, 256> _colorTable;
COLORREF _defaultFg = RGB(1, 2, 3);
@ -263,3 +264,56 @@ void TextAttributeTests::TestRoundtripDefaultColors()
// Reset the legacy default colors to white on black.
TextAttribute::SetLegacyDefaultAttributes(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
}
void TextAttributeTests::TestBoldAsBright()
{
const COLORREF darkBlack = til::at(_colorTable, 0);
const COLORREF brightBlack = til::at(_colorTable, 8);
const COLORREF darkGreen = til::at(_colorTable, 2);
TextAttribute attr{};
// verify that calculated foreground/background are the same as the direct
// values when not bold
VERIFY_IS_FALSE(attr.IsBold());
VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg));
VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg));
VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true));
VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false));
// with bold set, calculated foreground/background values shouldn't change for the default colors.
attr.SetBold(true);
VERIFY_IS_TRUE(attr.IsBold());
VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true));
VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false));
attr.SetIndexedForeground(0);
VERIFY_IS_TRUE(attr.IsBold());
Log::Comment(L"Foreground should be bright black when bold is bright is enabled");
VERIFY_ARE_EQUAL(std::make_pair(brightBlack, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true));
Log::Comment(L"Foreground should be dark black when bold is bright is disabled");
VERIFY_ARE_EQUAL(std::make_pair(darkBlack, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false));
attr.SetIndexedBackground(2);
VERIFY_IS_TRUE(attr.IsBold());
Log::Comment(L"background should be unaffected by 'bold is bright'");
VERIFY_ARE_EQUAL(std::make_pair(brightBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true));
VERIFY_ARE_EQUAL(std::make_pair(darkBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false));
attr.SetBold(false);
VERIFY_IS_FALSE(attr.IsBold());
Log::Comment(L"when not bold, 'bold is bright' changes nothing");
VERIFY_ARE_EQUAL(std::make_pair(darkBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true));
VERIFY_ARE_EQUAL(std::make_pair(darkBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false));
Log::Comment(L"When set to a bright color, and bold, 'bold is bright' changes nothing");
attr.SetBold(true);
attr.SetIndexedForeground(8);
VERIFY_IS_TRUE(attr.IsBold());
VERIFY_ARE_EQUAL(std::make_pair(brightBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true));
VERIFY_ARE_EQUAL(std::make_pair(brightBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false));
}

View file

@ -397,6 +397,10 @@ namespace SettingsModelLocalTests
"name":"action6",
"command": { "action": "newWindow", "startingDirectory":"C:\\foo", "commandline": "bar.exe" }
},
{
"name":"action7_startingDirectoryWithTrailingSlash",
"command": { "action": "newWindow", "startingDirectory":"C:\\", "commandline": "bar.exe" }
},
])" };
const auto commands0Json = VerifyParseSucceeded(commands0String);
@ -405,7 +409,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(0u, commands.Size());
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(7u, commands.Size());
VERIFY_ARE_EQUAL(8u, commands.Size());
{
auto command = commands.Lookup(L"action0");
@ -503,5 +507,20 @@ namespace SettingsModelLocalTests
L"cmdline: \"%s\"", cmdline.c_str()));
VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\foo\" -- \"bar.exe\"", terminalArgs.ToCommandline());
}
{
auto command = commands.Lookup(L"action7_startingDirectoryWithTrailingSlash");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.ActionAndArgs());
VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.ActionAndArgs().Action());
const auto& realArgs = command.ActionAndArgs().Args().try_as<NewWindowArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& terminalArgs = realArgs.TerminalArgs();
VERIFY_IS_NOT_NULL(terminalArgs);
auto cmdline = terminalArgs.ToCommandline();
Log::Comment(NoThrowString().Format(
L"cmdline: \"%s\"", cmdline.c_str()));
VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\\\\" -- \"bar.exe\"", terminalArgs.ToCommandline());
}
}
}

View file

@ -41,6 +41,10 @@ namespace SettingsModelLocalTests
TEST_METHOD(LayerKeybindings);
TEST_METHOD(UnbindKeybindings);
TEST_METHOD(LayerScancodeKeybindings);
TEST_METHOD(TestExplicitUnbind);
TEST_METHOD(TestArbitraryArgs);
TEST_METHOD(TestSplitPaneArgs);
@ -54,6 +58,7 @@ namespace SettingsModelLocalTests
TEST_METHOD(TestMoveTabArgs);
TEST_METHOD(TestGetKeyBindingForAction);
TEST_METHOD(KeybindingsWithoutVkey);
TEST_CLASS_SETUP(ClassSetup)
{
@ -95,23 +100,33 @@ namespace SettingsModelLocalTests
VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows,
255,
0,
L"ctrl+shift+alt+win+vk(255)",
L"win+ctrl+alt+shift+vk(255)",
},
testCase{
VirtualKeyModifiers::Windows,
VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows,
0,
123,
L"ctrl+shift+alt+win+sc(123)",
L"win+ctrl+alt+shift+sc(123)",
},
};
for (const auto& tc : testCases)
{
KeyChord expectedKeyChord{ tc.modifiers, tc.vkey, tc.scanCode };
const auto actualString = KeyChordSerialization::ToString(expectedKeyChord);
Log::Comment(NoThrowString().Format(L"Testing case:\"%s\"", tc.expected.data()));
const auto actualString = KeyChordSerialization::ToString({ tc.modifiers, tc.vkey, tc.scanCode });
VERIFY_ARE_EQUAL(tc.expected, actualString);
auto expectedVkey = tc.vkey;
if (!expectedVkey)
{
expectedVkey = MapVirtualKeyW(tc.scanCode, MAPVK_VSC_TO_VK_EX);
}
const auto actualKeyChord = KeyChordSerialization::FromString(actualString);
VERIFY_ARE_EQUAL(expectedKeyChord, actualKeyChord);
VERIFY_ARE_EQUAL(tc.modifiers, actualKeyChord.Modifiers());
VERIFY_ARE_EQUAL(expectedVkey, actualKeyChord.Vkey());
VERIFY_ARE_EQUAL(tc.scanCode, actualKeyChord.ScanCode());
}
}
@ -232,6 +247,31 @@ namespace SettingsModelLocalTests
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
}
void KeyBindingsTests::TestExplicitUnbind()
{
const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" };
const std::string bindings1String{ R"([ { "command": "unbound", "keys": ["ctrl+c"] } ])" };
const std::string bindings2String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" };
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
const auto bindings1Json = VerifyParseSucceeded(bindings1String);
const auto bindings2Json = VerifyParseSucceeded(bindings2String);
const KeyChord keyChord{ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 };
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
actionMap->LayerJson(bindings0Json);
VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
actionMap->LayerJson(bindings1Json);
VERIFY_IS_TRUE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
actionMap->LayerJson(bindings2Json);
VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
}
void KeyBindingsTests::TestArbitraryArgs()
{
const std::string bindings0String{ R"([
@ -722,4 +762,42 @@ namespace SettingsModelLocalTests
VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast<int32_t>('P'), 0 }, kbd);
}
}
void KeyBindingsTests::LayerScancodeKeybindings()
{
Log::Comment(L"Layering a keybinding with a character literal on top of"
L" an equivalent sc() key should replace it.");
// Wrap the first one in `R"!(...)!"` because it has `()` internally.
const std::string bindings0String{ R"!([ { "command": "quakeMode", "keys":"win+sc(41)" } ])!" };
const std::string bindings1String{ R"([ { "keys": "win+`", "command": { "action": "globalSummon", "monitor": "any" } } ])" };
const std::string bindings2String{ R"([ { "keys": "ctrl+shift+`", "command": { "action": "quakeMode" } } ])" };
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
const auto bindings1Json = VerifyParseSucceeded(bindings1String);
const auto bindings2Json = VerifyParseSucceeded(bindings2String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings1Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size(), L"Layering the second action should replace the first one.");
actionMap->LayerJson(bindings2Json);
VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size());
}
void KeyBindingsTests::KeybindingsWithoutVkey()
{
const auto json = VerifyParseSucceeded(R"!([{"command": "quakeMode", "keys":"shift+sc(255)"}])!");
const auto actionMap = winrt::make_self<implementation::ActionMap>();
actionMap->LayerJson(json);
const auto action = actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Shift, 0, 255 });
VERIFY_IS_NOT_NULL(action);
}
}

View file

@ -37,11 +37,13 @@ namespace SettingsModelLocalTests
TEST_METHOD(TestTerminalArgsForBinding);
TEST_METHOD(MakeSettingsForProfileThatDoesntExist);
TEST_METHOD(MakeSettingsForProfile);
TEST_METHOD(MakeSettingsForDefaultProfileThatDoesntExist);
TEST_METHOD(TestLayerProfileOnColorScheme);
TEST_METHOD(TestCommandlineToTitlePromotion);
TEST_CLASS_SETUP(ClassSetup)
{
return true;
@ -63,7 +65,7 @@ namespace SettingsModelLocalTests
const std::string settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
"profiles": { "list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
@ -82,6 +84,9 @@ namespace SettingsModelLocalTests
"commandline": "wsl.exe"
}
],
"defaults": {
"historySize": 29
} },
"keybindings": [
{ "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" } },
{ "keys": ["ctrl+b"], "command": { "action": "splitPane", "split": "vertical", "profile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } },
@ -126,10 +131,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid0, guid);
VERIFY_ARE_EQUAL(guid0, profile.Guid());
VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
@ -148,10 +153,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty());
VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", realArgs.TerminalArgs().Profile());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid1, guid);
VERIFY_ARE_EQUAL(guid1, profile.Guid());
VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(2, termSettings.HistorySize());
}
@ -170,10 +175,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty());
VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid1, guid);
VERIFY_ARE_EQUAL(guid1, profile.Guid());
VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(2, termSettings.HistorySize());
}
@ -192,10 +197,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty());
VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(profile2Guid, guid);
VERIFY_ARE_EQUAL(profile2Guid, profile.Guid());
VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(3, termSettings.HistorySize());
}
@ -214,12 +219,21 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid0, guid);
if constexpr (Feature_ShowProfileDefaultsInSettings::IsEnabled())
{
// This action specified a command but no profile; it gets reassigned to the base profile
VERIFY_ARE_EQUAL(settings.ProfileDefaults(), profile);
VERIFY_ARE_EQUAL(29, termSettings.HistorySize());
}
else
{
VERIFY_ARE_EQUAL(guid0, profile.Guid());
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, false, static_cast<int32_t>('F'), 0 };
@ -237,10 +251,10 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile());
VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid1, guid);
VERIFY_ARE_EQUAL(guid1, profile.Guid());
VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(2, termSettings.HistorySize());
}
@ -257,10 +271,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid0, guid);
VERIFY_ARE_EQUAL(guid0, profile.Guid());
VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
@ -278,10 +292,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid0, guid);
VERIFY_ARE_EQUAL(guid0, profile.Guid());
VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory());
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
@ -301,10 +315,10 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory());
VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(profile2Guid, guid);
VERIFY_ARE_EQUAL(profile2Guid, profile.Guid());
VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory());
VERIFY_ARE_EQUAL(3, termSettings.HistorySize());
@ -323,10 +337,10 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid0, guid);
VERIFY_ARE_EQUAL(guid0, profile.Guid());
VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle());
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
@ -346,10 +360,10 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle());
VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(profile2Guid, guid);
VERIFY_ARE_EQUAL(profile2Guid, profile.Guid());
VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle());
VERIFY_ARE_EQUAL(3, termSettings.HistorySize());
@ -371,10 +385,10 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle());
VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile());
const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) };
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) };
const auto termSettings = settingsStruct.DefaultSettings();
VERIFY_ARE_EQUAL(guid1, guid);
VERIFY_ARE_EQUAL(guid1, profile.Guid());
VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline());
VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle());
VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory());
@ -382,9 +396,9 @@ namespace SettingsModelLocalTests
}
}
void TerminalSettingsTests::MakeSettingsForProfileThatDoesntExist()
void TerminalSettingsTests::MakeSettingsForProfile()
{
// Test that making settings throws when the GUID doesn't exist
// Test that making settings generally works.
const std::string settingsString{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
@ -405,32 +419,32 @@ namespace SettingsModelLocalTests
const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
const auto guid2 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const auto guid3 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
const auto profile1 = settings.FindProfile(guid1);
const auto profile2 = settings.FindProfile(guid2);
try
{
auto terminalSettings{ TerminalSettings::CreateWithProfileByID(settings, guid1, nullptr) };
auto terminalSettings{ TerminalSettings::CreateWithProfile(settings, profile1, nullptr) };
VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings);
VERIFY_ARE_EQUAL(1, terminalSettings.DefaultSettings().HistorySize());
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to CreateWithProfileByID should succeed");
VERIFY_IS_TRUE(false, L"This call to CreateWithProfile should succeed");
}
try
{
auto terminalSettings{ TerminalSettings::CreateWithProfileByID(settings, guid2, nullptr) };
auto terminalSettings{ TerminalSettings::CreateWithProfile(settings, profile2, nullptr) };
VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings);
VERIFY_ARE_EQUAL(2, terminalSettings.DefaultSettings().HistorySize());
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to CreateWithProfileByID should succeed");
VERIFY_IS_TRUE(false, L"This call to CreateWithProfile should succeed");
}
VERIFY_THROWS(auto terminalSettings = TerminalSettings::CreateWithProfileByID(settings, guid3, nullptr), wil::ResultException, L"This call to constructor should fail");
try
{
const auto termSettings{ TerminalSettings::CreateWithNewTerminalArgs(settings, nullptr, nullptr) };
@ -554,4 +568,85 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(ARGB(0, 0x45, 0x67, 0x89), terminalSettings4->CursorColor()); // from profile (no color scheme)
VERIFY_ARE_EQUAL(DEFAULT_CURSOR_COLOR, terminalSettings5->CursorColor()); // default
}
void TerminalSettingsTests::TestCommandlineToTitlePromotion()
{
const std::string settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": { "list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
},
],
"defaults": {
"historySize": 29
} }
})" };
const winrt::guid guid0{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}") };
CascadiaSettings settings{ til::u8u16(settingsJson) };
{ // just a profile (profile wins)
NewTerminalArgs args{};
args.Profile(L"profile0");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"profile0", settingsStruct.DefaultSettings().StartingTitle());
}
{ // profile and command line -> no promotion (profile wins)
NewTerminalArgs args{};
args.Profile(L"profile0");
args.Commandline(L"foo.exe");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"profile0", settingsStruct.DefaultSettings().StartingTitle());
}
{ // just a title -> it is propagated
NewTerminalArgs args{};
args.TabTitle(L"Analog Kid");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"Analog Kid", settingsStruct.DefaultSettings().StartingTitle());
}
{ // title and command line -> no promotion
NewTerminalArgs args{};
args.TabTitle(L"Digital Man");
args.Commandline(L"foo.exe");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"Digital Man", settingsStruct.DefaultSettings().StartingTitle());
}
{ // just a commandline -> promotion
NewTerminalArgs args{};
args.Commandline(L"foo.exe");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"foo.exe", settingsStruct.DefaultSettings().StartingTitle());
}
// various typesof commandline follow
{
NewTerminalArgs args{};
args.Commandline(L"foo.exe bar");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"foo.exe", settingsStruct.DefaultSettings().StartingTitle());
}
{
NewTerminalArgs args{};
args.Commandline(L"\"foo exe.exe\" bar");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"foo exe.exe", settingsStruct.DefaultSettings().StartingTitle());
}
{
NewTerminalArgs args{};
args.Commandline(L"\"\" grand designs");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"", settingsStruct.DefaultSettings().StartingTitle());
}
{
NewTerminalArgs args{};
args.Commandline(L" imagine a man");
const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, args, nullptr) };
VERIFY_ARE_EQUAL(L"", settingsStruct.DefaultSettings().StartingTitle());
}
}
}

View file

@ -56,7 +56,7 @@ namespace TerminalAppLocalTests
TEST_METHOD(ParseComboCommandlineIntoArgs);
TEST_METHOD(ParseFocusTabArgs);
TEST_METHOD(ParseMoveFocusArgs);
TEST_METHOD(ParseMovePaneArgs);
TEST_METHOD(ParseSwapPaneArgs);
TEST_METHOD(ParseArgumentsWithParsingTerminators);
TEST_METHOD(ParseFocusPaneArgs);
@ -1208,14 +1208,9 @@ namespace TerminalAppLocalTests
}
}
void CommandlineTest::ParseMovePaneArgs()
void CommandlineTest::ParseSwapPaneArgs()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:useShortForm", L"{false, true}")
END_TEST_METHOD_PROPERTIES()
INIT_TEST_PROPERTY(bool, useShortForm, L"If true, use `mp` instead of `move-pane`");
const wchar_t* subcommand = useShortForm ? L"mp" : L"move-pane";
const wchar_t* subcommand = L"swap-pane";
{
AppCommandlineArgs appArgs{};
@ -1236,9 +1231,9 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
auto myArgs = actionAndArgs.Args().try_as<SwapPaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction());
}
@ -1253,9 +1248,9 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
auto myArgs = actionAndArgs.Args().try_as<SwapPaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction());
}
@ -1270,9 +1265,9 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
auto myArgs = actionAndArgs.Args().try_as<SwapPaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Up, myArgs.Direction());
}
@ -1287,9 +1282,9 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
auto myArgs = actionAndArgs.Args().try_as<SwapPaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Down, myArgs.Direction());
}
@ -1311,16 +1306,16 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
auto myArgs = actionAndArgs.Args().try_as<SwapPaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction());
actionAndArgs = appArgs._startupActions.at(2);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
myArgs = actionAndArgs.Args().try_as<SwapPaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction());
}

View file

@ -82,7 +82,7 @@ namespace TerminalAppLocalTests
TEST_METHOD(MoveFocusFromZoomedPane);
TEST_METHOD(CloseZoomedPane);
TEST_METHOD(MovePanes);
TEST_METHOD(SwapPanes);
TEST_METHOD(NextMRUTab);
TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder);
@ -430,7 +430,7 @@ namespace TerminalAppLocalTests
Log::Comment(L"Duplicate the tab, and don't crash");
result = RunOnUIThread([&page]() {
page->_DuplicateFocusedTab();
VERIFY_ARE_EQUAL(2u, page->_tabs.Size(), L"We should gracefully do nothing here - the profile no longer exists.");
VERIFY_ARE_EQUAL(3u, page->_tabs.Size(), L"We should successfully duplicate a tab hosting a deleted profile.");
});
VERIFY_SUCCEEDED(result);
}
@ -530,9 +530,9 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(2,
VERIFY_ARE_EQUAL(3,
tab->GetLeafPaneCount(),
L"We should gracefully do nothing here - the profile no longer exists.");
L"We should successfully duplicate a pane hosting a deleted profile.");
});
VERIFY_SUCCEEDED(result);
@ -821,7 +821,7 @@ namespace TerminalAppLocalTests
VERIFY_SUCCEEDED(result);
}
void TabTests::MovePanes()
void TabTests::SwapPanes()
{
auto page = _commonSetup();
@ -914,10 +914,10 @@ namespace TerminalAppLocalTests
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Left };
SwapPaneArgs args{ FocusDirection::Left };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
page->_HandleSwapPane(nullptr, eventArgs);
});
Sleep(250);
@ -945,10 +945,10 @@ namespace TerminalAppLocalTests
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Up };
SwapPaneArgs args{ FocusDirection::Up };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
page->_HandleSwapPane(nullptr, eventArgs);
});
Sleep(250);
@ -976,10 +976,10 @@ namespace TerminalAppLocalTests
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Right };
SwapPaneArgs args{ FocusDirection::Right };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
page->_HandleSwapPane(nullptr, eventArgs);
});
Sleep(250);
@ -1007,10 +1007,10 @@ namespace TerminalAppLocalTests
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Down };
SwapPaneArgs args{ FocusDirection::Down };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
page->_HandleSwapPane(nullptr, eventArgs);
});
Sleep(250);

View file

@ -28,8 +28,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
CATCH_LOG();
}
// This is a private constructor to be used in unit tests, where we don't
// want each Monarch to necessarily use the current PID.
// This constructor is intended to be used in unit tests,
// but we need to make it public in order to use make_self
// in the tests. It's not exposed through the idl though
// so it's not _truly_ fully public which should be acceptable.
Monarch::Monarch(const uint64_t testPID) :
_ourPID{ testPID }
{
@ -78,6 +80,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
peasant.IdentifyWindowsRequested({ this, &Monarch::_identifyWindows });
peasant.RenameRequested({ this, &Monarch::_renameRequested });
peasant.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); });
peasant.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); });
peasant.QuitAllRequested({ this, &Monarch::_handleQuitAll });
_peasants[newPeasantsId] = peasant;
TraceLoggingWrite(g_hRemotingProvider,
@ -86,6 +92,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TraceLoggingUInt64(newPeasantsId, "peasantID", "the ID of the new peasant"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
_WindowCreatedHandlers(nullptr, nullptr);
return newPeasantsId;
}
catch (...)
@ -102,6 +110,75 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
}
}
// Method Description:
// - Gives the host process an opportunity to run any pre-close logic then
// requests all peasants to close.
// Arguments:
// - <none> used
// Return Value:
// - <none>
void Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const winrt::Windows::Foundation::IInspectable& /*args*/)
{
// Let the process hosting the monarch run any needed logic before
// closing all windows.
_QuitAllRequestedHandlers(*this, nullptr);
// Tell all peasants to exit.
const auto callback = [&](const auto& /*id*/, const auto& p) {
p.Quit();
};
const auto onError = [&](const auto& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_handleQuitAll_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not close"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forEachPeasant(callback, onError);
}
// Method Description:
// - Tells the monarch that a peasant is being closed.
// Arguments:
// - peasantId: the id of the peasant
// Return Value:
// - <none>
void Monarch::SignalClose(const uint64_t peasantId)
{
_clearOldMruEntries(peasantId);
_peasants.erase(peasantId);
_WindowClosedHandlers(nullptr, nullptr);
}
// Method Description:
// - Counts the number of living peasants.
// Arguments:
// - <none>
// Return Value:
// - the number of active peasants.
uint64_t Monarch::GetNumberOfPeasants()
{
auto num = 0;
auto callback = [&](const auto& /*id*/, const auto& p) {
// Check that the peasant is alive, and if so increment the count
p.GetID();
num += 1;
};
auto onError = [](const auto& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_GetNumberOfPeasants_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not enumerate"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forEachPeasant(callback, onError);
return num;
}
// Method Description:
// - Event handler for the Peasant::WindowActivated event. Used as an
// opportunity for us to update our internal stack of the "most recent
@ -201,6 +278,12 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
_clearOldMruEntries(id);
}
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_lookupPeasantIdForName",
TraceLoggingWideString(std::wstring{ name }.c_str(), "name", "the name we're looking for"),
TraceLoggingUInt64(result, "peasantID", "the ID of the peasant with that name"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
return result;
}
@ -602,39 +685,6 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
return *result;
}
// Method Description:
// - Helper for doing something on each and every peasant, with no regard
// for if the peasant is living or dead.
// - We'll try calling callback on every peasant.
// - If any single peasant is dead, then we'll call errorCallback, and move on.
// - We're taking an errorCallback here, because the thing we usually want
// to do is TraceLog a message, but TraceLoggingWrite is actually a macro
// that _requires_ the second arg to be a string literal. It can't just be
// a variable.
// Arguments:
// - callback: The function to call on each peasant
// - errorCallback: The function to call if a peasant is dead.
// Return Value:
// - <none>
void Monarch::_forAllPeasantsIgnoringTheDead(std::function<void(const Remoting::IPeasant&, const uint64_t)> callback,
std::function<void(const uint64_t)> errorCallback)
{
for (const auto& [id, p] : _peasants)
{
try
{
callback(p, id);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
// If this fails, we don't _really_ care. Just move on to the
// next one. Someone else will clean up the dead peasant.
errorCallback(id);
}
}
}
// Method Description:
// - This is an event handler for the IdentifyWindowsRequested event. A
// Peasant may raise that event if they want _all_ windows to identify
@ -649,17 +699,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
const winrt::Windows::Foundation::IInspectable& /*args*/)
{
// Notify all the peasants to display their ID.
auto callback = [](auto&& p, auto&& /*id*/) {
const auto callback = [&](const auto& /*id*/, const auto& p) {
p.DisplayWindowId();
};
auto onError = [](auto&& id) {
const auto onError = [&](const auto& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_identifyWindows_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not identify"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forAllPeasantsIgnoringTheDead(callback, onError);
_forEachPeasant(callback, onError);
}
// Method Description:
@ -732,24 +783,55 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
try
{
args.FoundMatch(false);
// If a WindowID is provided from the args, use that first.
uint64_t windowId = 0;
// If no name was provided, then just summon the MRU window.
if (searchedForName.empty())
if (args.WindowID())
{
// Use the value of the `desktop` arg to determine if we should
// limit to the current desktop (desktop:onCurrent) or not
// (desktop:any or desktop:toCurrent)
windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false);
windowId = args.WindowID().Value();
}
else
{
// Try to find a peasant that currently has this name
windowId = _lookupPeasantIdForName(searchedForName);
// If no name was provided, then just summon the MRU window.
if (searchedForName.empty())
{
// Use the value of the `desktop` arg to determine if we should
// limit to the current desktop (desktop:onCurrent) or not
// (desktop:any or desktop:toCurrent)
windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false);
}
else
{
// Try to find a peasant that currently has this name
windowId = _lookupPeasantIdForName(searchedForName);
}
}
if (auto targetPeasant{ _getPeasant(windowId) })
{
targetPeasant.Summon(args.SummonBehavior());
args.FoundMatch(true);
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_SummonWindow_Success",
TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"),
TraceLoggingUInt64(windowId, "peasantID", "The id of the window we tried to summon"),
TraceLoggingBoolean(args.OnCurrentDesktop(), "OnCurrentDesktop", "true iff the window needs to be on the current virtual desktop"),
TraceLoggingBoolean(args.SummonBehavior().MoveToCurrentDesktop(), "MoveToCurrentDesktop", "if true, move the window to the current virtual desktop"),
TraceLoggingBoolean(args.SummonBehavior().ToggleVisibility(), "ToggleVisibility", "true if we should toggle the visibility of the window"),
TraceLoggingUInt32(args.SummonBehavior().DropdownDuration(), "DropdownDuration", "the duration to dropdown the window"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
else
{
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_SummonWindow_NoPeasant",
TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"),
TraceLoggingUInt64(windowId, "peasantID", "The id of the window we tried to summon"),
TraceLoggingBoolean(args.OnCurrentDesktop(), "OnCurrentDesktop", "true iff the window needs to be on the current virtual desktop"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
}
catch (...)
@ -762,4 +844,76 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
}
// Method Description:
// - This method creates a map of peasant IDs to peasant names
// while removing dead peasants.
// Arguments:
// - <none>
// Return Value:
// - A map of peasant IDs to their names.
Windows::Foundation::Collections::IVectorView<PeasantInfo> Monarch::GetPeasantInfos()
{
std::vector<PeasantInfo> names;
names.reserve(_peasants.size());
const auto func = [&](const auto& id, const auto& p) -> void {
names.push_back({ id, p.WindowName(), p.ActiveTabTitle() });
};
const auto onError = [&](const auto& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_identifyWindows_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not identify"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forEachPeasant(func, onError);
return winrt::single_threaded_vector<PeasantInfo>(std::move(names)).GetView();
}
bool Monarch::DoesQuakeWindowExist()
{
bool result = false;
const auto func = [&](const auto& /*id*/, const auto& p) {
if (p.WindowName() == QuakeWindowName)
{
result = true;
}
// continue if we didn't get a positive result
return !result;
};
const auto onError = [&](const auto& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_DoesQuakeWindowExist_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not ask for its name"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forEachPeasant(func, onError);
return result;
}
void Monarch::SummonAllWindows()
{
const auto func = [&](const auto& /*id*/, const auto& p) {
SummonWindowBehavior args{};
args.ToggleVisibility(false);
p.Summon(args);
};
const auto onError = [&](const auto& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_SummonAll_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not summon"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forEachPeasant(func, onError);
}
}

View file

@ -41,20 +41,32 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
struct Monarch : public MonarchT<Monarch>
{
Monarch();
Monarch(const uint64_t testPID);
~Monarch();
uint64_t GetPID();
uint64_t AddPeasant(winrt::Microsoft::Terminal::Remoting::IPeasant peasant);
void SignalClose(const uint64_t peasantId);
uint64_t GetNumberOfPeasants();
winrt::Microsoft::Terminal::Remoting::ProposeCommandlineResult ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args);
void HandleActivatePeasant(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args);
void SummonWindow(const Remoting::SummonWindowSelectionArgs& args);
void SummonAllWindows();
bool DoesQuakeWindowExist();
Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Remoting::PeasantInfo> GetPeasantInfos();
TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
private:
Monarch(const uint64_t testPID);
uint64_t _ourPID;
uint64_t _nextPeasantID{ 1 };
@ -84,6 +96,70 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args);
void _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
// Method Description:
// - Helper for doing something on each and every peasant.
// - We'll try calling func on every peasant.
// - If the return type of func is void, it will perform it for all peasants.
// - If the return type is a boolean, we'll break out of the loop once func
// returns false.
// - If any single peasant is dead, then we'll call onError and then add it to a
// list of peasants to clean up once the loop ends.
// Arguments:
// - func: The function to call on each peasant
// - onError: The function to call if a peasant is dead.
// Return Value:
// - <none>
template<typename F, typename T>
void _forEachPeasant(F&& func, T&& onError)
{
using Map = decltype(_peasants);
using R = std::invoke_result_t<F, Map::key_type, Map::mapped_type>;
static constexpr auto IsVoid = std::is_void_v<R>;
std::vector<uint64_t> peasantsToErase;
for (const auto& [id, p] : _peasants)
{
try
{
if constexpr (IsVoid)
{
func(id, p);
}
else
{
if (!func(id, p))
{
break;
}
}
}
catch (const winrt::hresult_error& exception)
{
onError(id);
if (exception.code() == 0x800706ba) // The RPC server is unavailable.
{
peasantsToErase.emplace_back(id);
}
else
{
LOG_CAUGHT_EXCEPTION();
throw;
}
}
}
for (const auto& id : peasantsToErase)
{
_peasants.erase(id);
_clearOldMruEntries(id);
}
}
friend class RemotingUnitTests::RemotingTests;
};
}

View file

@ -28,18 +28,36 @@ namespace Microsoft.Terminal.Remoting
Boolean FoundMatch;
SummonWindowBehavior SummonBehavior;
Windows.Foundation.IReference<UInt64> WindowID;
}
struct PeasantInfo
{
UInt64 Id;
String Name;
String TabTitle;
};
[default_interface] runtimeclass Monarch {
Monarch();
UInt64 GetPID();
UInt64 AddPeasant(IPeasant peasant);
UInt64 GetNumberOfPeasants();
ProposeCommandlineResult ProposeCommandline(CommandlineArgs args);
void HandleActivatePeasant(WindowActivatedArgs args);
void SummonWindow(SummonWindowSelectionArgs args);
void SignalClose(UInt64 peasantId);
void SummonAllWindows();
Boolean DoesQuakeWindowExist();
Windows.Foundation.Collections.IVectorView<PeasantInfo> GetPeasantInfos { get; };
event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> HideNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowCreated;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowClosed;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitAllRequested;
};
}

View file

@ -20,8 +20,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
{
}
// This is a private constructor to be used in unit tests, where we don't
// want each Peasant to necessarily use the current PID.
// This constructor is intended to be used in unit tests,
// but we need to make it public in order to use make_self
// in the tests. It's not exposed through the idl though
// so it's not _truly_ fully public which should be acceptable.
Peasant::Peasant(const uint64_t testPID) :
_ourPID{ testPID }
{
@ -31,6 +33,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
{
_id = id;
}
uint64_t Peasant::GetID()
{
return _id;
@ -222,4 +225,68 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
void Peasant::RequestShowNotificationIcon()
{
try
{
_ShowNotificationIconRequestedHandlers(*this, nullptr);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
TraceLoggingWrite(g_hRemotingProvider,
"Peasant_RequestShowNotificationIcon",
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
void Peasant::RequestHideNotificationIcon()
{
try
{
_HideNotificationIconRequestedHandlers(*this, nullptr);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
TraceLoggingWrite(g_hRemotingProvider,
"Peasant_RequestHideNotificationIcon",
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
void Peasant::RequestQuitAll()
{
try
{
_QuitAllRequestedHandlers(*this, nullptr);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
TraceLoggingWrite(g_hRemotingProvider,
"Peasant_RequestQuit",
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
void Peasant::Quit()
{
try
{
_QuitRequestedHandlers(*this, nullptr);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
TraceLoggingWrite(g_hRemotingProvider,
"Peasant_Quit",
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
}

View file

@ -28,11 +28,16 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
void RequestIdentifyWindows();
void DisplayWindowId();
void RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args);
void RequestShowNotificationIcon();
void RequestHideNotificationIcon();
void RequestQuitAll();
void Quit();
winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs();
winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs();
WINRT_PROPERTY(winrt::hstring, WindowName);
WINRT_PROPERTY(winrt::hstring, ActiveTabTitle);
TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs);
TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::CommandlineArgs);
@ -40,6 +45,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs);
TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior);
TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
private:
Peasant(const uint64_t testPID);

View file

@ -61,9 +61,14 @@ namespace Microsoft.Terminal.Remoting
void DisplayWindowId(); // Tells us to display its own ID (which causes a DisplayWindowIdRequested to be raised)
String WindowName { get; };
String ActiveTabTitle { get; };
void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested
void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested
void Summon(SummonWindowBehavior behavior);
void RequestShowNotificationIcon();
void RequestHideNotificationIcon();
void RequestQuitAll();
void Quit();
event Windows.Foundation.TypedEventHandler<Object, WindowActivatedArgs> WindowActivated;
event Windows.Foundation.TypedEventHandler<Object, CommandlineArgs> ExecuteCommandlineRequested;
@ -71,6 +76,10 @@ namespace Microsoft.Terminal.Remoting
event Windows.Foundation.TypedEventHandler<Object, Object> DisplayWindowIdRequested;
event Windows.Foundation.TypedEventHandler<Object, RenameRequestArgs> RenameRequested;
event Windows.Foundation.TypedEventHandler<Object, SummonWindowBehavior> SummonRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> HideNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitAllRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
};
[default_interface] runtimeclass Peasant : IPeasant

View file

@ -34,6 +34,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
WINRT_PROPERTY(bool, FoundMatch, false);
WINRT_PROPERTY(bool, OnCurrentDesktop, false);
WINRT_PROPERTY(SummonWindowBehavior, SummonBehavior);
WINRT_PROPERTY(Windows::Foundation::IReference<uint64_t>, WindowID);
};
}

View file

@ -54,6 +54,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
// monarch!
CoRevokeClassObject(_registrationHostClass);
_registrationHostClass = 0;
SignalClose();
_monarchWaitInterrupt.SetEvent();
// A thread is joinable once it's been started. Basically this just
@ -64,6 +65,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
}
}
void WindowManager::SignalClose()
{
if (_monarch)
{
try
{
_monarch.SignalClose(_peasant.GetID());
}
CATCH_LOG()
}
}
void WindowManager::ProposeCommandline(const Remoting::CommandlineArgs& args)
{
// If we're the king, we _definitely_ want to process the arguments, we were
@ -250,10 +263,15 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
// Here, we're the king!
//
// This is where you should do any additional setup that might need to be
// done when we become the king. THis will be called both for the first
// done when we become the king. This will be called both for the first
// window, and when the current monarch dies.
_monarch.WindowCreated({ get_weak(), &WindowManager::_WindowCreatedHandlers });
_monarch.WindowClosed({ get_weak(), &WindowManager::_WindowClosedHandlers });
_monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested });
_monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); });
_monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); });
_monarch.QuitAllRequested([this](auto&&, auto&&) { _QuitAllRequestedHandlers(*this, nullptr); });
_BecameMonarchHandlers(*this, nullptr);
}
@ -509,4 +527,79 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
_monarch.SummonWindow(args);
}
void WindowManager::SummonAllWindows()
{
if constexpr (Feature_NotificationIcon::IsEnabled())
{
_monarch.SummonAllWindows();
}
}
Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Remoting::PeasantInfo> WindowManager::GetPeasantInfos()
{
// We should only get called when we're the monarch since the monarch
// is the only one that knows about all peasants.
return _monarch.GetPeasantInfos();
}
uint64_t WindowManager::GetNumberOfPeasants()
{
if (_monarch)
{
try
{
return _monarch.GetNumberOfPeasants();
}
CATCH_LOG()
}
return 0;
}
// Method Description:
// - Ask the monarch to show a notification icon.
// Arguments:
// - <none>
// Return Value:
// - <none>
winrt::fire_and_forget WindowManager::RequestShowNotificationIcon()
{
co_await winrt::resume_background();
_peasant.RequestShowNotificationIcon();
}
// Method Description:
// - Ask the monarch to hide its notification icon.
// Arguments:
// - <none>
// Return Value:
// - <none>
winrt::fire_and_forget WindowManager::RequestHideNotificationIcon()
{
auto strongThis{ get_strong() };
co_await winrt::resume_background();
_peasant.RequestHideNotificationIcon();
}
// Method Description:
// - Ask the monarch to quit all windows.
// Arguments:
// - <none>
// Return Value:
// - <none>
winrt::fire_and_forget WindowManager::RequestQuitAll()
{
auto strongThis{ get_strong() };
co_await winrt::resume_background();
_peasant.RequestQuitAll();
}
bool WindowManager::DoesQuakeWindowExist()
{
return _monarch.DoesQuakeWindowExist();
}
void WindowManager::UpdateActiveTabTitle(winrt::hstring title)
{
winrt::get_self<implementation::Peasant>(_peasant)->ActiveTabTitle(title);
}
}

View file

@ -39,9 +39,25 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
winrt::Microsoft::Terminal::Remoting::Peasant CurrentWindow();
bool IsMonarch();
void SummonWindow(const Remoting::SummonWindowSelectionArgs& args);
void SignalClose();
void SummonAllWindows();
uint64_t GetNumberOfPeasants();
Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Remoting::PeasantInfo> GetPeasantInfos();
winrt::fire_and_forget RequestShowNotificationIcon();
winrt::fire_and_forget RequestHideNotificationIcon();
winrt::fire_and_forget RequestQuitAll();
bool DoesQuakeWindowExist();
void UpdateActiveTabTitle(winrt::hstring title);
TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
private:
bool _shouldCreateWindow{ false };

View file

@ -8,11 +8,25 @@ namespace Microsoft.Terminal.Remoting
{
WindowManager();
void ProposeCommandline(CommandlineArgs args);
void SignalClose();
Boolean ShouldCreateWindow { get; };
IPeasant CurrentWindow();
Boolean IsMonarch { get; };
void SummonWindow(SummonWindowSelectionArgs args);
void SummonAllWindows();
void RequestShowNotificationIcon();
void RequestHideNotificationIcon();
UInt64 GetNumberOfPeasants();
void RequestQuitAll();
void UpdateActiveTabTitle(String title);
Boolean DoesQuakeWindowExist();
Windows.Foundation.Collections.IVectorView<PeasantInfo> GetPeasantInfos();
event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> BecameMonarch;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowCreated;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowClosed;
event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> HideNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitAllRequested;
};
}

View file

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

View file

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

View file

@ -45,7 +45,7 @@
Color="{ThemeResource SystemErrorTextColor}" />
<!-- Suppress top padding -->
<Thickness x:Key="TabViewHeaderPadding">8,0,8,0</Thickness>
<Thickness x:Key="TabViewHeaderPadding">9,0,8,0</Thickness>
<!-- Remove when implementing WinUI 2.6 -->
<Thickness x:Key="FlyoutContentPadding">12</Thickness>
@ -55,12 +55,18 @@
<!-- Define resources for Dark mode here -->
<SolidColorBrush x:Key="TabViewBackground"
Color="#FF333333" />
<SolidColorBrush x:Key="UnfocusedBorderBrush"
Color="#FF333333" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<!-- Define resources for Light mode here -->
<SolidColorBrush x:Key="TabViewBackground"
Color="#FFCCCCCC" />
<SolidColorBrush x:Key="UnfocusedBorderBrush"
Color="#FFCCCCCC" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

View file

@ -78,7 +78,14 @@ namespace winrt::TerminalApp::implementation
void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
CloseWindow();
CloseWindow(false);
args.Handled(true);
}
void TerminalPage::_HandleQuit(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
RequestQuit();
args.Handled(true);
}
@ -143,6 +150,20 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (args == nullptr)
{
args.Handled(false);
}
else if (const auto& realArgs = args.ActionArgs().try_as<MovePaneArgs>())
{
auto moved = _MovePane(realArgs.TabIndex());
args.Handled(moved);
}
}
void TerminalPage::_HandleSplitPane(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
@ -264,12 +285,12 @@ namespace winrt::TerminalApp::implementation
{
if (args == nullptr)
{
_OpenNewTab(nullptr);
LOG_IF_FAILED(_OpenNewTab(nullptr));
args.Handled(true);
}
else if (const auto& realArgs = args.ActionArgs().try_as<NewTabArgs>())
{
_OpenNewTab(realArgs.TerminalArgs());
LOG_IF_FAILED(_OpenNewTab(realArgs.TerminalArgs()));
args.Handled(true);
}
}
@ -323,10 +344,10 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/,
void TerminalPage::_HandleSwapPane(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (const auto& realArgs = args.ActionArgs().try_as<MovePaneArgs>())
if (const auto& realArgs = args.ActionArgs().try_as<SwapPaneArgs>())
{
if (realArgs.Direction() == FocusDirection::None)
{
@ -335,8 +356,8 @@ namespace winrt::TerminalApp::implementation
}
else
{
_MovePane(realArgs.Direction());
args.Handled(true);
auto swapped = _SwapPane(realArgs.Direction());
args.Handled(swapped);
}
}
}
@ -739,11 +760,10 @@ namespace winrt::TerminalApp::implementation
newTerminalArgs = NewTerminalArgs();
}
const auto profileGuid{ _settings.GetProfileForArgs(newTerminalArgs) };
const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) };
const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
// Manually fill in the evaluated profile.
newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profileGuid));
newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid()));
_OpenNewWindow(false, newTerminalArgs);
actionArgs.Handled(true);
}
@ -861,4 +881,44 @@ namespace winrt::TerminalApp::implementation
}
}
}
void TerminalPage::_HandleOpenSystemMenu(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
_OpenSystemMenuHandlers(*this, nullptr);
args.Handled(true);
}
void TerminalPage::_HandleClearBuffer(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (args)
{
if (const auto& realArgs = args.ActionArgs().try_as<ClearBufferArgs>())
{
if (const auto termControl{ _GetActiveControl() })
{
termControl.ClearBuffer(realArgs.Clear());
args.Handled(true);
}
}
}
}
void TerminalPage::_HandleMultipleActions(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (args)
{
if (const auto& realArgs = args.ActionArgs().try_as<MultipleActionsArgs>())
{
for (const auto& action : realArgs.Actions())
{
_actionDispatch->DoAction(action);
}
args.Handled(true);
}
}
}
}

View file

@ -193,6 +193,7 @@ void AppCommandlineArgs::_buildParser()
_buildFocusTabParser();
_buildMoveFocusParser();
_buildMovePaneParser();
_buildSwapPaneParser();
_buildFocusPaneParser();
}
@ -297,6 +298,43 @@ void AppCommandlineArgs::_buildSplitPaneParser()
setupSubcommand(_newPaneCommand);
setupSubcommand(_newPaneShort);
}
// Method Description:
// - Adds the `move-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMovePaneParser()
{
_movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc"));
_movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc"));
auto setupSubcommand = [this](auto* subcommand) {
subcommand->add_option("-t,--tab",
_movePaneTabIndex,
RS_A(L"CmdMovePaneTabArgDesc"));
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
// that `this` will still be safe - this function just lets us know this
// command was parsed.
subcommand->callback([&, this]() {
// Build the action from the values we've parsed on the commandline.
ActionAndArgs movePaneAction{};
if (_movePaneTabIndex >= 0)
{
movePaneAction.Action(ShortcutAction::MovePane);
MovePaneArgs args{ static_cast<unsigned int>(_movePaneTabIndex) };
movePaneAction.Args(args);
_startupActions.push_back(movePaneAction);
}
});
};
setupSubcommand(_movePaneCommand);
setupSubcommand(_movePaneShort);
}
// Method Description:
// - Adds the `focus-tab` subcommand and related options to the commandline parser.
@ -342,6 +380,11 @@ void AppCommandlineArgs::_buildFocusTabParser()
else if (_focusNextTab || _focusPrevTab)
{
focusTabAction.Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab);
// GH#10070 - make sure to not use the MRU order when switching
// tabs on the commandline. That wouldn't make any sense!
focusTabAction.Args(_focusNextTab ?
static_cast<IActionArgs>(NextTabArgs(TabSwitcherMode::Disabled)) :
static_cast<IActionArgs>(PrevTabArgs(TabSwitcherMode::Disabled)));
_startupActions.push_back(std::move(focusTabAction));
}
});
@ -351,6 +394,17 @@ void AppCommandlineArgs::_buildFocusTabParser()
setupSubcommand(_focusTabShort);
}
static const std::map<std::string, FocusDirection> focusDirectionMap = {
{ "left", FocusDirection::Left },
{ "right", FocusDirection::Right },
{ "up", FocusDirection::Up },
{ "down", FocusDirection::Down },
{ "previous", FocusDirection::Previous },
{ "nextInOrder", FocusDirection::NextInOrder },
{ "previousInOrder", FocusDirection::PreviousInOrder },
{ "first", FocusDirection::First },
};
// Method Description:
// - Adds the `move-focus` subcommand and related options to the commandline parser.
// - Additionally adds the `mf` subcommand, which is just a shortened version of `move-focus`
@ -364,18 +418,11 @@ void AppCommandlineArgs::_buildMoveFocusParser()
_moveFocusShort = _app.add_subcommand("mf", RS_A(L"CmdMFDesc"));
auto setupSubcommand = [this](auto* subcommand) {
std::map<std::string, FocusDirection> map = {
{ "left", FocusDirection::Left },
{ "right", FocusDirection::Right },
{ "up", FocusDirection::Up },
{ "down", FocusDirection::Down }
};
auto* directionOpt = subcommand->add_option("direction",
_moveFocusDirection,
RS_A(L"CmdMoveFocusDirectionArgDesc"));
directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case));
directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case));
directionOpt->required();
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
@ -400,42 +447,33 @@ void AppCommandlineArgs::_buildMoveFocusParser()
}
// Method Description:
// - Adds the `move-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane`
// - Adds the `swap-pane` subcommand and related options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMovePaneParser()
void AppCommandlineArgs::_buildSwapPaneParser()
{
_movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc"));
_movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc"));
_swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc"));
auto setupSubcommand = [this](auto* subcommand) {
std::map<std::string, FocusDirection> map = {
{ "left", FocusDirection::Left },
{ "right", FocusDirection::Right },
{ "up", FocusDirection::Up },
{ "down", FocusDirection::Down }
};
auto* directionOpt = subcommand->add_option("direction",
_movePaneDirection,
RS_A(L"CmdMovePaneDirectionArgDesc"));
_swapPaneDirection,
RS_A(L"CmdSwapPaneDirectionArgDesc"));
directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case));
directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case));
directionOpt->required();
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
// that `this` will still be safe - this function just lets us know this
// command was parsed.
subcommand->callback([&, this]() {
if (_movePaneDirection != FocusDirection::None)
if (_swapPaneDirection != FocusDirection::None)
{
MovePaneArgs args{ _movePaneDirection };
SwapPaneArgs args{ _swapPaneDirection };
ActionAndArgs actionAndArgs{};
actionAndArgs.Action(ShortcutAction::MovePane);
actionAndArgs.Action(ShortcutAction::SwapPane);
actionAndArgs.Args(args);
_startupActions.push_back(std::move(actionAndArgs));
@ -443,8 +481,7 @@ void AppCommandlineArgs::_buildMovePaneParser()
});
};
setupSubcommand(_movePaneCommand);
setupSubcommand(_movePaneShort);
setupSubcommand(_swapPaneCommand);
}
// Method Description:
@ -625,6 +662,7 @@ bool AppCommandlineArgs::_noCommandsProvided()
*_moveFocusShort ||
*_movePaneCommand ||
*_movePaneShort ||
*_swapPaneCommand ||
*_focusPaneCommand ||
*_focusPaneShort ||
*_newPaneShort.subcommand ||
@ -653,12 +691,13 @@ void AppCommandlineArgs::_resetStateToDefault()
_splitPaneSize = 0.5f;
_splitDuplicate = false;
_movePaneTabIndex = -1;
_focusTabIndex = -1;
_focusNextTab = false;
_focusPrevTab = false;
_moveFocusDirection = FocusDirection::None;
_movePaneDirection = FocusDirection::None;
_swapPaneDirection = FocusDirection::None;
_focusPaneTarget = -1;

View file

@ -84,6 +84,7 @@ private:
CLI::App* _moveFocusShort;
CLI::App* _movePaneCommand;
CLI::App* _movePaneShort;
CLI::App* _swapPaneCommand;
CLI::App* _focusPaneCommand;
CLI::App* _focusPaneShort;
@ -97,7 +98,7 @@ private:
bool _suppressApplicationTitle{ false };
winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None };
winrt::Microsoft::Terminal::Settings::Model::FocusDirection _movePaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None };
winrt::Microsoft::Terminal::Settings::Model::FocusDirection _swapPaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None };
// _commandline will contain the command line with which we'll be spawning a new terminal
std::vector<std::string> _commandline;
@ -107,6 +108,7 @@ private:
bool _splitDuplicate{ false };
float _splitPaneSize{ 0.5f };
int _movePaneTabIndex{ -1 };
int _focusTabIndex{ -1 };
bool _focusNextTab{ false };
bool _focusPrevTab{ false };
@ -132,6 +134,7 @@ private:
void _buildFocusTabParser();
void _buildMoveFocusParser();
void _buildMovePaneParser();
void _buildSwapPaneParser();
void _buildFocusPaneParser();
bool _noCommandsProvided();
void _resetStateToDefault();

View file

@ -21,6 +21,11 @@ namespace winrt::TerminalApp::implementation
return false;
}
bool AppKeyBindings::IsKeyChordExplicitlyUnbound(const KeyChord& kc)
{
return _actionMap.IsKeyChordExplicitlyUnbound(kc);
}
void AppKeyBindings::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch)
{
_dispatch = dispatch;

View file

@ -20,6 +20,7 @@ namespace winrt::TerminalApp::implementation
AppKeyBindings() = default;
bool TryKeyChord(winrt::Microsoft::Terminal::Control::KeyChord const& kc);
bool IsKeyChordExplicitlyUnbound(winrt::Microsoft::Terminal::Control::KeyChord const& kc);
void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch);
void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap);

View file

@ -203,12 +203,16 @@ namespace winrt::TerminalApp::implementation
_isElevated = _isUserAdmin();
_root = winrt::make_self<TerminalPage>();
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(_root->Dispatcher(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() {
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() {
if (auto self{ weakSelf.get() })
{
self->_ReloadSettings();
}
});
_languageProfileNotifier = winrt::make_self<LanguageProfileNotifier>([this]() {
_reloadSettings->Run();
});
}
// Method Description:
@ -325,6 +329,14 @@ namespace winrt::TerminalApp::implementation
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
}
void AppLogic::Quit()
{
if (_root)
{
_root->CloseWindow(true);
}
}
// Method Description:
// - Show a ContentDialog with buttons to take further action. Uses the
// FrameworkElements provided as the title and content of this dialog, and
@ -592,12 +604,30 @@ namespace winrt::TerminalApp::implementation
LoadSettings();
}
// Use the default profile to determine how big of a window we need.
const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) };
auto proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), dpi);
winrt::Windows::Foundation::Size proposedSize{};
const float scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
if (_root->ShouldUsePersistedLayout(_settings))
{
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize())
{
proposedSize = layouts.GetAt(0).InitialSize().Value();
// The size is saved as a non-scaled real pixel size,
// so we need to scale it appropriately.
proposedSize.Height = proposedSize.Height * scale;
proposedSize.Width = proposedSize.Width * scale;
}
}
if (proposedSize.Width == 0 && proposedSize.Height == 0)
{
// Use the default profile to determine how big of a window we need.
const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) };
proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), dpi);
}
// GH#2061 - If the global setting "Always show tab bar" is
// set or if "Show tabs in title bar" is set, then we'll need to add
@ -679,7 +709,18 @@ namespace winrt::TerminalApp::implementation
LoadSettings();
}
const auto initialPosition{ _settings.GlobalSettings().InitialPosition() };
auto initialPosition{ _settings.GlobalSettings().InitialPosition() };
if (_root->ShouldUsePersistedLayout(_settings))
{
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition())
{
initialPosition = layouts.GetAt(0).InitialPosition().Value();
}
}
return {
initialPosition.X ? initialPosition.X.Value() : defaultInitialX,
initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY
@ -1121,32 +1162,15 @@ namespace winrt::TerminalApp::implementation
{
if (_root)
{
_root->CloseWindow();
_root->CloseWindow(false);
}
}
// Method Description:
// - Gets the taskbar state value from the last active control
// Return Value:
// - The taskbar state of the last active control
uint64_t AppLogic::GetLastActiveControlTaskbarState()
winrt::TerminalApp::TaskbarState AppLogic::TaskbarState()
{
if (_root)
{
return _root->GetLastActiveControlTaskbarState();
}
return {};
}
// Method Description:
// - Gets the taskbar progress value from the last active control
// Return Value:
// - The taskbar progress of the last active control
uint64_t AppLogic::GetLastActiveControlTaskbarProgress()
{
if (_root)
{
return _root->GetLastActiveControlTaskbarProgress();
return _root->TaskbarState();
}
return {};
}
@ -1229,6 +1253,11 @@ namespace winrt::TerminalApp::implementation
auto actions = winrt::single_threaded_vector<ActionAndArgs>(std::move(appArgs.GetStartupActions()));
_root->ProcessStartupActions(actions, false, cwd);
if (appArgs.IsHandoffListener())
{
_root->SetInboundListener(true);
}
}
// Return the result of parsing with commandline, though it may or may not be used.
return result;
@ -1437,6 +1466,14 @@ namespace winrt::TerminalApp::implementation
}
}
void AppLogic::SetNumberOfOpenWindows(const uint64_t num)
{
if (_root)
{
_root->SetNumberOfOpenWindows(num);
}
}
void AppLogic::RenameFailed()
{
if (_root)
@ -1450,4 +1487,44 @@ namespace winrt::TerminalApp::implementation
return _root->IsQuakeWindow();
}
bool AppLogic::GetMinimizeToNotificationArea()
{
if constexpr (Feature_NotificationIcon::IsEnabled())
{
if (!_loadedInitialSettings)
{
// Load settings if we haven't already
LoadSettings();
}
return _settings.GlobalSettings().MinimizeToNotificationArea();
}
else
{
return false;
}
}
bool AppLogic::GetAlwaysShowNotificationIcon()
{
if constexpr (Feature_NotificationIcon::IsEnabled())
{
if (!_loadedInitialSettings)
{
// Load settings if we haven't already
LoadSettings();
}
return _settings.GlobalSettings().AlwaysShowNotificationIcon();
}
else
{
return false;
}
}
bool AppLogic::GetShowTitleInTitlebar()
{
return _settings.GlobalSettings().ShowTitleInTitlebar();
}
}

View file

@ -5,8 +5,9 @@
#include "AppLogic.g.h"
#include "FindTargetWindowResult.g.h"
#include "TerminalPage.h"
#include "Jumplist.h"
#include "LanguageProfileNotifier.h"
#include "TerminalPage.h"
#include <inc/cppwinrt_utils.h>
#include <ThrottledFunc.h>
@ -52,6 +53,8 @@ namespace winrt::TerminalApp::implementation
void LoadSettings();
[[nodiscard]] Microsoft::Terminal::Settings::Model::CascadiaSettings GetSettings() const noexcept;
void Quit();
int32_t SetStartupCommandline(array_view<const winrt::hstring> actions);
int32_t ExecuteCommandline(array_view<const winrt::hstring> actions, const winrt::hstring& cwd);
TerminalApp::FindTargetWindowResult FindTargetWindow(array_view<const winrt::hstring> actions);
@ -68,6 +71,7 @@ namespace winrt::TerminalApp::implementation
void WindowName(const winrt::hstring& name);
uint64_t WindowId();
void WindowId(const uint64_t& id);
void SetNumberOfOpenWindows(const uint64_t num);
bool IsQuakeWindow() const noexcept;
Windows::Foundation::Size GetLaunchDimensions(uint32_t dpi);
@ -89,8 +93,11 @@ namespace winrt::TerminalApp::implementation
void WindowCloseButtonClicked();
uint64_t GetLastActiveControlTaskbarState();
uint64_t GetLastActiveControlTaskbarProgress();
winrt::TerminalApp::TaskbarState TaskbarState();
bool GetMinimizeToNotificationArea();
bool GetAlwaysShowNotificationIcon();
bool GetShowTitleInTitlebar();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog);
@ -110,12 +117,8 @@ namespace winrt::TerminalApp::implementation
// ALSO: If you add any UIElements as roots here, make sure they're
// updated in _ApplyTheme. The root currently is _root.
winrt::com_ptr<TerminalPage> _root{ nullptr };
Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };
wil::unique_folder_change_reader_nothrow _reader;
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
til::throttled_func_trailing<> _reloadState;
winrt::hstring _settingsLoadExceptionText;
HRESULT _settingsLoadedResult = S_OK;
bool _loadedInitialSettings = false;
@ -124,6 +127,15 @@ namespace winrt::TerminalApp::implementation
::TerminalApp::AppCommandlineArgs _appArgs;
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
til::throttled_func_trailing<> _reloadState;
// These fields invoke _reloadSettings and must be destroyed before _reloadSettings.
// (C++ destroys members in reverse-declaration-order.)
winrt::com_ptr<LanguageProfileNotifier> _languageProfileNotifier;
wil::unique_folder_change_reader_nothrow _reader;
static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view<const hstring> args,
const Microsoft::Terminal::Settings::Model::WindowingMode& windowingBehavior);
@ -163,6 +175,8 @@ namespace winrt::TerminalApp::implementation
FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested);
FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged);
FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested);
FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu);
FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested);
#ifdef UNIT_TESTING
friend class TerminalAppLocalTests::CommandlineTest;

View file

@ -39,6 +39,8 @@ namespace TerminalApp
String ParseCommandlineMessage { get; };
Boolean ShouldExitEarly { get; };
void Quit();
void LoadSettings();
Windows.UI.Xaml.UIElement GetRoot();
@ -53,6 +55,7 @@ namespace TerminalApp
void IdentifyWindow();
String WindowName;
UInt64 WindowId;
void SetNumberOfOpenWindows(UInt64 num);
void RenameFailed();
Boolean IsQuakeWindow();
@ -68,8 +71,11 @@ namespace TerminalApp
void TitlebarClicked();
void WindowCloseButtonClicked();
UInt64 GetLastActiveControlTaskbarState();
UInt64 GetLastActiveControlTaskbarProgress();
TaskbarState TaskbarState{ get; };
Boolean GetMinimizeToNotificationArea();
Boolean GetAlwaysShowNotificationIcon();
Boolean GetShowTitleInTitlebar();
FindTargetWindowResult FindTargetWindow(String[] args);
@ -93,5 +99,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> SettingsChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
}
}

View file

@ -36,7 +36,6 @@ namespace winrt::TerminalApp::implementation
_allCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_tabActions = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_mruTabActions = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_commandLineHistory = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_switchToMode(CommandPaletteMode::ActionMode);
@ -245,6 +244,17 @@ namespace winrt::TerminalApp::implementation
_PreviewActionHandlers(*this, actionPaletteItem.Command());
}
}
else if (_currentMode == CommandPaletteMode::CommandlineMode)
{
if (filteredCommand)
{
SearchBoxPlaceholderText(filteredCommand.Item().Name());
}
else
{
SearchBoxPlaceholderText(RS_(L"CmdPalCommandlinePrompt"));
}
}
}
void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/,
@ -365,6 +375,17 @@ namespace winrt::TerminalApp::implementation
_searchBox().PasteFromClipboard();
e.Handled(true);
}
else if (key == VirtualKey::Right && _currentMode == CommandPaletteMode::CommandlineMode)
{
if (const auto command{ _filteredActionsView().SelectedItem().try_as<winrt::TerminalApp::FilteredCommand>() })
{
_searchBox().Text(command.Item().Name());
_searchBox().Select(_searchBox().Text().size(), 0);
_searchBox().Focus(FocusState::Programmatic);
_filteredActionsView().SelectedIndex(-1);
e.Handled(true);
}
}
}
// Method Description:
@ -587,7 +608,7 @@ namespace winrt::TerminalApp::implementation
case CommandPaletteMode::TabSwitchMode:
return _tabSwitcherMode == TabSwitcherMode::MostRecentlyUsed ? _mruTabActions : _tabActions;
case CommandPaletteMode::CommandlineMode:
return _commandLineHistory;
return _loadRecentCommands();
default:
return _allCommands;
}
@ -720,14 +741,10 @@ namespace winrt::TerminalApp::implementation
// - <none>
void CommandPalette::_dispatchCommandline(winrt::TerminalApp::FilteredCommand const& command)
{
const auto filteredCommand = command ? command : _buildCommandLineCommand(_getTrimmedInput());
const auto filteredCommand = command ? command : _buildCommandLineCommand(winrt::hstring(_getTrimmedInput()));
if (filteredCommand.has_value())
{
if (_commandLineHistory.Size() == CommandLineHistoryLength)
{
_commandLineHistory.RemoveAtEnd();
}
_commandLineHistory.InsertAt(0, filteredCommand.value());
_updateRecentCommands(filteredCommand.value().Item().Name());
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
@ -744,15 +761,14 @@ namespace winrt::TerminalApp::implementation
}
}
std::optional<winrt::TerminalApp::FilteredCommand> CommandPalette::_buildCommandLineCommand(std::wstring const& commandLine)
std::optional<TerminalApp::FilteredCommand> CommandPalette::_buildCommandLineCommand(const hstring& commandLine)
{
if (commandLine.empty())
{
return std::nullopt;
}
winrt::hstring cl{ commandLine };
auto commandLinePaletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(cl) };
auto commandLinePaletteItem{ winrt::make<CommandLinePaletteItem>(commandLine) };
return winrt::make<FilteredCommand>(commandLinePaletteItem);
}
@ -1217,4 +1233,81 @@ namespace winrt::TerminalApp::implementation
itemContainer.DataContext(args.Item());
}
}
// Method Description:
// - Reads the list of recent commands from the persistent application state
// Return Value:
// - The list of FilteredCommand representing the ones stored in the state
IVector<TerminalApp::FilteredCommand> CommandPalette::_loadRecentCommands()
{
const auto recentCommands = ApplicationState::SharedInstance().RecentCommands();
// If this is the first time we've opened the commandline mode and
// there aren't any recent commands, then just return an empty vector.
if (!recentCommands)
{
return single_threaded_vector<TerminalApp::FilteredCommand>();
}
std::vector<TerminalApp::FilteredCommand> parsedCommands;
parsedCommands.reserve(std::min(recentCommands.Size(), CommandLineHistoryLength));
for (const auto& c : recentCommands)
{
if (parsedCommands.size() >= CommandLineHistoryLength)
{
// Don't load more than CommandLineHistoryLength commands
break;
}
if (const auto parsedCommand = _buildCommandLineCommand(c))
{
parsedCommands.push_back(*parsedCommand);
}
}
return single_threaded_vector(std::move(parsedCommands));
}
// Method Description:
// - Update recent commands by putting the provided command as most recent.
// Upon race condition might override an update made by another window.
// Return Value:
// - <none>
void CommandPalette::_updateRecentCommands(const hstring& command)
{
const auto recentCommands = ApplicationState::SharedInstance().RecentCommands();
// If this is the first time we've opened the commandline mode and
// there aren't any recent commands, then just store the new command.
if (!recentCommands)
{
ApplicationState::SharedInstance().RecentCommands(single_threaded_vector(std::move(std::vector{ command })));
return;
}
const auto numNewRecentCommands = std::min(recentCommands.Size() + 1, CommandLineHistoryLength);
std::vector<hstring> newRecentCommands;
newRecentCommands.reserve(numNewRecentCommands);
std::unordered_set<hstring> uniqueCommands;
uniqueCommands.reserve(numNewRecentCommands);
newRecentCommands.push_back(command);
uniqueCommands.insert(command);
for (const auto& c : recentCommands)
{
if (newRecentCommands.size() >= CommandLineHistoryLength)
{
// Don't store more than CommandLineHistoryLength commands
break;
}
if (uniqueCommands.emplace(c).second)
{
newRecentCommands.push_back(c);
}
}
ApplicationState::SharedInstance().RecentCommands(single_threaded_vector(std::move(newRecentCommands)));
}
}

View file

@ -123,15 +123,16 @@ namespace winrt::TerminalApp::implementation
void _dispatchCommand(winrt::TerminalApp::FilteredCommand const& command);
void _dispatchCommandline(winrt::TerminalApp::FilteredCommand const& command);
void _switchToTab(winrt::TerminalApp::FilteredCommand const& command);
std::optional<winrt::TerminalApp::FilteredCommand> _buildCommandLineCommand(std::wstring const& commandLine);
static std::optional<winrt::TerminalApp::FilteredCommand> _buildCommandLineCommand(const winrt::hstring& commandLine);
void _dismissPalette();
void _scrollToIndex(uint32_t index);
uint32_t _getNumVisibleItems();
static constexpr int CommandLineHistoryLength = 10;
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _commandLineHistory{ nullptr };
static constexpr uint32_t CommandLineHistoryLength = 20;
static Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _loadRecentCommands();
static void _updateRecentCommands(const winrt::hstring& command);
::TerminalApp::AppCommandlineArgs _appArgs;
void _choosingItemContainer(Windows::UI::Xaml::Controls::ListViewBase const& sender, Windows::UI::Xaml::Controls::ChoosingItemContainerEventArgs const& args);

View file

@ -407,7 +407,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Windows10version1903:Shadow="{StaticResource CommandPaletteShadow}"
CornerRadius="{ThemeResource ControlCornerRadius}"
CornerRadius="{ThemeResource OverlayCornerRadius}"
PointerPressed="_backdropPointerPressed"
Style="{ThemeResource CommandPaletteBackground}">

View file

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "LanguageProfileNotifier.h"
using namespace winrt::TerminalApp::implementation;
LanguageProfileNotifier::LanguageProfileNotifier(std::function<void()>&& callback) :
_callback{ std::move(callback) },
_currentKeyboardLayout{ GetKeyboardLayout(0) }
{
const auto manager = wil::CoCreateInstance<ITfThreadMgr>(CLSID_TF_ThreadMgr);
_source = manager.query<ITfSource>();
if (FAILED(_source->AdviseSink(IID_ITfInputProcessorProfileActivationSink, static_cast<ITfInputProcessorProfileActivationSink*>(this), &_cookie)))
{
_cookie = TF_INVALID_COOKIE;
THROW_LAST_ERROR();
}
}
LanguageProfileNotifier::~LanguageProfileNotifier()
{
if (_cookie != TF_INVALID_COOKIE)
{
_source->UnadviseSink(_cookie);
}
}
STDMETHODIMP LanguageProfileNotifier::OnActivated(DWORD /*dwProfileType*/, LANGID /*langid*/, REFCLSID /*clsid*/, REFGUID /*catid*/, REFGUID /*guidProfile*/, HKL hkl, DWORD /*dwFlags*/)
{
if (hkl && hkl != _currentKeyboardLayout)
{
_currentKeyboardLayout = hkl;
try
{
_callback();
}
CATCH_RETURN();
}
return S_OK;
}

View file

@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace winrt::TerminalApp::implementation
{
class LanguageProfileNotifier : public winrt::implements<LanguageProfileNotifier, ITfInputProcessorProfileActivationSink>
{
public:
explicit LanguageProfileNotifier(std::function<void()>&& callback);
~LanguageProfileNotifier();
STDMETHODIMP OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID clsid, REFGUID catid, REFGUID guidProfile, HKL hkl, DWORD dwFlags);
private:
std::function<void()> _callback;
wil::com_ptr<ITfSource> _source;
DWORD _cookie = TF_INVALID_COOKIE;
HKL _currentKeyboardLayout;
};
}

View file

@ -9,7 +9,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
HorizontalAlignment="Left"
VerticalAlignment="Top"
d:DesignHeight="36"
d:DesignHeight="40"
d:DesignWidth="400"
Background="Transparent"
Orientation="Horizontal"
@ -124,8 +124,9 @@
tabs will be flush with the top of the window. See GH#2541 for
details.
-->
<x:Double x:Key="CaptionButtonHeightWindowed">36.0</x:Double>
<x:Double x:Key="CaptionButtonHeightMaximized">32.0</x:Double>
<x:Double x:Key="CaptionButtonHeightWindowed">40.0</x:Double>
<!-- 32 + 1 to compensate for GH#10746 -->
<x:Double x:Key="CaptionButtonHeightMaximized">33.0</x:Double>
<Style x:Key="CaptionButton"
TargetType="Button">

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@
#pragma once
#include "../../cascadia/inc/cppwinrt_utils.h"
#include "TaskbarState.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
@ -28,6 +29,11 @@ namespace TerminalAppLocalTests
class TabTests;
};
namespace winrt::TerminalApp::implementation
{
struct TerminalTab;
}
enum class Borders : int
{
None = 0x0,
@ -41,13 +47,21 @@ DEFINE_ENUM_FLAG_OPERATORS(Borders);
class Pane : public std::enable_shared_from_this<Pane>
{
public:
Pane(const GUID& profile,
Pane(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
const winrt::Microsoft::Terminal::Control::TermControl& control,
const bool lastFocused = false);
std::shared_ptr<Pane> GetActivePane();
winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl();
std::optional<GUID> GetFocusedProfile();
winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile();
// Method Description:
// - If this is a leaf pane, return its profile.
// - If this is a branch/root pane, return nullptr.
winrt::Microsoft::Terminal::Settings::Model::Profile GetProfile() const
{
return _profile;
}
winrt::Windows::UI::Xaml::Controls::Grid GetRootElement();
@ -56,18 +70,32 @@ public:
void ClearActive();
void SetActive();
struct BuildStartupState
{
std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs> args;
std::shared_ptr<Pane> firstPane;
std::optional<uint32_t> focusedPaneId;
uint32_t panesCreated;
};
BuildStartupState BuildStartupActions(uint32_t currentId, uint32_t nextId);
winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs GetTerminalArgsForPane() const;
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings,
const GUID& profile);
const winrt::Microsoft::Terminal::Settings::Model::Profile& profile);
void ResizeContent(const winrt::Windows::Foundation::Size& newSize);
void Relayout();
bool ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
std::shared_ptr<Pane> NavigateDirection(const std::shared_ptr<Pane> sourcePane,
const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
const std::vector<uint32_t>& mruPanes);
bool SwapPanes(std::shared_ptr<Pane> first, std::shared_ptr<Pane> second);
std::shared_ptr<Pane> NextPane(const std::shared_ptr<Pane> pane);
std::shared_ptr<Pane> PreviousPane(const std::shared_ptr<Pane> pane);
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType,
const float splitSize,
const GUID& profile,
const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
const winrt::Microsoft::Terminal::Control::TermControl& control);
bool ToggleSplitOrientation();
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
@ -80,6 +108,10 @@ public:
void Shutdown();
void Close();
std::shared_ptr<Pane> AttachPane(std::shared_ptr<Pane> pane,
winrt::Microsoft::Terminal::Settings::Model::SplitState splitType);
std::shared_ptr<Pane> DetachPane(std::shared_ptr<Pane> pane);
int GetLeafPaneCount() const noexcept;
void Maximize(std::shared_ptr<Pane> zoomedPane);
@ -88,18 +120,47 @@ public:
std::optional<uint32_t> Id() noexcept;
void Id(uint32_t id) noexcept;
bool FocusPane(const uint32_t id);
bool FocusPane(const std::shared_ptr<Pane> pane);
std::shared_ptr<Pane> FindPane(const uint32_t id);
bool ContainsReadOnly() const;
// Method Description:
// - A helper method for ad-hoc recursion on a pane tree. Walks the pane
// tree, calling a predicate on each pane in a depth-first pattern.
// - If the predicate returns true, recursion is stopped early.
// Arguments:
// - f: The function to be applied to each pane.
// Return Value:
// - true if the predicate returned true on any pane.
template<typename F>
//requires std::predicate<F, std::shared_ptr<Pane>>
bool WalkTree(F f)
{
if (f(shared_from_this()))
{
return true;
}
if (!_IsLeaf())
{
return _firstChild->WalkTree(f) || _secondChild->WalkTree(f);
}
return false;
}
void CollectTaskbarStates(std::vector<winrt::TerminalApp::TaskbarState>& states);
WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>);
DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler<bool>);
DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate<std::shared_ptr<Pane>>);
private:
struct PanePoint;
struct FocusNeighborSearch;
struct PaneNeighborSearch;
struct SnapSizeResult;
struct SnapChildrenSizeResult;
struct LayoutSizeNode;
@ -119,7 +180,7 @@ private:
std::optional<uint32_t> _id;
bool _lastActive{ false };
std::optional<GUID> _profile{ std::nullopt };
winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr };
winrt::event_token _connectionStateChangedToken{ 0 };
winrt::event_token _firstClosedToken{ 0 };
winrt::event_token _secondClosedToken{ 0 };
@ -140,8 +201,7 @@ private:
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType,
const float splitSize,
const GUID& profile,
const winrt::Microsoft::Terminal::Control::TermControl& control);
std::shared_ptr<Pane> newPane);
void _CreateRowColDefinitions();
void _ApplySplitDefinitions();
@ -152,15 +212,17 @@ private:
bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
std::shared_ptr<Pane> _FindParentOfPane(const std::shared_ptr<Pane> pane);
bool _IsAdjacent(const std::shared_ptr<Pane> first, const PanePoint firstOffset, const std::shared_ptr<Pane> second, const PanePoint secondOffset, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction) const;
FocusNeighborSearch _FindNeighborForPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
FocusNeighborSearch searchResult,
const bool focusIsSecondSide,
const PanePoint offset);
FocusNeighborSearch _FindFocusAndNeighbor(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
const PanePoint offset);
std::pair<PanePoint, PanePoint> _GetOffsetsForPane(const PanePoint parentOffset) const;
bool _IsAdjacent(const PanePoint firstOffset, const PanePoint secondOffset, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction) const;
PaneNeighborSearch _FindNeighborForPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
PaneNeighborSearch searchResult,
const bool focusIsSecondSide,
const PanePoint offset);
PaneNeighborSearch _FindPaneAndNeighbor(const std::shared_ptr<Pane> sourcePane,
const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
const PanePoint offset);
void _CloseChild(const bool closeFirst);
void _CloseChild(const bool closeFirst, const bool isDetaching);
winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst);
void _FocusFirstChild();
@ -176,7 +238,6 @@ private:
SnapChildrenSizeResult _CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const;
SnapSizeResult _CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
void _AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const;
winrt::Windows::Foundation::Size _GetMinSize() const;
LayoutSizeNode _CreateMinSizeTree(const bool widthOrHeight) const;
float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const;
@ -225,13 +286,15 @@ private:
{
float x;
float y;
float scaleX;
float scaleY;
};
struct FocusNeighborSearch
struct PaneNeighborSearch
{
std::shared_ptr<Pane> focus;
std::shared_ptr<Pane> source;
std::shared_ptr<Pane> neighbor;
PanePoint focusOffset;
PanePoint sourceOffset;
};
struct SnapSizeResult
@ -271,5 +334,6 @@ private:
void _AssignChildNode(std::unique_ptr<LayoutSizeNode>& nodeField, const LayoutSizeNode* const newNode);
};
friend struct winrt::TerminalApp::implementation::TerminalTab;
friend class ::TerminalAppLocalTests::TabTests;
};

View file

@ -184,6 +184,9 @@
<data name="CloseAll" xml:space="preserve">
<value>Close all</value>
</data>
<data name="Quit" xml:space="preserve">
<value>Quit</value>
</data>
<data name="CloseWindowWarningTitle" xml:space="preserve">
<value>Do you want to close all tabs?</value>
</data>
@ -282,6 +285,16 @@
<data name="CmdFocusTabTargetArgDesc" xml:space="preserve">
<value>Move focus the tab at the given index</value>
</data>
<data name="CmdMovePaneTabArgDesc" xml:space="preserve">
<value>Move focused pane to the tab at the given index</value>
</data>
<data name="CmdMovePaneDesc" xml:space="preserve">
<value>Move focused pane to another tab</value>
</data>
<data name="CmdMPDesc" xml:space="preserve">
<value>An alias for the "move-pane" subcommand.</value>
<comment>{Locked="\"move-pane\""}</comment>
</data>
<data name="CmdSplitPaneSizeArgDesc" xml:space="preserve">
<value>Specify the size as a percentage of the parent pane. Valid values are between (0,1), exclusive.</value>
</data>
@ -359,14 +372,10 @@
<data name="CmdMoveFocusDirectionArgDesc" xml:space="preserve">
<value>The direction to move focus in</value>
</data>
<data name="CmdMovePaneDesc" xml:space="preserve">
<data name="CmdSwapPaneDesc" xml:space="preserve">
<value>Swap the focused pane with the adjacent pane in the specified direction</value>
</data>
<data name="CmdMPDesc" xml:space="preserve">
<value>An alias for the "move-pane" subcommand.</value>
<comment>{Locked="\"move-pane\""}</comment>
</data>
<data name="CmdMovePaneDirectionArgDesc" xml:space="preserve">
<data name="CmdSwapPaneDirectionArgDesc" xml:space="preserve">
<value>The direction to move the focused pane in</value>
</data>
<data name="CmdFocusDesc" xml:space="preserve">
@ -435,6 +444,15 @@
<value>Third-Party Notices</value>
<comment>A hyperlink name for the Terminal's third-party notices</comment>
</data>
<data name="QuitDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="QuitDialog.PrimaryButtonText" xml:space="preserve">
<value>Close all</value>
</data>
<data name="QuitDialog.Title" xml:space="preserve">
<value>Do you want to close all windows?</value>
</data>
<data name="CloseAllDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
@ -647,6 +665,14 @@
<data name="CommandPaletteMenuItem" xml:space="preserve">
<value>Command Palette</value>
</data>
<data name="NotificationIconFocusTerminal" xml:space="preserve">
<value>Focus Terminal</value>
<comment>This is displayed as a label for the context menu item that focuses the terminal.</comment>
</data>
<data name="NotificationIconWindowSubmenu" xml:space="preserve">
<value>Windows</value>
<comment>This is displayed as a label for the context menu item that holds the submenu of available windows.</comment>
</data>
<data name="ShellExtension_OpenInTerminalMenuItem_Dev" xml:space="preserve">
<value>Open in Windows Terminal (Dev)</value>
<comment>{Locked} The dev build will never be seen in multiple languages</comment>
@ -662,10 +688,31 @@
<data name="DropPathTabRun.Text" xml:space="preserve">
<value>Open a new tab in given starting directory</value>
</data>
<data name="SplitTabText" xml:space="preserve">
<value>Split Tab</value>
</data>
<data name="DropPathTabNewWindow.Text" xml:space="preserve">
<value>Open a new window with given starting directory</value>
</data>
<data name="DropPathTabSplit.Text" xml:space="preserve">
<value>Split the window and start in given directory</value>
</data>
</root>
<data name="ExportTabText" xml:space="preserve">
<value>Export Text</value>
</data>
<data name="ExportFailure" xml:space="preserve">
<value>Failed to export terminal content</value>
</data>
<data name="ExportSuccess" xml:space="preserve">
<value>Successfully exported terminal content</value>
</data>
<data name="PlainText" xml:space="preserve">
<value>Plain Text</value>
</data>
<data name="CloseOnExitInfoBar.Message" xml:space="preserve">
<value>Termination behavior can be configured in advanced profile settings.</value>
</data>
<data name="InfoBarDismissButton.Content" xml:space="preserve">
<value>Don't show again</value>
</data>
</root>

View file

@ -28,6 +28,9 @@ using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::System;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Pickers;
using namespace winrt::Windows::Storage::Provider;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal::TerminalConnection;
@ -56,13 +59,13 @@ namespace winrt::TerminalApp::implementation
// - existingConnection: An optional connection that is already established to a PTY
// for this tab to host instead of creating one.
// If not defined, the tab will create the connection.
void TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection)
HRESULT TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection)
try
{
const auto profileGuid{ _settings.GetProfileForArgs(newTerminalArgs) };
const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) };
_CreateNewTabFromSettings(profileGuid, settings, existingConnection);
_CreateNewTabWithProfileAndSettings(profile, settings, existingConnection);
const uint32_t tabCount = _tabs.Size();
const bool usedManualProfile = (newTerminalArgs != nullptr) &&
@ -70,7 +73,7 @@ namespace winrt::TerminalApp::implementation
newTerminalArgs.Profile().empty());
// Lookup the name of the color scheme used by this profile.
const auto scheme = _settings.GetColorSchemeForProfile(profileGuid);
const auto scheme = _settings.GetColorSchemeForProfile(profile);
// If they explicitly specified `null` as the scheme (indicating _no_ scheme), log
// that as the empty string.
const auto schemeName = scheme ? scheme.Name() : L"\0";
@ -82,56 +85,25 @@ namespace winrt::TerminalApp::implementation
TraceLoggingUInt32(1u, "EventVer", "Version of this event"),
TraceLoggingUInt32(tabCount, "TabCount", "Count of tabs currently opened in TerminalApp"),
TraceLoggingBool(usedManualProfile, "ProfileSpecified", "Whether the new tab specified a profile explicitly"),
TraceLoggingGuid(profileGuid, "ProfileGuid", "The GUID of the profile spawned in the new tab"),
TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The GUID of the profile spawned in the new tab"),
TraceLoggingBool(settings.DefaultSettings().UseAcrylic(), "UseAcrylic", "The acrylic preference from the settings"),
TraceLoggingFloat64(settings.DefaultSettings().TintOpacity(), "TintOpacity", "Opacity preference from the settings"),
TraceLoggingWideString(settings.DefaultSettings().FontFace().c_str(), "FontFace", "Font face chosen in the settings"),
TraceLoggingWideString(schemeName.data(), "SchemeName", "Color scheme set in the settings"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
return S_OK;
}
CATCH_LOG();
CATCH_RETURN();
// Method Description:
// - Creates a new tab with the given settings. If the tab bar is not being
// currently displayed, it will be shown.
// - Sets up state, event handlers, etc on a tab object that was just made.
// Arguments:
// - profileGuid: ID to use to lookup profile settings for this connection
// - settings: the TerminalSettings object to use to create the TerminalControl with.
// - existingConnection: optionally receives a connection from the outside world instead of attempting to create one
void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection)
// - newTabImpl: the uninitialized tab.
void TerminalPage::_InitializeTab(winrt::com_ptr<TerminalTab> newTabImpl)
{
// Initialize the new tab
// Create a connection based on the values in our settings object if we weren't given one.
auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings());
// If we had an `existingConnection`, then this is an inbound handoff from somewhere else.
// We need to tell it about our size information so it can match the dimensions of what
// we are about to present.
if (existingConnection)
{
connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols());
}
TerminalConnection::ITerminalConnection debugConnection{ nullptr };
if (_settings.GlobalSettings().DebugFeaturesEnabled())
{
const CoreWindow window = CoreWindow::GetForCurrentThread();
const auto rAltState = window.GetKeyState(VirtualKey::RightMenu);
const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu);
const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) &&
WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);
if (bothAltsPressed)
{
std::tie(connection, debugConnection) = OpenDebugTapConnection(connection);
}
}
// Give term control a child of the settings so that any overrides go in the child
// This way, when we do a settings reload we just update the parent and the overrides remain
auto term = _InitControl(settings, connection);
auto newTabImpl = winrt::make_self<TerminalTab>(profileGuid, term);
newTabImpl->Initialize();
// Add the new tab to the list of our tabs.
_tabs.Append(*newTabImpl);
@ -144,7 +116,7 @@ namespace winrt::TerminalApp::implementation
_UpdateTabIndices();
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(term, *newTabImpl);
_RegisterTabEvents(*newTabImpl);
// Don't capture a strong ref to the tab. If the tab is removed as this
// is called, we don't really care anymore about handling the event.
@ -192,14 +164,36 @@ namespace winrt::TerminalApp::implementation
}
});
newTabImpl->SplitTabRequested([weakTab, weakThis{ get_weak() }]() {
auto page{ weakThis.get() };
auto tab{ weakTab.get() };
if (page && tab)
{
page->_SplitTab(*tab);
}
});
newTabImpl->ExportTabRequested([weakTab, weakThis{ get_weak() }]() {
auto page{ weakThis.get() };
auto tab{ weakTab.get() };
if (page && tab)
{
page->_ExportTab(*tab);
}
});
auto tabViewItem = newTabImpl->TabViewItem();
_tabView.TabItems().Append(tabViewItem);
// Set this tab's icon to the icon from the user's profile
const auto profile = _settings.FindProfile(profileGuid);
if (profile != nullptr && !profile.Icon().empty())
if (const auto profile{ newTabImpl->GetFocusedProfile() })
{
newTabImpl->UpdateIcon(profile.Icon());
if (!profile.Icon().empty())
{
newTabImpl->UpdateIcon(profile.Icon());
}
}
tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabClick });
@ -232,19 +226,73 @@ namespace winrt::TerminalApp::implementation
}
});
if (debugConnection) // this will only be set if global debugging is on and tap is active
{
auto newControl = _InitControl(settings, debugConnection);
_RegisterTerminalEvents(newControl, *newTabImpl);
// Split (auto) with the debug tap.
newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profileGuid, newControl);
}
// This kicks off TabView::SelectionChanged, in response to which
// we'll attach the terminal's Xaml control to the Xaml root.
_tabView.SelectedItem(tabViewItem);
}
// Method Description:
// - Create a new tab using a specified pane as the root.
// Arguments:
// - pane: The pane to use as the root.
void TerminalPage::_CreateNewTabFromPane(std::shared_ptr<Pane> pane)
{
auto newTabImpl = winrt::make_self<TerminalTab>(pane);
_InitializeTab(newTabImpl);
}
// Method Description:
// - Creates a new tab with the given settings. If the tab bar is not being
// currently displayed, it will be shown.
// Arguments:
// - profile: profile settings for this connection
// - settings: the TerminalSettings object to use to create the TerminalControl with.
// - existingConnection: optionally receives a connection from the outside world instead of attempting to create one
void TerminalPage::_CreateNewTabWithProfileAndSettings(const Profile& profile, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection)
{
// Initialize the new tab
// Create a connection based on the values in our settings object if we weren't given one.
auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, settings.DefaultSettings());
// If we had an `existingConnection`, then this is an inbound handoff from somewhere else.
// We need to tell it about our size information so it can match the dimensions of what
// we are about to present.
if (existingConnection)
{
connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols());
}
TerminalConnection::ITerminalConnection debugConnection{ nullptr };
if (_settings.GlobalSettings().DebugFeaturesEnabled())
{
const CoreWindow window = CoreWindow::GetForCurrentThread();
const auto rAltState = window.GetKeyState(VirtualKey::RightMenu);
const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu);
const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) &&
WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);
if (bothAltsPressed)
{
std::tie(connection, debugConnection) = OpenDebugTapConnection(connection);
}
}
// Give term control a child of the settings so that any overrides go in the child
// This way, when we do a settings reload we just update the parent and the overrides remain
auto term = _InitControl(settings, connection);
auto newTabImpl = winrt::make_self<TerminalTab>(profile, term);
_RegisterTerminalEvents(term);
_InitializeTab(newTabImpl);
if (debugConnection) // this will only be set if global debugging is on and tap is active
{
auto newControl = _InitControl(settings, debugConnection);
_RegisterTerminalEvents(newControl);
// Split (auto) with the debug tap.
newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profile, newControl);
}
}
// Method Description:
// - Get the icon of the currently focused terminal control, and set its
// tab's icon to that icon.
@ -252,19 +300,9 @@ namespace winrt::TerminalApp::implementation
// - tab: the Tab to update the title for.
void TerminalPage::_UpdateTabIcon(TerminalTab& tab)
{
const auto lastFocusedProfileOpt = tab.GetFocusedProfile();
if (lastFocusedProfileOpt.has_value())
if (const auto profile = tab.GetFocusedProfile())
{
const auto lastFocusedProfile = lastFocusedProfileOpt.value();
const auto matchingProfile = _settings.FindProfile(lastFocusedProfile);
if (matchingProfile)
{
tab.UpdateIcon(matchingProfile.Icon());
}
else
{
tab.UpdateIcon({});
}
tab.UpdateIcon(profile.Icon());
}
}
@ -318,23 +356,18 @@ namespace winrt::TerminalApp::implementation
{
try
{
// TODO: GH#5047 - In the future, we should get the Profile of
// the focused pane, and use that to build a new instance of the
// settings so we can duplicate this tab/pane.
// TODO: GH#5047 - We're duplicating the whole profile, which might
// be a dangling reference to old settings.
//
// Currently, if the profile doesn't exist anymore in our
// settings, we'll silently do nothing.
//
// In the future, it will be preferable to just duplicate the
// current control's settings, but we can't do that currently,
// because we won't be able to create a new instance of the
// connection without keeping an instance of the original Profile
// object around.
// In the future, it may be preferable to just duplicate the
// current control's live settings (which will include changes
// made through VT).
const auto& profileGuid = tab.GetFocusedProfile();
if (profileGuid.has_value())
if (auto profile = tab.GetFocusedProfile())
{
const auto settingsCreateResult{ TerminalSettings::CreateWithProfileByID(_settings, profileGuid.value(), *_bindings) };
// TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this.
profile = GetClosestProfileForDuplicationOfProfile(profile);
const auto settingsCreateResult{ TerminalSettings::CreateWithProfile(_settings, profile, *_bindings) };
const auto workingDirectory = tab.GetActiveTerminalControl().WorkingDirectory();
const auto validWorkingDirectory = !workingDirectory.empty();
if (validWorkingDirectory)
@ -342,7 +375,7 @@ namespace winrt::TerminalApp::implementation
settingsCreateResult.DefaultSettings().StartingDirectory(workingDirectory);
}
_CreateNewTabFromSettings(profileGuid.value(), settingsCreateResult);
_CreateNewTabWithProfileAndSettings(profile, settingsCreateResult);
const auto runtimeTabText{ tab.GetTabText() };
if (!runtimeTabText.empty())
@ -357,6 +390,59 @@ namespace winrt::TerminalApp::implementation
CATCH_LOG();
}
// Method Description:
// - Sets the specified tab as the focused tab and splits its active pane
// Arguments:
// - tab: tab to split
void TerminalPage::_SplitTab(TerminalTab& tab)
{
try
{
_SetFocusedTab(tab);
_SplitPane(tab, SplitState::Automatic, SplitType::Duplicate);
}
CATCH_LOG();
}
// Method Description:
// - Exports the content of the Terminal Buffer inside the tab
// Arguments:
// - tab: tab to export
winrt::fire_and_forget TerminalPage::_ExportTab(const TerminalTab& tab)
{
try
{
if (const auto control{ tab.GetActiveTerminalControl() })
{
const FileSavePicker savePicker;
savePicker.as<IInitializeWithWindow>()->Initialize(*_hostingHwnd);
savePicker.SuggestedStartLocation(PickerLocationId::Downloads);
const auto fileChoices = single_threaded_vector<hstring>({ L".txt" });
savePicker.FileTypeChoices().Insert(RS_(L"PlainText"), fileChoices);
savePicker.SuggestedFileName(control.Title());
const StorageFile file = co_await savePicker.PickSaveFileAsync();
if (file != nullptr)
{
const auto buffer = control.ReadEntireBuffer();
CachedFileManager::DeferUpdates(file);
co_await FileIO::WriteTextAsync(file, buffer);
const auto status = co_await CachedFileManager::CompleteUpdatesAsync(file);
switch (status)
{
case FileUpdateStatus::Complete:
case FileUpdateStatus::CompleteAndRenamed:
_ShowControlNoticeDialog(RS_(L"NoticeInfo"), RS_(L"ExportSuccess"));
break;
default:
_ShowControlNoticeDialog(RS_(L"NoticeError"), RS_(L"ExportFailure"));
}
}
}
}
CATCH_LOG();
}
// Method Description:
// - Removes the tab (both TerminalControl and XAML) after prompting for approval
// Arguments:
@ -413,6 +499,14 @@ namespace winrt::TerminalApp::implementation
// To close the window here, we need to close the hosting window.
if (_tabs.Size() == 0)
{
// If we are supposed to save state, make sure we clear it out
// if the user manually closed all tabs.
if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings))
{
auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(nullptr);
}
_LastTabClosedHandlers(*this, nullptr);
}
else if (focusedTabIndex.has_value() && focusedTabIndex.value() == gsl::narrow_cast<uint32_t>(tabIndex))
@ -513,9 +607,13 @@ namespace winrt::TerminalApp::implementation
tabIndex = std::clamp(tabIndex, 0u, _tabs.Size() - 1);
auto tab{ _tabs.GetAt(tabIndex) };
// GH#11107 - Always just set the item directly first so that if
// tab movement is done as part of multiple actions following calls
// to _GetFocusedTab will return the correct tab.
_tabView.SelectedItem(tab.TabViewItem());
if (_startupState == StartupState::InStartup)
{
_tabView.SelectedItem(tab.TabViewItem());
_UpdatedSelectedTab(tab);
}
else

View file

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TaskbarState.h"
#include "TaskbarState.g.cpp"
namespace winrt::TerminalApp::implementation
{
// Default to unset, 0%.
TaskbarState::TaskbarState() :
TaskbarState(0, 0){};
TaskbarState::TaskbarState(const uint64_t dispatchTypesState, const uint64_t progressParam) :
_State{ dispatchTypesState },
_Progress{ progressParam } {}
uint64_t TaskbarState::Priority() const
{
// This seemingly nonsensical ordering is from
// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate#how-the-taskbar-button-chooses-the-progress-indicator-for-a-group
switch (_State)
{
case 0: // Clear = 0,
return 5;
case 1: // Set = 1,
return 3;
case 2: // Error = 2,
return 1;
case 3: // Indeterminate = 3,
return 4;
case 4: // Paused = 4
return 2;
}
// Here, return 6, to definitely be greater than all the other valid values.
// This should never really happen.
return 6;
}
int TaskbarState::ComparePriority(const winrt::TerminalApp::TaskbarState& lhs, const winrt::TerminalApp::TaskbarState& rhs)
{
return lhs.Priority() < rhs.Priority();
}
}

View file

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "inc/cppwinrt_utils.h"
#include "TaskbarState.g.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class TabTests;
};
namespace winrt::TerminalApp::implementation
{
struct TaskbarState : TaskbarStateT<TaskbarState>
{
public:
TaskbarState();
TaskbarState(const uint64_t dispatchTypesState, const uint64_t progress);
static int ComparePriority(const winrt::TerminalApp::TaskbarState& lhs, const winrt::TerminalApp::TaskbarState& rhs);
uint64_t Priority() const;
WINRT_PROPERTY(uint64_t, State, 0);
WINRT_PROPERTY(uint64_t, Progress, 0);
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(TaskbarState);
}

View file

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace TerminalApp
{
[default_interface] runtimeclass TaskbarState
{
TaskbarState();
TaskbarState(UInt64 dispatchTypesState, UInt64 progress);
UInt64 State{ get; };
UInt64 Progress{ get; };
UInt64 Priority { get; };
}
}

View file

@ -53,7 +53,7 @@
<Page Include="TabRowControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="TabHeaderControl.xaml">
<Page Include="TabHeaderControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="HighlightedTextControl.xaml">
@ -74,6 +74,7 @@
<ClInclude Include="Commandline.h" />
<ClInclude Include="CommandLinePaletteItem.h" />
<ClInclude Include="Jumplist.h" />
<ClInclude Include="LanguageProfileNotifier.h" />
<ClInclude Include="MinMaxCloseControl.h">
<DependentUpon>MinMaxCloseControl.xaml</DependentUpon>
</ClInclude>
@ -89,6 +90,9 @@
<DependentUpon>TabBase.idl</DependentUpon>
</ClInclude>
<ClInclude Include="TabPaletteItem.h" />
<ClInclude Include="TaskbarState.h">
<DependentUpon>TaskbarState.idl</DependentUpon>
</ClInclude>
<ClInclude Include="TerminalTab.h">
<DependentUpon>TerminalTab.idl</DependentUpon>
</ClInclude>
@ -112,7 +116,7 @@
<ClInclude Include="HighlightedTextControl.h">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="HighlightedText.h" />
<ClInclude Include="HighlightedText.h" />
<ClInclude Include="ColorPickupFlyout.h">
<DependentUpon>ColorPickupFlyout.xaml</DependentUpon>
</ClInclude>
@ -139,7 +143,7 @@
<ClInclude Include="AppLogic.h">
<DependentUpon>AppLogic.idl</DependentUpon>
</ClInclude>
<ClInclude Include="Toast.h"/>
<ClInclude Include="Toast.h" />
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
<ItemGroup>
@ -149,6 +153,7 @@
<ClCompile Include="AppCommandlineArgs.cpp" />
<ClCompile Include="Commandline.cpp" />
<ClCompile Include="Jumplist.cpp" />
<ClCompile Include="LanguageProfileNotifier.cpp" />
<ClCompile Include="MinMaxCloseControl.cpp">
<DependentUpon>MinMaxCloseControl.xaml</DependentUpon>
</ClCompile>
@ -164,6 +169,9 @@
<DependentUpon>TabBase.idl</DependentUpon>
</ClCompile>
<ClCompile Include="TabPaletteItem.cpp" />
<ClCompile Include="TaskbarState.cpp">
<DependentUpon>TaskbarState.idl</DependentUpon>
</ClCompile>
<ClCompile Include="TerminalTab.cpp">
<DependentUpon>TerminalTab.idl</DependentUpon>
</ClCompile>
@ -195,7 +203,7 @@
<ClCompile Include="HighlightedTextControl.cpp">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="HighlightedText.cpp" />
<ClCompile Include="HighlightedText.cpp" />
<ClCompile Include="ColorPickupFlyout.cpp">
<DependentUpon>ColorPickupFlyout.xaml</DependentUpon>
</ClCompile>
@ -256,6 +264,7 @@
</Midl>
<Midl Include="TabBase.idl" />
<Midl Include="TabPaletteItem.idl" />
<Midl Include="TaskbarState.idl" />
<Midl Include="TerminalTab.idl" />
<Midl Include="TerminalPage.idl">
<DependentUpon>TerminalPage.xaml</DependentUpon>
@ -280,7 +289,7 @@
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="HighlightedText.idl" />
<Midl Include="HighlightedText.idl" />
<Midl Include="ColorPickupFlyout.idl">
<DependentUpon>ColorPickupFlyout.xaml</DependentUpon>
<SubType>Code</SubType>

View file

@ -13,9 +13,6 @@
<ClCompile Include="Pane.cpp">
<Filter>pane</Filter>
</ClCompile>
<ClCompile Include="Tab.cpp">
<Filter>tab</Filter>
</ClCompile>
<ClCompile Include="Pane.LayoutSizeNode.cpp">
<Filter>pane</Filter>
</ClCompile>
@ -23,7 +20,6 @@
<ClCompile Include="Commandline.cpp" />
<ClCompile Include="ColorHelper.cpp" />
<ClCompile Include="DebugTapConnection.cpp" />
<ClCompile Include="Utils.cpp" />
<ClCompile Include="Jumplist.cpp" />
<ClCompile Include="Tab.cpp">
<Filter>tab</Filter>
@ -49,10 +45,10 @@
<ClCompile Include="HighlightedText.cpp">
<Filter>highlightedText</Filter>
</ClCompile>
<ClCompile Include="Toast.cpp" />
<ClCompile Include="LanguageProfileNotifier.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Utils.h" />
<ClInclude Include="TerminalWarnings.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="App.base.h">
<Filter>app</Filter>
@ -60,9 +56,6 @@
<ClInclude Include="Pane.h">
<Filter>pane</Filter>
</ClInclude>
<ClInclude Include="Tab.h">
<Filter>tab</Filter>
</ClInclude>
<ClInclude Include="AppCommandlineArgs.h" />
<ClInclude Include="Commandline.h" />
<ClInclude Include="DebugTapConnection.h" />
@ -92,14 +85,13 @@
<ClInclude Include="HighlightedText.h">
<Filter>highlightedText</Filter>
</ClInclude>
<ClInclude Include="Toast.h" />
<ClInclude Include="LanguageProfileNotifier.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="AppLogic.idl">
<Filter>app</Filter>
</Midl>
<Midl Include="ActionArgs.idl">
<Filter>settings</Filter>
</Midl>
<Midl Include="AppKeyBindings.idl">
<Filter>settings</Filter>
</Midl>
@ -107,9 +99,6 @@
<Filter>settings</Filter>
</Midl>
<Midl Include="IDirectKeyListener.idl" />
<Midl Include="ITab.idl">
<Filter>tab</Filter>
</Midl>
<Midl Include="SettingsTab.idl">
<Filter>tab</Filter>
</Midl>
@ -125,6 +114,9 @@
<Midl Include="TerminalTabStatus.idl">
<Filter>tab</Filter>
</Midl>
<Midl Include="PaletteItemTemplateSelector.idl" />
<Midl Include="TabBase.idl" />
<Midl Include="EmptyStringVisibilityConverter.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@ -160,9 +152,6 @@
<Midl Include="ActionPaletteItem.idl">
<Filter>commandPalette</Filter>
</Midl>
<Midl Include="FilterableListItem.idl">
<Filter>commandPalette</Filter>
</Midl>
<Midl Include="CommandLinePaletteItem.idl">
<Filter>commandPalette</Filter>
</Midl>

View file

@ -19,6 +19,8 @@
#include "RenameWindowRequestedArgs.g.cpp"
#include "../inc/WindowingBehavior.h"
#include <til/latch.h>
using namespace winrt;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::UI::Xaml;
@ -140,6 +142,44 @@ namespace winrt::TerminalApp::implementation
}
CATCH_LOG();
if (_settings.GlobalSettings().UseAcrylicInTabRow())
{
const auto res = Application::Current().Resources();
const auto lightKey = winrt::box_value(L"Light");
const auto darkKey = winrt::box_value(L"Dark");
const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground");
for (auto const& dictionary : res.MergedDictionaries())
{
// Don't change MUX resources
if (dictionary.Source())
{
continue;
}
for (auto const& kvPair : dictionary.ThemeDictionaries())
{
const auto themeDictionary = kvPair.Value().as<winrt::Windows::UI::Xaml::ResourceDictionary>();
if (themeDictionary.HasKey(tabViewBackgroundKey))
{
const auto backgroundSolidBrush = themeDictionary.Lookup(tabViewBackgroundKey).as<Media::SolidColorBrush>();
const til::color backgroundColor = backgroundSolidBrush.Color();
const auto acrylicBrush = Media::AcrylicBrush();
acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop);
acrylicBrush.FallbackColor(backgroundColor);
acrylicBrush.TintColor(backgroundColor);
acrylicBrush.TintOpacity(0.5);
themeDictionary.Insert(tabViewBackgroundKey, acrylicBrush);
}
}
}
}
_tabRow.PointerMoved({ get_weak(), &TerminalPage::_RestorePointerCursorHandler });
_tabView.CanReorderTabs(!isElevated);
_tabView.CanDragTabs(!isElevated);
@ -236,6 +276,21 @@ namespace winrt::TerminalApp::implementation
CATCH_LOG();
}
// Method Description;
// - Checks if the current terminal window should load or save its layout information.
// Arguments:
// - settings: The settings to use as this may be called before the page is
// fully initialized.
// Return Value:
// - true if the ApplicationState should be used.
bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const
{
// If the setting is enabled, and we are the only window.
return Feature_PersistedWindowLayout::IsEnabled() &&
settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout &&
_numOpenWindows == 1;
}
winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e)
{
Windows::Foundation::Collections::IVectorView<Windows::Storage::IStorageItem> items;
@ -253,16 +308,8 @@ namespace winrt::TerminalApp::implementation
path = path.parent_path();
}
std::wstring pathText = path.wstring();
// Handle edge case of "C:\\", seems like the "StartingDirectory" doesn't like path which ends with '\'
if (pathText.back() == L'\\')
{
pathText.erase(std::prev(pathText.end()));
}
NewTerminalArgs args;
args.StartingDirectory(winrt::hstring{ pathText });
args.StartingDirectory(winrt::hstring{ path.wstring() });
this->_OpenNewTerminal(args);
TraceLoggingWrite(
@ -326,6 +373,34 @@ namespace winrt::TerminalApp::implementation
if (_startupState == StartupState::NotInitialized)
{
_startupState = StartupState::InStartup;
// If the user selected to save their tab layout, we are the first
// window opened, and wt was not run with any other arguments, then
// we should use the saved settings.
auto firstActionIsDefault = [](ActionAndArgs action) {
if (action.Action() != ShortcutAction::NewTab)
{
return false;
}
// If no commands were given, we will have default args
if (const auto args = action.Args().try_as<NewTabArgs>())
{
NewTerminalArgs defaultArgs{};
return args.TerminalArgs() == nullptr || args.TerminalArgs().Equals(defaultArgs);
}
return false;
};
if (ShouldUsePersistedLayout(_settings) && _startupActions.Size() == 1 && firstActionIsDefault(_startupActions.GetAt(0)))
{
auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0)
{
_startupActions = layouts.GetAt(0).TabLayout();
}
}
ProcessStartupActions(_startupActions, true);
// If we were told that the COM server needs to be started to listen for incoming
@ -356,34 +431,12 @@ namespace winrt::TerminalApp::implementation
winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener();
}
// If we failed to start the listener, it will throw.
// We should fail fast here or the Terminal will be in a very strange state.
// We only start the listener if the Terminal was started with the COM server
// `-Embedding` flag and we make no tabs as a result.
// Therefore, if the listener cannot start itself up to make that tab with
// the inbound connection that caused the COM activation in the first place...
// we would be left with an empty terminal frame with no tabs.
// Instead, crash out so COM sees the server die and things unwind
// without a weird empty frame window.
// We don't want to fail fast here because if a peasant has some trouble with
// starting the listener, we don't want it to crash and take all its tabs down
// with it.
catch (...)
{
// However, we cannot always fail fast because of MSFT:33501832. Sometimes the COM catalog
// tears the state between old and new versions and fails here for that reason.
// As we're always becoming an inbound server in the monarch, even when COM didn't strictly
// ask us yet...we might just crash always.
// Instead... we're going to differentiate. If COM started us... we will fail fast
// so it sees the process die and falls back.
// If we were just starting normally as a Monarch and opportunistically listening for
// inbound connections... then we'll just log the failure and move on assuming
// the version state is torn and will fix itself whenever the packaging upgrade
// tasks decide to clean up.
if (_isEmbeddingInboundListener)
{
FAIL_FAST_CAUGHT_EXCEPTION();
}
else
{
LOG_CAUGHT_EXCEPTION();
}
LOG_CAUGHT_EXCEPTION();
}
}
}
@ -447,6 +500,14 @@ namespace winrt::TerminalApp::implementation
co_return;
}
}
// GH#6586: now that we're done processing all startup commands,
// focus the active control. This will work as expected for both
// commandline invocations and for `wt` action invocations.
if (const auto control = _GetActiveControl())
{
control.Focus(FocusState::Programmatic);
}
}
if (initial)
{
@ -497,6 +558,21 @@ namespace winrt::TerminalApp::implementation
ShellExecute(nullptr, nullptr, currentPath.c_str(), nullptr, nullptr, SW_SHOW);
}
// Method Description:
// - Displays a dialog to warn the user that they are about to close all open windows.
// Once the user clicks the OK button, shut down the application.
// If cancel is clicked, the dialog will close.
// - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens. See _ShowDialog for details
winrt::Windows::Foundation::IAsyncOperation<ContentDialogResult> TerminalPage::_ShowQuitDialog()
{
if (auto presenter{ _dialogPresenter.get() })
{
co_return co_await presenter.ShowDialog(FindName(L"QuitDialog").try_as<WUX::Controls::ContentDialog>());
}
co_return ContentDialogResult::None;
}
// Method Description:
// - Displays a dialog for warnings found while closing the terminal app using
// key binding with multiple tabs opened. Display messages to warn user
@ -761,13 +837,18 @@ namespace winrt::TerminalApp::implementation
// Manually fill in the evaluated profile.
if (newTerminalArgs.ProfileIndex() != nullptr)
{
newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(this->_settings.GetProfileForArgs(newTerminalArgs)));
// We want to promote the index to a GUID because there is no "launch to profile index" command.
const auto profile = _settings.GetProfileForArgs(newTerminalArgs);
if (profile)
{
newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid()));
}
}
this->_OpenNewWindow(false, newTerminalArgs);
}
else
{
this->_OpenNewTab(newTerminalArgs);
LOG_IF_FAILED(this->_OpenNewTab(newTerminalArgs));
}
}
@ -784,15 +865,13 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Creates a new connection based on the profile settings
// Arguments:
// - the profile GUID we want the settings from
// - the profile we want the settings from
// - the terminal settings
// Return value:
// - the desired connection
TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(GUID profileGuid,
TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile,
TerminalSettings settings)
{
const auto profile = _settings.FindProfile(profileGuid);
TerminalConnection::ITerminalConnection connection{ nullptr };
winrt::guid connectionType = profile.ConnectionType();
@ -816,7 +895,8 @@ namespace winrt::TerminalApp::implementation
else
{
std::wstring guidWString = Utils::GuidToString(profileGuid);
// profile is guaranteed to exist here
std::wstring guidWString = Utils::GuidToString(profile.Guid());
StringMap envMap{};
envMap.Insert(L"WT_PROFILE_ID", guidWString);
@ -865,7 +945,7 @@ namespace winrt::TerminalApp::implementation
"ConnectionCreated",
TraceLoggingDescription("Event emitted upon the creation of a connection"),
TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"),
TraceLoggingGuid(profileGuid, "ProfileGuid", "The profile's GUID"),
TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"),
TraceLoggingGuid(sessionGuid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
@ -997,7 +1077,7 @@ namespace winrt::TerminalApp::implementation
{
auto newTabTitle = tab.Title();
if (_settings.GlobalSettings().ShowTitleInTitlebar() && tab == _GetFocusedTab())
if (tab == _GetFocusedTab())
{
_TitleChangedHandlers(*this, newTabTitle);
}
@ -1008,11 +1088,9 @@ namespace winrt::TerminalApp::implementation
// handle. This includes:
// * the Copy and Paste events, for setting and retrieving clipboard data
// on the right thread
// * the TitleChanged event, for changing the text of the tab
// Arguments:
// - term: The newly created TermControl to connect the events for
// - hostingTab: The Tab that's hosting this TermControl instance
void TerminalPage::_RegisterTerminalEvents(TermControl term, TerminalTab& hostingTab)
void TerminalPage::_RegisterTerminalEvents(TermControl term)
{
term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler });
@ -1027,10 +1105,22 @@ namespace winrt::TerminalApp::implementation
term.HidePointerCursor({ get_weak(), &TerminalPage::_HidePointerCursorHandler });
term.RestorePointerCursor({ get_weak(), &TerminalPage::_RestorePointerCursorHandler });
// Add an event handler for when the terminal or tab wants to set a
// progress indicator on the taskbar
term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler });
// Bind Tab events to the TermControl and the Tab's Pane
hostingTab.Initialize(term);
term.ConnectionStateChanged({ get_weak(), &TerminalPage::_ConnectionStateChangedHandler });
}
// Method Description:
// - Connects event handlers to the TerminalTab for events that we want to
// handle. This includes:
// * the TitleChanged event, for changing the text of the tab
// * the Color{Selected,Cleared} events to change the color of a tab.
// Arguments:
// - hostingTab: The Tab that's hosting this TermControl instance
void TerminalPage::_RegisterTabEvents(TerminalTab& hostingTab)
{
auto weakTab{ hostingTab.get_weak() };
auto weakThis{ get_weak() };
// PropertyChanged is the generic mechanism by which the Tab
@ -1082,7 +1172,6 @@ namespace winrt::TerminalApp::implementation
// Add an event handler for when the terminal or tab wants to set a
// progress indicator on the taskbar
hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler });
term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler });
// TODO GH#3327: Once we support colorizing the NewTab button based on
// the color of the tab, we'll want to make sure to call
@ -1135,7 +1224,6 @@ namespace winrt::TerminalApp::implementation
{
if (const auto terminalTab{ _GetFocusedTabImpl() })
{
_UnZoomIfNeeded();
return terminalTab->NavigateFocus(direction);
}
return false;
@ -1143,18 +1231,19 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Attempt to swap the positions of the focused pane with another pane.
// See Pane::MovePane for details.
// See Pane::SwapPane for details.
// Arguments:
// - direction: The direction to move the focused pane in.
// Return Value:
// - <none>
void TerminalPage::_MovePane(const FocusDirection& direction)
// - true if panes were swapped.
bool TerminalPage::_SwapPane(const FocusDirection& direction)
{
if (const auto terminalTab{ _GetFocusedTabImpl() })
{
_UnZoomIfNeeded();
terminalTab->MovePane(direction);
return terminalTab->SwapPane(direction);
}
return false;
}
TermControl TerminalPage::_GetActiveControl()
@ -1165,13 +1254,116 @@ namespace winrt::TerminalApp::implementation
}
return nullptr;
}
// Method Description:
// - Warn the user that they are about to close all open windows, then
// signal that we want to close everything.
fire_and_forget TerminalPage::RequestQuit()
{
if (!_displayingCloseDialog)
{
_displayingCloseDialog = true;
ContentDialogResult warningResult = co_await _ShowQuitDialog();
_displayingCloseDialog = false;
if (warningResult != ContentDialogResult::Primary)
{
co_return;
}
_QuitRequestedHandlers(nullptr, nullptr);
}
}
// Method Description:
// - Saves the window position and tab layout to the application state
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalPage::PersistWindowLayout()
{
std::vector<ActionAndArgs> actions;
for (auto tab : _tabs)
{
if (auto terminalTab = _GetTerminalTabImpl(tab))
{
auto tabActions = terminalTab->BuildStartupActions();
actions.insert(actions.end(), tabActions.begin(), tabActions.end());
}
else if (tab.try_as<SettingsTab>())
{
ActionAndArgs action;
action.Action(ShortcutAction::OpenSettings);
OpenSettingsArgs args{ SettingsTarget::SettingsUI };
action.Args(args);
actions.push_back(action);
}
}
// if the focused tab was not the last tab, restore that
auto idx = _GetFocusedTabIndex();
if (idx && idx != _tabs.Size() - 1)
{
ActionAndArgs action;
action.Action(ShortcutAction::SwitchToTab);
SwitchToTabArgs switchToTabArgs{ idx.value() };
action.Args(switchToTabArgs);
actions.push_back(action);
}
WindowLayout layout{};
layout.TabLayout(winrt::single_threaded_vector<ActionAndArgs>(std::move(actions)));
// Only save the content size because the tab size will be added on load.
const float contentWidth = ::base::saturated_cast<float>(_tabContent.ActualWidth());
const float contentHeight = ::base::saturated_cast<float>(_tabContent.ActualHeight());
const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight };
layout.InitialSize(windowSize);
if (_hostingHwnd)
{
// Get the position of the current window. This includes the
// non-client already.
RECT window{};
GetWindowRect(_hostingHwnd.value(), &window);
// We want to remove the non-client area so calculate that.
// We don't have access to the (NonClient)IslandWindow directly so
// just replicate the logic.
const auto windowStyle = static_cast<DWORD>(GetWindowLong(_hostingHwnd.value(), GWL_STYLE));
auto dpi = GetDpiForWindow(_hostingHwnd.value());
RECT nonClientArea{};
LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&nonClientArea, windowStyle, false, 0, dpi));
// The nonClientArea adjustment is negative, so subtract that out.
// This way we save the user-visible location of the terminal.
LaunchPosition pos{};
pos.X = window.left - nonClientArea.left;
pos.Y = window.top;
layout.InitialPosition(pos);
}
auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(winrt::single_threaded_vector<WindowLayout>({ layout }));
}
// Method Description:
// - Close the terminal app. If there is more
// than one tab opened, show a warning dialog.
fire_and_forget TerminalPage::CloseWindow()
// Arguments:
// - bypassDialog: if true a dialog won't be shown even if the user would
// normally get confirmation. This is used in the case where the user
// has already been prompted by the Quit action.
fire_and_forget TerminalPage::CloseWindow(bool bypassDialog)
{
if (_HasMultipleTabs() &&
if (!bypassDialog &&
_HasMultipleTabs() &&
_settings.GlobalSettings().ConfirmCloseAllTabs() &&
!_displayingCloseDialog)
{
@ -1185,6 +1377,13 @@ namespace winrt::TerminalApp::implementation
}
}
if (ShouldUsePersistedLayout(_settings))
{
PersistWindowLayout();
// don't delete the ApplicationState when all of the tabs are removed.
_maintainStateOnTabClose = true;
}
_RemoveAllTabs();
}
@ -1216,6 +1415,56 @@ namespace winrt::TerminalApp::implementation
}
}
// Method Description:
// - Moves the currently active pane on the currently active tab to the
// specified tab. If the tab index is greater than the number of
// tabs, then a new tab will be created for the pane. Similarly, if a pane
// is the last remaining pane on a tab, that tab will be closed upon moving.
// - No move will occur if the tabIdx is the same as the current tab, or if
// the specified tab is not a host of terminals (such as the settings tab).
// Arguments:
// - tabIdx: The target tab index.
// Return Value:
// - true if the pane was successfully moved to the new tab.
bool TerminalPage::_MovePane(const uint32_t tabIdx)
{
auto focusedTab{ _GetFocusedTabImpl() };
if (!focusedTab)
{
return false;
}
// If we are trying to move from the current tab to the current tab do nothing.
if (_GetFocusedTabIndex() == tabIdx)
{
return false;
}
// Moving the pane from the current tab might close it, so get the next
// tab before its index changes.
if (_tabs.Size() > tabIdx)
{
auto targetTab = _GetTerminalTabImpl(_tabs.GetAt(tabIdx));
// if the selected tab is not a host of terminals (e.g. settings)
// don't attempt to add a pane to it.
if (!targetTab)
{
return false;
}
auto pane = focusedTab->DetachPane();
targetTab->AttachPane(pane);
_SetFocusedTab(*targetTab);
}
else
{
auto pane = focusedTab->DetachPane();
_CreateNewTabFromPane(pane);
}
return true;
}
// Method Description:
// - Split the focused pane either horizontally or vertically, and place the
// given TermControl into the newly created pane.
@ -1246,26 +1495,52 @@ namespace winrt::TerminalApp::implementation
return;
}
_SplitPane(*focusedTab, splitType, splitMode, splitSize, newTerminalArgs);
}
// Method Description:
// - Split the focused pane of the given tab, either horizontally or vertically, and place the
// given TermControl into the newly created pane.
// - If splitType == SplitState::None, this method does nothing.
// Arguments:
// - tab: The tab that is going to be split.
// - splitType: one value from the TerminalApp::SplitState enum, indicating how the
// new pane should be split from its parent.
// - splitMode: value from TerminalApp::SplitType enum, indicating the profile to be used in the newly split pane.
// - newTerminalArgs: An object that may contain a blob of parameters to
// control which profile is created and with possible other
// configurations. See CascadiaSettings::BuildSettings for more details.
void TerminalPage::_SplitPane(TerminalTab& tab,
const SplitState splitType,
const SplitType splitMode,
const float splitSize,
const NewTerminalArgs& newTerminalArgs)
{
// Do nothing if we're requesting no split.
if (splitType == SplitState::None)
{
return;
}
try
{
TerminalSettingsCreateResult controlSettings{ nullptr };
GUID realGuid;
bool profileFound = false;
Profile profile{ nullptr };
if (splitMode == SplitType::Duplicate)
{
std::optional<GUID> current_guid = focusedTab->GetFocusedProfile();
if (current_guid)
profile = tab.GetFocusedProfile();
if (profile)
{
profileFound = true;
controlSettings = TerminalSettings::CreateWithProfileByID(_settings, current_guid.value(), *_bindings);
const auto workingDirectory = focusedTab->GetActiveTerminalControl().WorkingDirectory();
// TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this.
profile = GetClosestProfileForDuplicationOfProfile(profile);
controlSettings = TerminalSettings::CreateWithProfile(_settings, profile, *_bindings);
const auto workingDirectory = tab.GetActiveTerminalControl().WorkingDirectory();
const auto validWorkingDirectory = !workingDirectory.empty();
if (validWorkingDirectory)
{
controlSettings.DefaultSettings().StartingDirectory(workingDirectory);
}
realGuid = current_guid.value();
}
// TODO: GH#5047 - In the future, we should get the Profile of
// the focused pane, and use that to build a new instance of the
@ -1280,13 +1555,13 @@ namespace winrt::TerminalApp::implementation
// connection without keeping an instance of the original Profile
// object around.
}
if (!profileFound)
if (!profile)
{
realGuid = _settings.GetProfileForArgs(newTerminalArgs);
profile = _settings.GetProfileForArgs(newTerminalArgs);
controlSettings = TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings);
}
const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings.DefaultSettings());
const auto controlConnection = _CreateConnectionFromSettings(profile, controlSettings.DefaultSettings());
const float contentWidth = ::base::saturated_cast<float>(_tabContent.ActualWidth());
const float contentHeight = ::base::saturated_cast<float>(_tabContent.ActualHeight());
@ -1295,10 +1570,10 @@ namespace winrt::TerminalApp::implementation
auto realSplitType = splitType;
if (realSplitType == SplitState::Automatic)
{
realSplitType = focusedTab->PreCalculateAutoSplit(availableSpace);
realSplitType = tab.PreCalculateAutoSplit(availableSpace);
}
const auto canSplit = focusedTab->PreCalculateCanSplit(realSplitType, splitSize, availableSpace);
const auto canSplit = tab.PreCalculateCanSplit(realSplitType, splitSize, availableSpace);
if (!canSplit)
{
return;
@ -1307,11 +1582,19 @@ namespace winrt::TerminalApp::implementation
auto newControl = _InitControl(controlSettings, controlConnection);
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(newControl, *focusedTab);
_RegisterTerminalEvents(newControl);
_UnZoomIfNeeded();
focusedTab->SplitPane(realSplitType, splitSize, realGuid, newControl);
tab.SplitPane(realSplitType, splitSize, profile, newControl);
// After GH#6586, the control will no longer focus itself
// automatically when it's finished being laid out. Manually focus
// the control here instead.
if (_startupState == StartupState::Initialized)
{
_GetActiveControl().Focus(FocusState::Programmatic);
}
}
CATCH_LOG();
}
@ -1894,40 +2177,56 @@ namespace winrt::TerminalApp::implementation
_HookupKeyBindings(_settings.ActionMap());
// Refresh UI elements
auto profiles = _settings.ActiveProfiles();
for (const auto& profile : profiles)
// Mapping by GUID isn't _excellent_ because the defaults profile doesn't have a stable GUID; however,
// when we stabilize its guid this will become fully safe.
std::unordered_map<winrt::guid, std::pair<Profile, TerminalSettingsCreateResult>> profileGuidSettingsMap;
const auto profileDefaults{ _settings.ProfileDefaults() };
const auto allProfiles{ _settings.AllProfiles() };
profileGuidSettingsMap.reserve(allProfiles.Size() + 1);
// Include the Defaults profile for consideration
profileGuidSettingsMap.insert_or_assign(profileDefaults.Guid(), std::pair{ profileDefaults, nullptr });
for (const auto& newProfile : allProfiles)
{
const auto profileGuid = profile.Guid();
try
{
// This can throw an exception if the profileGuid does
// not belong to an actual profile in the list of profiles.
auto settings{ TerminalSettings::CreateWithProfileByID(_settings, profileGuid, *_bindings) };
for (auto tab : _tabs)
{
if (auto terminalTab = _GetTerminalTabImpl(tab))
{
terminalTab->UpdateSettings(settings, profileGuid);
}
}
}
CATCH_LOG();
// Avoid creating a TerminalSettings right now. They're not totally cheap, and we suspect that users with many
// panes may not be using all of their profiles at the same time. Lazy evaluation is king!
profileGuidSettingsMap.insert_or_assign(newProfile.Guid(), std::pair{ newProfile, nullptr });
}
// GH#2455: If there are any panes with controls that had been
// initialized with a Profile that no longer exists in our list of
// profiles, we'll leave it unmodified. The profile doesn't exist
// anymore, so we can't possibly update its settings.
// Update the icon of the tab for the currently focused profile in that tab.
// Only do this for TerminalTabs. Other types of tabs won't have multiple panes
// and profiles so the Title and Icon will be set once and only once on init.
for (auto tab : _tabs)
for (const auto& tab : _tabs)
{
if (auto terminalTab = _GetTerminalTabImpl(tab))
if (auto terminalTab{ _GetTerminalTabImpl(tab) })
{
terminalTab->UpdateSettings();
// Manually enumerate the panes in each tab; this will let us recycle TerminalSettings
// objects but only have to iterate one time.
terminalTab->GetRootPane()->WalkTree([&](auto&& pane) {
if (const auto profile{ pane->GetProfile() })
{
const auto found{ profileGuidSettingsMap.find(profile.Guid()) };
// GH#2455: If there are any panes with controls that had been
// initialized with a Profile that no longer exists in our list of
// profiles, we'll leave it unmodified. The profile doesn't exist
// anymore, so we can't possibly update its settings.
if (found != profileGuidSettingsMap.cend())
{
auto& pair{ found->second };
if (!pair.second)
{
pair.second = TerminalSettings::CreateWithProfile(_settings, pair.first, *_bindings);
}
pane->UpdateSettings(pair.second, pair.first);
}
}
return false;
});
// Update the icon of the tab for the currently focused profile in that tab.
// Only do this for TerminalTabs. Other types of tabs won't have multiple panes
// and profiles so the Title and Icon will be set once and only once on init.
_UpdateTabIcon(*terminalTab);
// Force the TerminalTab to re-grab its currently active control's title.
@ -2078,29 +2377,35 @@ namespace winrt::TerminalApp::implementation
}
// Method Description:
// - Gets the taskbar state value from the last active control
// - Get the combined taskbar state for the page. This is the combination of
// all the states of all the tabs, which are themselves a combination of
// all their panes. Taskbar states are given a priority based on the rules
// in:
// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate
// under "How the Taskbar Button Chooses the Progress Indicator for a Group"
// Arguments:
// - <none>
// Return Value:
// - The taskbar state of the last active control
uint64_t TerminalPage::GetLastActiveControlTaskbarState()
// - A TaskbarState object representing the combined taskbar state and
// progress percentage of all our tabs.
winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const
{
if (auto control{ _GetActiveControl() })
{
return control.TaskbarState();
}
return {};
}
auto state{ winrt::make<winrt::TerminalApp::implementation::TaskbarState>() };
// Method Description:
// - Gets the taskbar progress value from the last active control
// Return Value:
// - The taskbar progress of the last active control
uint64_t TerminalPage::GetLastActiveControlTaskbarProgress()
{
if (auto control{ _GetActiveControl() })
for (const auto& tab : _tabs)
{
return control.TaskbarProgress();
if (auto tabImpl{ _GetTerminalTabImpl(tab) })
{
auto tabState{ tabImpl->GetCombinedTaskbarState() };
// lowest priority wins
if (tabState.Priority() < state.Priority())
{
state = tabState;
}
}
}
return {};
return state;
}
// Method Description:
@ -2386,13 +2691,52 @@ namespace winrt::TerminalApp::implementation
return _isAlwaysOnTop;
}
void TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection)
HRESULT TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection)
{
// TODO: GH 9458 will give us more context so we can try to choose a better profile.
_OpenNewTab(nullptr, connection);
// We need to be on the UI thread in order for _OpenNewTab to run successfully.
// HasThreadAccess will return true if we're currently on a UI thread and false otherwise.
// When we're on a COM thread, we'll need to dispatch the calls to the UI thread
// and wait on it hence the locking mechanism.
if (Dispatcher().HasThreadAccess())
{
try
{
NewTerminalArgs newTerminalArgs{};
// TODO GH#10952: When we pass the actual commandline (or originating application), the
// settings model can choose the right settings based on command matching, or synthesize
// a profile from the registry/link settings (TODO GH#9458).
// TODO GH#9458: Get and pass the LNK/EXE filenames.
// Passing in a commandline forces GetProfileForArgs to use Base Layer instead of Default Profile;
// in the future, it can make a better decision based on the value we pull out of the process handle.
// TODO GH#5047: When we hang on to the N.T.A., try not to spawn "default... .exe" :)
newTerminalArgs.Commandline(L"default-terminal-invocation-placeholder");
const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
const auto settings{ TerminalSettings::CreateWithProfile(_settings, profile, *_bindings) };
// Request a summon of this window to the foreground
_SummonWindowRequestedHandlers(*this, nullptr);
_CreateNewTabWithProfileAndSettings(profile, settings, connection);
// Request a summon of this window to the foreground
_SummonWindowRequestedHandlers(*this, nullptr);
}
CATCH_RETURN();
return S_OK;
}
else
{
til::latch latch{ 1 };
HRESULT finalVal = S_OK;
Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [&]() {
// Re-running ourselves under the dispatcher will cause us to take the first branch above.
finalVal = _OnNewConnection(connection);
latch.count_down();
});
latch.wait();
return finalVal;
}
}
// Method Description:
@ -2534,11 +2878,14 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Displays a dialog stating the "Touch Keyboard and Handwriting Panel
// Service" is disabled.
void TerminalPage::ShowKeyboardServiceWarning()
void TerminalPage::ShowKeyboardServiceWarning() const
{
if (auto keyboardWarningInfoBar = FindName(L"KeyboardWarningInfoBar").try_as<MUX::Controls::InfoBar>())
if (!_IsMessageDismissed(InfoBarMessage::KeyboardServiceWarning))
{
keyboardWarningInfoBar.IsOpen(true);
if (const auto keyboardServiceWarningInfoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as<MUX::Controls::InfoBar>())
{
keyboardServiceWarningInfoBar.IsOpen(true);
}
}
}
@ -2758,6 +3105,11 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::SetNumberOfOpenWindows(const uint64_t num)
{
_numOpenWindows = num;
}
// Method Description:
// - Returns a label like "Window: 1234" for the ID of this window
// Arguments:
@ -2883,4 +3235,111 @@ namespace winrt::TerminalApp::implementation
{
return WindowName() == QuakeWindowName;
}
// Method Description:
// - This function stops people from duplicating the base profile, because
// it gets ~ ~ weird ~ ~ when they do. Remove when TODO GH#5047 is done.
Profile TerminalPage::GetClosestProfileForDuplicationOfProfile(const Profile& profile) const noexcept
{
if (profile == _settings.ProfileDefaults())
{
return _settings.FindProfile(_settings.GlobalSettings().DefaultProfile());
}
return profile;
}
// Method Description:
// - Handles the change of connection state.
// If the connection state is failure show information bar suggesting to configure termination behavior
// (unless user asked not to show this message again)
// Arguments:
// - sender: the ICoreState instance containing the connection state
// Return Value:
// - <none>
winrt::fire_and_forget TerminalPage::_ConnectionStateChangedHandler(const IInspectable& sender, const IInspectable& /*args*/) const
{
if (const auto coreState{ sender.try_as<winrt::Microsoft::Terminal::Control::ICoreState>() })
{
const auto newConnectionState = coreState.ConnectionState();
if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo))
{
co_await winrt::resume_foreground(Dispatcher());
if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as<MUX::Controls::InfoBar>())
{
infoBar.IsOpen(true);
}
}
}
}
// Method Description:
// - Persists the user's choice not to show information bar guiding to configure termination behavior.
// Then hides this information buffer.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalPage::_CloseOnExitInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const
{
_DismissMessage(InfoBarMessage::CloseOnExitInfo);
if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as<MUX::Controls::InfoBar>())
{
infoBar.IsOpen(false);
}
}
// Method Description:
// - Persists the user's choice not to show information bar warning about "Touch keyboard and Handwriting Panel Service" disabled
// Then hides this information buffer.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalPage::_KeyboardServiceWarningInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const
{
_DismissMessage(InfoBarMessage::KeyboardServiceWarning);
if (const auto infoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as<MUX::Controls::InfoBar>())
{
infoBar.IsOpen(false);
}
}
// Method Description:
// - Checks whether information bar message was dismissed earlier (in the application state)
// Arguments:
// - message: message to look for in the state
// Return Value:
// - true, if the message was dismissed
bool TerminalPage::_IsMessageDismissed(const InfoBarMessage& message)
{
if (const auto dismissedMessages{ ApplicationState::SharedInstance().DismissedMessages() })
{
for (const auto& dismissedMessage : dismissedMessages)
{
if (dismissedMessage == message)
{
return true;
}
}
}
return false;
}
// Method Description:
// - Persists the user's choice to dismiss information bar message (in application state)
// Arguments:
// - message: message to dismiss
// Return Value:
// - <none>
void TerminalPage::_DismissMessage(const InfoBarMessage& message)
{
auto dismissedMessages = ApplicationState::SharedInstance().DismissedMessages();
if (!dismissedMessages)
{
dismissedMessages = winrt::single_threaded_vector<InfoBarMessage>();
}
dismissedMessages.Append(message);
ApplicationState::SharedInstance().DismissedMessages(dismissedMessages);
}
}

View file

@ -58,6 +58,8 @@ namespace winrt::TerminalApp::implementation
void Create();
bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const;
winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e);
hstring Title();
@ -69,7 +71,8 @@ namespace winrt::TerminalApp::implementation
winrt::hstring ApplicationDisplayName();
winrt::hstring ApplicationVersion();
winrt::fire_and_forget CloseWindow();
winrt::fire_and_forget RequestQuit();
winrt::fire_and_forget CloseWindow(bool bypassDialog);
void ToggleFocusMode();
void ToggleFullscreen();
@ -79,16 +82,17 @@ namespace winrt::TerminalApp::implementation
bool AlwaysOnTop() const;
void SetStartupActions(std::vector<Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions);
void PersistWindowLayout();
void SetInboundListener(bool isEmbedding);
static std::vector<Microsoft::Terminal::Settings::Model::ActionAndArgs> ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args);
winrt::TerminalApp::IDialogPresenter DialogPresenter() const;
void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter);
uint64_t GetLastActiveControlTaskbarState();
uint64_t GetLastActiveControlTaskbarProgress();
winrt::TerminalApp::TaskbarState TaskbarState() const;
void ShowKeyboardServiceWarning();
void ShowKeyboardServiceWarning() const;
winrt::hstring KeyboardServiceDisabledText();
winrt::fire_and_forget IdentifyWindow();
@ -105,6 +109,9 @@ namespace winrt::TerminalApp::implementation
winrt::fire_and_forget WindowName(const winrt::hstring& value);
uint64_t WindowId() const noexcept;
void WindowId(const uint64_t& value);
void SetNumberOfOpenWindows(const uint64_t value);
winrt::hstring WindowIdForDisplay() const noexcept;
winrt::hstring WindowNameForDisplay() const noexcept;
bool IsQuakeWindow() const noexcept;
@ -125,6 +132,8 @@ namespace winrt::TerminalApp::implementation
TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs);
TYPED_EVENT(IsQuakeWindowChanged, IInspectable, IInspectable);
TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable);
TYPED_EVENT(OpenSystemMenu, IInspectable, IInspectable);
TYPED_EVENT(QuitRequested, IInspectable, IInspectable);
private:
friend struct TerminalPageT<TerminalPage>; // for Xaml to bind events
@ -156,10 +165,12 @@ namespace winrt::TerminalApp::implementation
bool _isAlwaysOnTop{ false };
winrt::hstring _WindowName{};
uint64_t _WindowId{ 0 };
uint64_t _numOpenWindows{ 0 };
bool _rearranging;
std::optional<int> _rearrangeFrom;
std::optional<int> _rearrangeTo;
bool _maintainStateOnTabClose{ false };
bool _rearranging{ false };
std::optional<int> _rearrangeFrom{};
std::optional<int> _rearrangeTo{};
bool _removing{ false };
uint32_t _systemRowsToScroll{ DefaultRowsToScroll };
@ -181,6 +192,7 @@ namespace winrt::TerminalApp::implementation
std::shared_ptr<Toast> _windowRenameFailedToast{ nullptr };
void _ShowAboutDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowQuitDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowCloseWarningDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowCloseReadOnlyDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowMultiLinePasteWarningDialog();
@ -188,9 +200,10 @@ namespace winrt::TerminalApp::implementation
void _CreateNewTabFlyout();
void _OpenNewTabDropdown();
void _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
void _CreateNewTabFromSettings(GUID profileGuid, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings);
HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
void _CreateNewTabFromPane(std::shared_ptr<Pane> pane);
void _CreateNewTabWithProfileAndSettings(const Microsoft::Terminal::Settings::Model::Profile& profile, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Settings::Model::TerminalSettings settings);
winrt::fire_and_forget _OpenNewWindow(const bool elevate, const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs);
@ -219,12 +232,17 @@ namespace winrt::TerminalApp::implementation
void _DuplicateFocusedTab();
void _DuplicateTab(const TerminalTab& tab);
void _SplitTab(TerminalTab& tab);
winrt::fire_and_forget _ExportTab(const TerminalTab& tab);
winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::TabBase tab);
void _CloseTabAtIndex(uint32_t index);
void _RemoveTab(const winrt::TerminalApp::TabBase& tab);
winrt::fire_and_forget _RemoveTabs(const std::vector<winrt::TerminalApp::TabBase> tabs);
void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term, TerminalTab& hostingTab);
void _InitializeTab(winrt::com_ptr<TerminalTab> newTabImpl);
void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term);
void _RegisterTabEvents(TerminalTab& hostingTab);
void _DismissTabContextMenus();
void _FocusCurrentTab(const bool focusAlways);
@ -235,7 +253,8 @@ namespace winrt::TerminalApp::implementation
void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference<Microsoft::Terminal::Settings::Model::TabSwitcherMode>& customTabSwitcherMode);
bool _SelectTab(uint32_t tabIndex);
bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _MovePane(const uint32_t tabIdx);
winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl();
std::optional<uint32_t> _GetFocusedTabIndex() const noexcept;
@ -254,6 +273,11 @@ namespace winrt::TerminalApp::implementation
const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual,
const float splitSize = 0.5f,
const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr);
void _SplitPane(TerminalTab& tab,
const Microsoft::Terminal::Settings::Model::SplitState splitType,
const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual,
const float splitSize = 0.5f,
const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr);
void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
void _ToggleSplitOrientation();
@ -337,7 +361,7 @@ namespace winrt::TerminalApp::implementation
winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr };
winrt::Microsoft::Terminal::Settings::Model::TerminalSettings _originalSettings{ nullptr };
void _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection);
HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection);
void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs);
@ -348,6 +372,14 @@ namespace winrt::TerminalApp::implementation
void _SetFocusMode(const bool inFocusMode);
winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept;
winrt::fire_and_forget _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const;
void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const;
void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const;
static bool _IsMessageDismissed(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message);
static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message);
#pragma region ActionHandlers
// These are all defined in AppActionHandlers.cpp
#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action);

View file

@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "TaskbarState.idl";
namespace TerminalApp
{
@ -32,6 +33,7 @@ namespace TerminalApp
UInt64 WindowId;
String WindowNameForDisplay { get; };
String WindowIdForDisplay { get; };
void SetNumberOfOpenWindows(UInt64 num);
void RenameFailed();
Boolean IsQuakeWindow();
@ -42,8 +44,7 @@ namespace TerminalApp
void ShowKeyboardServiceWarning();
String KeyboardServiceDisabledText { get; };
UInt64 GetLastActiveControlTaskbarState();
UInt64 GetLastActiveControlTaskbarProgress();
TaskbarState TaskbarState{ get; };
event Windows.Foundation.TypedEventHandler<Object, String> TitleChanged;
event Windows.Foundation.TypedEventHandler<Object, LastTabClosedEventArgs> LastTabClosed;
@ -57,5 +58,6 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
}
}

View file

@ -47,6 +47,11 @@
</StackPanel>
</ContentDialog>
<ContentDialog x:Name="QuitDialog"
x:Uid="QuitDialog"
x:Load="False"
DefaultButton="Primary" />
<ContentDialog x:Name="CloseAllDialog"
x:Uid="CloseAllDialog"
x:Load="False"
@ -109,13 +114,33 @@
PreviewKeyDown="_KeyDownHandler"
Visibility="Collapsed" />
<mux:InfoBar x:Name="KeyboardWarningInfoBar"
x:Load="False"
IsClosable="True"
IsIconVisible="True"
IsOpen="False"
Message="{x:Bind KeyboardServiceDisabledText, Mode=OneWay}"
Severity="Warning" />
<StackPanel>
<mux:InfoBar x:Name="KeyboardServiceWarningInfoBar"
x:Load="False"
IsClosable="True"
IsIconVisible="True"
IsOpen="False"
Message="{x:Bind KeyboardServiceDisabledText, Mode=OneWay}"
Severity="Warning">
<mux:InfoBar.ActionButton>
<Button x:Uid="InfoBarDismissButton"
Click="_KeyboardServiceWarningInfoDismissHandler" />
</mux:InfoBar.ActionButton>
</mux:InfoBar>
<mux:InfoBar x:Name="CloseOnExitInfoBar"
x:Uid="CloseOnExitInfoBar"
x:Load="False"
IsClosable="True"
IsIconVisible="True"
IsOpen="False"
Severity="Informational">
<mux:InfoBar.ActionButton>
<Button x:Uid="InfoBarDismissButton"
Click="_CloseOnExitInfoDismissHandler" />
</mux:InfoBar.ActionButton>
</mux:InfoBar>
</StackPanel>
<!--
A TeachingTip with IsLightDismissEnabled="True" will immediately

View file

@ -25,19 +25,66 @@ namespace winrt
namespace winrt::TerminalApp::implementation
{
TerminalTab::TerminalTab(const GUID& profile, const TermControl& control)
TerminalTab::TerminalTab(const Profile& profile, const TermControl& control)
{
_rootPane = std::make_shared<Pane>(profile, control, true);
_rootPane->Id(_nextPaneId);
_activePane = _rootPane;
_mruPanes.insert(_mruPanes.begin(), _nextPaneId);
++_nextPaneId;
_rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) {
_Setup();
}
TerminalTab::TerminalTab(std::shared_ptr<Pane> rootPane)
{
_rootPane = rootPane;
_activePane = nullptr;
auto firstId = _nextPaneId;
_rootPane->WalkTree([&](std::shared_ptr<Pane> pane) {
// update the IDs on each pane
if (pane->_IsLeaf())
{
pane->Id(_nextPaneId);
_nextPaneId++;
}
// Try to find the pane marked active (if it exists)
if (pane->_lastActive)
{
_activePane = pane;
}
return false;
});
// In case none of the panes were already marked as the focus, just
// focus the first one.
if (_activePane == nullptr)
{
_rootPane->FocusPane(firstId);
_activePane = _rootPane->GetActivePane();
}
// Set the active control
_mruPanes.insert(_mruPanes.begin(), _activePane->Id().value());
_Setup();
}
// Method Description:
// - Shared setup for the constructors. Assumed that _rootPane has been set.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalTab::_Setup()
{
_rootClosedToken = _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) {
_ClosedHandlers(nullptr, nullptr);
});
_activePane = _rootPane;
Content(_rootPane->GetRootElement());
_MakeTabViewItem();
@ -144,19 +191,31 @@ namespace winrt::TerminalApp::implementation
// that was last focused.
TermControl TerminalTab::GetActiveTerminalControl() const
{
return _activePane->GetTerminalControl();
if (_activePane)
{
return _activePane->GetTerminalControl();
}
return nullptr;
}
// Method Description:
// - Called after construction of a Tab object to bind event handlers to its
// associated Pane and TermControl object
// associated Pane and TermControl objects
// Arguments:
// - control: reference to the TermControl object to bind event to
// - <none>
// Return Value:
// - <none>
void TerminalTab::Initialize(const TermControl& control)
void TerminalTab::Initialize()
{
_BindEventHandlers(control);
_rootPane->WalkTree([&](std::shared_ptr<Pane> pane) {
// Attach event handlers to each new pane
_AttachEventHandlersToPane(pane);
if (auto control = pane->GetTerminalControl())
{
_AttachEventHandlersToControl(pane->Id().value(), control);
}
return false;
});
}
// Method Description:
@ -177,10 +236,9 @@ namespace winrt::TerminalApp::implementation
{
lastFocusedControl.Focus(_focusState);
// Update our own progress state, and fire an event signaling
// Update our own progress state. This will fire an event signaling
// that our taskbar progress changed.
_UpdateProgressState();
_TaskbarProgressChangedHandlers(lastFocusedControl, nullptr);
}
// When we gain focus, remove the bell indicator if it is active
if (_tabStatus.BellIndicator())
@ -199,35 +257,19 @@ namespace winrt::TerminalApp::implementation
// Return Value:
// - nullopt if no children of this tab were the last control to be
// focused, else the GUID of the profile of the last control to be focused
std::optional<GUID> TerminalTab::GetFocusedProfile() const noexcept
Profile TerminalTab::GetFocusedProfile() const noexcept
{
return _activePane->GetFocusedProfile();
}
// Method Description:
// - Called after construction of a Tab object to bind event handlers to its
// associated Pane and TermControl object
// Arguments:
// - control: reference to the TermControl object to bind event to
// - Attempts to update the settings that apply to this tab.
// - Panes are handled elsewhere, by somebody who can establish broader knowledge
// of the settings that apply to all tabs.
// Return Value:
// - <none>
void TerminalTab::_BindEventHandlers(const TermControl& control) noexcept
void TerminalTab::UpdateSettings()
{
_AttachEventHandlersToPane(_rootPane);
_AttachEventHandlersToControl(control);
}
// Method Description:
// - Attempts to update the settings of this tab's tree of panes.
// Arguments:
// - settings: The new TerminalSettingsCreateResult to apply to any matching controls
// - profile: The GUID of the profile these settings should apply to.
// Return Value:
// - <none>
void TerminalTab::UpdateSettings(const TerminalSettingsCreateResult& settings, const GUID& profile)
{
_rootPane->UpdateSettings(settings, profile);
// The tabWidthMode may have changed, update the header control accordingly
_UpdateHeaderControlMaxWidth();
}
@ -395,6 +437,50 @@ namespace winrt::TerminalApp::implementation
control.ScrollViewport(::base::ClampAdd(currentOffset, delta));
}
// Method Description:
// - Serializes the state of this tab as a series of commands that can be
// executed to recreate it.
// Arguments:
// - <none>
// Return Value:
// - A vector of commands
std::vector<ActionAndArgs> TerminalTab::BuildStartupActions() const
{
// Give initial ids (0 for the child created with this tab,
// 1 for the child after the first split.
auto state = _rootPane->BuildStartupActions(0, 1);
ActionAndArgs newTabAction{};
newTabAction.Action(ShortcutAction::NewTab);
NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() };
newTabAction.Args(newTabArgs);
state.args.emplace(state.args.begin(), std::move(newTabAction));
// If we only have one arg, we only have 1 pane so we don't need any
// special focus logic
if (state.args.size() > 1 && state.focusedPaneId.has_value())
{
ActionAndArgs focusPaneAction{};
focusPaneAction.Action(ShortcutAction::FocusPane);
FocusPaneArgs focusArgs{ state.focusedPaneId.value() };
focusPaneAction.Args(focusArgs);
state.args.emplace_back(std::move(focusPaneAction));
}
if (_zoomedPane)
{
// we start without any panes zoomed so toggle zoom will enable zoom.
ActionAndArgs zoomPaneAction{};
zoomPaneAction.Action(ShortcutAction::TogglePaneZoom);
state.args.emplace_back(std::move(zoomPaneAction));
}
return state.args;
}
// Method Description:
// - Split the focused pane in our tree of panes, and place the
// given TermControl into the newly created pane.
@ -406,7 +492,7 @@ namespace winrt::TerminalApp::implementation
// - <none>
void TerminalTab::SplitPane(SplitState splitType,
const float splitSize,
const GUID& profile,
const Profile& profile,
TermControl& control)
{
// Make sure to take the ID before calling Split() - Split() will clear out the active pane's ID
@ -426,10 +512,10 @@ namespace winrt::TerminalApp::implementation
++_nextPaneId;
}
_activePane = first;
_AttachEventHandlersToControl(control);
// Add a event handlers to the new panes' GotFocus event. When the pane
// gains focus, we'll mark it as the new active pane.
_AttachEventHandlersToControl(second->Id().value(), control);
_AttachEventHandlersToPane(first);
_AttachEventHandlersToPane(second);
@ -440,11 +526,121 @@ namespace winrt::TerminalApp::implementation
_UpdateActivePane(second);
}
// Method Description:
// - Removes the currently active pane from this tab. If that was the only
// remaining pane, then the entire tab is closed as well.
// Arguments:
// - <none>
// Return Value:
// - The removed pane, if the remove succeeded.
std::shared_ptr<Pane> TerminalTab::DetachPane()
{
// if we only have one pane, remove it entirely
// and close this tab
if (_rootPane == _activePane)
{
return DetachRoot();
}
// Attempt to remove the active pane from the tree
if (const auto pane = _rootPane->DetachPane(_activePane))
{
// Just make sure that the remaining pane is marked active
_UpdateActivePane(_rootPane->GetActivePane());
return pane;
}
return nullptr;
}
// Method Description:
// - Closes this tab and returns the root pane to be used elsewhere.
// Arguments:
// - <none>
// Return Value:
// - The root pane.
std::shared_ptr<Pane> TerminalTab::DetachRoot()
{
// remove the closed event handler since we are closing the tab
// manually.
_rootPane->Closed(_rootClosedToken);
auto p = _rootPane;
p->WalkTree([](auto pane) {
pane->_PaneDetachedHandlers(pane);
return false;
});
// Clean up references and close the tab
_rootPane = nullptr;
_activePane = nullptr;
Content(nullptr);
_ClosedHandlers(nullptr, nullptr);
return p;
}
// Method Description:
// - Add an arbitrary pane to this tab. This will be added as a split on the
// currently active pane.
// Arguments:
// - pane: The pane to add.
// Return Value:
// - <none>
void TerminalTab::AttachPane(std::shared_ptr<Pane> pane)
{
// Add the new event handlers to the new pane(s)
// and update their ids.
pane->WalkTree([&](auto p) {
_AttachEventHandlersToPane(p);
if (p->_IsLeaf())
{
p->Id(_nextPaneId);
_nextPaneId++;
}
if (auto control = p->GetTerminalControl())
{
_AttachEventHandlersToControl(p->Id().value(), control);
}
return false;
});
// pass the old id to the new child
const auto previousId = _activePane->Id();
// Add the new pane as an automatic split on the active pane.
auto first = _activePane->AttachPane(pane, SplitState::Automatic);
// under current assumptions this condition should always be true.
if (previousId)
{
first->Id(previousId.value());
}
else
{
first->Id(_nextPaneId);
++_nextPaneId;
}
// Update with event handlers on the new child.
_activePane = first;
_AttachEventHandlersToPane(first);
// Make sure that we have the right pane set as the active pane
pane->WalkTree([&](auto p) {
if (p->_lastActive)
{
_UpdateActivePane(p);
return true;
}
return false;
});
}
// Method Description:
// - Find the currently active pane, and then switch the split direction of
// its parent. E.g. switch from Horizontal to Vertical.
// Return Value:
// - <none>
void TerminalTab::ToggleSplitOrientation()
{
@ -496,21 +692,21 @@ namespace winrt::TerminalApp::implementation
// to the terminal when no other panes are present (GH#6219)
bool TerminalTab::NavigateFocus(const FocusDirection& direction)
{
if (direction == FocusDirection::Previous)
// NOTE: This _must_ be called on the root pane, so that it can propagate
// throughout the entire tree.
if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes))
{
if (_mruPanes.size() < 2)
const auto res = _rootPane->FocusPane(newFocus);
if (_zoomedPane)
{
return false;
UpdateZoom(newFocus);
}
// To get to the previous pane, get the id of the previous pane and focus to that
return _rootPane->FocusPane(_mruPanes.at(1));
}
else
{
// NOTE: This _must_ be called on the root pane, so that it can propagate
// throughout the entire tree.
return _rootPane->NavigateFocus(direction);
return res;
}
return false;
}
// Method Description:
@ -520,26 +716,17 @@ namespace winrt::TerminalApp::implementation
// Arguments:
// - direction: The direction to move the pane in.
// Return Value:
// - <none>
void TerminalTab::MovePane(const FocusDirection& direction)
// - true if two panes were swapped.
bool TerminalTab::SwapPane(const FocusDirection& direction)
{
if (direction == FocusDirection::Previous)
// NOTE: This _must_ be called on the root pane, so that it can propagate
// throughout the entire tree.
if (auto neighbor = _rootPane->NavigateDirection(_activePane, direction, _mruPanes))
{
if (_mruPanes.size() < 2)
{
return;
}
if (auto lastPane = _rootPane->FindPane(_mruPanes.at(1)))
{
_rootPane->SwapPanes(_activePane, lastPane);
}
}
else
{
// NOTE: This _must_ be called on the root pane, so that it can propagate
// throughout the entire tree.
_rootPane->MovePane(direction);
return _rootPane->SwapPanes(_activePane, neighbor);
}
return false;
}
bool TerminalTab::FocusPane(const uint32_t id)
@ -551,7 +738,10 @@ namespace winrt::TerminalApp::implementation
// - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections.
void TerminalTab::Shutdown()
{
_rootPane->Shutdown();
if (_rootPane)
{
_rootPane->Shutdown();
}
}
// Method Description:
@ -596,6 +786,34 @@ namespace winrt::TerminalApp::implementation
_headerControl.BeginRename();
}
// Method Description:
// - Removes any event handlers set by the tab on the given pane's control.
// The pane's ID is the most stable identifier for a given control, because
// the control itself doesn't have a particular ID and its pointer is
// unstable since it is moved when panes split.
// Arguments:
// - paneId: The ID of the pane that contains the given control.
// - control: the control to remove events from.
// Return Value:
// - <none>
void TerminalTab::_DetachEventHandlersFromControl(const uint32_t paneId, const TermControl& control)
{
auto it = _controlEvents.find(paneId);
if (it != _controlEvents.end())
{
auto& events = it->second;
control.TitleChanged(events.titleToken);
control.FontSizeChanged(events.fontToken);
control.TabColorChanged(events.colorToken);
control.SetTaskbarProgress(events.taskbarToken);
control.ReadOnlyChanged(events.readOnlyToken);
control.FocusFollowMouseRequested(events.focusToken);
_controlEvents.erase(paneId);
}
}
// Method Description:
// - Register any event handlers that we may need with the given TermControl.
// This should be called on each and every TermControl that we add to the tree
@ -603,15 +821,17 @@ namespace winrt::TerminalApp::implementation
// * notify us when the control's title changed, so we can update our own
// title (if necessary)
// Arguments:
// - paneId: the ID of the pane that this control belongs to.
// - control: the TermControl to add events to.
// Return Value:
// - <none>
void TerminalTab::_AttachEventHandlersToControl(const TermControl& control)
void TerminalTab::_AttachEventHandlersToControl(const uint32_t paneId, const TermControl& control)
{
auto weakThis{ get_weak() };
auto dispatcher = TabViewItem().Dispatcher();
ControlEventTokens events{};
control.TitleChanged([weakThis](auto&&, auto&&) {
events.titleToken = control.TitleChanged([weakThis](auto&&, auto&&) {
// Check if Tab's lifetime has expired
if (auto tab{ weakThis.get() })
{
@ -626,16 +846,16 @@ namespace winrt::TerminalApp::implementation
// On the latter event, we tell the root pane to resize itself so that its descendants
// (including ourself) can properly snap to character grids. In future, we may also
// want to do that on regular font changes.
control.FontSizeChanged([this](const int /* fontWidth */,
const int /* fontHeight */,
const bool isInitialChange) {
events.fontToken = control.FontSizeChanged([this](const int /* fontWidth */,
const int /* fontHeight */,
const bool isInitialChange) {
if (isInitialChange)
{
_rootPane->Relayout();
}
});
control.TabColorChanged([weakThis](auto&&, auto&&) {
events.colorToken = control.TabColorChanged([weakThis](auto&&, auto&&) {
if (auto tab{ weakThis.get() })
{
// The control's tabColor changed, but it is not necessarily the
@ -645,7 +865,7 @@ namespace winrt::TerminalApp::implementation
}
});
control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget {
events.taskbarToken = control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget {
co_await winrt::resume_foreground(dispatcher);
// Check if Tab's lifetime has expired
if (auto tab{ weakThis.get() })
@ -654,14 +874,14 @@ namespace winrt::TerminalApp::implementation
}
});
control.ReadOnlyChanged([weakThis](auto&&, auto&&) {
events.readOnlyToken = control.ReadOnlyChanged([weakThis](auto&&, auto&&) {
if (auto tab{ weakThis.get() })
{
tab->_RecalculateAndApplyReadOnly();
}
});
control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) {
events.focusToken = control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) {
if (const auto tab{ weakThis.get() })
{
if (tab->_focusState != FocusState::Unfocused)
@ -673,6 +893,31 @@ namespace winrt::TerminalApp::implementation
}
}
});
_controlEvents[paneId] = events;
}
// Method Description:
// - Get the combined taskbar state for the tab. This is the combination of
// all the states of all our panes. Taskbar states are given a priority
// based on the rules in:
// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate
// under "How the Taskbar Button Chooses the Progress Indicator for a
// Group"
// Arguments:
// - <none>
// Return Value:
// - A TaskbarState object representing the combined taskbar state and
// progress percentage of all our panes.
winrt::TerminalApp::TaskbarState TerminalTab::GetCombinedTaskbarState() const
{
std::vector<winrt::TerminalApp::TaskbarState> states;
if (_rootPane)
{
_rootPane->CollectTaskbarStates(states);
}
return states.empty() ? winrt::make<winrt::TerminalApp::implementation::TaskbarState>() :
*std::min_element(states.begin(), states.end(), TerminalApp::implementation::TaskbarState::ComparePriority);
}
// Method Description:
@ -690,37 +935,39 @@ namespace winrt::TerminalApp::implementation
// - <none>
void TerminalTab::_UpdateProgressState()
{
if (const auto& activeControl{ GetActiveTerminalControl() })
{
const auto taskbarState = activeControl.TaskbarState();
// The progress of the control changed, but not necessarily the progress of the tab.
// Set the tab's progress ring to the active pane's progress
if (taskbarState > 0)
{
if (taskbarState == 3)
{
// 3 is the indeterminate state, set the progress ring as such
_tabStatus.IsProgressRingIndeterminate(true);
}
else
{
// any non-indeterminate state has a value, set the progress ring as such
_tabStatus.IsProgressRingIndeterminate(false);
const auto state{ GetCombinedTaskbarState() };
const auto progressValue = gsl::narrow<uint32_t>(activeControl.TaskbarProgress());
_tabStatus.ProgressValue(progressValue);
}
// Hide the tab icon (the progress ring is placed over it)
HideIcon(true);
_tabStatus.IsProgressRingActive(true);
const auto taskbarState = state.State();
// The progress of the control changed, but not necessarily the progress of the tab.
// Set the tab's progress ring to the active pane's progress
if (taskbarState > 0)
{
if (taskbarState == 3)
{
// 3 is the indeterminate state, set the progress ring as such
_tabStatus.IsProgressRingIndeterminate(true);
}
else
{
// Show the tab icon
HideIcon(false);
_tabStatus.IsProgressRingActive(false);
// any non-indeterminate state has a value, set the progress ring as such
_tabStatus.IsProgressRingIndeterminate(false);
const auto progressValue = gsl::narrow<uint32_t>(state.Progress());
_tabStatus.ProgressValue(progressValue);
}
// Hide the tab icon (the progress ring is placed over it)
HideIcon(true);
_tabStatus.IsProgressRingActive(true);
}
else
{
// Show the tab icon
HideIcon(false);
_tabStatus.IsProgressRingActive(false);
}
// fire an event signaling that our taskbar progress changed.
_TaskbarProgressChangedHandlers(nullptr, nullptr);
}
// Method Description:
@ -777,7 +1024,7 @@ namespace winrt::TerminalApp::implementation
auto weakThis{ get_weak() };
std::weak_ptr<Pane> weakPane{ pane };
pane->GotFocus([weakThis](std::shared_ptr<Pane> sender) {
auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr<Pane> sender) {
// Do nothing if the Tab's lifetime is expired or pane isn't new.
auto tab{ weakThis.get() };
@ -797,7 +1044,7 @@ namespace winrt::TerminalApp::implementation
}
});
pane->LostFocus([weakThis](std::shared_ptr<Pane> /*sender*/) {
auto lostFocusToken = pane->LostFocus([weakThis](std::shared_ptr<Pane> /*sender*/) {
// Do nothing if the Tab's lifetime is expired or pane isn't new.
auto tab{ weakThis.get() };
@ -811,7 +1058,7 @@ namespace winrt::TerminalApp::implementation
// Add a Closed event handler to the Pane. If the pane closes out from
// underneath us, and it's zoomed, we want to be able to make sure to
// update our state accordingly to un-zoom that pane. See GH#7252.
pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget {
auto closedToken = pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget {
if (auto tab{ weakThis.get() })
{
if (tab->_zoomedPane)
@ -836,7 +1083,7 @@ namespace winrt::TerminalApp::implementation
});
// Add a PaneRaiseBell event handler to the Pane
pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) {
auto bellToken = pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) {
if (auto tab{ weakThis.get() })
{
if (visual)
@ -858,6 +1105,40 @@ namespace winrt::TerminalApp::implementation
}
}
});
// box the event token so that we can give a reference to it in the
// event handler.
auto detachedToken = std::make_shared<winrt::event_token>();
// Add a Detached event handler to the Pane to clean up tab state
// and other event handlers when a pane is removed from this tab.
*detachedToken = pane->Detached([weakThis, weakPane, gotFocusToken, lostFocusToken, closedToken, bellToken, detachedToken](std::shared_ptr<Pane> /*sender*/) {
// Make sure we do this at most once
if (auto pane{ weakPane.lock() })
{
pane->Detached(*detachedToken);
pane->GotFocus(gotFocusToken);
pane->LostFocus(lostFocusToken);
pane->Closed(closedToken);
pane->PaneRaiseBell(bellToken);
if (auto tab{ weakThis.get() })
{
if (auto control = pane->GetTerminalControl())
{
tab->_DetachEventHandlersFromControl(pane->Id().value(), control);
}
for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i)
{
if (*i == pane->Id())
{
tab->_mruPanes.erase(i);
break;
}
}
}
}
});
}
// Method Description:
@ -936,12 +1217,48 @@ namespace winrt::TerminalApp::implementation
duplicateTabMenuItem.Icon(duplicateTabSymbol);
}
Controls::MenuFlyoutItem splitTabMenuItem;
{
// "Split Tab"
Controls::FontIcon splitTabSymbol;
splitTabSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" });
splitTabSymbol.Glyph(L"\xF246"); // ViewDashboard
splitTabMenuItem.Click([weakThis](auto&&, auto&&) {
if (auto tab{ weakThis.get() })
{
tab->_SplitTabRequestedHandlers();
}
});
splitTabMenuItem.Text(RS_(L"SplitTabText"));
splitTabMenuItem.Icon(splitTabSymbol);
}
Controls::MenuFlyoutItem exportTabMenuItem;
{
// "Split Tab"
Controls::FontIcon exportTabSymbol;
exportTabSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" });
exportTabSymbol.Glyph(L"\xE74E"); // Save
exportTabMenuItem.Click([weakThis](auto&&, auto&&) {
if (auto tab{ weakThis.get() })
{
tab->_ExportTabRequestedHandlers();
}
});
exportTabMenuItem.Text(RS_(L"ExportTabText"));
exportTabMenuItem.Icon(exportTabSymbol);
}
// Build the menu
Controls::MenuFlyout contextMenuFlyout;
Controls::MenuFlyoutSeparator menuSeparator;
contextMenuFlyout.Items().Append(chooseColorMenuItem);
contextMenuFlyout.Items().Append(renameTabMenuItem);
contextMenuFlyout.Items().Append(duplicateTabMenuItem);
contextMenuFlyout.Items().Append(splitTabMenuItem);
contextMenuFlyout.Items().Append(exportTabMenuItem);
contextMenuFlyout.Items().Append(menuSeparator);
// GH#5750 - When the context menu is dismissed with ESC, toss the focus
@ -1212,6 +1529,22 @@ namespace winrt::TerminalApp::implementation
return _rootPane->PreCalculateCanSplit(_activePane, splitType, splitSize, availableSpace).value_or(false);
}
// Method Description:
// - Updates the zoomed pane when the focus changes
// Arguments:
// - newFocus: the new pane to be zoomed
// Return Value:
// - <none>
void TerminalTab::UpdateZoom(std::shared_ptr<Pane> newFocus)
{
// clear the existing content so the old zoomed pane can be added back to the root tree
Content(nullptr);
_rootPane->Restore(_zoomedPane);
_zoomedPane = newFocus;
_rootPane->Maximize(_zoomedPane);
Content(_zoomedPane->GetRootElement());
}
// Method Description:
// - Toggle our zoom state.
// * If we're not zoomed, then zoom the active pane, making it take the
@ -1315,4 +1648,6 @@ namespace winrt::TerminalApp::implementation
DEFINE_EVENT(TerminalTab, ColorCleared, _colorCleared, winrt::delegate<>);
DEFINE_EVENT(TerminalTab, TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>);
DEFINE_EVENT(TerminalTab, DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>);
DEFINE_EVENT(TerminalTab, SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>);
DEFINE_EVENT(TerminalTab, ExportTabRequested, _ExportTabRequestedHandlers, winrt::delegate<>);
}

View file

@ -21,21 +21,26 @@ namespace winrt::TerminalApp::implementation
struct TerminalTab : TerminalTabT<TerminalTab, TabBase>
{
public:
TerminalTab(const GUID& profile, const winrt::Microsoft::Terminal::Control::TermControl& control);
TerminalTab(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, const winrt::Microsoft::Terminal::Control::TermControl& control);
TerminalTab(std::shared_ptr<Pane> rootPane);
// Called after construction to perform the necessary setup, which relies on weak_ptr
void Initialize(const winrt::Microsoft::Terminal::Control::TermControl& control);
void Initialize();
winrt::Microsoft::Terminal::Control::TermControl GetActiveTerminalControl() const;
std::optional<GUID> GetFocusedProfile() const noexcept;
winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile() const noexcept;
void Focus(winrt::Windows::UI::Xaml::FocusState focusState) override;
winrt::fire_and_forget Scroll(const int delta);
std::shared_ptr<Pane> DetachRoot();
std::shared_ptr<Pane> DetachPane();
void AttachPane(std::shared_ptr<Pane> pane);
void SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType,
const float splitSize,
const GUID& profile,
const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
winrt::Microsoft::Terminal::Control::TermControl& control);
void ToggleSplitOrientation();
@ -54,10 +59,10 @@ namespace winrt::TerminalApp::implementation
void ResizeContent(const winrt::Windows::Foundation::Size& newSize);
void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
void MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool FocusPane(const uint32_t id);
void UpdateSettings(const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, const GUID& profile);
void UpdateSettings();
winrt::fire_and_forget UpdateTitle();
void Shutdown() override;
@ -74,15 +79,21 @@ namespace winrt::TerminalApp::implementation
void ResetRuntimeTabColor();
void ActivateColorPicker();
void UpdateZoom(std::shared_ptr<Pane> newFocus);
void ToggleZoom();
bool IsZoomed();
void EnterZoom();
void ExitZoom();
std::vector<Microsoft::Terminal::Settings::Model::ActionAndArgs> BuildStartupActions() const;
int GetLeafPaneCount() const noexcept;
void TogglePaneReadOnly();
std::shared_ptr<Pane> GetActivePane() const;
winrt::TerminalApp::TaskbarState GetCombinedTaskbarState() const;
std::shared_ptr<Pane> GetRootPane() const { return _rootPane; }
winrt::TerminalApp::TerminalTabStatus TabStatus()
{
@ -94,12 +105,15 @@ namespace winrt::TerminalApp::implementation
DECLARE_EVENT(ColorCleared, _colorCleared, winrt::delegate<>);
DECLARE_EVENT(TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>);
DECLARE_EVENT(DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>);
DECLARE_EVENT(SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>);
DECLARE_EVENT(ExportTabRequested, _ExportTabRequestedHandlers, winrt::delegate<>);
TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable);
private:
std::shared_ptr<Pane> _rootPane{ nullptr };
std::shared_ptr<Pane> _activePane{ nullptr };
std::shared_ptr<Pane> _zoomedPane{ nullptr };
winrt::hstring _lastIconPath{};
winrt::TerminalApp::ColorPickupFlyout _tabColorPickup{};
std::optional<winrt::Windows::UI::Color> _themeTabColor{};
@ -107,6 +121,19 @@ namespace winrt::TerminalApp::implementation
winrt::TerminalApp::TabHeaderControl _headerControl{};
winrt::TerminalApp::TerminalTabStatus _tabStatus{};
struct ControlEventTokens
{
winrt::event_token titleToken;
winrt::event_token fontToken;
winrt::event_token colorToken;
winrt::event_token taskbarToken;
winrt::event_token readOnlyToken;
winrt::event_token focusToken;
};
std::unordered_map<uint32_t, ControlEventTokens> _controlEvents;
winrt::event_token _rootClosedToken{};
std::vector<uint32_t> _mruPanes;
uint32_t _nextPaneId{ 0 };
@ -119,6 +146,8 @@ namespace winrt::TerminalApp::implementation
winrt::TerminalApp::ShortcutActionDispatch _dispatch;
void _Setup();
std::optional<Windows::UI::Xaml::DispatcherTimer> _bellIndicatorTimer;
void _BellIndicatorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
@ -131,9 +160,8 @@ namespace winrt::TerminalApp::implementation
void _RefreshVisualState();
void _BindEventHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control) noexcept;
void _AttachEventHandlersToControl(const winrt::Microsoft::Terminal::Control::TermControl& control);
void _DetachEventHandlersFromControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control);
void _AttachEventHandlersToControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control);
void _AttachEventHandlersToPane(std::shared_ptr<Pane> pane);
void _UpdateActivePane(std::shared_ptr<Pane> pane);

View file

@ -2,5 +2,5 @@
<packages>
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
<package id="Microsoft.UI.Xaml" version="2.5.0-prerelease.201202003" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.210309.3" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.210825.3" targetFramework="native" />
</packages>

View file

@ -56,6 +56,9 @@
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include <winrt/Microsoft.Terminal.Settings.Editor.h>
#include <winrt/Microsoft.Terminal.Settings.Model.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Provider.h>
#include <winrt/Windows.Storage.Pickers.h>
#include <windows.ui.xaml.media.dxinterop.h>
@ -66,6 +69,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider);
#include <telemetry/ProjectTelemetry.h>
#include <TraceLoggingActivity.h>
#include <msctf.h>
#include <shellapi.h>
#include <shobjidl_core.h>

View file

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

View file

@ -11,6 +11,8 @@ using namespace Microsoft::WRL;
static NewHandoffFunction _pfnHandoff = nullptr;
// The registration ID of the class object for clean up later
static DWORD g_cTerminalHandoffRegistration = 0;
// Mutex so we only do start/stop/establish one at a time.
static std::shared_mutex _mtx;
// Routine Description:
// - Starts listening for TerminalHandoff requests by registering
@ -19,9 +21,11 @@ static DWORD g_cTerminalHandoffRegistration = 0;
// - pfnHandoff - Function to callback when a handoff is received
// Return Value:
// - S_OK, E_NOT_VALID_STATE (start called when already started) or relevant COM registration error.
HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff) noexcept
HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff)
try
{
std::unique_lock lock{ _mtx };
RETURN_HR_IF(E_NOT_VALID_STATE, _pfnHandoff != nullptr);
const auto classFactory = Make<SimpleClassFactory<CTerminalHandoff>>();
@ -31,7 +35,7 @@ try
ComPtr<IUnknown> unk;
RETURN_IF_FAILED(classFactory.As(&unk));
RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &g_cTerminalHandoffRegistration));
RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE, &g_cTerminalHandoffRegistration));
_pfnHandoff = pfnHandoff;
@ -46,8 +50,10 @@ CATCH_RETURN()
// - <none>
// Return Value:
// - S_OK, E_NOT_VALID_STATE (stop called when not started), or relevant COM class revoke error
HRESULT CTerminalHandoff::s_StopListening() noexcept
HRESULT CTerminalHandoff::s_StopListening()
{
std::unique_lock lock{ _mtx };
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff);
_pfnHandoff = nullptr;
@ -91,10 +97,19 @@ static HRESULT _duplicateHandle(const HANDLE in, HANDLE& out) noexcept
// - E_NOT_VALID_STATE if a event handler is not registered before calling. `::DuplicateHandle`
// error codes if we cannot manage to make our own copy of handles to retain. Or S_OK/error
// from the registered handler event function.
HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) noexcept
HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client)
{
// Stash a local copy of _pfnHandoff before we stop listening.
auto localPfnHandoff = _pfnHandoff;
// Because we are REGCLS_SINGLEUSE... we need to `CoRevokeClassObject` after we handle this ONE call.
// COM does not automatically clean that up for us. We must do it.
s_StopListening();
std::unique_lock lock{ _mtx };
// Report an error if no one registered a handoff function before calling this.
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff);
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, localPfnHandoff);
// Duplicate the handles from what we received.
// The contract with COM specifies that any HANDLEs we receive from the caller belong
@ -108,5 +123,5 @@ HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE sign
RETURN_IF_FAILED(_duplicateHandle(client, client));
// Call registered handler from when we started listening.
return _pfnHandoff(in, out, signal, ref, server, client);
return localPfnHandoff(in, out, signal, ref, server, client);
}

View file

@ -37,12 +37,12 @@ struct __declspec(uuid(__CLSID_CTerminalHandoff))
HANDLE signal,
HANDLE ref,
HANDLE server,
HANDLE client) noexcept override;
HANDLE client) override;
#pragma endregion
static HRESULT s_StartListening(NewHandoffFunction pfnHandoff) noexcept;
static HRESULT s_StopListening() noexcept;
static HRESULT s_StartListening(NewHandoffFunction pfnHandoff);
static HRESULT s_StopListening();
};
// Disable warnings from the CoCreatableClass macro as the value it provides for

View file

@ -513,6 +513,16 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
}
void ConptyConnection::ClearBuffer()
{
// If we haven't connected yet, then we really don't need to do
// anything. The connection should already start clear!
if (_isConnected())
{
THROW_IF_FAILED(ConptyClearPseudoConsole(_hPC.get()));
}
}
void ConptyConnection::Close() noexcept
try
{

View file

@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void WriteInput(hstring const& data);
void Resize(uint32_t rows, uint32_t columns);
void Close() noexcept;
void ClearBuffer();
winrt::guid Guid() const noexcept;

View file

@ -9,6 +9,7 @@ namespace Microsoft.Terminal.TerminalConnection
{
ConptyConnection();
Guid Guid { get; };
void ClearBuffer();
static event NewConnectionHandler NewConnection;
static void StartInboundListener();

Some files were not shown because too many files have changed in this diff Show more