pyDiffTools 0.1.51__tar.gz → 0.1.52__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 (129) hide show
  1. {pydifftools-0.1.51/pyDiffTools.egg-info → pydifftools-0.1.52}/PKG-INFO +56 -4
  2. {pydifftools-0.1.51 → pydifftools-0.1.52}/README.rst +55 -3
  3. {pydifftools-0.1.51 → pydifftools-0.1.52/pyDiffTools.egg-info}/PKG-INFO +56 -4
  4. {pydifftools-0.1.51 → pydifftools-0.1.52}/pyDiffTools.egg-info/SOURCES.txt +4 -0
  5. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/command_line.py +3 -0
  6. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/source_jump.js +24 -9
  7. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/watch_graph.py +2 -0
  8. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/wgrph_view.js +61 -27
  9. pydifftools-0.1.52/pydifftools/git_aliases.py +30 -0
  10. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/git_gd.py +19 -13
  11. pydifftools-0.1.52/pydifftools/pdf_diff.py +579 -0
  12. pydifftools-0.1.52/pydifftools/source_jump.py +203 -0
  13. {pydifftools-0.1.51 → pydifftools-0.1.52}/pyproject.toml +1 -1
  14. pydifftools-0.1.52/tests/test_git_pdf_diff.py +424 -0
  15. pydifftools-0.1.52/tests/test_pdf_diff.py +495 -0
  16. pydifftools-0.1.52/tests/test_source_jump.py +424 -0
  17. pydifftools-0.1.51/pydifftools/source_jump.py +0 -128
  18. pydifftools-0.1.51/tests/test_source_jump.py +0 -85
  19. {pydifftools-0.1.51 → pydifftools-0.1.52}/LICENSE.md +0 -0
  20. {pydifftools-0.1.51 → pydifftools-0.1.52}/MANIFEST.in +0 -0
  21. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/_quarto.yml +0 -0
  22. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  23. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  24. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  25. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  26. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  27. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  28. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  29. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  30. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  31. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  32. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  33. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  34. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  35. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  36. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  37. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  38. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  39. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  40. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  41. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  42. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  43. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  44. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/example.qmd +0 -0
  45. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/example.tex +0 -0
  46. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/index.qmd +0 -0
  47. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  48. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  49. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  50. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  51. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/independent.qmd +0 -0
  52. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/index.qmd +0 -0
  53. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  54. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  55. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  56. {pydifftools-0.1.51 → pydifftools-0.1.52}/example_notebook/project1/test_include.qmd +0 -0
  57. {pydifftools-0.1.51 → pydifftools-0.1.52}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  58. {pydifftools-0.1.51 → pydifftools-0.1.52}/pyDiffTools.egg-info/entry_points.txt +0 -0
  59. {pydifftools-0.1.51 → pydifftools-0.1.52}/pyDiffTools.egg-info/requires.txt +0 -0
  60. {pydifftools-0.1.51 → pydifftools-0.1.52}/pyDiffTools.egg-info/top_level.txt +0 -0
  61. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/__init__.py +0 -0
  62. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/bibliography.py +0 -0
  63. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/browser_lifecycle.py +0 -0
  64. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/check_numbers.py +0 -0
  65. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/citation_dialog.py +0 -0
  66. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/command_registry.py +0 -0
  67. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_filter_history.json +0 -0
  68. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_functions.py +0 -0
  69. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_migration.py +0 -0
  70. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_tags.lua +0 -0
  71. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_tags_margin.lua +0 -0
  72. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_tags_no_comments.lua +0 -0
  73. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comment_toggle.js +0 -0
  74. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comments.css +0 -0
  75. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/comments_author_colors.css +0 -0
  76. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/continuous.py +0 -0
  77. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/copy_files.py +0 -0
  78. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/diff-doc.js +0 -0
  79. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/doc_contents.py +0 -0
  80. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/__init__.py +0 -0
  81. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/comparison.py +0 -0
  82. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  83. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/graph.py +0 -0
  84. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  85. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/forward_search.py +0 -0
  86. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/git_gd_history.py +0 -0
  87. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/git_gd_image.py +0 -0
  88. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/git_gd_qt.py +0 -0
  89. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/html_comments.py +0 -0
  90. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/html_uncomments.py +0 -0
  91. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/line_alignment.py +0 -0
  92. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/log_example.py +0 -0
  93. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/match_spaces.py +0 -0
  94. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/notebook/__init__.py +0 -0
  95. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/notebook/display.py +0 -0
  96. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/notebook/fast_build.py +0 -0
  97. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/notebook/tex_to_qmd.py +0 -0
  98. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/onewordify.py +0 -0
  99. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/onewordify_undo.py +0 -0
  100. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/outline.py +0 -0
  101. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/rearrange_tex.py +0 -0
  102. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/searchacro.py +0 -0
  103. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/separate_comments.py +0 -0
  104. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/split_conflict.py +0 -0
  105. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/unseparate_comments.py +0 -0
  106. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/update_check.py +0 -0
  107. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/wrap_sentences.py +0 -0
  108. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/wrapping_options.py +0 -0
  109. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/xml2xlsx.vbs +0 -0
  110. {pydifftools-0.1.51 → pydifftools-0.1.52}/pydifftools/zotero.py +0 -0
  111. {pydifftools-0.1.51 → pydifftools-0.1.52}/setup.cfg +0 -0
  112. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_bibliography.py +0 -0
  113. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_browser_lifecycle.py +0 -0
  114. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_check_single_use.py +0 -0
  115. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_command_line_help.py +0 -0
  116. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_comment_filters.py +0 -0
  117. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_comment_migration.py +0 -0
  118. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_continuous_shutdown.py +0 -0
  119. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_forward_search.py +0 -0
  120. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_git_gd_history.py +0 -0
  121. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_git_gd_image.py +0 -0
  122. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_git_gd_qt.py +0 -0
  123. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_line_alignment.py +0 -0
  124. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_rrng.py +0 -0
  125. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_tex_to_qmd.py +0 -0
  126. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_update_check.py +0 -0
  127. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_wrap_check.py +0 -0
  128. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_wrapping.py +0 -0
  129. {pydifftools-0.1.51 → pydifftools-0.1.52}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.51
3
+ Version: 0.1.52
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -185,9 +185,13 @@ included are (listed in order of fun/utility):
185
185
  clearing the entire query shows the full plan, including completed and
186
186
  undated tasks. Each embedded SVG and live refresh uses the same URL mode.
187
187
  The search button beside Home and Zoom finds text in the visible graph,
188
- highlights matches, and centers them at a readable font size. Tab or Enter
189
- advances to the next match; Shift+Tab or Shift+Enter goes back. Esc closes
190
- search. Phrases can span wrapped lines and formatted text.
188
+ highlights matches, and centers the containing task at your chosen zoom.
189
+ Tab or Enter advances to the next match; Shift+Tab or Shift+Enter goes
190
+ back. Esc closes search. Phrases can span wrapped lines and formatted
191
+ text. Resizing the window scales the current view with the viewport.
192
+ The ``1:1`` button displays fonts at their declared pixel sizes, including
193
+ correction for GraphViz's SVG scaling. Right-click a task and select
194
+ Jump to source to open its YAML definition in gvim.
191
195
  Comparison uses the same repository-relative YAML path; a file absent
192
196
  from the revision is treated as an empty plan. Historical file renames
193
197
  are not followed, and comparison markup is never written to YAML.
@@ -227,6 +231,10 @@ included are (listed in order of fun/utility):
227
231
  changes, an additional red/blue alpha-difference view is included.
228
232
  Run ``pydifft gd --install`` to add the matching ``git gd`` alias to
229
233
  your global git config.
234
+ ``pydifft gd --add-to-git`` offers the same installation, retaining the
235
+ exact command of an existing global ``gd`` alias. If none is configured,
236
+ it installs ``!f() { pydifft gd "$@"; }; f``. Alias installation is shared
237
+ with ``pd`` and ``tree``.
230
238
  - `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
231
239
  the bundled templates and example ``project1`` hierarchy, then downloads
232
240
  MathJax into ``_template/mathjax`` so the builder can run immediately.
@@ -262,6 +270,50 @@ included are (listed in order of fun/utility):
262
270
  this rule. The option is shared by ``cpb``, ``wmatch``, ``wr`` and
263
271
  ``wrchk``; ``--punctuation-slop`` remains an alias.
264
272
 
273
+ - ``pydifft pd OLD NEW [--no-compile]`` ("PDF diff") compares two ``.md``
274
+ files or two ``.tex`` files. It writes ``NEW_diff.tex`` beside the newer
275
+ input and, by default, compiles ``NEW_diff.pdf`` there with ``pdflatex``.
276
+ For example, ``pydifft pd old.md paper.md`` produces ``paper_diff.tex``
277
+ and ``paper_diff.pdf``. ``--no-compile`` retains the diff TeX without
278
+ running a compiler and supports TeX fragments as well as full documents.
279
+
280
+ Both formats use ``mylatexdiff-preamble.sty`` and the existing custom
281
+ ``latexdiff`` options and postprocessing. The preamble is searched for
282
+ in the current directory, beside NEW, then through ``kpsewhich``.
283
+ Tools must be available on the caller's PATH, including any TeX Live
284
+ directory configured in your shell. Markdown additionally requires
285
+ Pandoc and pandoc-crossref; conversion honors declared bibliography and
286
+ CSL metadata, renders citations and cross-references, and resolves local
287
+ images beside each source. Converted TeX intermediates are temporary;
288
+ the Markdown sources are unchanged.
289
+
290
+ Markdown code blocks retain Pandoc syntax highlighting. Changed lines
291
+ get a colored background and a ``+`` or ``-`` marker, with each line
292
+ processed independently so highlighted code remains valid TeX.
293
+ Prose keeps the custom preamble styling.
294
+
295
+ Python runs ``pdflatex`` beside NEW until references and the table of
296
+ contents settle (at most six passes), running ``bibtex`` or ``biber``
297
+ when required. ``latexmk`` and its configuration are not used.
298
+ Compilation artifacts and the diff TeX remain available if compilation
299
+ fails. Existing generated outputs can be replaced on a successful rerun.
300
+ The former ``git-latexdiffnocompile.sh`` is a compatibility wrapper for
301
+ ``pydifft pd --no-compile``.
302
+
303
+ Run ``pydifft pd --add-to-git`` to install the global ``git pd`` alias.
304
+ It accepts Git diff arguments: ``git pd 00ffaa -- filename.md`` compares
305
+ that revision with the working file; bare ``git pd`` compares unstaged
306
+ changes; ``git pd --cached`` compares staged changes; and
307
+ ``git pd OLD NEW -- filename.md`` compares two revisions. Ranges and
308
+ Git pathspecs work as usual, including from repository subdirectories.
309
+ Each changed Markdown or TeX file produces its own ``NAME_diff.tex`` and
310
+ PDF beside the document's working-tree path. ``git pd --no-compile``
311
+ generates only TeX. Other file types are skipped, and unchanged files
312
+ produce no output. Additions, deletions, and renames are supported;
313
+ unresolved merge conflicts must be resolved first. Historical document
314
+ snapshots use the current project's bibliography, images, and TeX
315
+ includes. The shared installer edits only the requested global Git alias.
316
+
265
317
  - `pydifft wd` ("word diff"): generate "track changes" word files starting from pandoc markdown in a git history. Assuming that you have copied diff-doc.js (copied + licensed from elsewhere) into your home directory, this will use pandoc to convert the markdown files to MS Word, then use the MS Word comparison tool to generate a document where all relevant changes are shown with "track changes."
266
318
 
267
319
  * by default, this uses the file `template.docx` in the current directory as a pandoc word template
@@ -130,9 +130,13 @@ included are (listed in order of fun/utility):
130
130
  clearing the entire query shows the full plan, including completed and
131
131
  undated tasks. Each embedded SVG and live refresh uses the same URL mode.
132
132
  The search button beside Home and Zoom finds text in the visible graph,
133
- highlights matches, and centers them at a readable font size. Tab or Enter
134
- advances to the next match; Shift+Tab or Shift+Enter goes back. Esc closes
135
- search. Phrases can span wrapped lines and formatted text.
133
+ highlights matches, and centers the containing task at your chosen zoom.
134
+ Tab or Enter advances to the next match; Shift+Tab or Shift+Enter goes
135
+ back. Esc closes search. Phrases can span wrapped lines and formatted
136
+ text. Resizing the window scales the current view with the viewport.
137
+ The ``1:1`` button displays fonts at their declared pixel sizes, including
138
+ correction for GraphViz's SVG scaling. Right-click a task and select
139
+ Jump to source to open its YAML definition in gvim.
136
140
  Comparison uses the same repository-relative YAML path; a file absent
137
141
  from the revision is treated as an empty plan. Historical file renames
138
142
  are not followed, and comparison markup is never written to YAML.
@@ -172,6 +176,10 @@ included are (listed in order of fun/utility):
172
176
  changes, an additional red/blue alpha-difference view is included.
173
177
  Run ``pydifft gd --install`` to add the matching ``git gd`` alias to
174
178
  your global git config.
179
+ ``pydifft gd --add-to-git`` offers the same installation, retaining the
180
+ exact command of an existing global ``gd`` alias. If none is configured,
181
+ it installs ``!f() { pydifft gd "$@"; }; f``. Alias installation is shared
182
+ with ``pd`` and ``tree``.
175
183
  - `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
176
184
  the bundled templates and example ``project1`` hierarchy, then downloads
177
185
  MathJax into ``_template/mathjax`` so the builder can run immediately.
@@ -207,6 +215,50 @@ included are (listed in order of fun/utility):
207
215
  this rule. The option is shared by ``cpb``, ``wmatch``, ``wr`` and
208
216
  ``wrchk``; ``--punctuation-slop`` remains an alias.
209
217
 
218
+ - ``pydifft pd OLD NEW [--no-compile]`` ("PDF diff") compares two ``.md``
219
+ files or two ``.tex`` files. It writes ``NEW_diff.tex`` beside the newer
220
+ input and, by default, compiles ``NEW_diff.pdf`` there with ``pdflatex``.
221
+ For example, ``pydifft pd old.md paper.md`` produces ``paper_diff.tex``
222
+ and ``paper_diff.pdf``. ``--no-compile`` retains the diff TeX without
223
+ running a compiler and supports TeX fragments as well as full documents.
224
+
225
+ Both formats use ``mylatexdiff-preamble.sty`` and the existing custom
226
+ ``latexdiff`` options and postprocessing. The preamble is searched for
227
+ in the current directory, beside NEW, then through ``kpsewhich``.
228
+ Tools must be available on the caller's PATH, including any TeX Live
229
+ directory configured in your shell. Markdown additionally requires
230
+ Pandoc and pandoc-crossref; conversion honors declared bibliography and
231
+ CSL metadata, renders citations and cross-references, and resolves local
232
+ images beside each source. Converted TeX intermediates are temporary;
233
+ the Markdown sources are unchanged.
234
+
235
+ Markdown code blocks retain Pandoc syntax highlighting. Changed lines
236
+ get a colored background and a ``+`` or ``-`` marker, with each line
237
+ processed independently so highlighted code remains valid TeX.
238
+ Prose keeps the custom preamble styling.
239
+
240
+ Python runs ``pdflatex`` beside NEW until references and the table of
241
+ contents settle (at most six passes), running ``bibtex`` or ``biber``
242
+ when required. ``latexmk`` and its configuration are not used.
243
+ Compilation artifacts and the diff TeX remain available if compilation
244
+ fails. Existing generated outputs can be replaced on a successful rerun.
245
+ The former ``git-latexdiffnocompile.sh`` is a compatibility wrapper for
246
+ ``pydifft pd --no-compile``.
247
+
248
+ Run ``pydifft pd --add-to-git`` to install the global ``git pd`` alias.
249
+ It accepts Git diff arguments: ``git pd 00ffaa -- filename.md`` compares
250
+ that revision with the working file; bare ``git pd`` compares unstaged
251
+ changes; ``git pd --cached`` compares staged changes; and
252
+ ``git pd OLD NEW -- filename.md`` compares two revisions. Ranges and
253
+ Git pathspecs work as usual, including from repository subdirectories.
254
+ Each changed Markdown or TeX file produces its own ``NAME_diff.tex`` and
255
+ PDF beside the document's working-tree path. ``git pd --no-compile``
256
+ generates only TeX. Other file types are skipped, and unchanged files
257
+ produce no output. Additions, deletions, and renames are supported;
258
+ unresolved merge conflicts must be resolved first. Historical document
259
+ snapshots use the current project's bibliography, images, and TeX
260
+ includes. The shared installer edits only the requested global Git alias.
261
+
210
262
  - `pydifft wd` ("word diff"): generate "track changes" word files starting from pandoc markdown in a git history. Assuming that you have copied diff-doc.js (copied + licensed from elsewhere) into your home directory, this will use pandoc to convert the markdown files to MS Word, then use the MS Word comparison tool to generate a document where all relevant changes are shown with "track changes."
211
263
 
212
264
  * by default, this uses the file `template.docx` in the current directory as a pandoc word template
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.51
3
+ Version: 0.1.52
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -185,9 +185,13 @@ included are (listed in order of fun/utility):
185
185
  clearing the entire query shows the full plan, including completed and
186
186
  undated tasks. Each embedded SVG and live refresh uses the same URL mode.
187
187
  The search button beside Home and Zoom finds text in the visible graph,
188
- highlights matches, and centers them at a readable font size. Tab or Enter
189
- advances to the next match; Shift+Tab or Shift+Enter goes back. Esc closes
190
- search. Phrases can span wrapped lines and formatted text.
188
+ highlights matches, and centers the containing task at your chosen zoom.
189
+ Tab or Enter advances to the next match; Shift+Tab or Shift+Enter goes
190
+ back. Esc closes search. Phrases can span wrapped lines and formatted
191
+ text. Resizing the window scales the current view with the viewport.
192
+ The ``1:1`` button displays fonts at their declared pixel sizes, including
193
+ correction for GraphViz's SVG scaling. Right-click a task and select
194
+ Jump to source to open its YAML definition in gvim.
191
195
  Comparison uses the same repository-relative YAML path; a file absent
192
196
  from the revision is treated as an empty plan. Historical file renames
193
197
  are not followed, and comparison markup is never written to YAML.
@@ -227,6 +231,10 @@ included are (listed in order of fun/utility):
227
231
  changes, an additional red/blue alpha-difference view is included.
228
232
  Run ``pydifft gd --install`` to add the matching ``git gd`` alias to
229
233
  your global git config.
234
+ ``pydifft gd --add-to-git`` offers the same installation, retaining the
235
+ exact command of an existing global ``gd`` alias. If none is configured,
236
+ it installs ``!f() { pydifft gd "$@"; }; f``. Alias installation is shared
237
+ with ``pd`` and ``tree``.
230
238
  - `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
231
239
  the bundled templates and example ``project1`` hierarchy, then downloads
232
240
  MathJax into ``_template/mathjax`` so the builder can run immediately.
@@ -262,6 +270,50 @@ included are (listed in order of fun/utility):
262
270
  this rule. The option is shared by ``cpb``, ``wmatch``, ``wr`` and
263
271
  ``wrchk``; ``--punctuation-slop`` remains an alias.
264
272
 
273
+ - ``pydifft pd OLD NEW [--no-compile]`` ("PDF diff") compares two ``.md``
274
+ files or two ``.tex`` files. It writes ``NEW_diff.tex`` beside the newer
275
+ input and, by default, compiles ``NEW_diff.pdf`` there with ``pdflatex``.
276
+ For example, ``pydifft pd old.md paper.md`` produces ``paper_diff.tex``
277
+ and ``paper_diff.pdf``. ``--no-compile`` retains the diff TeX without
278
+ running a compiler and supports TeX fragments as well as full documents.
279
+
280
+ Both formats use ``mylatexdiff-preamble.sty`` and the existing custom
281
+ ``latexdiff`` options and postprocessing. The preamble is searched for
282
+ in the current directory, beside NEW, then through ``kpsewhich``.
283
+ Tools must be available on the caller's PATH, including any TeX Live
284
+ directory configured in your shell. Markdown additionally requires
285
+ Pandoc and pandoc-crossref; conversion honors declared bibliography and
286
+ CSL metadata, renders citations and cross-references, and resolves local
287
+ images beside each source. Converted TeX intermediates are temporary;
288
+ the Markdown sources are unchanged.
289
+
290
+ Markdown code blocks retain Pandoc syntax highlighting. Changed lines
291
+ get a colored background and a ``+`` or ``-`` marker, with each line
292
+ processed independently so highlighted code remains valid TeX.
293
+ Prose keeps the custom preamble styling.
294
+
295
+ Python runs ``pdflatex`` beside NEW until references and the table of
296
+ contents settle (at most six passes), running ``bibtex`` or ``biber``
297
+ when required. ``latexmk`` and its configuration are not used.
298
+ Compilation artifacts and the diff TeX remain available if compilation
299
+ fails. Existing generated outputs can be replaced on a successful rerun.
300
+ The former ``git-latexdiffnocompile.sh`` is a compatibility wrapper for
301
+ ``pydifft pd --no-compile``.
302
+
303
+ Run ``pydifft pd --add-to-git`` to install the global ``git pd`` alias.
304
+ It accepts Git diff arguments: ``git pd 00ffaa -- filename.md`` compares
305
+ that revision with the working file; bare ``git pd`` compares unstaged
306
+ changes; ``git pd --cached`` compares staged changes; and
307
+ ``git pd OLD NEW -- filename.md`` compares two revisions. Ranges and
308
+ Git pathspecs work as usual, including from repository subdirectories.
309
+ Each changed Markdown or TeX file produces its own ``NAME_diff.tex`` and
310
+ PDF beside the document's working-tree path. ``git pd --no-compile``
311
+ generates only TeX. Other file types are skipped, and unchanged files
312
+ produce no output. Additions, deletions, and renames are supported;
313
+ unresolved merge conflicts must be resolved first. Historical document
314
+ snapshots use the current project's bibliography, images, and TeX
315
+ includes. The shared installer edits only the requested global Git alias.
316
+
265
317
  - `pydifft wd` ("word diff"): generate "track changes" word files starting from pandoc markdown in a git history. Assuming that you have copied diff-doc.js (copied + licensed from elsewhere) into your home directory, this will use pandoc to convert the markdown files to MS Word, then use the MS Word comparison tool to generate a document where all relevant changes are shown with "track changes."
266
318
 
267
319
  * by default, this uses the file `template.docx` in the current directory as a pandoc word template
@@ -65,6 +65,7 @@ pydifftools/copy_files.py
65
65
  pydifftools/diff-doc.js
66
66
  pydifftools/doc_contents.py
67
67
  pydifftools/forward_search.py
68
+ pydifftools/git_aliases.py
68
69
  pydifftools/git_gd.py
69
70
  pydifftools/git_gd_history.py
70
71
  pydifftools/git_gd_image.py
@@ -77,6 +78,7 @@ pydifftools/match_spaces.py
77
78
  pydifftools/onewordify.py
78
79
  pydifftools/onewordify_undo.py
79
80
  pydifftools/outline.py
81
+ pydifftools/pdf_diff.py
80
82
  pydifftools/rearrange_tex.py
81
83
  pydifftools/searchacro.py
82
84
  pydifftools/separate_comments.py
@@ -111,7 +113,9 @@ tests/test_forward_search.py
111
113
  tests/test_git_gd_history.py
112
114
  tests/test_git_gd_image.py
113
115
  tests/test_git_gd_qt.py
116
+ tests/test_git_pdf_diff.py
114
117
  tests/test_line_alignment.py
118
+ tests/test_pdf_diff.py
115
119
  tests/test_rrng.py
116
120
  tests/test_source_jump.py
117
121
  tests/test_tex_to_qmd.py
@@ -36,6 +36,7 @@ from .copy_files import copy_image_files
36
36
  from .searchacro import replace_acros
37
37
  from .rearrange_tex import run as rearrange_tex_run
38
38
  from .git_gd import gd # registers git difftool review command
39
+ from .pdf_diff import pd, git_pd # registers PDF diff command
39
40
  from .flowchart.watch_graph import wgrph
40
41
  from .flowchart.graph import load_graph_yaml
41
42
  from .notebook.tex_to_qmd import tex2qmd
@@ -933,6 +934,8 @@ def build_parser():
933
934
  def main(argv=None):
934
935
  if argv is None:
935
936
  argv = sys.argv[1:]
937
+ if argv[:2] == ["pd", "--git"]:
938
+ return git_pd(argv[2:])
936
939
  if argv and argv[0] in ("--ver", "--version"):
937
940
  build_parser().parse_args(argv)
938
941
  return
@@ -28,12 +28,21 @@
28
28
 
29
29
  let phrase = '';
30
30
 
31
+ // Preserve HTML tag boundaries without adding spaces to words such as
32
+ // H<sub>2</sub>O. Python treats this reserved marker as a formatting gap.
33
+ function normalizeSourceText(text, trimEnd = true) {
34
+ const normalized = text.replace(/[\s\uE000]+/g, function (gap) {
35
+ return /\s/.test(gap) ? ' ' : '\uE000';
36
+ }).replace(/^[\s\uE000]+/, '');
37
+ return trimEnd ? normalized.replace(/[\s\uE000]+$/, '') : normalized;
38
+ }
39
+
31
40
  function hideMenu() {
32
41
  menu.style.display = 'none';
33
42
  }
34
43
 
35
44
  function showMenu(event, text) {
36
- phrase = (text || '').replace(/\s+/g, ' ').trim();
45
+ phrase = normalizeSourceText(text || '');
37
46
  if (!phrase) {
38
47
  hideMenu();
39
48
  return;
@@ -71,8 +80,8 @@
71
80
  const block = targetElement.closest(
72
81
  'p,li,blockquote,h1,h2,h3,h4,h5,h6,dt,dd,td,th'
73
82
  ) || targetElement;
74
- // Drop rendered math and cross-reference links so the phrase resembles
75
- // the searchable Markdown source, where mfs removes those constructs.
83
+ // Drop rendered math and figure-reference links, whose displayed text
84
+ // differs from their Markdown source.
76
85
  let text = '';
77
86
  let clickedOffset = 0;
78
87
  let found = false;
@@ -93,11 +102,18 @@
93
102
  text += ' ';
94
103
  return;
95
104
  }
105
+ const tagBoundary = node !== block && node.nodeType === Node.ELEMENT_NODE;
106
+ if (tagBoundary) {
107
+ text += '\uE000';
108
+ }
96
109
  node.childNodes.forEach(collectText);
110
+ if (tagBoundary) {
111
+ text += '\uE000';
112
+ }
97
113
  }
98
114
  collectText(block);
99
115
  const rawText = text;
100
- text = rawText.replace(/\s+/g, ' ').trim();
116
+ text = normalizeSourceText(rawText);
101
117
  if (!text) {
102
118
  return '';
103
119
  }
@@ -107,10 +123,9 @@
107
123
 
108
124
  // The raw DOM offset is used to find sentence bounds before whitespace is
109
125
  // collapsed. Recompute its visible offset after normalization.
110
- const normalizedPrefix = rawText
111
- .slice(0, clickedOffset)
112
- .replace(/\s+/g, ' ')
113
- .replace(/^\s+/, '');
126
+ const normalizedPrefix = normalizeSourceText(
127
+ rawText.slice(0, clickedOffset), false
128
+ );
114
129
  clickedOffset = normalizedPrefix.length;
115
130
  const before = text.slice(0, clickedOffset);
116
131
  const after = text.slice(clickedOffset);
@@ -119,7 +134,7 @@
119
134
  before.lastIndexOf('!'),
120
135
  before.lastIndexOf('?')
121
136
  ) + 1;
122
- const endMatch = after.match(/[.!?](?:\s|$)/);
137
+ const endMatch = after.match(/[.!?]\uE000*(?:\s|$)/);
123
138
  const end = endMatch ? clickedOffset + endMatch.index + 1 : text.length;
124
139
  return text.slice(start, end).trim() || text;
125
140
  }
@@ -239,6 +239,8 @@ def _watch_html(
239
239
  "</button>"
240
240
  "<button id='wgrph-zoom-in' title='Zoom in'>+</button>"
241
241
  "<button id='wgrph-zoom-out' title='Zoom out'>&#8722;</button>"
242
+ "<button id='wgrph-actual-size' title='Actual size: native font size'"
243
+ " aria-label='Actual size'>1:1</button>"
242
244
  "<button id='wgrph-search' title='Search graph (Ctrl+F)'"
243
245
  " aria-label='Search graph' aria-expanded='false'"
244
246
  " aria-controls='wgrph-search-panel'>&#128269;</button>"
@@ -43,19 +43,23 @@
43
43
  matchIndex = (matchIndex + step + matches.length) % matches.length;
44
44
  searchStatus.textContent = `${matchIndex + 1} of ${matches.length}`;
45
45
  const sizes = panZoom.getSizes();
46
- const pan = panZoom.getPan();
46
+ const viewport = svgDoc.querySelector('.svg-pan-zoom_viewport');
47
+ // DOM ranges reflect the last rendered frame, which can lag behind the
48
+ // pan/zoom state while typing. Use that frame's matrix for highlights.
49
+ const matrix = viewport.getScreenCTM();
47
50
  const highlight = svgDoc.createElementNS(
48
51
  'http://www.w3.org/2000/svg', 'g'
49
52
  );
50
53
  highlight.id = 'wgrph-search-highlight';
51
54
  highlight.setAttribute('pointer-events', 'none');
52
- const boxes = matches[matchIndex].map(function (range) {
55
+ const match = matches[matchIndex];
56
+ match.ranges.forEach(function (range) {
53
57
  const bounds = range.getBoundingClientRect();
54
58
  const box = {
55
- x: (bounds.x - pan.x) / sizes.realZoom,
56
- y: (bounds.y - pan.y) / sizes.realZoom,
57
- width: bounds.width / sizes.realZoom,
58
- height: bounds.height / sizes.realZoom,
59
+ x: (bounds.x - matrix.e) / matrix.a,
60
+ y: (bounds.y - matrix.f) / matrix.d,
61
+ width: bounds.width / matrix.a,
62
+ height: bounds.height / matrix.d,
59
63
  };
60
64
  const rect = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'rect');
61
65
  for (const key of ['x', 'y', 'width', 'height']) {
@@ -66,25 +70,20 @@
66
70
  rect.setAttribute('stroke', '#b77900');
67
71
  rect.setAttribute('vector-effect', 'non-scaling-stroke');
68
72
  highlight.appendChild(rect);
69
- return box;
70
73
  });
71
- svgDoc.querySelector('.svg-pan-zoom_viewport').appendChild(highlight);
74
+ viewport.appendChild(highlight);
72
75
  if (focus) {
73
- const left = Math.min(...boxes.map(b => b.x));
74
- const right = Math.max(...boxes.map(b => b.x + b.width));
75
- const top = Math.min(...boxes.map(b => b.y));
76
- const bottom = Math.max(...boxes.map(b => b.y + b.height));
77
- const element = matches[matchIndex][0].startContainer.parentElement;
78
- const fontSize = parseFloat(svgDoc.defaultView.getComputedStyle(element)
79
- .fontSize) || 14;
80
- // Target an 18 CSS-pixel font regardless of the graph's initial fit.
81
- const zoom = panZoom.getZoom() * 18 / fontSize / sizes.realZoom;
82
- panZoom.setMaxZoom(Math.max(50, zoom));
83
- panZoom.zoom(zoom);
84
- const realZoom = panZoom.getSizes().realZoom;
76
+ // Center the containing task while keeping the user's chosen scale.
77
+ const bounds = match.node.getBBox();
78
+ const point = svgDoc.documentElement.createSVGPoint();
79
+ point.x = bounds.x + bounds.width / 2;
80
+ point.y = bounds.y + bounds.height / 2;
81
+ const center = point.matrixTransform(
82
+ matrix.inverse().multiply(match.node.getScreenCTM())
83
+ );
85
84
  panZoom.pan({
86
- x: sizes.width / 2 - (left + right) / 2 * realZoom,
87
- y: sizes.height / 2 - (top + bottom) / 2 * realZoom,
85
+ x: sizes.width / 2 - center.x * sizes.realZoom,
86
+ y: sizes.height / 2 - center.y * sizes.realZoom,
88
87
  });
89
88
  }
90
89
  }
@@ -115,7 +114,7 @@
115
114
  }
116
115
  }
117
116
  if (ranges.length) {
118
- matches.push(ranges);
117
+ matches.push({ ranges, node: entry.node });
119
118
  }
120
119
  start = entry.text.indexOf(query, start + query.length);
121
120
  }
@@ -232,9 +231,11 @@
232
231
  home();
233
232
  } else {
234
233
  const restore = savedView;
235
- panZoom.zoom(
236
- (restore.realZoom / panZoom.getSizes().realZoom) * panZoom.getZoom()
237
- );
234
+ const zoom = (restore.realZoom / panZoom.getSizes().realZoom) *
235
+ panZoom.getZoom();
236
+ panZoom.setMinZoom(Math.min(0.1, zoom));
237
+ panZoom.setMaxZoom(Math.max(50, zoom));
238
+ panZoom.zoom(zoom);
238
239
  panZoom.pan(restore.pan);
239
240
  }
240
241
  saveView();
@@ -264,7 +265,7 @@
264
265
  }
265
266
  }
266
267
  }
267
- return { text, positions };
268
+ return { text, positions, node };
268
269
  });
269
270
  // }}}
270
271
  search(false);
@@ -390,11 +391,44 @@
390
391
  init();
391
392
  window.addEventListener('resize', function () {
392
393
  if (panZoom !== null) {
394
+ // Keep the same magnification relative to the whole-graph fit.
395
+ const before = panZoom.getSizes();
396
+ const pan = panZoom.getPan();
397
+ const zoom = panZoom.getZoom();
398
+ const atHome = Math.abs(zoom - 1) < 1e-6 &&
399
+ Math.abs(pan.x + before.viewBox.x * before.realZoom -
400
+ (before.width - before.viewBox.width * before.realZoom) / 2) < 0.5 &&
401
+ Math.abs(pan.y + before.viewBox.y * before.realZoom) < 0.5;
393
402
  panZoom.resize();
403
+ if (atHome) {
404
+ home();
405
+ } else {
406
+ panZoom.zoom(zoom);
407
+ const after = panZoom.getSizes();
408
+ const factor = after.realZoom / before.realZoom;
409
+ panZoom.pan({
410
+ x: after.width / 2 + (pan.x - before.width / 2) * factor,
411
+ y: after.height / 2 + (pan.y - before.height / 2) * factor,
412
+ });
413
+ }
414
+ saveView();
394
415
  }
395
416
  });
396
417
  document.addEventListener('keydown', handleKeys);
397
418
  document.getElementById('wgrph-home').addEventListener('click', home);
419
+ document.getElementById('wgrph-actual-size').addEventListener('click', function () {
420
+ // Graphviz may scale its graph group before viewer zoom is applied.
421
+ const viewport = initializedDoc.querySelector('.svg-pan-zoom_viewport');
422
+ const text = initializedDoc.querySelector('g.node text');
423
+ const matrix = text ? viewport.getScreenCTM().inverse().multiply(
424
+ text.getScreenCTM()
425
+ ) : { a: 1, b: 0 };
426
+ const zoom = panZoom.getZoom() / panZoom.getSizes().realZoom /
427
+ Math.hypot(matrix.a, matrix.b);
428
+ panZoom.setMinZoom(Math.min(0.1, zoom));
429
+ panZoom.setMaxZoom(Math.max(50, zoom));
430
+ panZoom.zoom(zoom);
431
+ });
398
432
  document
399
433
  .getElementById('wgrph-zoom-in')
400
434
  .addEventListener('click', function () {
@@ -0,0 +1,30 @@
1
+ """Shared installation of the package's global Git aliases."""
2
+
3
+ import subprocess
4
+
5
+
6
+ def install_git_alias(name, command, preserve_existing=False):
7
+ """Install an alias, optionally retaining its exact configured command."""
8
+ if preserve_existing:
9
+ current = subprocess.run(
10
+ ["git", "config", "--global", "--null", "--get", f"alias.{name}"],
11
+ capture_output=True,
12
+ text=True,
13
+ )
14
+ if current.returncode == 0 and current.stdout:
15
+ # Remove only Git's terminator, preserving command whitespace.
16
+ command = current.stdout[:-1]
17
+ elif current.returncode not in (0, 1):
18
+ raise SystemExit(
19
+ f"Cannot read global alias.{name}: {current.stderr.strip()}"
20
+ )
21
+ try:
22
+ subprocess.run(
23
+ ["git", "config", "--global", f"alias.{name}", command],
24
+ check=True,
25
+ )
26
+ except (OSError, subprocess.CalledProcessError) as exc:
27
+ raise SystemExit(
28
+ f"Cannot install global alias.{name}: {exc}"
29
+ ) from None
30
+ print(f"Installed global git alias: alias.{name} -> {command}")
@@ -24,6 +24,7 @@ from pathlib import Path
24
24
  from typing import Sequence
25
25
 
26
26
  from .command_registry import register_command
27
+ from .git_aliases import install_git_alias
27
28
 
28
29
  INSTALL_ALIAS_VALUE = '!f() { pydifft gd "$@"; }; f'
29
30
  DIFFTOOL_NAME = "mygvim"
@@ -186,6 +187,7 @@ def changed_paths(
186
187
  return [entry.path for entry in changed_entries(diff_args, pathspec)]
187
188
 
188
189
 
190
+ # also used by: git_gd_qt.py and tests/test_git_gd_history.py.
189
191
  def numstat_for_paths(diff_args: Sequence[str], paths: Sequence[str]):
190
192
  cmd = [
191
193
  "diff",
@@ -435,7 +437,7 @@ def main(argv: Sequence[str]) -> int:
435
437
  "With no arguments, review unstaged changes, like git diff.\n"
436
438
  "Use pydifft tree to browse Git history.\n\n"
437
439
  "Install the matching git alias automatically with:\n"
438
- " pydifft gd --install\n\n"
440
+ " pydifft gd --add-to-git (or --install)\n\n"
439
441
  "or add it yourself with:\n"
440
442
  " git config --global alias.gd '!f() { pydifft gd \"$@\"; }; f'\n\n"
441
443
  "This command shells out to git difftool --tool=mygvim, so keep\n"
@@ -446,19 +448,26 @@ def main(argv: Sequence[str]) -> int:
446
448
  "runs this subcommand."
447
449
  ),
448
450
  },
451
+ argument_options={
452
+ "add_to_git": {
453
+ "help": (
454
+ "Install the global git gd alias, preserving its current "
455
+ "command."
456
+ ),
457
+ },
458
+ },
449
459
  )
450
- def gd(arguments, install=False):
460
+ def gd(arguments, install=False, add_to_git=False):
451
461
  """Mirror ``git_gd_qt.py`` and optionally install ``git gd``."""
452
462
 
453
- if install:
463
+ if install or add_to_git:
454
464
  if arguments:
455
- raise SystemExit("pydifft gd --install does not take diff args")
465
+ flag = "--add-to-git" if add_to_git else "--install"
466
+ raise SystemExit(f"pydifft gd {flag} does not take diff args")
456
467
  # {{{ install the Git alias and check difftool configuration
457
- subprocess.run(
458
- ["git", "config", "--global", "alias.gd", INSTALL_ALIAS_VALUE],
459
- check=True,
468
+ install_git_alias(
469
+ "gd", INSTALL_ALIAS_VALUE, preserve_existing=True,
460
470
  )
461
- print("Installed global git alias: alias.gd -> pydifft gd")
462
471
  tool_cmd = subprocess.run(
463
472
  ["git", "config", "--global", "--get", "difftool.mygvim.cmd"],
464
473
  capture_output=True,
@@ -490,12 +499,9 @@ def tree(install=False):
490
499
  """Open the history browser or install its Git alias."""
491
500
  try:
492
501
  if install:
493
- subprocess.run(
494
- ["git", "config", "--global", "alias.tree",
495
- '!f() { pydifft tree "$@"; }; f'],
496
- check=True,
502
+ install_git_alias(
503
+ "tree", '!f() { pydifft tree "$@"; }; f',
497
504
  )
498
- print("Installed global git alias: alias.tree -> pydifft tree")
499
505
  return
500
506
  from PySide6.QtWidgets import QApplication
501
507
  from .git_gd_history import HistoryWindow, load_history