screenbuffer 0.1.0__tar.gz → 1.0.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: screenbuffer
3
- Version: 0.1.0
3
+ Version: 1.0.1
4
4
  Summary: A Python utility for managing dynamic output, tracking progress, and organizing information throughout data-driven workflows.
5
5
  Keywords: jupyter,notebook,progress,logging,display,data
6
6
  Classifier: Programming Language :: Python :: 3
@@ -14,45 +14,52 @@ Requires-Dist: ipython; extra == "jupyter"
14
14
 
15
15
  # ScreenBuffer
16
16
 
17
- **A Python utility for managing dynamic output, tracking progress, and organizing information throughout data-driven workflows.**
17
+ **A live output panel for Jupyter: a pinned header, a body that shows the newest N lines, and a footer for anything you want. Change any part, any time, and the rest stays exactly where it is.**
18
18
 
19
19
  Long-running loops (scrapers, ETL jobs, model sweeps, simulations, data pulls) leave you two bad choices in Jupyter:
20
20
 
21
21
  - `print()` every step: the cell fills with thousands of lines you cannot read.
22
22
  - `clear_output()` every step: you lose everything, including the parts you wanted to keep (a progress header, a running total, the last error).
23
23
 
24
- ScreenBuffer keeps the output as a **list of lines** and redraws the whole panel from that list. You change one line, the header, or the footer; the rest stays on screen. Every line is also appended to a log file, so the screen can stay small while the full history is kept.
24
+ ScreenBuffer is the third choice. It keeps the output as a **list of numbered lines** and redraws the whole panel from that list. You decide what lives at each index, and you rewrite a slot whenever you like. The rest of the screen never moves.
25
25
 
26
26
  ```python
27
27
  from screenbuffer import ScreenBuffer
28
28
 
29
- sb = ScreenBuffer(max_display=8, header="Scraper | page 0/200",
30
- log_file="scrape_run", max_wait=1000, start=True)
29
+ sb = ScreenBuffer(max_display=8, header="Scraper, page 0/200")
31
30
 
32
31
  for i, page in enumerate(pages, 1):
33
- sb.update(header=f"Scraper | page {i}/200")
32
+ sb.update(header=f"Scraper, page {i}/200")
34
33
  sb.put(f"page {i}: +{len(page)} rows")
35
34
  sb.display() # draw once per step
36
35
  ```
37
36
 
38
37
  The panel shows a fixed header, the newest 8 lines, and (optionally) a footer. Nothing else on screen moves.
39
38
 
39
+ That is the whole style: header, body, footer. [Header, body, footer](#header-body-footer) shows the different ways to play it.
40
+
40
41
  ---
41
42
 
42
43
  ## Who it is for
43
44
 
44
- Analysts, data engineers, data scientists and researchers who run long jobs in Jupyter and want to watch progress without flooding the cell, and who want a plain-text record of what happened.
45
+ Analysts, data engineers, data scientists and researchers who run long jobs in Jupyter and want to watch progress without flooding the cell, and who want a plain-text record of what happened if they ask for one.
45
46
 
46
- **Not** for: plain terminals or scripts outside Jupyter (it is Jupyter-only for now), rich dashboards (use `tqdm`, `rich`, or a BI tool), or log shipping (it writes local text files; sending them elsewhere is up to you).
47
+ ---
47
48
 
48
49
  ## Requirements
49
50
 
50
51
  - Python 3.8+ and Jupyter (Notebook, JupyterLab, VS Code, or a Jupyter console).
51
52
  - No third-party packages. Uses `IPython.display.clear_output`; if IPython is missing, drawing silently does nothing (the buffer and log still work).
52
53
 
54
+ ---
55
+
53
56
  ## Install
54
57
 
55
- Copy the `screenbuffer/` folder next to your work (or onto your `PYTHONPATH`), then:
58
+ ```
59
+ pip install screenbuffer
60
+ ```
61
+
62
+ In a notebook cell, use `%pip install screenbuffer` so it installs into the kernel's environment. Then:
56
63
 
57
64
  ```python
58
65
  from screenbuffer import ScreenBuffer
@@ -60,56 +67,122 @@ from screenbuffer import ScreenBuffer
60
67
 
61
68
  ---
62
69
 
63
- ## Create a buffer
70
+ ## Header, body, footer
64
71
 
65
- Everything starts here. All arguments are optional.
72
+ Every panel has three parts:
73
+
74
+ ```
75
+ header pinned at the top. Set it with header=, change it with sb.update(header=...)
76
+ body the lines you put(). max_display is how many of them are shown
77
+ footer pinned at the bottom. Anything you want: counters, status, ETA, an error banner
78
+ ```
79
+
80
+ The header and footer never scroll. The body is the window of `max_display` lines. You decide what goes in each part, and you can change any of them in the middle of a loop.
81
+
82
+ ### Ways to play
83
+
84
+ **1. Rolling body.** Append lines and let the window show the newest N. Use the footer for the running count.
66
85
 
67
86
  ```python
68
- sb = ScreenBuffer(
69
- wait=True, # False = redraw on every change
70
- log_file=None, # name for the log set; None = no files
71
- max_main=None, # history file line cap
72
- max_wait=None, # saved-buffer file line cap
73
- max_display=None, # lines drawn (newest N)
74
- start=False, # True = wipe existing files for this log_file
75
- header=None, header_alignment=None,
76
- footer=None, footer_alignment=None,
77
- show_header=None, show_footer=None,
78
- silent_display=None, only_display=None,
79
- auto_clean_edges=False,
80
- auto_clean_silent_only_display=False,
81
- shift_max_display=True,
82
- width=None, # None = detect terminal width
83
- )
87
+ sb = ScreenBuffer(max_display=8, width=70,
88
+ header="Scraper", header_alignment="center",
89
+ footer="starting", footer_alignment="center")
90
+
91
+ for i, page in enumerate(pages, 1):
92
+ sb.put(f"page {i}: +{len(page)} rows")
93
+ sb.update(footer=f"{i}/{len(pages)} pages")
94
+ sb.display() # one redraw per step
84
95
  ```
85
96
 
86
- | Argument | What it does | Details |
87
- |---|---|---|
88
- | `wait` | `True` (default): draw only when you call `display()`. `False`: redraw on every change | [Redraw modes](#redraw-modes) |
89
- | `log_file` | Name of the log set (creates `screen_buffer_logs/<name>.txt` and companions). `None` = nothing written to disk | [Log files](#log-files) |
90
- | `start` | `True` wipes existing files for that name; `False` restores the saved panel | [Persistence](#persistence-and-resume) |
91
- | `max_display` | Draw only the newest N lines | [Display window](#display-window) |
92
- | `max_main`, `max_wait` | Line caps for the history and saved-buffer files | [Limits](#limits-and-long-runs) |
93
- | `header`, `footer` | Fixed text above / below the lines | [Header and footer](#header-and-footer) |
94
- | `header_alignment`, `footer_alignment` | Alignment for those two | [Alignment](#alignment) |
95
- | `show_header`, `show_footer` | Hide or show them (default: shown when text is set) | [Header and footer](#header-and-footer) |
96
- | `silent_display`, `only_display` | Hide some lines / show only some lines | [Filtering](#filtering) |
97
- | `auto_clean_silent_only_display`, `shift_max_display`, `auto_clean_edges` | Fine-tune filtering and spacing | [Filtering](#filtering) |
98
- | `width` | Panel width in columns for `center` / `right`; `None` = detect | [Alignment](#alignment) |
97
+ **2. Slot body.** Treat the body as a few fixed slots and rewrite them by index. Writing to the highest index builds all the slots in one call, and `max_display` equals the number of slots, so every slot is always on screen.
99
98
 
100
- Typical setups:
99
+ ```python
100
+ sb = ScreenBuffer(max_display=3, header="Current job", footer="running")
101
+ sb.put("", index=2) # builds body slots 0, 1 and 2
102
+
103
+ for item in items:
104
+ sb.put(f"working on {item}", index=0) # slot 0: what is running now
105
+ try:
106
+ sb.put(f"last ok: {work(item)}", index=1) # slot 1: last success
107
+ except Exception as e:
108
+ sb.put(f"last error: {e}", index=2) # slot 2: last error
109
+ sb.display()
110
+ ```
111
+
112
+ **3. Ring of recent results.** Keep a small list and rewrite the slots each time. No deleting, and the newest result is always at the bottom.
101
113
 
102
114
  ```python
103
- sb = ScreenBuffer() # simplest: collect lines, then sb.display()
104
- sb = ScreenBuffer(wait=False, max_display=10) # live panel, newest 10 lines
105
- sb = ScreenBuffer(max_display=10, log_file="run1", max_wait=1000, start=True) # long job with a log
115
+ sb = ScreenBuffer(max_display=4, footer="0 found")
116
+ sb.put("", index=3)
117
+
118
+ recent = []
119
+ for n, item in enumerate(items, 1):
120
+ if hit(item):
121
+ recent = (recent + [str(item)])[-4:]
122
+ for k, text in enumerate(recent):
123
+ sb.put(text, index=k)
124
+ sb.update(footer=f"{n}/{len(items)} checked")
125
+ sb.display()
106
126
  ```
107
127
 
128
+ **4. Scoreboard.** One body slot per metric, rewritten every step.
129
+
130
+ ```python
131
+ sb = ScreenBuffer(max_display=3, header="Run stats")
132
+ sb.put("", index=2)
133
+ sb.put(f"rows: {rows}", index=0)
134
+ sb.put(f"errors: {errors}", index=1)
135
+ sb.put(f"speed: {speed}/s", index=2)
136
+ sb.display()
137
+ ```
138
+
139
+ **5. Footer as a banner.** The footer can carry a warning and then go back to normal.
140
+
141
+ ```python
142
+ sb.update(footer="!! network error, retrying !!")
143
+ sb.display()
144
+ # later
145
+ sb.update(footer="back online")
146
+ ```
147
+
148
+ ### Mixing and matching
149
+
150
+ | Move | How |
151
+ |---|---|
152
+ | Something that must always be visible | Put it in the header or footer |
153
+ | A rolling log | Append with `sb.put("text")`; `max_display` shows the newest N |
154
+ | Fixed slots | Build them with `put("", index=last)`, rewrite with `put(text, index=n)`, set `max_display` to the slot count |
155
+ | A spacer line | `sb.put("", index=n)` |
156
+ | Different looks per line | `alignment="center"`, `"right"`, or an indent |
157
+ | A rolling log plus a status footer | Append to the body and call `sb.update(footer=...)` |
158
+
159
+ ### House rules
160
+
161
+ 1. **Use the header and footer for what must stay visible.** They never scroll, whatever the body does.
162
+ 2. **The body is `max_display` lines.** Append for a rolling log, or rewrite by index for fixed slots.
163
+ 3. **For slots, match the window to the slot count.** Then nothing scrolls out, and appending would break the layout, so rewrite instead.
164
+ 4. **One redraw per step.** Keep the default `wait=True` and call `sb.display()` once at the end of each step, so you never see a half-updated panel.
165
+ 5. **Spacing is yours.** `auto_clean_edges=True` trims blank edge lines in the body and adds one blank line on each side. For a tight panel, leave it `False` and add your own blank lines.
166
+
167
+ It is still one plain list of lines, so everything else works too: per-line alignment, a log of every change, filters, and resume after a crash.
168
+
169
+ ---
170
+
171
+ ## Core concepts
172
+
173
+ - **Slots**: body lines used as fixed numbered positions that you rewrite by index. See [Header, body, footer](#header-body-footer).
174
+ - **Buffer**: the list of all lines you have `put()`. Each line has an index (0, 1, 2, ...).
175
+ - **View**: what is drawn. It is the header, then the visible lines (after the window and any filter), then the footer.
176
+ - **History log**: a timestamped file of every `put()`, only if you set `log_file`. Append-only record of the run.
177
+ - **Wait file**: a saved copy of the *current* buffer, so the panel can be restored (also only with `log_file`).
178
+
179
+ The screen is redrawn with Jupyter's own `clear_output(wait=True)`: the old frame stays until the new text is ready, then they are swapped. The new text is printed as one string, with your exact spacing.
180
+
108
181
  ---
109
182
 
110
183
  ## `put()`: the core call
111
184
 
112
- `put()` is how everything gets into the panel. Every other feature (alignment, window, filters, logs, resume) works on the lines you `put()`.
185
+ `put()` is how everything gets into the panel. It is also how you rewrite slots. Every other feature (alignment, window, filters, logs, resume) works on the lines you `put()`.
113
186
 
114
187
  ```python
115
188
  sb.put(text, state=0, index=None, fill_value=None, alignment=None)
@@ -135,7 +208,7 @@ sb.put("row 10", index=10) # lines 2..9 become blank, line 10 is "
135
208
  sb.put("progress: 80%", index=-1) # negative: replaces the LAST line
136
209
  ```
137
210
 
138
- **Replacing by index is the "update one part in a loop" trick.** Reserve a line, then keep rewriting it:
211
+ **Replacing by index is the "update one part in a loop" trick**, and it is how slots work (see [Header, body, footer](#header-body-footer)). Reserve a line, then keep rewriting it:
139
212
 
140
213
  ```python
141
214
  sb.put("Progress: 0%", index=0) # reserve line 0 as a status line
@@ -144,7 +217,7 @@ for i in range(100):
144
217
  sb.put(f"processed item {i}") # new lines keep appending below
145
218
  ```
146
219
 
147
- (In a window with `max_display`, a line that has scrolled out of the window is still updated in the buffer, but you won't see it. Keep status lines in the header or footer if they must always be visible.)
220
+ (In a window with `max_display`, a line that has scrolled out of the window is still updated in the buffer, but you won't see it. To keep something always visible, use the header or footer, or keep the buffer no longer than the window, as in the slot body (see [Header, body, footer](#header-body-footer)).)
148
221
 
149
222
  ### What happens on every `put()`
150
223
 
@@ -170,53 +243,78 @@ The history entry records the index, even for replacements:
170
243
 
171
244
  ---
172
245
 
173
- ## Feature index
246
+ ## Create a buffer
174
247
 
175
- Find what you need:
248
+ Everything starts here. All arguments are optional.
176
249
 
177
- | I want to... | Use | Section |
250
+ ```python
251
+ sb = ScreenBuffer(
252
+ wait=True, # False = redraw on every change
253
+ log_file=None, # name for the log set; None = no files
254
+ max_main=None, # history file line cap
255
+ max_wait=None, # saved-buffer file line cap
256
+ max_display=None, # lines drawn (newest N)
257
+ start=False, # True = wipe existing files for this log_file
258
+ header=None, header_alignment=None,
259
+ footer=None, footer_alignment=None,
260
+ show_header=None, show_footer=None,
261
+ silent_display=None, only_display=None,
262
+ auto_clean_edges=False,
263
+ auto_clean_silent_only_display=False,
264
+ shift_max_display=True,
265
+ width=None, # None = detect terminal width
266
+ )
267
+ ```
268
+
269
+ | Argument | What it does | Details |
178
270
  |---|---|---|
179
- | Create a buffer, see every option | `ScreenBuffer(...)` | [Create a buffer](#create-a-buffer) |
180
- | Add a line | `sb.put("text")` | [put()](#put-the-core-call) |
181
- | Replace / update an existing line | `sb.put("new", index=3)` | [put()](#put-the-core-call) |
182
- | Mark a line OK / warning / error in the log | `sb.put("x", state=0/1/2)` | [put()](#put-the-core-call) |
183
- | Center, right-align or indent a line | `put(..., alignment=...)` | [Alignment](#alignment) |
184
- | Show only the newest N lines | `max_display=N` | [Display window](#display-window) |
185
- | Have a fixed header / footer | `header=`, `footer=` | [Header and footer](#header-and-footer) |
186
- | Change header/footer in a loop | `sb.update(header=...)` | [Header and footer](#header-and-footer) |
187
- | Hide / show header or footer | `show_header`, `show_footer` | [Header and footer](#header-and-footer) |
188
- | Draw once per loop step (no flicker) | `wait=True` + `sb.display()` | [Smooth updates](#smooth-updates) |
189
- | Control when the screen redraws | `wait=True/False`, `sb.display()` | [Redraw modes](#redraw-modes) |
190
- | Hide some lines on screen, keep them stored | `silent_display=` | [Filtering](#filtering) |
191
- | Show only some lines on screen | `only_display=` | [Filtering](#filtering) |
192
- | Remove blank gaps left by filtering | `auto_clean_silent_only_display=True` | [Filtering](#filtering) |
193
- | Delete lines | `sb.delete(...)` | [Deleting and clearing](#deleting-and-clearing) |
194
- | Wipe the screen buffer | `sb.clear()`, `sb.clear_all()` | [Deleting and clearing](#deleting-and-clearing) |
195
- | Keep a full history file | `log_file="name"` | [Log files](#log-files) |
196
- | Read the history back (with its location) | `sb.read_log()` | [Log files](#log-files) |
197
- | Resume the panel after a kernel restart | `log_file=` without `start=True` | [Persistence](#persistence-and-resume) |
198
- | Start fresh and wipe old files | `start=True` | [Persistence](#persistence-and-resume) |
199
- | Cap file sizes | `max_main`, `max_wait` | [Limits](#limits-and-long-runs) |
200
- | Change any setting while running | `sb.update(...)` | [Changing settings](#changing-settings-at-runtime) |
201
- | Fix wrong center/right alignment | `width=` | [Alignment](#alignment) |
202
- | Get the panel as a string | `sb.get_text()` | [Reading state](#reading-state) |
203
- | See raw buffer with indexes | `sb.display_index()` | [Reading state](#reading-state) |
204
- | Use from several threads | built in | [Threads](#threads) |
271
+ | `wait` | `True` (default): draw only when you call `display()`. `False`: redraw on every change | [Redraw modes](#redraw-modes) |
272
+ | `log_file` | Name of the log set (creates `screen_buffer_logs/<name>.txt` and companions). `None` = nothing written to disk | [Log files](#log-files) |
273
+ | `start` | `True` wipes existing files for that name; `False` restores the saved panel | [Persistence](#persistence-and-resume) |
274
+ | `max_display` | Draw only the newest N lines | [Display window](#display-window) |
275
+ | `max_main`, `max_wait` | Line caps for the history and saved-buffer files | [Limits](#limits-and-long-runs) |
276
+ | `header`, `footer` | Fixed text above / below the lines | [Header and footer](#header-and-footer) |
277
+ | `header_alignment`, `footer_alignment` | Alignment for those two | [Alignment](#alignment) |
278
+ | `show_header`, `show_footer` | Hide or show them (default: shown when text is set) | [Header and footer](#header-and-footer) |
279
+ | `silent_display`, `only_display` | Hide some lines / show only some lines | [Filtering](#filtering) |
280
+ | `auto_clean_silent_only_display`, `shift_max_display`, `auto_clean_edges` | Fine-tune filtering and spacing | [Filtering](#filtering) |
281
+ | `width` | Panel width in columns for `center` / `right`; `None` = detect | [Alignment](#alignment) |
282
+
283
+ Typical setups:
284
+
285
+ ```python
286
+ sb = ScreenBuffer() # simplest: collect lines, then sb.display()
287
+ sb = ScreenBuffer(wait=False, max_display=10) # live panel, newest 10 lines
288
+ sb = ScreenBuffer(max_display=10, log_file="run1", max_wait=1000, start=True) # long job with a log
289
+ ```
205
290
 
206
291
  ---
207
292
 
208
- ## Core concepts
293
+ ## Usage
209
294
 
210
- - **Buffer**: the list of all lines you have `put()`. Each line has an index (0, 1, 2, ...).
211
- - **View**: what is drawn. It is the header, then the visible lines (after the window and any filter), then the footer.
212
- - **History log**: a timestamped file of every `put()`. Append-only record of the run.
213
- - **Wait file**: a saved copy of the *current* buffer, so the panel can be restored.
295
+ ### Display window
214
296
 
215
- The screen is redrawn with Jupyter's own `clear_output(wait=True)`: the old frame stays until the new text is ready, then they are swapped. The new text is printed as one string, with your exact spacing.
297
+ ```python
298
+ sb = ScreenBuffer(wait=False, max_display=10)
299
+ ```
216
300
 
217
- ---
301
+ Only the newest 10 lines are drawn; all lines stay in the buffer and the log. Drawing cost depends on `max_display`, not on how many lines exist (8 lines out of 100,000 draws in under a millisecond).
218
302
 
219
- ## Usage
303
+ When the buffer is exactly as long as `max_display`, the window shows the whole thing. That is how a slot body keeps every slot visible (see [Header, body, footer](#header-body-footer)).
304
+
305
+ ### Header and footer
306
+
307
+ ```python
308
+ sb = ScreenBuffer(wait=False, header="Job: nightly_etl", footer="status: running",
309
+ header_alignment="center")
310
+
311
+ sb.update(header="Job: nightly_etl, 42%") # change in a loop
312
+ sb.update(footer="!! source unreachable !!")
313
+ sb.update(show_footer=False) # hide it (the text is kept)
314
+ sb.update(show_footer=True) # bring it back
315
+ ```
316
+
317
+ Setting a non-empty header/footer turns it on. Header and footer are saved to files when `log_file` is set. `set_header(text, save_to_file=False)` / `set_footer(...)` keep one in memory only.
220
318
 
221
319
  ### Alignment
222
320
 
@@ -243,27 +341,14 @@ Alignment follows its line: it is saved and restored with the buffer, and re-key
243
341
 
244
342
  **Width:** by default the terminal width is detected. Jupyter front ends sometimes report the wrong width, which skews `center`/`right`. Set `width=70` (or whatever suits your output area) to fix it.
245
343
 
246
- ### Display window
247
-
248
- ```python
249
- sb = ScreenBuffer(wait=False, max_display=10)
250
- ```
251
-
252
- Only the newest 10 lines are drawn; all lines stay in the buffer and the log. Drawing cost depends on `max_display`, not on how many lines exist (8 lines out of 100,000 draws in under a millisecond).
253
-
254
- ### Header and footer
255
-
256
- ```python
257
- sb = ScreenBuffer(wait=False, header="Job: nightly_etl", footer="status: running",
258
- header_alignment="center")
344
+ ### Redraw modes
259
345
 
260
- sb.update(header="Job: nightly_etl | 42%") # change in a loop
261
- sb.update(footer="!! source unreachable !!")
262
- sb.update(show_footer=False) # hide it (the text is kept)
263
- sb.update(show_footer=True) # bring it back
264
- ```
346
+ | Mode | Behaviour |
347
+ |---|---|
348
+ | `wait=False` | Live. Every change redraws (skipped if nothing on screen changed). The panel draws once on creation. |
349
+ | `wait=True` (default) | Quiet. Changes are stored but nothing is drawn until you call `sb.display()`. Good for loading lots of lines first. |
265
350
 
266
- Setting a non-empty header/footer turns it on. Header and footer are saved to files when `log_file` is set. `set_header(text, save_to_file=False)` / `set_footer(...)` keep one in memory only.
351
+ `sb.display()` always draws, even if nothing changed. `sb.flush()` clears the Jupyter output immediately.
267
352
 
268
353
  ### Smooth updates
269
354
 
@@ -286,15 +371,6 @@ Other things that keep it smooth, all automatic:
286
371
 
287
372
  **Fast loops:** if a loop calls `put()` hundreds or thousands of times per second, Jupyter itself can be flooded (its output channel is rate-limited). Call `sb.display()` once per chunk of ~50 items instead of on every item, or add a short sleep.
288
373
 
289
- ### Redraw modes
290
-
291
- | Mode | Behaviour |
292
- |---|---|
293
- | `wait=False` | Live. Every change redraws (skipped if nothing on screen changed). The panel draws once on creation. |
294
- | `wait=True` (default) | Quiet. Changes are stored but nothing is drawn until you call `sb.display()`. Good for loading lots of lines first. |
295
-
296
- `sb.display()` always draws, even if nothing changed. `sb.flush()` clears the Jupyter output immediately.
297
-
298
374
  ### Filtering
299
375
 
300
376
  Hide or isolate lines **on screen only**. They stay in the buffer and the log. Indexes refer to the full buffer.
@@ -331,6 +407,33 @@ sb.clear_all() # empty the buffer AND remove hea
331
407
 
332
408
  `contains` is case-insensitive. With `exact=True` (default) it matches a whole word; with `exact=False`, any substring. Alignments move with their lines when others are deleted. Deleting does **not** remove entries from the history log.
333
409
 
410
+ With slots you rarely need `delete`: rewrite the slot instead, and the layout never shifts.
411
+
412
+ ### Reading state
413
+
414
+ ```python
415
+ sb.get_text() # the current panel as one string
416
+ sb.display_index() # prints every buffer line with its index (debugging)
417
+ sb.buffer # the raw list of lines
418
+ ```
419
+
420
+ `display_index()` is handy while designing slots: it shows which index holds which line.
421
+
422
+ ### Changing settings at runtime
423
+
424
+ ```python
425
+ sb.update(max_display=20, width=90, wait=False)
426
+ sb.update(log_file="second_phase") # switch to a different log set
427
+ ```
428
+
429
+ Settable: `wait`, `max_main`, `max_wait`, `max_display`, `shift_max_display`, `auto_clean_edges`, `auto_clean_silent_only_display`, `header`, `footer`, `header_alignment`, `footer_alignment`, `show_header`, `show_footer`, `silent_display`, `only_display`, `width`, `log_file`.
430
+
431
+ `update()` checks everything first: if any value is invalid it raises and changes **nothing**. Unknown names raise `ValueError`.
432
+
433
+ ### Threads
434
+
435
+ `put()`, `update()`, `delete()` and drawing are thread-safe. Disk writes keep buffer order even when several threads write at once.
436
+
334
437
  ### Log files
335
438
 
336
439
  ```python
@@ -390,30 +493,44 @@ Log file: /home/you/project/screen_buffer_logs/scrape_run.txt
390
493
 
391
494
  `max_wait` cannot be smaller than `max_display`. **Set `max_wait` (500–1000 is typical) on long runs**; it keeps the saved copy small and makes appends cheap.
392
495
 
393
- These cap files, **not memory**. The in-memory buffer keeps every line (roughly 100 MB per million short lines).
394
-
395
- ### Changing settings at runtime
396
-
397
- ```python
398
- sb.update(max_display=20, width=90, wait=False)
399
- sb.update(log_file="second_phase") # switch to a different log set
400
- ```
401
-
402
- Settable: `wait`, `max_main`, `max_wait`, `max_display`, `shift_max_display`, `auto_clean_edges`, `auto_clean_silent_only_display`, `header`, `footer`, `header_alignment`, `footer_alignment`, `show_header`, `show_footer`, `silent_display`, `only_display`, `width`, `log_file`.
403
-
404
- `update()` checks everything first: if any value is invalid it raises and changes **nothing**. Unknown names raise `ValueError`.
496
+ These cap files, **not memory**. The in-memory buffer keeps every line (roughly 100 MB per million short lines). A slot body has a fixed number of lines, so its memory never grows.
405
497
 
406
- ### Reading state
498
+ ---
407
499
 
408
- ```python
409
- sb.get_text() # the current panel as one string
410
- sb.display_index() # prints every buffer line with its index (debugging)
411
- sb.buffer # the raw list of lines
412
- ```
500
+ ## Feature index
413
501
 
414
- ### Threads
502
+ Find what you need:
415
503
 
416
- `put()`, `update()`, `delete()` and drawing are thread-safe. Disk writes keep buffer order even when several threads write at once.
504
+ | I want to... | Use | Section |
505
+ |---|---|---|
506
+ | Keep fixed slots in the body | `put(..., index=n)` + `max_display` = slot count | [Header, body, footer](#header-body-footer) |
507
+ | Keep something always visible | `header=` / `footer=`, changed with `sb.update(...)` | [Header, body, footer](#header-body-footer) |
508
+ | Create a buffer, see every option | `ScreenBuffer(...)` | [Create a buffer](#create-a-buffer) |
509
+ | Add a line | `sb.put("text")` | [put()](#put-the-core-call) |
510
+ | Replace / update an existing line | `sb.put("new", index=3)` | [put()](#put-the-core-call) |
511
+ | Mark a line OK / warning / error in the log | `sb.put("x", state=0/1/2)` | [put()](#put-the-core-call) |
512
+ | Center, right-align or indent a line | `put(..., alignment=...)` | [Alignment](#alignment) |
513
+ | Show only the newest N lines | `max_display=N` | [Display window](#display-window) |
514
+ | Have a fixed header / footer | `header=`, `footer=` | [Header and footer](#header-and-footer) |
515
+ | Change header/footer in a loop | `sb.update(header=...)` | [Header and footer](#header-and-footer) |
516
+ | Hide / show header or footer | `show_header`, `show_footer` | [Header and footer](#header-and-footer) |
517
+ | Draw once per loop step (no flicker) | `wait=True` + `sb.display()` | [Smooth updates](#smooth-updates) |
518
+ | Control when the screen redraws | `wait=True/False`, `sb.display()` | [Redraw modes](#redraw-modes) |
519
+ | Hide some lines on screen, keep them stored | `silent_display=` | [Filtering](#filtering) |
520
+ | Show only some lines on screen | `only_display=` | [Filtering](#filtering) |
521
+ | Remove blank gaps left by filtering | `auto_clean_silent_only_display=True` | [Filtering](#filtering) |
522
+ | Delete lines | `sb.delete(...)` | [Deleting and clearing](#deleting-and-clearing) |
523
+ | Wipe the screen buffer | `sb.clear()`, `sb.clear_all()` | [Deleting and clearing](#deleting-and-clearing) |
524
+ | Keep a full history file | `log_file="name"` | [Log files](#log-files) |
525
+ | Read the history back (with its location) | `sb.read_log()` | [Log files](#log-files) |
526
+ | Resume the panel after a kernel restart | `log_file=` without `start=True` | [Persistence](#persistence-and-resume) |
527
+ | Start fresh and wipe old files | `start=True` | [Persistence](#persistence-and-resume) |
528
+ | Cap file sizes | `max_main`, `max_wait` | [Limits](#limits-and-long-runs) |
529
+ | Change any setting while running | `sb.update(...)` | [Changing settings](#changing-settings-at-runtime) |
530
+ | Fix wrong center/right alignment | `width=` | [Alignment](#alignment) |
531
+ | Get the panel as a string | `sb.get_text()` | [Reading state](#reading-state) |
532
+ | See raw buffer with indexes | `sb.display_index()` | [Reading state](#reading-state) |
533
+ | Use from several threads | built in | [Threads](#threads) |
417
534
 
418
535
  ---
419
536
 
@@ -421,7 +538,7 @@ sb.buffer # the raw list of lines
421
538
 
422
539
  | Method | Purpose |
423
540
  |---|---|
424
- | `put(text, state=0, index=None, fill_value=None, alignment=None)` | Add or replace a line |
541
+ | `put(text, state=0, index=None, fill_value=None, alignment=None)` | Add or replace a line (also the slot move) |
425
542
  | `update(**settings)` | Change any setting (validated first) |
426
543
  | `display()` | Draw now |
427
544
  | `flush()` | Clear the Jupyter output now |
@@ -437,6 +554,34 @@ sb.buffer # the raw list of lines
437
554
 
438
555
  ---
439
556
 
557
+ ## Example: slots
558
+
559
+ The same scraper with a slot body: three body slots, a header and a footer.
560
+
561
+ ```python
562
+ import time, random
563
+ from screenbuffer import ScreenBuffer
564
+
565
+ sb = ScreenBuffer(max_display=3, width=70,
566
+ header="SCRAPER", header_alignment="center",
567
+ footer="0s elapsed", footer_alignment="center")
568
+ sb.put("", index=2) # builds body slots 0, 1 and 2
569
+
570
+ for t in range(1, 121):
571
+ sb.put(f"page {t}: +{random.randint(3, 9)} items", index=0) # current page
572
+ if t % 10 == 0:
573
+ sb.put(f"checkpoint at {t}s", index=1) # last checkpoint
574
+ if t % 40 == 0:
575
+ sb.put(f"retry at {t}s", index=2, alignment=4) # last retry
576
+ sb.update(footer=f"{t}s elapsed")
577
+ sb.display() # one redraw per step
578
+ time.sleep(0.5)
579
+ ```
580
+
581
+ Nothing scrolls. The header and footer stay put, and each slot keeps its latest value.
582
+
583
+ ---
584
+
440
585
  ## Example: network scraper with an error banner
441
586
 
442
587
  ```python
@@ -444,18 +589,18 @@ import time, random
444
589
  from screenbuffer import ScreenBuffer
445
590
 
446
591
  sb = ScreenBuffer(max_display=8, width=70,
447
- header="SCRAPER | ONLINE", header_alignment="center",
592
+ header="SCRAPER ONLINE", header_alignment="center",
448
593
  footer_alignment="center",
449
594
  log_file="scraper", max_wait=500, start=True)
450
595
 
451
596
  for t in range(120):
452
597
  online = not (40 <= t < 70)
453
598
  if online:
454
- sb.update(header=f"SCRAPER | ONLINE | {t}s", show_footer=False)
599
+ sb.update(header=f"SCRAPER ONLINE, {t}s", show_footer=False)
455
600
  sb.put(f"[{t}s] page {t//2}: +{random.randint(3, 9)} items")
456
601
  else:
457
- sb.update(header=f"SCRAPER | OFFLINE | {t}s",
458
- footer="!! NETWORK ERROR - retrying !!")
602
+ sb.update(header=f"SCRAPER OFFLINE, {t}s",
603
+ footer="!! NETWORK ERROR, retrying !!")
459
604
  if t % 5 == 0:
460
605
  sb.put(f"[{t}s] retrying...", state=1, alignment=4)
461
606
  sb.display() # one redraw per step
@@ -468,22 +613,10 @@ The error banner appears at the bottom during the outage and disappears when the
468
613
 
469
614
  ---
470
615
 
471
- ## Performance (measured in a sandbox)
472
-
473
- | Case | Before tuning | Now |
474
- |---|---|---|
475
- | `put()` with 20k lines in the buffer (no log / log / log + `max_main`) | 2.1 / 10.7 / 16.4 ms | 0.005 / 0.035 / 0.11 ms |
476
- | Draw 8 lines out of 100k | 30.5 ms | 0.72 ms |
477
- | Frame wait with a 20 ms disk delay | 202 ms | 0.1 ms |
478
- | Old frame cleared → new text shown | 100–200 ms | ~0.5 ms |
479
- | Redraws in a 300-step demo (one draw per step) | 241 | 81 |
480
-
481
- These come from a Linux sandbox with a simulated Jupyter kernel. Real browser rendering was not measurable there; if you see flicker, see Troubleshooting.
482
-
483
616
  ## Known limits
484
617
 
485
618
  - **Jupyter only.** Terminals and plain scripts are not supported (drawing is a no-op without IPython).
486
- - **Replacing or deleting an old line rewrites the saved-buffer file.** Appending is cheap; edits cost O(lines in that file), which `max_wait` keeps small.
619
+ - **Replacing or deleting an old line rewrites the saved-buffer file.** Appending is cheap; edits cost O(lines in that file), which `max_wait` keeps small. A slot body is small by design, so this stays cheap.
487
620
  - **`max_main` trimming** rewrites the history file once it goes over the cap.
488
621
  - **Memory is not capped** (see Limits).
489
622
  - **Queued writes** can be lost if the kernel is killed abruptly. Call `sb.read_log()` (it waits for writes) or let the cell finish before shutting down.
@@ -496,6 +629,8 @@ These come from a Linux sandbox with a simulated Jupyter kernel. Real browser re
496
629
  | Symptom | Likely cause / fix |
497
630
  |---|---|
498
631
  | Nothing appears | `wait=True` (default): call `sb.display()`, or create with `wait=False` |
632
+ | A slot scrolls out of view | The buffer is longer than `max_display`. Make the window equal the slot count, or stop appending and rewrite slots by index |
633
+ | Blank line above the first body line | `auto_clean_edges=True` adds one. Set it to `False` and add your own blank lines |
499
634
  | Flicker in a fast loop | Use `wait=True` and call `sb.display()` once per step (or per chunk of items); add a tiny sleep |
500
635
  | Header and footer change on separate frames | Use `wait=True`, make both updates, then call `sb.display()` once |
501
636
  | `center` / `right` look off | Set `width=` to your output area's real width |