diff --git a/assets/screenshots/terminal.png b/assets/screenshots/terminal.png index eaa24a2..a7e85ce 100644 Binary files a/assets/screenshots/terminal.png and b/assets/screenshots/terminal.png differ diff --git a/docs/conf.py b/docs/conf.py index 604ff94..12f38b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,7 +39,7 @@ try: from picklock import __version__ as release except Exception: # pragma: no cover - docs can build without the package installed - release = "0.1.1" + release = "0.2.0" version = ".".join(release.split(".")[:2]) diff --git a/docs/guide/scanning.md b/docs/guide/scanning.md index 1ee2f84..e9a0803 100644 --- a/docs/guide/scanning.md +++ b/docs/guide/scanning.md @@ -18,7 +18,7 @@ the thing on your screen changed. ``` picklock [game.exe:41902]> scan:value int32 100 --writable -Showing 20 of 3184 rows — page 1 of 160 (1.42 sec) +Showing 20 of 3184 rows — page 1 of 160 — writable regions only (1.42 sec) picklock [game.exe:41902]> scan:next 95 2 rows in set (0.02 sec) @@ -30,17 +30,18 @@ faster. `--all-regions` overrides it when you are looking for something in read-only data. Because that restriction is easy to forget and expensive to forget, a -result set that skipped read-only memory says so — on the scan, on every -refine, and again on `scan:results`: +result set that skipped read-only memory says so in its footer — on the scan, +on every refine, and again on `scan:results`: ``` -Note: Writable regions only — nothing in read-only memory was searched. -Use '--all-regions' on the first scan to include it. +20 rows in set — writable regions only (0.02 sec) ``` -You will also see it without having typed `--writable`, if the -`writable_only` setting is on. That is the point of the line: the reason an -address is missing should not be a setting you turned on last week. +It sits under the rows because that is where the question gets asked: you are +looking at what came back and wondering whether it is everything. You will also +see it without having typed `--writable`, if the `writable_only` setting is on +— the reason an address is missing should not be a setting you turned on last +week. ## When you cannot see the number diff --git a/docs/quickstart.md b/docs/quickstart.md index 3a2241a..cf5b62b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -10,7 +10,7 @@ picklock ``` ``` -Welcome to Picklock 0.1.1, a terminal client for PyMemoryEditor. +Welcome to Picklock 0.2.0, a terminal client for PyMemoryEditor. Type 'help' for the command list, or 'help scanning' for a walkthrough. picklock> @@ -67,7 +67,7 @@ for it: ``` picklock [game.exe:41902]> scan:value int32 100 --writable -Showing 20 of 3184 rows — page 1 of 160 (1.42 sec) +Showing 20 of 3184 rows — page 1 of 160 — writable regions only (1.42 sec) Next page: scan:results --page 2 ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 658f27b..1d162c0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -19,8 +19,8 @@ Work through these in order: instead: `scan:value float --between 0.7 0.8`. 3. **`--writable` excluded it.** A value that never changes may live in read-only data. `--all-regions` searches everything. Picklock says so when - a result set skipped read-only memory, so check for the - "Writable regions only" note — and remember the `writable_only` setting + a result set skipped read-only memory, so check the footer under the table + for "writable regions only" — and remember the `writable_only` setting produces it too. 4. **It moved.** Between your first scan and your refine, the target may have reallocated. Start over with `scan:reset`. diff --git a/picklock/__init__.py b/picklock/__init__.py index 6ff4111..401b0b0 100644 --- a/picklock/__init__.py +++ b/picklock/__init__.py @@ -14,7 +14,7 @@ """ __author__ = "Jean Loui Bernard Silva de Jesus" -__version__ = "0.1.1" +__version__ = "0.2.0" from .errors import CommandError, NoProcessError, PicklockError from .session import Session diff --git a/picklock/commands/__init__.py b/picklock/commands/__init__.py index 1a7adac..6c32b74 100644 --- a/picklock/commands/__init__.py +++ b/picklock/commands/__init__.py @@ -75,7 +75,7 @@ class Namespace: "Scanning", "Search memory for a value, then narrow what you found.", "picklock> scan:value int32 100 --writable\n" - "Showing 20 of 3184 rows (1.42 sec)\n" + "Showing 20 of 3184 rows — writable regions only (1.42 sec)\n" "\n" "picklock> scan:next 95\n" "+-----+--------------------+-------+\n" diff --git a/picklock/commands/scan_commands.py b/picklock/commands/scan_commands.py index df403a0..373a3f7 100644 --- a/picklock/commands/scan_commands.py +++ b/picklock/commands/scan_commands.py @@ -288,14 +288,12 @@ def _read_values( return [value_type.decode(found.get(address)) for address in addresses] -#: Said whenever a result set that skipped read-only memory is shown. A scan -#: that quietly searched a tenth of the address space is the kind of thing you -#: work out an hour later, from an address that should have been found and was -#: not. -_WRITABLE_ONLY = ( - "Writable regions only — nothing in read-only memory was searched. " - "Use '--all-regions' on the first scan to include it." -) +#: Marks every result set that skipped read-only memory. A scan that quietly +#: searched a tenth of the address space is the kind of thing you work out an +#: hour later, from an address that should have been found and was not — so it +#: rides in the footer, under the rows it applies to, on the scan and on every +#: later look at the results. +_WRITABLE_ONLY = "writable regions only" def _store( @@ -362,9 +360,6 @@ def _print_results( The next page is fetched with ``scan:results``, not by re-running the scan — which is why the hint names that command whatever produced the rows. """ - if state.writable_only: - session.printer.note(_WRITABLE_ONLY) - process = session.require_process() hex_output = bool(session.option("hex")) indexes = range(len(state.addresses)) @@ -390,6 +385,7 @@ def _print_results( page=page.number, pages=page.count, next_page=page.next_page, + marker=_WRITABLE_ONLY if state.writable_only else None, ) @@ -874,9 +870,6 @@ def cmd_results(session: Session, args: List[str]) -> None: session.printer.write() return - if state.writable_only: - session.printer.note(_WRITABLE_ONLY) - page = paginate( session, range(len(state.addresses)), @@ -919,6 +912,7 @@ def cmd_results(session: Session, args: List[str]) -> None: page=page.number, pages=page.count, next_page=page.next_page, + marker=_WRITABLE_ONLY if state.writable_only else None, ) diff --git a/picklock/commands/session_commands.py b/picklock/commands/session_commands.py index d72bc9d..0465845 100644 --- a/picklock/commands/session_commands.py +++ b/picklock/commands/session_commands.py @@ -289,7 +289,8 @@ def _print_overview(session: Session) -> None: "Attached to game.exe (PID 4242, 64-bit). (0.00 sec)\n" "\n" "picklock> scan:value int32 100 --writable\n" - "Showing 20 of 3184 rows — page 1 of 160 (1.42 sec)\n" + "Showing 20 of 3184 rows — page 1 of 160 — writable regions only " + "(1.42 sec)\n" "Next page: scan:results --page 2", indent=4, ) diff --git a/picklock/output.py b/picklock/output.py index 93ffca7..b307ecb 100644 --- a/picklock/output.py +++ b/picklock/output.py @@ -353,13 +353,15 @@ def table( page: Optional[int] = None, pages: Optional[int] = None, next_page: Optional[str] = None, + marker: Optional[str] = None, ) -> None: """Print a result table plus its footer. ``total`` names the number of rows that *matched* when ``rows`` only carries the ones that fit the display limit, so the footer can say how many were left out instead of pretending they do not exist, and - ``next_page`` is the command that shows them. + ``next_page`` is the command that shows them. ``marker`` is a short + caveat about the result set itself, carried in the same footer. """ self.clear_progress() if rows: @@ -371,6 +373,7 @@ def table( page=page, pages=pages, next_page=next_page, + marker=marker, ) def footer( @@ -382,8 +385,13 @@ def footer( page: Optional[int] = None, pages: Optional[int] = None, next_page: Optional[str] = None, + marker: Optional[str] = None, ) -> None: - """Print the ``N rows in set (0.01 sec)`` line, and how to see more.""" + """Print the ``N rows in set (0.01 sec)`` line, and how to see more. + + ``marker`` rides in that line rather than above the table, where a + caveat about the rows sits next to the rows it is about. + """ if count == 0 and not total: text = "Empty set" elif total is not None and total != count: @@ -396,6 +404,8 @@ def footer( text += f" — page {page} of {pages}" else: text = f"{count} row{'' if count == 1 else 's'} in set" + if marker: + text += f" — {marker}" if self.timing and elapsed is not None: text += f" ({format_duration(elapsed)})" self.write(text) diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 60e54df..376786c 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -319,14 +319,19 @@ def test_a_range_written_backwards_is_refused(target, capture): def test_a_writable_only_scan_says_so_and_keeps_saying_so(target, capture, block): """The restriction is easy to forget and expensive to forget.""" out = run(target, capture, f"scan:value int32 {MARKER} --writable") - assert "Writable regions only" in out + assert "writable regions only" in out + + # under the rows rather than above them: a caveat about the results + # belongs next to the results, where the eye already is when it asks + # whether they are all of them. + assert out.index("writable regions only") > out.rindex("+--") # and again when the results are looked at later, which is when the # question "why is my address not here?" actually gets asked - assert "Writable regions only" in run(target, capture, "scan:results") + assert "writable regions only" in run(target, capture, "scan:results") # a refine narrows what that scan found, so it inherits the caveat - assert "Writable regions only" in run(target, capture, "scan:next --unchanged") + assert "writable regions only" in run(target, capture, "scan:next --unchanged") @slow diff --git a/tests/test_output.py b/tests/test_output.py index b38f3e6..6216e3c 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -44,6 +44,19 @@ def test_footer_counts_rows_and_says_when_it_is_showing_fewer(capture): assert lines == ["3 rows in set", "Empty set", "Showing 3 of 90 rows"] +def test_footer_carries_a_marker_before_the_timing(capture): + """A caveat about the result set travels with the count line.""" + capture.printer.timing = True + capture.printer.footer(3, total=90, marker="writable regions only") + assert "Showing 3 of 90 rows — writable regions only" in capture.out + + +def test_an_empty_result_set_still_carries_its_marker(capture): + """"Nothing found" and "nothing found *there*" are different answers.""" + capture.printer.footer(0, marker="writable regions only") + assert "Empty set — writable regions only" in capture.out + + def test_footer_reports_one_row_in_the_singular(capture): capture.printer.footer(1) assert "1 row in set" in capture.out