package main import "core:fmt" import "core:os" import "core:os/os2" import "core:strings" import "core:strconv" import "core:sys/windows" dayrate : f64 = 3500 aliases : map[string]string exe_dir : string data_dir : string alias_dir : string ics_cache_dir : string when ODIN_OS == .Windows { SEPARATOR :: "\\" } else { SEPARATOR :: "/" } Arg_Type :: enum { NONE, FROM, TO, IN, SUBSTRING, ALIAS, } Arg_Flags :: bit_set[Arg_Type] Range_Flags_Enum :: enum { YEAR, MONTH, DAY, HOUR, MINUTE, } Range_Flags :: bit_set[Range_Flags_Enum] Filter :: struct { ranges : Range_Flags, time : Moment, } main :: proc() { when ODIN_OS == .Windows { windows.SetConsoleOutputCP(windows.CODEPAGE.UTF8) } exe_dir, _ = os2.get_executable_directory(context.allocator) data_dir = fmt.tprint(exe_dir, "statics_data", sep = SEPARATOR) alias_dir = fmt.tprint(data_dir, "aliases", sep = SEPARATOR) ics_cache_dir = fmt.tprint(data_dir, "ics_cache", sep = SEPARATOR) arg_count := len(os.args)-1 filters : Arg_Flags = {} parsing : Arg_Type = .NONE verbose := false to_filter : Filter from_filter : Filter in_filter : Filter in_filter_out : Filter substring : string files_to_process : [dynamic]string flag_start := 0 for &arg, i in os.args { if len(arg) == 2 { if arg[0] == '-' { flag_start = i break } } lower := strings.to_lower(arg, context.allocator) // I feel like there's a platform that doesn't supply the executable // as the first argument, so I'm doing this at runtime just in case if lower[max(0, len(lower)-4):len(lower)] == ".exe" do continue file_exists := os.is_file(arg) if file_exists { if lower[max(0, len(lower)-4):len(lower)] == ".ics" { append(&files_to_process, arg) } else { fmt.eprintln("ERROR: [{}] is not .ics file!", arg) } } if !file_exists { path := get_from_alias(arg) if path != "" { append(&files_to_process, path) } else { fmt.eprintln("ERROR: [{}] is neither a file nor alias!", arg) } } } for &arg, i in os.args[flag_start:] { switch parsing { case .NONE: lower := strings.to_lower(arg, context.temp_allocator) switch lower { case "-t": parsing = .TO case "-f": parsing = .FROM case "-i": parsing = .IN case "-s": parsing = .SUBSTRING case "-v": verbose = true case "-h": fmt.println("statICS by Sander J. Skjegstad\n") fmt.println("Usage:") fmt.println("statics path/to/file.ics path/to/another_file.ics [FLAGS]") fmt.println("\nFlags:") fmt.println("\t-t To: Filter up to and NOT including a specific time.") fmt.println("\t-f From: Filter from and including a specific time.") fmt.println("\t-i In: Filter to inside a specific year, month, etc.") fmt.println("\t-s Substring: Case sensitive filter based on event names.") fmt.println("\t-a Alias: Save an ICS URL to fetch via internet.") // TODO: fmt.println("\t-l List: Lists saved aliases.") // TODO: fmt.println("\t-d Delete: Deletes an alias.") fmt.println("\t-v Verbose: Prints more info.") fmt.println("\t-h Help: Show this screen.") fmt.println("\nFilter syntax:") fmt.println("Filters currently only filter based on the event start time.") fmt.println("Specify only as much as you want to filter by.") fmt.println("YYYY-MM-DD-Hr-Mn") fmt.println("\n-t 2015-12-31-12-45 will not count anything from that point and out.") fmt.println("-i 2018-12 will only count things within December of 2018.") fmt.println("\nAlias syntax:") fmt.println("-a Project https://example.org/") fmt.println("Depending on OS, be careful about special characters like / escaping.") fmt.println("This will save the URL https://example.org under the alias Project.") fmt.println("Later you can then specify Project instead of a path/to/a_file.ics,") fmt.println("and it will fetch the ICS from the web via the saved link.") fmt.println("The URL and latest ICS from it will be stored next to the executable.") fmt.println("") os.exit(0) case "-a": parsing = .ALIAS } case .FROM: text := strings.clone(arg) parse_to_filter(text, &from_filter) filters |= {.FROM} fmt.println("FROM filter set up from", momentToString(from_filter.time)) parsing = .NONE case .TO: text := strings.clone(arg) parse_to_filter(text, &to_filter) filters |= {.TO} fmt.println("TO filter set up to", momentToString(to_filter.time)) filter_maxx(&to_filter) parsing = .NONE case .IN: text := strings.clone(arg) parse_to_filter(text, &in_filter) in_filter_out = in_filter filter_maxx(&in_filter_out) filters |= {.IN} fmt.println("IN filter set up from", momentToString(in_filter.time), "to", momentToString(in_filter_out.time)) parsing = .NONE case .SUBSTRING: substring = arg fmt.printfln("SUBSTRING filter set to \"{}\"", substring) filters |= {.SUBSTRING} parsing = .NONE case .ALIAS: @static alias_stage := 0 @static alias_name := "" switch alias_stage { case 0: // Save name alias_name = arg alias_stage = 1 case 1: save_alias(alias_name, arg) alias_stage = 0 parsing = .NONE } } } total_minutes : int = 0; fmt.println() for file_path, i in files_to_process { fmt.printf("%d: ", i+1) fmt.println(file_path) timeblocks, ok := importICS(file_path) if ok { minutes : int = 0 for each_block in timeblocks { pass := true pass_from : bool = true pass_to : bool = true pass_in : bool = true pass_substring : bool = true if .FROM in filters { pass_from = greatEq(each_block.start, from_filter.time) pass &= pass_from } if .TO in filters { pass_to = lessEq(each_block.start, to_filter.time) pass &= pass_to } if .IN in filters { pass_in = lessEq(in_filter.time, each_block.start) && lessEq(each_block.start, in_filter_out.time) pass &= pass_in } if .SUBSTRING in filters { pass_substring = strings.contains(each_block.title, substring) pass &= pass_substring } icon := pass ? "✅" : "🚫" if verbose do fmt.println(icon, timeblockToString(each_block)) if verbose && !pass_from do fmt.println(" FILTERED by from-filter.") if verbose && !pass_to do fmt.println(" FILTERED by to-filter.") if verbose && !pass_in do fmt.println(" FILTERED by in-filter.") if verbose && !pass_substring do fmt.println(" FILTERED by substring-filter.") if !pass do continue minutes += minutecount(each_block) } display_minutes : int = minutes%60 display_hours : int = (minutes-display_minutes)/60 display_hour_count : f64 = f64(minutes)/60 fmt.printf(" Hour count: %f\nHours & Minutes: %d:%02d\n\n", display_hour_count, display_hours, display_minutes) total_minutes += minutes } else { // Noffin i guess fmt.printf("\n\n") } } if len(files_to_process)>0 { fmt.println("\n------- SUM TOTAL -------") display_minutes : int = total_minutes%60 display_hours : int = (total_minutes-display_minutes)/60 display_hour_count : f64 = f64(total_minutes)/60 fmt.printf(" Hour count: %f\nHours & Minutes: %d:%02d\n\n", display_hour_count, display_hours, display_minutes) } os.exit(0) } parse_to_filter :: proc(input : string, filter : ^Filter) { input := input i : int = 0 ok : bool for substring in strings.split_iterator(&input, "-") { switch i { case 0: filter.time.year, ok = strconv.parse_int(substring) if !ok {fmt.eprintln("ERROR: Failed to parse year:", substring)} filter.ranges |= {.YEAR} case 1: filter.time.month, ok = strconv.parse_int(substring) if !ok {fmt.eprintln("ERROR: Failed to parse month:", substring)} filter.ranges |= {.MONTH} case 2: filter.time.day, ok = strconv.parse_int(substring) if !ok {fmt.eprintln("ERROR: Failed to parse day:", substring)} filter.ranges |= {.DAY} case 3: filter.time.hours, ok = strconv.parse_int(substring) if !ok {fmt.eprintln("ERROR: Failed to parse hours:", substring)} filter.ranges |= {.HOUR} case 4: filter.time.minutes, ok = strconv.parse_int(substring) if !ok {fmt.eprintln("ERROR: Failed to parse minutes:", substring)} filter.ranges |= {.MINUTE} } i += 1 if !ok do os.exit(1) } } filter_maxx :: proc(filter : ^Filter) { // Super evil retarded bad hack, but it works! if .MONTH not_in filter.ranges { filter.time.month = 99 } if .DAY not_in filter.ranges { filter.time.day = 99 } if .HOUR not_in filter.ranges { filter.time.hours = 99 } if .MINUTE not_in filter.ranges { filter.time.minutes = 99 } } save_alias :: proc(name, url : string) -> bool { data := transmute([]u8)url output_file_path := fmt.tprint(alias_dir, name, sep=SEPARATOR) just_fucking_make_the_directory(alias_dir) os.write_entire_file(output_file_path, data) fmt.printfln("Saved alias [%s -> %s] to: %s", name, url, output_file_path) return true } // Returns false if no aliases exist load_aliases :: proc() -> bool { w := os2.walker_create(alias_dir) defer os2.walker_destroy(&w) found_alias := false for info in os2.walker_walk(&w) { if path, err := os2.walker_error(&w); err != nil { fmt.eprintfln("ERROR: Failed to walk %s: %s", path, err) continue } file_data, err := os2.read_entire_file_from_path(info.fullpath, allocator=context.allocator) if err != nil { fmt.eprintln("ERROR: Failed to load data from %s: %s", info.fullpath, err) } else { found_alias = true aliases[info.name] = string(file_data) } } return found_alias } /* This will return a path to a freshly downloaded ICS from an alias if it exists. If the alias doesn't exist, the string will be empty. */ get_from_alias :: proc(alias : string) -> string { @static loaded := false if !loaded { aliases_exist := load_aliases() if !aliases_exist { return "" } loaded = true } url, ok := aliases[alias] if !ok { fmt.eprintfln("ERROR: Could not find alias: %s", alias) return "" } just_fucking_make_the_directory(ics_cache_dir) download_path := fmt.tprint(ics_cache_dir, alias, sep=SEPARATOR) dl_ok := download_file(url, download_path) if !dl_ok { fmt.eprintfln("ERROR: Failed to download '%s' from: %s", alias, url) return "" } return download_path } import "core:path/filepath" // RECURSIVELY CREATES ANY DIRECTORY LIKE GOD INTENDED just_fucking_make_the_directory :: proc(path : string) -> (directory_now_exists : bool) { path_clean := filepath.clean(path) if os.is_dir(path_clean) { return true } // Get parent directory parent := filepath.dir(path_clean) // If parent doesn't exist, create it recursively if parent != path_clean && !os.is_dir(parent) { ok := just_fucking_make_the_directory(parent) if !ok { return false } } err := os.make_directory(path_clean, 0o755) return err == os.ERROR_NONE } exec :: proc(command: ^[]string) -> bool { desc := os2.Process_Desc{ command = command^, // Inherit parent's print pipes stdout = os2.stdout, stderr = os2.stderr, stdin = os2.stdin, } process, err := os2.process_start(desc) if err != nil { fmt.printf("Failed to start process: %v\n", err) return false } defer { if close_err := os2.process_close(process); close_err != nil { fmt.printf("Failed to close process: %v\n", close_err) } } state, wait_err := os2.process_wait(process) if wait_err != nil { fmt.printf("Error waiting for process: %v\n", wait_err) return false } return state.success && state.exit_code == 0 } // TODO: Don't call out to the system's curl, ship my own download_file :: proc(url: string, output_path: string) -> (ok: bool) { command : []string = {"curl", "-L", "-s", "-o", output_path, url} exec(&command) return os.exists(output_path) }