labnotes 0.0.97__tar.gz → 0.0.99__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labnotes-0.0.97 → labnotes-0.0.99}/PKG-INFO +5 -5
- {labnotes-0.0.97 → labnotes-0.0.99}/README.md +4 -4
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/labnotes.css +49 -17
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/new-note.js +0 -2
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/note-edit.js +9 -16
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/note-editor.js +113 -69
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/templates/editor_imports.html +1 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/browse_app.py +1 -1
- labnotes-0.0.99/src/labnotes/tui/leave_prompt.py +28 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/run_app.py +22 -8
- {labnotes-0.0.97 → labnotes-0.0.99}/.gitignore +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/CHANGELOG.md +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/MIGRATIONS.md +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/pyproject.toml +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/context.md +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/init.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/log.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/models.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/server.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/testing-conventions.toml +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.97 → labnotes-0.0.99}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.99
|
|
4
4
|
Summary: Browse experiment designs, notes, and run data from a folder.
|
|
5
5
|
Project-URL: Repository, https://github.com/thekevinscott/labnotes
|
|
6
6
|
Author-email: Kevin Scott <me@thekevinscott.com>
|
|
@@ -108,14 +108,14 @@ To exclude a run, add `"excluded": true` and an optional `"reason"` to its
|
|
|
108
108
|
replaces it; both show as columns in `runs`.
|
|
109
109
|
|
|
110
110
|
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
111
|
-
open when they finish;
|
|
111
|
+
open when they finish; Ctrl-C quits. Elsewhere, it prints a
|
|
112
112
|
line per event instead.
|
|
113
113
|
|
|
114
114
|
- `--concurrency N` runs N at a time (default 1).
|
|
115
115
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
116
|
-
- Ctrl-C
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
|
|
117
|
+
running. `s` stops the running scripts and records their end, then kills any
|
|
118
|
+
still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
|
|
119
119
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
120
120
|
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
121
121
|
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
@@ -84,14 +84,14 @@ To exclude a run, add `"excluded": true` and an optional `"reason"` to its
|
|
|
84
84
|
replaces it; both show as columns in `runs`.
|
|
85
85
|
|
|
86
86
|
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
87
|
-
open when they finish;
|
|
87
|
+
open when they finish; Ctrl-C quits. Elsewhere, it prints a
|
|
88
88
|
line per event instead.
|
|
89
89
|
|
|
90
90
|
- `--concurrency N` runs N at a time (default 1).
|
|
91
91
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
92
|
-
- Ctrl-C
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
|
|
93
|
+
running. `s` stops the running scripts and records their end, then kills any
|
|
94
|
+
still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
|
|
95
95
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
96
96
|
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
97
97
|
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
@@ -418,7 +418,7 @@ h3 {
|
|
|
418
418
|
top: 0.5rem;
|
|
419
419
|
right: 0.5rem;
|
|
420
420
|
}
|
|
421
|
-
.cell .run, .note-name .restart {
|
|
421
|
+
.cell .run, .editor .run, .note-name .restart {
|
|
422
422
|
font: inherit;
|
|
423
423
|
font-family: var(--sans);
|
|
424
424
|
font-size: 0.75rem;
|
|
@@ -429,8 +429,8 @@ h3 {
|
|
|
429
429
|
color: var(--ink-2);
|
|
430
430
|
cursor: pointer;
|
|
431
431
|
}
|
|
432
|
-
.cell .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
|
|
433
|
-
.cell .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
|
|
432
|
+
.cell .run:hover, .editor .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
|
|
433
|
+
.cell .run:disabled, .editor .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
|
|
434
434
|
.note-name:has(.restart) { display: flex; align-items: center; justify-content: flex-end; padding-top: 0.45rem; padding-bottom: 0.45rem; }
|
|
435
435
|
.cell-output { display: block; }
|
|
436
436
|
.cell-output:not(:empty) { margin-top: 0.4rem; padding-left: 0.9rem; border-left: 2px solid var(--accent-soft); overflow-x: auto; }
|
|
@@ -438,25 +438,57 @@ h3 {
|
|
|
438
438
|
.cell-output .stderr, .cell-output .error { color: var(--bad); }
|
|
439
439
|
.cell-output img, .cell-output svg { max-width: 100%; height: auto; }
|
|
440
440
|
|
|
441
|
-
/* Editing a note in place */
|
|
442
|
-
.prose.editable { cursor: text; }
|
|
441
|
+
/* Editing a note in place: CodeMirror styled to match .prose */
|
|
443
442
|
.edit-status { margin: 0; padding: 0.45rem 1.75rem; font-family: var(--sans); font-size: 0.8rem; color: var(--ink-3); border-bottom: 1px solid var(--rule); }
|
|
444
443
|
.editor { padding: 0.5rem 1.75rem 1.5rem; max-width: 46rem; }
|
|
445
444
|
.editor .cm-editor.cm-focused { outline: none; }
|
|
446
|
-
.editor .cm-content { font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
|
|
445
|
+
.editor .cm-content { padding: 0.8em 0; font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
|
|
447
446
|
.editor .cm-line { padding: 0; }
|
|
448
|
-
.editor .
|
|
449
|
-
.editor .
|
|
450
|
-
.editor .
|
|
451
|
-
.editor .
|
|
452
|
-
.editor .
|
|
453
|
-
.editor .
|
|
454
|
-
.editor .
|
|
455
|
-
.editor .
|
|
456
|
-
.editor .
|
|
457
|
-
.editor .
|
|
447
|
+
.editor .cm-line:has(> br:only-child):not(.cm-atomic-fenced-code) { line-height: 0.8em; }
|
|
448
|
+
.editor .cm-atomic-h1, .editor .cm-atomic-h2, .editor .cm-atomic-h3, .editor .cm-atomic-h4, .editor .cm-atomic-h5, .editor .cm-atomic-h6 { font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; padding: 1em 0 0.5em; }
|
|
449
|
+
.editor .cm-line.cm-atomic-h1 { font-size: 1.6rem; }
|
|
450
|
+
.editor .cm-line.cm-atomic-h2 { font-size: 1.3rem; }
|
|
451
|
+
.editor .cm-line.cm-atomic-h3 { font-size: 1.1rem; }
|
|
452
|
+
.editor .cm-line.cm-atomic-h4, .editor .cm-line.cm-atomic-h5, .editor .cm-line.cm-atomic-h6 { font-size: 1rem; }
|
|
453
|
+
.editor .cm-atomic-strong { font-weight: 700; }
|
|
454
|
+
.editor .cm-atomic-em { font-style: italic; }
|
|
455
|
+
.editor .cm-atomic-strike { text-decoration: line-through; }
|
|
456
|
+
.editor .cm-atomic-inline-code { font-family: var(--mono); font-size: 0.86em; background: var(--chip); padding: 0.1em 0.35em; border-radius: 4px; }
|
|
457
|
+
.editor .cm-atomic-link { text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: 3px; }
|
|
458
|
+
.editor .cm-atomic-link::after {
|
|
459
|
+
content: "↗";
|
|
460
|
+
margin-left: 0.15em;
|
|
461
|
+
font-family: var(--sans);
|
|
462
|
+
font-size: 0.75em;
|
|
463
|
+
color: var(--ink-3);
|
|
464
|
+
cursor: pointer;
|
|
465
|
+
}
|
|
466
|
+
.editor .cm-atomic-link:hover::after { color: var(--accent); }
|
|
467
|
+
.editor .cm-line.cm-atomic-blockquote { padding-left: 1rem; border-left: 2px solid var(--rule-strong); color: var(--ink-2); }
|
|
468
|
+
.editor .cm-atomic-list-marker { display: inline-block; width: 0.9em; margin-right: 0.3em; text-align: right; }
|
|
469
|
+
.editor .cm-atomic-list-marker, .editor .cm-atomic-list-marker .md-mark { color: var(--ink); }
|
|
470
|
+
.editor .cm-line.cm-atomic-hr { position: relative; }
|
|
471
|
+
.editor .cm-line.cm-atomic-hr::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px solid var(--rule); }
|
|
472
|
+
.editor .cm-line.cm-atomic-fenced-code {
|
|
473
|
+
position: relative;
|
|
474
|
+
font-family: var(--mono);
|
|
475
|
+
font-size: 0.8rem;
|
|
476
|
+
line-height: 1.5;
|
|
477
|
+
background: var(--paper);
|
|
478
|
+
padding: 0 1rem;
|
|
479
|
+
border: solid var(--rule);
|
|
480
|
+
border-width: 0 1px;
|
|
481
|
+
}
|
|
482
|
+
.editor .cm-line:not(.cm-atomic-fenced-code) + .cm-atomic-fenced-code, .editor .cm-atomic-fenced-code:first-child { border-top-width: 1px; border-radius: 6px 6px 0 0; }
|
|
483
|
+
.editor .cm-atomic-fenced-code:not(:has(+ .cm-atomic-fenced-code)) { border-bottom-width: 1px; border-radius: 0 0 6px 6px; }
|
|
484
|
+
.editor .cm-line:not(.cm-atomic-fenced-code) + .cm-atomic-fenced-code:not(:has(+ .cm-atomic-fenced-code)) { border-radius: 6px; }
|
|
485
|
+
.editor .cm-atomic-fenced-selection { background: var(--accent-soft); }
|
|
486
|
+
.editor .run { position: absolute; z-index: 1; top: 0.5rem; right: 0.5rem; }
|
|
487
|
+
/* CodeMirror measures block widgets without their margins. */
|
|
488
|
+
.editor .cell-below { display: flow-root; padding-bottom: 0.8em; }
|
|
489
|
+
.editor .cell-output pre { margin: 0; font-size: 0.8rem; line-height: 1.5; }
|
|
490
|
+
.editor .cm-line .cm-widgetBuffer { vertical-align: top; }
|
|
458
491
|
.editor .md-mark { color: var(--ink-3); }
|
|
459
|
-
.editor .md-code-line { font-family: var(--mono); font-size: 0.8rem; line-height: 1.5; background: var(--paper); padding: 0 1rem; }
|
|
460
492
|
.editor .py-keyword { color: var(--accent); }
|
|
461
493
|
.editor .py-string { color: var(--good); }
|
|
462
494
|
.editor .py-comment { color: var(--ink-3); font-style: italic; }
|
|
@@ -22,8 +22,6 @@ function show(note, button) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
for (const button of document.querySelectorAll(".new-note")) {
|
|
25
|
-
// An open editor closes, and reloads the page, on a mousedown outside it.
|
|
26
|
-
button.addEventListener("mousedown", (event) => event.stopPropagation());
|
|
27
25
|
button.addEventListener("click", async () => {
|
|
28
26
|
button.disabled = true;
|
|
29
27
|
const response = await fetch(button.dataset.url, {
|
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
let detach = () => {};
|
|
2
2
|
|
|
3
|
-
export function attach(note,
|
|
3
|
+
export function attach(note, focus) {
|
|
4
4
|
detach();
|
|
5
5
|
const prose = note.querySelector(".prose");
|
|
6
6
|
const status = note.querySelector(".edit-status");
|
|
7
|
-
const editor = import(note.dataset.editor);
|
|
8
7
|
const source = JSON.parse(note.querySelector(".note-source").textContent);
|
|
9
8
|
let editing = null;
|
|
10
9
|
let attached = true;
|
|
11
10
|
|
|
12
|
-
editor.then(
|
|
13
|
-
() =>
|
|
11
|
+
import(note.dataset.editor).then(
|
|
12
|
+
({ edit }) => {
|
|
13
|
+
if (!attached) return;
|
|
14
|
+
editing = edit(note, prose, status);
|
|
15
|
+
if (focus) editing.view.focus();
|
|
16
|
+
},
|
|
14
17
|
() => {},
|
|
15
18
|
);
|
|
16
19
|
|
|
17
|
-
const open = async (event) => {
|
|
18
|
-
if (event.target.closest("a, button, output") || getSelection().toString()) return;
|
|
19
|
-
const { edit } = await editor;
|
|
20
|
-
if (editing || !attached) return;
|
|
21
|
-
prose.removeEventListener("click", open);
|
|
22
|
-
editing = edit(note, prose, status);
|
|
23
|
-
};
|
|
24
|
-
prose.addEventListener("click", open);
|
|
25
|
-
if (openNow) open({ target: prose });
|
|
26
|
-
|
|
27
20
|
const changes = new EventSource(note.dataset.changesUrl);
|
|
28
21
|
changes.addEventListener("message", (event) => {
|
|
29
22
|
if (JSON.parse(event.data) !== note.dataset.note) return;
|
|
30
23
|
if (editing) return editing.pull();
|
|
31
|
-
//
|
|
32
|
-
//
|
|
24
|
+
// Without the editor the note is rendered HTML. A touch or a duplicate
|
|
25
|
+
// event leaves the text alone, and reloading then is wasted.
|
|
33
26
|
fetch(note.dataset.saveUrl, { cache: "no-store" })
|
|
34
27
|
.then((response) => response.text())
|
|
35
28
|
.then((text) => {
|
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
HighlightStyle,
|
|
5
5
|
Language,
|
|
6
6
|
defineLanguageFacet,
|
|
7
|
+
ensureSyntaxTree,
|
|
7
8
|
languageDataProp,
|
|
8
9
|
syntaxHighlighting,
|
|
9
10
|
syntaxTree,
|
|
10
11
|
} from "@codemirror/language";
|
|
11
|
-
import { EditorState,
|
|
12
|
-
import { Decoration, EditorView,
|
|
12
|
+
import { EditorState, StateField } from "@codemirror/state";
|
|
13
|
+
import { Decoration, EditorView, WidgetType, keymap } from "@codemirror/view";
|
|
14
|
+
import { inlinePreview } from "@atomic-editor/editor/inline-preview";
|
|
13
15
|
import { tags } from "@lezer/highlight";
|
|
14
16
|
import { GFM, parseCode, parser } from "@lezer/markdown";
|
|
15
17
|
import { DIFF_DELETE, DIFF_EQUAL, diff_match_patch } from "diff-match-patch";
|
|
@@ -31,16 +33,6 @@ const markdown = new Language(
|
|
|
31
33
|
);
|
|
32
34
|
|
|
33
35
|
const style = HighlightStyle.define([
|
|
34
|
-
{ tag: tags.heading1, class: "md-h1" },
|
|
35
|
-
{ tag: tags.heading2, class: "md-h2" },
|
|
36
|
-
{ tag: tags.heading3, class: "md-h3" },
|
|
37
|
-
{ tag: [tags.heading4, tags.heading5, tags.heading6], class: "md-h4" },
|
|
38
|
-
{ tag: tags.strong, class: "md-strong" },
|
|
39
|
-
{ tag: tags.emphasis, class: "md-em" },
|
|
40
|
-
{ tag: tags.strikethrough, class: "md-strike" },
|
|
41
|
-
{ tag: [tags.link, tags.url], class: "md-link" },
|
|
42
|
-
{ tag: tags.monospace, class: "md-code" },
|
|
43
|
-
{ tag: tags.quote, class: "md-quote" },
|
|
44
36
|
{ tag: [tags.processingInstruction, tags.contentSeparator], class: "md-mark" },
|
|
45
37
|
{ tag: [tags.keyword, tags.operatorKeyword, tags.bool, tags.null], class: "py-keyword" },
|
|
46
38
|
{ tag: [tags.string, tags.special(tags.string)], class: "py-string" },
|
|
@@ -49,54 +41,86 @@ const style = HighlightStyle.define([
|
|
|
49
41
|
{ tag: [tags.function(tags.variableName), tags.definition(tags.variableName)], class: "py-name" },
|
|
50
42
|
]);
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
function decorate(view) {
|
|
58
|
-
const builder = new RangeSetBuilder();
|
|
59
|
-
const { state } = view;
|
|
60
|
-
const active = new Set();
|
|
61
|
-
for (const range of state.selection.ranges) {
|
|
62
|
-
for (let n = state.doc.lineAt(range.from).number; n <= state.doc.lineAt(range.to).number; n++) active.add(n);
|
|
44
|
+
class RunButton extends WidgetType {
|
|
45
|
+
constructor(block, run) {
|
|
46
|
+
super();
|
|
47
|
+
this.block = block;
|
|
48
|
+
this.run = run;
|
|
63
49
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (!HIDDEN.has(node.name) || active.has(state.doc.lineAt(node.from).number)) return;
|
|
78
|
-
let end = node.to;
|
|
79
|
-
if (node.name === "HeaderMark" && state.doc.sliceString(end, end + 1) === " ") end += 1;
|
|
80
|
-
builder.add(node.from, end, hide);
|
|
81
|
-
},
|
|
82
|
-
});
|
|
50
|
+
eq(other) {
|
|
51
|
+
return other.block === this.block;
|
|
52
|
+
}
|
|
53
|
+
toDOM() {
|
|
54
|
+
const button = document.createElement("button");
|
|
55
|
+
button.type = "button";
|
|
56
|
+
button.className = "run";
|
|
57
|
+
button.textContent = "Run";
|
|
58
|
+
button.addEventListener("click", () => this.run(this.block, button));
|
|
59
|
+
return button;
|
|
60
|
+
}
|
|
61
|
+
ignoreEvent() {
|
|
62
|
+
return true;
|
|
83
63
|
}
|
|
84
|
-
return builder.finish();
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
66
|
+
class CellOutput extends WidgetType {
|
|
67
|
+
constructor(block, outputs) {
|
|
68
|
+
super();
|
|
69
|
+
this.block = block;
|
|
70
|
+
this.outputs = outputs;
|
|
71
|
+
}
|
|
72
|
+
eq(other) {
|
|
73
|
+
return other.block === this.block;
|
|
74
|
+
}
|
|
75
|
+
toDOM() {
|
|
76
|
+
const cell = document.createElement("div");
|
|
77
|
+
cell.className = "cell-below";
|
|
78
|
+
if (!this.outputs.has(this.block)) {
|
|
79
|
+
const output = document.createElement("output");
|
|
80
|
+
output.className = "cell-output";
|
|
81
|
+
this.outputs.set(this.block, output);
|
|
91
82
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
cell.append(this.outputs.get(this.block));
|
|
84
|
+
return cell;
|
|
85
|
+
}
|
|
86
|
+
ignoreEvent() {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Numbered in document order, as python_blocks numbers them on the server.
|
|
92
|
+
function cellWidgets(state, run, outputs) {
|
|
93
|
+
const widgets = [];
|
|
94
|
+
let block = 0;
|
|
95
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 200) ?? syntaxTree(state);
|
|
96
|
+
tree.iterate({
|
|
97
|
+
enter(node) {
|
|
98
|
+
if (node.name !== "FencedCode") return;
|
|
99
|
+
const info = node.node.getChild("CodeInfo");
|
|
100
|
+
if (info && state.doc.sliceString(info.from, info.to).split(/\s/)[0] === "python") {
|
|
101
|
+
const open = state.doc.lineAt(node.from);
|
|
102
|
+
widgets.push(Decoration.widget({ widget: new RunButton(block, run), side: 1 }).range(open.to));
|
|
103
|
+
widgets.push(
|
|
104
|
+
Decoration.widget({ widget: new CellOutput(block, outputs), block: true, side: 1 }).range(node.to),
|
|
105
|
+
);
|
|
106
|
+
block += 1;
|
|
95
107
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
108
|
+
return false;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return Decoration.set(widgets, true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cells(run, outputs) {
|
|
115
|
+
return StateField.define({
|
|
116
|
+
create: (state) => cellWidgets(state, run, outputs),
|
|
117
|
+
update(widgets, tr) {
|
|
118
|
+
if (!tr.docChanged && syntaxTree(tr.state) === syntaxTree(tr.startState)) return widgets;
|
|
119
|
+
return cellWidgets(tr.state, run, outputs);
|
|
120
|
+
},
|
|
121
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
100
124
|
|
|
101
125
|
function show(status, message) {
|
|
102
126
|
status.textContent = message;
|
|
@@ -196,15 +220,33 @@ export function edit(note, prose, status) {
|
|
|
196
220
|
|
|
197
221
|
const save = () => run(push);
|
|
198
222
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
// The rendered note's outputs move into the editor, so a run started
|
|
224
|
+
// before the editor loaded still shows its result.
|
|
225
|
+
const outputs = new Map();
|
|
226
|
+
for (const cell of prose.querySelectorAll(".cell")) {
|
|
227
|
+
outputs.set(Number(cell.dataset.block), cell.querySelector(".cell-output"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The server runs the block from the file, so unsaved edits go first.
|
|
231
|
+
const runCell = async (block, button) => {
|
|
232
|
+
button.disabled = true;
|
|
233
|
+
button.textContent = "Running";
|
|
234
|
+
try {
|
|
235
|
+
await save();
|
|
236
|
+
const response = await fetch(note.dataset.runUrl, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify({ block }),
|
|
240
|
+
});
|
|
241
|
+
outputs.get(block).innerHTML = response.ok
|
|
242
|
+
? await response.text()
|
|
243
|
+
: `<pre class="error">${response.status} ${response.statusText}</pre>`;
|
|
244
|
+
view.requestMeasure();
|
|
245
|
+
} finally {
|
|
246
|
+
button.disabled = false;
|
|
247
|
+
button.textContent = "Run";
|
|
248
|
+
}
|
|
206
249
|
};
|
|
207
|
-
document.addEventListener("mousedown", outside);
|
|
208
250
|
|
|
209
251
|
const view = new EditorView({
|
|
210
252
|
parent: host,
|
|
@@ -212,10 +254,15 @@ export function edit(note, prose, status) {
|
|
|
212
254
|
doc: base,
|
|
213
255
|
extensions: [
|
|
214
256
|
history(),
|
|
215
|
-
keymap.of([
|
|
257
|
+
keymap.of([
|
|
258
|
+
{ key: "Escape", run: () => (view.contentDOM.blur(), true) },
|
|
259
|
+
...defaultKeymap,
|
|
260
|
+
...historyKeymap,
|
|
261
|
+
]),
|
|
216
262
|
markdown,
|
|
217
263
|
syntaxHighlighting(style),
|
|
218
|
-
|
|
264
|
+
inlinePreview({ onLinkClick: (url) => location.assign(new URL(url, location.href)) }),
|
|
265
|
+
cells(runCell, outputs),
|
|
219
266
|
EditorView.lineWrapping,
|
|
220
267
|
EditorView.contentAttributes.of({ "aria-label": "Note" }),
|
|
221
268
|
EditorView.updateListener.of((update) => {
|
|
@@ -227,12 +274,9 @@ export function edit(note, prose, status) {
|
|
|
227
274
|
],
|
|
228
275
|
}),
|
|
229
276
|
});
|
|
230
|
-
view.focus();
|
|
231
277
|
return {
|
|
278
|
+
view,
|
|
232
279
|
pull: () => run(pull),
|
|
233
|
-
detach: () =>
|
|
234
|
-
document.removeEventListener("mousedown", outside);
|
|
235
|
-
save();
|
|
236
|
-
},
|
|
280
|
+
detach: () => save(),
|
|
237
281
|
};
|
|
238
282
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<script type="importmap">
|
|
3
3
|
{
|
|
4
4
|
"imports": {
|
|
5
|
+
"@atomic-editor/editor/inline-preview": "https://esm.sh/*@atomic-editor/editor@0.6.2/dist/inline-preview.js",
|
|
5
6
|
"@codemirror/autocomplete": "https://esm.sh/*@codemirror/autocomplete@6.20.4",
|
|
6
7
|
"@codemirror/commands": "https://esm.sh/*@codemirror/commands@6.11.1",
|
|
7
8
|
"@codemirror/lang-python": "https://esm.sh/*@codemirror/lang-python@6.2.1",
|
|
@@ -8,7 +8,7 @@ from labnotes.tui.experiments_screen import ExperimentsScreen
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BrowseApp(App[BaseException | None]):
|
|
11
|
-
BINDINGS = [Binding("
|
|
11
|
+
BINDINGS = [Binding("ctrl+c", "quit", "Quit")]
|
|
12
12
|
|
|
13
13
|
def __init__(self, config: LabnotesConfig, name: str | None) -> None:
|
|
14
14
|
super().__init__()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.binding import Binding
|
|
3
|
+
from textual.screen import ModalScreen
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LeavePrompt(ModalScreen[str | None]):
|
|
8
|
+
CSS = """
|
|
9
|
+
LeavePrompt { align: center middle; }
|
|
10
|
+
#prompt { width: auto; padding: 1 2; border: round $accent; }
|
|
11
|
+
"""
|
|
12
|
+
BINDINGS = [
|
|
13
|
+
Binding("enter,ctrl+c", "dismiss('leave')", "Leave running"),
|
|
14
|
+
Binding("s", "dismiss('stop')", "Stop them"),
|
|
15
|
+
Binding("escape", "dismiss", "Cancel"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def __init__(self, count: int) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.count = count
|
|
21
|
+
|
|
22
|
+
def compose(self) -> ComposeResult:
|
|
23
|
+
runs = "run" if self.count == 1 else "runs"
|
|
24
|
+
yield Static(
|
|
25
|
+
f"{self.count} {runs} still going. "
|
|
26
|
+
"Leave them running (enter) / stop them (s) / cancel (esc).",
|
|
27
|
+
id="prompt",
|
|
28
|
+
)
|
|
@@ -10,10 +10,11 @@ from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
|
10
10
|
|
|
11
11
|
from labnotes.models import Controls, Start
|
|
12
12
|
from labnotes.runner.describe_event import describe_event
|
|
13
|
-
from labnotes.runner.stop_then_kill import stop_then_kill
|
|
13
|
+
from labnotes.runner.stop_then_kill import GRACE_SECONDS, stop_then_kill
|
|
14
14
|
from labnotes.runner.param_args import param_args
|
|
15
15
|
from labnotes.tui.dim import dim
|
|
16
16
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
17
|
+
from labnotes.tui.leave_prompt import LeavePrompt
|
|
17
18
|
from labnotes.tui.run_output import RunOutput
|
|
18
19
|
from labnotes.tui.runs_table import RunsTable
|
|
19
20
|
|
|
@@ -29,7 +30,7 @@ GRABBING_ACTIONS = {*GRABBED_ACTIONS, "grab", "quit", "interrupt", "command_pale
|
|
|
29
30
|
class RunApp(App[None]):
|
|
30
31
|
CSS = "#concurrency { dock: right; width: auto; padding: 0 1; }"
|
|
31
32
|
BINDINGS = [
|
|
32
|
-
Binding("ctrl+c", "interrupt", "
|
|
33
|
+
Binding("ctrl+c", "interrupt", "Quit"),
|
|
33
34
|
Binding("n", "run_next", "Run next"),
|
|
34
35
|
Binding("m", "grab", "Move"),
|
|
35
36
|
Binding("j,down", "shift(1)", priority=True),
|
|
@@ -45,7 +46,6 @@ class RunApp(App[None]):
|
|
|
45
46
|
Binding("escape", "leave", "Back"),
|
|
46
47
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
47
48
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
48
|
-
Binding("q", "quit", "Quit"),
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
def __init__(self, start: Start, title: str) -> None:
|
|
@@ -111,7 +111,7 @@ class RunApp(App[None]):
|
|
|
111
111
|
|
|
112
112
|
def listen(self, controls: Controls) -> None:
|
|
113
113
|
self.controls = controls
|
|
114
|
-
stop_then_kill(self.
|
|
114
|
+
stop_then_kill(self.stop_runs, signal.SIGTERM)
|
|
115
115
|
asyncio.get_running_loop().add_signal_handler(signal.SIGHUP, self.exit)
|
|
116
116
|
|
|
117
117
|
def show_event(self, event: dict[str, object]) -> None:
|
|
@@ -200,7 +200,7 @@ class RunApp(App[None]):
|
|
|
200
200
|
def show_summary(self) -> None:
|
|
201
201
|
failed = len(self.failed)
|
|
202
202
|
passed = len(self.ended) - failed
|
|
203
|
-
self.show(f"{passed} passed, {failed} failed.
|
|
203
|
+
self.show(f"{passed} passed, {failed} failed. Ctrl-C to quit.")
|
|
204
204
|
|
|
205
205
|
def reposition(self, event: dict[str, object], run: str) -> None:
|
|
206
206
|
table = self.query_one(DataTable)
|
|
@@ -231,6 +231,8 @@ class RunApp(App[None]):
|
|
|
231
231
|
table.update_cell(run, column, cell, update_width=True)
|
|
232
232
|
|
|
233
233
|
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
234
|
+
if isinstance(self.screen, LeavePrompt):
|
|
235
|
+
return False
|
|
234
236
|
if self.grabbed is not None:
|
|
235
237
|
return action in GRABBING_ACTIONS
|
|
236
238
|
if action in GRABBED_ACTIONS:
|
|
@@ -254,18 +256,30 @@ class RunApp(App[None]):
|
|
|
254
256
|
self.next_run = None
|
|
255
257
|
self.query_one("#status", Static).update(text)
|
|
256
258
|
|
|
257
|
-
def
|
|
259
|
+
def stop_runs(self) -> None:
|
|
258
260
|
if self.idle:
|
|
259
261
|
self.controls.finish()
|
|
260
262
|
else:
|
|
261
263
|
self.controls.interrupt()
|
|
262
264
|
|
|
263
|
-
|
|
264
|
-
if self.
|
|
265
|
+
def action_interrupt(self) -> None:
|
|
266
|
+
if self.started:
|
|
267
|
+
self.push_screen(LeavePrompt(len(self.started)), self.answer)
|
|
268
|
+
elif self.idle:
|
|
265
269
|
self.controls.finish()
|
|
266
270
|
else:
|
|
267
271
|
self.exit()
|
|
268
272
|
|
|
273
|
+
async def action_quit(self) -> None:
|
|
274
|
+
self.action_interrupt()
|
|
275
|
+
|
|
276
|
+
def answer(self, choice: str | None) -> None:
|
|
277
|
+
if choice == "leave":
|
|
278
|
+
self.exit()
|
|
279
|
+
elif choice == "stop":
|
|
280
|
+
self.controls.interrupt()
|
|
281
|
+
self.set_timer(GRACE_SECONDS, self.controls.interrupt)
|
|
282
|
+
|
|
269
283
|
def cursor_run(self) -> str | None:
|
|
270
284
|
table = self.query_one(DataTable)
|
|
271
285
|
if not table.row_count:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|