Handlers

Route handlers, router initialization, and virtual collection wiring for the file browser.

FileBrowserRouters

Return type for init_router — contains both the browser and virtual collection routers.


FileBrowserRouters


def FileBrowserRouters(
    browser:APIRouter, collection:APIRouter, urls:VirtualCollectionUrls, render:Callable,
    render_selection_oobs:Callable=_no_selection_oobs, update_selection_oobs:Callable=_no_update_selection_oobs,
    current_path:Callable=_no_current_path, sync_items:Callable=_no_sync_items, kb_manager:Optional=None
)->None:

Return value from init_router — both routers, URL bundle, render, and OOB helpers.

Selection Handlers

Selection Toggle Handler

Targeted OOB Helpers

Router Initialization

The init_router function creates both the browser-specific router and the virtual collection router, managing internal VC state and the items list as closure state.


init_router


def init_router(
    config:FileBrowserConfig, # Browser configuration
    provider:FileSystemProvider, # File system provider
    state_getter:Callable, # Function to get current state
    state_setter:Callable, # Function to save state
    route_prefix:str='/browser', # Route prefix for browser routes
    vc_route_prefix:str='', # Route prefix for VC routes (auto: {route_prefix}/vc)
    callbacks:Optional=None, # Optional callbacks
    home_path:Optional=None, # Home directory (defaults to provider)
    manager_label:Optional=None, # Human-readable label for the file browser's ZoneManager (used as the modal section header when wired into a hierarchical hints display via FileBrowserRouters.kb_manager)
)->FileBrowserRouters: # Browser + collection routers and URL bundle

Initialize file browser and virtual collection routers.

import tempfile
from pathlib import Path
from fasthtml.common import to_xml
from cjm_fasthtml_file_browser.providers.local import LocalFileSystemProvider

# Create test fixtures
with tempfile.TemporaryDirectory() as tmpdir:
    (Path(tmpdir) / "file1.txt").write_text("content1")
    (Path(tmpdir) / "file2.py").write_text("print('hi')")
    (Path(tmpdir) / "subdir").mkdir()

    config = FileBrowserConfig()
    provider = LocalFileSystemProvider()

    _state = BrowserState(current_path=tmpdir)
    def get_state(): return _state
    def set_state(s):
        global _state
        _state = s

    result = init_router(
        config=config,
        provider=provider,
        state_getter=get_state,
        state_setter=set_state,
        route_prefix="/browser",
    )

    # Verify return type
    assert isinstance(result, FileBrowserRouters)
    assert result.browser.prefix == "/browser"
    assert result.collection is not None
    assert result.urls.nav_up != ""
    assert result.urls.nav_down != ""
    assert result.urls.focus_row != ""
    assert result.urls.activate != ""
    assert result.urls.sort != ""

    # Verify render callable produces HTML
    assert callable(result.render)
    html = to_xml(result.render())
    assert "file-browser" in html
    assert "file1.txt" in html or "subdir" in html

    # Verify render_selection_oobs callable exists
    assert callable(result.render_selection_oobs)

print("Router initialization tests passed!")
Router initialization tests passed!
# L7: FileBrowserRouters.kb_manager exposure + manager_label plumbing
# The kb_manager field carries the ZoneManager that backs the file browser's
# keyboard system, so consumers wiring this as a child of a hierarchical
# hints modal can pass it to render_keyboard_hints_modal(..., child_managers=[...]).

with tempfile.TemporaryDirectory() as tmpdir:
    config = FileBrowserConfig(vc_prefix="kbmgr")
    provider = LocalFileSystemProvider()
    _state = BrowserState(current_path=tmpdir)
    def get_state(): return _state
    def set_state(s):
        global _state
        _state = s

    # --- Default: kb_manager populated, label is None (falls back to system_id) ---
    routers_default = init_router(
        config=config, provider=provider,
        state_getter=get_state, state_setter=set_state,
        route_prefix="/browser",
    )
    assert routers_default.kb_manager is not None, \
        "FileBrowserRouters.kb_manager must be populated by init_router"
    assert isinstance(routers_default.kb_manager, ZoneManager)
    assert routers_default.kb_manager.label is None, \
        "Default manager_label is None — get_display_label falls back to system_id"

    # --- With manager_label: ZoneManager carries it and get_display_label returns it ---
    routers_labeled = init_router(
        config=FileBrowserConfig(vc_prefix="kbmgr2"),
        provider=provider,
        state_getter=get_state, state_setter=set_state,
        route_prefix="/browser2",
        manager_label="Local Files",
    )
    assert routers_labeled.kb_manager.label == "Local Files"
    assert routers_labeled.kb_manager.get_display_label() == "Local Files", \
        "When manager_label is set, get_display_label must return the label, not the system_id"

    # --- Single source of truth: the same kb_system is rendered AND surfaced.
    # If init_router accidentally built two managers (one for runtime, one for
    # the exposed handoff), the rendered HTML's keyboard script would reference
    # a different system_id than the exposed manager. We assert identity-of-systemid
    # to lock in single-source-of-truth.
    rendered_html = to_xml(routers_labeled.render())
    # The exposed kb_manager's system_id must appear in the rendered keyboard script
    assert f'"systemId": "{routers_labeled.kb_manager.system_id}"' in rendered_html, \
        "The rendered keyboard system must reference the same system_id as FileBrowserRouters.kb_manager " \
        "— prevents drift between the runtime manager and the manager exposed to consumers"

    print("L7 kb_manager + manager_label tests passed")
# Test render_selection_oobs — targeted checkbox OOB updates
with tempfile.TemporaryDirectory() as tmpdir:
    for i in range(5):
        (Path(tmpdir) / f"file{i}.txt").write_text(f"content{i}")

    config = FileBrowserConfig(selection_mode=SelectionMode.MULTIPLE, vc_prefix="test")
    provider = LocalFileSystemProvider()
    _state = BrowserState(current_path=tmpdir)
    def get_state(): return _state
    def set_state(s):
        global _state
        _state = s

    routers = init_router(
        config=config, provider=provider,
        state_getter=get_state, state_setter=set_state,
        route_prefix="/browser",
    )

    # Select file0 and file2 in browser state
    _state.selection.selected_paths = [str(Path(tmpdir) / "file0.txt"), str(Path(tmpdir) / "file2.txt")]

    # Get OOBs for paths in current directory
    oobs = routers.render_selection_oobs([str(Path(tmpdir) / "file0.txt"), str(Path(tmpdir) / "file2.txt")])
    assert len(oobs) > 0, "Should return OOB elements for visible items"
    for oob in oobs:
        oob_html = to_xml(oob)
        assert 'hx-swap-oob="outerHTML"' in oob_html
        assert 'col-select' in oob_html

    # Paths not in current directory return empty
    oobs_empty = routers.render_selection_oobs(["/nonexistent/path.txt"])
    assert oobs_empty == (), f"Expected empty tuple, got {len(oobs_empty)} elements"

    print("render_selection_oobs tests passed!")
# Test update_selection_oobs — sync + render in one call
with tempfile.TemporaryDirectory() as tmpdir:
    for i in range(5):
        (Path(tmpdir) / f"file{i}.txt").write_text(f"content{i}")

    config = FileBrowserConfig(selection_mode=SelectionMode.MULTIPLE, vc_prefix="upd")
    provider = LocalFileSystemProvider()
    _state = BrowserState(current_path=tmpdir)
    def get_state(): return _state
    def set_state(s):
        global _state
        _state = s

    routers = init_router(
        config=config, provider=provider,
        state_getter=get_state, state_setter=set_state,
        route_prefix="/browser",
    )

    assert callable(routers.update_selection_oobs)

    # Initially no selection
    assert _state.selection.selected_paths == []

    # update_selection_oobs syncs selection AND returns OOBs
    file0 = str(Path(tmpdir) / "file0.txt")
    file2 = str(Path(tmpdir) / "file2.txt")
    oobs = routers.update_selection_oobs([file0, file2], [file0, file2])

    # Browser state should now have the selection synced
    assert file0 in _state.selection.selected_paths
    assert file2 in _state.selection.selected_paths

    # OOBs should be returned for visible items
    assert len(oobs) > 0
    for oob in oobs:
        oob_html = to_xml(oob)
        assert 'hx-swap-oob="outerHTML"' in oob_html
        assert 'col-select' in oob_html

    # Deselect file0 — pass new full selection, changed path is file0
    oobs2 = routers.update_selection_oobs([file2], [file0])
    assert file0 not in _state.selection.selected_paths
    assert file2 in _state.selection.selected_paths

    print("update_selection_oobs tests passed!")
# Test current_path — returns browsed directory path
with tempfile.TemporaryDirectory() as tmpdir:
    config = FileBrowserConfig()
    provider = LocalFileSystemProvider()
    _state = BrowserState(current_path=tmpdir)
    def get_state(): return _state
    def set_state(s):
        global _state
        _state = s

    routers = init_router(
        config=config, provider=provider,
        state_getter=get_state, state_setter=set_state,
        route_prefix="/browser",
    )

    assert callable(routers.current_path)
    assert routers.current_path() == tmpdir

    # Simulate navigation by updating state
    _state.current_path = "/some/other/path"
    assert routers.current_path() == "/some/other/path"

    print("current_path tests passed!")