411 lines
18 KiB
Markdown
411 lines
18 KiB
Markdown
---
|
|
author: Mike Griese @zadjii-msft
|
|
created on: 2021-03-03
|
|
last updated: 2022-11-04
|
|
issue id: #2634
|
|
---
|
|
|
|
# Broadcast Input
|
|
|
|
## Abstract
|
|
|
|
"Broadcast Input" is a feature present on other terminals which allows the user
|
|
to send the same input to multiple tabs or panes at the same time. This can make
|
|
it simpler for the user to run the same command in multiple directories or
|
|
servers at the same time.
|
|
|
|
With a viable prototype in [#9222], it's important that we have a well-defined
|
|
plan for how we want this feature to be exposed before merging that PR. This
|
|
spec is intended to be a lighter-than-usual spec to build consensus on the
|
|
design of how the actions should be expressed.
|
|
|
|
## Background
|
|
|
|
### Inspiration
|
|
|
|
This spec is heavily inspired by the [iTerm2 implementation]. @carlos-zamora did
|
|
a great job of breaking down how iTerm2 works in [this comment].
|
|
|
|
SecureCRT also implements a similar feature using a "chat window" that can send
|
|
the input in the chat window to all tabs. This seemed like a less ergonomic
|
|
solution, so it was not seriously considered.
|
|
|
|
Additionally, Terminator (on \*nix) allows for a similar feature through the use
|
|
of "groups". From [@zljubisic]:
|
|
|
|
> In Linux terminator you can define groups, and than put each pane in one of
|
|
> defined groups. Afterwards, you can choose broadcasting to all panes, only
|
|
> certain group or no broadcast at all.
|
|
|
|
This also seemed like a less powerful version of broadcast input than the
|
|
iterm2-like version, so it was also not further investigated.
|
|
|
|
### User Stories
|
|
|
|
iTerm2 supports the following actions:
|
|
|
|
* **Story A:** _Send input to current session only_: The default setting.
|
|
* **Story B:** _Broadcast to all panes in all tabs_: Anything you type on the
|
|
keyboard goes to all sessions in this window.
|
|
* **Story C:** _Broadcast to all panes in current tab_: Anything you type on the
|
|
keyboard goes to all sessions in this tab.
|
|
* **Story D:** _Toggle broadcast input to current session_: Toggles whether this
|
|
session receives broadcasted keystrokes within this window.
|
|
|
|
|
|
## Solution Design
|
|
|
|
### Proposal 1: iTerm2-like Modal Input Broadcast
|
|
|
|
iTerm2 implements broadcast input as a type of "modal" system. The user is in
|
|
one of the following modes:
|
|
|
|
* Broadcast to all panes in all tabs
|
|
* Broadcast to all panes in the current tab
|
|
* Broadcast to some set of panes within the current tab
|
|
* Don't broadcast input at all (the default behavior)
|
|
|
|
These modes are vaguely per-tab state. There's a global "broadcast to all tabs &
|
|
panes" property. Then, each tab also has a pair of values:
|
|
* Should input be sent to all panes in this tab?
|
|
* If not, which panes should input be sent to?
|
|
|
|
It's not possible to send input to one pane in tab A, then another pane in tab
|
|
B, without enabling the global "broadcast to everyone" mode.
|
|
|
|
This seems to break down into the following actions:
|
|
|
|
```json
|
|
{ "action": "toggleBroadcastInput", "scope": "window" },
|
|
{ "action": "toggleBroadcastInput", "scope": "tab" },
|
|
{ "action": "toggleBroadcastInput", "scope": "pane" },
|
|
{ "action": "disableBroadcastInput" },
|
|
```
|
|
|
|
Which would be accompanied by the following internal properties:
|
|
* A window (`TerminalPage`-level) property for `broadcastToAllPanesAndTabs`
|
|
* A per-tab property for `broadcastToAllPanes`
|
|
* A per-tab set of panes to broadcast to
|
|
|
|
The scopes would work as follows:
|
|
|
|
* `"scope": "window"`: Toggle the window's "broadcast to all tabs and panes"
|
|
setting.
|
|
* `"scope": "tab"`: Toggle the tab's "broadcast to all panes in this tab"
|
|
setting.
|
|
- This does not modify the set of panes that the user is broadcasting to in
|
|
the tab, merely toggles the tab's setting. If the user has a set of panes
|
|
they're broadcasting to in this tab, then toggles this setting on and off,
|
|
we'll return to broadcasting to that set.
|
|
* `"scope": "pane"`: Add this pane to the set of panes being broadcasted to in
|
|
this tab.
|
|
- **TODO!: FOR DISCUSSION**: Should this disable the tab's
|
|
"broadcastToAllPanes" setting? Or should it leave that alone?
|
|
* `"disableBroadcastInput"`: Set the global setting to false, the tab's setting
|
|
to false, and clear the set of panes being broadcasted to for this tab.
|
|
- **TODO!** This could also just be `"action": "toggleBroadcastInput",
|
|
"scope": "none"`
|
|
|
|
#### Pros
|
|
* This is exactly how iTerm2 does it, so there's prior art.
|
|
* If you're not globally broadcasting, then you're only ever broadcasting to
|
|
some (sub)set of the panes in the current tab. So global broadcast mode is
|
|
the only time a user would need to worry about input being to be sent to
|
|
an inactive tab.
|
|
* You can have a set of panes to broadcast to in the first tab, then a
|
|
_separate_ set to broadcast to in a second tab. Broadcasting in one tab
|
|
does not affect the other.
|
|
|
|
#### Cons
|
|
* I frankly think the `tab`/`pane` interaction can be a little weird. Like for
|
|
this scenario:
|
|
- enable broadcast input for tab 1
|
|
- switch to tab 2
|
|
- enable broadcast input for a pane in tab 2
|
|
|
|
There's valid confusion to be had between the following two behaviors:
|
|
1. input goes to all of tab 1 _and_ the pane in tab 2
|
|
2. input only goes to the pane in tab 2
|
|
* You can't broadcast to a subset of panes in inactive tabs, in addition to
|
|
the active tab. All panes you want to broadcast to must be in the active
|
|
tab.
|
|
* Does creating a new split in a pane that's being broadcast to add that pane to
|
|
the broadcast set?
|
|
|
|
#### What would this mean for PR #9222?
|
|
|
|
The prototype PR [#9222] basically just implemented `{ "action":
|
|
"toggleBroadcastInput", "scope": "tab" }`. We could make `tab` the default
|
|
`scope` if no other one is specified, and then the PR would need basically no
|
|
modifications. Future PRs could add args to the `toggleBroadcastInput` action,
|
|
without breaking users who bind a key to that action now.
|
|
|
|
### Proposal 2: Broadcast Set
|
|
|
|
This was the design I had originally came up with before investigating iTerm2
|
|
much closer. This design involves a "broadcast set" of panes. All the panes in
|
|
the broadcast set would also get the `KeySent` and `CharSent` events, in
|
|
addition to the active pane. (The active pane may be a part of the broadcast
|
|
set). If a pane is read-only in the broadcast set, then it won't handle those
|
|
broadcasted events (obviously).
|
|
|
|
As far as actions, we're looking at something like:
|
|
|
|
* **A** Only send input to the active pane
|
|
* Remove all the panes from the broadcast set
|
|
* **B** send input to all panes in all tabs
|
|
* If all the panes are in the broadcast set, remove them all. Otherwise, add
|
|
all panes in all tabs to the broadcast set.
|
|
* **C** send input to all panes in the current tab
|
|
* If all the panes in the current tab are in the broadcast set, remove them
|
|
from the broadcast set. Otherwise, add all the panes from this tab to the
|
|
broadcast set.
|
|
* **D** toggle sending input to the current pane
|
|
* If this pane is in the broadcast set, remove it. Otherwise add it.
|
|
This seems to break down into the following actions:
|
|
|
|
```json
|
|
{ "action": "disableBroadcastInput" },
|
|
{ "action": "toggleBroadcastInput", "scope": "window" },
|
|
{ "action": "toggleBroadcastInput", "scope": "tab" },
|
|
{ "action": "toggleBroadcastInput", "scope": "pane" },
|
|
```
|
|
|
|
Which would be accompanied by the following internal properties:
|
|
* A window (`TerminalPage`-level) set of panes to broadcast to.
|
|
|
|
#### Pros:
|
|
* Mentally, you're either adding panes to the set of panes to broadcast to, or
|
|
removing them.
|
|
* You can broadcast to panes in multiple tabs, without broadcasting to _all_
|
|
panes in all tabs.
|
|
|
|
#### Cons:
|
|
* is _slightly_ different from iTerm2.
|
|
* Does creating a new split in a pane that's being broadcast to add that pane to
|
|
the broadcast set?
|
|
* You can't have a set of panes to broadcast to in the one tab, and a different
|
|
set in another tab. As an example:
|
|
1. in tab 1, you add panes A and B to the broadcast set. Typing in either one
|
|
goes to both A and B.
|
|
2. in tab 1, switch to pane C. Now input goes to A, B and C.
|
|
3. in tab 1, switch to pane D. Now input goes to A, B and D.
|
|
4. switch to tab 2, pane E. Now input goes to A, B and E.
|
|
|
|
You can't have like, a set with A & B (in 1), then E & F (in 2). So if someone
|
|
wants to type to both panes in 1, then both panes in 2, then both panes in 1,
|
|
they need to keep toggling which panes are in the broadcast set.
|
|
|
|
#### What would this mean for PR #9222?
|
|
|
|
Similar to Proposal 1, we'd use `tab` as the default value for `scope`. In the
|
|
future, when we add support for the other scopes, we'd change how the
|
|
broadcasting works, to use a set of panes to broadcast to, instead of just the
|
|
tab-level property.
|
|
|
|
### Proposal 3: It's iTerm2, but slightly different
|
|
|
|
While typing this up, I thought maybe it might make more sense if we took the
|
|
iTerm2 version, and changed it slightly:
|
|
|
|
* `"scope": "tab"`: If all the panes are in the broadcast set for this tab, then
|
|
remove them all. Otherwise, add all the panes in this tab to this tab's
|
|
broadcast set.
|
|
* `"scope": "pane"`: If this pane is in the broadcast set for a tab, then remove
|
|
it. Otherwise, add it.
|
|
|
|
With this, we get rid of the tab-level setting for "broadcast to all the panes
|
|
in this tab", and rely only on the broadcast set for that tab.
|
|
|
|
#### Pros:
|
|
* All the pros from proposal A
|
|
* Does away with the seemingly weird toggling between "all the panes in a tab"
|
|
and "some of the panes in a tab" that's possible with proposal A
|
|
|
|
#### Cons:
|
|
* You can't broadcast to a subset of panes in inactive tabs, in addition to
|
|
the active tab. All panes you want to broadcast to must be in the active
|
|
tab.
|
|
* is _slightly_ different from iTerm2. Just _slightly_.
|
|
* Does creating a new split in a pane that's being broadcast to add that pane to
|
|
the broadcast set?
|
|
|
|
#### What would this mean for PR #9222?
|
|
|
|
Same as with proposal A, we wouldn't change anything in the current PR. A future
|
|
PR that would add the other scope's to that action would need to change how the
|
|
broadcasting within a tab works, to use a set of panes to broadcast to, instead
|
|
of just the tab-level property.
|
|
|
|
## Conclusion
|
|
|
|
I'm proposing these settings for broader discussion. I'm not really sure which I
|
|
like most at this point. 1 & 3 have the advantage of being most similar to the
|
|
prior art, but 2 is more easily extendable to "groups" (see [Future
|
|
Considerations](#Future-Considerations)).
|
|
|
|
**TODO!**: Make a decision.
|
|
|
|
_**Fortunately**_: All these proposals actually use the same set of actions. So
|
|
it doesn't _really_ matter which we pick right now. We can unblock [#9222] as
|
|
the implementation of the `"tab"` scope, and address other scopes in the future.
|
|
We should still decide long-term which of these we'd like, but the actions seem
|
|
universal.
|
|
|
|
## UI/UX Design
|
|
|
|
This is supposed to be a quick & dirty spec, so I'm LARGELY skipping this.
|
|
|
|
As far as indicators go, we'll throw something like:
|
|
|
|

|
|
|
|
in the tab when a pane is being broadcasted to. If all tabs are being
|
|
broadcasted to, then they'll all have that icon. If a tab is inactive, and any
|
|
pane in that tab is being broadcast to, then show the icon in the tab.
|
|
|
|
It probably makes the most sense to have pane titlebars ([#4998]) also display
|
|
that icon.
|
|
|
|
In the original PR, it was suggested to use some variant of the [accent color]
|
|
to on the borders of panes that are currently receiving broadcasted input. We're
|
|
already using the accent color on the borders of the active pane.
|
|
`SystemAccentColorLight*`/`SystemAccentColorDark*` would provide a way of using
|
|
a similar hue with different lightness/saturation. This would be a decent visual
|
|
indicator that they're _not_ the active pane, but they are going to receive
|
|
input. Something a bit like:
|
|
|
|

|
|
|
|
This should obviously be able to be overridden in the user's theme, similar to
|
|
the pane border colors.
|
|
|
|
iTerm2 also supports displaying "stripes" in the background of all the panes
|
|
that are being broadcast too. That's certainly another way of indicating this
|
|
feature to the user. I'm not sure how we'd layer it with the background image
|
|
though. **I recommend we ignore this for now, and leave this as a follow-up**.
|
|
|
|
### Tab context menu items
|
|
|
|
For reference, refer to the following from iTerm2:
|
|

|
|
|
|
We don't have a menu bar like on macOS, but we do have a tab context menu. We
|
|
could add these items as a nested entry under each tab. If we wanted to do this,
|
|
we should also make sure to dynamically change the icon of the MenuItem to
|
|
reflect the current broadcast state.
|
|
|
|
## Potential Issues
|
|
|
|
<table>
|
|
|
|
<tr>
|
|
<td><strong>Compatibility</strong></td>
|
|
<td>
|
|
|
|
[comment]: # Will the proposed change break existing code/behaviors? If so, how, and is the breaking change "worth it"?
|
|
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
[comment]: # If there are any other potential issues, make sure to include them here.
|
|
|
|
## Implementation plan
|
|
|
|
* [ ] Resurrect [#9222], and use that to implement `"scope": "tab"`. This is
|
|
implemented the same, regardless of which proposal we chose.
|
|
* [ ] Add a tab context menu entry for toggling broadcast input, with a dynamic
|
|
icon based on the current state.
|
|
* [ ] Implement `"scope": "window"`. Again, this is implemented the same regardless
|
|
of which proposal we pursue.
|
|
* [ ] Decide between the two proposals here.
|
|
* [ ] Implement `"scope": "pane"`.
|
|
|
|
Doing the first element here is probably the most important one for most users,
|
|
and can be done regardless of the proposal chosen here. As such, we could even
|
|
suggest the default value of `scope` be `tab`. If we did that, then we wouldn't
|
|
need to do any args at all in the initial version.
|
|
|
|
## Future Considerations
|
|
|
|
Let's look to iTerm2, who's supported this feature for years, for some
|
|
inspiration of future things we should be worried about. If their users have
|
|
asked for these features, then it's inevitable that our users will too 😉
|
|
|
|
* [iterm2#6709] - Broadcast Input to multiple windows
|
|
- This is pretty straightforward. Would require coordination with the Monarch
|
|
though, and I'm worried about the perf hit of tossing every keystroke across
|
|
the process boundary.
|
|
- I suppose this would be `{ "action": "toggleBroadcastInput", "scope":
|
|
"global" }`
|
|
* [iterm2#6451], [iterm2#5563] - "Broadcast commands"
|
|
- iTerm2 has an action that lets the user manually clear the terminal-side
|
|
buffer. (This is tracked on the Windows Terminal as [#1882]). It might make
|
|
sense for there to be a mode where some _actions_ are also broadcast to
|
|
panes, not just key strokes. But which actions would those be? Moving the
|
|
selection anchors? Copy doesn't really make sense. Paste _does_ though.
|
|
Maybe the open find dialog / next&prev search match actions?
|
|
- This probably would require it's own spec.
|
|
* [iterm2#6007] - Different stripe color for different broadcast modes
|
|
- Have one color to indicate when broadcasting in `global` scope, another in
|
|
`tab` scope, a third in `pane` scope.
|
|
- This might mesh well with theming ([#3327]), for properties like
|
|
`pane.broadcastBorderColor.globalScope`,
|
|
`pane.broadcastBorderColor.paneScope`. Don't love those names, but you get
|
|
the idea.
|
|
* **[iterm2#5639]: Broadcast groups**, [iterm2#3372] - Broadcast Input to
|
|
multiple but not all tabs
|
|
- This is probably the most interesting request. I think this one identifies a
|
|
major shortcoming of the above proposals. With proposal 2, there's only ever
|
|
one top-level broadcast group. With proposals 1 & 3, there's per-tab
|
|
broadcast groups. In neither proposal can you have multiple concurrent
|
|
side-by-side broadcast groups.
|
|
- Groups should probably work across tabs. This would suggest that Proposal 2
|
|
is closer to how groups would work. Instead of there being one top-level
|
|
set, there would be multiple. **I'm not sure how proposals 1&3 would
|
|
seamlessly transition into also supporting groups**.
|
|
- The major trick here is: how do we differentiate these different groups to
|
|
the user? If we used the broadcast icon with a number, maybe in the corner
|
|
of the tab? Like [📡: 1]? Can a pane be in multiple broadcast sets at the
|
|
same time?
|
|
- The natural arg idea would be `{ "action": "toggleBroadcastInput", "scope":
|
|
"tab", "group": 1 }` to say "add all panes in the tab to broadcast group 1",
|
|
or "remove all panes in the tab from broadcast group 1". If panes are in
|
|
another group, they'd be moved to the specified group. If all panes are in
|
|
that group, then remove them all.
|
|
- The UI for this would certainly get complex fast.
|
|
- This also matches the Terminator-style broadcasting to groups.
|
|
* Re: stripes in the background of the tab. We could expose a pane's current
|
|
broadcast state to the pixel shader, and a user could use a custom pixel
|
|
shader to add stripes behind the text in the shader code. That's one possible
|
|
solution.
|
|
|
|
## Resources
|
|
|
|
[comment]: # Be sure to add links to references, resources, footnotes, etc.
|
|
|
|
|
|
### Footnotes
|
|
|
|
<a name="footnote-1"><a>[1]:
|
|
|
|
|
|
[#1882]: https://github.com/microsoft/terminal/issues/1882
|
|
[#2634]: https://github.com/microsoft/terminal/issues/2634
|
|
[#4998]: https://github.com/microsoft/terminal/issues/4998
|
|
[#3327]: https://github.com/microsoft/terminal/issues/3327
|
|
[#9222]: https://github.com/microsoft/terminal/pull/9222
|
|
[this comment]: https://github.com/microsoft/terminal/issues/2634#issuecomment-789116413
|
|
[iTerm2 implementation]: https://iterm2.com/documentation-one-page.html#documentation-menu-items.html
|
|
[@zljubisic]: https://github.com/microsoft/terminal/pull/9222#issuecomment-789143189
|
|
[accent color]: https://docs.microsoft.com/en-us/windows/uwp/design/style/color#accent-color-palette
|
|
|
|
|
|
[iterm2#6709]: https://gitlab.com/gnachman/iterm2/-/issues/6709
|
|
[iterm2#6451]: https://gitlab.com/gnachman/iterm2/-/issues/6451
|
|
[iterm2#6007]: https://gitlab.com/gnachman/iterm2/-/issues/6007
|
|
[iterm2#5639]: https://gitlab.com/gnachman/iterm2/-/issues/5639
|
|
[iterm2#5563]: https://gitlab.com/gnachman/iterm2/-/issues/5563
|
|
[iterm2#3372]: https://gitlab.com/gnachman/iterm2/-/issues/3372
|