Skip to content

Commit 0565ce5

Browse files
committed
Add bindable query state to command palette
1 parent ddec5bd commit 0565ce5

File tree

6 files changed

+272
-31
lines changed

6 files changed

+272
-31
lines changed

samples/ControlsDemo/Demos/CommandPaletteDemo.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public override Visual Build(DemoContext context)
2424
var verticalAlignment = new State<Align>(Align.Start);
2525
var offsetX = new State<int>(0);
2626
var offsetY = new State<int>(0);
27+
var clearQueryOnShow = new State<bool>(true);
28+
var queryText = new State<string?>(string.Empty);
2729
var palette = new CommandPalette().Style(() => CommandPaletteStyle.Default with
2830
{
2931
PopupWidthPercent = Math.Clamp(widthPercent.Value, 1, 100),
@@ -33,7 +35,9 @@ public override Visual Build(DemoContext context)
3335
PopupVerticalAlignment = verticalAlignment.Value,
3436
PopupOffsetX = offsetX.Value,
3537
PopupOffsetY = offsetY.Value,
36-
});
38+
})
39+
.QueryText(queryText)
40+
.ClearQueryOnShow(clearQueryOnShow);
3741

3842
void ShowPalette()
3943
{
@@ -44,6 +48,14 @@ void ShowPalette()
4448
DemoUi.Title("Command palette"),
4549
new TextBlock("Press Ctrl+P to open the command palette. Type to search, press Enter to run the top match, use arrows to navigate, or resize the window with the mouse.")
4650
.Wrap(true),
51+
new Group("Query state").Content(new VStack(
52+
new CheckBox("Clear query on show").IsChecked(clearQueryOnShow),
53+
new HStack(
54+
new Button("Preset query").Click(() => queryText.Value = "reset"),
55+
new Button("Clear query").Click(() => queryText.Value = string.Empty))
56+
.Spacing(1),
57+
new TextBlock(() => $"Current query: {queryText.Value}"))
58+
.Spacing(1)),
4759
new Group("Popup host style").Content(new VStack(
4860
new HStack(
4961
"Width (%):",

site/docs/controls/commandpalette.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ root.AddCommand(new Command
4747
When open:
4848

4949
- Type to search
50+
- `Show()` clears the previous search by default
5051
- Press Enter to execute the currently highlighted result (the first match by default)
5152
- Use Up/Down to navigate results
5253
- Press Down from the search box to move directly to the next result
@@ -79,9 +80,22 @@ palette.Style(CommandPaletteStyle.Default with
7980

8081
`PopupWidthPercent` and `PopupHeightPercent` are optional viewport-relative sizing hints. Alignment (`Start`, `Center`, `End`, `Stretch`) and `PopupOffsetX` / `PopupOffsetY` still control where the popup appears after that size is resolved.
8182

83+
## Query state
84+
85+
`QueryText` is a bindable property on `CommandPalette`, so applications can observe, prefill, or two-way bind the current search text. `ClearQueryOnShow` controls whether `Show()` resets that query before the palette takes focus. It defaults to `true`.
86+
87+
```csharp
88+
var palette = new CommandPalette
89+
{
90+
ClearQueryOnShow = false,
91+
QueryText = "reset",
92+
};
93+
```
94+
8295
## Defaults
8396

8497
- Default popup alignment: `PopupHorizontalAlignment = Align.Center`, `PopupVerticalAlignment = Align.Start`
98+
- Default query behavior: `ClearQueryOnShow = true`
8599

86100
## Related
87101
- [Commands](../commands.md)

site/docs/specs/controls/commandpalette.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ Introduce `CommandPaletteStyle` (or update existing one) with:
215215
- optional `DescriptionVisible` / `ShowDescription`
216216
- optional `MaxWidth`, `ResultsHeight`, padding, etc.
217217

218+
The palette control itself also exposes bindable query state so apps can integrate it with the rest of their UI:
219+
220+
- `QueryText` for the current search text
221+
- `ClearQueryOnShow` to control whether `Show()` starts from an empty query or preserves the existing one
222+
218223
### Default item template
219224

220225
Default row visual (single line):

site/img/controls/commandpalette.svg

Lines changed: 1 addition & 1 deletion
Loading

src/XenoAtom.Terminal.UI.Tests/CommandPaletteTests.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,163 @@ public void CommandPalette_Invokes_Action_On_Enter_From_Search()
112112
driver.TickUntil(() => invoked);
113113
}
114114

115+
[TestMethod]
116+
public void CommandPalette_QueryText_Tracks_Search_Box_Input()
117+
{
118+
var root = new VStack();
119+
using var driver = new TerminalAppTestDriver(root, TerminalHostKind.Fullscreen, new TerminalSize(60, 12));
120+
driver.Tick();
121+
122+
var palette = new CommandPalette();
123+
driver.App.AddGlobalCommand(new Command
124+
{
125+
Id = "cmd.open",
126+
LabelMarkup = "Open",
127+
Presentation = CommandPresentation.CommandPalette,
128+
Execute = _ => { },
129+
});
130+
131+
palette.Show();
132+
driver.Tick();
133+
134+
driver.Backend.PushEvent(new TerminalTextEvent { Text = "op" });
135+
driver.Tick();
136+
137+
Assert.AreEqual("op", palette.QueryText, "Expected typing in the search box to update the bindable QueryText property.");
138+
}
139+
140+
[TestMethod]
141+
public void CommandPalette_Show_Clears_Query_By_Default()
142+
{
143+
var root = new VStack();
144+
using var driver = new TerminalAppTestDriver(root, TerminalHostKind.Fullscreen, new TerminalSize(60, 12));
145+
driver.Tick();
146+
147+
var palette = new CommandPalette
148+
{
149+
QueryText = "op",
150+
};
151+
152+
driver.App.AddGlobalCommand(new Command
153+
{
154+
Id = "cmd.open",
155+
LabelMarkup = "Open",
156+
Presentation = CommandPresentation.CommandPalette,
157+
Execute = _ => { },
158+
});
159+
160+
driver.App.AddGlobalCommand(new Command
161+
{
162+
Id = "cmd.build",
163+
LabelMarkup = "Build",
164+
Presentation = CommandPresentation.CommandPalette,
165+
Execute = _ => { },
166+
});
167+
168+
palette.Show();
169+
driver.Tick();
170+
171+
Assert.AreEqual(string.Empty, palette.QueryText, "Expected Show() to clear the previous query by default.");
172+
173+
var outText = driver.Backend.GetOutText();
174+
var screen = new AnsiTestScreen(60, 12);
175+
screen.Apply(outText);
176+
var rendered = screen.GetText();
177+
178+
StringAssert.Contains(rendered, "Open");
179+
StringAssert.Contains(rendered, "Build");
180+
}
181+
182+
[TestMethod]
183+
public void CommandPalette_Show_Can_Preserve_Query_When_Configured()
184+
{
185+
var root = new VStack();
186+
using var driver = new TerminalAppTestDriver(root, TerminalHostKind.Fullscreen, new TerminalSize(60, 12));
187+
driver.Tick();
188+
189+
var palette = new CommandPalette
190+
{
191+
ClearQueryOnShow = false,
192+
QueryText = "op",
193+
};
194+
195+
driver.App.AddGlobalCommand(new Command
196+
{
197+
Id = "cmd.open",
198+
LabelMarkup = "Open",
199+
Presentation = CommandPresentation.CommandPalette,
200+
Execute = _ => { },
201+
});
202+
203+
driver.App.AddGlobalCommand(new Command
204+
{
205+
Id = "cmd.build",
206+
LabelMarkup = "Build",
207+
Presentation = CommandPresentation.CommandPalette,
208+
Execute = _ => { },
209+
});
210+
211+
palette.Show();
212+
driver.Tick();
213+
214+
Assert.AreEqual("op", palette.QueryText, "Expected Show() to preserve the query when ClearQueryOnShow is disabled.");
215+
216+
var outText = driver.Backend.GetOutText();
217+
var screen = new AnsiTestScreen(60, 12);
218+
screen.Apply(outText);
219+
var rendered = screen.GetText();
220+
221+
StringAssert.Contains(rendered, "Open");
222+
Assert.IsFalse(rendered.Contains("Build", StringComparison.Ordinal), "Expected the preserved query to remain active when the palette opens.");
223+
}
224+
225+
[TestMethod]
226+
public void CommandPalette_QueryText_Can_Be_Set_Programmatically_While_Open()
227+
{
228+
var root = new VStack();
229+
using var driver = new TerminalAppTestDriver(root, TerminalHostKind.Fullscreen, new TerminalSize(60, 12));
230+
driver.Tick();
231+
232+
var palette = new CommandPalette
233+
{
234+
ClearQueryOnShow = false,
235+
};
236+
237+
driver.App.AddGlobalCommand(new Command
238+
{
239+
Id = "cmd.open",
240+
LabelMarkup = "Open",
241+
Presentation = CommandPresentation.CommandPalette,
242+
Execute = _ => { },
243+
});
244+
245+
driver.App.AddGlobalCommand(new Command
246+
{
247+
Id = "cmd.build",
248+
LabelMarkup = "Build",
249+
Presentation = CommandPresentation.CommandPalette,
250+
Execute = _ => { },
251+
});
252+
253+
palette.Show();
254+
driver.Tick();
255+
256+
palette.QueryText = "build";
257+
driver.Tick();
258+
259+
var searchBox = GetPrivateField<TextBox>(palette, "_searchBox");
260+
Assert.AreEqual("build", palette.QueryText);
261+
Assert.AreEqual("build", searchBox.Text, "Expected programmatic QueryText updates to synchronize the search box content.");
262+
263+
var outText = driver.Backend.GetOutText();
264+
var screen = new AnsiTestScreen(60, 12);
265+
screen.Apply(outText);
266+
var rendered = screen.GetText();
267+
268+
StringAssert.Contains(rendered, "Build");
269+
Assert.IsFalse(rendered.Contains("Open", StringComparison.Ordinal), "Expected programmatic QueryText updates to re-filter the palette immediately.");
270+
}
271+
115272
[TestMethod]
116273
public void CommandPalette_Down_From_Search_Advances_To_Second_Item()
117274
{

0 commit comments

Comments
 (0)