Skip to content

7. Project structure

ANKDDEV edited this page Mar 27, 2025 · 5 revisions

Here you'll find some information about our code structure and how to add new feature.

Code strucuture

.
├── assets                  # Assets directory
│   └── default_config.toml # Default configuration file
├── install.ps1             # Windows install script
├── install.sh              # Unix install script
├── src
│   ├── commands.rs         # Commands handlers
│   ├── config.rs           # Configuration support
│   ├── interactive
│   │   ├── draw.rs         # Draw interactive mode
│   │   ├── list.rs         # List environment variables
│   │   └── tests.rs        # Tests for interactive mode
│   ├── interactive.rs      # Main interactive mode handler
│   ├── main.rs             # App's entrypoint
│   ├── models.rs           # Models and structures
│   ├── utils.rs            # Useful utilities
│   └── variables.rs        # Working with variables
└── tests                   # Integration tests
    └── cli.rs              # Integration tests for CLI

Scoop manifest

Scoop is a package manager for Windows. Our manifest for Scoop is publishhed as GitHub Gist in my account. Link.

Gist URLs

You can find some URLs to this Gist:

Install scripts

Install scripts are used to easily install envfetch on different platforms.

Windows

This is just PowerShell script, that downloads and extracts to specific directory executable form releases. Actual source of this script. Firstly, we create directory, where program will be installed and go to it:

$envfetchPath = "$env:APPDATA\envfetch"                                                          # Define directory path
New-Item -ItemType Directory -Force -Path $envfetchPath -ErrorAction SilentlyContinue | Out-Null # Create directory
Write-Host "Installing envfetch to $envfetchPath"                                                # Print message with path of directory

Push-Location $envfetchPath                                                                      # Go to this directory, as cd

Then, there are some code that can crash, so it is in try block with finally, where we go back to directory, where user did before:

try {
    ...
} finally {
    Pop-Location # Go back to directory
}

In next snippet we:

  • Check if file alreday exists and if yes, then print message about updating
  • Download file to installation directory
  • Compare checksums
  • Add this directory to PATH, so user can easily access it
$outFile = "envfetch.exe"
if ([System.IO.File]::Exists($outFile)) {
    Write-Host "envfetch already exists. Updating..."
}                                                                                                       # Define output path
Invoke-WebRequest -Uri "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-windows-amd64.exe" -OutFile $outFile # Download file from release

# Check integrity
$expectedChecksum = Invoke-WebRequest -Uri "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-windows-amd64.exe.sha256"
$expectedChecksum = [System.Text.Encoding]::UTF8.GetString($expectedChecksum.Content).TrimEnd()
$actualChecksum = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash.ToLower()
if ($actualChecksum -ne $expectedChecksum) {
    Write-Host "Checksum mismatch!"
    Write-Host "Expected: \"$expectedChecksum\""
    Write-Host "Actual:   \"$actualChecksum\""
    Write-Host "Please download the file manually and verify its integrity."
    exit 1
}

# Add directory to PATH
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*$envfetchPath*") {
    [Environment]::SetEnvironmentVariable("Path", "$userPath;$envfetchPath", "User")
}

And, finally, we print message that program installed successfully:

Write-Host "envfetch has been installed and added to your PATH. Please restart your terminal for the changes to take effect."

Unix

This is Bash script, that downloads and extracts to specific directory executable form releases. Actual source of this script. Firstly, we run following command to exit immediately on error:

set -e

Then, we define function to calculate file's checksums:

# Calculate checksum. We use this function because Linux and macOS have different commands
calculate_checksum() {
    if [ "$OS_NAME" = "Darwin" ]; then
        # macOS
        shasum -a 256 "$1" | cut -d ' ' -f 1
    else
        # Linux and others
        sha256sum "$1" | cut -d ' ' -f 1
    fi
}

Then, we define some variables, we'll use later:

OS_NAME=$(uname -s)          # Name of OS
ARCH_NAME=$(uname -m)        # Architecture name
OS=""                        # Actual OS
ARCH=""                      # Actual arch
INSTALL_DIR="/usr/local/bin" # Install directory path
NO_RELEASE_ASSET=""          # Helper variable that shows that release don't found

Then, we print path to user and activate sudo (currently install script installs only system-wide):

echo "Installing envfetch to $INSTALL_DIR"
echo "This script will activate sudo to install to $INSTALL_DIR"
sudo echo "Sudo activated"

Then we have large snippet that gets user's OS and arch:

if [ "$OS_NAME" = "Linux" ]; then
	OS="Linux"

    if [ "$ARCH_NAME" = "x86_64" ]; then
	    BUILD_TARGET="linux-amd64"
    elif [ "$ARCH_NAME" = "arm" ] || [ "$ARCH_NAME" = "arm64" ]; then
        BUILD_TARGET="linux-arm64"
    else
        NO_RELEASE_ASSET="true"
        echo "There is no release for this architecture: $ARCH_NAME" >&2
    fi
elif [ "$OS_NAME" = "Darwin" ]; then
	OS="macOS"
	
    if [ "$ARCH_NAME" = "x86_64" ]; then
	    BUILD_TARGET="darwin-amd64"
    elif [ "$ARCH_NAME" = "arm" ] || [ "$ARCH_NAME" = "arm64" ]; then
        BUILD_TARGET="darwin-arm64"
    else
        NO_RELEASE_ASSET="true"
        echo "There is no release for this architecture: $ARCH_NAME" >&2
    fi
else
	NO_RELEASE_ASSET="true"
	echo "There is no Unix release for this OS: $OS_NAME" >&2
fi

How it works? Firstly we check, if OS is Linux. If not, check, maybe it is Darwin (macOS). If not, then we set NO_RELEASE_ASSET to true, If OS is Linux or macOS, then we check architecture. And, finally, set neccessary build target. If no release target found, we exit with 1 code:

if [ "$NO_RELEASE_ASSET" ]; then
	exit 1 # Exit with non-zero exit code to indicate, that there is an error
fi

Then, check if file already exists and print message about updating:

if [ -f $INSTALL_DIR/envfetch ]; then
    echo "envfetch is already installed. Updating..."
fi

Then, we download executable from release to installation directory:

# Download file directly to install directory and give it permissions to execute:
sudo curl -sSL "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-$BUILD_TARGET" --output "$INSTALL_DIR/envfetch"
# Give permissions for executable
sudo chmod +x "$INSTALL_DIR/envfetch"

After that we compare checksums:

# Check integrity
EXPECTED_CHECKSUM=$(curl -sSL "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-$BUILD_TARGET.sha256" | tr -d '[:space:]' | tr -d '\n')
ACTUAL_CHECKSUM=$(calculate_checksum "$INSTALL_DIR/envfetch" | awk '{print $1}')

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
    echo "Checksum mismatch" >&2
    echo "Expected: $EXPECTED_CHECKSUM"
    echo "Actual: $ACTUAL_CHECKSUM"
    echo "Please try again later or report this issue to the developer."
    exit 1
fi

And, finally, we print message about successfull installation:

echo "Successfully installed envfetch"

Command structure

Every command has some structure. Let's go through it!

Command definition

In models.rs we have struct with some CLI arguments:

#[derive(Parser)]
#[command(
    author,
    version,
    after_help = "Get more info at project's repo: https://github.com/ankddev/envfetch",
    after_long_help = "Get more info at project's GitHub repo available at https://github.com/ankddev/envfetch",
    arg_required_else_help = true,
    name = "envfetch"
)]
#[command(
    about = "envfetch - lightweight tool for working with environment variables",
    long_about = "envfetch is a lightweight cross-platform CLI tool for working with environment variables"
)]
pub struct Cli {
    /// Tool commands
    #[command(subcommand)]
    pub command: Commands,
}

It contains only one field - command, which is Comamnds enum. Let's look in it!

/// All tool's commands
#[derive(Subcommand, Debug, PartialEq, Eq)]
pub enum Commands {
    /// Open envfetch in interactive mode with TUI.
    Interactive,
    /// Print value of environment variable.
    Get(GetArgs),
    /// Set environment variable and optionally run given process.
    Set(SetArgs),
    /// Add value to the end of environment variable and optionally run given process.
    Add(AddArgs),
    /// Delete environment variable and optionally run given process.
    Delete(DeleteArgs),
    /// Load environment variables from dotenv file and optionally run given process.
    Load(LoadArgs),
    /// Print all environment variables.
    Print(PrintArgs),
    /// Initialize config file.
    InitConfig,
}

We have several derive's, but they don't wonder as. Here we have list of all envfetch's commands. Note that doccomment on this enum's element is description in help menu (clap will generate it for us), so always write it. Some of commands have arguments. For example, let's look to args for set command (SetArgs struct):

/// Args for set command
#[derive(Args, Debug, PartialEq, Eq)]
pub struct SetArgs {
    /// Environment variable name
    #[arg(required = true)]
    pub key: String,
    /// Value for environment variable
    #[arg(required = true)]
    pub value: String,
    /// Globally set variable
    #[arg(required = false, long, short)]
    pub global: bool,
    /// Process to start, not required if --global flag is set
    #[arg(last = true, required_unless_present = "global")]
    pub process: Option<String>,
}

Comments on fields mean descriptions of argumnets. Here we see some arguments:

  • Fisrt is key. It's type is String, it's marked as required and don't have long or short, so it is positional.
  • Second is value. It also has type String, also required and positional.
  • Then we have global. It has type bool. It's marked as not required and has long and short (they aren't set, because clap automatically use field's name as long and first letter as shhort, but we can override it, e.g. long = "helloarg"), so it's optional flag.
  • And we have process arg. Firstly, it isn't required if global flag is present so has type Option<String> (you can notice that gloabl also optional, but don't have type Option. That's because if it's not present, false is automatically used). But what about last = true? This means that this arg is last argument - all content after -- will be assigned to it, e.g. with envfetch set hello hello -- npm run boot value npm run boot will be assigned to this field

run_command() function

Then, we have run_command() function in commands.rs that is runned from main() function. It's signature is:

pub fn run_command<W: Write>(
    command: &Commands,
    config: Option<Config>,
    mut buffer: W,
) -> ExitCode {

It accepts Commands enum with current command, optional config and.. buffer. Yes, we need buffer for testing purposes. If we will just use println!() macro everywhere, we won't test it, because we can't just read stdout or stderr. And it returns ExitCode to indicate, is program finished successfully. This functions matches command and run needed function.

Command's function

Then we have functions for specific commands. Here's code for set() function:

/// Set value to environment variable
pub fn set(args: &SetArgs) -> Result<(), ErrorKind> {
    validate_var_name(&args.key).map_err(ErrorKind::NameValidationError)?;

    variables::set_variable(&args.key, &args.value, args.global, args.process.clone())?;
    Ok(())
}

It validates name of variable and then.. runs another function. Why? This allows to reuse some logic to avoid duplication.

Function in variables module

Then, in variables.rs we have functions for working with variables directly. Let's check set_variable() function:

/// Set variable with given key and value
pub fn set_variable(
    key: &str,
    value: &str,
    global: bool,
    process: Option<String>,
) -> Result<(), ErrorKind> {
    if global {
        if let Err(err) = globalenv::set_var(key, value) {
            return Err(ErrorKind::CannotSetVariableGlobally(err.to_string()));
        }
    } else {
        unsafe { env::set_var(key, value) };
    }

    if let Some(process) = process {
        return run(process, false);
    }
    Ok(())
}

It checks thhat we want to set variable locally or globally, sets it with specific function and than, if needed, runs specified process.