Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ Access the keyboard interface with `/jb` or `/jobbinds`:
- `!` = Alt
- `+` = Shift

### Global Bindings

JobBinds now supports **Global Bindings** - keybinds that are active across all jobs and override job-specific bindings.

- **File:** `Ashita/scripts/JobBinds.txt`
- **Visual Indicator:** Global bindings appear in **blue** on the keyboard interface
- **Priority:** Global bindings always override job-specific bindings
- **Restriction:** Keys with global bindings **cannot** have job-specific bindings (including modifiers)
- Example: If `M` has a global binding, you cannot create a job-specific binding for `M`, `Shift+M`, `Ctrl+M`, or `Alt+M`
- However, you **can** create additional **global** bindings on that key (e.g., `Shift+M` as global)
- **Usage:**
1. Click a key in the interface
2. Check the **Global** checkbox
3. Configure the binding
4. Click **Save** - the binding will be saved to `JobBinds.txt`
- **Auto-Load:** Global bindings are automatically loaded after job-specific bindings when you change jobs, ensuring they take precedence

**Example Use Cases:**
- Map key (`/bind M /map`) - useful on all jobs
- Menu toggles - consistent across all characters
- Common macros - shared functionality

### Blocked Keys

Essential game keys are protected from being bound:
Expand Down
83 changes: 59 additions & 24 deletions jobbinds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,23 @@ addon.desc = 'Automatically loads keybind profile scripts based on current
addon.link = 'https://github.com/seekey13/jobbinds';

require('common');
local chat = require('chat')
local keyboard_ui = require('keyboard_ui');
local blocked_keybinds = require('blocked_keybinds');
local log = require('lib.log');
local keyboard_ui = require('lib.keyboard_ui');
local blocked_keybinds = require('lib.blocked_keybinds');

log.set_addon_name(addon.name)
local printf = log.printf
local errorf = log.errorf
local debugf = log.debugf

-- Use the blocked_keybinds module for consistency
local KEY_BLACKLIST = blocked_keybinds.blocked;

-- Custom print functions for categorized output.
local function printf(fmt, ...) print(chat.header(addon.name) .. chat.message(fmt:format(...))) end
local function warnf(fmt, ...) print(chat.header(addon.name) .. chat.warning(fmt:format(...))) end
local function errorf(fmt, ...) print(chat.header(addon.name) .. chat.error (fmt:format(...))) end
local function debugf(fmt, ...)
if debug_mode then
print(chat.header(addon.name) .. chat.message('[DEBUG] ' .. fmt:format(...)))
end
end

-- Holds the last loaded job/subjob profile info
local last_job = nil
local last_subjob = nil
local last_profile_keys = {}

-- Debug mode flag (off by default)
local debug_mode = false

-- Helper: Get current job and subjob
local function get_current_jobs()
local ok, party = pcall(function() return AshitaCore:GetMemoryManager():GetParty() end)
Expand Down Expand Up @@ -133,27 +125,69 @@ local function update_keyboard_ui(profile_filename, profile_path)
keyboard_ui.load_profile(profile_path)
end

-- Helper: Create an empty profile file for a job combination
local function create_empty_profile(jobid, subjobid, profile_path)
local job = get_job_shortname(jobid)
local subjob = get_job_shortname(subjobid)
local profile_filename = get_profile_filename(jobid, subjobid)

debugf('Creating empty profile: %s', profile_path)

local file = io.open(profile_path, "w")
if not file then
errorf('Failed to create profile file: %s', profile_path)
return false
end

-- Write header comment
file:write(string.format('# JobBinds Profile: %s/%s\n', job, subjob))
file:write('\n')

file:close()
printf('Created empty profile: %s', profile_filename)
debugf('Empty profile created successfully at: %s', profile_path)
return true
end

-- Load new profile via /exec
local function load_profile(jobid, subjobid)
local profile_path = get_profile_path(jobid, subjobid)
local profile_filename = get_profile_filename(jobid, subjobid)
debugf('Attempting to load profile: %s', profile_path)

-- Ensure file exists
-- Ensure job profile file exists, create empty one if it doesn't
local file = io.open(profile_path, "r")
if not file then
errorf('Profile %s not found.', profile_path)
printf('Profile %s not found, creating empty profile.', profile_filename)
debugf('Profile file does not exist at: %s', profile_path)
return false
if not create_empty_profile(jobid, subjobid, profile_path) then
return false
end
else
file:close()
end
file:close()
debugf('Profile file exists, executing: %s', profile_filename)

-- Load job-specific profile first
local ok = pcall(function()
AshitaCore:GetChatManager():QueueCommand(-1, string.format('/exec %s', profile_filename))
end)
if ok then
printf('Loaded jobbinds profile: %s', profile_filename)

-- Then load global bindings (JobBinds.txt) to override job-specific ones
local global_path = string.format('%s/scripts/JobBinds.txt', AshitaCore:GetInstallPath())
local global_file = io.open(global_path, "r")
if global_file then
global_file:close()
debugf('Loading global bindings from JobBinds.txt (overriding job-specific)')
pcall(function()
AshitaCore:GetChatManager():QueueCommand(-1, '/exec JobBinds.txt')
Comment on lines +178 to +185
end)
else
debugf('No global bindings file found at: %s', global_path)
end

-- Update the keyboard UI with the current profile
Comment on lines +178 to 191

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Track global keys in the unload set too.

load_profile() now applies JobBinds.txt, but last_profile_keys is still rebuilt from the job-specific file only. When a global bind is removed from JobBinds.txt, the next job switch never unbinds that old key before reloading, so stale global binds can stick around indefinitely. Include the global file’s keys in the tracked unload list, or keep a separate last_global_keys and unload both.

Also applies to: 201-206

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobbinds.lua` around lines 178 - 191, The profile loader (load_profile)
currently builds last_profile_keys only from the job-specific file, so global
bindings loaded via '/exec JobBinds.txt' are not tracked and thus never unbound;
update load_profile to also parse the global bindings (the same
global_path/JobBinds.txt used in the pcall) and include those keys in the set
used to populate last_profile_keys, or alternatively add a separate
last_global_keys and ensure the unload routine removes keys from both
last_profile_keys and last_global_keys; make sure the code that queues '/exec
JobBinds.txt' and the logic that constructs last_profile_keys (and the unload
sequence) reference the same key-identification logic so global binds get
tracked and removed when changed.

update_keyboard_ui(profile_filename, profile_path)
debugf('Successfully loaded and updated UI with profile: %s', profile_filename)
Expand Down Expand Up @@ -241,10 +275,11 @@ ashita.events.register('command', 'jobbinds_command', function(e)
printf('Opening JobBinds keyboard interface.')
elseif #args == 2 and args[2]:lower() == 'debug' then
-- Toggle debug mode
debug_mode = not debug_mode
keyboard_ui.set_debug_mode(debug_mode) -- Update keyboard UI debug mode
printf('Debug mode %s.', debug_mode and 'enabled' or 'disabled')
if debug_mode then
local new_state = not log.is_debug()
log.set_debug(new_state)
keyboard_ui.set_debug_mode(new_state) -- Update keyboard UI debug mode
printf('Debug mode %s.', new_state and 'enabled' or 'disabled')
if new_state then
debugf('Debug information will now be displayed.')
debugf('Current state: last_job=%s, last_subjob=%s',
get_safe_job_name(last_job),
Expand Down
84 changes: 20 additions & 64 deletions blocked_keybinds.lua → lib/blocked_keybinds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ M.blocked = {
['PAGEUP'] = true,
['PAGEDOWN'] = true,
['INSERT'] = true,
['HOME'] = true,
['END'] = true,
}

-- Keys that can be used alone or with Shift, but not with Ctrl or Alt
Expand All @@ -89,92 +87,50 @@ M.blocked_with_modifiers = {
['9'] = true,
}

-- Consolidated list of all blocked keys for easy checking
M.all_blocked = {}

-- Function to populate the consolidated list
local function populate_all_blocked()
for key, _ in pairs(M.blocked) do
M.all_blocked[key:upper()] = true
end
end

-- Function to check if a key is blocked
-- Function to check if a key is fully blocked (any modifier combination).
function M.is_key_blocked(key)
if not key then return false end
return M.all_blocked[key:upper()] == true
return M.blocked[key:upper()] == true
end

-- Function to check if a key combination is blocked
function M.is_combination_blocked(key, modifiers)
-- Function to check if a key combination is blocked.
-- Returns: blocked (bool), reason (string|nil)
function M.is_combination_blocked(key, modifiers_string)
if not key then return false end

local base_key = key:upper()
-- Check if base key is completely blocked

-- Fully blocked base key
if M.is_key_blocked(base_key) then
return true, string.format("Key '%s' is protected and cannot be rebound", base_key)
end

-- Check for restricted modifier combinations
if M.blocked_with_modifiers[base_key] and modifiers then
local mod_string = modifiers:upper()

-- Block Ctrl combinations for restricted keys

local mod_string = modifiers_string and modifiers_string:upper() or ''

-- Restricted modifier combinations for certain keys
if M.blocked_with_modifiers[base_key] then
if mod_string:find('CTRL') then
return true, string.format("'%s' cannot be used with Ctrl modifier", base_key)
return true, string.format("'%s' cannot be used with Ctrl modifier (use alone or with Shift)", base_key)
end

-- Block Alt combinations for restricted keys
if mod_string:find('ALT') then
return true, string.format("'%s' cannot be used with Alt modifier", base_key)
return true, string.format("'%s' cannot be used with Alt modifier (use alone or with Shift)", base_key)
end
end

-- Special cases for certain combinations that should always be blocked
if modifiers then
local mod_string = modifiers:upper()

-- Block Alt+F4 (close application)

-- Special always-blocked combinations
if mod_string ~= '' then
if base_key == 'F4' and mod_string:find('ALT') then
return true, "Alt+F4 is protected (closes application)"
end

-- Block Ctrl+Alt+Delete equivalent combinations
if mod_string:find('CTRL') and mod_string:find('ALT') and base_key == 'DELETE' then
if base_key == 'DELETE' and mod_string:find('CTRL') and mod_string:find('ALT') then
return true, "Ctrl+Alt+Delete combination is protected"
end

-- Block Windows key combinations
if mod_string:find('WIN') then
return true, "Windows key combinations are protected"
end
end

return false, nil
end

-- Function to get a user-friendly error message for blocked keys
function M.get_block_reason(key, modifiers)
if not key then return nil end

local base_key = key:upper()

-- Check for restricted modifier combinations first
if M.blocked_with_modifiers[base_key] and modifiers then
local mod_string = modifiers:upper()
if mod_string:find('CTRL') then
return string.format("'%s' cannot be used with Ctrl modifier (use alone or with Shift)", base_key)
elseif mod_string:find('ALT') then
return string.format("'%s' cannot be used with Alt modifier (use alone or with Shift)", base_key)
end
end

-- Default message for completely blocked keys
return string.format("Key '%s' is protected and cannot be rebound", base_key)
return false, nil
end

-- Initialize the consolidated list
populate_all_blocked()

return M
Loading