pygpt-net 2.6.3__py3-none-any.whl → 2.6.6__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 (78) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +2 -2
  3. pygpt_net/app.py +6 -1
  4. pygpt_net/config.py +55 -65
  5. pygpt_net/controller/__init__.py +5 -2
  6. pygpt_net/controller/chat/chat.py +38 -35
  7. pygpt_net/controller/chat/render.py +144 -217
  8. pygpt_net/controller/chat/stream.py +52 -25
  9. pygpt_net/controller/config/config.py +39 -42
  10. pygpt_net/controller/config/field/checkbox.py +16 -12
  11. pygpt_net/controller/config/field/checkbox_list.py +36 -31
  12. pygpt_net/controller/config/field/cmd.py +51 -57
  13. pygpt_net/controller/config/field/combo.py +33 -16
  14. pygpt_net/controller/config/field/dictionary.py +48 -55
  15. pygpt_net/controller/config/field/input.py +50 -32
  16. pygpt_net/controller/config/field/slider.py +40 -45
  17. pygpt_net/controller/config/field/textarea.py +20 -6
  18. pygpt_net/controller/config/placeholder.py +110 -231
  19. pygpt_net/controller/ctx/common.py +48 -49
  20. pygpt_net/controller/ctx/ctx.py +24 -4
  21. pygpt_net/controller/lang/mapping.py +57 -95
  22. pygpt_net/controller/lang/plugins.py +64 -55
  23. pygpt_net/controller/lang/settings.py +39 -38
  24. pygpt_net/controller/layout/layout.py +11 -2
  25. pygpt_net/controller/plugins/plugins.py +19 -1
  26. pygpt_net/controller/settings/profile.py +16 -4
  27. pygpt_net/controller/ui/mode.py +107 -125
  28. pygpt_net/core/bridge/bridge.py +5 -5
  29. pygpt_net/core/command/command.py +149 -219
  30. pygpt_net/core/ctx/ctx.py +94 -146
  31. pygpt_net/core/debug/debug.py +48 -58
  32. pygpt_net/core/models/models.py +74 -112
  33. pygpt_net/core/modes/modes.py +13 -21
  34. pygpt_net/core/plugins/plugins.py +154 -177
  35. pygpt_net/core/presets/presets.py +103 -176
  36. pygpt_net/core/render/web/body.py +50 -39
  37. pygpt_net/core/render/web/renderer.py +154 -251
  38. pygpt_net/core/text/utils.py +28 -44
  39. pygpt_net/core/tokens/tokens.py +104 -203
  40. pygpt_net/data/config/config.json +3 -3
  41. pygpt_net/data/config/models.json +3 -3
  42. pygpt_net/item/ctx.py +141 -139
  43. pygpt_net/plugin/agent/plugin.py +2 -1
  44. pygpt_net/plugin/audio_output/plugin.py +5 -2
  45. pygpt_net/plugin/base/plugin.py +77 -93
  46. pygpt_net/plugin/bitbucket/plugin.py +3 -2
  47. pygpt_net/plugin/cmd_code_interpreter/plugin.py +3 -2
  48. pygpt_net/plugin/cmd_custom/plugin.py +3 -2
  49. pygpt_net/plugin/cmd_files/plugin.py +3 -2
  50. pygpt_net/plugin/cmd_history/plugin.py +3 -2
  51. pygpt_net/plugin/cmd_mouse_control/plugin.py +5 -2
  52. pygpt_net/plugin/cmd_serial/plugin.py +3 -2
  53. pygpt_net/plugin/cmd_system/plugin.py +3 -6
  54. pygpt_net/plugin/cmd_web/plugin.py +3 -2
  55. pygpt_net/plugin/experts/plugin.py +2 -2
  56. pygpt_net/plugin/facebook/plugin.py +3 -4
  57. pygpt_net/plugin/github/plugin.py +4 -2
  58. pygpt_net/plugin/google/plugin.py +3 -3
  59. pygpt_net/plugin/idx_llama_index/plugin.py +3 -2
  60. pygpt_net/plugin/mailer/plugin.py +3 -5
  61. pygpt_net/plugin/openai_vision/plugin.py +3 -2
  62. pygpt_net/plugin/real_time/plugin.py +52 -60
  63. pygpt_net/plugin/slack/plugin.py +3 -4
  64. pygpt_net/plugin/telegram/plugin.py +3 -4
  65. pygpt_net/plugin/twitter/plugin.py +3 -4
  66. pygpt_net/tools/code_interpreter/tool.py +0 -1
  67. pygpt_net/tools/translator/tool.py +1 -1
  68. pygpt_net/ui/layout/ctx/ctx_list.py +10 -6
  69. pygpt_net/ui/main.py +46 -30
  70. pygpt_net/ui/tray.py +61 -60
  71. pygpt_net/ui/widget/lists/context.py +2 -2
  72. pygpt_net/ui/widget/textarea/web.py +161 -48
  73. pygpt_net/utils.py +8 -1
  74. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.6.dist-info}/METADATA +16 -2
  75. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.6.dist-info}/RECORD +78 -78
  76. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.6.dist-info}/LICENSE +0 -0
  77. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.6.dist-info}/WHEEL +0 -0
  78. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.6.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,13 @@
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.11 14:00:00 #
9
+ # Updated Date: 2025.08.16 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
13
- from typing import Optional
13
+ import gc
14
+ import io
15
+ from typing import Optional, Literal
14
16
 
15
17
  from PySide6.QtCore import QObject, Signal, Slot, QRunnable
16
18
 
@@ -20,6 +22,32 @@ from pygpt_net.core.types import MODE_ASSISTANT
20
22
  from pygpt_net.core.text.utils import has_unclosed_code_tag
21
23
  from pygpt_net.item.ctx import CtxItem
22
24
 
25
+ EventType = Literal[
26
+ "response.completed",
27
+ "response.output_text.delta",
28
+ "response.output_item.added",
29
+ "response.function_call_arguments.delta",
30
+ "response.function_call_arguments.done",
31
+ "response.output_text.annotation.added",
32
+ "response.reasoning_summary_text.delta",
33
+ "response.output_item.done",
34
+ "response.code_interpreter_call_code.delta",
35
+ "response.code_interpreter_call_code.done",
36
+ "response.image_generation_call.partial_image",
37
+ "response.created",
38
+ "response.done",
39
+ "response.failed",
40
+ "error",
41
+ ]
42
+ ChunkType = Literal[
43
+ "api_chat",
44
+ "api_chat_responses",
45
+ "api_completion",
46
+ "langchain_chat",
47
+ "llama_chat",
48
+ "raw",
49
+ ]
50
+
23
51
  class WorkerSignals(QObject):
24
52
  """
25
53
  Defines the signals available from a running worker thread.
@@ -54,15 +82,15 @@ class StreamWorker(QRunnable):
54
82
  output_tokens = 0
55
83
  begin = True
56
84
  error = None
57
- fn_args_buffers = {}
58
- citations = []
85
+ fn_args_buffers: dict[str, io.StringIO] = {}
86
+ citations: Optional[list] = []
59
87
  files = []
60
88
  img_path = core.image.gen_unique_path(ctx)
61
89
  is_image = False
62
90
  is_code = False
63
91
  force_func_call = False
64
92
  stopped = False
65
- chunk_type = "raw"
93
+ chunk_type: ChunkType = "raw"
66
94
  generator = ctx.stream
67
95
  ctx.stream = None
68
96
 
@@ -101,12 +129,12 @@ class StreamWorker(QRunnable):
101
129
  stopped = True
102
130
  break
103
131
 
104
- etype = None
132
+ etype: Optional[EventType] = None
105
133
  response = None
106
134
 
107
135
  if ctx.use_responses_api:
108
136
  if hasattr(chunk, 'type'):
109
- etype = chunk.type
137
+ etype = chunk.type # type: ignore[assignment]
110
138
  chunk_type = "api_chat_responses"
111
139
  else:
112
140
  continue
@@ -128,7 +156,6 @@ class StreamWorker(QRunnable):
128
156
  else:
129
157
  chunk_type = "raw"
130
158
 
131
- # OpenAI chat completion
132
159
  if chunk_type == "api_chat":
133
160
  citations = None
134
161
  delta = chunk.choices[0].delta
@@ -158,7 +185,6 @@ class StreamWorker(QRunnable):
158
185
  if getattr(tool_chunk.function, "arguments", None):
159
186
  tool_call["function"]["arguments"] += tool_chunk.function.arguments
160
187
 
161
- # OpenAI Responses API
162
188
  elif chunk_type == "api_chat_responses":
163
189
  if etype == "response.completed":
164
190
  for item in chunk.response.output:
@@ -197,7 +223,6 @@ class StreamWorker(QRunnable):
197
223
  elif etype == "response.output_text.delta":
198
224
  response = chunk.delta
199
225
 
200
- # function_call
201
226
  elif etype == "response.output_item.added" and chunk.item.type == "function_call":
202
227
  tool_calls.append({
203
228
  "id": chunk.item.id,
@@ -205,18 +230,23 @@ class StreamWorker(QRunnable):
205
230
  "type": "function",
206
231
  "function": {"name": chunk.item.name, "arguments": ""}
207
232
  })
208
- fn_args_buffers[chunk.item.id] = ""
233
+ fn_args_buffers[chunk.item.id] = io.StringIO()
209
234
  elif etype == "response.function_call_arguments.delta":
210
- fn_args_buffers[chunk.item_id] += chunk.delta
235
+ buf = fn_args_buffers.get(chunk.item_id)
236
+ if buf is not None:
237
+ buf.write(chunk.delta)
211
238
  elif etype == "response.function_call_arguments.done":
212
239
  buf = fn_args_buffers.pop(chunk.item_id, None)
213
240
  if buf is not None:
241
+ try:
242
+ args_val = buf.getvalue()
243
+ finally:
244
+ buf.close()
214
245
  for tc in tool_calls:
215
246
  if tc["id"] == chunk.item_id:
216
- tc["function"]["arguments"] = buf
247
+ tc["function"]["arguments"] = args_val
217
248
  break
218
249
 
219
- # annotations
220
250
  elif etype == "response.output_text.annotation.added":
221
251
  ann = chunk.annotation
222
252
  if ann['type'] == "url_citation":
@@ -231,7 +261,6 @@ class StreamWorker(QRunnable):
231
261
  "file_id": ann['file_id'],
232
262
  })
233
263
 
234
- # computer use
235
264
  elif etype == "response.reasoning_summary_text.delta":
236
265
  response = chunk.delta
237
266
 
@@ -240,7 +269,6 @@ class StreamWorker(QRunnable):
240
269
  if has_calls:
241
270
  force_func_call = True
242
271
 
243
- # code interpreter
244
272
  elif etype == "response.code_interpreter_call_code.delta":
245
273
  if not is_code:
246
274
  response = "\n\n**Code interpreter**\n```python\n" + chunk.delta
@@ -250,15 +278,14 @@ class StreamWorker(QRunnable):
250
278
  elif etype == "response.code_interpreter_call_code.done":
251
279
  response = "\n\n```\n-----------\n"
252
280
 
253
- # image gen
254
281
  elif etype == "response.image_generation_call.partial_image":
255
282
  image_base64 = chunk.partial_image_b64
256
283
  image_bytes = base64.b64decode(image_base64)
257
284
  with open(img_path, "wb") as f:
258
285
  f.write(image_bytes)
286
+ del image_bytes
259
287
  is_image = True
260
288
 
261
- # response ID
262
289
  elif etype == "response.created":
263
290
  ctx.msg_id = str(chunk.response.id)
264
291
  core.ctx.update_item(ctx)
@@ -266,18 +293,15 @@ class StreamWorker(QRunnable):
266
293
  elif etype in {"response.done", "response.failed", "error"}:
267
294
  pass
268
295
 
269
- # OpenAI completion
270
296
  elif chunk_type == "api_completion":
271
297
  choice0 = chunk.choices[0]
272
298
  if choice0.text is not None:
273
299
  response = choice0.text
274
300
 
275
- # langchain chat
276
301
  elif chunk_type == "langchain_chat":
277
302
  if chunk.content is not None:
278
303
  response = str(chunk.content)
279
304
 
280
- # llama chat
281
305
  elif chunk_type == "llama_chat":
282
306
  if chunk.delta is not None:
283
307
  response = str(chunk.delta)
@@ -301,7 +325,6 @@ class StreamWorker(QRunnable):
301
325
  tool_calls.clear()
302
326
  tool_calls.append(tool_call)
303
327
 
304
- # raw text (llama-index / langchain completion)
305
328
  else:
306
329
  if chunk is not None:
307
330
  response = str(chunk)
@@ -326,13 +349,11 @@ class StreamWorker(QRunnable):
326
349
 
327
350
  chunk = None
328
351
 
329
- # tool calls
330
352
  if tool_calls:
331
353
  ctx.force_call = force_func_call
332
354
  core.debug.info("[chat] Tool calls found, unpacking...")
333
355
  core.command.unpack_tool_calls_chunks(ctx, tool_calls)
334
356
 
335
- # image
336
357
  if is_image:
337
358
  core.debug.info("[chat] Image generation call found")
338
359
  ctx.images = [img_path]
@@ -371,11 +392,17 @@ class StreamWorker(QRunnable):
371
392
 
372
393
  emit_end(ctx)
373
394
 
395
+ for _buf in fn_args_buffers.values():
396
+ try:
397
+ _buf.close()
398
+ except Exception:
399
+ pass
374
400
  fn_args_buffers.clear()
375
401
  files.clear()
376
402
  tool_calls.clear()
377
- if citations is not None:
403
+ if citations is not None and citations is not ctx.urls:
378
404
  citations.clear()
405
+ citations = None
379
406
 
380
407
  self.cleanup()
381
408
 
@@ -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.06.30 02:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict, List
@@ -40,6 +40,24 @@ class Config:
40
40
  self.slider = Slider(window)
41
41
  self.textarea = Textarea(window)
42
42
 
43
+ self._apply_map = {
44
+ 'text': self.input.apply,
45
+ 'textarea': self.textarea.apply,
46
+ 'bool': self.checkbox.apply,
47
+ 'bool_list': self.checkbox_list.apply,
48
+ 'dict': self.dictionary.apply,
49
+ 'combo': self.combo.apply,
50
+ 'cmd': self.cmd.apply,
51
+ }
52
+ self._get_map = {
53
+ 'text': self.input.get_value,
54
+ 'textarea': self.textarea.get_value,
55
+ 'bool': self.checkbox.get_value,
56
+ 'bool_list': self.checkbox_list.get_value,
57
+ 'dict': self.dictionary.get_value,
58
+ 'cmd': self.cmd.get_value,
59
+ }
60
+
43
61
  def load_options(
44
62
  self,
45
63
  parent_id: str,
@@ -51,8 +69,7 @@ class Config:
51
69
  :param parent_id: Parent ID
52
70
  :param options: Options dict
53
71
  """
54
- for key in options:
55
- option = options[key]
72
+ for key, option in options.items():
56
73
  self.apply(parent_id, key, option)
57
74
 
58
75
  def apply(
@@ -68,25 +85,16 @@ class Config:
68
85
  :param key: Option key
69
86
  :param option: Option dict
70
87
  """
71
- if option['type'] == 'int' or option['type'] == 'float':
72
- if 'slider' in option and option['slider']:
88
+ t = option['type']
89
+ if t in ('int', 'float'):
90
+ if option.get('slider'):
73
91
  self.slider.apply(parent_id, key, option)
74
92
  else:
75
93
  self.input.apply(parent_id, key, option)
76
- elif option['type'] == 'text':
77
- self.input.apply(parent_id, key, option)
78
- elif option['type'] == 'textarea':
79
- self.textarea.apply(parent_id, key, option)
80
- elif option['type'] == 'bool':
81
- self.checkbox.apply(parent_id, key, option)
82
- elif option['type'] == 'bool_list':
83
- self.checkbox_list.apply(parent_id, key, option)
84
- elif option['type'] == 'dict':
85
- self.dictionary.apply(parent_id, key, option)
86
- elif option['type'] == 'combo':
87
- self.combo.apply(parent_id, key, option)
88
- elif option['type'] == 'cmd':
89
- self.cmd.apply(parent_id, key, option)
94
+ return
95
+ func = self._apply_map.get(t)
96
+ if func:
97
+ func(parent_id, key, option)
90
98
 
91
99
  def apply_value(
92
100
  self,
@@ -122,25 +130,16 @@ class Config:
122
130
  :param idx: return selected idx, not the value
123
131
  :return: Option value
124
132
  """
125
- if option['type'] == 'int' or option['type'] == 'float':
126
- if 'slider' in option and option['slider']:
133
+ t = option['type']
134
+ if t in ('int', 'float'):
135
+ if option.get('slider'):
127
136
  return self.slider.get_value(parent_id, key, option)
128
- else:
129
- return self.input.get_value(parent_id, key, option)
130
- elif option['type'] == 'text':
131
137
  return self.input.get_value(parent_id, key, option)
132
- elif option['type'] == 'textarea':
133
- return self.textarea.get_value(parent_id, key, option)
134
- elif option['type'] == 'bool':
135
- return self.checkbox.get_value(parent_id, key, option)
136
- elif option['type'] == 'bool_list':
137
- return self.checkbox_list.get_value(parent_id, key, option)
138
- elif option['type'] == 'dict':
139
- return self.dictionary.get_value(parent_id, key, option)
140
- elif option['type'] == 'combo':
138
+ if t == 'combo':
141
139
  return self.combo.get_value(parent_id, key, option, idx)
142
- elif option['type'] == 'cmd':
143
- return self.cmd.get_value(parent_id, key, option)
140
+ func = self._get_map.get(t)
141
+ if func:
142
+ return func(parent_id, key, option)
144
143
 
145
144
  def update_list(
146
145
  self,
@@ -159,13 +158,11 @@ class Config:
159
158
  """
160
159
  if "type" not in option:
161
160
  return
162
- if option['type'] == 'combo':
163
- as_dict = {}
164
- for item in items:
165
- for k, v in item.items():
166
- as_dict[k] = v
161
+ t = option['type']
162
+ if t == 'combo':
163
+ as_dict = {k: v for d in items for k, v in d.items()}
167
164
  self.update_combo(parent_id, key, as_dict)
168
- elif option['type'] == 'bool_list':
165
+ elif t == 'bool_list':
169
166
  self.update_bool_list(parent_id, key, items)
170
167
 
171
168
  def update_combo(
@@ -196,4 +193,4 @@ class Config:
196
193
  :param key: Option key
197
194
  :param items: Items dict
198
195
  """
199
- self.checkbox_list.update_list(parent_id, key, items)
196
+ self.checkbox_list.update_list(parent_id, key, items)
@@ -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.06.29 18:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict
@@ -26,7 +26,7 @@ class Checkbox:
26
26
  parent_id: str,
27
27
  key: str,
28
28
  option: Dict[str, Any]
29
- ):
29
+ ) -> None:
30
30
  """
31
31
  Apply value to checkbox
32
32
 
@@ -39,17 +39,19 @@ class Checkbox:
39
39
  value = option["value"]
40
40
  if value is None:
41
41
  value = False
42
- if key in self.window.ui.config[parent_id]:
43
- self.window.ui.config[parent_id][key].box.setChecked(bool(value))
42
+ cfg_parent = self.window.ui.config[parent_id]
43
+ row = cfg_parent.get(key)
44
+ if row is not None:
45
+ row.box.setChecked(bool(value))
44
46
 
45
47
  def on_update(
46
48
  self,
47
49
  parent_id: str,
48
50
  key: str,
49
- option: dict,
51
+ option: Dict[str, Any],
50
52
  value: Any,
51
53
  hooks: bool = True
52
- ):
54
+ ) -> None:
53
55
  """
54
56
  Event: on update checkbox value
55
57
 
@@ -59,11 +61,11 @@ class Checkbox:
59
61
  :param value: Option value
60
62
  :param hooks: Run hooks
61
63
  """
62
- # on update hooks
63
64
  if hooks:
64
- hook_name = "update.{}.{}".format(parent_id, key)
65
- if self.window.ui.has_hook(hook_name):
66
- hook = self.window.ui.get_hook(hook_name)
65
+ ui = self.window.ui
66
+ hook_name = f"update.{parent_id}.{key}"
67
+ if ui.has_hook(hook_name):
68
+ hook = ui.get_hook(hook_name)
67
69
  try:
68
70
  hook(key, value, 'checkbox')
69
71
  except Exception as e:
@@ -83,6 +85,8 @@ class Checkbox:
83
85
  :param option: Option data dict
84
86
  :return: Option value
85
87
  """
86
- if key not in self.window.ui.config[parent_id]:
88
+ cfg_parent = self.window.ui.config[parent_id]
89
+ row = cfg_parent.get(key)
90
+ if row is None:
87
91
  return False
88
- return self.window.ui.config[parent_id][key].box.isChecked()
92
+ return row.box.isChecked()
@@ -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.06.30 02:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict, List
@@ -37,19 +37,24 @@ class CheckboxList:
37
37
  if "value" not in option:
38
38
  return
39
39
  value = option["value"]
40
- exploded_list = value.split(",") if isinstance(value, str) else []
41
- if key not in self.window.ui.config[parent_id]:
42
- self.window.ui.config[parent_id][key] = {}
43
- for item in self.window.ui.config[parent_id][key].boxes:
44
- if self.window.ui.config[parent_id][key].boxes[item] is not None:
45
- self.window.ui.config[parent_id][key].boxes[item].setChecked(False)
46
- for item in exploded_list:
47
- item = item.strip()
48
- if item not in self.window.ui.config[parent_id][key].boxes:
49
- continue
50
- if self.window.ui.config[parent_id][key].boxes[item] is None:
40
+ selection = {s.strip() for s in value.split(",")} if isinstance(value, str) else set()
41
+ selection.discard("")
42
+
43
+ ui = self.window.ui
44
+ cfg_parent = ui.config.get(parent_id)
45
+ if not cfg_parent:
46
+ return
47
+ entry = cfg_parent.get(key)
48
+ if entry is None or not hasattr(entry, "boxes"):
49
+ return
50
+ boxes = entry.boxes
51
+
52
+ for name, cb in boxes.items():
53
+ if cb is None:
51
54
  continue
52
- self.window.ui.config[parent_id][key].boxes[item].setChecked(True)
55
+ desired = name in selection
56
+ if cb.isChecked() != desired:
57
+ cb.setChecked(desired)
53
58
 
54
59
  def on_update(
55
60
  self,
@@ -70,15 +75,16 @@ class CheckboxList:
70
75
  :param subkey: Subkey for specific checkbox
71
76
  :param hooks: Run hooks
72
77
  """
73
- # on update hooks
74
78
  if hooks:
75
- hook_name = "update.{}.{}".format(parent_id, key)
76
- if self.window.ui.has_hook(hook_name):
77
- hook = self.window.ui.get_hook(hook_name)
78
- try:
79
- hook(key, value, 'bool_list')
80
- except Exception as e:
81
- self.window.core.debug.log(e)
79
+ ui = self.window.ui
80
+ hook_name = f"update.{parent_id}.{key}"
81
+ if ui.has_hook(hook_name):
82
+ hook = ui.get_hook(hook_name)
83
+ if hook:
84
+ try:
85
+ hook(key, value, 'bool_list')
86
+ except Exception as e:
87
+ self.window.core.debug.log(e)
82
88
 
83
89
  def get_value(
84
90
  self,
@@ -94,16 +100,15 @@ class CheckboxList:
94
100
  :param option: Option data dict
95
101
  :return: Option value
96
102
  """
97
- if key not in self.window.ui.config[parent_id]:
103
+ ui = self.window.ui
104
+ cfg_parent = ui.config.get(parent_id)
105
+ if not cfg_parent:
98
106
  return ""
99
- imploded_list = []
100
- for item in self.window.ui.config[parent_id][key].boxes:
101
- if self.window.ui.config[parent_id][key].boxes[item] is None:
102
- continue
103
- if self.window.ui.config[parent_id][key].boxes[item].isChecked():
104
- imploded_list.append(item)
105
- return ",".join(imploded_list)
106
-
107
+ entry = cfg_parent.get(key)
108
+ if entry is None or not hasattr(entry, "boxes"):
109
+ return ""
110
+ boxes = entry.boxes
111
+ return ",".join(name for name, cb in boxes.items() if cb is not None and cb.isChecked())
107
112
 
108
113
  def update_list(
109
114
  self,
@@ -118,4 +123,4 @@ class CheckboxList:
118
123
  :param key: Option key
119
124
  :param items: Items dict
120
125
  """
121
- self.window.ui.config[parent_id][key].update_boxes_list(items)
126
+ self.window.ui.config[parent_id][key].update_boxes_list(items)