Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/analyze-hang-dump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
steps:

- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/finalize-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

steps:
- name: Checkout main
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-agent-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Run agent docs lint
shell: pwsh
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/perf-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion needs full history

Expand Down Expand Up @@ -73,7 +73,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:

steps:
- name: Checkout develop
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: develop
fetch-depth: 0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

steps:
- name: Checkout ${{ github.ref_name }}
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # fetch-depth is needed for GitVersion https://github.com/GitTools/actions/blob/main/docs/cloning.md

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/quick-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
steps:

- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Setup .NET Core
uses: actions/setup-dotnet@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:

steps:
- name: Checkout main
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/stress-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
DisableRealDriverIO: "1"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
DisableRealDriverIO: "1"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
steps:

- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down Expand Up @@ -85,7 +85,7 @@ jobs:
steps:

- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-doc-snippets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0 # GitVersion.MsBuild needs full history

Expand Down
53 changes: 53 additions & 0 deletions Examples/UICatalog/Scenarios/TreeUseCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ namespace UICatalog.Scenarios;
[ScenarioCategory ("TreeView")]
public partial class TreeUseCases : Scenario
{
private CheckBox? _customArmyColorsCheckBox;
private EventLog? _eventLog;
private Runnable? _appWindow;
private TreeView<GameObject>? _armyTree;
private TreeViewEditor? _treeViewEditor;
private ViewportSettingsEditor? _viewportSettingsEditor;

Expand All @@ -34,6 +36,15 @@ public override void Main ()
new MenuItem { Title = "Armies With _Delegate", Action = () => LoadArmies (true) }
]));

_customArmyColorsCheckBox = new CheckBox
{
Title = "_Army Type Colors",
Value = CheckState.Checked
};
_customArmyColorsCheckBox.ValueChanged += (_, _) => SetArmyTypeColors ();

menu.Add (new MenuBarItem ("_Style", [new MenuItem { CommandView = _customArmyColorsCheckBox }]));

// EventLog on the right
_eventLog = new EventLog
{
Expand Down Expand Up @@ -153,12 +164,16 @@ private void LoadArmies (bool useDelegate)
}

tree.AddObject (army);
_armyTree = tree;
SetArmyTypeColors ();

CurrentTree = tree;
}

private void LoadRooms ()
{
_armyTree = null;

House myHouse = new ()
{
Address = "23 Nowhere Street", Rooms = [new Room { Name = "Ballroom" }, new Room { Name = "Bedroom 1" }, new Room { Name = "Bedroom 2" }]
Expand All @@ -174,6 +189,8 @@ private void LoadRooms ()

private void LoadEnableForDesign ()
{
_armyTree = null;

TreeView tree = new ();
tree.EnableForDesign ();
tree.Title = "_EnableForDesign";
Expand All @@ -183,6 +200,42 @@ private void LoadEnableForDesign ()

private void Quit () => _appWindow?.RequestStop ();

private void SetArmyTypeColors ()
{
if (_armyTree is null || _customArmyColorsCheckBox is null)
{
return;
}

if (_customArmyColorsCheckBox.Value != CheckState.Checked)
{
_armyTree.ColorGetter = null;
_armyTree.SetNeedsDraw ();

return;
}

_armyTree.ColorGetter = model => model switch
{
Army => CreateArmyTypeScheme (_armyTree, Color.BrightYellow),
CorpsObject => CreateArmyTypeScheme (_armyTree, Color.BrightCyan),
Division => CreateArmyTypeScheme (_armyTree, Color.BrightGreen),
Brigade => CreateArmyTypeScheme (_armyTree, Color.BrightMagenta),
Unit => CreateArmyTypeScheme (_armyTree, Color.Gray),
_ => null
};

_armyTree.SetNeedsDraw ();
}

private static Scheme CreateArmyTypeScheme (TreeView<GameObject> tree, Color foreground) =>
new ()
{
Normal = tree.GetAttributeForRole (VisualRole.Normal) with { Foreground = foreground },
Focus = tree.GetAttributeForRole (VisualRole.Focus) with { Foreground = foreground },
Active = tree.GetAttributeForRole (VisualRole.Active) with { Foreground = foreground }
};

// ── House / Room model (unchanged) ─────────────────────────────────────

private class House : TreeNode
Expand Down
89 changes: 87 additions & 2 deletions Terminal.Gui/Drawing/Kitty/KittyGraphicsEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public class KittyGraphicsEncoder
/// </summary>
public const int MaxChunkSize = 4096;

// Each image id carries exactly one placement, so a single stable placement id is enough for an
// a=T to replace it in place across repaints (see BuildApcSequence).
private const int PlacementId = 1;

// APC = ESC _ ... ESC \
private const string APC_START = "\x1b_G";
private const string APC_END = "\x1b\\";
Expand Down Expand Up @@ -73,6 +77,83 @@ public string EncodeKitty (Color [,] pixels, int destCols, int destRows, int? im
/// <returns>The complete Kitty APC delete escape sequence string.</returns>
public static string EncodeDeletePlacements (int imageId) => $"{APC_START}a=d,d=i,i={imageId}{APC_END}";

/// <summary>
/// Encodes a transmit-only (<c>a=t</c>) Kitty sequence: it sends the full image data under the given
/// <paramref name="imageId"/> without creating a placement. The image stays resident in the terminal so
/// it can be displayed — and re-displayed with a different crop — via <see cref="EncodePut"/> without
/// re-sending the pixels. Used to pan/zoom a static image with tiny per-frame placement updates instead
/// of re-transmitting the whole image every frame (which reads as a flash for large images).
/// </summary>
/// <param name="pixels">The full source image to transmit, indexed as <c>[x, y]</c>.</param>
/// <param name="imageId">The stable image id (the Kitty <c>i</c> key).</param>
/// <returns>The complete Kitty APC transmit-only escape sequence (chunked).</returns>
public string EncodeTransmit (Color [,] pixels, int imageId)
{
int width = pixels.GetLength (0);
int height = pixels.GetLength (1);
byte [] rgba = PixelsToRgba (pixels, width, height);
string base64 = Convert.ToBase64String (rgba);

var sb = new StringBuilder ();
int total = base64.Length;
var offset = 0;

string firstChunk = offset + MaxChunkSize < total ? base64.Substring (offset, MaxChunkSize) : base64.Substring (offset);
bool isLastChunk = offset + firstChunk.Length >= total;

sb.Append (APC_START);
sb.Append ($"a=t,f=32,i={imageId},s={width},v={height},q=2,m={( isLastChunk ? 0 : 1 )}");
sb.Append (';');
sb.Append (firstChunk);
sb.Append (APC_END);
offset += firstChunk.Length;

while (offset < total)
{
string chunk = offset + MaxChunkSize < total ? base64.Substring (offset, MaxChunkSize) : base64.Substring (offset);
bool last = offset + chunk.Length >= total;

sb.Append (APC_START);
sb.Append ($"m={( last ? 0 : 1 )}");
sb.Append (';');
sb.Append (chunk);
sb.Append (APC_END);
offset += chunk.Length;
}

return sb.ToString ();
}

/// <summary>
/// Encodes a placement (<c>a=p</c>) that displays a crop of an already-transmitted image (see
/// <see cref="EncodeTransmit"/>) at the current cursor position. The source rectangle
/// (<paramref name="srcX"/>,<paramref name="srcY"/>,<paramref name="srcW"/>,<paramref name="srcH"/>) in
/// image pixels is scaled to fill <paramref name="destCols"/>×<paramref name="destRows"/> cells. A later
/// placement with the same (image id, placement id) replaces this one in place — so panning/zooming a
/// static image is a tiny, flash-free update rather than a full re-transmit.
/// </summary>
/// <param name="imageId">The id of the already-transmitted image.</param>
/// <param name="placementId">The placement id; reusing it replaces the placement in place.</param>
/// <param name="srcX">Left edge of the source crop, in image pixels.</param>
/// <param name="srcY">Top edge of the source crop, in image pixels.</param>
/// <param name="srcW">Width of the source crop, in image pixels.</param>
/// <param name="srcH">Height of the source crop, in image pixels.</param>
/// <param name="destCols">Number of columns to display the crop in.</param>
/// <param name="destRows">Number of rows to display the crop in.</param>
/// <returns>The complete Kitty APC placement escape sequence.</returns>
public static string EncodePut (int imageId, int placementId, int srcX, int srcY, int srcW, int srcH, int destCols, int destRows) =>
$"{APC_START}a=p,i={imageId},p={placementId},x={srcX},y={srcY},w={srcW},h={srcH},c={destCols},r={destRows},z=-1,C=1,q=2{APC_END}";

/// <summary>
/// Encodes a Kitty sequence that deletes a single placement (by image id + placement id), leaving the
/// transmitted image data and other placements intact.
/// </summary>
/// <param name="imageId">The image id (the Kitty <c>i</c> key).</param>
/// <param name="placementId">The placement id (the Kitty <c>p</c> key) to delete.</param>
/// <returns>The complete Kitty APC delete-placement escape sequence.</returns>
public static string EncodeDeletePlacement (int imageId, int placementId) =>
$"{APC_START}a=d,d=i,i={imageId},p={placementId}{APC_END}";

/// <summary>
/// Derives a stable, positive, non-zero Kitty image id from the given string identifier.
/// </summary>
Expand Down Expand Up @@ -132,8 +213,12 @@ private static string BuildApcSequence (string base64, int pixelWidth, int pixel
bool isLastChunk = offset + firstChunk.Length >= totalLength;

// i=<id> tags the placement with a stable image id so a prior placement can be deleted by
// id (see EncodeDeletePlacements) when the image is resized or moved.
string idField = imageId.HasValue ? $"i={imageId.Value}," : string.Empty;
// id (see EncodeDeletePlacements) when the image is resized or moved. p=<PlacementId> gives the
// placement a stable id too: a later a=T with the same (image id, placement id) REPLACES the
// placement in place — the previous pixels stay on screen until the new ones arrive — so an
// unchanged-geometry repaint (pan, zoom-in) never blanks. Without it, re-placing would have to
// delete first (image vanishes, then the full image is slowly re-sent = a visible flash).
string idField = imageId.HasValue ? $"i={imageId.Value},p={PlacementId}," : string.Empty;

// C=1 suppresses cursor movement after the image is displayed. Without it the terminal
// advances the cursor past the bottom-right of the image, and an image near the bottom of
Expand Down
Loading
Loading