pyDiffTools 0.1.58__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.58/pyDiffTools.egg-info → pydifftools-0.1.60}/PKG-INFO +37 -6
  2. {pydifftools-0.1.58 → pydifftools-0.1.60}/README.rst +36 -5
  3. {pydifftools-0.1.58 → pydifftools-0.1.60/pyDiffTools.egg-info}/PKG-INFO +37 -6
  4. {pydifftools-0.1.58 → pydifftools-0.1.60}/pyDiffTools.egg-info/SOURCES.txt +5 -0
  5. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/command_line.py +51 -4
  6. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/continuous.py +27 -5
  7. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/forward_search.py +76 -1
  8. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/git_aliases.py +2 -0
  9. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/git_gd.py +22 -3
  10. pydifftools-0.1.60/pydifftools/git_gd_terminal.py +182 -0
  11. pydifftools-0.1.60/pydifftools/git_reattach.py +145 -0
  12. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/line_alignment.py +28 -1
  13. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/match_spaces.py +10 -2
  14. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/wrap_sentences.py +6 -4
  15. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/wrapping_options.py +7 -0
  16. {pydifftools-0.1.58 → pydifftools-0.1.60}/pyproject.toml +1 -1
  17. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_browser_lifecycle.py +5 -2
  18. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_command_line_help.py +6 -2
  19. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_continuous_shutdown.py +10 -7
  20. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_forward_search.py +40 -0
  21. pydifftools-0.1.60/tests/test_git_gd_terminal.py +98 -0
  22. pydifftools-0.1.60/tests/test_git_reattach.py +186 -0
  23. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_source_jump.py +2 -2
  24. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_wrap_check.py +17 -12
  25. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_wrapping.py +2 -7
  26. pydifftools-0.1.60/tests/test_wrapping_width_limits.py +91 -0
  27. {pydifftools-0.1.58 → pydifftools-0.1.60}/LICENSE.md +0 -0
  28. {pydifftools-0.1.58 → pydifftools-0.1.60}/MANIFEST.in +0 -0
  29. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/_quarto.yml +0 -0
  30. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  31. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  32. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  33. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  34. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  35. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  36. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  37. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  38. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  39. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  40. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  41. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  42. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  43. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  44. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  45. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  46. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  47. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  48. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  49. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  50. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  51. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  52. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/example.qmd +0 -0
  53. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/example.tex +0 -0
  54. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/index.qmd +0 -0
  55. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  56. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  57. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  58. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  59. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/independent.qmd +0 -0
  60. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/index.qmd +0 -0
  61. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  62. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  63. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  64. {pydifftools-0.1.58 → pydifftools-0.1.60}/example_notebook/project1/test_include.qmd +0 -0
  65. {pydifftools-0.1.58 → pydifftools-0.1.60}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  66. {pydifftools-0.1.58 → pydifftools-0.1.60}/pyDiffTools.egg-info/entry_points.txt +0 -0
  67. {pydifftools-0.1.58 → pydifftools-0.1.60}/pyDiffTools.egg-info/requires.txt +0 -0
  68. {pydifftools-0.1.58 → pydifftools-0.1.60}/pyDiffTools.egg-info/top_level.txt +0 -0
  69. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/__init__.py +0 -0
  70. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/bibliography.py +0 -0
  71. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/browser_lifecycle.py +0 -0
  72. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/check_numbers.py +0 -0
  73. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/citation_dialog.py +0 -0
  74. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/command_registry.py +0 -0
  75. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_filter_history.json +0 -0
  76. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_functions.py +0 -0
  77. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_migration.py +0 -0
  78. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_tags.lua +0 -0
  79. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_tags_margin.lua +0 -0
  80. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_tags_no_comments.lua +0 -0
  81. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comment_toggle.js +0 -0
  82. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comments.css +0 -0
  83. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/comments_author_colors.css +0 -0
  84. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/completions/git-mergein +0 -0
  85. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/copy_files.py +0 -0
  86. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/diff-doc.js +0 -0
  87. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/doc_contents.py +0 -0
  88. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/__init__.py +0 -0
  89. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/comparison.py +0 -0
  90. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  91. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/graph.py +0 -0
  92. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/source_jump.js +0 -0
  93. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  94. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/watch_graph.py +0 -0
  95. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/flowchart/wgrph_view.js +0 -0
  96. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/git_gd_history.py +0 -0
  97. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/git_gd_image.py +0 -0
  98. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/git_gd_qt.py +0 -0
  99. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/html_comments.py +0 -0
  100. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/html_uncomments.py +0 -0
  101. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/log_example.py +0 -0
  102. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/notebook/__init__.py +0 -0
  103. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/notebook/display.py +0 -0
  104. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/notebook/fast_build.py +0 -0
  105. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/notebook/tex_to_qmd.py +0 -0
  106. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/onewordify.py +0 -0
  107. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/onewordify_undo.py +0 -0
  108. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/outline.py +0 -0
  109. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/pdf_diff.py +0 -0
  110. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/rearrange_tex.py +0 -0
  111. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/searchacro.py +0 -0
  112. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/separate_comments.py +0 -0
  113. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/source_jump.py +0 -0
  114. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/split_conflict.py +0 -0
  115. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/unseparate_comments.py +0 -0
  116. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/update_check.py +0 -0
  117. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/xml2xlsx.vbs +0 -0
  118. {pydifftools-0.1.58 → pydifftools-0.1.60}/pydifftools/zotero.py +0 -0
  119. {pydifftools-0.1.58 → pydifftools-0.1.60}/setup.cfg +0 -0
  120. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_bibliography.py +0 -0
  121. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_check_single_use.py +0 -0
  122. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_comment_filters.py +0 -0
  123. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_comment_migration.py +0 -0
  124. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_git_gd_history.py +0 -0
  125. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_git_gd_image.py +0 -0
  126. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_git_gd_qt.py +0 -0
  127. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_git_mergein.py +0 -0
  128. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_git_pdf_diff.py +0 -0
  129. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_line_alignment.py +0 -0
  130. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_pdf_diff.py +0 -0
  131. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_qt_lifecycle.py +0 -0
  132. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_rrng.py +0 -0
  133. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_tex_to_qmd.py +0 -0
  134. {pydifftools-0.1.58 → pydifftools-0.1.60}/tests/test_update_check.py +0 -0
  135. {pydifftools-0.1.58 → 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.58
3
+ Version: 0.1.60
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -274,10 +274,26 @@ 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
- - ``pydifft --add_to_git gd pd tree mergein`` installs any selected subset
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.
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
280
286
  ``--install`` and ``--add-to-git`` flags have been removed.
287
+ - ``pydifft reattach [--path DIRECTORY]`` attaches detached checkouts to
288
+ branches pointing at exactly their current commits, including initialized
289
+ submodules recursively. Remote-only matches create local tracking branches.
290
+ It never resets existing branches or pulls newer commits. With several
291
+ matches it prefers the origin default branch when possible, otherwise it
292
+ reports ambiguity and leaves the checkout detached. A local branch with
293
+ the required name pointing elsewhere is preserved. ``--dry-run`` previews
294
+ changes, ``--no-recursive`` limits the operation to one repository, and
295
+ ``--fetch`` refreshes remote refs before matching. Install the global
296
+ ``git reattach`` alias with ``pydifft --add_to_git reattach``.
281
297
  - ``pydifft mergein BRANCH [--remote REMOTE]`` updates an incoming local
282
298
  branch without switching branches, then merges it into the current
283
299
  branch. Install ``git mergein`` with ``pydifft --add_to_git mergein``.
@@ -318,10 +334,14 @@ included are (listed in order of fun/utility):
318
334
  their words using Git's minimal diff, and restores reference whitespace.
319
335
  It then considers extra breaks that preserve an unchanged reference line
320
336
  without stranding an edited fragment of three words or fewer, and checks
321
- wrapping rules. When matching a baseline, edited lines may exceed the
322
- character width by at most four words before wrapping is required.
323
-
324
- * ``--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.
325
345
  Wmatch also uses nearby reference widths for large edits, retaining
326
346
  unchanged reference lines and protected Markdown blocks.
327
347
 
@@ -398,3 +418,14 @@ Future versions will include:
398
418
  - Scripts for converting word html comments to latex commands.
399
419
 
400
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,10 +219,26 @@ 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
- - ``pydifft --add_to_git gd pd tree mergein`` installs any selected subset
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.
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
225
231
  ``--install`` and ``--add-to-git`` flags have been removed.
232
+ - ``pydifft reattach [--path DIRECTORY]`` attaches detached checkouts to
233
+ branches pointing at exactly their current commits, including initialized
234
+ submodules recursively. Remote-only matches create local tracking branches.
235
+ It never resets existing branches or pulls newer commits. With several
236
+ matches it prefers the origin default branch when possible, otherwise it
237
+ reports ambiguity and leaves the checkout detached. A local branch with
238
+ the required name pointing elsewhere is preserved. ``--dry-run`` previews
239
+ changes, ``--no-recursive`` limits the operation to one repository, and
240
+ ``--fetch`` refreshes remote refs before matching. Install the global
241
+ ``git reattach`` alias with ``pydifft --add_to_git reattach``.
226
242
  - ``pydifft mergein BRANCH [--remote REMOTE]`` updates an incoming local
227
243
  branch without switching branches, then merges it into the current
228
244
  branch. Install ``git mergein`` with ``pydifft --add_to_git mergein``.
@@ -263,10 +279,14 @@ included are (listed in order of fun/utility):
263
279
  their words using Git's minimal diff, and restores reference whitespace.
264
280
  It then considers extra breaks that preserve an unchanged reference line
265
281
  without stranding an edited fragment of three words or fewer, and checks
266
- wrapping rules. When matching a baseline, edited lines may exceed the
267
- character width by at most four words before wrapping is required.
268
-
269
- * ``--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.
270
290
  Wmatch also uses nearby reference widths for large edits, retaining
271
291
  unchanged reference lines and protected Markdown blocks.
272
292
 
@@ -343,3 +363,14 @@ Future versions will include:
343
363
  - Scripts for converting word html comments to latex commands.
344
364
 
345
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.58
3
+ Version: 0.1.60
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -274,10 +274,26 @@ 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
- - ``pydifft --add_to_git gd pd tree mergein`` installs any selected subset
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.
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
280
286
  ``--install`` and ``--add-to-git`` flags have been removed.
287
+ - ``pydifft reattach [--path DIRECTORY]`` attaches detached checkouts to
288
+ branches pointing at exactly their current commits, including initialized
289
+ submodules recursively. Remote-only matches create local tracking branches.
290
+ It never resets existing branches or pulls newer commits. With several
291
+ matches it prefers the origin default branch when possible, otherwise it
292
+ reports ambiguity and leaves the checkout detached. A local branch with
293
+ the required name pointing elsewhere is preserved. ``--dry-run`` previews
294
+ changes, ``--no-recursive`` limits the operation to one repository, and
295
+ ``--fetch`` refreshes remote refs before matching. Install the global
296
+ ``git reattach`` alias with ``pydifft --add_to_git reattach``.
281
297
  - ``pydifft mergein BRANCH [--remote REMOTE]`` updates an incoming local
282
298
  branch without switching branches, then merges it into the current
283
299
  branch. Install ``git mergein`` with ``pydifft --add_to_git mergein``.
@@ -318,10 +334,14 @@ included are (listed in order of fun/utility):
318
334
  their words using Git's minimal diff, and restores reference whitespace.
319
335
  It then considers extra breaks that preserve an unchanged reference line
320
336
  without stranding an edited fragment of three words or fewer, and checks
321
- wrapping rules. When matching a baseline, edited lines may exceed the
322
- character width by at most four words before wrapping is required.
323
-
324
- * ``--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.
325
345
  Wmatch also uses nearby reference widths for large edits, retaining
326
346
  unchanged reference lines and protected Markdown blocks.
327
347
 
@@ -398,3 +418,14 @@ Future versions will include:
398
418
  - Scripts for converting word html comments to latex commands.
399
419
 
400
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,8 @@ 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
74
+ pydifftools/git_reattach.py
73
75
  pydifftools/html_comments.py
74
76
  pydifftools/html_uncomments.py
75
77
  pydifftools/line_alignment.py
@@ -114,8 +116,10 @@ tests/test_forward_search.py
114
116
  tests/test_git_gd_history.py
115
117
  tests/test_git_gd_image.py
116
118
  tests/test_git_gd_qt.py
119
+ tests/test_git_gd_terminal.py
117
120
  tests/test_git_mergein.py
118
121
  tests/test_git_pdf_diff.py
122
+ tests/test_git_reattach.py
119
123
  tests/test_line_alignment.py
120
124
  tests/test_pdf_diff.py
121
125
  tests/test_qt_lifecycle.py
@@ -125,4 +129,5 @@ tests/test_tex_to_qmd.py
125
129
  tests/test_update_check.py
126
130
  tests/test_wrap_check.py
127
131
  tests/test_wrapping.py
132
+ tests/test_wrapping_width_limits.py
128
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
@@ -36,6 +37,7 @@ from .comment_functions import matchingbrackets
36
37
  from .copy_files import copy_image_files
37
38
  from .searchacro import replace_acros
38
39
  from .rearrange_tex import run as rearrange_tex_run
40
+ from .git_reattach import reattach # registers branch reattachment
39
41
  from .git_gd import gd # registers git difftool review command
40
42
  from .pdf_diff import pd, git_pd # registers PDF diff command
41
43
  from .git_aliases import (
@@ -54,7 +56,8 @@ from .notebook.fast_build import (
54
56
 
55
57
  from .command_registry import _COMMAND_SPECS, register_command
56
58
  from .wrapping_options import (
57
- DEFAULT_WIDTH, DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
59
+ DEFAULT_WIDTH, DEFAULT_DESIRED_WIDTH,
60
+ DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
58
61
  )
59
62
 
60
63
  _ARGCOMPLETE_SPEC = importlib.util.find_spec("argcomplete")
@@ -387,8 +390,11 @@ def gvr(arguments):
387
390
  def wmatch(
388
391
  arguments, wrapnumber=DEFAULT_WIDTH,
389
392
  trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
393
+ desired_width=DEFAULT_DESIRED_WIDTH,
390
394
  ):
391
- match_spaces.run(arguments, wrapnumber, trailing_dependent_phrase)
395
+ match_spaces.run(
396
+ arguments, wrapnumber, trailing_dependent_phrase, desired_width,
397
+ )
392
398
 
393
399
 
394
400
  @register_command("split conflict")
@@ -579,7 +585,38 @@ def mfs(text):
579
585
  def try_existing_listener():
580
586
  protocol_errors = []
581
587
  delivered = False
582
- 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"])
583
620
  try:
584
621
  send_forward_search(address, search_text)
585
622
  except ForwardSearchUnavailable:
@@ -909,7 +946,7 @@ def build_parser():
909
946
  # argparse does not validate REMAINDER choices; main checks them first.
910
947
  subparsers.container.add_argument(
911
948
  "--add_to_git", nargs=argparse.REMAINDER, choices=list(GIT_ALIASES),
912
- help=("Install one or more global Git aliases: gd, pd, tree, mergein "
949
+ help=("Install one or more global Git aliases: gd, pd, tree, mergein, reattach "
913
950
  "(without a subcommand)."),
914
951
  )
915
952
  for name, spec in _COMMAND_SPECS.items():
@@ -994,6 +1031,14 @@ def main(argv=None):
994
1031
  if subcommand in parser._pydifft_subparsers:
995
1032
  parser._pydifft_subparsers[subcommand].print_help()
996
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:])
997
1042
  namespace = parser.parse_args(argv)
998
1043
  if namespace.add_to_git is not None:
999
1044
  if not namespace.add_to_git:
@@ -1019,6 +1064,8 @@ def main(argv=None):
1019
1064
  parser._pydifft_subparsers[namespace.command].error(
1020
1065
  "--wrapnumber must be at least 1"
1021
1066
  )
1067
+ if getattr(namespace, "desired_width", 45) < 1:
1068
+ parser.error("--desired-width must be at least 1")
1022
1069
  if getattr(namespace, "trailing_dependent_phrase",
1023
1070
  getattr(namespace, "punctuation_slop", 0)) < 0:
1024
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")
@@ -11,10 +11,12 @@ GIT_ALIASES = {
11
11
  "gd": '!f() { pydifft gd "$@"; }; f',
12
12
  "pd": '!f() { pydifft pd --git "$@"; }; f',
13
13
  "tree": '!f() { pydifft tree "$@"; }; f',
14
+ "reattach": '!f() { pydifft reattach "$@"; }; f',
14
15
  "mergein": '!f() { pydifft mergein "$@"; }; f',
15
16
  }
16
17
 
17
18
 
19
+ # also used by: pydifftools/command_line.py for global CLI alias installation.
18
20
  def install_git_alias(name, command, preserve_existing=False):
19
21
  """Install an alias and its completion, optionally retaining its value."""
20
22
  if preserve_existing:
@@ -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