pygpt-net 2.6.20__py3-none-any.whl → 2.6.22__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.
Files changed (221) hide show
  1. pygpt_net/CHANGELOG.txt +13 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/controller/__init__.py +4 -8
  5. pygpt_net/controller/access/voice.py +2 -2
  6. pygpt_net/controller/agent/agent.py +130 -2
  7. pygpt_net/controller/agent/experts.py +93 -96
  8. pygpt_net/controller/agent/llama.py +2 -1
  9. pygpt_net/controller/assistant/assistant.py +18 -1
  10. pygpt_net/controller/assistant/batch.py +2 -3
  11. pygpt_net/controller/assistant/editor.py +2 -2
  12. pygpt_net/controller/assistant/files.py +2 -3
  13. pygpt_net/controller/assistant/store.py +2 -2
  14. pygpt_net/controller/attachment/attachment.py +17 -1
  15. pygpt_net/controller/audio/audio.py +2 -2
  16. pygpt_net/controller/camera/camera.py +15 -7
  17. pygpt_net/controller/chat/chat.py +2 -2
  18. pygpt_net/controller/chat/common.py +50 -33
  19. pygpt_net/controller/chat/image.py +67 -77
  20. pygpt_net/controller/chat/input.py +94 -166
  21. pygpt_net/controller/chat/output.py +83 -140
  22. pygpt_net/controller/chat/response.py +83 -102
  23. pygpt_net/controller/chat/text.py +116 -149
  24. pygpt_net/controller/ctx/common.py +2 -1
  25. pygpt_net/controller/ctx/ctx.py +87 -6
  26. pygpt_net/controller/files/files.py +13 -1
  27. pygpt_net/controller/idx/idx.py +26 -2
  28. pygpt_net/controller/idx/indexer.py +85 -76
  29. pygpt_net/controller/kernel/reply.py +53 -66
  30. pygpt_net/controller/kernel/stack.py +16 -16
  31. pygpt_net/controller/lang/lang.py +52 -34
  32. pygpt_net/controller/model/importer.py +3 -2
  33. pygpt_net/controller/model/model.py +62 -3
  34. pygpt_net/controller/notepad/notepad.py +86 -84
  35. pygpt_net/controller/plugins/settings.py +3 -4
  36. pygpt_net/controller/settings/editor.py +4 -4
  37. pygpt_net/controller/settings/profile.py +105 -124
  38. pygpt_net/controller/theme/menu.py +154 -57
  39. pygpt_net/controller/theme/nodes.py +51 -44
  40. pygpt_net/controller/theme/theme.py +33 -9
  41. pygpt_net/controller/tools/tools.py +2 -2
  42. pygpt_net/controller/ui/tabs.py +2 -3
  43. pygpt_net/controller/ui/ui.py +16 -2
  44. pygpt_net/core/agents/observer/evaluation.py +3 -3
  45. pygpt_net/core/agents/provider.py +25 -3
  46. pygpt_net/core/agents/runner.py +4 -1
  47. pygpt_net/core/agents/runners/llama_workflow.py +19 -7
  48. pygpt_net/core/agents/runners/loop.py +3 -1
  49. pygpt_net/core/agents/runners/openai_workflow.py +17 -3
  50. pygpt_net/core/agents/tools.py +4 -1
  51. pygpt_net/core/bridge/context.py +34 -37
  52. pygpt_net/core/ctx/container.py +13 -12
  53. pygpt_net/core/ctx/ctx.py +1 -1
  54. pygpt_net/core/ctx/output.py +7 -4
  55. pygpt_net/core/db/database.py +2 -2
  56. pygpt_net/core/debug/console/console.py +2 -2
  57. pygpt_net/core/debug/debug.py +12 -1
  58. pygpt_net/core/dispatcher/dispatcher.py +24 -1
  59. pygpt_net/core/events/app.py +7 -7
  60. pygpt_net/core/events/control.py +26 -26
  61. pygpt_net/core/events/event.py +6 -3
  62. pygpt_net/core/events/kernel.py +2 -2
  63. pygpt_net/core/events/render.py +13 -13
  64. pygpt_net/core/experts/experts.py +76 -82
  65. pygpt_net/core/experts/worker.py +12 -12
  66. pygpt_net/core/filesystem/actions.py +1 -2
  67. pygpt_net/core/models/models.py +5 -1
  68. pygpt_net/core/models/ollama.py +14 -5
  69. pygpt_net/core/render/plain/helpers.py +2 -5
  70. pygpt_net/core/render/plain/renderer.py +26 -30
  71. pygpt_net/core/render/web/body.py +1 -1
  72. pygpt_net/core/render/web/helpers.py +2 -2
  73. pygpt_net/core/render/web/renderer.py +4 -4
  74. pygpt_net/core/settings/settings.py +43 -13
  75. pygpt_net/core/tabs/tabs.py +20 -13
  76. pygpt_net/core/types/__init__.py +2 -1
  77. pygpt_net/core/types/agent.py +4 -4
  78. pygpt_net/core/types/base.py +19 -0
  79. pygpt_net/core/types/console.py +6 -6
  80. pygpt_net/core/types/mode.py +8 -8
  81. pygpt_net/core/types/multimodal.py +3 -3
  82. pygpt_net/core/types/openai.py +2 -1
  83. pygpt_net/data/config/config.json +5 -5
  84. pygpt_net/data/config/models.json +19 -3
  85. pygpt_net/data/config/settings.json +14 -14
  86. pygpt_net/data/locale/locale.de.ini +4 -1
  87. pygpt_net/data/locale/locale.en.ini +6 -3
  88. pygpt_net/data/locale/locale.es.ini +4 -1
  89. pygpt_net/data/locale/locale.fr.ini +4 -1
  90. pygpt_net/data/locale/locale.it.ini +4 -1
  91. pygpt_net/data/locale/locale.pl.ini +5 -4
  92. pygpt_net/data/locale/locale.uk.ini +4 -1
  93. pygpt_net/data/locale/locale.zh.ini +4 -1
  94. pygpt_net/item/ctx.py +256 -240
  95. pygpt_net/item/model.py +59 -116
  96. pygpt_net/item/preset.py +122 -105
  97. pygpt_net/plugin/twitter/plugin.py +2 -2
  98. pygpt_net/provider/agents/llama_index/workflow/planner.py +3 -3
  99. pygpt_net/provider/agents/openai/agent.py +4 -12
  100. pygpt_net/provider/agents/openai/agent_b2b.py +10 -15
  101. pygpt_net/provider/agents/openai/agent_planner.py +4 -4
  102. pygpt_net/provider/agents/openai/agent_with_experts.py +3 -7
  103. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -8
  104. pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -8
  105. pygpt_net/provider/agents/openai/bot_researcher.py +2 -18
  106. pygpt_net/provider/agents/openai/bots/__init__.py +0 -0
  107. pygpt_net/provider/agents/openai/bots/research_bot/__init__.py +0 -0
  108. pygpt_net/provider/agents/openai/bots/research_bot/agents/__init__.py +0 -0
  109. pygpt_net/provider/agents/openai/bots/research_bot/agents/planner_agent.py +1 -1
  110. pygpt_net/provider/agents/openai/bots/research_bot/agents/search_agent.py +1 -0
  111. pygpt_net/provider/agents/openai/bots/research_bot/agents/writer_agent.py +1 -1
  112. pygpt_net/provider/agents/openai/bots/research_bot/manager.py +1 -10
  113. pygpt_net/provider/agents/openai/evolve.py +5 -9
  114. pygpt_net/provider/agents/openai/supervisor.py +4 -8
  115. pygpt_net/provider/core/config/patch.py +10 -3
  116. pygpt_net/provider/core/ctx/db_sqlite/utils.py +43 -43
  117. pygpt_net/provider/core/model/patch.py +11 -1
  118. pygpt_net/provider/core/preset/json_file.py +47 -49
  119. pygpt_net/provider/gpt/agents/experts.py +2 -2
  120. pygpt_net/tools/audio_transcriber/ui/dialogs.py +44 -54
  121. pygpt_net/tools/code_interpreter/body.py +1 -2
  122. pygpt_net/tools/code_interpreter/tool.py +7 -4
  123. pygpt_net/tools/code_interpreter/ui/html.py +1 -3
  124. pygpt_net/tools/code_interpreter/ui/widgets.py +2 -3
  125. pygpt_net/tools/html_canvas/ui/widgets.py +1 -3
  126. pygpt_net/tools/image_viewer/ui/dialogs.py +40 -37
  127. pygpt_net/tools/indexer/ui/widgets.py +2 -4
  128. pygpt_net/tools/media_player/tool.py +2 -5
  129. pygpt_net/tools/media_player/ui/widgets.py +60 -36
  130. pygpt_net/tools/text_editor/ui/widgets.py +18 -19
  131. pygpt_net/tools/translator/ui/widgets.py +39 -35
  132. pygpt_net/ui/base/context_menu.py +9 -4
  133. pygpt_net/ui/dialog/db.py +1 -3
  134. pygpt_net/ui/dialog/models.py +1 -3
  135. pygpt_net/ui/dialog/models_importer.py +2 -4
  136. pygpt_net/ui/dialogs.py +34 -30
  137. pygpt_net/ui/layout/chat/attachments.py +72 -84
  138. pygpt_net/ui/layout/chat/attachments_ctx.py +40 -44
  139. pygpt_net/ui/layout/chat/attachments_uploaded.py +36 -39
  140. pygpt_net/ui/layout/chat/calendar.py +100 -70
  141. pygpt_net/ui/layout/chat/chat.py +23 -17
  142. pygpt_net/ui/layout/chat/input.py +95 -118
  143. pygpt_net/ui/layout/chat/output.py +100 -162
  144. pygpt_net/ui/layout/chat/painter.py +89 -61
  145. pygpt_net/ui/layout/ctx/ctx_list.py +43 -52
  146. pygpt_net/ui/layout/status.py +23 -14
  147. pygpt_net/ui/layout/toolbox/agent.py +27 -38
  148. pygpt_net/ui/layout/toolbox/agent_llama.py +42 -45
  149. pygpt_net/ui/layout/toolbox/assistants.py +42 -38
  150. pygpt_net/ui/layout/toolbox/computer_env.py +32 -23
  151. pygpt_net/ui/layout/toolbox/footer.py +13 -16
  152. pygpt_net/ui/layout/toolbox/image.py +18 -21
  153. pygpt_net/ui/layout/toolbox/indexes.py +46 -89
  154. pygpt_net/ui/layout/toolbox/mode.py +20 -7
  155. pygpt_net/ui/layout/toolbox/model.py +12 -10
  156. pygpt_net/ui/layout/toolbox/presets.py +68 -52
  157. pygpt_net/ui/layout/toolbox/prompt.py +31 -58
  158. pygpt_net/ui/layout/toolbox/toolbox.py +25 -21
  159. pygpt_net/ui/layout/toolbox/vision.py +20 -22
  160. pygpt_net/ui/main.py +2 -4
  161. pygpt_net/ui/menu/about.py +64 -84
  162. pygpt_net/ui/menu/audio.py +87 -63
  163. pygpt_net/ui/menu/config.py +121 -127
  164. pygpt_net/ui/menu/debug.py +69 -76
  165. pygpt_net/ui/menu/file.py +32 -35
  166. pygpt_net/ui/menu/menu.py +2 -3
  167. pygpt_net/ui/menu/plugins.py +69 -33
  168. pygpt_net/ui/menu/theme.py +45 -46
  169. pygpt_net/ui/menu/tools.py +56 -60
  170. pygpt_net/ui/menu/video.py +20 -25
  171. pygpt_net/ui/tray.py +1 -2
  172. pygpt_net/ui/widget/audio/bar.py +1 -3
  173. pygpt_net/ui/widget/audio/input_button.py +3 -4
  174. pygpt_net/ui/widget/calendar/select.py +1 -2
  175. pygpt_net/ui/widget/dialog/base.py +12 -9
  176. pygpt_net/ui/widget/dialog/editor_file.py +20 -23
  177. pygpt_net/ui/widget/dialog/find.py +25 -24
  178. pygpt_net/ui/widget/dialog/profile.py +57 -53
  179. pygpt_net/ui/widget/draw/painter.py +62 -93
  180. pygpt_net/ui/widget/element/button.py +42 -30
  181. pygpt_net/ui/widget/element/checkbox.py +23 -15
  182. pygpt_net/ui/widget/element/group.py +6 -5
  183. pygpt_net/ui/widget/element/labels.py +1 -2
  184. pygpt_net/ui/widget/filesystem/explorer.py +93 -102
  185. pygpt_net/ui/widget/image/display.py +1 -2
  186. pygpt_net/ui/widget/lists/assistant.py +1 -2
  187. pygpt_net/ui/widget/lists/attachment.py +1 -2
  188. pygpt_net/ui/widget/lists/attachment_ctx.py +1 -2
  189. pygpt_net/ui/widget/lists/context.py +2 -4
  190. pygpt_net/ui/widget/lists/index.py +1 -2
  191. pygpt_net/ui/widget/lists/model.py +1 -2
  192. pygpt_net/ui/widget/lists/model_editor.py +1 -2
  193. pygpt_net/ui/widget/lists/model_importer.py +1 -2
  194. pygpt_net/ui/widget/lists/preset.py +1 -2
  195. pygpt_net/ui/widget/lists/preset_plugins.py +1 -2
  196. pygpt_net/ui/widget/lists/profile.py +1 -2
  197. pygpt_net/ui/widget/lists/uploaded.py +1 -2
  198. pygpt_net/ui/widget/option/checkbox.py +2 -4
  199. pygpt_net/ui/widget/option/checkbox_list.py +1 -4
  200. pygpt_net/ui/widget/option/cmd.py +1 -4
  201. pygpt_net/ui/widget/option/dictionary.py +25 -28
  202. pygpt_net/ui/widget/option/input.py +1 -3
  203. pygpt_net/ui/widget/tabs/Input.py +16 -12
  204. pygpt_net/ui/widget/tabs/body.py +5 -3
  205. pygpt_net/ui/widget/tabs/layout.py +36 -25
  206. pygpt_net/ui/widget/tabs/output.py +96 -74
  207. pygpt_net/ui/widget/textarea/calendar_note.py +1 -2
  208. pygpt_net/ui/widget/textarea/editor.py +41 -73
  209. pygpt_net/ui/widget/textarea/find.py +11 -10
  210. pygpt_net/ui/widget/textarea/html.py +3 -6
  211. pygpt_net/ui/widget/textarea/input.py +63 -64
  212. pygpt_net/ui/widget/textarea/notepad.py +54 -38
  213. pygpt_net/ui/widget/textarea/output.py +65 -54
  214. pygpt_net/ui/widget/textarea/search_input.py +5 -4
  215. pygpt_net/ui/widget/textarea/web.py +2 -4
  216. pygpt_net/ui/widget/vision/camera.py +2 -31
  217. {pygpt_net-2.6.20.dist-info → pygpt_net-2.6.22.dist-info}/METADATA +25 -154
  218. {pygpt_net-2.6.20.dist-info → pygpt_net-2.6.22.dist-info}/RECORD +218 -217
  219. {pygpt_net-2.6.20.dist-info → pygpt_net-2.6.22.dist-info}/LICENSE +0 -0
  220. {pygpt_net-2.6.20.dist-info → pygpt_net-2.6.22.dist-info}/WHEEL +0 -0
  221. {pygpt_net-2.6.20.dist-info → pygpt_net-2.6.22.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.20 23:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -77,22 +77,19 @@ class Profile:
77
77
 
78
78
  if save_current:
79
79
  print("Saving all settings in current profile...")
80
- self.window.controller.settings.save_all(force=True) # save all current settings
81
- # self.window.controller.layout.save() # save layout state
80
+ self.window.controller.settings.save_all(force=True)
82
81
  self.window.core.config.profile.set_current(uuid)
83
82
 
84
- # switch to profile workdir
85
83
  path = self.window.core.config.profile.get_current_workdir()
86
84
  if path and os.path.exists(path):
87
85
  self.window.controller.settings.workdir.update(
88
86
  path,
89
87
  force=True,
90
88
  profile_name=profile['name']
91
- ) # self.after_update() is called in update worker on success
89
+ )
92
90
  else:
93
91
  self.after_update(profile['name'])
94
92
 
95
-
96
93
  def after_update(
97
94
  self,
98
95
  name: str,
@@ -113,13 +110,11 @@ class Profile:
113
110
  """Select current profile on list"""
114
111
  current = self.window.core.config.profile.get_current()
115
112
  profiles = self.get_profiles()
116
- idx = 0
117
- for uuid in profiles:
113
+ for idx, uuid in enumerate(profiles):
118
114
  if uuid == current:
119
115
  index = self.window.ui.models['profile.list'].index(idx, 0)
120
116
  self.window.ui.nodes['profile.list'].setCurrentIndex(index)
121
117
  break
122
- idx += 1
123
118
 
124
119
  def get_profiles(self) -> Dict[str, Dict[str, Any]]:
125
120
  """
@@ -131,15 +126,16 @@ class Profile:
131
126
 
132
127
  def new(self):
133
128
  """New profile dialog"""
134
- self.window.ui.nodes['dialog.profile.checkbox.switch'].setVisible(True)
135
- self.window.ui.dialog['profile.item'].checkboxes.setVisible(False)
136
- self.window.ui.dialog['profile.item'].id = 'profile'
137
- self.window.ui.dialog['profile.item'].uuid = None
138
- self.window.ui.dialog['profile.item'].mode = 'create'
139
- self.window.ui.dialog['profile.item'].path = ""
140
- self.window.ui.dialog['profile.item'].input.setText("")
141
- self.window.ui.dialog['profile.item'].prepare()
142
- self.window.ui.dialog['profile.item'].show()
129
+ ui = self.window.ui
130
+ ui.nodes['dialog.profile.checkbox.switch'].setVisible(True)
131
+ ui.dialog['profile.item'].checkboxes.setVisible(False)
132
+ ui.dialog['profile.item'].id = 'profile'
133
+ ui.dialog['profile.item'].uuid = None
134
+ ui.dialog['profile.item'].mode = 'create'
135
+ ui.dialog['profile.item'].path = ""
136
+ ui.dialog['profile.item'].input.setText("")
137
+ ui.dialog['profile.item'].prepare()
138
+ ui.dialog['profile.item'].show()
143
139
 
144
140
  def edit(self, uuid: str):
145
141
  """
@@ -147,19 +143,20 @@ class Profile:
147
143
 
148
144
  :param uuid: profile UUID
149
145
  """
150
- self.window.ui.nodes['dialog.profile.checkbox.switch'].setVisible(False)
146
+ ui = self.window.ui
147
+ ui.nodes['dialog.profile.checkbox.switch'].setVisible(False)
151
148
  profile = self.window.core.config.profile.get(uuid)
152
- self.window.ui.dialog['profile.item'].checkboxes.setVisible(False)
149
+ ui.dialog['profile.item'].checkboxes.setVisible(False)
153
150
  if profile is None:
154
- self.window.ui.dialogs.alert("Profile not found!")
151
+ ui.dialogs.alert("Profile not found!")
155
152
  return
156
- self.window.ui.dialog['profile.item'].id = 'profile'
157
- self.window.ui.dialog['profile.item'].uuid = uuid
158
- self.window.ui.dialog['profile.item'].mode = 'edit'
159
- self.window.ui.dialog['profile.item'].path = profile['workdir'].replace("%HOME%", str(Path.home()))
160
- self.window.ui.dialog['profile.item'].input.setText(profile['name'])
161
- self.window.ui.dialog['profile.item'].prepare()
162
- self.window.ui.dialog['profile.item'].show()
153
+ ui.dialog['profile.item'].id = 'profile'
154
+ ui.dialog['profile.item'].uuid = uuid
155
+ ui.dialog['profile.item'].mode = 'edit'
156
+ ui.dialog['profile.item'].path = profile['workdir'].replace("%HOME%", str(Path.home()))
157
+ ui.dialog['profile.item'].input.setText(profile['name'])
158
+ ui.dialog['profile.item'].prepare()
159
+ ui.dialog['profile.item'].show()
163
160
 
164
161
  def open(self, force: bool = False):
165
162
  """
@@ -207,84 +204,80 @@ class Profile:
207
204
  :param path: profile workdir path
208
205
  :param uuid: profile UUID (update and duplicate only)
209
206
  """
210
- current = self.window.core.config.profile.get_current()
211
- if name.strip() == "":
212
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.name.empty"))
207
+ ui = self.window.ui
208
+ fs = self.window.core.filesystem
209
+ cfg = self.window.core.config.profile
210
+ current = cfg.get_current()
211
+ name_stripped = name.strip()
212
+ path_stripped = path.strip()
213
+ if name_stripped == "":
214
+ ui.dialogs.alert(trans("dialog.profile.alert.name.empty"))
213
215
  return
214
- if path.strip() == "":
215
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.path.empty"))
216
+ if path_stripped == "":
217
+ ui.dialogs.alert(trans("dialog.profile.alert.path.empty"))
216
218
  return
217
- if not os.path.exists(path) or not os.path.isdir(path):
218
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.path.not_exists"))
219
+
220
+ p = Path(path_stripped)
221
+ if not p.is_dir():
222
+ ui.dialogs.alert(trans("dialog.profile.alert.path.not_exists"))
219
223
  return
220
224
 
221
- if not self.window.core.filesystem.is_directory_empty(path):
222
- if not self.window.core.filesystem.is_workdir_in_path(path):
223
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.duplicate.not_empty"))
225
+ if not fs.is_directory_empty(path_stripped):
226
+ if not fs.is_workdir_in_path(path_stripped):
227
+ ui.dialogs.alert(trans("dialog.profile.alert.duplicate.not_empty"))
224
228
  return
225
229
 
226
230
  if mode == 'create':
227
- # create new profile
228
- uuid = self.window.core.config.profile.add(name, path)
231
+ uuid = cfg.add(name_stripped, path_stripped)
229
232
  self.window.update_status(trans("dialog.profile.status.created"))
230
- if self.window.ui.nodes['dialog.profile.checkbox.switch'].isChecked():
233
+ if ui.nodes['dialog.profile.checkbox.switch'].isChecked():
231
234
  QTimer.singleShot(100, lambda: self.after_create(uuid))
232
235
  return
233
236
 
234
237
  elif mode == 'edit':
235
- # update profile
236
- profile = self.window.core.config.profile.get(uuid)
238
+ profile = cfg.get(uuid)
237
239
  old_path = profile['workdir'].replace("%HOME%", str(Path.home()))
238
- self.window.core.config.profile.update_profile(uuid, name, path)
240
+ cfg.update_profile(uuid, name_stripped, path_stripped)
239
241
  self.window.update_status(trans("dialog.profile.status.updated"))
240
-
241
- # if current profile and path was changed then reload:
242
242
  if uuid == current:
243
243
  self.window.ui.update_title()
244
- if old_path != path:
244
+ if old_path != path_stripped:
245
245
  self.switch(uuid, force=True)
246
246
 
247
247
  elif mode == 'duplicate':
248
- # duplicate profile (duplicate requires empty directory)
249
- if not self.window.core.filesystem.is_directory_empty(path):
250
- self.window.ui.dialogs.alert(trans("dialog.workdir.change.empty.alert"))
248
+ if not fs.is_directory_empty(path_stripped):
249
+ ui.dialogs.alert(trans("dialog.workdir.change.empty.alert"))
251
250
  return
252
251
 
253
252
  profiles = self.get_profiles()
254
253
  if uuid not in profiles:
255
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.src.empty"))
254
+ ui.dialogs.alert(trans("dialog.profile.alert.src.empty"))
256
255
  return
257
256
  profile = profiles[uuid]
258
257
 
259
- # check if not same path
260
- if profile['workdir'].replace("%HOME%", str(Path.home())) == path:
261
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.path.same"))
258
+ if profile['workdir'].replace("%HOME%", str(Path.home())) == path_stripped:
259
+ ui.dialogs.alert(trans("dialog.profile.alert.path.same"))
262
260
  return
263
261
 
264
- # check free space
265
262
  include_datadir = self.is_include_datadir()
266
263
  include_db = self.is_include_db()
267
264
  src_path = profile['workdir'].replace("%HOME%", str(Path.home()))
268
- space_required = self.window.core.filesystem.get_directory_size(src_path, human_readable=False)
265
+ space_required = fs.get_directory_size(src_path, human_readable=False)
269
266
  if not include_datadir:
270
- space_required -= self.window.core.filesystem.get_datadir_size(src_path, human_readable=False)
267
+ space_required -= fs.get_datadir_size(src_path, human_readable=False)
271
268
  if not include_db:
272
- space_required -= self.window.core.filesystem.get_db_size(src_path, human_readable=False)
273
- space_free = self.window.core.filesystem.get_free_disk_space(path, human_readable=False)
269
+ space_required -= fs.get_db_size(src_path, human_readable=False)
270
+ space_free = fs.get_free_disk_space(path_stripped, human_readable=False)
274
271
  if space_required > space_free:
275
- self.window.ui.dialogs.alert(trans("dialog.workdir.result.no_free_space").format(
276
- required=self.window.core.filesystem.sizeof_fmt(space_required),
277
- free=self.window.core.filesystem.sizeof_fmt(space_free),
272
+ ui.dialogs.alert(trans("dialog.workdir.result.no_free_space").format(
273
+ required=fs.sizeof_fmt(space_required),
274
+ free=fs.sizeof_fmt(space_free),
278
275
  ))
279
276
  return
280
277
 
281
- # make duplicate
282
- self.duplicate(uuid, name, path)
278
+ self.duplicate(uuid, name_stripped, path_stripped)
283
279
  self.window.update_status(trans("dialog.profile.status.duplicated"))
284
- # if self.window.ui.nodes['dialog.profile.checkbox.switch'].isChecked():
285
- # self.switch(uuid, force=True)
286
280
 
287
- # close dialog and update list
288
281
  self.window.ui.dialogs.close('profile.item')
289
282
  self.update_menu()
290
283
  self.update_list()
@@ -362,7 +355,6 @@ class Profile:
362
355
  if uuid in profiles:
363
356
  profile = profiles[uuid]
364
357
  name = profile['name']
365
- # remove profile
366
358
  if self.window.core.config.profile.remove(uuid):
367
359
  self.window.update_status(trans("dialog.profile.status.removed") + ": " + name)
368
360
  self.update_list()
@@ -397,18 +389,20 @@ class Profile:
397
389
  """
398
390
  uuid = self.get_id_by_idx(idx)
399
391
  profile = self.window.core.config.profile.get(uuid)
400
- self.window.ui.dialog['profile.item'].checkboxes.setVisible(True)
392
+ dialog = self.window.ui.dialog['profile.item']
393
+ dialog.checkboxes.setVisible(True)
401
394
  if profile is None:
402
395
  self.window.ui.dialogs.alert("Profile not found!")
403
396
  return
404
397
  self.window.ui.nodes['dialog.profile.checkbox.switch'].setVisible(True)
405
- self.window.ui.dialog['profile.item'].id = 'profile'
406
- self.window.ui.dialog['profile.item'].uuid = uuid
407
- self.window.ui.dialog['profile.item'].mode = 'duplicate'
408
- self.window.ui.dialog['profile.item'].path = ""
409
- self.window.ui.dialog['profile.item'].input.setText(profile['name'] + " - copy")
410
- self.window.ui.dialog['profile.item'].prepare()
411
- self.window.ui.dialog['profile.item'].show()
398
+
399
+ dialog.id = 'profile'
400
+ dialog.uuid = uuid
401
+ dialog.mode = 'duplicate'
402
+ dialog.path = ""
403
+ dialog.input.setText(profile['name'] + " - copy")
404
+ dialog.prepare()
405
+ dialog.show()
412
406
 
413
407
  def delete_all(self, uuid: str):
414
408
  """
@@ -514,61 +508,48 @@ class Profile:
514
508
  """Setup profile menu"""
515
509
  profiles = self.window.core.config.profile.get_all()
516
510
  current = self.window.core.config.profile.get_current()
517
- for uuid in profiles:
518
- if uuid not in self.window.ui.menu['config.profiles']:
519
- profile = profiles[uuid]
511
+ ui_menu = self.window.ui.menu
512
+ suffix = trans("profile.current.suffix")
513
+ for uuid, profile in profiles.items():
514
+ if uuid not in ui_menu['config.profiles']:
520
515
  name = profile['name']
521
- checked = False
522
- if uuid == current:
523
- name = name + " " + trans("profile.current.suffix")
524
- checked = True
525
- self.window.ui.menu['config.profiles'][uuid] = QAction(
526
- name,
527
- self.window,
528
- checkable=True,
529
- )
530
- self.window.ui.menu['config.profiles'][uuid].setChecked(checked)
531
- self.window.ui.menu['config.profiles'][uuid].triggered.connect(
532
- lambda checked=True, uuid=uuid: self.window.controller.settings.profile.switch(uuid))
533
- self.window.ui.menu['config.profile'].addAction(self.window.ui.menu['config.profiles'][uuid])
516
+ checked = uuid == current
517
+ text = f"{name} {suffix}" if checked else name
518
+ action = QAction(text, self.window, checkable=True)
519
+ action.setChecked(checked)
520
+ action.triggered.connect(lambda checked=False, u=uuid: self.window.controller.settings.profile.switch(u))
521
+ ui_menu['config.profiles'][uuid] = action
522
+ ui_menu['config.profile'].addAction(action)
534
523
 
535
524
  def update_menu(self):
536
525
  """Update menu"""
537
526
  profiles = self.window.core.config.profile.get_all()
538
527
  current = self.window.core.config.profile.get_current()
539
- for uuid in list(self.window.ui.menu['config.profiles'].keys()):
528
+ ui_menu = self.window.ui.menu
529
+ profile_actions = ui_menu['config.profiles']
530
+ suffix = trans("profile.current.suffix")
531
+
532
+ for uuid in list(profile_actions.keys()):
540
533
  if uuid in profiles:
541
534
  name = profiles[uuid]['name']
542
- checked = False
543
- if uuid == current:
544
- name = name + " " + trans("profile.current.suffix")
545
- checked = True
546
- self.window.ui.menu['config.profiles'][uuid].setText(name)
547
- self.window.ui.menu['config.profiles'][uuid].setChecked(checked)
548
-
549
- # add new profiles
550
- for uuid in list(profiles.keys()):
551
- if uuid not in self.window.ui.menu['config.profiles']:
552
- profile = profiles[uuid]
535
+ is_current = uuid == current
536
+ text = f"{name} {suffix}" if is_current else name
537
+ action = profile_actions[uuid]
538
+ action.setText(text)
539
+ action.setChecked(is_current)
540
+
541
+ for uuid, profile in profiles.items():
542
+ if uuid not in profile_actions:
553
543
  name = profile['name']
554
- checked = False
555
- if uuid == current:
556
- name = name + " " + trans("profile.current.suffix")
557
- checked = True
558
- self.window.ui.menu['config.profiles'][uuid] = QAction(
559
- name,
560
- self.window,
561
- checkable=True,
562
- )
563
- self.window.ui.menu['config.profiles'][uuid].setChecked(checked)
564
- self.window.ui.menu['config.profiles'][uuid].triggered.connect(
565
- lambda checked=True, uuid=uuid: self.window.controller.settings.profile.switch(uuid)
566
- )
567
- self.window.ui.menu['config.profile'].addAction(self.window.ui.menu['config.profiles'][uuid])
568
-
569
- # remove non-exist profiles
570
- for uuid in list(self.window.ui.menu['config.profiles'].keys()):
544
+ is_current = uuid == current
545
+ text = f"{name} {suffix}" if is_current else name
546
+ action = QAction(text, self.window, checkable=True)
547
+ action.setChecked(is_current)
548
+ action.triggered.connect(lambda checked=False, u=uuid: self.window.controller.settings.profile.switch(u))
549
+ profile_actions[uuid] = action
550
+ ui_menu['config.profile'].addAction(action)
551
+
552
+ for uuid in list(profile_actions.keys()):
571
553
  if uuid not in profiles:
572
- self.window.ui.menu['config.profile'].removeAction(self.window.ui.menu['config.profiles'][uuid])
573
- del self.window.ui.menu['config.profiles'][uuid]
574
-
554
+ ui_menu['config.profile'].removeAction(profile_actions[uuid])
555
+ del profile_actions[uuid]
@@ -6,10 +6,10 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.07.22 15:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtGui import QAction
12
+ from PySide6.QtGui import QAction, QActionGroup
13
13
 
14
14
 
15
15
  class Menu:
@@ -20,19 +20,65 @@ class Menu:
20
20
  :param window: Window instance
21
21
  """
22
22
  self.window = window
23
- self.density_values = [-2, -1, 0, 1, 2]
23
+ self.density_values = (-2, -1, 0, 1, 2)
24
24
  self.loaded = False
25
25
  self.syntax_loaded = False
26
26
  self.density_loaded = False
27
27
 
28
+ self._style_group = None
29
+ self._theme_group = None
30
+ self._density_group = None
31
+ self._syntax_group = None
32
+
33
+ def _on_style_triggered(self, action):
34
+ """
35
+ Handle style change triggered by menu action
36
+
37
+ :param action: QAction instance
38
+ """
39
+ self.window.controller.theme.toggle_style(action.data())
40
+
41
+ def _on_theme_triggered(self, action):
42
+ """
43
+ Handle theme change triggered by menu action
44
+
45
+ :param action: QAction instance
46
+ """
47
+ self.window.controller.theme.toggle_theme_by_menu(action.data())
48
+
49
+ def _on_syntax_triggered(self, action):
50
+ """
51
+ Handle syntax highlight change triggered by menu action
52
+
53
+ :param action: QAction instance
54
+ """
55
+ self.window.controller.theme.toggle_syntax(action.data(), update_menu=True)
56
+
57
+ def _on_density_triggered(self, action):
58
+ """
59
+ Handle layout density change triggered by menu action
60
+
61
+ :param action: QAction instance
62
+ """
63
+ self.window.controller.theme.toggle_option_by_menu('layout.density', action.data())
64
+
28
65
  def setup_list(self):
29
66
  """Setup menu list"""
30
- # setup themes list menu
31
67
  if self.loaded:
32
68
  return
33
69
 
34
- # styles
35
- styles = self.window.controller.theme.common.get_styles_list()
70
+ w = self.window
71
+ menu = w.ui.menu
72
+ common = w.controller.theme.common
73
+
74
+ if self._style_group is None:
75
+ self._style_group = QActionGroup(w)
76
+ self._style_group.setExclusive(True)
77
+ self._style_group.triggered.connect(self._on_style_triggered)
78
+
79
+ styles = common.get_styles_list()
80
+ menu_style_dict = menu['theme_style']
81
+ menu_style = menu['theme.style']
36
82
  for style in styles:
37
83
  style_id = style.lower()
38
84
  title = style.replace('_', ' ').title()
@@ -40,90 +86,141 @@ class Menu:
40
86
  title = "ChatGPT"
41
87
  elif title == "Chatgpt Wide":
42
88
  title = "ChatGPT (wide)"
43
- self.window.ui.menu['theme_style'][style_id] = QAction(title, self.window, checkable=True)
44
- self.window.ui.menu['theme_style'][style_id].triggered.connect(
45
- lambda checked=None, style=style_id: self.window.controller.theme.toggle_style(style))
46
- self.window.ui.menu['theme.style'].addAction(self.window.ui.menu['theme_style'][style_id])
47
-
48
- # color themes
49
- themes = self.window.controller.theme.common.get_themes_list()
50
- custom_themes = self.window.controller.theme.common.get_custom_themes_list()
51
- themes += custom_themes
89
+ act = QAction(title, w, checkable=True)
90
+ act.setData(style_id)
91
+ menu_style_dict[style_id] = act
92
+ self._style_group.addAction(act)
93
+ menu_style.addAction(act)
94
+
95
+ if self._theme_group is None:
96
+ self._theme_group = QActionGroup(w)
97
+ self._theme_group.setExclusive(True)
98
+ self._theme_group.triggered.connect(self._on_theme_triggered)
99
+
100
+ themes = common.get_themes_list()
101
+ themes += common.get_custom_themes_list()
52
102
  themes.sort()
53
- for theme in themes:
54
- name = self.window.controller.theme.common.translate(theme)
55
- self.window.ui.menu['theme'][theme] = QAction(name, self.window, checkable=True)
56
- self.window.ui.menu['theme'][theme].triggered.connect(
57
- lambda checked=None, theme=theme: self.window.controller.theme.toggle(theme))
58
103
 
59
- # append to dark or light menu
104
+ menu_theme_dict = menu['theme']
105
+ menu_dark = menu['theme.dark']
106
+ menu_light = menu['theme.light']
107
+ for theme in themes:
108
+ name = common.translate(theme)
109
+ act = QAction(name, w, checkable=True)
110
+ act.setData(theme)
111
+ menu_theme_dict[theme] = act
112
+ self._theme_group.addAction(act)
60
113
  if theme.startswith('dark'):
61
- self.window.ui.menu['theme.dark'].addAction(self.window.ui.menu['theme'][theme])
114
+ menu_dark.addAction(act)
62
115
  elif theme.startswith('light'):
63
- self.window.ui.menu['theme.light'].addAction(self.window.ui.menu['theme'][theme])
116
+ menu_light.addAction(act)
64
117
 
65
118
  self.loaded = True
66
119
 
67
120
  def setup_syntax(self):
68
121
  """Setup syntax menu"""
69
- styles = self.window.controller.chat.render.web_renderer.body.highlight.get_styles()
122
+ w = self.window
123
+ menu = w.ui.menu
124
+
125
+ styles = w.controller.chat.render.web_renderer.body.highlight.get_styles()
70
126
  styles.sort()
71
- # clear menu
72
- for style in self.window.ui.menu['theme_syntax']:
73
- self.window.ui.menu['theme.syntax'].removeAction(self.window.ui.menu['theme_syntax'][style])
74
- # setup syntax menu
127
+
128
+ if self.syntax_loaded:
129
+ existing = sorted(menu['theme_syntax'].keys())
130
+ if existing == styles:
131
+ return
132
+
133
+ menu_syntax_dict = menu['theme_syntax']
134
+ menu_syntax = menu['theme.syntax']
135
+
136
+ for act in menu_syntax_dict.values():
137
+ menu_syntax.removeAction(act)
138
+ act.deleteLater()
139
+ menu_syntax_dict.clear()
140
+
141
+ if self._syntax_group is None:
142
+ self._syntax_group = QActionGroup(w)
143
+ self._syntax_group.setExclusive(True)
144
+ self._syntax_group.triggered.connect(self._on_syntax_triggered)
145
+
75
146
  for style in styles:
76
- self.window.ui.menu['theme_syntax'][style] = QAction(style, self.window, checkable=True)
77
- self.window.ui.menu['theme_syntax'][style].triggered.connect(
78
- lambda checked=None, style=style: self.window.controller.theme.toggle_syntax(style, update_menu=True))
79
- self.window.ui.menu['theme.syntax'].addAction(self.window.ui.menu['theme_syntax'][style])
147
+ act = QAction(style, w, checkable=True)
148
+ act.setData(style)
149
+ menu_syntax_dict[style] = act
150
+ self._syntax_group.addAction(act)
151
+ menu_syntax.addAction(act)
152
+
153
+ self.syntax_loaded = True
80
154
 
81
155
  def setup_density(self):
82
156
  """Setup menu list"""
83
157
  if self.density_loaded:
84
158
  return
85
- # setup layout density menu
86
- current_density = self.window.core.config.get('layout.density')
159
+
160
+ w = self.window
161
+ menu = w.ui.menu
162
+
163
+ if self._density_group is None:
164
+ self._density_group = QActionGroup(w)
165
+ self._density_group.setExclusive(True)
166
+ self._density_group.triggered.connect(self._on_density_triggered)
167
+
168
+ current_density = w.core.config.get('layout.density')
169
+ menu_density_dict = menu['theme.layout.density']
170
+ menu_density = menu['theme.density']
171
+
87
172
  for value in self.density_values:
88
173
  name = str(value)
89
174
  if value > 0:
90
175
  name = '+' + name
91
- self.window.ui.menu['theme.layout.density'][value] = QAction(name, self.window, checkable=True)
92
- self.window.ui.menu['theme.layout.density'][value].triggered.connect(
93
- lambda checked=None, value=value: self.window.controller.theme.toggle_option('layout.density', value))
94
- self.window.ui.menu['theme.density'].addAction(self.window.ui.menu['theme.layout.density'][value])
176
+ act = QAction(name, w, checkable=True)
177
+ act.setData(value)
178
+ menu_density_dict[value] = act
179
+ self._density_group.addAction(act)
180
+ menu_density.addAction(act)
95
181
  if value == current_density:
96
- self.window.ui.menu['theme.layout.density'][value].setChecked(True)
182
+ act.setChecked(True)
183
+
97
184
  self.density_loaded = True
98
185
 
99
186
  def update_density(self):
100
187
  """Update layout density menu"""
101
188
  current_density = self.window.core.config.get('layout.density')
102
- for value in self.density_values:
103
- self.window.ui.menu['theme.layout.density'][value].setChecked(False)
104
- if value == current_density:
105
- self.window.ui.menu['theme.layout.density'][value].setChecked(True)
189
+ items = self.window.ui.menu['theme.layout.density']
190
+ act = items.get(current_density)
191
+ if act is not None:
192
+ act.setChecked(True)
193
+ else:
194
+ for a in items.values():
195
+ a.setChecked(False)
106
196
 
107
197
  def update_list(self):
108
198
  """Update theme list menu"""
109
- # styles
110
199
  current_style = self.window.core.config.get('theme.style')
111
- for style in self.window.ui.menu['theme_style']:
112
- self.window.ui.menu['theme_style'][style].setChecked(False)
113
- if current_style in self.window.ui.menu['theme_style']:
114
- self.window.ui.menu['theme_style'][current_style].setChecked(True)
200
+ style_items = self.window.ui.menu['theme_style']
201
+ act = style_items.get(current_style)
202
+ if act is not None:
203
+ act.setChecked(True)
204
+ else:
205
+ for a in style_items.values():
206
+ a.setChecked(False)
115
207
 
116
- # color themes
117
208
  current_theme = self.window.core.config.get('theme')
118
- for theme in self.window.ui.menu['theme']:
119
- self.window.ui.menu['theme'][theme].setChecked(False)
120
- if current_theme in self.window.ui.menu['theme']:
121
- self.window.ui.menu['theme'][current_theme].setChecked(True)
209
+ theme_items = self.window.ui.menu['theme']
210
+ act = theme_items.get(current_theme)
211
+ if act is not None:
212
+ act.setChecked(True)
213
+ else:
214
+ for a in theme_items.values():
215
+ a.setChecked(False)
122
216
 
123
217
  def update_syntax(self):
124
218
  """Update code syntax highlight menu"""
125
219
  current = self.window.core.config.get('render.code_syntax')
126
- for style in self.window.ui.menu['theme_syntax']:
127
- self.window.ui.menu['theme_syntax'][style].setChecked(False)
128
- if current in self.window.ui.menu['theme_syntax']:
129
- self.window.ui.menu['theme_syntax'][current].setChecked(True)
220
+ items = self.window.ui.menu['theme_syntax']
221
+ act = items.get(current)
222
+ if act is not None:
223
+ act.setChecked(True)
224
+ else:
225
+ for a in items.values():
226
+ a.setChecked(False)