edit-cfg-json-textual 0.0.2__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Björkholm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,524 @@
1
+ Metadata-Version: 2.4
2
+ Name: edit-cfg-json-textual
3
+ Version: 0.0.2
4
+ Summary: Library for editing config-as-json with textual.
5
+ Author: Tom Björkholm
6
+ Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/tom-bjorkholm/edit-cfg-json
9
+ Project-URL: Source code, https://github.com/tom-bjorkholm/edit-cfg-json
10
+ Project-URL: Documentation, https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/
11
+ Keywords: edit,configuration,JSON,validation,textual
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Development Status :: 3 - Alpha
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.txt
24
+ Requires-Dist: edit-cfg-json==0.0.*,>=0.0.2
25
+ Requires-Dist: textual>=8.2.8
26
+ Requires-Dist: argcomplete>=3.7.2
27
+ Requires-Dist: wizard-ui-bridge[textual]>=1.3
28
+ Dynamic: author
29
+ Dynamic: license-file
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+
33
+ # edit-cfg-json-textual
34
+
35
+ There are 3 related packages for editing a `config-as-json`
36
+ configuration:
37
+
38
+ - **[edit-cfg-json-tk](https://pypi.org/project/edit-cfg-json-tk/)** a
39
+ desktop editor based on Tkinter. It is a thin backend on top of the
40
+ core.
41
+
42
+ - **[edit-cfg-json-textual](https://pypi.org/project/edit-cfg-json-textual/)**
43
+ a terminal editor based on Textual. It is a thin backend on top of the
44
+ core.
45
+
46
+ - **[edit-cfg-json](https://pypi.org/project/edit-cfg-json/)** the user
47
+ interface agnostic core. It discovers the editable structure of a
48
+ `config_as_json.Config` object by introspection, and owns all editing,
49
+ validation and file handling. It is also the package a third party
50
+ writes a new user interface backend against. The only backend it ships
51
+ itself is a very limited non-interactive one that prints the model once
52
+ and returns, for a script, a test or a continuous integration job.
53
+
54
+ The application supplies its own `Config` object and gets a folding
55
+ editor for it, without writing any user interface code and without
56
+ describing its configuration schema a second time.
57
+
58
+ The three packages share a version number and are released together. The
59
+ first two are the editors: pick the one that matches how your application
60
+ is used, and it pulls in the core itself.
61
+
62
+ ## Project status
63
+
64
+ **Alpha. No API stability and no backward compatibility is offered while
65
+ this package is in Alpha.** That applies to the core and to both
66
+ backends. Public names may change without a major version bump.
67
+
68
+ Semantic versioning starts when the Alpha period ends. Until then, pin an
69
+ exact version if your build needs to be reproducible.
70
+
71
+ ## What this package does
72
+
73
+ `edit-cfg-json-textual` is the terminal editor. It is a thin backend on top of
74
+ `edit-cfg-json`, which it installs as a dependency together with
75
+ `textual`. All the editing, validation and file handling logic lives in
76
+ the core; this package only draws it and forwards the user's actions.
77
+
78
+ Use this backend when the application is used over ssh, in a container,
79
+ or anywhere else without a desktop.
80
+
81
+ ## Main entry points
82
+
83
+ Everything a user of this package needs is re-exported from the top-level
84
+ `edit_cfg_json_textual` package, so it can be imported directly:
85
+
86
+ ````python
87
+ from edit_cfg_json_textual import TextualEditor, edit
88
+ ````
89
+
90
+ `edit` is the short way in for an application that has already chosen
91
+ Textual. It is `edit_cfg_json.edit` with this package's backend filled in,
92
+ and it gives back the configuration object that was saved, or `None` when
93
+ nothing was:
94
+
95
+ ````python
96
+ from edit_cfg_json_textual import edit
97
+
98
+ saved = edit(config=config, in_file='my_config.json')
99
+ ````
100
+
101
+ `TextualEditor` is the Textual implementation of the `EditorBackend`
102
+ protocol of `edit-cfg-json`, for an application that builds the model
103
+ itself. It has the one method that protocol asks for:
104
+
105
+ ````python
106
+ from edit_cfg_json import EditModel, load_config
107
+ from edit_cfg_json_textual import TextualEditor
108
+
109
+ loaded = load_config(config=config, in_file='my_config.json')
110
+ model = EditModel(config=loaded.config, report=loaded.report,
111
+ out_file='my_config.json')
112
+ TextualEditor().run_editor(model)
113
+ saved = model.saved_config
114
+ ````
115
+
116
+ ## The edit-cfg-json-textual program
117
+
118
+ Installing this package also installs a program of the same name, so an
119
+ application author gets a Textual editor for their own configuration class
120
+ without writing a line of code:
121
+
122
+ ````sh
123
+ edit-cfg-json-textual --module myapp.config --class AppConfig -i /etc/myapp.json
124
+ ````
125
+
126
+ The screen it opens is the one this page describes, on the class that was
127
+ named. It is `edit_cfg_json.run_cli` with this package's backend filled in, so
128
+ the command line below is the same one that `edit-cfg-json-tk` has; what
129
+ differs is which of the two shows the configuration.
130
+
131
+ ### Telling it which class to edit
132
+
133
+ The class is told and never guessed. `--module` names a module that is
134
+ importable, `--file` names a Python file that is not, exactly one of the two is
135
+ required, and `--class` names the class in it:
136
+
137
+ ````sh
138
+ edit-cfg-json-textual --module myapp.config --class AppConfig -i /etc/myapp.json
139
+ edit-cfg-json-textual --file ./somewhere/cfg.py --class AppConfig
140
+ ````
141
+
142
+ `--module` uses the ordinary import path, so `PYTHONPATH` reaches a package
143
+ that is not installed. `--file` puts the folder of the file at the front of the
144
+ path and imports the file by its own name, so a file that imports its
145
+ neighbours works — but a file that belongs to a package and uses a relative
146
+ import cannot be loaded from a bare path at all, and is refused with a message
147
+ saying to use `--module` with `PYTHONPATH` instead.
148
+
149
+ **Importing a module runs it.** That is the same exposure as running the file
150
+ with `python`, and it is not guarded against, because a configuration class is
151
+ Python and reaching it means importing the module it is in.
152
+
153
+ ### The rest of the command line
154
+
155
+ | Option | Meaning |
156
+ | --- | --- |
157
+ | `-i`, `--input` | Configuration file to read. Without it the editor starts from the values the class declares. |
158
+ | `-o`, `--output` | Configuration file to write. Without it the input file is written, which is what an editor is normally asked to do. |
159
+ | `--policy` | What to do about a declared value the file does not hold: `strict-then-defaults`, which is the default, `strict` or `defaults`. |
160
+ | `--descriptions` | Name of an `edit_cfg_json.Descriptions` mapping beside the class, saying what its members are for. Without it the members are shown with whatever their own types say about them, which for most of them is nothing. |
161
+
162
+ A member has no docstring at runtime, so what a member is for is either in a
163
+ mapping like that or nowhere at all, which is why `--descriptions` exists: it is
164
+ the one thing an application knows that this program could not otherwise pass
165
+ on. The docstring of the configuration class needs no option, because the class
166
+ carries it.
167
+
168
+ An application that has more to say about its own configuration — the file name
169
+ extension it uses, the key combinations its own user interface has taken, and
170
+ what becomes of a file that a save writes over — says it in
171
+ `edit_cfg_json.Settings`, and gets there through `edit` rather than through this
172
+ program. Options for those are what a later version of this program adds.
173
+
174
+ ### A class this editor cannot construct on its own
175
+
176
+ Most configuration classes take the keyword arguments that `config_as_json`
177
+ documents and nothing else, and this program constructs them from the signature
178
+ it reads. A class that needs an argument of the application's own — a folder, a
179
+ connection, the list of names its own validators accept — is reached through
180
+ `--loader NAME` instead, which names an `edit_cfg_json.ConfigLoader` in the same
181
+ module or file:
182
+
183
+ ````sh
184
+ edit-cfg-json-textual --module myapp.config --loader make_config -i /etc/myapp.json
185
+ ````
186
+
187
+ Whatever the loader needs beyond the four keyword arguments of that protocol has
188
+ to be bound where the loader is written, for instance with
189
+ `functools.partial`, because a command line cannot supply an argument this
190
+ library knows nothing about. `edit_cfg_json.derived_loader` is one line for the
191
+ ordinary case:
192
+
193
+ ````python
194
+ make_config = derived_loader(partial(AppConfig, known_teams=TEAMS))
195
+ ````
196
+
197
+ At least one of `--class` and `--loader` is needed and both are allowed. A
198
+ loader may choose its class by looking at the file it is given, and `--class`
199
+ beside it is then how a script says which class it is prepared to go on with:
200
+ the run stops with its own exit code if the loader answers with another one.
201
+
202
+ ### How the run ends
203
+
204
+ The program is meant to be usable from a script, so each way of refusing has an
205
+ exit code of its own:
206
+
207
+ | Code | What it means |
208
+ | --- | --- |
209
+ | `0` | Everything the program was asked to do was done. |
210
+ | `1` | The input file cannot be opened for editing. |
211
+ | `2` | The command line itself is wrong. |
212
+ | `3` | The module that `--module` names cannot be imported. |
213
+ | `4` | The file that `--file` names cannot be read. |
214
+ | `5` | That file is not Python that can be imported. |
215
+ | `6` | That file needs the package it belongs to. |
216
+ | `7` | The module holds no such name. |
217
+ | `8` | That name is not a class based on `config_as_json.Config`. |
218
+ | `9` | The editor cannot construct that class on its own. |
219
+ | `10` | The values are not ones the application would accept. |
220
+ | `11` | The output file was asked for and was not written. |
221
+ | `12` | The values of that class cannot be written as JSON, so there is nothing to show. |
222
+ | `13` | The name that `--loader` names cannot be called at all. |
223
+ | `14` | The loader needs arguments that a command line cannot supply. |
224
+ | `15` | The loader did not construct the class that `--class` asked for. |
225
+ | `16` | The name that `--descriptions` names is no mapping of any kind. |
226
+
227
+ The numbers are `edit_cfg_json.ExitCode`, so a program that runs this one can
228
+ name them instead of writing them out.
229
+
230
+ Codes `10` and `11` are never answered by this program. They belong to a run
231
+ whose backend prints once and returns, which is the
232
+ `python3 -m edit_cfg_json.dump` utility of the core package.
233
+ A program that gave the user a session ends with success when the user closes
234
+ it, whatever is left in the fields, because closing an editor is not a failure.
235
+
236
+ ### If the script folder is not on the path
237
+
238
+ This program is also reachable through the package it belongs to, which needs
239
+ nothing to be on `PATH`:
240
+
241
+ ````sh
242
+ python3 -m edit_cfg_json_textual --module myapp.config --class AppConfig
243
+ ````
244
+
245
+ ### Completing the command line
246
+
247
+ The program completes its own options and file names with
248
+ [argcomplete](https://pypi.org/project/argcomplete), which is installed with
249
+ it. Register it once for your shell:
250
+
251
+ ````sh
252
+ eval "$(register-python-argcomplete edit-cfg-json-textual)"
253
+ ````
254
+
255
+ ## What the screen shows
256
+
257
+ The screen holds a header, then what the configuration class says about itself,
258
+ what reading the input file did, and one row per node of the configuration.
259
+ Below those, and not scrolling with them, are the validation verdict, the saving
260
+ line and the footer of keys. The title is marked while the model holds a change
261
+ worth saving.
262
+
263
+ Every change of a field goes straight into the model. The keys are the ones the
264
+ application chose in the `actions` of its `edit_cfg_json.Settings`, and with an
265
+ application that chose nothing they are the defaults of
266
+ `edit_cfg_json.ActionSettings`:
267
+
268
+ | Key | What it does |
269
+ | --- | --- |
270
+ | `ctrl+r`, or `f5` | Validate |
271
+ | `ctrl+s` | Save |
272
+ | `ctrl+shift+s` or `f12` | Save as |
273
+ | `f1`, or `ctrl+g` | Explain, or Hide explanation |
274
+ | `f2`, or `ctrl+t` | Fold all, or Unfold all |
275
+ | `ctrl+q` | Quit |
276
+
277
+ ### One row per node
278
+
279
+ A member that holds a list, a dict or a nested `config_as_json.Config` object
280
+ is not one field. It is a row of its own with the rows of what it holds
281
+ indented below it, a field at every value, and no field on the row of the
282
+ container itself — which says how many things it holds, or which class the
283
+ object at it is, where a value would be.
284
+
285
+ A container has a control at the left of its row, `-` while it is open and `+`
286
+ while it is folded, and pressing it hides or shows everything inside it. The
287
+ fold action does the same to all of them at once, and it is named for what the
288
+ next press will do — "Fold all" while anything is open, "Unfold all" once
289
+ nothing is — in the footer and in the command palette alike. A configuration
290
+ with nothing to fold is offered neither the action nor the column that the
291
+ controls sit in, so the values keep that width.
292
+
293
+ A nested configuration object shows its own docstring below its row and its own
294
+ members as the rows under that, in the order *its* class declares them. Folding
295
+ it leaves the first paragraph of that docstring, because an object showing less
296
+ of itself says less about itself.
297
+
298
+ ### What one nested object is on its own
299
+
300
+ Beside the class on the row of a nested object is what that object is when it is
301
+ asked about itself: *valid on its own* or *refused on its own*. A list or a dict
302
+ of such objects says what the objects in it amount to — *valid inside* or
303
+ *refused inside* — because its row is the only one that folding leaves on the
304
+ screen.
305
+
306
+ Folding a node asks every object at or inside it, and so does opening one, so
307
+ the badge appears as soon as a container is folded out of the way. A member that
308
+ one of those objects refused says why below itself, exactly as the verdict of
309
+ the whole configuration does; what an object refused about no member of itself
310
+ is said at the object.
311
+
312
+ The words that qualify the badge are the whole point. A rule of the class above
313
+ may relate two objects across the boundary between them, and then every object
314
+ is valid on its own while the configuration cannot be written. The verdict line
315
+ below the rows is the only thing that answers whether the file can be saved.
316
+
317
+ ### Changing how many things a member holds
318
+
319
+ At the end of the line of a node are the controls for its elements: `Add`,
320
+ `Del`, `Up` and `Down`, and only the ones that node really offers. They sit at
321
+ the end rather than in a column of their own, so a node that offers none of them
322
+ costs the values no width at all, which is what makes four of them affordable.
323
+
324
+ `Add` copies: a list or a dict whose class declares that its elements are
325
+ configuration objects gets one object of that class holding the values it
326
+ declares, and any other list gets a copy of the element the class declares for
327
+ it, or of the first element it holds now. Adding an entry to a dict opens a
328
+ small screen that asks for the key, because nothing but the person configuring
329
+ the application knows what a new entry is called; a key the dict already holds
330
+ is asked about again rather than allowed to take the place of what is there.
331
+
332
+ A container that cannot be given an element gets no `Add` at all, and says why
333
+ below itself instead — an ordinary dict member, for instance, because
334
+ `config_as_json` matches such a member against the keys its class declares, so
335
+ a dict that gained one would be refused by the configuration class itself. That
336
+ line is explanation rather than something to act on, so it is muted and the
337
+ explain action covers it.
338
+
339
+ ### Validating, saving and quitting
340
+
341
+ Validating runs the validation of the application's own configuration class
342
+ and shows what that class would say about the values that are in the fields.
343
+ What it said about one node is shown **below that node**, and the line
344
+ below the rows names the nodes it was about, by the whole path to each of them,
345
+ so a configuration too tall for the terminal does not leave the user hunting for
346
+ the field. What the class said that is about no single node — a
347
+ whole-configuration rule, a key that does not match — stays in that line,
348
+ because there is no field it belongs to. Every refused node is marked at once,
349
+ and not only the first one, because the editor walks the validation plan itself
350
+ rather than stopping where `Config.validate()` stops.
351
+
352
+ A pass is not read only: a validator returns the value that is stored back
353
+ into the member, so the fields are written back from the model afterwards, and
354
+ a member that a validator rewrote says so beside its field. A pass can also
355
+ change how many rows there are — a validator that sorts a list and removes its
356
+ duplicates removes one — and the screen then builds its rows again rather than
357
+ writing into a widget for a value that is no longer there.
358
+
359
+ **Leaving a field** asks a smaller question of that one member: whether what
360
+ was typed into it means a value of that member at all. It is the question a
361
+ `parse_converters()` entry answers, an enum being the case that arises in
362
+ practice, and it is asked when the field loses the focus rather than on every
363
+ key, because a name that is being typed is no name of a member for most of
364
+ the time it takes to type it.
365
+
366
+ Saving writes the output file, and refuses to write values the application
367
+ would not accept: the diagnostics then say what is wrong with them and the
368
+ file on disk is left exactly as it was. Saving runs the same pass as
369
+ validating does, so it can rewrite a value as well, and the fields show what
370
+ really reached the file. What was written is no longer waiting to be written,
371
+ so the title loses its mark and the editor stays open.
372
+
373
+ Save as asks for the file in a small screen of its own, where `enter` writes
374
+ it and the `cancel` key, `escape` unless the application moved it, leaves the
375
+ question unanswered. The screen names that key itself, so it cannot tell the
376
+ user to press one that does nothing. `ctrl+s` asks the same question when the
377
+ session has no file to write yet, which is what every editor does. The
378
+ question starts at the file that would be written now, so saving a copy
379
+ beside the original is a matter of changing a few characters.
380
+
381
+ **A save that would write over a file this session has not written asks
382
+ first**, on a modal screen whose focus is on the answer that leaves the file
383
+ alone. The previous content is then kept under the name the application chose,
384
+ and the saving line says where it went. Both the question and the name are the
385
+ core's, so this backend and the Tkinter one cannot treat the user's old
386
+ configuration differently.
387
+
388
+ Quitting writes nothing of its own. It is the "cancel" of the editor; saving
389
+ leaves the editor open, and what has been saved has been saved.
390
+
391
+ **Quitting an editor that holds something unsaved asks whether the changes may
392
+ be dropped**, on a modal screen whose focus is on the answer that keeps them, so
393
+ that a user who presses `enter` without reading keeps what they have. Quitting
394
+ again after a Save asks nothing, because a save leaves nothing to lose.
395
+
396
+ Explaining shows or hides what the application says about these values: the
397
+ whole docstring of the configuration class above the rows, the docstring of
398
+ each nested object, the description of each described member below its own
399
+ field, what kind of value each member holds, and why a container cannot be
400
+ given an element. The editor opens with them shown, and what is left when they
401
+ are hidden is the first paragraph of the class docstring, because one line for
402
+ the whole configuration is worth keeping. A member the application described
403
+ gets a line and one it said nothing about gets none, rather than an empty one.
404
+ Which of the two states the editor is in belongs to the model, so this backend
405
+ and the Tk one cannot disagree about it.
406
+
407
+ The action is named for what the next press of it will do: it is "Explain"
408
+ while the explanations are hidden and "Hide explanation" while they are shown,
409
+ in the footer and in the command palette alike. "Explain" beside explanations
410
+ that are already there would be offering something that has been done. The Tk
411
+ backend answers the same question with a tick-box, which a footer cannot be.
412
+
413
+ What reading the input file did is shown above the rows, when it did
414
+ anything, because it is what explains the marks below it: a member that the
415
+ file did not hold says so beside its field, and so does one whose value the
416
+ reading of the file put there or altered — with the older key it was read from,
417
+ where the class recorded one. Both the message and the marks are read from the
418
+ model, so the two backends cannot tell the user two different things about one
419
+ file.
420
+
421
+ ## Scrolling, and the colours
422
+
423
+ The docstring, the load message and the member rows are in the part of the
424
+ screen that scrolls, and the verdict, the saving line and the footer are below
425
+ it and stay where they are: they are what a user reaches for after editing
426
+ rather than something to scroll to. A configuration of any size therefore fits
427
+ a terminal of any size, and a container that would add more rows than the
428
+ editor opens at is folded to begin with, so that a long list does not fill the
429
+ screen before the user has seen the members below it.
430
+
431
+ Each kind of text has a colour, so that the explanations do not read as loudly
432
+ as the values and a refused validation does not read like an accepted one.
433
+ Which kind each piece of text is comes from `edit_cfg_json.Emphasis` and is
434
+ therefore the same here as in the Tkinter backend; what the colours are is
435
+ this package's own, and they are the colours of the terminal's theme rather
436
+ than colours named here, so the editor follows the terminal into its light or
437
+ its dark mode.
438
+
439
+ ## About the keys
440
+
441
+ None of the defaults is a plain letter, because an unmodified letter belongs
442
+ to whichever field has the focus: a user who types it expects to see it
443
+ appear in the field. Neither `ctrl+s` nor `ctrl+q` is taken for flow control,
444
+ because Textual's driver clears `IXON` and `IXOFF` when it puts the terminal
445
+ into raw mode. `ctrl+f` and `f3` are taken by no default of this editor,
446
+ because a search over a configuration too big for the terminal is something
447
+ this editor is likely to be asked for, and no version number protects a key a
448
+ user has learnt.
449
+
450
+ `f5` validates as well, and is left out of the footer so that one action is
451
+ not named twice there; a function key is the one of the two that a keyboard
452
+ or a terminal is most likely not to deliver, which is why the footer names
453
+ `ctrl+r` instead. The same holds for `ctrl+t` beside `f2`.
454
+
455
+ `ctrl+shift+s` needs a word of warning. A legacy terminal encodes a control
456
+ letter as a single byte with nowhere to put the shift, so on such a terminal
457
+ this key arrives as `ctrl+s` and saves instead of asking where to save.
458
+ Textual asks the terminal for the Kitty keyboard protocol at startup, and a
459
+ terminal that speaks it reports the two keys apart. That is why **Validate,
460
+ Save, Save as, Explain and the fold action are also in the command palette**,
461
+ which `ctrl+p` opens:
462
+ every terminal can reach the palette, because it is a letter typed into a
463
+ field and not a key combination at all. The palette's own **Keys** entry
464
+ lists every binding of the editor, including the ones the footer has no room
465
+ for.
466
+
467
+ An application that needs one of these combinations for itself moves it, or
468
+ empties it, in its own `ActionSettings`. An action with no key at all keeps
469
+ its command palette entry, so nothing becomes unreachable. The bindings are
470
+ made when the application starts, which is the one thing a later answer from
471
+ a settings callable cannot change; the two actions that are named for what
472
+ they will do next are the exception, because renaming one is making its
473
+ bindings afresh.
474
+
475
+ ## Installing edit-cfg-json-textual
476
+
477
+ ### On macOS and Linux
478
+
479
+ To install edit-cfg-json-textual on macOS and Linux, run the following command:
480
+
481
+ ````sh
482
+ pip3 install --upgrade edit-cfg-json-textual
483
+ ````
484
+
485
+ ### On Microsoft Windows
486
+
487
+ To install edit-cfg-json-textual on Microsoft Windows, run the following command:
488
+
489
+ ````sh
490
+ pip install --upgrade edit-cfg-json-textual
491
+ ````
492
+
493
+ ## Documentation
494
+
495
+ - Design and decisions:
496
+ [doc/design.md](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/design.md)
497
+
498
+ - Public API:
499
+ [edit-cfg-json](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/edit-cfg-json_api.md),
500
+ [edit-cfg-json-tk](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/edit-cfg-json-tk_api.md),
501
+ [edit-cfg-json-textual](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/edit-cfg-json-textual_api.md)
502
+
503
+ - Protected API:
504
+ [edit-cfg-json](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/edit-cfg-json_protected_api.md),
505
+ [edit-cfg-json-tk](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/edit-cfg-json-tk_protected_api.md),
506
+ [edit-cfg-json-textual](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/doc/edit-cfg-json-textual_protected_api.md)
507
+
508
+ - Worked examples:
509
+ [examples/src/example](https://github.com/tom-bjorkholm/edit-cfg-json/blob/master/examples/src/example)
510
+
511
+ ## License
512
+
513
+ edit-cfg-json-textual is released under the MIT License. See the `LICENSE.txt`
514
+ file included in the distribution.
515
+
516
+ ## Test summary
517
+
518
+ - Test result: 1465 passed, 3 deselected in 38s
519
+ - No flake8 warnings.
520
+ - No mypy errors found.
521
+ - No pylint warnings.
522
+ - No python layout warnings.
523
+ - Built version(s): 0.0.2
524
+ - Build and test using Python 3.14.6