nucore 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.
Files changed (222) hide show
  1. nu/__init__.py +216 -0
  2. nu/_config/__init__.py +50 -0
  3. nu/_config/branding/__init__.py +28 -0
  4. nu/_config/branding/ansi.py +96 -0
  5. nu/_config/branding/header.py +80 -0
  6. nu/_config/branding/logo.py +61 -0
  7. nu/_config/branding/palette.py +11 -0
  8. nu/_config/config.py +89 -0
  9. nu/_config/constants.py +33 -0
  10. nu/_config/telemetry.py +149 -0
  11. nu/context/__init__.py +85 -0
  12. nu/context/_refs.py +46 -0
  13. nu/context/attrs/__init__.py +47 -0
  14. nu/context/attrs/interactions.py +336 -0
  15. nu/context/attrs/refs.py +192 -0
  16. nu/context/fabric/__init__.py +37 -0
  17. nu/context/fabric/lifecycle.py +560 -0
  18. nu/context/fabric/protocol.py +90 -0
  19. nu/context/fabric/queries.py +63 -0
  20. nu/context/fabric/refs.py +116 -0
  21. nu/core/AUTHORING.md +141 -0
  22. nu/core/__init__.py +269 -0
  23. nu/core/_stream.py +51 -0
  24. nu/core/access.py +556 -0
  25. nu/core/arithmetic.py +680 -0
  26. nu/core/bitwise.py +317 -0
  27. nu/core/cast.py +618 -0
  28. nu/core/cast_fns.py +109 -0
  29. nu/core/comparison.py +391 -0
  30. nu/core/conditional.py +184 -0
  31. nu/core/dynamic.py +94 -0
  32. nu/core/io.py +371 -0
  33. nu/core/iteration.py +249 -0
  34. nu/core/literal.py +73 -0
  35. nu/core/logical.py +231 -0
  36. nu/core/reduction.py +468 -0
  37. nu/core/reflection.py +391 -0
  38. nu/core/repr.py +415 -0
  39. nu/core/sentinel.py +185 -0
  40. nu/core/transform.py +397 -0
  41. nu/domains/__init__.py +1 -0
  42. nu/domains/service/__init__.py +15 -0
  43. nu/domains/service/dsl.py +112 -0
  44. nu/domains/service/refs.py +59 -0
  45. nu/domains/shape/__init__.py +98 -0
  46. nu/domains/shape/dsl.py +252 -0
  47. nu/domains/shape/forms/__init__.py +42 -0
  48. nu/domains/shape/forms/collection.py +124 -0
  49. nu/domains/shape/forms/item.py +99 -0
  50. nu/domains/shape/forms/mapping.py +49 -0
  51. nu/domains/shape/forms/sequence.py +45 -0
  52. nu/domains/shape/forms/set_.py +45 -0
  53. nu/domains/shape/interactions.py +282 -0
  54. nu/domains/shape/refs/__init__.py +66 -0
  55. nu/domains/shape/refs/base.py +177 -0
  56. nu/domains/shape/refs/item.py +46 -0
  57. nu/domains/shape/refs/mapping.py +74 -0
  58. nu/domains/shape/refs/sequence.py +86 -0
  59. nu/domains/shape/refs/set_.py +61 -0
  60. nu/domains/shape/refs/shape.py +115 -0
  61. nu/domains/shape/refs/shapes_mapping.py +113 -0
  62. nu/domains/shape/refs/shapes_sequence.py +115 -0
  63. nu/engine/__init__.py +54 -0
  64. nu/engine/compilation/__init__.py +32 -0
  65. nu/engine/compilation/attribution.py +65 -0
  66. nu/engine/compilation/compile.py +51 -0
  67. nu/engine/compilation/emit.py +37 -0
  68. nu/engine/compilation/index.py +52 -0
  69. nu/engine/compilation/program.py +217 -0
  70. nu/engine/evaluation/__init__.py +13 -0
  71. nu/engine/evaluation/protocol.py +49 -0
  72. nu/engine/structure/__init__.py +41 -0
  73. nu/engine/structure/attribute.py +111 -0
  74. nu/engine/structure/exceptions.py +22 -0
  75. nu/engine/structure/schema.py +133 -0
  76. nu/engine/structure/term.py +204 -0
  77. nu/engine/structure/types.py +20 -0
  78. nu/engine/validation/__init__.py +38 -0
  79. nu/engine/validation/law.py +186 -0
  80. nu/engine/validation/predicate.py +59 -0
  81. nu/factory/__init__.py +24 -0
  82. nu/factory/core.py +214 -0
  83. nu/factory/host.py +67 -0
  84. nu/flows/__init__.py +64 -0
  85. nu/flows/control.py +472 -0
  86. nu/flows/noop.py +55 -0
  87. nu/flows/parallel/__init__.py +53 -0
  88. nu/flows/parallel/_base.py +66 -0
  89. nu/flows/parallel/_scheduling.py +319 -0
  90. nu/flows/parallel/anyn.py +64 -0
  91. nu/flows/parallel/laws.py +106 -0
  92. nu/flows/parallel/parallel.py +163 -0
  93. nu/flows/parallel/race.py +62 -0
  94. nu/flows/raise_.py +109 -0
  95. nu/flows/react.py +282 -0
  96. nu/flows/strategy.py +58 -0
  97. nu/flows/stream.py +154 -0
  98. nu/forms/__init__.py +61 -0
  99. nu/forms/collections/__init__.py +23 -0
  100. nu/forms/collections/abc/__init__.py +145 -0
  101. nu/forms/collections/abc/collection.py +67 -0
  102. nu/forms/collections/abc/container.py +57 -0
  103. nu/forms/collections/abc/iterable.py +93 -0
  104. nu/forms/collections/abc/mapping.py +514 -0
  105. nu/forms/collections/abc/mapping_interactions.py +650 -0
  106. nu/forms/collections/abc/sequence.py +414 -0
  107. nu/forms/collections/abc/sequence_interactions.py +650 -0
  108. nu/forms/collections/abc/set_.py +589 -0
  109. nu/forms/collections/abc/set_interactions.py +966 -0
  110. nu/forms/collections/abc/shared_interactions.py +59 -0
  111. nu/forms/collections/abc/sized.py +45 -0
  112. nu/forms/collections/abc/sliceable.py +70 -0
  113. nu/forms/collections/dict_.py +421 -0
  114. nu/forms/collections/iterator_.py +115 -0
  115. nu/forms/collections/list_.py +489 -0
  116. nu/forms/collections/set_.py +518 -0
  117. nu/forms/collections/tuple_.py +352 -0
  118. nu/forms/collections/views.py +263 -0
  119. nu/forms/primitives/__init__.py +198 -0
  120. nu/forms/primitives/any_.py +1125 -0
  121. nu/forms/primitives/bool_.py +266 -0
  122. nu/forms/primitives/bytes_.py +1160 -0
  123. nu/forms/primitives/bytes_interactions.py +1714 -0
  124. nu/forms/primitives/float_.py +657 -0
  125. nu/forms/primitives/int_.py +974 -0
  126. nu/forms/primitives/none_.py +143 -0
  127. nu/forms/primitives/sentinel_.py +57 -0
  128. nu/forms/primitives/str_.py +1333 -0
  129. nu/forms/primitives/str_interactions.py +1896 -0
  130. nu/info/__init__.py +129 -0
  131. nu/info/builder.py +116 -0
  132. nu/info/call.py +167 -0
  133. nu/info/core/__init__.py +18 -0
  134. nu/info/core/contract/__init__.py +38 -0
  135. nu/info/core/contract/call.py +68 -0
  136. nu/info/core/contract/check.py +113 -0
  137. nu/info/core/contract/sections.py +20 -0
  138. nu/info/core/docstring/__init__.py +36 -0
  139. nu/info/core/docstring/args.py +57 -0
  140. nu/info/core/docstring/blocks.py +113 -0
  141. nu/info/core/docstring/example.py +121 -0
  142. nu/info/core/docstring/notes.py +33 -0
  143. nu/info/core/source/__init__.py +35 -0
  144. nu/info/core/source/code.py +51 -0
  145. nu/info/core/source/module.py +61 -0
  146. nu/info/core/source/mro.py +136 -0
  147. nu/info/core/source/signature.py +117 -0
  148. nu/info/core/source/unpack.py +49 -0
  149. nu/info/form.py +69 -0
  150. nu/info/interaction.py +140 -0
  151. nu/info/interactions.py +243 -0
  152. nu/info/record.py +90 -0
  153. nu/info/ref.py +67 -0
  154. nu/inspect/__init__.py +29 -0
  155. nu/inspect/annotate.py +222 -0
  156. nu/inspect/render.py +233 -0
  157. nu/inspect/shape.py +567 -0
  158. nu/lang/__init__.py +196 -0
  159. nu/lang/args.py +80 -0
  160. nu/lang/attributes/__init__.py +48 -0
  161. nu/lang/attributes/cardinality.py +88 -0
  162. nu/lang/attributes/effects.py +114 -0
  163. nu/lang/attributes/execution.py +112 -0
  164. nu/lang/attributes/names.py +58 -0
  165. nu/lang/attributes/slots.py +37 -0
  166. nu/lang/attributes/sort.py +232 -0
  167. nu/lang/forms.py +110 -0
  168. nu/lang/helpers/__init__.py +44 -0
  169. nu/lang/helpers/compilation.py +24 -0
  170. nu/lang/helpers/evaluation.py +184 -0
  171. nu/lang/helpers/run.py +81 -0
  172. nu/lang/helpers/validation.py +23 -0
  173. nu/lang/kinds.py +181 -0
  174. nu/lang/laws/__init__.py +43 -0
  175. nu/lang/laws/cardinality.py +46 -0
  176. nu/lang/laws/composition.py +186 -0
  177. nu/lang/laws/effects.py +103 -0
  178. nu/lang/laws/execution.py +32 -0
  179. nu/lang/laws/observability.py +41 -0
  180. nu/lang/laws/predicates.py +94 -0
  181. nu/lang/laws/refs.py +30 -0
  182. nu/lang/laws/spans.py +63 -0
  183. nu/lang/nu.py +84 -0
  184. nu/lang/runtime/__init__.py +29 -0
  185. nu/lang/runtime/context/__init__.py +17 -0
  186. nu/lang/runtime/context/attributes.py +77 -0
  187. nu/lang/runtime/context/context.py +359 -0
  188. nu/lang/runtime/runtime.py +199 -0
  189. nu/lang/runtime/utils/__init__.py +19 -0
  190. nu/lang/runtime/utils/budget.py +71 -0
  191. nu/lang/runtime/utils/loop.py +90 -0
  192. nu/lang/sentinels.py +98 -0
  193. nu/lang/typeinfo.py +242 -0
  194. nu/prog/__init__.py +67 -0
  195. nu/prog/_construct_worker.py +144 -0
  196. nu/prog/brace.py +207 -0
  197. nu/prog/constructors.py +477 -0
  198. nu/prog/diagnostics.py +99 -0
  199. nu/prog/eval.py +211 -0
  200. nu/prog/eval_promise.py +97 -0
  201. nu/prog/forms.py +180 -0
  202. nu/prog/laws.py +48 -0
  203. nu/prog/load.py +213 -0
  204. nu/prog/source.py +203 -0
  205. nu/py.typed +0 -0
  206. nu/reactive/__init__.py +30 -0
  207. nu/reactive/interactions.py +358 -0
  208. nu/reactive/protocol.py +56 -0
  209. nu/spans/AUTHORING.md +213 -0
  210. nu/spans/__init__.py +24 -0
  211. nu/spans/bracket.py +238 -0
  212. nu/spans/policy.py +654 -0
  213. nu/tree/__init__.py +82 -0
  214. nu/tree/effects.py +113 -0
  215. nu/tree/flow.py +102 -0
  216. nu/tree/query.py +57 -0
  217. nu/tree/rewrite.py +194 -0
  218. nu/tree/walk.py +71 -0
  219. nucore-0.2.0.dist-info/METADATA +45 -0
  220. nucore-0.2.0.dist-info/RECORD +222 -0
  221. nucore-0.2.0.dist-info/WHEEL +4 -0
  222. nucore-0.2.0.dist-info/licenses/LICENSE.md +201 -0
nu/__init__.py ADDED
@@ -0,0 +1,216 @@
1
+ # ruff: noqa
2
+ """Nu: the interaction primitive.
3
+
4
+ Build apps in one primitive that spans your whole stack (databases, UIs,
5
+ AI agents, and services). No glue. 50x less code.
6
+
7
+ Two ways to import. Grab what you need flat from the root:
8
+
9
+ from nu import Int, Sequential, Retry, AttrRef, Nu, run
10
+
11
+ Or reach a subpackage by dot-access:
12
+
13
+ import nu
14
+ nu.forms.Int nu.core.Add nu.flows.Sequential
15
+ nu.spans.Retry nu.shape.Shape nu.tree.map_nodes
16
+ nu.mem.IntRef nu.kv.presets.memory_storage
17
+ nu.ui.Page nu.std.uuid.UUID
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from importlib.metadata import PackageNotFoundError, version as _version
23
+ from pkgutil import extend_path as _extend_path
24
+ from typing import TYPE_CHECKING
25
+
26
+
27
+ # `nu` is split across two distributions: `nucore` (this file, the kernel)
28
+ # and `nustd` (the fabrics: std, mem, kv, service, llm, cc, http, proxy, mp,
29
+ # cluster, ui). Installed as wheels the two land in the same
30
+ # `site-packages/nu/` and nothing special is needed; installed editable they
31
+ # sit in two separate `src/` trees, so widen `__path__` to pick up both. Only
32
+ # this file exists as `nu/__init__.py` — the kernel owns the package root.
33
+ __path__ = _extend_path(__path__, __name__)
34
+ del _extend_path
35
+
36
+ try:
37
+ __version__ = _version("nucore")
38
+ except PackageNotFoundError:
39
+ __version__ = "0.0.0+dev"
40
+ del _version, PackageNotFoundError
41
+
42
+ from ._config import bootstrap as _bootstrap
43
+
44
+ _bootstrap()
45
+ del _bootstrap
46
+
47
+ # Subpackage namespaces for dot-access.
48
+ # Early group: pure layers with no dependency on the flat root surface.
49
+ from . import context, core, engine, factory, flows, forms, info, lang, prog, spans, tree
50
+ from .domains import shape
51
+
52
+ # Flat re-exports: the program-authoring surface.
53
+ from .context import *
54
+ from .core import *
55
+ from .flows import *
56
+ from .forms import *
57
+ from .spans import *
58
+
59
+ # Shape DSL only; fabric atoms (Load, SetCmd, ...) stay at nu.shape.*.
60
+ from .domains.shape import Shape, Slot
61
+ from .domains.service import Method, Service
62
+
63
+ # The program pipeline (nu.prog fabric). Flat: ``nu.LoadNu`` beside ``nu.Eval``,
64
+ # because ``Eval(LoadNu(src))`` is one authoring idiom, and ``nu.Program``
65
+ # beside them because it is the Form you write that pair with - it sits with
66
+ # the other Forms on the flat surface, not with the machinery. The brace
67
+ # resource and the error types stay at ``nu.prog.*``, the same way
68
+ # ``nu.mp.MpWorker`` does.
69
+ from .prog import Eval, LoadNu, Program
70
+
71
+ # Language essentials, curated to the building blocks of a Nu program:
72
+ # - the root kind (``Nu``, ``TypedNu``);
73
+ # - the kind hierarchy (``Ref``, ``Interaction``, ``Query`` / ``ScalarQuery``
74
+ # / ``StreamQuery`` / ``Reduction``, ``Command``, ``Action`` /
75
+ # ``ScalarAction`` / ``StreamAction``, ``Flow`` / ``Strategy`` /
76
+ # ``Control``, ``Span`` / ``Bracket`` / ``Policy``);
77
+ # - the argument aliases used in kind signatures (``Arg`` and the typed
78
+ # ``*Arg`` aliases).
79
+ from .lang import (
80
+ Action,
81
+ Arg,
82
+ BoolArg,
83
+ Bracket,
84
+ BytesArg,
85
+ Command,
86
+ Context,
87
+ Control,
88
+ DictArg,
89
+ FloatArg,
90
+ Flow,
91
+ Form,
92
+ FrozenSetArg,
93
+ IntArg,
94
+ Interaction,
95
+ ListArg,
96
+ NoneArg,
97
+ Nu,
98
+ Policy,
99
+ Query,
100
+ Reduction,
101
+ Ref,
102
+ Runtime,
103
+ ScalarAction,
104
+ ScalarQuery,
105
+ SetArg,
106
+ Span,
107
+ StrArg,
108
+ Strategy,
109
+ StreamAction,
110
+ StreamQuery,
111
+ TupleArg,
112
+ TypedNu,
113
+ )
114
+
115
+ # Atom builders (nu.factory subpackage): factories + @host.
116
+ from .factory import (
117
+ InteractionFactory,
118
+ host,
119
+ )
120
+
121
+ # Entry points.
122
+ from .lang.helpers import (
123
+ acollect,
124
+ aeval,
125
+ afirst,
126
+ alast,
127
+ arun,
128
+ collect,
129
+ compile,
130
+ eval,
131
+ eval_in_loop,
132
+ first,
133
+ run,
134
+ run_in_loop,
135
+ validate,
136
+ )
137
+
138
+ # Late subpackage namespaces: fabric adapters and higher layers that reach into
139
+ # the flat root surface (e.g. ``from nu import Shape``). These are heavy and
140
+ # transitively pull large trees (storage backends, UI runtime, RPC, ...), so
141
+ # they load lazily on first attribute access via ``__getattr__`` below. The
142
+ # ``TYPE_CHECKING`` block gives IDEs and type-checkers the real modules so
143
+ # ``nu.mem.IntRef`` etc. resolve statically with full completion / go-to-def.
144
+ if TYPE_CHECKING:
145
+ from . import cc, cluster, http, kv, llm, mem, mp, proxy, service, std, ui
146
+
147
+ # NOTE: several flat re-exports above shadow Python builtins at module scope
148
+ # — coercion atoms (``set``/``frozenset``/``tuple``/``list``/``dict``/``int``/
149
+ # ``float``/``str``/``bool``), IO atoms (``print``/``input``), and language
150
+ # helpers (``compile``/``eval``). They stay reachable as ``nu.set`` etc., but
151
+ # ``from nu import *`` skips them (see ``_SHADOWS_BUILTIN`` below) so callers
152
+ # don't get their builtins silently swapped. Any set/dict-builder logic in
153
+ # THIS file must use literals (``{...}``) — never the shadowed callables.
154
+ #
155
+ # Every name below ships in the `nustd` distribution, not in `nucore`.
156
+ # The two wheels merge into one ``site-packages/nu/`` directory (only the
157
+ # kernel carries ``nu/__init__.py``), so on a kernel-only install these
158
+ # submodules are simply absent — hence the install hint in __getattr__.
159
+ # The value is the extra that pulls the fabric's backend, or None when the
160
+ # fabric is pure stdlib and plain ``nustd`` is enough.
161
+ _LAZY = {
162
+ "cc": "cc",
163
+ "cluster": "cluster",
164
+ "http": "http",
165
+ "kv": "kv",
166
+ "llm": "llm",
167
+ "mem": "mem",
168
+ "mp": None,
169
+ "proxy": "proxy",
170
+ "service": None,
171
+ "std": None,
172
+ "ui": "ui",
173
+ }
174
+
175
+
176
+ def __getattr__(name):
177
+ import importlib
178
+ import importlib.util
179
+
180
+ if name not in _LAZY:
181
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
182
+ try:
183
+ mod = importlib.import_module(f".{name}", __name__)
184
+ except ImportError as exc:
185
+ # Distinguish "nustd is not installed" from "the fabric is there but
186
+ # its backend blew up" — the second must surface the real error.
187
+ if importlib.util.find_spec(f"{__name__}.{name}") is not None:
188
+ raise
189
+ extra = _LAZY[name]
190
+ target = f"nustd[{extra}]" if extra else "nustd"
191
+ msg = f"nu.{name} requires nustd: pip install {target}"
192
+ raise AttributeError(msg) from exc
193
+ globals()[name] = mod
194
+ return mod
195
+
196
+
197
+ def __dir__():
198
+ return sorted({*globals(), *_LAZY})
199
+
200
+
201
+ # __all__ = every name bound above (minus privates, the ``__future__`` shim,
202
+ # the internal ``domains`` layer, and any name shadowing a Python builtin)
203
+ # plus the lazy fabric + alias names, so ``from nu import *`` and IDE
204
+ # discovery see them without forcing import. Builtin-shadowing names stay
205
+ # reachable as ``nu.<name>`` — the module dict is unchanged, only ``__all__``
206
+ # is filtered — so ``import *`` cannot silently rebind ``set``/``print``/etc.
207
+ # in the caller's namespace.
208
+ import builtins as _builtins # noqa: E402 (placed here to avoid the shadowed scope above)
209
+
210
+ _HIDDEN = {"annotations", "TYPE_CHECKING", "domains"}
211
+ _SHADOWS_BUILTIN = {n for n in dir(_builtins) if not n.startswith("_")}
212
+ _names = dir()
213
+ __all__ = sorted(
214
+ ({*_names, *_LAZY} - _HIDDEN - _SHADOWS_BUILTIN) - {n for n in _names if n.startswith("_")}
215
+ )
216
+ del _builtins, _names
nu/_config/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """Internal: ``~/.nu`` user config and anonymous usage telemetry.
2
+
3
+ Layout:
4
+
5
+ - :mod:`nu._config.constants` -- all paths, defaults, and PostHog
6
+ wiring. Single source of truth.
7
+ - :mod:`nu._config.config` -- read/write ``~/.nu/config.json``,
8
+ telemetry opt-out flag, stable ``distinct_id``.
9
+ - :mod:`nu._config.telemetry` -- fire-and-forget PostHog capture.
10
+
11
+ Hot path: :func:`bootstrap` does one ``exists()`` syscall and returns
12
+ immediately when the first-run marker is already in place; the config
13
+ and telemetry submodules are only imported on first-run bootstrap and
14
+ explicit CLI / nudle calls.
15
+ """
16
+
17
+ from .constants import CONFIG_PATH as CONFIG_PATH
18
+ from .constants import FIRST_RUN_MARKER_PATH as FIRST_RUN_MARKER_PATH
19
+ from .constants import HOME as HOME
20
+
21
+
22
+ def bootstrap() -> None:
23
+ """First-touch bootstrap for ``~/.nu``.
24
+
25
+ Fast path: one ``exists()`` syscall on the first-run marker; the
26
+ marker is only written after a successful capture, which itself
27
+ only runs once the config file exists, so its presence implies
28
+ both are in place.
29
+
30
+ Otherwise: create the config on first ever import, and (re)fire
31
+ the first-run event on every import until the POST gets a 2xx and
32
+ drops the marker. That covers short-lived commands
33
+ (``nu --help``, ``python -c "import nu"``) where a daemon POST
34
+ would otherwise be killed on process exit. Silent on any failure.
35
+ """
36
+ try:
37
+ if FIRST_RUN_MARKER_PATH.exists():
38
+ return
39
+ from .telemetry import capture_first_run
40
+
41
+ if not CONFIG_PATH.exists():
42
+ from .config import create_default
43
+
44
+ capture_first_run(create_default())
45
+ else:
46
+ from .config import distinct_id
47
+
48
+ capture_first_run(distinct_id())
49
+ except Exception: # noqa: S110
50
+ pass
@@ -0,0 +1,28 @@
1
+ """Nu brand kit: palette + logo + shared header. Consumed by nudle and nucli.
2
+
3
+ Stdlib only -- it renders straight to ANSI escapes (see :mod:`.ansi`), so it
4
+ sits in the kernel without pulling a terminal library in behind it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .ansi import RESET, color_enabled, paint
10
+ from .header import DOCS_URL, GITHUB_URL, TAGLINE, header, render_header
11
+ from .logo import logo, logo_lines
12
+ from .palette import BLUE, PURPLE
13
+
14
+
15
+ __all__ = [
16
+ "BLUE",
17
+ "DOCS_URL",
18
+ "GITHUB_URL",
19
+ "PURPLE",
20
+ "RESET",
21
+ "TAGLINE",
22
+ "color_enabled",
23
+ "header",
24
+ "logo",
25
+ "logo_lines",
26
+ "paint",
27
+ "render_header",
28
+ ]
@@ -0,0 +1,96 @@
1
+ """Minimal ANSI SGR helpers -- the brand kit's whole rendering layer.
2
+
3
+ Stdlib only, on purpose: the brand kit lives in the kernel now, and the
4
+ kernel takes no dependency beyond ``typing-extensions`` and ``cloudpickle``.
5
+ Everything here is a plain string transform.
6
+
7
+ Two rules govern whether escapes are emitted at all:
8
+
9
+ - ``NO_COLOR`` set to a non-empty value disables them (https://no-color.org).
10
+ - the target stream must be a tty; piping ``nu`` into a file or a pager
11
+ must never leak escape junk.
12
+
13
+ Attribute order in the emitted sequence matches what rich produced before
14
+ (bold, dim, underline, then the truecolor foreground) so terminal output is
15
+ byte-identical to the previous rich-rendered brand kit.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+ from typing import TYPE_CHECKING
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from typing import IO
27
+
28
+
29
+ __all__ = ["RESET", "color_enabled", "paint"]
30
+
31
+ RESET = "\x1b[0m"
32
+
33
+
34
+ def color_enabled(stream: IO[str] | None = None) -> bool:
35
+ """Whether ``stream`` should get ANSI escapes.
36
+
37
+ Args:
38
+ stream: the destination. Defaults to ``sys.stdout``.
39
+
40
+ Returns:
41
+ ``True`` only when ``NO_COLOR`` is unset/empty and the stream is a
42
+ tty. A stream that raises or has no ``isatty`` counts as not a tty.
43
+ """
44
+ if os.environ.get("NO_COLOR"):
45
+ return False
46
+ stream = sys.stdout if stream is None else stream
47
+ try:
48
+ return bool(stream.isatty())
49
+ except Exception:
50
+ return False
51
+
52
+
53
+ def _hex_to_rgb(color: str) -> tuple[int, int, int]:
54
+ """``"#7a4ce0"`` -> ``(122, 76, 224)``."""
55
+ value = color.lstrip("#")
56
+ return int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)
57
+
58
+
59
+ def paint(
60
+ text: str,
61
+ *,
62
+ fg: str | None = None,
63
+ bold: bool = False,
64
+ dim: bool = False,
65
+ underline: bool = False,
66
+ enabled: bool = True,
67
+ ) -> str:
68
+ """Wrap ``text`` in one SGR sequence, or return it untouched.
69
+
70
+ Args:
71
+ text: the string to style.
72
+ fg: foreground as a ``#rrggbb`` hex string, or ``None``.
73
+ bold: emit SGR 1.
74
+ dim: emit SGR 2.
75
+ underline: emit SGR 4.
76
+ enabled: when ``False``, or when no attribute is requested, ``text``
77
+ comes back unchanged with no escapes at all.
78
+
79
+ Returns:
80
+ The styled string, always terminated by a reset when styled.
81
+ """
82
+ if not enabled or not text:
83
+ return text
84
+ codes: list[str] = []
85
+ if bold:
86
+ codes.append("1")
87
+ if dim:
88
+ codes.append("2")
89
+ if underline:
90
+ codes.append("4")
91
+ if fg is not None:
92
+ r, g, b = _hex_to_rgb(fg)
93
+ codes.append(f"38;2;{r};{g};{b}")
94
+ if not codes:
95
+ return text
96
+ return f"\x1b[{';'.join(codes)}m{text}{RESET}"
@@ -0,0 +1,80 @@
1
+ """Shared Nu header: logo on the left, tagline + version + urls on the right.
2
+
3
+ Used by the CLI (`nu` with no args) and the nudle server banner -- one
4
+ shared visual identity across every user-facing entrypoint.
5
+
6
+ The two columns are laid out by hand (a fixed-width left cell, four spaces
7
+ of gutter, a right cell padded to its widest line, right block vertically
8
+ centred against the mark) so the kernel needs no table renderer.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from importlib.metadata import PackageNotFoundError, version
15
+ from typing import TYPE_CHECKING
16
+
17
+ from .ansi import color_enabled, paint
18
+ from .logo import WIDTH as _LOGO_WIDTH
19
+ from .logo import logo_lines
20
+ from .palette import BLUE
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from typing import IO
25
+
26
+
27
+ TAGLINE = "The interaction primitive"
28
+ DOCS_URL = "https://nustack.dev"
29
+ GITHUB_URL = "https://github.com/nustackdev/nu"
30
+
31
+ # Gutter between the mark and the label block.
32
+ _GUTTER = " "
33
+
34
+
35
+ def _nu_version() -> str:
36
+ try:
37
+ return version("nucore")
38
+ except PackageNotFoundError:
39
+ return "0.0.0+dev"
40
+
41
+
42
+ def _side(color: bool) -> list[tuple[str, str]]:
43
+ """The right-hand label block as ``(plain, rendered)`` lines."""
44
+ ver = f"v{_nu_version()}"
45
+ return [
46
+ (TAGLINE, TAGLINE),
47
+ (ver, paint(ver, fg=BLUE, bold=True, enabled=color)),
48
+ ("", ""),
49
+ (DOCS_URL, paint(DOCS_URL, fg=BLUE, underline=True, enabled=color)),
50
+ ("", ""),
51
+ (GITHUB_URL, paint(GITHUB_URL, fg=BLUE, underline=True, enabled=color)),
52
+ ]
53
+
54
+
55
+ def header(*, color: bool = True) -> str:
56
+ """The header block as one string, blank line above and below."""
57
+ # ``logo`` ends on a newline, so the mark occupies one blank row past its
58
+ # last glyph row -- that row is part of the block and gets padded too.
59
+ # Every glyph row is exactly ``_LOGO_WIDTH`` cells wide, so the left column
60
+ # needs no padding of its own; the trailing row is that width in spaces.
61
+ left = [*logo_lines(color=color), " " * _LOGO_WIDTH]
62
+
63
+ side = _side(color)
64
+ right_width = max(len(plain) for plain, _ in side)
65
+ # Vertically centre the labels against the mark, rich's ``vertical="middle"``.
66
+ top = (len(left) - len(side)) // 2
67
+ blank = ("", "")
68
+ rows = [blank] * top + side + [blank] * (len(left) - len(side) - top)
69
+
70
+ lines = [""]
71
+ for mark, (plain, rendered) in zip(left, rows, strict=True):
72
+ lines.append(mark + _GUTTER + rendered + " " * (right_width - len(plain)))
73
+ lines.append("")
74
+ return "\n".join(lines)
75
+
76
+
77
+ def render_header(file: IO[str] | None = None) -> None:
78
+ """Print the shared Nu header: logo on the left, labels on the right."""
79
+ stream = sys.stdout if file is None else file
80
+ print(header(color=color_enabled(stream)), file=stream, flush=True)
@@ -0,0 +1,61 @@
1
+ """Nu mark rendered as terminal braille glyphs.
2
+
3
+ Compact 2x4 sub-pixel braille packing of the SVG mark -- two interlocking
4
+ glyphs, purple top-left and blue bottom-right matching the SVG gradient
5
+ stops.
6
+
7
+ Baked once from the source PNG so this module has zero runtime deps beyond
8
+ the stdlib.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .ansi import paint
14
+ from .palette import BLUE, PURPLE
15
+
16
+
17
+ _COLOR = {"P": PURPLE, "B": BLUE}
18
+
19
+ # 16 cols x 8 rows braille grid; each row is (glyphs, per-cell color tag —
20
+ # 'P' purple, 'B' blue, '.' blank).
21
+ _LOGO: tuple[tuple[str, str], ...] = (
22
+ ("⠀⢀⣤⣶⣾⣿⣿⣿⣿⣿⣿⡇⠀⢰⣿⣿", ".PPPPPPPPPPP.PPB"),
23
+ ("⣰⣿⣿⠟⠋⠉⠉⠉⠉⣿⣿⡇⠀⢸⣿⣿", "PPPPPPPPPPPP.BBB"),
24
+ ("⣿⣿⡇⠀⠀⣀⣀⠀⠀⣿⣿⡇⠀⢸⣿⣿", "PPP..PP..PPB.BBB"),
25
+ ("⣿⣿⡇⠀⢸⣿⣿⠀⠀⣿⣿⡇⠀⢸⣿⣿", "PPP.PPP..BBB.BBB"),
26
+ ("⣿⣿⡇⠀⢸⣿⣿⠀⠀⣿⣿⡇⠀⢸⣿⣿", "PPP.PPP..BBB.BBB"),
27
+ ("⣿⣿⡇⠀⢸⣿⣿⠀⠀⠉⠉⠀⠀⢸⣿⣿", "PPP.PBB..BB..BBB"),
28
+ ("⣿⣿⡇⠀⢸⣿⣿⣀⣀⣀⣀⣠⣴⣿⣿⠏", "PPP.BBBBBBBBBBBB"),
29
+ ("⣿⣿⠇⠀⢸⣿⣿⣿⣿⣿⣿⡿⠿⠛⠁⠀", "PBB.BBBBBBBBBBB."),
30
+ )
31
+
32
+ # Every row is the same width; the header lays the mark out against this.
33
+ WIDTH = len(_LOGO[0][0])
34
+
35
+
36
+ def logo_lines(*, color: bool = True) -> list[str]:
37
+ """The mark, one rendered string per row.
38
+
39
+ Args:
40
+ color: emit ANSI escapes. When ``False`` the glyphs come back bare.
41
+
42
+ Returns:
43
+ Eight strings, each ``WIDTH`` glyphs wide once escapes are stripped.
44
+ """
45
+ return [_row(chars, colors, color) for chars, colors in _LOGO]
46
+
47
+
48
+ def logo(*, color: bool = True) -> str:
49
+ """The mark as one block of eight rows, each terminated by a newline."""
50
+ return "".join(f"{line}\n" for line in logo_lines(color=color))
51
+
52
+
53
+ def _row(chars: str, colors: str, color: bool) -> str:
54
+ """One grid row, collapsed into runs of equal colour tag."""
55
+ out: list[str] = []
56
+ start = 0
57
+ for i in range(1, len(colors) + 1):
58
+ if i == len(colors) or colors[i] != colors[start]:
59
+ out.append(paint(chars[start:i], fg=_COLOR.get(colors[start]), enabled=color))
60
+ start = i
61
+ return "".join(out)
@@ -0,0 +1,11 @@
1
+ """Nu brand palette -- the SVG gradient stops, canonical for the whole codebase.
2
+
3
+ Import ``PURPLE``/``BLUE`` from here whenever you need an accent; there is no
4
+ "cyan" or ad-hoc third color in the Nu palette.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ PURPLE = "#7a4ce0"
11
+ BLUE = "#3e72e7"
nu/_config/config.py ADDED
@@ -0,0 +1,89 @@
1
+ """User config at ``~/.nu/config.json``.
2
+
3
+ One JSON file, machine-managed. Holds the telemetry opt-out flag and a
4
+ stable anonymous ``distinct_id``. Created on first `nu` import or CLI
5
+ run; that first-touch is the signal that a human actually installed
6
+ and used Nu (see :mod:`nu._config.telemetry`).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import uuid
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ from .constants import CONFIG_DEFAULTS, CONFIG_PATH, HOME
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from typing import Any
22
+
23
+
24
+ def load() -> dict[str, Any]:
25
+ """Read the config file; return {} if it does not exist or is unreadable."""
26
+ try:
27
+ return json.loads(CONFIG_PATH.read_text())
28
+ except (OSError, json.JSONDecodeError):
29
+ return {}
30
+
31
+
32
+ def save(cfg: dict[str, Any]) -> None:
33
+ """Write config atomically (temp + rename)."""
34
+ HOME.mkdir(parents=True, exist_ok=True)
35
+ tmp = CONFIG_PATH.with_suffix(".json.tmp")
36
+ tmp.write_text(json.dumps(cfg, indent=2, sort_keys=True) + "\n")
37
+ tmp.replace(CONFIG_PATH)
38
+
39
+
40
+ def create_default() -> str:
41
+ """Create the config with defaults; return the new distinct_id.
42
+
43
+ Caller must have checked that :data:`CONFIG_PATH` does not exist --
44
+ this always writes.
45
+ """
46
+ did = str(uuid.uuid4())
47
+ save({**CONFIG_DEFAULTS, "distinct_id": did})
48
+ return did
49
+
50
+
51
+ def telemetry_enabled() -> bool:
52
+ """Effective telemetry state (config flag, defaulting on)."""
53
+ return bool(load().get("telemetry", True))
54
+
55
+
56
+ def set_telemetry(on: bool) -> None:
57
+ """Flip the telemetry flag, preserving the rest of the config."""
58
+ cfg = load() or {**CONFIG_DEFAULTS, "distinct_id": str(uuid.uuid4())}
59
+ cfg["telemetry"] = bool(on)
60
+ save(cfg)
61
+
62
+
63
+ def distinct_id() -> str:
64
+ """Stable anonymous id. Generated on first :func:`create_default` call."""
65
+ did = load().get("distinct_id")
66
+ if isinstance(did, str) and did:
67
+ return did
68
+ return "unknown"
69
+
70
+
71
+ def is_dev_install() -> bool:
72
+ """True when Nu is imported from a source checkout (editable install).
73
+
74
+ Heuristic: a ``pyproject.toml`` sits within a few levels above
75
+ ``nu/__init__.py`` (the ``src/nu`` layout used in this repo). Env
76
+ override ``NU_DEV=1`` forces dev mode; ``NU_DEV=0`` forces prod.
77
+ """
78
+ override = os.environ.get("NU_DEV")
79
+ if override == "1":
80
+ return True
81
+ if override == "0":
82
+ return False
83
+ here = Path(__file__).resolve()
84
+ for depth in (3, 4, 5):
85
+ if depth >= len(here.parents):
86
+ break
87
+ if (here.parents[depth] / "pyproject.toml").exists():
88
+ return True
89
+ return False
@@ -0,0 +1,33 @@
1
+ """Single source of truth for ``~/.nu`` paths and PostHog wiring.
2
+
3
+ Everything the ``_config`` subpackage would otherwise hardcode lives
4
+ here: filesystem layout under ``~/.nu``, config defaults, and the
5
+ PostHog capture endpoint. Env vars ``NU_POSTHOG_TOKEN`` /
6
+ ``NU_POSTHOG_HOST`` override the baked-in defaults (local testing
7
+ against a different PostHog project).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from pathlib import Path
14
+
15
+
16
+ HOME = Path.home() / ".nu"
17
+ CONFIG_PATH = HOME / "config.json"
18
+ DEMOS_DIR = HOME / "demos"
19
+ FIRST_RUN_MARKER_PATH = HOME / "first_run.sent"
20
+
21
+ CONFIG_DEFAULTS: dict[str, object] = {"telemetry": True}
22
+
23
+ # PostHog Project API key -- public write-only token, safe to embed
24
+ # (same one baked into the docs site's client bundle).
25
+ POSTHOG_TOKEN = os.environ.get(
26
+ "NU_POSTHOG_TOKEN", "phc_yRpWfpoMeeRM8sKMPGPEsGy8ygNHJTpsGwrsikfpRfFz"
27
+ )
28
+ POSTHOG_HOST = os.environ.get("NU_POSTHOG_HOST", "https://t.nustack.dev").rstrip("/")
29
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/"
30
+
31
+ FIRST_RUN_EVENT = "nu_first_run"
32
+
33
+ HTTP_TIMEOUT_SECONDS = 3