├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── build-aux
└── dist-vendor.sh
├── data
├── dev.vlinkz.NixosConfEditor.desktop.in.in
├── dev.vlinkz.NixosConfEditor.gschema.xml.in
├── dev.vlinkz.NixosConfEditor.metainfo.xml.in.in
├── dev.vlinkz.NixosConfEditor.policy.in.in
├── icons
│ ├── dev.vlinkz.NixosConfEditor-symbolic.svg
│ ├── dev.vlinkz.NixosConfEditor.Devel.svg
│ ├── dev.vlinkz.NixosConfEditor.svg
│ ├── dev.vlinkz.NixosConfEditor.template.svg
│ └── meson.build
├── meson.build
└── screenshots
│ ├── invaliddark.png
│ ├── invalidlight.png
│ ├── listviewdark.png
│ ├── listviewlight.png
│ ├── multiwindowdark.png
│ ├── multiwindowlight.png
│ ├── optiondark.png
│ ├── optiondark2.png
│ ├── optionlight.png
│ ├── optionlight2.png
│ ├── rebuilddark.png
│ ├── rebuildlight.png
│ ├── searchdark.png
│ └── searchlight.png
├── default.nix
├── flake.lock
├── flake.nix
├── meson.build
├── meson_options.txt
├── nce-helper
├── .gitignore
├── Cargo.lock
├── Cargo.toml
└── src
│ ├── main.rs
│ └── meson.build
├── packages
└── nixos-conf-editor
│ └── default.nix
├── po
├── LINGUAS
├── POTFILES.in
└── meson.build
├── shells
└── default
│ └── default.nix
└── src
├── config.rs.in
├── lib.rs
├── main.rs
├── meson.build
├── parse
├── config.rs
├── mod.rs
├── options.rs
└── preferences.rs
└── ui
├── about.rs
├── mod.rs
├── nameentry.rs
├── optionpage.rs
├── preferencespage.rs
├── quitdialog.rs
├── rebuild.rs
├── savechecking.rs
├── searchentry.rs
├── searchfactory.rs
├── searchpage.rs
├── treefactory.rs
├── welcome.rs
├── window.rs
└── windowloading.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | *.json
3 | *.br
4 | crf
5 | .vscode
6 | shell.nix
7 | /build
8 | TODO
9 | /src/config.rs
10 | /result
11 | result
12 | /.direnv
13 | .envrc
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nixos-conf-editor"
3 | version = "0.1.2"
4 | edition = "2021"
5 | default-run = "nixos-conf-editor"
6 |
7 | [dependencies]
8 | relm4 = { version = "0.5.1", features = ["libadwaita"] }
9 | relm4-components = { package = "relm4-components", version = "0.5.1"}
10 | adw = { package = "libadwaita", version = "0.2", features = ["v1_2", "gtk_v4_6"] }
11 | gtk = { package = "gtk4", version = "0.5", features = ["v4_6"] }
12 | sourceview5 = { version = "0.5", features = ["v5_4"] }
13 | vte = { package = "vte4", version = "0.5" }
14 | tracker = "0.2"
15 | tokio = { version = "1.24", features = ["rt", "macros", "time", "rt-multi-thread", "sync"] }
16 |
17 | serde_json = "1.0"
18 | serde = { version = "1.0", features = ["derive"] }
19 | ijson = "0.1"
20 |
21 | nix-editor = "0.3.0"
22 | nix-data = "0.0.3"
23 |
24 | anyhow = "1.0"
25 |
26 | html2pango = "0.5"
27 | pandoc = "0.8"
28 |
29 | log = "0.4"
30 | pretty_env_logger = "0.4"
31 | gettext-rs = { version = "0.7", features = ["gettext-system"] }
32 |
33 | [workspace]
34 | members = [".", "nce-helper"]
35 | default-members = [".", "nce-helper"]
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 | NixOS Configuration Editor
6 | ===
7 |
8 | [![Built with Nix][builtwithnix badge]][builtwithnix]
9 | [![License: GPLv3][GPLv3 badge]][GPLv3]
10 | [![Chat on Matrix][matrix badge]][matrix]
11 | [![Chat on Discord][discord badge]][discord]
12 |
13 | A simple NixOS configuration editor application built with [libadwaita](https://gitlab.gnome.org/GNOME/libadwaita), [GTK4](https://www.gtk.org/), and [Relm4](https://relm4.org/). The goal of this project is to provide a simple graphical tool for modifying and managing desktop NixOS configurations.
14 |
15 |

16 |

17 |
18 |
19 |
20 | ## NixOS Flakes Installation
21 | `flake.nix`
22 | ```nix
23 | {
24 | inputs = {
25 | # other inputs
26 | nixos-conf-editor.url = "github:snowfallorg/nixos-conf-editor";
27 | # rest of flake.nix
28 | ```
29 |
30 | `configuration.nix`
31 | ```nix
32 | environment.systemPackages = with pkgs; [
33 | inputs.nixos-conf-editor.packages.${system}.nixos-conf-editor
34 | # rest of your packages
35 | ];
36 | ```
37 |
38 | ## NixOS Installation
39 |
40 | Head of `configuration.nix`
41 |
42 | if you are on unstable channel or any version after 22.11:
43 | ```nix
44 | { config, pkgs, lib, ... }:
45 | let
46 | nixos-conf-editor = import (pkgs.fetchFromGitHub {
47 | owner = "snowfallorg";
48 | repo = "nixos-conf-editor";
49 | rev = "0.1.2";
50 | sha256 = "sha256-/ktLbmF1pU3vFHeGooDYswJipNE2YINm0WpF9Wd1gw8=";
51 | }) {};
52 | in
53 | ```
54 | Packages:
55 |
56 | ```nix
57 | environment.systemPackages =
58 | with pkgs; [
59 | nixos-conf-editor
60 | # rest of your packages
61 | ];
62 | ```
63 | For any other method of installation, when rebuilding you will be prompted to authenticate twice in a row
64 |
65 | ## 'nix profile' installation
66 | ```bash
67 | nix profile install github:snowfallorg/nixos-conf-editor
68 | ```
69 |
70 | ## 'nix-env' Installation
71 |
72 | ```bash
73 | git clone https://github.com/snowfallorg/nixos-conf-editor
74 | nix-env -f nixos-conf-editor -i nixos-conf-editor
75 | ```
76 |
77 | ## Single run on an flakes enabled system:
78 | ```bash
79 | nix run github:snowfallorg/nixos-conf-editor
80 | ```
81 |
82 | ## Single run on non-flakes enabled system:
83 | ```bash
84 | nix --extra-experimental-features "nix-command flakes" run github:snowfallorg/nixos-conf-editor
85 | ```
86 |
87 | ## Debugging
88 |
89 | ```bash
90 | RUST_LOG=nixos_conf_editor=trace nixos-conf-editor
91 | ```
92 |
93 | # Screenshots
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ## Licenses
121 |
122 | The icons in [data/icons](data/icons/) contains assets from the [NixOS logo](https://github.com/NixOS/nixos-artwork/tree/master/logo) and are licensed under a [CC-BY license](https://creativecommons.org/licenses/by/4.0/).
123 |
124 | [builtwithnix badge]: https://img.shields.io/badge/Built%20With-Nix-41439A?style=for-the-badge&logo=nixos&logoColor=white
125 | [builtwithnix]: https://builtwithnix.org/
126 | [GPLv3 badge]: https://img.shields.io/badge/License-GPLv3-blue.svg?style=for-the-badge
127 | [GPLv3]: https://opensource.org/licenses/GPL-3.0
128 | [matrix badge]: https://img.shields.io/badge/matrix-join%20chat-0cbc8c?style=for-the-badge&logo=matrix&logoColor=white
129 | [matrix]: https://matrix.to/#/#snowflakeos:matrix.org
130 | [discord badge]: https://img.shields.io/discord/1021080090676842506?color=7289da&label=Discord&logo=discord&logoColor=ffffff&style=for-the-badge
131 | [discord]: https://discord.gg/6rWNMmdkgT
132 |
--------------------------------------------------------------------------------
/build-aux/dist-vendor.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | export SOURCE_ROOT="$1"
3 | export DIST="$2"
4 |
5 | cd "$SOURCE_ROOT"
6 | mkdir "$DIST"/.cargo
7 | cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config
8 | # Move vendor into dist tarball directory
9 | mv vendor "$DIST"
10 |
--------------------------------------------------------------------------------
/data/dev.vlinkz.NixosConfEditor.desktop.in.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Configuration Editor
3 | Comment=Configure your NixOS system
4 | Type=Application
5 | Exec=nixos-conf-editor
6 | Terminal=false
7 | Categories=Settings;System;Utility;
8 | # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
9 | Keywords=Nix;Nixos;nix;nixos;configuration;settings;registry;
10 | # Translators: Do NOT translate or transliterate this text (this is an icon file name)!
11 | Icon=@icon@
12 | StartupNotify=true
13 |
--------------------------------------------------------------------------------
/data/dev.vlinkz.NixosConfEditor.gschema.xml.in:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/dev.vlinkz.NixosConfEditor.metainfo.xml.in.in:
--------------------------------------------------------------------------------
1 |
2 |
3 | @app-id@
4 | CC0-1.0
5 | GPL-3.0-or-later
6 | NixOS Configuration Editor
7 | A simple application to manage your NixOS system configuration.
8 |
9 | A simple graphical tool for modifying and managing desktop NixOS configurations built with libadwaita, GTK4, and Relm4.
10 |
11 |
12 |
13 | https://raw.githubusercontent.com/vlinkz/nixos-conf-editor/main/data/screenshots/listviewlight.png
14 | Main window
15 |
16 |
17 | https://raw.githubusercontent.com/vlinkz/nixos-conf-editor/main/data/screenshots/optionlight2.png
18 | Option Selection
19 |
20 |
21 | https://github.com/vlinkz/nixos-conf-editor
22 | https://github.com/vlinkz/nixos-conf-editor/issues
23 | Victor Fuentes
24 | vmfuentes64@gmail.com
25 | @gettext-package@
26 | @app-id@.desktop
27 |
28 |
--------------------------------------------------------------------------------
/data/dev.vlinkz.NixosConfEditor.policy.in.in:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | Victor Fuentes
6 | https://github.com/vlinkz
7 |
8 | Give NixOS Configuration Editor root access
9 | Authentication is required modify the NixOS system configuration
10 |
11 | no
12 | no
13 | auth_admin_keep
14 |
15 | @pkglibexecdir@/nce-helper
16 |
17 |
18 |
--------------------------------------------------------------------------------
/data/icons/dev.vlinkz.NixosConfEditor-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
27 |
--------------------------------------------------------------------------------
/data/icons/dev.vlinkz.NixosConfEditor.Devel.svg:
--------------------------------------------------------------------------------
1 |
2 |
98 |
--------------------------------------------------------------------------------
/data/icons/dev.vlinkz.NixosConfEditor.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/data/icons/meson.build:
--------------------------------------------------------------------------------
1 | install_data(
2 | '@0@.svg'.format(application_id),
3 | install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps'
4 | )
5 |
6 | install_data(
7 | '@0@-symbolic.svg'.format(base_id),
8 | install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps',
9 | rename: '@0@-symbolic.svg'.format(application_id)
10 | )
11 |
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | subdir('icons')
2 | # Desktop file
3 | desktop_conf = configuration_data()
4 | desktop_conf.set('icon', application_id)
5 | desktop_file = i18n.merge_file(
6 | type: 'desktop',
7 | input: configure_file(
8 | input: '@0@.desktop.in.in'.format(base_id),
9 | output: '@BASENAME@',
10 | configuration: desktop_conf
11 | ),
12 | output: '@0@.desktop'.format(application_id),
13 | po_dir: podir,
14 | install: true,
15 | install_dir: datadir / 'applications'
16 | )
17 | # Validate Desktop file
18 | if desktop_file_validate.found()
19 | test(
20 | 'validate-desktop',
21 | desktop_file_validate,
22 | args: [
23 | desktop_file.full_path()
24 | ],
25 | depends: desktop_file,
26 | )
27 | endif
28 |
29 | # Appdata
30 | appdata_conf = configuration_data()
31 | appdata_conf.set('app-id', application_id)
32 | appdata_conf.set('gettext-package', gettext_package)
33 | appdata_file = i18n.merge_file(
34 | input: configure_file(
35 | input: '@0@.metainfo.xml.in.in'.format(base_id),
36 | output: '@BASENAME@',
37 | configuration: appdata_conf
38 | ),
39 | output: '@0@.metainfo.xml'.format(application_id),
40 | po_dir: podir,
41 | install: true,
42 | install_dir: datadir / 'metainfo'
43 | )
44 |
45 | # Validate Appdata
46 | if appstream_util.found()
47 | test(
48 | 'validate-appdata', appstream_util,
49 | args: [
50 | 'validate', '--nonet', appdata_file.full_path()
51 | ],
52 | depends: appdata_file,
53 | )
54 | endif
55 |
56 | # Policy file
57 | dataconf = configuration_data()
58 | dataconf.set('pkglibexecdir',
59 | libexecdir
60 | )
61 | i18n.merge_file(
62 | input : configure_file(
63 | configuration: dataconf,
64 | input : '@0@.policy.in.in'.format(base_id),
65 | output: '@BASENAME@'
66 | ),
67 | output: '@0@.policy'.format(base_id),
68 | po_dir: podir,
69 | install: true,
70 | install_dir: datadir / 'polkit-1' / 'actions'
71 | )
72 |
73 | # GSchema
74 | gschema_conf = configuration_data()
75 | gschema_conf.set('app-id', application_id)
76 | gschema_conf.set('gettext-package', gettext_package)
77 | configure_file(
78 | input: '@0@.gschema.xml.in'.format(base_id),
79 | output: '@0@.gschema.xml'.format(application_id),
80 | configuration: gschema_conf,
81 | install: true,
82 | install_dir: datadir / 'glib-2.0' / 'schemas'
83 | )
84 |
85 | # Validate GSchema
86 | if glib_compile_schemas.found()
87 | test(
88 | 'validate-gschema', glib_compile_schemas,
89 | args: [
90 | '--strict', '--dry-run', meson.current_build_dir()
91 | ],
92 | )
93 | endif
94 |
--------------------------------------------------------------------------------
/data/screenshots/invaliddark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/invaliddark.png
--------------------------------------------------------------------------------
/data/screenshots/invalidlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/invalidlight.png
--------------------------------------------------------------------------------
/data/screenshots/listviewdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/listviewdark.png
--------------------------------------------------------------------------------
/data/screenshots/listviewlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/listviewlight.png
--------------------------------------------------------------------------------
/data/screenshots/multiwindowdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/multiwindowdark.png
--------------------------------------------------------------------------------
/data/screenshots/multiwindowlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/multiwindowlight.png
--------------------------------------------------------------------------------
/data/screenshots/optiondark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/optiondark.png
--------------------------------------------------------------------------------
/data/screenshots/optiondark2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/optiondark2.png
--------------------------------------------------------------------------------
/data/screenshots/optionlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/optionlight.png
--------------------------------------------------------------------------------
/data/screenshots/optionlight2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/optionlight2.png
--------------------------------------------------------------------------------
/data/screenshots/rebuilddark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/rebuilddark.png
--------------------------------------------------------------------------------
/data/screenshots/rebuildlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/rebuildlight.png
--------------------------------------------------------------------------------
/data/screenshots/searchdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/searchdark.png
--------------------------------------------------------------------------------
/data/screenshots/searchlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/data/screenshots/searchlight.png
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | (import
2 | (
3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
4 | fetchTarball {
5 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
6 | sha256 = lock.nodes.flake-compat.locked.narHash;
7 | }
8 | )
9 | { src = ./.; }
10 | ).defaultNix
11 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-compat": {
4 | "locked": {
5 | "lastModified": 1696426674,
6 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
7 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
8 | "revCount": 57,
9 | "type": "tarball",
10 | "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
11 | },
12 | "original": {
13 | "type": "tarball",
14 | "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
15 | }
16 | },
17 | "flake-compat_2": {
18 | "flake": false,
19 | "locked": {
20 | "lastModified": 1650374568,
21 | "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
22 | "owner": "edolstra",
23 | "repo": "flake-compat",
24 | "rev": "b4a34015c698c7793d592d66adbab377907a2be8",
25 | "type": "github"
26 | },
27 | "original": {
28 | "owner": "edolstra",
29 | "repo": "flake-compat",
30 | "type": "github"
31 | }
32 | },
33 | "flake-utils": {
34 | "inputs": {
35 | "systems": "systems"
36 | },
37 | "locked": {
38 | "lastModified": 1694529238,
39 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
40 | "owner": "numtide",
41 | "repo": "flake-utils",
42 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
43 | "type": "github"
44 | },
45 | "original": {
46 | "owner": "numtide",
47 | "repo": "flake-utils",
48 | "type": "github"
49 | }
50 | },
51 | "flake-utils-plus": {
52 | "inputs": {
53 | "flake-utils": "flake-utils"
54 | },
55 | "locked": {
56 | "lastModified": 1696331477,
57 | "narHash": "sha256-YkbRa/1wQWdWkVJ01JvV+75KIdM37UErqKgTf0L54Fk=",
58 | "owner": "gytis-ivaskevicius",
59 | "repo": "flake-utils-plus",
60 | "rev": "bfc53579db89de750b25b0c5e7af299e0c06d7d3",
61 | "type": "github"
62 | },
63 | "original": {
64 | "owner": "gytis-ivaskevicius",
65 | "repo": "flake-utils-plus",
66 | "type": "github"
67 | }
68 | },
69 | "nixpkgs": {
70 | "locked": {
71 | "lastModified": 1698318101,
72 | "narHash": "sha256-gUihHt3yPD7bVqg+k/UVHgngyaJ3DMEBchbymBMvK1E=",
73 | "owner": "nixos",
74 | "repo": "nixpkgs",
75 | "rev": "63678e9f3d3afecfeafa0acead6239cdb447574c",
76 | "type": "github"
77 | },
78 | "original": {
79 | "owner": "nixos",
80 | "ref": "nixos-unstable",
81 | "repo": "nixpkgs",
82 | "type": "github"
83 | }
84 | },
85 | "root": {
86 | "inputs": {
87 | "flake-compat": "flake-compat",
88 | "nixpkgs": "nixpkgs",
89 | "snowfall-lib": "snowfall-lib"
90 | }
91 | },
92 | "snowfall-lib": {
93 | "inputs": {
94 | "flake-compat": "flake-compat_2",
95 | "flake-utils-plus": "flake-utils-plus",
96 | "nixpkgs": [
97 | "nixpkgs"
98 | ]
99 | },
100 | "locked": {
101 | "lastModified": 1696432959,
102 | "narHash": "sha256-oJQZv2MYyJaVyVJY5IeevzqpGvMGKu5pZcCCJvb+xjc=",
103 | "owner": "snowfallorg",
104 | "repo": "lib",
105 | "rev": "92803a029b5314d4436a8d9311d8707b71d9f0b6",
106 | "type": "github"
107 | },
108 | "original": {
109 | "owner": "snowfallorg",
110 | "repo": "lib",
111 | "type": "github"
112 | }
113 | },
114 | "systems": {
115 | "locked": {
116 | "lastModified": 1681028828,
117 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
118 | "owner": "nix-systems",
119 | "repo": "default",
120 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
121 | "type": "github"
122 | },
123 | "original": {
124 | "owner": "nix-systems",
125 | "repo": "default",
126 | "type": "github"
127 | }
128 | }
129 | },
130 | "root": "root",
131 | "version": 7
132 | }
133 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | inputs = {
3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
4 | snowfall-lib = {
5 | url = "github:snowfallorg/lib";
6 | inputs.nixpkgs.follows = "nixpkgs";
7 | };
8 | flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
9 | };
10 |
11 | outputs = inputs:
12 | inputs.snowfall-lib.mkFlake {
13 | inherit inputs;
14 | alias.packages.default = "nixos-conf-editor";
15 | src = ./.;
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project(
2 | 'nixos-conf-editor',
3 | 'rust',
4 | version: '0.1.2',
5 | meson_version: '>= 0.59',
6 | license: 'GPL-3.0',
7 | )
8 |
9 | i18n = import('i18n')
10 | gnome = import('gnome')
11 |
12 | base_id = 'dev.vlinkz.NixosConfEditor'
13 |
14 | dependency('openssl', version: '>= 1.0')
15 | dependency('glib-2.0', version: '>= 2.66')
16 | dependency('gio-2.0', version: '>= 2.66')
17 | dependency('gtk4', version: '>= 4.6.0')
18 | dependency('libadwaita-1', version: '>=1.2.0')
19 | dependency('polkit-gobject-1', version: '>= 0.103')
20 |
21 | glib_compile_resources = find_program('glib-compile-resources', required: true)
22 | glib_compile_schemas = find_program('glib-compile-schemas', required: true)
23 | desktop_file_validate = find_program('desktop-file-validate', required: false)
24 | appstream_util = find_program('appstream-util', required: false)
25 | cargo = find_program('cargo', required: true)
26 |
27 | version = meson.project_version()
28 |
29 | prefix = get_option('prefix')
30 | bindir = prefix / get_option('bindir')
31 | libexecdir = prefix / get_option('libexecdir')
32 | localedir = prefix / get_option('localedir')
33 |
34 | datadir = prefix / get_option('datadir')
35 | pkgdatadir = datadir / meson.project_name()
36 | iconsdir = datadir / 'icons'
37 | podir = meson.project_source_root() / 'po'
38 | gettext_package = meson.project_name()
39 |
40 | if get_option('profile') == 'development'
41 | profile = 'Devel'
42 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip()
43 | if vcs_tag == ''
44 | version_suffix = '-devel'
45 | else
46 | version_suffix = '-@0@'.format(vcs_tag)
47 | endif
48 | application_id = '@0@.@1@'.format(base_id, profile)
49 | else
50 | profile = ''
51 | version_suffix = ''
52 | application_id = base_id
53 | endif
54 |
55 | meson.add_dist_script(
56 | 'build-aux/dist-vendor.sh',
57 | meson.project_build_root() / 'meson-dist' / meson.project_name() + '-' + version,
58 | meson.project_source_root()
59 | )
60 |
61 | subdir('data')
62 | subdir('po')
63 | subdir('src')
64 | subdir('nce-helper/src')
65 |
66 | gnome.post_install(
67 | gtk_update_icon_cache: true,
68 | glib_compile_schemas: true,
69 | update_desktop_database: true,
70 | )
71 |
--------------------------------------------------------------------------------
/meson_options.txt:
--------------------------------------------------------------------------------
1 | option(
2 | 'profile',
3 | type: 'combo',
4 | choices: [
5 | 'default',
6 | 'development'
7 | ],
8 | value: 'default',
9 | )
10 |
--------------------------------------------------------------------------------
/nce-helper/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/nce-helper/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "atty"
7 | version = "0.2.14"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
10 | dependencies = [
11 | "hermit-abi",
12 | "libc",
13 | "winapi",
14 | ]
15 |
16 | [[package]]
17 | name = "autocfg"
18 | version = "1.1.0"
19 | source = "registry+https://github.com/rust-lang/crates.io-index"
20 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
21 |
22 | [[package]]
23 | name = "bitflags"
24 | version = "1.3.2"
25 | source = "registry+https://github.com/rust-lang/crates.io-index"
26 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
27 |
28 | [[package]]
29 | name = "cfg-if"
30 | version = "1.0.0"
31 | source = "registry+https://github.com/rust-lang/crates.io-index"
32 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
33 |
34 | [[package]]
35 | name = "clap"
36 | version = "3.1.18"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
39 | dependencies = [
40 | "atty",
41 | "bitflags",
42 | "clap_derive",
43 | "clap_lex",
44 | "indexmap",
45 | "lazy_static",
46 | "strsim",
47 | "termcolor",
48 | "textwrap",
49 | ]
50 |
51 | [[package]]
52 | name = "clap_derive"
53 | version = "3.1.18"
54 | source = "registry+https://github.com/rust-lang/crates.io-index"
55 | checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
56 | dependencies = [
57 | "heck",
58 | "proc-macro-error",
59 | "proc-macro2",
60 | "quote",
61 | "syn",
62 | ]
63 |
64 | [[package]]
65 | name = "clap_lex"
66 | version = "0.2.0"
67 | source = "registry+https://github.com/rust-lang/crates.io-index"
68 | checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
69 | dependencies = [
70 | "os_str_bytes",
71 | ]
72 |
73 | [[package]]
74 | name = "hashbrown"
75 | version = "0.11.2"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
78 |
79 | [[package]]
80 | name = "heck"
81 | version = "0.4.0"
82 | source = "registry+https://github.com/rust-lang/crates.io-index"
83 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
84 |
85 | [[package]]
86 | name = "hermit-abi"
87 | version = "0.1.19"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
90 | dependencies = [
91 | "libc",
92 | ]
93 |
94 | [[package]]
95 | name = "indexmap"
96 | version = "1.8.2"
97 | source = "registry+https://github.com/rust-lang/crates.io-index"
98 | checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
99 | dependencies = [
100 | "autocfg",
101 | "hashbrown",
102 | ]
103 |
104 | [[package]]
105 | name = "lazy_static"
106 | version = "1.4.0"
107 | source = "registry+https://github.com/rust-lang/crates.io-index"
108 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
109 |
110 | [[package]]
111 | name = "libc"
112 | version = "0.2.126"
113 | source = "registry+https://github.com/rust-lang/crates.io-index"
114 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
115 |
116 | [[package]]
117 | name = "log"
118 | version = "0.4.17"
119 | source = "registry+https://github.com/rust-lang/crates.io-index"
120 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
121 | dependencies = [
122 | "cfg-if",
123 | ]
124 |
125 | [[package]]
126 | name = "nixos-conf-editor-helper"
127 | version = "0.1.0"
128 | dependencies = [
129 | "clap",
130 | "users",
131 | ]
132 |
133 | [[package]]
134 | name = "os_str_bytes"
135 | version = "6.1.0"
136 | source = "registry+https://github.com/rust-lang/crates.io-index"
137 | checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
138 |
139 | [[package]]
140 | name = "proc-macro-error"
141 | version = "1.0.4"
142 | source = "registry+https://github.com/rust-lang/crates.io-index"
143 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
144 | dependencies = [
145 | "proc-macro-error-attr",
146 | "proc-macro2",
147 | "quote",
148 | "syn",
149 | "version_check",
150 | ]
151 |
152 | [[package]]
153 | name = "proc-macro-error-attr"
154 | version = "1.0.4"
155 | source = "registry+https://github.com/rust-lang/crates.io-index"
156 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
157 | dependencies = [
158 | "proc-macro2",
159 | "quote",
160 | "version_check",
161 | ]
162 |
163 | [[package]]
164 | name = "proc-macro2"
165 | version = "1.0.39"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
168 | dependencies = [
169 | "unicode-ident",
170 | ]
171 |
172 | [[package]]
173 | name = "quote"
174 | version = "1.0.18"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
177 | dependencies = [
178 | "proc-macro2",
179 | ]
180 |
181 | [[package]]
182 | name = "strsim"
183 | version = "0.10.0"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
186 |
187 | [[package]]
188 | name = "syn"
189 | version = "1.0.95"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
192 | dependencies = [
193 | "proc-macro2",
194 | "quote",
195 | "unicode-ident",
196 | ]
197 |
198 | [[package]]
199 | name = "termcolor"
200 | version = "1.1.3"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
203 | dependencies = [
204 | "winapi-util",
205 | ]
206 |
207 | [[package]]
208 | name = "textwrap"
209 | version = "0.15.0"
210 | source = "registry+https://github.com/rust-lang/crates.io-index"
211 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
212 |
213 | [[package]]
214 | name = "unicode-ident"
215 | version = "1.0.0"
216 | source = "registry+https://github.com/rust-lang/crates.io-index"
217 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
218 |
219 | [[package]]
220 | name = "users"
221 | version = "0.11.0"
222 | source = "registry+https://github.com/rust-lang/crates.io-index"
223 | checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
224 | dependencies = [
225 | "libc",
226 | "log",
227 | ]
228 |
229 | [[package]]
230 | name = "version_check"
231 | version = "0.9.4"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
234 |
235 | [[package]]
236 | name = "winapi"
237 | version = "0.3.9"
238 | source = "registry+https://github.com/rust-lang/crates.io-index"
239 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
240 | dependencies = [
241 | "winapi-i686-pc-windows-gnu",
242 | "winapi-x86_64-pc-windows-gnu",
243 | ]
244 |
245 | [[package]]
246 | name = "winapi-i686-pc-windows-gnu"
247 | version = "0.4.0"
248 | source = "registry+https://github.com/rust-lang/crates.io-index"
249 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
250 |
251 | [[package]]
252 | name = "winapi-util"
253 | version = "0.1.5"
254 | source = "registry+https://github.com/rust-lang/crates.io-index"
255 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
256 | dependencies = [
257 | "winapi",
258 | ]
259 |
260 | [[package]]
261 | name = "winapi-x86_64-pc-windows-gnu"
262 | version = "0.4.0"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
265 |
--------------------------------------------------------------------------------
/nce-helper/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nce-helper"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | clap = { version = "4.1", features = ["derive"] }
8 | users = "0.11"
9 |
10 | [[bin]]
11 | name = "nce-helper"
12 | path = "src/main.rs"
13 |
--------------------------------------------------------------------------------
/nce-helper/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::{self, FromArgMatches, Subcommand};
2 | use std::{
3 | error::Error,
4 | fs::File,
5 | io::{self, Read, Write},
6 | process::Command,
7 | };
8 |
9 | #[derive(Subcommand, Debug)]
10 | enum SubCommands {
11 | Config {
12 | /// Write stdin to file in path output
13 | #[arg(short, long)]
14 | output: String,
15 | },
16 | Rebuild {
17 | /// Run `nixos-rebuild` with the given arguments
18 | arguments: Vec,
19 | },
20 | WriteRebuild {
21 | /// Content to write to file
22 | #[arg(short, long)]
23 | content: String,
24 | /// Write config to file in path output
25 | #[arg(short, long)]
26 | path: String,
27 | /// Run `nixos-rebuild` with the given arguments
28 | arguments: Vec,
29 | },
30 | }
31 |
32 | fn main() {
33 | let cli = SubCommands::augment_subcommands(clap::Command::new(
34 | "Helper binary for NixOS Configuration Editor",
35 | ));
36 | let matches = cli.get_matches();
37 | let derived_subcommands = SubCommands::from_arg_matches(&matches)
38 | .map_err(|err| err.exit())
39 | .unwrap();
40 |
41 | if users::get_effective_uid() != 0 {
42 | eprintln!("nixos-conf-editor-helper must be run as root");
43 | std::process::exit(1);
44 | }
45 |
46 | match derived_subcommands {
47 | SubCommands::Config { output } => {
48 | match write_file(&output) {
49 | Ok(_) => (),
50 | Err(err) => {
51 | eprintln!("{}", err);
52 | std::process::exit(1);
53 | }
54 | };
55 | }
56 | SubCommands::Rebuild { arguments } => match rebuild(arguments) {
57 | Ok(_) => (),
58 | Err(err) => {
59 | eprintln!("{}", err);
60 | std::process::exit(1);
61 | }
62 | },
63 | SubCommands::WriteRebuild {
64 | content,
65 | path,
66 | arguments,
67 | } => {
68 | match write_content(&content, &path) {
69 | Ok(_) => (),
70 | Err(err) => {
71 | eprintln!("{}", err);
72 | std::process::exit(1);
73 | }
74 | };
75 | match rebuild(arguments) {
76 | Ok(_) => (),
77 | Err(err) => {
78 | eprintln!("{}", err);
79 | std::process::exit(1);
80 | }
81 | };
82 | }
83 | }
84 | }
85 |
86 | fn write_file(path: &str) -> Result<(), Box> {
87 | let stdin = io::stdin();
88 | let mut buf = String::new();
89 | stdin.lock().read_to_string(&mut buf)?;
90 | let mut file = File::create(path)?;
91 | write!(file, "{}", &buf)?;
92 | Ok(())
93 | }
94 |
95 | fn write_content(content: &str, path: &str) -> Result<(), Box> {
96 | let mut file = File::create(path)?;
97 | write!(file, "{}", content)?;
98 | Ok(())
99 | }
100 |
101 | fn rebuild(args: Vec) -> Result<(), Box> {
102 | let mut cmd = Command::new("nixos-rebuild").args(args).spawn()?;
103 | let x = cmd.wait()?;
104 | if x.success() {
105 | Ok(())
106 | } else {
107 | eprintln!("nixos-rebuild failed with exit code {}", x.code().unwrap());
108 | std::process::exit(1);
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/nce-helper/src/meson.build:
--------------------------------------------------------------------------------
1 | global_conf = configuration_data()
2 | cargo_options = [ '--manifest-path', meson.project_source_root() / 'nce-helper' / 'Cargo.toml' ]
3 | cargo_options += [ '--target-dir', meson.project_build_root() / 'nce-helper' / 'src' ]
4 |
5 | if get_option('profile') == 'default'
6 | cargo_options += [ '--release' ]
7 | rust_target = 'release'
8 | message('Building in release mode')
9 | else
10 | rust_target = 'debug'
11 | message('Building in debug mode')
12 | endif
13 |
14 | cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'nce-helper' / 'cargo-home' ]
15 |
16 | cargo_build = custom_target(
17 | 'cargo-build',
18 | build_by_default: true,
19 | build_always_stale: true,
20 | output: 'nce-helper',
21 | console: true,
22 | install: true,
23 | install_dir: get_option('libexecdir'),
24 | command: [
25 | 'env',
26 | cargo_env,
27 | cargo, 'build',
28 | cargo_options,
29 | '&&',
30 | 'cp', 'nce-helper' / 'src' / rust_target / 'nce-helper', '@OUTPUT@',
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/packages/nixos-conf-editor/default.nix:
--------------------------------------------------------------------------------
1 | { stdenv
2 | , lib
3 | , appstream-glib
4 | , cargo
5 | , desktop-file-utils
6 | , gdk-pixbuf
7 | , gettext
8 | , git
9 | , glib
10 | , gnome
11 | , gtk4
12 | , gtksourceview5
13 | , libadwaita
14 | , meson
15 | , ninja
16 | , openssl
17 | , pandoc
18 | , pkg-config
19 | , polkit
20 | , rustc
21 | , rustPlatform
22 | , vte-gtk4
23 | , wrapGAppsHook4
24 | }:
25 | stdenv.mkDerivation rec {
26 | pname = "nixos-conf-editor";
27 | version = "0.1.2";
28 |
29 | src = [ ../.. ];
30 |
31 | cargoDeps = rustPlatform.importCargoLock {
32 | lockFile = ../../Cargo.lock;
33 | };
34 |
35 | nativeBuildInputs = [
36 | appstream-glib
37 | desktop-file-utils
38 | gettext
39 | git
40 | meson
41 | ninja
42 | pkg-config
43 | polkit
44 | wrapGAppsHook4
45 | ] ++ (with rustPlatform; [
46 | cargo
47 | cargoSetupHook
48 | rustc
49 | ]);
50 |
51 | buildInputs = [
52 | gdk-pixbuf
53 | glib
54 | gnome.adwaita-icon-theme
55 | gtk4
56 | gtksourceview5
57 | libadwaita
58 | openssl
59 | vte-gtk4
60 | ];
61 |
62 | postInstall = ''
63 | wrapProgram $out/bin/nixos-conf-editor --prefix PATH : '${lib.makeBinPath [ pandoc ]}'
64 | '';
65 | }
66 |
--------------------------------------------------------------------------------
/po/LINGUAS:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snowfallorg/nixos-conf-editor/27b5e92f580f794c690093503869aab242f075ab/po/LINGUAS
--------------------------------------------------------------------------------
/po/POTFILES.in:
--------------------------------------------------------------------------------
1 | data/dev.vlinkz.NixosConfEditor.policy.in.in
2 | data/dev.vlinkz.NixosConfEditor.desktop.in.in
3 | data/dev.vlinkz.NixosConfEditor.metainfo.xml.in.in
4 | data/dev.vlinkz.NixosConfEditor.metainfo.gschema.xml.in
5 |
--------------------------------------------------------------------------------
/po/meson.build:
--------------------------------------------------------------------------------
1 | i18n.gettext(gettext_package, preset: 'glib')
--------------------------------------------------------------------------------
/shells/default/default.nix:
--------------------------------------------------------------------------------
1 | { mkShell
2 | , cairo
3 | , cargo
4 | , clippy
5 | , desktop-file-utils
6 | , gdk-pixbuf
7 | , gettext
8 | , gobject-introspection
9 | , graphene
10 | , gtk4
11 | , gtksourceview5
12 | , libadwaita
13 | , meson
14 | , ninja
15 | , openssl
16 | , pandoc
17 | , pango
18 | , pkg-config
19 | , polkit
20 | , rust
21 | , rust-analyzer
22 | , rustc
23 | , rustfmt
24 | , vte-gtk4
25 | , wrapGAppsHook4
26 | }:
27 |
28 | mkShell {
29 | buildInputs = [
30 | cairo
31 | cargo
32 | clippy
33 | desktop-file-utils
34 | gdk-pixbuf
35 | gettext
36 | gobject-introspection
37 | graphene
38 | gtk4
39 | gtksourceview5
40 | libadwaita
41 | meson
42 | ninja
43 | openssl
44 | pandoc
45 | pango
46 | pkg-config
47 | polkit
48 | rust-analyzer
49 | rustc
50 | rustfmt
51 | vte-gtk4
52 | wrapGAppsHook4
53 | ];
54 | RUST_SRC_PATH = "${rust.packages.stable.rustPlatform.rustLibSrc}";
55 | }
56 |
--------------------------------------------------------------------------------
/src/config.rs.in:
--------------------------------------------------------------------------------
1 | pub const APP_ID: &str = @APP_ID@;
2 | pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@;
3 | pub const LOCALEDIR: &str = @LOCALEDIR@;
4 | pub const PKGDATADIR: &str = @PKGDATADIR@;
5 | pub const LIBEXECDIR: &str = @LIBEXECDIR@;
6 | pub const PROFILE: &str = @PROFILE@;
7 | pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource");
8 | pub const VERSION: &str = @VERSION@;
9 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod parse;
3 | pub mod ui;
4 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use gettextrs::{gettext, LocaleCategory};
2 | use gtk::{gio, glib, prelude::ApplicationExt};
3 | use nixos_conf_editor::{
4 | config::{APP_ID, GETTEXT_PACKAGE, LOCALEDIR},
5 | ui::window::AppModel,
6 | };
7 | use relm4::{actions::AccelsPlus, RelmApp};
8 |
9 | relm4::new_action_group!(WindowActionGroup, "window");
10 | relm4::new_stateless_action!(SearchAction, WindowActionGroup, "search");
11 |
12 | fn main() {
13 | gtk::init().unwrap();
14 | pretty_env_logger::init();
15 | setup_gettext();
16 | glib::set_application_name(&gettext("Configuration Editor"));
17 | let app = adw::Application::new(Some(APP_ID), gio::ApplicationFlags::empty());
18 | app.set_resource_base_path(Some("/dev/vlinkz/NixosConfEditor"));
19 | app.set_accelerators_for_action::(&["f"]);
20 | let app = RelmApp::with_app(app);
21 | app.run::(());
22 | }
23 |
24 | fn setup_gettext() {
25 | // Prepare i18n
26 | gettextrs::setlocale(LocaleCategory::LcAll, "");
27 | gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain");
28 | gettextrs::bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8")
29 | .expect("Unable to bind the text domain codeset to UTF-8");
30 | gettextrs::textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain");
31 | }
32 |
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | global_conf = configuration_data()
2 | global_conf.set_quoted('APP_ID', application_id)
3 | global_conf.set_quoted('PKGDATADIR', pkgdatadir)
4 | global_conf.set_quoted('PROFILE', profile)
5 | global_conf.set_quoted('VERSION', version + version_suffix)
6 | global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package)
7 | global_conf.set_quoted('LOCALEDIR', localedir)
8 | global_conf.set_quoted('LIBEXECDIR', libexecdir)
9 | config = configure_file(
10 | input: 'config.rs.in',
11 | output: 'config.rs',
12 | configuration: global_conf
13 | )
14 | # Copy the config.rs output to the source directory.
15 | run_command(
16 | 'cp',
17 | meson.project_build_root() / 'src' / 'config.rs',
18 | meson.project_source_root() / 'src' / 'config.rs',
19 | check: true
20 | )
21 |
22 | cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
23 | cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
24 |
25 | if get_option('profile') == 'default'
26 | cargo_options += [ '--release' ]
27 | rust_target = 'release'
28 | message('Building in release mode')
29 | else
30 | rust_target = 'debug'
31 | message('Building in debug mode')
32 | endif
33 |
34 | cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
35 |
36 | cargo_build = custom_target(
37 | 'cargo-build',
38 | build_by_default: true,
39 | build_always_stale: true,
40 | output: meson.project_name(),
41 | console: true,
42 | install: true,
43 | install_dir: bindir,
44 | command: [
45 | 'env',
46 | cargo_env,
47 | cargo, 'build',
48 | cargo_options,
49 | '&&',
50 | 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
51 | ]
52 | )
53 |
--------------------------------------------------------------------------------
/src/parse/config.rs:
--------------------------------------------------------------------------------
1 | use log::{debug, warn};
2 | use nix_editor;
3 | use std::{collections::HashMap, error::Error, fs, io, path::Path};
4 |
5 | pub fn parseconfig(path: &str) -> Result, Box> {
6 | let f = fs::read_to_string(Path::new(path))?;
7 | match nix_editor::parse::get_collection(f) {
8 | Ok(x) => Ok(x),
9 | Err(_) => Err(Box::new(io::Error::new(
10 | io::ErrorKind::InvalidData,
11 | "Failed to parse config",
12 | ))),
13 | }
14 | }
15 |
16 | pub fn opconfigured(
17 | conf: &HashMap,
18 | pos: &[String],
19 | attr: String,
20 | ) -> bool {
21 | let mut p = pos.to_vec();
22 | p.push(attr);
23 | conf.keys().any(|k| {
24 | let s = k.split('.').collect::>();
25 | if s.len() < p.len() {
26 | false
27 | } else {
28 | s[0..p.len()].eq(&p)
29 | }
30 | })
31 | }
32 |
33 | pub fn opconfigured2(path: &str, pos: &[String], refpos: &[String], attr: String) -> bool {
34 | let mut p = pos.to_vec();
35 | p.push(attr.clone());
36 | let mut r = refpos.to_vec();
37 | r.push(attr);
38 | readval(path, &p.join("."), &r.join(".")).is_ok()
39 | }
40 |
41 | pub fn getconfvals(conf: &HashMap, pos: &[String]) -> Vec {
42 | let mut out = vec![];
43 | for attr in conf.keys() {
44 | let k = attr.split('.').collect::>();
45 | if k.len() > pos.len() && k[0..pos.len()].eq(pos) {
46 | let x = k[pos.len()].to_string();
47 | if !out.contains(&x) {
48 | out.push(x);
49 | }
50 | }
51 | }
52 | out
53 | }
54 |
55 | pub fn getarrvals(path: &str, pos: &[String]) -> Vec {
56 | let f = fs::read_to_string(Path::new(path)).unwrap();
57 | let out = nix_editor::read::getarrvals(&f, &pos.join("."));
58 | match out {
59 | Ok(x) => x,
60 | Err(_) => vec![],
61 | }
62 | }
63 |
64 | pub fn editconfigpath(
65 | path: &str,
66 | editedopts: HashMap,
67 | ) -> Result> {
68 | let f = fs::read_to_string(Path::new(path))?;
69 | editconfig(f, editedopts)
70 | }
71 |
72 | pub fn editconfig(
73 | mut f: String,
74 | //path: &str,
75 | editedopts: HashMap,
76 | ) -> Result> {
77 | debug!("editedopts: {:#?}", editedopts);
78 | let mut k = editedopts.keys().collect::>();
79 | k.sort();
80 |
81 | let mut starops: HashMap> = HashMap::new();
82 | for (op, val) in editedopts.into_iter() {
83 | if op.split('.').any(|x| x.parse::().is_ok()) {
84 | let option = op.split('.').collect::>();
85 | let index = option
86 | .iter()
87 | .position(|x| x.parse::().is_ok())
88 | .unwrap();
89 | let o = &option[..index];
90 | let v = &option[index + 1..];
91 | let i = option[index].parse::().unwrap();
92 |
93 | let mut p = if let Some(y) = starops.get(&o.join(".")) {
94 | y.to_owned()
95 | } else {
96 | HashMap::new()
97 | };
98 | // fill up on first time
99 | if p.is_empty() {
100 | let arr = match nix_editor::read::getarrvals(&f, &o.join(".")) {
101 | Ok(x) => x,
102 | Err(_) => vec![],
103 | };
104 | for (j, a) in arr.iter().enumerate() {
105 | p.insert(j, a.to_string());
106 | }
107 | }
108 |
109 | let arrval = match p.get(&i) {
110 | Some(x) => x.to_string(),
111 | None => "{}".to_string(),
112 | };
113 | let mut h = HashMap::new();
114 | h.insert(v.join("."), val);
115 | p.insert(i, editconfig(arrval, h)?);
116 | starops.insert(o.join("."), p);
117 | } else if val.is_empty() {
118 | f = match nix_editor::write::deref(&f, &op) {
119 | Ok(x) => x,
120 | Err(_) => {
121 | return Err(Box::new(io::Error::new(
122 | io::ErrorKind::InvalidData,
123 | format!("Failed to deref {}", op),
124 | )))
125 | }
126 | };
127 | } else {
128 | f = match nix_editor::write::write(&f, &op, &val) {
129 | Ok(x) => x,
130 | Err(_) => {
131 | return Err(Box::new(io::Error::new(
132 | io::ErrorKind::InvalidData,
133 | format!("Failed to set value {} to {}", op, val),
134 | )))
135 | }
136 | };
137 | }
138 | }
139 | for (k, v) in starops {
140 | let mut arr = v.into_iter().collect::>();
141 | arr.sort_by(|(x, _), (y, _)| x.cmp(y));
142 | // Just use nixpkgs-fmt instead
143 | let valarr = format!(
144 | "[\n{}\n ]",
145 | arr.iter()
146 | .filter(|(_, x)| x.trim().replace(['\n', ' '], "") != "{}")
147 | .map(|(_, y)| format!(
148 | " {}",
149 | y.replace(";\n ", ";\n ")
150 | .replace("; }", ";\n }")
151 | .replace("; ", ";\n ")
152 | .replace(";\n ", ";\n ")
153 | .replace("{ ", "{\n ")
154 | .replace(";}", ";\n }")
155 | ))
156 | .collect::>()
157 | .join("\n")
158 | );
159 | f = match nix_editor::write::write(&f, &k, &valarr) {
160 | Ok(x) => x,
161 | Err(_) => {
162 | return Err(Box::new(io::Error::new(
163 | io::ErrorKind::InvalidData,
164 | format!("Failed to set value {} to {}", k, valarr),
165 | )))
166 | }
167 | };
168 | }
169 | Ok(f)
170 | }
171 |
172 | pub fn readval(path: &str, query: &str, refq: &str) -> Result> {
173 | warn!("READVAL: {} {} {}", path, query, refq);
174 | let f = fs::read_to_string(Path::new(path))?;
175 | let out = if !refq.contains(&String::from("*")) {
176 | nix_editor::read::readvalue(&f, query)
177 | } else {
178 | let p = refq.split('.').collect::>();
179 | let mut r: Vec> = vec![vec![]];
180 | let mut indexvec: Vec = vec![];
181 | let mut j = 0;
182 | for (i, attr) in p.iter().enumerate() {
183 | if *attr == "*" {
184 | r.push(vec![]);
185 | if let Ok(x) = query.split('.').collect::>()[i].parse::() {
186 | indexvec.push(x);
187 | }
188 | j += 1;
189 | } else {
190 | r[j].push(attr.to_string());
191 | }
192 | }
193 | let mut f = fs::read_to_string(Path::new(path)).unwrap();
194 | let mut i = 0;
195 | for y in r {
196 | if i < indexvec.len() {
197 | f = match nix_editor::read::getarrvals(&f, &y.join(".")) {
198 | Ok(x) => {
199 | warn!("x: {:?}", x);
200 | let o = match x.get(indexvec[i]) {
201 | Some(x) => x.to_string(),
202 | None => {
203 | return Err(Box::new(io::Error::new(
204 | io::ErrorKind::InvalidData,
205 | format!("Index out of bounds {}", refq),
206 | )))
207 | }
208 | };
209 | i += 1;
210 | o
211 | }
212 | Err(_) => {
213 | return Err(Box::new(io::Error::new(
214 | io::ErrorKind::InvalidData,
215 | "Failed to read value from configuration",
216 | )))
217 | }
218 | };
219 | } else {
220 | f = match nix_editor::read::readvalue(&f, &y.join(".")) {
221 | Ok(x) => x,
222 | Err(_) => {
223 | return Err(Box::new(io::Error::new(
224 | io::ErrorKind::InvalidData,
225 | "Failed to read value from configuration",
226 | )))
227 | }
228 | };
229 | }
230 | }
231 | Ok(f)
232 | };
233 | match out {
234 | Ok(x) => Ok(x),
235 | Err(_) => Err(Box::new(io::Error::new(
236 | io::ErrorKind::InvalidData,
237 | format!("Failed to read value {}", query),
238 | ))),
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/parse/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod options;
3 | pub mod preferences;
4 |
--------------------------------------------------------------------------------
/src/parse/options.rs:
--------------------------------------------------------------------------------
1 | use ijson::{IString, IValue};
2 | use serde::{Deserialize, Serialize};
3 | use serde_json;
4 | use std::{self, cmp::Ordering, collections::HashMap, error::Error, fs};
5 |
6 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone)]
7 | pub struct OptionData {
8 | pub default: Option,
9 | pub description: IValue,
10 | #[serde(alias = "readOnly")]
11 | pub read_only: bool,
12 | #[serde(alias = "type")]
13 | pub op_type: IString,
14 | pub declarations: Vec,
15 | pub example: Option,
16 | }
17 |
18 | #[derive(Default, Debug, PartialEq)]
19 | pub struct AttrTree {
20 | pub attributes: HashMap,
21 | pub options: Vec,
22 | }
23 |
24 | pub fn read(file: &str) -> Result<(HashMap, AttrTree), Box> {
25 | let f = fs::read_to_string(file)?;
26 | let data: HashMap = serde_json::from_str(&f)?;
27 | let ops = data.keys().map(|x| x.as_str()).collect::>();
28 | let tree = buildtree(ops)?;
29 | Ok((data, tree))
30 | }
31 |
32 | pub fn attrloc(tree: &AttrTree, pos: Vec) -> Option<&AttrTree> {
33 | match pos.len().cmp(&1) {
34 | Ordering::Greater => match tree.attributes.get(&pos[0]) {
35 | Some(x) => attrloc(x, pos[1..].to_vec()),
36 | None => None,
37 | },
38 | Ordering::Equal => tree.attributes.get(&pos[0]),
39 | Ordering::Less => Some(tree),
40 | }
41 | }
42 |
43 | fn buildtree(ops: Vec<&str>) -> Result> {
44 | let split = ops
45 | .into_iter()
46 | .map(|x| x.split('.').collect::>())
47 | .collect::>();
48 | let mut tree = AttrTree {
49 | attributes: HashMap::new(),
50 | options: vec![],
51 | };
52 | for attr in split {
53 | match attr.len().cmp(&1) {
54 | Ordering::Greater => {
55 | if tree.attributes.get(attr[0]).is_none() {
56 | tree.attributes.insert(
57 | attr[0].to_string(),
58 | AttrTree {
59 | attributes: HashMap::new(),
60 | options: vec![],
61 | },
62 | );
63 | }
64 | buildtree_child(
65 | tree.attributes.get_mut(attr[0]).unwrap(),
66 | attr[1..].to_vec(),
67 | )?;
68 | }
69 | Ordering::Equal => tree.options.push(attr[0].to_string()),
70 | Ordering::Less => {}
71 | }
72 | }
73 | Ok(tree)
74 | }
75 |
76 | fn buildtree_child(tree: &mut AttrTree, attr: Vec<&str>) -> Result<(), Box> {
77 | match attr.len().cmp(&1) {
78 | Ordering::Greater => {
79 | if tree.attributes.get(attr[0]).is_none() {
80 | tree.attributes.insert(
81 | attr[0].to_string(),
82 | AttrTree {
83 | attributes: HashMap::new(),
84 | options: vec![],
85 | },
86 | );
87 | }
88 | buildtree_child(
89 | tree.attributes.get_mut(attr[0]).unwrap(),
90 | attr[1..].to_vec(),
91 | )
92 | }
93 | Ordering::Equal => {
94 | tree.options.push(attr[0].to_string());
95 | Ok(())
96 | }
97 | Ordering::Less => Ok(()),
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/parse/preferences.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use nix_data::config::configfile::NixDataConfig;
3 |
4 | pub fn getconfig() -> Option {
5 | if let Ok(c) = nix_data::config::configfile::getconfig() {
6 | Some(c)
7 | } else {
8 | None
9 | }
10 | }
11 |
12 | pub fn editconfig(config: NixDataConfig) -> Result<()> {
13 | nix_data::config::configfile::setuserconfig(config)?;
14 | Ok(())
15 | }
16 |
--------------------------------------------------------------------------------
/src/ui/about.rs:
--------------------------------------------------------------------------------
1 | use super::window::AppMsg;
2 | use crate::config;
3 | use adw::prelude::*;
4 | use relm4::*;
5 |
6 | #[derive(Debug)]
7 | pub struct AboutPageModel;
8 |
9 | #[relm4::component(pub)]
10 | impl SimpleComponent for AboutPageModel {
11 | type Init = gtk::Window;
12 | type Input = ();
13 | type Output = AppMsg;
14 | type Widgets = AboutPageWidgets;
15 |
16 | view! {
17 | adw::AboutWindow {
18 | set_transient_for: Some(&parent_window),
19 | set_modal: true,
20 | set_application_name: "NixOS Configuration Editor",
21 | set_application_icon: config::APP_ID,
22 | set_developer_name: "Victor Fuentes",
23 | set_version: config::VERSION,
24 | set_issue_url: "https://github.com/vlinkz/nixos-conf-editor/issues",
25 | set_license_type: gtk::License::Gpl30,
26 | set_website: "https://github.com/vlinkz/nixos-conf-editor",
27 | set_developers: &["Victor Fuentes https://github.com/vlinkz"],
28 | }
29 | }
30 |
31 | fn init(
32 | parent_window: Self::Init,
33 | root: &Self::Root,
34 | _sender: ComponentSender,
35 | ) -> ComponentParts {
36 | let model = AboutPageModel;
37 |
38 | let widgets = view_output!();
39 |
40 | ComponentParts { model, widgets }
41 | }
42 |
43 | fn update(&mut self, _msg: Self::Input, _sender: ComponentSender) {}
44 | }
45 |
--------------------------------------------------------------------------------
/src/ui/mod.rs:
--------------------------------------------------------------------------------
1 | mod about;
2 | mod nameentry;
3 | mod optionpage;
4 | mod preferencespage;
5 | mod quitdialog;
6 | mod rebuild;
7 | mod savechecking;
8 | mod searchentry;
9 | mod searchfactory;
10 | mod searchpage;
11 | mod treefactory;
12 | mod welcome;
13 | pub mod window;
14 | mod windowloading;
15 |
--------------------------------------------------------------------------------
/src/ui/nameentry.rs:
--------------------------------------------------------------------------------
1 | use super::window::*;
2 | use adw::prelude::*;
3 | use relm4::*;
4 |
5 | pub struct NameEntryModel {
6 | hidden: bool,
7 | msg: String,
8 | existing: Vec,
9 | text: String,
10 | }
11 |
12 | #[derive(Debug)]
13 | pub enum NameEntryMsg {
14 | Show(String, Vec),
15 | Cancel,
16 | Save,
17 | SetText(String),
18 | }
19 |
20 | #[relm4::component(pub)]
21 | impl SimpleComponent for NameEntryModel {
22 | type Init = gtk::Window;
23 | type Input = NameEntryMsg;
24 | type Output = AppMsg;
25 | type Widgets = NameEntryWidgets;
26 |
27 | view! {
28 | dialog = gtk::MessageDialog {
29 | set_transient_for: Some(&parent_window),
30 | set_modal: true,
31 | #[watch]
32 | set_visible: !model.hidden,
33 | set_text: Some("Enter a new value"),
34 | set_secondary_text: None,
35 | add_button: ("Save", gtk::ResponseType::Accept),
36 | add_button: ("Cancel", gtk::ResponseType::Cancel),
37 | connect_response[sender] => move |_, resp| {
38 | sender.input(match resp {
39 | gtk::ResponseType::Accept => NameEntryMsg::Save,
40 | gtk::ResponseType::Cancel => NameEntryMsg::Cancel,
41 | _ => unreachable!(),
42 | });
43 | }
44 | }
45 | }
46 |
47 | additional_fields! {
48 | textentry: gtk::Entry,
49 | msgbuf: gtk::EntryBuffer,
50 | }
51 |
52 | fn init(
53 | parent_window: Self::Init,
54 | root: &Self::Root,
55 | sender: ComponentSender,
56 | ) -> ComponentParts {
57 | let model = NameEntryModel {
58 | hidden: true,
59 | msg: String::default(),
60 | text: String::default(),
61 | existing: vec![],
62 | };
63 |
64 | view! {
65 | textentry = gtk::Entry {
66 | set_margin_start: 20,
67 | set_margin_end: 20,
68 | set_hexpand: true,
69 | set_buffer: msgbuf = >k::EntryBuffer {
70 | connect_text_notify[sender] => move |x| {
71 | sender.input(NameEntryMsg::SetText(x.text()));
72 | },
73 | }
74 | }
75 | }
76 |
77 | let widgets = view_output!();
78 |
79 | widgets.dialog.content_area().append(&widgets.textentry);
80 | let accept_widget = widgets
81 | .dialog
82 | .widget_for_response(gtk::ResponseType::Accept)
83 | .expect("No button for accept response set");
84 | accept_widget.set_sensitive(false);
85 |
86 | ComponentParts { model, widgets }
87 | }
88 |
89 | fn pre_view() {
90 | let accept_widget = dialog
91 | .widget_for_response(gtk::ResponseType::Accept)
92 | .expect("No button for accept response set");
93 | if model.text.is_empty() || model.existing.contains(&model.text) {
94 | accept_widget.set_css_classes(&[]);
95 | accept_widget.set_sensitive(false);
96 | } else {
97 | accept_widget.set_css_classes(&["suggested-action"]);
98 | accept_widget.set_sensitive(true);
99 | }
100 | if model.hidden {
101 | msgbuf.set_text("");
102 | }
103 | }
104 |
105 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
106 | match msg {
107 | NameEntryMsg::Show(msg, existing) => {
108 | self.hidden = false;
109 | self.msg = msg;
110 | self.existing = existing;
111 | self.text = String::default();
112 | }
113 | NameEntryMsg::Cancel => self.hidden = true,
114 | NameEntryMsg::Save => {
115 | self.hidden = true;
116 | let _ = sender.output(AppMsg::AddNameAttr(None, self.text.clone()));
117 | }
118 | NameEntryMsg::SetText(s) => {
119 | self.text = s;
120 | }
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/ui/optionpage.rs:
--------------------------------------------------------------------------------
1 | use super::savechecking::*;
2 | use super::window::*;
3 | use crate::parse::options::OptionData;
4 | use adw::prelude::*;
5 | use html2pango;
6 | use log::*;
7 | use pandoc::{self, MarkdownExtension};
8 | use relm4::*;
9 | use sourceview5::prelude::*;
10 | use std::convert::identity;
11 |
12 | #[derive(Debug)]
13 | pub enum OptPageMsg {
14 | UpdateOption(
15 | Box,
16 | Vec,
17 | Vec,
18 | String,
19 | Vec,
20 | ),
21 | UpdateConf(String),
22 | UpdateConfMod(String),
23 | ResetConf,
24 | ClearConf,
25 | SaveConf,
26 | DoneSaving(bool, String),
27 | SetScheme(String),
28 | }
29 |
30 | #[tracker::track]
31 | pub struct OptPageModel {
32 | pub opt: Vec,
33 | pub refopt: Vec,
34 | pub data: OptionData,
35 | pub conf: String,
36 | pub modifiedconf: String,
37 | alloptions: Vec,
38 | scheme: Option,
39 | saving: bool,
40 | resettracker: u8,
41 | valuetracker: u8,
42 | #[tracker::no_eq]
43 | async_handler: WorkerController,
44 | }
45 |
46 | #[relm4::component(pub)]
47 | impl SimpleComponent for OptPageModel {
48 | type Init = ();
49 | type Input = OptPageMsg;
50 | type Output = AppMsg;
51 | type Widgets = OptPageWidgets;
52 |
53 | view! {
54 | optwindow = gtk::ScrolledWindow {
55 | adw::Clamp {
56 | gtk::Box {
57 | set_orientation: gtk::Orientation::Vertical,
58 | set_margin_all: 15,
59 | set_spacing: 15,
60 | set_vexpand: true,
61 | add_css_class: "labels",
62 | gtk::Label {
63 | set_margin_top: 5,
64 | set_margin_bottom: 5,
65 | set_halign: gtk::Align::Start,
66 | add_css_class: "title-1",
67 | #[watch]
68 | set_label: &model.opt.join(".")
69 | },
70 |
71 | gtk::Box {
72 | set_orientation: gtk::Orientation::Vertical,
73 | set_spacing: 10,
74 | gtk::Box {
75 | set_orientation: gtk::Orientation::Horizontal,
76 | add_css_class: "header",
77 | add_css_class: "single-line",
78 | gtk::Label {
79 | set_halign: gtk::Align::Start,
80 | add_css_class: "heading",
81 | set_label: "Description",
82 | }
83 | },
84 | #[name(desc)]
85 | gtk::Label {
86 | set_halign: gtk::Align::Start,
87 | set_wrap: true,
88 | add_css_class: "body",
89 | #[track(model.changed(OptPageModel::data()))]
90 | set_markup: {
91 | let mut pandoc = pandoc::new();
92 |
93 | let x = if let Some(description) = model.data.description.as_object().and_then(|x| x.get("text")).and_then(|x| x.as_string()).map(|x| x.to_string()) {
94 | description.trim().to_string()
95 | } else {
96 | model.data.description.as_string().map(|x| x.to_string()).unwrap_or_default().trim().to_string()
97 | };
98 |
99 | pandoc.set_input(pandoc::InputKind::Pipe(x.replace("{option}", "")));
100 | pandoc.set_output(pandoc::OutputKind::Pipe);
101 | pandoc.set_input_format(pandoc::InputFormat::Markdown, vec![MarkdownExtension::ListsWithoutPrecedingBlankline]);
102 | pandoc.set_output_format(pandoc::OutputFormat::Html, vec![]);
103 |
104 | let out = pandoc.execute().unwrap();
105 | let y = match out {
106 | pandoc::PandocOutput::ToBuffer(s) => s,
107 | _ => "".to_string(),
108 | };
109 | let mut pango = html2pango::markup_html(&y.replace('\n', " \n")).unwrap_or(y).replace("• \n", "• ").trim().to_string();
110 | while pango.ends_with('\n') {
111 | pango.pop();
112 | }
113 | pango.strip_prefix('\n').unwrap_or(&pango).to_string().as_str()
114 | },
115 | },
116 | },
117 |
118 |
119 | gtk::Box {
120 | set_orientation: gtk::Orientation::Vertical,
121 | set_spacing: 10,
122 | #[name(type_header_box)]
123 | gtk::Box {
124 | set_orientation: gtk::Orientation::Horizontal,
125 | add_css_class: "header",
126 | add_css_class: "single-line",
127 | #[name(type_header)]
128 | gtk::Label {
129 | set_halign: gtk::Align::Start,
130 | add_css_class: "heading",
131 | set_label: "Type",
132 | }
133 | },
134 | gtk::Label {
135 | set_halign: gtk::Align::Start,
136 | set_wrap: true,
137 | add_css_class: "body",
138 | #[watch]
139 | set_label: &model.data.op_type,
140 | },
141 | },
142 |
143 | gtk::Box {
144 | set_orientation: gtk::Orientation::Vertical,
145 | set_spacing: 10,
146 | #[watch]
147 | set_visible: model.data.default.is_some(),
148 | #[name(default_box)]
149 | gtk::Box {
150 | set_orientation: gtk::Orientation::Horizontal,
151 | add_css_class: "header",
152 | add_css_class: "single-line",
153 | #[name(default_header)]
154 | gtk::Label {
155 | set_halign: gtk::Align::Start,
156 | add_css_class: "heading",
157 | set_label: "Default",
158 | }
159 | },
160 | gtk::Frame {
161 | add_css_class: "code",
162 | sourceview5::View {
163 | set_editable: false,
164 | set_monospace: true,
165 | set_cursor_visible: false,
166 | set_top_margin: 5,
167 | set_bottom_margin: 5,
168 | set_left_margin: 5,
169 | #[wrap(Some)]
170 | set_buffer: defaultbuf = &sourceview5::Buffer {
171 | #[track(model.changed(OptPageModel::scheme()))]
172 | set_style_scheme: model.scheme.as_ref(),
173 | #[watch]
174 | set_text: {
175 | let x = &model.data.default.as_ref().map(|x| ijson::from_value::(x).unwrap_or_default()).unwrap_or(serde_json::Value::Null);
176 | &match x {
177 | serde_json::Value::Object(o) => match o.get("text") {
178 | Some(serde_json::Value::String(s)) => {
179 | if o.get("_type").unwrap_or(&serde_json::Value::Null).as_str().unwrap_or("").eq("literalExpression") {
180 | s.strip_suffix('\n').unwrap_or(s).to_string()
181 | } else {
182 | serde_json::to_string_pretty(x).unwrap_or_default()
183 | }
184 | },
185 | _ => serde_json::to_string_pretty(x).unwrap_or_default(),
186 | },
187 | _ => serde_json::to_string_pretty(x).unwrap_or_default(),
188 | }
189 | },
190 | }
191 | },
192 | },
193 | },
194 |
195 | gtk::Box {
196 | set_orientation: gtk::Orientation::Vertical,
197 | set_spacing: 10,
198 | #[watch]
199 | set_visible: model.data.example.is_some(),
200 | #[name(example_box)]
201 | gtk::Box {
202 | set_orientation: gtk::Orientation::Horizontal,
203 | add_css_class: "header",
204 | add_css_class: "single-line",
205 | #[name(example_header)]
206 | gtk::Label {
207 | set_halign: gtk::Align::Start,
208 | add_css_class: "heading",
209 | add_css_class: "h4",
210 | set_label: "Example",
211 | }
212 | },
213 | gtk::Frame {
214 | add_css_class: "code",
215 | sourceview5::View {
216 | set_editable: false,
217 | set_monospace: true,
218 | set_cursor_visible: false,
219 | set_top_margin: 5,
220 | set_bottom_margin: 5,
221 | set_left_margin: 5,
222 | #[wrap(Some)]
223 | set_buffer: exbuf = &sourceview5::Buffer {
224 | #[track(model.changed(OptPageModel::scheme()))]
225 | set_style_scheme: model.scheme.as_ref(),
226 | #[watch]
227 | set_text: {
228 | let x = &model.data.example.as_ref().map(|x| ijson::from_value::(x).unwrap_or_default()).unwrap_or(serde_json::Value::Null);
229 | &match x {
230 | serde_json::Value::Object(o) => match o.get("text") {
231 | Some(serde_json::Value::String(s)) => {
232 | if o.get("_type").unwrap_or(&serde_json::Value::Null).as_str().unwrap_or("").eq("literalExpression") {
233 | s.strip_suffix('\n').unwrap_or(s).to_string()
234 | } else {
235 | serde_json::to_string_pretty(x).unwrap_or_default()
236 | }
237 | },
238 | _ => serde_json::to_string_pretty(x).unwrap_or_default(),
239 | },
240 | _ => serde_json::to_string_pretty(x).unwrap_or_default(),
241 | }
242 | },
243 | }
244 | },
245 | },
246 | },
247 | gtk::Separator {
248 | set_opacity: 0.0,
249 | set_margin_top: 5,
250 | },
251 | gtk::Box {
252 | #[watch]
253 | set_visible: valuestack.is_child_visible(),
254 | set_orientation: gtk::Orientation::Vertical,
255 | set_spacing: 10,
256 | #[name(simplevalue_box)]
257 | gtk::Box {
258 | set_orientation: gtk::Orientation::Horizontal,
259 | add_css_class: "header",
260 | add_css_class: "single-line",
261 | append = >k::Label {
262 | set_halign: gtk::Align::Start,
263 | add_css_class: "heading",
264 | set_label: "Value",
265 | }
266 | },
267 | #[name(valuestack)]
268 | gtk::Stack {
269 | #[name(number)]
270 | gtk::SpinButton {
271 | set_halign: gtk::Align::Start,
272 | set_adjustment: >k::Adjustment::new(0.0, f64::MIN, f64::MAX, 1.0, 5.0, 0.0),
273 | set_climb_rate: 1.0,
274 | set_digits: 0,
275 | connect_value_changed[sender] => move |x| {
276 | if x.is_sensitive() {
277 | sender.input(OptPageMsg::UpdateConfMod(x.value().to_string()))
278 | }
279 | },
280 | },
281 | #[name(stringentry)]
282 | gtk::Entry {
283 | set_halign: gtk::Align::Start,
284 | connect_changed[sender] => move |x| {
285 | if x.is_sensitive() {
286 | sender.input(OptPageMsg::UpdateConfMod(format!("\"{}\"", x.text())));
287 | }
288 | },
289 | },
290 | #[name(truefalse)]
291 | gtk::Box {
292 | add_css_class: "linked",
293 | set_orientation: gtk::Orientation::Horizontal,
294 | #[name(truebtn)]
295 | gtk::ToggleButton {
296 | set_label: "True",
297 | connect_toggled[sender] => move |x| {
298 | if x.is_active() {
299 | sender.input(OptPageMsg::UpdateConfMod(String::from("true")))
300 | }
301 | }
302 | },
303 | #[name(falsebtn)]
304 | gtk::ToggleButton {
305 | set_label: "False",
306 | set_group: Some(&truebtn),
307 | connect_toggled[sender] => move |x| {
308 | if x.is_active() {
309 | sender.input(OptPageMsg::UpdateConfMod(String::from("false")))
310 | }
311 | }
312 | },
313 | // #[name(nullbtn)]
314 | // gtk::ToggleButton {
315 | // set_label: "null",
316 | // set_group: Some(&truebtn),
317 | // }
318 | },
319 | }
320 | },
321 |
322 | gtk::Box {
323 | set_orientation: gtk::Orientation::Vertical,
324 | set_spacing: 10,
325 | #[name(value_box)]
326 | gtk::Box {
327 | set_orientation: gtk::Orientation::Horizontal,
328 | add_css_class: "header",
329 | add_css_class: "single-line",
330 | gtk::Label {
331 | set_halign: gtk::Align::Start,
332 | add_css_class: "heading",
333 | set_label: "Attribute Value",
334 | }
335 | },
336 | gtk::Frame {
337 | add_css_class: "code",
338 | sourceview5::View {
339 | set_background_pattern: sourceview5::BackgroundPatternType::Grid,
340 | set_height_request: 100,
341 | set_editable: true,
342 | set_monospace: true,
343 | set_top_margin: 5,
344 | set_bottom_margin: 5,
345 | set_left_margin: 5,
346 | #[wrap(Some)]
347 | set_buffer: valuebuf = &sourceview5::Buffer {
348 | #[track(model.changed(OptPageModel::scheme()))]
349 | set_style_scheme: model.scheme.as_ref(),
350 | #[track(model.changed(OptPageModel::opt()))]
351 | set_text: {
352 | debug!("opt changing valuebuf to {:?}", model.conf);
353 | &model.conf
354 | },
355 | #[track(model.changed(OptPageModel::conf()))]
356 | set_text: {
357 | debug!("conf changing valuebuf to {:?}", model.conf);
358 | &model.conf
359 | },
360 | #[track(model.changed(OptPageModel::valuetracker()))]
361 | set_text: {
362 | debug!("valuetracker changing valuebuf to {:?}", model.modifiedconf);
363 | &model.modifiedconf
364 | },
365 | connect_changed[sender] => move |x| {
366 | let (start, end) = x.bounds();
367 | debug!("valuebuf changed to {:?}", x.text(&start, &end, true));
368 | let text = x.text(&start, &end, true).to_string();
369 | sender.input(OptPageMsg::UpdateConf(text))
370 | }
371 | }
372 | },
373 | },
374 | gtk::Box {
375 | set_orientation: gtk::Orientation::Horizontal,
376 | set_spacing: 10,
377 | gtk::Button {
378 | set_label: "Reset",
379 | #[watch]
380 | set_sensitive: model.conf != model.modifiedconf,
381 | connect_clicked[sender] => move |_| {
382 | sender.input(OptPageMsg::ResetConf)
383 | }
384 | },
385 | gtk::Button {
386 | set_label: "Clear",
387 | #[watch]
388 | set_sensitive: !model.modifiedconf.is_empty(),
389 | connect_clicked[sender] => move |_| {
390 | sender.input(OptPageMsg::ClearConf)
391 | }
392 | },
393 | #[name(savestack)]
394 | gtk::Stack {
395 | set_halign: gtk::Align::End,
396 | set_hexpand: true,
397 | #[name(savebtn)]
398 | gtk::Button {
399 | set_label: "Save",
400 | add_css_class: "suggested-action",
401 | #[watch]
402 | set_sensitive: model.conf != model.modifiedconf,
403 | connect_clicked[sender] => move |_| {
404 | sender.input(OptPageMsg::SaveConf)
405 | },
406 | },
407 | #[name(spinner)]
408 | gtk::Spinner {
409 | #[watch]
410 | set_spinning: model.saving,
411 | },
412 | },
413 | }
414 | }
415 | }
416 | }
417 | }
418 | }
419 |
420 | fn pre_view() {
421 | info!("pre_view");
422 | let set_val = || {
423 | debug!("SET VAL");
424 | if let Some(x) = valuestack.visible_child() {
425 | let val = model.conf.as_str();
426 | if x.eq(truefalse) {
427 | if val == "true" {
428 | truebtn.set_active(true);
429 | falsebtn.set_active(false);
430 | } else if val == "false" {
431 | truebtn.set_active(false);
432 | falsebtn.set_active(true);
433 | } else {
434 | truebtn.set_active(false);
435 | falsebtn.set_active(false);
436 | }
437 | } else if x.eq(number) {
438 | number.set_sensitive(false);
439 | if let Ok(x) = val.parse::() {
440 | number.set_value(x);
441 | } else {
442 | number.set_value(0.0);
443 | }
444 | number.set_sensitive(true);
445 | } else if x.eq(stringentry) {
446 | if let Some(x) = val.chars().next() {
447 | if let Some(y) = val.chars().last() {
448 | if x == '"' && y == '"' {
449 | if let Some(v) = val.get(1..val.len() - 1) {
450 | stringentry.set_sensitive(false);
451 | stringentry.set_text(v);
452 | stringentry.set_sensitive(true);
453 | return;
454 | }
455 | }
456 | }
457 | }
458 | stringentry.set_sensitive(false);
459 | stringentry.set_text("");
460 | stringentry.set_sensitive(true);
461 | } else {
462 | warn!("Unhandled valuestack child {:?}", x);
463 | }
464 | } else {
465 | info!(
466 | "No simple value widget for type '{}'",
467 | model.data.op_type.to_string()
468 | );
469 | }
470 | };
471 |
472 | if model.changed(OptPageModel::opt()) {
473 | let optype = model.data.op_type.as_str();
474 | valuestack.set_child_visible(true);
475 | match optype {
476 | "boolean" | "null or boolean" => valuestack.set_visible_child(truefalse),
477 | "signed integer" | "null or signed integer" => valuestack.set_visible_child(number),
478 | "string" | "null or string" | "string, not containing newlines or colons" => {
479 | valuestack.set_visible_child(stringentry)
480 | }
481 | _ => valuestack.set_child_visible(false),
482 | }
483 | if valuestack.is_child_visible() {
484 | set_val();
485 | }
486 | }
487 | if model.changed(OptPageModel::resettracker()) {
488 | // Reset button is pressed
489 | set_val();
490 | }
491 | if model.saving {
492 | savestack.set_visible_child(spinner)
493 | } else {
494 | savestack.set_visible_child(savebtn)
495 | }
496 | }
497 |
498 | fn init(
499 | _parent_window: Self::Init,
500 | root: &Self::Root,
501 | sender: ComponentSender,
502 | ) -> ComponentParts {
503 | let async_handler = SaveAsyncHandler::builder()
504 | .detach_worker(())
505 | .forward(sender.input_sender(), identity);
506 | let model = OptPageModel {
507 | opt: vec![], //parent_window.position.clone(),
508 | refopt: vec![], //parent_window.refposition.clone(),
509 | data: OptionData::default(),
510 | conf: String::new(),
511 | modifiedconf: String::new(),
512 | saving: false,
513 | alloptions: vec![], //parent_window.data.keys().map(|x| x.to_string()).collect::>(),
514 | scheme: None,
515 | resettracker: 0,
516 | valuetracker: 0,
517 | async_handler,
518 | tracker: 0,
519 | };
520 |
521 | let widgets = view_output!();
522 |
523 | ComponentParts { model, widgets }
524 | }
525 |
526 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
527 | self.reset();
528 | match msg {
529 | OptPageMsg::UpdateOption(data, opt, refopt, conf, alloptions) => {
530 | info!("OptPageMsg::UpdateOption");
531 | self.update_conf(|x| x.clear());
532 | self.update_modifiedconf(|x| x.clear());
533 | self.set_data(*data);
534 | self.update_opt(|o| *o = opt.to_vec());
535 | self.set_refopt(refopt);
536 | self.set_conf(conf.clone());
537 | self.set_modifiedconf(conf);
538 | self.set_alloptions(alloptions);
539 | }
540 | OptPageMsg::UpdateConf(conf) => {
541 | info!("OptPageMsg::UpdateConf");
542 | if conf != self.modifiedconf {
543 | self.set_modifiedconf(conf);
544 | }
545 | }
546 | OptPageMsg::UpdateConfMod(conf) => {
547 | info!("OptPageMsg::UpdateConfMod");
548 | if conf != self.modifiedconf {
549 | self.set_modifiedconf(conf);
550 | self.update_valuetracker(|_| ()); // Simulate change to conf
551 | }
552 | }
553 | OptPageMsg::ResetConf => {
554 | info!("OptPageMsg::ResetConf");
555 | let conf = self.conf.clone();
556 | self.set_modifiedconf(conf);
557 | self.update_valuetracker(|_| ()); // Simulate change to conf
558 | self.update_resettracker(|_| ()); // Simulate reset
559 | }
560 | OptPageMsg::ClearConf => {
561 | info!("OptPageMsg::ClearConf");
562 | self.set_modifiedconf(String::default());
563 | self.update_valuetracker(|_| ()); // Simulate change to conf
564 | }
565 | OptPageMsg::SaveConf => {
566 | info!("OptPageMsg::SaveConf");
567 | let opt = self.opt.join(".");
568 | let refopt = self.refopt.join(".");
569 | let mut conf = self.modifiedconf.clone();
570 | while conf.ends_with('\n') || conf.ends_with(' ') {
571 | conf.pop();
572 | }
573 | self.set_modifiedconf(conf.clone());
574 | if conf.is_empty() {
575 | sender.input(OptPageMsg::DoneSaving(true, "true\n".to_string()));
576 | } else {
577 | self.set_saving(true);
578 | let _ = sender.output(AppMsg::SetBusy(true));
579 | self.async_handler.emit(SaveAsyncHandlerMsg::SaveCheck(
580 | opt,
581 | refopt,
582 | conf,
583 | self.alloptions.to_vec(),
584 | ));
585 | }
586 | }
587 | OptPageMsg::DoneSaving(save, message) => {
588 | info!("OptPageMsg::DoneSaving");
589 | if save {
590 | if message.eq("true\n") {
591 | //Save
592 | self.set_conf(self.modifiedconf.clone());
593 | let _ = sender.output(AppMsg::EditOpt(
594 | self.opt.join("."),
595 | self.modifiedconf.clone(),
596 | ));
597 | self.update_resettracker(|_| ()); // Simulate reset
598 | } else {
599 | //Type mismatch
600 | let e = format!(
601 | "{} is not of type {}",
602 | self.modifiedconf,
603 | self.data.op_type.as_str()
604 | );
605 | let _ = sender.output(AppMsg::SaveError(e));
606 | }
607 | } else {
608 | //Error
609 | let _ = sender.output(AppMsg::SaveError(message));
610 | }
611 |
612 | self.set_saving(false);
613 | let _ = sender.output(AppMsg::SetBusy(false));
614 | }
615 | OptPageMsg::SetScheme(scheme) => {
616 | info!("OptPageMsg::SetScheme");
617 | self.set_scheme(sourceview5::StyleSchemeManager::default().scheme(&scheme));
618 | }
619 | }
620 | }
621 | }
622 |
--------------------------------------------------------------------------------
/src/ui/preferencespage.rs:
--------------------------------------------------------------------------------
1 | use super::window::AppMsg;
2 | use adw::prelude::*;
3 | use nix_data::config::configfile::NixDataConfig;
4 | use relm4::*;
5 | use relm4_components::open_dialog::*;
6 | use std::path::PathBuf;
7 |
8 | #[tracker::track]
9 | #[derive(Debug)]
10 | pub struct PreferencesPageModel {
11 | prefwindow: adw::PreferencesWindow,
12 | configpath: PathBuf,
13 | origconfigpath: PathBuf,
14 | flake: Option,
15 | origflake: Option,
16 | flakearg: Option,
17 | origflakearg: Option,
18 | generations: Option,
19 | #[tracker::no_eq]
20 | open_dialog: Controller,
21 | #[tracker::no_eq]
22 | flake_file_dialog: Controller,
23 | error: bool,
24 | }
25 |
26 | #[derive(Debug)]
27 | pub enum PreferencesPageMsg {
28 | Show(NixDataConfig),
29 | ShowErr(NixDataConfig),
30 | Open,
31 | OpenFlake,
32 | SetConfigPath(PathBuf),
33 | SetFlakePath(Option),
34 | SetFlakeArg(Option),
35 | Close,
36 | Ignore,
37 | }
38 |
39 | #[relm4::component(pub)]
40 | impl SimpleComponent for PreferencesPageModel {
41 | type Init = gtk::Window;
42 | type Input = PreferencesPageMsg;
43 | type Output = AppMsg;
44 | type Widgets = PreferencesPageWidgets;
45 |
46 | view! {
47 | adw::PreferencesWindow {
48 | set_transient_for: Some(&parent_window),
49 | set_modal: true,
50 | set_search_enabled: false,
51 | connect_close_request[sender] => move |_| {
52 | sender.input(PreferencesPageMsg::Close);
53 | gtk::Inhibit(true)
54 | },
55 | add = &adw::PreferencesPage {
56 | add = &adw::PreferencesGroup {
57 | add = &adw::ActionRow {
58 | set_title: "Configuration file",
59 | add_suffix = >k::Box {
60 | set_orientation: gtk::Orientation::Horizontal,
61 | set_halign: gtk::Align::End,
62 | set_valign: gtk::Align::Center,
63 | set_spacing: 10,
64 | gtk::Button {
65 | gtk::Box {
66 | set_orientation: gtk::Orientation::Horizontal,
67 | set_spacing: 5,
68 | gtk::Image {
69 | set_icon_name: Some("document-open-symbolic"),
70 | },
71 | gtk::Label {
72 | #[watch]
73 | set_label: {
74 | let x = model.configpath.to_str().unwrap_or_default();
75 | if x.is_empty() {
76 | "(None)"
77 | } else {
78 | x
79 | }
80 | }
81 | }
82 | },
83 | connect_clicked[sender] => move |_| {
84 | sender.input(PreferencesPageMsg::Open);
85 | }
86 | },
87 | }
88 | },
89 | add = &adw::ActionRow {
90 | set_title: "Use nix flakes",
91 | add_suffix = >k::Switch {
92 | set_valign: gtk::Align::Center,
93 | connect_state_set[sender] => move |_, b| {
94 | if b {
95 | sender.input(PreferencesPageMsg::SetFlakePath(Some(PathBuf::new())));
96 | } else {
97 | sender.input(PreferencesPageMsg::SetFlakePath(None));
98 | sender.input(PreferencesPageMsg::SetFlakeArg(None));
99 | }
100 | gtk::Inhibit(false)
101 | } @switched,
102 | #[track(model.changed(PreferencesPageModel::flake()))]
103 | #[block_signal(switched)]
104 | set_state: model.flake.is_some()
105 | }
106 | },
107 | add = &adw::ActionRow {
108 | set_title: "Flake file",
109 | #[watch]
110 | set_visible: model.flake.is_some(),
111 | add_suffix = >k::Box {
112 | set_orientation: gtk::Orientation::Horizontal,
113 | set_halign: gtk::Align::End,
114 | set_valign: gtk::Align::Center,
115 | set_spacing: 10,
116 | gtk::Button {
117 | gtk::Box {
118 | set_orientation: gtk::Orientation::Horizontal,
119 | set_spacing: 5,
120 | gtk::Image {
121 | set_icon_name: Some("document-open-symbolic"),
122 | },
123 | gtk::Label {
124 | #[watch]
125 | set_label: {
126 | let x = if let Some(f) = &model.flake {
127 | f.file_name().unwrap_or_default().to_str().unwrap_or_default()
128 | } else {
129 | ""
130 | };
131 | if x.is_empty() {
132 | "(None)"
133 | } else {
134 | x
135 | }
136 | }
137 | }
138 | },
139 | connect_clicked[sender] => move |_| {
140 | sender.input(PreferencesPageMsg::OpenFlake);
141 | }
142 | },
143 | }
144 | },
145 | add = &adw::EntryRow {
146 | #[watch]
147 | set_visible: model.flake.is_some(),
148 | set_title: "Flake arguments (--flake path/to/flake.nix#<THIS ENTRY>)",
149 | connect_changed[sender] => move |x| {
150 | sender.input(PreferencesPageMsg::SetFlakeArg({
151 | let text = x.text().to_string();
152 | if text.is_empty() {
153 | None
154 | } else {
155 | Some(text)
156 | }}));
157 | } @flakeentry,
158 | #[track(model.changed(PreferencesPageModel::flake()))]
159 | #[block_signal(flakeentry)]
160 | set_text: model.flakearg.as_ref().unwrap_or(&String::new())
161 | }
162 |
163 | }
164 | }
165 | }
166 | }
167 |
168 | fn init(
169 | parent_window: Self::Init,
170 | root: &Self::Root,
171 | sender: ComponentSender,
172 | ) -> ComponentParts {
173 | let open_dialog = OpenDialog::builder()
174 | .transient_for_native(root)
175 | .launch(OpenDialogSettings::default())
176 | .forward(sender.input_sender(), |response| match response {
177 | OpenDialogResponse::Accept(path) => PreferencesPageMsg::SetConfigPath(path),
178 | OpenDialogResponse::Cancel => PreferencesPageMsg::Ignore,
179 | });
180 | let flake_file_dialog = OpenDialog::builder()
181 | .transient_for_native(root)
182 | .launch(OpenDialogSettings::default())
183 | .forward(sender.input_sender(), |response| match response {
184 | OpenDialogResponse::Accept(path) => PreferencesPageMsg::SetFlakePath(Some(path)),
185 | OpenDialogResponse::Cancel => PreferencesPageMsg::Ignore,
186 | });
187 | let model = PreferencesPageModel {
188 | prefwindow: root.clone(),
189 | configpath: PathBuf::new(),
190 | origconfigpath: PathBuf::new(),
191 | flake: None,
192 | origflake: None,
193 | flakearg: None,
194 | origflakearg: None,
195 | generations: None,
196 | open_dialog,
197 | flake_file_dialog,
198 | error: false,
199 | tracker: 0,
200 | };
201 |
202 | let widgets = view_output!();
203 |
204 | ComponentParts { model, widgets }
205 | }
206 |
207 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
208 | self.reset();
209 | match msg {
210 | PreferencesPageMsg::Show(config) => {
211 | if let Some(systemconfig) = &config.systemconfig {
212 | self.configpath = PathBuf::from(systemconfig);
213 | self.origconfigpath = PathBuf::from(systemconfig);
214 | }
215 | self.set_flake(config.flake.as_ref().map(PathBuf::from));
216 | self.origflake = self.flake.clone();
217 | self.set_flakearg(config.flakearg);
218 | self.origflakearg = self.flakearg.clone();
219 | self.generations = config.generations;
220 | self.prefwindow.show();
221 | self.error = false;
222 | }
223 | PreferencesPageMsg::ShowErr(config) => {
224 | if let Some(systemconfig) = &config.systemconfig {
225 | self.configpath = PathBuf::from(systemconfig);
226 | self.origconfigpath = PathBuf::from(systemconfig);
227 | }
228 | self.set_flake(config.flake.as_ref().map(PathBuf::from));
229 | self.origflake = self.flake.clone();
230 | self.set_flakearg(config.flakearg);
231 | self.origflakearg = self.flakearg.clone();
232 | self.prefwindow.present();
233 | self.error = true;
234 | }
235 | PreferencesPageMsg::Open => self.open_dialog.emit(OpenDialogMsg::Open),
236 | PreferencesPageMsg::OpenFlake => self.flake_file_dialog.emit(OpenDialogMsg::Open),
237 | PreferencesPageMsg::SetConfigPath(path) => {
238 | self.configpath = path;
239 | }
240 | PreferencesPageMsg::SetFlakePath(path) => {
241 | self.flake = path;
242 | }
243 | PreferencesPageMsg::SetFlakeArg(arg) => {
244 | self.flakearg = arg;
245 | }
246 | PreferencesPageMsg::Close => {
247 | if !self.configpath.eq(&self.origconfigpath)
248 | || !self.flake.eq(&self.origflake)
249 | || !self.flakearg.eq(&self.origflakearg)
250 | || self.error
251 | {
252 | let _ = sender.output(AppMsg::SetConfig(NixDataConfig {
253 | systemconfig: Some(self.configpath.to_string_lossy().to_string()),
254 | flake: self.flake.as_ref().map(|x| x.to_string_lossy().to_string()),
255 | flakearg: self.flakearg.clone(),
256 | generations: self.generations,
257 | }));
258 | }
259 | self.prefwindow.hide();
260 | }
261 | _ => {}
262 | }
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/src/ui/quitdialog.rs:
--------------------------------------------------------------------------------
1 | use super::window::AppMsg;
2 | use adw::prelude::*;
3 | use relm4::*;
4 |
5 | pub struct QuitCheckModel {
6 | hidden: bool,
7 | busy: bool,
8 | }
9 |
10 | #[derive(Debug)]
11 | pub enum QuitCheckMsg {
12 | Show,
13 | Save,
14 | Rebuild,
15 | Quit,
16 | }
17 |
18 | #[relm4::component(pub)]
19 | impl SimpleComponent for QuitCheckModel {
20 | type Init = gtk::Window;
21 | type Input = QuitCheckMsg;
22 | type Output = AppMsg;
23 | type Widgets = QuitCheckWidgets;
24 |
25 | view! {
26 | dialog = gtk::MessageDialog {
27 | set_transient_for: Some(&init),
28 | set_modal: true,
29 | #[watch]
30 | set_visible: !model.hidden,
31 | set_resizable: false,
32 | #[watch]
33 | set_sensitive: !model.busy,
34 | set_text: Some("Save Changes?"),
35 | set_secondary_text: Some("Unsaved changes will be lost. You should rebuild your system now to ensure you configured everything properly. You can also save your configuration, however is is possible that your configuration is save in an unbuildable state."),
36 | set_default_width: 500,
37 | add_button: ("Quit", gtk::ResponseType::Close),
38 | add_button: ("Save", gtk::ResponseType::Reject),
39 | add_button: ("Rebuild", gtk::ResponseType::Accept),
40 | connect_response[sender] => move |_, resp| {
41 | sender.input(match resp {
42 | gtk::ResponseType::Accept => QuitCheckMsg::Rebuild,
43 | gtk::ResponseType::Reject => QuitCheckMsg::Save,
44 | gtk::ResponseType::Close => QuitCheckMsg::Quit,
45 | _ => unreachable!(),
46 | });
47 | }
48 | }
49 | }
50 |
51 | fn init(
52 | init: Self::Init,
53 | root: &Self::Root,
54 | sender: ComponentSender,
55 | ) -> ComponentParts {
56 | let model = QuitCheckModel {
57 | hidden: true,
58 | busy: false,
59 | };
60 |
61 | let widgets = view_output!();
62 |
63 | let rebuild_widget = widgets
64 | .dialog
65 | .widget_for_response(gtk::ResponseType::Accept)
66 | .expect("No button for accept response set");
67 | rebuild_widget.add_css_class("suggested-action");
68 | let save_widget = widgets
69 | .dialog
70 | .widget_for_response(gtk::ResponseType::Reject)
71 | .expect("No button for reject response set");
72 | save_widget.add_css_class("warning");
73 | let quit_widget = widgets
74 | .dialog
75 | .widget_for_response(gtk::ResponseType::Close)
76 | .expect("No button for close response set");
77 | quit_widget.add_css_class("destructive-action");
78 |
79 | ComponentParts { model, widgets }
80 | }
81 |
82 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
83 | match msg {
84 | QuitCheckMsg::Show => {
85 | self.hidden = false;
86 | self.busy = false;
87 | }
88 | QuitCheckMsg::Save => {
89 | self.busy = true;
90 | let _ = sender.output(AppMsg::SaveQuit);
91 | }
92 | QuitCheckMsg::Rebuild => {
93 | self.hidden = true;
94 | let _ = sender.output(AppMsg::Rebuild);
95 | }
96 | QuitCheckMsg::Quit => {
97 | relm4::main_application().quit();
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/ui/rebuild.rs:
--------------------------------------------------------------------------------
1 | use super::window::AppMsg;
2 | use crate::config::LIBEXECDIR;
3 | use adw::prelude::*;
4 | use gtk::{gio, glib};
5 | use relm4::*;
6 | use std::io::Write;
7 | use std::process::Command;
8 | use std::process::*;
9 | use vte::{TerminalExt, TerminalExtManual};
10 |
11 | #[tracker::track]
12 | pub struct RebuildModel {
13 | hidden: bool,
14 | status: RebuildStatus,
15 | config: String,
16 | path: String,
17 | flake: Option,
18 | scheme: Option,
19 | terminal: vte::Terminal,
20 | }
21 |
22 | #[derive(Debug)]
23 | pub enum RebuildMsg {
24 | Rebuild(String, String, Option),
25 | FinishSuccess,
26 | FinishError(Option),
27 | WriteConfig(String, String, bool),
28 | KeepEditing,
29 | Reset,
30 | Save,
31 | Close,
32 | SetScheme(String),
33 | WriteConfigQuit(String, String),
34 | Quit,
35 | }
36 |
37 | #[derive(PartialEq)]
38 | enum RebuildStatus {
39 | Building,
40 | Success,
41 | Error,
42 | }
43 |
44 | #[relm4::component(pub)]
45 | impl SimpleComponent for RebuildModel {
46 | type Init = gtk::Window;
47 | type Input = RebuildMsg;
48 | type Output = AppMsg;
49 | type Widgets = RebuildWidgets;
50 |
51 | view! {
52 | dialog = adw::Window {
53 | set_transient_for: Some(&parent_window),
54 | set_modal: true,
55 | #[track(model.changed(RebuildModel::hidden()))]
56 | set_default_width: 500,
57 | #[track(model.changed(RebuildModel::hidden()))]
58 | set_default_height: 200,//295),
59 | set_resizable: true,
60 | #[watch]
61 | set_visible: !model.hidden,
62 | add_css_class: "dialog",
63 | add_css_class: "message",
64 | gtk::Box {
65 | set_orientation: gtk::Orientation::Vertical,
66 | #[name(statusstack)]
67 | gtk::Stack {
68 | set_margin_top: 20,
69 | set_transition_type: gtk::StackTransitionType::Crossfade,
70 | set_vhomogeneous: false,
71 | #[name(building)]
72 | gtk::Box {
73 | set_orientation: gtk::Orientation::Vertical,
74 | set_spacing: 10,
75 | gtk::Spinner {
76 | set_spinning: true,
77 | set_height_request: 60,
78 | },
79 | gtk::Label {
80 | set_label: "Building...",
81 | add_css_class: "title-1",
82 | },
83 | },
84 | #[name(success)]
85 | gtk::Box {
86 | set_orientation: gtk::Orientation::Vertical,
87 | set_spacing: 10,
88 | gtk::Image {
89 | add_css_class: "success",
90 | set_icon_name: Some("object-select-symbolic"),
91 | set_pixel_size: 128,
92 | },
93 | gtk::Label {
94 | set_label: "Done!",
95 | add_css_class: "title-1",
96 | },
97 | gtk::Label {
98 | set_label: "Rebuild successful!",
99 | add_css_class: "dim-label",
100 | }
101 | },
102 | #[name(error)]
103 | gtk::Box {
104 | set_orientation: gtk::Orientation::Vertical,
105 | set_spacing: 10,
106 | gtk::Image {
107 | add_css_class: "error",
108 | set_icon_name: Some("dialog-error-symbolic"),
109 | set_pixel_size: 128,
110 | },
111 | gtk::Label {
112 | set_label: "Error!",
113 | add_css_class: "title-1",
114 | },
115 | gtk::Label {
116 | set_label: "Rebuild failed! See below for error message.",
117 | add_css_class: "dim-label",
118 | }
119 | }
120 | },
121 | gtk::Frame {
122 | set_margin_all: 20,
123 | #[name(scrollwindow)]
124 | gtk::ScrolledWindow {
125 | set_max_content_height: 500,
126 | set_min_content_height: 100,
127 | #[local_ref]
128 | terminal -> vte::Terminal {
129 | set_vexpand: true,
130 | set_hexpand: true,
131 | set_input_enabled: false,
132 | connect_child_exited[sender] => move |_term, status| {
133 | if status == 0 {
134 | sender.input(RebuildMsg::FinishSuccess);
135 | } else {
136 | sender.input(RebuildMsg::FinishError(None));
137 | }
138 | }
139 | }
140 | }
141 | },
142 | gtk::Box {
143 | add_css_class: "dialog-action-area",
144 | set_orientation: gtk::Orientation::Horizontal,
145 | set_homogeneous: true,
146 | #[track(model.changed(RebuildModel::status()))]
147 | set_visible: model.status != RebuildStatus::Building,
148 | gtk::Button {
149 | set_label: "Close",
150 | #[track(model.changed(RebuildModel::status()))]
151 | set_visible: model.status == RebuildStatus::Success,
152 | connect_clicked[sender] => move |_| {
153 | sender.input(RebuildMsg::Save)
154 | }
155 | },
156 | gtk::Button {
157 | add_css_class: "destructive-action",
158 | set_label: "Save Anyways",
159 | #[track(model.changed(RebuildModel::status()))]
160 | set_visible: model.status == RebuildStatus::Error,
161 | connect_clicked[sender] => move |_| {
162 | sender.input(RebuildMsg::Save)
163 | }
164 | },
165 | gtk::Button {
166 | set_label: "Reset Changes",
167 | #[track(model.changed(RebuildModel::status()))]
168 | set_visible: model.status == RebuildStatus::Error,
169 | connect_clicked[sender] => move |_| {
170 | sender.input(RebuildMsg::Reset)
171 | }
172 | },
173 | gtk::Button {
174 | set_label: "Keep Editing",
175 | #[track(model.changed(RebuildModel::status()))]
176 | set_visible: model.status == RebuildStatus::Error,
177 | connect_clicked[sender] => move |_| {
178 | sender.input(RebuildMsg::KeepEditing)
179 | }
180 | }
181 | }
182 | }
183 | }
184 | }
185 |
186 | fn pre_view() {
187 | match model.status {
188 | RebuildStatus::Building => {
189 | statusstack.set_visible_child(building);
190 | }
191 | RebuildStatus::Success => statusstack.set_visible_child(success),
192 | RebuildStatus::Error => statusstack.set_visible_child(error),
193 | }
194 | }
195 |
196 | fn init(
197 | parent_window: Self::Init,
198 | root: &Self::Root,
199 | sender: ComponentSender,
200 | ) -> ComponentParts {
201 | let model = RebuildModel {
202 | hidden: true,
203 | status: RebuildStatus::Building,
204 | config: String::new(),
205 | path: String::new(),
206 | flake: None,
207 | scheme: None,
208 | terminal: vte::Terminal::new(),
209 | tracker: 0,
210 | };
211 |
212 | let terminal = &model.terminal;
213 | let widgets = view_output!();
214 |
215 | ComponentParts { model, widgets }
216 | }
217 |
218 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
219 | self.reset();
220 | match msg {
221 | RebuildMsg::Rebuild(f, path, flake) => {
222 | self.update_hidden(|x| *x = false);
223 | self.set_config(f.to_string());
224 | self.set_path(path.to_string());
225 | self.set_flake(flake.clone());
226 | self.set_status(RebuildStatus::Building);
227 | if let Some(flake) = flake {
228 | self.terminal.spawn_async(
229 | vte::PtyFlags::DEFAULT,
230 | Some("/"),
231 | &[
232 | "/usr/bin/env",
233 | "pkexec",
234 | &format!("{}/nce-helper", LIBEXECDIR),
235 | "write-rebuild",
236 | "--content",
237 | &f,
238 | "--path",
239 | &path,
240 | "--",
241 | "switch",
242 | "--flake",
243 | &flake,
244 | ],
245 | &[],
246 | glib::SpawnFlags::DEFAULT,
247 | || (),
248 | -1,
249 | gio::Cancellable::NONE,
250 | |_, _, _| (),
251 | );
252 | } else {
253 | self.terminal.spawn_async(
254 | vte::PtyFlags::DEFAULT,
255 | Some("/"),
256 | &[
257 | "/usr/bin/env",
258 | "pkexec",
259 | &format!("{}/nce-helper", LIBEXECDIR),
260 | "write-rebuild",
261 | "--content",
262 | &f,
263 | "--path",
264 | &path,
265 | "--",
266 | "switch",
267 | "-I",
268 | &format!("nixos-config={}", path),
269 | ],
270 | &[],
271 | glib::SpawnFlags::DEFAULT,
272 | || (),
273 | -1,
274 | gio::Cancellable::NONE,
275 | |_, _, _| (),
276 | );
277 | }
278 | }
279 | RebuildMsg::FinishSuccess => {
280 | self.set_status(RebuildStatus::Success);
281 | }
282 | RebuildMsg::FinishError(_msg) => {
283 | self.update_hidden(|x| *x = false);
284 | self.set_status(RebuildStatus::Error);
285 | }
286 | RebuildMsg::KeepEditing => {
287 | sender.input(RebuildMsg::WriteConfig(
288 | self.config.to_string(),
289 | self.path.to_string(),
290 | false,
291 | ));
292 | sender.input(RebuildMsg::Close);
293 | }
294 | RebuildMsg::Reset => {
295 | let _ = sender.output(AppMsg::ResetConfig);
296 | sender.input(RebuildMsg::Close);
297 | }
298 | RebuildMsg::Save => {
299 | let _ = sender.output(AppMsg::SaveConfig);
300 | sender.input(RebuildMsg::Close);
301 | }
302 | RebuildMsg::Close => {
303 | self.terminal.reset(true, true);
304 | self.terminal.spawn_async(
305 | vte::PtyFlags::DEFAULT,
306 | Some("/"),
307 | &["/usr/bin/env", "clear"],
308 | &[],
309 | glib::SpawnFlags::DEFAULT,
310 | || (),
311 | -1,
312 | gio::Cancellable::NONE,
313 | |_, _, _| (),
314 | );
315 | self.update_hidden(|x| *x = true);
316 | }
317 | RebuildMsg::SetScheme(scheme) => {
318 | self.set_scheme(sourceview5::StyleSchemeManager::default().scheme(&scheme));
319 | }
320 | RebuildMsg::WriteConfigQuit(f, path) => {
321 | sender.input(RebuildMsg::WriteConfig(f, path, true));
322 | }
323 | RebuildMsg::WriteConfig(f, path, quit) => {
324 | let mut writecmd = Command::new("pkexec")
325 | .arg(&format!("{}/nce-helper", LIBEXECDIR))
326 | .arg("config")
327 | .arg("--output")
328 | .arg(path)
329 | .stdin(Stdio::piped())
330 | .spawn()
331 | .unwrap();
332 | writecmd
333 | .stdin
334 | .as_mut()
335 | .ok_or("stdin not available")
336 | .unwrap()
337 | .write_all(f.as_bytes())
338 | .unwrap();
339 | writecmd.wait().unwrap();
340 | if quit {
341 | sender.input(RebuildMsg::Quit);
342 | }
343 | }
344 | RebuildMsg::Quit => {
345 | let _ = sender.output(AppMsg::Close);
346 | }
347 | }
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/src/ui/savechecking.rs:
--------------------------------------------------------------------------------
1 | use super::window::AppMsg;
2 | use crate::ui::optionpage::OptPageMsg;
3 | use adw::prelude::*;
4 | use log::{debug, info};
5 | use relm4::*;
6 | use sourceview5::prelude::*;
7 | use std::{path::Path, process::Command};
8 |
9 | pub struct SaveAsyncHandler;
10 |
11 | #[derive(Debug)]
12 | pub enum SaveAsyncHandlerMsg {
13 | SaveCheck(String, String, String, Vec),
14 | }
15 |
16 | impl Worker for SaveAsyncHandler {
17 | type Init = ();
18 | type Input = SaveAsyncHandlerMsg;
19 | type Output = OptPageMsg;
20 |
21 | fn init(_params: Self::Init, _sender: relm4::ComponentSender) -> Self {
22 | Self
23 | }
24 |
25 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
26 | match msg {
27 | SaveAsyncHandlerMsg::SaveCheck(opt, refopt, conf, alloptions) => {
28 | info!("Received SaveCheck message");
29 | debug!("opt: {}\nrefopt: {}", opt, refopt);
30 | // For users.users..autoSubUidGidRange
31 | // (options.users.users.type.getSubOptions []).autoSubUidGidRange.type.check
32 | let checkcmd = {
33 | let p = refopt.split('.').collect::>();
34 | let mut r: Vec> = vec![vec![]];
35 | let mut indexvec: Vec = vec![];
36 | let mut j = 0;
37 | for i in 0..p.len() {
38 | if p[i] == "*" || p[i] == "" {
39 | r.push(vec![]);
40 | if let Ok(x) = opt.split('.').collect::>()[i].parse::() {
41 | indexvec.push(x);
42 | }
43 | j += 1;
44 | } else if alloptions.contains(&p[..i].join(".")) && i + 1 < p.len()
45 | /* Check if option exists */
46 | {
47 | r.push(vec![]);
48 | j += 1;
49 | r[j].push(p[i].to_string());
50 | } else {
51 | r[j].push(p[i].to_string());
52 | }
53 | }
54 | let mut s = format!("options.{}", r[0].join("."));
55 | for y in r[1..].iter() {
56 | s = format!("({}.type.getSubOptions []).{}", s, y.join("."));
57 | }
58 | format!("{}.type.check", s)
59 | };
60 | let output =
61 | if Path::new("/nix/var/nix/profiles/per-user/root/channels/nixos").exists() {
62 | Command::new("nix-instantiate")
63 | .arg("--eval")
64 | .arg("--expr")
65 | .arg(format!(
66 | "with import {{}}; {} ({})",
67 | checkcmd, conf
68 | ))
69 | .output()
70 | } else {
71 | match Command::new("nix").arg("eval").arg("nixpkgs#path").output() {
72 | Ok(nixpath) => {
73 | let nixospath = format!(
74 | "{}/nixos/lib/eval-config.nix",
75 | String::from_utf8_lossy(&nixpath.stdout).trim()
76 | );
77 | Command::new("nix-instantiate")
78 | .arg("--eval")
79 | .arg("--expr")
80 | .arg(format!(
81 | "with import {} {{ modules = []; }}; {} ({})",
82 | nixospath, checkcmd, conf
83 | ))
84 | .output()
85 | }
86 | Err(e) => Err(e),
87 | }
88 | };
89 | let (b, s) = match output {
90 | Ok(output) => {
91 | if output.status.success() {
92 | let output = String::from_utf8(output.stdout).unwrap();
93 | (true, output)
94 | } else {
95 | let output = String::from_utf8(output.stderr).unwrap();
96 | (false, output)
97 | }
98 | }
99 | Err(e) => (false, e.to_string()),
100 | };
101 | let _ = sender.output(OptPageMsg::DoneSaving(b, s));
102 | }
103 | }
104 | }
105 | }
106 |
107 | pub struct SaveErrorModel {
108 | hidden: bool,
109 | msg: String,
110 | scheme: Option,
111 | }
112 |
113 | #[derive(Debug)]
114 | pub enum SaveErrorMsg {
115 | Show(String),
116 | SaveError,
117 | Reset,
118 | Cancel,
119 | SetScheme(String),
120 | }
121 |
122 | #[relm4::component(pub)]
123 | impl SimpleComponent for SaveErrorModel {
124 | type Init = gtk::Window;
125 | type Input = SaveErrorMsg;
126 | type Output = AppMsg;
127 | type Widgets = SaveErrorWidgets;
128 |
129 | view! {
130 | dialog = gtk::MessageDialog {
131 | set_transient_for: Some(&parent_window),
132 | set_modal: true,
133 | #[watch]
134 | set_visible: !model.hidden,
135 | set_text: Some("Invalid configuration"),
136 | set_secondary_text: Some("Please fix the errors and try again."),
137 | #[watch]
138 | set_default_height: -1,
139 | set_default_width: 500,
140 | add_button: ("Keep changes", gtk::ResponseType::DeleteEvent),
141 | add_button: ("Reset", gtk::ResponseType::Reject),
142 | add_button: ("Edit", gtk::ResponseType::Cancel),
143 | connect_response[sender] => move |_, resp| {
144 | sender.input(match resp {
145 | gtk::ResponseType::DeleteEvent => SaveErrorMsg::SaveError,
146 | gtk::ResponseType::Reject => SaveErrorMsg::Reset,
147 | gtk::ResponseType::Cancel => SaveErrorMsg::Cancel,
148 | _ => unreachable!(),
149 | });
150 | },
151 | }
152 | }
153 |
154 | additional_fields! {
155 | frame: gtk::Frame,
156 | msgbuf: sourceview5::Buffer,
157 | }
158 |
159 | fn init(
160 | parent_window: Self::Init,
161 | root: &Self::Root,
162 | sender: ComponentSender,
163 | ) -> ComponentParts {
164 | let model = SaveErrorModel {
165 | hidden: true,
166 | msg: String::default(),
167 | scheme: None,
168 | };
169 |
170 | view! {
171 | frame = gtk::Frame {
172 | set_margin_start: 20,
173 | set_margin_end: 20,
174 | gtk::ScrolledWindow {
175 | set_vscrollbar_policy: gtk::PolicyType::Never,
176 | sourceview5::View {
177 | set_vexpand: true,
178 | set_editable: false,
179 | set_cursor_visible: false,
180 | set_monospace: true,
181 | set_top_margin: 5,
182 | set_bottom_margin: 5,
183 | set_left_margin: 5,
184 | #[wrap(Some)]
185 | set_buffer: msgbuf = &sourceview5::Buffer {
186 | #[track(model.scheme)]
187 | set_style_scheme: model.scheme.as_ref(),
188 | },
189 | }
190 | }
191 | }
192 | }
193 |
194 | let widgets = view_output!();
195 | widgets.dialog.content_area().append(&widgets.frame);
196 |
197 | let accept_widget = widgets
198 | .dialog
199 | .widget_for_response(gtk::ResponseType::DeleteEvent)
200 | .expect("No button for accept response set");
201 | accept_widget.add_css_class("destructive-action");
202 | ComponentParts { model, widgets }
203 | }
204 |
205 | fn pre_view() {
206 | msgbuf.set_text(&model.msg);
207 | }
208 |
209 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
210 | match msg {
211 | SaveErrorMsg::Show(s) => {
212 | self.hidden = false;
213 | self.msg = s;
214 | }
215 | SaveErrorMsg::SaveError => {
216 | self.hidden = true;
217 | let _ = sender.output(AppMsg::SaveWithError);
218 | }
219 | SaveErrorMsg::Reset => {
220 | self.hidden = true;
221 | let _ = sender.output(AppMsg::SaveErrorReset);
222 | }
223 | SaveErrorMsg::Cancel => self.hidden = true,
224 | SaveErrorMsg::SetScheme(scheme) => {
225 | self.scheme = sourceview5::StyleSchemeManager::default().scheme(&scheme);
226 | }
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/ui/searchentry.rs:
--------------------------------------------------------------------------------
1 | use super::window::*;
2 | use adw::prelude::*;
3 | use relm4::{factory::*, *};
4 | use std::collections::HashMap;
5 |
6 | pub struct SearchEntryModel {
7 | hidden: bool,
8 | position: Vec,
9 | data: FactoryVecDeque,
10 | nameopts: FactoryVecDeque,
11 | customopt: Vec,
12 | }
13 |
14 | #[derive(Debug)]
15 | pub enum SearchEntryMsg {
16 | Show(Vec, Vec),
17 | Close,
18 | Save(Option>),
19 | SetName(String, usize),
20 | }
21 |
22 | #[relm4::component(pub)]
23 | impl SimpleComponent for SearchEntryModel {
24 | type Init = gtk::Window;
25 | type Input = SearchEntryMsg;
26 | type Output = AppMsg;
27 | type Widgets = SearchEntryWidgets;
28 |
29 | view! {
30 | window = gtk::Dialog {
31 | set_default_height: -1,
32 | #[wrap(Some)]
33 | set_titlebar = &adw::HeaderBar {
34 | add_css_class: "flat"
35 | },
36 | set_default_width: 500,
37 | set_resizable: false,
38 | set_transient_for: Some(&parent_window),
39 | set_modal: true,
40 | #[watch]
41 | set_visible: !model.hidden,
42 | connect_close_request[sender] => move |_| {
43 | sender.input(SearchEntryMsg::Close);
44 | gtk::Inhibit(true)
45 | },
46 | connect_visible_notify => move |x| {
47 | if x.get_visible() {
48 | x.grab_focus();
49 | }
50 | },
51 | #[name(main_box)]
52 | gtk::Box {
53 | set_orientation: gtk::Orientation::Vertical,
54 | adw::Clamp {
55 | gtk::Box {
56 | set_margin_start: 15,
57 | set_margin_end: 15,
58 | set_spacing: 15,
59 | set_orientation: gtk::Orientation::Vertical,
60 | adw::PreferencesGroup {
61 | #[local_ref]
62 | add = datalistbox -> gtk::ListBox {
63 | add_css_class: "boxed-list",
64 | },
65 | #[watch]
66 | set_visible: !model.data.is_empty(),
67 | },
68 | adw::PreferencesGroup {
69 | #[local_ref]
70 | add = nameoptslistbox -> gtk::ListBox {
71 | set_margin_bottom: 15,
72 | add_css_class: "boxed-list",
73 | append = &adw::ActionRow {
74 | #[watch]
75 | set_title: &model.customopt.join("."),
76 | #[watch]
77 | set_sensitive: !model.customopt.contains(&String::from("<name>")),
78 | set_selectable: false,
79 | set_activatable: true,
80 | add_suffix = >k::Image {
81 | set_icon_name: Some("list-add-symbolic"),
82 | add_css_class: "accent",
83 | },
84 | connect_activated[sender] => move |_| {
85 | sender.input(SearchEntryMsg::Save(None));
86 | }
87 | }
88 | }
89 | },
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
96 | fn init(
97 | parent_window: Self::Init,
98 | root: &Self::Root,
99 | sender: ComponentSender,
100 | ) -> ComponentParts {
101 | let model = SearchEntryModel {
102 | hidden: true,
103 | position: Vec::default(),
104 | data: FactoryVecDeque::new(gtk::ListBox::new(), sender.input_sender()),
105 | nameopts: FactoryVecDeque::new(gtk::ListBox::new(), sender.input_sender()),
106 | customopt: Vec::default(),
107 | };
108 | let datalistbox = model.data.widget();
109 | let nameoptslistbox = model.nameopts.widget();
110 |
111 | let widgets = view_output!();
112 |
113 | ComponentParts { model, widgets }
114 | }
115 |
116 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
117 | let mut data_guard = self.data.guard();
118 | let mut nameopts_guard = self.nameopts.guard();
119 | match msg {
120 | SearchEntryMsg::Show(pos, optdata) => {
121 | data_guard.clear();
122 | nameopts_guard.clear();
123 | for v in optdata {
124 | data_guard.push_back(v);
125 | }
126 | for i in 0..pos.len() {
127 | if pos[i] == "" {
128 | let mut op = pos.to_vec();
129 | op[i] = "<name>".to_string();
130 | nameopts_guard
131 | .push_back((op.join(".").replace("", "<name>"), i));
132 | }
133 | }
134 | self.position = pos.clone();
135 | self.customopt = pos
136 | .iter()
137 | .map(|x| x.replace("", "<name>"))
138 | .collect();
139 | self.hidden = false;
140 | }
141 | SearchEntryMsg::Close => {
142 | self.hidden = true;
143 | data_guard.clear();
144 | }
145 | SearchEntryMsg::Save(dest) => {
146 | self.hidden = true;
147 | let mut n: HashMap = HashMap::new();
148 | if dest.is_none() {
149 | for i in 0..self.position.len() {
150 | if self.position[i] == "" {
151 | let mut existing = vec![];
152 | for i in 0..data_guard.len() {
153 | let dvec = &data_guard.get(i).unwrap().value;
154 | if !existing.contains(&dvec[i].to_string())
155 | && dvec[..i] == self.customopt[..i]
156 | {
157 | existing.push(dvec[i].to_string());
158 | }
159 | }
160 | if !existing.contains(&self.customopt[i]) {
161 | let _ = sender.output(AppMsg::AddNameAttr(
162 | Some(self.customopt[..i].join(".")),
163 | self.customopt[i].clone(),
164 | ));
165 | }
166 | } else if self.position[i] == "*" {
167 | let mut existing = vec![];
168 | for i in 0..data_guard.len() {
169 | let dvec = &data_guard.get(i).unwrap().value;
170 | if dvec[..i] == self.customopt[..i] {
171 | if let Some(v) = dvec.get(i) {
172 | if let Ok(x) = v.parse::() {
173 | if !existing.contains(&x) {
174 | existing.push(x);
175 | }
176 | }
177 | }
178 | }
179 | }
180 | existing.sort_unstable();
181 | let num = if let Some(x) = existing.last() {
182 | *x + 1
183 | } else {
184 | 0
185 | };
186 | n.insert(i, num);
187 | let _ = sender.output(AppMsg::AddStar(self.customopt[..i].join(".")));
188 | }
189 | }
190 | for (k, v) in n {
191 | self.customopt[k] = v.to_string();
192 | }
193 | }
194 | data_guard.clear();
195 | let _ = sender.output(AppMsg::OpenSearchOption(
196 | if let Some(x) = dest {
197 | x
198 | } else {
199 | self.customopt.to_vec()
200 | },
201 | self.position.to_vec(),
202 | ));
203 | }
204 | SearchEntryMsg::SetName(v, i) => {
205 | if self.position.get(i).is_some() {
206 | if v.is_empty() {
207 | self.customopt[i] = String::from("<name>");
208 | } else {
209 | self.customopt[i] = v;
210 | }
211 | }
212 | }
213 | }
214 | }
215 | }
216 |
217 | #[derive(Debug, PartialEq)]
218 | struct SearchEntryOption {
219 | value: Vec,
220 | }
221 |
222 | #[derive(Debug)]
223 | enum SearchEntryOptionOutput {
224 | Save(Vec),
225 | }
226 |
227 | #[relm4::factory]
228 | impl FactoryComponent for SearchEntryOption {
229 | type Init = String;
230 | type Input = ();
231 | type Output = SearchEntryOptionOutput;
232 | type Widgets = CounterWidgets;
233 | type ParentWidget = gtk::ListBox;
234 | type ParentInput = SearchEntryMsg;
235 | type CommandOutput = ();
236 |
237 | view! {
238 | adw::ActionRow {
239 | #[watch]
240 | set_title: &self.value.join("."),
241 | set_selectable: false,
242 | set_activatable: true,
243 | connect_activated[sender, value = self.value.clone()] => move |_| {
244 | sender.output(SearchEntryOptionOutput::Save(value.to_vec()));
245 | }
246 | }
247 | }
248 |
249 | fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self {
250 | let v = value
251 | .split('.')
252 | .map(|x| x.to_string())
253 | .collect::>();
254 | Self { value: v }
255 | }
256 |
257 | fn output_to_parent_input(output: Self::Output) -> Option {
258 | Some(match output {
259 | SearchEntryOptionOutput::Save(v) => SearchEntryMsg::Save(Some(v)),
260 | })
261 | }
262 | }
263 |
264 | #[derive(Debug, PartialEq)]
265 | struct SearchNameEntryOption {
266 | value: String,
267 | index: usize,
268 | }
269 |
270 | #[derive(Debug)]
271 | enum SearchNameEntryOptionOutput {
272 | SetName(String, usize),
273 | }
274 |
275 | #[relm4::factory]
276 | impl FactoryComponent for SearchNameEntryOption {
277 | type Init = (String, usize);
278 | type Input = ();
279 | type Output = SearchNameEntryOptionOutput;
280 | type Widgets = SearchNameWidgets;
281 | type ParentWidget = gtk::ListBox;
282 | type ParentInput = SearchEntryMsg;
283 | type CommandOutput = ();
284 |
285 | view! {
286 | adw::ActionRow {
287 | #[watch]
288 | set_title: &self.value,
289 | add_suffix = >k::Entry {
290 | set_valign: gtk::Align::Center,
291 | set_placeholder_text: Some(""),
292 | set_buffer = >k::EntryBuffer {
293 | connect_text_notify[sender, index = self.index] => move |x| {
294 | sender.output(SearchNameEntryOptionOutput::SetName(x.text(), index));
295 | }
296 | }
297 | },
298 | set_selectable: false,
299 | set_activatable: false,
300 | }
301 | }
302 |
303 | fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self {
304 | Self {
305 | value: value.0,
306 | index: value.1,
307 | }
308 | }
309 |
310 | fn output_to_parent_input(output: Self::Output) -> Option {
311 | Some(match output {
312 | SearchNameEntryOptionOutput::SetName(x, i) => SearchEntryMsg::SetName(x, i),
313 | })
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/ui/searchfactory.rs:
--------------------------------------------------------------------------------
1 | use super::searchpage::SearchPageMsg;
2 | use adw::prelude::*;
3 | use relm4::{factory::*, *};
4 |
5 | #[derive(Default, Debug, PartialEq, Eq)]
6 | pub struct SearchOption {
7 | pub value: Vec,
8 | pub configured: bool,
9 | pub modified: bool,
10 | }
11 |
12 | #[relm4::factory(pub)]
13 | impl FactoryComponent for SearchOption {
14 | type Init = SearchOption;
15 | type Input = ();
16 | type Output = ();
17 | type Widgets = SearchOptionWidgets;
18 | type ParentWidget = gtk::ListBox;
19 | type ParentInput = SearchPageMsg;
20 | type CommandOutput = ();
21 |
22 | view! {
23 | adw::PreferencesRow {
24 | #[wrap(Some)]
25 | set_child = >k::Box {
26 | set_orientation: gtk::Orientation::Horizontal,
27 | set_spacing: 6,
28 | set_margin_all: 15,
29 | gtk::Label {
30 | set_text: &self.value.join("."),
31 | },
32 | gtk::Separator {
33 | set_hexpand: true,
34 | set_opacity: 0.0,
35 | },
36 | gtk::Image {
37 | set_icon_name: if self.modified { Some("system-run-symbolic") } else { Some("object-select-symbolic") },
38 | set_visible: self.configured || self.modified,
39 | },
40 | },
41 | set_title: &self.value.join("."),
42 | }
43 | }
44 |
45 | fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self {
46 | value
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/ui/searchpage.rs:
--------------------------------------------------------------------------------
1 | use super::{searchfactory::SearchOption, window::*};
2 | use crate::parse::config::opconfigured;
3 | use adw::prelude::*;
4 | use relm4::{factory::*, *};
5 | use std::{cmp::Ordering, collections::HashMap};
6 |
7 | #[derive(Debug)]
8 | pub enum SearchPageMsg {
9 | Search(String, HashMap),
10 | OpenOption(Vec, Option>),
11 | LoadOptions(Vec<(String, bool, String)>),
12 | SetModifiedOnly(bool, bool),
13 | }
14 |
15 | pub struct SearchPageModel {
16 | pub options: Vec<(String, bool, String)>,
17 | pub oplst: FactoryVecDeque,
18 | query: String,
19 | modifiedonly: bool,
20 | }
21 |
22 | #[relm4::component(pub)]
23 | impl SimpleComponent for SearchPageModel {
24 | type Init = ();
25 | type Input = SearchPageMsg;
26 | type Output = AppMsg;
27 | type Widgets = SearchPageWidgets;
28 |
29 | view! {
30 | view = gtk::Stack {
31 | set_transition_type: gtk::StackTransitionType::Crossfade,
32 | #[name(options)]
33 | adw::PreferencesPage {
34 | set_title: "Attributes",
35 | add = &adw::PreferencesGroup {
36 | set_title: "Options",
37 | #[local_ref]
38 | add = oplstbox -> gtk::ListBox {
39 | add_css_class: "boxed-list",
40 | set_selection_mode: gtk::SelectionMode::None,
41 | connect_row_activated[sender] => move |_, y| {
42 | if let Ok(l) = y.clone().downcast::() {
43 | let text = l.title().to_string();
44 | let v = text.split('.').map(|x| x.to_string()).collect::>();
45 | sender.input(SearchPageMsg::OpenOption(v, None));
46 | }
47 | },
48 | },
49 | }
50 | },
51 | #[name(empty)]
52 | gtk::Box {
53 | set_orientation: gtk::Orientation::Vertical,
54 | set_valign: gtk::Align::Center,
55 | adw::StatusPage {
56 | set_icon_name: Some("edit-find-symbolic"),
57 | set_title: "No options found!",
58 | set_description: Some("Try a different search"),
59 | },
60 | }
61 | }
62 | }
63 |
64 | fn pre_view() {
65 | if model.oplst.is_empty() {
66 | view.set_visible_child(empty);
67 | } else {
68 | view.set_visible_child(options);
69 | }
70 | }
71 |
72 | fn init(
73 | _value: Self::Init,
74 | root: &Self::Root,
75 | sender: ComponentSender,
76 | ) -> ComponentParts {
77 | let model = SearchPageModel {
78 | options: vec![],
79 | oplst: FactoryVecDeque::new(gtk::ListBox::new(), sender.input_sender()),
80 | query: String::default(),
81 | modifiedonly: false,
82 | };
83 | let oplstbox = model.oplst.widget();
84 |
85 | let widgets = view_output!();
86 |
87 | ComponentParts { model, widgets }
88 | }
89 |
90 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
91 | let mut oplst_guard = self.oplst.guard();
92 | match msg {
93 | SearchPageMsg::Search(query, editedopts) => {
94 | self.query = query.to_string();
95 | oplst_guard.clear();
96 | let q = query.split_whitespace();
97 | let mut sortedoptions = self.options.clone();
98 | if q.clone().any(|x| x.len() > 2) {
99 | sortedoptions = sortedoptions
100 | .iter()
101 | .filter(|x| {
102 | for part in q.clone() {
103 | if x.0.to_lowercase().contains(&part.to_lowercase()) {
104 | return true;
105 | }
106 | }
107 | false
108 | })
109 | .map(|x| x.to_owned())
110 | .collect::>();
111 | sortedoptions.sort_by(|a, b| {
112 | let mut acount = 0;
113 | let mut bcount = 0;
114 | for part in q.clone() {
115 | acount += a.0.to_lowercase().matches(&part.to_lowercase()).count();
116 | bcount += b.0.to_lowercase().matches(&part.to_lowercase()).count();
117 | }
118 | match acount.cmp(&bcount) {
119 | Ordering::Less => Ordering::Less,
120 | Ordering::Greater => Ordering::Greater,
121 | Ordering::Equal => a.0.len().cmp(&b.0.len()),
122 | }
123 | });
124 | } else {
125 | sortedoptions.sort_by(|a, b| a.0.len().cmp(&b.0.len()));
126 | }
127 | for opt in sortedoptions {
128 | if q.clone().all(|part| {
129 | opt.0.to_lowercase().contains(&part.to_lowercase())
130 | || if q.clone().any(|x| x.len() > 2) {
131 | opt.2.to_lowercase().contains(&part.to_lowercase())
132 | } else {
133 | false
134 | }
135 | }) {
136 | let configured = opt.1;
137 | let modified = opconfigured(&editedopts, &[], opt.0.clone());
138 | if self.modifiedonly && !(configured || modified) {
139 | continue;
140 | }
141 | oplst_guard.push_back(SearchOption {
142 | value: opt
143 | .0
144 | .split('.')
145 | .map(|s| s.to_string())
146 | .collect::>(),
147 | configured,
148 | modified,
149 | });
150 | }
151 | if oplst_guard.len() >= 1000 {
152 | break;
153 | }
154 | }
155 | }
156 | SearchPageMsg::OpenOption(opt, refpos) => {
157 | if opt.contains(&String::from("*")) || opt.contains(&String::from("")) {
158 | let _ = sender.output(AppMsg::ShowSearchPageEntry(opt));
159 | } else {
160 | let _ = sender.output(AppMsg::OpenOption(
161 | opt.clone(),
162 | if let Some(x) = refpos { x } else { opt },
163 | ));
164 | }
165 | }
166 | SearchPageMsg::LoadOptions(options) => {
167 | self.options = options;
168 | }
169 | SearchPageMsg::SetModifiedOnly(modified, search) => {
170 | self.modifiedonly = modified;
171 | if search {
172 | let _ = sender.output(AppMsg::ShowSearchPage(self.query.clone()));
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/ui/treefactory.rs:
--------------------------------------------------------------------------------
1 | use super::window::*;
2 | use adw::prelude::*;
3 | use relm4::{factory::*, *};
4 |
5 | #[derive(Default, Debug, PartialEq, Eq, Clone)]
6 | pub struct AttrPos {
7 | pub value: Vec,
8 | pub refvalue: Vec,
9 | pub configured: bool,
10 | pub modified: bool,
11 | pub replacefor: Option,
12 | }
13 |
14 | #[relm4::factory(pub)]
15 | impl FactoryComponent for AttrPos {
16 | type Init = AttrPos;
17 | type Input = AppMsg;
18 | type Output = AppMsg;
19 | type Widgets = AttrWidgets;
20 | type ParentWidget = gtk::ListBox;
21 | type ParentInput = AppMsg;
22 | type CommandOutput = ();
23 |
24 | view! {
25 | adw::PreferencesRow {
26 | #[wrap(Some)]
27 | set_child = >k::Box {
28 | set_orientation: gtk::Orientation::Horizontal,
29 | set_spacing: 6,
30 | set_margin_all: 15,
31 | gtk::Label {
32 | set_text: &{
33 | if self.replacefor == Some(String::from("*")) {
34 | format!("[{}]", self.value.last().unwrap_or(&String::new()))
35 | } else {
36 | self.value.last().unwrap_or(&String::new()).to_string()
37 | }
38 | },
39 | set_use_markup: true,
40 | },
41 | gtk::Separator {
42 | set_hexpand: true,
43 | set_opacity: 0.0,
44 | },
45 | gtk::Image {
46 | set_icon_name: if self.modified { Some("system-run-symbolic") } else { Some("object-select-symbolic") },
47 | set_visible: self.configured || self.modified,
48 | },
49 | },
50 | set_title: &self.value.join("."),
51 | }
52 | }
53 |
54 | fn init_model(parent: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self {
55 | Self {
56 | value: parent.value,
57 | refvalue: parent.refvalue,
58 | configured: parent.configured,
59 | modified: parent.modified,
60 | replacefor: parent.replacefor,
61 | }
62 | }
63 | }
64 |
65 | #[derive(Default, Debug, PartialEq, Eq)]
66 | pub struct OptPos {
67 | pub value: Vec,
68 | pub refvalue: Vec,
69 | pub configured: bool,
70 | pub modified: bool,
71 | }
72 |
73 | #[relm4::factory(pub)]
74 | impl FactoryComponent for OptPos {
75 | type Init = OptPos;
76 | type Input = AppMsg;
77 | type Output = AppMsg;
78 | type Widgets = OptWidgets;
79 | type ParentWidget = gtk::ListBox;
80 | type ParentInput = AppMsg;
81 | type CommandOutput = ();
82 |
83 | view! {
84 | adw::PreferencesRow {
85 | #[wrap(Some)]
86 | set_child = >k::Box {
87 | set_orientation: gtk::Orientation::Horizontal,
88 | set_spacing: 6,
89 | set_margin_all: 15,
90 | gtk::Label {
91 | set_text: &{
92 | self.value.last().unwrap_or(&String::new()).to_string()
93 | },
94 | },
95 | gtk::Separator {
96 | set_hexpand: true,
97 | set_opacity: 0.0,
98 | },
99 | gtk::Image {
100 | set_icon_name: if self.modified { Some("system-run-symbolic") } else { Some("object-select-symbolic") },
101 | set_visible: self.configured || self.modified,
102 | },
103 | },
104 | set_title: &self.value.join("."),
105 | }
106 | }
107 |
108 | fn init_model(parent: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self {
109 | Self {
110 | value: parent.value,
111 | refvalue: parent.refvalue,
112 | configured: parent.configured,
113 | modified: parent.modified,
114 | }
115 | }
116 | }
117 |
118 | #[derive(Default, Debug, PartialEq, Eq)]
119 | pub struct AttrBtn {
120 | pub value: Vec,
121 | pub refvalue: Vec,
122 | pub opt: bool,
123 | }
124 |
125 | #[derive(Debug)]
126 | pub enum AttrBtnMsg {
127 | OpenOption(Vec, Vec),
128 | MoveTo(Vec, Vec),
129 | }
130 |
131 | #[relm4::factory(pub)]
132 | impl FactoryComponent for AttrBtn {
133 | type Init = AttrBtn;
134 | type Input = ();
135 | type Output = AttrBtnMsg;
136 | type Widgets = AttrBtnWidgets;
137 | type ParentWidget = gtk::Box;
138 | type ParentInput = AppMsg;
139 | type CommandOutput = ();
140 |
141 | view! {
142 | #[name(button)]
143 | gtk::Button {
144 | set_label: self.value.last().unwrap_or(&String::new()),
145 | connect_clicked[sender, value = self.value.clone(), refvalue = self.refvalue.clone(), opt = self.opt] => move |_| {
146 | if opt {
147 | sender.output(AttrBtnMsg::OpenOption(value.to_vec(), refvalue.to_vec()));
148 | } else {
149 | sender.output(AttrBtnMsg::MoveTo(value.to_vec(), refvalue.to_vec()));
150 | }
151 | }
152 | }
153 | }
154 |
155 | fn init_model(parent: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self {
156 | Self {
157 | value: parent.value,
158 | refvalue: parent.refvalue,
159 | opt: parent.opt,
160 | }
161 | }
162 |
163 | fn output_to_parent_input(output: Self::Output) -> Option {
164 | Some(match output {
165 | AttrBtnMsg::OpenOption(v, r) => AppMsg::OpenOption(v, r),
166 | AttrBtnMsg::MoveTo(v, r) => AppMsg::MoveTo(v, r),
167 | })
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/ui/welcome.rs:
--------------------------------------------------------------------------------
1 | use super::window::AppMsg;
2 | use adw::prelude::*;
3 | use log::info;
4 | use nix_data::config::configfile::NixDataConfig;
5 | use relm4::*;
6 | use relm4_components::open_dialog::*;
7 | use std::path::{Path, PathBuf};
8 |
9 | pub struct WelcomeModel {
10 | hidden: bool,
11 | confpath: Option,
12 | flakepath: Option,
13 | conf_dialog: Controller,
14 | flake_dialog: Controller,
15 | }
16 |
17 | #[derive(Debug)]
18 | pub enum WelcomeMsg {
19 | Show,
20 | Close,
21 | UpdateConfPath(PathBuf),
22 | UpdateFlakePath(PathBuf),
23 | ClearFlakePath,
24 | OpenConf,
25 | OpenFlake,
26 | Ignore,
27 | }
28 |
29 | #[relm4::component(pub)]
30 | impl SimpleComponent for WelcomeModel {
31 | type Init = gtk::Window;
32 | type Input = WelcomeMsg;
33 | type Output = AppMsg;
34 | type Widgets = WelcomeWidgets;
35 |
36 | view! {
37 | window = adw::Window {
38 | set_transient_for: Some(&parent_window),
39 | set_modal: true,
40 | #[watch]
41 | set_visible: !model.hidden,
42 | gtk::Box {
43 | set_orientation: gtk::Orientation::Vertical,
44 | gtk::Box {
45 | set_valign: gtk::Align::Center,
46 | set_vexpand: true,
47 | set_orientation: gtk::Orientation::Vertical,
48 | set_spacing: 20,
49 | set_margin_all: 20,
50 | gtk::Box {
51 | set_orientation: gtk::Orientation::Vertical,
52 | set_spacing: 10,
53 | gtk::Label {
54 | add_css_class: "title-1",
55 | set_text: "Welcome the NixOS Configuration Editor!",
56 | set_justify: gtk::Justification::Center,
57 | },
58 | gtk::Label {
59 | add_css_class: "dim-label",
60 | set_text: "If your configuration file is not in the default location, you can change it here.",
61 | },
62 | },
63 | gtk::ListBox {
64 | add_css_class: "boxed-list",
65 | set_halign: gtk::Align::Fill,
66 | set_selection_mode: gtk::SelectionMode::None,
67 | adw::ActionRow {
68 | set_title: "Configuration file",
69 | add_suffix = >k::Button {
70 | set_halign: gtk::Align::Center,
71 | set_valign: gtk::Align::Center,
72 | gtk::Box {
73 | set_orientation: gtk::Orientation::Horizontal,
74 | set_spacing: 5,
75 | gtk::Image {
76 | set_icon_name: Some("document-open-symbolic"),
77 | },
78 | gtk::Label {
79 | #[watch]
80 | set_label: {
81 | if let Some(path) = &model.confpath {
82 | let x = path.file_name().unwrap_or_default().to_str().unwrap_or_default();
83 | if x.is_empty() {
84 | "(None)"
85 | } else {
86 | x
87 | }
88 | } else {
89 | "(None)"
90 | }
91 | }
92 | }
93 | },
94 | connect_clicked[sender] => move |_| {
95 | sender.input(WelcomeMsg::OpenConf);
96 | }
97 | },
98 | },
99 | },
100 | gtk::ListBox {
101 | add_css_class: "boxed-list",
102 | set_halign: gtk::Align::Fill,
103 | set_selection_mode: gtk::SelectionMode::None,
104 | adw::ActionRow {
105 | set_title: "Flake file",
106 | set_subtitle: "If you are using flakes, you can specify the path to your flake.nix file here.",
107 | add_suffix = >k::Button {
108 | set_halign: gtk::Align::Center,
109 | set_valign: gtk::Align::Center,
110 | gtk::Box {
111 | set_orientation: gtk::Orientation::Horizontal,
112 | set_spacing: 5,
113 | gtk::Image {
114 | set_icon_name: Some("document-open-symbolic"),
115 | },
116 | gtk::Label {
117 | #[watch]
118 | set_label: {
119 | if let Some(path) = &model.flakepath {
120 | let x = path.file_name().unwrap_or_default().to_str().unwrap_or_default();
121 | if x.is_empty() {
122 | "(None)"
123 | } else {
124 | x
125 | }
126 | } else {
127 | "(None)"
128 | }
129 | }
130 | }
131 | },
132 | connect_clicked[sender] => move |_| {
133 | sender.input(WelcomeMsg::OpenFlake);
134 | }
135 | },
136 | add_suffix = >k::Button {
137 | set_halign: gtk::Align::Center,
138 | set_valign: gtk::Align::Center,
139 | set_icon_name: "user-trash-symbolic",
140 | connect_clicked[sender] => move |_| {
141 | sender.input(WelcomeMsg::ClearFlakePath);
142 | }
143 | }
144 | },
145 | },
146 | #[name(btn)]
147 | gtk::Button {
148 | #[watch]
149 | set_sensitive: model.confpath.is_some(),
150 | add_css_class: "pill",
151 | add_css_class: "suggested-action",
152 | set_label: "Continue",
153 | set_hexpand: false,
154 | set_halign: gtk::Align::Center,
155 | connect_clicked[sender] => move |_| {
156 | sender.input(WelcomeMsg::Close);
157 | },
158 | }
159 | }
160 | }
161 | }
162 | }
163 |
164 | fn init(
165 | parent_window: Self::Init,
166 | root: &Self::Root,
167 | sender: ComponentSender,
168 | ) -> ComponentParts {
169 | let conf_dialog = OpenDialog::builder()
170 | .transient_for_native(root)
171 | .launch(OpenDialogSettings::default())
172 | .forward(sender.input_sender(), |response| match response {
173 | OpenDialogResponse::Accept(path) => WelcomeMsg::UpdateConfPath(path),
174 | OpenDialogResponse::Cancel => WelcomeMsg::Ignore,
175 | });
176 |
177 | let flake_dialog = OpenDialog::builder()
178 | .transient_for_native(root)
179 | .launch(OpenDialogSettings::default())
180 | .forward(sender.input_sender(), |response| match response {
181 | OpenDialogResponse::Accept(path) => WelcomeMsg::UpdateFlakePath(path),
182 | OpenDialogResponse::Cancel => WelcomeMsg::Ignore,
183 | });
184 |
185 | let model = WelcomeModel {
186 | hidden: true,
187 | confpath: if Path::new("/etc/nixos/configuration.nix").exists() {
188 | Some(PathBuf::from("/etc/nixos/configuration.nix"))
189 | } else {
190 | None
191 | }, // parent_window.configpath.to_string(),
192 | flakepath: None,
193 | conf_dialog,
194 | flake_dialog,
195 | };
196 |
197 | let widgets = view_output!();
198 |
199 | widgets.btn.grab_focus();
200 |
201 | ComponentParts { model, widgets }
202 | }
203 |
204 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
205 | match msg {
206 | WelcomeMsg::Show => {
207 | self.hidden = false;
208 | }
209 | WelcomeMsg::Close => {
210 | if let Some(confpath) = &self.confpath {
211 | let _ = sender.output(AppMsg::SetConfig(NixDataConfig {
212 | systemconfig: Some(confpath.to_string_lossy().to_string()),
213 | flake: self
214 | .flakepath
215 | .as_ref()
216 | .map(|x| x.to_string_lossy().to_string()),
217 | flakearg: None,
218 | generations: None,
219 | }));
220 | self.hidden = true;
221 | }
222 | }
223 | WelcomeMsg::UpdateConfPath(s) => {
224 | info!("Set configuration path to {}", s.to_string_lossy());
225 | self.confpath = Some(s);
226 | }
227 | WelcomeMsg::UpdateFlakePath(s) => {
228 | info!("Set flake path to {}", s.to_string_lossy());
229 | self.flakepath = Some(s);
230 | }
231 | WelcomeMsg::ClearFlakePath => {
232 | info!("Clear flake path");
233 | self.flakepath = None;
234 | }
235 | WelcomeMsg::OpenConf => self.conf_dialog.emit(OpenDialogMsg::Open),
236 | WelcomeMsg::OpenFlake => self.flake_dialog.emit(OpenDialogMsg::Open),
237 | WelcomeMsg::Ignore => {}
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/ui/windowloading.rs:
--------------------------------------------------------------------------------
1 | use super::window::{AppMsg, LoadValues};
2 | use crate::parse::config::parseconfig;
3 | use crate::parse::options::read;
4 | use crate::parse::preferences::editconfig;
5 | use log::*;
6 | use nix_data::config::configfile::NixDataConfig;
7 | use relm4::adw::prelude::*;
8 | use relm4::*;
9 | use std::path::Path;
10 |
11 | pub struct WindowAsyncHandler;
12 |
13 | #[derive(Debug)]
14 | pub enum WindowAsyncHandlerMsg {
15 | RunWindow(String),
16 | GetConfigPath(Option),
17 | SetConfig(NixDataConfig),
18 | }
19 |
20 | impl Worker for WindowAsyncHandler {
21 | type Init = ();
22 | type Input = WindowAsyncHandlerMsg;
23 | type Output = AppMsg;
24 |
25 | fn init(_params: Self::Init, _sender: relm4::ComponentSender) -> Self {
26 | Self
27 | }
28 |
29 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
30 | match msg {
31 | WindowAsyncHandlerMsg::RunWindow(path) => {
32 | let optionfile = match nix_data::cache::nixos::nixosoptions() {
33 | Ok(x) => x,
34 | Err(e) => {
35 | error!("{}", e);
36 | let _ = sender.output(AppMsg::LoadError(
37 | String::from("Could not load cache"),
38 | String::from(
39 | "Try connecting to the internet or launching the application again",
40 | ),
41 | ));
42 | return;
43 | }
44 | };
45 |
46 | let (data, tree) = match read(&optionfile) {
47 | Ok(x) => x,
48 | Err(e) => {
49 | error!("{}", e);
50 | let _ = sender.output(AppMsg::LoadError(
51 | String::from("Could not load options"),
52 | String::from("Try launching the application again"),
53 | ));
54 | return;
55 | }
56 | };
57 |
58 | let conf = match parseconfig(&path) {
59 | Ok(x) => x,
60 | Err(e) => {
61 | error!("{}", e);
62 | let _ = sender.output(AppMsg::LoadError(
63 | String::from("Error loading configuration file"),
64 | format!("{} may be an invalid configuration file", path),
65 | ));
66 | return;
67 | }
68 | };
69 | let _ = sender.output(AppMsg::InitialLoad(LoadValues { data, tree, conf }));
70 | }
71 | WindowAsyncHandlerMsg::GetConfigPath(cfg) => {
72 | warn!("CFG: {:?}", cfg);
73 | if let Some(config) = cfg {
74 | if let Some(systemconfig) = &config.systemconfig {
75 | if Path::new(&systemconfig).exists() {
76 | if let Some(flakepath) = &config.flake {
77 | if !Path::new(flakepath).exists() {
78 | let _ = sender.output(AppMsg::Welcome);
79 | return;
80 | }
81 | }
82 | let _ = sender.output(AppMsg::SetConfig(config));
83 | } else {
84 | let _ = sender.output(AppMsg::Welcome);
85 | }
86 | } else {
87 | let _ = sender.output(AppMsg::Welcome);
88 | }
89 | } else {
90 | let _ = sender.output(AppMsg::Welcome);
91 | }
92 | }
93 | WindowAsyncHandlerMsg::SetConfig(cfg) => {
94 | let _ = match editconfig(cfg) {
95 | Ok(_) => sender.output(AppMsg::TryLoad),
96 | Err(_) => sender.output(AppMsg::LoadError(
97 | String::from("Error loading configuration file"),
98 | String::from("Try launching the application again"),
99 | )),
100 | };
101 | }
102 | }
103 | }
104 | }
105 |
106 | pub struct LoadErrorModel {
107 | hidden: bool,
108 | msg: String,
109 | msg2: String,
110 | }
111 |
112 | #[derive(Debug)]
113 | pub enum LoadErrorMsg {
114 | Show(String, String),
115 | Retry,
116 | Close,
117 | Preferences,
118 | }
119 |
120 | #[relm4::component(pub)]
121 | impl SimpleComponent for LoadErrorModel {
122 | type Init = gtk::Window;
123 | type Input = LoadErrorMsg;
124 | type Output = AppMsg;
125 | type Widgets = LoadErrorWidgets;
126 |
127 | view! {
128 | dialog = gtk::MessageDialog {
129 | set_transient_for: Some(&parent_window),
130 | set_modal: true,
131 | #[watch]
132 | set_visible: !model.hidden,
133 | #[watch]
134 | set_text: Some(&model.msg),
135 | #[watch]
136 | set_secondary_text: Some(&model.msg2),
137 | set_use_markup: true,
138 | set_secondary_use_markup: true,
139 | add_button: ("Retry", gtk::ResponseType::Accept),
140 | add_button: ("Preferences", gtk::ResponseType::Help),
141 | add_button: ("Quit", gtk::ResponseType::Close),
142 | connect_response[sender] => move |_, resp| {
143 | sender.input(match resp {
144 | gtk::ResponseType::Accept => LoadErrorMsg::Retry,
145 | gtk::ResponseType::Close => LoadErrorMsg::Close,
146 | gtk::ResponseType::Help => LoadErrorMsg::Preferences,
147 | _ => unreachable!(),
148 | });
149 | },
150 | }
151 | }
152 |
153 | fn init(
154 | parent_window: Self::Init,
155 | root: &Self::Root,
156 | sender: ComponentSender,
157 | ) -> ComponentParts {
158 | let model = LoadErrorModel {
159 | hidden: true,
160 | msg: String::default(),
161 | msg2: String::default(),
162 | };
163 | let widgets = view_output!();
164 | let accept_widget = widgets
165 | .dialog
166 | .widget_for_response(gtk::ResponseType::Accept)
167 | .expect("No button for accept response set");
168 | accept_widget.add_css_class("warning");
169 | let pref_widget = widgets
170 | .dialog
171 | .widget_for_response(gtk::ResponseType::Help)
172 | .expect("No button for help response set");
173 | pref_widget.add_css_class("suggested-action");
174 | ComponentParts { model, widgets }
175 | }
176 |
177 | fn update(&mut self, msg: Self::Input, sender: ComponentSender) {
178 | match msg {
179 | LoadErrorMsg::Show(s, s2) => {
180 | self.hidden = false;
181 | self.msg = s;
182 | self.msg2 = s2;
183 | }
184 | LoadErrorMsg::Retry => {
185 | self.hidden = true;
186 | let _ = sender.output(AppMsg::TryLoad);
187 | }
188 | LoadErrorMsg::Close => {
189 | let _ = sender.output(AppMsg::Close);
190 | }
191 | LoadErrorMsg::Preferences => {
192 | let _ = sender.output(AppMsg::ShowPrefMenuErr);
193 | self.hidden = true;
194 | }
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------