Skip to content
RustPluginGenerator

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

CommandMenu.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/menuOpens the command button gridcommandmenu.use
/menu.reloadReloads the config file and rebuilds the button listcommandmenu.admin
commandmenu.select <index>Internal button click handler: closes the panel and runs the button's command for the calling playercommandmenu.use
commandmenu.closeInternal close handler: removes the panel from the calling playercommandmenu.use
Permissions
PermissionGrants
commandmenu.useAllows opening the menu with /menu and using its buttons
commandmenu.adminAllows reloading the config with /menu.reload

How the code works

  1. Define the buttons in config

    The nested PluginConfig class holds a Title string, a Buttons list of MenuButton entries and grid sizing values. Each MenuButton has Label, Command and Permission (empty string means no permission check). Because LoadConfig falls back to LoadDefaultConfig on a bad file, admins always get a working menu after a broken edit. /menu.reload re-reads the file and is limited to commandmenu.admin.

  2. Open the menu from /menu

    The [ChatCommand("menu")] handler checks commandmenu.use, then destroys any leftover panel and builds a fresh CuiElementContainer. It loops the config buttons, skips entries whose Permission is set and not granted to the player, and adds a CuiButton for each visible entry. The container is drawn with CuiHelper.AddUi. Every click command embeds the button index, so the server can look up the label and command again instead of trusting client data.

  3. Handle the click and run the command

    Each button's Button.Command is a console command such as commandmenu.select 3. The [ConsoleCommand("commandmenu.select")] method resolves the player with arg.Player(), rejects calls from the server console, parses the index with arg.GetInt(0, -1), and validates it against the filtered button list. It then destroys the panel and calls player.SendConsoleCommand(command) so the target plugin receives the command as if the player typed it.

  4. 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 Permission against the calling player, and ignores empty Command values. Invalid input returns silently instead of printing warnings, because this path can be spammed.

  5. Close button and layout

    The panel includes a commandmenu.close console command that destroys the panel with CuiHelper.DestroyUi; any config entry can point at it. Grid geometry comes from ButtonsPerRow, ButtonWidth and ButtonHeight in the config: OpenMenu computes columns and rows, anchors each cell by index, and centers the block. Changing the number of buttons only requires a config edit and reload.

  6. Clean up on unload and disconnect

    Unload loops BasePlayer.activePlayerList and 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 in SaveConfig, which LoadConfig calls after a fallback.

Install it on your server

  1. Save CommandMenu.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. 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.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/CommandMenu.json (Carbon: carbon/configs/CommandMenu.json). After editing, reload it:oxide.reload CommandMenu c.reload CommandMenu # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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 version
  • Group buttons into pages

    Splits the button list into pages with next/previous buttons when the list is long.

    Generate this version
  • Lock 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.

Prompt: Create a CommandMenu Oxide plugin for Rust: /menu opens a CUI grid of config buttons (label, command, permission), clicking closes the panel and runs the command for the player, with commandmenu.use and commandmenu.admin permissions.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize command shortcut menu

Customize