config-cli-gui 0.3.4__tar.gz → 0.3.5__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 (86) hide show
  1. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/workflows/main.yml +1 -1
  2. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/HISTORY.md +19 -0
  3. {config_cli_gui-0.3.4/src/config_cli_gui.egg-info → config_cli_gui-0.3.5}/PKG-INFO +1 -1
  4. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/config.yaml +18 -24
  5. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/usage/config.md +20 -23
  6. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/_version.py +3 -3
  7. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/config.py +106 -0
  8. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/gui.py +65 -16
  9. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/logging.py +80 -6
  10. config_cli_gui-0.3.5/src/config_cli_gui/persistence.py +75 -0
  11. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5/src/config_cli_gui.egg-info}/PKG-INFO +1 -1
  12. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui.egg-info/SOURCES.txt +1 -1
  13. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/cli/cli_example.py +6 -1
  14. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/config/config_example.py +2 -81
  15. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/gui/gui_example.py +38 -6
  16. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/test_config_manager.py +1 -1
  17. config_cli_gui-0.3.4/tests/example_project/gui/config.yaml +0 -48
  18. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/FUNDING.yml +0 -0
  19. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/actions/setup-environment/action.yml +0 -0
  23. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/dependabot.yml +0 -0
  24. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/init.sh +0 -0
  25. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/release_message.sh +0 -0
  26. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/update_funding.py +0 -0
  27. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/workflows/release.yml +0 -0
  28. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.github/workflows/update_readme.yml +0 -0
  29. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.gitignore +0 -0
  30. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.idea/runConfigurations/config_generate.xml +0 -0
  31. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.idea/runConfigurations/example_project_cli.xml +0 -0
  32. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.idea/runConfigurations/example_project_gui.xml +0 -0
  33. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.pre-commit-config.yaml +0 -0
  34. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/.readthedocs.yaml +0 -0
  35. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/LICENSE +0 -0
  36. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/Makefile +0 -0
  37. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/README.md +0 -0
  38. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/.nav.yml +0 -0
  39. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/_static/img/favicon.png +0 -0
  40. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/_static/img/logo.png +0 -0
  41. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/_static/img/settings_dlg.png +0 -0
  42. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/_static/img/settings_doc.png +0 -0
  43. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/css/custom.css +0 -0
  44. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/develop/contributing.md +0 -0
  45. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/develop/make_windows.md +0 -0
  46. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/develop/naming_convention.md +0 -0
  47. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/funding/funding.md +0 -0
  48. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/getting-started/install.md +0 -0
  49. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/getting-started/virtual-environment.md +0 -0
  50. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/index.md +0 -0
  51. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/docs/usage/cli.md +0 -0
  52. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/mkdocs.yml +0 -0
  53. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/pyproject.toml +0 -0
  54. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/scripts/show_filelist.ps1 +0 -0
  55. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/scripts/show_tree.ps1 +0 -0
  56. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/scripts/show_tree.py +0 -0
  57. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/scripts/update_readme.py +0 -0
  58. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/setup.cfg +0 -0
  59. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/__init__.py +0 -0
  60. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/__init__.py +0 -0
  61. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/cli.py +0 -0
  62. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/configtypes/__init__.py +0 -0
  63. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/configtypes/color.py +0 -0
  64. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/configtypes/font.py +0 -0
  65. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/configtypes/vector.py +0 -0
  66. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui/docs.py +0 -0
  67. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui.egg-info/dependency_links.txt +0 -0
  68. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui.egg-info/entry_points.txt +0 -0
  69. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui.egg-info/requires.txt +0 -0
  70. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/src/config_cli_gui.egg-info/top_level.txt +0 -0
  71. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/template.yml.url +0 -0
  72. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/__init__.py +0 -0
  73. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/__init__.py +0 -0
  74. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/__main__.py +0 -0
  75. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/cli/__init__.py +0 -0
  76. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/cli/__main__.py +0 -0
  77. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/config/__init__.py +0 -0
  78. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/core/__init__.py +0 -0
  79. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/core/base.py +0 -0
  80. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/example.gpx +0 -0
  81. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/gui/__init__.py +0 -0
  82. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/example_project/gui/__main__.py +0 -0
  83. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/test_cli.py +0 -0
  84. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/test_docs.py +0 -0
  85. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/tests/test_generic_cli.py +0 -0
  86. {config_cli_gui-0.3.4 → config_cli_gui-0.3.5}/uv.lock +0 -0
@@ -63,7 +63,7 @@ jobs:
63
63
  fail-fast: false
64
64
  matrix:
65
65
  python-version: [ 3.11 ]
66
- os: [ ubuntu-latest, macos-latest ]
66
+ os: [ ubuntu-latest ]
67
67
  runs-on: ${{ matrix.os }}
68
68
  steps:
69
69
  - uses: actions/checkout@v4
@@ -4,6 +4,25 @@ Changelog
4
4
 
5
5
  (unreleased)
6
6
  ------------
7
+ - Improve gui. [Paul Magister]
8
+ - Fix gui log issue: even better fix. [Paul Magister]
9
+ - Fix gui log issue: [Paul Magister]
10
+
11
+ Previously the logger handler attempted to call the passed object like a function; GuiLogWriter provides write(), not a call. Because emit() swallowed exceptions, you saw nothing in the GUI log.
12
+ Now connect_gui_logging detects objects with .write() and registers the writer.write callable with the UI handler. GuiLogHandler.emit calls that callable, which writes to the text widget and the text appears.
13
+ - Improve config consistency: correctly consider logging parameters.
14
+ [Paul Magister]
15
+ - Improve config usage consistency: allways load last config. Move
16
+ AppConfig into config.py since it is used in the core lib component.
17
+ [Paul Magister]
18
+ - Save last opened config to start there on the next session, move theme
19
+ to app category. [Paul Magister]
20
+ - Do not run tests on macos. [Paul Magister]
21
+
22
+
23
+ 0.3.4 (2026-06-18)
24
+ ------------------
25
+ - Docs: Update HISTORY.md for release 0.3.4. [Paul Magister]
7
26
  - Bugfix: init color display properly. [Paul Magister]
8
27
  - Use ttkbootstrap gui styles. [Paul Magister]
9
28
  - Improve GUI scaling and font sizes. [Paul Magister]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: config-cli-gui
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Feature-rich Python project template for config-cli-gui.
5
5
  Author: pamagister
6
6
  Requires-Python: <3.12,>=3.11
@@ -1,3 +1,16 @@
1
+ app:
2
+ # Date format to use | type=str
3
+ date_format: '%Y-%m-%d'
4
+ # Logging level for the application | type=str | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
5
+ log_level: INFO
6
+ # Maximum log file size in MB before rotation | type=int
7
+ log_file_max_size: 2
8
+ # Enable logging to file | type=bool | choices=[True, False]
9
+ enable_file_logging: true
10
+ # Enable logging to console | type=bool | choices=[True, False]
11
+ enable_console_logging: true
12
+ # GUI theme setting supported by ttkbootstrap | type=str | choices=['cosmo', 'flatly', 'litera', 'minty', 'lumen', 'sandstone', 'yeti', 'pulse', 'united', 'darkly', 'superhero', 'solar', 'cyborg', 'vapor', 'simplex']
13
+ theme: superhero
1
14
  cli:
2
15
  # Path to input (file or folder) | type=str | [CLI]
3
16
  input: ''
@@ -9,26 +22,7 @@ cli:
9
22
  extract_waypoints: true
10
23
  # Include elevation data in waypoints | type=bool | [CLI] | choices=[True, False]
11
24
  elevation: false
12
- app:
13
- # Date format to use | type=str
14
- date_format: '%Y-%m-%d'
15
- # Logging level for the application | type=str | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
16
- log_level: INFO
17
- # Maximum log file size in MB before rotation | type=int
18
- log_file_max_size: 10
19
- # Number of backup log files to keep | type=int
20
- log_backup_count: 5
21
- # Log message format style | type=str | choices=['simple', 'detailed', 'json']
22
- log_format: detailed
23
- # Maximum number of worker threads | type=int
24
- max_workers: 4
25
- # Enable logging to file | type=bool | choices=[True, False]
26
- enable_file_logging: true
27
- # Enable logging to console | type=bool | choices=[True, False]
28
- enable_console_logging: true
29
25
  gui:
30
- # GUI theme setting suppoerted by ttkbootstrap | type=str | choices=['cosmo', 'flatly', 'litera', 'minty', 'lumen', 'sandstone', 'yeti', 'pulse', 'united', 'darkly', 'superhero', 'solar', 'cyborg', 'vapor', 'simplex']
31
- theme: darkly
32
26
  # Default window width | type=int
33
27
  window_width: 800
34
28
  # Default window height | type=int
@@ -40,21 +34,21 @@ gui:
40
34
  # Maximum number of log lines to keep in GUI | type=int
41
35
  max_log_lines: 1000
42
36
  # Point in 2D space | type=Vector
43
- point2D: (7, 11)
37
+ point2D: (7.0, 11.0)
44
38
  # Point in 3D space | type=Vector
45
39
  point3D: (1.2, 3.4, 5.6)
46
40
  misc:
47
41
  # Example integer | type=int
48
42
  some_numeric: 42
49
43
  # Example vector 2D | type=Vector
50
- some_vector2d: (1, 2)
44
+ some_vector2d: (1.0, 2.0)
51
45
  # Example vector 3D | type=Vector
52
46
  some_vector3d: (1.1, 2.2, 3.3)
53
- # Path to the file to use | type=PosixPath
47
+ # Path to the file to use | type=str
54
48
  some_file: some_file.txt
55
49
  # Color setting for the application | type=Color
56
50
  some_color: '#ff0000'
57
51
  # Date setting for the application | type=datetime
58
- some_date: '2025-12-31T10:30:45'
52
+ some_date: '2025-12-31T10:30:00'
59
53
  # Font setting for the application | type=Font
60
- some_font: 'DejaVuSans.ttf, 12, #0000ff'
54
+ some_font: 'DejaVuSans.ttf, 12.0, #0000ff'
@@ -3,6 +3,17 @@
3
3
  These parameters are available to configure the behavior of your application.
4
4
  The parameters in the cli category can be accessed via the command line interface.
5
5
 
6
+ ## Category "app"
7
+
8
+ | Name | Type | Description | Default | Choices |
9
+ |------------------------|------|---------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
10
+ | date_format | str | Date format to use | '%Y-%m-%d' | - |
11
+ | log_level | str | Logging level for the application | 'INFO' | ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] |
12
+ | log_file_max_size | int | Maximum log file size in MB before rotation | 2 | - |
13
+ | enable_file_logging | bool | Enable logging to file | True | [True, False] |
14
+ | enable_console_logging | bool | Enable logging to console | True | [True, False] |
15
+ | theme | str | GUI theme setting supported by ttkbootstrap | 'darkly' | ['cosmo', 'flatly', 'litera', 'minty', 'lumen', 'sandstone', 'yeti', 'pulse', 'united', 'darkly', 'superhero', 'solar', 'cyborg', 'vapor', 'simplex'] |
16
+
6
17
  ## Category "cli"
7
18
 
8
19
  | Name | Type | Description | Default | Choices |
@@ -13,31 +24,17 @@ The parameters in the cli category can be accessed via the command line interfac
13
24
  | extract_waypoints | bool | Extract starting points of each track as waypoint | True | [True, False] |
14
25
  | elevation | bool | Include elevation data in waypoints | False | [True, False] |
15
26
 
16
- ## Category "app"
17
-
18
- | Name | Type | Description | Default | Choices |
19
- |------------------------|------|---------------------------------------------|------------|---------------------------------------------------|
20
- | date_format | str | Date format to use | '%Y-%m-%d' | - |
21
- | log_level | str | Logging level for the application | 'INFO' | ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] |
22
- | log_file_max_size | int | Maximum log file size in MB before rotation | 10 | - |
23
- | log_backup_count | int | Number of backup log files to keep | 5 | - |
24
- | log_format | str | Log message format style | 'detailed' | ['simple', 'detailed', 'json'] |
25
- | max_workers | int | Maximum number of worker threads | 4 | - |
26
- | enable_file_logging | bool | Enable logging to file | True | [True, False] |
27
- | enable_console_logging | bool | Enable logging to console | True | [True, False] |
28
-
29
27
  ## Category "gui"
30
28
 
31
- | Name | Type | Description | Default | Choices |
32
- |-------------------|--------|------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
33
- | theme | str | GUI theme setting suppoerted by ttkbootstrap | 'darkly' | ['cosmo', 'flatly', 'litera', 'minty', 'lumen', 'sandstone', 'yeti', 'pulse', 'united', 'darkly', 'superhero', 'solar', 'cyborg', 'vapor', 'simplex'] |
34
- | window_width | int | Default window width | 800 | - |
35
- | window_height | int | Default window height | 600 | - |
36
- | log_window_height | int | Height of the log window in pixels | 200 | - |
37
- | auto_scroll_log | bool | Automatically scroll to the newest log entries | True | [True, False] |
38
- | max_log_lines | int | Maximum number of log lines to keep in GUI | 1000 | - |
39
- | point2D | Vector | Point in 2D space | Vector(7, 11) | - |
40
- | point3D | Vector | Point in 3D space | Vector(1.2, 3.4, 5.6) | - |
29
+ | Name | Type | Description | Default | Choices |
30
+ |-------------------|--------|------------------------------------------------|-----------------------|---------------|
31
+ | window_width | int | Default window width | 800 | - |
32
+ | window_height | int | Default window height | 600 | - |
33
+ | log_window_height | int | Height of the log window in pixels | 200 | - |
34
+ | auto_scroll_log | bool | Automatically scroll to the newest log entries | True | [True, False] |
35
+ | max_log_lines | int | Maximum number of log lines to keep in GUI | 1000 | - |
36
+ | point2D | Vector | Point in 2D space | Vector(7, 11) | - |
37
+ | point3D | Vector | Point in 3D space | Vector(1.2, 3.4, 5.6) | - |
41
38
 
42
39
  ## Category "misc"
43
40
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.4'
22
- __version_tuple__ = version_tuple = (0, 3, 4)
21
+ __version__ = version = '0.3.5'
22
+ __version_tuple__ = version_tuple = (0, 3, 5)
23
23
 
24
- __commit_id__ = commit_id = 'gf109662f6'
24
+ __commit_id__ = commit_id = 'g089c3018c'
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import logging
2
3
  from abc import ABC, abstractmethod
3
4
  from collections.abc import Iterable
4
5
  from dataclasses import dataclass
@@ -9,6 +10,7 @@ from typing import Any
9
10
  import yaml
10
11
  from pydantic import BaseModel
11
12
 
13
+ from config_cli_gui import persistence
12
14
  from config_cli_gui.configtypes.color import Color
13
15
  from config_cli_gui.configtypes.font import Font
14
16
  from config_cli_gui.configtypes.vector import Vector
@@ -108,6 +110,74 @@ class ConfigSerializer:
108
110
  return value
109
111
 
110
112
 
113
+ class AppConfig(ConfigCategory):
114
+ """Application-specific configuration parameters (centralized).
115
+
116
+ This category was moved from example project to the core library so that
117
+ every project uses a consistent, non-editable application configuration
118
+ section. It contains general application settings such as logging and
119
+ theme preferences.
120
+ """
121
+
122
+ def get_category_name(self) -> str:
123
+ return "app"
124
+
125
+ date_format: ConfigParameter = ConfigParameter(
126
+ name="date_format",
127
+ value="%Y-%m-%d",
128
+ help="Date format to use",
129
+ )
130
+
131
+ log_level: ConfigParameter = ConfigParameter(
132
+ name="log_level",
133
+ value="INFO",
134
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
135
+ help="Logging level for the application",
136
+ )
137
+
138
+ log_file_max_size: ConfigParameter = ConfigParameter(
139
+ name="log_file_max_size",
140
+ value=2,
141
+ help="Maximum log file size in MB before rotation",
142
+ )
143
+
144
+ enable_file_logging: ConfigParameter = ConfigParameter(
145
+ name="enable_file_logging",
146
+ value=True,
147
+ help="Enable logging to file",
148
+ )
149
+
150
+ enable_console_logging: ConfigParameter = ConfigParameter(
151
+ name="enable_console_logging",
152
+ value=True,
153
+ help="Enable logging to console",
154
+ )
155
+
156
+ # ttkbootstrap.Style().theme_names()
157
+ theme: ConfigParameter = ConfigParameter(
158
+ name="theme",
159
+ value="darkly",
160
+ choices=[
161
+ "cosmo",
162
+ "flatly",
163
+ "litera",
164
+ "minty",
165
+ "lumen",
166
+ "sandstone",
167
+ "yeti",
168
+ "pulse",
169
+ "united",
170
+ "darkly",
171
+ "superhero",
172
+ "solar",
173
+ "cyborg",
174
+ "vapor",
175
+ "simplex",
176
+ ],
177
+ help="GUI theme setting supported by ttkbootstrap",
178
+ )
179
+
180
+
111
181
  class ConfigManager:
112
182
  """Manages loading, saving, and accessing configuration categories."""
113
183
 
@@ -119,7 +189,9 @@ class ConfigManager:
119
189
  ):
120
190
  self._categories: dict[str, ConfigCategory] = {}
121
191
  self._serializer = ConfigSerializer()
192
+ self.app: AppConfig = AppConfig()
122
193
 
194
+ categories = (self.app, *categories)
123
195
  for category in categories:
124
196
  if not isinstance(category, ConfigCategory):
125
197
  raise TypeError(f"Expected ConfigCategory instance, got {type(category)}")
@@ -167,6 +239,18 @@ class ConfigManager:
167
239
  data = yaml.safe_load(f) if path.suffix.lower() in [".yml", ".yaml"] else json.load(f)
168
240
 
169
241
  self._apply_config_data(data)
242
+ # Persist the information about the last used configuration file so the
243
+ # application can restore the same config on next start.
244
+ try:
245
+ persistence.write_last_used_config("config-cli-gui", str(path))
246
+ except Exception:
247
+ # Persistence should not break normal config loading.
248
+ pass
249
+ try:
250
+ logging.getLogger("config_cli_gui").info(f"Loaded configuration from: {path}")
251
+ except Exception:
252
+ # Non-fatal: logging may not be configured yet.
253
+ pass
170
254
 
171
255
  def _apply_config_data(self, data: dict[str, Any]) -> None:
172
256
  """Apply loaded data to the configuration parameters."""
@@ -205,6 +289,28 @@ class ConfigManager:
205
289
  if file_format == "yaml":
206
290
  self._append_comments_to_yaml(path)
207
291
 
292
+ # Record the saved config as the last-used config as well.
293
+ try:
294
+ persistence.write_last_used_config("config-cli-gui", str(path))
295
+ except Exception:
296
+ # Ignore persistence failures when saving
297
+ pass
298
+ try:
299
+ logging.getLogger("config_cli_gui").info(f"Saved configuration to: {path}")
300
+ except Exception:
301
+ # Non-fatal: logging may not be configured yet.
302
+ pass
303
+
304
+ def get_last_used_config(self) -> str | None:
305
+ """Return the last used config file path recorded by the persistence layer.
306
+
307
+ This is a convenience wrapper around persistence.read_last_used_config.
308
+ """
309
+ try:
310
+ return persistence.read_last_used_config("config-cli-gui")
311
+ except Exception:
312
+ return None
313
+
208
314
  def to_dict(self) -> dict[str, Any]:
209
315
  """Convert all configuration categories to a dictionary."""
210
316
  result: dict[str, dict[str, Any]] = {}
@@ -155,8 +155,12 @@ class CalendarDialog:
155
155
  button_frame = ttk.Frame(main_frame)
156
156
  button_frame.pack(fill=tk.X, pady=(10, 0))
157
157
 
158
- ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=(5, 0))
159
- ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
158
+ ttk.Button(button_frame, text="OK", command=self._on_ok, width=10).pack(
159
+ side=tk.RIGHT, padx=(5, 0)
160
+ )
161
+ ttk.Button(button_frame, text="Cancel", command=self._on_cancel, width=10).pack(
162
+ side=tk.RIGHT
163
+ )
160
164
 
161
165
  def _update_calendar(self):
162
166
  """Update day spinbox based on selected month/year."""
@@ -213,7 +217,7 @@ class GenericSettingsDialog:
213
217
  config_file="config.yaml",
214
218
  ):
215
219
  self.parent = parent
216
- self.config_manager = config_manager
220
+ self.config_manager: ConfigManager = config_manager
217
221
  self.config_file = config_file
218
222
  self.result = None
219
223
  self.widgets = {}
@@ -252,11 +256,15 @@ class GenericSettingsDialog:
252
256
  button_frame.pack(fill=tk.X, pady=(10, 0))
253
257
 
254
258
  # Apply: apply current settings (in-memory) but keep dialog open
255
- ttk.Button(button_frame, text="Apply", command=self._on_apply).pack(
259
+ ttk.Button(button_frame, text="Apply", command=self._on_apply, width=10).pack(
256
260
  side=tk.RIGHT, padx=(5, 0)
257
261
  )
258
- ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=(5, 0))
259
- ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
262
+ ttk.Button(button_frame, text="OK", command=self._on_ok, width=10).pack(
263
+ side=tk.RIGHT, padx=(5, 0)
264
+ )
265
+ ttk.Button(button_frame, text="Cancel", command=self._on_cancel, width=10).pack(
266
+ side=tk.RIGHT
267
+ )
260
268
 
261
269
  def _create_category_tab(self, category_name: str, category):
262
270
  """Create a tab for a configuration category."""
@@ -297,13 +305,13 @@ class GenericSettingsDialog:
297
305
  continue
298
306
  """
299
307
 
300
- # Create label
301
- label = ttk.Label(parent, text=f"{param.name}:")
302
- label.grid(row=row, column=0, sticky="w", padx=5, pady=2)
308
+ # Create label (fixed width for alignment)
309
+ label = ttk.Label(parent, text=f"{param.name}:", width=28, anchor="w")
310
+ label.grid(row=row, column=0, sticky="w", padx=5, pady=4)
303
311
 
304
312
  # Create appropriate widget based on parameter type
305
313
  widget = self._create_parameter_widget(parent, param)
306
- widget.grid(row=row, column=1, sticky="ew", padx=5, pady=2)
314
+ widget.grid(row=row, column=1, sticky="ew", padx=5, pady=4)
307
315
 
308
316
  # Add tooltip
309
317
  ToolTip(label, param.help)
@@ -407,10 +415,10 @@ class GenericSettingsDialog:
407
415
  if path:
408
416
  var.set(path)
409
417
 
410
- browse_btn = ttk.Button(frame, text="File", command=browse_file)
418
+ browse_btn = ttk.Button(frame, text="File", command=browse_file, width=10)
411
419
  browse_btn.pack(side=tk.RIGHT, padx=(5, 0))
412
420
 
413
- browse_btn = ttk.Button(frame, text="Directory", command=browse_dir)
421
+ browse_btn = ttk.Button(frame, text="Directory", command=browse_dir, width=10)
414
422
  browse_btn.pack(side=tk.RIGHT, padx=(5, 0))
415
423
 
416
424
  frame.var = var
@@ -436,7 +444,7 @@ class GenericSettingsDialog:
436
444
  var.set(color[1])
437
445
  color_display.config(bg=color[1])
438
446
 
439
- pick_btn = ttk.Button(frame, text="Pick", command=pick_color)
447
+ pick_btn = ttk.Button(frame, text="Pick", command=pick_color, width=10)
440
448
  pick_btn.pack(side=tk.LEFT, padx=(5, 0))
441
449
  color_display.config(bg=color_value.to_hex())
442
450
 
@@ -485,7 +493,7 @@ class GenericSettingsDialog:
485
493
  color_var.set(color[1])
486
494
  color_display.config(bg=color[1])
487
495
 
488
- pick_btn = ttk.Button(frame, text="Pick Color", command=pick_color)
496
+ pick_btn = ttk.Button(frame, text="Pick Color", command=pick_color, width=10)
489
497
  pick_btn.pack(side=tk.LEFT, padx=(5, 0))
490
498
 
491
499
  def on_color_change(*args):
@@ -531,7 +539,7 @@ class GenericSettingsDialog:
531
539
  img_label.image = photo
532
540
  img_label.pack()
533
541
 
534
- preview_btn = ttk.Button(frame, text="Preview", command=show_preview)
542
+ preview_btn = ttk.Button(frame, text="Preview", command=show_preview, width=10)
535
543
  preview_btn.pack(side=tk.LEFT, padx=(5, 0))
536
544
 
537
545
  # Store variables in the frame for later access
@@ -584,7 +592,7 @@ class GenericSettingsDialog:
584
592
  if dialog.result:
585
593
  var.set(dialog.result.strftime("%Y-%m-%d %H:%M"))
586
594
 
587
- cal_btn = ttk.Button(frame, text="Calendar", command=pick_datetime)
595
+ cal_btn = ttk.Button(frame, text="Calendar", command=pick_datetime, width=10)
588
596
  cal_btn.pack(side=tk.RIGHT, padx=(5, 0))
589
597
 
590
598
  frame.var = var
@@ -636,7 +644,17 @@ class GenericSettingsDialog:
636
644
  def _on_ok(self):
637
645
  """Handle OK button click."""
638
646
  try:
647
+ # Remember old theme to detect changes
648
+ try:
649
+ old_theme = self.config_manager.app.theme.value
650
+ except Exception:
651
+ old_theme = None
652
+
639
653
  self.__persist_settings()
654
+
655
+ # Apply GUI updates (theme change etc.) after persisting
656
+ self._apply_gui_updates(old_theme)
657
+
640
658
  self.dialog.destroy()
641
659
 
642
660
  except Exception as e:
@@ -698,6 +716,30 @@ class GenericSettingsDialog:
698
716
 
699
717
  self.result = "ok"
700
718
 
719
+ def _apply_gui_updates(self, old_theme=None):
720
+ """Apply GUI-level updates that should happen after settings are persisted.
721
+
722
+ Currently this changes the ttkbootstrap theme if it was modified. The
723
+ logic is centralized here so it can be called from both Apply and OK
724
+ handlers.
725
+ """
726
+ try:
727
+ new_theme = None
728
+ try:
729
+ new_theme = self.config_manager.app.theme.value
730
+ except Exception:
731
+ new_theme = None
732
+
733
+ if new_theme and new_theme != old_theme:
734
+ try:
735
+ ttkbootstrap.Style().theme_use(new_theme)
736
+ except Exception:
737
+ # Best-effort; don't crash the settings dialog if theme switch fails
738
+ pass
739
+ except Exception:
740
+ # Swallow any unexpected errors to keep UI responsive
741
+ pass
742
+
701
743
  def _on_cancel(self):
702
744
  """Handle Cancel button click."""
703
745
  self.result = "cancel"
@@ -710,7 +752,14 @@ class GenericSettingsDialog:
710
752
  applies the theme change live if the GUI theme parameter is present.
711
753
  """
712
754
  try:
755
+ # Remember current theme, persist settings and apply GUI updates
756
+ try:
757
+ old_theme = self.config_manager.app.theme.value
758
+ except Exception:
759
+ old_theme = None
760
+
713
761
  self.__persist_settings()
762
+ self._apply_gui_updates(old_theme)
714
763
 
715
764
  except Exception as e:
716
765
  messagebox.showerror("Error", f"Failed to save configuration: {e}")
@@ -45,13 +45,21 @@ class GuiLogHandler(logging.Handler):
45
45
 
46
46
 
47
47
  class LoggerManager:
48
- """Manages all logging configuration and handlers."""
48
+ """Manages all logging configuration and handlers.
49
+
50
+ Accepts explicit parameters for integration with the project's
51
+ configuration system. All file-size related checks are performed once
52
+ during initialization and clamped to the allowed range.
53
+ """
49
54
 
50
55
  def __init__(
51
56
  self,
52
57
  log_level: str = "INFO",
53
58
  log_dir: Path = Path("logs"),
54
59
  log_file_name: str = "config_cli_gui.log",
60
+ log_file_max_size: int | None = None,
61
+ enable_file_logging: bool = True,
62
+ enable_console_logging: bool = True,
55
63
  ):
56
64
  """
57
65
  Initialize the logger manager.
@@ -59,15 +67,38 @@ class LoggerManager:
59
67
  log_level: The initial log level (e.g., "DEBUG", "INFO").
60
68
  log_dir: The directory to store log files.
61
69
  log_file_name: The name of the log file.
70
+ log_file_max_size: Maximum log file size in MB (clamped to 0..100).
71
+ enable_file_logging: Whether to enable file logging.
72
+ enable_console_logging: Whether to enable console logging.
62
73
  """
63
74
  self.log_level = log_level.upper()
64
75
  self.log_dir = log_dir
65
76
  self.log_file_name = log_file_name
66
77
 
78
+ # Validate and clamp file size (MB). Must be >= 0 and <= 100.
79
+ if log_file_max_size is None:
80
+ # keep the previous hard-coded default of 10 MB when not provided
81
+ self.log_file_max_size_mb = 10
82
+ else:
83
+ try:
84
+ size_mb = int(log_file_max_size)
85
+ except Exception:
86
+ size_mb = 10
87
+ # clamp to allowed range
88
+ if size_mb < 0:
89
+ size_mb = 0
90
+ if size_mb > 100:
91
+ size_mb = 100
92
+ self.log_file_max_size_mb = size_mb
93
+
94
+ # Enable/disable handlers
95
+ self.enable_file_logging = bool(enable_file_logging)
96
+ self.enable_console_logging = bool(enable_console_logging)
97
+
67
98
  self.logger = logging.getLogger("config_cli_gui")
68
- self.gui_handler: GuiLogHandler | None = None
99
+ self.gui_handler: logging.Handler | None = None
69
100
  self.file_handler: logging.handlers.RotatingFileHandler | None = None
70
- self.console_handler: logging.StreamHandler | None = None
101
+ self.console_handler: logging.Handler | None = None
71
102
 
72
103
  self._setup_logging()
73
104
 
@@ -87,12 +118,20 @@ class LoggerManager:
87
118
 
88
119
  def _setup_file_handler(self, formatter: logging.Formatter) -> None:
89
120
  """Set up a rotating file handler."""
121
+ # Only create file handler when enabled
122
+ if not self.enable_file_logging:
123
+ self.file_handler = None
124
+ return
125
+
90
126
  self.log_dir.mkdir(exist_ok=True)
91
127
  log_file = self.log_dir / self.log_file_name
92
128
 
129
+ max_bytes = int(self.log_file_max_size_mb) * 1024 * 1024
130
+
131
+ # If max_bytes is 0, RotatingFileHandler will effectively never rotate.
93
132
  self.file_handler = logging.handlers.RotatingFileHandler(
94
133
  log_file,
95
- maxBytes=10 * 1024 * 1024, # 10MB
134
+ maxBytes=max_bytes,
96
135
  backupCount=5,
97
136
  encoding="utf-8",
98
137
  )
@@ -101,6 +140,10 @@ class LoggerManager:
101
140
 
102
141
  def _setup_console_handler(self, formatter: logging.Formatter) -> None:
103
142
  """Set up a console handler."""
143
+ if not self.enable_console_logging:
144
+ self.console_handler = None
145
+ return
146
+
104
147
  self.console_handler = logging.StreamHandler(sys.stdout)
105
148
  self.console_handler.setFormatter(formatter)
106
149
  self.logger.addHandler(self.console_handler)
@@ -158,17 +201,48 @@ class LoggerManager:
158
201
  _logger_manager: LoggerManager | None = None
159
202
 
160
203
 
161
- def initialize_logging(log_level: str = "INFO") -> LoggerManager:
204
+ def initialize_logging(
205
+ log_level: str = "INFO",
206
+ log_file_max_size: int | None = None,
207
+ enable_file_logging: bool | None = None,
208
+ enable_console_logging: bool | None = None,
209
+ log_dir: Path | None = None,
210
+ log_file_name: str | None = None,
211
+ ) -> LoggerManager | None:
162
212
  """
163
213
  Initialize the global logging system.
214
+
215
+ Accepts individual configuration values (no dependency on AppConfig).
164
216
  Args:
165
217
  log_level: The initial log level.
218
+ log_file_max_size: Maximum log file size in MB (clamped to 0..100).
219
+ enable_file_logging: Enable or disable file logging. When None, defaults to True.
220
+ enable_console_logging: Enable or disable console logging. When None, defaults to True.
221
+ log_dir: Directory for log files. When None, defaults to Path('logs').
222
+ log_file_name: Log filename. When None, defaults to 'config_cli_gui.log'.
166
223
  Returns:
167
224
  The initialized LoggerManager instance.
168
225
  """
169
226
  global _logger_manager
170
227
  if _logger_manager is None:
171
- _logger_manager = LoggerManager(log_level)
228
+ # Defaults: file and console logging enabled unless explicitly disabled
229
+ if enable_file_logging is None:
230
+ enable_file_logging = True
231
+ if enable_console_logging is None:
232
+ enable_console_logging = True
233
+ if log_dir is None:
234
+ log_dir = Path("logs")
235
+ if log_file_name is None:
236
+ log_file_name = "config_cli_gui.log"
237
+
238
+ _logger_manager = LoggerManager(
239
+ log_level=log_level,
240
+ log_dir=log_dir,
241
+ log_file_name=log_file_name,
242
+ log_file_max_size=log_file_max_size,
243
+ enable_file_logging=enable_file_logging,
244
+ enable_console_logging=enable_console_logging,
245
+ )
172
246
  return _logger_manager
173
247
 
174
248