How to make a Rust command menu plugin (CUI button grid)
Adds a /menu chat command that opens a CUI grid of configurable buttons; each button closes the panel and runs its configured chat command for that player. Uses CUI plus chat commands and console commands for button clicks.
Admins often want a single entry point for the server's utility commands: kits, teleports, shops, info pages. Typing long commands with arguments is error-prone, especially for new players. A menu button runs the command for the player who clicked it, so you do not have to teach anyone the syntax.
This plugin reads a list of buttons from the config file (label, command, optional permission). /menu builds a CUI panel with one button per entry, laid out in a grid. Clicking a button destroys the panel and sends the configured chat command through the player's own console, so the target plugin handles it exactly as if the player typed it. Buttons with a permission requirement are hidden from players who lack it, and the click console command re-checks the permission so a crafted CUI command cannot bypass it. The panel is destroyed on unload for every active player.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Command Menu", "RustPluginGenerator", "1.0.0")]
[Description("Opens a configurable CUI grid of buttons that run chat commands for the clicking player")]
public class CommandMenu : RustPlugin
{
private const string PermUse = "commandmenu.use";
private const string PermAdmin = "commandmenu.admin";
private const string PanelId = "commandmenu.panel";
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Menu title")]
public string Title = "Server Menu";
[JsonProperty("Buttons per row")]
public int ButtonsPerRow = 3;
[JsonProperty("Button width")]
public float ButtonWidth = 0.18f;
[JsonProperty("Button height")]
public float ButtonHeight = 0.07f;
[JsonProperty("Buttons")]
public List<MenuButton> Buttons = new List<MenuButton>
{
new MenuButton { Label = "Kits", Command = "kit", Permission = "" },
new MenuButton { Label = "Shop", Command = "shop", Permission = "" },
new MenuButton { Label = "Info", Command = "info", Permission = "" },
new MenuButton { Label = "Close", Command = "commandmenu.close", Permission = "" }
};
}
private class MenuButton
{
[JsonProperty("Label")]
public string Label = "Button";
[JsonProperty("Command")]
public string Command = "";
[JsonProperty("Permission")]
public string Permission = "";
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You do not have permission to use this menu.",
["Reloaded"] = "Menu configuration reloaded."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
=> string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermAdmin, this);
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player != null && player.IsConnected)
CuiHelper.DestroyUi(player, PanelId);
}
}
[ChatCommand("menu")]
private void CmdMenu(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
OpenMenu(player);
}
[ChatCommand("menu.reload")]
private void CmdReload(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
LoadConfig();
SendReply(player, Msg("Reloaded", player.UserIDString));
}
[ConsoleCommand("commandmenu.select")]
private void CcmdSelect(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse)) return;
int index = arg.GetInt(0, -1);
var visible = GetVisibleButtons(player);
if (index < 0 || index >= visible.Count) return;
var button = visible[index];
if (button == null || string.IsNullOrEmpty(button.Command)) return;
if (!string.IsNullOrEmpty(button.Permission) && !permission.UserHasPermission(player.UserIDString, button.Permission)) return;
CuiHelper.DestroyUi(player, PanelId);
player.SendConsoleCommand(button.Command);
}
[ConsoleCommand("commandmenu.close")]
private void CcmdClose(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse)) return;
CuiHelper.DestroyUi(player, PanelId);
}
private List<MenuButton> GetVisibleButtons(BasePlayer player)
{
var result = new List<MenuButton>();
if (config?.Buttons == null) return result;
foreach (var button in config.Buttons)
{
if (button == null || string.IsNullOrEmpty(button.Label)) continue;
if (!string.IsNullOrEmpty(button.Permission) && !permission.UserHasPermission(player.UserIDString, button.Permission)) continue;
result.Add(button);
}
return result;
}
private void OpenMenu(BasePlayer player)
{
CuiHelper.DestroyUi(player, PanelId);
var buttons = GetVisibleButtons(player);
if (buttons.Count == 0) return;
int perRow = Mathf.Max(1, config.ButtonsPerRow);
int rows = Mathf.CeilToInt((float)buttons.Count / perRow);
float buttonW = Mathf.Clamp(config.ButtonWidth, 0.05f, 0.5f);
float buttonH = Mathf.Clamp(config.ButtonHeight, 0.03f, 0.2f);
float gap = 0.01f;
float gridW = perRow * buttonW + (perRow - 1) * gap;
float gridH = rows * buttonH + (rows - 1) * gap + 0.08f;
float minX = 0.5f - gridW / 2f;
float maxY = 0.5f + gridH / 2f;
var container = new CuiElementContainer();
container.Add(new CuiPanel
{
Image = { Color = "0.1 0.1 0.1 0.85" },
RectTransform = { AnchorMin = "0.2 0.2", AnchorMax = "0.8 0.8" },
CursorEnabled = true
}, "Overlay", PanelId);
container.Add(new CuiLabel
{
Text = { Text = config.Title, FontSize = 18, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" },
RectTransform = { AnchorMin = "0 0.88", AnchorMax = "1 1" }
}, PanelId);
for (int i = 0; i < buttons.Count; i++)
{
int row = i / perRow;
int col = i % perRow;
float x = minX + col * (buttonW + gap);
float y = maxY - 0.08f - (row + 1) * buttonH - row * gap;
container.Add(new CuiButton
{
Button = { Command = $"commandmenu.select {i}", Color = "0.25 0.45 0.25 1" },
RectTransform =
{
AnchorMin = $"{x} {y}",
AnchorMax = $"{x + buttonW} {y + buttonH}"
},
Text = { Text = buttons[i].Label, FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, PanelId);
}
CuiHelper.AddUi(player, container);
}
}
}Config file
Created automatically on first load at oxide/config/CommandMenu.json.
{
"Menu title": "Server Menu",
"Buttons per row": 3,
"Button width": 0.18,
"Button height": 0.07,
"Buttons": [
{
"Label": "Kits",
"Command": "kit",
"Permission": ""
},
{
"Label": "Shop",
"Command": "shop",
"Permission": ""
},
{
"Label": "Info",
"Command": "info",
"Permission": ""
},
{
"Label": "Close",
"Command": "commandmenu.close",
"Permission": ""
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/menu | Opens the command button grid | commandmenu.use |
/menu.reload | Reloads the config file and rebuilds the button list | commandmenu.admin |
commandmenu.select <index> | Internal button click handler: closes the panel and runs the button's command for the calling player | commandmenu.use |
commandmenu.close | Internal close handler: removes the panel from the calling player | commandmenu.use |
| Permission | Grants |
|---|---|
commandmenu.use | Allows opening the menu with /menu and using its buttons |
commandmenu.admin | Allows reloading the config with /menu.reload |
How the code works
Define the buttons in config
The nested
PluginConfigclass holds aTitlestring, aButtonslist ofMenuButtonentries and grid sizing values. EachMenuButtonhasLabel,CommandandPermission(empty string means no permission check). BecauseLoadConfigfalls back toLoadDefaultConfigon a bad file, admins always get a working menu after a broken edit./menu.reloadre-reads the file and is limited tocommandmenu.admin.Open the menu from /menu
The
[ChatCommand("menu")]handler checkscommandmenu.use, then destroys any leftover panel and builds a freshCuiElementContainer. It loops the config buttons, skips entries whosePermissionis set and not granted to the player, and adds aCuiButtonfor each visible entry. The container is drawn withCuiHelper.AddUi. Every click command embeds the button index, so the server can look up the label and command again instead of trusting client data.Handle the click and run the command
Each button's
Button.Commandis a console command such ascommandmenu.select 3. The[ConsoleCommand("commandmenu.select")]method resolves the player witharg.Player(), rejects calls from the server console, parses the index witharg.GetInt(0, -1), and validates it against the filtered button list. It then destroys the panel and callsplayer.SendConsoleCommand(command)so the target plugin receives the command as if the player typed it.Validate before executing
A CUI command can be re-sent by anything that can write to the player's console, so the handler never trusts the index blindly. It bounds-checks the index, re-checks the button's
Permissionagainst the calling player, and ignores emptyCommandvalues. Invalid input returns silently instead of printing warnings, because this path can be spammed.Close button and layout
The panel includes a
commandmenu.closeconsole command that destroys the panel withCuiHelper.DestroyUi; any config entry can point at it. Grid geometry comes fromButtonsPerRow,ButtonWidthandButtonHeightin the config:OpenMenucomputes columns and rows, anchors each cell by index, and centers the block. Changing the number of buttons only requires a config edit and reload.Clean up on unload and disconnect
UnloadloopsBasePlayer.activePlayerListand destroys the panel id for each player, so a reload never leaves a dead panel that blocks input. The click and close commands are registered as console commands, so they exist for every connected player without per-player registration. Config is written inSaveConfig, whichLoadConfigcalls after a fallback.
Install it on your server
Save CommandMenu.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/CommandMenu.json(Carbon:carbon/configs/CommandMenu.json). After editing, reload it:oxide.reload CommandMenu c.reload CommandMenu # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default commandmenu.use c.grant group default commandmenu.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Show commands in the chat log
Adds an optional config flag that prints the command to the player's chat before running it, for transparency.
Generate this versionGroup buttons into pages
Splits the button list into pages with next/previous buttons when the list is long.
Generate this versionLock buttons behind permissions per rank
Shows locked buttons greyed out instead of hiding them.
Generate this version
FAQ
Does the target plugin see the command as coming from the player?
Yes. The handler calls player.SendConsoleCommand(command), so Oxide chat and console command handlers attached to that player run with the same user id as a typed command. The command string can include arguments, for example kit vip, and it is sent to the game console, not the chat input.
Why are some buttons missing for certain players?
Each button has a Permission field. If it is set and the player lacks it, the button is not added to the container and its index is not accepted by the click handler. Leave the field empty to show the button to everyone who can open the menu.
Can I add a button for a command from another plugin?
Yes. Add a new object to Buttons with the label and the exact command line. After saving, run /menu.reload or reload the plugin. The command does not have to be registered yet; it is only sent when clicked.
How do I install it on Oxide or Carbon?
Drop the compiled or .cs plugin file into oxide/plugins, wait for compilation, then edit oxide/config/CommandMenu.json to set your buttons and reload the plugin. Carbon loads the same file. Grant commandmenu.use to your default group and commandmenu.admin to admins.
Why does the panel stay after the player dies?
It should not: the click handler destroys the panel before running the command. If a panel is stuck, reloading the plugin runs Unload and removes it from every active player. Sleeping players get no panel because CUI is only sent to connected clients.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More ui plugins
- Admin menuBuilds a CUI admin panel that opens with /am and renders one button per action defined in the config, each button running a console command throughโฆ
- Announcement bannerAdds an /announce chat command that draws a CUI banner at the top of every connected player's screen for a configurable duration, plus timedโฆOnServerInitializedOnPlayerConnected
- Kill feedDraws a CUI kill feed on the right side of every player's screen showing the last 5 player kills with killer, weapon and victim, using OnEntityDeath.OnServerInitializedOnEntityDeathOnPlayerSleepEnded
Last updated Oct 26, 2026.
Customize command shortcut menu
Customize