diff options
| author | San Jacobs | 2026-01-23 17:32:47 +0100 |
|---|---|---|
| committer | San Jacobs | 2026-01-23 17:32:47 +0100 |
| commit | 10be2f44f633a2208f41f9ea2ee233caf1266627 (patch) | |
| tree | 153b7db07827c054c51d6f6e6e20ac7eb9529255 | |
| parent | 3fd7d818b53fe80fb521fd24c024f27f38fcc7bd (diff) | |
| download | statics-10be2f44f633a2208f41f9ea2ee233caf1266627.tar.gz statics-10be2f44f633a2208f41f9ea2ee233caf1266627.tar.bz2 statics-10be2f44f633a2208f41f9ea2ee233caf1266627.zip | |
New feature: Aliases live fetching ICSes from the internet!
| -rw-r--r-- | .focus-config | 45 | ||||
| -rw-r--r-- | main.odin | 315 |
2 files changed, 308 insertions, 52 deletions
diff --git a/.focus-config b/.focus-config new file mode 100644 index 0000000..71d347c --- /dev/null +++ b/.focus-config @@ -0,0 +1,45 @@ +[25] # Version number. Do not delete. + +[[workspace]] +# These directories and files will be scanned when a workspace is opened so that search etc. works. +. + +[ignore] +# Files and directories matching the following wildcards will not be loaded or descended into +# Example: +# *.js - will ignore all files with a '.js' extension +# tmp* - will ignore any files or directories which start with 'tmp' +# C:/project/dirname/** - will ignore everything under `dirname` +# C:/project/dirname/* - will ignore all files under `dirname`, but not recursively +.vs +.git +.svn +build +*.gltf +*.glb +*.PNG +lib + +[[build commands]] +# build_working_dir D:\Documents\Programming\statics +# build_working_dir C:\Users\Sander\Programming\statics +build_working_dir . +open_panel_on_build true +error_regex ^(?P<file>.*)\((?P<line>\d+):(?P<col>\d+)\) (?P<type>Error|Syntax Error): (?P<msg>.*)$ + + +auto_jump_to_error false + +#[build_and_debug] +#build_command build.bat +#key_binding Ctrl-B + +[build_direct] +build_command build.bat +key_binding Ctrl-B + + +[[settings]] + +indent_using: tabs +tab_size: 4 @@ -2,12 +2,24 @@ 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, @@ -15,6 +27,7 @@ Arg_Type :: enum { TO, IN, SUBSTRING, + ALIAS, } Arg_Flags :: bit_set[Arg_Type] @@ -35,7 +48,12 @@ Filter :: struct { 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 @@ -48,48 +66,92 @@ main :: proc() { in_filter_out : Filter substring : string - file_index_buffer : [dynamic]int + 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.temp_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) - if lower[max(0, len(lower)-4):len(lower)] == ".ics" { - append(&file_index_buffer, i) - } else { - 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-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.") - os.exit(0) - } + 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: parse_to_filter(&arg, &from_filter) @@ -114,6 +176,19 @@ main :: proc() { 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 + } } } @@ -122,12 +197,12 @@ main :: proc() { fmt.println() - for i in file_index_buffer { + for file_path, i in files_to_process { - fmt.printf("%d: ", i) - fmt.println(os.args[i]) + fmt.printf("%d: ", i+1) + fmt.println(file_path) - timeblocks, ok := importICS(os.args[i]) + timeblocks, ok := importICS(file_path) if ok { minutes : int = 0 @@ -162,7 +237,7 @@ main :: proc() { 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: %02d:%02d\n\n", + fmt.printf(" Hour count: %f\nHours & Minutes: %d:%02d\n\n", display_hour_count, display_hours, display_minutes) total_minutes += minutes } else { @@ -171,15 +246,16 @@ main :: proc() { } } - fmt.printf("\nTOTAL\n\n") - - 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: %02d:%02d\n\n", - display_hour_count, display_hours, display_minutes) - + if len(files_to_process)>0 { + fmt.printf("\nTOTAL\n\n") + + 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) } @@ -221,4 +297,139 @@ filter_maxx :: proc(filter : ^Filter) { 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) }
\ No newline at end of file |