backlogops-gui 0.1__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ #! /usr/local/bin/python3
2
+ """Graphical user interface for library with backlog operations."""
3
+
4
+ # Copyright (c) 2026, Tom Björkholm
5
+ # MIT License
@@ -0,0 +1,11 @@
1
+ #! /usr/local/bin/python3
2
+ """Start the backlog operations GUI package."""
3
+
4
+ # Copyright (c) 2026, Tom Björkholm
5
+ # MIT License
6
+
7
+ from backlogops_gui.application import main
8
+
9
+
10
+ if __name__ == '__main__': # pragma: no cover
11
+ main()
@@ -0,0 +1,313 @@
1
+ #! /usr/local/bin/python3
2
+ """Tkinter application for backlog operations.
3
+
4
+ The application opens a main window whose menu reads a backlog from a file,
5
+ runs the teams configuration wizard, writes the running configuration to a
6
+ file, and creates a demonstration backlog. Each backlog opens in its own
7
+ window. On macOS the menu bar sits at the top of the display rather than in
8
+ the window, so the main window body shows a short description, the current
9
+ configuration status, and a log of the most recent diagnostic messages, to
10
+ make clear that the application is running. The teams configuration is
11
+ taken from the file given with ``-c`` or from the configured locations;
12
+ when no configuration is found the wizard runs at startup, and cancelling
13
+ it ends the application.
14
+ """
15
+
16
+ # PYTHON_ARGCOMPLETE_OK
17
+ # Copyright (c) 2026, Tom Björkholm
18
+ # MIT License
19
+
20
+ import argparse
21
+ import tkinter as tk
22
+ from tkinter import messagebox, ttk
23
+ from typing import Optional, TextIO
24
+ import argcomplete
25
+ from config_as_json.file_extension import fix_file_extension
26
+ from backlogops import (
27
+ AvailableTeams, AvailableTeamsConfig, BacklogReleases, InputFormatConfig,
28
+ NoTextIO, OutputFormatConfig, get_demo_backlog, get_available_teams,
29
+ teams_config_wizard)
30
+ from backlogops_gui.backlog_io import read_backlog
31
+ from backlogops_gui.backlog_window import BacklogWindow
32
+ from backlogops_gui.gui_wizard import TkWizardBridge
33
+ from backlogops_gui.io_dialogs import (
34
+ ask_read_options, choose_config_file, choose_input_file)
35
+ from backlogops_gui.log_buffer import LogBuffer
36
+ from backlogops_gui.tcltk_version import check_tcltk_version
37
+
38
+ APP_TITLE = 'Backlog operations GUI'
39
+ CONFIG_EXTENSION = '.cfg'
40
+ WRAP_LENGTH = 520
41
+ LOG_REFRESH_MS = 800
42
+ HEADING_FONT = ('TkDefaultFont', 14, 'bold')
43
+ INSTRUCTIONS = (
44
+ 'Use the menus to read a backlog file, run the teams wizard, write the '
45
+ 'current configuration to a file, or create a demonstration backlog. '
46
+ 'Each backlog opens in its own window. On macOS the menu bar is at the '
47
+ 'top of the display.')
48
+ DESCRIPTION = 'Graphical user interface for backlog operations'
49
+ CONFIG_ERRORS = (FileNotFoundError, NotADirectoryError, RuntimeError,
50
+ ValueError, TypeError, KeyError, OSError)
51
+ IO_ERRORS = (ValueError, TypeError, KeyError, OSError)
52
+
53
+
54
+ def initial_config(config_arg: Optional[str], sink: Optional[TextIO] = None
55
+ ) -> tuple[Optional[AvailableTeamsConfig], Optional[str]]:
56
+ """Return the startup configuration and an optional error message.
57
+
58
+ The configuration is looked up as documented for
59
+ :func:`backlogops.get_available_teams`. A failure is mapped to a None
60
+ configuration and the error text, so the caller can decide whether to
61
+ show the error and whether to run the wizard.
62
+
63
+ Args:
64
+ config_arg: The file from ``-c``, or None to search the defaults.
65
+ sink: Stream for diagnostics, or None to discard them.
66
+
67
+ Returns:
68
+ The loaded configuration and None, or None and the error text.
69
+ """
70
+ out = NoTextIO() if sink is None else sink
71
+ try:
72
+ return get_available_teams(config_arg, out), None
73
+ except CONFIG_ERRORS as error:
74
+ return None, str(error)
75
+
76
+
77
+ class BacklogApp:
78
+ """The backlog operations application and its menu actions."""
79
+
80
+ def __init__(self, root: tk.Tk,
81
+ config: Optional[AvailableTeamsConfig] = None) -> None:
82
+ """Store the main window, configuration, and a log buffer."""
83
+ self.root = root
84
+ self.config = config
85
+ self.log = LogBuffer()
86
+ self.config_source: Optional[str] = None
87
+ self.log_view: Optional[tk.Text] = None
88
+ self._status: Optional[tk.StringVar] = None
89
+
90
+ def in_presets(self) -> Optional[dict[str, InputFormatConfig]]:
91
+ """Return the input presets of the current configuration."""
92
+ return self.config.input_configs if self.config else None
93
+
94
+ def out_presets(self) -> Optional[dict[str, OutputFormatConfig]]:
95
+ """Return the output presets of the current configuration."""
96
+ return self.config.output_configs if self.config else None
97
+
98
+ def available_teams(self) -> Optional[AvailableTeams]:
99
+ """Return the loaded teams configuration, or None when absent."""
100
+ return self.config
101
+
102
+ def show_error(self, title: str, message: str) -> None:
103
+ """Show an error message to the user."""
104
+ messagebox.showerror(title, message, parent=self.root)
105
+
106
+ def show_info(self, title: str, message: str) -> None:
107
+ """Show an informational message to the user."""
108
+ messagebox.showinfo(title, message, parent=self.root)
109
+
110
+ def start(self, config_arg: Optional[str]) -> bool:
111
+ """Load the startup configuration, running the wizard if needed.
112
+
113
+ A configuration named with ``-c`` that cannot be read is reported
114
+ before the wizard runs. When no configuration is loaded and the
115
+ wizard is cancelled, the application is not ready to run.
116
+
117
+ Args:
118
+ config_arg: The file from ``-c``, or None to search defaults.
119
+
120
+ Returns:
121
+ Whether the application is ready to enter its main loop.
122
+ """
123
+ config, error = initial_config(config_arg, self.log)
124
+ if config is None:
125
+ if config_arg is not None and error is not None:
126
+ self.show_error('Configuration error', error)
127
+ config = self.run_wizard()
128
+ if config is None:
129
+ return False
130
+ self.config_source = 'the wizard'
131
+ else:
132
+ self.config_source = (config_arg if config_arg is not None
133
+ else 'the default location')
134
+ self.config = config
135
+ return True
136
+
137
+ def run_wizard(self) -> Optional[AvailableTeamsConfig]:
138
+ """Run the teams wizard and return its configuration, or None."""
139
+ bridge = TkWizardBridge(self.root, self.log)
140
+ try:
141
+ return teams_config_wizard(bridge)
142
+ except EOFError:
143
+ return None
144
+ except IO_ERRORS as error:
145
+ self.show_error('Wizard error', str(error))
146
+ return None
147
+ finally:
148
+ bridge.close()
149
+
150
+ def run_teams_wizard(self) -> None:
151
+ """Run the wizard and make a new configuration active on success."""
152
+ config = self.run_wizard()
153
+ if config is not None:
154
+ self.config = config
155
+ self.config_source = 'the wizard'
156
+ self._update_status()
157
+ self.show_info('Wizard', 'The new configuration is now active.')
158
+
159
+ def write_config(self) -> None:
160
+ """Write the running configuration to a chosen file."""
161
+ if self.config is None:
162
+ self.show_error('No configuration',
163
+ 'There is no configuration to write.')
164
+ return
165
+ path = choose_config_file(self.root)
166
+ if path is None:
167
+ return
168
+ path = fix_file_extension(path, CONFIG_EXTENSION)
169
+ try:
170
+ self.config.write(to_json_filename=path, stderr_file=self.log)
171
+ except IO_ERRORS as error:
172
+ self.show_error('Could not write configuration', str(error))
173
+ return
174
+ self.show_info('Wrote configuration', f'Wrote {path}')
175
+
176
+ def read_backlog_file(self) -> None:
177
+ """Read a backlog from a chosen file into a new window."""
178
+ path = choose_input_file(self.root)
179
+ if path is None:
180
+ return
181
+ presets = self.in_presets()
182
+ options = ask_read_options(self.root,
183
+ sorted(presets) if presets else None)
184
+ if options is None:
185
+ return
186
+ try:
187
+ data = read_backlog(path, options.config_value, presets, self.log)
188
+ except IO_ERRORS as error:
189
+ self.show_error('Could not read file', str(error))
190
+ return
191
+ self.open_backlog(data, path)
192
+
193
+ def new_demo_backlog(self) -> None:
194
+ """Open a demonstration backlog in a new window."""
195
+ self.open_backlog(get_demo_backlog(), 'Demo backlog')
196
+
197
+ def open_backlog(self, data: BacklogReleases, title: str) -> None:
198
+ """Open one backlog and its releases in a new window."""
199
+ BacklogWindow(self.root, data, title, self.out_presets,
200
+ self.available_teams, self.log)
201
+
202
+ def build_menu(self) -> None:
203
+ """Build the menu bar of the main window."""
204
+ menubar = tk.Menu(self.root)
205
+ self._add_file_menu(menubar)
206
+ self._add_config_menu(menubar)
207
+ self.root.config(menu=menubar)
208
+
209
+ def _add_file_menu(self, menubar: tk.Menu) -> None:
210
+ """Add the file menu with the backlog and exit actions."""
211
+ menu = tk.Menu(menubar, tearoff=False)
212
+ menu.add_command(label='Read backlog…', command=self.read_backlog_file)
213
+ menu.add_command(label='New demo backlog',
214
+ command=self.new_demo_backlog)
215
+ menu.add_separator()
216
+ menu.add_command(label='Exit', command=self.root.destroy)
217
+ menubar.add_cascade(label='File', menu=menu)
218
+
219
+ def _add_config_menu(self, menubar: tk.Menu) -> None:
220
+ """Add the configuration menu with the wizard and write actions."""
221
+ menu = tk.Menu(menubar, tearoff=False)
222
+ menu.add_command(label='Run teams wizard…',
223
+ command=self.run_teams_wizard)
224
+ menu.add_command(label='Write configuration…',
225
+ command=self.write_config)
226
+ menubar.add_cascade(label='Configuration', menu=menu)
227
+
228
+ def build_body(self) -> None:
229
+ """Build the main window body and start the log refresh."""
230
+ tk.Label(self.root, text=APP_TITLE,
231
+ font=HEADING_FONT).pack(anchor='w', padx=12, pady=(12, 4))
232
+ tk.Label(self.root, text=INSTRUCTIONS, wraplength=WRAP_LENGTH,
233
+ justify='left').pack(anchor='w', padx=12)
234
+ warning = check_tcltk_version(self.root)
235
+ if warning is not None:
236
+ tk.Label(self.root, text=warning, fg='red', justify='left',
237
+ wraplength=WRAP_LENGTH).pack(anchor='w', padx=12, pady=4)
238
+ self._status = tk.StringVar(self.root, self._status_text())
239
+ tk.Label(self.root, textvariable=self._status).pack(anchor='w',
240
+ padx=12, pady=4)
241
+ self._build_log_view()
242
+ self._schedule_refresh()
243
+
244
+ def _build_log_view(self) -> None:
245
+ """Build the read-only log view in the main window."""
246
+ frame = tk.LabelFrame(self.root, text='Log (most recent messages)')
247
+ frame.pack(fill='both', expand=True, padx=12, pady=8)
248
+ text = tk.Text(frame, height=8, wrap='word', state='disabled')
249
+ scroll = ttk.Scrollbar(frame, orient='vertical', command=text.yview)
250
+ text.configure(yscrollcommand=scroll.set)
251
+ scroll.pack(side='right', fill='y')
252
+ text.pack(side='left', fill='both', expand=True)
253
+ self.log_view = text
254
+
255
+ def _status_text(self) -> str:
256
+ """Return the configuration status line for the main window."""
257
+ if self.config is None:
258
+ return 'No configuration loaded.'
259
+ return f'Configuration loaded from {self.config_source}.'
260
+
261
+ def _update_status(self) -> None:
262
+ """Refresh the configuration status line, when it is shown."""
263
+ if self._status is not None:
264
+ self._status.set(self._status_text())
265
+
266
+ def _refresh_log(self) -> None:
267
+ """Copy the latest log lines into the read-only log view."""
268
+ if self.log_view is None:
269
+ return
270
+ self.log_view.configure(state='normal')
271
+ self.log_view.delete('1.0', 'end')
272
+ self.log_view.insert('end', self.log.text())
273
+ self.log_view.see('end')
274
+ self.log_view.configure(state='disabled')
275
+
276
+ def _schedule_refresh(self) -> None:
277
+ """Refresh the log view and schedule the next refresh."""
278
+ try:
279
+ self._refresh_log()
280
+ self.root.after(LOG_REFRESH_MS, self._schedule_refresh)
281
+ except tk.TclError:
282
+ pass
283
+
284
+
285
+ def _build_parser() -> argparse.ArgumentParser:
286
+ """Build the command line parser for the GUI launcher."""
287
+ parser = argparse.ArgumentParser(description=DESCRIPTION)
288
+ parser.add_argument('-c', '--config', dest='config',
289
+ help='Teams configuration file to start with. '
290
+ 'Without -c the file is found from $BACKLOGOPS_CFG, '
291
+ 'else backlogops.cfg in $BACKLOGOPS_DIR, else '
292
+ '$HOME/.backlogops.cfg.')
293
+ return parser
294
+
295
+
296
+ def main(args: Optional[list[str]] = None) -> None:
297
+ """Start the backlog operations GUI.
298
+
299
+ Args:
300
+ args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
301
+ """
302
+ parser = _build_parser()
303
+ argcomplete.autocomplete(parser)
304
+ parsed = parser.parse_args(args)
305
+ root = tk.Tk()
306
+ app = BacklogApp(root)
307
+ if not app.start(parsed.config):
308
+ root.destroy()
309
+ return
310
+ root.title(APP_TITLE)
311
+ app.build_menu()
312
+ app.build_body()
313
+ root.mainloop()
@@ -0,0 +1,68 @@
1
+ #! /usr/local/bin/python3
2
+ """Read and write a backlog and releases with format options.
3
+
4
+ These helpers wrap the library read and write functions and resolve the
5
+ format the same way the command line does: an empty value infers the
6
+ format from the file name, a value of only letters and digits is a preset
7
+ name looked up in the presets from the teams configuration, and any other
8
+ value is the path of a stand-alone format configuration file. Diagnostics
9
+ go to the given sink, because a graphical application shows them in a log
10
+ view rather than on a console.
11
+ """
12
+
13
+ # Copyright (c) 2026, Tom Björkholm
14
+ # MIT License
15
+
16
+ from typing import Optional, TextIO
17
+ from backlogops import (
18
+ BacklogReleases, FormatRules, InputFormatConfig, OutputFormatConfig,
19
+ NoTextIO, read_backlog_releases, write_backlog_releases,
20
+ resolve_input_config, resolve_output_config)
21
+
22
+
23
+ def _sink(sink: Optional[TextIO]) -> TextIO:
24
+ """Return the given diagnostics sink, or a discarding one."""
25
+ return sink if sink is not None else NoTextIO()
26
+
27
+
28
+ def read_backlog(path: str, value: Optional[str],
29
+ presets: Optional[dict[str, InputFormatConfig]],
30
+ sink: Optional[TextIO] = None) -> BacklogReleases:
31
+ """Read and validate a backlog and releases from one file.
32
+
33
+ Args:
34
+ path: The data file to read.
35
+ value: The format selection, as documented for the module.
36
+ presets: Named input presets, or None when none are configured.
37
+ sink: Stream for diagnostics, or None to discard them.
38
+
39
+ Returns:
40
+ The validated backlog and releases read from the file.
41
+ """
42
+ out = _sink(sink)
43
+ config = resolve_input_config(value, data_file=path, presets=presets,
44
+ stderr_file=out)
45
+ data = read_backlog_releases(path, config, None, out)
46
+ data.check_consistency(out)
47
+ return data
48
+
49
+
50
+ # pylint: disable-next=too-many-arguments,too-many-positional-arguments
51
+ def write_backlog(data: BacklogReleases, path: str, value: Optional[str],
52
+ presets: Optional[dict[str, OutputFormatConfig]],
53
+ releases_first: bool, sink: Optional[TextIO] = None) -> None:
54
+ """Write a backlog and releases to one file.
55
+
56
+ Args:
57
+ data: The backlog and releases to write.
58
+ path: The data file to create.
59
+ value: The format selection, as documented for the module.
60
+ presets: Named output presets, or None when none are configured.
61
+ releases_first: Whether to write the releases before the backlog.
62
+ sink: Stream for diagnostics, or None to discard them.
63
+ """
64
+ out = _sink(sink)
65
+ config = resolve_output_config(value, data_file=path, presets=presets,
66
+ stderr_file=out)
67
+ rules = FormatRules(backlog_first=not releases_first)
68
+ write_backlog_releases(data, path, config, rules, stderr_file=out)