sovabids.tui¶
Terminal User Interface for sovabids.
Classes¶
Base filesystem browser modal: an editable location bar + an Up button + a |
|
Modal that lets user browse and confirm a directory. |
|
Modal that lets user browse and confirm a file (e.g. a rules YAML). |
|
Modal showing all matched files with extraction details on click. |
|
Modal showing channel names and types from a file. |
|
Blank is allowed (the field is optional); a non-blank value must be a positive, |
|
sovabids TUI — EEG to BIDS conversion wizard. |
Functions¶
|
Return a valid directory to root a |
|
Flatten nested dict with dot-notation keys. |
|
Return why |
|
Open a file with the OS default handler, cross-platform. Returns True if a |
|
Module Contents¶
- sovabids.tui._tree_root(start: str) str¶
Return a valid directory to root a
DirectoryTreeat.A
DirectoryTreeroot must be a directory, so normalize the seed value: a file opens at its parent directory (fixes reopening a picker whose input already holds a file path), and an empty / nonexistent path falls back to the nearest existing ancestor, then the home directory.
- class sovabids.tui.FSPickerScreen(start: str = '.')¶
Bases:
textual.screen.ModalScreen[str]Base filesystem browser modal: an editable location bar + an Up button + a
DirectoryTree, so the user can navigate anywhere (not just below the seed folder). Subclasses decide whether OK returns a directory or a file.- DEFAULT_CSS = Multiline-String¶
Show Value
""" FSPickerScreen { align: center middle; } FSPickerScreen > Vertical { width: 80%; height: 80%; border: thick $primary; background: $surface; padding: 1; } FSPickerScreen DirectoryTree { height: 1fr; } FSPickerScreen Label { height: 1; margin-bottom: 1; } FSPickerScreen .loc-row { height: 3; } FSPickerScreen .loc-row Input { width: 1fr; } FSPickerScreen .newdir-row { height: 3; } FSPickerScreen .newdir-row Input { width: 1fr; } FSPickerScreen .btn-row { height: 3; align: right middle; } FSPickerScreen Button { margin-left: 1; } """
- _prompt: ClassVar[str] = 'Select a path:'¶
- _DOUBLE_CLICK_SECONDS: ClassVar[float] = 0.5¶
- _selected = ''¶
- _root¶
- _last_dir = ''¶
- _last_dir_t = 0.0¶
- compose() textual.app.ComposeResult¶
- _extra_controls() list¶
Widgets to place between the location bar and the tree. Base: none.
- _reroot(path: str) None¶
Point the tree (and the location bar) at
path’s nearest directory.
- on_input_submitted(event: textual.widgets.Input.Submitted) None¶
- on_directory_tree_directory_selected(event: textual.widgets.DirectoryTree.DirectorySelected) None¶
- _on_dir_selected(path: str) None¶
Single-selection hook. Base does nothing (a file picker ignores directories).
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- _make_folder() None¶
Create a folder named after the
new-folder-namebox inside the folder currently shown, then step into it and pre-select it. Only the directory picker renders that box, so file mode never reaches here.
- abstractmethod _confirm() None¶
- on_key(event) None¶
- class sovabids.tui.DirPickerScreen(start: str = '.')¶
Bases:
FSPickerScreenModal that lets user browse and confirm a directory.
- _prompt = 'Pick a directory: click to select, double-click to enter; OK confirms.'¶
- _extra_controls() list¶
Widgets to place between the location bar and the tree. Base: none.
- _on_dir_selected(path: str) None¶
Single-selection hook. Base does nothing (a file picker ignores directories).
- _confirm() None¶
- class sovabids.tui.FilePickerScreen(start: str = '.')¶
Bases:
FSPickerScreenModal that lets user browse and confirm a file (e.g. a rules YAML).
Unlike
DirPickerScreen, only a file selection sets the confirmable value; clicking a folder just navigates the tree and never becomes the result. That is what keeps a stray navigation click before OK from returning a directory — the exact failure in issue #89. (It inherits the base directory handler, which only navigates into folders and never sets the confirmable value here.)- _prompt = 'Pick a file: double-click folders to enter, click a file, then OK.'¶
- on_directory_tree_file_selected(event: textual.widgets.DirectoryTree.FileSelected) None¶
- _confirm() None¶
- class sovabids.tui.SetupPane¶
Bases:
textual.widgets.Static- DEFAULT_CSS = Multiline-String¶
Show Value
""" SetupPane { padding: 1 2; } SetupPane Label { margin-top: 1; } SetupPane .path-row { height: 3; } SetupPane Input { width: 1fr; } SetupPane Button { width: 14; margin-left: 1; } SetupPane .hint { color: $text-muted; margin-bottom: 1; } """
- compose() textual.app.ComposeResult¶
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- get_values() dict¶
- class sovabids.tui.FilesListScreen(files: list[str], pattern: str = '', mode: str = 'placeholder', fields: list[str] | None = None)¶
Bases:
textual.screen.ModalScreenModal showing all matched files with extraction details on click.
- DEFAULT_CSS = Multiline-String¶
Show Value
""" FilesListScreen { align: center middle; } FilesListScreen > Vertical { width: 92%; height: 90%; border: thick $primary; background: $surface; padding: 1; } FilesListScreen DataTable { height: 1fr; } FilesListScreen #extraction-panel { height: auto; max-height: 40%; border: solid $primary; padding: 0 1; color: $success; overflow-y: auto; } FilesListScreen #extraction-panel.muted { color: $text-muted; } FilesListScreen #extraction-panel.error { color: $error; } FilesListScreen Horizontal { height: 3; align: right middle; } """
- _files¶
- _pattern = ''¶
- _mode = 'placeholder'¶
- _fields = []¶
- compose() textual.app.ComposeResult¶
- on_mount() None¶
- on_data_table_row_highlighted(event: textual.widgets.DataTable.RowHighlighted) None¶
- _show_extraction(filepath: str) None¶
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- on_key(event) None¶
- class sovabids.tui.ChannelNamesScreen(filepath: str)¶
Bases:
textual.screen.ModalScreenModal showing channel names and types from a file.
- DEFAULT_CSS = Multiline-String¶
Show Value
""" ChannelNamesScreen { align: center middle; } ChannelNamesScreen > Vertical { width: 70%; height: 85%; border: thick $primary; background: $surface; padding: 1; } ChannelNamesScreen DataTable { height: 1fr; } ChannelNamesScreen #ch-status { color: $text-muted; height: 1; } ChannelNamesScreen Horizontal { height: 3; align: right middle; } """
- _filepath¶
- compose() textual.app.ComposeResult¶
- on_mount() None¶
- _load_worker() None¶
- _set_ch_error(msg: str) None¶
- _populate(ch_names: list, ch_types: list) None¶
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- on_key(event) None¶
- sovabids.tui._flatten_dict(d: dict, prefix: str = '') dict¶
Flatten nested dict with dot-notation keys.
- sovabids.tui._plf_problem(value: str) str¶
Return why
valueis not a usable power-line frequency, or “” if fine. Blank is fine (optional); otherwise it must parse and be a positive, finite number (rejects nan/inf/negative, which float() would otherwise accept).
- sovabids.tui._open_path_in_viewer(path: str) bool¶
Open a file with the OS default handler, cross-platform. Returns True if a viewer/browser was launched.
Only waits briefly for the opener to detect an immediate failure (e.g.
xdg-openwith no handler exits non-zero right away); a still-running opener is treated as a successful hand-off and left alone — never killed — because some openers stay attached to the viewer for its whole lifetime. Falls back to the browser with a validfile://URI (as_urihandles Windows drives/spaces).
- class sovabids.tui._OptionalNumber¶
Bases:
textual.validation.ValidatorBlank is allowed (the field is optional); a non-blank value must be a positive, finite number. Flags an unusable power-line frequency instead of dropping it.
- validate(value: str) textual.validation.ValidationResult¶
- class sovabids.tui.RulesPane¶
Bases:
textual.widgets.Static- DEFAULT_CSS = Multiline-String¶
Show Value
""" RulesPane { padding: 1 2; } RulesPane Label { margin-top: 1; } RulesPane .hint { color: $text-muted; } RulesPane .pattern-examples { color: $text-muted; margin-bottom: 1; padding: 0 2; border-left: solid $primary; } RulesPane .rulesfile-row { height: 3; } RulesPane .rulesfile-row Input { width: 1fr; } RulesPane .rulesfile-row Button { width: 14; margin-left: 1; } RulesPane #rules-lock-note { display: none; height: auto; color: $warning; border: round $warning; padding: 0 1; margin-bottom: 1; } RulesPane #rules-lock-note.locked { display: block; } RulesPane #rules-builder { height: auto; } RulesPane #ext-count { color: $text-muted; height: 1; margin-bottom: 1; } RulesPane #sample-paths { display: none; height: auto; margin-bottom: 1; padding: 0 2; border-left: solid $success; } RulesPane #preview-row { height: auto; margin-top: 1; } RulesPane #preview-label { padding: 1; border: solid $primary; color: $success; width: 1fr; height: auto; min-height: 3; } RulesPane #preview-label.error { color: $error; } RulesPane #preview-label.muted { color: $text-muted; } RulesPane #show-files { width: 18; margin-left: 1; } RulesPane Select { margin-bottom: 1; } RulesPane Input { margin-bottom: 1; } RulesPane .section-head { margin-top: 1; } RulesPane RadioSet { margin-bottom: 1; } RulesPane #regex-fields-row { display: none; height: auto; } RulesPane #regex-fields-row.visible { display: block; } RulesPane #regex-examples { display: none; } RulesPane #regex-examples.visible { display: block; } RulesPane #io-example-row { display: none; height: auto; } RulesPane #io-examples { display: none; } RulesPane #io-src-row { height: 3; } RulesPane #io-src-row Input { width: 1fr; } RulesPane #io-src-row Button { width: 18; margin-left: 1; } RulesPane #pattern-section { height: auto; } RulesPane .inspect-row { height: 3; margin-bottom: 1; } RulesPane .inspect-row Button { margin-right: 1; } """
- _SAMPLE_SHOW: ClassVar[int] = 2¶
- _SAMPLE_COLLECT_CAP: ClassVar[int] = 200¶
- _preview_timer = None¶
- _ext_count_timer = None¶
- _preview_gen = 0¶
- _matched_files: list[str] = []¶
- _psd_in_flight = False¶
- compose() textual.app.ComposeResult¶
- _compose_builder() textual.app.ComposeResult¶
- on_input_changed(event: textual.widgets.Input.Changed) None¶
- _set_builder_locked(locked: bool) None¶
- get_rules_file() str¶
- plf_error() str¶
Return an error message if the power-line frequency is set but not a usable (positive, finite) number, else “”. Lets an action block instead of silently dropping it (#102).
- on_select_changed(event: textual.widgets.Select.Changed) None¶
- on_radio_set_changed(event: textual.widgets.RadioSet.Changed) None¶
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- _get_mode_and_fields() tuple[str, list[str]]¶
- _get_io_example() tuple[str, str]¶
- _schedule_ext_count() None¶
- _run_ext_count() None¶
- _ext_count_worker(source: str, ext: str) None¶
- _set_ext_count(text: str) None¶
- _psd_png_path() str¶
A single, reused temp file for the PSD preview so repeated clicks overwrite one PNG instead of leaking a new one each time. The directory is removed in
on_unmount.
- on_unmount() None¶
- _psd_worker(filepath: str, out_path: str) None¶
- _psd_done() None¶
- _update_psd_btn() None¶
- _schedule_preview() None¶
- _run_preview() None¶
- _preview_worker(gen: int, source: str, pattern: str, ext: str, mode: str, fields: list[str], io_src: str = '', io_tgt: str = '') None¶
- _apply_preview(gen: int, msg: str, css: str, matched: list) None¶
- _update_show_files_btn(files: list[str]) None¶
- _set_preview(text: str, css_class: str) None¶
- _update_samples(samples: list, source: str) None¶
Show a couple of real source-file paths so the user can read off the structure and build the pattern from it. Hidden when nothing is found.
- _get_source() str¶
- get_rules() dict¶
- class sovabids.tui.MappingsPane¶
Bases:
textual.widgets.Static- DEFAULT_CSS = Multiline-String¶
Show Value
""" MappingsPane { padding: 1 2; } MappingsPane #mappings-status { margin-bottom: 1; color: $text-muted; } MappingsPane DataTable { height: 1fr; } MappingsPane Horizontal { height: 3; margin-bottom: 1; } MappingsPane Button { margin-right: 1; } """
- compose() textual.app.ComposeResult¶
- on_mount() None¶
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- _generate() None¶
- _gen_worker(source: str, bids: str, rules: dict, token: int) None¶
- _apply_mappings(mapping_data: dict, token: int) None¶
- _gen_error(msg: str, token: int) None¶
- _populate_table(individuals: list) None¶
- _save_mappings_yaml() None¶
- _set_status(msg: str, error: bool = False) None¶
- class sovabids.tui.ConvertPane¶
Bases:
textual.widgets.Static- DEFAULT_CSS = Multiline-String¶
Show Value
""" ConvertPane { padding: 1 2; } ConvertPane Horizontal { height: 3; margin-bottom: 1; } ConvertPane Button { margin-right: 1; } ConvertPane RichLog { height: 1fr; border: solid $primary; } """
- compose() textual.app.ComposeResult¶
- on_button_pressed(event: textual.widgets.Button.Pressed) None¶
- _start_convert() None¶
- _convert_worker(mapping_data: dict, log: textual.widgets.RichLog) None¶
- _log(msg: str) None¶
- class sovabids.tui.SovabidsApp¶
Bases:
textual.app.Appsovabids TUI — EEG to BIDS conversion wizard.
- TITLE = 'sovabids'¶
- SUB_TITLE = 'EEG → BIDS conversion'¶
- CSS = Multiline-String¶
Show Value
""" TabbedContent { height: 1fr; } TabPane { height: 1fr; } """
- BINDINGS = [('q', 'quit', 'Quit'), ('ctrl+s', 'save_rules', 'Save rules')]¶
- _mapping_data: dict | None = None¶
- _pending_input_id: str = ''¶
- _gen_token: int = 0¶
- compose() textual.app.ComposeResult¶
- _on_path_picked(path: str) None¶
- on_tabbed_content_tab_activated(event: textual.widgets.TabbedContent.TabActivated) None¶
- on_input_changed(event: textual.widgets.Input.Changed) None¶
- on_select_changed(event: textual.widgets.Select.Changed) None¶
- on_radio_set_changed(event: textual.widgets.RadioSet.Changed) None¶
- _invalidate_mappings_if_edit(control) None¶
- _set_mappings_valid(valid: bool) None¶
- action_save_rules() None¶
- sovabids.tui.main() None¶