diff --git a/.gitignore b/.gitignore index d41b961a4..2fcb1c1d9 100644 --- a/.gitignore +++ b/.gitignore @@ -263,6 +263,9 @@ build*.metadata # .razzlerc.cmd file - used by dev environment tools/.razzlerc.* +# .PowershellModules - if one needs a powershell module dependency, one +# can save it here. used by tools/OpenConsole.psm1 +.PowershellModules # message compiler output MSG*.bin /*.exe @@ -275,3 +278,4 @@ MSG*.bin **/Unmerged/* profiles.json *.metaproj +*.swp diff --git a/README.md b/README.md index fe0a084f5..63c6a1ffd 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Secondly, try pressing Ctrl + T. The tabs are hidden when We are excited to work alongside you, our amazing community, to build and enhance Windows Terminal\! -We ask that **before you start work on a feature that you would like to contribute, please file an issue describing your proposed change**: We will be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. +We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](https://github.com/microsoft/terminal/blob/master/doc/contributing.md). We will be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. > 👉 **Remember\!** Your contributions may be incorporated into future versions of Windows\! Because of this, all pull requests will be subject to the same level of scrutiny for quality, coding standards, performance, globalization, accessibility, and compatibility as those of our internal contributors. diff --git a/doc/cascadia/Panes.md b/doc/cascadia/Panes.md new file mode 100644 index 000000000..e165c208a --- /dev/null +++ b/doc/cascadia/Panes.md @@ -0,0 +1,232 @@ +--- +author: "Mike Griese @zadjii-msft" +created on: 2019-May-16 +--- + +# Panes in the Windows Terminal + +## Abstract + +Panes are an abstraction by which the terminal can display multiple terminal +instances simultaneously in a single terminal window. While tabs allow for a +single terminal window to have many terminal sessions running simultaneously +within a single window, only one tab can be visible at a time. Panes, on the +other hand, allow a user to have many different terminal sessions visible to the +user within the context of a single window at the same time. This can enable +greater productivity from the user, as they can see the output of one terminal +window while working in another. + +This spec will help outline the design of the implementation of panes in the +Windows Terminal. + +## Inspirations + +Panes within the context of a single terminal window are not a new idea. The +design of the panes for the Windows Terminal was heavily inspired by the +application `tmux`, which is a commandline application which acts as a "terminal +multiplexer", allowing for the easy managment of many terminal sessions from a +single application. + +Other applications that include pane-like functionality include (but are not +limited to): + +* screen +* terminator +* emacs & vim +* Iterm2 + +## Design + +The architecture of the Windows Terminal can be broken into two main pieces: +Tabs and Panes. The Windows Terminal supports _top-level_ tabs, with nested +panes inside the tabs. This means that there's a single strip of tabs along the +application, and each tab has a set of panes that are visible within the context +of that tab. + +Panes are implemented as a binary tree of panes. A Pane can either be a leaf +pane, with it's own terminal control that it displays, or it could be a parent +pane, where it has two children, each with their own terminal control. + +When a pane is a parent, its two children are either split vertically or +horizontally. Parent nodes don't have a terminal of their own, they merely +display the terminals of their children. + + * If a Pane is split vertically, the two panes are seperated by a vertical + split, as to appear side-by-side. Think `[|]` + * If a Pane is split horizontally, the two panes are split by a horizontal + separator, and appear above/below one another. Think `[-]`. + +As additional panes are created, panes will continue to subdivide the space of +their parent. It's up to the parent pane to control the sizing and display of +it's children. + +### Example + +We'll start by taking the terminal and creating a single vertical split. There +are now two panes in the terminal, side by side. The original terminal is `A`, +and the newly created one is `B`. The terminal now looks like this: + +``` + +---------------+ + | | | 1: parent [|] + | | | ├── 2: A + | | | └── 3: B + | A | B | + | | | + | | | + | | | + +---------------+ +``` + +Here, there are actually 3 nodes: 1 is the parent of both 2 and 3. 2 is the node +containing the `A` terminal, and 3 is the node with the `B` terminal. + + +We could now split `B` in two horizontally, creating a third terminal pane `C`. + +``` + +---------------+ + | | | 1: parent [|] + | | B | ├── 2: A + | | | └── 3: parent [-] + | A +-------+ ├── 4: B + | | | └── 5: C + | | C | + | | | + +---------------+ +``` + +Node 3 is now a parent node, and the terminal `B` has moved into a new node as a +sibling of the new terminal `C`. + +We could also split `A` in horizontally, creating a fourth terminal pane `D`. + +``` + +---------------+ + | | | 1: parent [|] + | A | B | ├── 2: parent [-] + | | | | ├── 4: A + +-------+-------+ | └── 5: D + | | | └── 3: parent [-] + | D | C | ├── 4: B + | | | └── 5: C + +---------------+ +``` + +While it may appear that there's a single horizonal separator and a single +vertical separator here, that's not actually the case. Due to the tree-like +structure of the pane splitting, the horizontal splits exist only between the +two panes they're splitting. So, the user could move each of the horizontal +splits independently, without affecting the other set of panes. As an example: + +``` + +---------------+ + | | | + | A | | + +-------+ B | + | | | + | D | | + | +-------+ + | | C | + +---------------+ +``` + +### Creating a pane + +In the basic use case, the user will decide to split the currently focused pane. +The currently focused pane is always a leaf, because as parent's can't be +focused (they don't have their own terminal). When a user decides to add a new +pane, the child will: + + 1. Convert into a parent + 2. Move its terminal into its first child + 3. Split its UI in half, and display each child in one half. + +It's up to the app hosting the panes to tell the pane what kind of terminal in +wants created in the new pane. By default, the new pane will be created with the +default settings profile. + +### While panes are open + +When a tab has multiple panes open, only one is the "active" pane. This is the +pane that was last focused in the tab. If the tab is the currently open tab, +then this is the pane with the currently focused terminal control. When the user +brings the tab into focus, the last focused pane is the pane that should become +focused again. + +The tab's state will be updated to reflect the state of it's focused pane. The +title text and icon of the tab will reflect that of the focused pane. Should the +focus switch from one pane to another, the tab's text and icon should update to +reflect the newly focused control. Any additional state that the tab would +display for a single pane should also be reflected in the tab for a tab with +multiple panes. + +While panes are open, the user should be able to move any split between panes. +In moving the split, the sizes of the terminal controls should be resized to +match. + +### Closing a pane + +A pane can either be closed by the user manually, or when the terminal it's +attached to raises its ConnectionClosed event. When this happens, we should +remove this pane from the tree. The parent of the closing pane will have to +remove the pane as one of it's children. If the sibling of the closing pane is a +leaf, then the parent should just take all of the state from the remaining pane. +This will cause the remaining pane's content to expand to take the entire +boundaries of the parent's pane. If the remaining child was a parent itself, +then the parent will take both the children of the remaining pane, and make them +the parent's children, as if the parent node was taken from the tree and +replaced by the remaining child. + +## Future considerations + +The Pane implementation isn't complete in it's current form. There are many +additional things that could be done to improve the user experience. This is by +no means a comprehensive list. + +* [ ] Panes should be resizable with the mouse. The user should be able to drag + the separator for a pair of panes, and have the content between them resize as + the separator moves. +* [ ] There's no keyboard shortcut for "ClosePane" +* [ ] The user should be able to configure what profile is used for splitting a + pane. Currently, the default profile is used, but it's possible a user might + want to create a new pane with the parent pane's profile. +* [ ] There should be some sort of UI to indicate that a particular pane is + focused, more than just the blinking cursor. `tmux` accomplishes this by + colorizing the separators adjacent to the active pane. Another idea is + displaying a small outline around the focused pane (like when tabbing through + controls on a webpage). +* [ ] The user should be able to navigate the focus of panes with the keyboard, + instead of requiring the mouse. +* [ ] The user should be able to zoom a pane, to make the pane take the entire + size of the terminal window temporarily. +* [ ] A pane doesn't necessarily need to host a terminal. It could potentially + host another UIElement. One could imagine enabling a user to quickly open up a + Browser pane to search for a particular string without needing to leave the + terminal. + +## Footnotes + +### Why not top-level panes, and nested tabs? + +If each pane were to have it's own set of tabs, then each pane would need to +reserve screen real estate for a row of tabs. As a user continued to split the +window, more and more of the screen would be dedicated to just displaying a row +of tabs, which isn't really the important part of the application, the terminal +is. + +Additionally, if there were top-level panes, once the root was split, it would +not be possible to move a single pane to be the full size of the window. The +user would need to somehow close the other panes, to be able to make the split +the size of the dull window. + +One con of this design is that if a control is hosted in a pane, the current +design makes it hard to move out of a pane into it's own tab, or into another +pane. This could be solved a number of ways. There could be keyboard shortcuts +for swapping the positions of tabs, or a shortcut for both "zooming" a tab +(temporarily making it the full size) or even popping a pane out to it's own +tab. Additionally, a right-click menu option could be added to do the +aformentioned actions. Discoverability of these two actions is not as high as +just dragging a tab from one pane to another; however, it's believed that panes +are more of a power-user scenario, and power users will not neccessarily be +turned off by the feature's discoverability. diff --git a/doc/contributing.md b/doc/contributing.md new file mode 100644 index 000000000..7d8b6c0b2 --- /dev/null +++ b/doc/contributing.md @@ -0,0 +1,155 @@ +# Terminal Contributor's Guide + +Below is our guidance for how to report issues, propose new features, and submit contributions via Pull Requests (PRs). + +## Open Development Workflow + +The Windows Terminal team is VERY active in this GitHub Repo. In fact, we live in it all day long and carry out all our development in the open! + +When the team finds issues we file them in the repo. When we propose new ideas or think-up new features, we file new feature requests. When we work on fixes or features, we create branches and work on those improvements. And when PRs are reviewed, we review in public - including all the good, the bad, and the ugly parts. + +The point of doing all this work in public is to ensure that we are holding ourselves to a high degree of transparency, and so that the community sees that we apply the same processes and hold ourselves to the same quality-bar as we do to community-submitted issues and PRs. We also want to make sure that we expose our team culture and "tribal knowledge" that is inherent in any closely-knit team, which often contains considerable value to those new to the project who are trying to figure out "why the heck does this thing look/work like this???" + +### Repo Bot + +The team triages new issues several times a week. During triage, the team uses labels to categorize, manage, and drive the project workflow. + +We employ [a bot engine](https://github.com/microsoft/terminal/blob/master/doc/bot.md) to help us automate common processes within our workflow. + +We drive the bot by tagging issues with specific labels which cause the bot engine to close issues, merge branches, etc. This bot engine helps us keep the repo clean by automating the process of notifying appropriate parties if/when information/follow-up is needed, and closing stale issues/PRs after reminders have remained unanswered for several days. + +Therefore, if you do file issues, or create PRs, please keep an eye on your GitHub notifications. If you do not respond to requests for information, your issues/PRs may be closed automatically. + +--- + +## Before you start, file an issue + +Please follow this simple rule to help us eliminate any unnecessary wasted effort & frustration, and ensure an efficient and effective use of everyone's time - yours, ours, and other community members': + +> 👉 If you have a question, think you've discovered an issue, would like to propose a new feature, etc., then find/file an issue **BEFORE** starting work to fix/implement it. + +### Search existing issues first + +Before filing a new issue, search existing open and closed issues first: This project is moving fast! It is likely someone else has found the problem you're seeing, and someone may be working on or have already contributed a fix! + +If no existing item describes your issue/feature, great - please file a new issue: + +### File a new Issue + +* Don't know whether you're reporting an issue or requesting a feature? File an issue +* Have a question that you don't see answered in docs, videos, etc.? File an issue +* Want to know if we're planning on building a particular feature? File an issue +* Got a great idea for a new feature? File an issue/request/idea +* Don't understand how to do something? File an issue/Community Guidance Request +* Found an existing issue that describes yours? Great - upvote and add additional commentary / info / repro-steps / etc. + +When you hit "New Issue", select the type of issue closest to what you want to report/ask/request: +![New issue types](/doc/images/new-issue-template.png) + +### Complete the template + +**Complete the information requested in the issue template, providing as much information as possible**. The more information you provide, the more likely your issue/ask will be understood and implemented. Helpful information includes: + +* What device you're running (inc. CPU type, memory, disk, etc.) +* What build of Windows your device is running + + 👉 Tip: Run the following in PowerShell Core + + ```powershell + C:\> $PSVersionTable.OS + Microsoft Windows 10.0.18909 + ``` + + ... or in Windows PowerShell + + ```powershell + C:\> $PSVersionTable.BuildVersion + + Major Minor Build Revision + ----- ----- ----- -------- + 10 0 18912 1001 + ``` + + ... or Cmd: + + ```cmd + C:\> ver + + Microsoft Windows [Version 10.0.18900.1001] + ``` + +* What tools and apps you're using (e.g. VS 2019, VSCode, etc.) +* Don't assume we're experts in setting up YOUR environment and don't assume we are experts in ``. Teach us to help you! +* **We LOVE detailed repro steps!** What steps do we need to take to reproduce the issue? Assume we love to read repro steps. As much detail as you can stand is probably _barely_ enough detail for us! +* If you're reporting a particular character/glyph not rendering correctly, the specific Unicode codepoint would be MOST welcome (e.g. U+1F4AF, U+4382) +* Prefer error message text where possible or screenshots of errors if text cannot be captured +* We MUCH prefer text command-line script than screenshots of command-line script. +* **If you intend to implement the fix/feature yourself then say so!** If you do not indicate otherwise we will assume that the issue is our to solve, or may label the issue as `Help-Wanted`. + +### DO NOT post "+1" comments + +> ⚠ DO NOT post "+1", "me too", or similar comments - they just add noise to an issue. + +If you don't have any additional info/context to add but would like to indicate that you're affected by the issue, upvote the original issue by clicking its [+😊] button and hitting 👍 (+1) icon. This way we can actually measure how impactful an issue is. + +--- + +## Contributing fixes / features + +For those able & willing to help fix issues and/or implement features ... + +### To Spec or not to Spec + +Some issues/features may be quick and simple to describe and understand. For such scenarios, once a team member has agreed with your approach, skip ahead to the section headed "Fork, Branch, and Create your PR", below. + +Small issues that do not require a spec will be labelled Issue-Bug or Issue-Task. + +However, some issues/features will require careful thought & formal design before implementation. For these scenarios, we'll request that a spec is written and the associated issue will be labeled Issue-Feature. + +Specs help collaborators discuss different approaches to solve a problem, describe how the feature will behave, how the feature will impact the user, what happens if something goes wrong, etc. Driving towards agreement in a spec, before any code is written, often results in simpler code, and less wasted effort in the long run. + +Specs will be managed in a very similar manner as code contributions so please follow the "Fork, Branch and Create your PR" below. + +### Writing / Contributing-to a Spec + +To write/contribute to a spec: fork, branch and commit via PRs, as you would with any code changes. + +Specs are written in markdown, stored under the `\doc\spec` folder and named `[issue id] - [spec description].md`. + +👉 **It is important to follow the spec templates and complete the requested information**. The available spec templates will help ensure that specs contain the minimum information & decisions necessary to permit development to begin. In particular, specs require you to confirm that you've already discussed the issue/idea with the team in an issue and that you provide the issue ID for reference. + +Team members will be happy to help review specs and guide them to completion. + +### Help Wanted + +Once the team have approved an issue/spec, development can proceed. If no developers are immediately available, the spec can be parked ready for a developer to get started. Parked specs' issues will be labeled "Help Wanted". To find a list of development opportunities waiting for developer involvement, visit the Issues and filter on [the Help-Wanted label](https://github.com/microsoft/terminal/labels/Help-Wanted). + +--- + +## Development + +### Fork, Clone, Branch and Create your PR + +Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed an approach or a spec has been written and approved, it's time to start development: + +1. Fork the repo if you haven't already +1. Clone your fork locally +1. Create & push a feature branch +1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/) +1. Work on your changes + +### Code Review + +When you'd like the team to take a look, (even if the work is not yet fully-complete), mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge. + +> ⚠ Remember: **changes you make may affect both Windows Terminal and Windows Console and may end up being re-incorporated into Windows itself!** Because of this, we will treat community PR's with the same level of scrutiny and rigor as commits submitted to the official Windows source by team members and partners. + +### Merge + +Once your code has been reviewed and approved by the requisite number of team members, it will be merged into the master branch. Once merged, your PR will be automatically closed. + +--- + +## Thank you + +Thank you in advance for your contribution! Now, [what's next on the list](https://github.com/microsoft/terminal/labels/Help-Wanted)? 😜 diff --git a/doc/images/new-issue-template.png b/doc/images/new-issue-template.png new file mode 100644 index 000000000..04a9a42c5 Binary files /dev/null and b/doc/images/new-issue-template.png differ diff --git a/doc/reference/UTF8-torture-test.txt b/doc/reference/UTF8-torture-test.txt new file mode 100644 index 000000000..d475c0808 --- /dev/null +++ b/doc/reference/UTF8-torture-test.txt @@ -0,0 +1,212 @@ + +UTF-8 encoded sample plain-text file +‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + +Markus Kuhn [ˈmaʳkʊs kuːn] — 2002-07-25 + + +The ASCII compatible UTF-8 encoding used in this plain-text file +is defined in Unicode, ISO 10646-1, and RFC 2279. + + +Using Unicode/UTF-8, you can write in emails and source code things such as + +Mathematics and sciences: + + ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫ + ⎪⎢⎜│a²+b³ ⎟⎥⎪ + ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), ⎪⎢⎜│───── ⎟⎥⎪ + ⎪⎢⎜⎷ c₈ ⎟⎥⎪ + ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⎨⎢⎜ ⎟⎥⎬ + ⎪⎢⎜ ∞ ⎟⎥⎪ + ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫), ⎪⎢⎜ ⎲ ⎟⎥⎪ + ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪ + 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm ⎩⎣⎝i=1 ⎠⎦⎭ + +Linguistics and dictionaries: + + ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn + Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] + +APL: + + ((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ + +Nicer typography in plain text files: + + ╔══════════════════════════════════════════╗ + ║ ║ + ║ • ‘single’ and “double” quotes ║ + ║ ║ + ║ • Curly apostrophes: “We’ve been here” ║ + ║ ║ + ║ • Latin-1 apostrophe and accents: '´` ║ + ║ ║ + ║ • ‚deutsche‘ „Anführungszeichen“ ║ + ║ ║ + ║ • †, ‡, ‰, •, 3–4, —, −5/+5, ™, … ║ + ║ ║ + ║ • ASCII safety test: 1lI|, 0OD, 8B ║ + ║ ╭─────────╮ ║ + ║ • the euro symbol: │ 14.95 € │ ║ + ║ ╰─────────╯ ║ + ╚══════════════════════════════════════════╝ + +Combining characters: + + STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑ + +Greek (in Polytonic): + + The Greek anthem: + + Σὲ γνωρίζω ἀπὸ τὴν κόψη + τοῦ σπαθιοῦ τὴν τρομερή, + σὲ γνωρίζω ἀπὸ τὴν ὄψη + ποὺ μὲ βία μετράει τὴ γῆ. + + ᾿Απ᾿ τὰ κόκκαλα βγαλμένη + τῶν ῾Ελλήνων τὰ ἱερά + καὶ σὰν πρῶτα ἀνδρειωμένη + χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! + + From a speech of Demosthenes in the 4th century BC: + + Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, + ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς + λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ + τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿ + εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ + πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν + οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι, + οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν + ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον + τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι + γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν + προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους + σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ + τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ + τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς + τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον. + + Δημοσθένους, Γ´ ᾿Ολυνθιακὸς + +Georgian: + + From a Unicode conference invitation: + + გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო + კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს, + ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს + ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი, + ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება + ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში, + ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში. + +Russian: + + From a Unicode conference invitation: + + Зарегистрируйтесь сейчас на Десятую Международную Конференцию по + Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии. + Конференция соберет широкий круг экспертов по вопросам глобального + Интернета и Unicode, локализации и интернационализации, воплощению и + применению Unicode в различных операционных системах и программных + приложениях, шрифтах, верстке и многоязычных компьютерных системах. + +Thai (UCS Level 2): + + Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese + classic 'San Gua'): + + [----------------------------|------------------------] + ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่ + สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา + ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา + โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ + เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ + ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ + พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้ + ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ + + (The above is a two-column text. If combining characters are handled + correctly, the lines of the second column should be aligned with the + | character above.) + +Ethiopian: + + Proverbs in the Amharic language: + + ሰማይ አይታረስ ንጉሥ አይከሰስ። + ብላ ካለኝ እንደአባቴ በቆመጠኝ። + ጌጥ ያለቤቱ ቁምጥና ነው። + ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው። + የአፍ ወለምታ በቅቤ አይታሽም። + አይጥ በበላ ዳዋ ተመታ። + ሲተረጉሙ ይደረግሙ። + ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል። + ድር ቢያብር አንበሳ ያስር። + ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም። + እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም። + የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ። + ሥራ ከመፍታት ልጄን ላፋታት። + ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል። + የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ። + ተንጋሎ ቢተፉ ተመልሶ ባፉ። + ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው። + እግርህን በፍራሽህ ልክ ዘርጋ። + +Runes: + + ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ + + (Old English, which transcribed into Latin reads 'He cwaeth that he + bude thaem lande northweardum with tha Westsae.' and means 'He said + that he lived in the northern land near the Western Sea.') + +Braille: + + ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ + + ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞ + ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎ + ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂ + ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙ + ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ + ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲ + + ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ + + ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹ + ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞ + ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕ + ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹ + ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎ + ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎ + ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳ + ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞ + ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ + + (The first couple of paragraphs of "A Christmas Carol" by Dickens) + +Compact font selection example text: + + ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789 + abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ + –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд + ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა + +Greetings in various languages: + + Hello world, Καλημέρα κόσμε, コンニチハ + +Box drawing alignment tests: █ + ▉ + ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ + ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ + ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ + ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ + ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ + ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ + ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ ▗▄▖▛▀▜ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ + ▝▀▘▙▄▟ diff --git a/doc/reference/solution-dependencygraph.png b/doc/reference/solution-dependencygraph.png new file mode 100644 index 000000000..62970e970 Binary files /dev/null and b/doc/reference/solution-dependencygraph.png differ diff --git a/doc/specs/spec-template.md b/doc/specs/spec-template.md new file mode 100644 index 000000000..36444c5b9 --- /dev/null +++ b/doc/specs/spec-template.md @@ -0,0 +1,58 @@ +--- +author: / +created on: +last updated: +issue id: +--- + +# Spec Title + +## Abstract + +[comment]: # Outline what this spec describes + +## Inspiration + +[comment]: # What were the drivers/inspiration behind the creation of this spec. + +## Solution Design + +[comment]: # Outline the design of the solution. Feel free to include ASCII-art diagrams, etc. + +## UI/UX Design + +[comment]: # What will this fix/feature look like? How will it affect the end user? + +## Capabilities + +[comment]: # Discuss how the proposed fixes/features impact the following key considerations: + +### Accessibility + +[comment]: # How will the proposed change impact accessibility for users of screen readers, assistive input devices, etc. + +### Security + +[comment]: # How will the proposed change impact security? + +### Reliability + +[comment]: # Will the proposed change improve reliabilty? If not, why make the change? + +### Compatibility + +[comment]: # Will the proposed change break existing code/behaviors? If so, how, and is the breaking change "worth it"? + +### Performance, Power, and Efficiency + +## Potential Issues + +[comment]: # What are some of the things that might cause problems with the fixes/features proposed? Consider how the user might be negatively impacted. + +## Future considerations + +[comment]: # What are some of the things that the fixes/features might unlock in the future? Does the implementation of this spec enable scenarios? + +## Resources + +[comment]: # Be sure to add links to references, resources, footnotes, etc. diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index a968d8bab..556565f5f 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -424,6 +424,8 @@ namespace winrt::TerminalApp::implementation bindings.ScrollDown([this]() { _Scroll(1); }); bindings.NextTab([this]() { _SelectNextTab(true); }); bindings.PrevTab([this]() { _SelectNextTab(false); }); + bindings.SplitVertical([this]() { _SplitVertical(std::nullopt); }); + bindings.SplitHorizontal([this]() { _SplitHorizontal(std::nullopt); }); bindings.ScrollUpPage([this]() { _ScrollPage(-1); }); bindings.ScrollDownPage([this]() { _ScrollPage(1); }); bindings.SwitchToTab([this](const auto index) { _SelectTab({ index }); }); @@ -579,23 +581,16 @@ namespace winrt::TerminalApp::implementation for (auto &tab : _tabs) { - const auto term = tab->GetTerminalControl(); - const GUID tabProfile = tab->GetProfile(); - - if (profileGuid == tabProfile) - { - term.UpdateSettings(settings); - - // Update the icons of the tabs with this profile open. - auto tabViewItem = tab->GetTabViewItem(); - tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [profile, tabViewItem]() { - // _GetIconFromProfile has to run on the main thread - tabViewItem.Icon(App::_GetIconFromProfile(profile)); - }); - } + // Attempt to reload the settings of any panes with this profile + tab->UpdateSettings(settings, profileGuid); } } + // Update the icon of the tab for the currently focused profile in that tab. + for (auto& tab : _tabs) + { + _UpdateTabIcon(tab); + } _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() { // Refresh the UI theme @@ -608,6 +603,50 @@ namespace winrt::TerminalApp::implementation } + // Method Description: + // - Get the icon of the currently focused terminal control, and set its + // tab's icon to that icon. + // Arguments: + // - tab: the Tab to update the title for. + void App::_UpdateTabIcon(std::shared_ptr tab) + { + const auto lastFocusedProfileOpt = tab->GetFocusedProfile(); + if (lastFocusedProfileOpt.has_value()) + { + const auto lastFocusedProfile = lastFocusedProfileOpt.value(); + + auto tabViewItem = tab->GetTabViewItem(); + tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this, lastFocusedProfile, tabViewItem]() { + // _GetIconFromProfile has to run on the main thread + const auto* const matchingProfile = _settings->FindProfile(lastFocusedProfile); + if (matchingProfile) + { + tabViewItem.Icon(App::_GetIconFromProfile(*matchingProfile)); + } + }); + } + } + + // Method Description: + // - Get the title of the currently focused terminal control, and set it's + // tab's text to that text. If this tab is the focused tab, then also + // bubble this title to any listeners of our TitleChanged event. + // Arguments: + // - tab: the Tab to update the title for. + void App::_UpdateTitle(std::shared_ptr tab) + { + auto newTabTitle = tab->GetFocusedTitle(); + + // TODO #608: If the settings don't want the terminal's text in the + // tab, then display something else. + tab->SetTabText(newTabTitle); + if (_settings->GlobalSettings().GetShowTitleInTitlebar() && + tab->IsFocused()) + { + _titleChangeHandlers(newTabTitle); + } + } + // Method Description: // - Update the current theme of the application. This will manually update // all of the elements in our UI to match the given theme. @@ -727,6 +766,60 @@ namespace winrt::TerminalApp::implementation eventArgs.HandleClipboardData(text); } + // Method Description: + // - Connects event handlers to the TermControl for events that we want to + // 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 + // * the GotFocus event, for changing the title/icon in the tab when a new + // control is focused + // Arguments: + // - term: The newly created TermControl to connect the events for + // - hostingTab: The Tab that's hosting this TermControl instance + void App::_RegisterTerminalEvents(TermControl term, std::shared_ptr hostingTab) + { + // Add an event handler when the terminal's selection wants to be copied. + // When the text buffer data is retrieved, we'll copy the data into the Clipboard + term.CopyToClipboard({ this, &App::_CopyToClipboardHandler }); + + // Add an event handler when the terminal wants to paste data from the Clipboard. + term.PasteFromClipboard({ this, &App::_PasteFromClipboardHandler }); + + // 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. + std::weak_ptr weakTabPtr = hostingTab; + term.TitleChanged([this, weakTabPtr](auto newTitle){ + auto tab = weakTabPtr.lock(); + if (!tab) + { + return; + } + // The title of the control changed, but not necessarily the title + // of the tab. Get the title of the focused pane of the tab, and set + // the tab's text to the focused panes' text. + _UpdateTitle(tab); + }); + + term.GetControl().GotFocus([this, weakTabPtr](auto&&, auto&&) + { + auto tab = weakTabPtr.lock(); + if (!tab) + { + return; + } + // Update the focus of the tab's panes + tab->UpdateFocus(); + + // Possibly update the title of the tab, window to match the newly + // focused pane. + _UpdateTitle(tab); + + // Possibly update the icon of the tab. + _UpdateTabIcon(tab); + }); + } + // Method Description: // - Creates a new tab with the given settings. If the tab bar is not being // currently displayed, it will be shown. @@ -737,41 +830,11 @@ namespace winrt::TerminalApp::implementation // Initialize the new tab TermControl term{ settings }; - // Add an event handler when the terminal's selection wants to be copied. - // When the text buffer data is retrieved, we'll copy the data into the Clipboard - term.CopyToClipboard([=](auto copiedData) { - _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [copiedData]() { - DataPackage dataPack = DataPackage(); - dataPack.RequestedOperation(DataPackageOperation::Copy); - dataPack.SetText(copiedData); - Clipboard::SetContent(dataPack); - - // TODO: MSFT 20642290 and 20642291 - // rtf copy and html copy - }); - }); - - // Add an event handler when the terminal wants to paste data from the Clipboard. - term.PasteFromClipboard([=](auto /*sender*/, auto eventArgs) { - _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [eventArgs]() { - PasteFromClipboard(eventArgs); - }); - }); - // Add the new tab to the list of our tabs. auto newTab = _tabs.emplace_back(std::make_shared(profileGuid, term)); - // Add an event handler when the terminal's title changes. When the - // title changes, we'll bubble it up to listeners of our own title - // changed event, so they can handle it. - newTab->GetTerminalControl().TitleChanged([=](auto newTitle){ - // Only bubble the change if this tab is the focused tab. - if (_settings->GlobalSettings().GetShowTitleInTitlebar() && - newTab->IsFocused()) - { - _titleChangeHandlers(newTitle); - } - }); + // Hookup our event handlers to the new terminal + _RegisterTerminalEvents(term, newTab); auto tabViewItem = newTab->GetTabViewItem(); _tabView.Items().Append(tabViewItem); @@ -784,24 +847,15 @@ namespace winrt::TerminalApp::implementation tabViewItem.Icon(_GetIconFromProfile(*profile)); } - // Add an event handler when the terminal's connection is closed. - newTab->GetTerminalControl().ConnectionClosed([=]() { - _tabView.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [newTab, tabViewItem, this]() { - const GUID tabProfile = newTab->GetProfile(); - // Don't just capture this pointer, because the profile might - // get destroyed before this is called (case in point - - // reloading settings) - const auto* const p = _settings->FindProfile(tabProfile); + tabViewItem.PointerPressed({ this, &App::_OnTabClick }); - if (p != nullptr && p->GetCloseOnExit()) - { - _RemoveTabViewItem(tabViewItem); - } + // When the tab is closed, remove it from our list of tabs. + newTab->Closed([tabViewItem, this](){ + _tabView.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [tabViewItem, this]() { + _RemoveTabViewItem(tabViewItem); }); }); - tabViewItem.PointerPressed({ this, &App::_OnTabClick }); - // This is one way to set the tab's selected background color. // tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), a Brush?); @@ -865,7 +919,7 @@ namespace winrt::TerminalApp::implementation { delta = std::clamp(delta, -1, 1); const auto focusedTabIndex = _GetFocusedTabIndex(); - const auto control = _tabs[focusedTabIndex]->GetTerminalControl(); + const auto control = _GetFocusedControl(); const auto termHeight = control.GetViewHeight(); _tabs[focusedTabIndex]->Scroll(termHeight * delta); } @@ -877,10 +931,7 @@ namespace winrt::TerminalApp::implementation // and get text to appear on separate lines. void App::_CopyText(const bool trimTrailingWhitespace) { - const int focusedTabIndex = _GetFocusedTabIndex(); - std::shared_ptr focusedTab{ _tabs[focusedTabIndex] }; - - const auto control = focusedTab->GetTerminalControl(); + const auto control = _GetFocusedControl(); control.CopySelectionToClipboard(trimTrailingWhitespace); } @@ -930,10 +981,9 @@ namespace winrt::TerminalApp::implementation try { auto tab = _tabs.at(selectedIndex); - auto control = tab->GetTerminalControl().GetControl(); _tabContent.Children().Clear(); - _tabContent.Children().Append(control); + _tabContent.Children().Append(tab->GetRootElement()); tab->SetFocused(true); _titleChangeHandlers(GetTitle()); @@ -986,8 +1036,7 @@ namespace winrt::TerminalApp::implementation { try { - auto tab = _tabs.at(selectedIndex); - return tab->GetTerminalControl().Title(); + return _GetFocusedControl().Title(); } CATCH_LOG(); } @@ -1076,6 +1125,98 @@ namespace winrt::TerminalApp::implementation } } + winrt::Microsoft::Terminal::TerminalControl::TermControl App::_GetFocusedControl() + { + int focusedTabIndex = _GetFocusedTabIndex(); + auto focusedTab = _tabs[focusedTabIndex]; + return focusedTab->GetFocusedTerminalControl(); + } + + // Method Description: + // - Vertically split the focused pane, and place the given TermControl into + // the newly created pane. + // Arguments: + // - profile: The profile GUID to associate with the newly created pane. If + // this is nullopt, use the default profile. + void App::_SplitVertical(const std::optional& profileGuid) + { + _SplitPane(Pane::SplitState::Vertical, profileGuid); + } + + // Method Description: + // - Horizontally split the focused pane and place the given TermControl + // into the newly created pane. + // Arguments: + // - profile: The profile GUID to associate with the newly created pane. If + // this is nullopt, use the default profile. + void App::_SplitHorizontal(const std::optional& profileGuid) + { + _SplitPane(Pane::SplitState::Horizontal, profileGuid); + } + + // Method Description: + // - Split the focused pane either horizontally or vertically, and place the + // given TermControl into the newly created pane. + // - If splitType == SplitState::None, this method does nothing. + // Arguments: + // - splitType: one value from the Pane::SplitState enum, indicating how the + // new pane should be split from its parent. + // - profile: The profile GUID to associate with the newly created pane. If + // this is nullopt, use the default profile. + void App::_SplitPane(const Pane::SplitState splitType, const std::optional& profileGuid) + { + // Do nothing if we're requesting no split. + if (splitType == Pane::SplitState::None) + { + return; + } + + const auto realGuid = profileGuid ? profileGuid.value() : + _settings->GlobalSettings().GetDefaultProfile(); + const auto controlSettings = _settings->MakeSettings(realGuid); + TermControl newControl{ controlSettings }; + + const int focusedTabIndex = _GetFocusedTabIndex(); + auto focusedTab = _tabs[focusedTabIndex]; + + // Hookup our event handlers to the new terminal + _RegisterTerminalEvents(newControl, focusedTab); + + return splitType == Pane::SplitState::Horizontal ? focusedTab->AddHorizontalSplit(realGuid, newControl) : + focusedTab->AddVerticalSplit(realGuid, newControl); + } + + // Method Description: + // - Place `copiedData` into the clipboard as text. Triggered when a + // terminal control raises it's CopyToClipboard event. + // Arguments: + // - copiedData: the new string content to place on the clipboard. + void App::_CopyToClipboardHandler(const winrt::hstring& copiedData) + { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [copiedData]() { + DataPackage dataPack = DataPackage(); + dataPack.RequestedOperation(DataPackageOperation::Copy); + dataPack.SetText(copiedData); + Clipboard::SetContent(dataPack); + + // TODO: MSFT 20642290 and 20642291 + // rtf copy and html copy + }); + } + + // Method Description: + // - Fires an async event to get data from the clipboard, and paste it to + // the terminal. Triggered when the Terminal Control requests clipboard + // data with it's PasteFromClipboard event. + // Arguments: + // - eventArgs: the PasteFromClipboard event sent from the TermControl + void App::_PasteFromClipboardHandler(const IInspectable& /*sender*/, + const PasteFromClipboardEventArgs& eventArgs) + { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [eventArgs]() { + PasteFromClipboard(eventArgs); + }); + } // Method Description: // - Takes a MenuFlyoutItem and a corresponding KeyChord value and creates the accelerator for UI display. diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 1e33c7a7e..f8a5c66bd 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -89,6 +89,11 @@ namespace winrt::TerminalApp::implementation void _FeedbackButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); void _UpdateTabView(); + void _UpdateTabIcon(std::shared_ptr tab); + void _UpdateTitle(std::shared_ptr tab); + + + void _RegisterTerminalEvents(Microsoft::Terminal::TerminalControl::TermControl term, std::shared_ptr hostingTab); void _CreateNewTabFromSettings(GUID profileGuid, winrt::Microsoft::Terminal::Settings::TerminalSettings settings); @@ -102,6 +107,9 @@ namespace winrt::TerminalApp::implementation void _Scroll(int delta); void _CopyText(const bool trimTrailingWhitespace); + void _SplitVertical(const std::optional& profileGuid); + void _SplitHorizontal(const std::optional& profileGuid); + void _SplitPane(const Pane::SplitState splitType, const std::optional& profileGuid); // Todo: add more event implementations here // MSFT:20641986: Add keybindings for New Window void _ScrollPage(int delta); @@ -117,6 +125,12 @@ namespace winrt::TerminalApp::implementation void _ApplyTheme(const Windows::UI::Xaml::ElementTheme& newTheme); static Windows::UI::Xaml::Controls::IconElement _GetIconFromProfile(const ::TerminalApp::Profile& profile); + + winrt::Microsoft::Terminal::TerminalControl::TermControl _GetFocusedControl(); + + void _CopyToClipboardHandler(const winrt::hstring& copiedData); + void _PasteFromClipboardHandler(const IInspectable& sender, const Microsoft::Terminal::TerminalControl::PasteFromClipboardEventArgs& eventArgs); + static void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Settings::KeyChord& keyChord); }; } diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp index 2c74cfe3f..91c459d91 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -114,6 +114,13 @@ namespace winrt::TerminalApp::implementation _PrevTabHandlers(); return true; + case ShortcutAction::SplitVertical: + _SplitVerticalHandlers(); + return true; + case ShortcutAction::SplitHorizontal: + _SplitHorizontalHandlers(); + return true; + case ShortcutAction::SwitchToTab0: _SwitchToTabHandlers(0); return true; @@ -211,6 +218,8 @@ namespace winrt::TerminalApp::implementation DEFINE_EVENT(AppKeyBindings, SwitchToTab, _SwitchToTabHandlers, TerminalApp::SwitchToTabEventArgs); DEFINE_EVENT(AppKeyBindings, NextTab, _NextTabHandlers, TerminalApp::NextTabEventArgs); DEFINE_EVENT(AppKeyBindings, PrevTab, _PrevTabHandlers, TerminalApp::PrevTabEventArgs); + DEFINE_EVENT(AppKeyBindings, SplitVertical, _SplitVerticalHandlers, TerminalApp::SplitVerticalEventArgs); + DEFINE_EVENT(AppKeyBindings, SplitHorizontal, _SplitHorizontalHandlers, TerminalApp::SplitHorizontalEventArgs); DEFINE_EVENT(AppKeyBindings, IncreaseFontSize, _IncreaseFontSizeHandlers, TerminalApp::IncreaseFontSizeEventArgs); DEFINE_EVENT(AppKeyBindings, DecreaseFontSize, _DecreaseFontSizeHandlers, TerminalApp::DecreaseFontSizeEventArgs); DEFINE_EVENT(AppKeyBindings, ScrollUp, _ScrollUpHandlers, TerminalApp::ScrollUpEventArgs); diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h index 503796f21..6bbd657f7 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.h +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -49,6 +49,8 @@ namespace winrt::TerminalApp::implementation DECLARE_EVENT(SwitchToTab, _SwitchToTabHandlers, TerminalApp::SwitchToTabEventArgs); DECLARE_EVENT(NextTab, _NextTabHandlers, TerminalApp::NextTabEventArgs); DECLARE_EVENT(PrevTab, _PrevTabHandlers, TerminalApp::PrevTabEventArgs); + DECLARE_EVENT(SplitVertical, _SplitVerticalHandlers, TerminalApp::SplitVerticalEventArgs); + DECLARE_EVENT(SplitHorizontal, _SplitHorizontalHandlers, TerminalApp::SplitHorizontalEventArgs); DECLARE_EVENT(IncreaseFontSize, _IncreaseFontSizeHandlers, TerminalApp::IncreaseFontSizeEventArgs); DECLARE_EVENT(DecreaseFontSize, _DecreaseFontSizeHandlers, TerminalApp::DecreaseFontSizeEventArgs); DECLARE_EVENT(ScrollUp, _ScrollUpHandlers, TerminalApp::ScrollUpEventArgs); diff --git a/src/cascadia/TerminalApp/AppKeyBindings.idl b/src/cascadia/TerminalApp/AppKeyBindings.idl index 630407108..1e91de4ed 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.idl +++ b/src/cascadia/TerminalApp/AppKeyBindings.idl @@ -22,6 +22,8 @@ namespace TerminalApp CloseTab, NextTab, PrevTab, + SplitVertical, + SplitHorizontal, SwitchToTab0, SwitchToTab1, SwitchToTab2, @@ -49,6 +51,8 @@ namespace TerminalApp delegate void CloseTabEventArgs(); delegate void NextTabEventArgs(); delegate void PrevTabEventArgs(); + delegate void SplitVerticalEventArgs(); + delegate void SplitHorizontalEventArgs(); delegate void SwitchToTabEventArgs(Int32 profileIndex); delegate void IncreaseFontSizeEventArgs(); delegate void DecreaseFontSizeEventArgs(); @@ -76,6 +80,8 @@ namespace TerminalApp event SwitchToTabEventArgs SwitchToTab; event NextTabEventArgs NextTab; event PrevTabEventArgs PrevTab; + event SplitVerticalEventArgs SplitVertical; + event SplitHorizontalEventArgs SplitHorizontal; event IncreaseFontSizeEventArgs IncreaseFontSize; event DecreaseFontSizeEventArgs DecreaseFontSize; event ScrollUpEventArgs ScrollUp; diff --git a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp index e32d2adc1..9ef61436f 100644 --- a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp @@ -47,6 +47,8 @@ static constexpr std::string_view SwitchToTab6Key{ "switchToTab6" }; static constexpr std::string_view SwitchToTab7Key{ "switchToTab7" }; static constexpr std::string_view SwitchToTab8Key{ "switchToTab8" }; static constexpr std::string_view OpenSettingsKey{ "openSettings" }; +static constexpr std::string_view SplitHorizontalKey{ "splitHorizontal" }; +static constexpr std::string_view SplitVerticalKey{ "splitVertical" }; // Specifically use a map here over an unordered_map. We want to be able to // iterate over these entries in-order when we're serializing the keybindings. @@ -89,6 +91,8 @@ static const std::map> commandName { SwitchToTab6Key, ShortcutAction::SwitchToTab6 }, { SwitchToTab7Key, ShortcutAction::SwitchToTab7 }, { SwitchToTab8Key, ShortcutAction::SwitchToTab8 }, + { SplitHorizontalKey, ShortcutAction::SplitHorizontal }, + { SplitVerticalKey, ShortcutAction::SplitVertical }, }; // Function Description: diff --git a/src/cascadia/TerminalApp/CascadiaSettings.cpp b/src/cascadia/TerminalApp/CascadiaSettings.cpp index f0e31ea9c..9262df258 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettings.cpp @@ -4,6 +4,8 @@ #include "pch.h" #include #include +#include +#include #include "CascadiaSettings.h" #include "../../types/inc/utils.hpp" #include "../../inc/DefaultSettings.h" @@ -21,6 +23,7 @@ static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = static constexpr std::wstring_view PACKAGED_PROFILE_ICON_PATH{ L"ms-appx:///ProfileIcons/" }; static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" }; +static constexpr std::wstring_view DEFAULT_LINUX_ICON_GUID{ L"{9acb9455-ca41-5af7-950f-6bca1bc9722f}" }; CascadiaSettings::CascadiaSettings() : _globals{}, @@ -227,6 +230,11 @@ void CascadiaSettings::_CreateDefaultProfiles() _profiles.emplace_back(powershellProfile); _profiles.emplace_back(cmdProfile); + try + { + _AppendWslProfiles(_profiles); + } + CATCH_LOG() } // Method Description: @@ -467,6 +475,78 @@ bool CascadiaSettings::_isPowerShellCoreInstalledInPath(const std::wstring_view return false; } +// Function Description: +// - Adds all of the WSL profiles to the provided container. +// Arguments: +// - A ref to the profiles container where the WSL profiles are to be added +// Return Value: +// - +void CascadiaSettings::_AppendWslProfiles(std::vector& profileStorage) +{ + wil::unique_handle readPipe; + wil::unique_handle writePipe; + SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, true }; + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, &sa, 0)); + STARTUPINFO si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = writePipe.get(); + si.hStdError = writePipe.get(); + wil::unique_process_information pi{}; + wil::unique_cotaskmem_string systemPath; + THROW_IF_FAILED(wil::GetSystemDirectoryW(systemPath)); + std::wstring command(systemPath.get()); + command += L"\\wsl.exe --list"; + + THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, const_cast(command.c_str()), nullptr, nullptr, + TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)); + switch (WaitForSingleObject(pi.hProcess, INFINITE)) + { + case WAIT_OBJECT_0: + break; + case WAIT_ABANDONED: + case WAIT_TIMEOUT: + THROW_HR(ERROR_CHILD_NOT_COMPLETE); + case WAIT_FAILED: + THROW_LAST_ERROR(); + default: + THROW_HR(ERROR_UNHANDLED_EXCEPTION); + } + DWORD exitCode; + if (GetExitCodeProcess(pi.hProcess, &exitCode) == false) + { + THROW_HR(E_INVALIDARG); + } + else if (exitCode != 0) + { + return; + } + DWORD bytesAvailable; + THROW_IF_WIN32_BOOL_FALSE(PeekNamedPipe(readPipe.get(), nullptr, NULL, nullptr, &bytesAvailable, nullptr)); + std::wfstream pipe{ _wfdopen(_open_osfhandle((intptr_t)readPipe.get(), _O_WTEXT | _O_RDONLY), L"r") }; + //don't worry about the handle returned from wfdOpen, readPipe handle is already managed by wil and closing the file handle will cause an error. + std::wstring wline; + std::getline(pipe, wline); //remove the header from the output. + while (pipe.tellp() < bytesAvailable) + { + std::getline(pipe, wline); + std::wstringstream wlinestream(wline); + if (wlinestream) + { + std::wstring distName; + std::getline(wlinestream, distName, L' '); + auto WSLDistro{ _CreateDefaultProfile(distName) }; + WSLDistro.SetCommandline(L"wsl.exe -d " + distName); + WSLDistro.SetColorScheme({ L"Campbell" }); + std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH }; + iconPath.append(DEFAULT_LINUX_ICON_GUID); + iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION); + WSLDistro.SetIconPath(iconPath); + profileStorage.emplace_back(WSLDistro); + } + } +} + // Function Description: // - Get a environment variable string. // Arguments: diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index 16e263a70..74d069f1f 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -70,6 +70,7 @@ private: static std::optional _LoadAsUnpackagedApp(); static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline); static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline); + static void _AppendWslProfiles(std::vector& profileStorage); static std::wstring ExpandEnvironmentVariableString(std::wstring_view source); static Profile _CreateDefaultProfile(const std::wstring_view name); }; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp new file mode 100644 index 000000000..1c24f6f2a --- /dev/null +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -0,0 +1,586 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Pane.h" + +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::TerminalControl; + +static const int PaneSeparatorSize = 4; + +Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocused) : + _control{ control }, + _lastFocused{ lastFocused }, + _profile{ profile } +{ + _root.Children().Append(_control.GetControl()); + _connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler }); + + // Set the background of the pane to match that of the theme's default grid + // background. This way, we'll match the small underline under the tabs, and + // the UI will be consistent on bot light and dark modes. + const auto res = Application::Current().Resources(); + const auto key = winrt::box_value(L"BackgroundGridThemeStyle"); + if (res.HasKey(key)) + { + const auto g = res.Lookup(key); + const auto style = g.try_as(); + // try_as fails by returning nullptr + if (style) + { + _root.Style(style); + } + } +} + +// Method Description: +// - Called when our attached control is closed. Triggers listeners to our close +// event, if we're a leaf pane. +// - If this was called, and we became a parent pane (due to work on another +// thread), this function will do nothing (allowing the control's new parent +// to handle the event instead). +// Arguments: +// - +// Return Value: +// - +void Pane::_ControlClosedHandler() +{ + std::unique_lock lock{ _createCloseLock }; + // It's possible that this event handler started being executed, then before + // we got the lock, another thread created another child. So our control is + // actually no longer _our_ control, and instead could be a descendant. + // + // When the control's new Pane takes ownership of the control, the new + // parent will register it's own event handler. That event handler will get + // fired after this handler returns, and will properly cleanup state. + if (!_IsLeaf()) + { + return; + } + + if (_control.ShouldCloseOnExit()) + { + // Fire our Closed event to tell our parent that we should be removed. + _closedHandlers(); + } +} + +// Method Description: +// - Get the root UIElement of this pane. There may be a single TermControl as a +// child, or an entire tree of grids and panes as children of this element. +// Arguments: +// - +// Return Value: +// - the Grid acting as the root of this pane. +Controls::Grid Pane::GetRootElement() +{ + return _root; +} + +// Method Description: +// - If this is the last focused pane, returns itself. Returns nullptr if this +// is a leaf and it's not focused. If it's a parent, it returns nullptr if no +// children of this pane were the last pane to be focused, or the Pane that +// _was_ the last pane to be focused (if there was one). +// - This Pane's control might not currently be focused, if the tab itself is +// not currently focused. +// Return Value: +// - nullptr if we're a leaf and unfocused, or no children were marked +// `_lastFocused`, else returns this +std::shared_ptr Pane::GetFocusedPane() +{ + if (_IsLeaf()) + { + return _lastFocused ? shared_from_this() : nullptr; + } + else + { + auto firstFocused = _firstChild->GetFocusedPane(); + if (firstFocused != nullptr) + { + return firstFocused; + } + return _secondChild->GetFocusedPane(); + } +} + +// Method Description: +// - Returns nullptr if no children of this pane were the last control to be +// focused, or the TermControl that _was_ the last control to be focused (if +// there was one). +// - This control might not currently be focused, if the tab itself is not +// currently focused. +// Arguments: +// - +// Return Value: +// - nullptr if no children were marked `_lastFocused`, else the TermControl +// that was last focused. +TermControl Pane::GetFocusedTerminalControl() +{ + auto lastFocused = GetFocusedPane(); + return lastFocused ? lastFocused->_control : nullptr; +} + +// Method Description: +// - Returns nullopt if no children of this pane were the last control to be +// focused, or the GUID of the profile of the last control to be focused (if +// there was one). +// Arguments: +// - +// Return Value: +// - nullopt if no children of this pane were the last control to be +// focused, else the GUID of the profile of the last control to be focused +std::optional Pane::GetFocusedProfile() +{ + auto lastFocused = GetFocusedPane(); + return lastFocused ? lastFocused->_profile : std::nullopt; +} + +// Method Description: +// - Returns true if this pane was the last pane to be focused in a tree of panes. +// Arguments: +// - +// Return Value: +// - true iff we were the last pane focused in this tree of panes. +bool Pane::WasLastFocused() const noexcept +{ + return _lastFocused; +} + +// Method Description: +// - Returns true iff this pane has no child panes. +// Arguments: +// - +// Return Value: +// - true iff this pane has no child panes. +bool Pane::_IsLeaf() const noexcept +{ + return _splitState == SplitState::None; +} + +// Method Description: +// - Returns true if this pane is currently focused, or there is a pane which is +// a child of this pane that is actively focused +// Arguments: +// - +// Return Value: +// - true if the currently focused pane is either this pane, or one of this +// pane's descendants +bool Pane::_HasFocusedChild() const noexcept +{ + // We're intentionally making this one giant expression, so the compiler + // will skip the following lookups if one of the lookups before it returns + // true + return (_control && _control.GetControl().FocusState() != FocusState::Unfocused) || + (_firstChild && _firstChild->_HasFocusedChild()) || + (_secondChild && _secondChild->_HasFocusedChild()); +} + +// Method Description: +// - Update the focus state of this pane, and all its descendants. +// * If this is a leaf node, and our control is actively focused, we'll mark +// ourselves as the _lastFocused. +// * If we're not a leaf, we'll recurse on our children to check them. +// Arguments: +// - +// Return Value: +// - +void Pane::UpdateFocus() +{ + if (_IsLeaf()) + { + const auto controlFocused = _control && + _control.GetControl().FocusState() != FocusState::Unfocused; + + _lastFocused = controlFocused; + } + else + { + _lastFocused = false; + _firstChild->UpdateFocus(); + _secondChild->UpdateFocus(); + } +} + +// Method Description: +// - Focuses this control if we're a leaf, or attempts to focus the first leaf +// of our first child, recursively. +// Arguments: +// - +// Return Value: +// - +void Pane::_FocusFirstChild() +{ + if (_IsLeaf()) + { + _control.GetControl().Focus(FocusState::Programmatic); + } + else + { + _firstChild->_FocusFirstChild(); + } +} + +// Method Description: +// - Attempts to update the settings of this pane or any children of this pane. +// * If this pane is a leaf, and our profile guid matches the parameter, then +// we'll apply the new settings to our control. +// * If we're not a leaf, we'll recurse on our children. +// Arguments: +// - settings: The new TerminalSettings to apply to any matching controls +// - profile: The GUID of the profile these settings should apply to. +// Return Value: +// - +void Pane::UpdateSettings(const TerminalSettings& settings, const GUID& profile) +{ + if (!_IsLeaf()) + { + _firstChild->UpdateSettings(settings, profile); + _secondChild->UpdateSettings(settings, profile); + } + else + { + if (profile == _profile) + { + _control.UpdateSettings(settings); + } + } +} + +// Method Description: +// - Closes one of our children. In doing so, takes the control from the other +// child, and makes this pane a leaf node again. +// Arguments: +// - closeFirst: if true, the first child should be closed, and the second +// should be preserved, and vice-versa for false. +// Return Value: +// - +void Pane::_CloseChild(const bool closeFirst) +{ + // Lock the create/close lock so that another operation won't concurrently + // modify our tree + std::unique_lock lock{ _createCloseLock }; + + // If we're a leaf, then chances are both our children closed in close + // succession. We waited on the lock while the other child was closed, so + // now we don't have a child to close anymore. Return here. When we moved + // the non-closed child into us, we also set up event handlers that will be + // triggered when we return from this. + if (_IsLeaf()) + { + return; + } + + auto closedChild = closeFirst ? _firstChild : _secondChild; + auto remainingChild = closeFirst ? _secondChild : _firstChild; + + // If the only child left is a leaf, that means we're a leaf now. + if (remainingChild->_IsLeaf()) + { + // take the control and profile of the pane that _wasn't_ closed. + _control = remainingChild->_control; + _profile = remainingChild->_profile; + + // Add our new event handler before revoking the old one. + _connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler }); + + // Revoke the old event handlers. Remove both the handlers for the panes + // themselves closing, and remove their handlers for their controls + // closing. At this point, if the remaining child's control is closed, + // they'll trigger only our event handler for the control's close. + _firstChild->Closed(_firstClosedToken); + _secondChild->Closed(_secondClosedToken); + closedChild->_control.ConnectionClosed(closedChild->_connectionClosedToken); + remainingChild->_control.ConnectionClosed(remainingChild->_connectionClosedToken); + + // If either of our children was focused, we want to take that focus from + // them. + _lastFocused = _firstChild->_lastFocused || _secondChild->_lastFocused; + + // Remove all the ui elements of our children. This'll make sure we can + // re-attach the TermControl to our Grid. + _firstChild->_root.Children().Clear(); + _secondChild->_root.Children().Clear(); + + // Reset our UI: + _root.Children().Clear(); + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); + _separatorRoot = { nullptr }; + + // Reattach the TermControl to our grid. + _root.Children().Append(_control.GetControl()); + + if (_lastFocused) + { + _control.GetControl().Focus(FocusState::Programmatic); + } + + _splitState = SplitState::None; + + // Release our children. + _firstChild = nullptr; + _secondChild = nullptr; + } + else + { + // First stash away references to the old panes and their tokens + const auto oldFirstToken = _firstClosedToken; + const auto oldSecondToken = _secondClosedToken; + const auto oldFirst = _firstChild; + const auto oldSecond = _secondClosedToken; + + // Steal all the state from our child + _splitState = remainingChild->_splitState; + _separatorRoot = remainingChild->_separatorRoot; + _firstChild = remainingChild->_firstChild; + _secondChild = remainingChild->_secondChild; + + // Set up new close handlers on the children + _SetupChildCloseHandlers(); + + // Revoke the old event handlers. + _firstChild->Closed(_firstClosedToken); + _secondChild->Closed(_secondClosedToken); + + // Reset our UI: + _root.Children().Clear(); + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); + + // Copy the old UI over to our grid. + // Start by copying the row/column definitions. Iterate over the + // rows/cols, and remove each one from the old grid, and attach it to + // our grid instead. + while (remainingChild->_root.ColumnDefinitions().Size() > 0) + { + auto col = remainingChild->_root.ColumnDefinitions().GetAt(0); + remainingChild->_root.ColumnDefinitions().RemoveAt(0); + _root.ColumnDefinitions().Append(col); + } + while (remainingChild->_root.RowDefinitions().Size() > 0) + { + auto row = remainingChild->_root.RowDefinitions().GetAt(0); + remainingChild->_root.RowDefinitions().RemoveAt(0); + _root.RowDefinitions().Append(row); + } + + // Remove the child's UI elements from the child's grid, so we can + // attach them to us instead. + remainingChild->_root.Children().Clear(); + + _root.Children().Append(_firstChild->GetRootElement()); + _root.Children().Append(_separatorRoot); + _root.Children().Append(_secondChild->GetRootElement()); + + + // If the closed child was focused, transfer the focus to it's first sibling. + if (closedChild->_lastFocused) + { + _FocusFirstChild(); + } + + // Release the pointers that the child was holding. + remainingChild->_firstChild = nullptr; + remainingChild->_secondChild = nullptr; + remainingChild->_separatorRoot = { nullptr }; + } +} + +// Method Description: +// - Adds event handlers to our children to handle their close events. +// Arguments: +// - +// Return Value: +// - +void Pane::_SetupChildCloseHandlers() +{ + _firstClosedToken = _firstChild->Closed([this](){ + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=](){ + _CloseChild(true); + }); + }); + + _secondClosedToken = _secondChild->Closed([this](){ + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=](){ + _CloseChild(false); + }); + }); +} + +// Method Description: +// - Initializes our UI for a new split in this pane. Sets up row/column +// definitions, and initializes the separator grid. Does nothing if our split +// state is currently set to SplitState::None +// Arguments: +// - +// Return Value: +// - +void Pane::_CreateSplitContent() +{ + if (_splitState == SplitState::Vertical) + { + // Create three columns in this grid: one for each pane, and one for the separator. + auto separatorColDef = Controls::ColumnDefinition(); + separatorColDef.Width(GridLengthHelper::Auto()); + + _root.ColumnDefinitions().Append(Controls::ColumnDefinition{}); + _root.ColumnDefinitions().Append(separatorColDef); + _root.ColumnDefinitions().Append(Controls::ColumnDefinition{}); + + // Create the pane separator + _separatorRoot = Controls::Grid{}; + _separatorRoot.Width(PaneSeparatorSize); + // NaN is the special value XAML uses for "Auto" sizing. + _separatorRoot.Height(NAN); + } + else if (_splitState == SplitState::Horizontal) + { + // Create three rows in this grid: one for each pane, and one for the separator. + auto separatorRowDef = Controls::RowDefinition(); + separatorRowDef.Height(GridLengthHelper::Auto()); + + _root.RowDefinitions().Append(Controls::RowDefinition{}); + _root.RowDefinitions().Append(separatorRowDef); + _root.RowDefinitions().Append(Controls::RowDefinition{}); + + // Create the pane separator + _separatorRoot = Controls::Grid{}; + _separatorRoot.Height(PaneSeparatorSize); + // NaN is the special value XAML uses for "Auto" sizing. + _separatorRoot.Width(NAN); + } +} + +// Method Description: +// - Sets the row/column of our child UI elements, to match our current split type. +// Arguments: +// - +// Return Value: +// - +void Pane::_ApplySplitDefinitions() +{ + if (_splitState == SplitState::Vertical) + { + Controls::Grid::SetColumn(_firstChild->GetRootElement(), 0); + Controls::Grid::SetColumn(_separatorRoot, 1); + Controls::Grid::SetColumn(_secondChild->GetRootElement(), 2); + + } + else if (_splitState == SplitState::Horizontal) + { + Controls::Grid::SetRow(_firstChild->GetRootElement(), 0); + Controls::Grid::SetRow(_separatorRoot, 1); + Controls::Grid::SetRow(_secondChild->GetRootElement(), 2); + } +} + + +// Method Description: +// - Vertically split the focused pane in our tree of panes, and place the given +// TermControl into the newly created pane. If we're the focused pane, then +// we'll create two new children, and place them side-by-side in our Grid. +// Arguments: +// - profile: The profile GUID to associate with the newly created pane. +// - control: A TermControl to use in the new pane. +// Return Value: +// - +void Pane::SplitVertical(const GUID& profile, const TermControl& control) +{ + // If we're not the leaf, recurse into our children to split them. + if (!_IsLeaf()) + { + if (_firstChild->_HasFocusedChild()) + { + _firstChild->SplitVertical(profile, control); + } + else if (_secondChild->_HasFocusedChild()) + { + _secondChild->SplitVertical(profile, control); + } + + return; + } + + _DoSplit(SplitState::Vertical, profile, control); +} + +// Method Description: +// - Horizontally split the focused pane in our tree of panes, and place the given +// TermControl into the newly created pane. If we're the focused pane, then +// we'll create two new children, and place them side-by-side in our Grid. +// Arguments: +// - profile: The profile GUID to associate with the newly created pane. +// - control: A TermControl to use in the new pane. +// Return Value: +// - +void Pane::SplitHorizontal(const GUID& profile, const TermControl& control) +{ + if (!_IsLeaf()) + { + if (_firstChild->_HasFocusedChild()) + { + _firstChild->SplitHorizontal(profile, control); + } + else if (_secondChild->_HasFocusedChild()) + { + _secondChild->SplitHorizontal(profile, control); + } + + return; + } + + _DoSplit(SplitState::Horizontal, profile, control); +} + +// Method Description: +// - Does the bulk of the work of creating a new split. Initializes our UI, +// creates a new Pane to host the control, registers event handlers. +// Arguments: +// - splitType: what type of split we should create. +// - profile: The profile GUID to associate with the newly created pane. +// - control: A TermControl to use in the new pane. +// Return Value: +// - +void Pane::_DoSplit(SplitState splitType, const GUID& profile, const TermControl& control) +{ + // Lock the create/close lock so that another operation won't concurrently + // modify our tree + std::unique_lock lock{ _createCloseLock }; + + // revoke our handler - the child will take care of the control now. + _control.ConnectionClosed(_connectionClosedToken); + _connectionClosedToken.value = 0; + + _splitState = splitType; + + _CreateSplitContent(); + + // Remove any children we currently have. We can't add the existing + // TermControl to a new grid until we do this. + _root.Children().Clear(); + + // Create two new Panes + // Move our control, guid into the first one. + // Move the new guid, control into the second. + _firstChild = std::make_shared(_profile.value(), _control); + _profile = std::nullopt; + _control = { nullptr }; + _secondChild = std::make_shared(profile, control); + + _root.Children().Append(_firstChild->GetRootElement()); + _root.Children().Append(_separatorRoot); + _root.Children().Append(_secondChild->GetRootElement()); + + _ApplySplitDefinitions(); + + // Register event handlers on our children to handle their Close events + _SetupChildCloseHandlers(); + + _lastFocused = false; +} + +DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h new file mode 100644 index 000000000..737b9f0b8 --- /dev/null +++ b/src/cascadia/TerminalApp/Pane.h @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Module Name: +// - Pane.h +// +// Abstract: +// - Panes are an abstraction by which the terminal can dislay multiple terminal +// instances simultaneously in a single terminal window. While tabs allow for +// a single terminal window to have many terminal sessions running +// simultaneously within a single window, only one tab can be visible at a +// time. Panes, on the other hand, allow a user to have many different +// terminal sessions visible to the user within the context of a single window +// at the same time. This can enable greater productivity from the user, as +// they can see the output of one terminal window while working in another. +// - See doc/cascadia/Panes.md for a detailed description. +// +// Author: +// - Mike Griese (zadjii-msft) 16-May-2019 + + +#pragma once +#include +#include "../../cascadia/inc/cppwinrt_utils.h" + +class Pane : public std::enable_shared_from_this +{ + +public: + + enum class SplitState : int + { + None = 0, + Vertical = 1, + Horizontal = 2 + }; + + Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, const bool lastFocused = false); + + std::shared_ptr GetFocusedPane(); + winrt::Microsoft::Terminal::TerminalControl::TermControl GetFocusedTerminalControl(); + std::optional GetFocusedProfile(); + + winrt::Windows::UI::Xaml::Controls::Grid GetRootElement(); + + bool WasLastFocused() const noexcept; + void UpdateFocus(); + + void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, const GUID& profile); + + void SplitHorizontal(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + void SplitVertical(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + + DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs); + +private: + winrt::Windows::UI::Xaml::Controls::Grid _root{}; + winrt::Windows::UI::Xaml::Controls::Grid _separatorRoot{ nullptr }; + winrt::Microsoft::Terminal::TerminalControl::TermControl _control{ nullptr }; + + std::shared_ptr _firstChild{ nullptr }; + std::shared_ptr _secondChild{ nullptr }; + SplitState _splitState{ SplitState::None }; + + bool _lastFocused{ false }; + std::optional _profile{ std::nullopt }; + winrt::event_token _connectionClosedToken{ 0 }; + winrt::event_token _firstClosedToken{ 0 }; + winrt::event_token _secondClosedToken{ 0 }; + + std::shared_mutex _createCloseLock{}; + + bool _IsLeaf() const noexcept; + bool _HasFocusedChild() const noexcept; + void _SetupChildCloseHandlers(); + + void _DoSplit(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + void _CreateSplitContent(); + void _ApplySplitDefinitions(); + + void _CloseChild(const bool closeFirst); + + void _FocusFirstChild(); + void _ControlClosedHandler(); +}; diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index f97aaffc5..cd239337f 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -6,41 +6,47 @@ using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Core; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::TerminalControl; -Tab::Tab(GUID profile, winrt::Microsoft::Terminal::TerminalControl::TermControl control) : - _control{ control }, - _focused{ false }, - _profile{ profile }, - _tabViewItem{ nullptr } +static const int TabViewFontSize = 12; + +Tab::Tab(const GUID& profile, const TermControl& control) { + _rootPane = std::make_shared(profile, control, true); + + _rootPane->Closed([=]() { + _closedHandlers(); + }); + _MakeTabViewItem(); } -Tab::~Tab() -{ - // When we're destructed, winrt will automatically decrement the refcount - // of our terminalcontrol. - // Assuming that refcount hits 0, it'll destruct it on its own, including - // calling Close on the terminal and connection. -} - void Tab::_MakeTabViewItem() { _tabViewItem = ::winrt::Microsoft::UI::Xaml::Controls::TabViewItem{}; - const auto title = _control.Title(); - - _tabViewItem.Header(title); - - _control.TitleChanged([=](auto newTitle){ - _tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=](){ - _tabViewItem.Header(newTitle); - }); - }); + _tabViewItem.FontSize(TabViewFontSize); } -winrt::Microsoft::Terminal::TerminalControl::TermControl Tab::GetTerminalControl() +UIElement Tab::GetRootElement() { - return _control; + return _rootPane->GetRootElement(); +} + +// Method Description: +// - Returns nullptr if no children of this tab were the last control to be +// focused, or the TermControl that _was_ the last control to be focused (if +// there was one). +// - This control might not currently be focused, if the tab itself is not +// currently focused. +// Arguments: +// - +// Return Value: +// - nullptr if no children were marked `_lastFocused`, else the TermControl +// that was last focused. +TermControl Tab::GetFocusedTerminalControl() +{ + return _rootPane->GetFocusedTerminalControl(); } winrt::Microsoft::UI::Xaml::Controls::TabViewItem Tab::GetTabViewItem() @@ -48,13 +54,28 @@ winrt::Microsoft::UI::Xaml::Controls::TabViewItem Tab::GetTabViewItem() return _tabViewItem; } - -bool Tab::IsFocused() +// Method Description: +// - Returns true if this is the currently focused tab. For any set of tabs, +// there should only be one tab that is marked as focused, though each tab has +// no control over the other tabs in the set. +// Arguments: +// - +// Return Value: +// - true iff this tab is focused. +bool Tab::IsFocused() const noexcept { return _focused; } -void Tab::SetFocused(bool focused) +// Method Description: +// - Updates our focus state. If we're gaining focus, make sure to transfer +// focus to the last focused terminal control in our tree of controls. +// Arguments: +// - focused: our new focus state. If true, we should be focused. If false, we +// should be unfocused. +// Return Value: +// - +void Tab::SetFocused(const bool focused) { _focused = focused; @@ -64,15 +85,89 @@ void Tab::SetFocused(bool focused) } } -GUID Tab::GetProfile() const noexcept +// Method Description: +// - Returns nullopt if no children of this tab were the last control to be +// focused, or the GUID of the profile of the last control to be focused (if +// there was one). +// Arguments: +// - +// 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 Tab::GetFocusedProfile() const noexcept { - return _profile; + return _rootPane->GetFocusedProfile(); } +// Method Description: +// - Attempts to update the settings of this tab's tree of panes. +// Arguments: +// - settings: The new TerminalSettings to apply to any matching controls +// - profile: The GUID of the profile these settings should apply to. +// Return Value: +// - +void Tab::UpdateSettings(const TerminalSettings& settings, const GUID& profile) +{ + _rootPane->UpdateSettings(settings, profile); +} + +// Method Description: +// - Focus the last focused control in our tree of panes. +// Arguments: +// - +// Return Value: +// - void Tab::_Focus() { _focused = true; - _control.GetControl().Focus(FocusState::Programmatic); + + auto lastFocusedControl = _rootPane->GetFocusedTerminalControl(); + if (lastFocusedControl) + { + lastFocusedControl.GetControl().Focus(FocusState::Programmatic); + } +} + +// Method Description: +// - Update the focus state of this tab's tree of panes. If one of the controls +// under this tab is focused, then it will be marked as the last focused. If +// there are no focused panes, then there will not be a last focused control +// when this returns. +// Arguments: +// - +// Return Value: +// - +void Tab::UpdateFocus() +{ + _rootPane->UpdateFocus(); +} + +// Method Description: +// - Gets the title string of the last focused terminal control in our tree. +// Returns the empty string if there is no such control. +// Arguments: +// - +// Return Value: +// - the title string of the last focused terminal control in our tree. +winrt::hstring Tab::GetFocusedTitle() const +{ + const auto lastFocusedControl = _rootPane->GetFocusedTerminalControl(); + return lastFocusedControl ? lastFocusedControl.Title() : L""; +} + +// Method Description: +// - Set the text on the TabViewItem for this tab. +// Arguments: +// - text: The new text string to use as the Header for our TabViewItem +// Return Value: +// - +void Tab::SetTabText(const winrt::hstring& text) +{ + // Copy the hstring, so we don't capture a dead reference + winrt::hstring textCopy{ text }; + _tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [text = std::move(textCopy), this](){ + _tabViewItem.Header(text); + }); } // Method Description: @@ -83,10 +178,39 @@ void Tab::_Focus() // - delta: a number of lines to move the viewport relative to the current viewport. // Return Value: // - -void Tab::Scroll(int delta) +void Tab::Scroll(const int delta) { - _control.GetControl().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=](){ - const auto currentOffset = _control.GetScrollOffset(); - _control.KeyboardScrollViewport(currentOffset + delta); + auto control = GetFocusedTerminalControl(); + control.GetControl().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [control, delta](){ + const auto currentOffset = control.GetScrollOffset(); + control.KeyboardScrollViewport(currentOffset + delta); }); } + +// Method Description: +// - Vertically split the focused pane in our tree of panes, and place the +// given TermControl into the newly created pane. +// Arguments: +// - profile: The profile GUID to associate with the newly created pane. +// - control: A TermControl to use in the new pane. +// Return Value: +// - +void Tab::AddVerticalSplit(const GUID& profile, TermControl& control) +{ + _rootPane->SplitVertical(profile, control); +} + +// Method Description: +// - Horizontally split the focused pane in our tree of panes, and place the +// given TermControl into the newly created pane. +// Arguments: +// - profile: The profile GUID to associate with the newly created pane. +// - control: A TermControl to use in the new pane. +// Return Value: +// - +void Tab::AddHorizontalSplit(const GUID& profile, TermControl& control) +{ + _rootPane->SplitHorizontal(profile, control); +} + +DEFINE_EVENT(Tab, Closed, _closedHandlers, ConnectionClosedEventArgs); diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index da3f4e3b6..5cb383394 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -3,30 +3,40 @@ #pragma once #include -#include +#include "Pane.h" class Tab { public: - Tab(GUID profile, winrt::Microsoft::Terminal::TerminalControl::TermControl control); - ~Tab(); + Tab(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); winrt::Microsoft::UI::Xaml::Controls::TabViewItem GetTabViewItem(); - winrt::Microsoft::Terminal::TerminalControl::TermControl GetTerminalControl(); + winrt::Windows::UI::Xaml::UIElement GetRootElement(); + winrt::Microsoft::Terminal::TerminalControl::TermControl GetFocusedTerminalControl(); + std::optional GetFocusedProfile() const noexcept; - bool IsFocused(); - void SetFocused(bool focused); + bool IsFocused() const noexcept; + void SetFocused(const bool focused); - GUID GetProfile() const noexcept; + void Scroll(const int delta); + void AddVerticalSplit(const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + void AddHorizontalSplit(const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); - void Scroll(int delta); + void UpdateFocus(); + + void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, const GUID& profile); + winrt::hstring GetFocusedTitle() const; + void SetTabText(const winrt::hstring& text); + + DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs); private: - winrt::Microsoft::Terminal::TerminalControl::TermControl _control; - bool _focused; - GUID _profile; - winrt::Microsoft::UI::Xaml::Controls::TabViewItem _tabViewItem; + + std::shared_ptr _rootPane{ nullptr }; + + bool _focused{ false }; + winrt::Microsoft::UI::Xaml::Controls::TabViewItem _tabViewItem{ nullptr }; void _MakeTabViewItem(); void _Focus(); diff --git a/src/cascadia/TerminalApp/TerminalApp.vcxproj b/src/cascadia/TerminalApp/TerminalApp.vcxproj index d890f6add..a986dd41d 100644 --- a/src/cascadia/TerminalApp/TerminalApp.vcxproj +++ b/src/cascadia/TerminalApp/TerminalApp.vcxproj @@ -37,6 +37,7 @@ + @@ -56,6 +57,7 @@ + diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 9490763d5..efb29141a 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -266,7 +266,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation || imageSource.UriSource().RawUri() != imageUri.RawUri()) { // Note that BitmapImage handles the image load asynchronously, - // which is especially important since the image + // which is especially important since the image // may well be both large and somewhere out on the // internet. Media::Imaging::BitmapImage image(imageUri); @@ -359,12 +359,22 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Don't let anyone else do something to the buffer. auto lock = _terminal->LockForWriting(); - if (_connection != nullptr) + // Clear out the cursor timer, so it doesn't trigger again on us once we're destructed. + if (_cursorTimer) + { + _cursorTimer.value().Stop(); + _cursorTimer = std::nullopt; + } + + if (_connection) { _connection.Close(); } - _renderer->TriggerTeardown(); + if (_renderer) + { + _renderer->TriggerTeardown(); + } _swapChainPanel = nullptr; _root = nullptr; @@ -694,9 +704,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse) { - // Ignore mouse events while the terminal does not have focus. - // This prevents the user from selecting and copying text if they - // click inside the current tab to refocus the terminal window. + // Ignore mouse events while the terminal does not have focus. + // This prevents the user from selecting and copying text if they + // click inside the current tab to refocus the terminal window. if (!_focused) { args.Handled(true); @@ -970,6 +980,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void TermControl::_GotFocusHandler(Windows::Foundation::IInspectable const& /* sender */, RoutedEventArgs const& /* args */) { + if (_closing) + { + return; + } _focused = true; if (_cursorTimer.has_value()) @@ -984,6 +998,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void TermControl::_LostFocusHandler(Windows::Foundation::IInspectable const& /* sender */, RoutedEventArgs const& /* args */) { + if (_closing) + { + return; + } _focused = false; if (_cursorTimer.has_value()) @@ -1055,7 +1073,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void TermControl::_BlinkCursor(Windows::Foundation::IInspectable const& /* sender */, Windows::Foundation::IInspectable const& /* e */) { - if (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible()) + if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible())) { return; } @@ -1191,7 +1209,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // - Scrolls the viewport of the terminal and updates the scroll bar accordingly // Arguments: // - viewTop: the viewTop to scroll to - // The difference between this function and ScrollViewport is that this one also + // The difference between this function and ScrollViewport is that this one also // updates the _scrollBar after the viewport scroll. The reason _scrollBar is not updated in // ScrollViewport is because ScrollViewport is being called by _ScrollbarChangeHandler void TermControl::KeyboardScrollViewport(int viewTop) @@ -1346,7 +1364,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // don't necessarily include that state. // Return Value: // - a KeyModifiers value with flags set for each key that's pressed. - Settings::KeyModifiers TermControl::_GetPressedModifierKeys() const{ + Settings::KeyModifiers TermControl::_GetPressedModifierKeys() const + { CoreWindow window = CoreWindow::GetForCurrentThread(); // DONT USE // != CoreVirtualKeyStates::None @@ -1368,6 +1387,17 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation (shift ? Settings::KeyModifiers::Shift : Settings::KeyModifiers::None) }; } + // Method Description: + // - Returns true if this control should close when its connection is closed. + // Arguments: + // - + // Return Value: + // - true iff the control should close when the connection is closed. + bool TermControl::ShouldCloseOnExit() const noexcept + { + return _settings.CloseOnExit(); + } + // Method Description: // - Gets the corresponding viewport terminal position for the cursor // by excluding the padding and normalizing with the font size. @@ -1385,11 +1415,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation static_cast(cursorPosition.X - _root.Padding().Left), static_cast(cursorPosition.Y - _root.Padding().Top) }; - + const auto fontSize = _actualFont.GetSize(); FAIL_FAST_IF(fontSize.X == 0); FAIL_FAST_IF(fontSize.Y == 0); - + // Normalize to terminal coordinates by using font size terminalPosition.X /= fontSize.X; terminalPosition.Y /= fontSize.Y; diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 7a7de08d1..5abe60e20 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -41,6 +41,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation hstring Title(); void CopySelectionToClipboard(bool trimTrailingWhitespace); void Close(); + bool ShouldCloseOnExit() const noexcept; void ScrollViewport(int viewTop); void KeyboardScrollViewport(int viewTop); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index bc133d5de..c098fe339 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -33,6 +33,7 @@ namespace Microsoft.Terminal.TerminalControl String Title { get; }; void CopySelectionToClipboard(Boolean trimTrailingWhitespace); void Close(); + Boolean ShouldCloseOnExit { get; }; void ScrollViewport(Int32 viewTop); void KeyboardScrollViewport(Int32 viewTop); diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index b6c7abff7..10cb6f84d 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -14,15 +14,11 @@ using namespace winrt::Microsoft::Terminal::Settings; using namespace Microsoft::Terminal::Core; +using namespace Microsoft::Console; using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Types; using namespace Microsoft::Console::VirtualTerminal; -static constexpr short _ClampToShortMax(int value, short min) -{ - return static_cast(std::clamp(value, static_cast(min), SHRT_MAX)); -} - static std::wstring _KeyEventsToText(std::deque>& inEventsToWrite) { std::wstring wstr = L""; @@ -70,7 +66,8 @@ void Terminal::Create(COORD viewportSize, SHORT scrollbackLines, IRenderTarget& { _mutableViewport = Viewport::FromDimensions({ 0,0 }, viewportSize); _scrollbackLines = scrollbackLines; - const COORD bufferSize { viewportSize.X, _ClampToShortMax(viewportSize.Y + scrollbackLines, 1) }; + const COORD bufferSize { viewportSize.X, + Utils::ClampToShortMax(viewportSize.Y + scrollbackLines, 1) }; const TextAttribute attr{}; const UINT cursorSize = 12; _buffer = std::make_unique(bufferSize, attr, cursorSize, renderTarget); @@ -84,9 +81,10 @@ void Terminal::Create(COORD viewportSize, SHORT scrollbackLines, IRenderTarget& void Terminal::CreateFromSettings(winrt::Microsoft::Terminal::Settings::ICoreSettings settings, Microsoft::Console::Render::IRenderTarget& renderTarget) { - const COORD viewportSize{ _ClampToShortMax(settings.InitialCols(), 1), _ClampToShortMax(settings.InitialRows(), 1) }; + const COORD viewportSize{ Utils::ClampToShortMax(settings.InitialCols(), 1), + Utils::ClampToShortMax(settings.InitialRows(), 1) }; // TODO:MSFT:20642297 - Support infinite scrollback here, if HistorySize is -1 - Create(viewportSize, _ClampToShortMax(settings.HistorySize(), 0), renderTarget); + Create(viewportSize, Utils::ClampToShortMax(settings.HistorySize(), 0), renderTarget); UpdateSettings(settings); } @@ -499,11 +497,11 @@ void Terminal::_InitializeColorTable() { gsl::span tableView = { &_colorTable[0], gsl::narrow(_colorTable.size()) }; // First set up the basic 256 colors - ::Microsoft::Console::Utils::Initialize256ColorTable(tableView); + Utils::Initialize256ColorTable(tableView); // Then use fill the first 16 values with the Campbell scheme - ::Microsoft::Console::Utils::InitializeCampbellColorTable(tableView); + Utils::InitializeCampbellColorTable(tableView); // Then make sure all the values have an alpha of 255 - ::Microsoft::Console::Utils::SetColorTableAlpha(tableView, 0xff); + Utils::SetColorTableAlpha(tableView, 0xff); } // Method Description: diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 0ac7d9a70..12f0132e4 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -4,12 +4,14 @@ #include "pch.h" #include "AppHost.h" #include "../types/inc/Viewport.hpp" +#include "../types/inc/Utils.hpp" using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Composition; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Hosting; using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; // The tabs are 34.8px tall. This is their default height - we're not @@ -131,8 +133,10 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, const RECT proposedRect) auto initialSize = _app.GetLaunchDimensions(dpix); - const short _currentWidth = gsl::narrow(ceil(initialSize.X)); - const short _currentHeight = gsl::narrow(ceil(initialSize.Y)); + const short _currentWidth = Utils::ClampToShortMax( + static_cast(ceil(initialSize.X)), 1); + const short _currentHeight = Utils::ClampToShortMax( + static_cast(ceil(initialSize.Y)), 1); // Create a RECT from our requested client size auto nonClient = Viewport::FromDimensions({ _currentWidth, @@ -172,8 +176,8 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, const RECT proposedRect) const COORD origin{ gsl::narrow(proposedRect.left), gsl::narrow(proposedRect.top) }; - const COORD dimensions{ gsl::narrow(adjustedWidth), - gsl::narrow(adjustedHeight) }; + const COORD dimensions{ Utils::ClampToShortMax(adjustedWidth, 1), + Utils::ClampToShortMax(adjustedHeight, 1) }; const auto newPos = Viewport::FromDimensions(origin, dimensions); diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index 705a06237..b44c35167 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp index 432d287ce..79ed50a03 100644 --- a/src/terminal/parser/stateMachine.cpp +++ b/src/terminal/parser/stateMachine.cpp @@ -450,12 +450,16 @@ void StateMachine::_ActionParam(const wchar_t wch) // multiply existing values by 10 to make space in the 1s digit *_pusActiveParam *= 10; - // mark that we've now stored another digit. - _iParamAccumulatePos++; - // store the digit in the 1s place. *_pusActiveParam += usDigit; + // if the total is zero, it must be a leading zero digit, so we don't count it. + if (*_pusActiveParam != 0) + { + // otherwise mark that we've now stored another digit. + _iParamAccumulatePos++; + } + if (*_pusActiveParam > SHORT_MAX) { *_pusActiveParam = SHORT_MAX; @@ -530,12 +534,16 @@ void StateMachine::_ActionOscParam(const wchar_t wch) // multiply existing values by 10 to make space in the 1s digit _sOscParam *= 10; - // mark that we've now stored another digit. - _iParamAccumulatePos++; - // store the digit in the 1s place. _sOscParam += usDigit; + // if the total is zero, it must be a leading zero digit, so we don't count it. + if (_sOscParam != 0) + { + // otherwise mark that we've now stored another digit. + _iParamAccumulatePos++; + } + if (_sOscParam > SHORT_MAX) { _sOscParam = SHORT_MAX; diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp index 15e423fff..84fed5c35 100644 --- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp @@ -273,6 +273,30 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); } + TEST_METHOD(TestLeadingZeroCsiParam) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + for (int i = 0; i < 50; i++) // Any number of leading zeros should be supported + { + mach.ProcessCharacter(L'0'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + } + for (int i = 0; i < 5; i++) // We're only expecting to be able to keep 5 digits max + { + mach.ProcessCharacter((wchar_t)(L'1' + i)); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + } + VERIFY_ARE_EQUAL(*mach._pusActiveParam, 12345); + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + TEST_METHOD(TestCsiIgnore) { StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); @@ -429,6 +453,35 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final mach.ProcessCharacter(AsciiChars::BEL); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); } + + TEST_METHOD(TestLeadingZeroOscParam) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + for (int i = 0; i < 50; i++) // Any number of leading zeros should be supported + { + mach.ProcessCharacter(L'0'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + } + for (int i = 0; i < 5; i++) // We're only expecting to be able to keep 5 digits max + { + mach.ProcessCharacter((wchar_t)(L'1' + i)); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + } + VERIFY_ARE_EQUAL(mach._sOscParam, 12345); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(AsciiChars::BEL); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + TEST_METHOD(TestLongOscParam) { StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index eb0d6c752..66f8ee978 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -15,6 +15,8 @@ namespace Microsoft::Console::Utils { bool IsValidHandle(const HANDLE handle) noexcept; + short ClampToShortMax(const long value, const short min); + std::wstring GuidToString(const GUID guid); GUID GuidFromString(const std::wstring wstr); GUID CreateGuid(); diff --git a/src/types/ut_types/Types.Unit.Tests.vcxproj b/src/types/ut_types/Types.Unit.Tests.vcxproj index 614999d03..4feb7f168 100644 --- a/src/types/ut_types/Types.Unit.Tests.vcxproj +++ b/src/types/ut_types/Types.Unit.Tests.vcxproj @@ -2,6 +2,7 @@ + Create diff --git a/src/types/ut_types/UtilsTests.cpp b/src/types/ut_types/UtilsTests.cpp new file mode 100644 index 000000000..513dedf5a --- /dev/null +++ b/src/types/ut_types/UtilsTests.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "..\inc\utils.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace Microsoft::Console::Utils; + +class UtilsTests +{ + TEST_CLASS(UtilsTests); + + TEST_METHOD(TestClampToShortMax) + { + const short min = 1; + + // Test outside the lower end of the range + const short minExpected = min; + auto minActual = ClampToShortMax(0, min); + VERIFY_ARE_EQUAL(minExpected, minActual); + + // Test negative numbers + const short negativeExpected = min; + auto negativeActual = ClampToShortMax(-1, min); + VERIFY_ARE_EQUAL(negativeExpected, negativeActual); + + // Test outside the upper end of the range + const short maxExpected = SHRT_MAX; + auto maxActual = ClampToShortMax(50000, min); + VERIFY_ARE_EQUAL(maxExpected, maxActual); + + // Test within the range + const short withinRangeExpected = 100; + auto withinRangeActual = ClampToShortMax(withinRangeExpected, min); + VERIFY_ARE_EQUAL(withinRangeExpected, withinRangeActual); + } +}; diff --git a/src/types/ut_types/sources b/src/types/ut_types/sources index a09fa19cf..71fce07be 100644 --- a/src/types/ut_types/sources +++ b/src/types/ut_types/sources @@ -15,6 +15,7 @@ DLLDEF = SOURCES = \ $(SOURCES) \ UuidTests.cpp \ + UtilsTests.cpp \ DefaultResource.rc \ INCLUDES = \ diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 8016491df..51f4567ef 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -6,6 +6,20 @@ using namespace Microsoft::Console; +// Function Description: +// - Clamps a long in between `min` and `SHRT_MAX` +// Arguments: +// - value: the value to clamp +// - min: the minimum value to clamp to +// Return Value: +// - The clamped value as a short. +short Utils::ClampToShortMax(const long value, const short min) +{ + return static_cast(std::clamp(value, + static_cast(min), + static_cast(SHRT_MAX))); +} + // Function Description: // - Creates a String representation of a guid, in the format // "{12345678-ABCD-EF12-3456-7890ABCDEF12}" diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index b9b28afe6..8c7918302 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -2,21 +2,87 @@ # The project's root directory. Set-Item -force -path "env:OpenConsoleRoot" -value "$PSScriptRoot\.." +#.SYNOPSIS +# Finds and imports a module that should be local to the project +#.PARAMETER ModuleName +# The name of the module to import +function Import-LocalModule +{ + [CmdletBinding()] + param( + [parameter(Mandatory=$true, Position=0)] + [string]$Name + ) + + $ErrorActionPreference = 'Stop' + + $modules_root = "$env:OpenConsoleRoot\.PowershellModules" + + $local = $null -eq (Get-Module -Name $Name) + + if (-not $local) + { + return + } + + if (-not (Test-Path $modules_root)) { + New-Item $modules_root -ItemType 'directory' | Out-Null + } + + if (-not (Test-Path "$modules_root\$Name")) { + Write-Verbose "$Name not downloaded -- downloading now" + $module = Find-Module "$Name" + $version = $module.Version + + Write-Verbose "Saving $Name to $modules_root" + Save-Module -InputObject $module -Path $modules_root + Import-Module "$modules_root\$Name\$version\$Name.psd1" + } else { + Write-Verbose "$Name already downloaded" + $versions = Get-ChildItem "$modules_root\$Name" | Sort-Object + + Get-ChildItem -Path $versions[0] "$Name.psd1" | Import-Module + } +} + #.SYNOPSIS # Grabs all environment variable set after vcvarsall.bat is called and pulls # them into the Powershell environment. -function Set-MsbuildDevEnvironment() +function Set-MsbuildDevEnvironment { - $path = "$env:VS140COMNTOOLS\..\.." - pushd $path - cmd /c "vcvarsall.bat&set" | foreach { - if ($_ -match "=") + [CmdletBinding()] + param() + + $ErrorActionPreference = 'Stop' + + Import-LocalModule -Name 'VSSetup' + + Write-Verbose 'Searching for VC++ instances' + $vsinfo = ` + Get-VSSetupInstance -All ` + | Select-VSSetupInstance ` + -Latest -Product * ` + -Require 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' + + $vspath = $vsinfo.InstallationPath + + switch ($env:PROCESSOR_ARCHITECTURE) { + "amd64" { $arch = "x64" } + "x86" { $arch = "x86" } + default { throw "Unknown architecture: $switch" } + } + + $vcvarsall = "$vspath\VC\Auxiliary\Build\vcvarsall.bat" + + Write-Verbose 'Setting up environment variables' + cmd /c ("`"$vcvarsall`" $arch & set") | ForEach-Object { + if ($_ -match '=') { $s = $_.Split("="); Set-Item -force -path "env:\$($s[0])" -value "$($s[1])" } } - popd + Write-Host "Dev environment variables set" -ForegroundColor Green }