labnotes 0.0.96__tar.gz → 0.0.98__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.
Files changed (163) hide show
  1. {labnotes-0.0.96 → labnotes-0.0.98}/PKG-INFO +5 -5
  2. {labnotes-0.0.96 → labnotes-0.0.98}/README.md +4 -4
  3. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/run-output.js +10 -2
  4. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/templates/run.html +1 -1
  5. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/browse_app.py +1 -1
  6. labnotes-0.0.98/src/labnotes/tui/leave_prompt.py +28 -0
  7. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/run_app.py +22 -8
  8. {labnotes-0.0.96 → labnotes-0.0.98}/.gitignore +0 -0
  9. {labnotes-0.0.96 → labnotes-0.0.98}/CHANGELOG.md +0 -0
  10. {labnotes-0.0.96 → labnotes-0.0.98}/MIGRATIONS.md +0 -0
  11. {labnotes-0.0.96 → labnotes-0.0.98}/pyproject.toml +0 -0
  12. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/__init__.py +0 -0
  13. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/__main__.py +0 -0
  14. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/asset_url.py +0 -0
  15. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/check_counts.py +0 -0
  16. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/cli.py +0 -0
  17. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/context.md +0 -0
  18. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/context_guide.py +0 -0
  19. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/count_groups.py +0 -0
  20. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/count_runs.py +0 -0
  21. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/create_app.py +0 -0
  22. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/create_experiment.py +0 -0
  23. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/create_note.py +0 -0
  24. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/__init__.py +0 -0
  25. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/control_run.py +0 -0
  26. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/daemon_app.py +0 -0
  27. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/daemon_client.py +0 -0
  28. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/daemon_running.py +0 -0
  29. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/daemon_server.py +0 -0
  30. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/describe_status.py +0 -0
  31. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/ensure_daemon.py +0 -0
  32. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/exit_when_idle.py +0 -0
  33. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/remote_start.py +0 -0
  34. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/run_daemon.py +0 -0
  35. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/run_logs.py +0 -0
  36. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/serve_daemon.py +0 -0
  37. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/socket_path.py +0 -0
  38. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/stop_runs.py +0 -0
  39. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/watch_daemon.py +0 -0
  40. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon/watch_status.py +0 -0
  41. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/daemon_command.py +0 -0
  42. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/data_html.py +0 -0
  43. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/default_config_text.py +0 -0
  44. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/default_counts.py +0 -0
  45. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/drain_watch.py +0 -0
  46. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/experiment_notes.py +0 -0
  47. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/experiment_runs.py +0 -0
  48. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/experiments.py +0 -0
  49. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/file_changes.py +0 -0
  50. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/format_duration.py +0 -0
  51. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/format_started.py +0 -0
  52. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/format_value.py +0 -0
  53. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/group_runs.py +0 -0
  54. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/init.py +0 -0
  55. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/invocation_started.py +0 -0
  56. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/is_positive_int.py +0 -0
  57. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/list_experiments.py +0 -0
  58. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/list_runs.py +0 -0
  59. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/load_config.py +0 -0
  60. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/load_experiment.py +0 -0
  61. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/load_run.py +0 -0
  62. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/log.py +0 -0
  63. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/log_path.py +0 -0
  64. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/models.py +0 -0
  65. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/modes.py +0 -0
  66. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/nests_runs.py +0 -0
  67. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/note_path.py +0 -0
  68. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/notebook_kernel.py +0 -0
  69. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/output_html.py +0 -0
  70. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/parse_run_view.py +0 -0
  71. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/print_rows.py +0 -0
  72. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/python_blocks.py +0 -0
  73. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/read_config.py +0 -0
  74. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/read_path.py +0 -0
  75. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/render_markdown.py +0 -0
  76. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/run_command.py +0 -0
  77. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/run_control.py +0 -0
  78. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/run_output.py +0 -0
  79. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/run_status.py +0 -0
  80. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/__init__.py +0 -0
  81. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/describe_event.py +0 -0
  82. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/execute_run.py +0 -0
  83. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/hold_folder.py +0 -0
  84. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/new_param_run_id.py +0 -0
  85. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/new_run_id.py +0 -0
  86. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  87. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/param_args.py +0 -0
  88. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/param_grid.py +0 -0
  89. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/param_run_name.py +0 -0
  90. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/plan_runs.py +0 -0
  91. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/prepare_experiment.py +0 -0
  92. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/read_experiment_config.py +0 -0
  93. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/run_experiment.py +0 -0
  94. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/run_folder.py +0 -0
  95. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/run_headless.py +0 -0
  96. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/runs_parent.py +0 -0
  97. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/shell_command.py +0 -0
  98. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/signal_tree.py +0 -0
  99. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/stop_then_kill.py +0 -0
  100. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/wait_for_tree.py +0 -0
  101. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/runner/write_run_json.py +0 -0
  102. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/save_conflict_copy.py +0 -0
  103. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/save_note.py +0 -0
  104. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/saved_outputs.py +0 -0
  105. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/serve_app.py +0 -0
  106. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/server.py +0 -0
  107. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/shared_params.py +0 -0
  108. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/sort_href.py +0 -0
  109. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/sql_quote.py +0 -0
  110. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/elapsed.js +0 -0
  111. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/experiment.js +0 -0
  112. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/labnotes.css +0 -0
  113. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/local-time.js +0 -0
  114. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/new-note.js +0 -0
  115. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/note-edit.js +0 -0
  116. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/note-editor.js +0 -0
  117. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/row-link.js +0 -0
  118. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/run-cell.js +0 -0
  119. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/static/view-toggle.js +0 -0
  120. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/summarize_config.py +0 -0
  121. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/__init__.py +0 -0
  122. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/experiment_rows.py +0 -0
  123. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/experiment_run_ids.py +0 -0
  124. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/file_prefix.py +0 -0
  125. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/file_text.py +0 -0
  126. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/iso_time.py +0 -0
  127. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/list_experiment_runs.py +0 -0
  128. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/note_text.py +0 -0
  129. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/open_tables.py +0 -0
  130. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/path_glob.py +0 -0
  131. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/path_pattern.py +0 -0
  132. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/read_experiment.py +0 -0
  133. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/read_file.py +0 -0
  134. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/read_log.py +0 -0
  135. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/read_run.py +0 -0
  136. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tables/summarize_experiments.py +0 -0
  137. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/templates/base.html +0 -0
  138. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/templates/editor_imports.html +0 -0
  139. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/templates/experiment.html +0 -0
  140. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/templates/index.html +0 -0
  141. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/templates/note.html +0 -0
  142. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/__init__.py +0 -0
  143. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/browse_tui.py +0 -0
  144. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/dim.py +0 -0
  145. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/experiment_screen.py +0 -0
  146. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/experiments_screen.py +0 -0
  147. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/fill_table.py +0 -0
  148. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/format_elapsed.py +0 -0
  149. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/group_cells.py +0 -0
  150. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/invocation_columns.py +0 -0
  151. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/invocation_rows.py +0 -0
  152. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/reload_on_change.py +0 -0
  153. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/run_cells.py +0 -0
  154. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/run_output.py +0 -0
  155. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/run_screen.py +0 -0
  156. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/run_tui.py +0 -0
  157. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/runs_table.py +0 -0
  158. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/tui/stopped_title.py +0 -0
  159. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/wait_for_note.py +0 -0
  160. {labnotes-0.0.96 → labnotes-0.0.98}/src/labnotes/watch_rows.py +0 -0
  161. {labnotes-0.0.96 → labnotes-0.0.98}/testing-conventions.toml +0 -0
  162. {labnotes-0.0.96 → labnotes-0.0.98}/tests/integration/conftest.py +0 -0
  163. {labnotes-0.0.96 → labnotes-0.0.98}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.96
3
+ Version: 0.0.98
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; `q` quits. Elsewhere, it prints a
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 stops the running scripts and records their end. A second Ctrl-C
117
- kills them. Closing the terminal or sending SIGTERM stops them the same way,
118
- then kills any still going 5 seconds later.
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; `q` quits. Elsewhere, it prints a
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 stops the running scripts and records their end. A second Ctrl-C
93
- kills them. Closing the terminal or sending SIGTERM stops them the same way,
94
- then kills any still going 5 seconds later.
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.
@@ -1,16 +1,24 @@
1
+ import { AnsiUp } from "https://esm.sh/ansi_up@6.0.6";
2
+
1
3
  const output = document.querySelector(".run-output");
2
4
  const state = document.querySelector(".run-state");
3
5
  const source = new EventSource(output.dataset.url);
4
6
 
7
+ let parsers = {};
8
+
5
9
  // The server resends the whole log on each connection, so a reconnect starts clean.
6
- source.addEventListener("open", () => output.replaceChildren());
10
+ source.addEventListener("open", () => {
11
+ output.replaceChildren();
12
+ parsers = {};
13
+ });
7
14
 
8
15
  source.addEventListener("message", (event) => {
9
16
  const { stream, text } = JSON.parse(event.data);
10
17
  const following = output.scrollTop + output.clientHeight >= output.scrollHeight - 4;
11
18
  const chunk = document.createElement("span");
12
19
  chunk.className = stream;
13
- chunk.textContent = text;
20
+ parsers[stream] ??= new AnsiUp();
21
+ chunk.innerHTML = parsers[stream].ansi_to_html(text);
14
22
  output.append(chunk);
15
23
  if (following) output.scrollTop = output.scrollHeight;
16
24
  });
@@ -15,5 +15,5 @@
15
15
  <header><span class="stdout">stdout</span><span class="stderr">stderr</span></header>
16
16
  <pre class="run-output" data-url="{{ url_for('output', name=run.experiment, run=run.id) }}"></pre>
17
17
  </section>
18
- <script src="{{ asset('run-output.js') }}" defer></script>
18
+ <script type="module" src="{{ asset('run-output.js') }}"></script>
19
19
  {% endblock %}
@@ -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("q", "quit", "Quit")]
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", "Stop runs"),
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.action_interrupt, signal.SIGTERM)
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. q to quit.")
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 action_interrupt(self) -> None:
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
- async def action_quit(self) -> None:
264
- if self.idle:
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