backlogops-gui 0.3__tar.gz → 0.4__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 (51) hide show
  1. {backlogops_gui-0.3 → backlogops_gui-0.4}/PKG-INFO +21 -5
  2. {backlogops_gui-0.3 → backlogops_gui-0.4}/README_pypi.md +18 -2
  3. {backlogops_gui-0.3 → backlogops_gui-0.4}/backlogops_gui.egg-info/PKG-INFO +21 -5
  4. {backlogops_gui-0.3 → backlogops_gui-0.4}/backlogops_gui.egg-info/SOURCES.txt +4 -0
  5. {backlogops_gui-0.3 → backlogops_gui-0.4}/backlogops_gui.egg-info/requires.txt +1 -1
  6. {backlogops_gui-0.3 → backlogops_gui-0.4}/pyproject.toml +2 -3
  7. {backlogops_gui-0.3 → backlogops_gui-0.4}/setup.py +2 -2
  8. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/application.py +226 -29
  9. backlogops_gui-0.4/src/backlogops_gui/auto_scroll.py +37 -0
  10. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/backlog_actions.py +8 -4
  11. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/backlog_window.py +206 -35
  12. backlogops_gui-0.4/src/backlogops_gui/choice_dialogs.py +132 -0
  13. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/file_choosers.py +7 -0
  14. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/gui_wizard.py +33 -2
  15. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_actions.py +1 -1
  16. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_base.py +4 -4
  17. backlogops_gui-0.4/src/backlogops_gui/jira_read.py +108 -0
  18. backlogops_gui-0.4/src/backlogops_gui/token_dialog.py +143 -0
  19. backlogops_gui-0.4/src/backlogops_gui/wizard_form.py +486 -0
  20. backlogops_gui-0.4/src/backlogops_gui/wizard_path.py +166 -0
  21. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/wizard_table.py +56 -26
  22. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/wizard_window.py +84 -7
  23. backlogops_gui-0.3/src/backlogops_gui/choice_dialogs.py +0 -139
  24. backlogops_gui-0.3/src/backlogops_gui/jira_read.py +0 -79
  25. {backlogops_gui-0.3 → backlogops_gui-0.4}/LICENSE.txt +0 -0
  26. {backlogops_gui-0.3 → backlogops_gui-0.4}/backlogops_gui.egg-info/dependency_links.txt +0 -0
  27. {backlogops_gui-0.3 → backlogops_gui-0.4}/backlogops_gui.egg-info/entry_points.txt +0 -0
  28. {backlogops_gui-0.3 → backlogops_gui-0.4}/backlogops_gui.egg-info/top_level.txt +0 -0
  29. {backlogops_gui-0.3 → backlogops_gui-0.4}/setup.cfg +0 -0
  30. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/__init__.py +0 -0
  31. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/__main__.py +0 -0
  32. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/_migrate_warn.py +0 -0
  33. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/backlog_dialogs.py +0 -0
  34. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/backlog_io.py +0 -0
  35. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/blog_version_reporter.py +0 -0
  36. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/close_binding.py +0 -0
  37. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/format_dialogs.py +0 -0
  38. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/gui_style.py +0 -0
  39. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_dialogs.py +0 -0
  40. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_order.py +0 -0
  41. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_rank.py +0 -0
  42. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_rename.py +0 -0
  43. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_update.py +0 -0
  44. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/jira_write.py +0 -0
  45. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/log_buffer.py +0 -0
  46. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/modal_dialog.py +0 -0
  47. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/py.typed +0 -0
  48. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/python_version.py +0 -0
  49. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/report_windows.py +0 -0
  50. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/table_view.py +0 -0
  51. {backlogops_gui-0.3 → backlogops_gui-0.4}/src/backlogops_gui/tcltk_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops-gui
3
- Version: 0.3
3
+ Version: 0.4
4
4
  Summary: Graphical user interface for backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Development Status :: 4 - Beta
18
18
  Classifier: Intended Audience :: End Users/Desktop
19
19
  Classifier: Intended Audience :: Information Technology
20
20
  Classifier: Topic :: Office/Business :: Scheduling
@@ -22,7 +22,7 @@ Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.12
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE.txt
25
- Requires-Dist: backlogops>=0.3
25
+ Requires-Dist: backlogops>=0.4
26
26
  Requires-Dist: argcomplete>=3.7.0
27
27
  Requires-Dist: versionreporter>=0.4
28
28
  Dynamic: author
@@ -46,6 +46,18 @@ There are 3 related packages for backlog operations:
46
46
  interface to use the functions in the library. It is based on TkInter. The
47
47
  ambition is to keep it as a thin wrapper around the library.
48
48
 
49
+ ## Project status
50
+
51
+ backlogops is in beta. The algorithms intended for its initial scope are in
52
+ place, so the main work from here is on the user experience rather than on new
53
+ core functionality. The three packages share a version number and are released
54
+ together.
55
+
56
+ The library is not meant to ever be "finished": using it for real planning keeps
57
+ surfacing new ideas. Some of those will be added to backlogops itself, and
58
+ others will fit better in separate packages built on top of backlogops. Early
59
+ adopters are very welcome, and their feedback shapes what comes next.
60
+
49
61
  ## Available functionality
50
62
 
51
63
  The following functionality is available in all 3 packages:
@@ -285,6 +297,10 @@ is in the menus.
285
297
  - Write configuration...: This lets you write the configuration you
286
298
  have in application to a file.
287
299
 
300
+ - Encrypt Jira API token file...: Encrypt a Jira API token to a
301
+ pass-phrase-protected file, for use with the ENCRYPTED_FILE token
302
+ storage mode.
303
+
288
304
  - Help
289
305
 
290
306
  - Report version information: Show version information for the
@@ -337,9 +353,9 @@ one with the list of releases. You will want to use the menus.
337
353
 
338
354
  ## Test summary
339
355
 
340
- - Test result: 2284 passed, 7 deselected in 44s
356
+ - Test result: 2638 passed, 7 deselected in 51s
341
357
  - No flake8 warnings.
342
358
  - No mypy errors found.
343
359
  - No python layout warnings.
344
- - Built version(s): 0.3
360
+ - Built version(s): 0.4
345
361
  - Build and test using Python 3.14.6
@@ -14,6 +14,18 @@ There are 3 related packages for backlog operations:
14
14
  interface to use the functions in the library. It is based on TkInter. The
15
15
  ambition is to keep it as a thin wrapper around the library.
16
16
 
17
+ ## Project status
18
+
19
+ backlogops is in beta. The algorithms intended for its initial scope are in
20
+ place, so the main work from here is on the user experience rather than on new
21
+ core functionality. The three packages share a version number and are released
22
+ together.
23
+
24
+ The library is not meant to ever be "finished": using it for real planning keeps
25
+ surfacing new ideas. Some of those will be added to backlogops itself, and
26
+ others will fit better in separate packages built on top of backlogops. Early
27
+ adopters are very welcome, and their feedback shapes what comes next.
28
+
17
29
  ## Available functionality
18
30
 
19
31
  The following functionality is available in all 3 packages:
@@ -253,6 +265,10 @@ is in the menus.
253
265
  - Write configuration...: This lets you write the configuration you
254
266
  have in application to a file.
255
267
 
268
+ - Encrypt Jira API token file...: Encrypt a Jira API token to a
269
+ pass-phrase-protected file, for use with the ENCRYPTED_FILE token
270
+ storage mode.
271
+
256
272
  - Help
257
273
 
258
274
  - Report version information: Show version information for the
@@ -305,9 +321,9 @@ one with the list of releases. You will want to use the menus.
305
321
 
306
322
  ## Test summary
307
323
 
308
- - Test result: 2284 passed, 7 deselected in 44s
324
+ - Test result: 2638 passed, 7 deselected in 51s
309
325
  - No flake8 warnings.
310
326
  - No mypy errors found.
311
327
  - No python layout warnings.
312
- - Built version(s): 0.3
328
+ - Built version(s): 0.4
313
329
  - Build and test using Python 3.14.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops-gui
3
- Version: 0.3
3
+ Version: 0.4
4
4
  Summary: Graphical user interface for backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Development Status :: 4 - Beta
18
18
  Classifier: Intended Audience :: End Users/Desktop
19
19
  Classifier: Intended Audience :: Information Technology
20
20
  Classifier: Topic :: Office/Business :: Scheduling
@@ -22,7 +22,7 @@ Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.12
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE.txt
25
- Requires-Dist: backlogops>=0.3
25
+ Requires-Dist: backlogops>=0.4
26
26
  Requires-Dist: argcomplete>=3.7.0
27
27
  Requires-Dist: versionreporter>=0.4
28
28
  Dynamic: author
@@ -46,6 +46,18 @@ There are 3 related packages for backlog operations:
46
46
  interface to use the functions in the library. It is based on TkInter. The
47
47
  ambition is to keep it as a thin wrapper around the library.
48
48
 
49
+ ## Project status
50
+
51
+ backlogops is in beta. The algorithms intended for its initial scope are in
52
+ place, so the main work from here is on the user experience rather than on new
53
+ core functionality. The three packages share a version number and are released
54
+ together.
55
+
56
+ The library is not meant to ever be "finished": using it for real planning keeps
57
+ surfacing new ideas. Some of those will be added to backlogops itself, and
58
+ others will fit better in separate packages built on top of backlogops. Early
59
+ adopters are very welcome, and their feedback shapes what comes next.
60
+
49
61
  ## Available functionality
50
62
 
51
63
  The following functionality is available in all 3 packages:
@@ -285,6 +297,10 @@ is in the menus.
285
297
  - Write configuration...: This lets you write the configuration you
286
298
  have in application to a file.
287
299
 
300
+ - Encrypt Jira API token file...: Encrypt a Jira API token to a
301
+ pass-phrase-protected file, for use with the ENCRYPTED_FILE token
302
+ storage mode.
303
+
288
304
  - Help
289
305
 
290
306
  - Report version information: Show version information for the
@@ -337,9 +353,9 @@ one with the list of releases. You will want to use the menus.
337
353
 
338
354
  ## Test summary
339
355
 
340
- - Test result: 2284 passed, 7 deselected in 44s
356
+ - Test result: 2638 passed, 7 deselected in 51s
341
357
  - No flake8 warnings.
342
358
  - No mypy errors found.
343
359
  - No python layout warnings.
344
- - Built version(s): 0.3
360
+ - Built version(s): 0.4
345
361
  - Build and test using Python 3.14.6
@@ -12,6 +12,7 @@ src/backlogops_gui/__init__.py
12
12
  src/backlogops_gui/__main__.py
13
13
  src/backlogops_gui/_migrate_warn.py
14
14
  src/backlogops_gui/application.py
15
+ src/backlogops_gui/auto_scroll.py
15
16
  src/backlogops_gui/backlog_actions.py
16
17
  src/backlogops_gui/backlog_dialogs.py
17
18
  src/backlogops_gui/backlog_io.py
@@ -39,5 +40,8 @@ src/backlogops_gui/python_version.py
39
40
  src/backlogops_gui/report_windows.py
40
41
  src/backlogops_gui/table_view.py
41
42
  src/backlogops_gui/tcltk_version.py
43
+ src/backlogops_gui/token_dialog.py
44
+ src/backlogops_gui/wizard_form.py
45
+ src/backlogops_gui/wizard_path.py
42
46
  src/backlogops_gui/wizard_table.py
43
47
  src/backlogops_gui/wizard_window.py
@@ -1,3 +1,3 @@
1
- backlogops>=0.3
1
+ backlogops>=0.4
2
2
  argcomplete>=3.7.0
3
3
  versionreporter>=0.4
@@ -4,7 +4,6 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "backlogops-gui"
7
- version = "0.3"
8
7
  authors = [
9
8
  { name="Tom Björkholm", email="klausuler_linnet0q@icloud.com" },
10
9
  ]
@@ -22,13 +21,13 @@ classifiers = [
22
21
  "Programming Language :: Python :: 3.13",
23
22
  "Programming Language :: Python :: 3.14",
24
23
  "Operating System :: OS Independent",
25
- "Development Status :: 3 - Alpha",
24
+ "Development Status :: 4 - Beta",
26
25
  "Intended Audience :: End Users/Desktop",
27
26
  "Intended Audience :: Information Technology",
28
27
  "Topic :: Office/Business :: Scheduling",
29
28
  "Typing :: Typed"
30
29
  ]
31
- dynamic = ["dependencies"]
30
+ dynamic = ["dependencies", "version"]
32
31
 
33
32
  [project.urls]
34
33
  "Homepage" = "https://github.com/tom-bjorkholm/backlog-ops"
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='backlogops-gui',
8
- version='0.3',
8
+ version='0.4',
9
9
  description='Graphical user interface for backlog operations.',
10
10
  author='Tom Björkholm',
11
11
  author_email='klausuler_linnet0q@icloud.com',
@@ -19,7 +19,7 @@ setup(
19
19
  ]
20
20
  },
21
21
  install_requires=[
22
- 'backlogops >= 0.3',
22
+ 'backlogops >= 0.4',
23
23
  'argcomplete >= 3.7.0',
24
24
  'versionreporter >= 0.4'
25
25
  ]
@@ -6,8 +6,13 @@ or from Jira, loads or replaces the active configuration from a file, runs
6
6
  the teams configuration wizard, creates a stand-alone input or output
7
7
  preset file, migrates a stand-alone preset file to the current format,
8
8
  writes the running configuration to a file, and creates a demonstration
9
- backlog. Each backlog opens in its own
10
- window. On macOS the menu bar sits at the top of the display rather than in
9
+ backlog. The configuration wizard and the preset wizard first ask whether
10
+ to start empty or be pre-filled from an existing file, so the user can
11
+ edit an existing configuration instead of entering everything again. Each
12
+ backlog opens in its own window, whose information region records where the
13
+ data came from and when, marks the window when the backlog has been
14
+ modified, and offers a "Read again" button that re-reads the same source.
15
+ On macOS the menu bar sits at the top of the display rather than in
11
16
  the window, so the main window body shows a short description, the current
12
17
  configuration status, and a log of the most recent diagnostic messages, to
13
18
  make clear that the application is running. The teams configuration is
@@ -28,29 +33,35 @@ import argparse
28
33
  import threading
29
34
  import tkinter as tk
30
35
  from io import StringIO
36
+ from pathlib import Path
31
37
  from tkinter import messagebox, ttk
32
38
  from typing import Callable, Optional, TextIO, TypeVar
33
39
  import argcomplete
34
40
  from config_as_json import Config, migrate_cfg
35
41
  from config_as_json.file_extension import fix_file_extension
36
- from tableio_cfg_json import WizardUiBridge
37
42
  from backlogops import (
38
43
  AvailableTeams, BacklogOpsConfig, BacklogReleases, GuiDisplayConfig,
39
44
  InputFormatConfig, Levels, OutputFormatConfig, Status, get_demo_backlog,
40
- get_backlog_ops_config, backlog_ops_wizard, preset_wizard)
45
+ get_backlog_ops_config, backlog_ops_wizard, preset_wizard,
46
+ read_backlog_ops_config, read_io_preset, safe_write_config,
47
+ encrypt_token_file, encrypt_token_to_file)
41
48
  from backlogops_gui.backlog_io import read_backlog
42
- from backlogops_gui.backlog_window import BacklogWindow, JiraHandlers
49
+ from backlogops_gui.backlog_window import (
50
+ BacklogWindow, BacklogSource, JiraHandlers, current_time)
43
51
  from backlogops_gui.blog_version_reporter import BloGuiVersionReporter
44
52
  from backlogops_gui.gui_wizard import TkWizardBridge
45
53
  from backlogops_gui.choice_dialogs import (
46
- ConfigChoice, PresetKind, ask_no_config_choice, ask_preset_kind)
54
+ ConfigChoice, PresetKind, SourceChoice, ask_no_config_choice,
55
+ ask_preset_kind, ask_source_choice)
47
56
  from backlogops_gui.file_choosers import (
48
- choose_config_file, choose_existing_config, choose_input_file,
49
- choose_migrated_preset, choose_preset_to_migrate)
57
+ choose_config_file, choose_existing_config, choose_existing_preset,
58
+ choose_input_file, choose_migrated_preset, choose_preset_to_migrate)
50
59
  from backlogops_gui.format_dialogs import ask_read_options
60
+ from backlogops_gui.token_dialog import ask_encrypt_token, EncryptTokenRequest
51
61
  from backlogops_gui.jira_actions import JiraActions
52
62
  from backlogops_gui.log_buffer import LogBuffer
53
- from backlogops_gui._migrate_warn import GuiMigrateWarnHook
63
+ from backlogops_gui._migrate_warn import GuiMigrateWarnHook, \
64
+ GuiPresetMigrateWarnHook
54
65
  from backlogops_gui.python_version import check_python_version
55
66
  from backlogops_gui.tcltk_version import check_tcltk_version
56
67
 
@@ -75,6 +86,28 @@ PRESET_CLASSES: dict[PresetKind, type[Config]] = {
75
86
  PresetKind.INPUT: InputFormatConfig,
76
87
  PresetKind.OUTPUT: OutputFormatConfig}
77
88
  _WizardConfig = TypeVar('_WizardConfig', bound=Config)
89
+ _CONFIG_SOURCE_TITLE = 'Configuration wizard'
90
+ _CONFIG_SOURCE_TEXT = (
91
+ 'Start the configuration wizard from scratch, or base it on an '
92
+ 'existing configuration file so you edit its values instead of '
93
+ 'entering everything again?')
94
+ _PRESET_SOURCE_TITLE = 'IO preset'
95
+ _PRESET_SOURCE_TEXT = (
96
+ 'Start the preset wizard from scratch, or base it on an existing '
97
+ 'preset file so you edit its values instead of entering everything '
98
+ 'again?')
99
+
100
+
101
+ def _read_config_prefill(path: str, captured: TextIO) -> BacklogOpsConfig:
102
+ """Read an existing configuration to pre-fill the wizard."""
103
+ return read_backlog_ops_config(path, captured,
104
+ auto_ch_hook=GuiMigrateWarnHook())
105
+
106
+
107
+ def _read_preset_prefill(path: str, captured: TextIO
108
+ ) -> InputFormatConfig | OutputFormatConfig:
109
+ """Read an existing preset, auto-detecting direction, to pre-fill."""
110
+ return read_io_preset(path, GuiPresetMigrateWarnHook(), captured)
78
111
 
79
112
 
80
113
  def initial_config(config_arg: Optional[str], sink: Optional[TextIO] = None
@@ -229,18 +262,20 @@ class BacklogApp:
229
262
  self.config_source = path
230
263
  return True
231
264
 
232
- def _run_bridge_wizard(self,
233
- wizard: Callable[[WizardUiBridge], _WizardConfig],
234
- error_title: str) -> Optional[_WizardConfig]:
265
+ def _run_bridge_wizard(self, wizard: Callable[..., _WizardConfig],
266
+ error_title: str,
267
+ default: Optional[_WizardConfig] = None
268
+ ) -> Optional[_WizardConfig]:
235
269
  """Run a wizard over a fresh Tk bridge, returning its config or None.
236
270
 
237
- An abandoned wizard ends in ``EOFError`` and yields None; any other
271
+ The wizard is pre-filled from ``default`` when one is given. An
272
+ abandoned wizard ends in ``EOFError`` and yields None; any other
238
273
  wizard failure is reported under ``error_title`` and also yields
239
274
  None. The bridge window is always closed afterwards.
240
275
  """
241
276
  bridge = TkWizardBridge(self.root, self.log)
242
277
  try:
243
- return wizard(bridge)
278
+ return wizard(bridge, default=default)
244
279
  except EOFError:
245
280
  return None
246
281
  except IO_ERRORS as error:
@@ -249,9 +284,57 @@ class BacklogApp:
249
284
  finally:
250
285
  bridge.close()
251
286
 
252
- def run_wizard(self) -> Optional[BacklogOpsConfig]:
287
+ def run_wizard(self, default: Optional[BacklogOpsConfig] = None
288
+ ) -> Optional[BacklogOpsConfig]:
253
289
  """Run the config wizard and return its configuration, or None."""
254
- return self._run_bridge_wizard(backlog_ops_wizard, 'Wizard error')
290
+ return self._run_bridge_wizard(backlog_ops_wizard, 'Wizard error',
291
+ default)
292
+
293
+ def _prefill_default(self, title: str, text: str,
294
+ chooser: Callable[[tk.Misc], Optional[str]],
295
+ reader: Callable[[str, TextIO], _WizardConfig]
296
+ ) -> tuple[bool, Optional[_WizardConfig]]:
297
+ """Ask whether to base a wizard on a file and read it if so.
298
+
299
+ Returns a ``(proceed, default)`` pair. ``proceed`` is False when
300
+ the user cancels the source dialog or the file chooser, or the
301
+ chosen file cannot be read, and the caller then does nothing.
302
+ ``default`` is the configuration read from the chosen file, or
303
+ None to start the wizard empty.
304
+ """
305
+ choice = ask_source_choice(self.root, title, text)
306
+ if choice is SourceChoice.CANCEL:
307
+ return False, None
308
+ if choice is SourceChoice.SCRATCH:
309
+ return True, None
310
+ path = chooser(self.root)
311
+ if path is None:
312
+ return False, None
313
+ return self._read_prefill(path, reader)
314
+
315
+ def _read_prefill(self, path: str,
316
+ reader: Callable[[str, TextIO], _WizardConfig]
317
+ ) -> tuple[bool, Optional[_WizardConfig]]:
318
+ """Read a pre-fill file, reporting a failure and yielding no default.
319
+
320
+ The reader can raise the IO errors, or ``SystemExit`` from the
321
+ configuration factory when the file is missing or is not a preset.
322
+ """
323
+ captured = StringIO()
324
+ try:
325
+ return True, reader(path, captured)
326
+ except SystemExit:
327
+ return self._prefill_failed(captured, f'Could not read {path}.')
328
+ except IO_ERRORS as error:
329
+ return self._prefill_failed(captured, str(error))
330
+
331
+ def _prefill_failed(self, captured: StringIO,
332
+ fallback: str) -> tuple[bool, None]:
333
+ """Log captured diagnostics and report a pre-fill read failure."""
334
+ self.log.write(captured.getvalue())
335
+ self.show_error('Could not read file',
336
+ _config_failure(captured, fallback))
337
+ return False, None
255
338
 
256
339
  def _load_config_file(self) -> None:
257
340
  """Load a chosen configuration file and make it the active config.
@@ -276,8 +359,20 @@ class BacklogApp:
276
359
  f'Loaded configuration from {path}.')
277
360
 
278
361
  def run_config_wizard(self) -> None:
279
- """Run the wizard and make a new configuration active on success."""
280
- config = self.run_wizard()
362
+ """Ask the source, run the wizard, and activate a new config.
363
+
364
+ The wizard may start from scratch or be pre-filled from an
365
+ existing configuration file the user chooses. Its result becomes
366
+ the active configuration; writing it to a file stays with the
367
+ ``Write configuration…`` action.
368
+ """
369
+ proceed, default = self._prefill_default(_CONFIG_SOURCE_TITLE,
370
+ _CONFIG_SOURCE_TEXT,
371
+ choose_existing_config,
372
+ _read_config_prefill)
373
+ if not proceed:
374
+ return
375
+ config = self.run_wizard(default)
281
376
  if config is not None:
282
377
  self.config = config
283
378
  self.config_source = 'the wizard'
@@ -285,8 +380,20 @@ class BacklogApp:
285
380
  self.show_info('Wizard', 'The new configuration is now active.')
286
381
 
287
382
  def create_preset_file(self) -> None:
288
- """Run the IO preset wizard and write the preset to a chosen file."""
289
- config = self._run_bridge_wizard(preset_wizard, 'Preset wizard error')
383
+ """Ask the source, run the IO preset wizard, and write the preset.
384
+
385
+ The wizard may start from scratch or be pre-filled from an
386
+ existing preset file the user chooses; its direction is detected
387
+ from the file.
388
+ """
389
+ proceed, default = self._prefill_default(_PRESET_SOURCE_TITLE,
390
+ _PRESET_SOURCE_TEXT,
391
+ choose_existing_preset,
392
+ _read_preset_prefill)
393
+ if not proceed:
394
+ return
395
+ config = self._run_bridge_wizard(preset_wizard, 'Preset wizard error',
396
+ default)
290
397
  if config is not None:
291
398
  self._write_to_chosen(config, 'Could not write preset',
292
399
  'Wrote preset')
@@ -351,20 +458,74 @@ class BacklogApp:
351
458
  """Write a configuration to a user-chosen file and report the outcome.
352
459
 
353
460
  The chosen filename receives the ``.cfg`` extension when missing. A
354
- cancelled chooser writes nothing; a write failure is reported under
355
- ``fail_title`` and a success under ``ok_title``.
461
+ cancelled chooser writes nothing, and overwriting an existing file
462
+ is confirmed first. The write is crash-safe: the configuration is
463
+ put in a ``.in_progress`` sibling and only then moved onto the
464
+ output file, so a crash loses neither the old file nor the new one.
465
+ A write failure is reported under ``fail_title`` and a success
466
+ under ``ok_title``.
356
467
  """
357
468
  path = choose_config_file(self.root)
358
469
  if path is None:
359
470
  return
360
471
  path = fix_file_extension(path, CONFIG_EXTENSION)
472
+ if not self._confirm_overwrite(path):
473
+ return
361
474
  try:
362
- config.write(to_json_filename=path, stderr_file=self.log)
475
+ safe_write_config(config, path, self.log)
363
476
  except IO_ERRORS as error:
364
477
  self.show_error(fail_title, str(error))
365
478
  return
366
479
  self.show_info(ok_title, f'Wrote {path}')
367
480
 
481
+ def _confirm_overwrite(self, path: str) -> bool:
482
+ """Confirm overwriting an existing file; a new file needs no asking."""
483
+ if not Path(path).exists():
484
+ return True
485
+ return self._ok_to_overwrite(Path(path))
486
+
487
+ def _ok_to_overwrite(self, path: Path) -> bool:
488
+ """Ask whether to overwrite a file, as the token writer requires."""
489
+ return messagebox.askyesno('Overwrite file?',
490
+ f'{path} already exists. Overwrite it?',
491
+ parent=self.root)
492
+
493
+ def encrypt_token(self) -> None:
494
+ """Encrypt a Jira API token to a file chosen in a dialog.
495
+
496
+ The dialog gathers a typed token or a clear text token file, the
497
+ encrypted file to write, and the pass phrase entered twice. An
498
+ existing output file is only overwritten after confirmation, and
499
+ any failure is reported.
500
+ """
501
+ request = ask_encrypt_token(self.root)
502
+ if request is None:
503
+ return
504
+ if self._run_encrypt(request):
505
+ self.show_info('Token encrypted', f'Wrote {request.out_file}')
506
+
507
+ def _run_encrypt(self, request: EncryptTokenRequest) -> bool:
508
+ """Encrypt the requested token, reporting any failure."""
509
+ try:
510
+ self._do_encrypt(request)
511
+ except IO_ERRORS as error:
512
+ self.show_error('Could not encrypt token', str(error))
513
+ return False
514
+ return True
515
+
516
+ def _do_encrypt(self, request: EncryptTokenRequest) -> None:
517
+ """Dispatch to the token or the file encryption library function."""
518
+ if request.token is not None:
519
+ encrypt_token_to_file(request.token, passphrase=request.passphrase,
520
+ filename=request.out_file,
521
+ ok_to_overwrite=self._ok_to_overwrite)
522
+ return
523
+ assert request.clear_file is not None
524
+ encrypt_token_file(clear_file=request.clear_file,
525
+ encrypted_file=request.out_file,
526
+ passphrase=request.passphrase,
527
+ ok_to_overwrite=self._ok_to_overwrite)
528
+
368
529
  def read_backlog_file(self) -> None:
369
530
  """Read a backlog from a chosen file into a new window."""
370
531
  path = choose_input_file(self.root)
@@ -375,20 +536,52 @@ class BacklogApp:
375
536
  sorted(presets) if presets else None)
376
537
  if options is None:
377
538
  return
539
+ value = options.config_value
378
540
  try:
379
- data = read_backlog(path, options.config_value, presets, self.log,
380
- self.levels(), self.status_map())
541
+ data = read_backlog(path, value, presets, self.log, self.levels(),
542
+ self.status_map())
381
543
  except IO_ERRORS as error:
382
544
  self.show_error('Could not read file', str(error))
383
545
  return
384
- self.open_backlog(data, path)
546
+ source = self._file_source(path, value, presets)
547
+ self.open_backlog(data, path, source=source,
548
+ reload=self._file_reload(path, value))
549
+
550
+ @staticmethod
551
+ def _file_source(path: str, value: Optional[str],
552
+ presets: Optional[dict[str, InputFormatConfig]]
553
+ ) -> BacklogSource:
554
+ """Describe a file backlog source, naming a preset when used."""
555
+ preset = value if value and presets and value in presets else None
556
+ return BacklogSource(kind='file', read_time=current_time(),
557
+ file_name=path, preset_name=preset)
558
+
559
+ def _file_reload(self, path: str, value: Optional[str]) -> Callable[
560
+ [Callable[[BacklogReleases, Optional[str]], None]], None]:
561
+ """Return a reload re-reading the same file with the current config."""
562
+ def reload(apply: Callable[[BacklogReleases, Optional[str]], None]
563
+ ) -> None:
564
+ """Re-read the file, reporting a failure and applying success."""
565
+ try:
566
+ data = read_backlog(path, value, self.in_presets(), self.log,
567
+ self.levels(), self.status_map())
568
+ except IO_ERRORS as error:
569
+ self.show_error('Could not read file', str(error))
570
+ return
571
+ apply(data, None)
572
+ return reload
385
573
 
386
574
  def new_demo_backlog(self) -> None:
387
575
  """Open a demonstration backlog in a new window."""
388
- self.open_backlog(get_demo_backlog(), 'Demo backlog')
576
+ source = BacklogSource(kind='demo', read_time=current_time())
577
+ self.open_backlog(get_demo_backlog(), 'Demo backlog', source=source)
389
578
 
390
579
  def open_backlog(self, data: BacklogReleases, title: str,
391
- warning: Optional[str] = None) -> None:
580
+ warning: Optional[str] = None, *,
581
+ source: Optional[BacklogSource] = None,
582
+ reload: Optional[Callable[
583
+ [Callable[[BacklogReleases, Optional[str]], None]],
584
+ None]] = None) -> None:
392
585
  """Open one backlog and its releases in a new window."""
393
586
  handlers = JiraHandlers(
394
587
  add_backlog=self.jira.writer.backlog_action(),
@@ -400,7 +593,8 @@ class BacklogApp:
400
593
  rename_releases=self.jira.renamer.rename_action())
401
594
  BacklogWindow(self.root, data, title, self.out_presets,
402
595
  self.available_teams, self.log, self.levels,
403
- self.gui_display, warning, handlers)
596
+ self.gui_display, warning, handlers, source=source,
597
+ reload=reload)
404
598
 
405
599
  def report_versions(self) -> None:
406
600
  """Report version information into the log on a worker thread.
@@ -461,6 +655,9 @@ class BacklogApp:
461
655
  command=self.migrate_preset_file)
462
656
  menu.add_command(label='Write configuration…',
463
657
  command=self.write_config)
658
+ menu.add_separator()
659
+ menu.add_command(label='Encrypt Jira API token file…',
660
+ command=self.encrypt_token)
464
661
  menubar.add_cascade(label='Configuration', menu=menu)
465
662
 
466
663
  def _add_help_menu(self, menubar: tk.Menu) -> None:
@@ -0,0 +1,37 @@
1
+ #! /usr/local/bin/python3
2
+ """A scroll command that shows a scrollbar only while it can scroll.
3
+
4
+ A table that fits its area needs no scrollbar, and a scrollbar that is
5
+ always shown wastes space and hints at hidden content that is not there.
6
+ :func:`auto_hide` returns the scroll command for a scrolling widget: the
7
+ command hides the scrollbar while the whole range is visible and shows it
8
+ again once the widget grows past its area. It works for any widget that
9
+ reports its position through an ``xscrollcommand`` or ``yscrollcommand``,
10
+ so the wizard table canvas and the read-only backlog tree can share it.
11
+ """
12
+
13
+ # Copyright (c) 2026, Tom Björkholm
14
+ # MIT License
15
+
16
+ from tkinter import ttk
17
+ from typing import Callable
18
+
19
+
20
+ def auto_hide(scrollbar: ttk.Scrollbar
21
+ ) -> Callable[[float | str, float | str], None]:
22
+ """Return a scroll command that hides the scrollbar when it is full.
23
+
24
+ The result is used as a widget's ``xscrollcommand`` or
25
+ ``yscrollcommand``. Tk reports the position as two fractions, which it
26
+ passes as strings, so the command accepts either a number or its
27
+ string form. The scrollbar must be laid out with the grid manager,
28
+ whose ``grid_remove`` remembers its cell across the hide.
29
+ """
30
+ def command(first: float | str, last: float | str) -> None:
31
+ """Hide or show the scrollbar, then move its thumb to the view."""
32
+ if float(first) <= 0.0 and float(last) >= 1.0:
33
+ scrollbar.grid_remove()
34
+ else:
35
+ scrollbar.grid()
36
+ scrollbar.set(first, last)
37
+ return command