pyDiffTools 0.1.59__tar.gz → 0.1.60__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 (135) hide show
  1. {pydifftools-0.1.59/pyDiffTools.egg-info → pydifftools-0.1.60}/PKG-INFO +26 -5
  2. {pydifftools-0.1.59 → pydifftools-0.1.60}/README.rst +25 -4
  3. {pydifftools-0.1.59 → pydifftools-0.1.60/pyDiffTools.egg-info}/PKG-INFO +26 -5
  4. {pydifftools-0.1.59 → pydifftools-0.1.60}/pyDiffTools.egg-info/SOURCES.txt +3 -0
  5. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/command_line.py +49 -3
  6. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/continuous.py +27 -5
  7. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/forward_search.py +76 -1
  8. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/git_gd.py +22 -3
  9. pydifftools-0.1.60/pydifftools/git_gd_terminal.py +182 -0
  10. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/line_alignment.py +28 -1
  11. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/match_spaces.py +10 -2
  12. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/wrap_sentences.py +6 -4
  13. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/wrapping_options.py +7 -0
  14. {pydifftools-0.1.59 → pydifftools-0.1.60}/pyproject.toml +1 -1
  15. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_browser_lifecycle.py +5 -2
  16. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_command_line_help.py +6 -2
  17. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_continuous_shutdown.py +10 -7
  18. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_forward_search.py +40 -0
  19. pydifftools-0.1.60/tests/test_git_gd_terminal.py +98 -0
  20. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_source_jump.py +2 -2
  21. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_wrap_check.py +17 -12
  22. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_wrapping.py +2 -7
  23. pydifftools-0.1.60/tests/test_wrapping_width_limits.py +91 -0
  24. {pydifftools-0.1.59 → pydifftools-0.1.60}/LICENSE.md +0 -0
  25. {pydifftools-0.1.59 → pydifftools-0.1.60}/MANIFEST.in +0 -0
  26. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/_quarto.yml +0 -0
  27. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  28. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  29. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  30. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  31. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  32. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  33. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  34. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  35. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  36. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  37. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  38. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  39. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  40. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  41. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  42. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  43. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  44. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  45. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  46. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  47. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  48. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  49. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/example.qmd +0 -0
  50. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/example.tex +0 -0
  51. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/index.qmd +0 -0
  52. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  53. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  54. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  55. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  56. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/independent.qmd +0 -0
  57. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/index.qmd +0 -0
  58. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  59. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  60. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  61. {pydifftools-0.1.59 → pydifftools-0.1.60}/example_notebook/project1/test_include.qmd +0 -0
  62. {pydifftools-0.1.59 → pydifftools-0.1.60}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  63. {pydifftools-0.1.59 → pydifftools-0.1.60}/pyDiffTools.egg-info/entry_points.txt +0 -0
  64. {pydifftools-0.1.59 → pydifftools-0.1.60}/pyDiffTools.egg-info/requires.txt +0 -0
  65. {pydifftools-0.1.59 → pydifftools-0.1.60}/pyDiffTools.egg-info/top_level.txt +0 -0
  66. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/__init__.py +0 -0
  67. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/bibliography.py +0 -0
  68. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/browser_lifecycle.py +0 -0
  69. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/check_numbers.py +0 -0
  70. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/citation_dialog.py +0 -0
  71. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/command_registry.py +0 -0
  72. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_filter_history.json +0 -0
  73. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_functions.py +0 -0
  74. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_migration.py +0 -0
  75. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_tags.lua +0 -0
  76. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_tags_margin.lua +0 -0
  77. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_tags_no_comments.lua +0 -0
  78. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comment_toggle.js +0 -0
  79. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comments.css +0 -0
  80. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/comments_author_colors.css +0 -0
  81. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/completions/git-mergein +0 -0
  82. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/copy_files.py +0 -0
  83. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/diff-doc.js +0 -0
  84. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/doc_contents.py +0 -0
  85. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/__init__.py +0 -0
  86. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/comparison.py +0 -0
  87. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  88. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/graph.py +0 -0
  89. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/source_jump.js +0 -0
  90. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  91. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/watch_graph.py +0 -0
  92. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/flowchart/wgrph_view.js +0 -0
  93. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/git_aliases.py +0 -0
  94. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/git_gd_history.py +0 -0
  95. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/git_gd_image.py +0 -0
  96. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/git_gd_qt.py +0 -0
  97. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/git_reattach.py +0 -0
  98. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/html_comments.py +0 -0
  99. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/html_uncomments.py +0 -0
  100. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/log_example.py +0 -0
  101. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/notebook/__init__.py +0 -0
  102. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/notebook/display.py +0 -0
  103. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/notebook/fast_build.py +0 -0
  104. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/notebook/tex_to_qmd.py +0 -0
  105. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/onewordify.py +0 -0
  106. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/onewordify_undo.py +0 -0
  107. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/outline.py +0 -0
  108. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/pdf_diff.py +0 -0
  109. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/rearrange_tex.py +0 -0
  110. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/searchacro.py +0 -0
  111. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/separate_comments.py +0 -0
  112. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/source_jump.py +0 -0
  113. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/split_conflict.py +0 -0
  114. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/unseparate_comments.py +0 -0
  115. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/update_check.py +0 -0
  116. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/xml2xlsx.vbs +0 -0
  117. {pydifftools-0.1.59 → pydifftools-0.1.60}/pydifftools/zotero.py +0 -0
  118. {pydifftools-0.1.59 → pydifftools-0.1.60}/setup.cfg +0 -0
  119. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_bibliography.py +0 -0
  120. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_check_single_use.py +0 -0
  121. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_comment_filters.py +0 -0
  122. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_comment_migration.py +0 -0
  123. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_git_gd_history.py +0 -0
  124. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_git_gd_image.py +0 -0
  125. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_git_gd_qt.py +0 -0
  126. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_git_mergein.py +0 -0
  127. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_git_pdf_diff.py +0 -0
  128. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_git_reattach.py +0 -0
  129. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_line_alignment.py +0 -0
  130. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_pdf_diff.py +0 -0
  131. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_qt_lifecycle.py +0 -0
  132. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_rrng.py +0 -0
  133. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_tex_to_qmd.py +0 -0
  134. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_update_check.py +0 -0
  135. {pydifftools-0.1.59 → pydifftools-0.1.60}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.59
3
+ Version: 0.1.60
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -274,6 +274,12 @@ included are (listed in order of fun/utility):
274
274
  retaining the exact command of an existing global ``gd`` alias. If none
275
275
  is configured, it installs ``!f() { pydifft gd "$@"; }; f``.
276
276
  Configure ``difftool.mygvim.cmd`` for the GUI diff tool.
277
+ Use ``pydifft gd --terminal [git diff args...]`` for a colored terminal
278
+ picker that opens diffs in foreground Vim without GUI configuration.
279
+ Navigate with arrows or ``j``/``k``, press Enter to open a diff, Space to
280
+ toggle its reviewed marker, and ``q`` to quit. Closing Vim returns to the
281
+ picker. Images and other binary files are skipped. Set ``NO_COLOR`` to
282
+ disable colors.
277
283
  - ``pydifft --add_to_git gd pd tree mergein reattach`` installs any selected subset
278
284
  of these global Git aliases in one invocation. Installation appears
279
285
  alongside the subcommands in root help; the former per-command
@@ -328,10 +334,14 @@ included are (listed in order of fun/utility):
328
334
  their words using Git's minimal diff, and restores reference whitespace.
329
335
  It then considers extra breaks that preserve an unchanged reference line
330
336
  without stranding an edited fragment of three words or fewer, and checks
331
- wrapping rules. When matching a baseline, edited lines may exceed the
332
- character width by at most four words before wrapping is required.
333
-
334
- * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
337
+ wrapping rules. ``--desired-width`` (default 45) limits the width of
338
+ lines created by removing existing breaks to match Git. Breaks are
339
+ retained when joining would exceed that desired width.
340
+
341
+ * ``--wrapnumber`` independently sets the maximum width (79 for ``cpb``
342
+ and ``wmatch``), including for stable prose from the Git baseline.
343
+ For example, ``cpb --desired-width 50 --wrapnumber 85 manuscript.md``
344
+ changes both limits.
335
345
  Wmatch also uses nearby reference widths for large edits, retaining
336
346
  unchanged reference lines and protected Markdown blocks.
337
347
 
@@ -408,3 +418,14 @@ Future versions will include:
408
418
  - Scripts for converting word html comments to latex commands.
409
419
 
410
420
  - converting to/form one word per line files (for doing things like wdiff, but with more control)
421
+
422
+ Concurrent cpb previews
423
+ -----------------------
424
+
425
+ ``cpb`` starts at loopback port 51235 and reports occupied ports to stdout
426
+ before trying the next port. The first process keeps a directory-to-port
427
+ registry; subsequent previews register their source file's directory there.
428
+ Run ``pydifft mfs`` from that directory to search the matching preview.
429
+ Directory paths are resolved so symlink aliases identify the same project.
430
+ Keep the first process open while using the other previews: its registry
431
+ lives in memory and ends when that process exits.
@@ -219,6 +219,12 @@ included are (listed in order of fun/utility):
219
219
  retaining the exact command of an existing global ``gd`` alias. If none
220
220
  is configured, it installs ``!f() { pydifft gd "$@"; }; f``.
221
221
  Configure ``difftool.mygvim.cmd`` for the GUI diff tool.
222
+ Use ``pydifft gd --terminal [git diff args...]`` for a colored terminal
223
+ picker that opens diffs in foreground Vim without GUI configuration.
224
+ Navigate with arrows or ``j``/``k``, press Enter to open a diff, Space to
225
+ toggle its reviewed marker, and ``q`` to quit. Closing Vim returns to the
226
+ picker. Images and other binary files are skipped. Set ``NO_COLOR`` to
227
+ disable colors.
222
228
  - ``pydifft --add_to_git gd pd tree mergein reattach`` installs any selected subset
223
229
  of these global Git aliases in one invocation. Installation appears
224
230
  alongside the subcommands in root help; the former per-command
@@ -273,10 +279,14 @@ included are (listed in order of fun/utility):
273
279
  their words using Git's minimal diff, and restores reference whitespace.
274
280
  It then considers extra breaks that preserve an unchanged reference line
275
281
  without stranding an edited fragment of three words or fewer, and checks
276
- wrapping rules. When matching a baseline, edited lines may exceed the
277
- character width by at most four words before wrapping is required.
278
-
279
- * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
282
+ wrapping rules. ``--desired-width`` (default 45) limits the width of
283
+ lines created by removing existing breaks to match Git. Breaks are
284
+ retained when joining would exceed that desired width.
285
+
286
+ * ``--wrapnumber`` independently sets the maximum width (79 for ``cpb``
287
+ and ``wmatch``), including for stable prose from the Git baseline.
288
+ For example, ``cpb --desired-width 50 --wrapnumber 85 manuscript.md``
289
+ changes both limits.
280
290
  Wmatch also uses nearby reference widths for large edits, retaining
281
291
  unchanged reference lines and protected Markdown blocks.
282
292
 
@@ -353,3 +363,14 @@ Future versions will include:
353
363
  - Scripts for converting word html comments to latex commands.
354
364
 
355
365
  - converting to/form one word per line files (for doing things like wdiff, but with more control)
366
+
367
+ Concurrent cpb previews
368
+ -----------------------
369
+
370
+ ``cpb`` starts at loopback port 51235 and reports occupied ports to stdout
371
+ before trying the next port. The first process keeps a directory-to-port
372
+ registry; subsequent previews register their source file's directory there.
373
+ Run ``pydifft mfs`` from that directory to search the matching preview.
374
+ Directory paths are resolved so symlink aliases identify the same project.
375
+ Keep the first process open while using the other previews: its registry
376
+ lives in memory and ends when that process exits.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.59
3
+ Version: 0.1.60
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -274,6 +274,12 @@ included are (listed in order of fun/utility):
274
274
  retaining the exact command of an existing global ``gd`` alias. If none
275
275
  is configured, it installs ``!f() { pydifft gd "$@"; }; f``.
276
276
  Configure ``difftool.mygvim.cmd`` for the GUI diff tool.
277
+ Use ``pydifft gd --terminal [git diff args...]`` for a colored terminal
278
+ picker that opens diffs in foreground Vim without GUI configuration.
279
+ Navigate with arrows or ``j``/``k``, press Enter to open a diff, Space to
280
+ toggle its reviewed marker, and ``q`` to quit. Closing Vim returns to the
281
+ picker. Images and other binary files are skipped. Set ``NO_COLOR`` to
282
+ disable colors.
277
283
  - ``pydifft --add_to_git gd pd tree mergein reattach`` installs any selected subset
278
284
  of these global Git aliases in one invocation. Installation appears
279
285
  alongside the subcommands in root help; the former per-command
@@ -328,10 +334,14 @@ included are (listed in order of fun/utility):
328
334
  their words using Git's minimal diff, and restores reference whitespace.
329
335
  It then considers extra breaks that preserve an unchanged reference line
330
336
  without stranding an edited fragment of three words or fewer, and checks
331
- wrapping rules. When matching a baseline, edited lines may exceed the
332
- character width by at most four words before wrapping is required.
333
-
334
- * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
337
+ wrapping rules. ``--desired-width`` (default 45) limits the width of
338
+ lines created by removing existing breaks to match Git. Breaks are
339
+ retained when joining would exceed that desired width.
340
+
341
+ * ``--wrapnumber`` independently sets the maximum width (79 for ``cpb``
342
+ and ``wmatch``), including for stable prose from the Git baseline.
343
+ For example, ``cpb --desired-width 50 --wrapnumber 85 manuscript.md``
344
+ changes both limits.
335
345
  Wmatch also uses nearby reference widths for large edits, retaining
336
346
  unchanged reference lines and protected Markdown blocks.
337
347
 
@@ -408,3 +418,14 @@ Future versions will include:
408
418
  - Scripts for converting word html comments to latex commands.
409
419
 
410
420
  - converting to/form one word per line files (for doing things like wdiff, but with more control)
421
+
422
+ Concurrent cpb previews
423
+ -----------------------
424
+
425
+ ``cpb`` starts at loopback port 51235 and reports occupied ports to stdout
426
+ before trying the next port. The first process keeps a directory-to-port
427
+ registry; subsequent previews register their source file's directory there.
428
+ Run ``pydifft mfs`` from that directory to search the matching preview.
429
+ Directory paths are resolved so symlink aliases identify the same project.
430
+ Keep the first process open while using the other previews: its registry
431
+ lives in memory and ends when that process exits.
@@ -70,6 +70,7 @@ pydifftools/git_gd.py
70
70
  pydifftools/git_gd_history.py
71
71
  pydifftools/git_gd_image.py
72
72
  pydifftools/git_gd_qt.py
73
+ pydifftools/git_gd_terminal.py
73
74
  pydifftools/git_reattach.py
74
75
  pydifftools/html_comments.py
75
76
  pydifftools/html_uncomments.py
@@ -115,6 +116,7 @@ tests/test_forward_search.py
115
116
  tests/test_git_gd_history.py
116
117
  tests/test_git_gd_image.py
117
118
  tests/test_git_gd_qt.py
119
+ tests/test_git_gd_terminal.py
118
120
  tests/test_git_mergein.py
119
121
  tests/test_git_pdf_diff.py
120
122
  tests/test_git_reattach.py
@@ -127,4 +129,5 @@ tests/test_tex_to_qmd.py
127
129
  tests/test_update_check.py
128
130
  tests/test_wrap_check.py
129
131
  tests/test_wrapping.py
132
+ tests/test_wrapping_width_limits.py
130
133
  tests/test_zotero.py
@@ -25,6 +25,7 @@ from .forward_search import (
25
25
  ForwardSearchProtocolError,
26
26
  ForwardSearchUnavailable,
27
27
  send_forward_search,
28
+ directory_port,
28
29
  )
29
30
  from .wrap_sentences import wr as wrap_sentences_wr # registers wrap command
30
31
  from .wrap_sentences import ( # registers wrchk command
@@ -55,7 +56,8 @@ from .notebook.fast_build import (
55
56
 
56
57
  from .command_registry import _COMMAND_SPECS, register_command
57
58
  from .wrapping_options import (
58
- DEFAULT_WIDTH, DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
59
+ DEFAULT_WIDTH, DEFAULT_DESIRED_WIDTH,
60
+ DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
59
61
  )
60
62
 
61
63
  _ARGCOMPLETE_SPEC = importlib.util.find_spec("argcomplete")
@@ -388,8 +390,11 @@ def gvr(arguments):
388
390
  def wmatch(
389
391
  arguments, wrapnumber=DEFAULT_WIDTH,
390
392
  trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
393
+ desired_width=DEFAULT_DESIRED_WIDTH,
391
394
  ):
392
- match_spaces.run(arguments, wrapnumber, trailing_dependent_phrase)
395
+ match_spaces.run(
396
+ arguments, wrapnumber, trailing_dependent_phrase, desired_width,
397
+ )
393
398
 
394
399
 
395
400
  @register_command("split conflict")
@@ -580,7 +585,38 @@ def mfs(text):
580
585
  def try_existing_listener():
581
586
  protocol_errors = []
582
587
  delivered = False
583
- for address in socket_addresses:
588
+ addresses = socket_addresses
589
+ try:
590
+ route = directory_port(socket_addresses[0], os.getcwd())
591
+ except ForwardSearchUnavailable:
592
+ route = None
593
+ except ForwardSearchProtocolError as exc:
594
+ raise RuntimeError(
595
+ "Refusing to open another browser window: " + str(exc)
596
+ ) from exc
597
+ if route and route["registry"]:
598
+ if route["port"] is not None:
599
+ try:
600
+ send_forward_search(
601
+ (FORWARD_SEARCH_HOST, route["port"]), search_text
602
+ )
603
+ return True
604
+ except ForwardSearchUnavailable:
605
+ directory_port(socket_addresses[0], os.getcwd(), port=None)
606
+ addresses = socket_addresses[1:]
607
+ for address in addresses:
608
+ if route and route["registry"] and address != socket_addresses[0]:
609
+ try:
610
+ candidate = directory_port(address, os.getcwd())
611
+ except ForwardSearchUnavailable:
612
+ continue
613
+ except ForwardSearchProtocolError as exc:
614
+ protocol_errors.append(str(exc))
615
+ continue
616
+ if candidate["registry"]:
617
+ if candidate["port"] is None:
618
+ continue
619
+ address = (FORWARD_SEARCH_HOST, candidate["port"])
584
620
  try:
585
621
  send_forward_search(address, search_text)
586
622
  except ForwardSearchUnavailable:
@@ -995,6 +1031,14 @@ def main(argv=None):
995
1031
  if subcommand in parser._pydifft_subparsers:
996
1032
  parser._pydifft_subparsers[subcommand].print_help()
997
1033
  return
1034
+ # gd forwards Git's options verbatim, including leading --cached/-M.
1035
+ # argparse's REMAINDER only accepts those after a positional argument.
1036
+ if argv[0] == "gd":
1037
+ option_end = argv.index("--") if "--" in argv else len(argv)
1038
+ if any(arg in ("-h", "--help") for arg in argv[1:option_end]):
1039
+ parser._pydifft_subparsers["gd"].print_help()
1040
+ return
1041
+ return gd(argv[1:])
998
1042
  namespace = parser.parse_args(argv)
999
1043
  if namespace.add_to_git is not None:
1000
1044
  if not namespace.add_to_git:
@@ -1020,6 +1064,8 @@ def main(argv=None):
1020
1064
  parser._pydifft_subparsers[namespace.command].error(
1021
1065
  "--wrapnumber must be at least 1"
1022
1066
  )
1067
+ if getattr(namespace, "desired_width", 45) < 1:
1068
+ parser.error("--desired-width must be at least 1")
1023
1069
  if getattr(namespace, "trailing_dependent_phrase",
1024
1070
  getattr(namespace, "punctuation_slop", 0)) < 0:
1025
1071
  parser._pydifft_subparsers[namespace.command].error(
@@ -21,7 +21,8 @@ from watchdog.observers import Observer
21
21
  from watchdog.observers.polling import PollingObserver
22
22
  from .command_registry import register_command
23
23
  from .wrapping_options import (
24
- DEFAULT_WIDTH, DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
24
+ DEFAULT_WIDTH, DEFAULT_DESIRED_WIDTH,
25
+ DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
25
26
  )
26
27
  from .zotero import recover_bibliography, zotero_notice
27
28
  from .comment_migration import prepare_comment_source
@@ -35,7 +36,10 @@ from .browser_lifecycle import (
35
36
  from .forward_search import (
36
37
  CPB_FORWARD_SEARCH_PORT,
37
38
  FORWARD_SEARCH_HOST,
38
- bind_forward_search_server,
39
+ bind_cpb_server,
40
+ directory_port,
41
+ ForwardSearchUnavailable,
42
+ ForwardSearchProtocolError,
39
43
  drain_forward_search_queue,
40
44
  serve_forward_search,
41
45
  )
@@ -513,6 +517,7 @@ def run_pandoc(
513
517
  wrapnumber=DEFAULT_WIDTH,
514
518
  trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
515
519
  diff=None,
520
+ desired_width=DEFAULT_DESIRED_WIDTH,
516
521
  ):
517
522
  # {{{ automatically fix Markdown source and request edits for
518
523
  # unfinished spans
@@ -523,6 +528,7 @@ def run_pandoc(
523
528
  report = autofix_markdown_file(
524
529
  filename, wrapnumber=wrapnumber, git_index=True, git_ref=diff,
525
530
  punctuation_slop=trailing_dependent_phrase,
531
+ desired_width=desired_width,
526
532
  )
527
533
  automatic_fixes_were_made |= bool(report["fixes"])
528
534
  if report["fixes"] or report["warnings"]:
@@ -1215,6 +1221,7 @@ def cpb(
1215
1221
  wrapnumber=DEFAULT_WIDTH,
1216
1222
  trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
1217
1223
  diff=None,
1224
+ desired_width=DEFAULT_DESIRED_WIDTH,
1218
1225
  ):
1219
1226
  source_path = os.path.normpath(os.path.abspath(filename))
1220
1227
  source_dir = os.path.dirname(source_path)
@@ -1222,12 +1229,12 @@ def cpb(
1222
1229
  comment_filter_session = {}
1223
1230
  search_queue = queue.Queue()
1224
1231
  stop_event = threading.Event()
1225
- forward_search_server = bind_forward_search_server(
1226
- (FORWARD_SEARCH_HOST, FORWARD_SEARCH_PORT), "cpb"
1232
+ forward_search_server = bind_cpb_server(
1233
+ (FORWARD_SEARCH_HOST, FORWARD_SEARCH_PORT)
1227
1234
  )
1228
1235
  socket_thread = threading.Thread(
1229
1236
  target=serve_forward_search,
1230
- args=(forward_search_server, stop_event, search_queue),
1237
+ args=(forward_search_server, stop_event, search_queue, source_dir),
1231
1238
  daemon=True,
1232
1239
  )
1233
1240
  chrome = None
@@ -1247,6 +1254,14 @@ def cpb(
1247
1254
  # port authoritative even while a slow initial build is in progress.
1248
1255
  socket_thread.start()
1249
1256
  socket_thread_started = True
1257
+ registry_address = (FORWARD_SEARCH_HOST, FORWARD_SEARCH_PORT)
1258
+ session_port = forward_search_server.getsockname()[1]
1259
+ if session_port != FORWARD_SEARCH_PORT:
1260
+ try:
1261
+ directory_port(registry_address, source_dir, port=session_port)
1262
+ except (ForwardSearchUnavailable, ForwardSearchProtocolError):
1263
+ print("cpb: standard port has no directory registry; "
1264
+ "mfs discovery is unavailable.")
1250
1265
 
1251
1266
  # Build before opening the browser so a failed build cannot leave an
1252
1267
  # orphaned browser session. Remember the source version from before
@@ -1271,6 +1286,7 @@ def cpb(
1271
1286
  wrapnumber=wrapnumber,
1272
1287
  trailing_dependent_phrase=trailing_dependent_phrase,
1273
1288
  diff=diff,
1289
+ desired_width=desired_width,
1274
1290
  )
1275
1291
  source_jump_server = SourceJumpServer(source_path)
1276
1292
  source_jump_server.start()
@@ -1418,6 +1434,7 @@ def cpb(
1418
1434
  trailing_dependent_phrase
1419
1435
  ),
1420
1436
  diff=diff,
1437
+ desired_width=desired_width,
1421
1438
  )
1422
1439
  append_autorefresh(
1423
1440
  html_file, source_jump_server.url
@@ -1463,6 +1480,11 @@ def cpb(
1463
1480
  except KeyboardInterrupt:
1464
1481
  pass
1465
1482
  finally:
1483
+ if socket_thread_started and session_port != FORWARD_SEARCH_PORT:
1484
+ try:
1485
+ directory_port(registry_address, source_dir, port=None)
1486
+ except (ForwardSearchUnavailable, ForwardSearchProtocolError):
1487
+ pass
1466
1488
  stop_event.set()
1467
1489
  forward_search_server.close()
1468
1490
  if source_jump_server is not None:
@@ -1,5 +1,8 @@
1
1
  """Acknowledged loopback transport for browser forward-search requests."""
2
2
 
3
+ import errno
4
+ import json
5
+ import os
3
6
  import queue
4
7
  import socket
5
8
 
@@ -41,9 +44,13 @@ def bind_forward_search_server(address, service_name):
41
44
  return server
42
45
 
43
46
 
44
- def serve_forward_search(server, stop_event, search_queue):
47
+ def serve_forward_search(server, stop_event, search_queue, directory=None):
45
48
  """Accept clients, queue valid UTF-8 payloads, and acknowledge receipt."""
46
49
 
50
+ registrations = {}
51
+ if directory is not None:
52
+ registrations[os.path.realpath(directory)] = server.getsockname()[1]
53
+
47
54
  while not stop_event.is_set():
48
55
  try:
49
56
  connection, _ = server.accept()
@@ -71,6 +78,24 @@ def serve_forward_search(server, stop_event, search_queue):
71
78
  search_text = payload.decode("utf-8")
72
79
  except UnicodeDecodeError:
73
80
  continue
81
+ if search_text.startswith("pydifft-directory\n"):
82
+ try:
83
+ request = json.loads(search_text.split("\n", 1)[1])
84
+ directory_key = os.path.realpath(request["directory"])
85
+ if "port" in request:
86
+ port = request["port"]
87
+ if port is None:
88
+ registrations.pop(directory_key, None)
89
+ elif isinstance(port, int) and 0 < port < 65536:
90
+ registrations[directory_key] = port
91
+ response = json.dumps({
92
+ "port": registrations.get(directory_key),
93
+ "registry": directory is not None,
94
+ }).encode()
95
+ connection.sendall(response)
96
+ except (ValueError, KeyError, TypeError, OSError):
97
+ pass
98
+ continue
74
99
  search_queue.put(search_text)
75
100
  try:
76
101
  connection.sendall(FORWARD_SEARCH_ACK)
@@ -127,3 +152,53 @@ def drain_forward_search_queue(search_queue):
127
152
  searches.append(search_queue.get_nowait())
128
153
  except queue.Empty:
129
154
  return searches
155
+
156
+
157
+ # also used by: continuous.py and command_line.py
158
+ def directory_port(address, directory, **registration):
159
+ """Query or update the first cpb process's directory registry."""
160
+ request = {"directory": os.path.realpath(directory), **registration}
161
+ try:
162
+ with socket.create_connection(address, timeout=1) as client:
163
+ client.settimeout(1)
164
+ client.sendall(("pydifft-directory\n" + json.dumps(request)).encode())
165
+ client.shutdown(socket.SHUT_WR)
166
+ response = bytearray()
167
+ while True:
168
+ chunk = client.recv(4096)
169
+ if not chunk:
170
+ break
171
+ response.extend(chunk)
172
+ if len(response) > 4096:
173
+ raise ValueError("oversized registry response")
174
+ except OSError as exc:
175
+ raise ForwardSearchUnavailable(address) from exc
176
+ try:
177
+ result = json.loads(response)
178
+ if not isinstance(result, dict) or not isinstance(
179
+ result.get("registry"), bool
180
+ ):
181
+ raise ValueError("invalid registry response")
182
+ port = result.get("port")
183
+ if port is not None and (type(port) is not int or not 0 < port < 65536):
184
+ raise ValueError("invalid port")
185
+ return result
186
+ except (ValueError, TypeError) as exc:
187
+ raise ForwardSearchProtocolError(
188
+ f"Invalid directory registry response from {address}"
189
+ ) from exc
190
+
191
+
192
+ # also used by: continuous.py and tests/test_forward_search.py
193
+ def bind_cpb_server(address):
194
+ """Take the first available port, reporting every occupied port."""
195
+ for port in range(address[1], 65536):
196
+ try:
197
+ return bind_forward_search_server((address[0], port), "cpb")
198
+ except RuntimeError as exc:
199
+ if not isinstance(exc.__cause__, OSError) or (
200
+ exc.__cause__.errno != errno.EADDRINUSE
201
+ ):
202
+ raise
203
+ print(f"cpb: port {port} is taken; trying port {port + 1}.")
204
+ raise RuntimeError("No free cpb forward-search port remains")
@@ -273,6 +273,7 @@ def build_image_difftool_command(
273
273
  )
274
274
 
275
275
 
276
+ # also used by: tests/cli/test_commands.py.
276
277
  def build_image_score_command(
277
278
  diff_args: Sequence[str], entry: DiffEntry
278
279
  ) -> list[str]:
@@ -363,6 +364,7 @@ def repo_name() -> str:
363
364
  return Path(top).name
364
365
 
365
366
 
367
+ # also used by: git_gd_history.py and tests/cli/test_commands.py.
366
368
  def build_entries(argv: Sequence[str]):
367
369
  # {{{ separate Git options/revisions from file filters
368
370
  if "--" in argv:
@@ -407,9 +409,18 @@ def diff_entry_sort_key(entry: DiffEntry):
407
409
  return (group, 2, entry.path)
408
410
 
409
411
 
412
+ # also used by: tests/test_git_gd_terminal.py.
410
413
  def main(argv: Sequence[str]) -> int:
414
+ # Only consume our flag before Git's pathspec separator.
415
+ args = list(argv)
416
+ option_end = args.index("--") if "--" in args else len(args)
417
+ terminal = "--terminal" in args[:option_end]
418
+ if terminal:
419
+ args = [arg for arg in args[:option_end] if arg != "--terminal"] + (
420
+ args[option_end:]
421
+ )
411
422
  try:
412
- diff_args, entries = build_entries(argv)
423
+ diff_args, entries = build_entries(args)
413
424
  name = repo_name()
414
425
  except subprocess.CalledProcessError as exc:
415
426
  if isinstance(exc.stderr, bytes):
@@ -417,6 +428,10 @@ def main(argv: Sequence[str]) -> int:
417
428
  elif exc.stderr:
418
429
  sys.stderr.write(exc.stderr)
419
430
  return exc.returncode
431
+ if terminal:
432
+ from .git_gd_terminal import launch_review
433
+
434
+ return launch_review(name, diff_args, entries)
420
435
  try:
421
436
  from .git_gd_qt import launch_review
422
437
  except ModuleNotFoundError as exc:
@@ -429,10 +444,13 @@ def main(argv: Sequence[str]) -> int:
429
444
  return launch_review(name, diff_args, entries, command_args=argv)
430
445
 
431
446
 
447
+ # also used by: command_line.py's registered command dispatcher.
432
448
  @register_command(
433
449
  "review changed files in a Qt table before launching git difftool",
434
450
  "review changed files in a Qt table before launching git difftool\n\n"
435
451
  "With no arguments, review unstaged changes, like git diff.\n"
452
+ "Add --terminal for a colored terminal picker and Vim diffs;\n"
453
+ "images and binary files are skipped in terminal mode.\n"
436
454
  "Use pydifft tree to browse Git history.\n\n"
437
455
  "Install the matching git alias automatically with:\n"
438
456
  " pydifft --add_to_git gd\n\n"
@@ -440,11 +458,12 @@ def main(argv: Sequence[str]) -> int:
440
458
  " git config --global alias.gd '!f() { pydifft gd \"$@\"; }; f'\n\n"
441
459
  "This command shells out to git difftool --tool=mygvim, so keep\n"
442
460
  "difftool.mygvim.cmd configured in your git config.",
461
+ help={"terminal": "Use a terminal picker and Vim; skip images/binaries."},
443
462
  )
444
- def gd(arguments):
463
+ def gd(arguments, terminal=False):
445
464
  """Mirror ``git_gd_qt.py``."""
446
465
 
447
- return_code = main(arguments)
466
+ return_code = main(["--terminal", *arguments] if terminal else arguments)
448
467
  if return_code != 0:
449
468
  raise SystemExit(return_code)
450
469