screenbuffer 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,505 @@
1
+ Metadata-Version: 2.4
2
+ Name: screenbuffer
3
+ Version: 0.1.0
4
+ Summary: A Python utility for managing dynamic output, tracking progress, and organizing information throughout data-driven workflows.
5
+ Keywords: jupyter,notebook,progress,logging,display,data
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Framework :: Jupyter
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: jupyter
13
+ Requires-Dist: ipython; extra == "jupyter"
14
+
15
+ # ScreenBuffer
16
+
17
+ **A Python utility for managing dynamic output, tracking progress, and organizing information throughout data-driven workflows.**
18
+
19
+ Long-running loops (scrapers, ETL jobs, model sweeps, simulations, data pulls) leave you two bad choices in Jupyter:
20
+
21
+ - `print()` every step: the cell fills with thousands of lines you cannot read.
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
+
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.
25
+
26
+ ```python
27
+ from screenbuffer import ScreenBuffer
28
+
29
+ sb = ScreenBuffer(max_display=8, header="Scraper | page 0/200",
30
+ log_file="scrape_run", max_wait=1000, start=True)
31
+
32
+ for i, page in enumerate(pages, 1):
33
+ sb.update(header=f"Scraper | page {i}/200")
34
+ sb.put(f"page {i}: +{len(page)} rows")
35
+ sb.display() # draw once per step
36
+ ```
37
+
38
+ The panel shows a fixed header, the newest 8 lines, and (optionally) a footer. Nothing else on screen moves.
39
+
40
+ ---
41
+
42
+ ## Who it is for
43
+
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
+
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
+
48
+ ## Requirements
49
+
50
+ - Python 3.8+ and Jupyter (Notebook, JupyterLab, VS Code, or a Jupyter console).
51
+ - 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
+ ## Install
54
+
55
+ Copy the `screenbuffer/` folder next to your work (or onto your `PYTHONPATH`), then:
56
+
57
+ ```python
58
+ from screenbuffer import ScreenBuffer
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Create a buffer
64
+
65
+ Everything starts here. All arguments are optional.
66
+
67
+ ```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
+ )
84
+ ```
85
+
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) |
99
+
100
+ Typical setups:
101
+
102
+ ```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
106
+ ```
107
+
108
+ ---
109
+
110
+ ## `put()`: the core call
111
+
112
+ `put()` is how everything gets into the panel. Every other feature (alignment, window, filters, logs, resume) works on the lines you `put()`.
113
+
114
+ ```python
115
+ sb.put(text, state=0, index=None, fill_value=None, alignment=None)
116
+ ```
117
+
118
+ | Argument | Meaning |
119
+ |---|---|
120
+ | `text` | The line. Non-strings are converted with `str()`. Keep it to one line (a `\n` inside would show as several screen lines but count as one buffer line, so `center`/`right` would apply to the whole block). |
121
+ | `state` | `0` 🟢 ok (default), `1` 🟡 warning, `2` 🔴 error. Written to the **history log only**; it does not change how the line looks on screen. |
122
+ | `index` | Where to put it. `None` = append at the end. An existing index = **replace that line in place**. Past the end = pad the gap, then add. Negative counts from the end. |
123
+ | `fill_value` | Text for the padding lines when `index` is past the end. Default: blank lines. |
124
+ | `alignment` | `"center"`, `"right"`, an indent (`4`), or from-the-right (`-10`). Applies to this line only. See [Alignment](#alignment). |
125
+
126
+ Returns nothing.
127
+
128
+ ### Append, replace, pad
129
+
130
+ ```python
131
+ sb.put("loaded 1,200 rows") # line 0 (append)
132
+ sb.put("retrying...", state=1) # line 1, logged as 🟡
133
+ sb.put("progress: 40%", index=0) # replace line 0 in place: the screen updates, nothing moves
134
+ sb.put("row 10", index=10) # lines 2..9 become blank, line 10 is "row 10"
135
+ sb.put("progress: 80%", index=-1) # negative: replaces the LAST line
136
+ ```
137
+
138
+ **Replacing by index is the "update one part in a loop" trick.** Reserve a line, then keep rewriting it:
139
+
140
+ ```python
141
+ sb.put("Progress: 0%", index=0) # reserve line 0 as a status line
142
+ for i in range(100):
143
+ sb.put(f"Progress: {i}%", index=0) # rewrite just that line
144
+ sb.put(f"processed item {i}") # new lines keep appending below
145
+ ```
146
+
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.)
148
+
149
+ ### What happens on every `put()`
150
+
151
+ 1. The buffer changes (append / replace / pad).
152
+ 2. The line's alignment is stored if you passed one (replacing a line without `alignment=` keeps its old alignment).
153
+ 3. If `log_file` is set: one timestamped entry is appended to the history log, and the saved buffer is updated.
154
+ 4. In live mode (`wait=False`) the panel redraws. In the default mode it does not, until `display()`.
155
+
156
+ The history entry records the index, even for replacements:
157
+
158
+ ```
159
+ 2026-10-21 00:12:07 🟡 retrying... [📍1]
160
+ ```
161
+
162
+ ### Errors
163
+
164
+ - A negative `index` further back than the buffer length raises `IndexError`.
165
+ - A failed history write raises `RuntimeError` (lines are never silently lost).
166
+
167
+ ### Speed
168
+
169
+ `put()` stays cheap as the buffer grows: about 0.005 ms at 20,000 lines without a log file, 0.035 ms with one. Disk writes are queued, so a slow disk does not slow `put()`.
170
+
171
+ ---
172
+
173
+ ## Feature index
174
+
175
+ Find what you need:
176
+
177
+ | I want to... | Use | Section |
178
+ |---|---|---|
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) |
205
+
206
+ ---
207
+
208
+ ## Core concepts
209
+
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.
214
+
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.
216
+
217
+ ---
218
+
219
+ ## Usage
220
+
221
+ ### Alignment
222
+
223
+ Per line, set when you `put()`:
224
+
225
+ ```python
226
+ sb.put("RESULTS", alignment="center")
227
+ sb.put("total: 4,210", alignment="right")
228
+ sb.put("nested detail", alignment=4) # 4 spaces of indent
229
+ sb.put("ends 10 columns from the right edge", alignment=-10)
230
+ sb.put("also works as text", alignment="4")
231
+ ```
232
+
233
+ | Value | Effect |
234
+ |---|---|
235
+ | `None` / `"left"` | unchanged |
236
+ | `"center"` | centred in the panel width |
237
+ | `"right"` | right-justified |
238
+ | int ≥ 0 | that many spaces of left indent |
239
+ | int < 0 | ends that many columns from the right edge |
240
+ | numeric string | same as the int |
241
+
242
+ Alignment follows its line: it is saved and restored with the buffer, and re-keyed when lines are deleted. Header and footer have their own: `header_alignment=`, `footer_alignment=`.
243
+
244
+ **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
+
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")
259
+
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
+ ```
265
+
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.
267
+
268
+ ### Smooth updates
269
+
270
+ With `wait=False` (live mode) every `put()` / `update()` redraws immediately. That is fine for one change per step. If a step makes several changes (new header, new footer, new line), each one draws its own frame, and you briefly see half-updated screens.
271
+
272
+ For loops, use the default `wait=True`: make all your changes, then call `sb.display()` once. One step, one redraw.
273
+
274
+ ```python
275
+ sb.update(header="OFFLINE", footer="retrying in 30s")
276
+ sb.put("connection lost", state=2)
277
+ sb.display() # one redraw for all three changes
278
+ ```
279
+
280
+ Other things that keep it smooth, all automatic:
281
+
282
+ - New frames are flushed immediately.
283
+ - A redraw is skipped if nothing on screen would change.
284
+ - Only the `max_display` window is built when drawing.
285
+ - Disk writes are queued and done in order, outside the drawing path, so a slow disk (antivirus, OneDrive, network drive) does not stall the screen.
286
+
287
+ **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
+
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
+ ### Filtering
299
+
300
+ Hide or isolate lines **on screen only**. They stay in the buffer and the log. Indexes refer to the full buffer.
301
+
302
+ ```python
303
+ sb.update(silent_display=[2, 5]) # hide lines 2 and 5
304
+ sb.update(silent_display=slice(0, 10)) # hide a range
305
+ sb.update(silent_display="all") # hide every line (header/footer stay)
306
+ sb.update(only_display=[7]) # show only line 7
307
+ sb.update(only_display=None) # remove the filter
308
+ ```
309
+
310
+ Accepts an int, a list of ints, a slice, or `"all"`. `silent_display` and `only_display` cannot be used together (setting one clears the other).
311
+
312
+ Related options:
313
+
314
+ | Option | Effect |
315
+ |---|---|
316
+ | `auto_clean_silent_only_display=True` | Hidden lines disappear instead of leaving blank rows |
317
+ | `shift_max_display=True` (default) | Filter first, then take the newest `max_display` visible lines (hidden lines do not use up slots) |
318
+ | `shift_max_display=False` | Take the newest `max_display` buffer lines first, then filter |
319
+ | `auto_clean_edges=True` | Trim blank lines at the top/bottom of the body, then add one blank line each side |
320
+
321
+ ### Deleting and clearing
322
+
323
+ ```python
324
+ sb.delete(3) # delete line 3
325
+ sb.delete(slice(0, 5)) # delete a range
326
+ sb.delete(slice(0, 20), contains="error") # only lines in that range containing "error"
327
+ sb.delete(slice(0, 20), contains="error", exact=False) # substring match
328
+ sb.clear() # empty the buffer (header/footer stay)
329
+ sb.clear_all() # empty the buffer AND remove header/footer
330
+ ```
331
+
332
+ `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
+
334
+ ### Log files
335
+
336
+ ```python
337
+ sb = ScreenBuffer(log_file="scrape_run")
338
+ ```
339
+
340
+ Creates a `screen_buffer_logs/` folder next to where you run it (relative to the current working directory):
341
+
342
+ | File | Contents |
343
+ |---|---|
344
+ | `screen_buffer_logs/scrape_run.txt` | **History**: one timestamped line per `put()`, never rewritten by edits |
345
+ | `screen_buffer_logs/scrape_run_wait.txt` | **Saved buffer**: current lines and their alignments, for restoring the panel |
346
+ | `screen_buffer_logs/scrape_run_header.txt` | Saved header |
347
+ | `screen_buffer_logs/scrape_run_footer.txt` | Saved footer |
348
+
349
+ History line format:
350
+
351
+ ```
352
+ 2026-10-21 00:12:07 🟡 retrying... [📍14]
353
+ ```
354
+
355
+ (timestamp, state emoji, text, buffer index). Characters that would break a log entry across lines (`\n`, `\r`, Unicode line separators) are escaped in the history file, so one `put()` is always exactly one log line and text cannot forge fake entries.
356
+
357
+ Read it back:
358
+
359
+ ```python
360
+ sb.read_log() # everything
361
+ sb.read_log(last=5) # newest 5
362
+ text = sb.read_log(show=False) # return as a string instead of printing
363
+ ```
364
+
365
+ Output starts with the file location:
366
+
367
+ ```
368
+ Log file: /home/you/project/screen_buffer_logs/scrape_run.txt
369
+ (5 of 312 entries)
370
+ ----------------------------------------
371
+ 2026-10-21 00:12:07 🟡 retrying... [📍14]
372
+ ...
373
+ ```
374
+
375
+ `read_log()` raises `ValueError` if no `log_file` is set. A write failure raises `RuntimeError` rather than silently losing log lines.
376
+
377
+ ### Persistence and resume
378
+
379
+ - `ScreenBuffer(log_file="x")`: if the saved buffer exists, it is **restored** (lines, alignments, header, footer). Kernel crashed? Re-run the cell and the panel comes back.
380
+ - `ScreenBuffer(log_file="x", start=True)`: **start fresh**; existing files for that name are emptied.
381
+ - An explicit `header=` / `footer=` always wins over a stale saved one.
382
+
383
+ ### Limits and long runs
384
+
385
+ | Option | Limits |
386
+ |---|---|
387
+ | `max_main=N` | Lines kept in the **history** file (oldest dropped) |
388
+ | `max_wait=N` | Lines kept in the **saved buffer** file (oldest dropped) |
389
+ | `max_display=N` | Lines **drawn** |
390
+
391
+ `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
+
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`.
405
+
406
+ ### Reading state
407
+
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
+ ```
413
+
414
+ ### Threads
415
+
416
+ `put()`, `update()`, `delete()` and drawing are thread-safe. Disk writes keep buffer order even when several threads write at once.
417
+
418
+ ---
419
+
420
+ ## Method reference
421
+
422
+ | Method | Purpose |
423
+ |---|---|
424
+ | `put(text, state=0, index=None, fill_value=None, alignment=None)` | Add or replace a line |
425
+ | `update(**settings)` | Change any setting (validated first) |
426
+ | `display()` | Draw now |
427
+ | `flush()` | Clear the Jupyter output now |
428
+ | `delete(index, contains=None, exact=True)` | Delete by index/slice, optionally filtered by text |
429
+ | `clear()` | Empty the buffer |
430
+ | `clear_all()` | Empty the buffer, header and footer |
431
+ | `set_header(text, save_to_file=True, refresh=True)` / `set_footer(...)` | Set header/footer |
432
+ | `get_header()` / `get_footer()` | Current header/footer text |
433
+ | `get_text()` | Panel as one string |
434
+ | `read_log(last=None, show=True)` | Read the history log |
435
+ | `display_index()` | Print lines with indexes |
436
+ | `refresh_buffer_from_wait_log()` | Reload the buffer from the saved file (only when the buffer is empty) |
437
+
438
+ ---
439
+
440
+ ## Example: network scraper with an error banner
441
+
442
+ ```python
443
+ import time, random
444
+ from screenbuffer import ScreenBuffer
445
+
446
+ sb = ScreenBuffer(max_display=8, width=70,
447
+ header="SCRAPER | ONLINE", header_alignment="center",
448
+ footer_alignment="center",
449
+ log_file="scraper", max_wait=500, start=True)
450
+
451
+ for t in range(120):
452
+ online = not (40 <= t < 70)
453
+ if online:
454
+ sb.update(header=f"SCRAPER | ONLINE | {t}s", show_footer=False)
455
+ sb.put(f"[{t}s] page {t//2}: +{random.randint(3, 9)} items")
456
+ else:
457
+ sb.update(header=f"SCRAPER | OFFLINE | {t}s",
458
+ footer="!! NETWORK ERROR - retrying !!")
459
+ if t % 5 == 0:
460
+ sb.put(f"[{t}s] retrying...", state=1, alignment=4)
461
+ sb.display() # one redraw per step
462
+ time.sleep(0.5)
463
+
464
+ sb.read_log(last=5)
465
+ ```
466
+
467
+ The error banner appears at the bottom during the outage and disappears when the network returns, while the page counter keeps climbing above it.
468
+
469
+ ---
470
+
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
+ ## Known limits
484
+
485
+ - **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.
487
+ - **`max_main` trimming** rewrites the history file once it goes over the cap.
488
+ - **Memory is not capped** (see Limits).
489
+ - **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.
490
+ - **Very fast loops** can exceed Jupyter's output rate limit regardless of this tool. Draw once per chunk or add a short sleep.
491
+ - **Wide characters** (emoji, CJK) count as one column for alignment, so `center`/`right` can be off by a column or two.
492
+ - Not tested on Windows, in real browsers, or in the Jupyter console (the console front end).
493
+
494
+ ## Troubleshooting
495
+
496
+ | Symptom | Likely cause / fix |
497
+ |---|---|
498
+ | Nothing appears | `wait=True` (default): call `sb.display()`, or create with `wait=False` |
499
+ | 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
+ | Header and footer change on separate frames | Use `wait=True`, make both updates, then call `sb.display()` once |
501
+ | `center` / `right` look off | Set `width=` to your output area's real width |
502
+ | `ValueError: max_wait cannot be less than max_display` | Raise `max_wait` or lower `max_display` |
503
+ | Old panel returns after re-running a cell | Saved buffer was restored; use `start=True` to begin fresh |
504
+ | `read_log()` says no log | Create the buffer with `log_file="name"` |
505
+ | Output scrolls instead of replacing | Output area is detached or using a front end without `clear_output` support |
@@ -0,0 +1,9 @@
1
+ screenbuffer/__init__.py,sha256=2vCuAPq2ykeF9WtokNeRu4-oeu2nuEpvGemoWeSKqLg,104
2
+ screenbuffer/align.py,sha256=__v-sB0mdrcTJEm60xKiwv_XsD1LzpfD9gx_cdUwc3M,1718
3
+ screenbuffer/buffer.py,sha256=TBIjAczUChDyXZaLDrPUX9ID19YbOyw1hTQTgahC3WY,21636
4
+ screenbuffer/store.py,sha256=keAfhCgOjAmKSAHfj73c2kz7Bt39N-48RWJpo9rXto0,8342
5
+ screenbuffer/view.py,sha256=tm8YBPAMwMxfQ45EH2WKMaFczYGPEMNSgYVkY1-wYJU,4256
6
+ screenbuffer-0.1.0.dist-info/METADATA,sha256=2BcayX5fTg0Ja575hNviDCkQcfxGlZlt0jl8gQ4WxJU,24679
7
+ screenbuffer-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ screenbuffer-0.1.0.dist-info/top_level.txt,sha256=2PszMhH9tnwjNJO5d5INqkHQj2akg_brkakmHZVkCdI,13
9
+ screenbuffer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ screenbuffer