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[1:] { if len(arg) == 2 { if arg[0] == '-' { flag_start = i break } } lower := strings.to_lower(arg, context.allocator) 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.eprintfln("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("-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/project.ics") fmt.println("This will save the URL 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("Depending on OS, be careful about special characters like / escaping.") 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[strings.clone(info.name, context.allocator)] = 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' among:", alias) i:=0 for key, value in aliases { fmt.eprintfln(" %d: %s -> %s...", i, key, value[:min(len(value), 64)]) i+=1 } return "" } just_fucking_make_the_directory(ics_cache_dir) download_path := fmt.tprint(ics_cache_dir, SEPARATOR, alias, ".ics", sep="") 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) }