├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.sh ├── certs ├── ca.crt └── sign.crt ├── check-qml-resources.sh ├── config.example.yml ├── do-release.sh ├── doc ├── developing.md ├── images │ ├── LightkeeperRM-overview.png │ ├── lightkeeper-cert-monitor.png │ ├── lightkeeper-custom-command.png │ ├── lightkeeper-log-viewer.png │ ├── lightkeeper-overview.png │ └── lightkeeper-terminal.png ├── modules.md ├── roadmap.md └── testing.md ├── flatpak ├── cargo-sources.json ├── flatpak-cargo-generator │ ├── README.md │ └── flatpak-cargo-generator.py ├── io.github.kalaksi.Lightkeeper-128px.png ├── io.github.kalaksi.Lightkeeper-64px.png ├── io.github.kalaksi.Lightkeeper-local.desktop ├── io.github.kalaksi.Lightkeeper-local.metainfo.xml ├── io.github.kalaksi.Lightkeeper-local.yml ├── io.github.kalaksi.Lightkeeper-rounded.png ├── io.github.kalaksi.Lightkeeper.desktop ├── io.github.kalaksi.Lightkeeper.metainfo.xml ├── io.github.kalaksi.Lightkeeper.yml ├── lightkeeper-icon-2.png ├── lightkeeper-icon-cropped.png └── lightkeeper-icon.png ├── groups.example.yml ├── hosts.example.yml ├── macros └── lightkeeper_module │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── lib.rs ├── run-test-env.sh ├── run.sh ├── rustfmt.toml ├── src ├── cache.rs ├── command_handler.rs ├── configuration.rs ├── connection_manager.rs ├── enums.rs ├── enums │ ├── criticality.rs │ └── host_status.rs ├── error.rs ├── file_handler.rs ├── frontend.rs ├── frontend │ ├── cli │ │ └── mod.rs │ ├── display_options.rs │ ├── frontend.rs │ ├── hot_reload.rs │ ├── qt.rs │ └── qt │ │ ├── fonts │ │ ├── Pixeloid │ │ │ ├── LICENSE │ │ │ └── PixeloidSans-nR3g1.ttf │ │ └── PressStart2P │ │ │ ├── LICENSE │ │ │ └── PressStart2P-vaV7.ttf │ │ ├── images │ │ ├── breeze │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── VERSION │ │ │ ├── dark │ │ │ │ ├── akonadiconsole.svg │ │ │ │ ├── alarm-symbolic.svg │ │ │ │ ├── application-menu.svg │ │ │ │ ├── arrow-down.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── configure.svg │ │ │ │ ├── data-error.svg │ │ │ │ ├── data-information.svg │ │ │ │ ├── data-warning.svg │ │ │ │ ├── delete.svg │ │ │ │ ├── dialog-cancel.svg │ │ │ │ ├── dialog-ok.svg │ │ │ │ ├── document-open-folder.svg │ │ │ │ ├── document-open.svg │ │ │ │ ├── document-preview.svg │ │ │ │ ├── document-save.svg │ │ │ │ ├── download.svg │ │ │ │ ├── drive-harddisk.svg │ │ │ │ ├── edit-clear-all.svg │ │ │ │ ├── edit-clear.svg │ │ │ │ ├── edit-copy.svg │ │ │ │ ├── edit-undo.svg │ │ │ │ ├── entry-edit.svg │ │ │ │ ├── find-location.svg │ │ │ │ ├── gnumeric-column-size.svg │ │ │ │ ├── group-new.svg │ │ │ │ ├── help-keyboard-shortcuts.svg │ │ │ │ ├── list-add.svg │ │ │ │ ├── list-remove.svg │ │ │ │ ├── media-playback-start.svg │ │ │ │ ├── media-playback-stop.svg │ │ │ │ ├── office-chart-area-stacked.svg │ │ │ │ ├── office-chart-bar-stacked.svg │ │ │ │ ├── overflow-menu.svg │ │ │ │ ├── process-working.svg │ │ │ │ ├── project-open.svg │ │ │ │ ├── resizecol.svg │ │ │ │ ├── run-build-file.svg │ │ │ │ ├── run-build.svg │ │ │ │ ├── story-editor.svg │ │ │ │ ├── system-search.svg │ │ │ │ ├── system-shutdown.svg │ │ │ │ ├── tab-close.svg │ │ │ │ ├── tag.svg │ │ │ │ ├── update-high.svg │ │ │ │ ├── update-low.svg │ │ │ │ ├── update-none.svg │ │ │ │ ├── utilities-terminal.svg │ │ │ │ ├── view-certificate.svg │ │ │ │ ├── view-refresh.svg │ │ │ │ └── window-new.svg │ │ │ └── light │ │ │ │ ├── akonadiconsole.svg │ │ │ │ ├── alarm-symbolic.svg │ │ │ │ ├── application-menu.svg │ │ │ │ ├── arrow-down.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── configure.svg │ │ │ │ ├── data-error.svg │ │ │ │ ├── data-information.svg │ │ │ │ ├── data-warning.svg │ │ │ │ ├── delete.svg │ │ │ │ ├── dialog-cancel.svg │ │ │ │ ├── dialog-ok.svg │ │ │ │ ├── document-open-folder.svg │ │ │ │ ├── document-open.svg │ │ │ │ ├── document-preview.svg │ │ │ │ ├── document-save.svg │ │ │ │ ├── download.svg │ │ │ │ ├── drive-harddisk.svg │ │ │ │ ├── edit-clear-all.svg │ │ │ │ ├── edit-clear.svg │ │ │ │ ├── edit-copy.svg │ │ │ │ ├── edit-undo.svg │ │ │ │ ├── entry-edit.svg │ │ │ │ ├── find-location.svg │ │ │ │ ├── gnumeric-column-size.svg │ │ │ │ ├── group-new.svg │ │ │ │ ├── help-keyboard-shortcuts.svg │ │ │ │ ├── list-add.svg │ │ │ │ ├── list-remove.svg │ │ │ │ ├── media-playback-start.svg │ │ │ │ ├── media-playback-stop.svg │ │ │ │ ├── office-chart-area-stacked.svg │ │ │ │ ├── office-chart-bar-stacked.svg │ │ │ │ ├── overflow-menu.svg │ │ │ │ ├── preferences-system-linux.svg │ │ │ │ ├── process-working.svg │ │ │ │ ├── project-open.svg │ │ │ │ ├── resizecol.svg │ │ │ │ ├── run-build-file.svg │ │ │ │ ├── run-build.svg │ │ │ │ ├── story-editor.svg │ │ │ │ ├── system-search.svg │ │ │ │ ├── system-shutdown.svg │ │ │ │ ├── tab-close.svg │ │ │ │ ├── tag.svg │ │ │ │ ├── update-high.svg │ │ │ │ ├── update-low.svg │ │ │ │ ├── update-none.svg │ │ │ │ ├── utilities-terminal.svg │ │ │ │ ├── view-certificate.svg │ │ │ │ ├── view-refresh.svg │ │ │ │ └── window-new.svg │ │ ├── fontawesome │ │ │ ├── LICENSE.txt │ │ │ ├── VERSION │ │ │ ├── check.svg │ │ │ ├── circle-arrow-down.svg │ │ │ ├── circle-arrow-up.svg │ │ │ ├── circle-check.svg │ │ │ ├── circle-exclamation.svg │ │ │ ├── docker.svg │ │ │ ├── exclamation.svg │ │ │ ├── terminal.svg │ │ │ ├── triangle-exclamation.svg │ │ │ └── xmark.svg │ │ ├── lightkeeper-tray-icon.png │ │ └── nixos.svg │ │ ├── models.rs │ │ ├── models │ │ ├── command_handler_model.rs │ │ ├── config_manager_model.rs │ │ ├── file_chooser_model.rs │ │ ├── host_data_manager_model.rs │ │ ├── host_data_model.rs │ │ ├── host_table_model.rs │ │ ├── lkbackend_model.rs │ │ ├── metrics_manager_model.rs │ │ ├── monitor_data_model.rs │ │ ├── property_table_model.rs │ │ ├── qmetatypes.rs │ │ └── theme_model.rs │ │ ├── qml │ │ ├── Button │ │ │ ├── AutoRefreshButton.qml │ │ │ ├── CommandButton.qml │ │ │ ├── ImageButton.qml │ │ │ └── RefreshButton.qml │ │ ├── ChartsView │ │ │ └── ChartsView.qml │ │ ├── DebugRectangle.qml │ │ ├── DetailsView │ │ │ ├── CategoryGroupBox.qml │ │ │ ├── CommandButtonRow.qml │ │ │ ├── CustomCommandGroupBox.qml │ │ │ ├── GroupBoxLabel.qml │ │ │ ├── Header.qml │ │ │ ├── HostDetails.qml │ │ │ ├── HostDetailsLogView.qml │ │ │ ├── HostDetailsMainView.qml │ │ │ ├── HostDetailsTerminalView.qml │ │ │ ├── HostDetailsTextEditorView.qml │ │ │ ├── HostDetailsTextView.qml │ │ │ ├── HostGroupBox.qml │ │ │ ├── LogList.qml │ │ │ ├── PropertyTable.qml │ │ │ └── PropertyTableCell.qml │ │ ├── Dialog │ │ │ ├── CertificateMonitorDialog.qml │ │ │ ├── CommandOutputDialog.qml │ │ │ ├── ConfigHelperDialog.qml │ │ │ ├── ConfirmationDialog.qml │ │ │ ├── CustomCommandsDialog.qml │ │ │ ├── DialogBackground.qml │ │ │ ├── GroupConfigurationDialog.qml │ │ │ ├── HostConfigurationDialog.qml │ │ │ ├── HotkeyHelp.qml │ │ │ ├── InputDialog.qml │ │ │ ├── LightkeeperDialog.qml │ │ │ ├── ModuleSettingsDialog.qml │ │ │ ├── PreferencesDialog.qml │ │ │ └── TextDialog.qml │ │ ├── DialogHandler.qml │ │ ├── DynamicObjectManager.qml │ │ ├── HostStatus.qml │ │ ├── HostTable.qml │ │ ├── JsonTextFormat.qml │ │ ├── LightkeeperTray.qml │ │ ├── Main.qml │ │ ├── MainMenuBar.qml │ │ ├── Misc │ │ │ ├── BorderRectangle.qml │ │ │ ├── CooldownTimer.qml │ │ │ ├── LKListView.qml │ │ │ ├── LKTabButton.qml │ │ │ ├── OverlayImage.qml │ │ │ ├── SemiCircle.qml │ │ │ └── Trapezoid.qml │ │ ├── MonitorSummary.qml │ │ ├── RowHighlight.qml │ │ ├── Snackbar.qml │ │ ├── SnackbarContainer.qml │ │ ├── StatusBar.qml │ │ ├── StyleOverride │ │ │ ├── Button.qml │ │ │ ├── Label.qml │ │ │ ├── README.md │ │ │ ├── RoundButton.qml │ │ │ ├── ScrollBar.qml │ │ │ ├── ScrollView.qml │ │ │ ├── TextField.qml │ │ │ └── ToolButton.qml │ │ ├── TableCell.qml │ │ ├── Tests │ │ │ └── Test.qml │ │ ├── Text │ │ │ ├── AlertText.qml │ │ │ ├── BaseText.qml │ │ │ ├── BigText.qml │ │ │ ├── NormalText.qml │ │ │ ├── OptionalText.qml │ │ │ ├── PillText.qml │ │ │ ├── PixelatedText.qml │ │ │ ├── ScrollableNormalText.qml │ │ │ ├── ScrollableSmallerText.qml │ │ │ ├── SmallText.qml │ │ │ └── SmallerText.qml │ │ ├── WaveAnimation.qml │ │ ├── WorkingSprite.qml │ │ └── js │ │ │ ├── Parse.js │ │ │ ├── Test.js │ │ │ ├── TextTransform.js │ │ │ ├── Utils.js │ │ │ └── ValueUnit.js │ │ ├── qml_frontend.rs │ │ ├── resources.rs │ │ └── resources_qml.rs ├── host.rs ├── host_manager.rs ├── lib.rs ├── main.rs ├── metrics.rs ├── metrics │ ├── download-token.txt │ ├── metric.rs │ ├── metrics_manager.rs │ ├── tmserver.rs │ └── tmserver │ │ ├── connection.rs │ │ └── tmsrequest.rs ├── module │ ├── command.rs │ ├── command │ │ ├── command_module.rs │ │ ├── docker.rs │ │ ├── docker │ │ │ ├── compose.rs │ │ │ ├── compose │ │ │ │ ├── build.rs │ │ │ │ ├── edit.rs │ │ │ │ ├── logs.rs │ │ │ │ ├── pull.rs │ │ │ │ ├── shell.rs │ │ │ │ ├── start.rs │ │ │ │ ├── stop.rs │ │ │ │ └── up.rs │ │ │ ├── image.rs │ │ │ ├── image │ │ │ │ ├── prune.rs │ │ │ │ ├── remote_tags.rs │ │ │ │ └── remove.rs │ │ │ ├── inspect.rs │ │ │ ├── restart.rs │ │ │ └── shell.rs │ │ ├── internal.rs │ │ ├── internal │ │ │ └── custom_command.rs │ │ ├── linux.rs │ │ ├── linux │ │ │ ├── logs.rs │ │ │ ├── packages.rs │ │ │ ├── packages │ │ │ │ ├── clean.rs │ │ │ │ ├── install.rs │ │ │ │ ├── logs.rs │ │ │ │ ├── refresh.rs │ │ │ │ ├── uninstall.rs │ │ │ │ ├── update.rs │ │ │ │ └── update_all.rs │ │ │ └── shell.rs │ │ ├── network.rs │ │ ├── network │ │ │ ├── socket_listen.rs │ │ │ └── socket_tcp.rs │ │ ├── nixos.rs │ │ ├── nixos │ │ │ ├── channel_update.rs │ │ │ ├── collectgarbage.rs │ │ │ ├── rebuild_boot.rs │ │ │ ├── rebuild_dryrun.rs │ │ │ ├── rebuild_rollback.rs │ │ │ └── rebuild_switch.rs │ │ ├── os.rs │ │ ├── os │ │ │ ├── reboot.rs │ │ │ └── shutdown.rs │ │ ├── storage.rs │ │ ├── storage │ │ │ ├── file_space_usage.rs │ │ │ ├── lvm.rs │ │ │ └── lvm │ │ │ │ ├── lvrefresh.rs │ │ │ │ ├── lvremove.rs │ │ │ │ ├── lvresize.rs │ │ │ │ └── snapshot.rs │ │ ├── systemd.rs │ │ └── systemd │ │ │ ├── service.rs │ │ │ └── service │ │ │ ├── logs.rs │ │ │ ├── mask.rs │ │ │ ├── start.rs │ │ │ ├── stop.rs │ │ │ └── unmask.rs │ ├── connection.rs │ ├── connection │ │ ├── connection_module.rs │ │ ├── http.rs │ │ ├── http_jwt.rs │ │ ├── local_command.rs │ │ ├── request_response.rs │ │ ├── ssh.rs │ │ └── tcp.rs │ ├── metadata.rs │ ├── mod.rs │ ├── module.rs │ ├── module_factory.rs │ ├── module_specification.rs │ ├── monitoring.rs │ ├── monitoring │ │ ├── data_point.rs │ │ ├── docker.rs │ │ ├── docker │ │ │ ├── compose.rs │ │ │ ├── containers.rs │ │ │ ├── image_updates.rs │ │ │ └── images.rs │ │ ├── internal.rs │ │ ├── internal │ │ │ ├── cert_monitor.rs │ │ │ └── platform_info_ssh.rs │ │ ├── linux.rs │ │ ├── linux │ │ │ ├── interface.rs │ │ │ ├── kernel.rs │ │ │ ├── load.rs │ │ │ ├── package.rs │ │ │ ├── ram.rs │ │ │ ├── uptime.rs │ │ │ └── who.rs │ │ ├── monitoring_module.rs │ │ ├── network.rs │ │ ├── network │ │ │ ├── dns.rs │ │ │ ├── oping.rs │ │ │ ├── ping.rs │ │ │ ├── routes.rs │ │ │ ├── ssh.rs │ │ │ └── tcp_connect.rs │ │ ├── nixos.rs │ │ ├── nixos │ │ │ └── rebuild_generations.rs │ │ ├── os.rs │ │ ├── storage.rs │ │ ├── storage │ │ │ ├── cryptsetup.rs │ │ │ ├── filesystem.rs │ │ │ ├── lvm.rs │ │ │ └── lvm │ │ │ │ ├── logical_volume.rs │ │ │ │ ├── physical_volume.rs │ │ │ │ └── volume_group.rs │ │ ├── systemd.rs │ │ └── systemd │ │ │ └── service.rs │ └── platform_info.rs ├── monitor_manager.rs ├── utils.rs └── utils │ ├── download.rs │ ├── error_message.rs │ ├── sha256.rs │ ├── shell_command.rs │ ├── string_manipulation.rs │ ├── string_validation.rs │ └── version_number.rs ├── test-env ├── alpine318 │ └── Vagrantfile ├── centos7 │ └── Vagrantfile ├── centos8 │ └── Vagrantfile ├── config.yml ├── debian11 │ └── Vagrantfile ├── fedora38 │ └── Vagrantfile ├── groups.yml ├── hosts.yml ├── start.sh ├── stop.sh ├── ubuntu2004 │ └── Vagrantfile └── ubuntu2204 │ └── Vagrantfile ├── tests ├── common │ └── mod.rs └── smoke.rs └── third_party └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | /target 3 | /flatpak/build 4 | .* 5 | vendor 6 | android 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | # These are third party modules, but forked. 2 | [submodule "third_party/qmltermwidget"] 3 | path = third_party/qmltermwidget 4 | url = https://github.com/kalaksi/qmltermwidget.git 5 | branch = lightkeeper 6 | [submodule "third_party/ChartJs2QML"] 7 | path = third_party/ChartJs2QML 8 | url = https://github.com/kalaksi/ChartJs2QML.git 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'lightkeeper'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=lightkeeper", 15 | "--package=lightkeeper" 16 | ], 17 | "filter": { 18 | "name": "lightkeeper", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}", 24 | "env": { 25 | "RUST_LOG": "debug", 26 | "QT_LOGGING_RULES": "*.debug=true; qt.*.debug=false" 27 | } 28 | }, 29 | { 30 | "type": "lldb", 31 | "request": "launch", 32 | "name": "Debug unit tests in executable 'lightkeeper'", 33 | "cargo": { 34 | "args": [ 35 | "", 36 | "--no-run", 37 | "--bin=lightkeeper", 38 | "--package=lightkeeper" 39 | ], 40 | "filter": { 41 | "name": "lightkeeper", 42 | "kind": "bin" 43 | } 44 | }, 45 | "args": [], 46 | "cwd": "${workspaceFolder}" 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.showUnlinkedFileNotification": false, 3 | "rust-analyzer.cargo.extraEnv": { 4 | "QMAKE": "/usr/lib/qt6/bin/qmake" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lightkeeper" 3 | version = "0.28.4" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "lightkeeper" 8 | path = "src/main.rs" 9 | 10 | [dependencies] 11 | oping = "0.4.0" 12 | dbus = "0.9.7" 13 | clap = { version = "4.5.16", features = ["derive"] } 14 | log = "0.4.22" 15 | env_logger = "0.11.5" 16 | ssh2 = "0.9.4" 17 | serde = "1.0.200" 18 | serde_derive = "1.0.200" 19 | serde_yaml = "0.9.33" 20 | serde_json = "1.0.116" 21 | chrono = { version = "0.4.38", features = ["serde"]} 22 | qmetaobject = "0.2.10" 23 | cstr = "0.2.12" 24 | sha2 = "0.10.8" 25 | strum = "0.26.3" 26 | strum_macros = "0.26.4" 27 | ureq = "2.10.1" 28 | rayon = "1.10.0" 29 | rand = "0.8.5" 30 | regex = "1.10.6" 31 | rustls = "0.22.4" 32 | rustls-native-certs = "0.7.2" 33 | rustls-pemfile = "2.1.3" 34 | x509-parser = "0.16.0" 35 | base64 = "0.22.1" 36 | hex = "0.4.3" 37 | bincode = "1.3.3" 38 | openssl = { version = "0.10.66", features = ["vendored"] } 39 | # Only for hot reload. 40 | notify = { version = "7.0.0" } 41 | 42 | lightkeeper_module = { path = "macros/lightkeeper_module" } 43 | 44 | # [dev-dependencies] 45 | # notify = { version = "7.0.0" } 46 | 47 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | # qmake path can be overridden with this (for cargo and qmetaobject-rs too): 4 | # export QMAKE="/usr/lib/qt6/bin/qmake" 5 | 6 | if [ ! -e third_party/qmltermwidget ] || \ 7 | [ ! -e third_party/ChartJs2QML ]; then 8 | 9 | git submodule update --init --recursive --remote 10 | fi 11 | 12 | if [ ! -e third_party/qmltermwidget/QMLTermWidget/libqmltermwidget.so ]; then 13 | pushd third_party/qmltermwidget 14 | qmake && make -j 4 15 | popd 16 | fi 17 | 18 | if [ ! -z "$(git status -s)" ]; then 19 | # Expand use later. Currently, rustfmt in some cases makes readability worse. 20 | rustfmt +nightly src/utils.rs 21 | fi 22 | 23 | cargo build 24 | -------------------------------------------------------------------------------- /certs/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBnjCCAUWgAwIBAgIUStoLPWZvbi6jb8YRGjonbl7HJdMwCgYIKoZIzj0EAwIw 3 | JTEjMCEGA1UEAwwaTGlnaHRrZWVwZXIgUHJvIExpY2Vuc2UgQ0EwHhcNMjQwOTE0 4 | MTk0OTQxWhcNMzkwOTExMTk0OTQxWjAlMSMwIQYDVQQDDBpMaWdodGtlZXBlciBQ 5 | cm8gTGljZW5zZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNtwE8pXurWY 6 | gCiQT5S5tjQftJQGM2TgPYXDgEXzk+myQtgycZjpDxtL59Ojic9zUrsnn1t/mQav 7 | YwwledENqvWjUzBRMB0GA1UdDgQWBBQwQ00JHMba+aeyu/uqMrxcmcpsHDAfBgNV 8 | HSMEGDAWgBQwQ00JHMba+aeyu/uqMrxcmcpsHDAPBgNVHRMBAf8EBTADAQH/MAoG 9 | CCqGSM49BAMCA0cAMEQCIBLrrHqkWjmz9yUoVdyBIJvWKw7Xpt583Lu3LvWHxcsh 10 | AiBSvN2jpQsXZ4myDLwJ0FZ6xNQCa2q1+f0S43Q0mtcd5A== 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /certs/sign.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBuTCCAV+gAwIBAgIUaLPJjErlG+MnFq3yAa+RFcrsZFgwCgYIKoZIzj0EAwIw 3 | JTEjMCEGA1UEAwwaTGlnaHRrZWVwZXIgUHJvIExpY2Vuc2UgQ0EwHhcNMjQwOTE5 4 | MTY0MjM5WhcNMzkwOTE2MTY0MjM5WjAiMSAwHgYDVQQDDBdMaWdodGtlZXBlciBT 5 | aWduaW5nIEtleTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEkJu56uG7PZCh5r 6 | 1tKjKKdaHPmByPiw2d+JyVPxCO1hEAjV83oX46Cmw8L2VHB4i2BKl+3IX6JZLQ/P 7 | hbGT/nmjcDBuMB8GA1UdIwQYMBaAFDBDTQkcxtr5p7K7+6oyvFyZymwcMAkGA1Ud 8 | EwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAdBgNVHQ4E 9 | FgQU9FecaBRkukGX1dPqFeryD2vUq9EwCgYIKoZIzj0EAwIDSAAwRQIgfIHec3YX 10 | 1JW8V8ql133x4huPuaHu9ZttO2QlFgHtn8oCIQDpOQJkKkomrMaBZKUTwKZ5BhQb 11 | YiyUHEWuNUsKC68nJg== 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /check-qml-resources.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | qml_files=$(find src/frontend/qt/qml \( -name '*.qml' -or -name '*.js' \) | sed 's/^src\/frontend\/qt\/qml\///') 4 | for f in $qml_files; do 5 | if ! fgrep -q "\"$f\"" "src/frontend/qt/resources_qml.rs"; then 6 | echo "File $f not found in QML resource file" 7 | exit 1 8 | fi 9 | done 10 | -------------------------------------------------------------------------------- /doc/developing.md: -------------------------------------------------------------------------------- 1 | # Conventions 2 | - When naming command or monitoring modules, prefer familiar terminology and naming from the underlying program. 3 | - Prefer doing text processing in Lightkeeper, not on target host (awk, sed...). 4 | 5 | # Limitations 6 | ## qmetaobject (rust crate) 7 | It doesn't seem possible to return custom QML objects from rust to QML, so that QML could access the properties. 8 | It is possible to pass custom QObjects *through* QML (i.e. rust -> QML -> rust) using QMetaType **or** instantiate custom QML types in QML side. 9 | 10 | This limitation is the reason there may, in some cases, be a lot of function calls that return simple values instead of complex objects or they return objects as JSON strings. 11 | -------------------------------------------------------------------------------- /doc/images/LightkeeperRM-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/images/LightkeeperRM-overview.png -------------------------------------------------------------------------------- /doc/images/lightkeeper-cert-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/images/lightkeeper-cert-monitor.png -------------------------------------------------------------------------------- /doc/images/lightkeeper-custom-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/images/lightkeeper-custom-command.png -------------------------------------------------------------------------------- /doc/images/lightkeeper-log-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/images/lightkeeper-log-viewer.png -------------------------------------------------------------------------------- /doc/images/lightkeeper-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/images/lightkeeper-overview.png -------------------------------------------------------------------------------- /doc/images/lightkeeper-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/images/lightkeeper-terminal.png -------------------------------------------------------------------------------- /doc/modules.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | 3 | Lightkeeper aims to be modular so it's easier to extend and customize. 4 | The main types of modules are: 5 | - **Monitoring modules (or monitors):** They gather information and often report status information. 6 | - **Command modules:** They execute commands. Usually through clickable buttons in the UI. Commands often depend on monitors. 7 | E.g. you need a monitor `systemd-service` to be able to use command `systemd-service-start` which allows you to start individual services. 8 | - **Connector modules:** Most monitors and commands use the `ssh`-connector, which sends commands over SSH. Usually you don't need custom connector modules, 9 | but you do need to configure e.g. the `ssh`-connector before it can work. For example, you need to set the username and private key path or password for successful login. 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/roadmap.md: -------------------------------------------------------------------------------- 1 | # Features 2 | More important features first. 3 | - more keyboard shortcuts and GUI hints 4 | - view progress of long commands (such as system upgrades or image building). 5 | - Check and display vuln status of system (or docker) packages using mitre, DSA, NVD etc. 6 | - cache for monitor's command results to minimize amount of commands actually run (crude implementation done) 7 | - automatic module testing (QML too) 8 | - new module property `privileged` for modules that require sudo/root 9 | - Hot-reloading QML? 10 | - Utilize system icons and themes? Can still offer and default to baked in style when necessary. 11 | - Maybe this would be suitable for managing localhost (desktop) too? 12 | - mobile version? 13 | - easily add self-made scripts (shell scripts etc.) as commands? 14 | - modules as shared objects instead of compiled in? or loadable scripts? 15 | - Support SaaS-platforms? (probably outside of scope) 16 | 17 | # Problems 18 | - There doesn't seem to be a default hover-animation for RoundButton. Add a custom one? 19 | -------------------------------------------------------------------------------- /doc/testing.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/doc/testing.md -------------------------------------------------------------------------------- /flatpak/flatpak-cargo-generator/README.md: -------------------------------------------------------------------------------- 1 | This script is from https://github.com/flatpak/flatpak-builder-tools/tree/master/cargo. 2 | Script's contents indicate that the license is MIT. 3 | 4 | Usage: 5 | python3 ./flatpak-cargo-generator/flatpak-cargo-generator.py ../Cargo.lock -o cargo-sources.json 6 | -------------------------------------------------------------------------------- /flatpak/io.github.kalaksi.Lightkeeper-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/flatpak/io.github.kalaksi.Lightkeeper-128px.png -------------------------------------------------------------------------------- /flatpak/io.github.kalaksi.Lightkeeper-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/flatpak/io.github.kalaksi.Lightkeeper-64px.png -------------------------------------------------------------------------------- /flatpak/io.github.kalaksi.Lightkeeper-local.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | 5 | Name=Lightkeeper (local) 6 | Comment=A standalone, customizable server management tool. 7 | Categories=Network;RemoteAccess; 8 | 9 | Icon=io.github.kalaksi.Lightkeeper-local 10 | Exec=lightkeeper 11 | Terminal=false 12 | -------------------------------------------------------------------------------- /flatpak/io.github.kalaksi.Lightkeeper-local.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.kalaksi.Lightkeeper-local 4 | LightkeeperRM 5 | Linux server management tool 6 | CC-BY-SA-4.0 7 | GPL-3.0-or-later 8 | 9 | Kalaksi 10 | 11 | 12 |

13 | LightkeeperRM is a standalone and customizable server management and monitoring tool. 14 | It works as a drop-in replacement for maintaining servers over SSH with shell commands. 15 | 16 | Beta version. There will be some bugs and features missing. 17 |

18 |
19 | https://github.com/kalaksi/lightkeeper 20 | https://github.com/kalaksi/lightkeeper 21 | 22 | 23 | https://raw.githubusercontent.com/kalaksi/lightkeeper/master/doc/images/LightkeeperRM-overview.png 24 | Overview of LightkeeperRM. 25 | 26 | 27 | io.github.kalaksi.Lightkeeper-local.desktop 28 | 29 | 30 | Local test release 31 | 32 | 33 | 34 |
35 | -------------------------------------------------------------------------------- /flatpak/io.github.kalaksi.Lightkeeper-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/flatpak/io.github.kalaksi.Lightkeeper-rounded.png -------------------------------------------------------------------------------- /flatpak/io.github.kalaksi.Lightkeeper.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | 5 | Name=Lightkeeper 6 | Comment=A standalone, customizable server management tool. 7 | Categories=Network;RemoteAccess; 8 | 9 | Icon=io.github.kalaksi.Lightkeeper 10 | Exec=lightkeeper 11 | Terminal=false 12 | -------------------------------------------------------------------------------- /flatpak/lightkeeper-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/flatpak/lightkeeper-icon-2.png -------------------------------------------------------------------------------- /flatpak/lightkeeper-icon-cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/flatpak/lightkeeper-icon-cropped.png -------------------------------------------------------------------------------- /flatpak/lightkeeper-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/flatpak/lightkeeper-icon.png -------------------------------------------------------------------------------- /hosts.example.yml: -------------------------------------------------------------------------------- 1 | hosts: 2 | example-host: 3 | address: 127.0.0.1 4 | # Alternatively: 5 | # fqdn: localhost 6 | 7 | # Groups are defined in groups.yml 8 | groups: 9 | - defaults 10 | - linux 11 | - docker 12 | - docker-compose 13 | - systemd-service 14 | 15 | # Groups should define the bulk of configurations, but every module (monitors, commands, connectors) 16 | # can also be configured on host-level which will override settings from groups. 17 | monitors: 18 | ping: 19 | # If a critical monitor (ping, in this case), fails, the host is considered to be down. 20 | is_critical: true 21 | 22 | # You could also set settings for SSH connections on the group level. 23 | connectors: 24 | ssh: 25 | # Every module has settings defined this way. Valid settings are defined by the module 26 | settings: 27 | username: example-user 28 | # Default is 22. 29 | port: 12345 30 | -------------------------------------------------------------------------------- /macros/lightkeeper_module/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .* 3 | -------------------------------------------------------------------------------- /macros/lightkeeper_module/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lightkeeper_module" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | syn = "2.0.28" 11 | quote = "1.0.32" 12 | -------------------------------------------------------------------------------- /run-test-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | # RUST_LOG=debug ./target/debug/lightkeeper --config-dir test-env 4 | RUST_LOG=debug flatpak run --filesystem=~/git/lightkeeper/test-env io.github.kalaksi.Lightkeeper-local --config-dir test-env 5 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # With different Qt theme: 5 | # export QT_QUICK_CONTROLS_STYLE=org.kde.desktop 6 | # export QT_QUICK_CONTROLS_STYLE=Material 7 | # export QT_STYLE_OVERRIDE=Breeze 8 | 9 | # For testing QML modules: 10 | # export QML2_IMPORT_PATH="./third_party/qmltermwidget" 11 | # For debugging imports: 12 | # export QML_IMPORT_TRACE=1 13 | 14 | # Some OSes may disable QML debug logging so console.log() doesn't work. 15 | QT_LOGGING_RULES="*.debug=true; qt.*.debug=false" \ 16 | RUST_LOG=debug ./target/debug/lightkeeper 17 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | reorder_imports = true 3 | imports_layout = "HorizontalVertical" 4 | max_width = 150 5 | newline_style = "Unix" 6 | group_imports = "StdExternalCrate" 7 | control_brace_style = "ClosingNextLine" 8 | binop_separator = "Back" 9 | # attr_fn_like_width = 120 10 | # indent_style = "Visual" 11 | # wrap_comments = true 12 | -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | pub mod criticality; 2 | pub use criticality::Criticality; 3 | 4 | pub mod host_status; 5 | pub use host_status::HostStatus; 6 | -------------------------------------------------------------------------------- /src/enums/criticality.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | use strum_macros::{Display, EnumString}; 3 | 4 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy, Serialize, Deserialize, Display, EnumString)] 5 | pub enum Criticality { 6 | Ignore, 7 | Normal, 8 | /// Info is basically Normal level but it will be displayed to user in some cases where Normal won't. 9 | Info, 10 | /// Currently same as "unknown" or "pending". Initial result. 11 | NoData, 12 | Warning, 13 | Error, 14 | Critical, 15 | /// When command or service is not available in the system and therefore can't be monitored. 16 | NotAvailable, 17 | } 18 | -------------------------------------------------------------------------------- /src/enums/host_status.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | use std::fmt::Display; 3 | use std::str::FromStr; 4 | 5 | use serde_derive::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Default, Deserialize, Clone, Copy)] 8 | #[serde(rename_all = "lowercase")] 9 | pub enum HostStatus { 10 | Unknown, 11 | #[default] 12 | Pending, 13 | Up, 14 | Down, 15 | } 16 | 17 | impl FromStr for HostStatus { 18 | type Err = (); 19 | 20 | fn from_str(s: &str) -> Result { 21 | match String::from(s).to_lowercase().as_str() { 22 | "unknown" => Ok(HostStatus::Unknown), 23 | "pending" => Ok(HostStatus::Pending), 24 | "up" => Ok(HostStatus::Up), 25 | "down" => Ok(HostStatus::Down), 26 | _ => panic!("Invalid HostStatus '{}'", s), 27 | } 28 | } 29 | } 30 | 31 | impl Display for HostStatus { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | match self { 34 | HostStatus::Unknown => write!(f, "unknown"), 35 | HostStatus::Pending => write!(f, "pending"), 36 | HostStatus::Up => write!(f, "up"), 37 | HostStatus::Down => write!(f, "down"), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/frontend.rs: -------------------------------------------------------------------------------- 1 | pub mod frontend; 2 | pub use frontend::DisplayData; 3 | pub use frontend::HostDisplayData; 4 | pub use frontend::UIUpdate; 5 | 6 | pub mod display_options; 7 | pub use display_options::DisplayOptions; 8 | pub use display_options::DisplayStyle; 9 | pub use display_options::UserInputField; 10 | 11 | // TODO: Not sure if going to implement this. Remove later. 12 | // pub mod cli; 13 | pub mod qt; 14 | 15 | pub mod hot_reload; -------------------------------------------------------------------------------- /src/frontend/hot_reload.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | thread, 4 | time::Instant 5 | }; 6 | use qmetaobject; 7 | use std::sync::Arc; 8 | 9 | #[cfg(debug_assertions)] 10 | pub fn watch(path: PathBuf, engine: Arc) { 11 | 12 | use notify::{self, Watcher}; 13 | 14 | thread::spawn(move || { 15 | let (notify_sender, notify_receiver) = std::sync::mpsc::channel(); 16 | let mut watcher = notify::RecommendedWatcher::new(notify_sender, notify::Config::default()).unwrap(); 17 | 18 | if let Err(error) = watcher.watch(&path, notify::RecursiveMode::Recursive) { 19 | log::error!("Hot reload failed to initialize: {:?}", error); 20 | return; 21 | } 22 | 23 | log::info!("Hot reload is in use"); 24 | 25 | let mut last_reload = Instant::now(); 26 | 27 | loop { 28 | let event_result = notify_receiver.recv().unwrap(); 29 | if let Ok(event) = event_result { 30 | if event.kind.is_access() || last_reload.elapsed().as_millis() < 500 { 31 | continue; 32 | } 33 | 34 | log::debug!("Reload triggered by file {:?}", event.paths[0].display()); 35 | engine.trim_component_cache(); 36 | engine.clear_component_cache(); 37 | last_reload = Instant::now(); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | #[cfg(not(debug_assertions))] 44 | pub fn watch(_path: PathBuf, _engine: Arc) { 45 | // No hot reload in release mode. 46 | } 47 | -------------------------------------------------------------------------------- /src/frontend/qt.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod qml_frontend; 3 | pub use qml_frontend::QmlFrontend; 4 | 5 | mod models; 6 | mod resources; 7 | mod resources_qml; -------------------------------------------------------------------------------- /src/frontend/qt/fonts/Pixeloid/LICENSE: -------------------------------------------------------------------------------- 1 | license: SIL Open Font License (OFL) 2 | link: https://www.fontspace.com/pixeloid-font-f69232 -------------------------------------------------------------------------------- /src/frontend/qt/fonts/Pixeloid/PixeloidSans-nR3g1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/src/frontend/qt/fonts/Pixeloid/PixeloidSans-nR3g1.ttf -------------------------------------------------------------------------------- /src/frontend/qt/fonts/PressStart2P/PressStart2P-vaV7.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/src/frontend/qt/fonts/PressStart2P/PressStart2P-vaV7.ttf -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/README.md: -------------------------------------------------------------------------------- 1 | Breeze icons copyright KDE and licenced under the GNU LGPL version 3 or later (https://develop.kde.org/frameworks/breeze-icons/). 2 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/VERSION: -------------------------------------------------------------------------------- 1 | 5.95.0 2 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/akonadiconsole.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/alarm-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/application-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/configure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/data-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/data-information.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/data-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/dialog-cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/dialog-ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/document-open-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/document-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/document-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/document-save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/drive-harddisk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/edit-clear-all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/edit-clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/edit-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/edit-undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/entry-edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/find-location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/gnumeric-column-size.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/group-new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/help-keyboard-shortcuts.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/list-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/list-remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/media-playback-start.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/media-playback-stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/office-chart-area-stacked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/office-chart-bar-stacked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/overflow-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/project-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/resizecol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/run-build-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/run-build.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/story-editor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/system-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/system-shutdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/tab-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/update-high.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/update-low.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/update-none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/view-certificate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/view-refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/dark/window-new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/akonadiconsole.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/alarm-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/application-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/configure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/data-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/data-information.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/data-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/dialog-cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/dialog-ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/document-open-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/document-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/document-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/document-save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/drive-harddisk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/edit-clear-all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/edit-clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/edit-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/edit-undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/entry-edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/find-location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/gnumeric-column-size.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/group-new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/help-keyboard-shortcuts.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/list-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/list-remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/media-playback-start.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/media-playback-stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/office-chart-area-stacked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/office-chart-bar-stacked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/project-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/resizecol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/run-build-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/run-build.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/story-editor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/system-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/system-shutdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/tab-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/update-high.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/update-low.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/update-none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/view-certificate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/view-refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/frontend/qt/images/breeze/light/window-new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/VERSION: -------------------------------------------------------------------------------- 1 | 6.1.1 2 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/circle-arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/circle-arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/circle-check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/circle-exclamation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/docker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/exclamation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/terminal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/triangle-exclamation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/fontawesome/xmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/images/lightkeeper-tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalaksi/lightkeeper/ebf3e041f9680e5bfd276a4d67f34b9723a61d3c/src/frontend/qt/images/lightkeeper-tray-icon.png -------------------------------------------------------------------------------- /src/frontend/qt/images/nixos.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/qt/models.rs: -------------------------------------------------------------------------------- 1 | pub mod lkbackend_model; 2 | pub use lkbackend_model::LkBackend; 3 | 4 | pub mod theme_model; 5 | pub use theme_model::ThemeModel; 6 | 7 | pub mod host_data_manager_model; 8 | pub use host_data_manager_model::HostDataManagerModel; 9 | 10 | pub mod config_manager_model; 11 | pub use config_manager_model::ConfigManagerModel; 12 | 13 | pub mod host_table_model; 14 | pub use host_table_model::HostTableModel; 15 | 16 | pub mod command_handler_model; 17 | pub use command_handler_model::CommandHandlerModel; 18 | 19 | pub mod host_data_model; 20 | pub use host_data_model::HostDataModel; 21 | 22 | pub mod monitor_data_model; 23 | pub use monitor_data_model::MonitorDataModel; 24 | 25 | pub mod property_table_model; 26 | pub use property_table_model::PropertyTableModel; 27 | 28 | pub mod file_chooser_model; 29 | pub use file_chooser_model::FileChooserModel; 30 | 31 | pub mod metrics_manager_model; 32 | pub use metrics_manager_model::MetricsManagerModel; 33 | 34 | pub mod qmetatypes; -------------------------------------------------------------------------------- /src/frontend/qt/models/host_data_model.rs: -------------------------------------------------------------------------------- 1 | extern crate qmetaobject; 2 | use qmetaobject::*; 3 | 4 | use crate::frontend; 5 | use super::MonitorDataModel; 6 | 7 | /// HostData is the corresponding Qt struct for frontend::HostDisplayData. 8 | /// Contains host and state information. 9 | #[derive(QGadget, Default, Clone)] 10 | pub struct HostDataModel { 11 | pub status: qt_property!(QString), 12 | pub name: qt_property!(QString), 13 | pub fqdn: qt_property!(QString), 14 | pub ip_address: qt_property!(QString), 15 | pub monitor_data: qt_property!(MonitorDataModel), 16 | } 17 | 18 | impl HostDataModel { 19 | pub fn from(host_display_data: &frontend::HostDisplayData) -> Self { 20 | HostDataModel { 21 | status: host_display_data.host_state.status.clone().to_string().into(), 22 | name: host_display_data.host_state.host.name.clone().into(), 23 | fqdn: host_display_data.host_state.host.fqdn.clone().into(), 24 | ip_address: host_display_data.host_state.host.ip_address.to_string().into(), 25 | monitor_data: MonitorDataModel::new(&host_display_data), 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/frontend/qt/models/monitor_data_model.rs: -------------------------------------------------------------------------------- 1 | extern crate qmetaobject; 2 | use qmetaobject::*; 3 | 4 | use crate::frontend; 5 | 6 | #[derive(Default, Clone)] 7 | pub struct MonitorDataModel { 8 | pub data: QVariantMap, 9 | } 10 | 11 | impl MonitorDataModel { 12 | pub fn new(host_display_data: &frontend::HostDisplayData) -> Self { 13 | let mut model = MonitorDataModel { 14 | ..Default::default() 15 | }; 16 | 17 | for (monitor_id, monitor_data) in &host_display_data.host_state.monitor_data { 18 | model.data.insert( 19 | QString::from(monitor_id.clone()), 20 | serde_json::to_string(&monitor_data).unwrap().to_qvariant() 21 | ); 22 | } 23 | 24 | model 25 | } 26 | } 27 | 28 | 29 | impl QMetaType for MonitorDataModel { 30 | 31 | } -------------------------------------------------------------------------------- /src/frontend/qt/models/qmetatypes.rs: -------------------------------------------------------------------------------- 1 | extern crate qmetaobject; 2 | use qmetaobject::*; 3 | 4 | // QMetaType implementation makes it possible to pass the object to QML and back, 5 | // but it is not possible to otherwise use the object in a QML context. 6 | 7 | impl QMetaType for crate::module::monitoring::DataPoint { 8 | } 9 | 10 | impl QMetaType for crate::module::monitoring::MonitoringData { 11 | } 12 | 13 | impl QMetaType for crate::command_handler::CommandButtonData { 14 | } 15 | 16 | impl QMetaType for crate::frontend::DisplayData { 17 | } 18 | 19 | impl QMetaType for crate::configuration::DisplayOptions { 20 | } 21 | 22 | impl QMetaType for crate::configuration::Configuration { 23 | } 24 | 25 | impl QMetaType for crate::configuration::Hosts { 26 | } 27 | 28 | impl QMetaType for crate::configuration::Groups { 29 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/DebugRectangle.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | 3 | Rectangle { 4 | id: root 5 | color: "red" 6 | anchors.fill: parent 7 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/DetailsView/CategoryGroupBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | 5 | import "../Text" 6 | import "../js/TextTransform.js" as TextTransform 7 | 8 | 9 | GroupBox { 10 | id: root 11 | 12 | default property alias content: contentItem.data 13 | property string categoryName: "" 14 | property alias refreshProgress: groupBoxLabel.refreshProgress 15 | property bool isBlocked: refreshProgress < 100 16 | 17 | leftPadding: Theme.spacingTight 18 | rightPadding: Theme.spacingTight 19 | 20 | background: Rectangle { 21 | color: Theme.categoryBackgroundColor 22 | } 23 | 24 | 25 | // Custom label provides more flexibility. 26 | label: GroupBoxLabel { 27 | id: groupBoxLabel 28 | width: root.width 29 | 30 | text: TextTransform.capitalize(categoryName) 31 | icon: Theme.categoryIcon(categoryName) 32 | color: Theme.categoryColor(categoryName) 33 | 34 | showRefreshButton: true 35 | onRefreshClicked: root.refreshClicked() 36 | } 37 | 38 | signal refreshClicked() 39 | 40 | 41 | // Child components get put here. 42 | Item { 43 | id: contentItem 44 | anchors.fill: parent 45 | } 46 | 47 | Rectangle { 48 | anchors.fill: parent 49 | color: Theme.categoryRefreshMask 50 | visible: root.isBlocked 51 | 52 | MouseArea { 53 | anchors.fill: parent 54 | preventStealing: true 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Dialog/ConfirmationDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | 5 | import "../Text" 6 | import "../js/Utils.js" as Utils 7 | 8 | 9 | LightkeeperDialog { 10 | id: root 11 | 12 | property string text: "" 13 | // Center short text automatically. 14 | property bool centerText: text.length < 40 15 | 16 | title: "Confirmation" 17 | standardButtons: Dialog.Yes | Dialog.No 18 | implicitWidth: Utils.clamp(dialogText.implicitWidth, 300, 1000) + 100 19 | implicitHeight: Utils.clamp(dialogText.implicitHeight, 200, 600) + 50 20 | anchors.centerIn: parent 21 | 22 | contentItem: Item { 23 | id: content 24 | anchors.fill: parent 25 | anchors.margins: Theme.marginDialog 26 | anchors.topMargin: Theme.marginDialogTop 27 | anchors.bottomMargin: Theme.marginDialogBottom 28 | 29 | NormalText { 30 | id: dialogText 31 | text: root.text 32 | width: parent.width 33 | wrapMode: Text.Wrap 34 | horizontalAlignment: root.centerText ? Text.AlignHCenter : Text.AlignLeft 35 | } 36 | } 37 | 38 | Component.onCompleted: { 39 | visible = true 40 | } 41 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Dialog/DialogBackground.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | 4 | Rectangle { 5 | color: Theme.backgroundColor 6 | border.width: 1 7 | border.color: Theme.borderColor 8 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Dialog/LightkeeperDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | 5 | import ".." 6 | import "../Text" 7 | import "../Misc" 8 | 9 | 10 | Dialog { 11 | id: root 12 | modal: true 13 | opacity: visible ? 1.0 : 0.0 14 | title: "" 15 | // Use `anchors.centerIn: undefined` to avoid automatic centering. 16 | anchors.centerIn: parent 17 | 18 | property int borderRadius: 6 19 | property color headerBackground: Theme.titleBarColor 20 | 21 | background: Rectangle { 22 | width: root.width 23 | // To hide the top border, add border.width to the height so it goes under header. 24 | height: root.height - customHeader.height + border.width 25 | anchors.bottom: parent.bottom 26 | color: Theme.backgroundColor 27 | border.color: Theme.borderColor 28 | border.width: 1 29 | } 30 | 31 | Overlay.modal: Rectangle { 32 | color: "#60000000" 33 | } 34 | 35 | header: Rectangle { 36 | id: customHeader 37 | width: root.width 38 | height: 30 39 | radius: root.borderRadius 40 | color: root.headerBackground 41 | border.color: Theme.borderColor 42 | border.width: 1 43 | 44 | // Cover the border and rounding on bottom corners. 45 | BorderRectangle { 46 | anchors.bottom: parent.bottom 47 | width: root.width 48 | height: root.borderRadius 49 | backgroundColor: root.headerBackground 50 | borderColor: customHeader.border.color 51 | borderLeft: customHeader.border.width 52 | borderRight: borderLeft 53 | } 54 | 55 | NormalText { 56 | anchors.centerIn: parent 57 | text: root.title 58 | } 59 | } 60 | 61 | Behavior on opacity { 62 | NumberAnimation { 63 | duration: Theme.animationDurationFast 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Dialog/TextDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.11 4 | 5 | import ".." 6 | import "../Text" 7 | import "../StyleOverride" 8 | 9 | 10 | LightkeeperDialog { 11 | id: root 12 | property string text: "" 13 | property alias textFormat: dialogText.textFormat 14 | property alias wrapMode: dialogText.wrapMode 15 | 16 | modal: true 17 | implicitWidth: dialogText.contentWidth + 100 18 | implicitHeight: dialogText.contentHeight + 100 19 | standardButtons: Dialog.Close 20 | 21 | onClosed: { 22 | root.text = "" 23 | } 24 | 25 | // ScrollView doesn't have boundsBehavior so this is the workaround. 26 | Binding { 27 | target: scrollView.contentItem 28 | property: "boundsBehavior" 29 | value: Flickable.StopAtBounds 30 | } 31 | 32 | WorkingSprite { 33 | visible: root.text === "" 34 | } 35 | 36 | ScrollView { 37 | id: scrollView 38 | visible: root.text !== "" 39 | anchors.fill: parent 40 | anchors.margins: Theme.marginDialog 41 | 42 | NormalText { 43 | id: dialogText 44 | anchors.fill: parent 45 | wrapMode: Text.WordWrap 46 | textFormat: Text.MarkdownText 47 | text: root.text 48 | } 49 | } 50 | 51 | Behavior on width { 52 | NumberAnimation { 53 | duration: Theme.animationDurationFast 54 | } 55 | } 56 | 57 | Behavior on height { 58 | NumberAnimation { 59 | duration: Theme.animationDurationFast 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/DynamicObjectManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | 4 | Item { 5 | id: root 6 | default required property Component component 7 | 8 | property var _instances: [] 9 | property int _currentInstanceId: 1 10 | 11 | function create(properties = {}, signalHandlers = {}) { 12 | let instanceId = _currentInstanceId 13 | _currentInstanceId += 1 14 | 15 | let instance = component.createObject(root, properties) 16 | if (instance !== null) { 17 | _instances[instanceId] = instance 18 | for (const [name, handler] of Object.entries(signalHandlers)) { 19 | instance[name].connect(handler) 20 | } 21 | } 22 | else { 23 | console.log("Error creating object") 24 | } 25 | 26 | return [instanceId, instance] 27 | } 28 | 29 | function get(instanceId) { 30 | let instance = root._instances[instanceId] 31 | if (typeof instance === "undefined") { 32 | console.log(`Object ${instanceId} does not exist or is not ready yet`) 33 | } 34 | return instance 35 | } 36 | 37 | function destroyInstance(instanceId) { 38 | let instance = root.get(instanceId) 39 | if (typeof instance !== "undefined") { 40 | // console.log("Destroying instance " + instanceId) 41 | instance.destroy() 42 | delete root._instances[instanceId] 43 | } 44 | } 45 | 46 | function clear() { 47 | for (let instance of root._instances) { 48 | instance.destroy() 49 | } 50 | root._instances = [] 51 | root._currentInstanceId = 1 52 | } 53 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/HostStatus.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import Qt.labs.qmlmodels 1.0 3 | import QtQuick.Layouts 1.15 4 | 5 | import "Text" 6 | import "Misc" 7 | 8 | Item { 9 | id: root 10 | required property string status 11 | property var colors: {} 12 | property bool showIcon: true 13 | anchors.fill: parent 14 | 15 | Component.onCompleted: { 16 | colors = { 17 | up: "forestgreen", 18 | down: "firebrick", 19 | _: "orange", 20 | } 21 | } 22 | 23 | FontLoader { 24 | id: fontStatus 25 | source: "qrc:/main/fonts/pressstart2p" 26 | } 27 | 28 | RowLayout { 29 | anchors.fill: parent 30 | 31 | OverlayImage { 32 | id: image 33 | antialiasing: true 34 | source: "qrc:/main/images/status/" + root.status 35 | color: getColor(root.status) 36 | visible: showIcon 37 | 38 | Layout.leftMargin: showIcon ? 0.4 * parent.height : 0 39 | Layout.rightMargin: showIcon ? 0.4 * parent.height : 0 40 | Layout.preferredWidth: showIcon ? 0.7 * parent.height : 0 41 | Layout.preferredHeight: showIcon ? 0.7 * parent.height : 0 42 | Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter 43 | } 44 | 45 | NormalText { 46 | text: status.toUpperCase() 47 | font.family: fontStatus.name 48 | color: Theme.criticalityColor(root.status === "up" ? "normal" : root.status === "down" ? "error" : "_") 49 | 50 | Layout.fillWidth: true 51 | Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter 52 | } 53 | } 54 | 55 | function getColor(status) { 56 | let result = colors[status] 57 | if (typeof result === "undefined") { 58 | return colors["_"] 59 | } 60 | return result 61 | } 62 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/JsonTextFormat.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import Qt.labs.qmlmodels 1.0 4 | import QtQuick.Layouts 1.15 5 | 6 | import "Text" 7 | import "js/Parse.js" as Parse 8 | 9 | /// A component that displays JSON text in a markdown-like format. 10 | /// If the text doesn't seem to be JSON, it will be displayed as is. 11 | Item { 12 | id: root 13 | required property string jsonText 14 | implicitHeight: textContent.height 15 | implicitWidth: textContent.width 16 | 17 | NormalText { 18 | id: textContent 19 | wrapMode: Text.WordWrap 20 | textFormat: Text.MarkdownText 21 | text: createMarkdown(root.jsonText) 22 | } 23 | 24 | // TODO: Move these to a rust model? 25 | function createMarkdown(jsonText) { 26 | let text = "" 27 | 28 | if (jsonText !== "") { 29 | let data = Parse.TryParseJson(jsonText) 30 | 31 | if (data === null) { 32 | text += jsonText 33 | } 34 | else { 35 | text += objectToMarkdown(data, 0) 36 | } 37 | } 38 | 39 | return text 40 | } 41 | 42 | function objectToMarkdown(jsObject, indentLevel) { 43 | let text = "" 44 | let prefix = " ".repeat(indentLevel) 45 | let entries = Object.entries(jsObject) 46 | 47 | entries.forEach(item => { 48 | text += `${prefix}* ${item[0]}: ` 49 | 50 | if (typeof item[1] === "object") { 51 | text += "\n" 52 | 53 | if (item[1] !== null) { 54 | // Recursive call 55 | text += objectToMarkdown(item[1], indentLevel + 1) 56 | } 57 | } 58 | else { 59 | text += `${item[1]}\n` 60 | } 61 | }) 62 | 63 | return text 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Misc/BorderRectangle.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | 4 | Rectangle { 5 | id: root 6 | default property alias contentItem: background.data 7 | property int border: 0 8 | property int borderBottom: border > 0 ? border : 0 9 | property int borderTop: border > 0 ? border : 0 10 | property int borderLeft: border > 0 ? border : 0 11 | property int borderRight: border > 0 ? border : 0 12 | property color borderColor: palette.base 13 | property alias backgroundColor: background.color 14 | 15 | color: borderColor 16 | 17 | Rectangle { 18 | id: background 19 | anchors.fill: parent 20 | anchors.topMargin: root.borderTop 21 | anchors.bottomMargin: root.borderBottom 22 | anchors.leftMargin: root.borderLeft 23 | anchors.rightMargin: root.borderRight 24 | } 25 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Misc/CooldownTimer.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import Qt.labs.qmlmodels 1.0 3 | 4 | import CooldownTimerModel 1.0 5 | 6 | 7 | // Countdown timer for the cooldown canvas animation 8 | // TODO: deprecated 9 | Item { 10 | id: root 11 | 12 | signal triggered() 13 | signal allFinished() 14 | 15 | 16 | CooldownTimerModel { 17 | id: cooldowns 18 | 19 | onAllFinished: { 20 | root.allFinished() 21 | } 22 | } 23 | 24 | Timer { 25 | id: timer 26 | 27 | interval: 20 28 | repeat: true 29 | running: false 30 | onTriggered: { 31 | let cooldownCount = cooldowns.updateCooldowns(interval) 32 | if (cooldownCount === 0) { 33 | timer.stop() 34 | } 35 | 36 | root.triggered() 37 | } 38 | } 39 | 40 | function startCooldown(buttonIdentifier, invocationId) { 41 | cooldowns.startCooldown(buttonIdentifier, invocationId) 42 | 43 | // Does nothing if timer is already running. 44 | timer.start() 45 | } 46 | 47 | function finishCooldown(invocationId) { 48 | cooldowns.finishCooldown(invocationId) 49 | } 50 | 51 | function getCooldown(buttonIdentifier) { 52 | return cooldowns.getCooldown(buttonIdentifier) 53 | } 54 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Misc/OverlayImage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import Qt5Compat.GraphicalEffects 4 | 5 | 6 | Item { 7 | id: root 8 | property bool antialiasing: false 9 | property alias source: image.source 10 | property alias color: overlay.color 11 | property alias sourceSize: image.sourceSize 12 | 13 | Image { 14 | id: image 15 | anchors.fill: parent 16 | source: root.imageSource 17 | antialiasing: root.antialiasing 18 | } 19 | 20 | ColorOverlay { 21 | id: overlay 22 | anchors.fill: image 23 | source: image 24 | antialiasing: root.antialiasing 25 | } 26 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Misc/SemiCircle.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import Qt.labs.qmlmodels 1.0 3 | import QtQuick.Shapes 1.15 4 | 5 | 6 | 7 | // Semi-circle, with round end to the left. 8 | Shape { 9 | id: root 10 | property int radius: 20 11 | property alias color: shapePath.fillColor 12 | width: radius * 2 13 | height: parent.height 14 | 15 | ShapePath { 16 | id: shapePath 17 | strokeColor: "transparent" 18 | startX: root.width / 2 19 | startY: 0 20 | 21 | PathArc { 22 | direction: PathArc.Counterclockwise 23 | useLargeArc: true 24 | relativeX: 0 25 | relativeY: root.height 26 | radiusX: root.width / 2 27 | radiusY: root.height / 2 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Misc/Trapezoid.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Shapes 1.8 3 | 4 | Shape { 5 | id: root 6 | property alias color: shape.fillColor 7 | 8 | ShapePath { 9 | id: shape 10 | fillColor: "#ffffff" 11 | strokeWidth: 0 12 | strokeColor: "transparent" 13 | startX: 0 14 | startY: 0 15 | 16 | // Top edge. 17 | PathLine { x: 150; y: 0 } 18 | // Right slant. 19 | PathLine { x: 200; y: 100 } 20 | // Bottom edge. 21 | PathLine { x: 0; y: 100 } 22 | // Close the shape. 23 | PathLine { x: 0; y: 0 } 24 | } 25 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/RowHighlight.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.11 4 | 5 | Rectangle { 6 | id: root 7 | property alias containsMouse: mouseArea.containsMouse 8 | property bool selected: false 9 | 10 | color: Theme.backgroundColor 11 | radius: 4 12 | 13 | signal clicked 14 | 15 | MouseArea { 16 | id: mouseArea 17 | anchors.fill: parent 18 | hoverEnabled: true 19 | preventStealing: true 20 | 21 | onEntered: { 22 | if (!root.selected) { 23 | root.color = Theme.highlightColor 24 | } 25 | } 26 | 27 | onExited: { 28 | if (!root.selected) { 29 | root.color = Theme.backgroundColor 30 | } 31 | } 32 | 33 | onClicked: { 34 | root.clicked() 35 | root.selected = !root.selected 36 | 37 | if (root.selected) { 38 | root.color = Theme.highlightColor 39 | } 40 | else { 41 | root.color = Theme.backgroundColor 42 | } 43 | } 44 | 45 | // Child components get put here. 46 | Item { 47 | id: contentItem 48 | anchors.fill: parent 49 | } 50 | } 51 | 52 | Behavior on height { 53 | NumberAnimation { 54 | duration: { 55 | if (root.height > 0) { 56 | return Theme.animationDuration 57 | } 58 | else { 59 | // Usually, the initial size is often 0 and unnecessary animating happens when contents are rendered. 60 | return 0 61 | } 62 | } 63 | } 64 | } 65 | 66 | Behavior on color { 67 | ColorAnimation { 68 | duration: Theme.animationDuration 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/Button.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Controls.impl 4 | import QtQuick.Controls.Fusion 5 | import QtQuick.Controls.Fusion.impl 6 | 7 | Button { 8 | id: control 9 | 10 | contentItem: IconLabel { 11 | spacing: control.spacing 12 | mirrored: control.mirrored 13 | display: control.display 14 | 15 | icon: control.icon 16 | text: control.text 17 | font: control.font 18 | color: Theme.textColor 19 | } 20 | 21 | background: ButtonPanel { 22 | implicitWidth: 80 23 | implicitHeight: 24 24 | 25 | control: control 26 | visible: !control.flat || control.down || control.checked || control.highlighted || control.visualFocus || (enabled && control.hovered) 27 | } 28 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/Label.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Controls.impl 4 | import QtQuick.Controls.Fusion 5 | import QtQuick.Controls.Fusion.impl 6 | 7 | Label { 8 | id: control 9 | 10 | color: Theme.textColor 11 | linkColor: "#1d99f3" 12 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/README.md: -------------------------------------------------------------------------------- 1 | For some reason, palette colors don't get applied correctly on KDE. 2 | This directory therefore contains necessary patches to fix the colors. Usually they contain modified original implementation. 3 | 4 | To make matters more weird, it seems that locally built flatpak applies palette correctly just by re-defining some components, but 5 | the published flatpak doesn't seem to respect palette at all if not forcing the colors. -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/RoundButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import org.kde.kirigami as Kirigami 4 | 5 | RoundButton { 6 | Kirigami.Theme.inherit: false 7 | Kirigami.Theme.backgroundColor: palette.window 8 | Kirigami.Theme.alternateBackgroundColor: Qt.darker(palette.window, 1.05) 9 | Kirigami.Theme.hoverColor: palette.highlight 10 | Kirigami.Theme.focusColor: palette.highlight 11 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/ScrollBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls.impl 3 | import QtQuick.Controls.Fusion 4 | import QtQuick.Controls.Fusion.impl 5 | 6 | ScrollBar { 7 | id: control 8 | 9 | contentItem: Rectangle { 10 | implicitWidth: control.interactive ? 6 : 2 11 | implicitHeight: control.interactive ? 6 : 2 12 | 13 | radius: width / 2 14 | color: control.pressed ? "#3a4045" : "#474d54" 15 | opacity: 0.0 16 | 17 | states: State { 18 | name: "active" 19 | when: control.policy === ScrollBar.AlwaysOn || (control.active && control.size < 1.0) 20 | PropertyChanges { control.contentItem.opacity: 0.75 } 21 | } 22 | 23 | transitions: Transition { 24 | from: "active" 25 | SequentialAnimation { 26 | PauseAnimation { duration: 450 } 27 | NumberAnimation { target: control.contentItem; duration: 200; property: "opacity"; to: 0.0 } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/ScrollView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls.impl 3 | import QtQuick.Controls.Fusion 4 | import QtQuick.Controls.Fusion.impl 5 | 6 | import "." 7 | 8 | ScrollView { 9 | id: control 10 | 11 | ScrollBar.vertical: ScrollBar { 12 | parent: control 13 | x: control.mirrored ? 0 : control.width - width 14 | y: control.topPadding 15 | height: control.availableHeight 16 | active: control.ScrollBar.horizontal.active 17 | } 18 | 19 | ScrollBar.horizontal: ScrollBar { 20 | parent: control 21 | x: control.leftPadding 22 | y: control.height - height 23 | width: control.availableWidth 24 | active: control.ScrollBar.vertical.active 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/TextField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Controls.impl 4 | import QtQuick.Controls.Fusion 5 | import QtQuick.Controls.Fusion.impl 6 | 7 | 8 | TextField { 9 | id: control 10 | 11 | // This is actually the original implementation but defining it again makes the palette work. 12 | background: Rectangle { 13 | implicitWidth: 120 14 | implicitHeight: 24 15 | 16 | radius: 2 17 | color: Theme.baseColor 18 | border.color: control.activeFocus ? Fusion.highlightedOutline(control.palette) : Fusion.outline(control.palette) 19 | 20 | Rectangle { 21 | x: 1; y: 1 22 | width: parent.width - 2 23 | height: parent.height - 2 24 | color: "transparent" 25 | border.color: Color.transparent(Fusion.highlightedOutline(control.palette), 40 / 255) 26 | visible: control.activeFocus 27 | radius: 1.7 28 | } 29 | 30 | Rectangle { 31 | x: 2 32 | y: 1 33 | width: parent.width - 4 34 | height: 1 35 | color: Fusion.topShadow 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/StyleOverride/ToolButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Controls.impl 4 | import QtQuick.Controls.Fusion 5 | import QtQuick.Controls.Fusion.impl 6 | 7 | 8 | ToolButton { 9 | id: control 10 | 11 | contentItem: IconLabel { 12 | spacing: control.spacing 13 | mirrored: control.mirrored 14 | display: control.display 15 | 16 | icon: control.icon 17 | text: control.text 18 | font: control.font 19 | color: Theme.textColor 20 | } 21 | 22 | background: ButtonPanel { 23 | implicitWidth: 20 24 | implicitHeight: 20 25 | 26 | control: control 27 | visible: control.down || control.checked || control.highlighted || control.visualFocus || (enabled && control.hovered) 28 | } 29 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/TableCell.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | 4 | Item { 5 | id: root 6 | default property alias contentItem: contentItem.data 7 | property bool useRounding: true 8 | property bool firstItem: false 9 | property bool selected: false 10 | property int padding: 0 11 | implicitHeight: 40 12 | implicitWidth: parent.width 13 | 14 | signal clicked() 15 | 16 | // Stylish rounded cell for first item. 17 | Rectangle { 18 | id: rounded 19 | anchors.fill: parent 20 | radius: root.useRounding && parent.firstItem ? 9 : 0 21 | color: getBackgroundColor(root.selected) 22 | 23 | MouseArea { 24 | anchors.fill: parent 25 | onClicked: root.clicked() 26 | } 27 | } 28 | 29 | Rectangle { 30 | color: getBackgroundColor(root.selected) 31 | width: rounded.radius 32 | anchors.top: rounded.top 33 | anchors.bottom: rounded.bottom 34 | anchors.right: rounded.right 35 | } 36 | 37 | Item { 38 | id: contentItem 39 | height: parent.height 40 | width: parent.width - padding 41 | anchors.centerIn: parent 42 | } 43 | 44 | // It seems that at least Qt 6.6 now has native support for alternating row colors. 45 | // TODO: use the native support when it's available. 46 | function getBackgroundColor(selected) { 47 | if (selected === true) { 48 | return Qt.darker(palette.highlight) 49 | } 50 | else if (model.row % 2 == 0) { 51 | return Theme.alternateBaseColor 52 | } 53 | else { 54 | return Theme.baseColor 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Tests/Test.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtTest 1.2 4 | 5 | import "../" 6 | import "../js/Test.js" as TestUtils 7 | 8 | 9 | TestCase { 10 | id: root 11 | name: "StartupTest" 12 | 13 | function run(app) { 14 | testCheckInitialStatus(app) 15 | } 16 | // Component { 17 | // id: app 18 | 19 | // Main { 20 | // } 21 | // } 22 | 23 | // property var appRoot: null 24 | 25 | // function initTestCase() { 26 | // appRoot = appComponent.createObject(root) 27 | // verify(appRoot !== null, "Application should be created successfully") 28 | // } 29 | 30 | // function cleanupTestCase() { 31 | // appRoot.destroy() 32 | // } 33 | 34 | // function test_navigation() { 35 | // compare("no", "Test Input", "Text field should contain 'Test Input'") 36 | // } 37 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/AlertText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | 3 | 4 | Item { 5 | id: root 6 | required property string text 7 | required property string criticality 8 | property real imageScale: 2.0 9 | 10 | implicitWidth: textContent.contentWidth + image.width 11 | implicitHeight: Math.max(textContent.implicitHeight, image.height) 12 | 13 | Row { 14 | id: row 15 | padding: 20 16 | spacing: 10 17 | anchors.verticalCenter: parent.verticalCenter 18 | 19 | Image { 20 | id: image 21 | antialiasing: true 22 | source: Theme.iconForCriticality(root.criticality) 23 | width: 22 * root.imageScale 24 | height: 22 * root.imageScale 25 | anchors.verticalCenter: parent.verticalCenter 26 | } 27 | 28 | NormalText { 29 | id: textContent 30 | anchors.verticalCenter: parent.verticalCenter 31 | text: root.text 32 | wrapMode: Text.Wrap 33 | width: root.width - image.width - row.spacing - row.padding * 2 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/BaseText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Text { 4 | id: root 5 | color: Theme.textColor 6 | textFormat: Text.PlainText 7 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/BigText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | BaseText { 4 | font.pointSize: Theme.fontSize + 2 5 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/NormalText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | BaseText { 4 | font.pointSize: Theme.fontSize 5 | } 6 | 7 | // NOTE: There's currently a bug in Qt where Text is not selectable: https://bugreports.qt.io/browse/QTBUG-14077 8 | // Workaround: 9 | // TextEdit { 10 | // font.pointSize: 10 11 | // readOnly: true 12 | // wrapMode: Text.WordWrap 13 | // selectByMouse: true 14 | // } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/OptionalText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import Qt.labs.qmlmodels 1.0 3 | 4 | Item { 5 | // NOTE: Required properties can cause issues if used with modelData, like errors such as: 6 | // "ReferenceError: modelData is not defined" 7 | // That's why these are not required properties anymore: 8 | property string placeholder: "" 9 | property string text: "" 10 | 11 | implicitWidth: textComponent.width 12 | implicitHeight: textComponent.height 13 | 14 | NormalText { 15 | id: textComponent 16 | anchors.verticalCenter: parent.verticalCenter 17 | text: parent.text.length === 0 ? placeholder : parent.text 18 | font.italic: parent.text.length === 0 19 | opacity: parent.text.length === 0 ? 0.3 : 1 20 | } 21 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/PillText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | 3 | import "../js/TextTransform.js" as TextTransform 4 | 5 | Item { 6 | id: root 7 | property string text: "" 8 | property alias textColor: textElement.color 9 | property string pillColor: "#30FFFFFF" 10 | property int padding: 2 11 | 12 | implicitWidth: textElement.implicitWidth + padding * 2 13 | implicitHeight: textElement.implicitHeight 14 | 15 | Rectangle { 16 | color: root.pillColor 17 | anchors.fill: parent 18 | radius: parent.height 19 | visible: TextTransform.removeWhitespaces(textElement.text).length > 0 20 | } 21 | 22 | Text { 23 | id: textElement 24 | text: root.text 25 | anchors.fill: parent 26 | font.pointSize: 8 27 | verticalAlignment: Text.AlignVCenter 28 | horizontalAlignment: Text.AlignHCenter 29 | leftPadding: root.padding 30 | rightPadding: root.padding 31 | } 32 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/PixelatedText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | 3 | Text { 4 | color: Theme.textColorDark 5 | font.pixelSize: 8 6 | font.family: fontLabel.name 7 | antialiasing: false 8 | font.letterSpacing: 1 9 | 10 | FontLoader { 11 | id: fontLabel 12 | source: "qrc:/main/fonts/pixeloid" 13 | } 14 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/SmallText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | BaseText { 4 | font.pointSize: Theme.fontSize - 2 5 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/Text/SmallerText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | BaseText { 4 | font.pointSize: Theme.fontSize - 4 5 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/WaveAnimation.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import Qt.labs.qmlmodels 1.0 4 | 5 | Item { 6 | id: root 7 | property string color: "#555555" 8 | property real radius: Math.min(parent.width, parent.height) * 0.5 9 | property int duration: 500 10 | 11 | Rectangle { 12 | id: rectangle 13 | anchors.centerIn: parent 14 | radius: root.radius 15 | color: root.color 16 | 17 | opacity: 0.7 18 | width: radius * 1.5 19 | height: width 20 | 21 | NumberAnimation on radius { 22 | to: root.radius * 2.5 23 | duration: root.duration 24 | } 25 | 26 | NumberAnimation on opacity { 27 | to: 0.0 28 | duration: root.duration 29 | } 30 | } 31 | 32 | states: [ 33 | // Set visibility to false when completely transparent to disable unneeded components. 34 | // Not sure if this is needed or already done automatically. 35 | State { 36 | name: "hideWhenTransparent" 37 | when: rectangle.opacity < 0.01 38 | 39 | PropertyChanges { 40 | target: root 41 | visible: false 42 | } 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/WorkingSprite.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import Qt.labs.qmlmodels 1.0 4 | import QtQuick.Layouts 1.15 5 | 6 | import "Text" 7 | 8 | 9 | /// Don't put directly under Layout-components. Wrap inside an Item then. 10 | Item { 11 | id: root 12 | property real scale: 1.5 13 | property string text: "" 14 | 15 | anchors.centerIn: parent 16 | anchors.verticalCenterOffset: -0.1 * parent.height 17 | 18 | Column { 19 | anchors.fill: parent 20 | spacing: 10 21 | 22 | AnimatedSprite { 23 | id: sprite 24 | anchors.horizontalCenter: parent.horizontalCenter 25 | source: "qrc:/main/images/animations/working" 26 | frameWidth: 22 27 | frameHeight: 22 28 | frameCount: 15 29 | frameDuration: 60 30 | scale: root.scale 31 | } 32 | 33 | NormalText { 34 | id: textContent 35 | anchors.horizontalCenter: parent.horizontalCenter 36 | visible: root.text !== "" 37 | text: root.text 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/js/Parse.js: -------------------------------------------------------------------------------- 1 | 2 | function ParseIfJson(text) { 3 | try { 4 | return JSON.parse(text) 5 | } 6 | catch(_) { 7 | return text 8 | } 9 | } 10 | 11 | function TryParseJson(text) { 12 | try { 13 | return JSON.parse(text) 14 | } 15 | catch(_) { 16 | return null 17 | } 18 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/js/Test.js: -------------------------------------------------------------------------------- 1 | 2 | function test(app) { 3 | try { 4 | testCheckInitialStatus(app) 5 | testSelectHost(app) 6 | } 7 | catch (e) { 8 | return "FAIL: " + e 9 | } 10 | return "OK" 11 | } 12 | 13 | function testCheckInitialStatus(app) { 14 | let table = findByName(app.contentItem, "hostTable") 15 | 16 | assert(table.model.rowCount(), 5) 17 | assert(table.model.getSelectedHostId(), "") 18 | assert(getTableElement(table, 0, 1), "centos7") 19 | assert(getTableElement(table, 1, 1), "centos8") 20 | assert(getTableElement(table, 2, 1), "debian10") 21 | assert(getTableElement(table, 3, 1), "debian11") 22 | assert(getTableElement(table, 4, 1), "ubuntu2004") 23 | } 24 | 25 | function testSelectHost(app) { 26 | let table = findByName(app.contentItem, "hostTable") 27 | table.model.selectedRow = 1 28 | 29 | assert(table.model.getSelectedHostId(), "centos8") 30 | 31 | let details = findByName(app.contentItem, "detailsView") 32 | assert(detailsView.visible, true) 33 | } 34 | 35 | // 36 | // Test helpers for often used operations. 37 | // 38 | 39 | function getTableElement(table, row, column) { 40 | let tableIndex = table.model.index(row, column) 41 | return table.model.data(tableIndex, 256) 42 | } 43 | 44 | 45 | // 46 | // Common helper functions. 47 | // 48 | function assert(value, expected) { 49 | if (value !== expected) { 50 | let caller = new Error().stack.split("\n")[1].trim() 51 | throw `${caller}: expected: ${expected}, got: ${value}` 52 | } 53 | } 54 | 55 | function findByName(parent, name) { 56 | for (let i = 0; i < parent.children.length; i++) { 57 | let item = parent.children[i] 58 | 59 | if (item.objectName === name) { 60 | return item 61 | } 62 | else { 63 | let result = findByName(item, name) 64 | if (result) { 65 | return result 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/js/TextTransform.js: -------------------------------------------------------------------------------- 1 | 2 | function trimNewline(text) { 3 | if (text.endsWith('\n')) { 4 | return text.slice(0, -1) 5 | } 6 | else { 7 | return text 8 | } 9 | } 10 | 11 | function capitalize(text) { 12 | if (text.length > 1) { 13 | return text[0].toUpperCase() + text.slice(1) 14 | } 15 | else { 16 | return text 17 | } 18 | } 19 | 20 | function truncate(text, maxLength) { 21 | if (text.length > maxLength) { 22 | return text.substr(0, maxLength-1) + '…' 23 | } 24 | return text 25 | } 26 | 27 | function removeWhitespaces(text) { 28 | return text.replace(/\s/g, '') 29 | } 30 | 31 | /// This is probably not completely secure so don't use it for anything important 32 | function escapeHtml(text) 33 | { 34 | return text.replace(//g, ">") 36 | .replace(/&/g, "&") 37 | .replace(/"/g, """) 38 | .replace(/'/g, "'"); 39 | } -------------------------------------------------------------------------------- /src/frontend/qt/qml/js/ValueUnit.js: -------------------------------------------------------------------------------- 1 | 2 | function AsText(value, unit, empty = "") { 3 | if (typeof value === "undefined" || value === null || value.length === 0) { 4 | return empty 5 | } 6 | else { 7 | return value + " " + unit 8 | } 9 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::redundant_field_names)] 2 | #![allow(clippy::needless_return)] 3 | #![forbid(unsafe_code)] 4 | 5 | use clap::Parser; 6 | use lightkeeper::*; 7 | 8 | #[derive(Parser, Clone)] 9 | pub struct Args { 10 | #[clap(short, long, default_value = "")] 11 | pub config_dir: String, 12 | #[clap(long)] 13 | pub monitoring_module_info: bool, 14 | #[clap(long)] 15 | pub command_module_info: bool, 16 | #[clap(long)] 17 | pub connector_module_info: bool, 18 | } 19 | 20 | fn main() { 21 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 22 | let args = Args::parse(); 23 | 24 | if args.monitoring_module_info { 25 | let module_factory = ModuleFactory::new(); 26 | print!("{}", module_factory.get_monitoring_module_info()); 27 | return; 28 | } 29 | if args.command_module_info { 30 | let module_factory = ModuleFactory::new(); 31 | print!("{}", module_factory.get_command_module_info()); 32 | return; 33 | } 34 | if args.connector_module_info { 35 | let module_factory = ModuleFactory::new(); 36 | print!("{}", module_factory.get_connector_module_info()); 37 | return; 38 | } 39 | 40 | loop { 41 | log::info!("Lightkeeper starting..."); 42 | 43 | let (main_config, hosts_config, group_config) = match Configuration::read(&args.config_dir) { 44 | Ok(configuration) => configuration, 45 | Err(error) => { 46 | log::error!("Error while reading configuration files: {}", error); 47 | break; 48 | } 49 | }; 50 | 51 | let exit_reason = lightkeeper::run(&args.config_dir, &main_config, &hosts_config, &group_config, false); 52 | 53 | match exit_reason { 54 | ExitReason::Quit => break, 55 | ExitReason::Error => break, 56 | ExitReason::Restart => continue, 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | pub mod tmserver; 2 | 3 | pub mod metric; 4 | pub use metric::*; 5 | 6 | pub mod metrics_manager; 7 | pub use metrics_manager::MetricsManager; 8 | -------------------------------------------------------------------------------- /src/metrics/download-token.txt: -------------------------------------------------------------------------------- 1 | DBgeWUJfABYMMCBCYFQ5I3Z1ViVuFFw8W1s= -------------------------------------------------------------------------------- /src/metrics/metric.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Default, Serialize, Deserialize)] 4 | pub struct Metric { 5 | pub time: i64, 6 | pub label: String, 7 | pub value: i64, 8 | } 9 | 10 | impl From for crate::metrics::tmserver::Metric { 11 | fn from(metric: Metric) -> Self { 12 | crate::metrics::tmserver::Metric { 13 | time: metric.time, 14 | label: metric.label, 15 | value: metric.value, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/metrics/tmserver.rs: -------------------------------------------------------------------------------- 1 | pub mod tmsrequest; 2 | pub use tmsrequest::*; 3 | 4 | pub mod connection; 5 | pub use connection::*; 6 | -------------------------------------------------------------------------------- /src/metrics/tmserver/tmsrequest.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This module contains the client-server communication protocol of the locally run TMServer metrics server. 3 | /// Protocol version 1.0 4 | /// 5 | use std::collections::HashMap; 6 | use std::fmt::Debug; 7 | 8 | use serde_derive::{Deserialize, Serialize}; 9 | 10 | #[derive(Clone, Default, Serialize, Deserialize)] 11 | pub struct Metric { 12 | pub time: i64, 13 | pub label: String, 14 | pub value: i64, 15 | } 16 | 17 | #[derive(Serialize, Deserialize)] 18 | pub struct TMSRequest { 19 | pub request_id: u64, 20 | /// Requester sets this to unix time in milliseconds. 21 | pub time: u32, 22 | pub request_type: RequestType, 23 | } 24 | 25 | #[derive(Serialize, Deserialize)] 26 | pub enum RequestType { 27 | Healthcheck, 28 | Exit, 29 | MetricsInsert { 30 | host_id: String, 31 | metric_id: String, 32 | metrics: Vec, 33 | }, 34 | MetricsQuery { 35 | host_id: String, 36 | metric_id: String, 37 | /// Unix timestamp in seconds. 38 | start_time: i64, 39 | /// Unix timestamp in seconds. 40 | end_time: i64, 41 | }, 42 | } 43 | 44 | impl TMSRequest { 45 | pub fn exit() -> Self { 46 | TMSRequest { 47 | request_id: 0, 48 | time: 0, 49 | request_type: RequestType::Exit, 50 | } 51 | } 52 | } 53 | 54 | impl Debug for TMSRequest { 55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 | write!(f, "ServiceRequest({})", self.request_id) 57 | } 58 | } 59 | 60 | #[derive(Clone, Default, Serialize, Deserialize)] 61 | pub struct TMSResponse { 62 | pub request_id: u64, 63 | /// In milliseconds. 0 if not set. 64 | pub lag: u32, 65 | pub metrics: HashMap>, 66 | pub errors: Vec, 67 | } 68 | -------------------------------------------------------------------------------- /src/module/command.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod command_module; 3 | pub use command_module::CommandModule; 4 | pub use command_module::Command; 5 | pub use command_module::CommandResult; 6 | pub use command_module::UIAction; 7 | pub use command_module::BoxCloneableCommand; 8 | 9 | pub mod docker; 10 | pub mod linux; 11 | pub mod os; 12 | pub mod storage; 13 | pub mod systemd; 14 | pub mod nixos; 15 | pub mod network; 16 | pub mod internal; 17 | -------------------------------------------------------------------------------- /src/module/command/docker.rs: -------------------------------------------------------------------------------- 1 | pub mod inspect; 2 | pub use inspect::Inspect; 3 | 4 | pub mod restart; 5 | pub use restart::Restart; 6 | 7 | pub mod shell; 8 | pub use shell::Shell; 9 | 10 | pub mod compose; 11 | 12 | pub mod image; -------------------------------------------------------------------------------- /src/module/command/docker/compose.rs: -------------------------------------------------------------------------------- 1 | pub mod edit; 2 | pub use edit::Edit; 3 | 4 | pub mod pull; 5 | pub use pull::Pull; 6 | 7 | pub mod up; 8 | pub use up::Up; 9 | 10 | pub mod start; 11 | pub use start::Start; 12 | 13 | pub mod stop; 14 | pub use stop::Stop; 15 | 16 | pub mod shell; 17 | pub use shell::Shell; 18 | 19 | pub mod logs; 20 | pub use logs::Logs; 21 | 22 | pub mod build; 23 | pub use build::Build; -------------------------------------------------------------------------------- /src/module/command/docker/compose/edit.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use crate::error::LkError; 3 | use crate::frontend; 4 | use crate::host::*; 5 | use crate::module::*; 6 | use crate::module::command::*; 7 | use lightkeeper_module::command_module; 8 | 9 | #[command_module( 10 | name="docker-compose-edit", 11 | version="0.0.1", 12 | description="Launches an editor for editing a compose-file.", 13 | )] 14 | pub struct Edit { 15 | } 16 | 17 | impl Module for Edit { 18 | fn new(_settings: &HashMap) -> Edit { 19 | Edit { 20 | } 21 | } 22 | } 23 | 24 | impl CommandModule for Edit { 25 | fn get_connector_spec(&self) -> Option { 26 | Some(ModuleSpecification::connector("ssh", "0.0.1")) 27 | } 28 | 29 | fn get_display_options(&self) -> frontend::DisplayOptions { 30 | frontend::DisplayOptions { 31 | category: String::from("docker-compose"), 32 | parent_id: String::from("docker-compose"), 33 | multivalue_level: 1, 34 | display_style: frontend::DisplayStyle::Icon, 35 | display_icon: String::from("story-editor"), 36 | display_text: String::from("Edit compose-file"), 37 | action: UIAction::TextEditor, 38 | tab_title: String::from("Editor"), 39 | ..Default::default() 40 | } 41 | } 42 | 43 | fn get_connector_message(&self, _host: Host, parameters: Vec) -> Result { 44 | let compose_file = parameters.first().unwrap().clone(); 45 | Ok(compose_file) 46 | } 47 | } -------------------------------------------------------------------------------- /src/module/command/docker/image.rs: -------------------------------------------------------------------------------- 1 | pub mod remove; 2 | pub use remove::Remove; 3 | 4 | pub mod prune; 5 | pub use prune::Prune; 6 | 7 | pub mod remote_tags; 8 | pub use remote_tags::RemoteTags; -------------------------------------------------------------------------------- /src/module/command/internal.rs: -------------------------------------------------------------------------------- 1 | pub mod custom_command; 2 | pub use custom_command::CustomCommand; -------------------------------------------------------------------------------- /src/module/command/linux.rs: -------------------------------------------------------------------------------- 1 | pub mod logs; 2 | pub use logs::Logs; 3 | 4 | pub mod packages; 5 | 6 | pub mod shell; 7 | pub use shell::Shell; -------------------------------------------------------------------------------- /src/module/command/linux/packages.rs: -------------------------------------------------------------------------------- 1 | pub mod clean; 2 | pub use clean::Clean; 3 | 4 | pub mod install; 5 | pub use install::Install; 6 | 7 | pub mod uninstall; 8 | pub use uninstall::Uninstall; 9 | 10 | pub mod update; 11 | pub use update::Update; 12 | 13 | pub mod update_all; 14 | pub use update_all::UpdateAll; 15 | 16 | pub mod refresh; 17 | pub use refresh::Refresh; 18 | 19 | pub mod logs; 20 | pub use logs::Logs; -------------------------------------------------------------------------------- /src/module/command/linux/packages/install.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use crate::error::LkError; 3 | use crate::frontend; 4 | use crate::host::Host; 5 | use crate::module::connection::ResponseMessage; 6 | use crate::module::*; 7 | use crate::module::command::*; 8 | use lightkeeper_module::command_module; 9 | 10 | #[command_module( 11 | name="linux-packages-install", 12 | version="0.0.1", 13 | description="Installs system packages.", 14 | )] 15 | pub struct Install; 16 | 17 | impl Module for Install { 18 | fn new(_settings: &HashMap) -> Self { 19 | Self { } 20 | } 21 | } 22 | 23 | impl CommandModule for Install { 24 | fn get_connector_spec(&self) -> Option { 25 | Some(ModuleSpecification::connector("ssh", "0.0.1")) 26 | } 27 | 28 | fn get_display_options(&self) -> frontend::DisplayOptions { 29 | frontend::DisplayOptions { 30 | category: String::from("packages"), 31 | display_style: frontend::DisplayStyle::Icon, 32 | display_icon: String::from(""), 33 | display_text: String::from("Install packages"), 34 | ..Default::default() 35 | } 36 | } 37 | 38 | fn get_connector_message(&self, _host: Host, _parameters: Vec) -> Result { 39 | todo!() 40 | } 41 | 42 | fn process_response(&self, _host: Host, _response: &ResponseMessage) -> Result { 43 | todo!() 44 | } 45 | } -------------------------------------------------------------------------------- /src/module/command/network.rs: -------------------------------------------------------------------------------- 1 | pub mod socket_listen; 2 | pub use socket_listen::SocketListen; 3 | 4 | pub mod socket_tcp; 5 | pub use socket_tcp::SocketTcp; -------------------------------------------------------------------------------- /src/module/command/nixos.rs: -------------------------------------------------------------------------------- 1 | pub mod rebuild_dryrun; 2 | pub use rebuild_dryrun::RebuildDryrun; 3 | 4 | pub mod rebuild_switch; 5 | pub use rebuild_switch::RebuildSwitch; 6 | 7 | pub mod rebuild_boot; 8 | pub use rebuild_boot::RebuildBoot; 9 | 10 | pub mod collectgarbage; 11 | pub use collectgarbage::CollectGarbage; 12 | 13 | pub mod channel_update; 14 | pub use channel_update::ChannelUpdate; 15 | 16 | pub mod rebuild_rollback; 17 | pub use rebuild_rollback::RebuildRollback; -------------------------------------------------------------------------------- /src/module/command/os.rs: -------------------------------------------------------------------------------- 1 | pub mod reboot; 2 | pub use reboot::Reboot; 3 | 4 | pub mod shutdown; 5 | pub use shutdown::Shutdown; 6 | -------------------------------------------------------------------------------- /src/module/command/storage.rs: -------------------------------------------------------------------------------- 1 | pub mod lvm; 2 | 3 | pub mod file_space_usage; 4 | pub use file_space_usage::FileSpaceUsage; -------------------------------------------------------------------------------- /src/module/command/storage/lvm.rs: -------------------------------------------------------------------------------- 1 | pub mod snapshot; 2 | pub use snapshot::Snapshot; 3 | 4 | pub mod lvremove; 5 | pub use lvremove::LVRemove; 6 | 7 | pub mod lvresize; 8 | pub use lvresize::LVResize; 9 | 10 | pub mod lvrefresh; 11 | pub use lvrefresh::LVRefresh; -------------------------------------------------------------------------------- /src/module/command/systemd.rs: -------------------------------------------------------------------------------- 1 | pub mod service; -------------------------------------------------------------------------------- /src/module/command/systemd/service.rs: -------------------------------------------------------------------------------- 1 | pub mod start; 2 | pub use start::Start; 3 | 4 | pub mod stop; 5 | pub use stop::Stop; 6 | 7 | pub mod mask; 8 | pub use mask::Mask; 9 | 10 | pub mod unmask; 11 | pub use unmask::Unmask; 12 | 13 | pub mod logs; 14 | pub use logs::Logs; -------------------------------------------------------------------------------- /src/module/connection.rs: -------------------------------------------------------------------------------- 1 | pub mod connection_module; 2 | pub use connection_module::ConnectionModule; 3 | pub use connection_module::Connector; 4 | 5 | pub mod request_response; 6 | pub use request_response::ResponseMessage; 7 | pub use request_response::RequestResponse; 8 | 9 | pub mod ssh; 10 | pub use ssh::Ssh2; 11 | 12 | pub mod http; 13 | pub use http::Http; 14 | 15 | pub mod http_jwt; 16 | pub use http_jwt::HttpJwt; 17 | 18 | pub mod local_command; 19 | pub use local_command::LocalCommand; 20 | 21 | pub mod tcp; 22 | pub use tcp::Tcp; -------------------------------------------------------------------------------- /src/module/connection/connection_module.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::error::LkError; 4 | use crate::file_handler::FileMetadata; 5 | use crate::module::connection::ResponseMessage; 6 | use crate::module::module::Module; 7 | use crate::module::MetadataSupport; 8 | 9 | pub type Connector = Box; 10 | 11 | pub trait ConnectionModule: MetadataSupport + Module { 12 | fn set_target(&self, _address: &str) {} 13 | 14 | /// Sends a request / message and waits for response. Response can be complete or partial. 15 | fn send_message(&self, message: &str) -> Result; 16 | 17 | fn send_message_partial(&self, _message: &str, _invocation_id: u64) -> Result { 18 | Err(LkError::not_implemented()) 19 | } 20 | 21 | /// For partial responses. Should be called until the response is complete. 22 | fn receive_partial_response(&self, _invocation_id: u64) -> Result { 23 | Err(LkError::not_implemented()) 24 | } 25 | 26 | fn download_file(&self, _source: &str) -> Result<(FileMetadata, Vec), LkError> { 27 | Err(LkError::not_implemented()) 28 | } 29 | 30 | fn upload_file(&self, _metadata: &FileMetadata, _contents: Vec) -> Result<(), LkError> { 31 | Err(LkError::not_implemented()) 32 | } 33 | 34 | fn verify_host_key(&self, _hostname: &str, _key_id: &str) -> Result<(), LkError> { 35 | Err(LkError::not_implemented()) 36 | } 37 | 38 | fn new_connection_module(settings: &HashMap) -> Connector 39 | where 40 | Self: Sized + 'static + Send + Sync, 41 | { 42 | Box::new(Self::new(settings)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/module/connection/http.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use ureq; 3 | 4 | use lightkeeper_module::stateless_connection_module; 5 | use crate::error::LkError; 6 | use crate::module::*; 7 | use crate::module::connection::*; 8 | 9 | #[stateless_connection_module( 10 | name="http", 11 | version="0.0.1", 12 | cache_scope="Global", 13 | description="Sends a simple HTTP request", 14 | )] 15 | pub struct Http { 16 | // A temporary state for resource reuse when receiving multiple commands. 17 | agent: ureq::Agent, 18 | } 19 | 20 | impl Http { 21 | } 22 | 23 | impl Module for Http { 24 | fn new(_settings: &HashMap) -> Self { 25 | Http { 26 | agent: ureq::Agent::new(), 27 | } 28 | } 29 | } 30 | 31 | impl ConnectionModule for Http { 32 | fn send_message(&self, message: &str) -> Result { 33 | if message.is_empty() { 34 | return Ok(ResponseMessage::empty()); 35 | } 36 | 37 | let mut parts = message.split("\n"); 38 | let url = parts.next().unwrap(); 39 | let data = parts.next().unwrap_or_default(); 40 | 41 | // Currently only supports GET and POST requests. 42 | let response = if data.is_empty() { 43 | self.agent.get(url).call()? 44 | } else { 45 | self.agent.post(url).send_string(data)? 46 | }; 47 | 48 | let response_string = response.into_string()?; 49 | Ok(ResponseMessage::new_success(response_string)) 50 | } 51 | } 52 | 53 | impl From for LkError { 54 | fn from(error: ureq::Error) -> Self { 55 | LkError::other(format!("HTTP request error: {}", error)) 56 | } 57 | } -------------------------------------------------------------------------------- /src/module/connection/local_command.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::process; 3 | 4 | use lightkeeper_module::stateless_connection_module; 5 | use crate::error::LkError; 6 | use crate::module::*; 7 | use crate::module::connection::*; 8 | 9 | #[stateless_connection_module( 10 | name="local-command", 11 | version="0.0.1", 12 | cache_scope="Global", 13 | description="Executes a command locally.", 14 | )] 15 | pub struct LocalCommand { 16 | } 17 | 18 | impl LocalCommand { 19 | } 20 | 21 | impl Module for LocalCommand { 22 | fn new(_settings: &HashMap) -> Self { 23 | LocalCommand { 24 | } 25 | } 26 | } 27 | 28 | impl ConnectionModule for LocalCommand { 29 | fn send_message(&self, message: &str) -> Result { 30 | // TODO: don't assume bash exists even though it's very common? 31 | let output = process::Command::new("bash") 32 | .args(&["-c", message]) 33 | .output() 34 | .map_err(|e| e.to_string())?; 35 | 36 | if output.status.success() { 37 | let stdout = String::from_utf8(output.stdout).unwrap(); 38 | Ok(ResponseMessage::new_success(stdout)) 39 | } 40 | else { 41 | let stderr = String::from_utf8(output.stderr).unwrap(); 42 | Ok(ResponseMessage::new(stderr, output.status.code().unwrap_or(1))) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/module/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use super::ModuleSpecification; 4 | use crate::cache::CacheScope; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Metadata { 8 | pub module_spec: ModuleSpecification, 9 | pub description: String, 10 | /// Setting key and description. 11 | pub settings: HashMap, 12 | /// Used with extension modules. 13 | /// Extension modules enrich or modify the original data and are processed after parent module. 14 | pub parent_module: Option, 15 | /// Stateless modules can be run in parallel. Stateful modules can currently run only 1 connection per host. 16 | pub is_stateless: bool, 17 | pub cache_scope: CacheScope, 18 | } 19 | -------------------------------------------------------------------------------- /src/module/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod module_factory; 2 | pub use module_factory::ModuleFactory; 3 | 4 | pub mod metadata; 5 | pub use metadata::Metadata; 6 | 7 | pub mod module; 8 | pub use module::Module; 9 | pub use module::MetadataSupport; 10 | 11 | pub mod connection; 12 | pub mod command; 13 | pub mod monitoring; 14 | 15 | pub mod module_specification; 16 | pub use module_specification::ModuleSpecification; 17 | pub use module_specification::ModuleType; 18 | 19 | pub mod platform_info; 20 | pub use platform_info::PlatformInfo; -------------------------------------------------------------------------------- /src/module/module.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use crate::module::metadata::Metadata; 3 | use crate::module::ModuleSpecification; 4 | 5 | pub trait Module { 6 | fn new(settings: &HashMap) -> Self where Self: Sized; 7 | } 8 | 9 | pub trait MetadataSupport { 10 | fn get_metadata() -> Metadata where Self: Sized; 11 | fn get_metadata_self(&self) -> Metadata; 12 | fn get_module_spec(&self) -> ModuleSpecification; 13 | 14 | /// IDs prefixed with _ are reserved for internal use. 15 | fn is_internal(&self) -> bool { 16 | self.get_module_spec().is_internal() 17 | } 18 | } -------------------------------------------------------------------------------- /src/module/monitoring.rs: -------------------------------------------------------------------------------- 1 | pub mod monitoring_module; 2 | pub use monitoring_module::MonitoringModule; 3 | pub use monitoring_module::Monitor; 4 | pub use monitoring_module::MonitoringData; 5 | pub use monitoring_module::BoxCloneableMonitor; 6 | 7 | pub mod data_point; 8 | pub use data_point::DataPoint; 9 | 10 | pub mod linux; 11 | 12 | pub mod network; 13 | 14 | pub mod internal; 15 | 16 | pub mod os; 17 | 18 | pub mod systemd; 19 | 20 | pub mod docker; 21 | 22 | pub mod storage; 23 | 24 | pub mod nixos; -------------------------------------------------------------------------------- /src/module/monitoring/docker.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod containers; 3 | pub use containers::Containers; 4 | 5 | pub mod compose; 6 | pub use compose::Compose; 7 | 8 | pub mod images; 9 | pub use images::Images; 10 | 11 | pub mod image_updates; 12 | pub use image_updates::ImageUpdates; -------------------------------------------------------------------------------- /src/module/monitoring/internal.rs: -------------------------------------------------------------------------------- 1 | pub mod platform_info_ssh; 2 | pub use platform_info_ssh::PlatformInfoSsh; 3 | 4 | pub mod cert_monitor; 5 | pub use cert_monitor::CertMonitor; -------------------------------------------------------------------------------- /src/module/monitoring/linux.rs: -------------------------------------------------------------------------------- 1 | pub mod uptime; 2 | pub use uptime::Uptime; 3 | 4 | pub mod kernel; 5 | pub use kernel::Kernel; 6 | 7 | pub mod interface; 8 | pub use interface::Interface; 9 | 10 | pub mod package; 11 | pub use package::Package; 12 | 13 | pub mod who; 14 | pub use who::Who; 15 | 16 | pub mod load; 17 | pub use load::Load; 18 | 19 | pub mod ram; 20 | pub use ram::Ram; -------------------------------------------------------------------------------- /src/module/monitoring/linux/kernel.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | 4 | use lightkeeper_module::monitoring_module; 5 | use crate::enums::Criticality; 6 | use crate::error::LkError; 7 | use crate::module::connection::ResponseMessage; 8 | use crate::{ 9 | Host, 10 | frontend, 11 | }; 12 | use crate::module::*; 13 | use crate::module::monitoring::*; 14 | 15 | #[monitoring_module( 16 | name="kernel", 17 | version="0.0.1", 18 | description="Provides kernel version and architecture information.", 19 | )] 20 | pub struct Kernel; 21 | 22 | impl Module for Kernel { 23 | fn new(_settings: &HashMap) -> Self { 24 | Kernel { } 25 | } 26 | } 27 | 28 | impl MonitoringModule for Kernel { 29 | fn get_display_options(&self) -> frontend::DisplayOptions { 30 | frontend::DisplayOptions { 31 | display_style: frontend::DisplayStyle::Text, 32 | display_text: String::from("Kernel version"), 33 | category: String::from("host"), 34 | use_without_summary: true, 35 | ..Default::default() 36 | } 37 | } 38 | 39 | fn get_connector_spec(&self) -> Option { 40 | Some(ModuleSpecification::connector("ssh", "0.0.1")) 41 | } 42 | 43 | fn get_connector_message(&self, host: Host, _result: DataPoint) -> Result { 44 | if host.platform.os == platform_info::OperatingSystem::Linux { 45 | Ok(String::from("uname -r -m")) 46 | } 47 | else { 48 | Err(LkError::unsupported_platform()) 49 | } 50 | } 51 | 52 | fn process_response(&self, _host: Host, response: ResponseMessage, _result: DataPoint) -> Result { 53 | if response.is_error() { 54 | return Ok(DataPoint::value_with_level(response.message, Criticality::Critical)) 55 | } 56 | 57 | Ok(DataPoint::new(response.message.replace(" ", " (") + ")")) 58 | } 59 | } -------------------------------------------------------------------------------- /src/module/monitoring/linux/load.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | use crate::enums::Criticality; 4 | use crate::error::LkError; 5 | use crate::module::connection::ResponseMessage; 6 | use crate::module::platform_info; 7 | use crate::{ 8 | Host, 9 | frontend, 10 | }; 11 | use lightkeeper_module::monitoring_module; 12 | use crate::module::*; 13 | use crate::module::monitoring::*; 14 | 15 | #[monitoring_module( 16 | name="load", 17 | version="0.0.1", 18 | description="Provides information about average load (using uptime-command).", 19 | )] 20 | pub struct Load; 21 | 22 | impl Module for Load { 23 | fn new(_settings: &HashMap) -> Self { 24 | Load { } 25 | } 26 | } 27 | 28 | impl MonitoringModule for Load { 29 | fn get_display_options(&self) -> frontend::DisplayOptions { 30 | frontend::DisplayOptions { 31 | display_style: frontend::DisplayStyle::Text, 32 | display_text: String::from("Loads"), 33 | category: String::from("host"), 34 | ..Default::default() 35 | } 36 | } 37 | 38 | fn get_connector_spec(&self) -> Option { 39 | Some(ModuleSpecification::connector("ssh", "0.0.1")) 40 | } 41 | 42 | fn get_connector_message(&self, host: Host, _parent_result: DataPoint) -> Result { 43 | if host.platform.os == platform_info::OperatingSystem::Linux { 44 | Ok(String::from("uptime")) 45 | } 46 | else { 47 | Err(LkError::unsupported_platform()) 48 | } 49 | } 50 | 51 | fn process_response(&self, _host: Host, response: ResponseMessage, _parent_result: DataPoint) -> Result { 52 | if response.is_error() { 53 | return Ok(DataPoint::value_with_level(response.message, Criticality::Critical)) 54 | } 55 | 56 | let parts = response.message.split("load average: ").collect::>(); 57 | Ok(DataPoint::new(parts[1].to_string())) 58 | } 59 | } -------------------------------------------------------------------------------- /src/module/monitoring/network.rs: -------------------------------------------------------------------------------- 1 | pub mod ping; 2 | pub use ping::Ping; 3 | 4 | pub mod oping; 5 | pub use self::oping::Oping; 6 | 7 | pub mod ssh; 8 | pub use ssh::Ssh; 9 | 10 | pub mod tcp_connect; 11 | pub use tcp_connect::TcpConnect; 12 | 13 | pub mod routes; 14 | pub use routes::Routes; 15 | 16 | pub mod dns; 17 | pub use dns::Dns; -------------------------------------------------------------------------------- /src/module/monitoring/network/oping.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | use oping; 4 | use crate::module::connection::ResponseMessage; 5 | use crate::{ Host, enums::Criticality, frontend }; 6 | use lightkeeper_module::monitoring_module; 7 | use crate::module::*; 8 | use crate::module::monitoring::*; 9 | 10 | #[monitoring_module( 11 | name="oping", 12 | version="0.0.1", 13 | description="Measures latency to host with ICMP echo request (uses internal oping library). Does not work with flatpak.", 14 | )] 15 | pub struct Oping; 16 | 17 | impl Module for Oping { 18 | fn new(_settings: &HashMap) -> Self { 19 | Oping { } 20 | } 21 | } 22 | 23 | impl MonitoringModule for Oping { 24 | fn get_display_options(&self) -> frontend::DisplayOptions { 25 | frontend::DisplayOptions { 26 | display_style: frontend::DisplayStyle::Text, 27 | display_text: String::from("Ping"), 28 | category: String::from("network"), 29 | unit: String::from("ms"), 30 | ..Default::default() 31 | } 32 | } 33 | 34 | fn process_response(&self, host: Host, _responses: ResponseMessage, _result: DataPoint) -> Result { 35 | let mut ping = oping::Ping::new(); 36 | 37 | ping.set_timeout(5.0) 38 | .map_err(|e| e.to_string())?; 39 | 40 | ping.add_host(host.ip_address.to_string().as_str()) 41 | .map_err(|e| e.to_string())?; 42 | 43 | let mut responses = ping.send() 44 | .map_err(|e| e.to_string())?; 45 | 46 | let response = responses.next().unwrap(); 47 | 48 | if response.latency_ms < 0.0 { 49 | Ok(DataPoint::value_with_level(String::from("-"), Criticality::Critical)) 50 | } 51 | else { 52 | Ok(DataPoint::value_with_level(response.latency_ms.to_string(), Criticality::Normal)) 53 | } 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /src/module/monitoring/network/ssh.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | use crate::module::connection::ResponseMessage; 4 | use crate::{ Host, enums::Criticality, frontend }; 5 | use lightkeeper_module::monitoring_module; 6 | use crate::module::*; 7 | use crate::module::monitoring::*; 8 | 9 | #[monitoring_module( 10 | name="ssh", 11 | version="0.0.1", 12 | description="DEPRECATED. Checks if the SSH service is available.", 13 | )] 14 | pub struct Ssh; 15 | 16 | impl Module for Ssh { 17 | fn new(_settings: &HashMap) -> Self { 18 | Ssh { } 19 | } 20 | } 21 | 22 | impl MonitoringModule for Ssh { 23 | fn get_connector_spec(&self) -> Option { 24 | Some(ModuleSpecification::connector("ssh", "0.0.1")) 25 | } 26 | 27 | fn get_display_options(&self) -> frontend::DisplayOptions { 28 | frontend::DisplayOptions { 29 | display_style: frontend::DisplayStyle::CriticalityLevel, 30 | display_text: String::from("SSH"), 31 | category: String::from("network"), 32 | ..Default::default() 33 | } 34 | } 35 | 36 | fn process_response(&self, host: Host, _responses: ResponseMessage, _result: DataPoint) -> Result { 37 | if !host.platform.is_set() { 38 | Ok(DataPoint::value_with_level(String::from("down"), Criticality::Critical)) 39 | } 40 | else { 41 | Ok(DataPoint::value_with_level(String::from("up"), Criticality::Normal)) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/module/monitoring/network/tcp_connect.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | use crate::module::connection::ResponseMessage; 4 | use crate::{ Host, enums::Criticality, frontend }; 5 | use lightkeeper_module::monitoring_module; 6 | use crate::module::*; 7 | use crate::module::monitoring::*; 8 | 9 | #[monitoring_module( 10 | name="tcp-connect", 11 | version="0.0.1", 12 | description="Tests connecting to a specified port via TCP.", 13 | settings={ 14 | port => "Port to connect to. Default: 22.", 15 | } 16 | )] 17 | pub struct TcpConnect { 18 | port: u16, 19 | } 20 | 21 | impl Module for TcpConnect { 22 | fn new(settings: &HashMap) -> Self { 23 | TcpConnect { 24 | port: settings.get("port").map(|value| value.parse().unwrap()).unwrap_or(22), 25 | } 26 | } 27 | } 28 | 29 | impl MonitoringModule for TcpConnect { 30 | fn get_connector_spec(&self) -> Option { 31 | Some(ModuleSpecification::connector("tcp", "0.0.1")) 32 | } 33 | 34 | fn get_display_options(&self) -> frontend::DisplayOptions { 35 | frontend::DisplayOptions { 36 | display_style: frontend::DisplayStyle::CriticalityLevel, 37 | display_text: format!("TCP port {}", self.port), 38 | category: String::from("network"), 39 | ..Default::default() 40 | } 41 | } 42 | 43 | fn get_connector_message(&self, host: Host, _parent_result: DataPoint) -> Result { 44 | Ok(format!("{}:{}", host.ip_address, self.port)) 45 | } 46 | 47 | fn process_response(&self, _host: Host, response: ResponseMessage, _result: DataPoint) -> Result { 48 | if response.is_error() { 49 | return Ok(DataPoint::value_with_level(response.message, Criticality::Error)); 50 | } 51 | else { 52 | Ok(DataPoint::new(String::from("open"))) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/module/monitoring/nixos.rs: -------------------------------------------------------------------------------- 1 | pub mod rebuild_generations; 2 | pub use rebuild_generations::RebuildGenerations; -------------------------------------------------------------------------------- /src/module/monitoring/os.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | 4 | use lightkeeper_module::monitoring_module; 5 | use crate::module::connection::ResponseMessage; 6 | use crate::{ 7 | Host, 8 | frontend, 9 | }; 10 | use crate::module::*; 11 | use crate::module::monitoring::*; 12 | 13 | #[monitoring_module( 14 | name="os", 15 | version="0.0.1", 16 | description="Provides basic information about the operating system." 17 | )] 18 | pub struct Os; 19 | 20 | impl Module for Os { 21 | fn new(_settings: &HashMap) -> Self { 22 | Os { } 23 | } 24 | } 25 | 26 | impl MonitoringModule for Os { 27 | fn get_display_options(&self) -> frontend::DisplayOptions { 28 | frontend::DisplayOptions { 29 | display_style: frontend::DisplayStyle::Text, 30 | display_text: String::from("Operating system"), 31 | category: String::from("host"), 32 | use_without_summary: true, 33 | ..Default::default() 34 | } 35 | } 36 | 37 | fn process_response(&self, host: Host, _response: ResponseMessage, _result: DataPoint) -> Result { 38 | Ok(DataPoint::new(format!("{} ({} {})", host.platform.os, host.platform.os_flavor, host.platform.os_version))) 39 | } 40 | } -------------------------------------------------------------------------------- /src/module/monitoring/storage.rs: -------------------------------------------------------------------------------- 1 | pub mod lvm; 2 | 3 | pub mod filesystem; 4 | pub use filesystem::Filesystem; 5 | 6 | pub mod cryptsetup; 7 | pub use cryptsetup::Cryptsetup; -------------------------------------------------------------------------------- /src/module/monitoring/storage/lvm.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod logical_volume; 3 | pub use logical_volume::LogicalVolume; 4 | 5 | pub mod volume_group; 6 | pub use volume_group::VolumeGroup; 7 | 8 | pub mod physical_volume; 9 | pub use physical_volume::PhysicalVolume; -------------------------------------------------------------------------------- /src/module/monitoring/systemd.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod service; 3 | pub use service::Service; -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod string_manipulation; 2 | pub use string_manipulation::*; 3 | 4 | pub mod version_number; 5 | pub use version_number::VersionNumber; 6 | 7 | pub mod string_validation; 8 | 9 | pub mod shell_command; 10 | pub use shell_command::ShellCommand; 11 | 12 | pub mod error_message; 13 | pub use error_message::ErrorMessage; 14 | 15 | pub mod sha256; 16 | 17 | pub mod download; 18 | -------------------------------------------------------------------------------- /src/utils/download.rs: -------------------------------------------------------------------------------- 1 | use std::{io, path}; 2 | 3 | /// Function to download a file using ureq. 4 | pub fn download_file(url: &str, output_path: &str) -> io::Result<()> { 5 | let response = ureq::get(url).call().map_err(|error| io::Error::new(io::ErrorKind::Other, error))?; 6 | 7 | if response.status() == 200 { 8 | let mut file = std::fs::File::create(output_path)?; 9 | let mut reader = response.into_reader(); 10 | io::copy(&mut reader, &mut file)?; 11 | 12 | log::info!("Downloaded file: {}", output_path); 13 | } 14 | else { 15 | log::error!("Failed to download file ({}): {}", response.status(), url); 16 | } 17 | Ok(()) 18 | } 19 | 20 | pub fn verify_signature(file_path: &path::Path, signature_path: &path::Path, sign_cert: &[u8]) -> io::Result<()> { 21 | let file_bytes = std::fs::read(file_path)?; 22 | let signature = std::fs::read(signature_path)?; 23 | let sign_cert = openssl::x509::X509::from_pem(sign_cert)?.public_key()?; 24 | 25 | let mut verifier = openssl::sign::Verifier::new(openssl::hash::MessageDigest::sha256(), &sign_cert)?; 26 | verifier.update(&file_bytes)?; 27 | 28 | // Don't verify when developing. 29 | let do_verification = !cfg!(debug_assertions); 30 | 31 | if do_verification && !verifier.verify(&signature)? { 32 | Err(io::Error::new(io::ErrorKind::Other, "Signature verification failed.")) 33 | } 34 | else { 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/error_message.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | 3 | use crate::{enums::Criticality, error::LkError}; 4 | 5 | #[derive(Clone, Serialize, Deserialize)] 6 | pub struct ErrorMessage { 7 | pub message: String, 8 | pub criticality: Criticality, 9 | } 10 | 11 | impl From for ErrorMessage { 12 | fn from(error: LkError) -> Self { 13 | ErrorMessage { 14 | message: error.to_string(), 15 | criticality: Criticality::Error, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/sha256.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | 3 | pub fn hash(contents: &[u8]) -> String { 4 | let mut hasher = Sha256::new(); 5 | hasher.update(contents); 6 | let hash = &hasher.finalize(); 7 | 8 | hex::encode(hash) 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/string_manipulation.rs: -------------------------------------------------------------------------------- 1 | pub fn strip_newline(input: &Stringable) -> String { 2 | let input = input.to_string(); 3 | input.strip_suffix("\r\n").or(input.strip_suffix('\n')).unwrap_or(&input).to_string() 4 | } 5 | 6 | pub fn remove_whitespace(input: &Stringable) -> String { 7 | input.to_string().chars().filter(|&c| !c.is_whitespace()).collect() 8 | } 9 | 10 | pub fn get_string_between(input: &Stringable, start: &str, end: &str) -> String { 11 | let input = input.to_string(); 12 | let start = input.find(start).unwrap() + start.len(); 13 | let end = input.find(end).unwrap(); 14 | input[start..end].to_string() 15 | } 16 | 17 | pub fn remove_quotes(input: &Stringable) -> String { 18 | let input = input.to_string(); 19 | input 20 | .strip_prefix('"') 21 | .or(input.strip_prefix('\'')) 22 | .and_then(|input| input.strip_suffix('"')) 23 | .or(input.strip_suffix('\'')) 24 | .unwrap_or(&input) 25 | .to_string() 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/string_validation.rs: -------------------------------------------------------------------------------- 1 | const DECIMAL_SEPARATOR: char = '.'; 2 | 3 | pub fn is_alphanumeric(string: &str) -> bool { 4 | // chars() also handles multibyte correctly. 5 | string.chars().all(|char| char.is_alphanumeric()) 6 | } 7 | 8 | pub fn is_alphanumeric_with(string: &str, allowed_chars: &str) -> bool { 9 | string.chars().all(|char| char.is_alphanumeric() || allowed_chars.contains(char)) 10 | } 11 | 12 | pub fn begins_with_dash(string: &str) -> bool { 13 | string.chars().next().unwrap_or(' ') == '-' 14 | } 15 | 16 | pub fn is_numeric(string: &str) -> bool { 17 | string.parse::().is_ok() 18 | } 19 | 20 | // Numeric value with unit (e.g. 100M, 1.5GB, 5 m). May contain a space between number and unit. 21 | pub fn is_numeric_with_unit(string: &str, valid_units: &[String]) -> bool { 22 | let value_chars = string 23 | .chars() 24 | .take_while(|char| char.is_numeric() || *char == DECIMAL_SEPARATOR) 25 | .collect::(); 26 | if !is_numeric(&value_chars) { 27 | return false; 28 | } 29 | 30 | let unit_chars = string 31 | .chars() 32 | .skip_while(|char| char.is_numeric() || *char == DECIMAL_SEPARATOR) 33 | .collect::(); 34 | let unit_string = unit_chars.trim().to_string(); 35 | !unit_string.is_empty() && valid_units.contains(&unit_string) 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/version_number.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | use std::fmt::Display; 3 | use std::str::FromStr; 4 | 5 | use serde_derive::{Deserialize, Serialize}; 6 | 7 | #[derive(Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Copy)] 8 | pub struct VersionNumber { 9 | major: u16, 10 | // In comparisons, None will always be considered less than some number, so in this context it's effectively 0. 11 | minor: Option, 12 | patch: Option, 13 | } 14 | 15 | impl VersionNumber { 16 | // Never panics or returns error. Zeroes will be used as defaults when parsing fails. 17 | pub fn from_string(version_string: &str) -> Self { 18 | let mut parts = version_string.split('.').collect::>(); 19 | 20 | // Drop everything after dashes. 21 | parts = parts.iter().map(|part| part.split('-').next().unwrap_or("0")).collect(); 22 | 23 | let major = parts.first().unwrap_or(&"0").parse::().unwrap_or_default(); 24 | 25 | let minor = match parts.get(1) { 26 | Some(minor) => minor.parse::().ok(), 27 | None => None, 28 | }; 29 | 30 | let patch = match parts.get(2) { 31 | Some(patch) => patch.parse::().ok(), 32 | None => None, 33 | }; 34 | 35 | VersionNumber { major, minor, patch } 36 | } 37 | } 38 | 39 | impl FromStr for VersionNumber { 40 | type Err = (); 41 | 42 | fn from_str(s: &str) -> Result { 43 | Ok(Self::from_string(s)) 44 | } 45 | } 46 | 47 | impl Display for VersionNumber { 48 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 49 | match self.minor { 50 | Some(minor) => match self.patch { 51 | Some(patch) => write!(f, "{}.{}.{}", self.major, minor, patch), 52 | None => write!(f, "{}.{}", self.major, minor), 53 | }, 54 | None => write!(f, "{}", self.major), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test-env/alpine318/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/alpine318" 6 | config.vm.network "forwarded_port", guest: 22, host: 33351 7 | config.vm.provider :libvirt do |libvirt| 8 | libvirt.cpus = 1 9 | libvirt.memory = 768 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | if [ ! -e /mnt/test.img ]; then 13 | DEBIAN_FRONTEND=noninteractive apk add lvm2 14 | truncate -s 256M /mnt/test.img 15 | losetup /dev/loop0 /mnt/test.img 16 | pvcreate /dev/loop0 17 | vgcreate test_vg /dev/loop0 18 | lvcreate -L 128M -n test_lv test_vg 19 | else 20 | losetup /dev/loop0 /mnt/test.img 21 | fi 22 | 23 | SHELL 24 | end 25 | -------------------------------------------------------------------------------- /test-env/centos7/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/centos7" 6 | config.vm.network "forwarded_port", guest: 22, host: 33311 7 | # config.vm.network "private_network", ip: "10.guest: 22, host: 33311 8 | # config.vm.network "private_network", ip: "10.222.1.2" 9 | config.vm.provider :libvirt do |libvirt| 10 | libvirt.cpus = 1 11 | libvirt.memory = 768 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test-env/centos8/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/centos8" 6 | config.vm.network "forwarded_port", guest: 22, host: 33312 7 | config.vm.provider :libvirt do |libvirt| 8 | libvirt.cpus = 1 9 | libvirt.memory = 768 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 13 | dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 14 | systemctl start docker 15 | 16 | docker pull nginx:stable 17 | docker run -d -p 80:80 --name http-server-nocompose nginx:stable 18 | 19 | if [ ! -e /mnt/containers/web-frontend ]; then 20 | mkdir -p /mnt/containers/web-frontend 21 | echo "version: '3.4' 22 | services: 23 | nginx: 24 | image: nginx:stable 25 | restart: unless-stopped 26 | ports: 27 | - 8080:8080" >> /mnt/containers/web-frontend/docker-compose.yml 28 | docker compose -f /mnt/containers/web-frontend/docker-compose.yml up -d 29 | fi 30 | 31 | if [ ! -e /mnt/test.img ]; then 32 | truncate -s 256M /mnt/test.img 33 | losetup /dev/loop0 /mnt/test.img 34 | pvcreate /dev/loop0 35 | vgcreate test_vg /dev/loop0 36 | lvcreate -L 128M -n test_lv test_vg 37 | else 38 | losetup /dev/loop0 /mnt/test.img 39 | fi 40 | SHELL 41 | end 42 | -------------------------------------------------------------------------------- /test-env/config.yml: -------------------------------------------------------------------------------- 1 | preferences: 2 | use_sandbox_mode: true 3 | refresh_hosts_on_start: false 4 | use_remote_editor: true 5 | sudo_remote_editor: true 6 | remote_text_editor: nano 7 | text_editor: internal 8 | terminal: internal 9 | terminal_args: [] 10 | close_to_tray: true 11 | show_monitor_notifications: true 12 | display_options: 13 | qtquick_style: '' 14 | hide_info_notifications: false 15 | show_status_bar: true 16 | show_charts: false 17 | cache_settings: 18 | enable_cache: false 19 | provide_initial_value: true 20 | initial_value_time_to_live: 604800 21 | prefer_cache: true 22 | time_to_live: 8400 23 | schema_version: 2 24 | -------------------------------------------------------------------------------- /test-env/debian11/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/debian11" 6 | config.vm.network "forwarded_port", guest: 22, host: 33303 7 | config.vm.provider :libvirt do |libvirt| 8 | libvirt.cpus = 1 9 | libvirt.memory = 1024 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | if [ ! -e /etc/apt/sources.list.d/docker.list ]; then 13 | curl -fsSL https://get.docker.com -o get-docker.sh 14 | sh ./get-docker.sh 15 | 16 | docker pull nginx:stable 17 | docker run -d -p 80:80 --name http-server-nocompose nginx:stable 18 | fi 19 | 20 | if [ ! -e /mnt/containers/web-frontend ]; then 21 | DEBIAN_FRONTEND=noninteractive apt install -y docker-compose 22 | mkdir -p /mnt/containers/web-frontend 23 | echo "version: '3.4' 24 | services: 25 | nginx: 26 | image: nginx:stable 27 | restart: unless-stopped 28 | ports: 29 | - 8080:8080" >> /mnt/containers/web-frontend/docker-compose.yml 30 | docker-compose -f /mnt/containers/web-frontend/docker-compose.yml up -d 31 | fi 32 | 33 | if [ ! -e /mnt/test.img ]; then 34 | DEBIAN_FRONTEND=noninteractive apt install -y lvm2 35 | truncate -s 256M /mnt/test.img 36 | losetup /dev/loop0 /mnt/test.img 37 | pvcreate /dev/loop0 38 | vgcreate test_vg /dev/loop0 39 | lvcreate -L 128M -n test_lv test_vg 40 | else 41 | losetup /dev/loop0 /mnt/test.img 42 | fi 43 | SHELL 44 | end 45 | -------------------------------------------------------------------------------- /test-env/fedora38/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/fedora38" 6 | config.vm.network "forwarded_port", guest: 22, host: 33341 7 | config.vm.provider :libvirt do |libvirt| 8 | libvirt.cpus = 1 9 | libvirt.memory = 768 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | if [ ! -e /etc/apt/sources.list.d/docker.list ]; then 13 | curl -fsSL https://get.docker.com -o get-docker.sh 14 | sh ./get-docker.sh 15 | 16 | docker pull nginx:stable 17 | docker run -d -p 80:80 --name http-server-nocompose nginx:stable 18 | fi 19 | 20 | if [ ! -e /mnt/containers/web-frontend ]; then 21 | mkdir -p /mnt/containers/web-frontend 22 | echo "version: '3.4' 23 | services: 24 | nginx: 25 | image: nginx:stable 26 | restart: unless-stopped 27 | ports: 28 | - 8080:8080" >> /mnt/containers/web-frontend/docker-compose.yml 29 | docker compose -f /mnt/containers/web-frontend/docker-compose.yml up -d 30 | fi 31 | 32 | if [ ! -e /mnt/test.img ]; then 33 | dnf install -y lvm2 34 | truncate -s 256M /mnt/test.img 35 | losetup /dev/loop0 /mnt/test.img 36 | pvcreate /dev/loop0 37 | vgcreate test_vg /dev/loop0 38 | lvcreate -L 128M -n test_lv test_vg 39 | else 40 | losetup /dev/loop0 /mnt/test.img 41 | fi 42 | 43 | SHELL 44 | end 45 | -------------------------------------------------------------------------------- /test-env/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | current_dir=$(dirname "$0") 5 | 6 | for vagrantfile in $current_dir/**/Vagrantfile; do 7 | dir="$(dirname "$vagrantfile")" 8 | pushd "$dir" 9 | vagrant up --no-tty 10 | popd 11 | done 12 | -------------------------------------------------------------------------------- /test-env/stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | current_dir=$(dirname "$0") 5 | 6 | for vagrantfile in $current_dir/**/Vagrantfile; do 7 | dir="$(dirname "$vagrantfile")" 8 | pushd "$dir" && vagrant halt --no-tty && popd 9 | done 10 | -------------------------------------------------------------------------------- /test-env/ubuntu2004/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/ubuntu2004" 6 | config.vm.network "forwarded_port", guest: 22, host: 33331 7 | config.vm.provider :libvirt do |libvirt| 8 | libvirt.cpus = 1 9 | libvirt.memory = 768 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | if [ ! -e /etc/apt/sources.list.d/docker.list ]; then 13 | curl -fsSL https://get.docker.com -o get-docker.sh 14 | sh ./get-docker.sh 15 | 16 | docker pull nginx:stable 17 | docker run -d -p 80:80 --name http-server-nocompose nginx:stable 18 | fi 19 | 20 | if [ ! -e /mnt/containers/web-frontend ]; then 21 | DEBIAN_FRONTEND=noninteractive apt install -y docker-compose 22 | mkdir -p /mnt/containers/web-frontend 23 | echo "version: '3.4' 24 | services: 25 | nginx: 26 | image: nginx:stable 27 | restart: unless-stopped 28 | ports: 29 | - 8080:8080" >> /mnt/containers/web-frontend/docker-compose.yml 30 | docker-compose -f /mnt/containers/web-frontend/docker-compose.yml up -d 31 | fi 32 | 33 | if [ ! -e /mnt/test.img ]; then 34 | DEBIAN_FRONTEND=noninteractive apt install -y lvm2 35 | truncate -s 256M /mnt/test.img 36 | losetup /dev/loop0 /mnt/test.img 37 | pvcreate /dev/loop0 38 | vgcreate test_vg /dev/loop0 39 | lvcreate -L 128M -n test_lv test_vg 40 | else 41 | losetup /dev/loop0 /mnt/test.img 42 | fi 43 | 44 | SHELL 45 | end 46 | -------------------------------------------------------------------------------- /test-env/ubuntu2204/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/ubuntu2204" 6 | config.vm.network "forwarded_port", guest: 22, host: 33332 7 | config.vm.provider :libvirt do |libvirt| 8 | libvirt.cpus = 1 9 | libvirt.memory = 768 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | if [ ! -e /etc/apt/sources.list.d/docker.list ]; then 13 | curl -fsSL https://get.docker.com -o get-docker.sh 14 | sh ./get-docker.sh 15 | 16 | docker pull nginx:stable 17 | docker run -d -p 80:80 --name http-server-nocompose nginx:stable 18 | fi 19 | 20 | if [ ! -e /mnt/containers/web-frontend ]; then 21 | DEBIAN_FRONTEND=noninteractive apt install -y docker-compose 22 | mkdir -p /mnt/containers/web-frontend 23 | echo "version: '3.4' 24 | services: 25 | nginx: 26 | image: nginx:stable 27 | restart: unless-stopped 28 | ports: 29 | - 8080:8080" >> /mnt/containers/web-frontend/docker-compose.yml 30 | docker compose -f /mnt/containers/web-frontend/docker-compose.yml up -d 31 | fi 32 | 33 | if [ ! -e /mnt/test.img ]; then 34 | DEBIAN_FRONTEND=noninteractive apt install -y lvm2 35 | truncate -s 256M /mnt/test.img 36 | losetup /dev/loop0 /mnt/test.img 37 | pvcreate /dev/loop0 38 | vgcreate test_vg /dev/loop0 39 | lvcreate -L 128M -n test_lv test_vg 40 | else 41 | losetup /dev/loop0 /mnt/test.img 42 | fi 43 | 44 | SHELL 45 | end 46 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | use lightkeeper::*; 3 | 4 | 5 | static mut MAIN_CONFIG: Option = None; 6 | static mut HOSTS_CONFIG: Option = None; 7 | static mut GROUP_CONFIG: Option = None; 8 | const CONFIG_DIR: &str = "test-env"; 9 | 10 | /// Unsafe because of static mutable variables for caching configs. 11 | pub unsafe fn setup() -> (String, Configuration, configuration::Hosts, configuration::Groups) { 12 | 13 | if MAIN_CONFIG.is_none() { 14 | let (main_config, hosts_config, group_config) = match Configuration::read(CONFIG_DIR) { 15 | Ok(configuration) => configuration, 16 | Err(error) => { 17 | panic!("Error while reading configuration files: {}", error); 18 | } 19 | }; 20 | 21 | MAIN_CONFIG = Some(main_config); 22 | HOSTS_CONFIG = Some(hosts_config); 23 | GROUP_CONFIG = Some(group_config); 24 | } 25 | 26 | (CONFIG_DIR.to_string(), MAIN_CONFIG.clone().unwrap(),HOSTS_CONFIG.clone().unwrap(), GROUP_CONFIG.clone().unwrap()) 27 | } -------------------------------------------------------------------------------- /tests/smoke.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | // use lightkeeper; 3 | // extern crate qmetaobject; 4 | // use qmetaobject::*; 5 | 6 | 7 | #[test] 8 | fn test_smoke() { 9 | // Disabled for now. This doesn't work well for any tests that require the Qt event loop. 10 | // It seems that it'd be best if qmetaobject-rs added better support for testing. 11 | // Additionally, returning engine produces lots of warnings on exit. Maybe the engine outlives some properties. 12 | 13 | // let (config_dir, main_config, hosts_config, group_config) = unsafe { common::setup() }; 14 | // let (_, mut engine) = lightkeeper::run(&config_dir, &main_config, &hosts_config, &group_config, true); 15 | 16 | // let result = engine.invoke_method(QByteArray::from("test"), &[]); 17 | // let text = result.to_qbytearray().to_string(); 18 | // assert_eq!(text, "OK"); 19 | } -------------------------------------------------------------------------------- /third_party/README.md: -------------------------------------------------------------------------------- 1 | # qmltermwidget 2 | Used for integrated terminal. GPL-licensed. 3 | 4 | # ChartJs2QML 5 | Alternative to Qt Charts (or newer Qt Graphs) which don't seem quite mature yet and 6 | are GPL-licensed which may limit dual-licensing severily. 7 | --------------------------------------------------------------------------------