-
Notifications
You must be signed in to change notification settings - Fork 0
Show which pane and which command line are selected, plus Ctrl+arrow navigation and a real Alt+⏎ newline #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| namespace SharpMUTerm.Core.Workspaces; | ||
|
|
||
| /// <summary>Which way a directional focus move goes.</summary> | ||
| public enum PaneDirection | ||
| { | ||
| /// <summary>Toward smaller X.</summary> | ||
| Left, | ||
|
|
||
| /// <summary>Toward larger X.</summary> | ||
| Right, | ||
|
|
||
| /// <summary>Toward smaller Y.</summary> | ||
| Up, | ||
|
|
||
| /// <summary>Toward larger Y.</summary> | ||
| Down, | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Directional pane focus — which pane is "left of" this one, and so on. It answers from | ||
| /// <em>geometry</em> (the solved <see cref="PaneRect"/>s) rather than from the split tree, because the | ||
| /// question the user is asking is about what they can see: in a tree of nested splits, "the pane to my | ||
| /// left" is a spatial fact, and the same tree walked structurally gives answers that are correct about | ||
| /// the tree and wrong about the screen. | ||
| /// <para> | ||
| /// Pure and UI-agnostic. The caller supplies the rectangles, so the same rule serves the live layout | ||
| /// (the arranged pane rectangles, which is what the app passes — they already reflect zoom, since a | ||
| /// zoomed workspace realises exactly one pane) and a unit test (a hand-written dictionary, or | ||
| /// <see cref="LayoutSolver.Solve"/> over any bounds). | ||
| /// </para> | ||
| /// </summary> | ||
| public static class PaneNavigation | ||
| { | ||
| /// <summary> | ||
| /// The pane <paramref name="direction"/> of <paramref name="fromPaneId"/>, or null when there is | ||
| /// none — which the caller is expected to <em>report</em> rather than swallow, because a navigation | ||
| /// key that silently does nothing is indistinguishable from one that is not bound. | ||
| /// <para> | ||
| /// Candidates are the panes that begin beyond the near edge of the starting pane on the axis of | ||
| /// travel. Among them the winner is the one that <em>overlaps</em> the starting pane on the cross | ||
| /// axis and lies nearest along the axis of travel; overlap is preferred absolutely, so a wide pane | ||
| /// directly alongside always beats a nearer one that is merely diagonal. Ties — two panes stacked in | ||
| /// the column you are stepping into — go to the one whose cross-axis centre is closest to yours, | ||
| /// then to the lower pane id, so the answer is stable frame to frame. | ||
| /// </para> | ||
| /// </summary> | ||
| public static string? Neighbour( | ||
| IReadOnlyDictionary<string, PaneRect> rects, | ||
| string fromPaneId, | ||
| PaneDirection direction) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(rects); | ||
| ArgumentNullException.ThrowIfNull(fromPaneId); | ||
|
|
||
| if (!rects.TryGetValue(fromPaneId, out var from)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var horizontal = direction is PaneDirection.Left or PaneDirection.Right; | ||
| string? best = null; | ||
| (bool Diagonal, int Gap, int Cross, string Id) bestScore = default; | ||
|
|
||
| foreach (var (id, rect) in rects) | ||
| { | ||
| if (string.Equals(id, fromPaneId, StringComparison.Ordinal) || rect.IsEmpty) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (Gap(from, rect, direction) is not { } gap) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| // Overlap on the cross axis is what makes a pane "alongside" rather than "diagonal from". | ||
| var diagonal = !Overlaps(from, rect, horizontal); | ||
| var cross = Math.Abs(Centre(rect, horizontal) - Centre(from, horizontal)); | ||
| var score = (diagonal, gap, cross, id); | ||
| if (best is null || Better(score, bestScore)) | ||
| { | ||
| best = id; | ||
| bestScore = score; | ||
| } | ||
| } | ||
|
|
||
| return best; | ||
| } | ||
|
|
||
| /// <summary>Whether one candidate beats another: alongside first, then nearest, then most aligned.</summary> | ||
| private static bool Better( | ||
| (bool Diagonal, int Gap, int Cross, string Id) candidate, | ||
| (bool Diagonal, int Gap, int Cross, string Id) incumbent) | ||
| { | ||
| if (candidate.Diagonal != incumbent.Diagonal) | ||
| { | ||
| return !candidate.Diagonal; | ||
| } | ||
|
|
||
| if (candidate.Gap != incumbent.Gap) | ||
| { | ||
| return candidate.Gap < incumbent.Gap; | ||
| } | ||
|
|
||
| if (candidate.Cross != incumbent.Cross) | ||
| { | ||
| return candidate.Cross < incumbent.Cross; | ||
| } | ||
|
|
||
| return string.CompareOrdinal(candidate.Id, incumbent.Id) < 0; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// How far <paramref name="candidate"/> lies beyond <paramref name="from"/> in the direction of | ||
| /// travel, or null when it does not lie beyond it at all. Measured edge to edge, so the one-cell | ||
| /// divider between two siblings scores 1 and a pane across a nested split scores more. | ||
| /// </summary> | ||
| private static int? Gap(PaneRect from, PaneRect candidate, PaneDirection direction) | ||
| { | ||
| var gap = direction switch | ||
| { | ||
| PaneDirection.Left => from.X - (candidate.X + candidate.Width), | ||
| PaneDirection.Right => candidate.X - (from.X + from.Width), | ||
| PaneDirection.Up => from.Y - (candidate.Y + candidate.Height), | ||
| _ => candidate.Y - (from.Y + from.Height), | ||
| }; | ||
|
|
||
| return gap >= 0 ? gap : null; | ||
| } | ||
|
|
||
| /// <summary>Whether two rectangles share any extent on the axis <em>across</em> the travel.</summary> | ||
| private static bool Overlaps(PaneRect a, PaneRect b, bool horizontal) => horizontal | ||
| ? a.Y < b.Y + b.Height && b.Y < a.Y + a.Height | ||
| : a.X < b.X + b.Width && b.X < a.X + a.Width; | ||
|
|
||
| /// <summary>A rectangle's centre on the axis across the travel, doubled to stay integral.</summary> | ||
| private static int Centre(PaneRect rect, bool horizontal) => | ||
| horizontal ? (rect.Y * 2) + rect.Height : (rect.X * 2) + rect.Width; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: SharpMUSH/SharpMUTerm
Length of output: 38530
🌐 Web query:
xterm modifyOtherKeys CSI-u modifier-aware sequences documentation💡 Result:
xterm's modifyOtherKeys is a resource that allows the terminal to report key combinations that are typically ambiguous or unmappable using standard terminal escape sequences [1]. It addresses the historical limitation where certain modified keys (like Ctrl+I) are indistinguishable from others (like Tab) [1]. Mechanism When enabled, xterm sends "CSI u" encoded sequences for "other" keys—typically printable keys like letters, numbers, and punctuation—when they are modified by keys such as Control, Alt, or Meta [2][3]. The general structure for this encoding is CSI [codepoint]; [modifiers] u [4]. Modes for modifyOtherKeys The behavior is controlled by the modifyOtherKeys resource, which accepts the following values [5]: 0: Disabled [5]. 1: Enabled for most keys, but excludes keys with well-known legacy behaviors (e.g., Tab, Backspace, Esc, and certain X11-defined control sequences like Ctrl+Space or Ctrl+3) [5][3]. 2: Enabled for all keys, including the exceptions listed in mode 1. This mode ignores legacy X11 library special cases and ensures modified keys send distinct sequences [5][3]. 3: (Introduced in xterm patch
#398) Extends the reporting to all keys, including unmodified ones, effectively forcing every keypress to send a sequence [6]. Runtime Control (XTMODKEYS) Applications can toggle this behavior at runtime using the XTMODKEYS control sequence [7][8]: CSI > 4; Pv m Where Pv corresponds to the mode (0, 1, 2, or 3) [7][6]. To disable the feature, an application can send CSI > 4; 0 m [7][6]. Relationship to CSI-u and Modern Protocols The "CSI u" encoding was a precursor to more modern keyboard protocols, such as the Kitty keyboard protocol [1][4]. While the Kitty protocol is richer and designed for modern, greenfield applications, modifyOtherKeys and the CSI u encoding remain the de-facto standard for compatibility with many existing terminal user interfaces (TUIs) [7][4]. Official Documentation The definitive technical references for these sequences are: 1. The xterm man page (xterm(1)) [9][5]. 2. The ctlseqs.txt file maintained by the xterm author [8]. 3. The XTerm "Other" Modified Keys FAQ page [2][10].Citations:
Broaden the input-protocol wording.
Ctrl+⏎/Shift+⏎need a modifier-preserving terminal protocol plus matching parser support; Kitty is one option, but xterm-stylemodifyOtherKeys/CSI-u modes are another where supported.🤖 Prompt for AI Agents