aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSan Jacobs2026-01-23 17:32:47 +0100
committerSan Jacobs2026-01-23 17:32:47 +0100
commit10be2f44f633a2208f41f9ea2ee233caf1266627 (patch)
tree153b7db07827c054c51d6f6e6e20ac7eb9529255
parent3fd7d818b53fe80fb521fd24c024f27f38fcc7bd (diff)
downloadstatics-10be2f44f633a2208f41f9ea2ee233caf1266627.tar.gz
statics-10be2f44f633a2208f41f9ea2ee233caf1266627.tar.bz2
statics-10be2f44f633a2208f41f9ea2ee233caf1266627.zip
New feature: Aliases live fetching ICSes from the internet!
-rw-r--r--.focus-config45
-rw-r--r--main.odin315
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
diff --git a/main.odin b/main.odin
index 0d57e34..f269a70 100644
--- a/main.odin
+++ b/main.odin
@@ -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