diff --git a/docs/manual/.buildinfo b/docs/manual/.buildinfo new file mode 100644 index 0000000..754d4d7 --- /dev/null +++ b/docs/manual/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 76bb8ae3fb1a993b055966a4ef5900e1 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/manual/_images/board_heltec32v20.png b/docs/manual/_images/board_heltec32v20.png new file mode 100644 index 0000000..9f16786 Binary files /dev/null and b/docs/manual/_images/board_heltec32v20.png differ diff --git a/docs/manual/_images/board_heltec32v30.png b/docs/manual/_images/board_heltec32v30.png new file mode 100644 index 0000000..3799666 Binary files /dev/null and b/docs/manual/_images/board_heltec32v30.png differ diff --git a/docs/manual/_images/board_heltec32v4.png b/docs/manual/_images/board_heltec32v4.png new file mode 100644 index 0000000..64b5a88 Binary files /dev/null and b/docs/manual/_images/board_heltec32v4.png differ diff --git a/docs/manual/_images/board_opencomxl.png b/docs/manual/_images/board_opencomxl.png new file mode 100644 index 0000000..c3b78bd Binary files /dev/null and b/docs/manual/_images/board_opencomxl.png differ diff --git a/docs/manual/_images/board_rak4631.png b/docs/manual/_images/board_rak4631.png new file mode 100644 index 0000000..194a5b8 Binary files /dev/null and b/docs/manual/_images/board_rak4631.png differ diff --git a/docs/manual/_images/board_rnodev2.png b/docs/manual/_images/board_rnodev2.png new file mode 100644 index 0000000..84efb21 Binary files /dev/null and b/docs/manual/_images/board_rnodev2.png differ diff --git a/docs/manual/_images/board_t114.png b/docs/manual/_images/board_t114.png new file mode 100644 index 0000000..ba7cbdc Binary files /dev/null and b/docs/manual/_images/board_t114.png differ diff --git a/docs/manual/_images/board_t3s3.png b/docs/manual/_images/board_t3s3.png new file mode 100644 index 0000000..04bcafb Binary files /dev/null and b/docs/manual/_images/board_t3s3.png differ diff --git a/docs/manual/_images/board_t3v10.png b/docs/manual/_images/board_t3v10.png new file mode 100644 index 0000000..01f01bd Binary files /dev/null and b/docs/manual/_images/board_t3v10.png differ diff --git a/docs/manual/_images/board_t3v20.png b/docs/manual/_images/board_t3v20.png new file mode 100644 index 0000000..369586a Binary files /dev/null and b/docs/manual/_images/board_t3v20.png differ diff --git a/docs/manual/_images/board_t3v21.png b/docs/manual/_images/board_t3v21.png new file mode 100644 index 0000000..28a62df Binary files /dev/null and b/docs/manual/_images/board_t3v21.png differ diff --git a/docs/manual/_images/board_tbeam.png b/docs/manual/_images/board_tbeam.png new file mode 100644 index 0000000..dcad0f0 Binary files /dev/null and b/docs/manual/_images/board_tbeam.png differ diff --git a/docs/manual/_images/board_tbeam_supreme.png b/docs/manual/_images/board_tbeam_supreme.png new file mode 100644 index 0000000..fcaacc4 Binary files /dev/null and b/docs/manual/_images/board_tbeam_supreme.png differ diff --git a/docs/manual/_images/board_tdeck.png b/docs/manual/_images/board_tdeck.png new file mode 100644 index 0000000..9eac1a0 Binary files /dev/null and b/docs/manual/_images/board_tdeck.png differ diff --git a/docs/manual/_images/board_techo.png b/docs/manual/_images/board_techo.png new file mode 100644 index 0000000..3958628 Binary files /dev/null and b/docs/manual/_images/board_techo.png differ diff --git a/docs/manual/_images/columba.webp b/docs/manual/_images/columba.webp new file mode 100644 index 0000000..956ea8e Binary files /dev/null and b/docs/manual/_images/columba.webp differ diff --git a/docs/manual/_images/if_mode_graph_b.png b/docs/manual/_images/if_mode_graph_b.png new file mode 100644 index 0000000..6606b60 Binary files /dev/null and b/docs/manual/_images/if_mode_graph_b.png differ diff --git a/docs/manual/_images/lxst_phone.webp b/docs/manual/_images/lxst_phone.webp new file mode 100644 index 0000000..697359a Binary files /dev/null and b/docs/manual/_images/lxst_phone.webp differ diff --git a/docs/manual/_images/meshchat_1.webp b/docs/manual/_images/meshchat_1.webp new file mode 100644 index 0000000..87b0ddb Binary files /dev/null and b/docs/manual/_images/meshchat_1.webp differ diff --git a/docs/manual/_images/meshchatx.webp b/docs/manual/_images/meshchatx.webp new file mode 100644 index 0000000..24fd7ba Binary files /dev/null and b/docs/manual/_images/meshchatx.webp differ diff --git a/docs/manual/_images/nomadnet_3.png b/docs/manual/_images/nomadnet_3.png new file mode 100644 index 0000000..4a8c0de Binary files /dev/null and b/docs/manual/_images/nomadnet_3.png differ diff --git a/docs/manual/_images/radio_is5ac.png b/docs/manual/_images/radio_is5ac.png new file mode 100644 index 0000000..e3988a6 Binary files /dev/null and b/docs/manual/_images/radio_is5ac.png differ diff --git a/docs/manual/_images/radio_rblhg5.png b/docs/manual/_images/radio_rblhg5.png new file mode 100644 index 0000000..59bd6c9 Binary files /dev/null and b/docs/manual/_images/radio_rblhg5.png differ diff --git a/docs/manual/_images/rbrowser.webp b/docs/manual/_images/rbrowser.webp new file mode 100644 index 0000000..e717d2b Binary files /dev/null and b/docs/manual/_images/rbrowser.webp differ diff --git a/docs/manual/_images/retibbs.webp b/docs/manual/_images/retibbs.webp new file mode 100644 index 0000000..5695b9f Binary files /dev/null and b/docs/manual/_images/retibbs.webp differ diff --git a/docs/manual/_images/rnphone.webp b/docs/manual/_images/rnphone.webp new file mode 100644 index 0000000..10bf686 Binary files /dev/null and b/docs/manual/_images/rnphone.webp differ diff --git a/docs/manual/_images/sideband_devices.webp b/docs/manual/_images/sideband_devices.webp new file mode 100644 index 0000000..a90154b Binary files /dev/null and b/docs/manual/_images/sideband_devices.webp differ diff --git a/docs/manual/_sources/examples.rst.txt b/docs/manual/_sources/examples.rst.txt new file mode 100644 index 0000000..4410983 --- /dev/null +++ b/docs/manual/_sources/examples.rst.txt @@ -0,0 +1,142 @@ +.. _examples-main: + +************* +Code Examples +************* + +A number of examples are included in the source distribution of Reticulum. +You can use these examples to learn how to write your own programs. + +.. _example-minimal: + +Minimal +======= + +The *Minimal* example demonstrates the bare-minimum setup required to connect to +a Reticulum network from your program. In about five lines of code, you will +have the Reticulum Network Stack initialised, and ready to pass traffic in your +program. + +.. literalinclude:: ../../Examples/Minimal.py + +This example can also be found at ``_. + +.. _example-announce: + +Announce +======== + +The *Announce* example builds upon the previous example by exploring how to +announce a destination on the network, and how to let your program receive +notifications about announces from relevant destinations. + +.. literalinclude:: ../../Examples/Announce.py + +This example can also be found at ``_. + +.. _example-broadcast: + +Broadcast +========= +The *Broadcast* example explores how to transmit plaintext broadcast messages +over the network. + +.. literalinclude:: ../../Examples/Broadcast.py + +This example can also be found at ``_. + +.. _example-echo: + +Echo +==== + +The *Echo* example demonstrates communication between two destinations using +the Packet interface. + +.. literalinclude:: ../../Examples/Echo.py + +This example can also be found at ``_. + +.. _example-link: + +Link +==== + +The *Link* example explores establishing an encrypted link to a remote +destination, and passing traffic back and forth over the link. + +.. literalinclude:: ../../Examples/Link.py + +This example can also be found at ``_. + +.. _example-identify: + +Identification +============== + +The *Identify* example explores identifying an intiator of a link, once +the link has been established. + +.. literalinclude:: ../../Examples/Identify.py + +This example can also be found at ``_. + +.. _example-request: + +Requests & Responses +==================== + +The *Request* example explores sending requests and receiving responses. + +.. literalinclude:: ../../Examples/Request.py + +This example can also be found at ``_. + +.. _example-channel: + +Channel +======= + +The *Channel* example explores using a ``Channel`` to send structured +data between peers of a ``Link``. + +.. literalinclude:: ../../Examples/Channel.py + +This example can also be found at ``_. + +Buffer +====== + +The *Buffer* example explores using buffered readers and writers to send +binary data between peers of a ``Link``. + +.. literalinclude:: ../../Examples/Buffer.py + +This example can also be found at ``_. + +.. _example-filetransfer: + +Filetransfer +============ + +The *Filetransfer* example implements a basic file-server program that +allow clients to connect and download files. The program uses the Resource +interface to efficiently pass files of any size over a Reticulum :ref:`Link`. + +.. literalinclude:: ../../Examples/Filetransfer.py + +This example can also be found at ``_. + +.. _example-custominterface: + +Custom Interfaces +================= + +The *ExampleInterface* demonstrates creating custom interfaces for Reticulum. +Any number of custom interfaces can be loaded and utilised by Reticulum, and +will be fully on-par with natively included interfaces, including all supported +:ref:`interface modes` and :ref:`common configuration options`. + +.. literalinclude:: ../../Examples/ExampleInterface.py + +This example can also be found at ``_. \ No newline at end of file diff --git a/docs/manual/_sources/forhumans.rst.txt b/docs/manual/_sources/forhumans.rst.txt new file mode 100644 index 0000000..e953d50 --- /dev/null +++ b/docs/manual/_sources/forhumans.rst.txt @@ -0,0 +1,4 @@ +******************************************** +An Explanation of Reticulum for Human Beings +******************************************** + diff --git a/docs/manual/_sources/gettingstartedfast.rst.txt b/docs/manual/_sources/gettingstartedfast.rst.txt new file mode 100644 index 0000000..a2d7831 --- /dev/null +++ b/docs/manual/_sources/gettingstartedfast.rst.txt @@ -0,0 +1,731 @@ +******************** +Getting Started Fast +******************** + +The best way to get started with the Reticulum Network Stack depends on what +you want to do. This guide will outline sensible starting paths for different +scenarios. + + +Standalone Reticulum Installation +================================= +If you simply want to install Reticulum and related utilities on a system, +the easiest way is via the ``pip`` package manager: + +.. code:: shell + + pip install rns + +If you do not already have pip installed, you can install it using the package manager +of your system with a command like ``sudo apt install python3-pip``, +``sudo pamac install python-pip`` or similar. + +You can also dowload the Reticulum release wheels from GitHub, or other release channels, +and install them offline using ``pip``: + +.. code:: shell + + pip install ./rns-1.1.2-py3-none-any.whl + +On platforms that limit user package installation via ``pip``, you may need to manually +allow this using the ``--break-system-packages`` command line flag when installing. This +will not actually break any packages, unless you have installed Reticulum directly via +your operating system's package manager. + +.. code:: shell + + pip install rns --break-system-packages + +For more detailed installation instructions, please see the +:ref:`Platform-Specific Install Notes` section. + +After installation is complete, it might be helpful to refer to the +:ref:`Using Reticulum on Your System` chapter. + +Resolving Dependency & Installation Issues +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +On some platforms, there may not be binary packages available for all dependencies, and +``pip`` installation may fail with an error message. In these cases, the issue can usually +be resolved by installing the development essentials packages for your platform: + +.. code:: shell + + # Debian / Ubuntu / Derivatives + sudo apt install build-essential + + # Arch / Manjaro / Derivatives + sudo pamac install base-devel + + # Fedora + sudo dnf groupinstall "Development Tools" "Development Libraries" + +With the base development packages installed, ``pip`` should be able to compile any missing +dependencies from source, and complete installation even on platforms that don't have pre- +compiled packages available. + +Try Using a Reticulum-based Program +============================================= + +If you simply want to try using a program built with Reticulum, a :ref:`range of different +programs ` exist that allow basic communication and a various other useful functions, +even over extremely low-bandwidth Reticulum networks. + + +Using the Included Utilities +============================================= +Reticulum comes with a range of included utilities that make it easier to +manage your network, check connectivity and make Reticulum available to other +programs on your system. + +You can use ``rnsd`` to run Reticulum as a background or foreground service, +and the ``rnstatus``, ``rnpath`` and ``rnprobe`` utilities to view and query +network status and connectivity. + +To learn more about these utility programs, have a look at the +:ref:`Using Reticulum on Your System` chapter of this manual. + + +Creating a Network With Reticulum +============================================= +To create a network, you will need to specify one or more *interfaces* for +Reticulum to use. This is done in the Reticulum configuration file, which by +default is located at ``~/.reticulum/config``. You can get an example +configuration file with all options via ``rnsd --exampleconfig``. + +When Reticulum is started for the first time, it will create a default +configuration file, with one active interface. This default interface uses +your existing Ethernet and WiFi networks (if any), and only allows you to +communicate with other Reticulum peers within your local broadcast domains. + +To communicate further, you will have to add one or more interfaces. The default +configuration includes a number of examples, ranging from using TCP over the +internet, to LoRa and Packet Radio interfaces. + +With Reticulum, you only need to configure what interfaces you want to communicate +over. There is no need to configure address spaces, subnets, routing tables, +or other things you might be used to from other network types. + +Once Reticulum knows which interfaces it should use, it will automatically +discover topography and configure transport of data to any destinations it +knows about. + +In situations where you already have an established WiFi or Ethernet network, and +many devices that want to utilise the same external Reticulum network paths (for example over +LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by +adding any external interfaces to the configuration of this system, and then enabling transport on it. Any +other device on your local WiFi will then be able to connect to this wider Reticulum +network just using the default (:ref:`AutoInterface`) configuration. + +Possibly, the examples in the config file are enough to get you started. If +you want more information, you can read the :ref:`Building Networks` +and :ref:`Interfaces` chapters of this manual, but most importantly, +start with reading the next section, :ref:`Bootstrapping Connectivity`, +as this provides the most essential understanding of how to ensure reliable +connectivity with a minimum of maintenance. + + +.. _bootstrapping-connectivity: + +Bootstrapping Connectivity +========================== + +Reticulum is not a service you subscribe to, nor is it a single global network you "join". It is a *networking stack*; a toolkit for building communications systems that align with your specific values, requirements, and operational environment. The way you choose to connect to other Reticulum peers is entirely your own choice. + +One of the most powerful aspects of Reticulum is that it provides a multitude of tools to establish, maintain, and optimize connectivity. You can use these tools in isolation or combine them in complex configurations to achieve a vast array of goals. + +Whether your aim is to create a completely private, air-gapped network for your family; to build a resilient community mesh that survives infrastructure collapse; to connect far and wide to as many nodes as possible; or simply to maintain a reliable, encrypted link to a specific organization you care about, Reticulum provides the mechanisms to make it happen. + +There is no "right" or "wrong" way to build a Reticulum network, and you don't need to be a network engineer just to get started. If the information flows in the way you intend, and your privacy and security requirements are met, your configuration is a success. Reticulum is designed to make the most challenging and difficult scenarios attainable, even when other networking technologies fail. + + +Finding Your Way +^^^^^^^^^^^^^^^^ + +When you first start using Reticulum, you need a way to obtain connectivity with the peers you want to communicate with - the process of *bootstrapping connectivity*. + +.. important:: + + A common mistake in modern networking is the reliance on a few centralized, hard-coded entrypoints. If every user simply connects to the same list of public IP addresses found on a website, the network becomes brittle, centralized, and ultimately fails to deliver on the promise of decentralization and resilience. You have a responsibility here. + +Reticulum encourages the approach of *organic growth*. Instead of relying on permanent static connections to distant servers, you can use temporary bootstrap connections to continously *discover* more relevant or local infrastructure. Once discovered, your system can automatically form stronger, more direct links to these peers, and discard the temporary bootstrap links. This results in a web of connections that are geographically relevant, resilient and efficient. + +It *is* possible to simply add a few public entrypoints to the ``[interfaces]`` section of your Reticulum configuration and be connected, but a better option is to enable :ref:`interface discovery` and either manually select relevant, local interfaces, or enable discovered interface auto-connection. + +A relevant option in this context is the :ref:`bootstrap only` interface option. This is an automated tool for better distributing connectivity. By enabling interface discovery and auto-connection, and marking an interface as ``bootstrap_only``, you tell Reticulum to use that interface primarliy to find connectivity options, and then disconnect it once sufficient entrypoints have been discovered. This helps create a network topology that favors locality and resilience over the simple centralization caused by using only a few static entrypoints. + +Good places to find interface definitions for bootstrapping connectivity are websites like +`directory.rns.recipes `_ and `rmap.world `_. + + +Build Personal Infrastructure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You do not need a datacenter to be a meaningful part of the Reticulum ecosystem. In fact, the most important nodes in the network are often the smallest ones. + +We strongly encourage everyone, even home users, to think in terms of building **personal infrastructure**. Don't connect every phone, tablet, and computer in your house directly to a public internet gateway. Instead, repurpose an old computer, a Raspberry Pi, or a supported router to act as your own, personal **Transport Node**: + +* Your local Transport Node sits in your home, connected to your WiFi and perhaps a radio interface (like an RNode). +* You configure this node with a ``bootstrap_only`` interface (perhaps a TCP tunnel to a wider network) and enable interface discovery. +* While you sleep, work, or cook, your node listens to the network. It discovers other local community members, validates their Network Identities, and automatically establishes direct links. +* Your personal devices now connect to your *local* node, which is integrated into a living, breathing local mesh. Your traffic flows through local paths provided by other real people in the community rather than bouncing off a distant server. + +**Don't wait for others to build the networks you want to see**. Every network is important, perhaps even most so those that support individual families and persons. Once enough of this personal, local infrastructure exist, connecting them directly to each other, without traversing the public Internet, becomes inevitable. + + +Mixing Strategies +^^^^^^^^^^^^^^^^^ + +There is no requirement to commit to a single strategy. The most robust setups often mix static, dynamic, and discovered interfaces. + +* **Static Interfaces:** You maintain a permanent interface to a trusted friend or organization using a static configuration. +* **Bootstrap Links:** You connect a ``bootstrap_only`` interface to a public gateway on the Internet to scan for new connectable peers or to regain connectivity if your other interfaces fail. +* **Local Wide-Area Connectivity:** You run a ``RNodeInterface`` on a shared frequency, giving you completely self-sovereign and private wide-area access to both your own network and other Reticulum peers globally, without any "service providers" being able to control or monitor how you interact with people. + +By combining these methods, you create a system that is secure against single points of failure, adaptable to changing network conditions, and better integrated into your physical and social reality. + + +Network Health & Responsibility +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As you participate in the wider networks you discover and build, you will inevitably encounter peers that are misconfigured, malicious, or simply broken. To protect your resources and those of your local peers, you can utilize the :ref:`Blackhole Management` system. + +Whether you manually block a spamming identity or subscribe to a blackhole list maintained by a trusted Network Identity, these tools help ensure that *your* transport capacity is used for what *you* consider legitimate communication. This keeps your local segment efficient and contributes to the health of the wider network. + + +Contributing to the Global Ret +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you have the means to host a stable node with a public IP address, consider becoming a :ref:`Public Entrypoint`. By :ref:`publishing your interface as discoverable`, you provide a potential connection point for others, helping the network grow and reach new areas. + +For guidelines on how to properly configure a public entrypoint, refer to the :ref:`Hosting Public Entrypoints` section. + +Connect to the Distributed Backbone +=================================== + +A global, distributed backbone of Reticulum Transport Nodes is being run by volunteers from around the world. This network constitutes a heterogenous collection of both public and private nodes that form an uncoordinated, voluntary inter-networking backbone that currently provides global transport and internetworking capabilities for Reticulum. + +As a good starting point, you can find interface definitions for connecting your own networks to this backbone on websites such as `directory.rns.recipes `_ and `rmap.world `_. + +.. tip:: + Don't rely on just a single connection to the distributed backbone for everyday use. It is much better to have several redundant connections configured, and enable the interface discovery options, so your nodes can continously discover peering opportunities as the network evolves. Refer to the :ref:`Bootstrapping Connectivity` section to understand the options. + + + +.. _hosting-entrypoints: + +Hosting Public Entrypoints +========================== + +If you want to help build a strong global interconnection backbone, you can host a public (or private) entry-point to a Reticulum network over the +Internet. This section offers some helpful pointers. Once you have set up your public entrypoint, it is a great idea to :ref:`make it discoverable over Reticulum`. + +You will need a machine, physical or virtual with a public IP address, that can be reached by other devices on the Internet. + +The most efficient and performant way to host a connectable entry-point supporting many +users is to use the ``BackboneInterface``. This interface type is fully compatible with +the ``TCPClientInterface`` and ``TCPServerInterface`` types, but much faster and uses +less system resources, allowing your device to handle thousands of connections even on +small systems. + +It is also important to set your connectable interface to ``gateway`` mode, since this +will greatly improve network convergence time and path resolution for anyone connecting +to your entry-point. + +.. code:: ini + + # This example demonstrates a backbone interface + # configured for acting as a gateway for users to + # connect to either a public or private network + + [[Public Gateway]] + type = BackboneInterface + enabled = yes + mode = gateway + listen_on = 0.0.0.0 + port = 4242 + + # On publicly available interfaces, it can be + # a good idea to configure sensible announce + # rate targets. + announce_rate_target = 3600 + announce_rate_penalty = 3600 + announce_rate_grace = 12 + +If instead you want to make a private entry-point from the Internet, you can use the +:ref:`IFAC name and passphrase options` to secure your interface with a network name and passphrase. + +.. code:: ini + + # A private entry-point requiring a pre-shared + # network name and passphrase to connect to. + + [[Private Gateway]] + type = BackboneInterface + enabled = yes + mode = gateway + listen_on = 0.0.0.0 + port = 4242 + network_name = private_ret + passphrase = 2owjajquafIanPecAc + +If you are hosting an entry-point on an operating system that does not support +``BackboneInterface``, you can use ``TCPServerInterface`` instead, although it will +not be as performant. + + +Connecting Reticulum Instances Over the Internet +================================================ +Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone`, :ref:`TCP` +and :ref:`I2P`. Each interface offers a different set of features, and Reticulum +users should carefully choose the interface which best suites their needs. + +The ``TCPServerInterface`` allows users to host an instance accessible over TCP/IP. This +method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``, +however it also leaks more data about the server host. + +The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating +systems, designed to handle thousands of connections simultaneously with low memory, processing +and I/O overhead. It is fully compatible with the TCP-based interface types. + +TCP connections reveal the IP address of both your instance and the server to anyone who can +inspect the connection. Someone could use this information to determine your location or identity. Adversaries +inspecting your packets may be able to record packet metadata like time of transmission and packet size. +Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use +packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it. +Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address, +which most Internet connections don't offer anymore. + +The ``I2PInterface`` routes messages through the `Invisible Internet Protocol +(I2P) `_. To use this interface, users must also run an I2P daemon in +parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd `_. + +By default, I2P will encrypt and mix all traffic sent over the Internet, and +hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node +will also relay other I2P user's encrypted packets, which will use extra +bandwidth and compute power, but also makes timing attacks and other forms of +deep-packet-inspection much more difficult. + +I2P also allows users to host globally available Reticulum instances from non-public IP's and behind firewalls and NAT. + +In general it is recommended to use an I2P node if you want to host a publicly accessible +instance, while preserving anonymity. If you care more about performance, and a slightly +easier setup, use TCP. + +Adding Radio Interfaces +======================= +Once you have Reticulum installed and working, you can add radio interfaces with +any compatible hardware you have available. Reticulum supports a wide range of radio +hardware, and if you already have any available, it is very likely that it will +work with Reticulum. For information on how to configure this, see the +:ref:`Interfaces` section of this manual. + +If you do not already have transceiver hardware available, you can easily and +cheaply build an :ref:`RNode`, which is a general-purpose long-range +digital radio transceiver, that integrates easily with Reticulum. + +To build one yourself requires installing a custom firmware on a supported LoRa +development board with an auto-install script or web-based flasher. +Please see the :ref:`Communications Hardware` chapter for a guide. +If you prefer purchasing a ready-made unit, you can refer to the +:ref:`list of suppliers`. + +Other radio-based hardware interfaces are being developed and made available by +the broader Reticulum community. You can find more information on such topics +over Reticulum-based information sharing systems. + +If you have communications hardware that is not already supported by any of the +:ref:`existing interface types`, it is easy to write (and potentially +publish) a :ref:`custom interface module` that makes it compatible with Reticulum. + + +Creating and Using Custom Interfaces +==================================== + +While Reticulum includes a flexible and broad range of built-in interfaces, these +will not cover every conceivable type of communications hardware that Reticulum +can potentially use to communicate. + +It is therefore possible to easily write your own interface modules, that can be +loaded at run-time and used on-par with any of the built-in interface types. + +For more information on this subject, and code examples to build on, please see +the :ref:`Configuring Interfaces` chapter. + + +Develop a Program with Reticulum +=========================================== +If you want to develop programs that use Reticulum, the easiest way to get +started is to install the latest release of Reticulum via pip: + +.. code:: + + pip install rns + +The above command will install Reticulum and dependencies, and you will be +ready to import and use RNS in your own programs. The next step will most +likely be to look at some :ref:`Example Programs`. + +The entire Reticulum API is documented in the :ref:`API Reference` +chapter of this manual. Before diving in, it's probably a good idea to read +this manual in full, but at least start with the :ref:`Understanding Reticulum` chapter. + + +.. _install-guides: + +Platform-Specific Install Notes +============================================== + +Some platforms require a slightly different installation procedure, or have +various quirks that are worth being aware of. These are listed here. + +Android +^^^^^^^^^^^^^^^^^^^^^^^^ +Reticulum can be used on Android in different ways. The easiest way to get +started is using an app like `Sideband `_. + +For more control and features, you can use Reticulum and related programs via +the `Termux app `_, at the time of writing available on +`F-droid `_. + +Termux is a terminal emulator and Linux environment for Android based devices, +which includes the ability to use many different programs and libraries, +including Reticulum. + +To use Reticulum within the Termux environment, you will need to install +``python`` and the ``python-cryptography`` library using ``pkg``, the package-manager +build into Termux. After that, you can use ``pip`` to install Reticulum. + +From within Termux, execute the following: + +.. code:: shell + + # First, make sure indexes and packages are up to date. + pkg update + pkg upgrade + + # Then install python and the cryptography library. + pkg install python python-cryptography + + # Make sure pip is up to date, and install the wheel module. + pip install wheel pip --upgrade + + # Install Reticulum + pip install rns + +If for some reason the ``python-cryptography`` package is not available for +your platform via the Termux package manager, you can attempt to build it +locally on your device using the following command: + +.. code:: shell + + # First, make sure indexes and packages are up to date. + pkg update + pkg upgrade + + # Then install dependencies for the cryptography library. + pkg install python build-essential openssl libffi rust + + # Make sure pip is up to date, and install the wheel module. + pip install wheel pip --upgrade + + # To allow the installer to build the cryptography module, + # we need to let it know what platform we are compiling for: + export CARGO_BUILD_TARGET="aarch64-linux-android" + + # Start the install process for the cryptography module. + # Depending on your device, this can take several minutes, + # since the module must be compiled locally on your device. + pip install cryptography + + # If the above installation succeeds, you can now install + # Reticulum and any related software + pip install rns + +It is also possible to include Reticulum in apps compiled and distributed as +Android APKs. A detailed tutorial and example source code will be included +here at a later point. Until then you can use the `Sideband source code `_ as an example and starting point. + + +ARM64 +^^^^^^^^^^^^^^^^^^^^^^^^ +On some architectures, including ARM64, not all dependencies have precompiled +binaries. On such systems, you may need to install ``python3-dev`` (or similar) before +installing Reticulum or programs that depend on Reticulum. + +.. code:: shell + + # Install Python and development packages + sudo apt update + sudo apt install python3 python3-pip python3-dev + + # Install Reticulum + python3 -m pip install rns + +With these packages installed, ``pip`` will be able to build any missing dependencies +on your system locally. + + +Debian Bookworm +^^^^^^^^^^^^^^^^^^^^^^^^ +On versions of Debian released after April 2023, it is no longer possible by default +to use ``pip`` to install packages onto your system. Unfortunately, you will need to +use the replacement ``pipx`` command instead, which places installed packages in an +isolated environment. This should not negatively affect Reticulum, but will not work +for including and using Reticulum in your own scripts and programs. + +.. code:: shell + + # Install pipx + sudo apt install pipx + + # Make installed programs available on the command line + pipx ensurepath + + # Install Reticulum + pipx install rns + +Alternatively, you can restore normal behaviour to ``pip`` by creating or editing +the configuration file located at ``~/.config/pip/pip.conf``, and adding the +following section: + +.. code:: ini + + [global] + break-system-packages = true + +For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages`` +option, you can use the following command: + +.. code:: shell + + pip install rns --break-system-packages + +.. note:: + The ``--break-system-packages`` directive is a somewhat misleading choice + of words. Setting it will of course not break any system packages, but will simply + allow installing ``pip`` packages user- and system-wide. While this *could* in rare + cases lead to version conflicts, it does not generally pose any problems, especially + not in the case of installing Reticulum. + + +MacOS +^^^^^^^^^^^^^^^^^^^^^^^^^ +To install Reticulum on macOS, you will need to have Python and the ``pip`` package +manager installed. + +Systems running macOS can vary quite widely in whether or not Python is pre-installed, +and if it is, which version is installed, and whether the ``pip`` package manager is +also installed and set up. If in doubt, you can `download and install `_ +Python manually. + +When Python and ``pip`` is available on your system, simply open a terminal window +and use one of the following commands: + +.. code:: shell + + # Install Reticulum and utilities with pip: + pip3 install rns + + # On some versions, you may need to use the + # flag --break-system-packages to install: + pip3 install rns --break-system-packages + +.. note:: + The ``--break-system-packages`` directive is a somewhat misleading choice + of words. Setting it will of course not break any system packages, but will simply + allow installing ``pip`` packages user- and system-wide. While this *could* in rare + cases lead to version conflicts, it does not generally pose any problems, especially + not in the case of installing Reticulum. + +Additionally, some version combinations of macOS and Python require you to +manually add your installed ``pip`` packages directory to your `PATH` environment +variable, before you can use installed commands in your terminal. Usually, adding +the following line to your shell init script (for example ``~/.zshrc``) will be enough: + +.. code:: shell + + export PATH=$PATH:~/Library/Python/3.9/bin + +Adjust Python version and shell init script location according to your system. + + +OpenWRT +^^^^^^^^^^^^^^^^^^^^^^^^^ +On OpenWRT systems with sufficient storage and memory, you can install +Reticulum and related utilities using the `opkg` package manager and `pip`. + +.. note:: + + At the time of releasing this manual, work is underway to create pre-built + Reticulum packages for OpenWRT, with full configuration, service + and ``uci`` integration. Please see the `feed-reticulum `_ + and `reticulum-openwrt `_ + repositories for more information. + +To install Reticulum on OpenWRT, first log into a command line session, and +then use the following instructions: + +.. code:: shell + + # Install dependencies + opkg install python3 python3-pip python3-cryptography python3-pyserial + + # Install Reticulum + pip install rns + + # Start rnsd with debug logging enabled + rnsd -vvv + +.. note:: + + The above instructions have been verified and tested on OpenWRT 21.02 only. + It is likely that other versions may require slightly altered installation + commands or package names. You will also need enough free space in your + overlay FS, and enough free RAM to actually run Reticulum and any related + programs and utilities. + +Depending on your device configuration, you may need to adjust firewall rules +for Reticulum connectivity to and from your device to work. Until proper +packaging is ready, you will also need to manually create a service or startup +script to automatically laucnh Reticulum at boot time. + +Please also note that the `AutoInterface` requires link-local IPv6 addresses +to be enabled for any Ethernet and WiFi devices you intend to use. If ``ip a`` +shows an address starting with ``fe80::`` for the device in question, +``AutoInterface`` should work for that device. + +Raspberry Pi +^^^^^^^^^^^^^^^^^^^^^^^^^ +It is currently recommended to use a 64-bit version of the Raspberry Pi OS +if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions +don't always have packages available for some dependencies. If Python and the +`pip` package manager is not already installed, do that first, and then +install Reticulum using `pip`. + +.. code:: shell + + # Install dependencies + sudo apt install python3 python3-pip python3-cryptography python3-pyserial + + # Install Reticulum + pip install rns --break-system-packages + +.. note:: + The ``--break-system-packages`` directive is a somewhat misleading choice + of words. Setting it will of course not break any system packages, but will simply + allow installing ``pip`` packages user- and system-wide. While this *could* in rare + cases lead to version conflicts, it does not generally pose any problems, especially + not in the case of installing Reticulum. + +While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes, +it will require manually configuring and installing required build dependencies, +and is not detailed in this manual. + + +RISC-V +^^^^^^^^^^^^^^^^^^^^^^^^ +On some architectures, including RISC-V, not all dependencies have precompiled +binaries. On such systems, you may need to install ``python3-dev`` (or similar) before +installing Reticulum or programs that depend on Reticulum. + +.. code:: shell + + # Install Python and development packages + sudo apt update + sudo apt install python3 python3-pip python3-dev + + # Install Reticulum + python3 -m pip install rns + +With these packages installed, ``pip`` will be able to build any missing dependencies +on your system locally. + + +Ubuntu Lunar +^^^^^^^^^^^^^^^^^^^^^^^^ +On versions of Ubuntu released after April 2023, it is no longer possible by default +to use ``pip`` to install packages onto your system. Unfortunately, you will need to +use the replacement ``pipx`` command instead, which places installed packages in an +isolated environment. This should not negatively affect Reticulum, but will not work +for including and using Reticulum in your own scripts and programs. + +.. code:: shell + + # Install pipx + sudo apt install pipx + + # Make installed programs available on the command line + pipx ensurepath + + # Install Reticulum + pipx install rns + +Alternatively, you can restore normal behaviour to ``pip`` by creating or editing +the configuration file located at ``~/.config/pip/pip.conf``, and adding the +following section: + +.. code:: text + + [global] + break-system-packages = true + +For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages`` +option, you can use the following command: + +.. code:: text + + pip install rns --break-system-packages + +.. note:: + The ``--break-system-packages`` directive is a somewhat misleading choice + of words. Setting it will of course not break any system packages, but will simply + allow installing ``pip`` packages user- and system-wide. While this *could* in rare + cases lead to version conflicts, it does not generally pose any problems, especially + not in the case of installing Reticulum. + +Windows +^^^^^^^^^^^^^^^^^^^^^^^^^ +On Windows operating systems, the easiest way to install Reticulum is by using the +``pip`` package manager from the command line (either the command prompt or Windows +Powershell). + +If you don't already have Python installed, `download and install Python `_. +At the time of publication of this manual, the recommended version is `Python 3.12.7 `_. + +**Important!** When asked by the installer, make sure to add the Python program to +your PATH environment variables. If you don't do this, you will not be able to +use the ``pip`` installer, or run the included Reticulum utility programs (such as +``rnsd`` and ``rnstatus``) from the command line. + +After installing Python, open the command prompt or Windows Powershell, and type: + +.. code:: shell + + pip install rns + +You can now use Reticulum and all included utility programs directly from your +preferred command line interface. + +Pure-Python Reticulum +============================================== + +.. warning:: + If you use the ``rnspure`` package to run Reticulum on systems that + do not support `PyCA/cryptography `_, it is + important that you read and understand the :ref:`Cryptographic Primitives ` + section of this manual. + +In some rare cases, and on more obscure system types, it is not possible to +install one or more dependencies. In such situations, +you can use the ``rnspure`` package instead of the ``rns`` package, or use ``pip`` +with the ``--no-dependencies`` command-line option. The ``rnspure`` +package requires no external dependencies for installation. Please note that the +actual contents of the ``rns`` and ``rnspure`` packages are *completely identical*. +The only difference is that the ``rnspure`` package lists no dependencies required +for installation. + +No matter how Reticulum is installed and started, it will load external dependencies +only if they are *needed* and *available*. If for example you want to use Reticulum +on a system that cannot support ``pyserial``, it is perfectly possible to do so using +the `rnspure` package, but Reticulum will not be able to use serial-based interfaces. +All other available modules will still be loaded when needed. diff --git a/docs/manual/_sources/hardware.rst.txt b/docs/manual/_sources/hardware.rst.txt new file mode 100644 index 0000000..443043e --- /dev/null +++ b/docs/manual/_sources/hardware.rst.txt @@ -0,0 +1,375 @@ +.. _hardware-main: + +*********************** +Communications Hardware +*********************** + +One of the truly valuable aspects of Reticulum is the ability to use it over +almost any conceivable kind of communications medium. The :ref:`interface types` +available for configuration in Reticulum are flexible enough to cover the use +of most wired and wireless communications hardware available, from decades-old +packet radio modems to modern millimeter-wave backhaul systems. + +If you already have or operate some kind of communications hardware, there is a +very good chance that it will work with Reticulum out of the box. In case it does +not, it is possible to provide the necessary glue with very little effort using +for example the :ref:`PipeInterface` or the :ref:`TCPClientInterface` +in combination with code like `TCP KISS Server `_ +by `simplyequipped `_. + +It is also very easy to write and load :ref:`custom interface modules` +into Reticulum, allowing you to communicate with more or less anything you can think of. + +While this broad support and flexibility is very useful, an abundance of options +can sometimes make it difficult to know where to begin, especially when you are +starting from scratch. + +This chapter will outline a few different sensible starting paths to get +real-world functional wireless communications up and running with minimal cost +and effort. Two fundamental devices categories will be covered, *RNodes* and +*WiFi-based radios*. Additionally, other common options will be briefly described. + +Knowing how to employ just a few different types of hardware will make it possible +to build a wide range of useful networks with little effort. + +Combining Hardware Types +======================== + +It is useful to combine different link and hardware types when designing and +building a network. One useful design pattern is to employ high-capacity point-to-point +links based on WiFi or millimeter-wave radios (with high-gain directional antennas) +for the network backbone, and using LoRa-based RNodes for covering large areas with +connectivity for client devices. + + +.. _rnode-main: + +RNode +===== + +Reliable and general-purpose long-range digital radio transceiver systems are +commonly either very expensive, difficult to set up and operate, hard to source, +power-hungry, or all of the above at the same time. In an attempt to alleviate +this situation, the transceiver system *RNode* was designed. It is important to +note that RNode is not one specific device, from one particular vendor, but +*an open plaform* that anyone can use to build interoperable digital transceivers +suited to their needs and particular situations. + +An RNode is a general purpose, interoperable, low-power and long-range, reliable, +open and flexible radio communications device. Depending on its components, it can +operate on many different frequency bands, and use many different modulation +schemes, but most commonly, and for the purposes of this chapter, we will limit +the discussion to RNodes using *LoRa* modulation in common ISM bands. + +**Avoid Confusion!** RNodes can use LoRa as a *physical-layer modulation*, but it +does not use, and has nothing to do with the *LoRaWAN* protocol and standard, commonly +used for centrally controlled IoT devices. RNodes use *raw LoRa modulation*, without +any additional protocol overhead. All high-level protocol functionality is handled +directly by Reticulum. + +.. _rnode-creating: + +Creating RNodes +^^^^^^^^^^^^^^^ +RNode has been designed as a system that is easy to replicate across time and +space. You can put together a functioning transceiver using commonly available +components, and a few open source software tools. While you can design and build RNodes +completely from scratch, to your exact desired specifications, this chapter +will explain the easiest possible approach to creating RNodes: Using common +LoRa development boards. This approach can be boiled down to two simple steps: + +1. Obtain one or more :ref:`supported development boards` +2. Install the RNode firmware with the :ref:`automated installer` + +Once the firmware has been installed and provisioned by the install script, it +is ready to use with any software that supports RNodes, including Reticulum. +The device can be used with Reticulum by adding an :ref:`RNodeInterface` +to the configuration. + +.. _rnode-supported: + +Supported Boards and Devices +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +To create one or more RNodes, you will need to obtain supported development +boards or completed devices. The following boards and devices are supported +by the auto-installer. + +------------ + +.. image:: graphics/board_tbeam_supreme.png + :width: 75% + :align: center + +LilyGO T-Beam Supreme +""""""""""""" +- **Transceiver IC** Semtech SX1262 or SX1268 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_tbeam.png + :width: 75% + :align: center + +LilyGO T-Beam +""""""""""""" +- **Transceiver IC** Semtech SX1262, SX1268, SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_t3s3.png + :width: 50% + :align: center + +LilyGO T3S3 +""""""""""" +- **Transceiver IC** Semtech SX1262, SX1268, SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_rak4631.png + :width: 45% + :align: center + +RAK4631-based Boards +"""""""""""""""""""" +- **Transceiver IC** Semtech SX1262 or SX1268 +- **Device Platform** nRF52 +- **Manufacturer** `RAK Wireless `_ + +------------ + +.. image:: graphics/board_opencomxl.png + :width: 45% + :align: center + +OpenCom XL +"""""""""""""""""""" +- **Transceiver ICs** Semtech SX1262 and SX1280 (dual transceiver) +- **Device Platform** nRF52 +- **Manufacturer** `Liberated Embedded Systems `_ + +------------ + +.. image:: graphics/board_rnodev2.png + :width: 68% + :align: center + +Unsigned RNode v2.x +""""""""""""""""""" +- **Transceiver IC** Semtech SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `unsigned.io `_ + +------------ + +.. image:: graphics/board_t3v21.png + :width: 46% + :align: center + +LilyGO LoRa32 v2.1 +"""""""""""""""""" +- **Transceiver IC** Semtech SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_t3v20.png + :width: 46% + :align: center + +LilyGO LoRa32 v2.0 +"""""""""""""""""" +- **Transceiver IC** Semtech SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_t3v10.png + :width: 46% + :align: center + +LilyGO LoRa32 v1.0 +"""""""""""""""""" +- **Transceiver IC** Semtech SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_tdeck.png + :width: 45% + :align: center + +LilyGO T-Deck +""""""""""""" +- **Transceiver IC** Semtech SX1262 or SX1268 +- **Device Platform** ESP32 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_techo.png + :width: 45% + :align: center + +LilyGO T-Echo +""""""""""""" +- **Transceiver IC** Semtech SX1262 or SX1268 +- **Device Platform** nRF52 +- **Manufacturer** `LilyGO `_ + +------------ + +.. image:: graphics/board_t114.png + :width: 58% + :align: center + +Heltec T114 +""""""""""" +- **Transceiver IC** Semtech SX1262 or SX1268 +- **Device Platform** nRF52 +- **Manufacturer** `Heltec Automation `_ + +------------ + +.. image:: graphics/board_heltec32v4.png + :width: 58% + :align: center + +Heltec LoRa32 v4.0 +"""""""""""""""""" +- **Transceiver IC** Semtech SX1262 +- **Device Platform** ESP32 +- **Manufacturer** `Heltec Automation `_ + +------------ + +.. image:: graphics/board_heltec32v30.png + :width: 58% + :align: center + +Heltec LoRa32 v3.0 +"""""""""""""""""" +- **Transceiver IC** Semtech SX1262 or SX1268 +- **Device Platform** ESP32 +- **Manufacturer** `Heltec Automation `_ + +------------ + +.. image:: graphics/board_heltec32v20.png + :width: 58% + :align: center + +Heltec LoRa32 v2.0 +"""""""""""""""""" +- **Transceiver IC** Semtech SX1276 or SX1278 +- **Device Platform** ESP32 +- **Manufacturer** `Heltec Automation `_ + +------------ + +.. _rnode-installation: + +Installation +^^^^^^^^^^^^ + +Once you have obtained compatible boards, you can install the `RNode Firmware `_ +using the `RNode Configuration Utility `_. +If you have installed Reticulum on your system, the ``rnodeconf`` program will already be +available. If not, make sure that ``Python3`` and ``pip`` is installed on your system, and +then install Reticulum with with ``pip``: + +.. code:: + + pip install rns + +Once installation has completed, it is time to start installing the firmware on your +devices. Run ``rnodeconf`` in auto-install mode like so: + +.. code:: + + rnodeconf --autoinstall + +The utility will guide you through the installation process by asking a series of +questions about your hardware. Simply follow the guide, and the utility will +auto-install and configure your devices. + +.. _rnode-usage: + +Usage with Reticulum +^^^^^^^^^^^^^^^^^^^^ +When the devices have been installed and provisioned, you can use them with Reticulum +by adding the :ref:`relevant interface section` to the configuration +file of Reticulum. In the configuraion you can specify all interface parameters, +such as serial port and on-air parameters. + + +WiFi-based Hardware +=================== + +It is possible to use all kinds of both short- and long-range WiFi-based hardware +with Reticulum. Any kind of hardware that fully supports bridged Ethernet over the +WiFi interface will work with the :ref:`AutoInterface` in Reticulum. +Most devices will behave like this by default, or allow it via configuration options. + +This means that you can simply configure the physical links of the WiFi based devices, +and start communicating over them using Reticulum. It is not necessary to enable any IP +infrastructure such as DHCP servers, DNS or similar, as long as at least Ethernet is +available, and packets are passed transparently over the physical WiFi-based devices. + +.. only:: html + + .. image:: graphics/radio_rblhg5.png + :width: 49% + + .. image:: graphics/radio_is5ac.png + :width: 49% + +Below is a list of example WiFi (and similar) radios that work well for high capacity +Reticulum links over long distances: + +- `Ubiquiti airMAX radios `_ +- `Ubiquiti LTU radios `_ +- `MikroTik radios `_ + +This list is by no means exhaustive, and only serves as a few examples of radio hardware +that is relatively cheap while providing long range and high capacity for Reticulum +networks. As in all other cases, it is also possible for Reticulum to co-exist with IP +networks running concurrently on such devices. + +Ethernet-based Hardware +======================= + +Reticulum can run over any kind of hardware that can provide a switched Ethernet-based +medium. This means that anything from a plain Ethernet switch, to fiber-optic systems, +to data radios with Ethernet interfaces can be used by Reticulum. + +The Ethernet medium does not need to have any IP infrastructure such as DHCP servers +or routing set up, but in case such infrastructure does exist, Reticulum will simply +co-exist with. + +To use Reticulum over Ethernet-based mediums, it is generally enough to use the included +:ref:`AutoInterface`. This interface also works over any kind of +virtual networking adapter, such as ``tun`` and ``tap`` devices in Linux. + +Serial Lines & Devices +====================== + +Using Reticulum over any kind of raw serial line is also possible with the +:ref:`SerialInterface`. This interface type is also useful for +using Reticulum over communications hardware that provides a serial port interface. + +Packet Radio Modems +=================== + +Any packet radio modem that provides a standard KISS interface over USB, serial or TCP +can be used with Reticulum. This includes virtual software modems such as +`FreeDV TNC `_ and `Dire Wolf `_. diff --git a/docs/manual/_sources/index.rst.txt b/docs/manual/_sources/index.rst.txt new file mode 100644 index 0000000..0d70d95 --- /dev/null +++ b/docs/manual/_sources/index.rst.txt @@ -0,0 +1,46 @@ +****************************** +Reticulum Network Stack Manual +****************************** + +This manual aims to provide you with all the information you need to +understand Reticulum, build networks or develop programs using it, or +to participate in the development of Reticulum itself. + +.. only:: builder_html + + This manual is also available in `PDF `_ and `EPUB `_ formats. + +.. only:: builder_html + + Table Of Contents + ================= + +.. toctree:: + :maxdepth: 3 + + whatis + gettingstartedfast + zen + software + using + understanding + hardware + interfaces + networks + support + examples + license + +.. toctree:: + :maxdepth: 2 + + reference + + +.. only:: html + + Indices and Tables + ================== + + * :ref:`genindex` + * :ref:`search` diff --git a/docs/manual/_sources/interfaces.rst.txt b/docs/manual/_sources/interfaces.rst.txt new file mode 100644 index 0000000..67f6dae --- /dev/null +++ b/docs/manual/_sources/interfaces.rst.txt @@ -0,0 +1,1404 @@ + +.. _interfaces-main: + +********************** +Configuring Interfaces +********************** + +Reticulum supports using many kinds of devices as networking interfaces, and +allows you to mix and match them in any way you choose. The number of distinct +network topologies you can create with Reticulum is more or less endless, but +common to them all is that you will need to define one or more *interfaces* +for Reticulum to use. + +The following sections describe the interfaces currently available in Reticulum, +and gives example configurations for the respective interface types. + +For a high-level overview of how networks can be formed over different interface +types, have a look at the :ref:`Building Networks` chapter of this +manual. + + +.. _interfaces-custom: + +Custom Interfaces +================= + +In addition to the built-in interface types, Reticulum is **fully extensible** with +custom, user- or community-supplied interfaces, and creating custom interface +modules is straightforward. Please see the :ref:`custom interface` +example for basic interface code to build upon. + +.. _interfaces-auto: + +Auto Interface +============== + +The ``AutoInterface`` enables communication with other discoverable Reticulum +nodes over any kind of local Ethernet or WiFi-based medium. Even though it uses IPv6 for peer +discovery, and UDP for packet transport, it **does not** need any functional IP +infrastructure like routers or DHCP servers, on your physical network. + +.. warning:: + If you have **firewall** software running on your computer, it may block traffic + required for ``AutoInterface`` to work. If this is the case, you will have to + allow UDP traffic on port ``29716`` and ``42671``. + +As long as there is at least some sort of switching medium present between peers (a +wired switch, a hub, a WiFi access point or similar, or simply two devices connected +directly by Ethernet cable), it will work without any configuration, setup or intermediary devices. + +For ``AutoInterface`` peer discovery to work, it's also required that link-local +IPv6 support is available on your system, which it should be by default in all +current operating systems, both desktop and mobile. + +.. note:: + Almost all current Ethernet and WiFi hardware will work without any kind + of configuration or setup with ``AutoInterface``, but a small subset of + devices turn on options that limit device-to-device communication by default, + resulting in ``AutoInterface`` peer discovery being blocked. This issue is + most commonly seen on very cheap, ISP-supplied WiFi routers, and can sometimes + be turned off in the router configuration. + +.. code:: ini + + # This example demonstrates a bare-minimum setup + # of an Auto Interface. It will allow communica- + # tion with all other reachable devices on all + # usable physical ethernet-based devices that + # are available on the system. + [[Default Interface]] + type = AutoInterface + enabled = yes + + # This example demonstrates an more specifically + # configured Auto Interface, that only uses spe- + # cific physical interfaces, and has a number of + # other configuration options set. + [[Default Interface]] + type = AutoInterface + enabled = yes + + # You can create multiple isolated Reticulum + # networks on the same physical LAN by + # specifying different Group IDs. + group_id = reticulum + + # You can also choose the multicast address type: + # temporary (default, Temporary Multicast Address) + # or permanent (Permanent Multicast Address) + multicast_address_type = permanent + + # You can also select specifically which + # kernel networking devices to use. + devices = wlan0,eth1 + + # Or let AutoInterface use all suitable + # devices except for a list of ignored ones. + ignored_devices = tun0,eth0 + + +If you are connected to the Internet with IPv6, and your provider will route +IPv6 multicast, you can potentially configure the Auto Interface to globally +autodiscover other Reticulum nodes within your selected Group ID. You can specify +the discovery scope by setting it to one of ``link``, ``admin``, ``site``, +``organisation`` or ``global``. + +.. code:: ini + + [[Default Interface]] + type = AutoInterface + enabled = yes + + # Configure global discovery + + group_id = custom_network_name + discovery_scope = global + + # Other configuration options + + discovery_port = 48555 + data_port = 49555 + + +.. _interfaces-backbone: + +Backbone Interface +==================== + +The Backbone interface is a very fast and resource efficient interface type, primarily +intended for interconnecting Reticulum instances over many different types of mediums. +It uses a kernel-event I/O backend, and can handle thousands of interfaces and/or clients +with relatively low system resource utilisation. **This interface type is currently only +supported on Linux and Android**. + +.. note:: + The Backbone Interface is fully compatible with the ``TCPServerInterface`` and ``TCPClientInterface`` + types, and they can be used interchangably, and cross-connect with each other. On systems that support + ``BackboneInterface``, it is generally recommended to use it, unless you need specific options or + features that the TCP server and client interfaces provide. + +While the goal is to support *all* socket types and I/O devices provided by the underlying +operating system, the initial release only provides support for TCP connections over IPv4 +and IPv6. + +For all types of connections over a ``BackboneInterface``, Reticulum will gracefully +handle intermittency, link loss, and connections that come and go. + +Listeners +--------- + +The following examples illustrates various ways to set up ``BackboneInterface`` listeners. + +.. code:: ini + + # This example demonstrates a backbone interface + # that listens for incoming connections on the + # specified IP address and port number. + [[Backbone Listener]] + type = BackboneInterface + enabled = yes + listen_on = 0.0.0.0 + port = 4242 + + # Alternatively you can bind to a specific IP + [[Backbone Listener]] + type = BackboneInterface + enabled = yes + listen_on = 10.0.0.88 + port = 4242 + + # Or a specific network device + [[Backbone Listener]] + type = BackboneInterface + enabled = yes + device = eth0 + port = 4242 + +If you are using the interface on a device which has both IPv4 and IPv6 addresses available, +you can use the ``prefer_ipv6`` option to bind to the IPv6 address: + +.. code:: ini + + # This example demonstrates a backbone interface + # listening on the IPv6 address of a specified + # kernel networking device. + [[Backbone Listener]] + type = BackboneInterface + enabled = yes + prefer_ipv6 = yes + device = eth0 + port = 4242 + +To use the ``BackboneInterface`` over `Yggdrasil `_, you +can simply specify the Yggdrasil ``tun`` device and a listening port, like so: + +.. code:: ini + + # This example demonstrates a backbone interface + # listening for connections over Yggdrasil. + [[Yggdrasil Backbone Interface]] + type = BackboneInterface + enabled = yes + device = tun0 + port = 4343 + +Connecting Remotes +------------------ +The following examples illustrates various ways to connect to remote ``BackboneInterface`` listeners. +As noted above, ``BackboneInterface`` interfaces can also connect to remote ``TCPServerInterface``, +and as such these interface types can be used interchangably. + +.. code:: ini + + # Here's an example of a backbone interface that + # connects to a remote listener. + [[Backbone Remote]] + type = BackboneInterface + enabled = yes + remote = amsterdam.connect.reticulum.network + target_port = 4251 + +To connect to remotes over `Yggdrasil `_, simply +specify the target Yggdrasil IPv6 address and port, like so: + +.. code:: ini + + [[Yggdrasil Remote]] + type = BackboneInterface + enabled = yes + target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5 + target_port = 4343 + +.. _interfaces-tcps: + +TCP Server Interface +==================== + +The TCP Server interface is suitable for allowing other peers to connect over +the Internet or private IPv4 and IPv6 networks. When a TCP server interface has been +configured, other Reticulum peers can connect to it with a TCP Client interface. + +.. code:: ini + + # This example demonstrates a TCP server interface. + # It will listen for incoming connections on all IP + # interfaces on port 4242. + [[TCP Server Interface]] + type = TCPServerInterface + enabled = yes + listen_ip = 0.0.0.0 + listen_port = 4242 + + # Alternatively you can bind to a specific IP + [[TCP Server Interface]] + type = TCPServerInterface + enabled = yes + listen_ip = 10.0.0.88 + listen_port = 4242 + + # Or a specific network device + [[TCP Server Interface]] + type = TCPServerInterface + enabled = yes + device = eth0 + listen_port = 4242 + +If you are using the interface on a device which has both IPv4 and IPv6 addresses available, +you can use the ``prefer_ipv6`` option to bind to the IPv6 address: + +.. code:: ini + + # This example demonstrates a TCP server interface. + # It will listen for incoming connections on the + # specified IP address and port number. + + [[TCP Server Interface]] + type = TCPServerInterface + enabled = yes + prefer_ipv6 = True + device = eth0 + port = 4242 + +To use the TCP Server Interface over `Yggdrasil `_, you +can simply specify the Yggdrasil ``tun`` device and a listening port, like so: + +.. code:: ini + + [[Yggdrasil TCP Server Interface]] + type = TCPServerInterface + enabled = yes + device = tun0 + listen_port = 4343 + +.. note:: + The TCP interfaces support tunneling over I2P, but to do so reliably, + you must use the i2p_tunneled option: + +.. code:: + + [[TCP Server on I2P]] + type = TCPServerInterface + enabled = yes + listen_ip = 127.0.0.1 + listen_port = 5001 + i2p_tunneled = yes + +In almost all cases, it is easier to use the dedicated ``I2PInterface``, but for complete +control, and using I2P routers running on external systems, this option also exists. + +.. _interfaces-tcpc: + +TCP Client Interface +==================== + +To connect to a TCP server interface, you can use the TCP client +interface. Many TCP Client interfaces from different peers can connect to the +same TCP Server interface at the same time. + +The TCP interface types can also tolerate intermittency in the IP link layer. +This means that Reticulum will gracefully handle IP links that go up and down, +and restore connectivity after a failure, once the other end of a TCP interface reappears. + +.. code:: ini + + # Here's an example of a TCP Client interface. The + # target_host can be a hostname or an IPv4 or IPv6 address. + [[TCP Client Interface]] + type = TCPClientInterface + enabled = yes + target_host = 127.0.0.1 + target_port = 4242 + +To use the TCP Client Interface over `Yggdrasil `_, simply +specify the target Yggdrasil IPv6 address and port, like so: + +.. code:: ini + + [[Yggdrasil TCP Client Interface]] + type = TCPClientInterface + enabled = yes + target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5 + target_port = 4343 + +It is also possible to use this interface type to connect via other programs +or hardware devices that expose a KISS interface on a TCP port, for example +software-based soundmodems. To do this, use the ``kiss_framing`` option: + +.. code:: ini + + # Here's an example of a TCP Client interface that connects + # to a software TNC soundmodem on a KISS over TCP port. + + [[TCP KISS Interface]] + type = TCPClientInterface + enabled = yes + kiss_framing = True + target_host = 127.0.0.1 + target_port = 8001 + fixed_mtu = 500 + +**Caution!** Only use the KISS framing option when connecting to external devices +and programs like soundmodems and similar over TCP. When using the +``TCPClientInterface`` in conjunction with the ``TCPServerInterface`` you should +never enable ``kiss_framing``, since this will disable internal reliability and +recovery mechanisms that greatly improves performance over unreliable and +intermittent TCP links. + +For KISS devices that need only supports a particular MTU, you can use the +``fixed_mtu`` option. + +.. note:: + The TCP interfaces support tunneling over I2P, but to do so reliably, + you must use the i2p_tunneled option: + +.. code:: ini + + [[TCP Client over I2P]] + type = TCPClientInterface + enabled = yes + target_host = 127.0.0.1 + target_port = 5001 + i2p_tunneled = yes + + +.. _interfaces-udp: + +UDP Interface +============= + +A UDP interface can be useful for communicating over IP networks, both +private and the internet. It can also allow broadcast communication +over IP networks, so it can provide an easy way to enable connectivity +with all other peers on a local area network. + +.. warning:: + Using broadcast UDP traffic has performance implications, + especially on WiFi. If your goal is simply to enable easy communication + with all peers in your local Ethernet broadcast domain, the + :ref:`Auto Interface` performs *much* better, and is even + easier to use. + +.. code:: ini + + # This example enables communication with other + # local Reticulum peers over UDP. + + [[UDP Interface]] + type = UDPInterface + enabled = yes + + listen_ip = 0.0.0.0 + listen_port = 4242 + forward_ip = 255.255.255.255 + forward_port = 4242 + + # The above configuration will allow communication + # within the local broadcast domains of all local + # IP interfaces. + + # Instead of specifying listen_ip, listen_port, + # forward_ip and forward_port, you can also bind + # to a specific network device like below. + + # device = eth0 + # port = 4242 + + # Assuming the eth0 device has the address + # 10.55.0.72/24, the above configuration would + # be equivalent to the following manual setup. + # Note that we are both listening and forwarding to + # the broadcast address of the network segments. + + # listen_ip = 10.55.0.255 + # listen_port = 4242 + # forward_ip = 10.55.0.255 + # forward_port = 4242 + + # You can of course also communicate only with + # a single IP address + + # listen_ip = 10.55.0.15 + # listen_port = 4242 + # forward_ip = 10.55.0.16 + # forward_port = 4242 + + +.. _interfaces-i2p: + +I2P Interface +============= + +The I2P interface lets you connect Reticulum instances over the +`Invisible Internet Protocol `_. This can be +especially useful in cases where you want to host a globally reachable +Reticulum instance, but do not have access to any public IP addresses, +have a frequently changing IP address, or have firewalls blocking +inbound traffic. + +Using the I2P interface, you will get a globally reachable, portable +and persistent I2P address that your Reticulum instance can be reached +at. + +To use the I2P interface, you must have an I2P router running +on your system. The easiest way to achieve this is to download and +install the `latest release `_ +of the ``i2pd`` package. For more details about I2P, see the +`geti2p.net website `_. + +When an I2P router is running on your system, you can simply add +an I2P interface to Reticulum: + +.. code:: ini + + [[I2P]] + type = I2PInterface + enabled = yes + connectable = yes + +On the first start, Reticulum will generate a new I2P address for the +interface and start listening for inbound traffic on it. This can take +a while the first time, especially if your I2P router was also just +started, and is not yet well-connected to the I2P network. When ready, +you should see I2P base32 address printed to your log file. You can +also inspect the status of the interface using the ``rnstatus`` utility. + +To connect to other Reticulum instances over I2P, just add a comma-separated +list of I2P base32 addresses to the ``peers`` option of the interface: + +.. code:: ini + + [[I2P]] + type = I2PInterface + enabled = yes + connectable = yes + peers = 5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq.b32.i2p + +It can take anywhere from a few seconds to a few minutes to establish +I2P connections to the desired peers, so Reticulum handles the process +in the background, and will output relevant events to the log. + +.. note:: + While the I2P interface is the simplest way to use + Reticulum over I2P, it is also possible to tunnel the TCP server and + client interfaces over I2P manually. This can be useful in situations + where more control is needed, but requires manual tunnel setup through + the I2P daemon configuration. + +It is important to note that the two methods are *interchangably compatible*. +You can use the I2PInterface to connect to a TCPServerInterface that +was manually tunneled over I2P, for example. This offers a high degree +of flexibility in network setup, while retaining ease of use in simpler +use-cases. + + +.. _interfaces-rnode: + +RNode LoRa Interface +==================== + +To use Reticulum over LoRa, the `RNode `_ interface +can be used, and offers full control over LoRa parameters. + +.. warning:: + Radio frequency spectrum is a legally controlled resource, and legislation + varies widely around the world. It is your responsibility to be aware of any + relevant regulation for your location, and to make decisions accordingly. + +.. code:: ini + + # Here's an example of how to add a LoRa interface + # using the RNode LoRa transceiver. + + [[RNode LoRa Interface]] + type = RNodeInterface + + # Enable interface if you want use it! + enabled = yes + + # Serial port for the device + port = /dev/ttyUSB0 + + # You can connect wirelessly to the + # RNode device if it supports WiFi. + + # Connect by IP address + # port = tcp://10.0.0.1 + + # Or, connect by hostname + # port = tcp://rnodef3b9.local + + # It is also possible to use BLE devices + # instead of wired serial ports. The + # target RNode must be paired with the + # host device before connecting. BLE + # devices can be connected by name, + # BLE MAC address or by any available. + + # Connect to specific device by name + # port = ble://RNode 3B87 + + # Or by BLE MAC address + # port = ble://F4:12:73:29:4E:89 + + # Or connect to the first available, + # paired device + # port = ble:// + + # Set frequency to 867.2 MHz + frequency = 867200000 + + # Set LoRa bandwidth to 125 KHz + bandwidth = 125000 + + # Set TX power to 7 dBm (5 mW) + txpower = 7 + + # Select spreading factor 8. Valid + # range is 7 through 12, with 7 + # being the fastest and 12 having + # the longest range. + spreadingfactor = 8 + + # Select coding rate 5. Valid range + # is 5 throough 8, with 5 being the + # fastest, and 8 the longest range. + codingrate = 5 + + # You can configure the RNode to send + # out identification on the channel with + # a set interval by configuring the + # following two parameters. + + # id_callsign = MYCALL-0 + # id_interval = 600 + + # For certain homebrew RNode interfaces + # with low amounts of RAM, using packet + # flow control can be useful. By default + # it is disabled. + + # flow_control = False + + # It is possible to limit the airtime + # utilisation of an RNode by using the + # following two configuration options. + # The short-term limit is applied in a + # window of approximately 15 seconds, + # and the long-term limit is enforced + # over a rolling 60 minute window. Both + # options are specified in percent. + + # airtime_limit_long = 1.5 + # airtime_limit_short = 33 + + +.. _interfaces-rnode-multi: + +RNode Multi Interface +===================== + +For RNodes that support multiple LoRa transceivers, the RNode +Multi interface can be used to configure sub-interfaces individually. + +.. warning:: + Radio frequency spectrum is a legally controlled resource, and legislation + varies widely around the world. It is your responsibility to be aware of any + relevant regulation for your location, and to make decisions accordingly. + +.. code:: ini + + # Here's an example of how to add an RNode Multi interface + # using the RNode LoRa transceiver. + + [[RNode Multi Interface]] + type = RNodeMultiInterface + + # Enable interface if you want to use it! + enabled = yes + + # Serial port for the device + port = /dev/ttyACM0 + + # You can configure the RNode to send + # out identification on the channel with + # a set interval by configuring the + # following two parameters. + + # id_callsign = MYCALL-0 + # id_interval = 600 + + # A subinterface + [[[High Datarate]]] + # Subinterfaces can be enabled and disabled in of themselves + enabled = yes + + # Set frequency to 2.4GHz + frequency = 2400000000 + + # Set LoRa bandwidth to 1625 KHz + bandwidth = 1625000 + + # Set TX power to 0 dBm (0.12 mW) + txpower = 0 + + # The virtual port, only the manufacturer + # or the person who wrote the board config + # can tell you what it will be for which + # physical hardware interface + vport = 1 + + # Select spreading factor 5. Valid + # range is 5 through 12, with 5 + # being the fastest and 12 having + # the longest range. + spreadingfactor = 5 + + # Select coding rate 5. Valid range + # is 5 throough 8, with 5 being the + # fastest, and 8 the longest range. + codingrate = 5 + + # It is possible to limit the airtime + # utilisation of an RNode by using the + # following two configuration options. + # The short-term limit is applied in a + # window of approximately 15 seconds, + # and the long-term limit is enforced + # over a rolling 60 minute window. Both + # options are specified in percent. + + # airtime_limit_long = 100 + # airtime_limit_short = 100 + + [[[Low Datarate]]] + # Subinterfaces can be enabled and disabled in of themselves + enabled = yes + + # Set frequency to 865.6 MHz + frequency = 865600000 + + # The virtual port, only the manufacturer + # or the person who wrote the board config + # can tell you what it will be for which + # physical hardware interface + vport = 0 + + # Set LoRa bandwidth to 125 KHz + bandwidth = 125000 + + # Set TX power to 0 dBm (0.12 mW) + txpower = 0 + + # Select spreading factor 7. Valid + # range is 5 through 12, with 5 + # being the fastest and 12 having + # the longest range. + spreadingfactor = 7 + + # Select coding rate 5. Valid range + # is 5 throough 8, with 5 being the + # fastest, and 8 the longest range. + codingrate = 5 + + # It is possible to limit the airtime + # utilisation of an RNode by using the + # following two configuration options. + # The short-term limit is applied in a + # window of approximately 15 seconds, + # and the long-term limit is enforced + # over a rolling 60 minute window. Both + # options are specified in percent. + + # airtime_limit_long = 100 + # airtime_limit_short = 100 + +.. _interfaces-serial: + +Serial Interface +================ + +Reticulum can be used over serial ports directly, or over any device with a +serial port, that will transparently pass data. Useful for communicating +directly over a wire-pair, or for using devices such as data radios and lasers. + +.. code:: ini + + [[Serial Interface]] + type = SerialInterface + enabled = yes + + # Serial port for the device + port = /dev/ttyUSB0 + + # Set the serial baud-rate and other + # configuration parameters. + speed = 115200 + databits = 8 + parity = none + stopbits = 1 + +.. _interfaces-pipe: + +Pipe Interface +============== + +Using this interface, Reticulum can use any program as an interface via `stdin` and +`stdout`. This can be used to easily create virtual interfaces, or to interface with +custom hardware or other systems. + +.. code:: ini + + [[Pipe Interface]] + type = PipeInterface + enabled = yes + + # External command to execute + command = netcat -l 5757 + + # Optional respawn delay, in seconds + respawn_delay = 5 + +Reticulum will write all packets to `stdin` of the ``command`` option, and will +continuously read and scan its `stdout` for Reticulum packets. If ``EOF`` is reached, +Reticulum will try to respawn the program after waiting for ``respawn_interval`` seconds. + +.. _interfaces-kiss: + +KISS Interface +============== + +With the KISS interface, you can use Reticulum over a variety of packet +radio modems and TNCs, including `OpenModem `_. +KISS interfaces can also be configured to periodically send out beacons +for station identification purposes. + +.. warning:: + Radio frequency spectrum is a legally controlled resource, and legislation + varies widely around the world. It is your responsibility to be aware of any + relevant regulation for your location, and to make decisions accordingly. + +.. code:: ini + + [[Packet Radio KISS Interface]] + type = KISSInterface + enabled = yes + + # Serial port for the device + port = /dev/ttyUSB1 + + # Set the serial baud-rate and other + # configuration parameters. + speed = 115200 + databits = 8 + parity = none + stopbits = 1 + + # Set the modem preamble. + preamble = 150 + + # Set the modem TX tail. + txtail = 10 + + # Configure CDMA parameters. These + # settings are reasonable defaults. + persistence = 200 + slottime = 20 + + # You can configure the interface to send + # out identification on the channel with + # a set interval by configuring the + # following two parameters. The KISS + # interface will only ID if the set + # interval has elapsed since it's last + # actual transmission. The interval is + # configured in seconds. + # This option is commented out and not + # used by default. + # id_callsign = MYCALL-0 + # id_interval = 600 + + # Whether to use KISS flow-control. + # This is useful for modems that have + # a small internal packet buffer, but + # support packet flow control instead. + flow_control = false + +.. _interfaces-ax25: + +AX.25 KISS Interface +==================== + +If you're using Reticulum on amateur radio spectrum, you might want to +use the AX.25 KISS interface. This way, Reticulum will automatically +encapsulate it's traffic in AX.25 and also identify your stations +transmissions with your callsign and SSID. + +Only do this if you really need to! Reticulum doesn't need the AX.25 +layer for anything, and it incurs extra overhead on every packet to +encapsulate in AX.25. + +A more efficient way is to use the plain KISS interface with the +beaconing functionality described above. + +.. warning:: + Radio frequency spectrum is a legally controlled resource, and legislation + varies widely around the world. It is your responsibility to be aware of any + relevant regulation for your location, and to make decisions accordingly. + +.. code:: ini + + [[Packet Radio AX.25 KISS Interface]] + type = AX25KISSInterface + + # Set the station callsign and SSID + callsign = NO1CLL + ssid = 0 + + # Enable interface if you want use it! + enabled = yes + + # Serial port for the device + port = /dev/ttyUSB2 + + # Set the serial baud-rate and other + # configuration parameters. + speed = 115200 + databits = 8 + parity = none + stopbits = 1 + + # Set the modem preamble. A 150ms + # preamble should be a reasonable + # default, but may need to be + # increased for radios with slow- + # opening squelch and long TX/RX + # turnaround + preamble = 150 + + # Set the modem TX tail. In most + # cases this should be kept as low + # as possible to not waste airtime. + txtail = 10 + + # Configure CDMA parameters. These + # settings are reasonable defaults. + persistence = 200 + slottime = 20 + + # Whether to use KISS flow-control. + # This is useful for modems with a + # small internal packet buffer. + flow_control = false + +.. _interfaces-discoverable: + +Discoverable Interfaces +======================= + +Reticulum includes a powerful system for publishing your local interfaces to the wider network, allowing other peers to :ref:`discover, validate, and automatically connect to them`. This feature is particularly useful for creating decentralized networks where peers can dynamically find entrypoints, such as public Internet gateways or local radio access points, without relying on static configuration files or centralized directories. + +When an interface is made **discoverable**, your Reticulum instance will periodically broadcast an announce packet containing the connection details and parameters required for other peers to establish a connection. These announces are propagated over the network using the standard Reticulum announce mechanism using the ``rnstransport.discovery.interface`` destination type. + +.. note:: + To use the interface discovery functionality, the ``LXMF`` module must be installed in your Python environment. You can install it using pip: + + .. code:: sh + + pip install lxmf + + +Enabling Discovery +------------------ + +Interface discovery is enabled on a per-interface basis. To make a specific interface discoverable, you must add the ``discoverable`` option to that interface's configuration block and set it to ``yes``. + +.. code:: ini + + [[My Public Gateway]] + type = BackboneInterface + ... + discoverable = yes + +Once enabled, Reticulum will automatically handle the generation, signing, stamping, and broadcasting of the discovery announces. It is not *required* to enable Transport to publish interface discovery information, but for most use cases where you want others to connect to you, you will likely want ``enable_transport`` set to ``yes`` in the ``[reticulum]`` section of your configuration. + + +Discovery Parameters +-------------------- + +When ``discoverable`` is enabled, a variety of additional options become available to control how the interface is presented to the network. These parameters allow you to fine-tune the metadata, security requirements, and visibility of your interface. + +**Basic Metadata** + +``discovery_name`` + A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used. + +``announce_interval`` + The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour). + +**Connectivity Specification** + +``reachable_on`` + Specifies the address that remote peers should use to connect to this interface. + + * For TCP and Backbone interfaces, this is typically the public IP address or hostname. Do not include the port, this is fetched automatically from the interface. + * For I2P interfaces, this is usually the I2P ``b32`` address. This value is fetched automatically from the ``I2PInterface`` once it is up and connected to the I2P network, so you should not set this manually, unless you absolutely know what you're doing. + + **Dynamic Resolution:** This option also accepts a path to an external executable script or binary. If a path is provided, Reticulum will execute the script and use its ``stdout`` as the reachability address. This is useful for devices behind dynamic DNS, NATs, or complex cloud environments where the external IP is not known locally. The script must simply print the address to stdout and exit. + +.. note:: + When using an executable script for ``reachable_on``, Reticulum expects the script to output only the IP address or hostname to ``stdout``, followed by a newline character. Any additional output or errors may cause the resolution to fail. Ensure the script has executable permissions and is robust against temporary network failures. + +A minimal example of a script that resolves the externally available, public IP of an internet-connected system could look like this: + +.. code:: bash + + #!/bin/bash + curl -s ip.me + exit $? + +On a real system, you should make the script robust enough to deal with intermittent Internet or service failures, such that the script *always* returns a sensible value, or if not possible at least exits with a non-zero exit return code, so Reticulum knows the output is invalid. + +**Security & Cost** + +``discovery_stamp_value`` + Defines the proof-of-work difficulty for the cryptographic stamp included in the announce. This value acts as a cost barrier to prevent network flooding. The default value is ``14``. Increasing this value makes it computationally more expensive to generate an announce, which can be useful to prevent spam on very large networks, but it also increases CPU load on your system when generating announces. Stamps are cached, and only generated if interface information changes, or at instance restart. If you have the computational resources, it is generally advisable to use as high a stamp value as possible. + +**Privacy & Encryption** + +``discovery_encrypt`` + If set to ``yes``, the discovery announce payload will be encrypted. To decrypt the announce, remote peers must possess the *network identity* configured for your instance (see ``network_identity`` in the ``[reticulum]`` section). This allows you to publish private interfaces that are only discoverable to specific trusted networks. + +.. important:: + If you enable ``discovery_encrypt`` but do not configure a valid ``network_identity`` in the ``[reticulum]`` section of your configuration, Reticulum will abort the interface discovery announce. Encryption requires a valid network identity key to function. + +``publish_ifac`` + If set to ``yes``, the Interface Access Code (IFAC) name and passphrase for this interface will be included in the discovery announce. This allows peers to automatically configure the correct authentication parameters when connecting to the interface. + +**Physical Location** + +``latitude``, ``longitude``, ``height`` + Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters. + +**Radio Parameters** + +For physical radio interfaces like ``RNodeInterface`` or ``KISSInterface``, the following optional parameters allow you to broadcast the operating frequency and characteristics, allowing clients to verify compatibility before connecting: + +``discovery_frequency`` + The operating frequency in Hz. Auto-configured on RNode interfaces. Necessary on KISS-based radio interfaces and ``TCPClientInterfaces`` connecting to radio modems. + +``discovery_bandwidth`` + The signal bandwidth in Hz. Auto-configured on RNode interfaces. Useful on KISS-based radio interfaces and ``TCPClientInterfaces`` connecting to radio modems. + +``discovery_modulation`` + The modulation type or scheme. Auto-configured on RNode interfaces, but highly advisable to include on other radio-based interfaces. + + +Interface Modes +--------------- + +When you enable discovery on an interface, Reticulum enforces certain interface modes to ensure the interface is actually useful for remote peers. + +If an interface is configured as ``discoverable``, but its mode is not explicitly set to ``gateway`` (for server-style interfaces like ``BackboneInterface`` or ``TCPServerInterface``) or ``access_point`` (for radio interfaces like ``RNodeInterface``), Reticulum will automatically configure the appropriate mode and log a notice. + +For example, if you enable discovery on a ``RNodeInterface`` without specifying the mode, Reticulum will automatically set it to ``access_point`` mode. + +Security Considerations +----------------------- + +When making interfaces discoverable, you are effectively broadcasting an invitation to connect to your system. It is important to understand the security implications of the configuration options you choose. + +**Publishing Credentials** + +If you enable ``publish_ifac = yes``, your interface's authentication passphrase will be included in the announce. If you are operating a public network and want anyone to connect, this is acceptable. However, if you wish to restrict access to a specific group of users, you **must** enable ``discovery_encrypt = yes``. This ensures that only peers possessing the correct ``network_identity`` can decode the passphrase. + +**Topology Exposure** + +A discoverable interface announces its presence, location (if configured), and capabilities to the network. Even if the connection details are encrypted, the *fact* that a connectable node exists within a certain network becomes public information. In high-security or scenarios requiring operational secrecy, consider the implications of advertising your infrastructure's existence. + +Example Configuration +--------------------- + +Below is an example configuration for a public backbone gateway. This configuration publishes a high-value, publicly discoverable interface, that anyone can connect to. + +.. code:: ini + + [[My Public Gateway]] + type = BackboneInterface + mode = gateway + listen_on = 0.0.0.0 + port = 4242 + + # Enable Discovery + discoverable = yes + + # Interface Details + discovery_name = Region A Public Entrypoint + announce_interval = 720 + + # Use external script to resolve dynamic IP + reachable_on = /usr/local/bin/get_external_ip.sh + + # Generate high stamp value + discovery_stamp_value = 24 + + # Optional location data + latitude = 51.99714 + longitude = -0.74195 + height = 15 + +The next example create an encrypted discovery-enabled interface, requiring a specific network identity to decode, and includes IFAC credentials for seamless authentication. + +.. code:: ini + + [[My Private Gateway]] + type = BackboneInterface + mode = gateway + listen_on = 0.0.0.0 + port = 5858 + network_name = internal_1 + passphrase = Mevpekyafshak5Wr + + # Enable Discovery + discoverable = yes + + # Interface Details + discovery_name = Region A Private Backbone + announce_interval = 720 + + # Use external script to resolve dynamic IP + reachable_on = /usr/local/bin/get_external_ip.sh + + # Target stamp value + discovery_stamp_value = 22 + + # Encrypt announces for our network only + discovery_encrypt = yes + + # Include credentials so trusted + # peers can connect automatically + publish_ifac = yes + + # Optional location data + latitude = 34.06915 + longitude = -118.44318 + height = 15 + +In the ``[reticulum]`` section of your configuration, you would define the network identity used for encryption as follows: + +.. code:: ini + + [reticulum] + ... + # The identity used to sign/encrypt discovery announces + network_identity = ~/.reticulum/storage/identities/my_network_identity + ... + +With these configuration options applied, your Reticulum instance will actively participate in the network's discovery ecosystem. Other peers running Reticulum with discovery enabled will be able to see your interface, validate its cryptographic stamp, and (depending on their configuration) automatically connect to it. + +For information on how to use these discovered interfaces and configure your system to auto-connect to them, refer to the :ref:`Discovering Interfaces` chapter. + +.. _interfaces-options: + +Common Interface Options +======================== + +A number of general configuration options are available on most interfaces. +These can be used to control various aspects of interface behaviour. + + + * | The ``enabled`` option tells Reticulum whether or not + to bring up the interface. Defaults to ``False``. For any + interface to be brought up, the ``enabled`` option + must be set to ``True`` or ``Yes``. + + * | The ``mode`` option allows selecting the high-level behaviour + of the interface from a number of options. + + - The default value is ``full``. In this mode, all discovery, + meshing and transport functionality is available. + + - In the ``access_point`` (or shorthand ``ap``) mode, the + interface will operate as a network access point. In this + mode, announces will not be automatically broadcasted on + the interface, and paths to destinations on the interface + will have a much shorter expiry time. This mode is useful + for creating interfaces that are mostly quiet, unless when + someone is actually using them. An example of this could + be a radio interface serving a wide area, where users are + expected to connect momentarily, use the network, and then + disappear again. + + * | The ``outgoing`` option sets whether an interface is allowed + to transmit. Defaults to ``True``. If set to ``False`` or ``No`` + the interface will only receive data, and never transmit. + + * | The ``network_name`` option sets the virtual network name for + the interface. This allows multiple separate network segments + to exist on the same physical channel or medium. + + * | The ``passphrase`` option sets an authentication passphrase on + the interface. This option can be used in conjunction with the + ``network_name`` option, or be used alone. + + * | The ``ifac_size`` option allows customising the length of the + Interface Authentication Codes carried by each packet on named + and/or authenticated network segments. It is set by default to + a size suitable for the interface in question, but can be set + to a custom size between 8 and 512 bits by using this option. + In normal usage, this option should not be changed from the + default. + + * | The ``announce_cap`` option lets you configure the maximum + bandwidth to allocate, at any given time, to propagating + announces and other network upkeep traffic. It is configured at + 2% by default, and should normally not need to be changed. Can + be set to any value between ``1`` and ``100``. + + *If an interface exceeds its announce cap, it will queue announces + for later transmission. Reticulum will always prioritise propagating + announces from nearby nodes first. This ensures that the local + topology is prioritised, and that slow networks are not overwhelmed + by interconnected fast networks.* + + *Destinations that are rapidly re-announcing will be down-prioritised + further. Trying to get "first-in-line" by announce spamming will have + the exact opposite effect: Getting moved to the back of the queue every + time a new announce from the excessively announcing destination is received.* + + *This means that it is always beneficial to select a balanced + announce rate, and not announce more often than is actually necesarry + for your application to function.* + + * | The ``bitrate`` option configures the interface bitrate. + Reticulum will use interface speeds reported by hardware, or + try to guess a suitable rate when the hardware doesn't report + any. In most cases, the automatically found rate should be + sufficient, but it can be configured by using the ``bitrate`` + option, to set the interface speed in *bits per second*. + + + * | The ``bootstrap_only`` option designates an interface as a temporary + bridge for initial connectivity. If this option is enabled, the + interface will be monitored and automatically detached once the + number of auto-connected interfaces reaches the limit configured by + ``autoconnect_discovered_interfaces``. This is particularly useful + for using a slow or expensive connection (such as a single LoRa + link or a remote TCP tunnel) solely to discover better local + infrastructure, which then supersedes the bootstrap interface. + +.. _interfaces-modes: + +Interface Modes +=============== + +The optional ``mode`` setting is available on all interfaces, and allows +selecting the high-level behaviour of the interface from a number of modes. +These modes affect how Reticulum selects paths in the network, how announces +are propagated, how long paths are valid and how paths are discovered. + +Configuring modes on interfaces is **not** strictly necessary, but can be useful +when building or connecting to more complex networks. If your Reticulum +instance is not running a Transport Node, it is rarely useful to configure +interface modes, and in such cases interfaces should generally be left in +the default mode. + + * | The default mode is ``full``. In this mode, all discovery, + meshing and transport functionality is activated. + + * | The ``gateway`` mode (or shorthand ``gw``) also has all + discovery, meshing and transport functionality available, + but will additionally try to discover unknown paths on + behalf of other nodes residing on the ``gateway`` interface. + If Reticulum receives a path request for an unknown + destination, from a node on a ``gateway`` interface, it + will try to discover this path via all other active interfaces, + and forward the discovered path to the requestor if one is + found. + + | If you want to allow other nodes to widely resolve paths or connect + to a network via an interface, it might be useful to put it in this + mode. By creating a chain of ``gateway`` interfaces, other + nodes will be able to immediately discover paths to any + destination along the chain. + + | *Please note!* It is the interface *facing the clients* that + must be put into ``gateway`` mode for this to work, not + the interface facing the wider network (for this, the ``boundary`` + mode can be useful, though). + + * | In the ``access_point`` (or shorthand ``ap``) mode, the + interface will operate as a network access point. In this + mode, announces will not be automatically broadcasted on + the interface, and paths to destinations on the interface + will have a much shorter expiry time. In addition, path + requests from clients on the access point interface will + be handled in the same way as the ``gateway`` interface. + + | This mode is useful for creating interfaces that remain + quiet, until someone actually starts using them. An example + of this could be a radio interface serving a wide area, + where users are expected to connect momentarily, use the + network, and then disappear again. + + * | The ``roaming`` mode should be used on interfaces that are + roaming (physically mobile), seen from the perspective of + other nodes in the network. As an example, if a vehicle is + equipped with an external LoRa interface, and an internal, + WiFi-based interface, that serves devices that are moving + *with* the vehicle, the external LoRa interface should be + configured as ``roaming``, and the internal interface can + be left in the default mode. With transport enabled, such + a setup will allow all internal devices to reach each other, + and all other devices that are available on the LoRa side + of the network, when they are in range. Devices on the LoRa + side of the network will also be able to reach devices + internal to the vehicle, when it is in range. Paths via + ``roaming`` interfaces also expire faster. + + * | The purpose of the ``boundary`` mode is to specify interfaces + that establish connectivity with network segments that are + significantly different than the one this node exists on. + As an example, if a Reticulum instance is part of a LoRa-based + network, but also has a high-speed connection to a + public Transport Node available on the Internet, the interface + connecting over the Internet should be set to ``boundary`` mode. + +For a table describing the impact of all modes on announce propagation, +please see the :ref:`Announce Propagation Rules` section. + +.. _interfaces-announcerates: + +Announce Rate Control +===================== + +The built-in announce control mechanisms and the default ``announce_cap`` +option described above are sufficient most of the time, but in some cases, especially on fast +interfaces, it may be useful to control the target announce rate. Using the +``announce_rate_target``, ``announce_rate_grace`` and ``announce_rate_penalty`` +options, this can be done on a per-interface basis, and moderates the *rate at +which received announces are re-broadcasted to other interfaces*. + + * | The ``announce_rate_target`` option sets the minimum amount of time, + in seconds, that should pass between received announces, for any one + destination. As an example, setting this value to ``3600`` means that + announces *received* on this interface will only be re-transmitted and + propagated to other interfaces once every hour, no matter how often they + are received. + + * | The optional ``announce_rate_grace`` defines the number of times a destination + can violate the announce rate before the target rate is enforced. + + * | The optional ``announce_rate_penalty`` configures an extra amount of + time that is added to the normal rate target. As an example, if a penalty + of ``7200`` seconds is defined, once the rate target is enforced, the + destination in question will only have its announces propagated every + 3 hours, until it lowers its actual announce rate to within the target. + +These mechanisms, in conjunction with the ``annouce_cap`` mechanisms mentioned +above means that it is essential to select a balanced announce strategy for +your destinations. The more balanced you can make this decision, the easier +it will be for your destinations to make it into slower networks that many hops +away. Or you can prioritise only reaching high-capacity networks with more frequent +announces. + +Current statistics and information about announce rates can be viewed using the +``rnpath -r`` command. + +It is important to note that there is no one right or wrong way to set up announce +rates. Slower networks will naturally tend towards using less frequent announces to +conserve bandwidth, while very fast networks can support applications that +need very frequent announces. Reticulum implements these mechanisms to ensure +that a large span of network types can seamlessly *co-exist* and interconnect. + +.. _interfaces-ingress-control: + +New Destination Rate Limiting +============================= + +On public interfaces, where anyone may connect and announce new destinations, +it can be useful to control the rate at which announces for *new* destinations are +processed. + +If a large influx of announces for newly created or previously unknown destinations +occur within a short amount of time, Reticulum will place these announces on hold, +so that announce traffic for known and previously established destinations can +continue to be processed without interruptions. + +After the burst subsides, and an additional waiting period has passed, the held +announces will be released at a slow rate, until the hold queue is cleared. This +also means, that should a node decide to connect to a public interface, announce +a large amount of bogus destinations, and then disconnect, these destination will +never make it into path tables and waste network bandwidth on retransmitted +announces. + +**It's important to note** that the ingress control works at the level of *individual +sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface` +cannot disrupt processing of incoming announces for other connected clients on the same +:ref:`TCP Server Interface`. All other clients on the same interface will still have new announces +processed without interruption. + +By default, Reticulum will handle this automatically, and ingress announce +control will be enabled on interface where it is sensible to do so. It should +generally not be neccessary to modify the ingress control configuration, +but all the parameters are exposed for configuration if needed. + + * | The ``ingress_control`` option tells Reticulum whether or not + to enable announce ingress control on the interface. Defaults to + ``True``. + + * | The ``ic_new_time`` option configures how long (in seconds) an + interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This + option is useful on publicly accessible interfaces that spawn new + sub-interfaces when a new client connects. + + * | The ``ic_burst_freq_new`` option sets the maximum announce ingress + frequency for newly spawned interfaces. Defaults to ``3.5`` + announces per second. + + * | The ``ic_burst_freq`` option sets the maximum announce ingress + frequency for other interfaces. Defaults to ``12`` announces + per second. + + *If an interface exceeds its burst frequency, incoming announces + for unknown destinations will be temporarily held in a queue, and + not processed until later.* + + * | The ``ic_max_held_announces`` option sets the maximum amount of + unique announces that will be held in the queue. Any additional + unique announces will be dropped. Defaults to ``256`` announces. + + * | The ``ic_burst_hold`` option sets how much time (in seconds) must + pass after the burst frequency drops below its threshold, for the + announce burst to be considered cleared. Defaults to ``60`` + seconds. + + * | The ``ic_burst_penalty`` option sets how much time (in seconds) must + pass after the burst is considered cleared, before held announces can + start being released from the queue. Defaults to ``5*60`` + seconds. + + * | The ``ic_held_release_interval`` option sets how much time (in seconds) + must pass between releasing each held announce from the queue. Defaults + to ``30`` seconds. + diff --git a/docs/manual/_sources/license.rst.txt b/docs/manual/_sources/license.rst.txt new file mode 100644 index 0000000..e906631 --- /dev/null +++ b/docs/manual/_sources/license.rst.txt @@ -0,0 +1,36 @@ +.. _license: + +Reticulum License +================= + +.. code:: text + + Reticulum License + + Copyright (c) 2016-2026 Mark Qvist + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + - The Software shall not be used in any kind of system which includes amongst + its functions the ability to purposefully do harm to human beings. + + - The Software shall not be used, directly or indirectly, in the creation of + an artificial intelligence, machine learning or language model training + dataset, including but not limited to any use that contributes to the + training or development of such a model or algorithm. + + - The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/docs/manual/_sources/networks.rst.txt b/docs/manual/_sources/networks.rst.txt new file mode 100644 index 0000000..dd378d3 --- /dev/null +++ b/docs/manual/_sources/networks.rst.txt @@ -0,0 +1,350 @@ +.. _networks-main: + +***************** +Building Networks +***************** + +This chapter will provide you with the high-level knowledge needed to build networks with +Reticulum. It will not, however tell you all you need to know to succesfully +design and configure every kind of network you can imagine. For this, you will +most likely need to read this manual in its entirity, invest significant time +into experimenting with the stack, and learning functionality intuitively. + +Still, after reading this chapter, you should be well equipped to *start* that +journey. While Reticulum is **fundamentally different** compared to other +networking technologies, it can often be easier than using traditional stacks. +If you've built networks before, you will probably have to forget, or at least +temporarily ignore, a lot of things at this point. It will all makes sense in +the end though. Hopefully. + +If you're used to protocols like IP, let's at least start with some relief: +You don't have to worry about coordinating addresses, subnets and routing for an +entire network that you might not know how will evolve in the future. With +Reticulum, you can simply add more segments to your network when it becomes +necessary, and Reticulum will handle the convergence of the entire network +automatically. There's plenty more neat aspects like that to Reticulum, but +we're getting ahead of ourselves. Let's cover the basics first. + +Concepts & Overview +-------------------- + +Before you start building your own networks, it's important to understand the +fundamental principles that distinguish Reticulum networks from traditional +networking approaches. These principles shape how you design your network, +what trade-offs you encounter, and what capabilities you can rely on. + +Reticulum is not a single network you "join", it is a toolkit for *creating* networks. +You decide what mediums to use, how nodes connect, what trust boundaries exist, +and what the network's purpose is. Reticulum provides the cryptographic foundation, +the transport mechanisms, and the convergence algorithms that make your design +workable. You provide the intent and the structure. + +This approach offers tremendous flexibility, but it requires thinking in terms of +different abstractions than those used in conventional networking. + +Introductory Considerations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There are important points that need to be kept in mind when building networks +with Reticulum: + + * | In a Reticulum network, any node can autonomously generate as many addresses + (called *destinations* in Reticulum terminology) as it needs, which become + globally reachable to the rest of the network. There is no central point of + control over the address space. + + * | Reticulum was designed to handle both very small, and very large networks. + While the address space can support billions of endpoints, Reticulum is + also very useful when just a few devices needs to communicate. + + * | Low-bandwidth networks, like LoRa and packet radio, can interoperate and + interconnect with much larger and higher bandwidth networks without issue. + Reticulum automatically manages the flow of information to and from various + network segments, and when bandwidth is limited, local traffic is prioritised. + You will, however, need to configure your interfaces correctly. If you tell + Reticulum to pass all announce traffic from a gigabit link to a LoRa interfaces, + it will try as best as possible to comply with this, while still respecting + bandwidth limits, but you *will* waste a lot of precious bandwidth and airtime, + and your LoRa network will not work very well. + + * | Reticulum provides sender/initiator anonymity by default. There is no way + to filter traffic or discriminate it based on the source of the traffic. + + * | All traffic is encrypted using ephemeral keys generated by an Elliptic Curve + Diffie-Hellman key exchange on Curve25519. There is no way to inspect traffic + contents, and no way to prioritise or throttle certain kinds of traffic. + All transport and routing layers are thus completely agnostic to traffic type, + and will pass all traffic equally. + + * | Reticulum can function both with and without infrastructure. When *transport + nodes* are available, they can route traffic over multiple hops for other + nodes, and will function as a distributed cryptographic keystore. When there + is no transport nodes available, all nodes that are within communication range + can still communicate. + + * | Every node can become a transport node, simply by enabling it in it's + configuration, but there is no need for every node on the network to be a + transport node. Letting every node be a transport node will in most cases + degrade the performance and reliability of the network. + + *In general terms, if a node is stationary, well-connected and kept running + most of the time, it is a good candidate to be a transport node. For optimal + performance, a network should contain the amount of transport nodes that + provides connectivity to the intended area / topography, and not many more + than that.* + + * | Reticulum is designed to work reliably in open, trustless environments. This + means you can use it to create open-access networks, where participants can + join and leave in a free and unorganised manner. This property allows an + entirely new, and so far, mostly unexplored class of networked applications, + where networks, and the information flow within them can form and dissolve + organically. + + * | You can just as easily create closed networks, since Reticulum allows you to + add authentication to any interface. This means you can restrict access on + any interface type, even when using legacy devices, such as modems. You can + also mix authenticated and open interfaces on the same system. See the + :ref:`Common Interface Options` section of the :ref:`Interfaces` + chapter of this manual for information on how to set up interface authentication. + + +Reticulum allows you to mix very different kinds of networking mediums into a +unified mesh, or to keep everything within one medium. You could build a "virtual +network" running entirely over the Internet, where all nodes communicate over TCP +and UDP "channels". You could also build such a network using other already-established +communications channels as the underlying carrier for Reticulum. + +However, most real-world networks will probably involve either some form of +wireless or direct hardline communications. To allow Reticulum to communicate +over any type of medium, you must specify it in the configuration file, by default +located at ``~/.reticulum/config``. See the :ref:`Supported Interfaces` +chapter of this manual for interface configuration examples. + +Any number of interfaces can be configured, and Reticulum will automatically +decide which are suitable to use in any given situation, depending on where +traffic needs to flow. + +Destinations, Not Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In traditional networking, addresses are allocated from a managed space. If you want to +communicate with another node, you need to know its address, and that address +must be unique within the network segment. This requires coordination, either +through manual assignment, DHCP servers, or other allocation mechanisms. + +Reticulum replaces addresses with **destinations**. A destination is identified by a 16-byte +hash (128 bits) derived from a SHA-256 hash of the destination's identifying +characteristics. This hash serves as the address on the network. On the network, it +is represented in binary, but when displayed to human users, it will usually look something like +this ``<13425ec15b621c1d928589718000d814>``. + +The critical difference is that *any node can generate as many destinations as it +needs, without coordination*. A destination's uniqueness is guaranteed by the +collision resistance of SHA-256 and the inclusion of the node's public key in the +hash calculation. Two nodes can both use the destination name +``messenger.user.inbox``, but they will have different destination hashes because +their public keys differ. Both can coexist on the same network without conflict. + +This has profound implications for network design: + +* **No address allocation planning:** You never need to reserve address ranges, + plan subnets, or coordinate with other network operators. Nodes simply generate + destinations and announce them. + +* **Global portability:** A destination is not tied to a physical location or + network segment. A node can move its destinations across interfaces, mediums, + or even between entirely separate Reticulum networks simply by sending an + announce on the new medium. + +* **Implicit authentication:** Because destinations are bound to public keys, + communication to a destination is inherently cryptographically authenticated. + Only the holder of the corresponding private key can decrypt and respond to + traffic addressed to that destination. This also makes application-level + authentication *much* simpler, since it can directly use the foundational + identity verification built into the core networking layer. + +* **Identity abstraction:** A single Reticulum Identity can create multiple + destinations. This allows a single entity (a person, a device, a service) to + present multiple endpoints without needing multiple cryptographic keypairs. + + +Transport Nodes and Instances +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Reticulum distinguishes between two types of nodes: **Instances** +and **Transport Nodes**. Every node running Reticulum is an Instance, but not +every Instance is a Transport Node. + +A **Reticulum Instance** is any system running the Reticulum stack. It can create +destinations, send and receive packets, establish links, and communicate with +other nodes. It can also host destinations that are connectable for *anyone* else +in the network. This means you can easily host globally available services from +any location, including your home or office. Network-wide, global connectivity +for all destinations is guaranteed, as long as there is *some* physical way to +actually transport the packets. Instances are the default state and are appropriate for most end-user devices, +such as phones, laptops, sensors, or any device that primarily consumes network services. + +A **Transport Node** is an Instance that has been explicitly configured to +participate in network-wide transport. Transport nodes forward packets across +hops, propagate announces, maintain path tables, and serve path requests on +behalf of other nodes. When a destination sends an announce, Transport Nodes +receive it, remember the path, and rebroadcast it to other interfaces. When a node +needs to reach a destination it doesn't have a path for, Transport Nodes help +resolve the path through the network. + +Even devices hosting services or serving content should probably just be configured +as instances, and themselves connect to wider networks via a Transport Node. +In some situations, this may not be practical though, and as an example, it is +entirely viable to host a personal Transport Node on a Raspberry Pi, while it +is at the same time running an LXMF propagation node, and hosting your personal +site or files over Reticulum. + +The distinction is important. **Not** every node should be a Transport Node: + +* **Resource consumption:** Transport nodes maintain path tables, process + announces, and forward traffic. This requires memory and CPU resources that + may be limited on low-powered devices. + +* **Stability requirements:** Transport nodes contribute to network convergence. + If Transport Nodes frequently go offline, path tables become stale and + convergence suffers. Stable, always-on nodes make better Transport Nodes. + +* **Bandwidth considerations:** Transport nodes process and rebroadcast network + maintenance traffic. On very low-bandwidth mediums, having too many Transport + Nodes will consume capacity that should be used for actual data. + +In practice, a network typically has a relatively small number of Transport Nodes +strategically placed to provide coverage and connectivity. End-user devices run +as Instances, connecting through nearby Transport Nodes to reach the wider network. +This pattern mirrors traditional networking where routers forward traffic while +end hosts simply consume connectivity, but with the crucial difference that any +node *can* become a router if needed, and the decision is yours to make based on +your network's requirements. + +Transport nodes also function as distributed cryptographic keystores. When a +destination announces itself, Transport Nodes cache the public key and destination +information. Other nodes can request unknown public keys from the network, and +Transport Nodes respond with the cached information. This eliminates the need for +a central directory service while ensuring that public keys remain available +throughout the network. + +Trustless Networking +^^^^^^^^^^^^^^^^^^^^ + +Traditional network security models assume high levels of trust at +specific layers. You might trust your ISP to deliver packets without inspection, +or trust your VPN provider to handle your traffic, or trust the network +administrator to configure firewalls appropriately. These trust relationships +create vulnerabilities and dependencies. + +Reticulum is designed to function in **open, trustless environments**. This +means the protocol makes no assumptions about the trustworthiness of the network +infrastructure, the other participants, or the transport mediums. Every aspect +of communication is secured cryptographically: + +* **Traffic encryption:** All traffic to single destinations is encrypted using + ephemeral keys. + +* **Source anonymity:** Reticulum packets do not include source addresses. + An observer intercepting a packet cannot determine who sent it, only who it is + addressed to (unless IFAC is enabled, in which case nothing can be determined). + This provides initiator anonymity by default. + +* **Path verification:** The announce mechanism includes cryptographic signatures that + prove the authenticity of destination announcements. + +* **Unforgeable delivery confirmations:** When a destination proves receipt of a + packet, the proof is signed with the destination's identity key. This prevents + false acknowledgments and ensures reliable delivery verification. + +* **Interface authentication:** When using Interface Access Codes (IFAC), packets + on authenticated interfaces carry signatures derived from a shared secret. Only + nodes with the correct network name and passphrase can generate valid packets, allowing creation + of virtual private networks on shared mediums. + +The trustless design has important consequences for network design: + +* **Open-access networks are viable:** You can build networks that anyone can + join without pre-approval. Because traffic is encrypted and authenticated end- + to-end, participants cannot interfere with each other's private communication, + even if they share the same transport infrastructure. + +* **No traffic inspection or prioritization:** Because traffic contents and + sources are opaque to intermediate nodes, there is no mechanism for filtering, + prioritizing, or throttling traffic based on its type or origin. All traffic + is treated equally. From a neutrality perspective, this is a feature. + +* **Adversarial resilience:** The network can operate even if some nodes are + malicious or controlled by adversaries. While a malicious Transport Node could + refuse to forward certain traffic or drop packets, it cannot decrypt, modify, + or impersonate legitimate traffic. Redundant paths and multiple Transport Nodes + mitigate the impact of malicious nodes. + +Of course, you can also create closed networks. Interface Access +Codes allow you to restrict participation on specific interfaces. Network +Identities enable you to verify that discovered interfaces belong to trusted +operators. Blackhole management lets you block malicious identities. Reticulum +provides both the tools for open networks and the controls for closed ones. The +choice is yours based on your requirements. + + +Heterogeneous Connectivity +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In conventional networking, mixing different transport mediums typically requires +gateways, translation layers, and careful configuration. A WiFi network doesn't +natively interoperate with a packet radio network without additional infrastructure, +and you can't just download a car over a serial port, or send an encrypted message +in a QR code. + +Reticulum treats **heterogeneity as a core premise**. The protocol is designed +to seamlessly mix mediums with vastly different characteristics: + +* **Bandwidth:** LoRa links operating at a few hundred bits per second can + interconnect with gigabit Ethernet backbones. Reticulum automatically manages + the flow of information, prioritizing local traffic on slow segments while + allowing global convergence. + +* **Latency:** Satellite links with multi-second latency can coexist with local + links measured in milliseconds. The transport system handles timing, asynchronous + delivery and retransmissions transparently. + +* **Topology:** Point-to-point microwave links, broadcast radio networks, + switched Ethernet fabrics, and virtual tunnels over the Internet can all be + part of the same Reticulum network. + +* **Reliability:** Intermittent connections that come and go (such as mobile + devices or opportunistic radio contacts) can participate alongside always-on + infrastructure. Reticulum gracefully handles link loss and reconnection. + +This heterogeneity is achieved through several design elements: + +* **Expandable, medium-agnostic interface system:** Reticulum communicates with the physical + world through interface modules. Adding support for a new medium is a matter + of implementing an interface class. The protocol itself remains unchanged. + +* **Interface modes:** Different modes (``full``, ``gateway``, ``access_point``, + ``roaming``, ``boundary``) allow you to configure how interfaces interact with + the wider network based on their characteristics and role. + +* **Announce propagation rules:** Announces are forwarded between interfaces + according to rules that account for bandwidth limitations and interface modes. + Slow segments are not overwhelmed by traffic from fast segments. + +* **Local traffic prioritization:** When bandwidth is constrained, Reticulum + prioritizes announces for nearby destinations. This ensures that local + connectivity remains functional even when global convergence is incomplete. + +For network designers, this means you are free to use whatever mediums are +available, affordable, or appropriate for your situation. You might use LoRa for +wide-area low-bandwidth coverage, WiFi for local high-capacity links, I2P for +anonymous Internet connectivity, and Ethernet for infrastructure backhauls, all +within the same network. Reticulum handles the translation and coordination +automatically. + +The key design consideration is not whether different mediums can work together +(they can), but **how** they should work together based on your goals. A node +with multiple interfaces spanning heterogeneous mediums needs to be configured +with appropriate interface modes so that traffic flows efficiently. A gateway +connecting a slow LoRa segment to a fast Internet backbone should be configured +differently than a mobile device roaming between radio cells. \ No newline at end of file diff --git a/docs/manual/_sources/reference.rst.txt b/docs/manual/_sources/reference.rst.txt new file mode 100644 index 0000000..d789201 --- /dev/null +++ b/docs/manual/_sources/reference.rst.txt @@ -0,0 +1,214 @@ +:tocdepth: 4 + +.. _api-main: + +************* +API Reference +************* +Communication over Reticulum networks is achieved by using a simple set of classes exposed by the RNS API. +This chapter lists and explains all classes exposed by the Reticulum Network Stack API, along with their method signatures and usage. It can be used as a reference while writing applications that utilise Reticulum, or it can be read in entirity to gain an understanding of the complete functionality of RNS from a developers perspective. + +.. _api-reticulum: + +.. only:: html + + |start-h3| Reticulum |end-h3| + +.. only:: latex + + Reticulum + --------- + +.. autoclass:: RNS.Reticulum + :members: + + +.. _api-identity: + +.. only:: html + + |start-h3| Identity |end-h3| + +.. only:: latex + + Identity + -------- + +.. autoclass:: RNS.Identity + :members: + +.. _api-destination: + +.. only:: html + + |start-h3| Destination |end-h3| + +.. only:: latex + + Destination + ----------- + +.. autoclass:: RNS.Destination + :members: + +.. _api-packet: + +.. only:: html + + |start-h3| Packet |end-h3| + +.. only:: latex + + Packet + ------ + +.. autoclass:: RNS.Packet(destination, data, create_receipt = True) + :members: + +.. _api-packetreceipt: + +.. only:: html + + |start-h3| Packet Receipt |end-h3| + +.. only:: latex + + Packet Receipt + -------------- + +.. autoclass:: RNS.PacketReceipt() + :members: + +.. _api-link: + +.. only:: html + + |start-h3| Link |end-h3| + +.. only:: latex + + Link + ---- + +.. autoclass:: RNS.Link(destination, established_callback=None, closed_callback = None) + :members: + +.. _api-requestreceipt: + +.. only:: html + + |start-h3| Request Receipt |end-h3| + +.. only:: latex + + Request Receipt + --------------- + +.. autoclass:: RNS.RequestReceipt() + :members: + +.. _api-resource: + +.. only:: html + + |start-h3| Resource |end-h3| + +.. only:: latex + + Resource + -------- + +.. autoclass:: RNS.Resource(data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout=None) + :members: + +.. _api-channel: + +.. only:: html + + |start-h3| Channel |end-h3| + +.. only:: latex + + Channel + ------- + +.. autoclass:: RNS.Channel.Channel() + :members: + +.. _api-messsagebase: + +.. only:: html + + |start-h3| MessageBase |end-h3| + +.. only:: latex + + MessageBase + ----------- + +.. autoclass:: RNS.MessageBase() + :members: + +.. _api-buffer: + +.. only:: html + + |start-h3| Buffer |end-h3| + +.. only:: latex + + Buffer + ------ + +.. autoclass:: RNS.Buffer + :members: + +.. _api-rawchannelreader: + +.. only:: html + + |start-h3| RawChannelReader |end-h3| + +.. only:: latex + + RawChannelReader + ---------------- + +.. autoclass:: RNS.RawChannelReader + :members: __init__, add_ready_callback, remove_ready_callback + +.. _api-rawchannelwriter: + +.. only:: html + + |start-h3| RawChannelWriter |end-h3| + +.. only:: latex + + RawChannelWriter + ---------------- + +.. autoclass:: RNS.RawChannelWriter + :members: __init__ + +.. _api-transport: + +.. only:: html + + |start-h3| Transport |end-h3| + +.. only:: latex + + Transport + --------- + +.. autoclass:: RNS.Transport + :members: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

\ No newline at end of file diff --git a/docs/manual/_sources/software.rst.txt b/docs/manual/_sources/software.rst.txt new file mode 100644 index 0000000..59069ac --- /dev/null +++ b/docs/manual/_sources/software.rst.txt @@ -0,0 +1,355 @@ +.. _software-main: + +************************ +Programs Using Reticulum +************************ + +This chapter provides a non-exhaustive list of notable programs, systems and application-layer +protocols that have been built using Reticulum. + +These programs will let you get a feel for how Reticulum works. Most of them have been designed +to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast +links, such as local WiFi, wired Ethernet, the Internet, or any combination. + +As such, it is easy to get started experimenting, without having to set up any radio +transceivers or infrastructure just to try it out. Launching the programs on separate +devices connected to the same WiFi network is enough to get started, and physical +radio interfaces can then be added later. + +Programs & Utilities +==================== + +Many different applications using Reticulum already exist, serving a wide variety of purposes +from day-to-day communication and information sharing to systems administration and tackling +advanced networking and communications challenges. + +Development of Reticulum-based applications and systems is ongoing, so consider this list +a non-exhaustive starting point of *some* of the options available. With a bit of searching, +primarily over Reticulum itself, you will find many more interesting things. + +Remote Shell +^^^^^^^^^^^^ + +The `rnsh `_ program lets you establish fully interactive +remote shell sessions over Reticulum. It also allows you to pipe any program to or from a +remote system, and is similar to how ``ssh`` works. The ``rnsh`` program is very efficient, and +can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, +such as LoRa or packet radio. + +In addition to the default, fully interactive terminal mode, +for extremely limited links, ``rnsh`` offers line-interactive mode, allowing you to interact +with remote systems, even when link throughput is counted in a few hundreds of bits per second. + +.. raw:: latex + + \newpage + +Nomad Network +^^^^^^^^^^^^^ + +The terminal-based program `Nomad Network `_ +provides a complete encrypted communications suite built with Reticulum. It features +encrypted messaging (both direct and delayed-delivery for offline users), file sharing, +and has a built-in text-browser and page server with support for dynamically rendered pages, +user authentication and more. + +.. image:: screenshots/nomadnet_3.png + :target: https://github.com/markqvist/nomadnet + +`Nomad Network `_ is a user-facing client +for the messaging and information-sharing protocol LXMF. + +RNS Page Node +^^^^^^^^^^^^^ + +`RNS Page Node `_ is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. + + +Retipedia +^^^^^^^^^ + +You can host the entirity of Wikipedia (or any ``.zim``) file to other Nomad Network clients using `Retipedia `_. + + +.. raw:: latex + + \newpage + +Sideband +^^^^^^^^ + +If you would rather use an LXMF client with a graphical user interface, you can take +a look at `Sideband `_, which is available for Android, +Linux, macOS and Windows. Sideband is an advanced LXMF and LXST client, and a multi-purpose Reticulum +utility, with features and functionality targeted at advanced users. + +.. only:: html + + .. image:: screenshots/sideband_devices.webp + :align: center + :target: https://unsigned.io/sideband + +.. only:: latex + + .. image:: screenshots/sideband_devices.png + :align: center + :target: https://unsigned.io/sideband + +Sideband allows you to communicate with other people or LXMF-compatible +systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR +Paper Messages, or anything else Reticulum supports. + +It also interoperates with all other LXMF clients, and provides advanced features such as voice messaging, +real-time voice calls, file attachments, private telemetry sharing, and a full +plugin system for expandability. + +.. raw:: latex + + \newpage + +MeshChatX +^^^^^^^^ + +A `Reticulum MeshChat fork from the future `_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project. + +.. only:: html + + .. image:: screenshots/meshchatx.webp + :align: center + :target: https://git.quad4.io/RNS-Things/MeshChatX + +.. only:: latex + + .. image:: screenshots/meshchatx.png + :align: center + :target: https://git.quad4.io/RNS-Things/MeshChatX + + +Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. + +.. raw:: latex + + \newpage + +MeshChat +^^^^^^^^ + +The `Reticulum MeshChat `_ application +is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network +page browser and other interesting functionality. + +.. only:: html + + .. image:: screenshots/meshchat_1.webp + :align: center + :target: https://github.com/liamcottle/reticulum-meshchat + +.. only:: latex + + .. image:: screenshots/meshchat_1.png + :align: center + :target: https://github.com/liamcottle/reticulum-meshchat + +Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or +any other LXMF client. + +Columba +^^^^^^^ + +`Columba `_ is a simple and familiar LXMF +messaging app Android, built with a native Android interface and Material Design 3. + +.. only:: html + + .. image:: screenshots/columba.webp + :align: center + :width: 25% + :target: https://github.com/torlando-tech/columba/ + +.. only:: latex + + .. image:: screenshots/columba.png + :align: center + :width: 25% + :target: https://github.com/torlando-tech/columba/ + +While still in early and very active development, it is of course also compatible +with all other LXMF clients, and allows you to message seamlessly with anyone else +using LXMF. + +.. raw:: latex + + \newpage + +Reticulum Relay Chat +^^^^^^^^^^^^^^^^^^^^ + +`Reticulum Relay Chat `_ is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for. + +The `rrcd `_ program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include `rrc-gui `_ and `rrc-web `_. + +RRC is closer in spirit to IRC than to modern “everything platforms.” You connect, you join a room, you talk, and then you leave. If you were present, you saw the conversation. If you were not, the conversation did not wait for you. This is not an accident. This is the entire design. + +RetiBBS +^^^^^^^ + +`RetiBBS `_ is a bulletin board system implementation for Reticulum networks. + +.. only:: html + + .. image:: screenshots/retibbs.webp + :align: center + :target: https://github.com/kc1awv/RetiBBS + +.. only:: latex + + .. image:: screenshots/retibbs.png + :align: center + :target: https://github.com/kc1awv/RetiBBS + +RetiBBS allows users to communicate through message boards in a secure manner. + +.. raw:: latex + + \newpage + +RBrowser +^^^^^^^^ + +The `rBrowser `_ program is a cross-platform, standalone, web-based browser for exploring NomadNetwork Nodes over Reticulum Network. It automatically discovers NomadNet nodes through network announces and provides a user-friendly interface for browsing distributed content with Micron markup support. + +.. only:: html + + .. image:: screenshots/rbrowser.webp + :align: center + :target: https://github.com/fr33n0w/rBrowser + +.. only:: latex + + .. image:: screenshots/rbrowser.png + :align: center + :target: https://github.com/fr33n0w/rBrowser + +Includes useful features like automatic listening for announce, adding nodes to favorites, browsing and rendering any kind of NomadNet links, downloading files from remote nodes, a unique local NomadNet Search Engine and more. + + +.. raw:: latex + + \newpage + +Reticulum Network Telephone +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``rnphone`` program, included as part of the `LXST `_ package is a command-line Reticulum telephone utility and daemon, that allows building physical, hardware telephones for LXST and Reticulum, as well as simply performing calls via the command line. + +.. only:: html + + .. image:: screenshots/rnphone.webp + :align: center + :target: https://github.com/markqvist/LXST + +.. only:: latex + + .. image:: screenshots/rnphone.jpg + :align: center + :target: https://github.com/markqvist/LXST + +It supports interfacing directly with hardware peripherals such as GPIO keypads and LCD displays, providing a modular system for building secure hardware telephones. + +.. raw:: latex + + \newpage + +LXST Phone +^^^^^^^^^^ + +The `LXST Phone `_ program is a cross-platform desktop application for performing LXST voice calls over Reticulum. + +.. only:: html + + .. image:: screenshots/lxst_phone.webp + :align: center + :target: https://github.com/kc1awv/lxst_phone + +.. only:: latex + + .. image:: screenshots/lxst_phone.png + :align: center + :target: https://github.com/kc1awv/lxst_phone + +It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. + + +.. raw:: latex + + \newpage + +LXMFy +^^^^^ + +`LXMFy `_ is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. `Bot implementations exist `_ for Home Assistant control, LLM integrations, and various other purposes. + + +LXMF Interactive Client +^^^^^^^^^^^^^^^^^^^^^^^ + +`LXMF Interactive Client `_ is a feature-rich, terminal-based LXMF messaging client with many advanced features and an extensible plugin architecture. + +RNS FileSync +^^^^^^^^^^^^ + +The `RNS FileSync `_ program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. + + +Micron Parser JS +^^^^^^^^^^^^^^^^ + +`Micron Parser JS `_ is the JavaScript-based parser for the Micron markup language, that most web-based Nomad Network browsers use. If you want to make utilities or tools that display Micron pages, this library is essential. + + +RNMon +^^^^^ + +`RNMon `_ is a monitoring daemon designed to monitor the status of multiple RNS applications and push the metrics to an InfluxDB instance over the influx line protocol. + + +.. raw:: latex + + \newpage + +Protocols +========= + +A number of standard protocols have emerged through real-world usage and testing in the Reticulum community. While you may sometimes want to use completely custom protocols and implementations when writing Reticulum-based software, using these protocols provides application developers with an easy way to implement advanced functionality quickly and effortlessly. Using them also ensures compatibility and interoperability between many different client applications, creating an open communications ecosystem where users are free to choose the applications that suit their needs, while remaining connected to everyone else. + +LXMF +^^^^ + +`LXMF `_ is a simple and flexible messaging format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It offers zero-conf message routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. + +LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog paper message transport. + +Using Propagation Nodes, LXMF also offer a way to store and forward messages to users or endpoints that are not directly reachable at the time of message emission. + +LXST +^^^^ + +`LXST `_ is a simple and flexible real-time streaming format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It is built on top of Reticulum and offers zero-conf stream routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. It currently powers real-time voice and telephony applications over Reticulum. + +RRC +^^^ + +The `Reticulum Relay Chat `_ protocol, is a live chat system built on top of the Reticulum Network Stack. It exists to provide near real-time group communication without dragging in message history databases, federation machinery, or architectural guilt. + +RRC is intentionally simple. It does not pretend to be email, a mailbox, or a distributed archive. It behaves more like a conversation in a room. If you were there, you heard it. If you were not, you did not. That is not a bug, that is the point. + +Interface Modules & Connectivity Resources +========================================== + +This section provides a list of various community-provided interface modules, guides and resources for creating Reticulum networks over special or challenging mediums. + +* Custom interface module for running `RNS over HTTP `_ +* Guide for running `Reticulum over ICMP `_ using ``PipeInterface`` +* Guide for running `Reticulum over DNS `_ with Iodine +* Guide for running `Reticulum over HF radio `_ +* `Modem73 `_ is a KISS TNC OFDM modem frontend that can be used with Reticulum \ No newline at end of file diff --git a/docs/manual/_sources/support.rst.txt b/docs/manual/_sources/support.rst.txt new file mode 100644 index 0000000..5683d78 --- /dev/null +++ b/docs/manual/_sources/support.rst.txt @@ -0,0 +1,60 @@ +.. _support-main: + +***************** +Support Reticulum +***************** +You can help support the continued development of open, free and private communications +systems by donating, providing feedback and contributing code and learning resources. + +Donations +========= +Donations are gratefully accepted via the following channels: + + +.. code:: text + + Monero: + 84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w + + Bitcoin: + bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6 + + Ethereum: + 0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E + + Liberapay: + https://liberapay.com/Reticulum/ + + Ko-Fi: + https://ko-fi.com/markqvist + +Are certain features in the development roadmap are important to you or your +organisation? Make them a reality quickly by sponsoring their implementation. + +Provide Feedback +================ +Feedback on the usage, functioning and potential dysfunctioning of any and +all components of the system is very valuable to the continued development and +improvement of Reticulum. But... + +.. warning:: + + **Think before you speak**. As time has shown, over 80% of the "feedback", + "bug reports" and "advice" the Reticulum project has received has been + irrelevant noise, stemming from erroneous assumptions, misunderstanding the + foundational functionality or philosophy behind the system, or simply + the malinformed (but overly opinionated) personal preferences of individual + drive-by architects. This wastes the time of everyone involved. + + The Reticulum project is not a public teahouse for serving the attention + needs of random bypassers, but a highly complex system engineered and + refined over more than a decade, designed to provide communication and + connectivity guarantees in highly adversarial environments. + + If you want to voice your opinion, it better be well-informed, and we + expect you to have a comprehensive and solid foundation for your points + of view. Everything else will be ignored. + +Absolutely no automated analytics, telemetry, error +reporting or statistics is collected and reported by Reticulum under any +circumstances, so we rely on old-fashioned human feedback. diff --git a/docs/manual/_sources/understanding.rst.txt b/docs/manual/_sources/understanding.rst.txt new file mode 100644 index 0000000..b1e45d2 --- /dev/null +++ b/docs/manual/_sources/understanding.rst.txt @@ -0,0 +1,1011 @@ +.. _understanding-main: + +*********************** +Understanding Reticulum +*********************** +This chapter will briefly describe the overall purpose and operating principles of Reticulum. +It should give you an overview of how the stack works, and an understanding of how to +develop networked applications using Reticulum. + +This chapter is not an exhaustive source of information on Reticulum, at least not yet. Currently, +the only complete repository, and final authority on how Reticulum actually functions, is the Python +reference implementation and API reference. That being said, this chapter is an essential resource in +understanding how Reticulum works from a high-level perspective, along with the general principles of +Reticulum, and how to apply them when creating your own networks or software. + +After reading this chapter, you should be well-equipped to understand how a Reticulum network +operates, what it can achieve, and how you can use it yourself. This chapter also seeks to provide an overview of the +sentiments and the philosophy behind Reticulum, what problems it seeks to solve, and how it +approaches those solutions. + +.. _understanding-motivation: + +Motivation +========== + +The primary motivation for designing and implementing Reticulum has been the current lack of +reliable, functional and secure minimal-infrastructure modes of digital communication. It is my +belief that it is highly desirable to create a reliable and efficient way to set up long-range digital +communication networks that can securely allow exchange of information between people and +machines, with no central point of authority, control, censorship or barrier to entry. + +Almost all of the various networking systems in use today share a common limitation: They +require large amounts of coordination and centralised trust and power to function. To join such networks, you need approval +of gatekeepers in control. This need for coordination and trust inevitably leads to an environment of +central control, where it's very easy for infrastructure operators or governments to control or alter +traffic, and censor or persecute unwanted actors. It also makes it completely impossible to freely deploy +and use networks at will, like one would use other common tools that enhance individual agency and freedom. + +Reticulum aims to require as little coordination and trust as possible. It aims to make secure, +anonymous and permissionless networking and information exchange a tool that anyone can just pick up and use. + +Since Reticulum is completely medium agnostic, it can be used to build networks on whatever is best +suited to the situation, or whatever you have available. In some cases, this might be packet radio +links over VHF frequencies, in other cases it might be a 2.4 GHz +network using off-the-shelf radios, or it might be using common LoRa development boards. + +At the time of release of this document, the fastest and easiest setup for development and testing is using +LoRa radio modules with an open source firmware (see the section :ref:`Reference Setup`), +connected to any kind of computer or mobile device that Reticulum can run on. + +The ultimate aim of Reticulum is to allow anyone to be their own network operator, and to make it +cheap and easy to cover vast areas with a myriad of independent, interconnectable and autonomous networks. +Reticulum **is not** *one network*, it **is a tool** to build *thousands of networks*. Networks without +kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate +with each other, and require no central oversight. Networks for human beings. *Networks for the people*. + +.. _understanding-goals: + +Goals +===== + +To be as widely usable and efficient to deploy as possible, the following goals have been used to +guide the design of Reticulum: + + +* **Fully useable as open source software stack** + Reticulum must be implemented with, and be able to run using only open source software. This is + critical to ensuring the availability, security and transparency of the system. +* **Hardware layer agnosticism** + Reticulum must be fully hardware agnostic, and shall be useable over a wide range of + physical networking layers, such as data radios, serial lines, modems, handheld transceivers, + wired Ethernet, WiFi, or anything else that can carry a digital data stream. Hardware made for + dedicated Reticulum use shall be as cheap as possible and use off-the-shelf components, so + it can be easily modified and replicated by anyone interested in doing so. +* **Very low bandwidth requirements** + Reticulum should be able to function reliably over links with a transmission capacity as low + as *5 bits per second*. +* **Encryption by default** + Reticulum must use strong encryption by default for all communication. +* **Initiator Anonymity** + It must be possible to communicate over a Reticulum network without revealing any identifying + information about oneself. +* **Unlicensed use** + Reticulum shall be functional over physical communication mediums that do not require any + form of license to use. Reticulum must be designed in a way, so it is usable over ISM radio + frequency bands, and can provide functional long distance links in such conditions, for example + by connecting a modem to a PMR or CB radio, or by using LoRa or WiFi modules. +* **Supplied software** + In addition to the core networking stack and API, that allows a developer to build + applications with Reticulum, a basic set of Reticulum-based communication tools must be + implemented and released along with Reticulum itself. These shall serve both as a + functional, basic communication suite, and as an example and learning resource to others wishing + to build applications with Reticulum. +* **Ease of use** + The reference implementation of Reticulum is written in Python, to make it easy to use + and understand. A programmer with only basic experience should be able to use + Reticulum to write networked applications. +* **Low cost** + It shall be as cheap as possible to deploy a communication system based on Reticulum. This + should be achieved by using cheap off-the-shelf hardware that potential users might already + own. The cost of setting up a functioning node should be less than $100 even if all parts + needs to be purchased. + +.. _understanding-basicfunctionality: + +Introduction & Basic Functionality +================================== + +Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its +core a *message oriented* system. It is suited for both local point-to-point or point-to-multipoint +scenarios where all nodes are within range of each other, as well as scenarios where packets need +to be transported over multiple hops in a complex network to reach the recipient. + +Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead +Reticulum uses the singular concept of *destinations*. Any application using Reticulum as its +networking stack will need to create one or more destinations to receive data, and know the +destinations it needs to send data to. + +All destinations in Reticulum are *represented* as a 16 byte hash. This hash is derived from truncating a full +SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses +will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``. + +The truncation size of 16 bytes (128 bits) for destinations has been chosen as a reasonable trade-off +between address space +and packet overhead. The address space accommodated by this size can support many billions of +simultaneously active devices on the same network, while keeping packet overhead low, which is +essential on low-bandwidth networks. In the very unlikely case that this address space nears +congestion, a one-line code change can upgrade the Reticulum address space all the way up to 256 +bits, ensuring the Reticulum address space could potentially support galactic-scale networks. +This is obviously complete and ridiculous over-allocation, and as such, the current 128 bits should +be sufficient, even far into the future. + +By default Reticulum encrypts all data using elliptic curve cryptography and AES. Any packet sent to a +destination is encrypted with a per-packet derived key. Reticulum can also set up an encrypted +channel to a destination, called a *Link*. Both data sent over Links and single packets offer +*Initiator Anonymity*. Links additionally offer *Forward Secrecy* by default, employing an Elliptic Curve +Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. Asymmetric, link-less +packet communication can also provide forward secrecy, with automatic key ratcheting, by enabling +ratchets on a per-destination basis. The multi-hop transport, coordination, verification and reliability +layers are fully autonomous and also based on elliptic curve cryptography. + +Reticulum also offers symmetric key encryption for group-oriented communications, as well as +unencrypted packets (for local broadcast purposes **only**). + +Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports, +and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or +private IP networks. + +.. _understanding-destinations: + +Destinations +------------ + +To receive and send data with the Reticulum stack, an application needs to create one or more +destinations. Reticulum uses three different basic destination types, and one special: + + +* **Single** + The *single* destination type is the most common type in Reticulum, and should be used for + most purposes. It is always identified by a unique public key. Any data sent to this + destination will be encrypted using ephemeral keys derived from an ECDH key exchange, and will + only be readable by the creator of the destination, who holds the corresponding private key. +* **Plain** + A *plain* destination type is unencrypted, and suited for traffic that should be broadcast to a + number of users, or should be readable by anyone. Traffic to a *plain* destination is not encrypted. + Generally, *plain* destinations can be used for broadcast information intended to be public. + Plain destinations are only reachable directly, and packets addressed to plain destinations are + never transported over multiple hops in the network. To be transportable over multiple hops in Reticulum, information + *must* be encrypted, since Reticulum uses the per-packet encryption to verify routing paths and + keep them alive. +* **Group** + The *group* special destination type, that defines a symmetrically encrypted virtual destination. + Data sent to this destination will be encrypted with a symmetric key, and will be readable by + anyone in possession of the key, but as with the *plain* destination type, packets to this type + of destination are not currently transported over multiple hops, although a planned upgrade + to Reticulum will allow globally reachable *group* destinations. +* **Link** + A *link* is a special destination type, that serves as an abstract channel to a *single* + destination, directly connected or over multiple hops. The *link* also offers reliability and + more efficient encryption, forward secrecy, initiator anonymity, and as such can be useful even + when a node is directly reachable. It also offers a more capable API and allows easily carrying + out requests and responses, large data transfers and more. + +.. _understanding-destinationnaming: + +Destination Naming +^^^^^^^^^^^^^^^^^^ + +Destinations are created and named in an easy to understand dotted notation of *aspects*, and +represented on the network as a hash of this value. The hash is a SHA-256 truncated to 128 bits. The +top level aspect should always be a unique identifier for the application using the destination. +The next levels of aspects can be defined in any way by the creator of the application. + +Aspects can be as long and as plentiful as required, and a resulting long destination name will not +impact efficiency, as names are always represented as truncated SHA-256 hashes on the network. + +As an example, a destination for a environmental monitoring application could be made up of the +application name, a device type and measurement type, like this: + +.. code-block:: text + + app name : environmentlogger + aspects : remotesensor, temperature + + full name : environmentlogger.remotesensor.temperature + hash : 4faf1b2e0a077e6a9d92fa051f256038 + +For the *single* destination, Reticulum will automatically append the associated public key as a +destination aspect before hashing. This is done to ensure only the correct destination is reached, +since anyone can listen to any destination name. Appending the public key ensures that a given +packet is only directed at the destination that holds the corresponding private key to decrypt the +packet. + +**Take note!** There is a very important concept to understand here: + +* Anyone can use the destination name ``environmentlogger.remotesensor.temperature`` + +* Each destination that does so will still have a unique destination hash, and thus be uniquely + addressable, because their public keys will differ. + +In actual use of *single* destination naming, it is advisable not to use any uniquely identifying +features in aspect naming. Aspect names should be general terms describing what kind of destination +is represented. The uniquely identifying aspect is always achieved by appending the public key, +which expands the destination into a uniquely identifiable one. Reticulum does this automatically. + +Any destination on a Reticulum network can be addressed and reached simply by knowing its +destination hash (and public key, but if the public key is not known, it can be requested from the +network simply by knowing the destination hash). The use of app names and aspects makes it easy to +structure Reticulum programs and makes it possible to filter what information and data your program +receives. + +To recap, the different destination types should be used in the following situations: + +* **Single** + When private communication between two endpoints is needed. Supports multiple hops. +* **Group** + When private communication between two or more endpoints is needed. Supports multiple hops + indirectly, but must first be established through a *single* destination. +* **Plain** + When plain-text communication is desirable, for example when broadcasting information, or for local discovery purposes. + +To communicate with a *single* destination, you need to know its public key. Any method for +obtaining the public key is valid, but Reticulum includes a simple mechanism for making other +nodes aware of your destinations public key, called the *announce*. It is also possible to request +an unknown public key from the network, as all transport instances serve as a distributed ledger +of public keys. + +Note that public key information can be shared and verified in other ways than using the +built-in *announce* functionality, and that it is therefore not required to use the *announce* and *path request* +functionality to obtain public keys. It is by far the easiest though, and should definitely be used +if there is not a very good reason for doing it differently. + +.. _understanding-keyannouncements: + +Public Key Announcements +------------------------ + +An *announce* will send a special packet over any relevant interfaces, containing all needed +information about the destination hash and public key, and can also contain some additional, +application specific data. The entire packet is signed by the sender to ensure authenticity. It is not +required to use the announce functionality, but in many cases it will be the simplest way to share +public keys on the network. The announce mechanism also serves to establish end-to-end connectivity +to the announced destination, as the announce propagates through the network. + +As an example, an announce in a simple messenger application might contain the following information: + + +* The announcers destination hash +* The announcers public key +* Application specific data, in this case the users nickname and availability status +* A random blob, making each new announce unique +* An Ed25519 signature of the above information, verifying authenticity + +With this information, any Reticulum node that receives it will be able to reconstruct an outgoing +destination to securely communicate with that destination. You might have noticed that there is one +piece of information lacking to reconstruct full knowledge of the announced destination, and that is +the aspect names of the destination. These are intentionally left out to save bandwidth, since they +will be implicit in almost all cases. The receiving application will already know them. If a destination +name is not entirely implicit, information can be included in the application specific data part that +will allow the receiver to infer the naming. + +It is important to note that announces will be forwarded throughout the network according to a +certain pattern. This will be detailed in the section +:ref:`The Announce Mechanism in Detail`. + +In Reticulum, destinations are allowed to move around the network at will. This is very different from +protocols such as IP, where an address is always expected to stay within the network segment it was assigned in. +This limitation does not exist in Reticulum, and any destination is *completely portable* over the entire topography +of the network, and *can even be moved to other Reticulum networks* than the one it was created in, and +still become reachable. To update its reachability, a destination simply needs to send an announce on any +networks it is part of. After a short while, it will be globally reachable in the network. + +Seeing how *single* destinations are always tied to a private/public key pair leads us to the next topic. + +.. _understanding-identities: + +Identities +---------- + +In Reticulum, an *identity* does not necessarily represent a personal identity, but is an abstraction that +can represent any kind of *verifiable entity*. This could very well be a person, but it could also be the +control interface of a machine, a program, robot, computer, sensor or something else entirely. In +general, any kind of agent that can act, or be acted upon, or store or manipulate information, can be +represented as an identity. An *identity* can be used to create any number of destinations. + +A *single* destination will always have an *identity* tied to it, but not *plain* or *group* +destinations. Destinations and identities share a multilateral connection. You can create a +destination, and if it is not connected to an identity upon creation, it will just create a new one to use +automatically. This may be desirable in some situations, but often you will probably want to create +the identity first, and then use it to create new destinations. + +As an example, we could use an identity to represent the user of a messaging application. +Destinations can then be created by this identity to allow communication to reach the user. +In all cases it is of great importance to store the private keys associated with any +Reticulum Identity securely and privately, since obtaining access to the identity keys equals +obtaining access and controlling reachability to any destinations created by that identity. + +.. _understanding-gettingfurther: + +Getting Further +--------------- + +The above functions and principles form the core of Reticulum, and would suffice to create +functional networked applications in local clusters, for example over radio links where all interested +nodes can directly hear each other. But to be truly useful, we need a way to direct traffic over multiple +hops in the network. + +In the following sections, two concepts that allow this will be introduced, *paths* and *links*. + +.. _understanding-transport: + +Reticulum Transport +=================== + +The methods of routing used in traditional networks are fundamentally incompatible with the physical medium +types and circumstances that Reticulum was designed to handle. These mechanisms mostly assume trust at the physical layer, +and often needs a lot more bandwidth than Reticulum can assume is available. Since Reticulum is designed to +survive running over open radio spectrum, no such trust can be assumed, and bandwidth is often very limited. + +To overcome such challenges, Reticulum’s *Transport* system uses asymmetric elliptic curve cryptography to +implement the concept of *paths* that allow discovery of how to get information closer to a certain +destination. It is important to note that no single node in a Reticulum network knows the complete +path to a destination. Every Transport node participating in a Reticulum network will only +know the most direct way to get a packet one hop closer to it's destination. + + +.. _understanding-nodetypes: + +Node Types +---------- + +Currently, Reticulum distinguishes between two types of network nodes. All nodes on a Reticulum network +are *Reticulum Instances*, and some are also *Transport Nodes*. If a system running Reticulum is fixed in +one place, and is intended to be kept available most of the time, it is a good contender to be a *Transport Node*. + +Any Reticulum Instance can become a Transport Node by enabling it in the configuration. +This distinction is made by the user configuring the node, and is used to determine what nodes on the +network will help forward traffic, and what nodes rely on other nodes for wider connectivity. + +If a node is an *Instance* it should be given the configuration directive ``enable_transport = No``, which +is the default setting. + +If it is a *Transport Node*, it should be given the configuration directive ``enable_transport = Yes``. + + +.. _understanding-announce: + +The Announce Mechanism in Detail +-------------------------------- + +When an *announce* for a destination is transmitted by a Reticulum instance, it will be forwarded by +any transport node receiving it, but according to some specific rules: + + +* | If this exact announce has already been received before, ignore it. + +* | If not, record into a table which Transport Node the announce was received from, and how many times in + total it has been retransmitted to get here. + +* | If the announce has been retransmitted *m+1* times, it will not be forwarded any more. By default, *m* is + set to 128. + +* | After a randomised delay, the announce will be retransmitted on all interfaces that have bandwidth + available for processing announces. By default, the maximum bandwidth allocation for processing + announces is set at 2%, but can be configured on a per-interface basis. + +* | If any given interface does not have enough bandwidth available for retransmitting the announce, + the announce will be assigned a priority inversely proportional to its hop count, and be inserted + into a queue managed by the interface. + +* | When the interface has bandwidth available for processing an announce, it will prioritise announces + for destinations that are closest in terms of hops, thus prioritising reachability and connectivity + of local nodes, even on slow networks that connect to wider and faster networks. + +* | After the announce has been re-transmitted, and if no other nodes are heard retransmitting the announce + with a greater hop count than when it left this node, transmitting it will be retried *r* times. By default, + *r* is set to 1. + +* | If a newer announce from the same destination arrives, while an identical one is already waiting + to be transmitted, the newest announce is discarded. If the newest announce contains different + application specific data, it will replace the old announce. + +Once an announce has reached a transport node in the network, any other node in direct contact with that +transport node will be able to reach the destination the announce originated from, simply by sending a packet +addressed to that destination. Any transport node with knowledge of the announce will be able to direct the +packet towards the destination by looking up the most efficient next node to the destination. + +According to these rules, an announce will propagate throughout the network in a predictable way, +and make the announced destination reachable in a short amount of time. Fast networks that have the +capacity to process many announces can reach full convergence very quickly, even when constantly adding +new destinations. Slower segments of such networks might take a bit longer to gain full knowledge about +the wide and fast networks they are connected to, but can still do so over time, while prioritising full +and quickly converging end-to-end connectivity for their local, slower segments. + +.. tip:: + + Even very slow networks, that simply don't have the capacity to ever reach *full* convergence + will generally still be able to reach **any other destination on any connected segments**, since + interconnecting transport nodes will prioritize announces into the slower segments that are + actually requested by nodes on these. + + This means that slow, low-capacity or low-resource segments **don't** need to have full network + knowledge, since paths can always be recursively resolved from other segments that do have + knowledge about them. + +In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full +end-to-end connectivity in about one minute, given there is enough bandwidth available to process +the required amount of announces. + +.. _understanding-paths: + +Reaching the Destination +------------------------ + +In networks with changing topology and trustless connectivity, nodes need a way to establish +*verified connectivity* with each other. Since the underlying network mediums are assumed to be trustless, Reticulum +must provide a way to guarantee that the peer you are communicating with is actually who you +expect. Reticulum offers two ways to do this. + +For exchanges of small amounts of information, Reticulum offers the *Packet* API, which works exactly like you would expect - on a per packet level. The following process is employed when sending a packet: + +* | A packet is always created with an associated destination and some payload data. When the packet is sent + to a *single* destination type, Reticulum will automatically create an ephemeral encryption key, perform + an ECDH key exchange with the destination's public key (or ratchet key, if available), and encrypt the information. + +* | It is important to note that this key exchange does not require any network traffic. The sender already + knows the public key of the destination from an earlier received announce, and can thus perform the ECDH + key exchange locally, before sending the packet. + +* | The public part of the newly generated ephemeral key-pair is included with the encrypted token, and sent + along with the encrypted payload data in the packet. + +* | When the destination receives the packet, it can itself perform an ECDH key exchange and decrypt the + packet. + +* | A new ephemeral key is used for every packet sent in this way. + +* | Once the packet has been received and decrypted by the addressed destination, that destination can opt + to *prove* its receipt of the packet. It does this by calculating the SHA-256 hash of the received packet, + and signing this hash with its Ed25519 signing key. Transport nodes in the network can then direct this + *proof* back to the packets origin, where the signature can be verified against the destination's known + public signing key. + +* | In case the packet is addressed to a *group* destination type, the packet will be encrypted with the + pre-shared AES-256 key associated with the destination. In case the packet is addressed to a *plain* + destination type, the payload data will not be encrypted. Neither of these two destination types can offer + forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is + strictly necessary to use one of the others. + + +For exchanges of larger amounts of data, or when longer sessions of bidirectional communication is desired, Reticulum offers the *Link* API. To establish a *link*, the following process is employed: + +* | First, the node that wishes to establish a link will send out a *link request* packet, that + traverses the network and locates the desired destination. Along the way, the Transport Nodes that + forward the packet will take note of this *link request*, and mark it as pending. + +* | Second, if the destination accepts the *link request* , it will send back a packet that proves the + authenticity of its identity (and the receipt of the link request) to the initiating node. All + nodes that initially forwarded the packet will also be able to verify this proof, and thus + accept the validity of the *link* throughout the network. The link is now marked as *established*. + +* | When the validity of the *link* has been accepted by forwarding nodes, these nodes will + remember the *link* , and it can subsequently be used by referring to a hash representing it. + +* | As a part of the *link request*, an Elliptic Curve Diffie-Hellman key exchange takes place, that sets up an + efficiently encrypted tunnel between the two nodes. As such, this mode of communication is preferred, + even for situations when nodes can directly communicate, when the amount of data to be exchanged numbers + in the tens of packets, or whenever the use of the more advanced API functions is desired. + +* | When a *link* has been set up, it automatically provides message receipt functionality, through + the same *proof* mechanism discussed before, so the sending node can obtain verified confirmation + that the information reached the intended recipient. + +* | Once the *link* has been set up, the initiator can remain anonymous, or choose to authenticate towards + the destination using a Reticulum Identity. This authentication is happening inside the encrypted + link, and is only revealed to the verified destination, and no intermediaries. + +In a moment, we will discuss the details of how this methodology is +implemented, but let’s first recap what purposes this methodology serves. We +first ensure that the node answering our request is actually the one we want to +communicate with, and not a malicious actor pretending to be so. At the same +time we establish an efficient encrypted channel. The setup of this is +relatively cheap in terms of bandwidth, so it can be used just for a short +exchange, and then recreated as needed, which will also rotate encryption keys. +The link can also be kept alive for longer periods of time, if this is more +suitable to the application. The procedure also inserts the *link id* , a hash +calculated from the link request packet, into the memory of forwarding nodes, +which means that the communicating nodes can thereafter reach each other simply +by referring to this *link id*. + +The combined bandwidth cost of setting up a link is 3 packets totalling 297 bytes (more info in the +:ref:`Binary Packet Format` section). The amount of bandwidth used on keeping +a link open is practically negligible, at 0.45 bits per second. Even on a slow 1200 bits per second packet +radio channel, 100 concurrent links will still leave 96% channel capacity for actual data. + + +Link Establishment in Detail +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After exploring the basics of the announce mechanism, finding a path through the network, and an overview +of the link establishment procedure, this section will go into greater detail about the Reticulum link +establishment process. + +The *link* in Reticulum terminology should not be viewed as a direct node-to-node link on the +physical layer, but as an abstract channel, that can be open for any amount of time, and can span +an arbitrary number of hops, where information will be exchanged between two nodes. + + +* | When a node in the network wants to establish verified connectivity with another node, it + will randomly generate a new X25519 private/public key pair. It then creates a *link request* + packet, and broadcast it. + | + | *It should be noted that the X25519 public/private keypair mentioned above is two separate keypairs: + An encryption key pair, used for derivation of a shared symmetric key, and a signing key pair, used + for signing and verifying messages on the link. They are sent together over the wire, and can be + considered as single public key for simplicity in this explanation.* + +* | The *link request* is addressed to the destination hash of the desired destination, and + contains the following data: The newly generated X25519 public key *LKi*. + +* | The broadcasted packet will be directed through the network according to the rules laid out + previously. + +* | Any node that forwards the link request will store a *link id* in it’s *link table* , along with the + amount of hops the packet had taken when received. The link id is a hash of the entire link + request packet. If the link request packet is not *proven* by the addressed destination within some + set amount of time, the entry will be dropped from the *link table* again. + +* | When the destination receives the link request packet, it will decide whether to accept the request. + If it is accepted, the destination will also generate a new X25519 private/public key pair, and + perform a Diffie Hellman Key Exchange, deriving a new symmetric key that will be used to encrypt the + channel, once it has been established. + +* | A *link proof* packet is now constructed and transmitted over the network. This packet is + addressed to the *link id* of the *link*. It contains the following data: The newly generated X25519 + public key *LKr* and an Ed25519 signature of the *link id* and *LKr* made by the *original signing key* of + the addressed destination. + +* | By verifying this *link proof* packet, all nodes that originally transported the *link request* + packet to the destination from the originator can now verify that the intended destination received + the request and accepted it, and that the path they chose for forwarding the request was valid. + In successfully carrying out this verification, the transporting nodes marks the link as active. + An abstract bi-directional communication channel has now been established along a path in the network. + Packets can now be exchanged bi-directionally from either end of the link simply by adressing the + packets to the *link id* of the link. + +* | When the source receives the *proof* , it will know unequivocally that a verified path has been + established to the destination. It can now also use the X25519 public key contained in the + *link proof* to perform it's own Diffie Hellman Key Exchange and derive the symmetric key + that is used to encrypt the channel. Information can now be exchanged reliably and securely. + +.. note:: + + It’s important to note that this methodology ensures that the source of the request does not need to + reveal any identifying information about itself. **The link initiator remains completely anonymous**. + +When using *links*, Reticulum will automatically verify all data sent over the link, and can also +automate retransmissions if *Resources* are used. + +.. _understanding-resources: + +Resources +--------- + +For exchanging small amounts of data over a Reticulum network, the :ref:`Packet` interface +is sufficient, but for exchanging data that would require many packets, an efficient way to coordinate +the transfer is needed. + +This is the purpose of the Reticulum :ref:`Resource`. A *Resource* can automatically +handle the reliable transfer of an arbitrary amount of data over an established :ref:`Link`. +Resources can auto-compress data, will handle breaking the data into individual packets, sequencing +the transfer, integrity verification and reassembling the data on the other end. + +:ref:`Resources` are programmatically very simple to use, and only requires a few lines +of codes to reliably transfer any amount of data. They can be used to transfer data stored in memory, +or stream data directly from files. + +.. _understanding-network_identities: + +Network Identities +================== + +In Reticulum, every peer and application utilizes a cryptographic **Identity** to verify authenticity and establish encrypted channels. While standard identities are typically used to represent a single user, device, or service, Reticulum introduces the concept of a **Network Identity** to represent a logical group of nodes or an entire community infrastructure. + +A Network Identity is, at its core, a standard Reticulum Identity keyset. However, its purpose and usage differ from a personal identity. Instead of identifying a single entity, a Network Identity acts as a shared credential that federates multiple independent Transport Instances under a single, verifiable administrative domain. + + +Conceptual Overview +------------------- + +You can think of a standard Reticulum Identity as a self-sovereign, privately created passport for a single person. A Network Identity, conversely, is akin to a cryptographic flag, or a charter that flies over a fleet of ships. It signifies that while the ships may operate independently and be physically distant, they belong to the same organization, follow the same protocols, and are expected to act in concert. + +When you configure a Network Identity on one or more of your nodes, you are effectively declaring that these nodes constitute a specific "network" within a broader Reticulum mesh. This allows other peers to recognize interfaces not just as "a node named Alice", but as "a gateway belonging to The Eastern Ret Of Freedom". + + +Current Usage +------------- + +At present, the primary function of a Network Identity is within the :ref:`Interface Discovery` system. + +When a Transport Instance broadcasts a discovery announce for an interface, it can optionally sign that announce with a Network Identity, instead of just its local transport identity. Remote peers receiving the announce can then verify the signature. This provides functionality for two important distinctions: + +1. **Authenticity:** It proves that the interface was published by an operator who possesses the private key for that Network Identity. +2. **Trust Boundaries:** It allows users to configure their systems to only accept and connect to interfaces that belong to specific Network Identities, effectively creating "whitelisted" zones of trusted infrastructure. + +.. note:: + If you enable encryption on your discovery announces, the Network Identity is used as the shared secret. Only peers who have been explicitly provided with the Network Identity's full keyset (and have it configured locally) will be able to decrypt and utilize the connection details. + + This functionality will be expanded in the future, so that peers with delegated keys can be allowed to decrypt discovery announces without holding the root network key. Currently, the functionality is sufficient for sharing interface information privately where you control all nodes that must decrypt the discovered interfaces. + + +Future Implications +------------------- + +While the current implementation focuses on interface discovery, the concept of Network Identities serves as the foundational building block for future Reticulum features designed to support large-scale, organic mesh formation. + +As the ecosystem evolves, Network Identities will facilitate: + +* **Distributed Name Resolution:** A system where networks can publish name-to-identity mappings, allowing human-readable names to resolve without centralized servers. +* **Service Publishing:** Networks will be able to announce specific capabilities, services, or information endpoints available publicly or to their members. +* **Inter-Network Federation:** Trust relationships between different networks, allowing for seamless but managed flow of traffic and information across distinct administrative boundaries. +* **Distributed Blackhole Management:** A reputation-based system for blackhole list distribution, where trusted Network Identities can sign and publish lists of blackholed identities. This allows communities to collaboratively enforce security standards and filter spam or malicious identities across the parts of the wider mesh that they are responsible for. + +By adopting the use of Network Identities now, you are preparing your infrastructure to be compatible with this future functionality. + + +Creating and Using a Network Identity +------------------------------------- + +Since a Network Identity is simply a standard Reticulum Identity, you create one using the built-in tools. + +1. **Generate the Identity:** + Use the ``rnid`` utility to generate a new identity file that will serve as your Network Identity. + + .. code:: sh + + $ rnid -g ~/.reticulum/storage/identities/my_network + +2. **Distribute the Public Key:** + The public key must be distributed to any Transport Instance that needs to verify your network's announces and discovery information. By default, if your node is set up to use a network identity, this happens automatically (using the standard announce mechanism). + +3. **Configure Instances:** + In the ``[reticulum]`` section of the configuration file on every node within your network, point the ``network_identity`` option to the file you created. + + .. code:: ini + + [reticulum] + ... + network_identity = ~/.reticulum/storage/identities/my_network + ... + +Once configured, your instances will automatically utilize this identity for signing discovery announces (and potentially decrypting network-private information), presenting a unified front to the wider network. + +.. _understanding-referencesystem: + +Reference Setup +====================== + +This section will detail a recommended *Reference Setup* for Reticulum. It is important to +note that Reticulum is designed to be usable on more or less any computing device, and over more +or less any medium that allows you to send and receive data, which satisfies some very low +minimum requirements. + +The communication channel must support at least half-duplex operation, and provide an average +throughput of 5 bits per second or greater, and supports a physical layer MTU of 500 bytes. The +Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x +runtime environment. + +That being said, this reference setup has been outlined to provide a common platform for anyone +who wants to help in the development of Reticulum, and for everyone who wants to know a +recommended setup to get started experimenting. A reference system consists of three parts: + +* **An Interface Device** + Which provides access to the physical medium whereupon the communication + takes place, for example a radio with an integrated modem. A setup with a separate modem + connected to a radio would also be an interface device. +* **A Host Device** + Some sort of computing device that can run the necessary software, communicate with the + interface device, and provide user interaction. +* **A Software Stack** + The software implementing the Reticulum protocol and applications using it. + +The reference setup can be considered a relatively stable platform to develop on, and also to start +building networks or applications on. While details of the implementation might change at the current stage of +development, it is the goal to maintain hardware compatibility for as long as entirely possible, and +the current reference setup has been determined to provide a functional platform for many years +into the future. The current Reference System Setup is as follows: + + +* **Interface Device** + A data radio consisting of a LoRa radio module, and a microcontroller with open source + firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900 + MHz frequency bands. More details can be found on the `RNode Page `_. +* **Host Device** + Any computer device running Linux and Python. A Raspberry Pi with a Debian based OS is + a good place to start, but anything can be used. +* **Software Stack** + The most recently released Python Implementation of Reticulum, running on a Linux-based + operating system. + +.. note:: + + To avoid confusion, it is very important to note, that the reference interface device **does not** + use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will + need a plain LoRa radio module connected to a controller with the correct firmware. Full details on how to + get or make such a device is available on the `RNode Page `_. + +With the current reference setup, it should be possible to get on a Reticulum network for around 100$ +even if you have none of the hardware already, and need to purchase everything. + +This reference setup is of course just a recommendation for getting started easily, and you should +tailor it to your own specific needs, or whatever hardware you have available. + +.. _understanding-protocolspecifics: + +Protocol Specifics +================== + +This chapter will detail protocol specific information that is essential to the implementation of +Reticulum, but non-critical in understanding how the protocol works on a general level. It should be +treated more as a reference than as essential reading. + + +Packet Prioritisation +--------------------- + +Currently, Reticulum is completely priority-agnostic regarding *general* traffic. All traffic is handled +on a first-come, first-serve basis. Announce re-transmission and other maintenance traffic is handled +according to the re-transmission times and priorities described earlier in this chapter. + + +Interface Access Codes +---------------------- + +Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared +passphrase. The configuration of this is detailed in the :ref:`Common Interface Options` +section. To implement this feature, Reticulum uses the concept of Interface Access Codes, that are calculated +and verified per-packet. + +An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519 +signing identity, and for every outbound packet generate a signature of the entire packet. This signature is +then inserted into the packet as an Interface Access Code before transmission. Depending on the speed and +capabilities of the interface, the IFAC can be the full 512-bit Ed25519 signature, or a truncated version. +Configured IFAC length can be inspected for all interfaces with the ``rnstatus`` utility. + +Upon receipt, the interface will check that the signature matches the expected value, and drop the packet if it +does not. This ensures that only packets sent with the correct naming and/or passphrase parameters are allowed to +pass onto the network. + + +.. _understanding-packetformat: + +Wire Format +----------- + +.. code-block:: text + + == Reticulum Wire Format ====== + + A Reticulum packet is composed of the following fields: + + [HEADER 2 bytes] [ADDRESSES 16/32 bytes] [CONTEXT 1 byte] [DATA 0-465 bytes] + + * The HEADER field is 2 bytes long. + * Byte 1: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type], + [Destination Type] and [Packet Type] + * Byte 2: Number of hops + + * Interface Access Code field if the IFAC flag was set. + * The length of the Interface Access Code can vary from + 1 to 64 bytes according to physical interface + capabilities and configuration. + + * The ADDRESSES field contains either 1 or 2 addresses. + * Each address is 16 bytes long. + * The Header Type flag in the HEADER field determines + whether the ADDRESSES field contains 1 or 2 addresses. + * Addresses are SHA-256 hashes truncated to 16 bytes. + + * The CONTEXT field is 1 byte. + * It is used by Reticulum to determine packet context. + + * The DATA field is between 0 and 465 bytes. + * It contains the packets data payload. + + IFAC Flag + ----------------- + open 0 Packet for publically accessible interface + authenticated 1 Interface authentication is included in packet + + + Header Types + ----------------- + type 1 0 Two byte header, one 16 byte address field + type 2 1 Two byte header, two 16 byte address fields + + + Context Flag + ----------------- + unset 0 The context flag is used for various types + set 1 of signalling, depending on packet context + + + Propagation Types + ----------------- + broadcast 0 + transport 1 + + + Destination Types + ----------------- + single 00 + group 01 + plain 10 + link 11 + + + Packet Types + ----------------- + data 00 + announce 01 + link request 10 + proof 11 + + + +- Packet Example -+ + + HEADER FIELD DESTINATION FIELDS CONTEXT FIELD DATA FIELD + _______|_______ ________________|________________ ________|______ __|_ + | | | | | | | | + 01010000 00000100 [HASH1, 16 bytes] [HASH2, 16 bytes] [CONTEXT, 1 byte] [DATA] + || | | | | + || | | | +-- Hops = 4 + || | | +------- Packet Type = DATA + || | +--------- Destination Type = SINGLE + || +----------- Propagation Type = TRANSPORT + |+------------- Header Type = HEADER_2 (two byte header, two address fields) + +-------------- Access Codes = DISABLED + + + +- Packet Example -+ + + HEADER FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD + _______|_______ _______|_______ ________|______ __|_ + | | | | | | | | + 00000000 00000111 [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA] + || | | | | + || | | | +-- Hops = 7 + || | | +------- Packet Type = DATA + || | +--------- Destination Type = SINGLE + || +----------- Propagation Type = BROADCAST + |+------------- Header Type = HEADER_1 (two byte header, one address field) + +-------------- Access Codes = DISABLED + + + +- Packet Example -+ + + HEADER FIELD IFAC FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD + _______|_______ ______|______ _______|_______ ________|______ __|_ + | | | | | | | | | | + 10000000 00000111 [IFAC, N bytes] [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA] + || | | | | + || | | | +-- Hops = 7 + || | | +------- Packet Type = DATA + || | +--------- Destination Type = SINGLE + || +----------- Propagation Type = BROADCAST + |+------------- Header Type = HEADER_1 (two byte header, one address field) + +-------------- Access Codes = ENABLED + + + Size examples of different packet types + --------------------------------------- + + The following table lists example sizes of various + packet types. The size listed are the complete on- + wire size counting all fields including headers, + but excluding any interface access codes. + + - Path Request : 51 bytes + - Announce : 167 bytes + - Link Request : 83 bytes + - Link Proof : 115 bytes + - Link RTT packet : 99 bytes + - Link keepalive : 20 bytes + + +.. _understanding-announcepropagation: + +Announce Propagation Rules +-------------------------- + +The following table illustrates the rules for automatically propagating announces +from one interface type to another, for all possible combinations. For the purpose +of announce propagation, the *Full* and *Gateway* modes are identical. + +.. image:: graphics/if_mode_graph_b.png + +See the :ref:`Interface Modes` section for a conceptual overview +of the different interface modes, and how they are configured. + +.. + (.. code-block:: text) + Full ────── ✓ ──┐ ┌── ✓ ── Full + AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP + Boundary ── ✓ ──┤ ├── ✓ ── Boundary + Roaming ─── ✓ ──┘ └── ✓ ── Roaming + + Full ────── ✕ ──┐ ┌── ✓ ── Full + AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP + Boundary ── ✕ ──┤ ├── ✓ ── Boundary + Roaming ─── ✕ ──┘ └── ✓ ── Roaming + + Full ────── ✓ ──┐ ┌── ✓ ── Full + AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP + Boundary ── ✕ ──┤ ├── ✕ ── Boundary + Roaming ─── ✕ ──┘ └── ✕ ── Roaming + + Full ────── ✓ ──┐ ┌── ✓ ── Full + AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP + Boundary ── ✓ ──┤ ├── ✓ ── Boundary + Roaming ─── ✕ ──┘ └── ✕ ── Roaming + + +.. _understanding-primitives: + +Cryptographic Primitives +------------------------ + +Reticulum uses a simple suite of efficient, strong and well-tested cryptographic +primitives, with widely available implementations that can be used both on +general-purpose CPUs and on microcontrollers. + +One of the primary considerations for choosing this particular set of primitives is +that they can be implemented *safely* with relatively few pitfalls, on practically +all current computing platforms. + +The primitives listed here **are authoritative**. Anything claiming to be Reticulum, +but not using these exact primitives **is not** Reticulum, and possibly an +intentionally compromised or weakened clone. The utilised primitives are: + +* Ed25519 for signatures + +* X25519 for ECDH key exchanges + +* HKDF for key derivation + +* Encrypted tokens are based on the Fernet spec + + * Ephemeral keys derived from an ECDH key exchange on Curve25519 + + * AES-256 in CBC mode with PKCS7 padding + + * HMAC using SHA256 for message authentication + + * IVs must be generated through ``os.urandom()`` or better + + * No Fernet version and timestamp metadata fields + +* SHA-256 + +* SHA-512 + +In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-256-CBC`` +primitives are provided by `OpenSSL `_ (via the `PyCA/cryptography `_ +package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard +Python `hashlib `_. The ``HKDF``, ``HMAC``, +``Token`` primitives, and the ``PKCS7`` padding function are always provided by the +following internal implementations: + +- ``RNS/Cryptography/HKDF.py`` +- ``RNS/Cryptography/HMAC.py`` +- ``RNS/Cryptography/Token.py`` +- ``RNS/Cryptography/PKCS7.py`` + + +Reticulum also includes a complete implementation of all necessary primitives in pure Python. +If OpenSSL & PyCA are not available on the system when Reticulum is started, Reticulum will +instead use the internal pure-python primitives. A trivial consequence of this is performance, +with the OpenSSL backend being *much* faster. The most important consequence however, is the +potential loss of security by using primitives that has not seen the same amount of scrutiny, +testing and review as those from OpenSSL. + +Using the normal RNS installation procedures, it is not possible to install Reticulum on a +system without the required OpenSSL primitives being available, and if they are not, they will +be resolved and installed as a dependency. It is only possible to use the pure-python primitives +by manually specifying this, for example by using the ``rnspure`` package. + +.. warning:: + If you want to use the internal pure-python primitives, it is **highly advisable** that you + have a good understanding of the risks that this pose, and make an informed decision on whether + those risks are acceptable to you. \ No newline at end of file diff --git a/docs/manual/_sources/using.rst.txt b/docs/manual/_sources/using.rst.txt new file mode 100644 index 0000000..bfb7d47 --- /dev/null +++ b/docs/manual/_sources/using.rst.txt @@ -0,0 +1,1227 @@ +.. _using-main: + +****************************** +Using Reticulum on Your System +****************************** + +Reticulum is not installed as a driver or kernel module, as one might expect +of a networking stack. Instead, Reticulum is distributed as a Python module, +containing the networking core, and a set of utility and daemon programs. + +This means that no special privileges are required to install or use it. It +is also very light-weight, and easy to transfer to, and install on new systems. + +When you have Reticulum installed, any program or application that uses Reticulum +will automatically load and initialise Reticulum when it starts, if it is not +already running. + +In many cases, this approach is sufficient. When any program needs to use +Reticulum, it is loaded, initialised, interfaces are brought up, and the +program can now communicate over any Reticulum networks available. If another +program starts up and also wants access to the same Reticulum network, the already +running instance is simply shared. This works for any number of programs running +concurrently, and is very easy to use, but depending on your use case, there +are other options. + +Configuration & Data +-------------------- + +Reticulum stores all information that it needs to function in a single file-system +directory. When Reticulum is started, it will look for a valid configuration +directory in the following places: + +- ``/etc/reticulum`` +- ``~/.config/reticulum`` +- ``~/.reticulum`` + +If no existing configuration directory is found, the directory ``~/.reticulum`` +is created, and the default configuration will be automatically created here. +You can move it to one of the other locations if you wish. + +It is also possible to use completely arbitrary configuration directories by +specifying the relevant command-line parameters when running Reticulum-based +programs. You can also run multiple separate Reticulum instances on the same +physical system, either in isolation from each other, or connected together. + +In most cases, a single physical system will only need to run one Reticulum +instance. This can either be launched at boot, as a system service, or simply +be brought up when a program needs it. In either case, any number of programs +running on the same system will automatically share the same Reticulum instance, +if the configuration allows for it, which it does by default. + +The entire configuration of Reticulum is found in the ``~/.reticulum/config`` +file. When Reticulum is first started on a new system, a basic, but fully functional +configuration file is created. The default configuration looks like this: + +.. code:: ini + + # This is the default Reticulum config file. + # You should probably edit it to include any additional, + # interfaces and settings you might need. + + # Only the most basic options are included in this default + # configuration. To see a more verbose, and much longer, + # configuration example, you can run the command: + # rnsd --exampleconfig + + + [reticulum] + + # If you enable Transport, your system will route traffic + # for other peers, pass announces and serve path requests. + # This should be done for systems that are suited to act + # as transport nodes, ie. if they are stationary and + # always-on. This directive is optional and can be removed + # for brevity. + + enable_transport = No + + + # By default, the first program to launch the Reticulum + # Network Stack will create a shared instance, that other + # programs can communicate with. Only the shared instance + # opens all the configured interfaces directly, and other + # local programs communicate with the shared instance over + # a local socket. This is completely transparent to the + # user, and should generally be turned on. This directive + # is optional and can be removed for brevity. + + share_instance = Yes + + + # If you want to run multiple *different* shared instances + # on the same system, you will need to specify different + # instance names for each. On platforms supporting domain + # sockets, this can be done with the instance_name option: + + instance_name = default + + # Some platforms don't support domain sockets, and if that + # is the case, you can isolate different instances by + # specifying a unique set of ports for each: + + # shared_instance_port = 37428 + # instance_control_port = 37429 + + + # If you want to explicitly use TCP for shared instance + # communication, instead of domain sockets, this is also + # possible, by using the following option: + + # shared_instance_type = tcp + + + # On systems where running instances may not have access + # to the same shared Reticulum configuration directory, + # it is still possible to allow full interactivity for + # running instances, by manually specifying a shared RPC + # key. In almost all cases, this option is not needed, but + # it can be useful on operating systems such as Android. + # The key must be specified as bytes in hexadecimal. + + # rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790 + + + # It is possible to allow remote management of Reticulum + # systems using the various built-in utilities, such as + # rnstatus and rnpath. You will need to specify one or + # more Reticulum Identity hashes for authenticating the + # queries from client programs. For this purpose, you can + # use existing identity files, or generate new ones with + # the rnid utility. + + # enable_remote_management = yes + # remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc + + + # You can configure Reticulum to panic and forcibly close + # if an unrecoverable interface error occurs, such as the + # hardware device for an interface disappearing. This is + # an optional directive, and can be left out for brevity. + # This behaviour is disabled by default. + + # panic_on_interface_error = No + + + # When Transport is enabled, it is possible to allow the + # Transport Instance to respond to probe requests from + # the rnprobe utility. This can be a useful tool to test + # connectivity. When this option is enabled, the probe + # destination will be generated from the Identity of the + # Transport Instance, and printed to the log at startup. + # Optional, and disabled by default. + + # respond_to_probes = No + + + [logging] + # Valid log levels are 0 through 7: + # 0: Log only critical information + # 1: Log errors and lower log levels + # 2: Log warnings and lower log levels + # 3: Log notices and lower log levels + # 4: Log info and lower (this is the default) + # 5: Verbose logging + # 6: Debug logging + # 7: Extreme logging + + loglevel = 4 + + + # The interfaces section defines the physical and virtual + # interfaces Reticulum will use to communicate on. This + # section will contain examples for a variety of interface + # types. You can modify these or use them as a basis for + # your own config, or simply remove the unused ones. + + [interfaces] + + # This interface enables communication with other + # link-local Reticulum nodes over UDP. It does not + # need any functional IP infrastructure like routers + # or DHCP servers, but will require that at least link- + # local IPv6 is enabled in your operating system, which + # should be enabled by default in almost any OS. See + # the Reticulum Manual for more configuration options. + + [[Default Interface]] + type = AutoInterface + interface_enabled = True + +If Reticulum infrastructure already exists locally, you probably don't need to +change anything, and you may already be connected to a wider network. If not, +you will probably need to add relevant *interfaces* to the configuration, in +order to communicate with other systems. + +You can generate a much more verbose configuration example by running the command: + +``rnsd --exampleconfig`` + +The output includes examples for most interface types supported +by Reticulum, along with additional options and configuration parameters. + +It is a good idea to read the comments and explanations in the above default config. +It will teach you the basic concepts you need to understand to configure your network. +Once you have done that, take a look at the :ref:`Interfaces` chapter +of this manual. + +Included Utility Programs +------------------------- + +Reticulum includes a range of useful utilities, both for managing your Reticulum +networks, and for carrying out common tasks over Reticulum networks, such as +transferring files to remote systems, and executing commands and programs remotely. + +If you often use Reticulum from several different programs, or simply want +Reticulum to stay available all the time, for example if you are hosting +a transport node, you might want to run Reticulum as a separate service that +other programs, applications and services can utilise. + +The rnsd Utility +================ + +It is very easy to run Reticulum as a service. Simply run the included ``rnsd`` command. +When ``rnsd`` is running, it will keep all configured interfaces open, handle transport if +it is enabled, and allow any other programs to immediately utilise the +Reticulum network it is configured for. + +You can even run multiple instances of ``rnsd`` with different configurations on +the same system. + +**Usage Examples** + +Run ``rnsd``: + +.. code:: text + + $ rnsd + + [2023-08-18 17:59:56] [Notice] Started rnsd version 0.5.8 + +Run ``rnsd`` in service mode, ensuring all logging output is sent directly to file: + +.. code:: text + + $ rnsd -s + +Generate a verbose and detailed configuration example, with explanations of all the +various configuration options, and interface configuration examples: + +.. code:: text + + $ rnsd --exampleconfig + +**All Command-Line Options** + +.. code:: text + + usage: rnsd.py [-h] [--config CONFIG] [-v] [-q] [-s] [--exampleconfig] [--version] + + Reticulum Network Stack Daemon + + options: + -h, --help show this help message and exit + --config CONFIG path to alternative Reticulum config directory + -v, --verbose + -q, --quiet + -s, --service rnsd is running as a service and should log to file + -i, --interactive drop into interactive shell after initialisation + --exampleconfig print verbose configuration example to stdout and exit + --version show program's version number and exit + +You can easily add ``rnsd`` as an always-on service by :ref:`configuring a service`. + +The rnstatus Utility +==================== + +Using the ``rnstatus`` utility, you can view the status of configured Reticulum +interfaces, similar to the ``ifconfig`` program. + +**Usage Examples** + +Run ``rnstatus``: + +.. code:: text + + $ rnstatus + + Shared Instance[37428] + Status : Up + Serving : 1 program + Rate : 1.00 Gbps + Traffic : 83.13 KB↑ + 86.10 KB↓ + + AutoInterface[Local] + Status : Up + Mode : Full + Rate : 10.00 Mbps + Peers : 1 reachable + Traffic : 63.23 KB↑ + 80.17 KB↓ + + TCPInterface[RNS Testnet Dublin/dublin.connect.reticulum.network:4965] + Status : Up + Mode : Full + Rate : 10.00 Mbps + Traffic : 187.27 KB↑ + 74.17 KB↓ + + RNodeInterface[RNode UHF] + Status : Up + Mode : Access Point + Rate : 1.30 kbps + Access : 64-bit IFAC by <…e702c42ba8> + Traffic : 8.49 KB↑ + 9.23 KB↓ + + Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running + +Filter output to only show some interfaces: + +.. code:: text + + $ rnstatus rnode + + RNodeInterface[RNode UHF] + Status : Up + Mode : Access Point + Rate : 1.30 kbps + Access : 64-bit IFAC by <…e702c42ba8> + Traffic : 8.49 KB↑ + 9.23 KB↓ + + Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running + +**All Command-Line Options** + +.. code:: text + + usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] + [-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path] + [-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter] + + Reticulum Network Stack Status + + positional arguments: + filter only display interfaces with names including filter + + options: + -h, --help show this help message and exit + --config CONFIG path to alternative Reticulum config directory + --version show program's version number and exit + -a, --all show all interfaces + -A, --announce-stats show announce stats + -l, --link-stats show link stats + -t, --totals display traffic totals + -s, --sort SORT sort interfaces by [rate, traffic, rx, tx, rxs, txs, + announces, arx, atx, held] + -r, --reverse reverse sorting + -j, --json output in JSON format + -R hash transport identity hash of remote instance to get status from + -i path path to identity used for remote management + -w seconds timeout before giving up on remote queries + -d, --discovered list discovered interfaces + -D show details and config entries for discovered interfaces + -m, --monitor continuously monitor status + -I, --monitor-interval seconds + refresh interval for monitor mode (default: 1) + -v, --verbose + + +.. note:: + When using ``-R`` to query a remote transport instance, you must also specify ``-i`` with the path to a management identity file that is authorized for remote management on the target system. + +The rnid Utility +==================== + +With the ``rnid`` utility, you can generate, manage and view Reticulum Identities. +The program can also calculate Destination hashes, and perform encryption and +decryption of files. + +Using ``rnid``, it is possible to asymmetrically encrypt files and information for +any Reticulum destination hash, and also to create and verify cryptographic signatures. + +**Usage Examples** + +Generate a new Identity: + +.. code:: text + + $ rnid -g ./new_identity + +Display Identity key information: + +.. code:: text + + $ rnid -i ./new_identity -p + + Loaded Identity <984b74a3f768bef236af4371e6f248cd> from new_id + Public Key : 0f4259fef4521ab75a3409e353fe9073eb10783b4912a6a9937c57bf44a62c1e + Private Key : Hidden + +Encrypt a file for an LXMF user: + +.. code:: text + + $ rnid -i 8dd57a738226809646089335a6b03695 -e my_file.txt + + Recalled Identity for destination <8dd57a738226809646089335a6b03695> + Encrypting my_file.txt + File my_file.txt encrypted for to my_file.txt.rfe + +If the Identity for the destination is not already known, you can fetch it from the network by using the ``-R`` command-line option: + +.. code:: text + + $ rnid -R -i 30602def3b3506a28ed33db6f60cc6c9 -e my_file.txt + + Requesting unknown Identity for <30602def3b3506a28ed33db6f60cc6c9>... + Received Identity <2b489d06eaf7c543808c76a5332a447d> for destination <30602def3b3506a28ed33db6f60cc6c9> from the network + Encrypting my_file.txt + File my_file.txt encrypted for <2b489d06eaf7c543808c76a5332a447d> to my_file.txt.rfe + +Decrypt a file using the Reticulum Identity it was encrypted for: + +.. code:: text + + $ rnid -i ./my_identity -d my_file.txt.rfe + + Loaded Identity <2225fdeecaf6e2db4556c3c2d7637294> from ./my_identity + Decrypting ./my_file.txt.rfe... + File ./my_file.txt.rfe decrypted with <2225fdeecaf6e2db4556c3c2d7637294> to ./my_file.txt + +**All Command-Line Options** + +.. code:: text + + usage: rnid.py [-h] [--config path] [-i identity] [-g path] [-v] [-q] [-a aspects] + [-H aspects] [-e path] [-d path] [-s path] [-V path] [-r path] [-w path] + [-f] [-R] [-t seconds] [-p] [-P] [--version] + + Reticulum Identity & Encryption Utility + + options: + -h, --help show this help message and exit + --config path path to alternative Reticulum config directory + -i, --identity identity + hexadecimal Reticulum identity or destination hash, or path to Identity file + -g, --generate file generate a new Identity + -m, --import identity_data + import Reticulum identity in hex, base32 or base64 format + -x, --export export identity to hex, base32 or base64 format + -v, --verbose increase verbosity + -q, --quiet decrease verbosity + -a, --announce aspects + announce a destination based on this Identity + -H, --hash aspects show destination hashes for other aspects for this Identity + -e, --encrypt file encrypt file + -d, --decrypt file decrypt file + -s, --sign path sign file + -V, --validate path validate signature + -r, --read file input file path + -w, --write file output file path + -f, --force write output even if it overwrites existing files + -R, --request request unknown Identities from the network + -t seconds identity request timeout before giving up + -p, --print-identity print identity info and exit + -P, --print-private allow displaying private keys + -b, --base64 Use base64-encoded input and output + -B, --base32 Use base32-encoded input and output + --version show program's version number and exit + +.. _utility-rnpath: + +The rnpath Utility +==================== + +With the ``rnpath`` utility, you can look up and view paths for +destinations on the Reticulum network. + +**Usage Examples** + +Resolve path to a destination: + +.. code:: text + + $ rnpath c89b4da064bf66d280f0e4d8abfd9806 + + Path found, destination is 4 hops away via on TCPInterface[Testnet/dublin.connect.reticulum.network:4965] + +**All Command-Line Options** + +.. code:: text + + usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-m hops] [-r] [-d] [-D] + [-x] [-w seconds] [-R hash] [-i path] [-W seconds] [-b] [-B] [-U] + [--duration DURATION] [--reason REASON] [-p] [-j] [-v] + [destination] [list_filter] + + Reticulum Path Management Utility + + positional arguments: + destination hexadecimal hash of the destination + list_filter filter for remote blackhole list view + + options: + -h, --help show this help message and exit + --config CONFIG path to alternative Reticulum config directory + --version show program's version number and exit + -t, --table show all known paths + -m, --max hops maximum hops to filter path table by + -r, --rates show announce rate info + -d, --drop remove the path to a destination + -D, --drop-announces drop all queued announces + -x, --drop-via drop all paths via specified transport instance + -w seconds timeout before giving up + -R hash transport identity hash of remote instance to manage + -i path path to identity used for remote management + -W seconds timeout before giving up on remote queries + -b, --blackholed list blackholed identities + -B, --blackhole blackhole identity + -U, --unblackhole unblackhole identity + --duration DURATION duration of blackhole enforcement in hours + --reason REASON reason for blackholing identity + -p, --blackholed-list + view published blackhole list for remote transport instance + -j, --json output in JSON format + -v, --verbose + + +The rnprobe Utility +==================== + +The ``rnprobe`` utility lets you probe a destination for connectivity, similar +to the ``ping`` program. Please note that probes will only be answered if the +specified destination is configured to send proofs for received packets. Many +destinations will not have this option enabled, so most destinations will not +be probable. + +You can enable a probe-reply destination on Reticulum Transport Instances by +setting the ``respond_to_probes`` configuration directive. Reticulum will then +print the probe destination to the log on Transport Instance startup. + +**Usage Examples** + +Probe a destination: + +.. code:: text + + $ rnprobe rnstransport.probe 2d03725b327348980d570f739a3a5708 + + Sent 16 byte probe to <2d03725b327348980d570f739a3a5708> + Valid reply received from <2d03725b327348980d570f739a3a5708> + Round-trip time is 38.469 milliseconds over 2 hops + +Send a larger probe: + +.. code:: text + + $ rnprobe rnstransport.probe 2d03725b327348980d570f739a3a5708 -s 256 + + Sent 16 byte probe to <2d03725b327348980d570f739a3a5708> + Valid reply received from <2d03725b327348980d570f739a3a5708> + Round-trip time is 38.781 milliseconds over 2 hops + +If the interface that receives the probe replies supports reporting radio +parameters such as **RSSI** and **SNR**, the ``rnprobe`` utility will print +these as part of the result as well. + +.. code:: text + + $ rnprobe rnstransport.probe e7536ee90bd4a440e130490b87a25124 + + Sent 16 byte probe to + Valid reply received from + Round-trip time is 1.809 seconds over 1 hop [RSSI -73 dBm] [SNR 12.0 dB] + +**All Command-Line Options** + +.. code:: text + + usage: rnprobe [-h] [--config CONFIG] [-s SIZE] [-n PROBES] + [-t seconds] [-w seconds] [--version] [-v] + [full_name] [destination_hash] + + Reticulum Probe Utility + + positional arguments: + full_name full destination name in dotted notation + destination_hash hexadecimal hash of the destination + + options: + -h, --help show this help message and exit + --config CONFIG path to alternative Reticulum config directory + -s SIZE, --size SIZE size of probe packet payload in bytes + -n PROBES, --probes PROBES + number of probes to send + -t seconds, --timeout seconds + timeout before giving up + -w seconds, --wait seconds + time between each probe + --version show program's version number and exit + -v, --verbose + + +The rncp Utility +================ + +The ``rncp`` utility is a simple file transfer tool. Using it, you can transfer +files through Reticulum. + +**Usage Examples** + +Run rncp on the receiving system, specifying which identities are allowed to send files: + +.. code:: text + + $ rncp --listen -a 1726dbad538775b5bf9b0ea25a4079c8 -a c50cc4e4f7838b6c31f60ab9032cbc62 + +You can also specify allowed identity hashes (one per line) in the file ~/.rncp/allowed_identities +and simply running the program in listener mode: + +.. code:: text + + $ rncp --listen + +From another system, copy a file to the receiving system: + +.. code:: text + + $ rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e + +Or fetch a file from the remote system: + +.. code:: text + + $ rncp --fetch ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e + +The default identity file is stored in ``~/.reticulum/identities/rncp``, but you can use +another one, which will be created if it does not already exist + +.. code:: text + + $ rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e -i /path/to/identity + +**All Command-Line Options** + +.. code:: text + + usage: rncp [-h] [--config path] [-v] [-q] [-S] [-l] [-F] [-f] + [-j path] [-b seconds] [-a allowed_hash] [-n] [-p] + [-i identity] [-w seconds] [--version] [file] [destination] + + Reticulum File Transfer Utility + + positional arguments: + file file to be transferred + destination hexadecimal hash of the receiver + + options: + -h, --help show this help message and exit + --config path path to alternative Reticulum config directory + -v, --verbose increase verbosity + -q, --quiet decrease verbosity + -S, --silent disable transfer progress output + -l, --listen listen for incoming transfer requests + -C, --no-compress disable automatic compression + -F, --allow-fetch allow authenticated clients to fetch files + -f, --fetch fetch file from remote listener instead of sending + -j, --jail path restrict fetch requests to specified path + -s, --save path save received files in specified path + -O, --overwrite Allow overwriting received files, instead of adding postfix + -b seconds announce interval, 0 to only announce at startup + -a allowed_hash allow this identity (or add in ~/.rncp/allowed_identities) + -n, --no-auth accept requests from anyone + -p, --print-identity print identity and destination info and exit + -i identity path to identity to use + -w seconds sender timeout before giving up + -P, --phy-rates display physical layer transfer rates + --version show program's version number and exit + + +The rnx Utility +================ + +The ``rnx`` utility is a basic remote command execution program. It allows you to +execute commands on remote systems over Reticulum, and to view returned command +output. For a fully interactive remote shell solution, be sure to also take a look +at the `rnsh `_ program. + +**Usage Examples** + +Run rnx on the listening system, specifying which identities are allowed to execute commands: + +.. code:: text + + $ rnx --listen -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10 + +From another system, run a command on the remote: + +.. code:: text + + $ rnx 7a55144adf826958a9529a3bcf08b149 "cat /proc/cpuinfo" + +Or enter the interactive mode pseudo-shell: + +.. code:: text + + $ rnx 7a55144adf826958a9529a3bcf08b149 -x + +The default identity file is stored in ``~/.reticulum/identities/rnx``, but you can use +another one, which will be created if it does not already exist + +.. code:: text + + $ rnx 7a55144adf826958a9529a3bcf08b149 -i /path/to/identity -x + +**All Command-Line Options** + +.. code:: text + + usage: rnx [-h] [--config path] [-v] [-q] [-p] [-l] [-i identity] [-x] [-b] [-n] [-N] + [-d] [-m] [-a allowed_hash] [-w seconds] [-W seconds] [--stdin STDIN] + [--stdout STDOUT] [--stderr STDERR] [--version] [destination] [command] + + Reticulum Remote Execution Utility + + positional arguments: + destination hexadecimal hash of the listener + command command to be execute + + optional arguments: + -h, --help show this help message and exit + --config path path to alternative Reticulum config directory + -v, --verbose increase verbosity + -q, --quiet decrease verbosity + -p, --print-identity print identity and destination info and exit + -l, --listen listen for incoming commands + -i identity path to identity to use + -x, --interactive enter interactive mode + -b, --no-announce don't announce at program start + -a allowed_hash accept from this identity + -n, --noauth accept files from anyone + -N, --noid don't identify to listener + -d, --detailed show detailed result output + -m mirror exit code of remote command + -w seconds connect and request timeout before giving up + -W seconds max result download time + --stdin STDIN pass input to stdin + --stdout STDOUT max size in bytes of returned stdout + --stderr STDERR max size in bytes of returned stderr + --version show program's version number and exit + + +The rnodeconf Utility +===================== + +The ``rnodeconf`` utility allows you to inspect and configure existing :ref:`RNodes`, and +to create and provision new :ref:`RNodes` from any supported hardware devices. + +**All Command-Line Options** + +.. code:: text + + usage: rnodeconf [-h] [-i] [-a] [-u] [-U] [--fw-version version] + [--fw-url url] [--nocheck] [-e] [-E] [-C] + [--baud-flash baud_flash] [-N] [-T] [-b] [-B] [-p] [-D i] + [--display-addr byte] [--freq Hz] [--bw Hz] [--txp dBm] + [--sf factor] [--cr rate] [--eeprom-backup] [--eeprom-dump] + [--eeprom-wipe] [-P] [--trust-key hexbytes] [--version] [-f] + [-r] [-k] [-S] [-H FIRMWARE_HASH] [--platform platform] + [--product product] [--model model] [--hwrev revision] + [port] + + RNode Configuration and firmware utility. This program allows you to change + various settings and startup modes of RNode. It can also install, flash and + update the firmware on supported devices. + + positional arguments: + port serial port where RNode is attached + + options: + -h, --help show this help message and exit + -i, --info Show device info + -a, --autoinstall Automatic installation on various supported devices + -u, --update Update firmware to the latest version + -U, --force-update Update to specified firmware even if version matches or is older than installed version + --fw-version version Use a specific firmware version for update or autoinstall + --fw-url url Use an alternate firmware download URL + --nocheck Don't check for firmware updates online + -e, --extract Extract firmware from connected RNode for later use + -E, --use-extracted Use the extracted firmware for autoinstallation or update + -C, --clear-cache Clear locally cached firmware files + --baud-flash baud_flash + Set specific baud rate when flashing device. Default is 921600 + -N, --normal Switch device to normal mode + -T, --tnc Switch device to TNC mode + -b, --bluetooth-on Turn device bluetooth on + -B, --bluetooth-off Turn device bluetooth off + -p, --bluetooth-pair Put device into bluetooth pairing mode + -D, --display i Set display intensity (0-255) + -t, --timeout s Set display timeout in seconds, 0 to disable + -R, --rotation rotation + Set display rotation, valid values are 0 through 3 + --display-addr byte Set display address as hex byte (00 - FF) + --recondition-display + Start display reconditioning + --np i Set NeoPixel intensity (0-255) + --freq Hz Frequency in Hz for TNC mode + --bw Hz Bandwidth in Hz for TNC mode + --txp dBm TX power in dBm for TNC mode + --sf factor Spreading factor for TNC mode (7 - 12) + --cr rate Coding rate for TNC mode (5 - 8) + -x, --ia-enable Enable interference avoidance + -X, --ia-disable Disable interference avoidance + -c, --config Print device configuration + --eeprom-backup Backup EEPROM to file + --eeprom-dump Dump EEPROM to console + --eeprom-wipe Unlock and wipe EEPROM + -P, --public Display public part of signing key + --trust-key hexbytes Public key to trust for device verification + --version Print program version and exit + -f, --flash Flash firmware and bootstrap EEPROM + -r, --rom Bootstrap EEPROM without flashing firmware + -k, --key Generate a new signing key and exit + -S, --sign Display public part of signing key + -H, --firmware-hash FIRMWARE_HASH + Set installed firmware hash + --platform platform Platform specification for device bootstrap + --product product Product specification for device bootstrap + --model model Model code for device bootstrap + --hwrev revision Hardware revision for device bootstrap + + +For more information on how to create your own RNodes, please read the :ref:`Creating RNodes` +section of this manual. + +.. _using-interface_discovery: +Discovering Interfaces +---------------------- + +Reticulum includes built-in functionality for discovering connectable interfaces over Reticulum itself. This is particularly useful in situations where you want to do one or more of the following: + +* Discover connectable entrypoints available on the Internet +* Find connectable radio access points in the physical world +* Maintain connectivity to RNS instances with unknown or changing IP addresses + +Discovered interfaces can be **auto-connected** by Reticulum, which makes it possible to create setups where an arbitrary interface can act simply as a bootstrap connection, that can be torn down again once more suitable interfaces have been discovered and connected. + +The interface discovery mechanism uses announces sent over Reticulum itself, and supports both publicly readable interfaces and private, encrypted discovery, that can only be decoded by specified *network identities*. It is also possible to specify which network identities should be considered valid sources for discovered interfaces, so that interfaces published by unknown entities are ignored. + +.. note:: + A *network identity* is a normal Reticulum identity keyset that can be used by + one or more transport nodes to identify them as belonging to the same overall + network. In the context of interface discovery, this makes it easy to manage + connecting to only the particular networks you care about, even if those networks + utilize many individual physical transport node. + + This also makes it convenient to auto-connect discovered interfaces only for networks you have some level of trust in. + +For information on how to make your interfaces discoverable, see the :ref:`Discoverable Interfaces` chapter of this manual. The current section will focus on how to actually *discover and connect to* interfaces available on the network. + +In its most basic form, enabling interface discovery is as simple as setting ``discover_interfaces`` to ``true`` in your Reticulum config: + +.. code:: text + + [reticulum] + ... + discover_interfaces = yes + ... + +Once this option is enabled, your RNS instance will start listening for interface discovery announces, and store them for later use or inspection. You can list discovered interfaces with the ``rnstatus`` utility: + +.. code:: text + + $ rnstatus -d + + Name Type Status Last Heard Value Location + ------------------------------------------------------------------------- + Sideband Hub Backbone ✓ Available 1h ago 16 46.2316, 6.0536 + RNS Amsterdam Backbone ✓ Available 32m ago 16 52.3865, 4.9037 + + +You can view more detailed information about discovered interfaces, including configuration snippets for pasting directly into your ``[interfaces]`` config, by using the ``rnstatus -D`` option: + +.. code:: text + + $ rnstatus -D sideband + + Transport ID : 521c87a83afb8f29e4455e77930b973b + Name : Sideband Hub + Type : BackboneInterface + Status : Available + Transport : Enabled + Distance : 2 hops + Discovered : 9h and 40m ago + Last Heard : 1h and 15m ago + Location : 46.2316, 6.0536 + Address : sideband.connect.reticulum.network:7822 + Stamp Value : 16 + + Configuration Entry: + [[Sideband Hub]] + type = BackboneInterface + enabled = yes + remote = sideband.connect.reticulum.network + target_port = 7822 + transport_identity = 521c87a83afb8f29e4455e77930b973b + +In addition to providing local interface discovery information and control, the ``rnstatus`` utility can export discovered interface data in machine-readable JSON format using the ``rnstatus -d --json`` option. This can be useful for exporting the data to external applications such as status pages, access point maps and similar. + +To control what sources are considered valid for discovered sources, additional +configuration options can be specified for the interface discovery system. + +* The ``interface_discovery_sources`` option is a list of the network or transport identities from which interfaces will be accepted. If this option is set, all others will be ignored. If this option is not set, discovered interfaces will be accepted from any source, but are still subject to stamp value requirements. + +* The ``required_discovery_value`` options specifies the minimum stamp value required for the interface announce to be considered valid. To make it computationally difficult to spam the network with a large number of defunct or malicious interfaces, each announced interface requires a valid cryptographical stamp, of configurable difficulty value. + +* The ``autoconnect_discovered_interfaces`` value defaults to ``0``, and specifies the maximum number of discovered interfaces that should be auto-connected at any given time. If set to a number greater than ``0``, Reticulum automatically manages discovered interface connections, and will bring discovered interfaces up and down based on availability. You can at any time add discovered interfaces to your configuration manually, to persistently keep them available. + +* The ``network_identity`` option specifies the *network identity* for this RNS instance. This identity is used both to sign (and potentially encrypt) *outgoing* interface discovery announces, and to decrypt incoming discovery information. + +The configuration snippet below contains an example of setting these additional configuration options: + +.. code:: text + + [reticulum] + ... + discover_interfaces = yes + interface_discovery_sources = 521c87a83afb8f29e4455e77930b973b + required_discovery_value = 16 + autoconnect_discovered_interfaces = 3 + network_identity = ~/.reticulum/storage/identities/my_network + ... + +Remote Management +----------------- + +It is possible to allow remote management of Reticulum +systems using the various built-in utilities, such as +``rnstatus`` and ``rnpath``. To do so, you will need to set +the ``enable_remote_management`` directive in the ``[reticulum]`` +section of the configuration file. You will also need to specify +one or more Reticulum Identity hashes for authenticating the +queries from client programs. For this purpose, you can use +existing identity files, or generate new ones with the rnid utility. + +The following is a truncated example of enabling remote management +in the Reticulum configuration file: + +.. code:: text + + [reticulum] + ... + enable_remote_management = yes + remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc + ... + +For a complete example configuration, you can run ``rnsd --exampleconfig``. + +.. _using-blackhole_management: + +Blackhole Management +-------------------- + +Reticulum networks are fundamentally permissionless and open, allowing anyone with a compatible interface to participate. While this openness is essential for a resilient and decentralized network, it also exposes the network to potential abuse, such as peers flooding the network with excessive announce broadcasts or other forms of resource exhaustion. + +The **Blackhole** system provides tools to help manage this problem. It allows operators and individual users to block specific identities at the Transport layer, preventing them from propagating announces through your node, and for other nodes to reach them through your network. + +.. important:: + + There is fundamentally **no way** to *globally* block or censor any identity or destination in Reticulum networks. The blackhole functionality will prevent announces from (and traffic to) all destinations associated with the blackholed identity *on your own network segments only*. + + This provides users and operators with control over what they want to allow *on their own network segments*, but there is no way to globally censor or remove an identity, as long as *someone* is willing to provide transport for it. + +This functionality serves a dual purpose: + +* **For Individual Users:** It offers a simple way to maintain a quiet and efficient local network by manually blocking spammy or unwanted peers. +* **For Network Operators:** It enables the creation of federated, community-wide security standards. By publishing and sharing blackhole lists, operators can protect large infrastructures and distribute spam filtering rules across the mesh without manual intervention. + + +Local Blackhole Management +========================== + +The most immediate way to manage unwanted identities is through manual configuration using the ``rnpath`` utility. This allows you to instantly block or unblock specific identities on your local Transport Instance. + +**Blackholing an Identity** + +To block an identity, use the ``-B`` (or ``--blackhole``) flag followed by the identity hash. You can optionally specify a duration and a reason, which are useful for logging and future reference. + +.. code:: text + + $ rnpath -B 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o + +You can also add a duration (in hours) and a reason: + +.. code:: text + + $ rnpath -B 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o --duration 24 --reason "Excessive announces" + +**Lifting Blackholes** + +To remove an identity from the blackhole, use the ``-U`` (or ``--unblackhole``) flag: + +.. code:: text + + $ rnpath -U 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o + +**Viewing the Blackhole List** + +To see all identities currently blackholed on your local instance, use the ``-b`` (or ``--blackholed``) flag: + +.. code:: text + + $ rnpath -b + + <3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o> blackholed for 23h, 56m (Excessive announces) + <399ea050ce0eed1816c300bcb0840938> blackholed indefinitely (Announce spam) + blackholed indefinitely (Announce spam) + <2b9ec651326d9bc274119054c70fb75e> blackholed indefinitely (Announce spam) + <1178a8f1fad405bf2ad153bf5036bdfd> blackholed indefinitely (Announce spam) + + + +Automated List Sourcing +======================= + +Manually blocking identities is effective for immediate threats, but maintaining an up-to-date blocklist for a large network is impractical. Reticulum supports **automated list sourcing**, allowing your node to subscribe to blackhole lists maintained by trusted peers, or a central authority you manage yourself. + +.. warning:: + **Verify Before Subscribing!** Subscribing to a blackhole source is a powerful action that grants that source the ability to dictate who you can communicate with. Before adding a source to your configuration, verify that the maintainer aligns with your usage policy and values. Blindly subscribing to untrusted lists could inadvertently block legitimate peers or essential services. + +When enabled, your Transport Instance will periodically (approximately once per hour) connect to configured sources, retrieve their latest blackhole lists, and automatically merge them into your local blocklist. This provides "set-and-forget" protection for both individual users and large networks. + +**Configuration** + +To enable automated sourcing, add the ``blackhole_sources`` option to the ``[reticulum]`` section of your configuration file. This option accepts a comma-separated list of Transport Identity hashes that you trust to provide valid blackhole lists. + +.. code:: ini + + [reticulum] + ... + # Automatically fetch blackhole lists from these trusted sources + blackhole_sources = 521c87a83afb8f29e4455e77930b973b, 68a4aa91ac350c4087564e8a69f84e86 + ... + +**How It Works** + +1. When enabled, the ``BlackholeUpdater`` service runs in the background. +2. For every identity hash listed in ``blackhole_sources``, it attempts to establish a temporary link to its associated``rnstransport.info.blackhole`` destination. +3. It requests the ``/list`` path, which returns a dictionary of blackholed identities and their associated metadata. +4. The received list is merged with your local ``blackholed_identities`` database. +5. The lists are persisted to disk, ensuring they survive restarts. + +.. note:: + You can verify the external lists you are subscribed to, and their contents, without importing them by using ``rnpath -p``. See the :ref:`rnpath utility documentation` for details on querying remote blackhole lists. + + +Publishing Blackhole Lists +========================== + +If you are operating a public gateway, a community hub, or simply wish to share your blackhole list with others, you can configure your instance to act as a blackhole list publisher. This allows other nodes to subscribe to *your* definitions of unwanted traffic. + +**Enabling Publishing** + +To publish your local blackhole list, enable the ``publish_blackhole`` option in the ``[reticulum]`` section: + +.. code:: ini + + [reticulum] + ... + publish_blackhole = yes + ... + +When this is enabled, your Transport Instance will register a request handler at ``rnstransport.info.blackhole``. Any peer that connects to this destination and requests ``/list`` will receive the complete set of identities currently present in your local blackhole database. + +**Federation and Trust** + +The blackhole system relies on the trust relationship between the subscriber and the publisher. By subscribing to a source, you are implicitly trusting that source to only block identities that are genuinely detrimental to the network. + +As the ecosystem matures, this system is designed to integrate with **Network Identities**. This allows communities to verify that a published blackhole list is actually provided by a specific network or organization with a certain level of reputation and trustworthiness, adding a layer of cryptographic trust to the federation process. This prevents malicious actors from publishing fake lists intended to censor legitimate traffic. + +For operators, this creates a scalable model where maintaining a single high-quality blocklist can protect thousands of downstream peers, drastically reducing the administrative. + +Improving System Configuration +------------------------------ + +If you are setting up a system for permanent use with Reticulum, there is a +few system configuration changes that can make this easier to administrate. +These changes will be detailed here. + + +Fixed Serial Port Names +======================= + +On a Reticulum instance with several serial port based interfaces, it can be +beneficial to use the fixed device names for the serial ports, instead +of the dynamically allocated shorthands such as ``/dev/ttyUSB0``. Under most +Debian-based distributions, including Ubuntu and Raspberry Pi OS, these nodes +can be found under ``/dev/serial/by-id``. + +You can use such a device path directly in place of the numbered shorthands. +Here is an example of a packet radio TNC configured as such: + +.. code:: text + + [[Packet Radio KISS Interface]] + type = KISSInterface + interface_enabled = True + outgoing = true + port = /dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_43891CKM-if00-port0 + speed = 115200 + databits = 8 + parity = none + stopbits = 1 + preamble = 150 + txtail = 10 + persistence = 200 + slottime = 20 + +Using this methodology avoids potential naming mix-ups where physical devices +might be plugged and unplugged in different orders, or when device name +assignment varies from one boot to another. + +.. _using-systemd: + +Reticulum as a System Service +============================= + +Instead of starting Reticulum manually, you can install ``rnsd`` as a system +service and have it start automatically at boot. + +Systemwide Service +^^^^^^^^^^^^^^^^^^ + +If you installed Reticulum with ``pip``, the ``rnsd`` program will most likely +be located in a user-local installation path only, which means ``systemd`` will not +be able to execute it. In this case, you can simply symlink the ``rnsd`` program +into a directory that is in systemd's path: + +.. code:: text + + sudo ln -s $(which rnsd) /usr/local/bin/ + +You can then create the service file ``/etc/systemd/system/rnsd.service`` with the +following content: + +.. code:: text + + [Unit] + Description=Reticulum Network Stack Daemon + After=multi-user.target + + [Service] + # If you run Reticulum on WiFi devices, + # or other devices that need some extra + # time to initialise, you might want to + # add a short delay before Reticulum is + # started by systemd: + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=USERNAMEHERE + ExecStart=rnsd --service + + [Install] + WantedBy=multi-user.target + +Be sure to replace ``USERNAMEHERE`` with the user you want to run ``rnsd`` as. + +To manually start ``rnsd`` run: + +.. code:: text + + sudo systemctl start rnsd + +If you want to automatically start ``rnsd`` at boot, run: + +.. code:: text + + sudo systemctl enable rnsd + +Userspace Service +^^^^^^^^^^^^^^^^^ + +Alternatively you can use a user systemd service instead of a system wide one. This way the whole setup can be done as a regular user. +Create a user systemd service files ``~/.config/systemd/user/rnsd.service`` with the following content: + +.. code:: text + + [Unit] + Description=Reticulum Network Stack Daemon + After=default.target + + [Service] + # If you run Reticulum on WiFi devices, + # or other devices that need some extra + # time to initialise, you might want to + # add a short delay before Reticulum is + # started by systemd: + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + ExecStart=RNS_BIN_DIR/rnsd --service + + [Install] + WantedBy=default.target + +Replace ``RNS_BIN_DIR`` with the path to your Reticulum binary directory (eg. /home/USERNAMEHERE/rns/bin). + +Start user service: + +.. code:: text + + systemctl --user daemon-reload + systemctl --user start rnsd.service + +If you want to automatically start ``rnsd`` without having to log in as the USERNAMEHERE, do: + +.. code:: text + + sudo loginctl enable-linger USERNAMEHERE + systemctl --user enable rnsd.service + + diff --git a/docs/manual/_sources/whatis.rst.txt b/docs/manual/_sources/whatis.rst.txt new file mode 100644 index 0000000..0e9e23f --- /dev/null +++ b/docs/manual/_sources/whatis.rst.txt @@ -0,0 +1,203 @@ +****************** +What is Reticulum? +****************** + +Reticulum is a cryptography-based networking stack for building both local and +wide-area networks with readily available hardware, that can continue to operate +under adverse conditions, such as extremely low bandwidth and very high latency. + +To understand the foundational philosophy and goals of this system, read the +:ref:`Zen of Reticulum `. + +Reticulum allows you to build wide-area networks with off-the-shelf tools, and +offers end-to-end encryption, forward secrecy, autoconfiguring cryptographically +backed multi-hop transport, efficient addressing, unforgeable packet +acknowledgements and more. + +From a users perspective, Reticulum allows the creation of applications that +respect and empower the autonomy and sovereignty of communities and individuals. +Reticulum enables secure digital communication that cannot be subjected to +outside control, manipulation or censorship. + +Reticulum enables the construction of both small and potentially planetary-scale +networks, without any need for hierarchical or bureaucratic structures to control +or manage them, while ensuring individuals and communities full sovereignty +over their own network segments. + +Reticulum is a **complete networking stack**, and does not need IP or higher +layers, although it is easy to utilise IP (with TCP or UDP) as the underlying +carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the +Internet or private IP networks. Reticulum is built directly on cryptographic +principles, allowing resilience and stable functionality in open and trustless +networks. + +No kernel modules or drivers are required. Reticulum can run completely in +userland, and will run on practically any system that runs Python 3. Reticulum +runs well even on small single-board computers like the Pi Zero. + + +Current Status +============== +All core protocol features are implemented and functioning, but additions will probably occur as +real-world use is explored. The API and wire-format can be considered complete and stable, but +could change if absolutely warranted. + + +Reference Implementation +======================== +The Python code, for which this documentation is written, and known as the Reticulum Network Stack, +is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely +and authoritatively by this reference implementation, and this manual. It is maintained by Mark Qvist, +identified by the Reticulum Identity ````. + +Compatibility with the Reticulum Protocol is defined as having full interoperability, +and sufficient functional parity with this reference implementation. Any specific protocol +implementation that achieves this is Reticulum. Any that does not is not Reticulum. + +The reference implementation is licensed under the :ref:`Reticulum License `. + +The Reticulum Protocol was dedicated to the Public Domain in 2016. + + +What does Reticulum Offer? +========================== + +* Coordination-less globally unique addressing and identification + +* Fully self-configuring multi-hop routing over heterogeneous carriers + +* Flexible scalability over heterogeneous topologies + + * Reticulum can carry data over any mixture of physical mediums and topologies + + * Low-bandwidth networks can co-exist and interoperate with large, high-bandwidth networks + +* Initiator anonymity, communicate without revealing your identity + + * Reticulum does not include source addresses on any packets + +* Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication + + * The foundational Reticulum Identity Keys are 512-bit Elliptic Curve keysets + +* Forward Secrecy is available for all communication types, both for single packets and over links + +* Reticulum uses the following format for encrypted tokens: + + * Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519 + + * AES-256 in CBC mode with PKCS7 padding + + * HMAC using SHA256 for authentication + + * IVs are generated through os.urandom() + +* Unforgeable packet delivery confirmations + +* Flexible and extensible interface system + + * Reticulum includes a large variety of built-in interface types + + * Ability to load and utilise custom user- or community-supplied interface types + + * Easily create your own custom interfaces for communicating over anything + +* Authentication and virtual network segmentation on all supported interface types + +* An intuitive and easy-to-use API + + * Simpler and easier to use than sockets APIs and simpler, but more powerful + + * Makes building distributed and decentralised applications much simpler + +* Reliable and efficient transfer of arbitrary amounts of data + + * Reticulum can handle a few bytes of data or files of many gigabytes + + * Sequencing, compression, transfer coordination and checksumming are automatic + + * The API is very easy to use, and provides transfer progress + +* Lightweight, flexible and expandable Request/Response mechanism + +* Efficient link establishment + + * Total cost of setting up an encrypted and verified link is only 3 packets, totalling 297 bytes + + * Low cost of keeping links open at only 0.44 bits per second + +* Reliable sequential delivery with Channel and Buffer mechanisms + + +Where can Reticulum be Used? +============================ +Over practically any medium that can support at least a half-duplex channel +with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, +modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, +ad-hoc WiFi, free-space optical links and similar systems are all examples +of the types of interfaces Reticulum was designed for. + +An open-source LoRa-based interface called `RNode `_ +has been designed as an example transceiver that is very suitable for +Reticulum. It is possible to build it yourself, to transform a common LoRa +development board into one, or it can be purchased as a complete transceiver +from various vendors. + +Reticulum can also be encapsulated over existing IP networks, so there's +nothing stopping you from using it over wired Ethernet or your local WiFi +network, where it'll work just as well. In fact, one of the strengths of +Reticulum is how easily it allows you to connect different mediums into a +self-configuring, resilient and encrypted mesh. + +As an example, it's possible to set up a Raspberry Pi connected to both a +LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are +added, Reticulum will take care of the rest, and any device on the WiFi +network can communicate with nodes on the LoRa and packet radio sides of the +network, and vice versa. + +Interface Types and Devices +=========================== +Reticulum implements a range of generalised interface types that covers the communications hardware that Reticulum can run over. If your hardware is not supported, it's simple to :ref:`implement an interface class`. Currently, Reticulum can use the following devices and communication mediums: + +* Any Ethernet device + + * WiFi devices + + * Wired Ethernet devices + + * Fibre-optic transceivers + + * Data radios with Ethernet ports + +* LoRa using `RNode `_ + + * Can be installed on `many popular LoRa boards `_ + + * Can be purchased as a `ready to use transceiver `_ + +* Packet Radio TNCs, such as `OpenModem `_ + + * Any packet radio TNC in KISS mode + + * Ideal for VHF and UHF radio + +* Any device with a serial port + +* The I2P network + +* TCP over IP networks + +* UDP over IP networks + +* Anything you can connect via stdio + + * Reticulum can use external programs and pipes as interfaces + + * This can be used to easily hack in virtual interfaces + + * Or to quickly create interfaces with custom hardware + +* Anything else using :ref:`custom interface modules` written in Python + +For a full list and more details, see the :ref:`Supported Interfaces` chapter. + diff --git a/docs/manual/_sources/zen.rst.txt b/docs/manual/_sources/zen.rst.txt new file mode 100644 index 0000000..4b996c8 --- /dev/null +++ b/docs/manual/_sources/zen.rst.txt @@ -0,0 +1,453 @@ +.. _zen: + +**************** +Zen of Reticulum +**************** + +The Illusion Of The Center +========================== + +For the better part of a generation, we have been taught to visualize the digital world through the lens of hierarchy. The mental maps we carry are dominated by a single, misleading image: **The Cloud**. + +We imagine the network as a vast, ethereal space "up there" or "out there". A centralized repository of services and data to which we, the lowly clients, must connect. We build our software with this assumption hardcoded into our logic: *There is a server. The server has the authority. The server knows the way. I must find the server to function*. + +This is the Client-Server mental model, and it is the primary obstacle to understanding Reticulum. + +Fallacy Of The Cloud +-------------------- + +The first step in the Zen of Reticulum is to realize that *there is no cloud*. There is only other people's computers. When you build for the cloud, you are building *for* a landlord. You are accepting that your application's existence is conditional on the permission, uptime, and continued goodwill of a central authority. + +In Reticulum, you must shift your thinking from "connecting to" to "being among". Reticulum is not a service you subscribe to - *it is a fabric you inhabit*. There is no "up there". There is only *here* and *there*, and the space between them is peer-to-peer. + +Decentralization Or Uncentralizability? +--------------------------------------- + +It is common to hear the word "decentralized" thrown around in modern tech circles. But often, this is merely a marketing term for "slightly distributed centralization". A blockchain with a few dominant miners, or a federated protocol with a few giant servers. *In practice*, it's still centralized. It simply has a few centers instead of one. + +Reticulum goes further. It wants **Uncentralizability**. + +This is not a wishful political stance, but a foundational mathematical characteristic of the protocol, onto which everything else has been built. Reticulum assumes that every peer on the network is potentially hostile, and every link is potentially compromised. It is designed with no "privileged" nodes. While some nodes may act as Transport Instances - forwarding traffic for others - they do so *blindly*, and they only know about their immediate surroundings, and nothing more. They route based on cryptographic proofs, not on administrative privilege. They cannot see who is talking to whom, nor can they selectively manipulate traffic without breaking their own ability to route entirely. + +The system is designed to make hierarchy structurally impossible. You cannot hijack an address, because there is no central registry to hijack. You cannot block a user, because there is no central switch to flip. You can offer paths through the network, but you can't force anyone to use them. + +Death To The Address +-------------------- + +To break free of the center, you must also let go of the concept of the "Address". + +In the IP world, an address is a location. It is a coordinate in a *deeply hierarchical* and static grid. If you move your computer to a different house, your address changes. If your router reboots, your address might change. Your *identity* is bound to your *location*, and therefore, it is fragile, and easily controlled. + +Reticulum abolishes this link between *Identity* and *Location*. + +In Reticulum, an address is not a place; it is a **Hash of an Identity**. It is a cryptographic representation of *who* you are, not *where* you are. Because of this, your address is portable. You can take a laptop from a WiFi cafe in Berlin, to a LoRa mesh in the mountains, to a packet radio link on a boat, and your "address" - your *Destination Hash* - never changes. + +The network does not route to a place; it routes to a *person* (or a machine). When you send a packet, you are not targeting a coordinate in a grid; you are encrypting a message for a specific entity. The network dynamically discovers where that entity currently resides, and it does so in a way where no one really knows where that entity is actually located physically. + +**Consider:** + +- **The Old Way:** *"I am at* ``192.168.1.5``. *Come find me"*. +- **The Zen Way:** *"I am* ``<327c1b2f87c9353e01769b01090b18f2>``. *Wherever I am, my peers can reach me"*. + +Once you stop thinking about servers and start thinking about portable identities, where everyone can always reach everyone else directly, the illusion of the center fades away. You realize there *is* no center holding the network together. No coordinators or bureaucrats required. The network is simply the sum of its peers, communicating directly, sovereignly, and without a master. + + +Physics Of Trust +================ +*Paranoia Is A Great Design Principle* + +If we accept that there is no center - that the network is a chaotic, peer-to-peer mesh - we are forced to confront a terrifying reality: **There is no one guarding the door**. + +In the traditional networking mindset, we rely on the concept of the "trusted core". We assume our local coffee shop WiFi is safe, or that the backbone providers are neutral custodians. We build our security like a castle: strong walls on the outside, soft and trusting on the inside. We use encryption only when we step out into the "wild" internet. + +Hostile Environments +-------------------- + +The Zen of Reticulum requires you to invert this. You must assume that *every* environment is hostile. This isn't cynicism, just uncaring physics. + +When you transmit information over radio waves, you are shouting into a crowded room. Anyone can listen. When you traverse the internet, your packets pass through routers controlled by strangers, corporations, and state actors. Assuming privacy in this environment without cryptographic protection is not optimism but gross negligence. + +Reticulum is built on the premise that every link is tapped, and every peer is a potential adversary. If your system cannot survive an adversary owning the physical layer, it cannot survive at all. + +But this is the paradox: By assuming the network is hostile, you make it safe. When you accept the dangers for what they are, they become manageable. When you stop trusting the infrastructure and start trusting the math, you eliminate the single point of failure: Human integrity. + +Encryption Is Not A Feature +--------------------------- + +In the world of TCP/IP, encryption is an afterthought. It is a layer we slap on top of the protocol (HTTPS, TLS) to patch the security holes of the original design. It is a "feature" you sometimes *enable* for "sensitive data". This is fundamentally flawed, since all data is sensitive. + +In Reticulum, encryption is **gravity**. + +It is not optional. It is not a plugin. It is the *fundamental force that allows the network to exist*. If you were to strip the encryption from Reticulum, the routing would break. The Transport system uses cryptographic signatures and entropy to verify paths and pass information. If packets were plaintext, intermediate nodes could not prove that a route was valid, nor could endpoints prevent spoofing or tampering. + +In Reticulum, the entropy of the encrypted packet *is* the routing logic. + +To ask for a version of Reticulum without encryption is like asking for a version of the ocean without liquid. You are not asking for a feature change; you're asking for a different physical universe. We design for a universe where information has mass, structure, and integrity. + +Zero-Trust Architectures +------------------------ + +We must unlearn our reliance on **Institutional Trust**. + +For decades, we have been trained to trust authorities. We trust a website because a chain of Certificate Authorities (companies we don't know) vouches for it. We trust an app because it is in an app store (run by a corporation we don't control). We trust a message because it comes from a phone number assigned by a telecom. Yet, everything in our digital information sphere today is more untrustworthy and risky than a medieval second-hand underwear market. + +Reticulum replaces institutional trust with **Cryptographic Proof**. + +In Reticulum, you do not trust a node because it has a nice hostname or because it is listed in a directory. You trust it because it holds the private key corresponding to the Destination Hash you are communicating with. This trust is binary, mathematical, and **absolute**. Either the signature matches, or it does not. There is no "maybe". + +This shift moves the power from the institution to the individual. You become the ultimate arbiter of your own trust relationships. You decide which keys to accept, which paths to follow, and which identities to recognize. + +**Consider:** + +- **The Old Way:** *"I trust this site because the browser says the lock icon is green"*. +- **The Zen Way:** *"I trust this destination because I have verified its hash fingerprint out-of-band, and the math confirms the signature"*. + +When you internalize the Physics of Trust, you stop looking for protection from firewalls, VPNs, and Terms of Service agreements. You realize that true security comes from the design of the protocol itself. You can stop trusting the cloud, and you start trusting the code - because you can verify it yourself. + + +Merits Of Scarcity +================== +*Every Bit Counts* + +We have grown addicted to abundance. In the modern digital ecosystem, bandwidth is treated as an endless, flat ocean. We stream high-definition video without a thought, we ship entire libraries of code just to render a single button, and we measure performance in gigabits per second. This abundance has hollowed out our craft. When constraints vanish, efficiency dies, and with it, a certain kind of Clarity and Quality. + +Reticulum asks you to step out of the ocean and onto the tightrope. + +The Bandwidth Fallacy +--------------------- + +The Zen of Reticulum requires the realization that **5 bits per second is a valid speed**. + +To a modern developer, this sounds like paralysis. But there is a profound freedom in limits: When you have a gigabit connection, you can be incredibly sloppy. You can be wasteful. You can push your problems onto the infrastructure. *"It’s slow? Get a faster router"*. + +But on a high-latency, low-bandwidth link (be it a noisy HF radio channel or a tenuous LoRa hop) you cannot push problems anywhere. You must solve them. The network does not negotiate with waste. + +This forces a shift from consumption to interaction. You are no longer, then, consuming a service provided by a fat pipe; you are engaging in a careful negotiation with the physical medium. The medium becomes a partner in the conversation, not just a dumb conduit. You suddenly need to *understand the world to be in it*. + +Cost Of A Byte +-------------- + +In a scarce economy, a byte is not just data, but energy, time, and space. + +Every byte you transmit consumes battery life on a solar-powered node. It occupies valuable airtime that could have been used by another peer. It represents a measurable slice of the electromagnetic spectrum. + +When you internalize this, you begin to write code differently. You stop asking, "How much data can I send?" and start asking, "What is the *minimum* amount of information required to convey this intent? How can I best utilize my informational entropy?" + +This is where the elegance of Reticulum shines. The protocol is designed to strip away the non-essential. A link establishment takes three very small packets. A destination hash fits in 16 bytes. The overhead is vanishingly small, leaving almost the entire channel for the message itself. + +**Consider:** + +- **The Old Way:** *"I need to send a status update. I'll send a JSON object with metadata, timestamps, and user profile info (15KB)."* +- **The Zen Way:** *"I need to send a status update. I'll send a single byte representing the state code. The context is already known."* + +This is of course optimization, but more importantly, *it is a form of respect*. Efficiency in a shared medium is an act of stewardship. By taking only what you need from the network, you leave room for others. The network listens to those who speak with purpose. + +Flow & Time +----------- + +Scarcity also teaches us about time. We have become addicted to the *synchronous* now - the instant ping, the real-time stream. But Reticulum embraces *asynchronous* time. + +When links are intermittent and latency is measured in minutes or hours, "real-time" is an illusion. Reticulum doesn't encourage **Store and Forward** as a mere fallback, but as a primary mode of existence. You write a message, it propagates when it can, and it arrives when it arrives. + +This changes the psychological texture of communication. It removes the anxiety of the immediate response. It allows for contemplation. You are not demanding the recipient's attention *right now*; you are placing a gift in their path, to be found when they are ready. + +By designing for delay, you design for resilience. You are no longer building a house of cards that collapses when a single packet drops. You are building a stone arch that distributes the load *over time*. + +Liberation From Limits +---------------------- + +There is a strange optimism in scarcity. When you are forced to work within strict constraints, you are forced to prioritize. *You* must decide what truly matters. *That* is the real core of agency. + +In the infinite fantasy world of The Cloud, everything is urgent, so nothing is. In the economy of Reticulum, the cost of transmission forces you to weigh the value of your message. Do you really need to send that heart beat? Is that photo essential? + +When you strip away the noise, what remains is *signal*. + +This discipline creates a different kind of developer. It creates a craftsman who understands that the best code is the code you don't have to write. It creates a user who understands that the most powerful message is the one that is *understood*, not the one that is loudest. In the world of Reticulum, you are not a mere consumer of bandwidth; you are an architect of intent. + + +Sovereignty Through Infrastructure +================================== +**Be Your Own Network** + +We live in an era of digital tenancy. We lease our connectivity from ISPs. We rent our storage from cloud providers. We even borrow our identity from social media platforms. We are tenants in a house we did not build, governed by rules we did not write, subject to eviction at the whim of a landlord who has never met us. + +The Zen of Reticulum is the realization that you *can* own the house. + +A Carrier-Grade Fallacy +----------------------- + +For decades, we have been gaslit into believing that networking is really not just hard, but impossible. It is presented as a dark art reserved for telcos and billionaires, requiring millions of dollars of fiber optics, climate-controlled data centers, and armies of engineers. We are told that building reliable infrastructure is "too complex" for the individual or small organization. + +This is a big, fat lie. + +Physics is simple. A radio wave needs a transmitter and a receiver. A packet needs a path. The "complexity" of the modern internet is largely bureaucratic - a mountain of billing systems, regulatory capture, and legacy cruft designed to keep the gatekeepers in power. + +Reticulum strips away the bureaucracy. It runs on hardware that costs the price of a dinner. It runs on spectrum that is free to use. It demonstrates that a robust, planetary-scale network does not require a Fortune 500 company. It requires only the will to deploy, and the distributed, uncoordinated efforts of many individuals. + +Personal Infrastructure +----------------------- + +This is where the rubber meets the road. You can read about Reticulum, you can understand the theory, but the insights only arrive when you plug in a radio and run a Transport Node. Suddenly, you are no longer a consumer. You're an operator. + +This shift is subtle but profound. When you run your own infrastructure, the network ceases to be a service that is provided *to* you. It becomes a space that you *inhabit*. You become responsible for the flow of information. You gain an intimate understanding of the medium - the way the weather affects the radio waves, the way the topology changes, the way the packets dance through the ether. + +There is a quiet competence that comes from this. You stop asking "Is the internet down?" and start asking "Is *my* links up?" You stop waiting for a technician and start checking the logs. This is a form of strength. To understand the system that carries your words is to be free from the mystery that keeps you dependent. + +The Ability To Disconnect +------------------------- + +Why go to the trouble? Why buy the radio, write the config, and leave the Pi running in the corner? + +Because the old, centralized network is fragile. And because most of us doesn't even really want to be there anymore. + +The internet we rely on today is a chain of single points of failure. Cut the undersea cable, and a continent goes dark. Shut down the power grid, and the cloud evaporates. Deprioritize the "wrong" traffic, and the flow of information is strangled. + +Sovereignty is the ability to survive the cut, whether or not that cut was an accident or on purpose. + +When you build your own infrastructure, you build a lifeline. Reticulum is designed to function over media that the traditional internet cannot touch - bare wires, battery-powered radios, ad-hoc WiFi meshes. When the grid fails, or the censors arrive, or the bill goes unpaid, your Reticulum network continues to hum. + +This is not about "dropping out" of society. It is about building a substrate on which an actual *Society* can function. + +**Consider:** + +- **The Old Way:** "My connection is slow. I should call my ISP and complain." +- **The Zen Way:** "The path is noisy. I will adjust the antenna or find a better route." + +By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen. + + +Identity and Nomadism +===================== +**A Fluid Self** + +In the old world, you are defined by your coordinates. If you are at ``34.109.71.5``, you're *here*. If you unplug the cable and walk down the street, you vanish. Your digital self evaporates because it was tethered to the wall. You are a ghost in the endless machinations of gears, levers and transistors, bound to the hardware, and those that own it. + +This creates a subtle, constant anxiety. We are terrified of disconnecting because, in the architecture of the old web, disconnecting is a kind of death. + +The Zen of Reticulum offers a different way to be. + +Portable Existence +------------------ + +In Reticulum, your identity is not a location, or a username granted by a service. It is a cryptographic key - a complex, unique mathematical signature that exists independently of the physical world. You can carry it only in your mind, if you want to. + +Think of it less like a street address and more like a name. *A true name*. + +If you travel from Berlin to Tokyo, you do not change your name. You are still you. The people who know you can still recognize you. Reticulum applies this principle to the network layer. Your Destination Hash is **invariant**. It travels with you, stored securely on your device, *immutable as a stone*. + +This changes the relationship between you and the machine. You are not "logged into" the network via a specific gateway. You *are* the endpoint. The network does not connect to a place; *it converges on you*. + +Roaming Nodes +------------- + +This freedom introduces a new concept of time and space: **Nomadism**. + +Because your identity is portable, your connectivity can be fluid. You can be sitting at a desk connected to a fiber backbone one moment, and walking through a field connected only to a long-range LoRa mesh the next. To the rest of the network, nothing has changed. Your friends do not need to update your contact info. The messages they send do not bounce back. The network senses the shift in the medium and reroutes the flow of data automatically. + +You are no longer a stationary node in a fixed grid. You are a wanderer in a fluid medium. + +The interfaces - whether it is WiFi, Ethernet, Packet Radio, or a physical wire - is merely the clothing your node wears. You change it to suit the environment. Underneath, you remain the same. This is the liberation of the protocol. It treats the physical medium as a transient circumstance, not a definition of self. + +**Consider:** + +- **The Old Way:** *"I lost connection. I have to reconnect to the VPN to tell them where I am now."* +- **The Zen Way:** *"I moved. The network subtly bends to accomodate this new reality."* + +Announcing Presence +------------------- + +How does the network find a wanderer? It listens. + +In the IP world, we query directories. We ask a server, "Where is Mark?" The server checks its database and gives us a coordinate. This means that someone, somewhere, is keeping track of you. It assumes and *requires* surveillance. + +Reticulum replaces surveillance with **Announces**. + +Instead of asking a central authority where you are, you simply state your presence. You broadcast a cryptographic proof: "I am here, and I am who I say I am". This ripples out through the mesh. Your neighbors hear it, update their path tables, and pass it on. + +This is a quiet, organic process. It is the digital equivalent of lighting lanterns in the dark. You do not need to chase the light; you let the light find you. It respects your autonomy. You choose when to announce, how often to speak, and to whom. You also choose when to disappear - for but a moment or perpetually. + +Anchor In The Flow +------------------ + +There is a deep peace in this nomadism. It teaches you that stability does not come from standing still. Stability comes from *internal coherence*. + +By holding your own private key, you hold your own center of gravity. The world around you; the infrastructure, the topography and the availability of links can all shift chaotically. Storms can knock out towers. Cables can be cut. The internet can go down. + +But as long as you possess your key, you possess your identity. The entire infrastructure can be destroyed and rebuilt, and you are still you. Nothing lasts, yet nothing is lost. + +You become a sovereign entity moving through the noise, connected not by the rigidity of cables, but by the fluidity of recognition. The network becomes a place you inhabit, rather than a utility you subscribe to: You are at home in the ether. + + +Ethics Of The Tool +================== +**Technology With Conscience** + +You have unlearned the center. You have accepted the physics of trust. You have embraced the economy of scarcity and the freedom of unbound nomadism. You are standing in a new space. Now, look at the tool in your hand. + +In the old world, we were taught that technology is neutral. We are told that "guns don't kill people, people do", or that a component is just a component, indifferent to what its combinatorial potential is. This is a convenient lie. It serves only to allow the builders to wash their hands of responsibility. + +But we know better now. We know that **architecture is politics**, and *politics is control*. The way you build a system determines how it will be used. If you build a system optimized for mass surveillance, you *will* get a panopticon. If you build a system optimized for centralized control, you *will* get a dictatorship. If you build a system optimized for extraction, you *will* get a parasite. + +The Zen of Reticulum asserts that a tool is never neutral. + +On the very contrary: A tool is intent, **crystallized**. + +The Harm Principle +------------------ + +Why does the Reticulum License forbid the software from being used in systems designed to harm humans? Is it not just a restriction on freedom? + +It is a restriction on *license*, yes, but it is an expansion of *freedom*. + +Building powerful tools without a moral compass is in no way virtuous or commendable, it is plain and simple irresponsibility. + +A tool that can easily be used to oppress is a real danger to the user. If you build a network that can be turned against you by a tyrant, you are not free. You are merely waiting for the leash to tighten. By encoding the "Harm Principle" into the legal DNA of the reference implementation, we are building a safeguard. We are stating, clearly and immutably, that *this tool* is for **life**, not for death. + +This aligns the software with the interests of humanity. It cements that the network cannot be conscripted into a kill-system, a weaponized drone controller, or a torture device without breaking the license and the law. It is a line drawn in the sand - not by a government or external authority, but by the creators of the tool itself. + +**Consider:** + +- **The Old Way:** *"It's just software. How people use it is not my problem."* +- **The Zen Way:** *"This software is a habitat. I will not allow it to be used to build a cage."* + +It is *your* choice whether to align with this - we are not forcing this stance on anyone. If you choose to align with life over death, with creativity over destruction, we grant you an immensely powerful tool, to own and build with as you please. If you do not, we deny it. + +If you do not like this, we most assuredly do not need you here, and you are on your own. + +Public Domain Protocol +---------------------- + +This leads to a vital distinction: The difference between the *idea* and the *implementation*. + +The protocol - the mathematical rules of how Reticulum works - is dedicated to the Public Domain. It belongs to humanity. **No one can own it**. Anyone can implement it, improve it, or adapt it. This is the core idea of free communication, which itself must be forever free. + +But the functional, deployed *reference implementation* - the Python code, the maintenance, the years of labor - has a conscience. This distinction is the engine of sustainability. It allows the protocol to be universal, while ensuring that the specific labor of the builders is not hijacked to undermine the foundational intent of the project itself. From this document, it should be very clear what this intent is. + +If you want to build a system with Reticulum that manipulates and damages users for profits or targets missiles, you can use the public domain protocol, and start from scratch. But you cannot take our work. You must do your own. This serves as a pillar of accountability. If you want to build a weapon, *you* go and forge the steel yourself, while the world observes. And when the blood is drawn - it is on **your** hands. + +Preserving Human Agency +----------------------- + +We live in an era of predatory extraction. The open-source commons is being scraped, ingested, and regurgitated by machine learning algorithms, whose corporate owners seek to replace the very humans who built those commons. Our code, our words, and our creativity is being used to train systems that are specifically designed to make us obsolete, without offering anything else in return than serfdom and leashes. + +Reticulum stands against this. + +The license protects the software from being used to feed the beast. It draws a hard line: This tool is for *people*. It is for human-to-human connection. It is not a dataset to be strip-mined for the purpose of building a synthetic overlord, puppeteered by a miniscule conglomerate of controllers. + +This is a radical act of preservation. By protecting the code from AI appropriation, we are protecting space for human agency. We are ensuring that there remains a digital realm where the actors are flesh, blood and soul, where decisions are made by minds, not overlords hiding behind models. + +When you use Reticulum, you are using a tool that respects you. It does not see you as a product to be tracked. It does not see your data as fuel for an algorithm. It sees you as a sovereign, equal peer. + +This changes the foundational premise of using the technology. It restores dignity to the interaction. You are not the user of a service; you are a participant in a mutual covenant. The tool aligns with your autonomy, rather than eroding it. + +In this way, ethics is not a restriction, but a foundation. It is the foundation that helps ensure the network will still belong to you tomorrow. + + +Design Patterns For Post-IP Systems +=================================== +**Practical Philosophy for Developers** + +The philosophy is useless if it cannot be hammered into code. The metaphors we have explored - nomadism, scarcity, trust - are not just poetry, but real-world engineering constraints. When you sit down to write software for Reticulum, these concepts must shape the very structure of your application. + +We are now moving from the *why* to the *how*. This is where the abstract becomes concrete, and where you will see the true depth of the patterns we have been weaving. + +Store & Forward +--------------- + +The web has trained us to be impatient. We write synchronous code. We fire a request and we wait, blocking the UI, holding our breath. If the response doesn't come in 250 milliseconds, we show a spinner. If it doesn't come in five seconds, we show an error. We treat network connectivity as a binary state: either we are "online" or we are "broken". + +This is brittle. It is a rejection of reality. + +In Reticulum, connectivity is a spectrum, and presence is asynchronous. If at all applicable to your intent, you must design your applications to embrace **Store & Forward**. + +Instead of demanding an immediate answer, your application should act as a patient participant. You create a message for someone or something in the mesh. The network holds it. It carries it from node to node, perhaps over hours or days, waiting for the recipient to appear. When they finally surface, the message is delivered. This requires a shift from "request/response" to "event/handler". How exactly you do this is a challenge for you to solve intelligently within your problem domain, but Reticulum-based systems already exist that does this extremely well, and you can use them for inspiration. + +**Consider:** + +- **The Old Way:** ``Connect() -> Send() -> Wait() -> Crash if timeout.`` +- **The Zen Way:** ``Send() -> Continue living. -> Receive() when it arrives.`` + +This changes the user experience profoundly. It removes the anxiety of the loading bar. It creates a sense of continuity. The user is not "waiting for the network"; they are interacting with a persistent log of communication that lives in the network itself. + +Naming Is Power +--------------- + +In the IP world, we are slaves to the Domain Name System. We rely on a hierarchy of registrars to map human-readable names to machine-readable addresses. This hierarchy is a choke point. If the registrar revokes your domain, or if the DNS server goes down, you vanish. + +Reticulum dissolves this hierarchy with **Hash-based Identity**. + +In this design pattern, a name is not a string you look up; it is a cryptographic destination you verify. When you design for Reticulum, you stop asking the user for a URL and start asking for a Destination or Identity Hash. + +This feels strange at first. A hash like ``<83b7328926fed0d2e6a10a7671f9e237>`` looks alien compared to ``myfriend.com``. But that alienness is the armor. It **cannot** be spoofed. It **cannot** be censored by a registrar. It is **absolute**. + +Designing for this means shifting your UI metaphors. You are no longer browsing a web of pages; you are managing a ledger of keys. You are building an "Address Book" that is actually a keyring. The names are given by the user, and the power stays with them. That hashes look complex is directly analogous to the strengths of the bonds formed by their use. It forces the user to engage in a moment of verification, an out-of-band handshake, which restores the human element of trust that SSL certificates stripped away. + +The Interface Is The Medium +--------------------------- + +One of the most liberating patterns in Reticulum is **Transport Agnosticism**. + +In traditional networking, your code is often littered with transport logic. "Am I on WiFi? Check bandwidth. Am I on Cellular? Check data plan. Am I on Ethernet?". You are constantly micromanaging the pipe. + +In Reticulum, you write to the API, and the API writes to the medium. You send a packet to a Destination. You do not care if that packet travels over a TCP tunnel, a LoRa radio wave, or a serial wire interface. That is the stack's concern. + +This allows you to write **Universal Applications**. +Imagine a messaging app. You write it once. It works on a laptop connected to fiber. It works on a phone in the city using WiFi. And, without a single line of code changed, it works on a device in the wilderness, talking only to other devices via radio. + +The pattern is simple: **Never code to the hardware. Code to the intent.** + +**Consider:** + +- **The Old Way:** ``socket.connect(ip, port)``, and then a whole lot more +- **The Zen Way:** ``RNS.Packet(destination, data).send()`` + +By abstracting the medium, you make your software immortal to changes in infrastructure. The user might switch from a 4G hotspot to a HF modem tomorrow. Your software doesn't need to know. It simply continues the conversation. + +Emergent Patterns +----------------- + +When you combine these patterns - *Store & Forward*, *Hash-based Identity*, and *Transport Agnosticism* - you create software that feels fundamentally different. + +It feels *grounded*. It doesn't flicker when the signal drops. It doesn't panic when the server is down. It has weight. It has persistence. It has *relevance*. + +You are no longer building a "client" that begs a "server" for attention. You are building an autonomous agent that exists within the mesh. It speaks when it needs to, listens when it can, and carries its identity with it wherever it goes. + +This is the culmination of the Zen. The code is not just a set of instructions: It is a behavioral envelope. It is a way of *being* in the network. + + +Fabric Of The Independent +========================= + +We have stripped away the illusions. We have seen that the center is empty, that trust *must* be hard, that resources are finite, and that we must own our infrastructure. We have seen that tools have ethics and that our identity can move fluidly. + +This is a reclaiming of the commons. For too long, we have allowed the most vital substrate of human society - *our ability to speak to one another* - to be colonized by entities that do not share our interests. We have allowed the architecture of our communication to be designed by accountants rather than architects. + +We are taking it back. Not by petitioning the masters, but by building the new world within, over, under and around the shell of the old. + +The Work Is Finished +-------------------- + +The heavy lifting is done. + +The protocol is in the public domain, a gift to humanity that can never be taken away. The software is written, tested, and running on devices scattered across the globe. The manual lies open before you. The source code for the reference implementation is now distributed on hundreds of thousands of devices across the planet. No one can delete or destroy it. The hardware is accessible and abundant. + +It was a hard road to get here, but we got here. Now, there is no roadmap committee waiting for approval. There is no venture capital dictating the user experience. There is no CEO to sign off on the next feature release. + +There is only you. + +The barrier to entry is no longer complexity: It is the mere habit of dependency. You were conditioned to wait. Wait for the app update. Wait for the ISP to fix the line. Wait for the platform to allow the post. Wait for the government to change the policies. Wait for the likes. Wait for the revolution to be televised. + +The revolution never was televised. + +It is packetized. + +Open Sky +-------- + +The future of this technology is a construction project. + +It looks like a single node on a windowsill, listening to the static. It looks like a message sent to a neighbor, bypassing the noise of the commercial web. It looks like a community mesh that grows, link by link, hop by hop, carried by hands that care more about connection than profit. + +You have the blueprints. You have the tools. You have the philosophy. The noise of the old world has fallen away, leaving you with the quiet clarity of the open spectrum. + +*Mark, early 2026* \ No newline at end of file diff --git a/docs/manual/_static/basic.css b/docs/manual/_static/basic.css new file mode 100644 index 0000000..4738b2e --- /dev/null +++ b/docs/manual/_static/basic.css @@ -0,0 +1,906 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/manual/_static/check-solid.svg b/docs/manual/_static/check-solid.svg new file mode 100644 index 0000000..92fad4b --- /dev/null +++ b/docs/manual/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/manual/_static/clipboard.min.js b/docs/manual/_static/clipboard.min.js new file mode 100644 index 0000000..54b3c46 --- /dev/null +++ b/docs/manual/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/docs/manual/_static/copybutton.css b/docs/manual/_static/copybutton.css new file mode 100644 index 0000000..f1916ec --- /dev/null +++ b/docs/manual/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/docs/manual/_static/copybutton.js b/docs/manual/_static/copybutton.js new file mode 100644 index 0000000..2ea7ff3 --- /dev/null +++ b/docs/manual/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/docs/manual/_static/copybutton_funcs.js b/docs/manual/_static/copybutton_funcs.js new file mode 100644 index 0000000..dbe1aaa --- /dev/null +++ b/docs/manual/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/docs/manual/_static/custom.css b/docs/manual/_static/custom.css new file mode 100644 index 0000000..603b20f --- /dev/null +++ b/docs/manual/_static/custom.css @@ -0,0 +1,20 @@ +h3 { + margin-top: 1.75rem; + margin-bottom: 0.5rem; +} + +code.literal { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + padding-top: 0.25rem !important; + padding-bottom: 0.15rem !important; +} + +img[src*="if_mode_graph_b.png"] { + background-color: rgb(169, 177, 186); +} + +dt.sig { + margin-bottom: 0.75rem; + margin-top: 1.75rem; +} diff --git a/docs/manual/_static/debug.css b/docs/manual/_static/debug.css new file mode 100644 index 0000000..74d4aec --- /dev/null +++ b/docs/manual/_static/debug.css @@ -0,0 +1,69 @@ +/* + This CSS file should be overridden by the theme authors. It's + meant for debugging and developing the skeleton that this theme provides. +*/ +body { + font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji"; + background: lavender; +} +.sb-announcement { + background: rgb(131, 131, 131); +} +.sb-announcement__inner { + background: black; + color: white; +} +.sb-header { + background: lightskyblue; +} +.sb-header__inner { + background: royalblue; + color: white; +} +.sb-header-secondary { + background: lightcyan; +} +.sb-header-secondary__inner { + background: cornflowerblue; + color: white; +} +.sb-sidebar-primary { + background: lightgreen; +} +.sb-main { + background: blanchedalmond; +} +.sb-main__inner { + background: antiquewhite; +} +.sb-header-article { + background: lightsteelblue; +} +.sb-article-container { + background: snow; +} +.sb-article-main { + background: white; +} +.sb-footer-article { + background: lightpink; +} +.sb-sidebar-secondary { + background: lightgoldenrodyellow; +} +.sb-footer-content { + background: plum; +} +.sb-footer-content__inner { + background: palevioletred; +} +.sb-footer { + background: pink; +} +.sb-footer__inner { + background: salmon; +} +.sb-article { + background: white; +} diff --git a/docs/manual/_static/doctools.js b/docs/manual/_static/doctools.js new file mode 100644 index 0000000..0398ebb --- /dev/null +++ b/docs/manual/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/manual/_static/documentation_options.js b/docs/manual/_static/documentation_options.js new file mode 100644 index 0000000..3cbc3d8 --- /dev/null +++ b/docs/manual/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '1.1.6', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/docs/manual/_static/file.png b/docs/manual/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/docs/manual/_static/file.png differ diff --git a/docs/manual/_static/language_data.js b/docs/manual/_static/language_data.js new file mode 100644 index 0000000..c7fe6c6 --- /dev/null +++ b/docs/manual/_static/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/docs/manual/_static/minus.png b/docs/manual/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/docs/manual/_static/minus.png differ diff --git a/docs/manual/_static/plus.png b/docs/manual/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/docs/manual/_static/plus.png differ diff --git a/docs/manual/_static/pygments.css b/docs/manual/_static/pygments.css new file mode 100644 index 0000000..9d1083b --- /dev/null +++ b/docs/manual/_static/pygments.css @@ -0,0 +1,250 @@ +.highlight pre { line-height: 125%; } +.highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #fdf2e2 } +.highlight { background: #f2f2f2; color: #1E1E1E } +.highlight .c { color: #515151 } /* Comment */ +.highlight .err { color: #D71835 } /* Error */ +.highlight .k { color: #8045E5 } /* Keyword */ +.highlight .l { color: #7F4707 } /* Literal */ +.highlight .n { color: #1E1E1E } /* Name */ +.highlight .o { color: #163 } /* Operator */ +.highlight .p { color: #1E1E1E } /* Punctuation */ +.highlight .ch { color: #515151 } /* Comment.Hashbang */ +.highlight .cm { color: #515151 } /* Comment.Multiline */ +.highlight .cp { color: #515151 } /* Comment.Preproc */ +.highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +.highlight .c1 { color: #515151 } /* Comment.Single */ +.highlight .cs { color: #515151 } /* Comment.Special */ +.highlight .gd { color: #00749C } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gh { color: #00749C } /* Generic.Heading */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #00749C } /* Generic.Subheading */ +.highlight .kc { color: #8045E5 } /* Keyword.Constant */ +.highlight .kd { color: #8045E5 } /* Keyword.Declaration */ +.highlight .kn { color: #8045E5 } /* Keyword.Namespace */ +.highlight .kp { color: #8045E5 } /* Keyword.Pseudo */ +.highlight .kr { color: #8045E5 } /* Keyword.Reserved */ +.highlight .kt { color: #7F4707 } /* Keyword.Type */ +.highlight .ld { color: #7F4707 } /* Literal.Date */ +.highlight .m { color: #7F4707 } /* Literal.Number */ +.highlight .s { color: #163 } /* Literal.String */ +.highlight .na { color: #7F4707 } /* Name.Attribute */ +.highlight .nb { color: #7F4707 } /* Name.Builtin */ +.highlight .nc { color: #00749C } /* Name.Class */ +.highlight .no { color: #00749C } /* Name.Constant */ +.highlight .nd { color: #7F4707 } /* Name.Decorator */ +.highlight .ni { color: #163 } /* Name.Entity */ +.highlight .ne { color: #8045E5 } /* Name.Exception */ +.highlight .nf { color: #00749C } /* Name.Function */ +.highlight .nl { color: #7F4707 } /* Name.Label */ +.highlight .nn { color: #1E1E1E } /* Name.Namespace */ +.highlight .nx { color: #1E1E1E } /* Name.Other */ +.highlight .py { color: #00749C } /* Name.Property */ +.highlight .nt { color: #00749C } /* Name.Tag */ +.highlight .nv { color: #D71835 } /* Name.Variable */ +.highlight .ow { color: #8045E5 } /* Operator.Word */ +.highlight .pm { color: #1E1E1E } /* Punctuation.Marker */ +.highlight .w { color: #1E1E1E } /* Text.Whitespace */ +.highlight .mb { color: #7F4707 } /* Literal.Number.Bin */ +.highlight .mf { color: #7F4707 } /* Literal.Number.Float */ +.highlight .mh { color: #7F4707 } /* Literal.Number.Hex */ +.highlight .mi { color: #7F4707 } /* Literal.Number.Integer */ +.highlight .mo { color: #7F4707 } /* Literal.Number.Oct */ +.highlight .sa { color: #163 } /* Literal.String.Affix */ +.highlight .sb { color: #163 } /* Literal.String.Backtick */ +.highlight .sc { color: #163 } /* Literal.String.Char */ +.highlight .dl { color: #163 } /* Literal.String.Delimiter */ +.highlight .sd { color: #163 } /* Literal.String.Doc */ +.highlight .s2 { color: #163 } /* Literal.String.Double */ +.highlight .se { color: #163 } /* Literal.String.Escape */ +.highlight .sh { color: #163 } /* Literal.String.Heredoc */ +.highlight .si { color: #163 } /* Literal.String.Interpol */ +.highlight .sx { color: #163 } /* Literal.String.Other */ +.highlight .sr { color: #D71835 } /* Literal.String.Regex */ +.highlight .s1 { color: #163 } /* Literal.String.Single */ +.highlight .ss { color: #00749C } /* Literal.String.Symbol */ +.highlight .bp { color: #7F4707 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00749C } /* Name.Function.Magic */ +.highlight .vc { color: #D71835 } /* Name.Variable.Class */ +.highlight .vg { color: #D71835 } /* Name.Variable.Global */ +.highlight .vi { color: #D71835 } /* Name.Variable.Instance */ +.highlight .vm { color: #7F4707 } /* Name.Variable.Magic */ +.highlight .il { color: #7F4707 } /* Literal.Number.Integer.Long */ +@media not print { +body[data-theme="dark"] .highlight pre { line-height: 125%; } +body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight .hll { background-color: #404040 } +body[data-theme="dark"] .highlight { background: #202020; color: #D0D0D0 } +body[data-theme="dark"] .highlight .c { color: #ABABAB; font-style: italic } /* Comment */ +body[data-theme="dark"] .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */ +body[data-theme="dark"] .highlight .esc { color: #D0D0D0 } /* Escape */ +body[data-theme="dark"] .highlight .g { color: #D0D0D0 } /* Generic */ +body[data-theme="dark"] .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */ +body[data-theme="dark"] .highlight .l { color: #D0D0D0 } /* Literal */ +body[data-theme="dark"] .highlight .n { color: #D0D0D0 } /* Name */ +body[data-theme="dark"] .highlight .o { color: #D0D0D0 } /* Operator */ +body[data-theme="dark"] .highlight .x { color: #D0D0D0 } /* Other */ +body[data-theme="dark"] .highlight .p { color: #D0D0D0 } /* Punctuation */ +body[data-theme="dark"] .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */ +body[data-theme="dark"] .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */ +body[data-theme="dark"] .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */ +body[data-theme="dark"] .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */ +body[data-theme="dark"] .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */ +body[data-theme="dark"] .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body[data-theme="dark"] .highlight .gd { color: #FF3A3A } /* Generic.Deleted */ +body[data-theme="dark"] .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */ +body[data-theme="dark"] .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body[data-theme="dark"] .highlight .gr { color: #FF3A3A } /* Generic.Error */ +body[data-theme="dark"] .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */ +body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */ +body[data-theme="dark"] .highlight .go { color: #CCC } /* Generic.Output */ +body[data-theme="dark"] .highlight .gp { color: #AAA } /* Generic.Prompt */ +body[data-theme="dark"] .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */ +body[data-theme="dark"] .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */ +body[data-theme="dark"] .highlight .gt { color: #FF3A3A } /* Generic.Traceback */ +body[data-theme="dark"] .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */ +body[data-theme="dark"] .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */ +body[data-theme="dark"] .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */ +body[data-theme="dark"] .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */ +body[data-theme="dark"] .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */ +body[data-theme="dark"] .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */ +body[data-theme="dark"] .highlight .ld { color: #D0D0D0 } /* Literal.Date */ +body[data-theme="dark"] .highlight .m { color: #51B2FD } /* Literal.Number */ +body[data-theme="dark"] .highlight .s { color: #ED9D13 } /* Literal.String */ +body[data-theme="dark"] .highlight .na { color: #BBB } /* Name.Attribute */ +body[data-theme="dark"] .highlight .nb { color: #2FBCCD } /* Name.Builtin */ +body[data-theme="dark"] .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */ +body[data-theme="dark"] .highlight .no { color: #40FFFF } /* Name.Constant */ +body[data-theme="dark"] .highlight .nd { color: #FFA500 } /* Name.Decorator */ +body[data-theme="dark"] .highlight .ni { color: #D0D0D0 } /* Name.Entity */ +body[data-theme="dark"] .highlight .ne { color: #BBB } /* Name.Exception */ +body[data-theme="dark"] .highlight .nf { color: #71ADFF } /* Name.Function */ +body[data-theme="dark"] .highlight .nl { color: #D0D0D0 } /* Name.Label */ +body[data-theme="dark"] .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */ +body[data-theme="dark"] .highlight .nx { color: #D0D0D0 } /* Name.Other */ +body[data-theme="dark"] .highlight .py { color: #D0D0D0 } /* Name.Property */ +body[data-theme="dark"] .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */ +body[data-theme="dark"] .highlight .nv { color: #40FFFF } /* Name.Variable */ +body[data-theme="dark"] .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */ +body[data-theme="dark"] .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */ +body[data-theme="dark"] .highlight .w { color: #666 } /* Text.Whitespace */ +body[data-theme="dark"] .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */ +body[data-theme="dark"] .highlight .mf { color: #51B2FD } /* Literal.Number.Float */ +body[data-theme="dark"] .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */ +body[data-theme="dark"] .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */ +body[data-theme="dark"] .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */ +body[data-theme="dark"] .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */ +body[data-theme="dark"] .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */ +body[data-theme="dark"] .highlight .sc { color: #ED9D13 } /* Literal.String.Char */ +body[data-theme="dark"] .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */ +body[data-theme="dark"] .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */ +body[data-theme="dark"] .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */ +body[data-theme="dark"] .highlight .se { color: #ED9D13 } /* Literal.String.Escape */ +body[data-theme="dark"] .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */ +body[data-theme="dark"] .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */ +body[data-theme="dark"] .highlight .sx { color: #FFA500 } /* Literal.String.Other */ +body[data-theme="dark"] .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */ +body[data-theme="dark"] .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */ +body[data-theme="dark"] .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */ +body[data-theme="dark"] .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */ +body[data-theme="dark"] .highlight .fm { color: #71ADFF } /* Name.Function.Magic */ +body[data-theme="dark"] .highlight .vc { color: #40FFFF } /* Name.Variable.Class */ +body[data-theme="dark"] .highlight .vg { color: #40FFFF } /* Name.Variable.Global */ +body[data-theme="dark"] .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */ +body[data-theme="dark"] .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */ +body[data-theme="dark"] .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */ +@media (prefers-color-scheme: dark) { +body:not([data-theme="light"]) .highlight pre { line-height: 125%; } +body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight .hll { background-color: #404040 } +body:not([data-theme="light"]) .highlight { background: #202020; color: #D0D0D0 } +body:not([data-theme="light"]) .highlight .c { color: #ABABAB; font-style: italic } /* Comment */ +body:not([data-theme="light"]) .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */ +body:not([data-theme="light"]) .highlight .esc { color: #D0D0D0 } /* Escape */ +body:not([data-theme="light"]) .highlight .g { color: #D0D0D0 } /* Generic */ +body:not([data-theme="light"]) .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */ +body:not([data-theme="light"]) .highlight .l { color: #D0D0D0 } /* Literal */ +body:not([data-theme="light"]) .highlight .n { color: #D0D0D0 } /* Name */ +body:not([data-theme="light"]) .highlight .o { color: #D0D0D0 } /* Operator */ +body:not([data-theme="light"]) .highlight .x { color: #D0D0D0 } /* Other */ +body:not([data-theme="light"]) .highlight .p { color: #D0D0D0 } /* Punctuation */ +body:not([data-theme="light"]) .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */ +body:not([data-theme="light"]) .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */ +body:not([data-theme="light"]) .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */ +body:not([data-theme="light"]) .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */ +body:not([data-theme="light"]) .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */ +body:not([data-theme="light"]) .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body:not([data-theme="light"]) .highlight .gd { color: #FF3A3A } /* Generic.Deleted */ +body:not([data-theme="light"]) .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */ +body:not([data-theme="light"]) .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body:not([data-theme="light"]) .highlight .gr { color: #FF3A3A } /* Generic.Error */ +body:not([data-theme="light"]) .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */ +body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */ +body:not([data-theme="light"]) .highlight .go { color: #CCC } /* Generic.Output */ +body:not([data-theme="light"]) .highlight .gp { color: #AAA } /* Generic.Prompt */ +body:not([data-theme="light"]) .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */ +body:not([data-theme="light"]) .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */ +body:not([data-theme="light"]) .highlight .gt { color: #FF3A3A } /* Generic.Traceback */ +body:not([data-theme="light"]) .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */ +body:not([data-theme="light"]) .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */ +body:not([data-theme="light"]) .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */ +body:not([data-theme="light"]) .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */ +body:not([data-theme="light"]) .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */ +body:not([data-theme="light"]) .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */ +body:not([data-theme="light"]) .highlight .ld { color: #D0D0D0 } /* Literal.Date */ +body:not([data-theme="light"]) .highlight .m { color: #51B2FD } /* Literal.Number */ +body:not([data-theme="light"]) .highlight .s { color: #ED9D13 } /* Literal.String */ +body:not([data-theme="light"]) .highlight .na { color: #BBB } /* Name.Attribute */ +body:not([data-theme="light"]) .highlight .nb { color: #2FBCCD } /* Name.Builtin */ +body:not([data-theme="light"]) .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */ +body:not([data-theme="light"]) .highlight .no { color: #40FFFF } /* Name.Constant */ +body:not([data-theme="light"]) .highlight .nd { color: #FFA500 } /* Name.Decorator */ +body:not([data-theme="light"]) .highlight .ni { color: #D0D0D0 } /* Name.Entity */ +body:not([data-theme="light"]) .highlight .ne { color: #BBB } /* Name.Exception */ +body:not([data-theme="light"]) .highlight .nf { color: #71ADFF } /* Name.Function */ +body:not([data-theme="light"]) .highlight .nl { color: #D0D0D0 } /* Name.Label */ +body:not([data-theme="light"]) .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */ +body:not([data-theme="light"]) .highlight .nx { color: #D0D0D0 } /* Name.Other */ +body:not([data-theme="light"]) .highlight .py { color: #D0D0D0 } /* Name.Property */ +body:not([data-theme="light"]) .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */ +body:not([data-theme="light"]) .highlight .nv { color: #40FFFF } /* Name.Variable */ +body:not([data-theme="light"]) .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */ +body:not([data-theme="light"]) .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */ +body:not([data-theme="light"]) .highlight .w { color: #666 } /* Text.Whitespace */ +body:not([data-theme="light"]) .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */ +body:not([data-theme="light"]) .highlight .mf { color: #51B2FD } /* Literal.Number.Float */ +body:not([data-theme="light"]) .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */ +body:not([data-theme="light"]) .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */ +body:not([data-theme="light"]) .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */ +body:not([data-theme="light"]) .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */ +body:not([data-theme="light"]) .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */ +body:not([data-theme="light"]) .highlight .sc { color: #ED9D13 } /* Literal.String.Char */ +body:not([data-theme="light"]) .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */ +body:not([data-theme="light"]) .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */ +body:not([data-theme="light"]) .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */ +body:not([data-theme="light"]) .highlight .se { color: #ED9D13 } /* Literal.String.Escape */ +body:not([data-theme="light"]) .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */ +body:not([data-theme="light"]) .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */ +body:not([data-theme="light"]) .highlight .sx { color: #FFA500 } /* Literal.String.Other */ +body:not([data-theme="light"]) .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */ +body:not([data-theme="light"]) .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */ +body:not([data-theme="light"]) .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */ +body:not([data-theme="light"]) .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */ +body:not([data-theme="light"]) .highlight .fm { color: #71ADFF } /* Name.Function.Magic */ +body:not([data-theme="light"]) .highlight .vc { color: #40FFFF } /* Name.Variable.Class */ +body:not([data-theme="light"]) .highlight .vg { color: #40FFFF } /* Name.Variable.Global */ +body:not([data-theme="light"]) .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */ +body:not([data-theme="light"]) .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */ +body:not([data-theme="light"]) .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */ +} +} \ No newline at end of file diff --git a/docs/manual/_static/rns_logo_512.png b/docs/manual/_static/rns_logo_512.png new file mode 100644 index 0000000..ba3940a Binary files /dev/null and b/docs/manual/_static/rns_logo_512.png differ diff --git a/docs/manual/_static/scripts/furo-extensions.js b/docs/manual/_static/scripts/furo-extensions.js new file mode 100644 index 0000000..e69de29 diff --git a/docs/manual/_static/scripts/furo.js b/docs/manual/_static/scripts/furo.js new file mode 100644 index 0000000..87e1767 --- /dev/null +++ b/docs/manual/_static/scripts/furo.js @@ -0,0 +1,3 @@ +/*! For license information please see furo.js.LICENSE.txt */ +(()=>{var t={856:function(t,e,n){var o,r;r=void 0!==n.g?n.g:"undefined"!=typeof window?window:this,o=function(){return function(t){"use strict";var e={navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:0,reflow:!1,events:!0},n=function(t,e,n){if(n.settings.events){var o=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(o)}},o=function(t){var e=0;if(t.offsetParent)for(;t;)e+=t.offsetTop,t=t.offsetParent;return e>=0?e:0},r=function(t){t&&t.sort(function(t,e){return o(t.content)=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},l=function(t,e){var n=t[t.length-1];if(function(t,e){return!(!s()||!c(t.content,e,!0))}(n,e))return n;for(var o=t.length-1;o>=0;o--)if(c(t[o].content,e))return t[o]},a=function(t,e){if(e.nested&&t.parentNode){var n=t.parentNode.closest("li");n&&(n.classList.remove(e.nestedClass),a(n,e))}},i=function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.remove(e.navClass),t.content.classList.remove(e.contentClass),a(o,e),n("gumshoeDeactivate",o,{link:t.nav,content:t.content,settings:e}))}},u=function(t,e){if(e.nested){var n=t.parentNode.closest("li");n&&(n.classList.add(e.nestedClass),u(n,e))}};return function(o,c){var s,a,d,f,m,v={setup:function(){s=document.querySelectorAll(o),a=[],Array.prototype.forEach.call(s,function(t){var e=document.getElementById(decodeURIComponent(t.hash.substr(1)));e&&a.push({nav:t,content:e})}),r(a)},detect:function(){var t=l(a,m);t?d&&t.content===d.content||(i(d,m),function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.add(e.navClass),t.content.classList.add(e.contentClass),u(o,e),n("gumshoeActivate",o,{link:t.nav,content:t.content,settings:e}))}}(t,m),d=t):d&&(i(d,m),d=null)}},h=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame(v.detect)},g=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame(function(){r(a),v.detect()})};return v.destroy=function(){d&&i(d,m),t.removeEventListener("scroll",h,!1),m.reflow&&t.removeEventListener("resize",g,!1),a=null,s=null,d=null,f=null,m=null},m=function(){var t={};return Array.prototype.forEach.call(arguments,function(e){for(var n in e){if(!e.hasOwnProperty(n))return;t[n]=e[n]}}),t}(e,c||{}),v.setup(),v.detect(),t.addEventListener("scroll",h,!1),m.reflow&&t.addEventListener("resize",g,!1),v}}(r)}.apply(e,[]),void 0===o||(t.exports=o)}},e={};function n(o){var r=e[o];if(void 0!==r)return r.exports;var c=e[o]={exports:{}};return t[o].call(c.exports,c,c.exports,n),c.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=n(856),e=n.n(t),o=null,r=null,c=document.documentElement.scrollTop;function s(){const t=localStorage.getItem("theme")||"auto";var e;"light"!==(e=window.matchMedia("(prefers-color-scheme: dark)").matches?"auto"===t?"light":"light"==t?"dark":"auto":"auto"===t?"dark":"dark"==t?"light":"auto")&&"dark"!==e&&"auto"!==e&&(console.error(`Got invalid theme mode: ${e}. Resetting to auto.`),e="auto"),document.body.dataset.theme=e,localStorage.setItem("theme",e),console.log(`Changed to ${e} mode.`)}function l(){!function(){const t=document.getElementsByClassName("theme-toggle");Array.from(t).forEach(t=>{t.addEventListener("click",s)})}(),function(){let t=0,e=!1;window.addEventListener("scroll",function(n){t=window.scrollY,e||(window.requestAnimationFrame(function(){var n;(function(t){t>0?r.classList.add("scrolled"):r.classList.remove("scrolled")})(n=t),function(t){t<64?document.documentElement.classList.remove("show-back-to-top"):tc&&document.documentElement.classList.remove("show-back-to-top"),c=t}(n),function(t){null!==o&&(0==t?o.scrollTo(0,0):Math.ceil(t)>=Math.floor(document.documentElement.scrollHeight-window.innerHeight)?o.scrollTo(0,o.scrollHeight):document.querySelector(".scroll-current"))}(n),e=!1}),e=!0)}),window.scroll()}(),null!==o&&new(e())(".toc-tree a",{reflow:!0,recursive:!0,navClass:"scroll-current",offset:()=>{let t=parseFloat(getComputedStyle(document.documentElement).fontSize);const e=r.getBoundingClientRect();return e.top+e.height+2.5*t+1}})}document.addEventListener("DOMContentLoaded",function(){document.body.parentNode.classList.remove("no-js"),r=document.querySelector("header"),o=document.querySelector(".toc-scroll"),l()})})()})(); +//# sourceMappingURL=furo.js.map \ No newline at end of file diff --git a/docs/manual/_static/scripts/furo.js.LICENSE.txt b/docs/manual/_static/scripts/furo.js.LICENSE.txt new file mode 100644 index 0000000..1632189 --- /dev/null +++ b/docs/manual/_static/scripts/furo.js.LICENSE.txt @@ -0,0 +1,7 @@ +/*! + * gumshoejs v5.1.2 (patched by @pradyunsg) + * A simple, framework-agnostic scrollspy script. + * (c) 2019 Chris Ferdinandi + * MIT License + * http://github.com/cferdinandi/gumshoe + */ diff --git a/docs/manual/_static/scripts/furo.js.map b/docs/manual/_static/scripts/furo.js.map new file mode 100644 index 0000000..3b316f3 --- /dev/null +++ b/docs/manual/_static/scripts/furo.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/furo.js","mappings":";iCAAA,MAQWA,SAWS,IAAX,EAAAC,EACH,EAAAA,EACkB,oBAAXC,OACLA,OACAC,KAbO,EAAF,WACP,OAaJ,SAAUD,GACR,aAMA,IAAIE,EAAW,CAEbC,SAAU,SACVC,aAAc,SAGdC,QAAQ,EACRC,YAAa,SAGbC,OAAQ,EACRC,QAAQ,EAGRC,QAAQ,GA6BNC,EAAY,SAAUC,EAAMC,EAAMC,GAEpC,GAAKA,EAAOC,SAASL,OAArB,CAGA,IAAIM,EAAQ,IAAIC,YAAYL,EAAM,CAChCM,SAAS,EACTC,YAAY,EACZL,OAAQA,IAIVD,EAAKO,cAAcJ,EAVgB,CAWrC,EAOIK,EAAe,SAAUR,GAC3B,IAAIS,EAAW,EACf,GAAIT,EAAKU,aACP,KAAOV,GACLS,GAAYT,EAAKW,UACjBX,EAAOA,EAAKU,aAGhB,OAAOD,GAAY,EAAIA,EAAW,CACpC,EAMIG,EAAe,SAAUC,GACvBA,GACFA,EAASC,KAAK,SAAUC,EAAOC,GAG7B,OAFcR,EAAaO,EAAME,SACnBT,EAAaQ,EAAMC,UACF,EACxB,CACT,EAEJ,EAwCIC,EAAW,SAAUlB,EAAME,EAAUiB,GACvC,IAAIC,EAASpB,EAAKqB,wBACd1B,EAnCU,SAAUO,GAExB,MAA+B,mBAApBA,EAASP,OACX2B,WAAWpB,EAASP,UAItB2B,WAAWpB,EAASP,OAC7B,CA2Be4B,CAAUrB,GACvB,OAAIiB,EAEAK,SAASJ,EAAOD,OAAQ,KACvB/B,EAAOqC,aAAeC,SAASC,gBAAgBC,cAG7CJ,SAASJ,EAAOS,IAAK,KAAOlC,CACrC,EAMImC,EAAa,WACf,OACEC,KAAKC,KAAK5C,EAAOqC,YAAcrC,EAAO6C,cAnCjCF,KAAKG,IACVR,SAASS,KAAKC,aACdV,SAASC,gBAAgBS,aACzBV,SAASS,KAAKE,aACdX,SAASC,gBAAgBU,aACzBX,SAASS,KAAKP,aACdF,SAASC,gBAAgBC,aAkC7B,EAmBIU,EAAY,SAAUzB,EAAUX,GAClC,IAAIqC,EAAO1B,EAASA,EAAS2B,OAAS,GACtC,GAbgB,SAAUC,EAAMvC,GAChC,SAAI4B,MAAgBZ,EAASuB,EAAKxB,QAASf,GAAU,GAEvD,CAUMwC,CAAYH,EAAMrC,GAAW,OAAOqC,EACxC,IAAK,IAAII,EAAI9B,EAAS2B,OAAS,EAAGG,GAAK,EAAGA,IACxC,GAAIzB,EAASL,EAAS8B,GAAG1B,QAASf,GAAW,OAAOW,EAAS8B,EAEjE,EAOIC,EAAmB,SAAUC,EAAK3C,GAEpC,GAAKA,EAAST,QAAWoD,EAAIC,WAA7B,CAGA,IAAIC,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASR,aAG7BkD,EAAiBG,EAAI7C,GAV0B,CAWjD,EAOIiD,EAAa,SAAUC,EAAOlD,GAEhC,GAAKkD,EAAL,CAGA,IAAIL,EAAKK,EAAMP,IAAIG,QAAQ,MACtBD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASX,UAC7B6D,EAAMnC,QAAQgC,UAAUC,OAAOhD,EAASV,cAGxCoD,EAAiBG,EAAI7C,GAGrBJ,EAAU,oBAAqBiD,EAAI,CACjCM,KAAMD,EAAMP,IACZ5B,QAASmC,EAAMnC,QACff,SAAUA,IAjBM,CAmBpB,EAOIoD,EAAiB,SAAUT,EAAK3C,GAElC,GAAKA,EAAST,OAAd,CAGA,IAAIsD,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASR,aAG1B4D,EAAeP,EAAI7C,GAVS,CAW9B,EA6LA,OA1JkB,SAAUsD,EAAUC,GAKpC,IACIC,EAAU7C,EAAU8C,EAASC,EAAS1D,EADtC2D,EAAa,CAUjBA,MAAmB,WAEjBH,EAAWhC,SAASoC,iBAAiBN,GAGrC3C,EAAW,GAGXkD,MAAMC,UAAUC,QAAQC,KAAKR,EAAU,SAAUjB,GAE/C,IAAIxB,EAAUS,SAASyC,eACrBC,mBAAmB3B,EAAK4B,KAAKC,OAAO,KAEjCrD,GAGLJ,EAAS0D,KAAK,CACZ1B,IAAKJ,EACLxB,QAASA,GAEb,GAGAL,EAAaC,EACf,EAKAgD,OAAoB,WAElB,IAAIW,EAASlC,EAAUzB,EAAUX,GAG5BsE,EASDb,GAAWa,EAAOvD,UAAY0C,EAAQ1C,UAG1CkC,EAAWQ,EAASzD,GAzFT,SAAUkD,EAAOlD,GAE9B,GAAKkD,EAAL,CAGA,IAAIL,EAAKK,EAAMP,IAAIG,QAAQ,MACtBD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASX,UAC1B6D,EAAMnC,QAAQgC,UAAUM,IAAIrD,EAASV,cAGrC8D,EAAeP,EAAI7C,GAGnBJ,EAAU,kBAAmBiD,EAAI,CAC/BM,KAAMD,EAAMP,IACZ5B,QAASmC,EAAMnC,QACff,SAAUA,IAjBM,CAmBpB,CAqEIuE,CAASD,EAAQtE,GAGjByD,EAAUa,GAfJb,IACFR,EAAWQ,EAASzD,GACpByD,EAAU,KAchB,GAMIe,EAAgB,SAAUvE,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,sBAAsBf,EAAWgB,OACpD,EAMIC,EAAgB,SAAU3E,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,sBAAsB,WACrChE,EAAaC,GACbgD,EAAWgB,QACb,EACF,EAkDA,OA7CAhB,EAAWkB,QAAU,WAEfpB,GACFR,EAAWQ,EAASzD,GAItBd,EAAO4F,oBAAoB,SAAUN,GAAe,GAChDxE,EAASN,QACXR,EAAO4F,oBAAoB,SAAUF,GAAe,GAItDjE,EAAW,KACX6C,EAAW,KACXC,EAAU,KACVC,EAAU,KACV1D,EAAW,IACb,EAOEA,EA3XS,WACX,IAAI+E,EAAS,CAAC,EAOd,OANAlB,MAAMC,UAAUC,QAAQC,KAAKgB,UAAW,SAAUC,GAChD,IAAK,IAAIC,KAAOD,EAAK,CACnB,IAAKA,EAAIE,eAAeD,GAAM,OAC9BH,EAAOG,GAAOD,EAAIC,EACpB,CACF,GACOH,CACT,CAkXeK,CAAOhG,EAAUmE,GAAW,CAAC,GAGxCI,EAAW0B,QAGX1B,EAAWgB,SAGXzF,EAAOoG,iBAAiB,SAAUd,GAAe,GAC7CxE,EAASN,QACXR,EAAOoG,iBAAiB,SAAUV,GAAe,GAS9CjB,CACT,CAOF,CArcW4B,CAAQvG,EAChB,UAFM,SAEN,oB,GCXDwG,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaE,QAGrB,IAAIC,EAASN,EAAyBE,GAAY,CAGjDG,QAAS,CAAC,GAOX,OAHAE,EAAoBL,GAAU1B,KAAK8B,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAGpEK,EAAOD,OACf,CCrBAJ,EAAoBO,EAAKF,IACxB,IAAIG,EAASH,GAAUA,EAAOI,WAC7B,IAAOJ,EAAiB,QACxB,IAAM,EAEP,OADAL,EAAoBU,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,GCLRR,EAAoBU,EAAI,CAACN,EAASQ,KACjC,IAAI,IAAInB,KAAOmB,EACXZ,EAAoBa,EAAED,EAAYnB,KAASO,EAAoBa,EAAET,EAASX,IAC5EqB,OAAOC,eAAeX,EAASX,EAAK,CAAEuB,YAAY,EAAMC,IAAKL,EAAWnB,MCJ3EO,EAAoBxG,EAAI,WACvB,GAA0B,iBAAf0H,WAAyB,OAAOA,WAC3C,IACC,OAAOxH,MAAQ,IAAIyH,SAAS,cAAb,EAChB,CAAE,MAAOC,GACR,GAAsB,iBAAX3H,OAAqB,OAAOA,MACxC,CACA,CAPuB,GCAxBuG,EAAoBa,EAAI,CAACrB,EAAK6B,IAAUP,OAAOzC,UAAUqB,eAAenB,KAAKiB,EAAK6B,G,yCCK9EC,EAAY,KACZC,EAAS,KACTC,EAAgBzF,SAASC,gBAAgByF,UA4E7C,SAASC,IACP,MAAMC,EAAeC,aAAaC,QAAQ,UAAY,OAZxD,IAAkBC,EACH,WADGA,EAaIrI,OAAOsI,WAAW,gCAAgCC,QAI/C,SAAjBL,EACO,QACgB,SAAhBA,EACA,OAEA,OAIU,SAAjBA,EACO,OACgB,QAAhBA,EACA,QAEA,SA9BoB,SAATG,GAA4B,SAATA,IACzCG,QAAQC,MAAM,2BAA2BJ,yBACzCA,EAAO,QAGT/F,SAASS,KAAK2F,QAAQC,MAAQN,EAC9BF,aAAaS,QAAQ,QAASP,GAC9BG,QAAQK,IAAI,cAAcR,UA0B5B,CAmDA,SAASlC,KART,WAEE,MAAM2C,EAAUxG,SAASyG,uBAAuB,gBAChDpE,MAAMqE,KAAKF,GAASjE,QAASoE,IAC3BA,EAAI7C,iBAAiB,QAAS6B,IAElC,CAGEiB,GA/CF,WAEE,IAAIC,EAA6B,EAC7BC,GAAU,EAEdpJ,OAAOoG,iBAAiB,SAAU,SAAUuB,GAC1CwB,EAA6BnJ,OAAOqJ,QAE/BD,IACHpJ,OAAOwF,sBAAsB,WAzDnC,IAAuB8D,GArDvB,SAAgCA,GAC1BA,EAAY,EACdxB,EAAOjE,UAAUM,IAAI,YAErB2D,EAAOjE,UAAUC,OAAO,WAE5B,EAgDEyF,CADqBD,EA0DDH,GAvGtB,SAAmCG,GAC7BA,EAXmB,GAYrBhH,SAASC,gBAAgBsB,UAAUC,OAAO,oBAEtCwF,EAAYvB,EACdzF,SAASC,gBAAgBsB,UAAUM,IAAI,oBAC9BmF,EAAYvB,GACrBzF,SAASC,gBAAgBsB,UAAUC,OAAO,oBAG9CiE,EAAgBuB,CAClB,CAoCEE,CAA0BF,GAlC5B,SAA6BA,GACT,OAAdzB,IAKa,GAAbyB,EACFzB,EAAU4B,SAAS,EAAG,GAGtB9G,KAAKC,KAAK0G,IACV3G,KAAK+G,MAAMpH,SAASC,gBAAgBS,aAAehD,OAAOqC,aAE1DwF,EAAU4B,SAAS,EAAG5B,EAAU7E,cAGhBV,SAASqH,cAAc,mBAc3C,CAKEC,CAAoBN,GAwDdF,GAAU,CACZ,GAEAA,GAAU,EAEd,GACApJ,OAAO6J,QACT,CA8BEC,GA3BkB,OAAdjC,GAKJ,IAAI,IAAJ,CAAY,cAAe,CACzBrH,QAAQ,EACRuJ,WAAW,EACX5J,SAAU,iBACVI,OAAQ,KACN,IAAIyJ,EAAM9H,WAAW+H,iBAAiB3H,SAASC,iBAAiB2H,UAChE,MAAMC,EAAarC,EAAO7F,wBAC1B,OAAOkI,EAAW1H,IAAM0H,EAAWC,OAAS,IAAMJ,EAAM,IAiB9D,CAcA1H,SAAS8D,iBAAiB,mBAT1B,WACE9D,SAASS,KAAKW,WAAWG,UAAUC,OAAO,SAE1CgE,EAASxF,SAASqH,cAAc,UAChC9B,EAAYvF,SAASqH,cAAc,eAEnCxD,GACF,E","sources":["webpack:///./src/furo/assets/scripts/gumshoe-patched.js","webpack:///webpack/bootstrap","webpack:///webpack/runtime/compat get default export","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/global","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./src/furo/assets/scripts/furo.js"],"sourcesContent":["/*!\n * gumshoejs v5.1.2 (patched by @pradyunsg)\n * A simple, framework-agnostic scrollspy script.\n * (c) 2019 Chris Ferdinandi\n * MIT License\n * http://github.com/cferdinandi/gumshoe\n */\n\n(function (root, factory) {\n if (typeof define === \"function\" && define.amd) {\n define([], function () {\n return factory(root);\n });\n } else if (typeof exports === \"object\") {\n module.exports = factory(root);\n } else {\n root.Gumshoe = factory(root);\n }\n})(\n typeof global !== \"undefined\"\n ? global\n : typeof window !== \"undefined\"\n ? window\n : this,\n function (window) {\n \"use strict\";\n\n //\n // Defaults\n //\n\n var defaults = {\n // Active classes\n navClass: \"active\",\n contentClass: \"active\",\n\n // Nested navigation\n nested: false,\n nestedClass: \"active\",\n\n // Offset & reflow\n offset: 0,\n reflow: false,\n\n // Event support\n events: true,\n };\n\n //\n // Methods\n //\n\n /**\n * Merge two or more objects together.\n * @param {Object} objects The objects to merge together\n * @returns {Object} Merged values of defaults and options\n */\n var extend = function () {\n var merged = {};\n Array.prototype.forEach.call(arguments, function (obj) {\n for (var key in obj) {\n if (!obj.hasOwnProperty(key)) return;\n merged[key] = obj[key];\n }\n });\n return merged;\n };\n\n /**\n * Emit a custom event\n * @param {String} type The event type\n * @param {Node} elem The element to attach the event to\n * @param {Object} detail Any details to pass along with the event\n */\n var emitEvent = function (type, elem, detail) {\n // Make sure events are enabled\n if (!detail.settings.events) return;\n\n // Create a new event\n var event = new CustomEvent(type, {\n bubbles: true,\n cancelable: true,\n detail: detail,\n });\n\n // Dispatch the event\n elem.dispatchEvent(event);\n };\n\n /**\n * Get an element's distance from the top of the Document.\n * @param {Node} elem The element\n * @return {Number} Distance from the top in pixels\n */\n var getOffsetTop = function (elem) {\n var location = 0;\n if (elem.offsetParent) {\n while (elem) {\n location += elem.offsetTop;\n elem = elem.offsetParent;\n }\n }\n return location >= 0 ? location : 0;\n };\n\n /**\n * Sort content from first to last in the DOM\n * @param {Array} contents The content areas\n */\n var sortContents = function (contents) {\n if (contents) {\n contents.sort(function (item1, item2) {\n var offset1 = getOffsetTop(item1.content);\n var offset2 = getOffsetTop(item2.content);\n if (offset1 < offset2) return -1;\n return 1;\n });\n }\n };\n\n /**\n * Get the offset to use for calculating position\n * @param {Object} settings The settings for this instantiation\n * @return {Float} The number of pixels to offset the calculations\n */\n var getOffset = function (settings) {\n // if the offset is a function run it\n if (typeof settings.offset === \"function\") {\n return parseFloat(settings.offset());\n }\n\n // Otherwise, return it as-is\n return parseFloat(settings.offset);\n };\n\n /**\n * Get the document element's height\n * @private\n * @returns {Number}\n */\n var getDocumentHeight = function () {\n return Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight,\n );\n };\n\n /**\n * Determine if an element is in view\n * @param {Node} elem The element\n * @param {Object} settings The settings for this instantiation\n * @param {Boolean} bottom If true, check if element is above bottom of viewport instead\n * @return {Boolean} Returns true if element is in the viewport\n */\n var isInView = function (elem, settings, bottom) {\n var bounds = elem.getBoundingClientRect();\n var offset = getOffset(settings);\n if (bottom) {\n return (\n parseInt(bounds.bottom, 10) <\n (window.innerHeight || document.documentElement.clientHeight)\n );\n }\n return parseInt(bounds.top, 10) <= offset;\n };\n\n /**\n * Check if at the bottom of the viewport\n * @return {Boolean} If true, page is at the bottom of the viewport\n */\n var isAtBottom = function () {\n if (\n Math.ceil(window.innerHeight + window.pageYOffset) >=\n getDocumentHeight()\n )\n return true;\n return false;\n };\n\n /**\n * Check if the last item should be used (even if not at the top of the page)\n * @param {Object} item The last item\n * @param {Object} settings The settings for this instantiation\n * @return {Boolean} If true, use the last item\n */\n var useLastItem = function (item, settings) {\n if (isAtBottom() && isInView(item.content, settings, true)) return true;\n return false;\n };\n\n /**\n * Get the active content\n * @param {Array} contents The content areas\n * @param {Object} settings The settings for this instantiation\n * @return {Object} The content area and matching navigation link\n */\n var getActive = function (contents, settings) {\n var last = contents[contents.length - 1];\n if (useLastItem(last, settings)) return last;\n for (var i = contents.length - 1; i >= 0; i--) {\n if (isInView(contents[i].content, settings)) return contents[i];\n }\n };\n\n /**\n * Deactivate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var deactivateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested || !nav.parentNode) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Remove the active class\n li.classList.remove(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n deactivateNested(li, settings);\n };\n\n /**\n * Deactivate a nav and content area\n * @param {Object} items The nav item and content to deactivate\n * @param {Object} settings The settings for this instantiation\n */\n var deactivate = function (items, settings) {\n // Make sure there are items to deactivate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Remove the active class from the nav and content\n li.classList.remove(settings.navClass);\n items.content.classList.remove(settings.contentClass);\n\n // Deactivate any parent navs in a nested navigation\n deactivateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeDeactivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Activate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var activateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Add the active class\n li.classList.add(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n activateNested(li, settings);\n };\n\n /**\n * Activate a nav and content area\n * @param {Object} items The nav item and content to activate\n * @param {Object} settings The settings for this instantiation\n */\n var activate = function (items, settings) {\n // Make sure there are items to activate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Add the active class to the nav and content\n li.classList.add(settings.navClass);\n items.content.classList.add(settings.contentClass);\n\n // Activate any parent navs in a nested navigation\n activateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeActivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Create the Constructor object\n * @param {String} selector The selector to use for navigation items\n * @param {Object} options User options and settings\n */\n var Constructor = function (selector, options) {\n //\n // Variables\n //\n\n var publicAPIs = {};\n var navItems, contents, current, timeout, settings;\n\n //\n // Methods\n //\n\n /**\n * Set variables from DOM elements\n */\n publicAPIs.setup = function () {\n // Get all nav items\n navItems = document.querySelectorAll(selector);\n\n // Create contents array\n contents = [];\n\n // Loop through each item, get it's matching content, and push to the array\n Array.prototype.forEach.call(navItems, function (item) {\n // Get the content for the nav item\n var content = document.getElementById(\n decodeURIComponent(item.hash.substr(1)),\n );\n if (!content) return;\n\n // Push to the contents array\n contents.push({\n nav: item,\n content: content,\n });\n });\n\n // Sort contents by the order they appear in the DOM\n sortContents(contents);\n };\n\n /**\n * Detect which content is currently active\n */\n publicAPIs.detect = function () {\n // Get the active content\n var active = getActive(contents, settings);\n\n // if there's no active content, deactivate and bail\n if (!active) {\n if (current) {\n deactivate(current, settings);\n current = null;\n }\n return;\n }\n\n // If the active content is the one currently active, do nothing\n if (current && active.content === current.content) return;\n\n // Deactivate the current content and activate the new content\n deactivate(current, settings);\n activate(active, settings);\n\n // Update the currently active content\n current = active;\n };\n\n /**\n * Detect the active content on scroll\n * Debounced for performance\n */\n var scrollHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(publicAPIs.detect);\n };\n\n /**\n * Update content sorting on resize\n * Debounced for performance\n */\n var resizeHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(function () {\n sortContents(contents);\n publicAPIs.detect();\n });\n };\n\n /**\n * Destroy the current instantiation\n */\n publicAPIs.destroy = function () {\n // Undo DOM changes\n if (current) {\n deactivate(current, settings);\n }\n\n // Remove event listeners\n window.removeEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.removeEventListener(\"resize\", resizeHandler, false);\n }\n\n // Reset variables\n contents = null;\n navItems = null;\n current = null;\n timeout = null;\n settings = null;\n };\n\n /**\n * Initialize the current instantiation\n */\n var init = function () {\n // Merge user options into defaults\n settings = extend(defaults, options || {});\n\n // Setup variables based on the current DOM\n publicAPIs.setup();\n\n // Find the currently active content\n publicAPIs.detect();\n\n // Setup event listeners\n window.addEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.addEventListener(\"resize\", resizeHandler, false);\n }\n };\n\n //\n // Initialize and return the public APIs\n //\n\n init();\n return publicAPIs;\n };\n\n //\n // Return the Constructor\n //\n\n return Constructor;\n },\n);\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","import Gumshoe from \"./gumshoe-patched.js\";\n\n////////////////////////////////////////////////////////////////////////////////\n// Scroll Handling\n////////////////////////////////////////////////////////////////////////////////\nvar tocScroll = null;\nvar header = null;\nvar lastScrollTop = document.documentElement.scrollTop;\nconst GO_TO_TOP_OFFSET = 64;\n\nfunction scrollHandlerForHeader(positionY) {\n if (positionY > 0) {\n header.classList.add(\"scrolled\");\n } else {\n header.classList.remove(\"scrolled\");\n }\n}\n\nfunction scrollHandlerForBackToTop(positionY) {\n if (positionY < GO_TO_TOP_OFFSET) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n } else {\n if (positionY < lastScrollTop) {\n document.documentElement.classList.add(\"show-back-to-top\");\n } else if (positionY > lastScrollTop) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n }\n }\n lastScrollTop = positionY;\n}\n\nfunction scrollHandlerForTOC(positionY) {\n if (tocScroll === null) {\n return;\n }\n\n // top of page.\n if (positionY == 0) {\n tocScroll.scrollTo(0, 0);\n } else if (\n // bottom of page.\n Math.ceil(positionY) >=\n Math.floor(document.documentElement.scrollHeight - window.innerHeight)\n ) {\n tocScroll.scrollTo(0, tocScroll.scrollHeight);\n } else {\n // somewhere in the middle.\n const current = document.querySelector(\".scroll-current\");\n if (current == null) {\n return;\n }\n\n // https://github.com/pypa/pip/issues/9159 This breaks scroll behaviours.\n // // scroll the currently \"active\" heading in toc, into view.\n // const rect = current.getBoundingClientRect();\n // if (0 > rect.top) {\n // current.scrollIntoView(true); // the argument is \"alignTop\"\n // } else if (rect.bottom > window.innerHeight) {\n // current.scrollIntoView(false);\n // }\n }\n}\n\nfunction scrollHandler(positionY) {\n scrollHandlerForHeader(positionY);\n scrollHandlerForBackToTop(positionY);\n scrollHandlerForTOC(positionY);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Theme Toggle\n////////////////////////////////////////////////////////////////////////////////\nfunction setTheme(mode) {\n if (mode !== \"light\" && mode !== \"dark\" && mode !== \"auto\") {\n console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);\n mode = \"auto\";\n }\n\n document.body.dataset.theme = mode;\n localStorage.setItem(\"theme\", mode);\n console.log(`Changed to ${mode} mode.`);\n}\n\nfunction cycleThemeOnce() {\n const currentTheme = localStorage.getItem(\"theme\") || \"auto\";\n const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (prefersDark) {\n // Auto (dark) -> Light -> Dark\n if (currentTheme === \"auto\") {\n setTheme(\"light\");\n } else if (currentTheme == \"light\") {\n setTheme(\"dark\");\n } else {\n setTheme(\"auto\");\n }\n } else {\n // Auto (light) -> Dark -> Light\n if (currentTheme === \"auto\") {\n setTheme(\"dark\");\n } else if (currentTheme == \"dark\") {\n setTheme(\"light\");\n } else {\n setTheme(\"auto\");\n }\n }\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Setup\n////////////////////////////////////////////////////////////////////////////////\nfunction setupScrollHandler() {\n // Taken from https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event\n let last_known_scroll_position = 0;\n let ticking = false;\n\n window.addEventListener(\"scroll\", function (e) {\n last_known_scroll_position = window.scrollY;\n\n if (!ticking) {\n window.requestAnimationFrame(function () {\n scrollHandler(last_known_scroll_position);\n ticking = false;\n });\n\n ticking = true;\n }\n });\n window.scroll();\n}\n\nfunction setupScrollSpy() {\n if (tocScroll === null) {\n return;\n }\n\n // Scrollspy -- highlight table on contents, based on scroll\n new Gumshoe(\".toc-tree a\", {\n reflow: true,\n recursive: true,\n navClass: \"scroll-current\",\n offset: () => {\n let rem = parseFloat(getComputedStyle(document.documentElement).fontSize);\n const headerRect = header.getBoundingClientRect();\n return headerRect.top + headerRect.height + 2.5 * rem + 1;\n },\n });\n}\n\nfunction setupTheme() {\n // Attach event handlers for toggling themes\n const buttons = document.getElementsByClassName(\"theme-toggle\");\n Array.from(buttons).forEach((btn) => {\n btn.addEventListener(\"click\", cycleThemeOnce);\n });\n}\n\nfunction setup() {\n setupTheme();\n setupScrollHandler();\n setupScrollSpy();\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Main entrypoint\n////////////////////////////////////////////////////////////////////////////////\nfunction main() {\n document.body.parentNode.classList.remove(\"no-js\");\n\n header = document.querySelector(\"header\");\n tocScroll = document.querySelector(\".toc-scroll\");\n\n setup();\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", main);\n"],"names":["root","g","window","this","defaults","navClass","contentClass","nested","nestedClass","offset","reflow","events","emitEvent","type","elem","detail","settings","event","CustomEvent","bubbles","cancelable","dispatchEvent","getOffsetTop","location","offsetParent","offsetTop","sortContents","contents","sort","item1","item2","content","isInView","bottom","bounds","getBoundingClientRect","parseFloat","getOffset","parseInt","innerHeight","document","documentElement","clientHeight","top","isAtBottom","Math","ceil","pageYOffset","max","body","scrollHeight","offsetHeight","getActive","last","length","item","useLastItem","i","deactivateNested","nav","parentNode","li","closest","classList","remove","deactivate","items","link","activateNested","add","selector","options","navItems","current","timeout","publicAPIs","querySelectorAll","Array","prototype","forEach","call","getElementById","decodeURIComponent","hash","substr","push","active","activate","scrollHandler","cancelAnimationFrame","requestAnimationFrame","detect","resizeHandler","destroy","removeEventListener","merged","arguments","obj","key","hasOwnProperty","extend","setup","addEventListener","factory","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","n","getter","__esModule","d","a","definition","o","Object","defineProperty","enumerable","get","globalThis","Function","e","prop","tocScroll","header","lastScrollTop","scrollTop","cycleThemeOnce","currentTheme","localStorage","getItem","mode","matchMedia","matches","console","error","dataset","theme","setItem","log","buttons","getElementsByClassName","from","btn","setupTheme","last_known_scroll_position","ticking","scrollY","positionY","scrollHandlerForHeader","scrollHandlerForBackToTop","scrollTo","floor","querySelector","scrollHandlerForTOC","scroll","setupScrollHandler","recursive","rem","getComputedStyle","fontSize","headerRect","height"],"sourceRoot":""} \ No newline at end of file diff --git a/docs/manual/_static/searchtools.js b/docs/manual/_static/searchtools.js new file mode 100644 index 0000000..91f4be5 --- /dev/null +++ b/docs/manual/_static/searchtools.js @@ -0,0 +1,635 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + // find documents, if any, containing the query word in their text/title term indices + // use Object.hasOwnProperty to avoid mismatching against prototype properties + const arr = [ + { files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term }, + { files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, new Map()); + const fileScores = scoreMap.get(file); + fileScores.set(word, record.score); + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/manual/_static/skeleton.css b/docs/manual/_static/skeleton.css new file mode 100644 index 0000000..467c878 --- /dev/null +++ b/docs/manual/_static/skeleton.css @@ -0,0 +1,296 @@ +/* Some sane resets. */ +html { + height: 100%; +} + +body { + margin: 0; + min-height: 100%; +} + +/* All the flexbox magic! */ +body, +.sb-announcement, +.sb-content, +.sb-main, +.sb-container, +.sb-container__inner, +.sb-article-container, +.sb-footer-content, +.sb-header, +.sb-header-secondary, +.sb-footer { + display: flex; +} + +/* These order things vertically */ +body, +.sb-main, +.sb-article-container { + flex-direction: column; +} + +/* Put elements in the center */ +.sb-header, +.sb-header-secondary, +.sb-container, +.sb-content, +.sb-footer, +.sb-footer-content { + justify-content: center; +} +/* Put elements at the ends */ +.sb-article-container { + justify-content: space-between; +} + +/* These elements grow. */ +.sb-main, +.sb-content, +.sb-container, +article { + flex-grow: 1; +} + +/* Because padding making this wider is not fun */ +article { + box-sizing: border-box; +} + +/* The announcements element should never be wider than the page. */ +.sb-announcement { + max-width: 100%; +} + +.sb-sidebar-primary, +.sb-sidebar-secondary { + flex-shrink: 0; + width: 17rem; +} + +.sb-announcement__inner { + justify-content: center; + + box-sizing: border-box; + height: 3rem; + + overflow-x: auto; + white-space: nowrap; +} + +/* Sidebars, with checkbox-based toggle */ +.sb-sidebar-primary, +.sb-sidebar-secondary { + position: fixed; + height: 100%; + top: 0; +} + +.sb-sidebar-primary { + left: -17rem; + transition: left 250ms ease-in-out; +} +.sb-sidebar-secondary { + right: -17rem; + transition: right 250ms ease-in-out; +} + +.sb-sidebar-toggle { + display: none; +} +.sb-sidebar-overlay { + position: fixed; + top: 0; + width: 0; + height: 0; + + transition: width 0ms ease 250ms, height 0ms ease 250ms, opacity 250ms ease; + + opacity: 0; + background-color: rgba(0, 0, 0, 0.54); +} + +#sb-sidebar-toggle--primary:checked + ~ .sb-sidebar-overlay[for="sb-sidebar-toggle--primary"], +#sb-sidebar-toggle--secondary:checked + ~ .sb-sidebar-overlay[for="sb-sidebar-toggle--secondary"] { + width: 100%; + height: 100%; + opacity: 1; + transition: width 0ms ease, height 0ms ease, opacity 250ms ease; +} + +#sb-sidebar-toggle--primary:checked ~ .sb-container .sb-sidebar-primary { + left: 0; +} +#sb-sidebar-toggle--secondary:checked ~ .sb-container .sb-sidebar-secondary { + right: 0; +} + +/* Full-width mode */ +.drop-secondary-sidebar-for-full-width-content + .hide-when-secondary-sidebar-shown { + display: none !important; +} +.drop-secondary-sidebar-for-full-width-content .sb-sidebar-secondary { + display: none !important; +} + +/* Mobile views */ +.sb-page-width { + width: 100%; +} + +.sb-article-container, +.sb-footer-content__inner, +.drop-secondary-sidebar-for-full-width-content .sb-article, +.drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 100vw; +} + +.sb-article, +.match-content-width { + padding: 0 1rem; + box-sizing: border-box; +} + +@media (min-width: 32rem) { + .sb-article, + .match-content-width { + padding: 0 2rem; + } +} + +/* Tablet views */ +@media (min-width: 42rem) { + .sb-article-container { + width: auto; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 42rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} +@media (min-width: 46rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 46rem; + } + .sb-article, + .match-content-width { + width: 46rem; + } +} +@media (min-width: 50rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 50rem; + } + .sb-article, + .match-content-width { + width: 50rem; + } +} + +/* Tablet views */ +@media (min-width: 59rem) { + .sb-sidebar-secondary { + position: static; + } + .hide-when-secondary-sidebar-shown { + display: none !important; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 59rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} +@media (min-width: 63rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 63rem; + } + .sb-article, + .match-content-width { + width: 46rem; + } +} +@media (min-width: 67rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } + .sb-article, + .match-content-width { + width: 50rem; + } +} + +/* Desktop views */ +@media (min-width: 76rem) { + .sb-sidebar-primary { + position: static; + } + .hide-when-primary-sidebar-shown { + display: none !important; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 59rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} + +/* Full desktop views */ +@media (min-width: 80rem) { + .sb-article, + .match-content-width { + width: 46rem; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 63rem; + } +} + +@media (min-width: 84rem) { + .sb-article, + .match-content-width { + width: 50rem; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } +} + +@media (min-width: 88rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } + .sb-page-width { + width: 88rem; + } +} diff --git a/docs/manual/_static/sphinx_highlight.js b/docs/manual/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/docs/manual/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/manual/_static/styles/furo-extensions.css b/docs/manual/_static/styles/furo-extensions.css new file mode 100644 index 0000000..2d74267 --- /dev/null +++ b/docs/manual/_static/styles/furo-extensions.css @@ -0,0 +1,2 @@ +#furo-sidebar-ad-placement{padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)}#furo-sidebar-ad-placement .ethical-sidebar{background:var(--color-background-secondary);border:none;box-shadow:none}#furo-sidebar-ad-placement .ethical-sidebar:hover{background:var(--color-background-hover)}#furo-sidebar-ad-placement .ethical-sidebar a{color:var(--color-foreground-primary)}#furo-sidebar-ad-placement .ethical-callout a{color:var(--color-foreground-secondary)!important}#furo-readthedocs-versions{background:transparent;display:block;position:static;width:100%}#furo-readthedocs-versions .rst-versions{background:#1a1c1e}#furo-readthedocs-versions .rst-current-version{background:var(--color-sidebar-item-background);cursor:unset}#furo-readthedocs-versions .rst-current-version:hover{background:var(--color-sidebar-item-background)}#furo-readthedocs-versions .rst-current-version .fa-book{color:var(--color-foreground-primary)}#furo-readthedocs-versions>.rst-other-versions{padding:0}#furo-readthedocs-versions>.rst-other-versions small{opacity:1}#furo-readthedocs-versions .injected .rst-versions{position:unset}#furo-readthedocs-versions:focus-within,#furo-readthedocs-versions:hover{box-shadow:0 0 0 1px var(--color-sidebar-background-border)}#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:hover .rst-current-version{background:#1a1c1e;font-size:inherit;height:auto;line-height:inherit;padding:12px;text-align:right}#furo-readthedocs-versions:focus-within .rst-current-version .fa-book,#furo-readthedocs-versions:hover .rst-current-version .fa-book{color:#fff;float:left}#furo-readthedocs-versions:focus-within .fa-caret-down,#furo-readthedocs-versions:hover .fa-caret-down{display:none}#furo-readthedocs-versions:focus-within .injected,#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:focus-within .rst-other-versions,#furo-readthedocs-versions:hover .injected,#furo-readthedocs-versions:hover .rst-current-version,#furo-readthedocs-versions:hover .rst-other-versions{display:block}#furo-readthedocs-versions:focus-within>.rst-current-version,#furo-readthedocs-versions:hover>.rst-current-version{display:none}.highlight:hover button.copybtn{color:var(--color-code-foreground)}.highlight button.copybtn{align-items:center;background-color:var(--color-code-background);border:none;color:var(--color-background-item);cursor:pointer;height:1.25em;right:.5rem;top:.625rem;transition:color .3s,opacity .3s;width:1.25em}.highlight button.copybtn:hover{background-color:var(--color-code-background);color:var(--color-brand-content)}.highlight button.copybtn:after{background-color:transparent;color:var(--color-code-foreground);display:none}.highlight button.copybtn.success{color:#22863a;transition:color 0s}.highlight button.copybtn.success:after{display:block}.highlight button.copybtn svg{padding:0}body{--sd-color-primary:var(--color-brand-primary);--sd-color-primary-highlight:var(--color-brand-content);--sd-color-primary-text:var(--color-background-primary);--sd-color-shadow:rgba(0,0,0,.05);--sd-color-card-border:var(--color-card-border);--sd-color-card-border-hover:var(--color-brand-content);--sd-color-card-background:var(--color-card-background);--sd-color-card-text:var(--color-foreground-primary);--sd-color-card-header:var(--color-card-marginals-background);--sd-color-card-footer:var(--color-card-marginals-background);--sd-color-tabs-label-active:var(--color-brand-content);--sd-color-tabs-label-hover:var(--color-foreground-muted);--sd-color-tabs-label-inactive:var(--color-foreground-muted);--sd-color-tabs-underline-active:var(--color-brand-content);--sd-color-tabs-underline-hover:var(--color-foreground-border);--sd-color-tabs-underline-inactive:var(--color-background-border);--sd-color-tabs-overline:var(--color-background-border);--sd-color-tabs-underline:var(--color-background-border)}.sd-tab-content{box-shadow:0 -2px var(--sd-color-tabs-overline),0 1px var(--sd-color-tabs-underline)}.sd-card{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)}.sd-shadow-sm{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-md{box-shadow:0 .3rem .75rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-lg{box-shadow:0 .6rem 1.5rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-card-hover:hover{transform:none}.sd-cards-carousel{gap:.25rem;padding:.25rem}body{--tabs--label-text:var(--color-foreground-muted);--tabs--label-text--hover:var(--color-foreground-muted);--tabs--label-text--active:var(--color-brand-content);--tabs--label-text--active--hover:var(--color-brand-content);--tabs--label-background:transparent;--tabs--label-background--hover:transparent;--tabs--label-background--active:transparent;--tabs--label-background--active--hover:transparent;--tabs--padding-x:0.25em;--tabs--margin-x:1em;--tabs--border:var(--color-background-border);--tabs--label-border:transparent;--tabs--label-border--hover:var(--color-foreground-muted);--tabs--label-border--active:var(--color-brand-content);--tabs--label-border--active--hover:var(--color-brand-content)}[role=main] .container{max-width:none;padding-left:0;padding-right:0}.shadow.docutils{border:none;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)!important}.sphinx-bs .card{background-color:var(--color-background-secondary);color:var(--color-foreground)} +/*# sourceMappingURL=furo-extensions.css.map*/ \ No newline at end of file diff --git a/docs/manual/_static/styles/furo-extensions.css.map b/docs/manual/_static/styles/furo-extensions.css.map new file mode 100644 index 0000000..68fb7fd --- /dev/null +++ b/docs/manual/_static/styles/furo-extensions.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles/furo-extensions.css","mappings":"AAGA,2BACE,oFACA,4CAKE,6CAHA,YACA,eAEA,CACA,kDACE,yCAEF,8CACE,sCAEJ,8CACE,kDAEJ,2BAGE,uBACA,cAHA,gBACA,UAEA,CAGA,yCACE,mBAEF,gDAEE,gDADA,YACA,CACA,sDACE,gDACF,yDACE,sCAEJ,+CACE,UACA,qDACE,UAGF,mDACE,eAEJ,yEAEE,4DAEA,mHASE,mBAPA,kBAEA,YADA,oBAGA,aADA,gBAIA,CAEA,qIAEE,WADA,UACA,CAEJ,uGACE,aAEF,iUAGE,cAEF,mHACE,aC1EJ,gCACE,mCAEF,0BAEE,mBAUA,8CACA,YAFA,mCAKA,eAZA,cAIA,YADA,YAYA,iCAdA,YAcA,CAEA,gCAEE,8CADA,gCACA,CAEF,gCAGE,6BADA,mCADA,YAEA,CAEF,kCAEE,cADA,mBACA,CACA,wCACE,cAEJ,8BACE,UCzCN,KAEE,6CAA8C,CAC9C,uDAAwD,CACxD,uDAAwD,CAGxD,iCAAsC,CAGtC,+CAAgD,CAChD,uDAAwD,CACxD,uDAAwD,CACxD,oDAAqD,CACrD,6DAA8D,CAC9D,6DAA8D,CAG9D,uDAAwD,CACxD,yDAA0D,CAC1D,4DAA6D,CAC7D,2DAA4D,CAC5D,8DAA+D,CAC/D,iEAAkE,CAClE,uDAAwD,CACxD,wDAAyD,CAG3D,gBACE,qFAGF,SACE,6EAEF,cACE,uFAEF,cACE,uFAEF,cACE,uFAGF,qBACE,eAEF,mBACE,WACA,eChDF,KACE,gDAAiD,CACjD,uDAAwD,CACxD,qDAAsD,CACtD,4DAA6D,CAC7D,oCAAqC,CACrC,2CAA4C,CAC5C,4CAA6C,CAC7C,mDAAoD,CACpD,wBAAyB,CACzB,oBAAqB,CACrB,6CAA8C,CAC9C,gCAAiC,CACjC,yDAA0D,CAC1D,uDAAwD,CACxD,8DAA+D,CCbjE,uBACE,eACA,eACA,gBAGF,iBACE,YACA,+EAGF,iBACE,mDACA","sources":["webpack:///./src/furo/assets/styles/extensions/_readthedocs.sass","webpack:///./src/furo/assets/styles/extensions/_copybutton.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-design.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-inline-tabs.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-panels.sass"],"sourcesContent":["// This file contains the styles used for tweaking how ReadTheDoc's embedded\n// contents would show up inside the theme.\n\n#furo-sidebar-ad-placement\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n .ethical-sidebar\n // Remove the border and box-shadow.\n border: none\n box-shadow: none\n // Manage the background colors.\n background: var(--color-background-secondary)\n &:hover\n background: var(--color-background-hover)\n // Ensure the text is legible.\n a\n color: var(--color-foreground-primary)\n\n .ethical-callout a\n color: var(--color-foreground-secondary) !important\n\n#furo-readthedocs-versions\n position: static\n width: 100%\n background: transparent\n display: block\n\n // Make the background color fit with the theme's aesthetic.\n .rst-versions\n background: rgb(26, 28, 30)\n\n .rst-current-version\n cursor: unset\n background: var(--color-sidebar-item-background)\n &:hover\n background: var(--color-sidebar-item-background)\n .fa-book\n color: var(--color-foreground-primary)\n\n > .rst-other-versions\n padding: 0\n small\n opacity: 1\n\n .injected\n .rst-versions\n position: unset\n\n &:hover,\n &:focus-within\n box-shadow: 0 0 0 1px var(--color-sidebar-background-border)\n\n .rst-current-version\n // Undo the tweaks done in RTD's CSS\n font-size: inherit\n line-height: inherit\n height: auto\n text-align: right\n padding: 12px\n\n // Match the rest of the body\n background: #1a1c1e\n\n .fa-book\n float: left\n color: white\n\n .fa-caret-down\n display: none\n\n .rst-current-version,\n .rst-other-versions,\n .injected\n display: block\n\n > .rst-current-version\n display: none\n",".highlight\n &:hover button.copybtn\n color: var(--color-code-foreground)\n\n button.copybtn\n // Align things correctly\n align-items: center\n\n height: 1.25em\n width: 1.25em\n\n top: 0.625rem // $code-spacing-vertical\n right: 0.5rem\n\n // Make it look better\n color: var(--color-background-item)\n background-color: var(--color-code-background)\n border: none\n\n // Change to cursor to make it obvious that you can click on it\n cursor: pointer\n\n // Transition smoothly, for aesthetics\n transition: color 300ms, opacity 300ms\n\n &:hover\n color: var(--color-brand-content)\n background-color: var(--color-code-background)\n\n &::after\n display: none\n color: var(--color-code-foreground)\n background-color: transparent\n\n &.success\n transition: color 0ms\n color: #22863a\n &::after\n display: block\n\n svg\n padding: 0\n","body\n // Colors\n --sd-color-primary: var(--color-brand-primary)\n --sd-color-primary-highlight: var(--color-brand-content)\n --sd-color-primary-text: var(--color-background-primary)\n\n // Shadows\n --sd-color-shadow: rgba(0, 0, 0, 0.05)\n\n // Cards\n --sd-color-card-border: var(--color-card-border)\n --sd-color-card-border-hover: var(--color-brand-content)\n --sd-color-card-background: var(--color-card-background)\n --sd-color-card-text: var(--color-foreground-primary)\n --sd-color-card-header: var(--color-card-marginals-background)\n --sd-color-card-footer: var(--color-card-marginals-background)\n\n // Tabs\n --sd-color-tabs-label-active: var(--color-brand-content)\n --sd-color-tabs-label-hover: var(--color-foreground-muted)\n --sd-color-tabs-label-inactive: var(--color-foreground-muted)\n --sd-color-tabs-underline-active: var(--color-brand-content)\n --sd-color-tabs-underline-hover: var(--color-foreground-border)\n --sd-color-tabs-underline-inactive: var(--color-background-border)\n --sd-color-tabs-overline: var(--color-background-border)\n --sd-color-tabs-underline: var(--color-background-border)\n\n// Tabs\n.sd-tab-content\n box-shadow: 0 -2px var(--sd-color-tabs-overline), 0 1px var(--sd-color-tabs-underline)\n\n// Shadows\n.sd-card // Have a shadow by default\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n.sd-shadow-sm\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-md\n box-shadow: 0 0.3rem 0.75rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-lg\n box-shadow: 0 0.6rem 1.5rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Cards\n.sd-card-hover:hover // Don't change scale on hover\n transform: none\n\n.sd-cards-carousel // Have a bit of gap in the carousel by default\n gap: 0.25rem\n padding: 0.25rem\n","// This file contains styles to tweak sphinx-inline-tabs to work well with Furo.\n\nbody\n --tabs--label-text: var(--color-foreground-muted)\n --tabs--label-text--hover: var(--color-foreground-muted)\n --tabs--label-text--active: var(--color-brand-content)\n --tabs--label-text--active--hover: var(--color-brand-content)\n --tabs--label-background: transparent\n --tabs--label-background--hover: transparent\n --tabs--label-background--active: transparent\n --tabs--label-background--active--hover: transparent\n --tabs--padding-x: 0.25em\n --tabs--margin-x: 1em\n --tabs--border: var(--color-background-border)\n --tabs--label-border: transparent\n --tabs--label-border--hover: var(--color-foreground-muted)\n --tabs--label-border--active: var(--color-brand-content)\n --tabs--label-border--active--hover: var(--color-brand-content)\n","// This file contains styles to tweak sphinx-panels to work well with Furo.\n\n// sphinx-panels includes Bootstrap 4, which uses .container which can conflict\n// with docutils' `.. container::` directive.\n[role=\"main\"] .container\n max-width: initial\n padding-left: initial\n padding-right: initial\n\n// Make the panels look nicer!\n.shadow.docutils\n border: none\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Make panel colors respond to dark mode\n.sphinx-bs .card\n background-color: var(--color-background-secondary)\n color: var(--color-foreground)\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/docs/manual/_static/styles/furo.css b/docs/manual/_static/styles/furo.css new file mode 100644 index 0000000..592d5bf --- /dev/null +++ b/docs/manual/_static/styles/furo.css @@ -0,0 +1,2 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}@media print{.content-icon-container,.headerlink,.mobile-header,.related-pages{display:none!important}.highlight{border:.1pt solid var(--color-foreground-border)}a,blockquote,dl,ol,p,pre,table,ul{page-break-inside:avoid}caption,figure,h1,h2,h3,h4,h5,h6,img{page-break-after:avoid;page-break-inside:avoid}dl,ol,ul{page-break-before:avoid}}.visually-hidden{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;clip:rect(0,0,0,0)!important;background:var(--color-background-primary);border:0!important;color:var(--color-foreground-primary);white-space:nowrap!important}:-moz-focusring{outline:auto}body{--font-stack:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;--font-stack--monospace:"SFMono-Regular",Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace;--font-stack--headings:var(--font-stack);--font-size--normal:100%;--font-size--small:87.5%;--font-size--small--2:81.25%;--font-size--small--3:75%;--font-size--small--4:62.5%;--sidebar-caption-font-size:var(--font-size--small--2);--sidebar-item-font-size:var(--font-size--small);--sidebar-search-input-font-size:var(--font-size--small);--toc-font-size:var(--font-size--small--3);--toc-font-size--mobile:var(--font-size--normal);--toc-title-font-size:var(--font-size--small--4);--admonition-font-size:0.8125rem;--admonition-title-font-size:0.8125rem;--code-font-size:var(--font-size--small--2);--api-font-size:var(--font-size--small);--header-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*4);--header-padding:0.5rem;--sidebar-tree-space-above:1.5rem;--sidebar-caption-space-above:1rem;--sidebar-item-line-height:1rem;--sidebar-item-spacing-vertical:0.5rem;--sidebar-item-spacing-horizontal:1rem;--sidebar-item-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*2);--sidebar-expander-width:var(--sidebar-item-height);--sidebar-search-space-above:0.5rem;--sidebar-search-input-spacing-vertical:0.5rem;--sidebar-search-input-spacing-horizontal:0.5rem;--sidebar-search-input-height:1rem;--sidebar-search-icon-size:var(--sidebar-search-input-height);--toc-title-padding:0.25rem 0;--toc-spacing-vertical:1.5rem;--toc-spacing-horizontal:1.5rem;--toc-item-spacing-vertical:0.4rem;--toc-item-spacing-horizontal:1rem;--icon-search:url('data:image/svg+xml;charset=utf-8,');--icon-pencil:url('data:image/svg+xml;charset=utf-8,');--icon-abstract:url('data:image/svg+xml;charset=utf-8,');--icon-info:url('data:image/svg+xml;charset=utf-8,');--icon-flame:url('data:image/svg+xml;charset=utf-8,');--icon-question:url('data:image/svg+xml;charset=utf-8,');--icon-warning:url('data:image/svg+xml;charset=utf-8,');--icon-failure:url('data:image/svg+xml;charset=utf-8,');--icon-spark:url('data:image/svg+xml;charset=utf-8,');--color-admonition-title--caution:#ff9100;--color-admonition-title-background--caution:rgba(255,145,0,.2);--color-admonition-title--warning:#ff9100;--color-admonition-title-background--warning:rgba(255,145,0,.2);--color-admonition-title--danger:#ff5252;--color-admonition-title-background--danger:rgba(255,82,82,.2);--color-admonition-title--attention:#ff5252;--color-admonition-title-background--attention:rgba(255,82,82,.2);--color-admonition-title--error:#ff5252;--color-admonition-title-background--error:rgba(255,82,82,.2);--color-admonition-title--hint:#00c852;--color-admonition-title-background--hint:rgba(0,200,82,.2);--color-admonition-title--tip:#00c852;--color-admonition-title-background--tip:rgba(0,200,82,.2);--color-admonition-title--important:#00bfa5;--color-admonition-title-background--important:rgba(0,191,165,.2);--color-admonition-title--note:#00b0ff;--color-admonition-title-background--note:rgba(0,176,255,.2);--color-admonition-title--seealso:#448aff;--color-admonition-title-background--seealso:rgba(68,138,255,.2);--color-admonition-title--admonition-todo:grey;--color-admonition-title-background--admonition-todo:hsla(0,0%,50%,.2);--color-admonition-title:#651fff;--color-admonition-title-background:rgba(101,31,255,.2);--icon-admonition-default:var(--icon-abstract);--color-topic-title:#14b8a6;--color-topic-title-background:rgba(20,184,166,.2);--icon-topic-default:var(--icon-pencil);--color-problematic:#b30000;--color-foreground-primary:#000;--color-foreground-secondary:#5a5c63;--color-foreground-muted:#6b6f76;--color-foreground-border:#878787;--color-background-primary:#fff;--color-background-secondary:#f8f9fb;--color-background-hover:#efeff4;--color-background-hover--transparent:#efeff400;--color-background-border:#eeebee;--color-background-item:#ccc;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#0a4bff;--color-brand-content:#2757dd;--color-brand-visited:#872ee0;--color-api-background:var(--color-background-hover--transparent);--color-api-background-hover:var(--color-background-hover);--color-api-overall:var(--color-foreground-secondary);--color-api-name:var(--color-problematic);--color-api-pre-name:var(--color-problematic);--color-api-paren:var(--color-foreground-secondary);--color-api-keyword:var(--color-foreground-primary);--color-api-added:#21632c;--color-api-added-border:#38a84d;--color-api-changed:#046172;--color-api-changed-border:#06a1bc;--color-api-deprecated:#605706;--color-api-deprecated-border:#f0d90f;--color-api-removed:#b30000;--color-api-removed-border:#ff5c5c;--color-highlight-on-target:#ffc;--color-inline-code-background:var(--color-background-secondary);--color-highlighted-background:#def;--color-highlighted-text:var(--color-foreground-primary);--color-guilabel-background:#ddeeff80;--color-guilabel-border:#bedaf580;--color-guilabel-text:var(--color-foreground-primary);--color-admonition-background:transparent;--color-table-header-background:var(--color-background-secondary);--color-table-border:var(--color-background-border);--color-card-border:var(--color-background-secondary);--color-card-background:transparent;--color-card-marginals-background:var(--color-background-secondary);--color-header-background:var(--color-background-primary);--color-header-border:var(--color-background-border);--color-header-text:var(--color-foreground-primary);--color-sidebar-background:var(--color-background-secondary);--color-sidebar-background-border:var(--color-background-border);--color-sidebar-brand-text:var(--color-foreground-primary);--color-sidebar-caption-text:var(--color-foreground-muted);--color-sidebar-link-text:var(--color-foreground-secondary);--color-sidebar-link-text--top-level:var(--color-brand-primary);--color-sidebar-item-background:var(--color-sidebar-background);--color-sidebar-item-background--current:var( --color-sidebar-item-background );--color-sidebar-item-background--hover:linear-gradient(90deg,var(--color-background-hover--transparent) 0%,var(--color-background-hover) var(--sidebar-item-spacing-horizontal),var(--color-background-hover) 100%);--color-sidebar-item-expander-background:transparent;--color-sidebar-item-expander-background--hover:var( --color-background-hover );--color-sidebar-search-text:var(--color-foreground-primary);--color-sidebar-search-background:var(--color-background-secondary);--color-sidebar-search-background--focus:var(--color-background-primary);--color-sidebar-search-border:var(--color-background-border);--color-sidebar-search-icon:var(--color-foreground-muted);--color-toc-background:var(--color-background-primary);--color-toc-title-text:var(--color-foreground-muted);--color-toc-item-text:var(--color-foreground-secondary);--color-toc-item-text--hover:var(--color-foreground-primary);--color-toc-item-text--active:var(--color-brand-primary);--color-content-foreground:var(--color-foreground-primary);--color-content-background:transparent;--color-link:var(--color-brand-content);--color-link-underline:var(--color-background-border);--color-link--hover:var(--color-brand-content);--color-link-underline--hover:var(--color-foreground-border);--color-link--visited:var(--color-brand-visited);--color-link-underline--visited:var(--color-background-border);--color-link--visited--hover:var(--color-brand-visited);--color-link-underline--visited--hover:var(--color-foreground-border)}.only-light{display:block!important}html body .only-dark{display:none!important}@media not print{body[data-theme=dark]{--color-problematic:#ee5151;--color-foreground-primary:#cfd0d0;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body[data-theme=dark] .only-light{display:none!important}body[data-theme=dark] .only-dark{display:block!important}@media(prefers-color-scheme:dark){body:not([data-theme=light]){--color-problematic:#ee5151;--color-foreground-primary:#cfd0d0;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body:not([data-theme=light]) .only-light{display:none!important}body:not([data-theme=light]) .only-dark{display:block!important}}}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:block}@media(prefers-color-scheme:dark){body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-dark{display:block}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:none}}body[data-theme=dark] .theme-toggle svg.theme-icon-when-dark,body[data-theme=light] .theme-toggle svg.theme-icon-when-light{display:block}body{font-family:var(--font-stack)}code,kbd,pre,samp{font-family:var(--font-stack--monospace)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article{line-height:1.5}h1,h2,h3,h4,h5,h6{border-radius:.5rem;font-family:var(--font-stack--headings);font-weight:700;line-height:1.25;margin:.5rem -.5rem;padding-left:.5rem;padding-right:.5rem}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p{margin-top:0}h1{font-size:2.5em;margin-bottom:1rem}h1,h2{margin-top:1.75rem}h2{font-size:2em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1.125em}h6{font-size:1em}small{font-size:80%;opacity:75%}p{margin-bottom:.75rem;margin-top:.5rem}hr.docutils{background-color:var(--color-background-border);border:0;height:1px;margin:2rem 0;padding:0}.centered{text-align:center}a{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}a:visited{color:var(--color-link--visited);text-decoration-color:var(--color-link-underline--visited)}a:visited:hover{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}a:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link{color:inherit}a.muted-link:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link:hover:visited{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}html{overflow-x:hidden;overflow-y:scroll;scroll-behavior:smooth}.sidebar-scroll,.toc-scroll,article[role=main] *{scrollbar-color:var(--color-foreground-border) transparent;scrollbar-width:thin}body,html{height:100%}.skip-to-content,body,html{background:var(--color-background-primary);color:var(--color-foreground-primary)}.skip-to-content{border-radius:1rem;left:.25rem;padding:1rem;position:fixed;top:.25rem;transform:translateY(-200%);transition:transform .3s ease-in-out;z-index:40}.skip-to-content:focus-within{transform:translateY(0)}article{background:var(--color-content-background);color:var(--color-content-foreground);overflow-wrap:break-word}.page{display:flex;min-height:100%}.mobile-header{background-color:var(--color-header-background);border-bottom:1px solid var(--color-header-border);color:var(--color-header-text);display:none;height:var(--header-height);width:100%;z-index:10}.mobile-header.scrolled{border-bottom:none;box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2)}.mobile-header .header-center a{color:var(--color-header-text);text-decoration:none}.main{display:flex;flex:1}.sidebar-drawer{background:var(--color-sidebar-background);border-right:1px solid var(--color-sidebar-background-border);box-sizing:border-box;display:flex;justify-content:flex-end;min-width:15em;width:calc(50% - 26em)}.sidebar-container,.toc-drawer{box-sizing:border-box;width:15em}.toc-drawer{background:var(--color-toc-background);padding-right:1rem}.sidebar-sticky,.toc-sticky{display:flex;flex-direction:column;height:min(100%,100vh);height:100vh;position:sticky;top:0}.sidebar-scroll,.toc-scroll{flex-grow:1;flex-shrink:1;overflow:auto;scroll-behavior:smooth}.content{display:flex;flex-direction:column;justify-content:space-between;padding:0 3em;width:46em}.icon{display:inline-block;height:1rem;width:1rem}.icon svg{height:100%;width:100%}.announcement{align-items:center;background-color:var(--color-announcement-background);color:var(--color-announcement-text);display:flex;height:var(--header-height);overflow-x:auto}.announcement+.page{min-height:calc(100% - var(--header-height))}.announcement-content{box-sizing:border-box;min-width:100%;padding:.5rem;text-align:center;white-space:nowrap}.announcement-content a{color:var(--color-announcement-text);text-decoration-color:var(--color-announcement-text)}.announcement-content a:hover{color:var(--color-announcement-text);text-decoration-color:var(--color-link--hover)}.no-js .theme-toggle-container{display:none}.theme-toggle-container{display:flex}.theme-toggle{background:transparent;border:none;cursor:pointer;display:flex;padding:0}.theme-toggle svg{color:var(--color-foreground-primary);display:none;height:1.25rem;width:1.25rem}.theme-toggle-header{align-items:center;display:flex;justify-content:center}.nav-overlay-icon,.toc-overlay-icon{cursor:pointer;display:none}.nav-overlay-icon .icon,.toc-overlay-icon .icon{color:var(--color-foreground-secondary);height:1.5rem;width:1.5rem}.nav-overlay-icon,.toc-header-icon{align-items:center;justify-content:center}.toc-content-icon{height:1.5rem;width:1.5rem}.content-icon-container{display:flex;float:right;gap:.5rem;margin-bottom:1rem;margin-left:1rem;margin-top:1.5rem}.content-icon-container .edit-this-page svg,.content-icon-container .view-this-page svg{color:inherit;height:1.25rem;width:1.25rem}.sidebar-toggle{display:none;position:absolute}.sidebar-toggle[name=__toc]{left:20px}.sidebar-toggle:checked{left:40px}.overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0s,height 0s,opacity .25s ease-out;width:0}.sidebar-overlay{z-index:20}.toc-overlay{z-index:40}.sidebar-drawer{transition:left .25s ease-in-out;z-index:30}.toc-drawer{transition:right .25s ease-in-out;z-index:50}#__navigation:checked~.sidebar-overlay{height:100%;opacity:1;width:100%}#__navigation:checked~.page .sidebar-drawer{left:0;top:0}#__toc:checked~.toc-overlay{height:100%;opacity:1;width:100%}#__toc:checked~.page .toc-drawer{right:0;top:0}.back-to-top{background:var(--color-background-primary);border-radius:1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 1px 0 hsla(220,9%,46%,.502);display:none;font-size:.8125rem;left:0;margin-left:50%;padding:.5rem .75rem .5rem .5rem;position:fixed;text-decoration:none;top:1rem;transform:translateX(-50%);z-index:10}.back-to-top svg{height:1rem;width:1rem;fill:currentColor;display:inline-block}.back-to-top span{margin-left:.25rem}.show-back-to-top .back-to-top{align-items:center;display:flex}@media(min-width:97em){html{font-size:110%}}@media(max-width:82em){.toc-content-icon{display:flex}.toc-drawer{border-left:1px solid var(--color-background-muted);height:100vh;position:fixed;right:-15em;top:0}.toc-tree{border-left:none;font-size:var(--toc-font-size--mobile)}.sidebar-drawer{width:calc(50% - 18.5em)}}@media(max-width:67em){.content{margin-left:auto;margin-right:auto;padding:0 1em}}@media(max-width:63em){.nav-overlay-icon{display:flex}.sidebar-drawer{height:100vh;left:-15em;position:fixed;top:0;width:15em}.theme-toggle-header,.toc-header-icon{display:flex}.theme-toggle-content,.toc-content-icon{display:none}.mobile-header{align-items:center;display:flex;justify-content:space-between;position:sticky;top:0}.mobile-header .header-left,.mobile-header .header-right{display:flex;height:var(--header-height);padding:0 var(--header-padding)}.mobile-header .header-left label,.mobile-header .header-right label{height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.nav-overlay-icon .icon,.theme-toggle svg{height:1.5rem;width:1.5rem}:target{scroll-margin-top:calc(var(--header-height) + 2.5rem)}.back-to-top{top:calc(var(--header-height) + .5rem)}.page{flex-direction:column;justify-content:center}}@media(max-width:48em){.content{overflow-x:auto;width:100%}}@media(max-width:46em){article[role=main] aside.sidebar{float:none;margin:1rem 0;width:100%}}.admonition,.topic{background:var(--color-admonition-background);border-radius:.2rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1);font-size:var(--admonition-font-size);margin:1rem auto;overflow:hidden;padding:0 .5rem .5rem;page-break-inside:avoid}.admonition>:nth-child(2),.topic>:nth-child(2){margin-top:0}.admonition>:last-child,.topic>:last-child{margin-bottom:0}.admonition p.admonition-title,p.topic-title{font-size:var(--admonition-title-font-size);font-weight:500;line-height:1.3;margin:0 -.5rem .5rem;padding:.4rem .5rem .4rem 2rem;position:relative}.admonition p.admonition-title:before,p.topic-title:before{content:"";height:1rem;left:.5rem;position:absolute;width:1rem}p.admonition-title{background-color:var(--color-admonition-title-background)}p.admonition-title:before{background-color:var(--color-admonition-title);-webkit-mask-image:var(--icon-admonition-default);mask-image:var(--icon-admonition-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}p.topic-title{background-color:var(--color-topic-title-background)}p.topic-title:before{background-color:var(--color-topic-title);-webkit-mask-image:var(--icon-topic-default);mask-image:var(--icon-topic-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.admonition{border-left:.2rem solid var(--color-admonition-title)}.admonition.caution{border-left-color:var(--color-admonition-title--caution)}.admonition.caution>.admonition-title{background-color:var(--color-admonition-title-background--caution)}.admonition.caution>.admonition-title:before{background-color:var(--color-admonition-title--caution);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.warning{border-left-color:var(--color-admonition-title--warning)}.admonition.warning>.admonition-title{background-color:var(--color-admonition-title-background--warning)}.admonition.warning>.admonition-title:before{background-color:var(--color-admonition-title--warning);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.danger{border-left-color:var(--color-admonition-title--danger)}.admonition.danger>.admonition-title{background-color:var(--color-admonition-title-background--danger)}.admonition.danger>.admonition-title:before{background-color:var(--color-admonition-title--danger);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.attention{border-left-color:var(--color-admonition-title--attention)}.admonition.attention>.admonition-title{background-color:var(--color-admonition-title-background--attention)}.admonition.attention>.admonition-title:before{background-color:var(--color-admonition-title--attention);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.error{border-left-color:var(--color-admonition-title--error)}.admonition.error>.admonition-title{background-color:var(--color-admonition-title-background--error)}.admonition.error>.admonition-title:before{background-color:var(--color-admonition-title--error);-webkit-mask-image:var(--icon-failure);mask-image:var(--icon-failure)}.admonition.hint{border-left-color:var(--color-admonition-title--hint)}.admonition.hint>.admonition-title{background-color:var(--color-admonition-title-background--hint)}.admonition.hint>.admonition-title:before{background-color:var(--color-admonition-title--hint);-webkit-mask-image:var(--icon-question);mask-image:var(--icon-question)}.admonition.tip{border-left-color:var(--color-admonition-title--tip)}.admonition.tip>.admonition-title{background-color:var(--color-admonition-title-background--tip)}.admonition.tip>.admonition-title:before{background-color:var(--color-admonition-title--tip);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.important{border-left-color:var(--color-admonition-title--important)}.admonition.important>.admonition-title{background-color:var(--color-admonition-title-background--important)}.admonition.important>.admonition-title:before{background-color:var(--color-admonition-title--important);-webkit-mask-image:var(--icon-flame);mask-image:var(--icon-flame)}.admonition.note{border-left-color:var(--color-admonition-title--note)}.admonition.note>.admonition-title{background-color:var(--color-admonition-title-background--note)}.admonition.note>.admonition-title:before{background-color:var(--color-admonition-title--note);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition.seealso{border-left-color:var(--color-admonition-title--seealso)}.admonition.seealso>.admonition-title{background-color:var(--color-admonition-title-background--seealso)}.admonition.seealso>.admonition-title:before{background-color:var(--color-admonition-title--seealso);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.admonition-todo{border-left-color:var(--color-admonition-title--admonition-todo)}.admonition.admonition-todo>.admonition-title{background-color:var(--color-admonition-title-background--admonition-todo)}.admonition.admonition-todo>.admonition-title:before{background-color:var(--color-admonition-title--admonition-todo);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition-todo>.admonition-title{text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd{margin-left:2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:first-child{margin-top:.125rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list,dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:last-child{margin-bottom:.75rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list>dt{font-size:var(--font-size--small);text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd:empty{margin-bottom:.5rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul{margin-left:-1.2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p:nth-child(2){margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p+p:last-child:empty{margin-bottom:0;margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{color:var(--color-api-overall)}.sig:not(.sig-inline){background:var(--color-api-background);border-radius:.25rem;font-family:var(--font-stack--monospace);font-size:var(--api-font-size);font-weight:700;margin-left:-.25rem;margin-right:-.25rem;padding:.25rem .5rem .25rem 3em;text-indent:-2.5em;transition:background .1s ease-out}.sig:not(.sig-inline):hover{background:var(--color-api-background-hover)}.sig:not(.sig-inline) a.reference .viewcode-link{font-weight:400;width:4.25rem}em.property{font-style:normal}em.property:first-child{color:var(--color-api-keyword)}.sig-name{color:var(--color-api-name)}.sig-prename{color:var(--color-api-pre-name);font-weight:400}.sig-paren{color:var(--color-api-paren)}.sig-param{font-style:normal}div.deprecated,div.versionadded,div.versionchanged,div.versionremoved{border-left:.1875rem solid;border-radius:.125rem;padding-left:.75rem}div.deprecated p,div.versionadded p,div.versionchanged p,div.versionremoved p{margin-bottom:.125rem;margin-top:.125rem}div.versionadded{border-color:var(--color-api-added-border)}div.versionadded .versionmodified{color:var(--color-api-added)}div.versionchanged{border-color:var(--color-api-changed-border)}div.versionchanged .versionmodified{color:var(--color-api-changed)}div.deprecated{border-color:var(--color-api-deprecated-border)}div.deprecated .versionmodified{color:var(--color-api-deprecated)}div.versionremoved{border-color:var(--color-api-removed-border)}div.versionremoved .versionmodified{color:var(--color-api-removed)}.viewcode-back,.viewcode-link{float:right;text-align:right}.line-block{margin-bottom:.75rem;margin-top:.5rem}.line-block .line-block{margin-bottom:0;margin-top:0;padding-left:1rem}.code-block-caption,article p.caption,table>caption{font-size:var(--font-size--small);text-align:center}.toctree-wrapper.compound .caption,.toctree-wrapper.compound :not(.caption)>.caption-text{font-size:var(--font-size--small);margin-bottom:0;text-align:initial;text-transform:uppercase}.toctree-wrapper.compound>ul{margin-bottom:0;margin-top:0}.sig-inline,code.literal{background:var(--color-inline-code-background);border-radius:.2em;font-size:var(--font-size--small--2);padding:.1em .2em}pre.literal-block .sig-inline,pre.literal-block code.literal{font-size:inherit;padding:0}p .sig-inline,p code.literal{border:1px solid var(--color-background-border)}.sig-inline{font-family:var(--font-stack--monospace)}div[class*=" highlight-"],div[class^=highlight-]{display:flex;margin:1em 0}div[class*=" highlight-"] .table-wrapper,div[class^=highlight-] .table-wrapper,pre{margin:0;padding:0}pre{overflow:auto}article[role=main] .highlight pre{line-height:1.5}.highlight pre,pre.literal-block{font-size:var(--code-font-size);padding:.625rem .875rem}pre.literal-block{background-color:var(--color-code-background);border-radius:.2rem;color:var(--color-code-foreground);margin-bottom:1rem;margin-top:1rem}.highlight{border-radius:.2rem;width:100%}.highlight .gp,.highlight span.linenos{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.highlight .hll{display:block;margin-left:-.875rem;margin-right:-.875rem;padding-left:.875rem;padding-right:.875rem}.code-block-caption{background-color:var(--color-code-background);border-bottom:1px solid;border-radius:.25rem;border-bottom-left-radius:0;border-bottom-right-radius:0;border-color:var(--color-background-border);color:var(--color-code-foreground);display:flex;font-weight:300;padding:.625rem .875rem}.code-block-caption+div[class]{margin-top:0}.code-block-caption+div[class]>.highlight{border-top-left-radius:0;border-top-right-radius:0}.highlighttable{display:block;width:100%}.highlighttable tbody{display:block}.highlighttable tr{display:flex}.highlighttable td.linenos{background-color:var(--color-code-background);border-bottom-left-radius:.2rem;border-top-left-radius:.2rem;color:var(--color-code-foreground);padding:.625rem 0 .625rem .875rem}.highlighttable .linenodiv{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;font-size:var(--code-font-size);padding-right:.875rem}.highlighttable td.code{display:block;flex:1;overflow:hidden;padding:0}.highlighttable td.code .highlight{border-bottom-left-radius:0;border-top-left-radius:0}.highlight span.linenos{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;display:inline-block;margin-right:.875rem;padding-left:0;padding-right:.875rem}.footnote-reference{font-size:var(--font-size--small--4);vertical-align:super}dl.footnote.brackets{color:var(--color-foreground-secondary);display:grid;font-size:var(--font-size--small);grid-template-columns:max-content auto}dl.footnote.brackets dt{margin:0}dl.footnote.brackets dt>.fn-backref{margin-left:.25rem}dl.footnote.brackets dt:after{content:":"}dl.footnote.brackets dt .brackets:before{content:"["}dl.footnote.brackets dt .brackets:after{content:"]"}dl.footnote.brackets dd{margin:0;padding:0 1rem}aside.footnote{color:var(--color-foreground-secondary);font-size:var(--font-size--small)}aside.footnote>span,div.citation>span{float:left;font-weight:500;padding-right:.25rem}aside.footnote>:not(span),div.citation>p{margin-left:2rem}img{box-sizing:border-box;height:auto;max-width:100%}article .figure,article figure{border-radius:.2rem;margin:0}article .figure :last-child,article figure :last-child{margin-bottom:0}article .align-left{clear:left;float:left;margin:0 1rem 1rem}article .align-right{clear:right;float:right;margin:0 1rem 1rem}article .align-center,article .align-default{display:block;margin-left:auto;margin-right:auto;text-align:center}article table.align-default{display:table;text-align:initial}.domainindex-jumpbox,.genindex-jumpbox{border-bottom:1px solid var(--color-background-border);border-top:1px solid var(--color-background-border);padding:.25rem}.domainindex-section h2,.genindex-section h2{margin-bottom:.5rem;margin-top:.75rem}.domainindex-section ul,.genindex-section ul{margin-bottom:0;margin-top:0}ol,ul{margin-bottom:1rem;margin-top:1rem;padding-left:1.2rem}ol li>p:first-child,ul li>p:first-child{margin-bottom:.25rem;margin-top:.25rem}ol li>p:last-child,ul li>p:last-child{margin-top:.25rem}ol li>ol,ol li>ul,ul li>ol,ul li>ul{margin-bottom:.5rem;margin-top:.5rem}ol.arabic{list-style:decimal}ol.loweralpha{list-style:lower-alpha}ol.upperalpha{list-style:upper-alpha}ol.lowerroman{list-style:lower-roman}ol.upperroman{list-style:upper-roman}.simple li>ol,.simple li>ul,.toctree-wrapper li>ol,.toctree-wrapper li>ul{margin-bottom:0;margin-top:0}.field-list dt,.option-list dt,dl.footnote dt,dl.glossary dt,dl.simple dt,dl:not([class]) dt{font-weight:500;margin-top:.25rem}.field-list dt+dt,.option-list dt+dt,dl.footnote dt+dt,dl.glossary dt+dt,dl.simple dt+dt,dl:not([class]) dt+dt{margin-top:0}.field-list dt .classifier:before,.option-list dt .classifier:before,dl.footnote dt .classifier:before,dl.glossary dt .classifier:before,dl.simple dt .classifier:before,dl:not([class]) dt .classifier:before{content:":";margin-left:.2rem;margin-right:.2rem}.field-list dd ul,.field-list dd>p:first-child,.option-list dd ul,.option-list dd>p:first-child,dl.footnote dd ul,dl.footnote dd>p:first-child,dl.glossary dd ul,dl.glossary dd>p:first-child,dl.simple dd ul,dl.simple dd>p:first-child,dl:not([class]) dd ul,dl:not([class]) dd>p:first-child{margin-top:.125rem}.field-list dd ul,.option-list dd ul,dl.footnote dd ul,dl.glossary dd ul,dl.simple dd ul,dl:not([class]) dd ul{margin-bottom:.125rem}.math-wrapper{overflow-x:auto;width:100%}div.math{position:relative;text-align:center}div.math .headerlink,div.math:focus .headerlink{display:none}div.math:hover .headerlink{display:inline-block}div.math span.eqno{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);z-index:1}abbr[title]{cursor:help}.problematic{color:var(--color-problematic)}kbd:not(.compound){background-color:var(--color-background-secondary);border:1px solid var(--color-foreground-border);border-radius:.2rem;box-shadow:0 .0625rem 0 rgba(0,0,0,.2),inset 0 0 0 .125rem var(--color-background-primary);color:var(--color-foreground-primary);display:inline-block;font-size:var(--font-size--small--3);margin:0 .2rem;padding:0 .2rem;vertical-align:text-bottom}blockquote{background:var(--color-background-secondary);border-left:4px solid var(--color-background-border);margin-left:0;margin-right:0;padding:.5rem 1rem}blockquote .attribution{font-weight:600;text-align:right}blockquote.highlights,blockquote.pull-quote{font-size:1.25em}blockquote.epigraph,blockquote.pull-quote{border-left-width:0;border-radius:.5rem}blockquote.highlights{background:transparent;border-left-width:0}p .reference img{vertical-align:middle}p.rubric{font-size:1.125em;font-weight:700;line-height:1.25}dd p.rubric{font-size:var(--font-size--small);font-weight:inherit;line-height:inherit;text-transform:uppercase}article .sidebar{background-color:var(--color-background-secondary);border:1px solid var(--color-background-border);border-radius:.2rem;clear:right;float:right;margin-left:1rem;margin-right:0;width:30%}article .sidebar>*{padding-left:1rem;padding-right:1rem}article .sidebar>ol,article .sidebar>ul{padding-left:2.2rem}article .sidebar .sidebar-title{border-bottom:1px solid var(--color-background-border);font-weight:500;margin:0;padding:.5rem 1rem}[role=main] .table-wrapper.container{margin-bottom:.5rem;margin-top:1rem;overflow-x:auto;padding:.2rem .2rem .75rem;width:100%}table.docutils{border-collapse:collapse;border-radius:.2rem;border-spacing:0;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)}table.docutils th{background:var(--color-table-header-background)}table.docutils td,table.docutils th{border-bottom:1px solid var(--color-table-border);border-left:1px solid var(--color-table-border);border-right:1px solid var(--color-table-border);padding:0 .25rem}table.docutils td p,table.docutils th p{margin:.25rem}table.docutils td:first-child,table.docutils th:first-child{border-left:none}table.docutils td:last-child,table.docutils th:last-child{border-right:none}table.docutils td.text-left,table.docutils th.text-left{text-align:left}table.docutils td.text-right,table.docutils th.text-right{text-align:right}table.docutils td.text-center,table.docutils th.text-center{text-align:center}:target{scroll-margin-top:2.5rem}@media(max-width:67em){:target{scroll-margin-top:calc(2.5rem + var(--header-height))}section>span:target{scroll-margin-top:calc(2.8rem + var(--header-height))}}.headerlink{font-weight:100;-webkit-user-select:none;-moz-user-select:none;user-select:none}.code-block-caption>.headerlink,dl dt>.headerlink,figcaption p>.headerlink,h1>.headerlink,h2>.headerlink,h3>.headerlink,h4>.headerlink,h5>.headerlink,h6>.headerlink,p.caption>.headerlink,table>caption>.headerlink{margin-left:.5rem;visibility:hidden}.code-block-caption:hover>.headerlink,dl dt:hover>.headerlink,figcaption p:hover>.headerlink,h1:hover>.headerlink,h2:hover>.headerlink,h3:hover>.headerlink,h4:hover>.headerlink,h5:hover>.headerlink,h6:hover>.headerlink,p.caption:hover>.headerlink,table>caption:hover>.headerlink{visibility:visible}.code-block-caption>.toc-backref,dl dt>.toc-backref,figcaption p>.toc-backref,h1>.toc-backref,h2>.toc-backref,h3>.toc-backref,h4>.toc-backref,h5>.toc-backref,h6>.toc-backref,p.caption>.toc-backref,table>caption>.toc-backref{color:inherit;text-decoration-line:none}figure:hover>figcaption>p>.headerlink,table:hover>caption>.headerlink{visibility:visible}:target>h1:first-of-type,:target>h2:first-of-type,:target>h3:first-of-type,:target>h4:first-of-type,:target>h5:first-of-type,:target>h6:first-of-type,span:target~h1:first-of-type,span:target~h2:first-of-type,span:target~h3:first-of-type,span:target~h4:first-of-type,span:target~h5:first-of-type,span:target~h6:first-of-type{background-color:var(--color-highlight-on-target)}:target>h1:first-of-type code.literal,:target>h2:first-of-type code.literal,:target>h3:first-of-type code.literal,:target>h4:first-of-type code.literal,:target>h5:first-of-type code.literal,:target>h6:first-of-type code.literal,span:target~h1:first-of-type code.literal,span:target~h2:first-of-type code.literal,span:target~h3:first-of-type code.literal,span:target~h4:first-of-type code.literal,span:target~h5:first-of-type code.literal,span:target~h6:first-of-type code.literal{background-color:transparent}.literal-block-wrapper:target .code-block-caption,.this-will-duplicate-information-and-it-is-still-useful-here li :target,figure:target,table:target>caption{background-color:var(--color-highlight-on-target)}dt:target{background-color:var(--color-highlight-on-target)!important}.footnote-reference:target,.footnote>dt:target+dd{background-color:var(--color-highlight-on-target)}.guilabel{background-color:var(--color-guilabel-background);border:1px solid var(--color-guilabel-border);border-radius:.5em;color:var(--color-guilabel-text);font-size:.9em;padding:0 .3em}footer{display:flex;flex-direction:column;font-size:var(--font-size--small);margin-top:2rem}.bottom-of-page{align-items:center;border-top:1px solid var(--color-background-border);color:var(--color-foreground-secondary);display:flex;justify-content:space-between;line-height:1.5;margin-top:1rem;padding-bottom:1rem;padding-top:1rem}@media(max-width:46em){.bottom-of-page{flex-direction:column-reverse;gap:.25rem;text-align:center}}.bottom-of-page .left-details{font-size:var(--font-size--small)}.bottom-of-page .right-details{display:flex;flex-direction:column;gap:.25rem;text-align:right}.bottom-of-page .icons{display:flex;font-size:1rem;gap:.25rem;justify-content:flex-end}.bottom-of-page .icons a{text-decoration:none}.bottom-of-page .icons img,.bottom-of-page .icons svg{font-size:1.125rem;height:1em;width:1em}.related-pages a{align-items:center;display:flex;text-decoration:none}.related-pages a:hover .page-info .title{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}.related-pages a svg.furo-related-icon,.related-pages a svg.furo-related-icon>use{color:var(--color-foreground-border);flex-shrink:0;height:.75rem;margin:0 .5rem;width:.75rem}.related-pages a.next-page{clear:right;float:right;max-width:50%;text-align:right}.related-pages a.prev-page{clear:left;float:left;max-width:50%}.related-pages a.prev-page svg{transform:rotate(180deg)}.page-info{display:flex;flex-direction:column;overflow-wrap:anywhere}.next-page .page-info{align-items:flex-end}.page-info .context{align-items:center;color:var(--color-foreground-muted);display:flex;font-size:var(--font-size--small);padding-bottom:.1rem;text-decoration:none}ul.search{list-style:none;padding-left:0}ul.search li{border-bottom:1px solid var(--color-background-border);padding:1rem 0}[role=main] .highlighted{background-color:var(--color-highlighted-background);color:var(--color-highlighted-text)}.sidebar-brand{display:flex;flex-direction:column;flex-shrink:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none}.sidebar-brand-text{color:var(--color-sidebar-brand-text);font-size:1.5rem;overflow-wrap:break-word}.sidebar-brand-text,.sidebar-logo-container{margin:var(--sidebar-item-spacing-vertical) 0}.sidebar-logo{display:block;margin:0 auto;max-width:100%}.sidebar-search-container{align-items:center;background:var(--color-sidebar-search-background);display:flex;margin-top:var(--sidebar-search-space-above);position:relative}.sidebar-search-container:focus-within,.sidebar-search-container:hover{background:var(--color-sidebar-search-background--focus)}.sidebar-search-container:before{background-color:var(--color-sidebar-search-icon);content:"";height:var(--sidebar-search-icon-size);left:var(--sidebar-item-spacing-horizontal);-webkit-mask-image:var(--icon-search);mask-image:var(--icon-search);position:absolute;width:var(--sidebar-search-icon-size)}.sidebar-search{background:transparent;border:none;border-bottom:1px solid var(--color-sidebar-search-border);border-top:1px solid var(--color-sidebar-search-border);box-sizing:border-box;color:var(--color-sidebar-search-foreground);padding:var(--sidebar-search-input-spacing-vertical) var(--sidebar-search-input-spacing-horizontal) var(--sidebar-search-input-spacing-vertical) calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size));width:100%;z-index:10}.sidebar-search:focus{outline:none}.sidebar-search::-moz-placeholder{font-size:var(--sidebar-search-input-font-size)}.sidebar-search::placeholder{font-size:var(--sidebar-search-input-font-size)}#searchbox .highlight-link{margin:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0;text-align:center}#searchbox .highlight-link a{color:var(--color-sidebar-search-icon);font-size:var(--font-size--small--2)}.sidebar-tree{font-size:var(--sidebar-item-font-size);margin-bottom:var(--sidebar-item-spacing-vertical);margin-top:var(--sidebar-tree-space-above)}.sidebar-tree ul{display:flex;flex-direction:column;list-style:none;margin-bottom:0;margin-top:0;padding:0}.sidebar-tree li{margin:0;position:relative}.sidebar-tree li>ul{margin-left:var(--sidebar-item-spacing-horizontal)}.sidebar-tree .icon,.sidebar-tree .reference{color:var(--color-sidebar-link-text)}.sidebar-tree .reference{box-sizing:border-box;display:inline-block;height:100%;line-height:var(--sidebar-item-line-height);overflow-wrap:anywhere;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none;width:100%}.sidebar-tree .reference:hover{background:var(--color-sidebar-item-background--hover);color:var(--color-sidebar-link-text)}.sidebar-tree .reference.external:after{color:var(--color-sidebar-link-text);content:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23607d8b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M11 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-5M10 14 20 4M15 4h5v5'/%3E%3C/svg%3E");margin:0 .25rem;vertical-align:middle}.sidebar-tree .current-page>.reference{font-weight:700}.sidebar-tree label{align-items:center;cursor:pointer;display:flex;height:var(--sidebar-item-height);justify-content:center;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--sidebar-expander-width)}.sidebar-tree .caption,.sidebar-tree :not(.caption)>.caption-text{color:var(--color-sidebar-caption-text);font-size:var(--sidebar-caption-font-size);font-weight:700;margin:var(--sidebar-caption-space-above) 0 0 0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-transform:uppercase}.sidebar-tree li.has-children>.reference{padding-right:var(--sidebar-expander-width)}.sidebar-tree .toctree-l1>.reference,.sidebar-tree .toctree-l1>label .icon{color:var(--color-sidebar-link-text--top-level)}.sidebar-tree label{background:var(--color-sidebar-item-expander-background)}.sidebar-tree label:hover{background:var(--color-sidebar-item-expander-background--hover)}.sidebar-tree .current>.reference{background:var(--color-sidebar-item-background--current)}.sidebar-tree .current>.reference:hover{background:var(--color-sidebar-item-background--hover)}.toctree-checkbox{display:none;position:absolute}.toctree-checkbox~ul{display:none}.toctree-checkbox~label .icon svg{transform:rotate(90deg)}.toctree-checkbox:checked~ul{display:block}.toctree-checkbox:checked~label .icon svg{transform:rotate(-90deg)}.toc-title-container{padding:var(--toc-title-padding);padding-top:var(--toc-spacing-vertical)}.toc-title{color:var(--color-toc-title-text);font-size:var(--toc-title-font-size);padding-left:var(--toc-spacing-horizontal);text-transform:uppercase}.no-toc{display:none}.toc-tree-container{padding-bottom:var(--toc-spacing-vertical)}.toc-tree{border-left:1px solid var(--color-background-border);font-size:var(--toc-font-size);line-height:1.3;padding-left:calc(var(--toc-spacing-horizontal) - var(--toc-item-spacing-horizontal))}.toc-tree>ul>li:first-child{padding-top:0}.toc-tree>ul>li:first-child>ul{padding-left:0}.toc-tree>ul>li:first-child>a{display:none}.toc-tree ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:var(--toc-item-spacing-horizontal)}.toc-tree li{padding-top:var(--toc-item-spacing-vertical)}.toc-tree li.scroll-current>.reference{color:var(--color-toc-item-text--active);font-weight:700}.toc-tree a.reference{color:var(--color-toc-item-text);overflow-wrap:anywhere;text-decoration:none}.toc-scroll{max-height:100vh;overflow-y:scroll}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here){background:rgba(255,0,0,.25);color:var(--color-problematic)}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here):before{content:"ERROR: Adding a table of contents in Furo-based documentation is unnecessary, and does not work well with existing styling. Add a 'this-will-duplicate-information-and-it-is-still-useful-here' class, if you want an escape hatch."}.text-align\:left>p{text-align:left}.text-align\:center>p{text-align:center}.text-align\:right>p{text-align:right} +/*# sourceMappingURL=furo.css.map*/ \ No newline at end of file diff --git a/docs/manual/_static/styles/furo.css.map b/docs/manual/_static/styles/furo.css.map new file mode 100644 index 0000000..280b3fe --- /dev/null +++ b/docs/manual/_static/styles/furo.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles/furo.css","mappings":"AAAA,2EAA2E,CAU3E,KACE,gBAAiB,CACjB,6BACF,CASA,KACE,QACF,CAMA,KACE,aACF,CAOA,GACE,aAAc,CACd,cACF,CAUA,GACE,sBAAuB,CACvB,QAAS,CACT,gBACF,CAOA,IACE,+BAAiC,CACjC,aACF,CASA,EACE,4BACF,CAOA,YACE,kBAAmB,CACnB,yBAA0B,CAC1B,gCACF,CAMA,SAEE,kBACF,CAOA,cAGE,+BAAiC,CACjC,aACF,CAeA,QAEE,aAAc,CACd,aAAc,CACd,iBAAkB,CAClB,uBACF,CAEA,IACE,aACF,CAEA,IACE,SACF,CASA,IACE,iBACF,CAUA,sCAKE,mBAAoB,CACpB,cAAe,CACf,gBAAiB,CACjB,QACF,CAOA,aAEE,gBACF,CAOA,cAEE,mBACF,CAMA,gDAIE,yBACF,CAMA,wHAIE,iBAAkB,CAClB,SACF,CAMA,4GAIE,6BACF,CAMA,SACE,0BACF,CASA,OACE,qBAAsB,CACtB,aAAc,CACd,aAAc,CACd,cAAe,CACf,SAAU,CACV,kBACF,CAMA,SACE,uBACF,CAMA,SACE,aACF,CAOA,6BAEE,qBAAsB,CACtB,SACF,CAMA,kFAEE,WACF,CAOA,cACE,4BAA6B,CAC7B,mBACF,CAMA,yCACE,uBACF,CAOA,6BACE,yBAA0B,CAC1B,YACF,CASA,QACE,aACF,CAMA,QACE,iBACF,CAiBA,kBACE,YACF,CCvVA,aAcE,kEACE,uBAOF,WACE,iDAMF,kCACE,wBAEF,qCAEE,uBADA,uBACA,CAEF,SACE,wBAtBA,CCpBJ,iBAGE,qBAEA,sBACA,0BAFA,oBAHA,4BACA,oBAKA,6BAIA,2CAFA,mBACA,sCAFA,4BAGA,CAEF,gBACE,aCPF,KCCE,mHAGA,wGAGA,wCAAyC,CAEzC,wBAAyB,CACzB,wBAAyB,CACzB,4BAA6B,CAC7B,yBAA0B,CAC1B,2BAA4B,CAG5B,sDAAuD,CACvD,gDAAiD,CACjD,wDAAyD,CAGzD,0CAA2C,CAC3C,gDAAiD,CACjD,gDAAiD,CAKjD,gCAAiC,CACjC,sCAAuC,CAGvC,2CAA4C,CAG5C,uCAAwC,CCnCxC,+FAIA,uBAAwB,CAGxB,iCAAkC,CAClC,kCAAmC,CAEnC,+BAAgC,CAChC,sCAAuC,CACvC,sCAAuC,CACvC,qGAIA,mDAAoD,CAEpD,mCAAoC,CACpC,8CAA+C,CAC/C,gDAAiD,CACjD,kCAAmC,CACnC,6DAA8D,CAG9D,6BAA8B,CAC9B,6BAA8B,CAC9B,+BAAgC,CAChC,kCAAmC,CACnC,kCAAmC,CCRjC,+jBCaA,iqCAZF,iaCXA,8KAOA,4SAWA,4SAUA,0CACA,gEAGA,0CAGA,gEAGA,yCACA,+DAIA,4CACA,kEAGA,wCAUA,8DACA,uCAGA,4DACA,sCACA,2DAGA,4CACA,kEACA,uCAGA,6DACA,2GAGA,sHAEA,yFAEA,+CACA,+EAGA,4MAOA,gCACA,sHAIA,kCACA,uEACA,gEACA,4DACA,kEAGA,2DACA,sDACA,0CACA,8CACA,wGAGA,0BACA,iCAGA,+DACA,+BACA,sCACA,+DAEA,kGACA,oCACA,yDACA,sCL3HF,kCAEA,sDAIA,0CKyHE,kEAIA,oDACA,sDAGA,oCACA,oEAEA,0DACA,qDAIA,oDACA,6DAIA,iEAIA,2DAIA,2DAGA,4DACA,gEAIA,gEAEA,gFAEA,oNASA,qDLtKE,gFAGE,4DAIF,oEKgHF,yEAEA,6DAGA,0DAEA,uDACA,qDACA,wDAIA,6DAIA,yDACA,2DAIA,uCAGA,wCACA,sDAGA,+CAGA,6DAEA,iDACA,+DAEA,wDAEA,sEAMA,0DACA,sBACA,mEL5JI,wEAEA,iCACE,+BAMN,wEAGA,iCACE,kFAEA,uEAIF,gEACE,8BAGF,qEMzDA,sCAKA,wFAKA,iCAIA,0BAWA,iCACA,4BACA,mCAGA,+BAEA,sCACA,4BAEA,mCAEA,sCAKA,sDAIA,gCAEA,gEAQF,wCAME,sBACA,kCAKA,uBAEA,gEAIA,2BAIA,mCAEA,qCACA,iCAGE,+BACA,wEAEE,iCACA,kFAGF,6BACA,0CACF,kCAEE,8BACE,8BACA,qEAEE,sCACA,wFClFN,iCAGF,2DACE,4BACA,oCAKF,8BAGE,sCACA,+DAIA,sCAEA,sDAGA,gCACA,gEAGA,+CAEA,sBACE,yCAGF,uBACA,sEAIA,aAEA,mCAIA,kEACA,aACA,oEACA,YAIA,EAQE,4HAGA,gDACE,mBACA,wCAON,wCAGE,0DACA,mBAKA,mBACA,CANA,uCAKA,iBALA,iBAWA,mBAGF,mBACE,mDAIF,+BAEE,CAEA,yBAFA,kBAMA,CAJA,GACA,aAGA,mBAEF,wBAEE,iBACA,iBAEA,OACA,aAGF,CAHE,WAGF,GAEE,oBAEA,CAJF,gBAIE,aAEA,+CAKA,UANA,WACA,cADA,SAMA,WACA,iBAEE,GAMF,wBANE,yBAMF,kDACA,WAEA,gCACA,2DAGA,iBACE,uCAEJ,kEAIE,uCAGA,yDACE,cACA,+DAEA,yDAEE,mEAMJ,kEAMA,uBACA,kBAEA,uBACA,kDAKA,0DAIA,CALA,oBAKA,WACA,WAQA,4BAFF,0CAEE,CARA,qCAsBA,CAdA,iBAEA,kBACE,aADF,4BACE,WAMF,2BAGF,qCAEE,CAXE,UAWF,+BAGA,uBAEA,SAEA,0CAIE,CANF,qCAEA,CAIE,2DACE,gBAIN,+CAIA,CAEA,kDAKE,CAPF,8BAEA,CAOE,YACA,CAjBI,2BAGN,CAHM,WAcJ,UAGA,CAEA,2GAIF,iCAGE,8BAIA,qBACA,oBACF,uBAOI,0CAIA,CATF,6DAKE,CALF,sBASE,qCAKF,CACE,cACA,CAFF,sBAEE,CACA,+BAEA,qBAEE,WAKN,aACE,sCAGA,mBAEA,6BAMA,kCACA,CAJA,sBACA,aAEA,CAJA,eACA,MAIA,2FAEA,UAGA,YACA,sBACE,8BAEA,CALF,aACA,WAIE,OACA,oBAEF,uBACE,WAEF,YAFE,UAEF,eAgBA,kBACE,CAhBA,qDAQF,qCAGF,CAGI,YACF,CAJF,2BAGI,CAEA,eACA,qBAGA,mEAEA,qBACA,8BAIA,kBADF,kBACE,yBAEJ,oCAGI,qDAIJ,+BAGI,oCAEA,+CAQF,4CACE,yBACF,2BAOE,sBACA,CAHA,WACA,CAFF,cACE,CAJA,YAGF,CAEE,SAEA,mBAGA,kDAEE,CAJF,cAEA,cAEE,sBAEA,mBADA,YACA,uBACA,mDACE,CADF,YACE,iDAEA,uCAEN,+DAOE,mBADF,sBACE,mBAGF,aACE,sCAIA,aADF,WACE,CAKF,SACE,CAHJ,kBAEE,CAJE,gBAEJ,CAHI,iBAMA,yFAKA,aACA,eACA,cCxaJ,iBAEE,aADA,iBACA,6BAEA,kCAEA,SACA,UAIA,gCACA,CALA,SAEA,SAEA,CAJA,wEAEA,CAFA,OAKA,CAGA,mDACE,iBAGF,gCACE,CADF,UACE,aAEJ,iCAEE,CAFF,UAEE,wCAEA,WACA,WADA,UACA,CACA,4CAGA,MACA,CADA,KACA,wCACA,UAGA,CAJA,UAIA,6DAUA,0CACE,CAFF,mBAEE,wEACA,CAVA,YACA,CAMF,mBAJE,OAOA,gBAJJ,gCACE,CANE,cACA,CAHA,oBACA,CAGA,QAGJ,CAII,0BACA,CADA,UACA,wCAEJ,kBACE,0DACA,gCACE,kBACA,CADA,YACA,oEACA,2CAMF,mDAII,CALN,YACE,CANE,cAKJ,CACE,iBAII,kEACA,yCACE,kDACA,yDACE,+CACA,uBANN,CAMM,+BANN,uCACE,qDACA,4BAEE,mBADA,0CACA,CADA,qBACA,0DACE,wCACA,sGALJ,oCACA,sBACE,kBAFF,UAEE,2CACA,wFACE,cACA,kEANN,uBACE,iDACA,CADA,UACA,0DACE,wDAEE,iEACA,qEANN,sCACE,CAGE,iBAHF,gBAGE,qBACE,CAJJ,uBACA,gDACE,wDACA,6DAHF,2CACA,CADA,gBACA,eACE,CAGE,sBANN,8BACE,CAII,iBAFF,4DACA,WACE,YADF,uCACE,6EACA,2BANN,8CACE,kDACA,0CACE,8BACA,yFACE,sBACA,sFALJ,mEACA,sBACE,kEACA,6EACE,uCACA,kEALJ,qGAEE,kEACA,6EACE,uCACA,kEALJ,8CACA,uDACE,sEACA,2EACE,sCACA,iEALJ,mGACA,qCACE,oDACA,0DACE,6GACA,gDAGR,yDCvEA,sEACE,CACA,6GACE,gEACF,iGAIF,wFACE,qDAGA,mGAEE,2CAEF,4FACE,gCACF,wGACE,8DAEE,6FAIA,iJAKN,6GACE,gDAKF,yDACA,qCAGA,6BACA,kBACA,qDAKA,oCAEA,+DAGA,2CAGE,oDAIA,oEAEE,qBAGJ,wDAEE,uCAEF,kEAGA,8CAEA,uDAIF,gEAIE,6BACA,gEAIA,+CACE,0EAIF,sDAEE,+DAGF,sCACA,8BACE,oCAEJ,wBACE,4FAEE,gBAEJ,yGAGI,kBAGJ,CCnHE,2MCFF,oBAGE,wGAKA,iCACE,CADF,wBACE,8GAQA,mBCjBJ,2GAIE,mBACA,6HAMA,YACE,mIAYF,eACA,CAHF,YAGE,4FAGE,8BAKF,uBAkBE,sCACA,CADA,qBAbA,wCAIA,CALF,8BACE,CADF,gBAKE,wCACA,CAOA,kDACA,CACA,kCAKF,6BAGA,4CACE,kDACA,eAGF,cACE,aACA,iBACA,yBACA,8BACA,WAGJ,2BACE,cAGA,+BACA,CAHA,eAGA,wCACA,YACA,iBACA,uEAGA,0BACA,2CAEA,8EAGI,qBACA,CAFF,kBAEE,4DAMJ,mCACE,4BAGA,oBAGF,4CACE,qCACA,8BACA,gBACA,+CAEA,iCAEF,iCACE,oBACA,4CACA,qCAGF,8BAEE,+BAEA,WAEA,8BACE,oBACA,CADA,gBACA,yBAKF,gBADF,YACE,CACA,iBACA,qDAEA,mDCvIJ,2FAMA,iCACE,CACA,eAEA,CAFA,mBADA,wBAIA,8BACA,gBADA,YACA,0BAEE,8CAGA,wDAIE,gFAGE,iBAEN,wCAKF,+CACE,CACA,oDAEF,kDAIE,YAEF,CAHE,YAGF,CCpCE,mFAFA,QACA,UAIA,CAHA,IAGA,gDAGE,eACA,iEAGF,wBAEE,mBAMA,6CAEF,CAJE,mBACA,CAGF,kCAGE,CARF,kBACE,CAHA,eAUA,YACA,mBACA,CAFA,UAEA,wCC/BJ,mBACE,CDkCE,wBACA,sBCpCJ,iBACE,mDACA,2CACA,sBAGA,qBCDA,6CAIE,CATJ,uBAKE,CDGE,oBACF,yDAEE,CCDE,2CAGF,CAJA,kCACE,CDJJ,aAKE,eCXJ,CDME,uBCOE,gCACE,YAEF,2CAEE,wBACA,0BAIF,iBAEA,cADF,UACE,uBAEA,iCAEA,wCAEA,6CAMA,CAYF,gCATI,4BASJ,CAZE,mCAEE,iCAUJ,4BAGE,4DADA,+BACA,CAHF,qBAGE,sCACE,OAEF,iBAHA,SAGA,iHACE,2DAKF,CANA,8EAMA,uSAEE,kBAEF,+FACE,yCCjEJ,WACA,yBAGA,uBACA,gBAEA,uCAIA,CAJA,iCAIA,uCAGA,UACE,gBACA,qBAEA,0CClBJ,gBACE,KAGF,qBACE,YAGF,CAHE,cAGF,gCAEE,mBACA,iEAEA,oCACA,wCAEA,sBACA,WAEA,CAFA,YAEA,8EAEA,mCAFA,iBAEA,6BAIA,wEAKA,sDAIE,CARF,mDAIA,CAIE,cAEF,8CAIA,oBAFE,iBAEF,8CAGE,eAEF,CAFE,YAEF,OAEE,kBAGJ,CAJI,eACA,CAFF,mBAKF,yCCjDE,oBACA,CAFA,iBAEA,uCAKE,iBACA,qCAGA,mBCZJ,CDWI,gBCXJ,6BAEE,eACA,sBAGA,eAEA,sBACA,oDACA,iGAMA,gBAFE,YAEF,8FAME,iJCnBF,YACA,gNAWE,gDAEF,iSAaE,kBACE,gHAKF,oCACE,eACF,CADE,UACF,8CACE,gDACF,wCACE,oBCtCJ,oBAEF,6BACE,QACE,kDAGF,yBACE,kDAmBA,kDAEF,CAhBA,+CAaA,CAbA,oBAaA,0FACE,CADF,gGAfF,cACE,gBACA,CAaA,0BAGA,mQACE,gBAGF,oMACE,iBACA,CAFF,eACE,CADF,gBAEE,aAGJ,iCAEE,CAFF,wCAEE,wBAUE,+VAIE,uEAHA,2BAGA,wXAKJ,iDAGF,CARM,+CACE,iDAIN,CALI,gBAQN,mHACE,gBAGF,2DACE,0EAOA,0EAGF,gBAEE,6DCjFA,kDACA,gCACA,qDAGA,qBACA,qDCDA,cACA,eAEA,yBAGF,sBAEE,iBACA,sNAWA,iBACE,kBACA,wRAgBA,kBAEA,iOAgBA,uCACE,uEAEA,kBAEF,qUAuBE,iDAIJ,CACA,geCzFF,4BAEE,CAQA,6JACA,iDAIA,sEAGA,mDAOF,iDAGE,4DAIA,8CACA,qDAEE,eAFF,cAEE,oBAEF,uBAFE,kCAGA,eACA,iBACA,mBAIA,mDACA,CAHA,uCAEA,CAJA,0CACA,CAIA,gBAJA,gBACA,oBADA,gBAIA,wBAEJ,gBAGE,6BACA,YAHA,iBAGA,gCACA,iEAEA,6CACA,sDACA,0BADA,wBACA,0BACA,oIAIA,mBAFA,YAEA,qBACA,0CAIE,uBAEF,CAHA,yBACE,CAEF,iDACE,mFAKJ,oCACE,CANE,aAKJ,CACE,qEAIA,YAFA,WAEA,CAHA,aACA,CAEA,gBACE,4BACA,sBADA,aACA,gCAMF,oCACA,yDACA,2CAEA,qBAGE,kBAEA,CACA,mCAIF,CARE,YACA,CAOF,iCAEE,CAPA,oBACA,CAQA,oBACE,uDAEJ,sDAGA,CAHA,cAGA,0BACE,oDAIA,oCACA,4BACA,sBAGA,cAEA,oFAGA,sBAEA,yDACE,CAIF,iBAJE,wBAIF,6CAHE,6CAKA,eACA,aACA,CADA,cACA,yCAGJ,kBACE,CAKA,iDAEA,CARF,aACE,4CAGA,kBAIA,wEAGA,wDAGA,kCAOA,iDAGA,CAPF,WAEE,sCAEA,CAJF,2CACE,CAMA,qCACA,+BARF,kBACE,qCAOA,iBAsBA,sBACE,CAvBF,WAKA,CACE,0DAIF,CALA,uDACE,CANF,sBAqBA,4CACA,CALA,gRAIA,YAEE,6CAEN,mCAEE,+CASA,6EAIA,4BChNA,SDmNA,qFCnNA,gDACA,sCAGA,qCACA,sDACA,CAKA,kDAGA,CARA,0CAQA,kBAGA,YACA,sBACA,iBAFA,gBADF,YACE,CAHA,SAKA,kBAEA,SAFA,iBAEA,uEAGA,CAEE,6CAFF,oCAgBI,CAdF,yBACE,qBACF,CAGF,oBACE,CAIF,WACE,CALA,2CAGA,uBACF,CACE,mFAGE,CALF,qBAEA,UAGE,gCAIF,sDAEA,CALE,oCAKF,yCC7CJ,oCACE,CD+CA,yXAQE,sCCrDJ,wCAGA,oCACE","sources":["webpack:///./node_modules/normalize.css/normalize.css","webpack:///./src/furo/assets/styles/base/_print.sass","webpack:///./src/furo/assets/styles/base/_screen-readers.sass","webpack:///./src/furo/assets/styles/base/_theme.sass","webpack:///./src/furo/assets/styles/variables/_fonts.scss","webpack:///./src/furo/assets/styles/variables/_spacing.scss","webpack:///./src/furo/assets/styles/variables/_icons.scss","webpack:///./src/furo/assets/styles/variables/_admonitions.scss","webpack:///./src/furo/assets/styles/variables/_colors.scss","webpack:///./src/furo/assets/styles/base/_typography.sass","webpack:///./src/furo/assets/styles/_scaffold.sass","webpack:///./src/furo/assets/styles/content/_admonitions.sass","webpack:///./src/furo/assets/styles/content/_api.sass","webpack:///./src/furo/assets/styles/content/_blocks.sass","webpack:///./src/furo/assets/styles/content/_captions.sass","webpack:///./src/furo/assets/styles/content/_code.sass","webpack:///./src/furo/assets/styles/content/_footnotes.sass","webpack:///./src/furo/assets/styles/content/_images.sass","webpack:///./src/furo/assets/styles/content/_indexes.sass","webpack:///./src/furo/assets/styles/content/_lists.sass","webpack:///./src/furo/assets/styles/content/_math.sass","webpack:///./src/furo/assets/styles/content/_misc.sass","webpack:///./src/furo/assets/styles/content/_rubrics.sass","webpack:///./src/furo/assets/styles/content/_sidebar.sass","webpack:///./src/furo/assets/styles/content/_tables.sass","webpack:///./src/furo/assets/styles/content/_target.sass","webpack:///./src/furo/assets/styles/content/_gui-labels.sass","webpack:///./src/furo/assets/styles/components/_footer.sass","webpack:///./src/furo/assets/styles/components/_sidebar.sass","webpack:///./src/furo/assets/styles/components/_table_of_contents.sass","webpack:///./src/furo/assets/styles/_shame.sass"],"sourcesContent":["/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n line-height: 1.15; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n/* Grouping content\n ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n box-sizing: content-box; /* 1 */\n height: 0; /* 1 */\n overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n border-bottom: none; /* 1 */\n text-decoration: underline; /* 2 */\n text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/* Embedded content\n ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n border-style: none;\n}\n\n/* Forms\n ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-size: 100%; /* 1 */\n line-height: 1.15; /* 1 */\n margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n border-style: none;\n padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n * `fieldset` elements in all browsers.\n */\n\nlegend {\n box-sizing: border-box; /* 1 */\n color: inherit; /* 2 */\n display: table; /* 1 */\n max-width: 100%; /* 1 */\n padding: 0; /* 3 */\n white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n box-sizing: border-box; /* 1 */\n padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/* Interactive\n ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n display: list-item;\n}\n\n/* Misc\n ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n display: none;\n}\n","// This file contains styles for managing print media.\n\n////////////////////////////////////////////////////////////////////////////////\n// Hide elements not relevant to print media.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n // Hide icon container.\n .content-icon-container\n display: none !important\n\n // Hide showing header links if hovering over when printing.\n .headerlink\n display: none !important\n\n // Hide mobile header.\n .mobile-header\n display: none !important\n\n // Hide navigation links.\n .related-pages\n display: none !important\n\n////////////////////////////////////////////////////////////////////////////////\n// Tweaks related to decolorization.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n // Apply a border around code which no longer have a color background.\n .highlight\n border: 0.1pt solid var(--color-foreground-border)\n\n////////////////////////////////////////////////////////////////////////////////\n// Avoid page break in some relevant cases.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n ul, ol, dl, a, table, pre, blockquote, p\n page-break-inside: avoid\n\n h1, h2, h3, h4, h5, h6, img, figure, caption\n page-break-inside: avoid\n page-break-after: avoid\n\n ul, ol, dl\n page-break-before: avoid\n",".visually-hidden\n position: absolute !important\n width: 1px !important\n height: 1px !important\n padding: 0 !important\n margin: -1px !important\n overflow: hidden !important\n clip: rect(0,0,0,0) !important\n white-space: nowrap !important\n border: 0 !important\n color: var(--color-foreground-primary)\n background: var(--color-background-primary)\n\n:-moz-focusring\n outline: auto\n","// This file serves as the \"skeleton\" of the theming logic.\n//\n// This contains the bulk of the logic for handling dark mode, color scheme\n// toggling and the handling of color-scheme-specific hiding of elements.\n\n@use \"../variables\" as *\n\nbody\n @include fonts\n @include spacing\n @include icons\n @include admonitions\n @include default-admonition(#651fff, \"abstract\")\n @include default-topic(#14B8A6, \"pencil\")\n\n @include colors\n\n.only-light\n display: block !important\nhtml body .only-dark\n display: none !important\n\n// Ignore dark-mode hints if print media.\n@media not print\n // Enable dark-mode, if requested.\n body[data-theme=\"dark\"]\n @include colors-dark\n\n html & .only-light\n display: none !important\n .only-dark\n display: block !important\n\n // Enable dark mode, unless explicitly told to avoid.\n @media (prefers-color-scheme: dark)\n body:not([data-theme=\"light\"])\n @include colors-dark\n\n html & .only-light\n display: none !important\n .only-dark\n display: block !important\n\n//\n// Theme toggle presentation\n//\nbody[data-theme=\"auto\"]\n .theme-toggle svg.theme-icon-when-auto-light\n display: block\n\n @media (prefers-color-scheme: dark)\n .theme-toggle svg.theme-icon-when-auto-dark\n display: block\n .theme-toggle svg.theme-icon-when-auto-light\n display: none\n\nbody[data-theme=\"dark\"]\n .theme-toggle svg.theme-icon-when-dark\n display: block\n\nbody[data-theme=\"light\"]\n .theme-toggle svg.theme-icon-when-light\n display: block\n","// Fonts used by this theme.\n//\n// There are basically two things here -- using the system font stack and\n// defining sizes for various elements in %ages. We could have also used `em`\n// but %age is easier to reason about for me.\n\n@mixin fonts {\n // These are adapted from https://systemfontstack.com/\n --font-stack:\n -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,\n Apple Color Emoji, Segoe UI Emoji;\n --font-stack--monospace:\n \"SFMono-Regular\", Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,\n monospace;\n --font-stack--headings: var(--font-stack);\n\n --font-size--normal: 100%;\n --font-size--small: 87.5%;\n --font-size--small--2: 81.25%;\n --font-size--small--3: 75%;\n --font-size--small--4: 62.5%;\n\n // Sidebar\n --sidebar-caption-font-size: var(--font-size--small--2);\n --sidebar-item-font-size: var(--font-size--small);\n --sidebar-search-input-font-size: var(--font-size--small);\n\n // Table of Contents\n --toc-font-size: var(--font-size--small--3);\n --toc-font-size--mobile: var(--font-size--normal);\n --toc-title-font-size: var(--font-size--small--4);\n\n // Admonitions\n //\n // These aren't defined in terms of %ages, since nesting these is permitted.\n --admonition-font-size: 0.8125rem;\n --admonition-title-font-size: 0.8125rem;\n\n // Code\n --code-font-size: var(--font-size--small--2);\n\n // API\n --api-font-size: var(--font-size--small);\n}\n","// Spacing for various elements on the page\n//\n// If the user wants to tweak things in a certain way, they are permitted to.\n// They also have to deal with the consequences though!\n\n@mixin spacing {\n // Header!\n --header-height: calc(\n var(--sidebar-item-line-height) + 4 *\n #{var(--sidebar-item-spacing-vertical)}\n );\n --header-padding: 0.5rem;\n\n // Sidebar\n --sidebar-tree-space-above: 1.5rem;\n --sidebar-caption-space-above: 1rem;\n\n --sidebar-item-line-height: 1rem;\n --sidebar-item-spacing-vertical: 0.5rem;\n --sidebar-item-spacing-horizontal: 1rem;\n --sidebar-item-height: calc(\n var(--sidebar-item-line-height) + 2 *#{var(--sidebar-item-spacing-vertical)}\n );\n\n --sidebar-expander-width: var(--sidebar-item-height); // be square\n\n --sidebar-search-space-above: 0.5rem;\n --sidebar-search-input-spacing-vertical: 0.5rem;\n --sidebar-search-input-spacing-horizontal: 0.5rem;\n --sidebar-search-input-height: 1rem;\n --sidebar-search-icon-size: var(--sidebar-search-input-height);\n\n // Table of Contents\n --toc-title-padding: 0.25rem 0;\n --toc-spacing-vertical: 1.5rem;\n --toc-spacing-horizontal: 1.5rem;\n --toc-item-spacing-vertical: 0.4rem;\n --toc-item-spacing-horizontal: 1rem;\n}\n","// Expose theme icons as CSS variables.\n\n$icons: (\n // Adapted from tabler-icons\n // url: https://tablericons.com/\n \"search\":\n url('data:image/svg+xml;charset=utf-8,'),\n // Factored out from mkdocs-material on 24-Aug-2020.\n // url: https://squidfunk.github.io/mkdocs-material/reference/admonitions/\n \"pencil\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"abstract\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"info\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"flame\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"question\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"warning\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"failure\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"spark\":\n url('data:image/svg+xml;charset=utf-8,')\n);\n\n@mixin icons {\n @each $name, $glyph in $icons {\n --icon-#{$name}: #{$glyph};\n }\n}\n","@use \"sass:list\";\n// Admonitions\n\n// Structure of these is:\n// admonition-class: color \"icon-name\";\n//\n// The colors are translated into CSS variables below. The icons are\n// used directly in the main declarations to set the `mask-image` in\n// the title.\n\n// prettier-ignore\n$admonitions: (\n // Each of these has an reST directives for it.\n \"caution\": #ff9100 \"spark\",\n \"warning\": #ff9100 \"warning\",\n \"danger\": #ff5252 \"spark\",\n \"attention\": #ff5252 \"warning\",\n \"error\": #ff5252 \"failure\",\n \"hint\": #00c852 \"question\",\n \"tip\": #00c852 \"info\",\n \"important\": #00bfa5 \"flame\",\n \"note\": #00b0ff \"pencil\",\n \"seealso\": #448aff \"info\",\n \"admonition-todo\": #808080 \"pencil\"\n);\n\n@mixin default-admonition($color, $icon-name) {\n --color-admonition-title: #{$color};\n --color-admonition-title-background: #{rgba($color, 0.2)};\n\n --icon-admonition-default: var(--icon-#{$icon-name});\n}\n\n@mixin default-topic($color, $icon-name) {\n --color-topic-title: #{$color};\n --color-topic-title-background: #{rgba($color, 0.2)};\n\n --icon-topic-default: var(--icon-#{$icon-name});\n}\n\n@mixin admonitions {\n @each $name, $values in $admonitions {\n --color-admonition-title--#{$name}: #{list.nth($values, 1)};\n --color-admonition-title-background--#{$name}: #{rgba(\n list.nth($values, 1),\n 0.2\n )};\n }\n}\n","// Colors used throughout this theme.\n//\n// The aim is to give the user more control. Thus, instead of hard-coding colors\n// in various parts of the stylesheet, the approach taken is to define all\n// colors as CSS variables and reusing them in all the places.\n//\n// `colors-dark` depends on `colors` being included at a lower specificity.\n\n@mixin colors {\n --color-problematic: #b30000;\n\n // Base Colors\n --color-foreground-primary: black; // for main text and headings\n --color-foreground-secondary: #5a5c63; // for secondary text\n --color-foreground-muted: #6b6f76; // for muted text\n --color-foreground-border: #878787; // for content borders\n\n --color-background-primary: white; // for content\n --color-background-secondary: #f8f9fb; // for navigation + ToC\n --color-background-hover: #efeff4ff; // for navigation-item hover\n --color-background-hover--transparent: #efeff400;\n --color-background-border: #eeebee; // for UI borders\n --color-background-item: #ccc; // for \"background\" items (eg: copybutton)\n\n // Announcements\n --color-announcement-background: #000000dd;\n --color-announcement-text: #eeebee;\n\n // Brand colors\n --color-brand-primary: #0a4bff;\n --color-brand-content: #2757dd;\n --color-brand-visited: #872ee0;\n\n // API documentation\n --color-api-background: var(--color-background-hover--transparent);\n --color-api-background-hover: var(--color-background-hover);\n --color-api-overall: var(--color-foreground-secondary);\n --color-api-name: var(--color-problematic);\n --color-api-pre-name: var(--color-problematic);\n --color-api-paren: var(--color-foreground-secondary);\n --color-api-keyword: var(--color-foreground-primary);\n\n --color-api-added: #21632c;\n --color-api-added-border: #38a84d;\n --color-api-changed: #046172;\n --color-api-changed-border: #06a1bc;\n --color-api-deprecated: #605706;\n --color-api-deprecated-border: #f0d90f;\n --color-api-removed: #b30000;\n --color-api-removed-border: #ff5c5c;\n\n --color-highlight-on-target: #ffffcc;\n\n // Inline code background\n --color-inline-code-background: var(--color-background-secondary);\n\n // Highlighted text (search)\n --color-highlighted-background: #ddeeff;\n --color-highlighted-text: var(--color-foreground-primary);\n\n // GUI Labels\n --color-guilabel-background: #ddeeff80;\n --color-guilabel-border: #bedaf580;\n --color-guilabel-text: var(--color-foreground-primary);\n\n // Admonitions!\n --color-admonition-background: transparent;\n\n //////////////////////////////////////////////////////////////////////////////\n // Everything below this should be one of:\n // - var(...)\n // - *-gradient(...)\n // - special literal values (eg: transparent, none)\n //////////////////////////////////////////////////////////////////////////////\n\n // Tables\n --color-table-header-background: var(--color-background-secondary);\n --color-table-border: var(--color-background-border);\n\n // Cards\n --color-card-border: var(--color-background-secondary);\n --color-card-background: transparent;\n --color-card-marginals-background: var(--color-background-secondary);\n\n // Header\n --color-header-background: var(--color-background-primary);\n --color-header-border: var(--color-background-border);\n --color-header-text: var(--color-foreground-primary);\n\n // Sidebar (left)\n --color-sidebar-background: var(--color-background-secondary);\n --color-sidebar-background-border: var(--color-background-border);\n\n --color-sidebar-brand-text: var(--color-foreground-primary);\n --color-sidebar-caption-text: var(--color-foreground-muted);\n --color-sidebar-link-text: var(--color-foreground-secondary);\n --color-sidebar-link-text--top-level: var(--color-brand-primary);\n\n --color-sidebar-item-background: var(--color-sidebar-background);\n --color-sidebar-item-background--current: var(\n --color-sidebar-item-background\n );\n --color-sidebar-item-background--hover: linear-gradient(\n 90deg,\n var(--color-background-hover--transparent) 0%,\n var(--color-background-hover) var(--sidebar-item-spacing-horizontal),\n var(--color-background-hover) 100%\n );\n\n --color-sidebar-item-expander-background: transparent;\n --color-sidebar-item-expander-background--hover: var(\n --color-background-hover\n );\n\n --color-sidebar-search-text: var(--color-foreground-primary);\n --color-sidebar-search-background: var(--color-background-secondary);\n --color-sidebar-search-background--focus: var(--color-background-primary);\n --color-sidebar-search-border: var(--color-background-border);\n --color-sidebar-search-icon: var(--color-foreground-muted);\n\n // Table of Contents (right)\n --color-toc-background: var(--color-background-primary);\n --color-toc-title-text: var(--color-foreground-muted);\n --color-toc-item-text: var(--color-foreground-secondary);\n --color-toc-item-text--hover: var(--color-foreground-primary);\n --color-toc-item-text--active: var(--color-brand-primary);\n\n // Actual page contents\n --color-content-foreground: var(--color-foreground-primary);\n --color-content-background: transparent;\n\n // Links\n --color-link: var(--color-brand-content);\n --color-link-underline: var(--color-background-border);\n --color-link--hover: var(--color-brand-content);\n --color-link-underline--hover: var(--color-foreground-border);\n\n --color-link--visited: var(--color-brand-visited);\n --color-link-underline--visited: var(--color-background-border);\n --color-link--visited--hover: var(--color-brand-visited);\n --color-link-underline--visited--hover: var(--color-foreground-border);\n}\n\n@mixin colors-dark {\n --color-problematic: #ee5151;\n\n // Base Colors\n --color-foreground-primary: #cfd0d0; // for main text and headings\n --color-foreground-secondary: #9ca0a5; // for secondary text\n --color-foreground-muted: #81868d; // for muted text\n --color-foreground-border: #666666; // for content borders\n\n --color-background-primary: #131416; // for content\n --color-background-secondary: #1a1c1e; // for navigation + ToC\n --color-background-hover: #1e2124ff; // for navigation-item hover\n --color-background-hover--transparent: #1e212400;\n --color-background-border: #303335; // for UI borders\n --color-background-item: #444; // for \"background\" items (eg: copybutton)\n\n // Announcements\n --color-announcement-background: #000000dd;\n --color-announcement-text: #eeebee;\n\n // Brand colors\n --color-brand-primary: #3d94ff;\n --color-brand-content: #5ca5ff;\n --color-brand-visited: #b27aeb;\n\n // Highlighted text (search)\n --color-highlighted-background: #083563;\n\n // GUI Labels\n --color-guilabel-background: #08356380;\n --color-guilabel-border: #13395f80;\n\n // API documentation\n --color-api-keyword: var(--color-foreground-secondary);\n --color-highlight-on-target: #333300;\n\n --color-api-added: #3db854;\n --color-api-added-border: #267334;\n --color-api-changed: #09b0ce;\n --color-api-changed-border: #056d80;\n --color-api-deprecated: #b1a10b;\n --color-api-deprecated-border: #6e6407;\n --color-api-removed: #ff7575;\n --color-api-removed-border: #b03b3b;\n\n // Admonitions\n --color-admonition-background: #18181a;\n\n // Cards\n --color-card-border: var(--color-background-secondary);\n --color-card-background: #18181a;\n --color-card-marginals-background: var(--color-background-hover);\n}\n","// This file contains the styling for making the content throughout the page,\n// including fonts, paragraphs, headings and spacing among these elements.\n\nbody\n font-family: var(--font-stack)\npre,\ncode,\nkbd,\nsamp\n font-family: var(--font-stack--monospace)\n\n// Make fonts look slightly nicer.\nbody\n -webkit-font-smoothing: antialiased\n -moz-osx-font-smoothing: grayscale\n\n// Line height from Bootstrap 4.1\narticle\n line-height: 1.5\n\n//\n// Headings\n//\nh1,\nh2,\nh3,\nh4,\nh5,\nh6\n line-height: 1.25\n font-family: var(--font-stack--headings)\n font-weight: bold\n\n border-radius: 0.5rem\n margin-top: 0.5rem\n margin-bottom: 0.5rem\n margin-left: -0.5rem\n margin-right: -0.5rem\n padding-left: 0.5rem\n padding-right: 0.5rem\n\n + p\n margin-top: 0\n\nh1\n font-size: 2.5em\n margin-top: 1.75rem\n margin-bottom: 1rem\nh2\n font-size: 2em\n margin-top: 1.75rem\nh3\n font-size: 1.5em\nh4\n font-size: 1.25em\nh5\n font-size: 1.125em\nh6\n font-size: 1em\n\nsmall\n opacity: 75%\n font-size: 80%\n\n// Paragraph\np\n margin-top: 0.5rem\n margin-bottom: 0.75rem\n\n// Horizontal rules\nhr.docutils\n height: 1px\n padding: 0\n margin: 2rem 0\n background-color: var(--color-background-border)\n border: 0\n\n.centered\n text-align: center\n\n// Links\na\n text-decoration: underline\n\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline)\n\n &:visited\n color: var(--color-link--visited)\n text-decoration-color: var(--color-link-underline--visited)\n &:hover\n color: var(--color-link--visited--hover)\n text-decoration-color: var(--color-link-underline--visited--hover)\n\n &:hover\n color: var(--color-link--hover)\n text-decoration-color: var(--color-link-underline--hover)\n &.muted-link\n color: inherit\n &:hover\n color: var(--color-link--hover)\n text-decoration-color: var(--color-link-underline--hover)\n &:visited\n color: var(--color-link--visited--hover)\n text-decoration-color: var(--color-link-underline--visited--hover)\n","// This file contains the styles for the overall layouting of the documentation\n// skeleton, including the responsive changes as well as sidebar toggles.\n//\n// This is implemented as a mobile-last design, which isn't ideal, but it is\n// reasonably good-enough and I got pretty tired by the time I'd finished this\n// to move the rules around to fix this. Shouldn't take more than 3-4 hours,\n// if you know what you're doing tho.\n\n// HACK: Not all browsers account for the scrollbar width in media queries.\n// This results in horizontal scrollbars in the breakpoint where we go\n// from displaying everything to hiding the ToC. We accomodate for this by\n// adding a bit of padding to the TOC drawer, disabling the horizontal\n// scrollbar and allowing the scrollbars to cover the padding.\n// https://www.456bereastreet.com/archive/201301/media_query_width_and_vertical_scrollbars/\n\n// HACK: Always having the scrollbar visible, prevents certain browsers from\n// causing the content to stutter horizontally between taller-than-viewport and\n// not-taller-than-viewport pages.\n@use \"variables\" as *\n\nhtml\n overflow-x: hidden\n overflow-y: scroll\n scroll-behavior: smooth\n\n.sidebar-scroll, .toc-scroll, article[role=main] *\n scrollbar-width: thin\n scrollbar-color: var(--color-foreground-border) transparent\n\n//\n// Overalls\n//\nhtml,\nbody\n height: 100%\n color: var(--color-foreground-primary)\n background: var(--color-background-primary)\n\n.skip-to-content\n position: fixed\n padding: 1rem\n border-radius: 1rem\n left: 0.25rem\n top: 0.25rem\n z-index: 40\n background: var(--color-background-primary)\n color: var(--color-foreground-primary)\n\n transform: translateY(-200%)\n transition: transform 300ms ease-in-out\n\n &:focus-within\n transform: translateY(0%)\n\narticle\n color: var(--color-content-foreground)\n background: var(--color-content-background)\n overflow-wrap: break-word\n\n.page\n display: flex\n // fill the viewport for pages with little content.\n min-height: 100%\n\n.mobile-header\n width: 100%\n height: var(--header-height)\n background-color: var(--color-header-background)\n color: var(--color-header-text)\n border-bottom: 1px solid var(--color-header-border)\n\n // Looks like sub-script/super-script have this, and we need this to\n // be \"on top\" of those.\n z-index: 10\n\n // We don't show the header on large screens.\n display: none\n\n // Add shadow when scrolled\n &.scrolled\n border-bottom: none\n box-shadow: 0 0 0.2rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2)\n\n .header-center\n a\n color: var(--color-header-text)\n text-decoration: none\n\n.main\n display: flex\n flex: 1\n\n// Sidebar (left) also covers the entire left portion of screen.\n.sidebar-drawer\n box-sizing: border-box\n\n border-right: 1px solid var(--color-sidebar-background-border)\n background: var(--color-sidebar-background)\n\n display: flex\n justify-content: flex-end\n // These next two lines took me two days to figure out.\n width: calc((100% - #{$full-width}) / 2 + #{$sidebar-width})\n min-width: $sidebar-width\n\n// Scroll-along sidebars\n.sidebar-container,\n.toc-drawer\n box-sizing: border-box\n width: $sidebar-width\n\n.toc-drawer\n background: var(--color-toc-background)\n // See HACK described on top of this document\n padding-right: 1rem\n\n.sidebar-sticky,\n.toc-sticky\n position: sticky\n top: 0\n height: min(100%, 100vh)\n height: 100vh\n\n display: flex\n flex-direction: column\n\n.sidebar-scroll,\n.toc-scroll\n flex-grow: 1\n flex-shrink: 1\n\n overflow: auto\n scroll-behavior: smooth\n\n// Central items.\n.content\n padding: 0 $content-padding\n width: $content-width\n\n display: flex\n flex-direction: column\n justify-content: space-between\n\n.icon\n display: inline-block\n height: 1rem\n width: 1rem\n svg\n width: 100%\n height: 100%\n\n//\n// Accommodate announcement banner\n//\n.announcement\n background-color: var(--color-announcement-background)\n color: var(--color-announcement-text)\n\n height: var(--header-height)\n display: flex\n align-items: center\n overflow-x: auto\n & + .page\n min-height: calc(100% - var(--header-height))\n\n.announcement-content\n box-sizing: border-box\n padding: 0.5rem\n min-width: 100%\n white-space: nowrap\n text-align: center\n\n a\n color: var(--color-announcement-text)\n text-decoration-color: var(--color-announcement-text)\n\n &:hover\n color: var(--color-announcement-text)\n text-decoration-color: var(--color-link--hover)\n\n////////////////////////////////////////////////////////////////////////////////\n// Toggles for theme\n////////////////////////////////////////////////////////////////////////////////\n.no-js .theme-toggle-container // don't show theme toggle if there's no JS\n display: none\n\n.theme-toggle-container\n display: flex\n\n.theme-toggle\n display: flex\n cursor: pointer\n border: none\n padding: 0\n background: transparent\n\n.theme-toggle svg\n height: 1.25rem\n width: 1.25rem\n color: var(--color-foreground-primary)\n display: none\n\n.theme-toggle-header\n display: flex\n align-items: center\n justify-content: center\n\n////////////////////////////////////////////////////////////////////////////////\n// Toggles for elements\n////////////////////////////////////////////////////////////////////////////////\n.toc-overlay-icon, .nav-overlay-icon\n display: none\n cursor: pointer\n\n .icon\n color: var(--color-foreground-secondary)\n height: 1.5rem\n width: 1.5rem\n\n.toc-header-icon, .nav-overlay-icon\n // for when we set display: flex\n justify-content: center\n align-items: center\n\n.toc-content-icon\n height: 1.5rem\n width: 1.5rem\n\n.content-icon-container\n float: right\n display: flex\n margin-top: 1.5rem\n margin-left: 1rem\n margin-bottom: 1rem\n gap: 0.5rem\n\n .edit-this-page, .view-this-page\n svg\n color: inherit\n height: 1.25rem\n width: 1.25rem\n\n.sidebar-toggle\n position: absolute\n display: none\n// \n.sidebar-toggle[name=\"__toc\"]\n left: 20px\n.sidebar-toggle:checked\n left: 40px\n// \n\n.overlay\n position: fixed\n top: 0\n width: 0\n height: 0\n\n transition: width 0ms, height 0ms, opacity 250ms ease-out\n\n opacity: 0\n background-color: rgba(0, 0, 0, 0.54)\n.sidebar-overlay\n z-index: 20\n.toc-overlay\n z-index: 40\n\n// Keep things on top and smooth.\n.sidebar-drawer\n z-index: 30\n transition: left 250ms ease-in-out\n.toc-drawer\n z-index: 50\n transition: right 250ms ease-in-out\n\n// Show the Sidebar\n#__navigation:checked\n & ~ .sidebar-overlay\n width: 100%\n height: 100%\n opacity: 1\n & ~ .page\n .sidebar-drawer\n top: 0\n left: 0\n // Show the toc sidebar\n#__toc:checked\n & ~ .toc-overlay\n width: 100%\n height: 100%\n opacity: 1\n & ~ .page\n .toc-drawer\n top: 0\n right: 0\n\n////////////////////////////////////////////////////////////////////////////////\n// Back to top\n////////////////////////////////////////////////////////////////////////////////\n.back-to-top\n text-decoration: none\n\n display: none\n position: fixed\n left: 0\n top: 1rem\n padding: 0.5rem\n padding-right: 0.75rem\n border-radius: 1rem\n font-size: 0.8125rem\n\n background: var(--color-background-primary)\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), #6b728080 0px 0px 1px 0px\n\n z-index: 10\n\n margin-left: 50%\n transform: translateX(-50%)\n svg\n height: 1rem\n width: 1rem\n fill: currentColor\n display: inline-block\n\n span\n margin-left: 0.25rem\n\n .show-back-to-top &\n display: flex\n align-items: center\n\n////////////////////////////////////////////////////////////////////////////////\n// Responsive layouting\n////////////////////////////////////////////////////////////////////////////////\n// Make things a bit bigger on bigger screens.\n@media (min-width: $full-width + $sidebar-width)\n html\n font-size: 110%\n\n@media (max-width: $full-width)\n // Collapse \"toc\" into the icon.\n .toc-content-icon\n display: flex\n .toc-drawer\n position: fixed\n height: 100vh\n top: 0\n right: -$sidebar-width\n border-left: 1px solid var(--color-background-muted)\n .toc-tree\n border-left: none\n font-size: var(--toc-font-size--mobile)\n\n // Accomodate for a changed content width.\n .sidebar-drawer\n width: calc((100% - #{$full-width - $sidebar-width}) / 2 + #{$sidebar-width})\n\n@media (max-width: $content-padded-width + $sidebar-width)\n // Center the page\n .content\n margin-left: auto\n margin-right: auto\n padding: 0 $content-padding--small\n\n@media (max-width: $content-padded-width--small + $sidebar-width)\n // Collapse \"navigation\".\n .nav-overlay-icon\n display: flex\n .sidebar-drawer\n position: fixed\n height: 100vh\n width: $sidebar-width\n\n top: 0\n left: -$sidebar-width\n\n // Swap which icon is visible.\n .toc-header-icon, .theme-toggle-header\n display: flex\n .toc-content-icon, .theme-toggle-content\n display: none\n\n // Show the header.\n .mobile-header\n position: sticky\n top: 0\n display: flex\n justify-content: space-between\n align-items: center\n\n .header-left,\n .header-right\n display: flex\n height: var(--header-height)\n padding: 0 var(--header-padding)\n label\n height: 100%\n width: 100%\n user-select: none\n\n .nav-overlay-icon .icon,\n .theme-toggle svg\n height: 1.5rem\n width: 1.5rem\n\n // Add a scroll margin for the content\n :target\n scroll-margin-top: calc(var(--header-height) + 2.5rem)\n\n // Show back-to-top below the header\n .back-to-top\n top: calc(var(--header-height) + 0.5rem)\n\n // Accommodate for the header.\n .page\n flex-direction: column\n justify-content: center\n\n@media (max-width: $content-width + 2* $content-padding--small)\n // Content should respect window limits.\n .content\n width: 100%\n overflow-x: auto\n\n@media (max-width: $content-width)\n article[role=main] aside.sidebar\n float: none\n width: 100%\n margin: 1rem 0\n","@use \"sass:list\"\n@use \"../variables\" as *\n\n// The design here is strongly inspired by mkdocs-material.\n.admonition, .topic\n margin: 1rem auto\n padding: 0 0.5rem 0.5rem 0.5rem\n\n background: var(--color-admonition-background)\n\n border-radius: 0.2rem\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n font-size: var(--admonition-font-size)\n\n overflow: hidden\n page-break-inside: avoid\n\n // First element should have no margin, since the title has it.\n > :nth-child(2)\n margin-top: 0\n\n // Last item should have no margin, since we'll control that w/ padding\n > :last-child\n margin-bottom: 0\n\n.admonition p.admonition-title,\np.topic-title\n position: relative\n margin: 0 -0.5rem 0.5rem\n padding-left: 2rem\n padding-right: .5rem\n padding-top: .4rem\n padding-bottom: .4rem\n\n font-weight: 500\n font-size: var(--admonition-title-font-size)\n line-height: 1.3\n\n // Our fancy icon\n &::before\n content: \"\"\n position: absolute\n left: 0.5rem\n width: 1rem\n height: 1rem\n\n// Default styles\np.admonition-title\n background-color: var(--color-admonition-title-background)\n &::before\n background-color: var(--color-admonition-title)\n mask-image: var(--icon-admonition-default)\n mask-repeat: no-repeat\n\np.topic-title\n background-color: var(--color-topic-title-background)\n &::before\n background-color: var(--color-topic-title)\n mask-image: var(--icon-topic-default)\n mask-repeat: no-repeat\n\n//\n// Variants\n//\n.admonition\n border-left: 0.2rem solid var(--color-admonition-title)\n\n @each $type, $value in $admonitions\n &.#{$type}\n border-left-color: var(--color-admonition-title--#{$type})\n > .admonition-title\n background-color: var(--color-admonition-title-background--#{$type})\n &::before\n background-color: var(--color-admonition-title--#{$type})\n mask-image: var(--icon-#{list.nth($value, 2)})\n\n.admonition-todo > .admonition-title\n text-transform: uppercase\n","// This file stylizes the API documentation (stuff generated by autodoc). It's\n// deeply nested due to how autodoc structures the HTML without enough classes\n// to select the relevant items.\n\n// API docs!\ndl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)\n // Tweak the spacing of all the things!\n dd\n margin-left: 2rem\n > :first-child\n margin-top: 0.125rem\n > :last-child\n margin-bottom: 0.75rem\n\n // This is used for the arguments\n .field-list\n margin-bottom: 0.75rem\n\n // \"Headings\" (like \"Parameters\" and \"Return\")\n > dt\n text-transform: uppercase\n font-size: var(--font-size--small)\n\n dd:empty\n margin-bottom: 0.5rem\n dd > ul\n margin-left: -1.2rem\n > li\n > p:nth-child(2)\n margin-top: 0\n // When the last-empty-paragraph follows a paragraph, it doesn't need\n // to augument the existing spacing.\n > p + p:last-child:empty\n margin-top: 0\n margin-bottom: 0\n\n // Colorize the elements\n > dt\n color: var(--color-api-overall)\n\n.sig:not(.sig-inline)\n font-weight: bold\n\n font-size: var(--api-font-size)\n font-family: var(--font-stack--monospace)\n\n margin-left: -0.25rem\n margin-right: -0.25rem\n padding-top: 0.25rem\n padding-bottom: 0.25rem\n padding-right: 0.5rem\n\n // These are intentionally em, to properly match the font size.\n padding-left: 3em\n text-indent: -2.5em\n\n border-radius: 0.25rem\n\n background: var(--color-api-background)\n transition: background 100ms ease-out\n\n &:hover\n background: var(--color-api-background-hover)\n\n // adjust the size of the [source] link on the right.\n a.reference\n .viewcode-link\n font-weight: normal\n width: 4.25rem\n\nem.property\n font-style: normal\n &:first-child\n color: var(--color-api-keyword)\n.sig-name\n color: var(--color-api-name)\n.sig-prename\n font-weight: normal\n color: var(--color-api-pre-name)\n.sig-paren\n color: var(--color-api-paren)\n.sig-param\n font-style: normal\n\ndiv.versionadded,\ndiv.versionchanged,\ndiv.deprecated,\ndiv.versionremoved\n border-left: 0.1875rem solid\n border-radius: 0.125rem\n\n padding-left: 0.75rem\n\n p\n margin-top: 0.125rem\n margin-bottom: 0.125rem\n\ndiv.versionadded\n border-color: var(--color-api-added-border)\n .versionmodified\n color: var(--color-api-added)\n\ndiv.versionchanged\n border-color: var(--color-api-changed-border)\n .versionmodified\n color: var(--color-api-changed)\n\ndiv.deprecated\n border-color: var(--color-api-deprecated-border)\n .versionmodified\n color: var(--color-api-deprecated)\n\ndiv.versionremoved\n border-color: var(--color-api-removed-border)\n .versionmodified\n color: var(--color-api-removed)\n\n// Align the [docs] and [source] to the right.\n.viewcode-link, .viewcode-back\n float: right\n text-align: right\n",".line-block\n margin-top: 0.5rem\n margin-bottom: 0.75rem\n .line-block\n margin-top: 0rem\n margin-bottom: 0rem\n padding-left: 1rem\n","// Captions\narticle p.caption,\ntable > caption,\n.code-block-caption\n font-size: var(--font-size--small)\n text-align: center\n\n// Caption above a TOCTree\n.toctree-wrapper.compound\n .caption, :not(.caption) > .caption-text\n font-size: var(--font-size--small)\n text-transform: uppercase\n\n text-align: initial\n margin-bottom: 0\n\n > ul\n margin-top: 0\n margin-bottom: 0\n","// Inline code\ncode.literal, .sig-inline\n background: var(--color-inline-code-background)\n border-radius: 0.2em\n // Make the font smaller, and use padding to recover.\n font-size: var(--font-size--small--2)\n padding: 0.1em 0.2em\n\n pre.literal-block &\n font-size: inherit\n padding: 0\n\n p &\n border: 1px solid var(--color-background-border)\n\n.sig-inline\n font-family: var(--font-stack--monospace)\n\n// Code and Literal Blocks\n$code-spacing-vertical: 0.625rem\n$code-spacing-horizontal: 0.875rem\n\n// Wraps every literal block + line numbers.\ndiv[class*=\" highlight-\"],\ndiv[class^=\"highlight-\"]\n margin: 1em 0\n display: flex\n\n .table-wrapper\n margin: 0\n padding: 0\n\npre\n margin: 0\n padding: 0\n overflow: auto\n\n // Needed to have more specificity than pygments' \"pre\" selector. :(\n article[role=\"main\"] .highlight &\n line-height: 1.5\n\n &.literal-block,\n .highlight &\n font-size: var(--code-font-size)\n padding: $code-spacing-vertical $code-spacing-horizontal\n\n // Make it look like all the other blocks.\n &.literal-block\n margin-top: 1rem\n margin-bottom: 1rem\n\n border-radius: 0.2rem\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n\n// All code is always contained in this.\n.highlight\n width: 100%\n border-radius: 0.2rem\n\n // Make line numbers and prompts un-selectable.\n .gp, span.linenos\n user-select: none\n pointer-events: none\n\n // Expand the line-highlighting.\n .hll\n display: block\n margin-left: -$code-spacing-horizontal\n margin-right: -$code-spacing-horizontal\n padding-left: $code-spacing-horizontal\n padding-right: $code-spacing-horizontal\n\n/* Make code block captions be nicely integrated */\n.code-block-caption\n display: flex\n padding: $code-spacing-vertical $code-spacing-horizontal\n\n border-radius: 0.25rem\n border-bottom-left-radius: 0\n border-bottom-right-radius: 0\n font-weight: 300\n border-bottom: 1px solid\n\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n border-color: var(--color-background-border)\n\n + div[class]\n margin-top: 0\n > .highlight\n border-top-left-radius: 0\n border-top-right-radius: 0\n\n// When `html_codeblock_linenos_style` is table.\n.highlighttable\n width: 100%\n display: block\n tbody\n display: block\n\n tr\n display: flex\n\n // Line numbers\n td.linenos\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n padding: $code-spacing-vertical $code-spacing-horizontal\n padding-right: 0\n border-top-left-radius: 0.2rem\n border-bottom-left-radius: 0.2rem\n\n .linenodiv\n padding-right: $code-spacing-horizontal\n font-size: var(--code-font-size)\n box-shadow: -0.0625rem 0 var(--color-foreground-border) inset\n\n // Actual code\n td.code\n padding: 0\n display: block\n flex: 1\n overflow: hidden\n\n .highlight\n border-top-left-radius: 0\n border-bottom-left-radius: 0\n\n// When `html_codeblock_linenos_style` is inline.\n.highlight\n span.linenos\n display: inline-block\n padding-left: 0\n padding-right: $code-spacing-horizontal\n margin-right: $code-spacing-horizontal\n box-shadow: -0.0625rem 0 var(--color-foreground-border) inset\n","// Inline Footnote Reference\n.footnote-reference\n font-size: var(--font-size--small--4)\n vertical-align: super\n\n// Definition list, listing the content of each note.\n// docutils <= 0.17\ndl.footnote.brackets\n font-size: var(--font-size--small)\n color: var(--color-foreground-secondary)\n\n display: grid\n grid-template-columns: max-content auto\n dt\n margin: 0\n > .fn-backref\n margin-left: 0.25rem\n\n &:after\n content: \":\"\n\n .brackets\n &:before\n content: \"[\"\n &:after\n content: \"]\"\n\n dd\n margin: 0\n padding: 0 1rem\n\n// docutils >= 0.18\naside.footnote\n font-size: var(--font-size--small)\n color: var(--color-foreground-secondary)\n\naside.footnote > span,\ndiv.citation > span\n float: left\n font-weight: 500\n padding-right: 0.25rem\n\naside.footnote > *:not(span),\ndiv.citation > p\n margin-left: 2rem\n","//\n// Figures\n//\nimg\n box-sizing: border-box\n max-width: 100%\n height: auto\n\narticle\n figure, .figure\n border-radius: 0.2rem\n\n margin: 0\n :last-child\n margin-bottom: 0\n\n .align-left\n float: left\n clear: left\n margin: 0 1rem 1rem\n\n .align-right\n float: right\n clear: right\n margin: 0 1rem 1rem\n\n .align-default,\n .align-center\n display: block\n text-align: center\n margin-left: auto\n margin-right: auto\n\n // WELL, table needs to be stylised like a table.\n table.align-default\n display: table\n text-align: initial\n",".genindex-jumpbox, .domainindex-jumpbox\n border-top: 1px solid var(--color-background-border)\n border-bottom: 1px solid var(--color-background-border)\n padding: 0.25rem\n\n.genindex-section, .domainindex-section\n h2\n margin-top: 0.75rem\n margin-bottom: 0.5rem\n ul\n margin-top: 0\n margin-bottom: 0\n","ul,\nol\n padding-left: 1.2rem\n\n // Space lists out like paragraphs\n margin-top: 1rem\n margin-bottom: 1rem\n // reduce margins within li.\n li\n > p:first-child\n margin-top: 0.25rem\n margin-bottom: 0.25rem\n\n > p:last-child\n margin-top: 0.25rem\n\n > ul,\n > ol\n margin-top: 0.5rem\n margin-bottom: 0.5rem\n\nol\n &.arabic\n list-style: decimal\n &.loweralpha\n list-style: lower-alpha\n &.upperalpha\n list-style: upper-alpha\n &.lowerroman\n list-style: lower-roman\n &.upperroman\n list-style: upper-roman\n\n// Don't space lists out when they're \"simple\" or in a `.. toctree::`\n.simple,\n.toctree-wrapper\n li\n > ul,\n > ol\n margin-top: 0\n margin-bottom: 0\n\n// Definition Lists\n.field-list,\n.option-list,\ndl:not([class]),\ndl.simple,\ndl.footnote,\ndl.glossary\n dt\n font-weight: 500\n margin-top: 0.25rem\n + dt\n margin-top: 0\n\n .classifier::before\n content: \":\"\n margin-left: 0.2rem\n margin-right: 0.2rem\n\n dd\n > p:first-child,\n ul\n margin-top: 0.125rem\n\n ul\n margin-bottom: 0.125rem\n",".math-wrapper\n width: 100%\n overflow-x: auto\n\ndiv.math\n position: relative\n text-align: center\n\n .headerlink,\n &:focus .headerlink\n display: none\n\n &:hover .headerlink\n display: inline-block\n\n span.eqno\n position: absolute\n right: 0.5rem\n top: 50%\n transform: translate(0, -50%)\n z-index: 1\n","// Abbreviations\nabbr[title]\n cursor: help\n\n// \"Problematic\" content, as identified by Sphinx\n.problematic\n color: var(--color-problematic)\n\n// Keyboard / Mouse \"instructions\"\nkbd:not(.compound)\n margin: 0 0.2rem\n padding: 0 0.2rem\n border-radius: 0.2rem\n border: 1px solid var(--color-foreground-border)\n color: var(--color-foreground-primary)\n vertical-align: text-bottom\n\n font-size: var(--font-size--small--3)\n display: inline-block\n\n box-shadow: 0 0.0625rem 0 rgba(0, 0, 0, 0.2), inset 0 0 0 0.125rem var(--color-background-primary)\n\n background-color: var(--color-background-secondary)\n\n// Blockquote\nblockquote\n border-left: 4px solid var(--color-background-border)\n background: var(--color-background-secondary)\n\n margin-left: 0\n margin-right: 0\n padding: 0.5rem 1rem\n\n .attribution\n font-weight: 600\n text-align: right\n\n &.pull-quote,\n &.highlights\n font-size: 1.25em\n\n &.epigraph,\n &.pull-quote\n border-left-width: 0\n border-radius: 0.5rem\n\n &.highlights\n border-left-width: 0\n background: transparent\n\n// Center align embedded-in-text images\np .reference img\n vertical-align: middle\n","p.rubric\n line-height: 1.25\n font-weight: bold\n font-size: 1.125em\n\n // For Numpy-style documentation that's got rubrics within it.\n // https://github.com/pradyunsg/furo/discussions/505\n dd &\n line-height: inherit\n font-weight: inherit\n\n font-size: var(--font-size--small)\n text-transform: uppercase\n","article .sidebar\n float: right\n clear: right\n width: 30%\n\n margin-left: 1rem\n margin-right: 0\n\n border-radius: 0.2rem\n background-color: var(--color-background-secondary)\n border: var(--color-background-border) 1px solid\n\n > *\n padding-left: 1rem\n padding-right: 1rem\n\n > ul, > ol // lists need additional padding, because bullets.\n padding-left: 2.2rem\n\n .sidebar-title\n margin: 0\n padding: 0.5rem 1rem\n border-bottom: var(--color-background-border) 1px solid\n\n font-weight: 500\n\n// TODO: subtitle\n// TODO: dedicated variables?\n","[role=main] .table-wrapper.container\n width: 100%\n overflow-x: auto\n margin-top: 1rem\n margin-bottom: 0.5rem\n padding: 0.2rem 0.2rem 0.75rem\n\ntable.docutils\n border-radius: 0.2rem\n border-spacing: 0\n border-collapse: collapse\n\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n th\n background: var(--color-table-header-background)\n\n td,\n th\n // Space things out properly\n padding: 0 0.25rem\n\n // Get the borders looking just-right.\n border-left: 1px solid var(--color-table-border)\n border-right: 1px solid var(--color-table-border)\n border-bottom: 1px solid var(--color-table-border)\n\n p\n margin: 0.25rem\n\n &:first-child\n border-left: none\n &:last-child\n border-right: none\n\n // MyST-parser tables set these classes for control of column alignment\n &.text-left\n text-align: left\n &.text-right\n text-align: right\n &.text-center\n text-align: center\n","@use \"../variables\" as *\n\n:target\n scroll-margin-top: 2.5rem\n\n@media (max-width: $full-width - $sidebar-width)\n :target\n scroll-margin-top: calc(2.5rem + var(--header-height))\n\n // When a heading is selected\n section > span:target\n scroll-margin-top: calc(2.8rem + var(--header-height))\n\n// Permalinks\n.headerlink\n font-weight: 100\n user-select: none\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\ndl dt,\np.caption,\nfigcaption p,\ntable > caption,\n.code-block-caption\n > .headerlink\n margin-left: 0.5rem\n visibility: hidden\n &:hover > .headerlink\n visibility: visible\n\n // Don't change to link-like, if someone adds the contents directive.\n > .toc-backref\n color: inherit\n text-decoration-line: none\n\n// Figure and table captions are special.\nfigure:hover > figcaption > p > .headerlink,\ntable:hover > caption > .headerlink\n visibility: visible\n\n:target >, // Regular section[id] style anchors\nspan:target ~ // Non-regular span[id] style \"extra\" anchors\n h1,\n h2,\n h3,\n h4,\n h5,\n h6\n &:nth-of-type(1)\n background-color: var(--color-highlight-on-target)\n // .headerlink\n // visibility: visible\n code.literal\n background-color: transparent\n\ntable:target > caption,\nfigure:target\n background-color: var(--color-highlight-on-target)\n\n// Inline page contents\n.this-will-duplicate-information-and-it-is-still-useful-here li :target\n background-color: var(--color-highlight-on-target)\n\n// Code block permalinks\n.literal-block-wrapper:target .code-block-caption\n background-color: var(--color-highlight-on-target)\n\n// When a definition list item is selected\n//\n// There isn't really an alternative to !important here, due to the\n// high-specificity of API documentation's selector.\ndt:target\n background-color: var(--color-highlight-on-target) !important\n\n// When a footnote reference is selected\n.footnote > dt:target + dd,\n.footnote-reference:target\n background-color: var(--color-highlight-on-target)\n",".guilabel\n background-color: var(--color-guilabel-background)\n border: 1px solid var(--color-guilabel-border)\n color: var(--color-guilabel-text)\n\n padding: 0 0.3em\n border-radius: 0.5em\n font-size: 0.9em\n","// This file contains the styles used for stylizing the footer that's shown\n// below the content.\n@use \"../variables\" as *\n\nfooter\n font-size: var(--font-size--small)\n display: flex\n flex-direction: column\n\n margin-top: 2rem\n\n// Bottom of page information\n.bottom-of-page\n display: flex\n align-items: center\n justify-content: space-between\n\n margin-top: 1rem\n padding-top: 1rem\n padding-bottom: 1rem\n\n color: var(--color-foreground-secondary)\n border-top: 1px solid var(--color-background-border)\n\n line-height: 1.5\n\n @media (max-width: $content-width)\n text-align: center\n flex-direction: column-reverse\n gap: 0.25rem\n\n .left-details\n font-size: var(--font-size--small)\n\n .right-details\n display: flex\n flex-direction: column\n gap: 0.25rem\n text-align: right\n\n .icons\n display: flex\n justify-content: flex-end\n gap: 0.25rem\n font-size: 1rem\n\n a\n text-decoration: none\n\n svg,\n img\n font-size: 1.125rem\n height: 1em\n width: 1em\n\n// Next/Prev page information\n.related-pages\n a\n display: flex\n align-items: center\n\n text-decoration: none\n &:hover .page-info .title\n text-decoration: underline\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline)\n\n svg.furo-related-icon,\n svg.furo-related-icon > use\n flex-shrink: 0\n\n color: var(--color-foreground-border)\n\n width: 0.75rem\n height: 0.75rem\n margin: 0 0.5rem\n\n &.next-page\n max-width: 50%\n\n float: right\n clear: right\n text-align: right\n\n &.prev-page\n max-width: 50%\n\n float: left\n clear: left\n\n svg\n transform: rotate(180deg)\n\n.page-info\n display: flex\n flex-direction: column\n overflow-wrap: anywhere\n\n .next-page &\n align-items: flex-end\n\n .context\n display: flex\n align-items: center\n\n padding-bottom: 0.1rem\n\n color: var(--color-foreground-muted)\n font-size: var(--font-size--small)\n text-decoration: none\n","// This file contains the styles for the contents of the left sidebar, which\n// contains the navigation tree, logo, search etc.\n\n////////////////////////////////////////////////////////////////////////////////\n// Brand on top of the scrollable tree.\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-brand\n display: flex\n flex-direction: column\n flex-shrink: 0\n\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n text-decoration: none\n\n.sidebar-brand-text\n color: var(--color-sidebar-brand-text)\n overflow-wrap: break-word\n margin: var(--sidebar-item-spacing-vertical) 0\n font-size: 1.5rem\n\n.sidebar-logo-container\n margin: var(--sidebar-item-spacing-vertical) 0\n\n.sidebar-logo\n margin: 0 auto\n display: block\n max-width: 100%\n\n////////////////////////////////////////////////////////////////////////////////\n// Search\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-search-container\n display: flex\n align-items: center\n margin-top: var(--sidebar-search-space-above)\n\n position: relative\n\n background: var(--color-sidebar-search-background)\n &:hover,\n &:focus-within\n background: var(--color-sidebar-search-background--focus)\n\n &::before\n content: \"\"\n position: absolute\n left: var(--sidebar-item-spacing-horizontal)\n width: var(--sidebar-search-icon-size)\n height: var(--sidebar-search-icon-size)\n\n background-color: var(--color-sidebar-search-icon)\n mask-image: var(--icon-search)\n\n.sidebar-search\n box-sizing: border-box\n\n border: none\n border-top: 1px solid var(--color-sidebar-search-border)\n border-bottom: 1px solid var(--color-sidebar-search-border)\n\n padding-top: var(--sidebar-search-input-spacing-vertical)\n padding-bottom: var(--sidebar-search-input-spacing-vertical)\n padding-right: var(--sidebar-search-input-spacing-horizontal)\n padding-left: calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size))\n\n width: 100%\n\n color: var(--color-sidebar-search-foreground)\n background: transparent\n z-index: 10\n\n &:focus\n outline: none\n\n &::placeholder\n font-size: var(--sidebar-search-input-font-size)\n\n//\n// Hide Search Matches link\n//\n#searchbox .highlight-link\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0\n margin: 0\n text-align: center\n\n a\n color: var(--color-sidebar-search-icon)\n font-size: var(--font-size--small--2)\n\n////////////////////////////////////////////////////////////////////////////////\n// Structure/Skeleton of the navigation tree (left)\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-tree\n font-size: var(--sidebar-item-font-size)\n margin-top: var(--sidebar-tree-space-above)\n margin-bottom: var(--sidebar-item-spacing-vertical)\n\n ul\n padding: 0\n margin-top: 0\n margin-bottom: 0\n\n display: flex\n flex-direction: column\n\n list-style: none\n\n li\n position: relative\n margin: 0\n\n > ul\n margin-left: var(--sidebar-item-spacing-horizontal)\n\n .icon\n color: var(--color-sidebar-link-text)\n\n .reference\n box-sizing: border-box\n color: var(--color-sidebar-link-text)\n\n // Fill the parent.\n display: inline-block\n line-height: var(--sidebar-item-line-height)\n text-decoration: none\n\n // Don't allow long words to cause wrapping.\n overflow-wrap: anywhere\n\n height: 100%\n width: 100%\n\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n\n &:hover\n color: var(--color-sidebar-link-text)\n background: var(--color-sidebar-item-background--hover)\n\n // Add a nice little \"external-link\" arrow here.\n &.external::after\n content: url('data:image/svg+xml,')\n margin: 0 0.25rem\n vertical-align: middle\n color: var(--color-sidebar-link-text)\n\n // Make the current page reference bold.\n .current-page > .reference\n font-weight: bold\n\n label\n position: absolute\n top: 0\n right: 0\n height: var(--sidebar-item-height)\n width: var(--sidebar-expander-width)\n\n cursor: pointer\n user-select: none\n\n display: flex\n justify-content: center\n align-items: center\n\n .caption, :not(.caption) > .caption-text\n font-size: var(--sidebar-caption-font-size)\n color: var(--color-sidebar-caption-text)\n\n font-weight: bold\n text-transform: uppercase\n\n margin: var(--sidebar-caption-space-above) 0 0 0\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n\n // If it has children, add a bit more padding to wrap the content to avoid\n // overlapping with the