Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
/target
.api_manifest
.last_modified
downloads/*
vmm_config.toml
/coverage
benches/fixtures/*.bin.zst
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ reqwest = {version = "0.12.14", features = ["json", "stream", "rustls-tls"], def
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shellexpand = "3.1"
xdg = "2.5"
thiserror = "2.0"
time = { version = "0.3", features = ["serde-well-known"] }
tokio = { version = "1.32", features = ["full"] }
Expand Down
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,26 @@ cargo install --path .

## Configuration

The first time you run `vmm`, it will create a default configuration file at `vmm_config.toml` in your current directory. You can edit this file to customize:
`vmm` looks for configuration in the following order:

1. A `vmm_config.toml` in the current directory (local config)
2. `~/.config/vmm/vmm_config.toml` (global XDG config)

If neither exists, a default config is created at the global location on first run.

You can also specify a config file directly with the `--config` flag, which bypasses the above lookup entirely.

The config file supports the following settings:

- `mod_list`: List of mods to install (in the format `"Owner-ModName"`)
- `log_level`: Logging verbosity (`error`, `warn`, `info`, `debug`, `trace`)
- `cache_dir`: Directory to store cached mod information (default: `~/.config/vmm`)
- `install_dir`: Optional directory where unzipped mods will be copied (e.g., your Valheim mods folder)

Example configuration:

```toml
mod_list = ["denikson-BepInExPack_Valheim", "ValheimModding-Jotunn"]
log_level = "info"
cache_dir = "~/.config/vmm"
install_dir = "~/some/path/to/Valheim/BepInEx/plugins"
```

Expand All @@ -65,6 +72,34 @@ Downloads all mods in your configuration, including their dependencies:
vmm update mods
```

### List Mods

Lists all mods from your configuration including resolved dependencies:

```bash
vmm list
```

By default outputs one mod per line. Use `--format json` for structured output:

```bash
vmm list --format json
```

**Text output (default):**
```
denikson-BepInExPack_Valheim 5.4.2202
ValheimModding-Jotunn 2.28.0
```

**JSON output:**
```json
[
{"full_name": "denikson-BepInExPack_Valheim", "version": "5.4.2202"},
{"full_name": "ValheimModding-Jotunn", "version": "2.28.0"}
]
```

### Search for Mods

Searches available mods by name:
Expand All @@ -75,6 +110,18 @@ vmm search <term>

This performs a case-insensitive search for mods containing the specified term in their name, displaying matching mods with their owner, name, version, and description.

## Global Options

### `--config <path>`

Override the config file location, bypassing the local/global lookup:

```bash
vmm --config /path/to/my/vmm_config.toml update mods
```

Downloads and cached data always go to `~/.config/vmm/` regardless of which config file is used. Respects `$XDG_CONFIG_HOME` if set.

## How It Works

1. Reads your configuration to determine which mods to download
Expand All @@ -86,20 +133,18 @@ This performs a case-insensitive search for mods containing the specified term i

## Directory Structure

- `~/.config/vmm/` (or custom `cache_dir` setting in config): Cache directory for mod information
- `~/.config/vmm/downloads/` (or custom `cache_dir/downloads`): Location for downloaded mod archives and extracted files
- `~/.config/vmm/`: Config and cache directory (respects `$XDG_CONFIG_HOME`)
- `~/.config/vmm/downloads/`: Downloaded mod archives and extracted files

## Advanced Features
## Troubleshooting

### Troubleshooting
If you encounter issues, increase log verbosity in your config:

If you encounter issues:
```toml
log_level = "debug"
```

1. Increase log verbosity in your config:
```toml
log_level = "debug"
```
2. Run the command again to see more detailed output
Then run the command again to see more detailed output.

## License

Expand Down
54 changes: 22 additions & 32 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,64 +24,52 @@ const API_MANIFEST_FILENAME_V1: &str = "api_manifest.bin.zst";
///
/// # Parameters
///
/// * `cache_dir` - The cache directory path (supports tilde expansion)
/// * `cache_dir` - The cache directory path
///
/// # Returns
///
/// The full path to the last_modified file
fn last_modified_path(cache_dir: &str) -> PathBuf {
let expanded_path = shellexpand::tilde(cache_dir);
let mut path = PathBuf::from(expanded_path.as_ref());
path.push(LAST_MODIFIED_FILENAME);
path
PathBuf::from(cache_dir).join(LAST_MODIFIED_FILENAME)
}

/// Returns the path to the v3 API manifest file in the cache directory.
///
/// # Parameters
///
/// * `cache_dir` - The cache directory path (supports tilde expansion)
/// * `cache_dir` - The cache directory path
///
/// # Returns
///
/// The full path to the v3 manifest file
fn api_manifest_path_v3(cache_dir: &str) -> PathBuf {
let expanded_path = shellexpand::tilde(cache_dir);
let mut path = PathBuf::from(expanded_path.as_ref());
path.push(API_MANIFEST_FILENAME_V3);
path
PathBuf::from(cache_dir).join(API_MANIFEST_FILENAME_V3)
}

/// Returns the path to the v2 API manifest file in the cache directory.
///
/// # Parameters
///
/// * `cache_dir` - The cache directory path (supports tilde expansion)
/// * `cache_dir` - The cache directory path
///
/// # Returns
///
/// The full path to the v2 manifest file
fn api_manifest_path_v2(cache_dir: &str) -> PathBuf {
let expanded_path = shellexpand::tilde(cache_dir);
let mut path = PathBuf::from(expanded_path.as_ref());
path.push(API_MANIFEST_FILENAME_V2);
path
PathBuf::from(cache_dir).join(API_MANIFEST_FILENAME_V2)
}

/// Returns the path to the v1 API manifest file in the cache directory.
///
/// # Parameters
///
/// * `cache_dir` - The cache directory path (supports tilde expansion)
/// * `cache_dir` - The cache directory path
///
/// # Returns
///
/// The full path to the v1 manifest file
fn api_manifest_path_v1(cache_dir: &str) -> PathBuf {
let expanded_path = shellexpand::tilde(cache_dir);
let mut path = PathBuf::from(expanded_path.as_ref());
path.push(API_MANIFEST_FILENAME_V1);
path
PathBuf::from(cache_dir).join(API_MANIFEST_FILENAME_V1)
}

/// Retrieves the manifest of available packages.
Expand Down Expand Up @@ -614,8 +602,7 @@ async fn download_file(
progress_style: ProgressStyle,
cache_dir: &str,
) -> AppResult<()> {
let expanded_path = shellexpand::tilde(cache_dir);
let mut downloads_directory = PathBuf::from(expanded_path.as_ref());
let mut downloads_directory = PathBuf::from(cache_dir);
downloads_directory.push("downloads");
let mut file_path = downloads_directory.clone();
file_path.push(filename);
Expand Down Expand Up @@ -678,8 +665,7 @@ mod tests {
let temp_dir = tempdir().unwrap();
let cache_dir = temp_dir.path().to_str().unwrap();

let expanded_path = shellexpand::tilde(cache_dir);
let mut downloads_directory = PathBuf::from(expanded_path.as_ref());
let mut downloads_directory = PathBuf::from(cache_dir);
downloads_directory.push("downloads");

let expected_directory = PathBuf::from(cache_dir).join("downloads");
Expand All @@ -701,16 +687,20 @@ mod tests {
}

#[test]
fn test_path_with_tilde() {
let home_path = "~/some_test_dir";
let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
let expected_path = Path::new(&home_dir).join("some_test_dir");
fn test_path_construction() {
let cache_dir = "/some/cache/dir";

let last_modified = last_modified_path(home_path);
let api_manifest = api_manifest_path_v3(home_path);
let last_modified = last_modified_path(cache_dir);
let api_manifest = api_manifest_path_v3(cache_dir);

assert_eq!(last_modified.parent().unwrap(), expected_path);
assert_eq!(api_manifest.parent().unwrap(), expected_path);
assert_eq!(
last_modified,
Path::new(cache_dir).join(LAST_MODIFIED_FILENAME)
);
assert_eq!(
api_manifest,
Path::new(cache_dir).join(API_MANIFEST_FILENAME_V3)
);
}

#[test]
Expand Down
48 changes: 47 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::{Args, Parser, Subcommand};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;

/// Root command-line interface structure for the application.
///
Expand All @@ -7,6 +8,9 @@ use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct AppCli {
/// Path to a config file, overriding the default XDG location.
#[arg(long, global = true)]
pub config: Option<PathBuf>,
/// The subcommand to execute.
#[command(subcommand)]
pub command: Command,
Expand All @@ -19,6 +23,25 @@ pub enum Command {
Update(CommandArgs),
/// Search for mods by name.
Search(SearchArgs),
/// List all mods from config including resolved dependencies.
List(ListArgs),
}

/// Arguments for the list command.
#[derive(Args)]
pub struct ListArgs {
/// Output format.
#[arg(long, value_enum, default_value_t = ListFormat::Text)]
pub format: ListFormat,
}

/// Output format for the list command.
#[derive(Clone, ValueEnum)]
pub enum ListFormat {
/// Plain text, one mod per line.
Text,
/// JSON array.
Json,
}

/// Arguments for the update command.
Expand Down Expand Up @@ -122,4 +145,27 @@ mod tests {
.contains("Update installed mods to their latest versions")
);
}

#[test]
fn test_command_list() {
let app = AppCli::command();
let list_command = app.find_subcommand("list").unwrap();

assert_eq!(list_command.get_name(), "list");
assert!(list_command.get_about().is_some());
assert!(
list_command
.get_about()
.unwrap()
.to_string()
.contains("List all mods from config including resolved dependencies")
);

let list_args = list_command.get_arguments().collect::<Vec<_>>();
let format_arg = list_args
.iter()
.find(|a| a.get_id().as_str() == "format")
.unwrap();
assert!(format_arg.get_default_values().iter().any(|v| v == "text"));
}
}
Loading
Loading