pyqt-code-editor 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyqt_code_editor/__init__.py +5 -0
- pyqt_code_editor/_settings.py +232 -0
- pyqt_code_editor/app.py +438 -0
- pyqt_code_editor/code_editors/__init__.py +1 -0
- pyqt_code_editor/code_editors/code_editors.py +33 -0
- pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- pyqt_code_editor/code_editors/languages/generic.py +11 -0
- pyqt_code_editor/code_editors/languages/markdown.py +11 -0
- pyqt_code_editor/code_editors/languages/python.py +13 -0
- pyqt_code_editor/code_editors/languages/text.py +9 -0
- pyqt_code_editor/components/__init__.py +0 -0
- pyqt_code_editor/components/editor_panel.py +399 -0
- pyqt_code_editor/components/find_in_files.py +199 -0
- pyqt_code_editor/components/jupyter_console.py +487 -0
- pyqt_code_editor/components/project_explorer.py +515 -0
- pyqt_code_editor/components/settings_panel.py +162 -0
- pyqt_code_editor/components/sigmund.py +75 -0
- pyqt_code_editor/components/workspace_explorer.py +162 -0
- pyqt_code_editor/mixins/__init__.py +15 -0
- pyqt_code_editor/mixins/auto_pair.py +201 -0
- pyqt_code_editor/mixins/base.py +142 -0
- pyqt_code_editor/mixins/check.py +94 -0
- pyqt_code_editor/mixins/comment.py +91 -0
- pyqt_code_editor/mixins/complete.py +482 -0
- pyqt_code_editor/mixins/execute.py +182 -0
- pyqt_code_editor/mixins/file_link.py +166 -0
- pyqt_code_editor/mixins/line_number.py +206 -0
- pyqt_code_editor/mixins/python_auto_indent.py +239 -0
- pyqt_code_editor/mixins/search_replace.py +396 -0
- pyqt_code_editor/mixins/shortcuts.py +228 -0
- pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
- pyqt_code_editor/mixins/symbols.py +45 -0
- pyqt_code_editor/mixins/theme.py +99 -0
- pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- pyqt_code_editor/mixins/zoom.py +51 -0
- pyqt_code_editor/python_utils.py +936 -0
- pyqt_code_editor/signal_router.py +116 -0
- pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
- pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
- pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
- pyqt_code_editor/themes.py +36 -0
- pyqt_code_editor/utils.py +96 -0
- pyqt_code_editor/watchdog.py +141 -0
- pyqt_code_editor/widgets/__init__.py +6 -0
- pyqt_code_editor/widgets/calltip_widget.py +44 -0
- pyqt_code_editor/widgets/completion_popup.py +89 -0
- pyqt_code_editor/widgets/dock.py +15 -0
- pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
- pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
- pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
- pyqt_code_editor/widgets/search_replace_frame.py +128 -0
- pyqt_code_editor/widgets/tab_splitter.py +22 -0
- pyqt_code_editor/widgets/tabbed_editor.py +351 -0
- pyqt_code_editor/worker/__init__.py +0 -0
- pyqt_code_editor/worker/languages/__init__.py +0 -0
- pyqt_code_editor/worker/languages/generic.py +15 -0
- pyqt_code_editor/worker/languages/python.py +17 -0
- pyqt_code_editor/worker/manager.py +113 -0
- pyqt_code_editor/worker/process.py +128 -0
- pyqt_code_editor/worker/providers/__init__.py +1 -0
- pyqt_code_editor/worker/providers/codestral.py +56 -0
- pyqt_code_editor/worker/providers/jedi.py +115 -0
- pyqt_code_editor/worker/providers/ruff.py +45 -0
- pyqt_code_editor/worker/providers/sigmund.py +29 -0
- pyqt_code_editor/worker/providers/symbol.py +78 -0
- pyqt_code_editor/worker/settings.py +1 -0
- pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
- pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
- pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
- pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
- pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
from qtpy.QtWidgets import (QTabWidget, QWidget, QVBoxLayout, QToolButton, QMenu,
|
|
2
|
+
QAction, QHBoxLayout)
|
|
3
|
+
from qtpy.QtCore import Signal, QTimer
|
|
4
|
+
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
|
5
|
+
from qtconsole.manager import QtKernelManager
|
|
6
|
+
from jupyter_client.kernelspec import KernelSpecManager
|
|
7
|
+
import qtawesome as qta
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
import json
|
|
12
|
+
from concurrent.futures import Future
|
|
13
|
+
from .. import settings
|
|
14
|
+
from ..themes import THEMES, OUTER_CONTENT_MARGINS, HORIZONTAL_SPACING
|
|
15
|
+
from ..widgets import Dock
|
|
16
|
+
from .. import watchdog
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class JupyterConsoleTab(QWidget):
|
|
21
|
+
"""Individual tab containing a Jupyter console with its own kernel"""
|
|
22
|
+
|
|
23
|
+
execution_complete = Signal(str, object) # Signal for output interception
|
|
24
|
+
workspace_updated = Signal(dict) # Signal for workspace updates
|
|
25
|
+
|
|
26
|
+
def __init__(self, kernel_name=None, parent=None):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self.kernel_name = kernel_name or 'python3'
|
|
29
|
+
self.layout = QVBoxLayout(self)
|
|
30
|
+
self.layout.setContentsMargins(*OUTER_CONTENT_MARGINS)
|
|
31
|
+
|
|
32
|
+
# Dictionary to track pending message results
|
|
33
|
+
self._pending_messages = {}
|
|
34
|
+
# Set of internal message IDs (for workspace queries, etc.)
|
|
35
|
+
self._internal_messages = set()
|
|
36
|
+
# Set of message IDs where output should be hidden
|
|
37
|
+
self._silent_messages = set()
|
|
38
|
+
|
|
39
|
+
# Create Jupyter console widget
|
|
40
|
+
self.jupyter_widget = RichJupyterWidget()
|
|
41
|
+
self.layout.addWidget(self.jupyter_widget)
|
|
42
|
+
|
|
43
|
+
# Set up kernel - using out-of-process kernel
|
|
44
|
+
self.kernel_manager = QtKernelManager(kernel_name=self.kernel_name)
|
|
45
|
+
self.kernel_manager.start_kernel()
|
|
46
|
+
|
|
47
|
+
self.kernel_client = self.kernel_manager.client()
|
|
48
|
+
self.kernel_client.start_channels()
|
|
49
|
+
|
|
50
|
+
# Connect the console to the kernel
|
|
51
|
+
self.jupyter_widget.kernel_manager = self.kernel_manager
|
|
52
|
+
self.jupyter_widget.kernel_client = self.kernel_client
|
|
53
|
+
|
|
54
|
+
# Set up output capture
|
|
55
|
+
self._setup_output_interception()
|
|
56
|
+
self.jupyter_widget.set_default_style(colors='linux')
|
|
57
|
+
background_color = THEMES[settings.color_scheme]['background_color']
|
|
58
|
+
stylesheet = f'''QPlainTextEdit, QTextEdit {{
|
|
59
|
+
background-color: '{background_color}';
|
|
60
|
+
background-clip: padding;
|
|
61
|
+
color: white;
|
|
62
|
+
font-size: {settings.font_size}pt;
|
|
63
|
+
font-family: '{settings.font_family}';
|
|
64
|
+
selection-background-color: #555;
|
|
65
|
+
}}
|
|
66
|
+
.inverted {{
|
|
67
|
+
background-color: white;
|
|
68
|
+
color: black;
|
|
69
|
+
}}
|
|
70
|
+
.error {{ color: red; }}
|
|
71
|
+
.in-prompt-number {{ font-weight: bold; }}
|
|
72
|
+
.out-prompt-number {{ font-weight: bold; }}
|
|
73
|
+
.in-prompt,
|
|
74
|
+
.in-prompt-number {{ color: lime; }}
|
|
75
|
+
.out-prompt,
|
|
76
|
+
.out-prompt-number {{ color: red; }}
|
|
77
|
+
'''
|
|
78
|
+
self.jupyter_widget.setStyleSheet(stylesheet)
|
|
79
|
+
|
|
80
|
+
# Connect execution_complete to auto-update workspace
|
|
81
|
+
self.execution_complete.connect(self._on_execution_complete)
|
|
82
|
+
|
|
83
|
+
def _setup_output_interception(self):
|
|
84
|
+
"""Set up output interception to capture kernel output"""
|
|
85
|
+
# Save reference to the original handler
|
|
86
|
+
self._original_iopub_handler = self.jupyter_widget._dispatch
|
|
87
|
+
# Disconnect the original handler
|
|
88
|
+
self.jupyter_widget.kernel_client.iopub_channel.message_received.disconnect(
|
|
89
|
+
self._original_iopub_handler)
|
|
90
|
+
# Connect our handler first
|
|
91
|
+
self.jupyter_widget.kernel_client.iopub_channel.message_received.connect(
|
|
92
|
+
self._handle_iopub_message)
|
|
93
|
+
|
|
94
|
+
def _handle_iopub_message(self, msg):
|
|
95
|
+
"""Handle messages from the kernel's IOPub channel"""
|
|
96
|
+
msg_type = msg.get('msg_type', '')
|
|
97
|
+
content = msg.get('content', {})
|
|
98
|
+
parent_header = msg.get('parent_header', {})
|
|
99
|
+
msg_id = parent_header.get('msg_id')
|
|
100
|
+
|
|
101
|
+
# Check if this is a silent execution (output should be hidden)
|
|
102
|
+
is_silent = msg_id in self._silent_messages
|
|
103
|
+
|
|
104
|
+
# Check if this message is a response to a tracked request
|
|
105
|
+
if msg_id in self._pending_messages:
|
|
106
|
+
# Handle responses for workspace queries
|
|
107
|
+
if msg_type == 'stream' and 'text' in content:
|
|
108
|
+
future = self._pending_messages[msg_id]
|
|
109
|
+
if not future.done():
|
|
110
|
+
try:
|
|
111
|
+
# Try to parse JSON output for workspace queries
|
|
112
|
+
output = content['text'].strip()
|
|
113
|
+
if output:
|
|
114
|
+
try:
|
|
115
|
+
data = json.loads(output)
|
|
116
|
+
future.set_result(data)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
future.set_result(output)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Error processing message: {e}")
|
|
121
|
+
future.set_exception(e)
|
|
122
|
+
|
|
123
|
+
# Check if response is complete
|
|
124
|
+
elif msg_type == 'status' and content.get('execution_state') == 'idle':
|
|
125
|
+
# Message processing is complete
|
|
126
|
+
if msg_id in self._pending_messages:
|
|
127
|
+
future = self._pending_messages[msg_id]
|
|
128
|
+
# If the future is still not done, set an empty result
|
|
129
|
+
if not future.done():
|
|
130
|
+
future.set_result({})
|
|
131
|
+
# Clean up
|
|
132
|
+
self._cleanup_message_ids(msg_id)
|
|
133
|
+
|
|
134
|
+
# Only emit execution_complete for non-internal messages
|
|
135
|
+
if msg_id not in self._internal_messages:
|
|
136
|
+
# Detect execution completion with status messages
|
|
137
|
+
if not is_silent and msg_type == 'status' and content.get('execution_state') == 'idle':
|
|
138
|
+
# This indicates the execution is complete, regardless of output
|
|
139
|
+
|
|
140
|
+
self.execution_complete.emit('', {})
|
|
141
|
+
# Capture execution results for regular code execution
|
|
142
|
+
if msg_type == 'execute_result':
|
|
143
|
+
data = content.get('data', {})
|
|
144
|
+
text_output = data.get('text/plain', '')
|
|
145
|
+
self.execution_complete.emit(text_output, content)
|
|
146
|
+
|
|
147
|
+
# Capture stdout/stderr
|
|
148
|
+
elif msg_type in ('stream', 'display_data', 'error'):
|
|
149
|
+
if msg_type == 'stream':
|
|
150
|
+
output = content.get('text', '')
|
|
151
|
+
elif msg_type == 'display_data':
|
|
152
|
+
output = str(content.get('data', {}).get('text/plain', ''))
|
|
153
|
+
else: # error
|
|
154
|
+
output = '\n'.join(content.get('traceback', []))
|
|
155
|
+
|
|
156
|
+
self.execution_complete.emit(output, content)
|
|
157
|
+
|
|
158
|
+
# IMPORTANT: Only forward non-silent messages to the widget
|
|
159
|
+
if not is_silent or msg_type not in ('execute_result', 'display_data',
|
|
160
|
+
'stream', 'error'):
|
|
161
|
+
self._original_iopub_handler(msg)
|
|
162
|
+
|
|
163
|
+
def _cleanup_message_ids(self, msg_id):
|
|
164
|
+
"""Clean up message IDs from tracking sets"""
|
|
165
|
+
self._pending_messages.pop(msg_id, None)
|
|
166
|
+
self._internal_messages.discard(msg_id)
|
|
167
|
+
self._silent_messages.discard(msg_id)
|
|
168
|
+
|
|
169
|
+
def _on_execution_complete(self, output, content):
|
|
170
|
+
"""Automatically update workspace after regular code execution"""
|
|
171
|
+
# Use a small delay to ensure the kernel has processed everything
|
|
172
|
+
QTimer.singleShot(100, self.update_workspace)
|
|
173
|
+
|
|
174
|
+
def update_workspace(self):
|
|
175
|
+
"""Trigger a workspace update and emit the signal when complete"""
|
|
176
|
+
future = self.get_workspace_async()
|
|
177
|
+
future.add_done_callback(self._on_workspace_update_complete)
|
|
178
|
+
|
|
179
|
+
def _on_workspace_update_complete(self, future):
|
|
180
|
+
"""Handle completion of workspace update"""
|
|
181
|
+
try:
|
|
182
|
+
workspace_data = future.result()
|
|
183
|
+
logger.info("Workspace updated")
|
|
184
|
+
kernel_pid = workspace_data.pop('__kernel_pid', None)
|
|
185
|
+
if kernel_pid is not None:
|
|
186
|
+
watchdog.register_subprocess(kernel_pid)
|
|
187
|
+
self.workspace_updated.emit(workspace_data)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Error updating workspace: {e}")
|
|
190
|
+
# Emit empty workspace on error
|
|
191
|
+
self.workspace_updated.emit({})
|
|
192
|
+
|
|
193
|
+
def execute_code(self, code):
|
|
194
|
+
"""Execute a code snippet in this kernel"""
|
|
195
|
+
return self.jupyter_widget.execute(code)
|
|
196
|
+
|
|
197
|
+
def execute_silently(self, code, internal=False, hide_output=True):
|
|
198
|
+
"""
|
|
199
|
+
Execute code silently without showing it in the console
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
code (str): The code to execute
|
|
203
|
+
internal (bool): If True, marks this as an internal query that
|
|
204
|
+
shouldn't trigger workspace updates
|
|
205
|
+
hide_output (bool): If True, hide any output generated by this execution
|
|
206
|
+
"""
|
|
207
|
+
msg_id = self.kernel_client.execute(code, silent=True, store_history=False)
|
|
208
|
+
if msg_id:
|
|
209
|
+
if internal:
|
|
210
|
+
self._internal_messages.add(msg_id)
|
|
211
|
+
if hide_output:
|
|
212
|
+
self._silent_messages.add(msg_id)
|
|
213
|
+
return msg_id
|
|
214
|
+
|
|
215
|
+
def execute_and_return_result(self, code, timeout=5.0):
|
|
216
|
+
"""Execute code silently and return the captured output synchronously"""
|
|
217
|
+
future = self.execute_and_get_future(code)
|
|
218
|
+
try:
|
|
219
|
+
return future.result(timeout=timeout)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Failed to get result: {e}")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def execute_and_get_future(self, code):
|
|
225
|
+
"""Execute code silently and return a future for the result"""
|
|
226
|
+
future = Future()
|
|
227
|
+
msg_id = self.execute_silently(code, internal=True, hide_output=True)
|
|
228
|
+
if not msg_id:
|
|
229
|
+
future.set_result(None)
|
|
230
|
+
return future
|
|
231
|
+
|
|
232
|
+
self._pending_messages[msg_id] = future
|
|
233
|
+
return future
|
|
234
|
+
|
|
235
|
+
def get_workspace_async(self):
|
|
236
|
+
"""Asynchronously get the variables in the kernel's workspace"""
|
|
237
|
+
# Create a unique variable name for results
|
|
238
|
+
result_var = f"_workspace_data_{uuid.uuid4().hex[:8]}"
|
|
239
|
+
|
|
240
|
+
# Define the code to execute in the kernel
|
|
241
|
+
code = f"""
|
|
242
|
+
import sys
|
|
243
|
+
import os
|
|
244
|
+
import json
|
|
245
|
+
|
|
246
|
+
{result_var} = {{'__kernel_pid': os.getpid()}}
|
|
247
|
+
|
|
248
|
+
# Get all variables from globals
|
|
249
|
+
for __var_name, __var_value in list(globals().items()):
|
|
250
|
+
# Skip internal variables and modules
|
|
251
|
+
if __var_name.startswith('_') or __var_name == '{result_var}' \
|
|
252
|
+
or __var_name in ('quit', 'exit', 'In', 'Out', 'get_ipython'):
|
|
253
|
+
continue
|
|
254
|
+
# Determine type
|
|
255
|
+
__var_type = type(__var_value).__name__
|
|
256
|
+
if __var_type in ('module', 'function', 'type'):
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Create a preview based on type
|
|
260
|
+
try:
|
|
261
|
+
if __var_type == 'DataMatrix':
|
|
262
|
+
__preview = f"DataMatrix: {{__var_value.shape}}"
|
|
263
|
+
elif __var_type == 'DataFrame':
|
|
264
|
+
__preview = f"DataFrame: {{__var_value.shape[0]}}×{{__var_value.shape[1]}}"
|
|
265
|
+
elif __var_type == 'Series':
|
|
266
|
+
__preview = f"Series: {{len(__var_value)}}"
|
|
267
|
+
elif __var_type == 'ndarray':
|
|
268
|
+
__preview = f"Array: {{__var_value.shape}}"
|
|
269
|
+
elif __var_type in ('list', 'tuple', 'dict', 'set'):
|
|
270
|
+
__preview = f"{{__var_type}}[{{len(__var_value)}}]"
|
|
271
|
+
elif __var_type in ('str', 'int', 'float', 'bool'):
|
|
272
|
+
# For simple types, just use repr with limits
|
|
273
|
+
__preview = repr(__var_value)
|
|
274
|
+
if len(__preview) > 50:
|
|
275
|
+
preview = preview[:47] + '...'
|
|
276
|
+
else:
|
|
277
|
+
# For other types, just show the type
|
|
278
|
+
__preview = f"{{__var_type}} object"
|
|
279
|
+
|
|
280
|
+
{result_var}[__var_name] = {{'type': __var_type, 'preview': __preview}}
|
|
281
|
+
except Exception as e:
|
|
282
|
+
{result_var}[__var_name] = {{'type': __var_type, 'preview': f"<Error: {{str(e)}}>"}};
|
|
283
|
+
|
|
284
|
+
# Print as JSON for easy parsing
|
|
285
|
+
print(json.dumps({result_var}))
|
|
286
|
+
"""
|
|
287
|
+
return self.execute_and_get_future(code)
|
|
288
|
+
|
|
289
|
+
def execute_file(self, filepath):
|
|
290
|
+
"""Execute a file in this kernel"""
|
|
291
|
+
code = f"%run {filepath}"
|
|
292
|
+
self.execute_code(code)
|
|
293
|
+
|
|
294
|
+
def change_directory(self, directory):
|
|
295
|
+
"""Change the kernel's working directory"""
|
|
296
|
+
if os.path.exists(directory):
|
|
297
|
+
code = f"import os\nos.chdir(r'{directory}')"
|
|
298
|
+
self.execute_silently(code)
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
def interrupt_kernel(self):
|
|
303
|
+
"""Send interrupt (SIGINT) to the kernel"""
|
|
304
|
+
if self.kernel_manager.has_kernel:
|
|
305
|
+
logger.info(f"Sending interrupt to kernel {self.kernel_name}")
|
|
306
|
+
self.kernel_manager.interrupt_kernel()
|
|
307
|
+
return True
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def restart_kernel(self):
|
|
311
|
+
"""Restart the kernel"""
|
|
312
|
+
if self.kernel_manager.has_kernel:
|
|
313
|
+
logger.info(f"Restarting kernel {self.kernel_name}")
|
|
314
|
+
self.jupyter_widget.request_restart_kernel()
|
|
315
|
+
return True
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
def shutdown_kernel(self):
|
|
319
|
+
"""Shutdown the kernel"""
|
|
320
|
+
self.kernel_client.stop_channels()
|
|
321
|
+
self.kernel_manager.shutdown_kernel()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class JupyterConsole(Dock):
|
|
325
|
+
"""Dockable widget containing tabbed Jupyter consoles"""
|
|
326
|
+
|
|
327
|
+
execution_complete = Signal(str, object) # Signal for output interception
|
|
328
|
+
workspace_updated = Signal(dict) # Signal for workspace updates
|
|
329
|
+
|
|
330
|
+
def __init__(self, parent=None, default_kernel='python3'):
|
|
331
|
+
super().__init__("Jupyter Console", parent)
|
|
332
|
+
self.setObjectName('jupyter_console')
|
|
333
|
+
self.default_kernel = default_kernel
|
|
334
|
+
|
|
335
|
+
# Initialize the tab widget
|
|
336
|
+
self.tab_widget = QTabWidget()
|
|
337
|
+
self.tab_widget.setTabsClosable(True)
|
|
338
|
+
self.tab_widget.tabCloseRequested.connect(self.close_tab)
|
|
339
|
+
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
|
340
|
+
self.setWidget(self.tab_widget)
|
|
341
|
+
|
|
342
|
+
# Create corner widget with multiple buttons
|
|
343
|
+
corner_widget = QWidget()
|
|
344
|
+
corner_layout = QHBoxLayout(corner_widget)
|
|
345
|
+
corner_layout.setContentsMargins(0, 0, 0, 0)
|
|
346
|
+
corner_layout.setSpacing(HORIZONTAL_SPACING)
|
|
347
|
+
|
|
348
|
+
# Add kernel button (for new kernels)
|
|
349
|
+
self.kernel_button = QToolButton()
|
|
350
|
+
self.kernel_button.setIcon(qta.icon('mdi6.plus'))
|
|
351
|
+
self.kernel_button.setToolTip("Add new kernel")
|
|
352
|
+
self.kernel_button.setPopupMode(QToolButton.InstantPopup)
|
|
353
|
+
self.kernel_button.setAutoRaise(True) # Make the button flat
|
|
354
|
+
corner_layout.addWidget(self.kernel_button)
|
|
355
|
+
|
|
356
|
+
# Add restart kernel button
|
|
357
|
+
self.restart_button = QToolButton()
|
|
358
|
+
self.restart_button.setIcon(qta.icon('mdi6.restart'))
|
|
359
|
+
self.restart_button.setToolTip("Restart current kernel")
|
|
360
|
+
self.restart_button.clicked.connect(self.restart_current_kernel)
|
|
361
|
+
self.restart_button.setAutoRaise(True) # Make the button flat
|
|
362
|
+
corner_layout.addWidget(self.restart_button)
|
|
363
|
+
|
|
364
|
+
# Add interrupt kernel button
|
|
365
|
+
self.interrupt_button = QToolButton()
|
|
366
|
+
self.interrupt_button.setIcon(qta.icon('mdi6.stop'))
|
|
367
|
+
self.interrupt_button.setToolTip("Interrupt current kernel (Ctrl+C)")
|
|
368
|
+
self.interrupt_button.clicked.connect(self.interrupt_current_kernel)
|
|
369
|
+
self.interrupt_button.setAutoRaise(True) # Make the button flat
|
|
370
|
+
corner_layout.addWidget(self.interrupt_button)
|
|
371
|
+
|
|
372
|
+
# Set the corner widget
|
|
373
|
+
self.tab_widget.setCornerWidget(corner_widget)
|
|
374
|
+
|
|
375
|
+
# Create kernel menu
|
|
376
|
+
self.kernel_menu = QMenu(self.kernel_button)
|
|
377
|
+
self.kernel_button.setMenu(self.kernel_menu)
|
|
378
|
+
self.refresh_kernel_menu()
|
|
379
|
+
|
|
380
|
+
# Start with a default kernel
|
|
381
|
+
self.add_console_tab(self.default_kernel)
|
|
382
|
+
|
|
383
|
+
def _on_tab_changed(self, index):
|
|
384
|
+
console_tab = self.tab_widget.widget(index)
|
|
385
|
+
console_tab.update_workspace()
|
|
386
|
+
|
|
387
|
+
def refresh_kernel_menu(self):
|
|
388
|
+
"""Refresh the list of available kernels"""
|
|
389
|
+
self.kernel_menu.clear()
|
|
390
|
+
|
|
391
|
+
# Get available kernelspecs
|
|
392
|
+
kernel_spec_manager = KernelSpecManager()
|
|
393
|
+
specs = kernel_spec_manager.get_all_specs()
|
|
394
|
+
|
|
395
|
+
for spec_name, spec in specs.items():
|
|
396
|
+
display_name = spec['spec']['display_name']
|
|
397
|
+
action = QAction(display_name, self)
|
|
398
|
+
action.setData(spec_name)
|
|
399
|
+
action.triggered.connect(self.kernel_menu_triggered)
|
|
400
|
+
self.kernel_menu.addAction(action)
|
|
401
|
+
|
|
402
|
+
def kernel_menu_triggered(self):
|
|
403
|
+
"""Handle kernel menu item selection"""
|
|
404
|
+
action = self.sender()
|
|
405
|
+
if action:
|
|
406
|
+
kernel_name = action.data()
|
|
407
|
+
self.add_console_tab(kernel_name)
|
|
408
|
+
|
|
409
|
+
def add_console_tab(self, kernel_name):
|
|
410
|
+
"""Add a new console tab with the specified kernel"""
|
|
411
|
+
# Create and add the new tab
|
|
412
|
+
console_tab = JupyterConsoleTab(kernel_name=kernel_name, parent=self)
|
|
413
|
+
console_tab.execution_complete.connect(self.handle_execution_complete)
|
|
414
|
+
console_tab.workspace_updated.connect(self.handle_workspace_updated)
|
|
415
|
+
index = self.tab_widget.addTab(console_tab, kernel_name)
|
|
416
|
+
self.tab_widget.setCurrentIndex(index)
|
|
417
|
+
return console_tab
|
|
418
|
+
|
|
419
|
+
def close_tab(self, index):
|
|
420
|
+
"""Close a console tab and shut down its kernel"""
|
|
421
|
+
widget = self.tab_widget.widget(index)
|
|
422
|
+
if widget:
|
|
423
|
+
# Shut down the kernel
|
|
424
|
+
widget.shutdown_kernel()
|
|
425
|
+
|
|
426
|
+
# Remove the tab
|
|
427
|
+
self.tab_widget.removeTab(index)
|
|
428
|
+
|
|
429
|
+
# If that was the last tab, add a new one with the default kernel
|
|
430
|
+
if self.tab_widget.count() == 0:
|
|
431
|
+
self.add_console_tab(self.default_kernel)
|
|
432
|
+
|
|
433
|
+
def get_current_console(self):
|
|
434
|
+
"""Get the currently active console tab"""
|
|
435
|
+
return self.tab_widget.currentWidget()
|
|
436
|
+
|
|
437
|
+
def restart_current_kernel(self):
|
|
438
|
+
"""Restart the kernel in the current tab"""
|
|
439
|
+
console = self.get_current_console()
|
|
440
|
+
if console:
|
|
441
|
+
logger.info("Restarting current kernel")
|
|
442
|
+
success = console.restart_kernel()
|
|
443
|
+
if success:
|
|
444
|
+
logger.info("Kernel restart initiated")
|
|
445
|
+
else:
|
|
446
|
+
logger.warning("Failed to restart kernel")
|
|
447
|
+
|
|
448
|
+
def interrupt_current_kernel(self):
|
|
449
|
+
"""Send interrupt signal to the current kernel"""
|
|
450
|
+
console = self.get_current_console()
|
|
451
|
+
if console:
|
|
452
|
+
logger.info("Interrupting current kernel")
|
|
453
|
+
success = console.interrupt_kernel()
|
|
454
|
+
if success:
|
|
455
|
+
logger.info("Interrupt signal sent to kernel")
|
|
456
|
+
else:
|
|
457
|
+
logger.warning("Failed to interrupt kernel")
|
|
458
|
+
|
|
459
|
+
def execute_code(self, code, silent=False):
|
|
460
|
+
"""Execute code in the current console"""
|
|
461
|
+
console = self.get_current_console()
|
|
462
|
+
if console:
|
|
463
|
+
if silent:
|
|
464
|
+
console.execute_silently(code)
|
|
465
|
+
else:
|
|
466
|
+
console.execute_code(code)
|
|
467
|
+
|
|
468
|
+
def execute_file(self, filepath):
|
|
469
|
+
"""Execute a file in the current console"""
|
|
470
|
+
console = self.get_current_console()
|
|
471
|
+
if console:
|
|
472
|
+
console.execute_file(filepath)
|
|
473
|
+
|
|
474
|
+
def change_directory(self, directory):
|
|
475
|
+
"""Change working directory of the current console"""
|
|
476
|
+
console = self.get_current_console()
|
|
477
|
+
if console:
|
|
478
|
+
return console.change_directory(directory)
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def handle_execution_complete(self, output, result):
|
|
482
|
+
"""Handle execution complete signal from a console tab"""
|
|
483
|
+
self.execution_complete.emit(output, result)
|
|
484
|
+
|
|
485
|
+
def handle_workspace_updated(self, dict):
|
|
486
|
+
"""Handle workspace updated signal from a console tab"""
|
|
487
|
+
self.workspace_updated.emit(dict)
|