glyph-cli 0.2.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.
glyph_cli/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """glyph: the dictionary and toolkit for the grid script of Ross 128 b."""
2
+
3
+ from .script import Entry, GlyphError, Part, Vocabulary, Word
4
+
5
+ __version__ = "0.2.0"
6
+
7
+ __all__ = ["Entry", "GlyphError", "Part", "Vocabulary", "Word", "__version__"]
glyph_cli/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
glyph_cli/cli.py ADDED
@@ -0,0 +1,269 @@
1
+ """The ``glyph`` command line."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+
12
+ from . import __version__
13
+ from .drawing import decode, draw_words, render, render_svg
14
+ from .export import vocabulary_markdown
15
+ from .graph import format_graph, read_graph, to_dot
16
+ from .page import check_symbol, format_page, parse_page
17
+ from .script import NUMBER, GlyphError, Vocabulary
18
+
19
+ ENV_VOCAB = "GLYPH_VOCAB"
20
+
21
+ DESCRIPTION = """\
22
+ glyph: the dictionary and toolkit for the grid script of Ross 128 b.
23
+
24
+ Words are written KIND.WHICH (BODY.OTHER = "your world"), or KIND alone.
25
+ A number is COUNT.<n> (0-511); COUNT alone is zero. "_" is an empty position or an empty word.
26
+ Pages are text files, one band per line: "SYMBOL: node | relation | node".
27
+ """
28
+
29
+ EPILOG = f"""\
30
+ The vocabulary is the bundled one unless --vocab or ${ENV_VOCAB} names a file.
31
+ Run `glyph init` to make an editable copy. Docs: https://interimm.github.io/glyph-cli/
32
+ """
33
+
34
+
35
+ # ---------- helpers ----------
36
+
37
+
38
+ def read_input(path: str) -> str:
39
+ if path == "-":
40
+ return sys.stdin.read()
41
+ try:
42
+ return Path(path).read_text(encoding="utf-8")
43
+ except FileNotFoundError:
44
+ raise GlyphError(f"file not found: {path}") from None
45
+
46
+
47
+ def write_output(text: str, path: str | None) -> None:
48
+ if path:
49
+ Path(path).write_text(text, encoding="utf-8")
50
+ print(f"wrote {path}", file=sys.stderr)
51
+ else:
52
+ sys.stdout.write(text)
53
+
54
+
55
+ def emit_json(data: object) -> None:
56
+ print(json.dumps(data, indent=2, ensure_ascii=False))
57
+
58
+
59
+ # ---------- commands ----------
60
+
61
+
62
+ def cmd_parts(v: Vocabulary, a: argparse.Namespace) -> int:
63
+ if a.json:
64
+ emit_json({"parts": v.to_json()["parts"], "markers": v.markers})
65
+ return 0
66
+ for p in v.parts.values():
67
+ print(f"{p.name:<7} {p.shape[0]} {p.thing:<22} as relation: {p.relation or '—'}")
68
+ print(f"{'':<7} {p.shape[1]}")
69
+ print(f"{'':<7} {p.shape[2]}")
70
+ print("\nRelation markers (the which of a relation word):")
71
+ for m, g in v.markers.items():
72
+ print(f" {m:<7} {g}")
73
+ return 0
74
+
75
+
76
+ def cmd_list(v: Vocabulary, a: argparse.Namespace) -> int:
77
+ entries = [e for e in v.entries if not a.domain or a.domain.lower() in e.domain.lower()]
78
+ if a.json:
79
+ emit_json([dict(e.to_json(), word=str(e.word)) for e in entries])
80
+ return 0
81
+ if not entries:
82
+ print(f'no domain matches "{a.domain}"; domains: {", ".join(v.domains())}', file=sys.stderr)
83
+ return 1
84
+ for domain in v.domains():
85
+ rows = [e for e in entries if e.domain == domain]
86
+ if not rows:
87
+ continue
88
+ print(f"\n{domain}")
89
+ for e in rows:
90
+ note = f" ({e.note})" if e.note else ""
91
+ print(f" {str(e.word):<14} {e.gloss}{note}")
92
+ return 0
93
+
94
+
95
+ def cmd_check(v: Vocabulary, a: argparse.Namespace) -> int:
96
+ word = v.parse(a.word)
97
+ e = v.lookup(word)
98
+ if e:
99
+ print(f'yes: {word} = "{e.gloss}" ({e.domain})')
100
+ return 0
101
+ if word.kind == NUMBER:
102
+ print(f"yes: {word} = the number {word.which or 0} (numbers are built in)")
103
+ return 0
104
+ print(f"no: {word} is not in the vocabulary. Literal reading: {v.gloss(word)}")
105
+ same_kind = [x for x in v.entries if x.word.kind == word.kind]
106
+ if same_kind:
107
+ print(" other words of this kind: " + ", ".join(f"{x.word} {x.gloss}" for x in same_kind))
108
+ return 1
109
+
110
+
111
+ def cmd_find(v: Vocabulary, a: argparse.Namespace) -> int:
112
+ words, parts = v.search(a.text)
113
+ for e in words:
114
+ print(f"{str(e.word):<14} {e.gloss} [{e.domain}]")
115
+ for p in parts:
116
+ print(f"{p.name:<14} part: {p.thing}; as relation: {p.relation or '—'}")
117
+ if not words and not parts:
118
+ print(f'nothing matches "{a.text}"')
119
+ return 1
120
+ return 0
121
+
122
+
123
+ def cmd_show(v: Vocabulary, a: argparse.Namespace) -> int:
124
+ words = [v.parse(s) for s in a.words]
125
+ print("\n".join(draw_words(v, words, check_symbol(a.symbol))))
126
+ for w in words:
127
+ print(f" {w}: {v.gloss(w)}")
128
+ return 0
129
+
130
+
131
+ def cmd_add(v: Vocabulary, a: argparse.Namespace) -> int:
132
+ e = v.add(v.parse(a.word), a.gloss, a.domain, a.note, a.force)
133
+ path = v.save()
134
+ print(f'added {e.word} = "{e.gloss}" to {path}')
135
+ return 0
136
+
137
+
138
+ def cmd_remove(v: Vocabulary, a: argparse.Namespace) -> int:
139
+ e = v.remove(v.parse(a.word))
140
+ path = v.save()
141
+ print(f'removed {e.word} ("{e.gloss}") from {path}')
142
+ return 0
143
+
144
+
145
+ def cmd_validate(v: Vocabulary, a: argparse.Namespace) -> int:
146
+ errors = v.validate()
147
+ for x in errors:
148
+ print("error:", x)
149
+ print(f"{len(v.parts)} parts, {len(v.entries)} words, {len(errors)} problems")
150
+ return 1 if errors else 0
151
+
152
+
153
+ def cmd_render(v: Vocabulary, a: argparse.Namespace) -> int:
154
+ page = parse_page(read_input(a.file), v)
155
+ if a.svg:
156
+ write_output(render_svg(v, page, cell=a.cell, grid=not a.no_grid), a.output)
157
+ else:
158
+ write_output("\n".join(render(v, page)) + "\n", a.output)
159
+ return 0
160
+
161
+
162
+ def cmd_graph(v: Vocabulary, a: argparse.Namespace) -> int:
163
+ g = read_graph(v, parse_page(read_input(a.file), v))
164
+ if a.json:
165
+ emit_json(g.to_json())
166
+ elif a.dot:
167
+ sys.stdout.write(to_dot(g))
168
+ else:
169
+ print("\n".join(format_graph(v, g)))
170
+ return 0
171
+
172
+
173
+ def cmd_decode(v: Vocabulary, a: argparse.Namespace) -> int:
174
+ sys.stdout.write(format_page(decode(v, read_input(a.file))))
175
+ return 0
176
+
177
+
178
+ def cmd_export(v: Vocabulary, a: argparse.Namespace) -> int:
179
+ write_output(vocabulary_markdown(v, a.title), a.output)
180
+ return 0
181
+
182
+
183
+ def cmd_init(v: Vocabulary, a: argparse.Namespace) -> int:
184
+ target = Path(a.path)
185
+ if target.exists() and not a.force:
186
+ raise GlyphError(f"{target} already exists; use --force to overwrite")
187
+ v.save(target)
188
+ print(f"wrote {target} ({len(v.entries)} words). Use it with:")
189
+ print(f" export {ENV_VOCAB}={target.resolve()}")
190
+ return 0
191
+
192
+
193
+ # ---------- parser ----------
194
+
195
+
196
+ def build_parser() -> argparse.ArgumentParser:
197
+ ap = argparse.ArgumentParser(
198
+ prog="glyph", description=DESCRIPTION, epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter
199
+ )
200
+ ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
201
+ ap.add_argument("--vocab", metavar="PATH", help=f"vocabulary file (default: ${ENV_VOCAB}, else the bundled one)")
202
+ sub = ap.add_subparsers(dest="cmd", required=True, metavar="COMMAND")
203
+
204
+ def add(name: str, func: Callable, help: str) -> argparse.ArgumentParser:
205
+ p = sub.add_parser(name, help=help, description=help)
206
+ p.set_defaults(func=func)
207
+ return p
208
+
209
+ p = add("parts", cmd_parts, "list the parts and the relation markers")
210
+ p.add_argument("--json", action="store_true", help="print JSON")
211
+ p = add("list", cmd_list, "list the vocabulary")
212
+ p.add_argument("--domain", help="only domains containing this text")
213
+ p.add_argument("--json", action="store_true", help="print JSON")
214
+ p = add("check", cmd_check, "is a word in the vocabulary? (exit 1 if not)")
215
+ p.add_argument("word")
216
+ p = add("find", cmd_find, "search meanings and notes")
217
+ p.add_argument("text")
218
+ p = add("show", cmd_show, "draw words")
219
+ p.add_argument("words", nargs="+", metavar="WORD")
220
+ p.add_argument("--symbol", default="+", help="the party's symbol (default +)")
221
+ p = add("add", cmd_add, "add a word to an editable vocabulary")
222
+ p.add_argument("word")
223
+ p.add_argument("gloss", help="its meaning")
224
+ p.add_argument("--domain", default="Unsorted")
225
+ p.add_argument("--note", default="")
226
+ p.add_argument("--force", action="store_true", help="add even if the meaning is already used")
227
+ p = add("remove", cmd_remove, "remove a word from an editable vocabulary")
228
+ p.add_argument("word")
229
+ add("validate", cmd_validate, "check the vocabulary (exit 1 on problems)")
230
+ p = add("render", cmd_render, "draw a page from page source")
231
+ p.add_argument("file", help="page source file, or - for stdin")
232
+ p.add_argument("--svg", action="store_true", help="draw SVG pixels instead of text")
233
+ p.add_argument("--cell", type=int, default=12, help="SVG cell size in pixels (default 12)")
234
+ p.add_argument("--no-grid", action="store_true", help="SVG: leave empty cells undrawn")
235
+ p.add_argument("-o", "--output", help="write to a file instead of stdout")
236
+ p = add("graph", cmd_graph, "read a page as a knowledge graph")
237
+ p.add_argument("file", help="page source file, or - for stdin")
238
+ fmt = p.add_mutually_exclusive_group()
239
+ fmt.add_argument("--json", action="store_true", help="print JSON")
240
+ fmt.add_argument("--dot", action="store_true", help="print Graphviz DOT")
241
+ p = add("decode", cmd_decode, "read a drawing back into page source")
242
+ p.add_argument("file", help="drawing file, or - for stdin")
243
+ p = add("export-md", cmd_export, "print the vocabulary as Markdown")
244
+ p.add_argument("--title", help="add a top-level heading")
245
+ p.add_argument("-o", "--output", help="write to a file instead of stdout")
246
+ p = add("init", cmd_init, "copy the vocabulary to a file you can edit")
247
+ p.add_argument("path", nargs="?", default="vocab.json")
248
+ p.add_argument("--force", action="store_true", help="overwrite an existing file")
249
+ return ap
250
+
251
+
252
+ def main(argv: list[str] | None = None) -> int:
253
+ for stream in (sys.stdout, sys.stderr):
254
+ if hasattr(stream, "reconfigure"):
255
+ stream.reconfigure(errors="replace")
256
+ ap = build_parser()
257
+ a = ap.parse_args(argv)
258
+ try:
259
+ vocab = Vocabulary.load(a.vocab or os.environ.get(ENV_VOCAB) or None)
260
+ return a.func(vocab, a)
261
+ except GlyphError as exc:
262
+ print(f"glyph: error: {exc}", file=sys.stderr)
263
+ return 2
264
+ except BrokenPipeError:
265
+ return 0
266
+
267
+
268
+ if __name__ == "__main__":
269
+ sys.exit(main())