Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
75f424b
Cleaning up some typos
WillJRoper Nov 12, 2025
f04982c
Initial templating of binding centralisation in class
WillJRoper Nov 12, 2025
3cc6779
Relocating tree and motion bindings to new class
WillJRoper Nov 12, 2025
951ad21
Modified developer block of the documentation on config
WillJRoper Nov 12, 2025
525a6d7
Relocated attribute expansion to tree functions
WillJRoper Nov 12, 2025
75d783e
Moved dataset mode over
WillJRoper Nov 12, 2025
9dea343
Migrated search into new bindings class
WillJRoper Nov 12, 2025
559a8ca
Relocating window mode bindings
WillJRoper Nov 12, 2025
c3a5708
Moved jump mode bindings
WillJRoper Nov 12, 2025
a2764d6
Migrated the init process to new workflow
WillJRoper Nov 12, 2025
c497ab6
Dynamically update the title on refresh
WillJRoper Nov 12, 2025
f96eb68
Migrated plotting modes to finish off the migration
WillJRoper Nov 12, 2025
692d28e
Fixing some function naming
WillJRoper Nov 12, 2025
89ff0b6
Fix binding refactor issues and update tests
WillJRoper Nov 12, 2025
aeadc96
Adding some extra badges
WillJRoper Nov 12, 2025
8e017bb
Achieve 100% test coverage with comprehensive tests
WillJRoper Nov 12, 2025
c1c8afc
Merge branch 'update-test-suite' into centralise-bindings
WillJRoper Nov 12, 2025
53a8fff
Add cross-platform CI testing for Windows and macOS
WillJRoper Nov 12, 2025
f970e9f
Merge branch 'update-test-suite' into centralise-bindings
WillJRoper Nov 12, 2025
958b5e0
Fix cross-platform test failures for Windows and macOS
WillJRoper Nov 12, 2025
b0f21d3
Merge branch 'update-test-suite' into centralise-bindings
WillJRoper Nov 12, 2025
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
18 changes: 16 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
Expand All @@ -21,11 +23,23 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install system dependencies
- name: Install system dependencies (Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libhdf5-dev pkg-config

- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install hdf5 pkg-config

- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
run: |
# HDF5 will be installed via pip wheel on Windows
echo "HDF5 dependencies handled by h5py wheel"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# h5forest

[![PyPI - Version](https://img.shields.io/pypi/v/h5forest.svg?logo=pypi)](https://pypi.org/project/h5forest/)
[![PyPI downloads](https://img.shields.io/pypi/dm/h5forest.svg)](https://pypi.org/project/h5forest/)
[![Python versions](https://img.shields.io/pypi/pyversions/h5forest.svg)](https://pypi.org/project/h5forest/)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/WillJRoper/h5forest/blob/main/CONTRIBUTING.md)
[![Test Suite](https://github.com/WillJRoper/h5forest/actions/workflows/test.yml/badge.svg)](https://github.com/WillJRoper/h5forest/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/WillJRoper/h5forest/branch/main/graph/badge.svg)](https://codecov.io/gh/WillJRoper/h5forest)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)

HDF5 Forest (`h5forest`) is a Text-based User Interface (TUI) for exploring HDF5 files.

HDF5 Forest (`h5forest`) is a Text-based User Interface (TUI) for exploring HDF5 files.

`h5ls` works, and `h5glance` is a great improvement, so "Why bother?" I hear you ask.
`h5ls` works, and `h5glance` is a great improvement, so "Why bother?" I hear you ask.

Well, `h5forest` brings interactivity and new functionality not available in its long-standing brethren. `h5forest` includes:

Expand All @@ -23,7 +26,7 @@ Well, `h5forest` brings interactivity and new functionality not available in its
- A real-time metadata and attribute display.
- Memory efficiency with lazy loading.
- Peeking inside Datasets.
- On-the-fly statistics.
- On-the-fly statistics.
- Fuzzy search with real-time filtering to quickly find datasets and groups.
- A fully terminal-based interface with optional vim-style navigation (configurable in `~/.h5forest/config.yaml`).

Expand Down Expand Up @@ -54,6 +57,3 @@ on the command line to get started exploring a file.
"Why has no one done this before? Let’s nominate him for a peerage." - Professor incapable of peerage nomination.

"Nice" - PhD supervisor.



20 changes: 1 addition & 19 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,22 +157,4 @@ These keys provide vim-style navigation throughout the application. If you try t

## Extending Keybindings (For Developers)

To integrate configuration-based keybindings in h5forest's code:

```python
def _init_app_bindings(app):
"""Set up keybindings using configuration."""

# Get the configured key for quit action
quit_key = app.config.get_keymap("normal_mode", "quit")

def exit_app(event):
"""Exit the app."""
event.app.exit()

# Bind using configured key (falls back to default if not set)
app.kb.add(
quit_key or "q",
filter=Condition(lambda: app.flag_normal_mode)
)(exit_app)
```
To integrate configuration-based keybindings in h5forest's code you can simply add them to the config, define the functions to bind to them and then add them to H5KeyBindings with a binding and a label:
197 changes: 196 additions & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,206 @@ PRs are reviewed for:

All PRs automatically run through CI which checks:

- Tests against Python 3.9, 3.10, 3.11, 3.12
- Tests against Python 3.9, 3.10, 3.11, 3.12, 3.13
- Cross-platform testing on Ubuntu, Windows, and macOS
- Code linting and formatting
- Test coverage reporting
- Documentation building (if applicable)

## Adding New Key Bindings

The key binding system in `h5forest` is centralized in the `H5KeyBindings` class located in `src/h5forest/bindings/bindings.py`. This section explains how to add new key bindings to the application.

### Architecture Overview

The binding system consists of:

- **`H5KeyBindings` class** (`src/h5forest/bindings/bindings.py`): Central class that manages all key bindings
- **Function modules** (`src/h5forest/bindings/*_funcs.py`): Contain the actual handler functions for each mode
- `normal_funcs.py` - Mode switching and core operations
- `tree_funcs.py` - Tree navigation operations
- `dataset_funcs.py` - Dataset inspection operations
- `hist_funcs.py` - Histogram mode operations
- `plot_funcs.py` - Scatter plot mode operations
- `jump_funcs.py` - Jump/goto operations
- `search_funcs.py` - Search operations
- `window_funcs.py` - Window focus management
- `utils.py` - Utility functions

### Steps to Add a New Binding

#### 1. Create the Handler Function

Add your handler function to the appropriate `*_funcs.py` file. All handlers should:

- Accept an `event` parameter
- Use the `@error_handler` decorator
- Get the app instance via the singleton: `app = H5Forest()`

**Example:**

```python
@error_handler
def my_new_function(event):
"""Brief description of what this function does."""
# Avoid circular imports
from h5forest.h5_forest import H5Forest

# Access the application instance
app = H5Forest()

# Your logic here
app.print("My new function executed!")
```

#### 2. Import the Handler in `bindings.py`

Add your function to the imports at the top of `src/h5forest/bindings/bindings.py`:

```python
from h5forest.bindings.your_funcs import (
my_new_function,
# ... other functions
)
```

#### 3. Add the Key Configuration

In the `H5KeyBindings.__init__()` method, add a key configuration attribute:

```python
self.my_new_key = self.config.get_keymap(
"your_mode", # e.g., "hist_mode", "plot_mode", "normal_mode"
"your_action", # e.g., "my_action"
)
```

#### 4. Create a Filter (if needed)

If your binding should only work in certain conditions, add a filter lambda in `__init__()`:

```python
self.filter_my_condition = lambda: app.some_condition
```

Common filters:
- `self.filter_normal_mode` - Only in normal mode
- `self.filter_hist_mode` - Only in histogram mode
- `self.filter_tree_focus` - Only when tree has focus
- `self.filter_not_normal_mode` - In any leader mode

#### 5. Create a Label

Add a label for the hotkey display:

```python
self.my_action_label = Label(
translate_key_label(self.my_new_key) + ": Description"
)
```

#### 6. Bind the Function

In the appropriate `_init_*_bindings()` method, bind your function:

```python
def _init_your_mode_bindings(self):
"""Initialize your mode keybindings."""
self.bind_function(
self.my_new_key,
my_new_function,
self.filter_your_mode, # Or lambda: True for always active
)
```

#### 7. Update Hotkey Display

In the `get_current_hotkeys()` method, add your label to the appropriate mode section:

```python
if self.filter_your_mode():
hotkeys.append(self.my_action_label)
```

#### 8. Add Configuration Defaults

Update `src/h5forest/data/default_config.yaml` to include the default key mapping:

```yaml
keymaps:
your_mode:
your_action: "k" # Or whatever key you want
```

#### 9. Write Tests

Add tests in `tests/unit/test_your_mode_bindings.py`:

```python
@patch('h5forest.h5_forest.H5Forest')
def test_my_new_function(self, mock_h5forest_class, mock_app, mock_event):
"""Test my new function."""
mock_h5forest_class.return_value = mock_app

_init_your_mode_bindings(mock_app)

# Find the binding
bindings = [b for b in mock_app.kb.bindings if b.keys == ("k",)]
handler = bindings[0].handler

# Call the handler
handler(mock_event)

# Assert expected behavior
mock_app.print.assert_called_once_with("My new function executed!")
```

### Example: Adding a "Copy Value" Binding to Histogram Mode

**1. Create handler in `hist_funcs.py`:**

```python
@error_handler
def copy_hist_value(event):
"""Copy the current histogram bin value to clipboard."""
from h5forest.h5_forest import H5Forest
app = H5Forest()

# Get current bin value logic here
value = app.histogram_plotter.get_current_bin_value()
# Copy to clipboard logic
subprocess.run(["xclip", "-selection", "clipboard"],
input=str(value).encode())
app.print(f"Copied value: {value}")
```

**2. Import in `bindings.py`:**

```python
from h5forest.bindings.hist_funcs import (
...,
copy_hist_value,
)
```

**3-7. Add configuration, filter, label, binding, and hotkey display** (as shown above)

**8. Update `default_config.yaml`:**

```yaml
keymaps:
hist_mode:
copy_value: "c"
```

**9. Write tests** (as shown above)

### Testing Your Binding

1. Run unit tests: `pytest tests/unit/test_your_mode_bindings.py`
2. Test manually: Start `h5forest` and try your new key binding
3. Check pre-commit hooks: `pre-commit run --all-files`

## Getting Help

### Communication Channels
Expand Down
9 changes: 1 addition & 8 deletions src/h5forest/bindings/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
from h5forest.bindings.bindings import _init_app_bindings
from h5forest.bindings.dataset_bindings import _init_dataset_bindings
from h5forest.bindings.hist_bindings import _init_hist_bindings
from h5forest.bindings.jump_bindings import _init_goto_bindings
from h5forest.bindings.plot_bindings import _init_plot_bindings
from h5forest.bindings.search_bindings import _init_search_bindings
from h5forest.bindings.tree_bindings import _init_tree_bindings
from h5forest.bindings.window_bindings import _init_window_bindings
from h5forest.bindings.bindings import H5KeyBindings
Loading
Loading