jvim 0.1.0__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.
- jets/__init__.py +6 -0
- jets/__main__.py +5 -0
- jets/app.py +341 -0
- jets/app.tcss +73 -0
- jets/data/help.json +49 -0
- jets/data/sample.json +25 -0
- jets/editor.py +2002 -0
- jets/py.typed +0 -0
- jvim-0.1.0.dist-info/METADATA +193 -0
- jvim-0.1.0.dist-info/RECORD +12 -0
- jvim-0.1.0.dist-info/WHEEL +4 -0
- jvim-0.1.0.dist-info/entry_points.txt +2 -0
jets/__init__.py
ADDED
jets/__main__.py
ADDED
jets/app.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Demo application for the JSON editor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from textual.app import App, ComposeResult
|
|
11
|
+
from textual.containers import Horizontal, Vertical
|
|
12
|
+
from textual.widgets import Button, Header, Static
|
|
13
|
+
|
|
14
|
+
from .editor import JsonEditor
|
|
15
|
+
|
|
16
|
+
# Data directory path
|
|
17
|
+
_DATA_DIR = Path(__file__).parent / "data"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_data(filename: str) -> str:
|
|
21
|
+
"""Load content from data directory."""
|
|
22
|
+
return (_DATA_DIR / filename).read_text(encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JsonEditorApp(App):
|
|
26
|
+
"""TUI app that wraps the JsonEditor widget."""
|
|
27
|
+
|
|
28
|
+
CSS_PATH = "app.tcss"
|
|
29
|
+
TITLE = "JSON Editor"
|
|
30
|
+
BINDINGS = []
|
|
31
|
+
ENABLE_COMMAND_PALETTE = False
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
file_path: str = "",
|
|
36
|
+
initial_content: str = "",
|
|
37
|
+
read_only: bool = False,
|
|
38
|
+
jsonl: bool = False,
|
|
39
|
+
**kwargs,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
self.file_path = file_path
|
|
43
|
+
self.initial_content = initial_content
|
|
44
|
+
self.read_only = read_only
|
|
45
|
+
self.jsonl = jsonl
|
|
46
|
+
# Embedded edit state - stack of (row, col_start, col_end, parent_content, original_content)
|
|
47
|
+
self._ej_stack: list[tuple[int, int, int, str, str]] = []
|
|
48
|
+
self._main_was_read_only: bool = False
|
|
49
|
+
|
|
50
|
+
def compose(self) -> ComposeResult:
|
|
51
|
+
yield Header(show_clock=True)
|
|
52
|
+
yield JsonEditor(
|
|
53
|
+
self.initial_content,
|
|
54
|
+
read_only=self.read_only,
|
|
55
|
+
jsonl=self.jsonl,
|
|
56
|
+
id="editor",
|
|
57
|
+
)
|
|
58
|
+
with Vertical(id="help-panel"):
|
|
59
|
+
with Horizontal(id="help-header"):
|
|
60
|
+
yield Static("[b]Help[/b]", id="help-title")
|
|
61
|
+
yield Button("\u2715", id="help-close", variant="error")
|
|
62
|
+
yield JsonEditor(_load_data("help.json"), read_only=True, id="help-editor")
|
|
63
|
+
with Vertical(id="ej-panel"):
|
|
64
|
+
with Horizontal(id="ej-header"):
|
|
65
|
+
yield Static("[b]Edit Embedded JSON[/b]", id="ej-title")
|
|
66
|
+
yield Button("\u2715", id="ej-close", variant="error")
|
|
67
|
+
yield JsonEditor("", id="ej-editor")
|
|
68
|
+
|
|
69
|
+
def on_mount(self) -> None:
|
|
70
|
+
self._update_title()
|
|
71
|
+
self.query_one("#editor").focus()
|
|
72
|
+
|
|
73
|
+
def _update_title(self) -> None:
|
|
74
|
+
ro = " [RO]" if self.read_only else ""
|
|
75
|
+
if self.file_path:
|
|
76
|
+
self.sub_title = self.file_path + ro
|
|
77
|
+
else:
|
|
78
|
+
self.sub_title = "[new]" + ro
|
|
79
|
+
|
|
80
|
+
# -- Event handlers ----------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def _is_help_editor_focused(self) -> bool:
|
|
83
|
+
focused = self.focused
|
|
84
|
+
return focused is not None and focused.id == "help-editor"
|
|
85
|
+
|
|
86
|
+
def _is_ej_editor_focused(self) -> bool:
|
|
87
|
+
focused = self.focused
|
|
88
|
+
return focused is not None and focused.id == "ej-editor"
|
|
89
|
+
|
|
90
|
+
def on_key(self) -> None:
|
|
91
|
+
"""Update ej title on key press to reflect modified state."""
|
|
92
|
+
if self._is_ej_editor_focused() and self._ej_stack:
|
|
93
|
+
self._update_ej_title()
|
|
94
|
+
|
|
95
|
+
def on_json_editor_quit(self, event: JsonEditor.Quit) -> None:
|
|
96
|
+
if self._is_help_editor_focused():
|
|
97
|
+
self.query_one("#help-panel").remove_class("visible")
|
|
98
|
+
self.query_one("#editor").focus()
|
|
99
|
+
elif self._is_ej_editor_focused():
|
|
100
|
+
if self._ej_has_unsaved_changes():
|
|
101
|
+
self.notify("Unsaved changes! Use :w to save or :q! to discard", severity="warning")
|
|
102
|
+
else:
|
|
103
|
+
self._close_ej_panel()
|
|
104
|
+
else:
|
|
105
|
+
self.exit()
|
|
106
|
+
|
|
107
|
+
def on_json_editor_force_quit(self, event: JsonEditor.ForceQuit) -> None:
|
|
108
|
+
"""Handle :q! to discard changes."""
|
|
109
|
+
if self._is_help_editor_focused():
|
|
110
|
+
self.query_one("#help-panel").remove_class("visible")
|
|
111
|
+
self.query_one("#editor").focus()
|
|
112
|
+
elif self._is_ej_editor_focused():
|
|
113
|
+
self._close_ej_panel()
|
|
114
|
+
else:
|
|
115
|
+
self.exit()
|
|
116
|
+
|
|
117
|
+
def on_json_editor_json_validated(
|
|
118
|
+
self, event: JsonEditor.JsonValidated
|
|
119
|
+
) -> None:
|
|
120
|
+
if event.valid:
|
|
121
|
+
self.notify("JSON is valid", severity="information")
|
|
122
|
+
else:
|
|
123
|
+
self.notify(f"Invalid JSON: {event.error}", severity="error", timeout=6)
|
|
124
|
+
|
|
125
|
+
def on_json_editor_file_save_requested(
|
|
126
|
+
self, event: JsonEditor.FileSaveRequested
|
|
127
|
+
) -> None:
|
|
128
|
+
# Help editor is read-only, so this shouldn't happen, but just in case
|
|
129
|
+
if self._is_help_editor_focused():
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# EJ editor: update parent and close/pop panel
|
|
133
|
+
if self._is_ej_editor_focused() and self._ej_stack:
|
|
134
|
+
row, col_start, col_end, prev_content, _ = self._ej_stack.pop()
|
|
135
|
+
# Minify the JSON to a single line
|
|
136
|
+
try:
|
|
137
|
+
parsed = json.loads(event.content)
|
|
138
|
+
minified = json.dumps(parsed, ensure_ascii=False)
|
|
139
|
+
except json.JSONDecodeError:
|
|
140
|
+
self.notify("Invalid JSON", severity="error")
|
|
141
|
+
# Restore the popped entry with current content as new original
|
|
142
|
+
self._ej_stack.append((row, col_start, col_end, prev_content, event.content))
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if self._ej_stack:
|
|
146
|
+
# Update previous ej content and show it
|
|
147
|
+
ej_editor = self.query_one("#ej-editor", JsonEditor)
|
|
148
|
+
lines = prev_content.split("\n")
|
|
149
|
+
line = lines[row]
|
|
150
|
+
escaped = json.dumps(minified, ensure_ascii=False)
|
|
151
|
+
lines[row] = line[:col_start] + escaped + line[col_end:]
|
|
152
|
+
new_content = "\n".join(lines)
|
|
153
|
+
ej_editor.set_content(new_content)
|
|
154
|
+
# Keep original content unchanged so modified indicator stays
|
|
155
|
+
self._update_ej_title()
|
|
156
|
+
self.notify("Embedded JSON updated", severity="information")
|
|
157
|
+
else:
|
|
158
|
+
# Update main editor and restore its read-only state
|
|
159
|
+
main_editor = self.query_one("#editor", JsonEditor)
|
|
160
|
+
main_editor.read_only = self._main_was_read_only
|
|
161
|
+
main_editor.update_embedded_string(row, col_start, col_end, minified)
|
|
162
|
+
self.notify("Embedded JSON updated", severity="information")
|
|
163
|
+
if event.quit_after:
|
|
164
|
+
self.query_one("#ej-panel").remove_class("visible")
|
|
165
|
+
main_editor.focus()
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
target = event.file_path or self.file_path
|
|
169
|
+
if not target:
|
|
170
|
+
self.notify("No file name — use :w <file>", severity="warning")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
path = Path(target)
|
|
175
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
path.write_text(event.content, encoding="utf-8")
|
|
177
|
+
self.file_path = str(path)
|
|
178
|
+
self._update_title()
|
|
179
|
+
self.notify(f"Saved: {self.file_path}", severity="information")
|
|
180
|
+
if event.quit_after:
|
|
181
|
+
self.exit()
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
self.notify(f"Save failed: {exc}", severity="error", timeout=6)
|
|
184
|
+
|
|
185
|
+
def on_json_editor_file_open_requested(
|
|
186
|
+
self, event: JsonEditor.FileOpenRequested
|
|
187
|
+
) -> None:
|
|
188
|
+
target = event.file_path
|
|
189
|
+
try:
|
|
190
|
+
content = Path(target).read_text(encoding="utf-8")
|
|
191
|
+
except FileNotFoundError:
|
|
192
|
+
self.notify(f"File not found: {target}", severity="error", timeout=6)
|
|
193
|
+
return
|
|
194
|
+
except OSError as exc:
|
|
195
|
+
self.notify(f"Cannot open: {exc}", severity="error", timeout=6)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
editor = self.query_one("#editor", JsonEditor)
|
|
199
|
+
editor.set_content(content)
|
|
200
|
+
self.jsonl = target.lower().endswith(".jsonl")
|
|
201
|
+
editor.jsonl = self.jsonl
|
|
202
|
+
self.file_path = target
|
|
203
|
+
self._update_title()
|
|
204
|
+
self.notify(f"Opened: {target}", severity="information")
|
|
205
|
+
|
|
206
|
+
def on_json_editor_help_toggle_requested(self) -> None:
|
|
207
|
+
help_panel = self.query_one("#help-panel")
|
|
208
|
+
help_panel.toggle_class("visible")
|
|
209
|
+
if help_panel.has_class("visible"):
|
|
210
|
+
self.query_one("#help-editor").focus()
|
|
211
|
+
else:
|
|
212
|
+
self.query_one("#editor").focus()
|
|
213
|
+
|
|
214
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
215
|
+
if event.button.id == "help-close":
|
|
216
|
+
self.query_one("#help-panel").remove_class("visible")
|
|
217
|
+
self.query_one("#editor").focus()
|
|
218
|
+
elif event.button.id == "ej-close":
|
|
219
|
+
self._close_ej_panel()
|
|
220
|
+
|
|
221
|
+
def _ej_has_unsaved_changes(self) -> bool:
|
|
222
|
+
"""Check if current ej content differs from original."""
|
|
223
|
+
if not self._ej_stack:
|
|
224
|
+
return False
|
|
225
|
+
ej_editor = self.query_one("#ej-editor", JsonEditor)
|
|
226
|
+
current = ej_editor.get_content()
|
|
227
|
+
_, _, _, _, original = self._ej_stack[-1]
|
|
228
|
+
return current != original
|
|
229
|
+
|
|
230
|
+
def _update_ej_title(self) -> None:
|
|
231
|
+
"""Update ej panel title with current nesting level and modified indicator."""
|
|
232
|
+
level = len(self._ej_stack)
|
|
233
|
+
title = self.query_one("#ej-title", Static)
|
|
234
|
+
modified = " [+]" if self._ej_has_unsaved_changes() else ""
|
|
235
|
+
title.update(f"[b]Edit Embedded JSON[/b] [dim](level {level}){modified}[/dim]")
|
|
236
|
+
|
|
237
|
+
def _close_ej_panel(self) -> None:
|
|
238
|
+
"""Close or pop one level of ej editing."""
|
|
239
|
+
if not self._ej_stack:
|
|
240
|
+
self.query_one("#ej-panel").remove_class("visible")
|
|
241
|
+
main_editor = self.query_one("#editor", JsonEditor)
|
|
242
|
+
main_editor.read_only = self._main_was_read_only
|
|
243
|
+
main_editor.focus()
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# Pop current level and get content to restore
|
|
247
|
+
_, _, _, restore_content, _ = self._ej_stack.pop()
|
|
248
|
+
|
|
249
|
+
if self._ej_stack:
|
|
250
|
+
# Restore previous level content
|
|
251
|
+
ej_editor = self.query_one("#ej-editor", JsonEditor)
|
|
252
|
+
ej_editor.set_content(restore_content)
|
|
253
|
+
self._update_ej_title()
|
|
254
|
+
else:
|
|
255
|
+
# No more levels, close panel and restore main editor state
|
|
256
|
+
self.query_one("#ej-panel").remove_class("visible")
|
|
257
|
+
main_editor = self.query_one("#editor", JsonEditor)
|
|
258
|
+
main_editor.read_only = self._main_was_read_only
|
|
259
|
+
main_editor.focus()
|
|
260
|
+
|
|
261
|
+
def on_json_editor_embedded_edit_requested(
|
|
262
|
+
self, event: JsonEditor.EmbeddedEditRequested
|
|
263
|
+
) -> None:
|
|
264
|
+
ej_editor = self.query_one("#ej-editor", JsonEditor)
|
|
265
|
+
|
|
266
|
+
if self._is_ej_editor_focused():
|
|
267
|
+
# Nested ej from ej panel - push to stack
|
|
268
|
+
current_content = ej_editor.get_content()
|
|
269
|
+
self._ej_stack.append((
|
|
270
|
+
event.source_row,
|
|
271
|
+
event.source_col_start,
|
|
272
|
+
event.source_col_end,
|
|
273
|
+
current_content,
|
|
274
|
+
event.content, # original content for change detection
|
|
275
|
+
))
|
|
276
|
+
else:
|
|
277
|
+
# From main editor - reset stack to level 1
|
|
278
|
+
main_editor = self.query_one("#editor", JsonEditor)
|
|
279
|
+
self._main_was_read_only = main_editor.read_only
|
|
280
|
+
main_editor.read_only = True
|
|
281
|
+
self._ej_stack = [(
|
|
282
|
+
event.source_row,
|
|
283
|
+
event.source_col_start,
|
|
284
|
+
event.source_col_end,
|
|
285
|
+
"", # No previous ej content
|
|
286
|
+
event.content, # original content for change detection
|
|
287
|
+
)]
|
|
288
|
+
|
|
289
|
+
# Set content and show panel
|
|
290
|
+
ej_editor.set_content(event.content)
|
|
291
|
+
self._update_ej_title()
|
|
292
|
+
self.query_one("#ej-panel").add_class("visible")
|
|
293
|
+
ej_editor.focus()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main() -> None:
|
|
297
|
+
parser = argparse.ArgumentParser(
|
|
298
|
+
prog="vj",
|
|
299
|
+
description="JSON Editor in Textual",
|
|
300
|
+
)
|
|
301
|
+
parser.add_argument(
|
|
302
|
+
"file",
|
|
303
|
+
nargs="?",
|
|
304
|
+
default="",
|
|
305
|
+
help="JSON file to open",
|
|
306
|
+
)
|
|
307
|
+
parser.add_argument(
|
|
308
|
+
"-R", "--read-only",
|
|
309
|
+
action="store_true",
|
|
310
|
+
default=False,
|
|
311
|
+
help="open in read-only mode",
|
|
312
|
+
)
|
|
313
|
+
args = parser.parse_args()
|
|
314
|
+
|
|
315
|
+
file_path: str = args.file
|
|
316
|
+
initial_content: str = _load_data("sample.json")
|
|
317
|
+
jsonl: bool = file_path.lower().endswith(".jsonl") if file_path else False
|
|
318
|
+
|
|
319
|
+
if file_path:
|
|
320
|
+
path = Path(file_path)
|
|
321
|
+
try:
|
|
322
|
+
if path.exists():
|
|
323
|
+
initial_content = path.read_text(encoding="utf-8")
|
|
324
|
+
else:
|
|
325
|
+
# New file — start with empty object / empty line
|
|
326
|
+
initial_content = "" if jsonl else "{}"
|
|
327
|
+
except PermissionError as exc:
|
|
328
|
+
print(f"vj: {exc}", file=sys.stderr)
|
|
329
|
+
sys.exit(1)
|
|
330
|
+
|
|
331
|
+
app = JsonEditorApp(
|
|
332
|
+
file_path=file_path,
|
|
333
|
+
initial_content=initial_content,
|
|
334
|
+
read_only=args.read_only,
|
|
335
|
+
jsonl=jsonl,
|
|
336
|
+
)
|
|
337
|
+
app.run()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
main()
|
jets/app.tcss
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
layout: vertical;
|
|
3
|
+
}
|
|
4
|
+
#editor {
|
|
5
|
+
width: 1fr;
|
|
6
|
+
height: 1fr;
|
|
7
|
+
}
|
|
8
|
+
#help-panel {
|
|
9
|
+
height: 20%;
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
#help-panel.visible {
|
|
13
|
+
display: block;
|
|
14
|
+
}
|
|
15
|
+
#help-header {
|
|
16
|
+
height: auto;
|
|
17
|
+
padding: 0 1;
|
|
18
|
+
background: $accent;
|
|
19
|
+
}
|
|
20
|
+
#help-title {
|
|
21
|
+
width: 1fr;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
#help-close {
|
|
25
|
+
min-width: 3;
|
|
26
|
+
width: 3;
|
|
27
|
+
height: 1;
|
|
28
|
+
padding: 0;
|
|
29
|
+
margin: 0;
|
|
30
|
+
border: none;
|
|
31
|
+
background: transparent;
|
|
32
|
+
color: $text;
|
|
33
|
+
}
|
|
34
|
+
#help-close:hover {
|
|
35
|
+
background: $error;
|
|
36
|
+
color: $text;
|
|
37
|
+
}
|
|
38
|
+
#help-editor {
|
|
39
|
+
height: 1fr;
|
|
40
|
+
}
|
|
41
|
+
#ej-panel {
|
|
42
|
+
height: 50%;
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
#ej-panel.visible {
|
|
46
|
+
display: block;
|
|
47
|
+
}
|
|
48
|
+
#ej-header {
|
|
49
|
+
height: auto;
|
|
50
|
+
padding: 0 1;
|
|
51
|
+
background: $warning;
|
|
52
|
+
}
|
|
53
|
+
#ej-title {
|
|
54
|
+
width: 1fr;
|
|
55
|
+
padding: 0;
|
|
56
|
+
}
|
|
57
|
+
#ej-close {
|
|
58
|
+
min-width: 3;
|
|
59
|
+
width: 3;
|
|
60
|
+
height: 1;
|
|
61
|
+
padding: 0;
|
|
62
|
+
margin: 0;
|
|
63
|
+
border: none;
|
|
64
|
+
background: transparent;
|
|
65
|
+
color: $text;
|
|
66
|
+
}
|
|
67
|
+
#ej-close:hover {
|
|
68
|
+
background: $error;
|
|
69
|
+
color: $text;
|
|
70
|
+
}
|
|
71
|
+
#ej-editor {
|
|
72
|
+
height: 1fr;
|
|
73
|
+
}
|
jets/data/help.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Movement": {
|
|
3
|
+
"h j k l": "left/down/up/right",
|
|
4
|
+
"w b": "word forward/backward",
|
|
5
|
+
"0 $ ^": "line start/end/first char",
|
|
6
|
+
"gg G": "file start/end",
|
|
7
|
+
"%": "jump to matching bracket",
|
|
8
|
+
"PgUp PgDn": "page up/down",
|
|
9
|
+
"Ctrl+d/u": "half page down/up"
|
|
10
|
+
},
|
|
11
|
+
"Search": {
|
|
12
|
+
"/": "search forward",
|
|
13
|
+
"?": "search backward",
|
|
14
|
+
"n": "next match",
|
|
15
|
+
"N": "previous match",
|
|
16
|
+
"Up/Down": "search history",
|
|
17
|
+
"\\c \\C": "case insensitive/sensitive",
|
|
18
|
+
"/$. /$[": "JSONPath search (auto-detect)",
|
|
19
|
+
"\\j": "JSONPath suffix for other patterns"
|
|
20
|
+
},
|
|
21
|
+
"Insert Mode": {
|
|
22
|
+
"i I": "insert at cursor/line start",
|
|
23
|
+
"a A": "append after cursor/line end",
|
|
24
|
+
"o O": "open line below/above"
|
|
25
|
+
},
|
|
26
|
+
"Editing": {
|
|
27
|
+
"x": "delete char",
|
|
28
|
+
"dd": "delete line",
|
|
29
|
+
"dw d$": "delete word/to end",
|
|
30
|
+
"cw cc": "change word/line",
|
|
31
|
+
"r{c}": "replace char",
|
|
32
|
+
"J": "join lines",
|
|
33
|
+
"yy p P": "yank/paste after/before",
|
|
34
|
+
"u": "undo",
|
|
35
|
+
"Ctrl+r": "redo",
|
|
36
|
+
".": "repeat last edit",
|
|
37
|
+
"ej": "edit embedded JSON string"
|
|
38
|
+
},
|
|
39
|
+
"Commands": {
|
|
40
|
+
":w": "save",
|
|
41
|
+
":w {file}": "save as",
|
|
42
|
+
":e {file}": "open file",
|
|
43
|
+
":fmt": "format JSON",
|
|
44
|
+
":q": "quit (confirm if changed)",
|
|
45
|
+
":q!": "quit (discard changes)",
|
|
46
|
+
":wq": "save and quit",
|
|
47
|
+
":help": "toggle this help"
|
|
48
|
+
}
|
|
49
|
+
}
|
jets/data/sample.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "json-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A modal JSON editor built with Textual",
|
|
5
|
+
"features": [
|
|
6
|
+
"normal mode",
|
|
7
|
+
"insert mode",
|
|
8
|
+
"command mode",
|
|
9
|
+
"syntax highlighting",
|
|
10
|
+
"json validation",
|
|
11
|
+
"bracket matching"
|
|
12
|
+
],
|
|
13
|
+
"config": {
|
|
14
|
+
"theme": "dark",
|
|
15
|
+
"indent_size": 4,
|
|
16
|
+
"auto_format": true,
|
|
17
|
+
"max_undo": 200,
|
|
18
|
+
"nested": {
|
|
19
|
+
"deep": {
|
|
20
|
+
"value": null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scores": [100, 200, 300]
|
|
25
|
+
}
|