cjm-fasthtml-token-selector

A token-level interactive text selector for FastHTML with gap, word, and span selection modes, custom key repeat navigation, and HTMX state sync.

Install

pip install cjm_fasthtml_token_selector

Project Structure

nbs/
├── components/ (2)
│   ├── inputs.ipynb  # Hidden input rendering for HTMX state sync.
│   └── tokens.ipynb  # Token grid rendering for all three selection modes (gap, word, span).
├── core/ (4)
│   ├── config.ipynb     # Configuration dataclass for token selector initialization.
│   ├── constants.ipynb  # CSS class constants, selection mode type, timing defaults, and key defaults for the token selector.
│   ├── html_ids.ipynb   # Prefix-based HTML ID generator for token selector DOM elements.
│   └── models.ipynb     # Data models for tokens, render context, and mutable runtime state.
├── helpers/ (1)
│   └── tokenizer.ipynb  # Tokenization utilities for splitting text into tokens and converting between token indices and character positions.
├── js/ (4)
│   ├── core.ipynb        # Master IIFE composer for the token selector JS runtime.
│   ├── display.ipynb     # Generates JS functions for updating token display state (caret indicators, highlights, dimming, hidden inputs).
│   ├── navigation.ipynb  # Generates mode-specific navigation and selection JS functions.
│   └── repeat.ipynb      # Custom key repeat engine with configurable initial delay, repeat interval, and throttle floor.
└── keyboard/ (1)
    └── actions.ipynb  # Keyboard navigation library integration factories for token selector mode, actions, URL maps, and hidden action buttons.

Total: 12 notebooks across 5 directories

Module Dependencies

graph LR
    components_inputs[components.inputs<br/>Inputs]
    components_tokens[components.tokens<br/>Tokens]
    core_config[core.config<br/>Config]
    core_constants[core.constants<br/>Constants]
    core_html_ids[core.html_ids<br/>HTML IDs]
    core_models[core.models<br/>Models]
    helpers_tokenizer[helpers.tokenizer<br/>Tokenizer]
    js_core[js.core<br/>Core JS]
    js_display[js.display<br/>Display JS]
    js_navigation[js.navigation<br/>Navigation JS]
    js_repeat[js.repeat<br/>Key Repeat JS]
    keyboard_actions[keyboard.actions<br/>Keyboard Actions]

    components_inputs --> core_models
    components_inputs --> core_html_ids
    components_tokens --> core_constants
    components_tokens --> core_models
    components_tokens --> core_config
    components_tokens --> core_html_ids
    components_tokens --> helpers_tokenizer
    core_config --> core_constants
    helpers_tokenizer --> core_models
    js_core --> js_display
    js_core --> core_models
    js_core --> core_html_ids
    js_core --> core_config
    js_core --> js_repeat
    js_core --> js_navigation
    js_display --> core_constants
    js_display --> core_html_ids
    js_display --> core_config
    js_navigation --> core_config
    js_navigation --> core_html_ids
    js_repeat --> core_config
    keyboard_actions --> core_html_ids
    keyboard_actions --> core_config
    keyboard_actions --> js_core

24 cross-module dependencies detected

CLI Reference

No CLI commands found in this project.

Module Overview

Detailed documentation for each module in the project:

Keyboard Actions (actions.ipynb)

Keyboard navigation library integration factories for token selector mode, actions, URL maps, and hidden action buttons.

Import

from cjm_fasthtml_token_selector.keyboard.actions import (
    create_token_selector_mode,
    create_token_nav_actions,
    build_token_selector_url_map,
    render_token_action_buttons
)

Functions

def create_token_selector_mode(
    config:TokenSelectorConfig,         # config for this instance
    mode_name:str = "token-select",     # mode name for the keyboard nav system
    indicator_text:str = "Token Select", # mode indicator text
    exit_key:str = "",                  # exit key (empty = programmatic only via Escape KeyAction)
    exit_on_zone_change:bool = False,   # whether to exit on zone change
) -> KeyboardMode:  # configured keyboard mode
    "Create a keyboard mode that activates/deactivates the token selector."
def create_token_nav_actions(
    config:TokenSelectorConfig,           # config for this instance
    zone_id:str,                          # focus zone ID
    mode_name:str = "token-select",       # mode name (must match the mode)
    confirm_button_id:str = "",           # HTMX button ID for confirm action
    cancel_button_id:str = "",            # HTMX button ID for cancel action
) -> Tuple[KeyAction, ...]:  # non-movement keyboard actions
    "Create keyboard actions for the token selector."
def build_token_selector_url_map(
    confirm_button_id:str,  # button ID for confirm action
    cancel_button_id:str,   # button ID for cancel action
    confirm_url:str,        # URL for confirm action
    cancel_url:str,         # URL for cancel action
) -> Dict[str, str]:  # button ID -> URL mapping
    "Build URL map for keyboard system with token selector action buttons."
def render_token_action_buttons(
    confirm_button_id:str,          # button ID for confirm
    cancel_button_id:str,           # button ID for cancel
    confirm_url:str,                # URL for confirm action
    cancel_url:str,                 # URL for cancel action
    ids:TokenSelectorHtmlIds,       # HTML IDs (for hx_include)
    extra_include:str = "",         # additional hx_include selectors
) -> Any:  # Div containing hidden action buttons
    "Render hidden HTMX buttons for confirm/cancel actions."

Config (config.ipynb)

Configuration dataclass for token selector initialization.

Import

from cjm_fasthtml_token_selector.core.config import (
    TokenSelectorConfig
)

Functions

def _auto_prefix() -> str: # unique prefix string
    """Generate an auto-incrementing unique prefix."""
    global _prefix_counter
    p = f"ts{_prefix_counter}"
    _prefix_counter += 1
    return p

def _reset_prefix_counter() -> None
    "Generate an auto-incrementing unique prefix."
def _reset_prefix_counter() -> None
    "Reset the prefix counter (for testing only)."

Classes

@dataclass
class TokenSelectorConfig:
    "Initialization-time settings for a token selector instance."
    
    prefix: str = field(...)  # unique instance prefix
    selection_mode: SelectionMode = 'gap'  # selection behavior: "gap", "word", or "span"
    initial_delay: int = DEFAULT_INITIAL_DELAY  # ms before first repeat
    repeat_interval: int = DEFAULT_REPEAT_INTERVAL  # ms between repeats
    throttle_floor: int = DEFAULT_THROTTLE_FLOOR  # minimum ms between movements
    left_key: str = DEFAULT_LEFT_KEY  # key for leftward navigation
    right_key: str = DEFAULT_RIGHT_KEY  # key for rightward navigation
    end_token_text: str = DEFAULT_END_TOKEN_TEXT  # text for the sentinel end token
    show_end_token: bool = True  # whether to show the end token
    read_only: bool = False  # disable click/keyboard interaction
    wrap_navigation: bool = False  # wrap around at boundaries
    on_change_callback: str = ''  # JS function name called on selection change

Variables

_prefix_counter: int = 0

Constants (constants.ipynb)

CSS class constants, selection mode type, timing defaults, and key defaults for the token selector.

Import

from cjm_fasthtml_token_selector.core.constants import (
    SelectionMode,
    OPACITY_50_CLS,
    CARET_INDICATOR_CLS,
    HIGHLIGHT_CLS,
    DEFAULT_INITIAL_DELAY,
    DEFAULT_REPEAT_INTERVAL,
    DEFAULT_THROTTLE_FLOOR,
    DEFAULT_LEFT_KEY,
    DEFAULT_RIGHT_KEY,
    DEFAULT_END_TOKEN_TEXT
)

Variables

OPACITY_50_CLS
CARET_INDICATOR_CLS
HIGHLIGHT_CLS
DEFAULT_INITIAL_DELAY: int = 400
DEFAULT_REPEAT_INTERVAL: int = 80
DEFAULT_THROTTLE_FLOOR: int = 50
DEFAULT_LEFT_KEY: str = 'ArrowLeft'
DEFAULT_RIGHT_KEY: str = 'ArrowRight'
DEFAULT_END_TOKEN_TEXT: str = '(End)'

Core JS (core.ipynb)

Master IIFE composer for the token selector JS runtime.

Import

from cjm_fasthtml_token_selector.js.core import (
    global_callback_name,
    generate_token_selector_js
)

Functions

def global_callback_name(
    prefix:str,    # token selector instance prefix
    callback:str,  # base callback name
) -> str:  # global function name
    "Generate a prefix-unique global callback name."
def _generate_state_init_js(
    config:TokenSelectorConfig,  # config for this instance
    state:TokenSelectorState,    # initial state
) -> str:  # JS code fragment
    "Generate state initialization code."
def _generate_on_change_js(
    config:TokenSelectorConfig,  # config for this instance
) -> str:  # JS code fragment
    "Generate the on-change callback dispatcher."
def _generate_activation_js(
    config:TokenSelectorConfig,  # config for this instance
    ids:TokenSelectorHtmlIds,    # HTML IDs
) -> str:  # JS code fragment
    "Generate activate/deactivate functions."
def _generate_global_callbacks_js(
    config:TokenSelectorConfig,  # config for this instance
) -> str:  # JS code fragment
    "Generate global callback wrappers."
def _generate_settle_handler_js(
    config:TokenSelectorConfig,  # config for this instance
    ids:TokenSelectorHtmlIds,    # HTML IDs
) -> str:  # JS code fragment
    """
    Generate htmx:afterSettle handler for swap resilience.
    
    Stores the handler reference globally and replaces it on
    re-initialization to avoid stale closure references when
    the IIFE re-runs after HTMX page transitions.
    """
def generate_token_selector_js(
    config:TokenSelectorConfig,              # config for this instance
    ids:TokenSelectorHtmlIds,                # HTML IDs
    state:TokenSelectorState = None,         # initial state
    extra_scripts:Tuple[str, ...] = (),      # additional JS to include in the IIFE
) -> Any:  # Script element with the complete IIFE
    "Compose all token selector JS into a single namespaced IIFE."

Variables

_GLOBAL_CALLBACKS

Display JS (display.ipynb)

Generates JS functions for updating token display state (caret indicators, highlights, dimming, hidden inputs).

Import

from cjm_fasthtml_token_selector.js.display import (
    generate_display_js
)

Functions

def _generate_update_inputs_js(
    ids:TokenSelectorHtmlIds,  # HTML IDs
) -> str:  # JS code fragment
    "Generate the hidden input sync function."
def _generate_gap_display_js(
    ids:TokenSelectorHtmlIds,  # HTML IDs
) -> str:  # JS code fragment
    "Generate gap mode display update function."
def _generate_word_display_js(
    ids:TokenSelectorHtmlIds,  # HTML IDs
) -> str:  # JS code fragment
    "Generate word mode display update function."
def _generate_span_display_js(
    ids:TokenSelectorHtmlIds,  # HTML IDs
) -> str:  # JS code fragment
    "Generate span mode display update function."
def generate_display_js(
    config:TokenSelectorConfig,  # config for this instance
    ids:TokenSelectorHtmlIds,    # HTML IDs
) -> str:  # JS code fragment for the IIFE
    "Generate display update and hidden input sync JS functions."

HTML IDs (html_ids.ipynb)

Prefix-based HTML ID generator for token selector DOM elements.

Import

from cjm_fasthtml_token_selector.core.html_ids import (
    TokenSelectorHtmlIds
)

Classes

@dataclass
class TokenSelectorHtmlIds:
    "Prefix-based HTML ID generator for token selector DOM elements."
    
    prefix: str  # unique instance prefix
    
    def container(self) -> str:  # outer wrapper ID
            """Outer token selector wrapper."""
            return f"{self.prefix}-token-selector"
    
        @property
        def token_grid(self) -> str:  # flex-wrap token container ID
        "Outer token selector wrapper."
    
    def token_grid(self) -> str:  # flex-wrap token container ID
            """Flex-wrap token container."""
            return f"{self.prefix}-token-grid"
    
        @property
        def anchor_input(self) -> str:  # hidden input ID for anchor position
        "Flex-wrap token container."
    
    def anchor_input(self) -> str:  # hidden input ID for anchor position
            """Hidden input ID for anchor position (hyphenated, for CSS selectors)."""
            return f"{self.prefix}-anchor"
    
        @property
        def focus_input(self) -> str:  # hidden input ID for focus position
        "Hidden input ID for anchor position (hyphenated, for CSS selectors)."
    
    def focus_input(self) -> str:  # hidden input ID for focus position
            """Hidden input ID for focus position (hyphenated, for CSS selectors)."""
            return f"{self.prefix}-focus"
    
        @property
        def anchor_name(self) -> str:  # form field name for anchor position
        "Hidden input ID for focus position (hyphenated, for CSS selectors)."
    
    def anchor_name(self) -> str:  # form field name for anchor position
            """Form field name for anchor position (underscored, for Python kwargs)."""
            return f"{self.prefix}_anchor"
    
        @property
        def focus_name(self) -> str:  # form field name for focus position
        "Form field name for anchor position (underscored, for Python kwargs)."
    
    def focus_name(self) -> str:  # form field name for focus position
            """Form field name for focus position (underscored, for Python kwargs)."""
            return f"{self.prefix}_focus"
    
        def token(self,
                  index:int,  # token position index
                 ) -> str:  # individual token span ID
        "Form field name for focus position (underscored, for Python kwargs)."
    
    def token(self,
                  index:int,  # token position index
                 ) -> str:  # individual token span ID
        "Individual token span ID."

Inputs (inputs.ipynb)

Hidden input rendering for HTMX state sync.

Import

from cjm_fasthtml_token_selector.components.inputs import (
    render_hidden_inputs,
    build_include_selector
)

Functions

def render_hidden_inputs(
    ids:TokenSelectorHtmlIds,                   # HTML IDs for this instance
    state:Optional[TokenSelectorState] = None,  # current state
    oob:bool = False,                           # render with hx-swap-oob
) -> Any:  # tuple of anchor and focus hidden inputs
    "Render hidden inputs for HTMX form submission."
def build_include_selector(
    ids:TokenSelectorHtmlIds,  # HTML IDs for this instance
) -> str:  # CSS selector string for hx_include
    "Build a CSS selector for including anchor and focus inputs in HTMX requests."

Models (models.ipynb)

Data models for tokens, render context, and mutable runtime state.

Import

from cjm_fasthtml_token_selector.core.models import (
    Token,
    TokenRenderContext,
    TokenSelectorState
)

Classes

@dataclass
class Token:
    "A single token in the token grid."
    
    text: str  # the token text content
    index: int  # 0-based position in the token list
    metadata: Any  # optional consumer metadata per token
@dataclass
class TokenRenderContext:
    "Context passed to per-token styling callbacks."
    
    token: Token  # the token being rendered
    is_selected: bool  # whether this token is in the current selection
    is_anchor: bool  # whether this token is at the anchor position
    is_focus: bool  # whether this token is at the focus position
    selection_mode: str  # current selection mode
@dataclass
class TokenSelectorState:
    "Mutable runtime state for a token selector instance."
    
    anchor: int = 0  # anchor position (gap index or token index)
    focus: int = 0  # focus position (same as anchor in gap/word mode)
    word_count: int = 0  # total number of tokens
    active: bool = False  # whether the key repeat engine is active

Key Repeat JS (repeat.ipynb)

Custom key repeat engine with configurable initial delay, repeat interval, and throttle floor.

Import

from cjm_fasthtml_token_selector.js.repeat import (
    generate_key_repeat_js
)

Functions

def _generate_movement_dispatch_js(
    config:TokenSelectorConfig,  # config for this instance
) -> str:  # JS code fragment for the movement dispatcher
    "Generate the key-to-movement dispatch logic."
def generate_key_repeat_js(
    config:TokenSelectorConfig,  # config with timing settings
) -> str:  # JS code fragment for the IIFE
    "Generate the custom key repeat engine JS."

Tokenizer (tokenizer.ipynb)

Tokenization utilities for splitting text into tokens and converting between token indices and character positions.

Import

from cjm_fasthtml_token_selector.helpers.tokenizer import (
    count_tokens,
    get_token_list,
    token_index_to_char_position,
    tokenize
)

Functions

def count_tokens(
    text:str,  # text to count tokens in
) -> int:  # token count
    "Count the number of whitespace-delimited tokens in text."
def get_token_list(
    text:str,  # text to split into tokens
) -> List[str]:  # list of token strings
    "Split text into a list of whitespace-delimited tokens."
def token_index_to_char_position(
    text:str,         # full text string
    token_index:int,  # 0-based token index
) -> int:  # character position for split
    "Convert a token index to the character position where a split should occur."
def tokenize(
    text_or_tokens:Union[str, List[str]],  # raw text string or pre-tokenized list
    metadata:Optional[List[Any]] = None,   # per-token metadata (must match token count)
) -> List[Token]:  # list of Token objects
    "Convert text or a pre-tokenized list into Token objects."

Tokens (tokens.ipynb)

Token grid rendering for all three selection modes (gap, word, span).

Import

from cjm_fasthtml_token_selector.components.tokens import (
    render_token,
    render_end_token,
    render_token_grid
)

Functions

def _is_token_selected(
    index:int,              # token index
    mode:str,               # selection mode
    anchor:int,             # anchor position
    focus:int,              # focus position
) -> bool:  # whether the token is in the selection
    "Check if a token at the given index is within the current selection."
def _build_token_render_context(
    token:Token,                    # token being rendered
    mode:str,                       # selection mode
    anchor:int,                     # anchor position
    focus:int,                      # focus position
) -> TokenRenderContext:  # render context for styling callback
    "Build a render context for the per-token styling callback."
def _render_caret_indicator() -> Any:  # caret indicator Div element
    "Render the pulsing caret indicator bar."
def render_token(
    token:Token,                                      # token to render
    config:TokenSelectorConfig,                       # config for this instance
    ids:TokenSelectorHtmlIds,                         # HTML IDs
    state:Optional[TokenSelectorState] = None,        # current state for highlighting
    style_callback:Optional[Callable] = None,         # (TokenRenderContext) -> str for extra CSS
    read_only:bool = False,                           # disable interaction
) -> Any:  # Span element for this token
    "Render a single interactive word token."
def render_end_token(
    config:TokenSelectorConfig,                 # config for this instance
    ids:TokenSelectorHtmlIds,                   # HTML IDs
    state:Optional[TokenSelectorState] = None,  # current state
    read_only:bool = False,                     # disable interaction
) -> Any:  # end sentinel Span element
    "Render the end-of-text sentinel token."
def render_token_grid(
    tokens:List[Token],                               # token list to render
    config:TokenSelectorConfig,                       # config for this instance
    ids:TokenSelectorHtmlIds,                         # HTML IDs
    state:Optional[TokenSelectorState] = None,        # current state for highlighting
    style_callback:Optional[Callable] = None,         # (TokenRenderContext) -> str for extra CSS
    read_only:bool = False,                           # disable all interaction
) -> Any:  # Div containing the complete token grid
    "Render the full interactive token grid."