labnotes 0.0.84__tar.gz → 0.0.86__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 (154) hide show
  1. {labnotes-0.0.84 → labnotes-0.0.86}/PKG-INFO +2 -2
  2. {labnotes-0.0.84 → labnotes-0.0.86}/README.md +1 -1
  3. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/context.md +4 -2
  4. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/models.py +1 -0
  5. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/run_control.py +2 -1
  6. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/describe_event.py +4 -0
  7. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/run_experiment.py +12 -1
  8. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/experiment_screen.py +2 -2
  9. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/run_app.py +79 -4
  10. {labnotes-0.0.84 → labnotes-0.0.86}/.gitignore +0 -0
  11. {labnotes-0.0.84 → labnotes-0.0.86}/CHANGELOG.md +0 -0
  12. {labnotes-0.0.84 → labnotes-0.0.86}/MIGRATIONS.md +0 -0
  13. {labnotes-0.0.84 → labnotes-0.0.86}/pyproject.toml +0 -0
  14. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/__init__.py +0 -0
  15. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/__main__.py +0 -0
  16. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/asset_url.py +0 -0
  17. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/check_counts.py +0 -0
  18. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/cli.py +0 -0
  19. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/context_guide.py +0 -0
  20. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/count_groups.py +0 -0
  21. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/count_runs.py +0 -0
  22. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/create_app.py +0 -0
  23. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/create_experiment.py +0 -0
  24. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/create_note.py +0 -0
  25. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/__init__.py +0 -0
  26. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/control_run.py +0 -0
  27. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/daemon_app.py +0 -0
  28. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/daemon_client.py +0 -0
  29. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/daemon_running.py +0 -0
  30. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/daemon_server.py +0 -0
  31. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/describe_status.py +0 -0
  32. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/ensure_daemon.py +0 -0
  33. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/exit_when_idle.py +0 -0
  34. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/remote_start.py +0 -0
  35. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/run_daemon.py +0 -0
  36. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/run_logs.py +0 -0
  37. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/serve_daemon.py +0 -0
  38. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/socket_path.py +0 -0
  39. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/stop_runs.py +0 -0
  40. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/watch_daemon.py +0 -0
  41. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon/watch_status.py +0 -0
  42. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/daemon_command.py +0 -0
  43. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/data_html.py +0 -0
  44. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/default_config_text.py +0 -0
  45. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/default_counts.py +0 -0
  46. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/drain_watch.py +0 -0
  47. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/experiment_notes.py +0 -0
  48. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/experiment_runs.py +0 -0
  49. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/experiments.py +0 -0
  50. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/file_changes.py +0 -0
  51. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/format_duration.py +0 -0
  52. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/format_started.py +0 -0
  53. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/format_value.py +0 -0
  54. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/group_runs.py +0 -0
  55. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/init.py +0 -0
  56. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/invocation_started.py +0 -0
  57. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/is_positive_int.py +0 -0
  58. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/list_experiments.py +0 -0
  59. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/list_runs.py +0 -0
  60. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/load_config.py +0 -0
  61. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/load_experiment.py +0 -0
  62. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/load_run.py +0 -0
  63. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/modes.py +0 -0
  64. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/nests_runs.py +0 -0
  65. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/note_path.py +0 -0
  66. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/notebook_kernel.py +0 -0
  67. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/output_html.py +0 -0
  68. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/parse_run_view.py +0 -0
  69. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/print_rows.py +0 -0
  70. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/python_blocks.py +0 -0
  71. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/read_config.py +0 -0
  72. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/read_path.py +0 -0
  73. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/render_markdown.py +0 -0
  74. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/run_command.py +0 -0
  75. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/run_output.py +0 -0
  76. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/run_status.py +0 -0
  77. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/__init__.py +0 -0
  78. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/execute_run.py +0 -0
  79. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/hold_folder.py +0 -0
  80. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/new_param_run_id.py +0 -0
  81. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/new_run_id.py +0 -0
  82. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  83. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/param_args.py +0 -0
  84. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/param_grid.py +0 -0
  85. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/param_run_name.py +0 -0
  86. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/plan_runs.py +0 -0
  87. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/prepare_experiment.py +0 -0
  88. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/read_experiment_config.py +0 -0
  89. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/run_folder.py +0 -0
  90. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/run_headless.py +0 -0
  91. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/runs_parent.py +0 -0
  92. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/shell_command.py +0 -0
  93. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/signal_tree.py +0 -0
  94. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/stop_then_kill.py +0 -0
  95. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/wait_for_tree.py +0 -0
  96. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/runner/write_run_json.py +0 -0
  97. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/save_conflict_copy.py +0 -0
  98. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/save_note.py +0 -0
  99. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/saved_outputs.py +0 -0
  100. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/serve_app.py +0 -0
  101. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/server.py +0 -0
  102. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/shared_params.py +0 -0
  103. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/sort_href.py +0 -0
  104. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/sql_quote.py +0 -0
  105. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/elapsed.js +0 -0
  106. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/experiment.js +0 -0
  107. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/labnotes.css +0 -0
  108. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/local-time.js +0 -0
  109. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/new-note.js +0 -0
  110. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/note-edit.js +0 -0
  111. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/note-editor.js +0 -0
  112. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/row-link.js +0 -0
  113. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/run-cell.js +0 -0
  114. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/run-output.js +0 -0
  115. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/static/view-toggle.js +0 -0
  116. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/summarize_config.py +0 -0
  117. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/__init__.py +0 -0
  118. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/experiment_rows.py +0 -0
  119. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/experiment_run_ids.py +0 -0
  120. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/file_prefix.py +0 -0
  121. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/file_text.py +0 -0
  122. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/iso_time.py +0 -0
  123. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/list_experiment_runs.py +0 -0
  124. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/note_text.py +0 -0
  125. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/open_tables.py +0 -0
  126. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/path_glob.py +0 -0
  127. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/path_pattern.py +0 -0
  128. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/read_experiment.py +0 -0
  129. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/read_file.py +0 -0
  130. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/read_log.py +0 -0
  131. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/read_run.py +0 -0
  132. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tables/summarize_experiments.py +0 -0
  133. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/templates/base.html +0 -0
  134. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/templates/editor_imports.html +0 -0
  135. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/templates/experiment.html +0 -0
  136. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/templates/index.html +0 -0
  137. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/templates/note.html +0 -0
  138. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/templates/run.html +0 -0
  139. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/__init__.py +0 -0
  140. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/browse_app.py +0 -0
  141. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/browse_tui.py +0 -0
  142. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/dim.py +0 -0
  143. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/experiments_screen.py +0 -0
  144. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/format_elapsed.py +0 -0
  145. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/group_cells.py +0 -0
  146. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/invocation_columns.py +0 -0
  147. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/invocation_rows.py +0 -0
  148. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/run_cells.py +0 -0
  149. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/run_tui.py +0 -0
  150. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/tui/runs_table.py +0 -0
  151. {labnotes-0.0.84 → labnotes-0.0.86}/src/labnotes/wait_for_note.py +0 -0
  152. {labnotes-0.0.84 → labnotes-0.0.86}/testing-conventions.toml +0 -0
  153. {labnotes-0.0.84 → labnotes-0.0.86}/tests/integration/conftest.py +0 -0
  154. {labnotes-0.0.84 → labnotes-0.0.86}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.84
3
+ Version: 0.0.86
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>
@@ -71,7 +71,7 @@ labnotes experiments foo runs # foo's runs, oldest first
71
71
  Each takes `--format table` or `--format json`, which print rows and exit.
72
72
  Without `--format`, `labnotes experiments` and `labnotes experiments foo` open
73
73
  a browser in a terminal: the experiment list, Enter to open one, Escape to go
74
- back. An experiment shows its runs grouped by invocation; Space opens one.
74
+ back. An experiment shows its runs grouped by invocation; Enter expands or collapses one.
75
75
  Piped, they print a table.
76
76
 
77
77
  `labnotes experiments new [NAME]` creates an experiment folder with a starter
@@ -47,7 +47,7 @@ labnotes experiments foo runs # foo's runs, oldest first
47
47
  Each takes `--format table` or `--format json`, which print rows and exit.
48
48
  Without `--format`, `labnotes experiments` and `labnotes experiments foo` open
49
49
  a browser in a terminal: the experiment list, Enter to open one, Escape to go
50
- back. An experiment shows its runs grouped by invocation; Space opens one.
50
+ back. An experiment shows its runs grouped by invocation; Enter expands or collapses one.
51
51
  Piped, they print a table.
52
52
 
53
53
  `labnotes experiments new [NAME]` creates an experiment folder with a starter
@@ -100,7 +100,8 @@ open when they finish. Failed runs show their status in red; `f` hides or shows
100
100
  runs it again under a new id. `j` and `k` move the cursor, `g` and `G` jump to the
101
101
  first and last row. Runs not started yet show grayed as `pending`, and as
102
102
  `skipped` once stopped. Move the cursor to a pending run and press `n` to run it
103
- next. Space deselects a pending run so it won't run; Space again puts it back at
103
+ next. `m` grabs a pending run; `j` and `k` move it through the queue, Enter or `m`
104
+ drops it, Escape puts it back. Space deselects a pending run so it won't run; Space again puts it back at
104
105
  the end of the queue. `/` filters rows by params text; Enter keeps the filter,
105
106
  Escape clears it. `D` deselects every pending run the filter shows. `x` stops the
106
107
  running run under the cursor; `x` again kills it. `+` and
@@ -129,7 +130,8 @@ going. Elsewhere, it prints a line per event instead.
129
130
  waits for a daemon when none is up. `--json` prints one JSON object per line.
130
131
  Following doesn't keep the daemon up.
131
132
  - `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
132
- running experiment: `kill`, `retry`, `run_next` or `toggle` a run,
133
+ running experiment: `kill`, `retry`, `run_next` or `toggle` a run, `move` a
134
+ run to a place in the queue counted from 0,
133
135
  `set_concurrency` to a number, `interrupt` or `finish`.
134
136
  - A second `run` of the same experiment fails while the first is going.
135
137
  - It exits 0 even if runs fail; the summary line counts failures. If it can't
@@ -18,6 +18,7 @@ class Controls:
18
18
  kill: Callable[[str], None]
19
19
  retry: Callable[[str], None]
20
20
  finish: Callable[[], None]
21
+ move: Callable[[str, int], None]
21
22
 
22
23
 
23
24
  Listen = Callable[[Controls], None]
@@ -18,7 +18,8 @@ PARAMS = {
18
18
  name="control",
19
19
  help=(
20
20
  "Send one control to an experiment's runs, as the live view's keys do. "
21
- "kill, retry, run_next and toggle take a run; set_concurrency a number; "
21
+ "kill, retry, run_next and toggle take a run; move a run and its new place "
22
+ "in the queue, from 0; set_concurrency a number; "
22
23
  "interrupt and finish nothing."
23
24
  ),
24
25
  )
@@ -14,6 +14,10 @@ def describe_event(event: dict[str, object]) -> str:
14
14
  return f"{event['run']} {outcome}"
15
15
  case "queued":
16
16
  return f"{event['run']} runs next."
17
+ case "moved":
18
+ pending: list[str] = event["pending"] # type: ignore[assignment]
19
+ place = pending.index(str(event["run"])) + 1
20
+ return f"{event['run']} moved to {place} of {len(pending)} in the queue."
17
21
  case "deselected":
18
22
  return f"{event['run']} deselected."
19
23
  case "reselected":
@@ -125,6 +125,13 @@ async def run_experiment(
125
125
  plan.insert(0, entry)
126
126
  arrange("queued", run)
127
127
 
128
+ def move(run: str, place: int) -> None:
129
+ entry = find(plan, run)
130
+ if entry is not None:
131
+ plan.remove(entry)
132
+ plan.insert(max(0, place), entry)
133
+ arrange("moved", run)
134
+
128
135
  def toggle(run: str) -> None:
129
136
  if stopping.is_set():
130
137
  return
@@ -202,7 +209,11 @@ async def run_experiment(
202
209
  ended[run] = params
203
210
  results.append(result)
204
211
 
205
- listen(Controls(interrupt, run_next, set_concurrency, toggle, kill, retry, finish))
212
+ listen(
213
+ Controls(
214
+ interrupt, run_next, set_concurrency, toggle, kill, retry, finish, move
215
+ )
216
+ )
206
217
  fill()
207
218
  await done.wait()
208
219
  await asyncio.gather(*started)
@@ -17,7 +17,7 @@ from labnotes.tui.runs_table import RunsTable
17
17
 
18
18
  class ExperimentScreen(Screen[None]):
19
19
  BINDINGS = [
20
- Binding("space", "open_invocation", "Open invocation"),
20
+ Binding("enter", "toggle_invocation", "Expand", priority=True),
21
21
  Binding("escape", "app.pop_screen", "Back"),
22
22
  ]
23
23
 
@@ -65,7 +65,7 @@ class ExperimentScreen(Screen[None]):
65
65
  table.add_row(*cells, key=key)
66
66
  table.move_cursor(row=row)
67
67
 
68
- def action_open_invocation(self) -> None:
68
+ def action_toggle_invocation(self) -> None:
69
69
  table = self.query_one(DataTable)
70
70
  if not table.row_count:
71
71
  return
@@ -23,6 +23,8 @@ PENDING, PASSED, FAILED = "○", "✓", "✗"
23
23
  SPINNER = "|/-\\"
24
24
  SKIPPED, DESELECTED = "–", "⊘"
25
25
  VIEWING_ACTIONS = {"leave", "quit", "interrupt", "command_palette"}
26
+ GRABBED_ACTIONS = {"shift", "drop", "put_back"}
27
+ GRABBING_ACTIONS = {*GRABBED_ACTIONS, "grab", "quit", "interrupt", "command_palette"}
26
28
 
27
29
 
28
30
  class RunApp(App[None]):
@@ -30,6 +32,11 @@ class RunApp(App[None]):
30
32
  BINDINGS = [
31
33
  Binding("ctrl+c", "interrupt", "Stop runs"),
32
34
  Binding("n", "run_next", "Run next"),
35
+ Binding("m", "grab", "Move"),
36
+ Binding("j,down", "shift(1)", priority=True),
37
+ Binding("k,up", "shift(-1)", priority=True),
38
+ Binding("enter", "drop", "Drop", priority=True),
39
+ Binding("escape", "put_back", "Put back"),
33
40
  Binding("space", "toggle_run", "Deselect"),
34
41
  Binding("D", "deselect_shown", "Deselect shown"),
35
42
  Binding("x", "kill_run", "Stop run"),
@@ -56,6 +63,7 @@ class RunApp(App[None]):
56
63
  lambda run: None,
57
64
  lambda run: None,
58
65
  lambda: None,
66
+ lambda run, place: None,
59
67
  )
60
68
  self.concurrency = 1
61
69
  self.idle = False
@@ -70,6 +78,8 @@ class RunApp(App[None]):
70
78
  self.hiding = False
71
79
  self.folders: dict[str, Path] = {}
72
80
  self.viewing: str | None = None
81
+ self.grabbed: str | None = None
82
+ self.grabbed_from: list[str] = []
73
83
  self.offsets: dict[str, int]
74
84
  self.decoders = {
75
85
  stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
@@ -129,6 +139,8 @@ class RunApp(App[None]):
129
139
  case "adhoc", [only]:
130
140
  self.view(only["run"]) # type: ignore[index]
131
141
  case "started":
142
+ if run == self.grabbed:
143
+ self.action_put_back()
132
144
  self.idle = False
133
145
  self.pending.discard(run)
134
146
  self.started[run] = time.monotonic()
@@ -158,10 +170,10 @@ class RunApp(App[None]):
158
170
  del self.started[run]
159
171
  self.redraw(table.cursor_row)
160
172
  case "queued":
161
- self.reorder(event, table.cursor_row)
162
- table.move_cursor(row=table.get_row_index(run))
163
- self.show(describe_event(event))
173
+ self.reposition(event, run)
164
174
  self.next_run = run
175
+ case "moved":
176
+ self.reposition(event, run)
165
177
  case "deselected":
166
178
  self.pending.discard(run)
167
179
  self.set_cell(run, "status", dim(DESELECTED))
@@ -198,7 +210,14 @@ class RunApp(App[None]):
198
210
  passed = len(self.ended) - failed
199
211
  self.show(f"{passed} passed, {failed} failed. q to quit.")
200
212
 
213
+ def reposition(self, event: dict[str, object], run: str) -> None:
214
+ table = self.query_one(DataTable)
215
+ self.reorder(event, table.cursor_row)
216
+ table.move_cursor(row=table.get_row_index(run))
217
+ self.show(describe_event(event))
218
+
201
219
  def reorder(self, event: dict[str, object], row: int) -> None:
220
+ self.release()
202
221
  waiting: list[str] = [*event["pending"], *event["held"]] # type: ignore[misc]
203
222
  for run in waiting:
204
223
  self.cells[run] = self.cells.pop(run)
@@ -220,6 +239,10 @@ class RunApp(App[None]):
220
239
  table.update_cell(run, column, cell, update_width=True)
221
240
 
222
241
  def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
242
+ if self.grabbed is not None:
243
+ return action in GRABBING_ACTIONS
244
+ if action in GRABBED_ACTIONS:
245
+ return False
223
246
  return self.viewing is None or action in VIEWING_ACTIONS
224
247
 
225
248
  def show_concurrency(self, event: dict[str, object]) -> None:
@@ -262,6 +285,57 @@ class RunApp(App[None]):
262
285
  if (run := self.cursor_run()) is not None:
263
286
  self.controls.run_next(run)
264
287
 
288
+ def queue(self) -> list[str]:
289
+ return [run for run in self.cells if run in self.pending]
290
+
291
+ def action_grab(self) -> None:
292
+ if self.grabbed is not None:
293
+ self.action_drop()
294
+ return
295
+ run = self.cursor_run()
296
+ if run not in self.pending:
297
+ return
298
+ self.grabbed = run
299
+ self.grabbed_from = list(self.cells)
300
+ self.refresh_bindings()
301
+ self.show(f"Moving {run}. j/k moves it, Enter drops it, Escape puts it back.")
302
+
303
+ def action_shift(self, step: int) -> None:
304
+ run = str(self.grabbed)
305
+ queue = self.queue()
306
+ place = queue.index(run) + step
307
+ if place not in range(len(queue)):
308
+ return
309
+ order = list(self.cells)
310
+ here, there = order.index(run), order.index(queue[place])
311
+ order[here], order[there] = order[there], order[here]
312
+ self.cells = {key: self.cells[key] for key in order}
313
+ self.redraw_on(run)
314
+
315
+ def action_drop(self) -> None:
316
+ run = str(self.grabbed)
317
+ self.release()
318
+ self.show("")
319
+ self.controls.move(run, self.queue().index(run))
320
+
321
+ def action_put_back(self) -> None:
322
+ run = str(self.grabbed)
323
+ self.release()
324
+ places = {key: place for place, key in enumerate(self.grabbed_from)}
325
+ ordered = sorted(self.cells, key=lambda key: places.get(key, len(places)))
326
+ self.cells = {key: self.cells[key] for key in ordered}
327
+ self.redraw_on(run)
328
+ self.show("")
329
+
330
+ def release(self) -> None:
331
+ self.grabbed = None
332
+ self.refresh_bindings()
333
+
334
+ def redraw_on(self, run: str) -> None:
335
+ table = self.query_one(DataTable)
336
+ self.redraw(table.cursor_row)
337
+ table.move_cursor(row=table.get_row_index(run))
338
+
265
339
  def action_toggle_run(self) -> None:
266
340
  if (run := self.cursor_run()) is not None:
267
341
  self.controls.toggle(run)
@@ -294,7 +368,8 @@ class RunApp(App[None]):
294
368
  field.focus()
295
369
 
296
370
  def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
297
- self.view(str(event.row_key.value))
371
+ if self.grabbed is None:
372
+ self.view(str(event.row_key.value))
298
373
 
299
374
  def view(self, run: str) -> None:
300
375
  self.viewing = run
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes