Skip to content

Improved option management, add hook management#516

Merged
tony merged 119 commits into
masterfrom
improved-options
Nov 30, 2025
Merged

Improved option management, add hook management#516
tony merged 119 commits into
masterfrom
improved-options

Conversation

@tony

@tony tony commented Feb 7, 2024

Copy link
Copy Markdown
Member

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

codecov Bot commented Feb 7, 2024

Copy link
Copy Markdown

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 30 commits November 30, 2025 13:17
why: Document new OptionsMixin and HooksMixin features for 0.50.x release.
what:
- Add breaking changes for renamed Window option methods
- Document OptionsMixin with set_option, show_option, show_options, unset_option
- Document HooksMixin with set_hook, show_hook, unset_hook, set_hooks
- Document new Window.set_option() arguments
why: Deprecated method should guide users to public API, not private
what:
- Update docstring to reference show_option() instead of _show_option()
- Change method call from _show_option() to show_option()
- Aligns with show_window_options() which correctly uses show_options()
why: The g parameter was accepted but silently ignored, breaking
backward compatibility for callers using the legacy API
what:
- Forward g parameter to _show_option() via global_ or g
- Ensures show_option("foo", g=True) works as expected
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant