Nix-Gui has two components: the backend and the frontend.
The backend (nixui/options
) reads information from two sources:
- NixOS (
<nixpkgs>
and<nixos>
) - and the configuration path being edited.
It then converts the read information to data-structures useful for navigation, search, and editing. The backend maintains in-memory state for all changes and commits those changes to disk by writing to the configuration path.
The frontend (nixui/graphics
) constructs various widgets comprising Nix-Gui and loads them with data from the backend. The frontend doesn’t manage any edit state. All “edit” actions performed in the GUI send a request-for-update to the backend.
The backend handles three major tasks: configuration retrieval to initialize data-structures, in-memory configuration mutation, and configuration committing. It provides APIs for frontend interaction, but doesn’t concern itself with how the frontend renders any of its data, similar to most websites wherein the webpage defines the Javascript determining how the backend will be queried, and the HTML / CSS determining how it will be rendered to the user.
Nix-Gui reads information from the configuration path and <nixos>
in order to construct a data-structure (nixui.options.option_tree.OptionTree
) mapping options and attributes to metadata (nixui.options.option_tree.OptionData
) including the attributes type and definition:
_type
: A static python-object representation of the option type (NixType
). Informs the frontend of howFieldWidget
should be generated and input checking should be performed.system_default_definition
: Provided by<nixpkgs>
, the default value if nothing is set in the configuration path.configured_definition
: Provided by the configuration path, overridessystem_default_definition
.in_memory_definition
: Undefined when the data-structure is loaded, can be mutated by the user and later committed to disk.
nixui/nix/lib.nix
(invoked by nixui.options.nix_eval
) contains the function get_all_nixos_options()
which is used to retrieve OptionData
including system_default_definition
and _type
for all options from <nixos>
.
nixui/options/parser.py
contains the function get_all_option_values(module_path)
which
- retrieves all
attributePath = string representation of nix expression;
pairs in the file by parsing thenixui.options.syntax_tree.SyntaxTree
constructed usingnix_dump_syntax_tree_json
(a wrapper for rnix-parser) - resolves the path of all
imports
and recurses
The result of get_all_option_values(module_path)
is used to set OptionData.configured_definition
where applicable.
Attributes in the OptionTree
can either be schema-defined or user-defined.
These attributes are defined in the NixOS config schema, so we call them “schema-defined” attributes.
In the <nixos>
source code, schema-defined attributes are specified by mkOption
, for example:
hardware.bluetooth.powerOnBoot = mkOption {
type = types.bool;
default = true;
description = "Whether to power up the default Bluetooth controller on boot.";
};
For types.attrsOf
options, users can specify the name of the attribute being defined, such as the names of users, the paths to files, and systemd service names. For example in <nixos>
source code, the schema-defined attribute environment.etc
provides the ability to set the User-Defined Attribute environment.etc."resolv.conf"= in =configuration.nix
.
nixos/modules/system/etc/etc.nix
:
environment.etc = mkOption {
default = {};
type = with types; attrsOf (submodule (
{ name, config, options, ... }:
{ options = {
text = mkOption {
default = null;
type = types.nullOr types.lines;
};
...
configuration.nix
:
environment.etc."resolv.conf".text = "text here";
types.listOf
options are also part of the OptionTree
despite not having a proper attribute path, e.g. a list of allowed TCP ports in the firewall will have the element
networking.firewall.allowedTCPPorts[0]
This is not the same as
networking.firewall.allowedTCPPorts[0] = <expression>; # illegal syntax in nix
rather, it is equivalent to
networking.firewall.allowedTCPPorts = [<expression> ...];
Nix expression strings are immediately converted to nixui.options.option_definition.OptionDefinition
Python objects. The OptionDefinition
class provides methods to get the python representation of an expression string. E.g. an attribute set is converted to a dict, =”true”= is converted to True
, =”5.424”= is converted to float(5.424)
).
OptionDefinition
’s also have the _type
property, which returns the NixType
the current definition is compatible with.
Additionally OptionDefinition
’s can be used to convert Python objects to nix expressions, which is useful for the “Commit Changes to Disk” section below.
Once constructed, the backend can perform four mutation operations:
- replacing an old
in_memory_definition
(OptionDefinition
) with a new one - adding a new attribute with a new
OptionDefinition
- renaming an attribute
- removing an attribute
OptionTree.in_memory_diff
contains a cache of all state changes represented simply as a mapping between attributes and their new OptionDefinition
(or None
if deleted).
All updates to the OptionTree
coming from the frontend pass through nixui.state_model.StateModel
, which is a layer on top of the OptionTree
with Update
’s integrated. Each mutating method results in an Update
(an object containing information necessary to revert a change) being appended to StateModel.update_history
.
The StateModel
’s mutating methods include
record_update
: Update thein_memory_definition
of an attribute in theOptionTree
rename_option
: Generally used to rename a submodule, e.g.filesystems."/".foo
-> =”filesystems.”/boot”.foo=add_new_option
: Generally used to add an attribute or element to asubmodule
orlist of
.undo
: Revert the latestUpdate
inupdate_history
The StateModel
also provides the method persist_changes
, a pass-through function which
- Calls
OptionTree.get_changes()
, which retrieves a mapping of attributes to new definitions for which thein_memory_diff
andconfigured_definition
differ. - calls
parser.calculate_changed_module
, which extracts theexpression_string
from each changedOptionDefinition
and calls one of threeSyntaxTree
-modifying functions (parser.add_definition()
,parser.apply_replace_definition()
, orparser.apply_remove_definition()
ifNone
). Each of these three functions calladd_comment
with distinct parameters to note changes by Nix-Gui. Each of these three functions apply modifications to the syntax tree using methods provided bySyntaxTree
.
We are left with a new module string which will be written to module_path
.
The frontend renders a graphical tool for changing configurations. A primer on frontend functionality can be found in Usage#Interface.
The Nav Interface (nixui.graphics.nav_interface.OptionNavigationInterface
) is the main widget. It contains a layout with place-holders for three widgets:
- Navbar: View and update the URI.
- Navlist: A list of attribute paths which, if clicked, updates the URI.
- Options Editor: A container for a list of
FieldWidgets
which contains option/attribute metadata and editing widgets.
Each time the URI changes, the Nav Interface creates a new instance of each widget, replacing the old instance.
When loading a new URI,
- A Navbar is instantiated which displays the new URIs
- A Navlist is instantiated which displays the children of the attribute path, or search results. Selects the navlist item if the URI instructs to.
- An Options Editor is instantiated which either is blank or shows a list of Option Displays an item in the navlist is selected.
There are currently two types of URIs, config:option.path.here
and search:search text here
.
The Navbar displays the URI and has four widgets, each of which results in a callback telling the Nav Interface to change the URI:
- Up Arrow: Change the URI from
config:foo.bar.baz
toconfig:foo.bar
. (disabled forsearch:anything
and top levelconfig:
) - Back Arrow: Change the URI to the previous URI.
- URI Box: Shows a pretty format of the URI, allow for direct editing of the URI when clicked.
- Search Box: Change URI to
search:<entered text>
.
The Navlist displays navigable options based on the URI. If the URI is config:parent.option.path
, the navlist will display each option which is a member of the set parent.option.path
. If the URI is search:<search string>
, the navlist will display each option matching the search.
If a Navlist item is clicked, the Nav Interface will load the clicked items URI.
There are a variety of Navlist types defined in nixui.graphics.navlist
:
StaticAttrsOf
: immutable listing of attributes of the URIs config path.DynamicAttrsOf
: mutable list of attributes. Useful forattribute set of <t>
type attributes.DynamicListOf
: mutable list elements, shown for alist of <t>
type attributes.SearchResultListDisplay
: immutable list of search results including details about why it matched the search. Searches are matched based onAttribute Path
,Type
, andDescription
.
The Options Editor is comprised of an Option Display Group (nixui.graphics.option_display_group.OptionDisplayGroupBox
), a QGroupBox
containing one or many Option Displays (nixui.graphics.option_display.GenericOptionDisplay
).
An Option Display is a tool for editing the value of a single option or attribute. The current value and option/attribute type impact how it is rendered.
A Field Widget (nixui.graphics.field_widgets
) is the component of an Option Display which allows the user to edit the value of an option/attribute.
There are a variety of Field Widgets, and types of functionality for Field Widgets:
- Standard Field Widget: allows changes to
OptionDefinition.obj
which will be converted to a nix expression - Expression Field Widget: allows changes to the nix expression itself (
OptionDefinition.expression_string
) - Reference Field Widget: (NOT IMPLEMENTED) allows users to refer to a package, option, or other variable in scope. This is a more constrained form of the Expression Field Widget and allows users to reference variables more easily.
- Redirect Field Widget: For
ListOf
andAttrsOf
, changes the URI so the navlist is the editor for the elements / set members for the list / set.