Download this file and place it within your LuaUI/Widgets folder:
https://github.com/FrequentPilgrim/FrequentPilgrim-Files/blob/main/gui_letter_auto_groups.luaGreat News!
I was able to put together a fully functional widget that has all of the features I was trying to implement.
This is working much better than the previous attempt in that it's now trivially easy to adjust your groupings exactly how you want. The groups get saved to your LUAui configuration files and persist until you decide to clear the group. There is no limitation on how many groups a unit can be added to. It's a good idea to save your configurations as backup before diving into this widget, just in case.
It feels great to combine groupings now with the shift key: combining your minotaurs and cyclops groups with the press of a button; Quickly pairing your Grizzly and Scallops to a lobster within a control group without lassoing with a mouse.
The filtering is also great. If you've ever been frustrated about trying to pull your artillery out of a unit blob, ctrl + space + T after I've selected the blob now filters my selection to artillery.
Having a factory selected no longer preventes spacebar + letter from selecting units.

I have not been able to figure out how to implement controls for this widget into the menu system, despite spending many hours attempting to get it working. I would appreciate any assistance on menu integration.
Instead, the widget uses the debug text box that appears by default when you press (f8). This will inform you of all of the notifications happening with your groupings.

The controls also exists as part of the widget description, so if you forget in game you can simply hover over your widget in the alt+f11 window to get refreshed.

My recommendation is to set up all of the groups you want first in a singleplayer match against Inactive AI. Use either cheats or income multipliers to quickly produce 1 of every factory and 1 of every unit. Then save this match so that you can quickly make adjustments to your groupings by loading it.
I've been using the previous widget to make to make this grouping process streamlined by clicking my premade group buttons in the menu system and then assigning them to a letter. My groupings and letters are mostly unchanged from my most recent posting about groupings. You'll want to clear all of the spacebar selections being used for the Global Selection Hotkeys widget. I'm still using global selections for air units, but these can also be assigned to 'Spacebar + Letter' groupings as you desire.
If you're not sure how to group things, I feel like this is a really solid start that you can tweak as desired instead of having to make everything from scratch:















Just for reference these are my Air Hotkeys that Im still using the older widget to control.

Click this spoiler to see the categories I'm currently using and how they are saved within Zero-K\LuaUI\Config ZK_data.lua
[Spoiler]
["Letter UnitType Multi Auto Groups"] = {
A = {
[1] = "hoverassault",
[2] = "cloakassault",
[3] = "jumpassault",
[4] = "spiderassault",
[5] = "tankassault",
[6] = "shieldassault",
[7] = "vehassault",
},
B = {
[1] = "tankaa",
[2] = "shieldbomb",
[3] = "hoverassault",
[4] = "vehaa",
[5] = "hoverdepthcharge",
[6] = "jumpscout",
[7] = "hoverraid",
[8] = "jumpbomb",
[9] = "shieldscout",
[10] = "amphtele",
[11] = "spideremp",
[12] = "shieldassault",
[13] = "cloakbomb",
[14] = "spiderscout",
[15] = "jumpaa",
[16] = "shieldraid",
[17] = "tankheavyassault",
[18] = "vehscout",
[19] = "tankarty",
[20] = "amphfloater",
[21] = "cloakheavyraid",
[22] = "shieldshield",
[23] = "shieldriot",
[24] = "veharty",
[25] = "shieldarty",
[26] = "cloakaa",
[27] = "vehassault",
[28] = "spiderriot",
[29] = "jumpsumo",
[30] = "hoverarty",
[31] = "shieldfelon",
[32] = "jumpblackhole",
[33] = "tankheavyarty",
[34] = "tankheavyraid",
[35] = "vehsupport",
[36] = "jumpassault",
[37] = "amphbomb",
[38] = "shieldaa",
[39] = "spiderassault",
[40] = "vehriot",
[41] = "amphlaunch",
[42] = "spideraa",
[43] = "jumpskirm",
[44] = "vehcapture",
[45] = "cloakraid",
[46] = "amphraid",
[47] = "tankassault",
[48] = "cloaksnipe",
[49] = "shieldskirm",
[50] = "spiderskirm",
[51] = "spidercrabe",
[52] = "amphassault",
[53] = "cloakarty",
[54] = "hoverriot",
[55] = "jumparty",
[56] = "cloakjammer",
[57] = "amphsupport",
[58] = "cloakskirm",
[59] = "striderarty",
[60] = "hoverheavyraid",
[61] = "jumpraid",
[62] = "amphriot",
[63] = "amphaa",
[64] = "cloakassault",
[65] = "vehheavyarty",
[66] = "spiderantiheavy",
[67] = "tankraid",
[68] = "vehraid",
[69] = "cloakriot",
[70] = "amphimpulse",
[71] = "tankriot",
[72] = "hoverskirm",
[73] = "hoveraa",
},
C = {
[1] = "vehsupport",
[2] = "amphsupport",
},
D = {
[1] = "tankaa",
[2] = "jumpaa",
[3] = "spideraa",
[4] = "hoveraa",
[5] = "shieldaa",
[6] = "amphaa",
[7] = "cloakaa",
[8] = "vehaa",
},
E = {
[1] = "cloakskirm",
[2] = "amphfloater",
[3] = "spiderskirm",
[4] = "jumpskirm",
[5] = "hoverskirm",
[6] = "shieldskirm",
},
F = {
[1] = "tankheavyassault",
[2] = "amphassault",
[3] = "jumpsumo",
[4] = "cloaksnipe",
[5] = "shieldfelon",
[6] = "spidercrabe",
},
G = {
[1] = "spiderantiheavy",
[2] = "hoverdepthcharge",
[3] = "amphbomb",
[4] = "shieldbomb",
[5] = "jumpbomb",
[6] = "cloakbomb",
},
H = {
[1] = "amphtele",
[2] = "shieldshield",
[3] = "cloakjammer",
},
M = {
[1] = "athena",
[2] = "shipcon",
[3] = "shieldcon",
[4] = "jumpcon",
[5] = "gunshipcon",
[6] = "planecon",
[7] = "hovercon",
[8] = "amphcon",
[9] = "tankcon",
[10] = "cloakcon",
[11] = "vehcon",
[12] = "spidercon",
},
N = {
[1] = "shiptorpraider",
[2] = "shipassault",
[3] = "shipriot",
[4] = "shipheavyarty",
[5] = "shipaa",
[6] = "shipcarrier",
[7] = "shiparty",
[8] = "subraider",
[9] = "subtacmissile",
[10] = "shipscout",
[11] = "shipskirm",
},
Q = {
[1] = "spideremp",
[2] = "hoverheavyraid",
[3] = "tankheavyraid",
[4] = "amphimpulse",
},
R = {
[1] = "cloakriot",
[2] = "tankriot",
[3] = "vehriot",
[4] = "spiderriot",
[5] = "amphriot",
[6] = "hoverriot",
[7] = "jumpblackhole",
[8] = "shieldriot",
},
S = {
[1] = "jumpscout",
[2] = "vehscout",
[3] = "spiderscout",
[4] = "shieldscout",
},
T = {
[1] = "veharty",
[2] = "cloakarty",
[3] = "hoverarty",
[4] = "jumparty",
[5] = "shieldarty",
[6] = "tankarty",
},
V = {
[1] = "bomberriot",
[2] = "planeheavyfighter",
[3] = "gunshipheavyskirm",
[4] = "dronecarry",
[5] = "gunshipemp",
[6] = "planelightscout",
[7] = "bomberprec",
[8] = "gunshipskirm",
[9] = "planescout",
[10] = "bomberstrike",
[11] = "bomberheavy",
[12] = "bomberassault",
[13] = "gunshipraid",
[14] = "gunshipassault",
[15] = "gunshipkrow",
[16] = "gunshipbomb",
[17] = "gunshipaa",
[18] = "bomberdisarm",
[19] = "planefighter",
},
W = {
[1] = "tankraid",
[2] = "shieldraid",
[3] = "jumpraid",
[4] = "amphraid",
[5] = "hoverraid",
[6] = "cloakraid",
[7] = "vehraid",
},
X = {
[1] = "cloakheavyraid",
[2] = "amphlaunch",
[3] = "vehcapture",
},
Y = {
[1] = "striderarty",
[2] = "vehheavyarty",
[3] = "tankheavyarty",
},
Z = {
[1] = "striderbantha",
[2] = "striderdetriment",
[3] = "striderdante",
[4] = "striderscorpion",
[5] = "striderantiheavy",
},
},
Full Widget Code:
[Spoiler] function widget:GetInfo()
return {
name = "Letter UnitType Multi Auto Groups",
desc = "Assign multiple unit types to letter groups, select/add/filter units with modifiers, persistent across matches. Open Debug Panel to see results (f8). alt + Space + Letter - assign units. space + letter - select units. backspace + space + letter - clear group. shift + space + letter - add group to selection. Ctrl + space + letter - filter from current selection instead of globally.",
author = "FrequentPilgrim",
date = "2025-06-01",
license = "GNU GPL v2",
layer = 0,
enabled = true
}
end
local assignedGroups
{} -- letter -> set of unitDefNames { [unitDefName]
true, ... }
local keycodeToLetter = {}
local backspaceKey = Spring.GetKeyCode("backspace")
local backspaceDown = false
-- Fill keycodeToLetter for letters A-Z
for i = 65, 90 do -- ASCII A-Z
local ch = string.char(i)
keycodeToLetter[Spring.GetKeyCode(ch:lower())] = ch
end
local function ShowMessage(msg)
Spring.Echo("[LetterUnitTypeGroups] " .. msg)
end
local function AssignLetterGroup(letter)
local selUnits = Spring.GetSelectedUnits()
if #selUnits == 0 then
ShowMessage("No units selected to assign group " .. letter)
return
end
assignedGroups[letter] = assignedGroups[letter] or {}
local addedCount = 0
for _, unitID in ipairs(selUnits) do
local udid = Spring.GetUnitDefID(unitID)
if udid then
local defName = UnitDefs[udid].name
if defName and not assignedGroups[letter][defName] then
assignedGroups[letter][defName] = true
addedCount = addedCount + 1
end
end
end
if addedCount > 0 then
ShowMessage("Added " .. addedCount .. " unit types to group " .. letter)
else
ShowMessage("No new unit types added to group " .. letter)
end
end
local function ClearLetterGroup(letter)
if assignedGroups[letter] then
assignedGroups[letter] = nil
ShowMessage("Cleared group " .. letter)
else
ShowMessage("Group " .. letter .. " is not assigned")
end
end
local function GetUnitsInGroup(letter)
local defNameSet = assignedGroups[letter]
if not defNameSet then return {} end
local myTeam = Spring.GetMyTeamID()
local units = Spring.GetTeamUnits(myTeam)
local toSelect = {}
for _, unitID in ipairs(units) do
local udid = Spring.GetUnitDefID(unitID)
if udid then
local defName = UnitDefs[udid].name
if defNameSet[defName] then
table.insert(toSelect, unitID)
end
end
end
return toSelect
end
local function SelectLetterGroup(letter)
local toSelect = GetUnitsInGroup(letter)
if #toSelect == 0 then
ShowMessage("No alive units of group " .. letter)
return
end
Spring.SelectUnitArray(toSelect, false)
ShowMessage("Selected " .. #toSelect .. " units from group " .. letter)
end
local function AddToSelection(letter)
local toSelect = GetUnitsInGroup(letter)
if #toSelect == 0 then
ShowMessage("No units to add from group " .. letter)
return
end
Spring.SelectUnitArray(toSelect, true)
ShowMessage("Added " .. #toSelect .. " units from group " .. letter)
end
local function FilterSelection(letter)
local current = Spring.GetSelectedUnits()
if #current == 0 then return end
local defNameSet = assignedGroups[letter]
if not defNameSet then
ShowMessage("Group " .. letter .. " not assigned")
return
end
local filtered = {}
for _, unitID in ipairs(current) do
local udid = Spring.GetUnitDefID(unitID)
if udid then
local defName = UnitDefs[udid].name
if defNameSet[defName] then
table.insert(filtered, unitID)
end
end
end
Spring.SelectUnitArray(filtered, false)
ShowMessage("Filtered selection to " .. #filtered .. " units in group " .. letter)
end
function widget:KeyPress(key, mods, isRepeat)
if key == backspaceKey then
backspaceDown = true
return false
end
local letter = keycodeToLetter[key]
if not letter or not mods.meta then return false end
if backspaceDown then
ClearLetterGroup(letter)
return true
elseif mods.alt then
AssignLetterGroup(letter)
return true
elseif mods.shift then
AddToSelection(letter)
return true
elseif mods.ctrl then
FilterSelection(letter)
return true
else
SelectLetterGroup(letter)
return true
end
end
function widget:KeyRelease(key)
if key == backspaceKey then
backspaceDown = false
end
end
function widget:GetConfigData()
local saved = {}
for letter, defSet in pairs(assignedGroups) do
saved[letter] = {}
for defName in pairs(defSet) do
table.insert(saved[letter], defName)
end
end
return saved
end
function widget:SetConfigData(data)
if type(data) == "table" then
assignedGroups = {}
for letter, defList in pairs(data) do
assignedGroups[letter] = {}
for _, defName in ipairs(defList) do
assignedGroups[letter][defName] = true
end
end
Spring.Echo("[LetterUnitTypeGroups] Loaded saved groups")
end
end