Skip to content

Conversation

@tony
Copy link
Member

@tony tony commented Feb 7, 2024

OptionsMixin, HooksMixin, and SparseArray

Extracted from #513

Warning

APIs below are subject to change (both params, return types, and structures)

Summary

This PR introduces a comprehensive refactor of tmux option and hook management
through new mixin classes (OptionsMixin, HooksMixin) and a SparseArray
data structure for handling tmux's indexed arrays.

Key additions:

  • OptionsMixin - Unified option management across Server, Session, Window,
    and Pane
  • HooksMixin - Hook management with bulk operations API
  • SparseArray - Data structure for tmux's sparse indexed arrays
    (e.g., command-alias[0], command-alias[99])

Changes

New internal: SparseArray

A dict-based data structure that preserves sparse indices while maintaining
list-like behavior:

>>> from libtmux._internal.sparse_array import SparseArray
>>> arr: SparseArray[str] = SparseArray()
>>> arr.add(0, "first")
>>> arr.add(99, "ninety-ninth")
>>> arr[0]
'first'
>>> arr[99]
'ninety-ninth'
>>> list(arr.iter_values())
['first', 'ninety-ninth']

This is essential for handling tmux options like command-alias[0],
command-alias[5], terminal-features[0], etc.

New internal: OptionsMixin

Unified option management across all tmux objects (Server, Session, Window,
Pane).

High-level API:

  • show_options() - Returns structured data with SparseArray preservation

    >>> server.show_options()
    {'command-alias': {'split-pane': 'split-window', ...}}
  • show_option(option) - Returns a single option value

    >>> server.show_option('command-alias')
    {'split-pane': 'split-window', 'splitp': 'split-window', ...}
    
    >>> server.show_option('buffer-limit')
    50
  • set_option(option, value, **kwargs) - Set an option with full flag support

  • unset_option(option) - Unset/remove an option

Low-level API:

  • _show_options() - Map of options split by key with raw values
  • _show_options_raw() - Raw stdout from tmux show-options
  • _show_option() - Single option raw value
  • _show_option_raw() - Raw stdout from tmux show-option [option]

Backward compatibility:

  • The legacy g parameter is still accepted but deprecated in favor of global_
  • Using g will emit a DeprecationWarning

New internal: HooksMixin

Hook management for tmux 3.0+ with full support for indexed hooks.

Basic operations:

  • set_hook(hook, value) - Set a hook
  • show_hook(hook) - Get current hook value (returns SparseArray for indexed
    hooks)
  • show_hooks() - Get all hooks
  • unset_hook(hook) - Remove a hook
  • run_hook(hook) - Run a hook immediately (useful for testing)

Bulk operations:

  • set_hooks(hook, values) - Set multiple hooks at once

    >>> session.set_hooks('session-renamed', {
    ...     0: 'display-message "hook 0"',
    ...     1: 'display-message "hook 1"',
    ...     5: 'run-shell "echo hook 5"',
    ... })

Working with indexed hooks:

>>> session.set_hook('session-renamed[0]', 'display-message "test"')
>>> session.set_hook('session-renamed[5]', 'display-message "test2"')

# show_hook returns SparseArray for indexed hooks
>>> hooks = session.show_hook('session-renamed')
>>> isinstance(hooks, SparseArray)
True
>>> sorted(hooks.keys())
[0, 5]
>>> hooks[0]
'display-message "test"'

New features

set_option() params

Param Flag Description
_format -F Expand format strings
unset -u Unset the option
global_ -g Set as global option
unset_panes -U Also unset in other panes
prevent_overwrite -o Don't overwrite if exists
suppress_warnings -q Suppress warnings
append -a Append to existing value

show_option() / show_options() params

  • scope - Specify option scope (Server, Session, Window, Pane)
  • global_ - Show global options
  • include_inherited - Include inherited options (with * suffix in tmux)
  • include_hooks - Include hooks in output

Breaking changes

Deprecations

  • Window.set_window_option() deprecated in favor of Window.set_option()
  • Window.show_window_option() deprecated in favor of Window.show_option()
  • Window.show_window_options() deprecated in favor of Window.show_options()
  • g parameter deprecated in favor of global_ (emits DeprecationWarning)

New constants

  • OptionScope enum: Server, Session, Window, Pane
  • OPTION_SCOPE_FLAG_MAP - Maps scope to tmux flags (-s, -w, -p)
  • HOOK_SCOPE_FLAG_MAP - Maps scope to hook flags

tmux Version Compatibility

Feature Minimum tmux
All options/hooks features 3.2+
Window/Pane hooks (-w, -p) 3.2+
client-active, window-resized hooks 3.3+
pane-title-changed hook 3.5+

Testing

  • Added comprehensive test grids for options (tests/test_options.py)
  • Added comprehensive test grids for hooks (tests/test_hooks.py)
  • Tests cover all option scopes, types (int, bool, str, style, choice), and
    tmux versions
  • Added test for g parameter deprecation warning
  • Added tests for SparseArray utility class

Files Changed

New Files

File Description
src/libtmux/options.py OptionsMixin class (1,256 lines)
src/libtmux/hooks.py HooksMixin class (525 lines)
src/libtmux/_internal/sparse_array.py SparseArray data structure
src/libtmux/_internal/constants.py Options/Hooks dataclasses
tests/test_options.py Comprehensive option tests (1,496 lines)
tests/test_hooks.py Comprehensive hook tests (1,119 lines)
tests/test/test_sparse_array.py SparseArray tests
docs/api/options.md Options API documentation
docs/api/hooks.md Hooks API documentation
docs/internals/sparse_array.md SparseArray documentation
docs/internals/constants.md Internal constants documentation

Modified Files

File Changes
src/libtmux/constants.py Added OptionScope, scope flag maps
src/libtmux/common.py Added CmdMixin, CmdProtocol
src/libtmux/server.py Uses OptionsMixin, HooksMixin
src/libtmux/session.py Uses OptionsMixin, HooksMixin
src/libtmux/window.py Uses OptionsMixin, HooksMixin, deprecated methods
src/libtmux/pane.py Uses OptionsMixin, HooksMixin
CHANGES Release notes for 0.50.0
tests/test_window.py Updated for new API
tests/test_session.py Updated for new API
tests/legacy_api/test_window.py Legacy API tests
tests/legacy_api/test_session.py Legacy API tests

@tony tony force-pushed the improved-options branch from 6cd43f0 to 3399c7d Compare February 7, 2024 16:28
@codecov
Copy link

codecov bot commented Feb 7, 2024

Codecov Report

❌ Patch coverage is 39.73799% with 414 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.99%. Comparing base (309c27f) to head (d24226e).
⚠️ Report is 120 commits behind head on master.

Files with missing lines Patch % Lines
src/libtmux/_internal/constants.py 5.53% 237 Missing and 2 partials ⚠️
src/libtmux/options.py 67.65% 65 Missing and 22 partials ⚠️
src/libtmux/hooks.py 50.39% 41 Missing and 22 partials ⚠️
src/libtmux/_internal/sparse_array.py 53.84% 6 Missing ⚠️
src/libtmux/window.py 50.00% 6 Missing ⚠️
src/libtmux/server.py 0.00% 5 Missing ⚠️
src/libtmux/pane.py 0.00% 3 Missing ⚠️
src/libtmux/session.py 0.00% 3 Missing ⚠️
src/libtmux/common.py 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #516      +/-   ##
==========================================
- Coverage   46.77%   43.99%   -2.79%     
==========================================
  Files          18       22       +4     
  Lines        1708     2305     +597     
  Branches      277      362      +85     
==========================================
+ Hits          799     1014     +215     
- Misses        799     1145     +346     
- Partials      110      146      +36     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony mentioned this pull request Feb 7, 2024
@tony tony force-pushed the improved-options branch 4 times, most recently from e5c6186 to 37898a4 Compare February 8, 2024 12:35
@tony tony mentioned this pull request Feb 8, 2024
@tony tony force-pushed the improved-options branch 22 times, most recently from 97d74c1 to 36c5907 Compare February 8, 2024 18:54
tony added 27 commits November 30, 2025 13:17
why: The g parameter should emit DeprecationWarning like set_option() does
what:
- Add deprecation warning when g parameter is used
- Follow same pattern as set_option() (lines 669-671)
- Ensures backward compatibility while guiding users to global_
why: Deprecation warnings should use proper DeprecationWarning category
to be properly filtered by Python's warning system
what:
- Update all 4 instances of g deprecation warning to use category=DeprecationWarning
- Affects set_option, _show_options_raw, _show_option_raw, show_option
why: Verify that show_option() emits DeprecationWarning when deprecated g
parameter is used
what:
- Add test_show_option_g_parameter_emits_deprecation_warning test
- Ensures backward compatibility warning is properly raised
why: The global_ parameter was accepted but not forwarded, preventing
users from querying server-wide hooks with -g flag
what:
- Forward global_=global_ when calling _show_hook()
why: CHANGES documented bulk hook APIs that don't exist in HooksMixin
what:
- Remove get_hook_indices, get_hook_values, append_hook, clear_hook
- These methods were documented but never implemented
- Keep only the implemented methods: set_hook, show_hook, show_hooks,
  unset_hook, run_hook, set_hooks
why: Server.set_hook() was broken - HOOK_SCOPE_FLAG_MAP[Server] was ""
which added an empty string argument to tmux commands. Server/global
hooks require -g flag per tmux documentation.

what:
- Change OptionScope.Server from "" to "-g" in HOOK_SCOPE_FLAG_MAP
- Fixes Server.set_hook(), Server.show_hooks(), Server.run_hook()
why: Control-mode hooks like %output, %window-add have % prefix that
Hooks.from_stdout() strips when creating attributes, but show_hook()
didn't strip it before lookup, causing all %-prefixed hooks to return
None.

what:
- Add lstrip("%") before replace("-", "_") in show_hook() attribute lookup
why: The g parameter was silently ignored in set_hook(), breaking
backward compatibility. Code calling set_hook(..., g=True) would not
get global behavior.

what:
- Add DeprecationWarning when g parameter is used
- Forward g value to global_ for backward compatibility
- Matches pattern used in OptionsMixin.set_option()
why: Hooks set globally (with global_=True) could not be run via
run_hook() because it had no way to pass -g flag to tmux.

what:
- Add global_: bool | None = None parameter to run_hook()
- Add -g flag handling when global_ is True
why: The SparseArray-specific branch at lines 248-251 was unreachable
dead code. Since SparseArray inherits from dict, isinstance(value, dict)
returns True and the dict branch handles it correctly first.

what:
- Remove unreachable SparseArray branch that contained buggy logic
- Add comment explaining dict branch handles SparseArray too
why: tmux set-hook does not accept -F flag (only set-option does).
     Verified against ~/study/c/tmux/cmd-set-option.c:65 which shows
     set-hook accepts "agpRt:uw" only.
what:
- Remove _format parameter from set_hook() signature
- Remove _format flag handling code
why: tmux set-hook does not accept -o flag (only set-option does).
     Verified against ~/study/c/tmux/cmd-set-option.c:65 which shows
     set-hook accepts "agpRt:uw" only.
what:
- Remove prevent_overwrite parameter from set_hook() signature
- Remove prevent_overwrite flag handling code
why: tmux set-hook does not accept -q flag (only set-option does).
     Verified against ~/study/c/tmux/cmd-set-option.c:65 which shows
     set-hook accepts "agpRt:uw" only.
what:
- Remove ignore_errors parameter from set_hook() signature
- Remove ignore_errors flag handling code
why: tmux set-hook (used with -u for unset) does not accept -q flag.
     Verified against ~/study/c/tmux/cmd-set-option.c:65 which shows
     set-hook accepts "agpRt:uw" only.
what:
- Remove ignore_errors parameter from unset_hook() signature
- Remove ignore_errors flag handling code
why: tmux show-hooks does not accept -q flag.
     Verified against ~/study/c/tmux/cmd-show-options.c:67 which shows
     show-hooks accepts "gpt:w" only.
what:
- Remove ignore_errors parameter from show_hooks() signature
- Remove ignore_errors flag handling code
- Remove ignore_errors from docstring parameters
…rameter

why: tmux show-hooks does not accept -q flag.
     Verified against ~/study/c/tmux/cmd-show-options.c:67 which shows
     show-hooks accepts "gpt:w" only.
what:
- Remove ignore_errors parameter from _show_hook() signature
- Remove ignore_errors parameter from show_hook() signature
- Remove ignore_errors flag handling code
- Remove ignore_errors from _show_hook() call in show_hook()
why: Verify show_option correctly handles bracketed array indices like
'status-format[0]'. Currently returns None instead of the value.
what:
- Add ShowOptionIndexedTestCase NamedTuple with test_id pattern
- Add parametrized test_show_option_indexed_array test
- Test verifies indexed query returns value, base name returns SparseArray
- Test follows TDD RED phase (currently failing as expected)
why: Querying 'status-format[0]' returned None because explode_arrays()
transforms the key to 'status-format', losing the original indexed key.
what:
- Parse raw output first before exploding arrays
- Direct lookup for indexed queries (key with brackets found in raw dict)
- For base name queries, continue with explode_arrays transformation
- Avoids duplicating regex parsing logic already in explode_arrays()
…test

why: The test `test_deprecated_window_methods_emit_warning[show_window_option_global]`
triggered two warnings: the expected one (Window method deprecated) and an unexpected
secondary one (g argument deprecated). The secondary warning leaked to pytest output.
what:
- Add @pytest.mark.filterwarnings to ignore "g argument is deprecated" warning
- Test still validates the primary deprecation warning via pytest.warns()
why: Per tmux.1, terminal-overrides entries are colon-separated strings where
the first part is a terminal pattern and remaining parts are individual
features. The previous code used `split(":", maxsplit=1)` which collapsed all
features after the pattern into a single key.
what:
- Split on all colons, not just the first
- Iterate over each feature part individually
- Add parametrized tests for multi-feature entries
why: When calling show_hook("session-renamed[0]"), the code attempted to find
an attribute named "session_renamed[0]" on the Hooks dataclass, which doesn't
exist. Per tmux.1, hooks are array options that can be queried by index.
what:
- Extract index from bracketed suffix before attribute lookup
- Return specific indexed value from SparseArray when present
- Add parametrized tests for indexed hook lookups
why: tmux appends "*" to option names that are inherited from parent scopes
(e.g., "visual-activity*" when the value comes from global scope). The
lookup code was checking for the exact option name, missing inherited values.
what:
- Check for both exact key and key with "*" suffix in raw output lookup
- Check for inherited marker in exploded output lookup as well
- Fix test to properly capture inherited value before set/unset cycle
why: When tmux outputs inherited array options with -A flag (e.g.,
"status-format[0]*"), the asterisk was being stripped during explosion.
This caused inconsistency: scalar inherited options preserved the "*"
marker but array options did not.
what:
- Update regex to capture trailing "*" in new `inherited` group
- Append "*" to base key when inherited marker is present
- Ensures inherited array options like "status-format[0]*" produce
  "status-format*" keys, consistent with scalar inherited options
…tests

why: Ensure explode_arrays correctly preserves the "*" marker for inherited
array options (e.g., "status-format[0]*" → "status-format*"), consistent
with scalar inherited options.
what:
- Add ExplodeArraysInheritedCase NamedTuple for parametrized tests
- Add 3 test cases: inherited arrays, non-inherited arrays, mixed indices
- Import explode_arrays function for direct unit testing
why: Users upgrading to 0.50.0 need clear guidance on deprecated methods
and the new unified options/hooks API.

what:
- Document new unified options API (show_options, show_option, set_option,
  unset_option) available on all tmux objects
- Document new hooks API (set_hook, show_hook, show_hooks, unset_hook)
- Add deprecation notes for Window.set_window_option(),
  Window.show_window_option(), Window.show_window_options()
- Add deprecation notes for `g` parameter in favor of `global_`
- Include before/after code examples for each migration
why: Users need a conceptual guide explaining the unified options/hooks
API, when to use different methods, and how to work with indexed hooks.

what:
- Create docs/topics/options_and_hooks.md with comprehensive guide
- Cover getting/setting options with show_options(), show_option(),
  set_option(), unset_option()
- Cover hooks API with set_hook(), show_hook(), show_hooks(), unset_hook()
- Document indexed hooks and SparseArray return type
- Include bulk hook operations with set_hooks()
- Add tmux version compatibility table
- Add options_and_hooks to topics/index.md toctree
- All doctests pass via pytest --doctest-glob
why: Users following the quickstart guide need to see basic options
usage alongside other common operations like creating windows and panes.

what:
- Add "Working with options" section before "Final notes"
- Include examples for show_option(), show_options(), set_option(),
  unset_option()
- Add seealso reference to the detailed options-and-hooks topic guide
- All doctests pass via pytest --doctest-glob
@tony tony merged commit 7ec766b into master Nov 30, 2025
13 of 14 checks passed
@tony tony deleted the improved-options branch November 30, 2025 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants