ワンクリックで
ImGui is an immediate mode UI library. KSA uses ImGui for UI
npx skills add https://github.com/meow-sci/ksa-mod-experiments --skill imguiこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストール
ImGui is an immediate mode UI library. KSA uses ImGui for UI
npx skills add https://github.com/meow-sci/ksa-mod-experiments --skill imguiこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストール
| name | imgui |
| description | ImGui is an immediate mode UI library. KSA uses ImGui for UI |
KSA (Kitten Space Agency) uses the ImGui UI library for its user interface.
ImGui itself is a C/C++ library, but KSA uses a C# wrapper around it which is custom to the game exposed via an internal framework called Brutal.
The complete ImGui API is exposed and is accessible by a using declaration in csharp code:
using Brutal.ImGuiApi;
This makes ImGui available using a ImGui class with static functions for ImGui API calls. For example:
ImGui.Begin("My Window");
if (ImGui.Button("Click me!")) {
Console.WriteLine("clicked!");
}
VERY IMPORTANT! ALWAYS OBEY THIS!
All the BRUTAL ImGui display strings take ImString types.
A C# string can be used but is NOT preferable
For static strings use the utf-8 string syntax like "some string"u8 for better performance to avoid UTF-16 to UTF-8 conversions.
Interpolated strings like $"val: {val}" can be used but should be assigned to an ImString variable first to avoid unnecessary conversions and allocations. For example:
float val = 123.45f;
ImString valStr = $"val: {val}"u8; // assign to ImString variable first to avoid multiple conversions
ImGui.Text(valStr);
The entire ImGui API should be exposed via this Brutal C# wrapper, so use your knowledge of the official ImGui
For the imgui c++ demo app examples you can find the table of contents at ./examples-toc.md which links to the individual example files in ./examples/. These examples can be used as a reference for how to use ImGui to achieve various UI features and patterns.
IMPORTANT NOTE - these are the original C++ examples. The examples must be adapted to the KSA Brutal C# ImGui wrapper which is 1:1 but using C# syntax and conventions instead of C++.
These are some examples using Brutal ImGui API calls to demonstrate common ImGui features.
float4 currentColor = GetGForceColor(recorder.Latest.Magnitude);
ImGui.TextColored(currentColor, $"Current: {recorder.Latest.Magnitude:F2} g");
ImGui.SameLine(0, 20);
ImGui.TextColored(ColorRed, $"Peak: {recorder.PeakG:F2} g");
ImGui.SameLine(0, 20);
ImGui.Text($"Avg: {recorder.AvgG:F2} g");
ImGui.Separator();
ImGui.SeparatorText("One-liner variants");
ImGui.Indent();
ImGui.Text("abc");
ImGui.Unindent();
ImGui.Text("Color widget with Float Display:");
float4 color = new float4(1.0f, 0.5f, 0.2f, 1.0f);
ImGui.ColorEdit4("MyColor##2f", ref color, ImGuiColorEditFlags.Float);
ImGui.Text("Color button with Picker:");
// With the ImGuiColorEditFlags.NoInputs flag you can hide all the slider/text inputs
// With the ImGuiColorEditFlags.NoLabel flag you can pass a non-empty label which will only be used for the tooltip and picker popup
float4 color = new float4(0.2f, 0.8f, 0.4f, 1.0f);
ImGui.ColorEdit4("MyColor##3", ref color, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel);
if (ImGui.CollapsingHeader("thing", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.Text("content");
}
if (ImGui.IsKeyPressed(ImGuiKey.F11))
{
_windowVisible = !_windowVisible;
}
There are also 2/3/4 value slider widget variants (see later)
Prefer ImGui.DragFloat over ImGui.SliderFloat since it allows for both dragging and manual input
// Speed slider arguments are: (label, ref to value, min, max)
if (ImGui.DragFloat("Speed (m/s)", ref _actualValue, 1.0f, 250.0f))
{
// Value updated
}
string[] easingNames = { "Linear", "Ease In", "Ease Out", "Ease In-Out" };
// Combo box arguments are: (label, ref to selected index, array of options, number of options)
if (ImGui.Combo("Easing##ZoomOut", ref _selectedValue, easingNames, easingNames.Length))
{
// Value updated
}
The preferred pattern uses ImInputString as a class-level field for the filter input, InputTextWithHint with a utf-8 hint string, and string.Contains with OrdinalIgnoreCase for filtering.
// Class-level field — survives across frames while the combo is open
private readonly ImInputString _itemFilter = new(128);
// In your render method:
string[] items = { "Item 1", "Item 2", "Item 3" };
int selectedItemIndex = 0;
string previewValue = selectedItemIndex >= 0 ? items[selectedItemIndex] : "Select...";
if (ImGui.BeginCombo("##combo", previewValue))
{
// Auto-focus the filter box and clear it each time the combo opens
if (ImGui.IsWindowAppearing())
{
ImGui.SetKeyboardFocusHere();
_itemFilter.Clear();
}
ImGui.SetNextItemWidth(-1f);
ImGui.InputTextWithHint("##combo_filter", "filter..."u8, _itemFilter);
string filterText = _itemFilter.ToString().Trim();
for (int i = 0; i < items.Length; i++)
{
if (filterText.Length > 0 && !items[i].Contains(filterText, StringComparison.OrdinalIgnoreCase))
continue;
bool sel = selectedItemIndex == i;
if (ImGui.Selectable(items[i], sel))
selectedItemIndex = i;
if (sel) ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
ImGui.Spacing();
progress = Math.Clamp(progress, 0.0f, 1.0f);
ImGui.ProgressBar(progress, new float2(-1, 0));
These are multi-input sliders for float2/3/4 and int2/3/4. The arguments are (label, ref to value array, min, max) for the drag and slider variants, and (label, ref to value array) for the input variant. The drag variant allows both dragging and manual input, while the slider variant only allows dragging.
static float vec4f[4] = { 0.10f, 0.20f, 0.30f, 0.44f };
static int vec4i[4] = { 1, 5, 100, 255 };
ImGui.SeparatorText("2-wide");
// manual input
ImGui.InputFloat2("input float2", vec4f);
// drag OR manual input
ImGui.DragFloat2("drag float2", vec4f, 0.01f, 0.0f, 1.0f);
// drag only
ImGui.SliderFloat2("slider float2", vec4f, 0.0f, 1.0f);
// manual input
ImGui.InputInt2("input int2", vec4i);
// drag OR manual input
ImGui.DragInt2("drag int2", vec4i, 1, 0, 255);
// drag only
ImGui.SliderInt2("slider int2", vec4i, 0, 255);
ImGui.SeparatorText("3-wide");
// manual input
ImGui.InputFloat3("input float3", vec4f);
// drag OR manual input
ImGui.DragFloat3("drag float3", vec4f, 0.01f, 0.0f, 1.0f);
// drag only
ImGui.SliderFloat3("slider float3", vec4f, 0.0f, 1.0f);
// manual input
ImGui.InputInt3("input int3", vec4i);
// drag OR manual input
ImGui.DragInt3("drag int3", vec4i, 1, 0, 255);
// drag only
ImGui.SliderInt3("slider int3", vec4i, 0, 255);
ImGui.SeparatorText("4-wide");
// manual input
ImGui.InputFloat4("input float4", vec4f);
// drag OR manual input
ImGui.DragFloat4("drag float4", vec4f, 0.01f, 0.0f, 1.0f);
// drag only
ImGui.SliderFloat4("slider float4", vec4f, 0.0f, 1.0f);
// manual input
ImGui.InputInt4("input int4", vec4i);
// drag OR manual input
ImGui.DragInt4("drag int4", vec4i, 1, 0, 255);
// drag only
ImGui.SliderInt4("slider int4", vec4i, 0, 255);
When a mod has InputText or other text-entry widgets, typing in them will also trigger game hotkeys (e.g. \ toggles the in-game console, Enter submits console commands). To prevent this, use a scoped focus trap pattern.
Critical rules:
ImGui.GetIO().WantTextInput globally — it is true whenever any ImGui text input has focus, including the game's own in-game console. Blocking hotkeys based on this flag will break the console (can't close it, can't press Enter).ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) — this scopes the check to only your mod's windows.Begin/End block — after ImGui.End() there is no valid window context and IsWindowFocused won't return correct results.Pattern — render method returns focus state:
// In the Mod class, expose a static flag for the Harmony patch to read
internal static bool ModHasFocusedTextInput;
[StarMapAfterGui]
public void OnAfterUi(double dt)
{
bool anyTextInput = false;
if (_windowVisible)
anyTextInput |= RenderMyWindow();
ModHasFocusedTextInput = anyTextInput;
}
// Each render method checks focus INSIDE the Begin/End block and returns it
private bool RenderMyWindow()
{
bool hasFocusedText = false;
ImGui.SetNextWindowSize(new float2(400, 300), ImGuiCond.FirstUseEver);
if (ImGui.Begin("My Window", ref _windowVisible))
{
// Check INSIDE Begin/End — this is where the window context is valid
hasFocusedText = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)
&& ImGui.GetIO().WantTextInput;
ImGui.InputText("##name", _nameInput);
// ... other widgets ...
}
ImGui.End();
return hasFocusedText;
}
Harmony patch — block game hotkeys only for this mod:
[HarmonyPatch(typeof(GameSettings), nameof(GameSettings.OnKeyAll))]
static class PatchGameSettingsOnKeyAll
{
static bool Prefix(ref bool __result)
{
if (Mod.ModHasFocusedTextInput)
{
__result = true;
return false; // skip original — hotkey is consumed
}
return true; // run original
}
}
Why this works: GameSettings.OnKeyAll is the first handler in the game's input chain (Program.cs). If it returns true, all downstream handlers (including ConsoleWindow.OnKey) are short-circuited. By scoping the check to only your mod's windows, the in-game console and other input handlers remain functional.
details about the ksa game code and behavior
how to use fd cli tool
how to use ripgrep `rg` cli tool
preferred imgui layout patterns
how to use genhttp effectively for http server
details about the rpc mechanism in unladen swallow