Skip to content

Commit

Permalink
add --info and --time-format
Browse files Browse the repository at this point in the history
  • Loading branch information
figsoda committed Oct 25, 2022
1 parent 376c909 commit acff0b9
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 33 deletions.
47 changes: 46 additions & 1 deletion 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 @@ -21,6 +21,7 @@ is_executable = "1.0.1"
java-properties = "1.4.1"
rpassword = "7.0.0"
serde = { version = "1.0.147", features = ["derive"] }
time = { version = "0.3.16", features = ["formatting", "macros", "parsing"] }
toml = "0.5.9"
ureq = "2.5.0"
webbrowser = "0.8.0"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ Arguments:
Options:
-n, --no-submit Don't submit the project
-i, --info Show information about the project and exit
-o, --open Open the project page in a web browser
--color <WHEN> Controls when to use color [default: auto] [possible values: auto, always, never]
-c, --config <FILE> Specify the path to the config file, looks for sagoin/config.toml under XDG configuration directories on unix-like systems, and defaults to {FOLDERID_RoamingAppData}\sagoin\config.toml on windows when unspecified [env: SAGOIN_CONFIG=]
-t, --time-format <FORMAT> Specify how to format the due date, ignored without the --info flag, defaults to "[month repr:short] [day padding:none], [hour]:[minute]" when unspecified [env: SAGOIN_TIME_FORMAT=]
-u, --username <USERNAME> Specify the username for authentication, see --username-type for more information [env: SAGOIN_USERNAME=]
-U, --username-type <TYPE> Specify the type for the username, defaults to text when unspecified [env: SAGOIN_USERNAME_TYPE=] [possible values: command, file, text]
-p, --password <PASSWORD> Specify the password for authentication, see --password-type for more information [env: SAGOIN_PASSWORD=]
Expand Down
11 changes: 11 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ pub(crate) struct Opts {
#[arg(short, long)]
pub no_submit: bool,

/// Show information about the project and exit
#[arg(short, long)]
pub info: bool,

/// Open the project page in a web browser
#[arg(short, long)]
pub open: bool,
Expand All @@ -36,6 +40,13 @@ pub(crate) struct Opts {
#[arg(short, long, env = "SAGOIN_CONFIG", value_name = "FILE")]
pub config: Option<PathBuf>,

/// Specify how to format the due date, ignored without the --info flag,
/// defaults to "[month repr:short] [day padding:none], [hour]:[minute]" when unspecified
///
/// See https://time-rs.github.io/book/api/format-description.html for more information
#[arg(short, long, env = "SAGOIN_TIME_FORMAT", value_name = "FORMAT")]
pub time_format: Option<String>,

/// Specify the username for authentication,
/// see --username-type for more information
#[arg(short, long, env = "SAGOIN_USERNAME")]
Expand Down
14 changes: 14 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use crate::{
pub struct Config {
pub dir: Option<PathBuf>,
pub no_submit: bool,
pub info: bool,
pub open: bool,
pub time_format: String,
pub(crate) username: Option<Credential>,
pub(crate) password: Option<Credential>,
pub(crate) pre_submit_hook: Option<OsString>,
Expand All @@ -33,6 +35,7 @@ pub(crate) enum Credential {

#[derive(Deserialize)]
struct ConfigFile {
time_format: Option<String>,
username: Option<String>,
username_type: Option<InputType>,
password: Option<String>,
Expand All @@ -56,7 +59,12 @@ pub fn load_config() -> Result<(Config, State<StderrLock<'static>>)> {
Config {
dir: opts.dir,
no_submit: opts.no_submit,
info: opts.info,
open: opts.open,
time_format: opts
.time_format
.or(cfg.time_format)
.unwrap_or_else(default_time_format),
username: Credential::from_fallback(
&mut state,
"username",
Expand All @@ -78,7 +86,9 @@ pub fn load_config() -> Result<(Config, State<StderrLock<'static>>)> {
Config {
dir: opts.dir,
no_submit: opts.no_submit,
info: opts.info,
open: opts.open,
time_format: opts.time_format.unwrap_or_else(default_time_format),
username: opts.username.and_then(|user| {
Credential::from_os_string(&mut state, "username", user, opts.username_type)
}),
Expand All @@ -93,6 +103,10 @@ pub fn load_config() -> Result<(Config, State<StderrLock<'static>>)> {
))
}

fn default_time_format() -> String {
"[month repr:short] [day padding:none], [hour]:[minute]".into()
}

impl Credential {
fn from_fallback(
state: &mut State<impl Write>,
Expand Down
150 changes: 124 additions & 26 deletions src/course.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,103 @@
use eyre::{eyre, Result, WrapErr};
use icalendar::parser::{read_calendar_simple, unfold};
use eyre::{eyre, Report, Result, WrapErr};
use icalendar::parser::{read_calendar_simple, unfold, Component};
use time::{format_description, macros::format_description, PrimitiveDateTime};

use std::io::{stdout, Write};

use crate::{Props, PropsExt};

pub fn get_course_url(props: &Props) -> Result<String> {
let proj = format!(
"{} project {}: ",
props.get_prop("courseName")?,
props.get_prop("projectNumber")?,
);
#[derive(Default)]
pub struct CourseInfo {
summary: String,
due: Option<String>,
description: Option<String>,
url: Option<String>,
}

read_calendar_simple(&unfold(
&ureq::get(&format!(
"{}/feed/CourseCalendar?courseKey={}",
props.get_prop("baseURL")?,
props.get_prop("courseKey")?,
))
.call()
.wrap_err("failed to download the course calendar")?
.into_string()
.wrap_err("failed to parse the course calendar")?,
))
.map_err(|e| eyre!("{e}").wrap_err("failed to parse the course calendar"))?
.get(0)
.and_then(|root| {
root.components.iter().find_map(|component| {
pub fn print_course_info(props: &Props, fmt: String) -> Result<()> {
let info = get_course_info(props)?;
let mut out = stdout().lock();

writeln!(out, "{}", info.summary)?;
if let Some(due) = info.due {
write!(out, "Due: ")?;

PrimitiveDateTime::parse(
&due,
format_description!("[year][month][day]T[hour][minute][second]Z"),
)
.wrap_err("failed to parse time")?
.format_into(
&mut out,
&format_description::parse(&fmt).wrap_err("failed to parse time format")?,
)
.context("failed to format time")?;

writeln!(out)?;
}
if let Some(description) = info.description {
// https://github.com/hoodie/icalendar-rs/issues/53
writeln!(out, "{}", description.replace('\\', ""))?;
}
if let Some(url) = info.url {
writeln!(out, "{url}")?;
}

Ok(())
}

fn get_course_info(props: &Props) -> Result<CourseInfo> {
get_course_props(
props,
|component, prefix| {
let mut summary = None;
let mut due = None;
let mut description = None;
let mut url = None;

for prop in component.properties.iter() {
match prop.name.as_str() {
"SUMMARY" => {
let s = prop.val.to_string();
if s.starts_with(prefix) {
summary = Some(s);
} else {
return None;
}
}

"DTSTART" => due = Some(prop.val.to_string()),

"DESCRIPTION" => description = Some(prop.val.to_string()),

"URL" => url = Some(prop.val.to_string()),

_ => {}
}
}

summary.map(|summary| CourseInfo {
summary,
due,
description,
url,
})
},
|| eyre!("failed to find information for the course"),
)
}

pub fn get_course_url(props: &Props) -> Result<String> {
get_course_props(
props,
|component, prefix| {
let mut url = None;
let mut found = false;

for prop in component.properties.iter() {
match prop.name.as_str() {
"SUMMARY" => {
if prop.val.as_str().starts_with(&proj) {
if prop.val.as_str().starts_with(prefix) {
found = true;
} else {
return None;
Expand All @@ -49,7 +115,39 @@ pub fn get_course_url(props: &Props) -> Result<String> {
} else {
None
}
})
},
|| eyre!("failed to find the course url"),
)
}

fn get_course_props<A>(
props: &Props,
f: fn(&Component, &str) -> Option<A>,
e: fn() -> Report,
) -> Result<A> {
let prefix = format!(
"{} project {}: ",
props.get_prop("courseName")?,
props.get_prop("projectNumber")?,
);

read_calendar_simple(&unfold(
&ureq::get(&format!(
"{}/feed/CourseCalendar?courseKey={}",
props.get_prop("baseURL")?,
props.get_prop("courseKey")?,
))
.call()
.wrap_err("failed to download the course calendar")?
.into_string()
.wrap_err("failed to parse the course calendar")?,
))
.map_err(|e| eyre!("{e}").wrap_err("failed to parse the course calendar"))?
.get(0)
.and_then(|root| {
root.components
.iter()
.find_map(|component| f(component, &prefix))
})
.ok_or_else(|| eyre!("failed to find the course url"))
.ok_or_else(e)
}
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use std::{
io::{self, Cursor},
};

use sagoin::{config::load_config, course::get_course_url};
use sagoin::{
config::load_config,
course::{get_course_url, print_course_info},
};

fn main() -> Result<()> {
let (cfg, mut state) = load_config()?;
Expand All @@ -23,6 +26,10 @@ fn main() -> Result<()> {
let props = java_properties::read(File::open(".submit").wrap_err("failed to read .submit")?)
.wrap_err("failed to parse .submit")?;

if cfg.info {
return print_course_info(&props, cfg.time_format);
}

if !cfg.no_submit {
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
zip.set_comment("");
Expand Down
6 changes: 3 additions & 3 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use color_eyre::config::{HookBuilder, Theme};
use eyre::{Result, WrapErr};

use std::{
fmt::Display,
io::{self, StderrLock, Write},
};

use color_eyre::config::{HookBuilder, Theme};
use eyre::{Result, WrapErr};

pub struct State<W: Write> {
pub(crate) color: bool,
pub(crate) out: W,
Expand Down
4 changes: 2 additions & 2 deletions src/submit.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::io::Write;

use eyre::{eyre, Result, WrapErr};
use multipart::client::lazy::Multipart;

use std::io::Write;

use crate::{config::Config, state::State, warn, Props, PropsExt};

impl<W: Write> State<W> {
Expand Down

0 comments on commit acff0b9

Please sign in to comment.