diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96afd06..5ec6ad1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,14 @@
## Unreleased
+### Added
+
+- Add `--completions` CLI flag.
+
### Changed
- **BREAKING**: Use gauges for `system_used_memory` and `system_total_memory`.
+- Update dependencies.
## 0.9.5 (2026-05-25)
diff --git a/Cargo.lock b/Cargo.lock
index 227a638..1182379 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe"
dependencies = [
"crypto-common 0.2.1",
- "inout 0.2.2",
+ "inout",
]
[[package]]
@@ -562,15 +562,6 @@ dependencies = [
"rand_pcg",
]
-[[package]]
-name = "block-padding"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
-dependencies = [
- "generic-array 0.14.7",
-]
-
[[package]]
name = "block-padding"
version = "0.4.2"
@@ -749,7 +740,7 @@ checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea"
dependencies = [
"block-buffer 0.12.0",
"crypto-common 0.2.1",
- "inout 0.2.2",
+ "inout",
"zeroize",
]
@@ -775,6 +766,15 @@ dependencies = [
"strsim 0.11.1",
]
+[[package]]
+name = "clap_complete"
+version = "4.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
+dependencies = [
+ "clap",
+]
+
[[package]]
name = "clap_derive"
version = "4.6.1"
@@ -1122,12 +1122,12 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
-version = "5.0.0-pre.6"
+version = "5.0.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3"
+checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf"
dependencies = [
"cfg-if",
- "cpufeatures 0.2.17",
+ "cpufeatures 0.3.0",
"curve25519-dalek-derive",
"digest 0.11.2",
"fiat-crypto",
@@ -1453,9 +1453,9 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
-version = "3.0.0-pre.7"
+version = "3.0.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6"
+checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60"
dependencies = [
"curve25519-dalek",
"ed25519",
@@ -1475,22 +1475,22 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elliptic-curve"
-version = "0.14.0-rc.32"
+version = "0.14.0-rc.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda94f31325c4275e9706adecbb6f0650dee2f904c915a98e3d81adaaaa757aa"
+checksum = "102d3643d30dd8b559613c5cced68317199597fffb278cdc88daa2ef7fafc935"
dependencies = [
"base16ct 1.0.0",
"crypto-bigint",
"crypto-common 0.2.1",
"digest 0.11.2",
+ "ff",
+ "group",
"hkdf",
"hybrid-array",
"once_cell",
"pem-rfc7468",
"pkcs8",
"rand_core 0.10.0",
- "rustcrypto-ff",
- "rustcrypto-group",
"sec1",
"subtle",
"zeroize",
@@ -1640,6 +1640,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+[[package]]
+name = "ff"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f"
+dependencies = [
+ "rand_core 0.10.0",
+ "subtle",
+]
+
[[package]]
name = "fiat-crypto"
version = "0.3.0"
@@ -1928,11 +1938,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.0",
"wasip2",
"wasip3",
+ "wasm-bindgen",
]
[[package]]
@@ -1950,6 +1962,17 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+[[package]]
+name = "group"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4"
+dependencies = [
+ "ff",
+ "rand_core 0.10.0",
+ "subtle",
+]
+
[[package]]
name = "h2"
version = "0.4.12"
@@ -2474,23 +2497,13 @@ dependencies = [
"libc",
]
-[[package]]
-name = "inout"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
-dependencies = [
- "block-padding 0.3.3",
- "generic-array 0.14.7",
-]
-
[[package]]
name = "inout"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
dependencies = [
- "block-padding 0.4.2",
+ "block-padding",
"hybrid-array",
]
@@ -2902,9 +2915,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md5"
-version = "0.7.0"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
[[package]]
name = "memchr"
@@ -3304,9 +3317,9 @@ dependencies = [
[[package]]
name = "p256"
-version = "0.14.0-rc.9"
+version = "0.14.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b97e3bf0465157ae90975ff52dbeb1362ba618924878c9f74c25baa27a65f9a"
+checksum = "41adc63effe99d48837a8cc0e6d7a77e32ae6a07f6000df466178dbc2193093e"
dependencies = [
"ecdsa",
"elliptic-curve",
@@ -3317,9 +3330,9 @@ dependencies = [
[[package]]
name = "p384"
-version = "0.14.0-rc.9"
+version = "0.14.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "437f30ebcb1e16ff48acead5f08bd69fbcdbc82421687bb48af5c315a0bfab03"
+checksum = "9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02"
dependencies = [
"ecdsa",
"elliptic-curve",
@@ -3331,9 +3344,9 @@ dependencies = [
[[package]]
name = "p521"
-version = "0.14.0-rc.9"
+version = "0.14.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e9fd792bab86ecf6249561752fb5a413511f999887107dd054bbda5143743d7"
+checksum = "a3a5297f53dc16d35909060ba3032cff7867e8809f01e273ff325579d5f0ceae"
dependencies = [
"base16ct 1.0.0",
"ecdsa",
@@ -3637,23 +3650,23 @@ dependencies = [
[[package]]
name = "primefield"
-version = "0.14.0-rc.9"
+version = "0.14.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6"
+checksum = "f845ec3240cd5ed5e1e31cf3ff633a5bf47c698dc4092ba9e767415b3d393406"
dependencies = [
"crypto-bigint",
"crypto-common 0.2.1",
+ "ff",
"rand_core 0.10.0",
- "rustcrypto-ff",
"subtle",
"zeroize",
]
[[package]]
name = "primeorder"
-version = "0.14.0-rc.9"
+version = "0.14.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a"
+checksum = "7d2793f22b9b6fd11ef3ac1d59bf003c2573593e4968702341605c2748fd90bf"
dependencies = [
"elliptic-curve",
]
@@ -4144,14 +4157,14 @@ dependencies = [
[[package]]
name = "russh"
-version = "0.61.1"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f67013f080c226e5a34db1c71f2567f44d95a6300005bb6cd4e2c8fe3c326d1b"
+checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0"
dependencies = [
"aes",
"aws-lc-rs",
"bitflags 2.10.0",
- "block-padding 0.4.2",
+ "block-padding",
"byteorder",
"bytes",
"cbc",
@@ -4170,12 +4183,11 @@ dependencies = [
"flate2",
"futures",
"generic-array 1.3.5",
- "getrandom 0.2.16",
+ "getrandom 0.4.2",
"ghash",
"hex-literal",
- "hkdf",
"hmac",
- "inout 0.1.4",
+ "inout",
"internal-russh-num-bigint",
"keccak",
"log",
@@ -4266,27 +4278,6 @@ dependencies = [
"semver",
]
-[[package]]
-name = "rustcrypto-ff"
-version = "0.14.0-rc.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6"
-dependencies = [
- "rand_core 0.10.0",
- "subtle",
-]
-
-[[package]]
-name = "rustcrypto-group"
-version = "0.14.0-rc.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed"
-dependencies = [
- "rand_core 0.10.0",
- "rustcrypto-ff",
- "subtle",
-]
-
[[package]]
name = "rusticata-macros"
version = "4.1.0"
@@ -4475,6 +4466,7 @@ dependencies = [
"bytesize",
"chrono",
"clap",
+ "clap_complete",
"color-eyre",
"crossterm",
"dashmap",
diff --git a/Cargo.toml b/Cargo.toml
index afb91fb..cd133b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ bytes = "1.11.1"
bytesize = "2.3.1"
chrono = "0.4.44"
clap = { version = "4.6.1", features = ["derive", "string"] }
+clap_complete = "4.6.5"
color-eyre = "0.6.5"
crossterm = { version = "0.29.0", default-features = false }
dashmap = "6.1.0"
@@ -71,7 +72,7 @@ reqwest = { version = "0.13.3", optional = true, default-features = false, featu
"json",
"rustls",
] }
-russh = "0.61.1"
+russh = "0.61.2"
rustls = "0.23.40"
rustls-acme = { version = "0.15.2", optional = true, default-features = false, features = [
"tokio",
diff --git a/book/src/cli.md b/book/src/cli.md
index 3da592b..c017589 100644
--- a/book/src/cli.md
+++ b/book/src/cli.md
@@ -18,6 +18,11 @@ Expose HTTP/SSH/TCP services through SSH port forwarding.
This option disables subdomains.
+ --completions <COMPLETIONS>
+ Generate shell completions for Sandhole
+
+ [possible values: bash, elvish, fish, powershell, zsh]
+
--domain-redirect <URL>
Where to redirect requests to the root domain
diff --git a/flake.nix b/flake.nix
index faa06f7..c7ba36b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -98,18 +98,15 @@
{
packages.${system} = packages;
- apps.${system}.default = {
- type = "app";
- program = lib.getExe self.packages.${system}.default;
- meta = {
- name = "sandhole";
- description = "Expose HTTP/SSH/TCP services through SSH port forwarding";
- homepage = "https://sandhole.com.br";
- license = lib.licenses.mit;
- mainProgram = "sandhole";
- platforms = lib.platforms.linux ++ lib.platforms.darwin;
+ apps.${system}.default =
+ let
+ sandhole = self.packages.${system}.default;
+ in
+ {
+ type = "app";
+ program = lib.getExe sandhole;
+ inherit (sandhole) meta;
};
- };
checks.${system} = checks;
diff --git a/justfile b/justfile
index 97c7b4a..22bc7f8 100644
--- a/justfile
+++ b/justfile
@@ -18,7 +18,7 @@ cli:
echo "" >> book/src/cli.md
echo "---" >> book/src/cli.md
echo "" >> book/src/cli.md
- cat result >> book/src/cli.md
+ cat result/cli.html >> book/src/cli.md
nixos-docs:
nix-build ./nix -A packages._docs
diff --git a/nix/checks.nix b/nix/checks.nix
index 1f7fc73..57ca82b 100644
--- a/nix/checks.nix
+++ b/nix/checks.nix
@@ -4,21 +4,12 @@
craneLib,
pkgs,
sandhole,
+ sandhole-no-default-features,
src,
...
}:
{
- inherit sandhole;
-
- sandhole-no-default-features = craneLib.buildPackage (
- commonArgs
- // {
- inherit cargoArtifacts;
- pname = "sandhole-no-default-features";
- cargoExtraArgs = "--locked --no-default-features";
- doCheck = false;
- }
- );
+ inherit sandhole sandhole-no-default-features;
sandhole-clippy = craneLib.cargoClippy (
commonArgs
diff --git a/nix/default.nix b/nix/default.nix
index c279da5..ce9a8e9 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -28,6 +28,7 @@ let
nativeBuildInputs = [
pkgs.cmake
+ pkgs.installShellFiles
pkgs.perl
]
++ lib.optionals (system == "x86_64-darwin" || system == "aarch64-darwin") [ pkgs.lld ];
@@ -40,17 +41,36 @@ let
// {
inherit cargoArtifacts;
doCheck = false;
- meta.mainProgram = "sandhole";
+ postInstall = lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) ''
+ $out/bin/sandhole --completions bash
+ installShellCompletion --cmd sandhole \
+ --bash <($out/bin/sandhole --completions bash) \
+ --fish <($out/bin/sandhole --completions fish) \
+ --zsh <($out/bin/sandhole --completions zsh)
+ '';
+ meta = {
+ name = "sandhole";
+ description = "Expose HTTP/SSH/TCP services through SSH port forwarding";
+ homepage = "https://sandhole.com.br";
+ license = lib.licenses.mit;
+ mainProgram = "sandhole";
+ platforms = lib.platforms.linux ++ lib.platforms.darwin;
+ };
}
);
+
+ sandhole-no-default-features = sandhole.overrideAttrs {
+ cargoExtraArgs = "--locked --no-default-features";
+ };
in
{
- inherit sandhole;
+ inherit sandhole sandhole-no-default-features;
packages = import ./packages.nix {
inherit
pkgs
sandhole
+ sandhole-no-default-features
;
};
@@ -61,6 +81,7 @@ in
craneLib
pkgs
sandhole
+ sandhole-no-default-features
src
;
};
diff --git a/nix/packages.nix b/nix/packages.nix
index 865149d..c6bcf47 100644
--- a/nix/packages.nix
+++ b/nix/packages.nix
@@ -1,6 +1,7 @@
{
pkgs,
sandhole,
+ sandhole-no-default-features,
...
}:
let
@@ -25,7 +26,7 @@ let
};
in
{
- inherit sandhole;
+ inherit sandhole sandhole-no-default-features;
default = sandhole;
_docs =
@@ -42,7 +43,8 @@ in
];
}
''
- to-html --no-prompt "sandhole --help" > $out
+ mkdir $out
+ to-html --no-prompt "sandhole --help" > $out/cli.html
'';
_book = pkgs.stdenv.mkDerivation {
diff --git a/npins/default.nix b/npins/default.nix
index 884fc8c..8ec5eca 100644
--- a/npins/default.nix
+++ b/npins/default.nix
@@ -65,7 +65,9 @@ let
if pkgs == null then
{
inherit (builtins) fetchTarball fetchurl;
- # For some fucking reason, fetchGit has a different signature than the other builtin fetchers …
+ # Frustratingly, due to flakes and `fetchTree`, `fetchGit`
+ # has a different signature than the other builtin
+ # fetchers
fetchGit = args: (builtins.fetchGit args).outPath;
}
else
@@ -95,7 +97,6 @@ let
};
};
- # Dispatch to the correct code path based on the type
path =
if spec.type == "Git" then
mkGitSource fetchers spec
@@ -105,8 +106,8 @@ let
mkPyPiSource fetchers spec
else if spec.type == "Channel" then
mkChannelSource fetchers spec
- else if spec.type == "Tarball" then
- mkTarballSource fetchers spec
+ else if spec.type == "Url" || spec.type == "MutableUrl" then
+ mkUrlSource fetchers spec
else if spec.type == "Container" then
mkContainerSource pkgs spec
else
@@ -192,16 +193,20 @@ let
sha256 = hash;
};
- mkTarballSource =
- { fetchTarball, ... }:
+ mkUrlSource =
+ {
+ fetchTarball,
+ fetchurl,
+ ...
+ }:
{
url,
- locked_url ? url,
hash,
+ unpack,
...
}:
- fetchTarball {
- url = locked_url;
+ (if unpack then fetchTarball else fetchurl) {
+ inherit url;
sha256 = hash;
};
@@ -211,16 +216,22 @@ let
image_name,
image_tag,
image_digest,
+ hash,
...
- }:
+ }@args:
if pkgs == null then
builtins.throw "container sources require passing in a Nixpkgs value: https://github.com/andir/npins/blob/master/README.md#using-the-nixpkgs-fetchers"
else
- pkgs.dockerTools.pullImage {
- imageName = image_name;
- imageDigest = image_digest;
- finalImageTag = image_tag;
- };
+ pkgs.dockerTools.pullImage (
+ {
+ imageName = image_name;
+ imageDigest = image_digest;
+ finalImageTag = image_tag;
+ hash = hash;
+ }
+ // (if args.arch or null != null then { arch = args.arch; } else { })
+ );
+
in
mkFunctor (
{
@@ -231,7 +242,7 @@ mkFunctor (
if builtins.isPath input then
# while `readFile` will throw an error anyways if the path doesn't exist,
# we still need to check beforehand because *our* error can be caught but not the one from the builtin
- # *piegames sighs*
+ # See:
if builtins.pathExists input then
builtins.fromJSON (builtins.readFile input)
else
@@ -242,7 +253,7 @@ mkFunctor (
throw "Unsupported input type ${builtins.typeOf input}, must be a path or an attrset";
version = data.version;
in
- if version == 7 then
+ if version == 8 then
builtins.mapAttrs (name: spec: mkFunctor (mkSource name spec)) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
diff --git a/npins/sources.json b/npins/sources.json
index d1c8677..ab92f6d 100644
--- a/npins/sources.json
+++ b/npins/sources.json
@@ -9,15 +9,16 @@
},
"branch": "master",
"submodules": false,
- "revision": "27711550d109bf6236478dc9f53b9e29c1a374c5",
- "url": "https://github.com/ipetkov/crane/archive/27711550d109bf6236478dc9f53b9e29c1a374c5.tar.gz",
- "hash": "sha256-5fDkKTYEgue2klksd52WvcXfZdY1EIlbk0QggAwpFog="
+ "revision": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
+ "url": "https://github.com/ipetkov/crane/archive/59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37.tar.gz",
+ "hash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0="
},
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
- "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre958799.917fec990948/nixexprs.tar.xz",
- "hash": "sha256-QKxBW7o8gmUwDXQhsbIb0JCoUgCr8B80ltkjbg840UQ="
+ "artifact": "nixexprs.tar.xz",
+ "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1009182.ffa10e26ae11/nixexprs.tar.xz",
+ "hash": "sha256-JX05Ms/dk0c+UoW9IqQriB53HNZFckX9Qd3EJqmcqEw="
},
"rust-overlay": {
"type": "Git",
@@ -28,10 +29,10 @@
},
"branch": "master",
"submodules": false,
- "revision": "40b0a3a193e0840c76174b4a322874c8f6dd0a63",
- "url": "https://github.com/oxalica/rust-overlay/archive/40b0a3a193e0840c76174b4a322874c8f6dd0a63.tar.gz",
- "hash": "sha256-aU9nlrS9S+IJ2EiCzsaxzOXUhggogqTrJojBicE6Oeg="
+ "revision": "27b7e78c6935293ee868469cc4172e9b8b17823b",
+ "url": "https://github.com/oxalica/rust-overlay/archive/27b7e78c6935293ee868469cc4172e9b8b17823b.tar.gz",
+ "hash": "sha256-4+8gPeiPeud89mjo865RwpqmKwa1MThRsq2SvRxo7mg="
}
},
- "version": 7
+ "version": 8
}
diff --git a/src/config.rs b/src/config.rs
index d34eec8..e7e4f50 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -79,20 +79,24 @@ pub enum LogFormat {
#[derive(Debug, Args, PartialEq)]
#[group(required = true, multiple = false)]
-pub struct Domain {
+pub struct Mode {
/// The root domain of the application.
#[arg(
long,
value_parser = validate_domain,
- group = "sandhole_domain"
+ group = "sandhole_mode"
)]
pub domain: Option,
/// Whether to run Sandhole without a root domain.
///
/// This option disables subdomains.
- #[arg(long, default_value_t = false, group = "sandhole_domain")]
+ #[arg(long, default_value_t = false, group = "sandhole_mode")]
pub no_domain: bool,
+
+ /// Generate shell completions for Sandhole.
+ #[arg(long, group = "sandhole_mode")]
+ pub completions: Option,
}
// CLI configuration for Sandhole.
@@ -101,7 +105,7 @@ pub struct Domain {
#[command(version, about, long_about = None)]
pub struct ApplicationConfig {
#[command(flatten)]
- pub domain: Domain,
+ pub mode: Mode,
/// Where to redirect requests to the root domain.
#[arg(
@@ -569,7 +573,7 @@ mod application_config_tests {
use clap::Parser;
use ipnet::IpNet;
- use crate::config::{Domain, LoadBalancingAlgorithm, LogFormat};
+ use crate::config::{LoadBalancingAlgorithm, LogFormat, Mode};
use super::{ApplicationConfig, BindHostnames, LoadBalancingStrategy, RandomSubdomainSeed};
@@ -579,9 +583,10 @@ mod application_config_tests {
assert_eq!(
config,
ApplicationConfig {
- domain: Domain {
+ mode: Mode {
domain: Some("foobar.tld".into()),
no_domain: false,
+ completions: None,
},
domain_redirect: "https://github.com/EpicEric/sandhole".into(),
user_keys_directory: "./deploy/user_keys/".into(),
@@ -712,9 +717,10 @@ mod application_config_tests {
assert_eq!(
config,
ApplicationConfig {
- domain: Domain {
+ mode: Mode {
domain: None,
no_domain: true,
+ completions: None,
},
domain_redirect: "https://sandhole.eric.dev.br".into(),
user_keys_directory: "/etc/user_keys/".into(),
@@ -781,6 +787,19 @@ mod application_config_tests {
)
}
+ #[test_log::test]
+ fn parses_completions() {
+ let config = ApplicationConfig::parse_from(["sandhole", "--completions", "fish"]);
+ assert_eq!(
+ config.mode,
+ Mode {
+ domain: None,
+ no_domain: false,
+ completions: Some(clap_complete::Shell::Fish),
+ }
+ )
+ }
+
#[test_log::test]
fn fails_to_parse_if_invalid_domain() {
let Err(error) = ApplicationConfig::try_parse_from(["sandhole", "--domain=.foobar.tld"])
diff --git a/src/entrypoint.rs b/src/entrypoint.rs
index 11dbfa2..6e4b12a 100644
--- a/src/entrypoint.rs
+++ b/src/entrypoint.rs
@@ -103,7 +103,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> {
.into());
}
}
- if config.domain.no_domain {
+ if config.mode.no_domain {
if config.allow_requested_subdomains {
#[cfg(not(coverage_nightly))]
tracing::warn!("--allow-requested-subdomains has no effect with --no-domain");
@@ -332,7 +332,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> {
let builder = AddressDelegator::builder()
.resolver(DnsResolver::new()?)
.txt_record_prefix(config.txt_record_prefix)
- .maybe_root_domain(config.domain.domain.clone())
+ .maybe_root_domain(config.mode.domain.clone())
.bind_hostnames(config.bind_hostnames)
.force_random_subdomains(!config.allow_requested_subdomains)
.maybe_random_subdomain_seed(config.random_subdomain_seed)
@@ -369,7 +369,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> {
}
});
// Configure the default domain redirect for Sandhole.
- let domain_redirect = config.domain.domain.clone().map(|from| {
+ let domain_redirect = config.mode.domain.clone().map(|from| {
Arc::new(DomainRedirect {
from,
to: config.domain_redirect,
@@ -601,7 +601,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> {
});
let admin_notifications = Arc::new(Mutex::default());
let notifications_clone = Arc::clone(&admin_notifications);
- let domain_clone = config.domain.domain.clone();
+ let domain_clone = config.mode.domain.clone();
let certificates_clone = Arc::clone(&certificates);
// Periodically check for problems (every 10 seconds) and update notifications.
tokio::spawn(async move {
@@ -711,7 +711,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> {
api_login,
address_delegator: addressing,
tcp_handler,
- domain: config.domain.domain,
+ domain: config.mode.domain,
http_port: config.http_port.into(),
https_port: config.https_port.into(),
ssh_port: config.ssh_port.into(),
diff --git a/src/main.rs b/src/main.rs
index 783bccf..a1bf958 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
-use clap::Parser;
+use clap::{CommandFactory, Parser};
use sandhole::{ApplicationConfig, LogFormat, entrypoint};
use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt};
@@ -8,6 +8,16 @@ async fn main() -> color_eyre::Result<()> {
let config = ApplicationConfig::parse();
+ if let Some(shell) = config.mode.completions {
+ clap_complete::generate(
+ shell,
+ &mut ApplicationConfig::command(),
+ env!("CARGO_BIN_NAME"),
+ &mut std::io::stdout(),
+ );
+ return Ok(());
+ }
+
let env_filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
.from_env_lossy();