diff --git a/Cargo.lock b/Cargo.lock index 753d61c..5610882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,7 @@ dependencies = [ "csscolorparser", "env_logger", "futures", + "html2md", "human_bytes", "indicatif", "inquire", @@ -193,6 +194,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -299,6 +306,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "confy" version = "0.6.0" @@ -491,6 +508,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.30" @@ -652,6 +679,34 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "html2md" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cff9891f2e0d9048927fbdfc28b11bf378f6a93c7ba70b23d0fbee9af6071b4" +dependencies = [ + "html5ever", + "jni", + "lazy_static", + "markup5ever_rcdom", + "percent-encoding", + "regex", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "http" version = "1.3.1" @@ -888,6 +943,26 @@ dependencies = [ "syn", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.77" @@ -943,6 +1018,38 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "memchr" version = "2.7.1" @@ -1004,6 +1111,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "newline-converter" version = "0.3.0" @@ -1152,6 +1265,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.2" @@ -1217,6 +1340,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -1430,6 +1559,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.23" @@ -1588,6 +1726,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1654,6 +1817,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.56" @@ -1906,6 +2080,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1924,6 +2104,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2048,6 +2238,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2303,6 +2502,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 71c4bd6..22ba146 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,16 +19,12 @@ csscolorparser = "0.7.0" env_logger = "0.11.7" futures = "0.3.30" human_bytes = "0.4.3" +html2md = "0.2.14" indicatif = "0.17.7" inquire = "0.7.5" log = "0.4.20" regex = "1.10.2" -reqwest = { version = "0.12.13", features = [ - "stream", - "multipart", - "json", - "native-tls-vendored", -] } +reqwest = { version = "0.12.13", features = [ "stream", "multipart", "json", "native-tls-vendored", ] } serde = "1.0.195" serde_derive = "1.0.195" serde_json = "1.0.133" diff --git a/src/assignments.rs b/src/assignments.rs new file mode 100644 index 0000000..d5d6534 --- /dev/null +++ b/src/assignments.rs @@ -0,0 +1,554 @@ +use crate::{Config, NonEmptyConfig}; +use chrono::{DateTime, Utc}; +use colored::Colorize; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// Assignment data structure matching Canvas API response +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Assignment { + pub id: u32, + pub name: String, + #[serde(default)] + pub description: String, + #[serde(rename = "due_at")] + pub due_at: Option>, + #[serde(rename = "points_possible")] + pub points_possible: Option, + #[serde(rename = "submission_types", default)] + pub submission_types: Vec, + #[serde(rename = "workflow_state", default)] + pub workflow_state: String, + #[serde(rename = "html_url", default)] + pub html_url: String, + pub submission: Option, + #[serde(rename = "assignment_group_id")] + pub assignment_group_id: Option, + #[serde(default)] + pub locked: bool, + #[serde(rename = "lock_info")] + pub lock_info: Option, + #[serde(rename = "lock_at")] + pub lock_at: Option>, + #[serde(rename = "unlock_at")] + pub unlock_at: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SubmissionInfo { + pub body: Option, + #[serde(rename = "submitted_at")] + pub submitted_at: Option>, + pub grade: Option, + pub score: Option, + #[serde(rename = "workflow_state")] + pub workflow_state: String, + pub attempt: Option, + #[serde(rename = "submission_type")] + pub submission_type: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct LockInfo { + #[serde(rename = "lock_at")] + pub lock_at: Option>, + #[serde(rename = "unlock_at")] + pub unlock_at: Option>, + #[serde(rename = "can_view")] + pub can_view: bool, +} + +/// Output format options +#[derive(Debug, Clone, clap::ValueEnum)] +pub enum OutputFormat { + Table, + Json, + Markdown, + Csv, +} + +impl Display for OutputFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OutputFormat::Table => write!(f, "table"), + OutputFormat::Json => write!(f, "json"), + OutputFormat::Markdown => write!(f, "markdown"), + OutputFormat::Csv => write!(f, "csv"), + } + } +} + +#[derive(clap::Parser, Debug)] +/// View assignment information for a course +pub struct AssignmentsCommand { + /// Canvas course ID + #[clap(long, short)] + course: Option, + + /// Canvas course or assignment URL to parse + #[clap(long, short)] + url: Option, + + /// Specific assignment ID (shows only that assignment) + #[clap(long, short)] + assignment: Option, + + /// Output format + #[clap(long, short = 'f', default_value = "table", value_enum)] + format: OutputFormat, + + /// Filter by assignment type (e.g., "online_upload", "online_text_entry") + #[clap(long = "type")] + type_filter: Option, + + /// Show only upcoming assignments (due in the future) + #[clap(long)] + upcoming: bool, + + /// Show only incomplete assignments (not yet submitted) + #[clap(long)] + incomplete: bool, + + /// Show only missing or late assignments + #[clap(long)] + missing: bool, + + /// Include assignment description in output + #[clap(long, short)] + verbose: bool, + + /// Limit number of assignments shown (0 for all) + #[clap(long, default_value = "0")] + limit: usize, +} + +impl AssignmentsCommand { + pub async fn action(&self, cfg: &Config) -> Result<(), anyhow::Error> { + let NonEmptyConfig { + url: mut base_url, + access_token, + } = cfg.ensure_non_empty()?; + + let client = reqwest::Client::builder() + .default_headers( + std::iter::once(( + reqwest::header::AUTHORIZATION, + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", access_token)) + .unwrap(), + )) + .collect(), + ) + .build() + .unwrap(); + + let mut course_id = self.course; + let mut assignment_id = self.assignment; + + // Parse URL if provided + if let Some(canvas_url) = &self.url { + let regex = + Regex::new(r#"(https://.+)/courses/(\d+)(?:/assignments/(\d+))?"#).unwrap(); + if let Some(captures) = regex.captures(canvas_url) { + base_url = captures.get(1).unwrap().as_str().to_string(); + course_id = Some(captures.get(2).unwrap().as_str().parse::().unwrap()); + if let Some(assignment_match) = captures.get(3) { + assignment_id = Some(assignment_match.as_str().parse::().unwrap()); + } + } + } + + // Check environment variables + if let Ok(env_course_id) = std::env::var("CANVAS_COURSE_ID") { + course_id = Some(env_course_id.parse::()?); + } + + let course_id = course_id.ok_or_else(|| { + anyhow::anyhow!( + "Course ID required. Use --course, --url, or set CANVAS_COURSE_ID environment variable." + ) + })?; + + // Fetch course information + let course = crate::Course::fetch(Some(course_id), &base_url, &client).await?; + + // Fetch assignments + let mut assignments = Assignment::fetch_all(course_id, &base_url, &client).await?; + + // Filter by specific assignment if requested + if let Some(assignment_id) = assignment_id { + assignments.retain(|a| a.id == assignment_id); + if assignments.is_empty() { + anyhow::bail!("Assignment {} not found in course {}", assignment_id, course_id); + } + } + + // Apply filters + let now = Utc::now(); + + if self.upcoming { + assignments.retain(|a| a.due_at.map(|d| d > now).unwrap_or(false)); + } + + if self.incomplete { + assignments.retain(|a| { + a.submission + .as_ref() + .map(|s| s.workflow_state != "submitted" && s.workflow_state != "graded") + .unwrap_or(true) + }); + } + + if self.missing { + assignments.retain(|a| { + a.submission + .as_ref() + .map(|s| s.workflow_state == "missing" || s.workflow_state == "late") + .unwrap_or(false) + }); + } + + if let Some(type_filter) = &self.type_filter { + assignments.retain(|a| { + a.submission_types + .iter() + .any(|t| t.to_lowercase().contains(&type_filter.to_lowercase())) + }); + } + + // Sort by due date (earliest first), with null dates at the end + assignments.sort_by(|a, b| { + a.due_at + .cmp(&b.due_at) + .then_with(|| a.name.cmp(&b.name)) + }); + + // Apply limit + if self.limit > 0 && assignments.len() > self.limit { + assignments.truncate(self.limit); + } + + // Output based on format + match self.format { + OutputFormat::Json => { + self.output_json(&assignments, course_id, &course.name)?; + } + OutputFormat::Markdown => { + self.output_markdown(&assignments)?; + } + OutputFormat::Csv => { + self.output_csv(&assignments)?; + } + OutputFormat::Table => { + self.output_table(&assignments)?; + } + } + + Ok(()) + } + + fn output_json( + &self, + assignments: &[Assignment], + course_id: u32, + course_name: &str, + ) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct AssignmentsOutput { + course_id: u32, + course_name: String, + count: usize, + fetched_at: DateTime, + assignments: Vec, + } + + let output = AssignmentsOutput { + course_id, + course_name: course_name.to_string(), + count: assignments.len(), + fetched_at: Utc::now(), + assignments: assignments.to_vec(), + }; + + println!("{}", serde_json::to_string_pretty(&output)?); + Ok(()) + } + + fn output_markdown(&self, assignments: &[Assignment]) -> Result<(), anyhow::Error> { + if assignments.is_empty() { + println!("No assignments found."); + return Ok(()); + } + + // Header + println!("| ID | Name | Due Date | Points | Status |"); + println!("|----|------|----------|--------|--------|"); + + // Rows + for assignment in assignments { + let due_date = assignment + .due_at + .map(|d| d.format("%b %d, %Y %H:%M").to_string()) + .unwrap_or_else(|| "No due date".to_string()); + + let points = assignment + .points_possible + .map(|p| p.to_string()) + .unwrap_or_else(|| "—".to_string()); + + let status = match &assignment.submission { + Some(sub) => match sub.workflow_state.as_str() { + "submitted" => "✓ Submitted".to_string(), + "graded" => format!("✓ Graded ({})", sub.grade.clone().unwrap_or_default()), + "missing" => "❌ Missing".to_string(), + "late" => "⏰ Late".to_string(), + _ => sub.workflow_state.clone(), + }, + None => "📝 Not submitted".to_string(), + }; + + let name = if self.verbose { + format!("{}\n> {}", assignment.name, self.truncate(&assignment.description, 100)) + } else { + assignment.name.clone() + }; + + println!( + "| {} | {} | {} | {} | {} |", + assignment.id, name, due_date, points, status + ); + } + + Ok(()) + } + + fn output_csv(&self, assignments: &[Assignment]) -> Result<(), anyhow::Error> { + if assignments.is_empty() { + return Ok(()); + } + + // Header + println!( + "id,name,due_at,points_possible,workflow_state,submission_status,score,grade" + ); + + // Rows + for assignment in assignments { + let due_at = assignment + .due_at + .map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()) + .unwrap_or_else(|| "".to_string()); + + let points = assignment + .points_possible + .map(|p| p.to_string()) + .unwrap_or_else(|| "".to_string()); + + let (submission_status, score, grade) = match &assignment.submission { + Some(sub) => ( + sub.workflow_state.clone(), + sub.score + .map(|s| s.to_string()) + .unwrap_or_else(|| "".to_string()), + sub.grade.clone().unwrap_or_else(|| "".to_string()), + ), + None => ("not_submitted".to_string(), "".to_string(), "".to_string()), + }; + + // Escape commas and quotes in name + let name = format!("\"{}\"", assignment.name.replace('"', "\"\"")); + + println!( + "{},{},{},{},{},{},{},{}", + assignment.id, name, due_at, points, assignment.workflow_state, submission_status, score, grade + ); + } + + Ok(()) + } + + fn output_table(&self, assignments: &[Assignment]) -> Result<(), anyhow::Error> { + if assignments.is_empty() { + println!("{}", "No assignments found.".bright_yellow()); + return Ok(()); + } + + println!( + "{}", + format!("Found {} assignment(s)", assignments.len()).bright_green() + ); + println!(); + + for assignment in assignments { + // Assignment name and ID + println!( + "{} {}", + "Assignment:".bright_cyan(), + assignment.name.bold() + ); + println!("{} {}", "ID:".cyan(), assignment.id.to_string()); + + // Due date + if let Some(due_at) = assignment.due_at { + let now = Utc::now(); + let due_str = due_at.format("%B %d, %Y at %I:%M %p").to_string(); + + let status = if due_at < now { + "Past due".red() + } else { + "Upcoming".green() + }; + + println!("{} {} ({})", "Due:".cyan(), due_str, status); + } else { + println!("{} {}", "Due:".cyan(), "No due date".bright_yellow()); + } + + // Points + if let Some(points) = assignment.points_possible { + println!("{} {}", "Points:".cyan(), format!("{}", points)); + } + + // Submission status + match &assignment.submission { + Some(sub) => { + let status_icon = match sub.workflow_state.as_str() { + "submitted" => "✓", + "graded" => "✓", + "missing" => "❌", + "late" => "⏰", + _ => "○", + }; + + let status_text = match sub.workflow_state.as_str() { + "graded" => { + if let Some(grade) = &sub.grade { + format!("Graded: {}", grade) + } else if let Some(score) = sub.score { + format!("Graded: {}", score) + } else { + "Graded".to_string() + } + } + "submitted" => "Submitted".to_string(), + "missing" => "Missing".to_string(), + "late" => "Late".to_string(), + other => format!("{}", other), + }; + + println!( + "{} {} {}", + "Status:".cyan(), + status_icon, + status_text + ); + } + None => { + println!( + "{} {} {}", + "Status:".cyan(), + "📝", + "Not submitted".bright_yellow() + ); + } + } + + // Submission type + if !assignment.submission_types.is_empty() { + println!( + "{} {}", + "Type:".cyan(), + assignment + .submission_types + .join(", ") + .replace("online_", "") + .replace("_", " ") + ); + } + + // Description (verbose mode) + if self.verbose && !assignment.description.is_empty() { + let description_plain = html2md::parse_html(&assignment.description); + println!( + "\n{} {}", + "Description:".cyan(), + self.truncate(&description_plain, 500) + ); + } + + // URL + if !assignment.html_url.is_empty() { + println!("{} {}", "URL:".cyan(), assignment.html_url); + } + + println!("{}", "─".repeat(60)); + } + + Ok(()) + } + + fn truncate(&self, s: &str, max_len: usize) -> String { + if s.len() <= max_len { + s.to_string() + } else { + format!("{}...", &s[..max_len.saturating_sub(3)]) + } + } +} + +impl Assignment { + /// Fetch all assignments for a course + pub async fn fetch_all( + course_id: u32, + base_url: &str, + client: &reqwest::Client, + ) -> Result, anyhow::Error> { + let response = client + .get(format!( + "{}/api/v1/courses/{}/assignments?per_page=100&include[]=submissions", + base_url, course_id + )) + .send() + .await?; + + if !response.status().is_success() { + anyhow::bail!( + "Failed to fetch assignments: {} {}", + response.status(), + response.text().await? + ); + } + + let assignments: Vec = response.json().await?; + log::info!("Fetched {} assignments for course {}", assignments.len(), course_id); + Ok(assignments) + } + + /// Fetch a single assignment by ID + pub async fn fetch( + course_id: u32, + assignment_id: u32, + base_url: &str, + client: &reqwest::Client, + ) -> Result { + let response = client + .get(format!( + "{}/api/v1/courses/{}/assignments/{}?include[]=submissions", + base_url, course_id, assignment_id + )) + .send() + .await?; + + if !response.status().is_success() { + anyhow::bail!( + "Failed to fetch assignment: {} {}", + response.status(), + response.text().await? + ); + } + + let assignment: Assignment = response.json().await?; + log::info!("Fetched assignment {} for course {}", assignment_id, course_id); + Ok(assignment) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2dfb586..fba9185 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::env; pub mod auth; pub mod download; pub mod submit; +pub mod assignments; #[derive(Serialize, Deserialize, Debug, Default)] pub struct Config { @@ -15,8 +16,8 @@ pub struct Config { #[derive(Debug)] pub struct NonEmptyConfig { - url: String, - access_token: String, + pub url: String, + pub access_token: String, } impl Config { @@ -52,7 +53,8 @@ enum Action { Auth(auth::AuthCommand), Submit(submit::SubmitCommand), Download(download::DownloadCommand), - + /// View assignment information + Assignments(assignments::AssignmentsCommand), /// Generate shell completions Completions { /// The shell to generate the completions for @@ -64,7 +66,6 @@ enum Action { #[tokio::main] async fn main() -> Result<(), anyhow::Error> { env_logger::init(); - let args = Args::parse(); // Don't load the config if doing completions, since that accesses the home directory and breaks the nix build @@ -87,7 +88,7 @@ async fn main() -> Result<(), anyhow::Error> { Action::Auth(command) => command.action(&mut cfg).await, Action::Submit(command) => command.action(&cfg).await, Action::Download(command) => command.action(&cfg).await, - + Action::Assignments(command) => command.action(&cfg).await, Action::Completions { shell: _ } => unreachable!(), } }