s-py-editor 0.1.0__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.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: s_py_editor
3
+ Version: 0.1.0
4
+ Summary: A terminal-based Python editor with a sidebar, loop runner, and more.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: prompt_toolkit>=3.0
8
+ Requires-Dist: pygments>=2.0
9
+
10
+ # s-py-editor
11
+
12
+ A terminal-based Python code editor with syntax highlighting, auto-completion,
13
+ a file sidebar, and the ability to run code (selections, loops, or whole files)
14
+ without leaving the editor.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pipx install s-py-editor
20
+ ```
21
+
22
+ Or with pip:
23
+
24
+ ```bash
25
+ pip install s-py-editor
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ spy # start with empty buffer
32
+ spy myfile.py # open a file
33
+ spy . # start in a directory (sidebar shows it)
34
+ ```
35
+
36
+ ## Key bindings
37
+
38
+ | Key | Action |
39
+ |-----|--------|
40
+ | `F5` / `Ctrl+R` | Run selection → enclosing loop → whole file |
41
+ | `Ctrl+S` | Save the buffer |
42
+ | `Ctrl+A` | Select all |
43
+ | `Ctrl+B` | Toggle sidebar focus |
44
+ | `Ctrl+E` | Toggle output popup |
45
+ | `Ctrl+X` | Stop running code |
46
+ | `Ctrl+Q` / `Ctrl+C` | Quit |
47
+
48
+ ## Sidebar
49
+
50
+ When the sidebar is focused (`Ctrl+B`):
51
+
52
+ - `↑` / `↓` — move selection
53
+ - `→` / `←` — expand / collapse folders
54
+ - `Enter` — open file or toggle folder
55
+ - `Ctrl+R` — refresh directory listing
56
+ - `Tab` — focus the editor
57
+
58
+ ## Requirements
59
+
60
+ - Python 3.9+
61
+ - `prompt_toolkit>=3.0`
62
+ - `pygments>=2.0`
63
+
64
+ ## License
65
+
66
+ MIT (or your choice — update this line)
@@ -0,0 +1,57 @@
1
+ # s-py-editor
2
+
3
+ A terminal-based Python code editor with syntax highlighting, auto-completion,
4
+ a file sidebar, and the ability to run code (selections, loops, or whole files)
5
+ without leaving the editor.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pipx install s-py-editor
11
+ ```
12
+
13
+ Or with pip:
14
+
15
+ ```bash
16
+ pip install s-py-editor
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ spy # start with empty buffer
23
+ spy myfile.py # open a file
24
+ spy . # start in a directory (sidebar shows it)
25
+ ```
26
+
27
+ ## Key bindings
28
+
29
+ | Key | Action |
30
+ |-----|--------|
31
+ | `F5` / `Ctrl+R` | Run selection → enclosing loop → whole file |
32
+ | `Ctrl+S` | Save the buffer |
33
+ | `Ctrl+A` | Select all |
34
+ | `Ctrl+B` | Toggle sidebar focus |
35
+ | `Ctrl+E` | Toggle output popup |
36
+ | `Ctrl+X` | Stop running code |
37
+ | `Ctrl+Q` / `Ctrl+C` | Quit |
38
+
39
+ ## Sidebar
40
+
41
+ When the sidebar is focused (`Ctrl+B`):
42
+
43
+ - `↑` / `↓` — move selection
44
+ - `→` / `←` — expand / collapse folders
45
+ - `Enter` — open file or toggle folder
46
+ - `Ctrl+R` — refresh directory listing
47
+ - `Tab` — focus the editor
48
+
49
+ ## Requirements
50
+
51
+ - Python 3.9+
52
+ - `prompt_toolkit>=3.0`
53
+ - `pygments>=2.0`
54
+
55
+ ## License
56
+
57
+ MIT (or your choice — update this line)
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "s_py_editor"
7
+ version = "0.1.0"
8
+ description = "A terminal-based Python editor with a sidebar, loop runner, and more."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "prompt_toolkit>=3.0",
13
+ "pygments>=2.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ spy = "s_py_editor.__main__:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,479 @@
1
+ import sys
2
+ import os
3
+ import io
4
+ import ast
5
+ import threading
6
+ import traceback
7
+
8
+ from prompt_toolkit import Application
9
+ from prompt_toolkit.buffer import Buffer
10
+ from prompt_toolkit.completion import WordCompleter
11
+ from prompt_toolkit.key_binding import KeyBindings
12
+ from prompt_toolkit.layout.containers import (
13
+ HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer
14
+ )
15
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
16
+ from prompt_toolkit.layout.layout import Layout
17
+ from prompt_toolkit.layout.menus import CompletionsMenu
18
+ from prompt_toolkit.lexers import PygmentsLexer
19
+ from prompt_toolkit.filters import Condition, has_focus
20
+ from prompt_toolkit.selection import SelectionState
21
+ from prompt_toolkit.styles import Style
22
+ from pygments.lexers.python import PythonLexer
23
+
24
+
25
+ # ---------- Config ----------
26
+
27
+ FILE_PATH = sys.argv[1] if len(sys.argv) > 1 else None
28
+ ROOT_DIR = os.getcwd()
29
+ SIDEBAR_WIDTH = 32
30
+
31
+ PYTHON_WORDS = [
32
+ 'print', 'import', 'from', 'def', 'class', 'return',
33
+ 'for', 'while', 'if', 'elif', 'else', 'try', 'except',
34
+ 'finally', 'with', 'as', 'lambda', 'yield', 'pass',
35
+ 'break', 'continue', 'raise', 'assert', 'del', 'global',
36
+ 'nonlocal', 'True', 'False', 'None', 'and', 'or', 'not',
37
+ 'in', 'is', 'self', 'len', 'range', 'str', 'int', 'float',
38
+ 'list', 'dict', 'set', 'tuple', 'open', 'enumerate', 'zip',
39
+ ]
40
+
41
+
42
+ # ---------- Buffer ----------
43
+
44
+ buffer = Buffer(
45
+ completer=WordCompleter(PYTHON_WORDS, ignore_case=True),
46
+ complete_while_typing=True,
47
+ name='main',
48
+ )
49
+
50
+ if FILE_PATH and os.path.exists(FILE_PATH):
51
+ with open(FILE_PATH, 'r') as f:
52
+ buffer.text = f.read()
53
+ buffer.cursor_position = 0
54
+
55
+
56
+ # ---------- State ----------
57
+
58
+ status = {'message': 'Ready'}
59
+
60
+ exec_state = {
61
+ 'namespace': {'__name__': '__main__'},
62
+ 'output': '',
63
+ 'running': False,
64
+ 'thread': None,
65
+ 'stop_event': threading.Event(),
66
+ 'show_output': False,
67
+ }
68
+
69
+ tree_state = {
70
+ 'root': ROOT_DIR,
71
+ 'expanded': set(),
72
+ 'selected': 0,
73
+ 'items': [],
74
+ }
75
+
76
+
77
+ def rebuild_tree():
78
+ items = []
79
+
80
+ def walk(path, depth):
81
+ try:
82
+ entries = os.listdir(path)
83
+ except PermissionError:
84
+ return
85
+ entries = [e for e in entries if not e.startswith('.')]
86
+ entries.sort(key=lambda x: (not os.path.isdir(os.path.join(path, x)), x.lower()))
87
+ for name in entries:
88
+ full = os.path.join(path, name)
89
+ is_dir = os.path.isdir(full)
90
+ is_exp = full in tree_state['expanded']
91
+ items.append((full, depth, is_dir, is_exp))
92
+ if is_dir and is_exp:
93
+ walk(full, depth + 1)
94
+
95
+ walk(tree_state['root'], 0)
96
+ tree_state['items'] = items
97
+ if tree_state['selected'] >= len(items):
98
+ tree_state['selected'] = max(0, len(items) - 1)
99
+
100
+
101
+ rebuild_tree()
102
+
103
+
104
+ # ---------- Rendering ----------
105
+
106
+ def get_status():
107
+ path = FILE_PATH or '<untitled>'
108
+ run = ' [RUNNING]' if exec_state['running'] else ''
109
+ return (
110
+ f' {path} | {status["message"]}{run} | '
111
+ f'Ctrl+B sidebar Ctrl+A sel-all F5 run Ctrl+S save '
112
+ f'Ctrl+X stop Ctrl+E output Ctrl+Q quit '
113
+ )
114
+
115
+
116
+ def get_sidebar_text():
117
+ result = []
118
+ for i, (path, depth, is_dir, is_exp) in enumerate(tree_state['items']):
119
+ name = os.path.basename(path) or path
120
+ indent = ' ' * depth
121
+ if is_dir:
122
+ arrow = '▼ ' if is_exp else '▶ '
123
+ line = f'{indent}{arrow}{name}/'
124
+ else:
125
+ line = f'{indent} {name}'
126
+ style = 'class:sidebar.selected' if i == tree_state['selected'] else 'class:sidebar'
127
+ padded = line[:SIDEBAR_WIDTH].ljust(SIDEBAR_WIDTH)
128
+ result.append((style, padded + '\n'))
129
+ if not result:
130
+ result.append(('class:sidebar', '(empty)'))
131
+ return result
132
+
133
+
134
+ # ---------- Loop detection ----------
135
+
136
+ def enclosing_loop_source(text, cursor_index):
137
+ try:
138
+ tree = ast.parse(text)
139
+ except SyntaxError:
140
+ return None
141
+ lines = text.splitlines(keepends=True)
142
+
143
+ def offset_of_line(n):
144
+ return sum(len(l) for l in lines[:n - 1])
145
+
146
+ target = None
147
+ for node in ast.walk(tree):
148
+ if isinstance(node, (ast.For, ast.While, ast.AsyncFor)):
149
+ start_off = offset_of_line(node.lineno)
150
+ end_line = getattr(node, 'end_lineno', node.lineno) or node.lineno
151
+ end_off = offset_of_line(end_line + 1)
152
+ if start_off <= cursor_index <= end_off:
153
+ target = node
154
+ if target is None:
155
+ return None
156
+ try:
157
+ return ast.get_source_segment(text, target)
158
+ except Exception:
159
+ return None
160
+
161
+
162
+ # ---------- Code execution ----------
163
+
164
+ def run_code(code, label):
165
+ if exec_state['running']:
166
+ status['message'] = 'Already running — Ctrl+X to stop'
167
+ return
168
+ exec_state['running'] = True
169
+ exec_state['output'] = ''
170
+ exec_state['show_output'] = True
171
+ exec_state['stop_event'].clear()
172
+ status['message'] = f'Running {label}...'
173
+
174
+ def worker():
175
+ buf = io.StringIO()
176
+ old_out, old_err = sys.stdout, sys.stderr
177
+ sys.stdout = sys.stderr = buf
178
+ try:
179
+ compiled = compile(code, f'<{label}>', 'exec')
180
+ exec(compiled, exec_state['namespace'])
181
+ except SystemExit:
182
+ pass
183
+ except BaseException:
184
+ traceback.print_exc(file=buf)
185
+ finally:
186
+ sys.stdout, sys.stderr = old_out, old_err
187
+ raw = buf.getvalue()
188
+ exec_state['output'] = (
189
+ f"=== ran ({label}) ===\n{code}\n"
190
+ f"=== stdout / stderr ===\n"
191
+ f"{raw if raw else '(no print output)'}"
192
+ )
193
+ exec_state['running'] = False
194
+ status['message'] = f'{label} finished'
195
+ try:
196
+ app.invalidate()
197
+ except Exception:
198
+ pass
199
+
200
+ t = threading.Thread(target=worker, daemon=True)
201
+ exec_state['thread'] = t
202
+ t.start()
203
+
204
+
205
+ def get_output_text():
206
+ if exec_state['running']:
207
+ return 'Running... (Ctrl+X to request stop)'
208
+ return exec_state['output'] or '(no output yet — press F5 to run)'
209
+
210
+
211
+ # ---------- Controls ----------
212
+
213
+ buffer_control = BufferControl(buffer=buffer, lexer=PygmentsLexer(PythonLexer))
214
+ editor_window = Window(buffer_control, wrap_lines=False)
215
+
216
+ output_window = Window(
217
+ FormattedTextControl(get_output_text),
218
+ style='class:output',
219
+ wrap_lines=True,
220
+ )
221
+
222
+
223
+ # ---------- Sidebar key bindings ----------
224
+
225
+ sidebar_kb = KeyBindings()
226
+
227
+
228
+ @sidebar_kb.add('up')
229
+ def _sb_up(event):
230
+ if tree_state['selected'] > 0:
231
+ tree_state['selected'] -= 1
232
+ event.app.invalidate()
233
+
234
+
235
+ @sidebar_kb.add('down')
236
+ def _sb_down(event):
237
+ if tree_state['selected'] < len(tree_state['items']) - 1:
238
+ tree_state['selected'] += 1
239
+ event.app.invalidate()
240
+
241
+
242
+ @sidebar_kb.add('enter')
243
+ def _sb_enter(event):
244
+ global FILE_PATH
245
+ if not tree_state['items']:
246
+ return
247
+ path, depth, is_dir, is_exp = tree_state['items'][tree_state['selected']]
248
+ if is_dir:
249
+ if is_exp:
250
+ tree_state['expanded'].discard(path)
251
+ else:
252
+ tree_state['expanded'].add(path)
253
+ rebuild_tree()
254
+ else:
255
+ try:
256
+ with open(path, 'r') as f:
257
+ buffer.text = f.read()
258
+ buffer.cursor_position = 0
259
+ FILE_PATH = path
260
+ status['message'] = f'Opened {os.path.basename(path)}'
261
+ event.app.layout.focus(buffer_control)
262
+ except Exception as e:
263
+ status['message'] = f'Open failed: {e}'
264
+ event.app.invalidate()
265
+
266
+
267
+ @sidebar_kb.add('right')
268
+ def _sb_right(event):
269
+ if not tree_state['items']:
270
+ return
271
+ path, depth, is_dir, is_exp = tree_state['items'][tree_state['selected']]
272
+ if is_dir and not is_exp:
273
+ tree_state['expanded'].add(path)
274
+ rebuild_tree()
275
+ elif is_dir and is_exp:
276
+ if tree_state['selected'] + 1 < len(tree_state['items']):
277
+ nxt = tree_state['items'][tree_state['selected'] + 1]
278
+ if nxt[1] == depth + 1:
279
+ tree_state['selected'] += 1
280
+ event.app.invalidate()
281
+
282
+
283
+ @sidebar_kb.add('left')
284
+ def _sb_left(event):
285
+ if not tree_state['items']:
286
+ return
287
+ path, depth, is_dir, is_exp = tree_state['items'][tree_state['selected']]
288
+ if is_dir and is_exp:
289
+ tree_state['expanded'].discard(path)
290
+ rebuild_tree()
291
+ elif depth > 0:
292
+ parent = os.path.dirname(path)
293
+ for i, (p, _, _, _) in enumerate(tree_state['items']):
294
+ if p == parent:
295
+ tree_state['selected'] = i
296
+ break
297
+ event.app.invalidate()
298
+
299
+
300
+ @sidebar_kb.add('tab')
301
+ def _sb_tab(event):
302
+ event.app.layout.focus(buffer_control)
303
+ status['message'] = 'Editor focused'
304
+ event.app.invalidate()
305
+
306
+
307
+ @sidebar_kb.add('c-r')
308
+ def _sb_refresh(event):
309
+ rebuild_tree()
310
+ status['message'] = 'Sidebar refreshed'
311
+ event.app.invalidate()
312
+
313
+
314
+ sidebar_control = FormattedTextControl(
315
+ get_sidebar_text,
316
+ focusable=True,
317
+ key_bindings=sidebar_kb,
318
+ )
319
+ sidebar_window = Window(
320
+ sidebar_control,
321
+ width=SIDEBAR_WIDTH,
322
+ style='class:sidebar',
323
+ wrap_lines=False,
324
+ )
325
+
326
+
327
+ # ---------- Layout ----------
328
+
329
+ body = VSplit([
330
+ sidebar_window,
331
+ HSplit([
332
+ editor_window,
333
+ Window(FormattedTextControl(get_status), height=1, style='reverse'),
334
+ ]),
335
+ ])
336
+
337
+ root_container = FloatContainer(
338
+ content=body,
339
+ floats=[
340
+ Float(
341
+ xcursor=True,
342
+ ycursor=True,
343
+ content=CompletionsMenu(max_height=10, scroll_offset=1),
344
+ ),
345
+ Float(
346
+ content=ConditionalContainer(
347
+ output_window,
348
+ filter=Condition(lambda: exec_state['show_output']),
349
+ ),
350
+ top=2, right=2, width=60, height=15,
351
+ ),
352
+ ],
353
+ )
354
+
355
+
356
+ # ---------- Global key bindings ----------
357
+
358
+ kb = KeyBindings()
359
+ not_sidebar = ~has_focus(sidebar_control)
360
+
361
+
362
+ @kb.add('c-b')
363
+ def toggle_sidebar(event):
364
+ try:
365
+ current = event.app.layout.current_control
366
+ except Exception:
367
+ current = None
368
+ if current is sidebar_control:
369
+ event.app.layout.focus(buffer_control)
370
+ status['message'] = 'Editor focused'
371
+ else:
372
+ event.app.layout.focus(sidebar_control)
373
+ status['message'] = 'Sidebar — arrows/Enter, Tab to edit, Ctrl+R refresh'
374
+ event.app.invalidate()
375
+
376
+
377
+ @kb.add('c-a', filter=not_sidebar)
378
+ def select_all(event):
379
+ """Ctrl+A: select the entire buffer."""
380
+ if buffer.text:
381
+ # Anchor at offset 0, move cursor to the end → full selection
382
+ buffer.selection_state = SelectionState(original_cursor_position=0)
383
+ buffer.cursor_position = len(buffer.text)
384
+ status['message'] = 'Selected all — Backspace to clear, F5 to run'
385
+ else:
386
+ status['message'] = 'Buffer is empty'
387
+ event.app.invalidate()
388
+
389
+
390
+ @kb.add('c-s')
391
+ def save(event):
392
+ global FILE_PATH
393
+ if FILE_PATH is None:
394
+ FILE_PATH = 'untitled.py'
395
+ try:
396
+ with open(FILE_PATH, 'w') as f:
397
+ f.write(buffer.text)
398
+ status['message'] = f'Saved to {FILE_PATH}'
399
+ except Exception as e:
400
+ status['message'] = f'Save failed: {e}'
401
+ event.app.invalidate()
402
+
403
+
404
+ def _do_run(event):
405
+ doc = buffer.document
406
+ sel = doc.selection_range()
407
+ if sel and sel[0] != sel[1]:
408
+ start, end = sel
409
+ code = doc.text[start:end]
410
+ label = 'selection'
411
+ else:
412
+ loop_src = enclosing_loop_source(doc.text, doc.cursor_position)
413
+ if loop_src:
414
+ code = loop_src
415
+ label = 'enclosing loop'
416
+ else:
417
+ code = doc.text
418
+ label = 'file'
419
+ status['message'] = f'Running {label}'
420
+ run_code(code, label)
421
+ event.app.invalidate()
422
+
423
+
424
+ kb.add('f5')(_do_run)
425
+ kb.add('c-r', filter=not_sidebar)(_do_run)
426
+
427
+
428
+ @kb.add('c-e')
429
+ def toggle_output(event):
430
+ exec_state['show_output'] = not exec_state['show_output']
431
+ event.app.invalidate()
432
+
433
+
434
+ @kb.add('c-x')
435
+ def stop_run(event):
436
+ if exec_state['running']:
437
+ exec_state['stop_event'].set()
438
+ status['message'] = 'Stop requested'
439
+ else:
440
+ status['message'] = 'Nothing running'
441
+ event.app.invalidate()
442
+
443
+
444
+ @kb.add('c-q')
445
+ def quit_(event):
446
+ event.app.exit()
447
+
448
+
449
+ @kb.add('c-c')
450
+ def quit_c(event):
451
+ event.app.exit()
452
+
453
+
454
+ # ---------- Style ----------
455
+
456
+ style = Style.from_dict({
457
+ 'sidebar': 'bg:#1e1e2e #c0c0d0',
458
+ 'sidebar.selected': 'bg:#4444aa #ffffff bold',
459
+ 'output': 'bg:#1a1a2a #dddddd',
460
+ })
461
+
462
+
463
+ # ---------- Application ----------
464
+
465
+ layout = Layout(root_container, focused_element=buffer_control)
466
+ app = Application(
467
+ layout=layout,
468
+ key_bindings=kb,
469
+ full_screen=True,
470
+ style=style,
471
+ )
472
+
473
+
474
+ def main():
475
+ app.run()
476
+
477
+
478
+ if __name__ == '__main__':
479
+ main()
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: s_py_editor
3
+ Version: 0.1.0
4
+ Summary: A terminal-based Python editor with a sidebar, loop runner, and more.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: prompt_toolkit>=3.0
8
+ Requires-Dist: pygments>=2.0
9
+
10
+ # s-py-editor
11
+
12
+ A terminal-based Python code editor with syntax highlighting, auto-completion,
13
+ a file sidebar, and the ability to run code (selections, loops, or whole files)
14
+ without leaving the editor.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pipx install s-py-editor
20
+ ```
21
+
22
+ Or with pip:
23
+
24
+ ```bash
25
+ pip install s-py-editor
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ spy # start with empty buffer
32
+ spy myfile.py # open a file
33
+ spy . # start in a directory (sidebar shows it)
34
+ ```
35
+
36
+ ## Key bindings
37
+
38
+ | Key | Action |
39
+ |-----|--------|
40
+ | `F5` / `Ctrl+R` | Run selection → enclosing loop → whole file |
41
+ | `Ctrl+S` | Save the buffer |
42
+ | `Ctrl+A` | Select all |
43
+ | `Ctrl+B` | Toggle sidebar focus |
44
+ | `Ctrl+E` | Toggle output popup |
45
+ | `Ctrl+X` | Stop running code |
46
+ | `Ctrl+Q` / `Ctrl+C` | Quit |
47
+
48
+ ## Sidebar
49
+
50
+ When the sidebar is focused (`Ctrl+B`):
51
+
52
+ - `↑` / `↓` — move selection
53
+ - `→` / `←` — expand / collapse folders
54
+ - `Enter` — open file or toggle folder
55
+ - `Ctrl+R` — refresh directory listing
56
+ - `Tab` — focus the editor
57
+
58
+ ## Requirements
59
+
60
+ - Python 3.9+
61
+ - `prompt_toolkit>=3.0`
62
+ - `pygments>=2.0`
63
+
64
+ ## License
65
+
66
+ MIT (or your choice — update this line)
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/s_py_editor/__init__.py
4
+ src/s_py_editor/__main__.py
5
+ src/s_py_editor.egg-info/PKG-INFO
6
+ src/s_py_editor.egg-info/SOURCES.txt
7
+ src/s_py_editor.egg-info/dependency_links.txt
8
+ src/s_py_editor.egg-info/entry_points.txt
9
+ src/s_py_editor.egg-info/requires.txt
10
+ src/s_py_editor.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spy = s_py_editor.__main__:main
@@ -0,0 +1,2 @@
1
+ prompt_toolkit>=3.0
2
+ pygments>=2.0
@@ -0,0 +1 @@
1
+ s_py_editor