#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
mod common;
mod deck;
mod doctor;
mod generate;
mod launch;
mod progress;
mod share;
use std::path::PathBuf;
use alix::config::{self, Config};
use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand};
use launch::launch;
#[derive(Parser)]
#[command(version, about)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[command(flatten)]
launch: LaunchArgs,
}
#[derive(Args)]
struct LaunchArgs {
dir: Option<PathBuf>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
lan: bool,
#[arg(long)]
token: Option<String>,
#[arg(long)]
new: Option<usize>,
#[arg(long)]
limit: Option<usize>,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Subcommand)]
enum Command {
Doctor(DoctorArgs),
Generate(GenerateArgs),
Stats(DeckArgs),
List(DeckArgs),
Reset(ResetArgs),
#[command(subcommand)]
Deck(DeckAction),
#[command(subcommand)]
Workspace(WorkspaceAction),
Share(ShareArgs),
Receive(ReceiveArgs),
Config {
#[arg(long)]
init: bool,
},
}
#[derive(Args)]
struct DoctorArgs {
dir: Option<PathBuf>,
#[arg(long)]
backends: bool,
#[arg(long, conflicts_with = "backends")]
all_backends: bool,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Subcommand)]
enum WorkspaceAction {
Init(WorkspaceInitArgs),
}
#[derive(Args)]
struct WorkspaceInitArgs {
dir: PathBuf,
#[arg(long)]
title: Option<String>,
}
#[derive(Args)]
struct ShareArgs {
path: PathBuf,
#[arg(long)]
zip: bool,
#[arg(long, requires = "zip")]
output: Option<PathBuf>,
}
#[derive(Args)]
struct ReceiveArgs {
#[arg(value_name = "CODE|ZIP")]
code: String,
#[arg(long)]
workspace: Option<PathBuf>,
#[arg(long)]
force: bool,
}
#[derive(Args)]
struct GenerateArgs {
source: String,
#[arg(long)]
goal: Option<String>,
#[arg(long)]
plan: bool,
#[arg(long)]
trace: bool,
#[arg(long, conflicts_with = "trace")]
deck: bool,
#[arg(long)]
workspace: Option<PathBuf>,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
cards: Option<usize>,
#[arg(long)]
review: bool,
#[arg(long)]
print: bool,
#[arg(long)]
force: bool,
#[arg(long)]
title: Option<String>,
#[arg(long)]
icon: Option<PathBuf>,
#[arg(short, long)]
yes: bool,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Args)]
struct GenerateDeckArgs {
source: String,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
cards: Option<usize>,
#[arg(long)]
review: bool,
#[arg(long)]
print: bool,
#[arg(long)]
force: bool,
#[arg(short, long)]
yes: bool,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Subcommand)]
enum DeckAction {
Augment(AugmentArgs),
Import(ImportArgs),
}
#[derive(Args)]
struct AugmentArgs {
deck: PathBuf,
#[arg(long, value_enum)]
target: AugmentTarget,
#[arg(long)]
with: Option<String>,
#[arg(long)]
store: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum AugmentTarget {
Choices,
Notes,
Questions,
Keypoints,
Topology,
Format,
}
#[derive(Args)]
struct ImportArgs {
file: PathBuf,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
workspace: Option<PathBuf>,
#[arg(long)]
print: bool,
#[arg(long)]
force: bool,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Args)]
struct DeckArgs {
#[arg(value_name = "DECK|FOLDER|WORKSPACE")]
target: PathBuf,
#[arg(long)]
store: Option<PathBuf>,
}
#[derive(Args)]
struct ResetArgs {
#[arg(value_name = "DECK|FOLDER|WORKSPACE")]
target: Option<PathBuf>,
#[arg(long)]
card: Option<String>,
#[arg(long, conflicts_with_all = ["target", "card"])]
all: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long)]
store: Option<PathBuf>,
}
fn main() -> Result<()> {
alix::store::migrate_legacy_data_dir();
let cli = Cli::parse();
match cli.command {
None => launch(cli.launch),
Some(Command::Stats(args)) => progress::stats(args),
Some(Command::List(args)) => progress::list(args),
Some(Command::Reset(args)) => progress::reset(args),
Some(Command::Generate(args)) => generate::generate_cmd(args),
Some(Command::Deck(action)) => match action {
DeckAction::Augment(args) => deck::augment_cmd(args),
DeckAction::Import(args) => deck::import_cmd(args),
},
Some(Command::Workspace(action)) => match action {
WorkspaceAction::Init(args) => deck::workspace_init_cmd(args),
},
Some(Command::Share(args)) => share::share_cmd(args),
Some(Command::Receive(args)) => share::receive_cmd(args),
Some(Command::Config { init }) => config_cmd(init),
Some(Command::Doctor(args)) => doctor::doctor_cmd(args),
}
}
fn config_cmd(init: bool) -> Result<()> {
let path = config::default_config_path().context("cannot determine the config directory")?;
if init {
if path.exists() {
bail!("{} already exists; edit it directly", path.display());
}
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)
.with_context(|| format!("cannot create {}", dir.display()))?;
}
std::fs::write(&path, config::default_config_toml())
.with_context(|| format!("cannot write {}", path.display()))?;
println!("wrote {}", path.display());
return Ok(());
}
if path.exists() {
println!("config file: {}", path.display());
} else {
println!(
"no config file at {} — using defaults; create one with \
`alix config --init`",
path.display()
);
}
let config = Config::load(None)?;
let keys = &config.keys;
let show = |action: &str, list: &[config::KeyPattern]| {
let keys: Vec<String> = list.iter().map(|p| p.to_string()).collect();
println!(" {action:<9} {}", keys.join(", "));
};
println!("key bindings:");
show("failed", &keys.failed);
show("partly", &keys.partly);
show("passed", &keys.passed);
show("reveal", &keys.reveal);
show("hint", &keys.hint);
show("submit", &keys.submit);
show("skip", &keys.skip);
show("remove", &keys.remove);
show("continue", &keys.cont);
show("restart", &keys.restart);
show("ask", &keys.ask);
show("save_note", &keys.save_note);
show("quit", &keys.quit);
println!("browse bindings (first/last fixed: g/G/Home/End):");
show("next", &config.browse.next);
show("prev", &config.browse.prev);
show("remove", &config.browse.remove);
show("quit", &config.browse.quit);
println!("ask:");
println!(" command {}", config.ask.command);
println!(
" model {}",
config.ask.model.as_deref().unwrap_or("(CLI default)")
);
println!(" timeout {}s", config.ask.timeout_secs);
println!(" permission {}", config.ask.permission_mode);
println!(" tools {}", config.ask.allowed_tools.join(", "));
println!("generate:");
println!(
" model {}",
config
.generate
.model
.as_deref()
.unwrap_or("(ask / CLI default)")
);
println!(" timeout {}s", config.generate.timeout_secs);
println!(" max_cards {}", config.generate.max_cards);
println!(" review {}", config.generate.review);
Ok(())
}