This commit is contained in:
atagen 2025-08-21 23:51:24 +10:00
commit b2e0936d4a
7 changed files with 982 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.direnv

501
Cargo.lock generated Normal file
View file

@ -0,0 +1,501 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cade"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"clap",
"microxdg",
"sled",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "clap"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "microxdg"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0ba94daabea2c803df477d4dc1d37a6f15726e4bed53be7067022ddb89328e"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "proc-macro2"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sled"
version = "0.34.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
dependencies = [
"crc32fast",
"crossbeam-epoch",
"crossbeam-utils",
"fs2",
"fxhash",
"libc",
"log",
"parking_lot",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "cade"
version = "0.1.0"
edition = "2024"
[profile.release]
strip = true
opt-level = "z"
codegen-units = 1
# lto = true
# panic = "abort"
[dependencies]
anyhow = "1.0.99"
bincode = "2.0.1"
clap = { version = "4.5.45", features = ["derive"] }
microxdg = "0.2.0"
sled = "0.34.7"

43
flake.lock generated Normal file
View file

@ -0,0 +1,43 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1755175540,
"narHash": "sha256-V0j2S1r25QnbqBLzN2Rg/dKKil789bI3P3id7bDPVc4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a595dde4d0d31606e19dcec73db02279db59d201",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

44
flake.nix Normal file
View file

@ -0,0 +1,44 @@
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
inputs.systems.url = "github:nix-systems/default-linux";
outputs =
{
self,
nixpkgs,
systems,
}:
let
forAllSystems =
function: nixpkgs.lib.genAttrs (import systems) (system: function nixpkgs.legacyPackages.${system});
in
{
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
RUSTFLAGS = "-C prefer-dynamic=yes";
packages = builtins.attrValues {
inherit (pkgs)
rustc
cargo
rust-analyzer
rustfmt
clippy
;
};
};
});
packages = forAllSystems (pkgs: {
default =
let
project = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package;
in
pkgs.rustPlatform.buildRustPackage (finalAttrs: {
pname = project.name;
version = project.version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
RUSTFLAGS = "-C prefer-dynamic=yes";
});
});
};
}

373
src/main.rs Normal file
View file

@ -0,0 +1,373 @@
use std::{
collections::HashMap,
ffi::OsString,
fmt::{Debug, Display},
io::{BufRead, Read},
os::unix::process::CommandExt,
path::{Path, PathBuf},
str::{FromStr, SplitWhitespace},
};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Parser, Subcommand};
#[repr(u8)]
#[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)]
enum Permission {
Allowed,
Disallowed,
}
impl Display for Permission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
#[derive(Subcommand)]
enum Action {
Activate,
Allow,
Disallow,
Edit,
}
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
action: Action,
}
#[derive(Debug)]
enum Loadable {
Default,
Flake(String),
Shell(String),
Env(String),
}
#[derive(Debug)]
enum Keyword {
Pure,
Call(Vec<String>),
Load(Loadable),
}
#[derive(Debug)]
enum ParseError {
InvalidKeyword,
UnknownLoadable,
TooManyOptions,
TooFewOptions,
}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
impl FromStr for Keyword {
type Err = ParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let lower = s.to_lowercase();
use Keyword::*;
if s.len() >= 4 {
let res = match &lower.get(..4) {
Some("pure") => Pure,
Some("call") => {
let target: Vec<String> = lower[4..]
.split_whitespace()
.map(|s| s.to_owned())
.collect();
if target.is_empty() {
return Err(ParseError::TooFewOptions);
}
Call(target)
}
Some("load") => {
let mut words = lower[4..].split_whitespace();
if words.clone().count() > 2 {
return Err(ParseError::TooManyOptions);
} else {
match words.next() {
None => Load(Loadable::Default),
Some("shell") => {
Load(Loadable::Shell(s.get(11..).unwrap_or("").to_string()))
}
Some("flake") => {
Load(Loadable::Flake(s.get(11..).unwrap_or("").to_string()))
}
Some("env") => {
Load(Loadable::Env(s.get(9..).unwrap_or("").to_string()))
}
Some(_) => return Err(ParseError::UnknownLoadable),
}
}
}
Some(n) => {
eprintln!("found invalid command {n}, dying..");
return Err(ParseError::InvalidKeyword);
}
None => {
return Err(ParseError::InvalidKeyword);
}
};
Ok(res)
} else {
Err(ParseError::InvalidKeyword)
}
}
}
fn read_cade(path: &Path) -> Result<Vec<Keyword>> {
let contents = std::fs::read(path).context("reading cade file")?;
let mut accum = Vec::new();
let mut lines = contents.lines();
while let Some(Ok(line)) = lines.next() {
let instruction: Keyword = line
.parse()
.map_err(|e| anyhow!("while parsing cade file at {}: {e:?}", path.display()))?;
accum.push(instruction);
}
Ok(accum)
}
#[derive(Debug)]
enum EnvAction {
Add(String, String),
Clear(String),
Purify,
}
fn load_flake(path: &Path, output: Option<String>) -> Result<Vec<EnvAction>> {
let mut nix_cmd = String::from("nix develop ");
if let Some(flake_output) = output {
nix_cmd.push_str(&[&flake_output, " "].concat());
}
nix_cmd.push_str("-ic env --null");
let mut proc = std::process::Command::new("sh");
// proc.env_clear();
proc.arg("-c");
proc.arg(nix_cmd);
proc.current_dir(path);
let output = proc
.output()
.with_context(|| format!("loading flake at {}", path.display()))?
.stdout;
let text = String::from_utf8(output).context("converting output to text")?;
parse_envs(text)
}
fn load_shell(path: &Path, filename: String) -> Result<Vec<EnvAction>> {
let mut nix_cmd = String::from("nix-shell ");
if filename.is_empty() {
nix_cmd.push_str(&[&filename, " "].concat());
}
nix_cmd.push_str("--pure --command env --null");
let mut proc = std::process::Command::new("sh");
proc.env_clear();
proc.arg("-c");
proc.arg(nix_cmd);
proc.current_dir(path);
let output = proc
.output()
.with_context(|| format!("loading shell at {}", path.display()))?
.stdout;
let text = String::from_utf8(output).context("converting output to text")?;
parse_envs(text)
}
fn load_env(path: &Path, filename: String) -> Result<Vec<EnvAction>> {
let mut p = path.to_path_buf();
if filename.is_empty() {
p.push(".env");
} else {
p.push(filename);
}
let mut file = std::fs::File::open(p)
.with_context(|| format!("opening env file at {}", path.display()))?;
let mut buf = String::new();
file.read_to_string(&mut buf).context("reading env file")?;
parse_envs(buf)
}
fn call(path: &Path, argv: Vec<String>) -> Result<Vec<EnvAction>> {
let mut it = argv.iter();
// safety: already checked at parsing
let mut process = std::process::Command::new(it.next().unwrap());
process.current_dir(path);
process.args(it);
let output = process
.output()
.with_context(|| format!("running process {}", argv.concat()))?
.stdout;
let text = String::from_utf8(output)
.with_context(|| format!("converting call {} output to text", argv.concat()))?
.replace(' ', "\0");
parse_envs(text)
}
fn parse_envs(text: String) -> Result<Vec<EnvAction>> {
let mut envs = Vec::new();
use EnvAction::*;
for line in text.split('\0') {
let split: Vec<&str> = line.split('=').collect();
let parse = match split.len() {
0 => {
return Err(anyhow!("processing returned env var {text}"));
}
1 => Clear(split[0].to_string()),
_ => Add(split[0].to_string(), split[1..].concat().to_string()),
};
envs.push(parse);
}
Ok(envs)
}
fn interpret(cascade: HashMap<PathBuf, Vec<Keyword>>) -> Result<Vec<EnvAction>> {
let mut actions = Vec::new();
for (path, layer) in cascade {
for keyword in layer {
use EnvAction::*;
use Keyword::*;
use Loadable::*;
let acts = match keyword {
Pure => Ok(Vec::from([Purify])),
Call(argv) => call(&path, argv).context("calling process"),
Load(loadable) => match loadable {
Default => load_flake(&path, None).context("loading flake"),
Flake(output) => load_flake(&path, Some(output)),
Shell(filename) => load_shell(&path, filename).context("loading shell"),
Env(filename) => load_env(&path, filename).context("loading env file"),
},
}?;
actions.extend(acts);
}
}
Ok(actions)
}
fn do_activation() -> Result<()> {
let dir = ensure_dir()?;
let db = sled::open(dir).context("open database")?;
let cwd = std::env::current_dir()
.context("determine CWD")?
.into_os_string();
if db
.get(cwd.into_encoded_bytes())
.map(|perm| {
perm.map(|inner| {
let (p, _) =
bincode::decode_from_slice(inner.as_ref(), bincode::config::standard())
.unwrap_or((Permission::Disallowed, 0));
p
})
.unwrap_or(Permission::Disallowed)
})
.unwrap_or(Permission::Disallowed)
== Permission::Disallowed
{
bail!("cade is not permitted to operate here; use 'cade allow'.");
}
let mut current_dir = std::env::current_dir().context("determine CWD")?;
let base_env = std::env::vars().collect::<HashMap<String, String>>();
let mut cascade = HashMap::new();
cascade.insert(
current_dir.clone(),
read_cade(&current_dir.join(".cade")).context("reading cade file")?,
);
while let Some(parent) = current_dir.parent()
&& std::fs::exists(parent.join(".cade"))
.context("check for .cade file in parent directory")?
{
current_dir = parent.to_path_buf();
cascade.insert(
current_dir.clone(),
read_cade(&current_dir.join(".cade")).context("reading cade file")?,
);
}
let mut env = HashMap::new();
let adjustments = interpret(cascade)?;
use EnvAction::*;
for action in adjustments {
let _ = match action {
Add(k, v) => env.insert(k, v),
Clear(k) => env.remove(&k),
Purify => {
for (bk, bv) in base_env.iter() {
if env.get(bk).is_some_and(|inner| inner == bv) {
env.remove(bk);
}
}
None
}
};
}
for (k, v) in env {
println!("{k}={v}");
}
Ok(())
}
fn set_permission(permission: Permission) -> Result<()> {
let dir = ensure_dir()?;
let db = sled::open(dir).context("open database")?;
let cwd = std::env::current_dir()
.context("determine CWD")?
.into_os_string();
let encoded_permission = bincode::encode_to_vec(&permission, bincode::config::standard())
.context("encode permission value")?;
let _ = db
.insert(cwd.into_encoded_bytes(), encoded_permission)
.context("update permissions database")?;
eprintln!(
"cade is now {} here.",
permission.to_string().to_lowercase()
);
Ok(())
}
fn ensure_dir() -> Result<PathBuf> {
let xdg = microxdg::Xdg::new().context("establish XDG paths")?;
let mut path = xdg.state().context("find xdg state dir")?;
path.push("cade");
if !std::fs::exists(&path).is_ok_and(|v| v) {
std::fs::create_dir(&path).context("create cade's state path")?;
}
path.push("permissions.db");
Ok(path)
}
fn try_main() -> Result<()> {
let args = Cli::parse();
use Action::*;
match args.action {
// TODO recursively ascend until we hit a top level .cade, then activate downwards
Activate => do_activation().context("activate cade environment")?,
Allow => set_permission(Permission::Allowed)?,
Disallow => set_permission(Permission::Disallowed)?,
Edit => {
let editor = std::env::var("EDITOR").context("find EDITOR variable")?;
let mut session = std::process::Command::new(Into::<OsString>::into(editor))
.arg(Into::<OsString>::into(".cade"))
.spawn()
.context("spawn editor process")?;
session.wait().context("wait for editor process")?;
set_permission(Permission::Allowed)?;
}
};
Ok(())
}
fn main() {
if let Err(e) = try_main() {
eprintln!("failed to {e}\n{}", e.root_cause());
std::process::exit(1);
}
}