From bd29a8ce716e28d3bec962539a90972b768594f2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 16 Aug 2026 19:10:14 -0500 Subject: [PATCH] Client(fix[wire]): Read client_control_mode why: IsControlClient read "client_control", which is not a tmux format token on any supported version -- it renders exactly like a token that does not exist. The lookup missed, so the property was false for every client, including one attached with `tmux -C`. Checked against a live control client on 3.2a through 3.7b. The same spelling sat in the query field catalog, the published JSON schema and three READMEs. Python libtmux, which this port tracks, records client_control_mode. The schema enum is corrected in place rather than versioned: the package documents itself as alpha that can change between prereleases, and its PublicAPI.Shipped.txt is still empty. what: - Read client_control_mode, the only client-control field FormatCatalog ever asks tmux for - Rename it in the field catalog generator, the query translator's example, and the three libtmux-query-v1 schema enums - Correct the three READMEs that published the old spelling - Assert a real control client reads back as one; the suite only asserted the false case, which a broken read also satisfies --- README.md | 2 +- .../FieldCatalogGenerator.cs | 4 ++-- src/LibTmux.Query.Json/README.md | 2 +- .../libtmux-query-v1.schema.json | 6 ++--- src/LibTmux/Client.Administration.cs | 2 +- src/LibTmux/Query/QueryTranslator.cs | 2 +- src/LibTmux/README.md | 2 +- .../Clients/ClientAdministrationTests.cs | 23 +++++++++++++++++++ .../Query/QuerySemanticsTests.cs | 2 +- 9 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f7b8518..ba7d9fb 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ Console.WriteLine(document.Target); // Session ``` You write C# and tmux receives tmux: `Session.Name` goes on the wire as -`session_name`, and `Client.IsControlClient` as `client_control`. The catalog +`session_name`, and `Client.IsControlClient` as `client_control_mode`. The catalog carries that pair for all twelve queryable fields, and it is closed — a field outside it throws `UnsupportedQueryExpressionException` rather than falling back, so an expression that translates is one tmux can answer. diff --git a/src/LibTmux.Generators/FieldCatalogGenerator.cs b/src/LibTmux.Generators/FieldCatalogGenerator.cs index 1c14625..02ad8ec 100644 --- a/src/LibTmux.Generators/FieldCatalogGenerator.cs +++ b/src/LibTmux.Generators/FieldCatalogGenerator.cs @@ -16,13 +16,13 @@ public sealed class FieldCatalogGenerator : IIncrementalGenerator /// The closed catalog, and where each field lives on its entity. /// /// Wire name and property are declared explicitly because the mapping is - /// not systematic (client_controlIsControlClient, and two + /// not systematic (client_control_modeIsControlClient, and two /// fields have no property at all). /// private static readonly (string WireName, string Target, bool Relation, string? Property)[] Fields = { - ("client_control", "Client", false, "IsControlClient"), + ("client_control_mode", "Client", false, "IsControlClient"), ("client_id", "Client", false, null), ("client_name", "Client", false, "Name"), ("pane_command", "Pane", false, null), diff --git a/src/LibTmux.Query.Json/README.md b/src/LibTmux.Query.Json/README.md index 293e413..8266786 100644 --- a/src/LibTmux.Query.Json/README.md +++ b/src/LibTmux.Query.Json/README.md @@ -82,7 +82,7 @@ Console.WriteLine($"depth {QueryJsonLimits.V1.MaximumDepth}, nodes {QueryJsonLim `session_name`, `session_attached`, `session_id`, `session_windows`, `window_name`, `window_id`, `window_panes`, `pane_id`, `pane_command`, -`client_id`, `client_name`, `client_control`. +`client_id`, `client_name`, `client_control_mode`. You write these as the properties they are — `Session.Name`, `Client.IsControlClient` — and the wire carries the tmux spelling. diff --git a/src/LibTmux.Query.Json/libtmux-query-v1.schema.json b/src/LibTmux.Query.Json/libtmux-query-v1.schema.json index adf694e..98a4dbc 100644 --- a/src/LibTmux.Query.Json/libtmux-query-v1.schema.json +++ b/src/LibTmux.Query.Json/libtmux-query-v1.schema.json @@ -50,7 +50,7 @@ "target": { "$ref": "#/$defs/target" }, "wireName": { "enum": [ - "client_control", + "client_control_mode", "client_id", "client_name", "pane_command", @@ -99,7 +99,7 @@ "properties": { "target": { "const": "client" }, "wireName": { - "enum": ["client_control", "client_id", "client_name"] + "enum": ["client_control_mode", "client_id", "client_name"] } } } @@ -112,7 +112,7 @@ { "$ref": "#/$defs/field" }, { "properties": { - "wireName": { "enum": ["client_control", "session_attached"] } + "wireName": { "enum": ["client_control_mode", "session_attached"] } } } ] diff --git a/src/LibTmux/Client.Administration.cs b/src/LibTmux/Client.Administration.cs index 58b669e..4589295 100644 --- a/src/LibTmux/Client.Administration.cs +++ b/src/LibTmux/Client.Administration.cs @@ -41,7 +41,7 @@ internal Client( public string? Tty => ReadSnapshot("client_tty"); /// Gets whether the client speaks tmux's control protocol. - public bool IsControlClient => ReadSnapshot("client_control") == "1"; + public bool IsControlClient => ReadSnapshot("client_control_mode") == "1"; /// Gets the session the client was attached to when it was read. /// diff --git a/src/LibTmux/Query/QueryTranslator.cs b/src/LibTmux/Query/QueryTranslator.cs index 7e16a1a..26cc4d2 100644 --- a/src/LibTmux/Query/QueryTranslator.cs +++ b/src/LibTmux/Query/QueryTranslator.cs @@ -213,7 +213,7 @@ private static QueryNode TranslateOperand( private static FieldNode FieldFor(MemberInfo member) { // What tmux calls a field is not a transformation of what C# calls - // it -- Client.IsControlClient is client_control, not + // it -- Client.IsControlClient is client_control_mode, not // is_control_client. The catalog carries that pairing; an unknown // type is a caller's own row, whose property names are wire names already. string wireName = diff --git a/src/LibTmux/README.md b/src/LibTmux/README.md index a732a7e..009741b 100644 --- a/src/LibTmux/README.md +++ b/src/LibTmux/README.md @@ -213,7 +213,7 @@ QueryDocument document = QueryExtensions.Translate( You write C# and tmux receives tmux. The catalog carries the pair for all twelve queryable fields — `Session.Name` is `session_name`, -`Client.IsControlClient` is `client_control` — and it is closed: +`Client.IsControlClient` is `client_control_mode` — and it is closed: | Session | Window | Pane | Client | |---|---|---|---| diff --git a/tests/LibTmux.IntegrationTests/Clients/ClientAdministrationTests.cs b/tests/LibTmux.IntegrationTests/Clients/ClientAdministrationTests.cs index 9327d0c..0095080 100644 --- a/tests/LibTmux.IntegrationTests/Clients/ClientAdministrationTests.cs +++ b/tests/LibTmux.IntegrationTests/Clients/ClientAdministrationTests.cs @@ -72,6 +72,29 @@ await Assert.ThrowsAsync( } } + [Fact( + Skip = "Requires a Unix process environment.", + SkipType = typeof(UnixTestEnvironment), + SkipUnless = nameof(UnixTestEnvironment.IsUnix))] + public async Task Control_client_reads_back_as_one() + { + await using RawTmuxTestContext raw = await RawTmuxTestContext.StartAsync( + TestContext.Current.CancellationToken); + CancellationToken token = TestContext.Current.CancellationToken; + Server server = await ConnectAsync(raw, token); + + await using ControlModeClientScope control = await ControlModeClientScope.StartAsync( + raw, + token); + + // The only assertion in the suite a broken read cannot satisfy: every + // other client test attaches a terminal, for which false is the right + // answer whether or not the field resolves. + Client client = await WaitForClientAsync(server, token); + Assert.Equal(control.ClientName, client.Name); + Assert.True(client.IsControlClient); + } + [Fact( Skip = "Requires a Unix process environment.", SkipType = typeof(UnixTestEnvironment), diff --git a/tests/LibTmux.UnitTests/Query/QuerySemanticsTests.cs b/tests/LibTmux.UnitTests/Query/QuerySemanticsTests.cs index 669c896..306cbf5 100644 --- a/tests/LibTmux.UnitTests/Query/QuerySemanticsTests.cs +++ b/tests/LibTmux.UnitTests/Query/QuerySemanticsTests.cs @@ -24,7 +24,7 @@ public void An_entity_translates_through_the_name_tmux_uses_for_the_field() // The one that a naming rule would never produce. Assert.Equal( - "client_control", + "client_control_mode", Field(QueryExtensions.Translate(client => client.IsControlClient))); }