diff options
| author | dam <dam@gudinoff> | 2024-07-09 17:38:52 +0100 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2024-07-24 12:21:59 +0100 |
| commit | b8eeb1f44b083ecc25bdaa351540beeab112af07 (patch) | |
| tree | 1d40c19bad347e6411feda0028dddadcffc20996 | |
| parent | 45aef04664df07913c9ab75a0b329a2e370a5ecc (diff) | |
| download | task-time-tracker-3.0.tar.zst task-time-tracker-3.0.zip | |
Store all user data as CSV files.v3.0
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | modules/README.md | 2 | ||||
| -rw-r--r-- | ttt.jai | 543 | ||||
| -rw-r--r-- | unused.jai | 68 |
4 files changed, 381 insertions, 240 deletions
@@ -27,12 +27,14 @@ archive view and restore them. Ever felt like your data is being held hostage? Not anymore! Import and export your tasks using the widely supported CSV text file format. -Want to be part of the last frontier? Grab the bleeding edge version 2 -which brings: sorting capabilities; better text input with UTF8 +Want to be part of the last frontier? Grab the new bleeding edge stuff: +Version 2 brings sorting capabilities; better text input with UTF8 support; possibility to archive and reset all current tasks with a single command; and allows to merge tasks with the same name. As an extra , you'll need to export and re-import your tasks due to some -database incompatibility (oh, the joy). +database incompatibility (oh, the joy). With version 3 all your data is +stored in CSV format, so that you can migrate your database one last +time (yay)! And if you don't like what you're seeing, this is your lucky day! Because you have access to the source code, you can adapt it to your diff --git a/modules/README.md b/modules/README.md index d9b5839..13179e6 100644 --- a/modules/README.md +++ b/modules/README.md @@ -3,6 +3,8 @@ jai-modules Modules for the language being developed by Thekla, Inc. +Available at https://github.com/gudinoff/jai-modules + # Saturation This module provides basic integer [saturation arithmetic](https://en.wikipedia.org/wiki/Saturation_arithmetic) procedures: `add`, `sub`, `mul`, and `div`. @@ -14,14 +14,14 @@ DEBUG :: false; #import "UTF8"; TUI :: #import "TUI"(COLOR_MODE_BITS=4); -VERSION :: "2.2"; // Use only 3 chars (to fit layouts). +VERSION :: "3.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; NUM_WEEK_DAYS :: 7; APP_FOLDER_NAME :: "task_time_tracker"; -DB_FILE_NAME :: "database.bin"; +ST_FILE_NAME :: "app_state.csv"; +DB_FILE_NAME :: "database.csv"; AR_FILE_NAME :: "archive.csv"; -DB_FILE_SIGN_STR :: "TTT:B:02"; ASSERT_NOT_NULL :: "Parameter '%' is null."; ASSERT_NOT_EMPTY :: "Parameter '%' is empty."; @@ -30,6 +30,7 @@ ASSERT_INVALID_INDEX:: "Invalid index '%'."; SECONDS_IN_MINUTE :: cast(s64)60; SECONDS_IN_HOUR :: cast(s64)60*SECONDS_IN_MINUTE; SECONDS_IN_DAY :: cast(s64)24*SECONDS_IN_HOUR; +SECONDS_IN_WEEK :: cast(s64)7*SECONDS_IN_DAY; SECONDS_IN_YEAR :: cast(s64)365*SECONDS_IN_DAY; MAX_DATABASE_TASKS :: S64_MAX; @@ -52,6 +53,7 @@ is_autosave_enabled := true; countdown_to_autosave := -1; first_day_of_week := 1; // (0-6, Sunday = 0, Monday = 1, ...) app_directory : string; +st_file_path : string; db_file_path : string; ar_file_path : string; @@ -173,9 +175,42 @@ hide_processing :: () { TUI.tui_write_string(TUI.Commands.TextMode); } +// Advance to next item in array. +advance :: inline (array: *[] $T, amount: int = 1) { + assert(amount >= 0); + assert(array.count >= amount); + array.count -= amount; + array.data += amount; +} + +// Advances to next line and returns it. +// Taken from Text_File_Handler module. +// Returns the next line and success. +consume_next_line :: (sp: *string) -> next_line: string, success: bool { + s := sp.*; + found, result, right := split_from_left(s, 10,, temporary_allocator); + + if !found { + sp.* = ""; + return s, (s.count > 0); + } + + advance(sp, result.count + 1); + + if result { + // If there's a carriage return at the end, remove it by decrementing the string's length. + if result[result.count-1] == 13 result.count -= 1; + } + + return result, true; +} + // Returns true if string to_compare is equal to any of the other passed strings, false otherwise. -is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool { - return to_compare == test_a || to_compare == test_b; +is_equal_to_any :: (to_compare :string, tests : .. string) -> bool { + for tests + if to_compare == it + return true; + return false; } // Count digits required to represent number on base. Sign is discarded. @@ -292,7 +327,7 @@ add_task :: (db: *Database, task: *Task = null) -> task: *Task, index: s64 { // If the task belongs to this database, calling array_add might invalidate the pointer // because the memory may be reallocated, thus we always use a copy of the task. - new_task := ifx task == null then .{} else <<task; + new_task := ifx task == null then .{} else task.*; array_add(*db.tasks, new_task); for * db.total_times { @@ -419,6 +454,17 @@ update_times :: (db: *Database) { active_task := get_active_task(db); if active_task == null return; + + // Performance tweak: if we're updating over more than a week, take a shortcut. + if to_seconds(stop_time - start_time) > SECONDS_IN_WEEK { + total_weeks := cast,trunc(s64)(to_seconds(stop_time - start_time) / SECONDS_IN_WEEK); + elapsed_time_per_weekday := SECONDS_IN_DAY * total_weeks; + for 0..NUM_WEEK_DAYS-1 { + active_task.times[it] += elapsed_time_per_weekday; + db.total_times[it] += elapsed_time_per_weekday; + } + start_time += seconds_to_apollo(total_weeks * SECONDS_IN_WEEK); + } start_week_day: s8; while (start_time < stop_time) { @@ -436,8 +482,8 @@ update_times :: (db: *Database) { next_day := start_of_day + #run seconds_to_apollo(SECONDS_IN_DAY); next_start := ifx next_day < stop_time then next_day else stop_time; elapsed_time := to_seconds(next_start - start_time); - active_task.times[start_week_day] += elapsed_time; - db.total_times[start_week_day] += elapsed_time; + active_task.times[start_week_day] += elapsed_time; + db.total_times[start_week_day] += elapsed_time; start_time = next_start; } @@ -447,11 +493,11 @@ update_times :: (db: *Database) { update_total_times :: (db: *Database) { assert(db != null, ASSERT_NOT_NULL, "db"); - for *total: db.total_times { <<total = 0; } + for *total: db.total_times { total.* = 0; } for task: db.tasks { for *total, index: db.total_times { - <<total = add(<<total, task.times[index]); + total.* = add(total.*, task.times[index]); } } } @@ -465,8 +511,8 @@ reset_task_times :: (db: *Database, index: s64) { update_times(db); for * db.tasks[index].times { - db.total_times[it_index] = sub(db.total_times[it_index], <<it); - <<it = 0; + db.total_times[it_index] = sub(db.total_times[it_index], it.*); + it.* = 0; } } @@ -512,67 +558,108 @@ add_task_times :: (db: *Database, index: s64, times: [NUM_WEEK_DAYS] s64) { reset_database :: (db: *Database) { assert(db != null, ASSERT_NOT_NULL, "db"); array_reset(*db.tasks); - <<db = .{}; + db.* = .{}; } -// Stores data from database into binary file. +// Stores application state. // Returns success. -store_database :: (db: Database, path: string) -> success: bool #must { - assert(xx path, ASSERT_NOT_EMPTY, "path"); +store_app_state :: (db: Database, st_path: string, db_path: string) -> success: bool #must { + assert(xx st_path, ASSERT_NOT_EMPTY, "st_path"); + assert(xx db_path, ASSERT_NOT_EMPTY, "db_path"); + + // Export database to CSV file. + if export_to_csv(db, db_path) == false { + return false; + } - // Open file. - file, open_success := file_open(path, for_writing = true); - if open_success == false return false; - defer file_close(*file); + auto_release_temp(); - file_write(*file, DB_FILE_SIGN_STR); - file_write(*file, *db, size_of(Database)); - file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); - - return true; + builder: String_Builder; + append(*builder, "key, value\n" ,, temporary_allocator); + print_to_builder(*builder, "active_idx,%\n", db.active_idx ,, temporary_allocator); + print_to_builder(*builder, "selected_idx,%\n", db.selected_idx ,, temporary_allocator); + print_to_builder(*builder, "modified_on,%\n", to_nanoseconds(db.modified_on) ,, temporary_allocator); + print_to_builder(*builder, "first_day_of_week,%\n", first_day_of_week ,, temporary_allocator); + + return write_entire_file(st_path, *builder); } -// Loads data from binary file into database. +// Loads application state. // Returns success. -load_database :: (db: *Database, path: string) -> success: bool #must { +load_app_state :: (db: *Database, st_path: string, db_path: string) -> success: bool #must { assert(db != null, ASSERT_NOT_NULL, "db"); - assert(xx path, ASSERT_NOT_EMPTY, "path"); + assert(xx st_path, ASSERT_NOT_EMPTY, "st_path"); + assert(xx db_path, ASSERT_NOT_EMPTY, "db_path"); - // Open file. - file, open_success := file_open(path); - if open_success == false then return false; - defer file_close(*file); + // Make sure the database clear. + reset_database(db); - // Validate file signature. - file_signature: [DB_FILE_SIGN_STR.count] u8; - read_success := file_read(file, *file_signature, DB_FILE_SIGN_STR.count); - if read_success == false log_error("Failed to read file signature."); - if cast(string)file_signature != DB_FILE_SIGN_STR { - log_error("Invalid file signature while loading database."); + // Load database entries before continuing to load the app state. + if import_from_csv(db, db_path) == false { return false; } - - // Read database structure. - read_success = file_read(file, db, size_of(Database)); - if read_success == false { - log_error("Failed to read database info."); + + // Set default state values. + db.modified_on = current_time_consensus(); + db.active_idx = -1; + db.selected_idx = -1; + + // Load state file. + data, success := read_entire_file(st_path); + if success == false { + log_error("Failed to read file '%'.", st_path); return false; } + defer free(data); + + // Work on a string struct copy, otherwise the free(data) will fail. + csv := data; - // Reserve database capacity for tasks. - tasks_count := db.tasks.count; - Initialize(*db.tasks); // Cleanup whatever was read from file. - array_reserve(*db.tasks, tasks_count); + // Skip header line. + consume_next_line(*csv); - // Read database tasks. - file_read(file, db.tasks.data, size_of(Task) * tasks_count); - db.tasks.count = tasks_count; + // Parse CSV lines. + CSV_SEPARATOR : u8 : #char ","; + was_modified_on_parsed := false; + while (true) { + auto_release_temp(); + + line, success := consume_next_line(*csv); + if success == false then break; + + csv_line := split(line, CSV_SEPARATOR,, temporary_allocator); + assert(csv_line.count == 2, "Invalid line '%' on file '%'.", line, st_path); + + // Parse state key and value pairs. + key := trim(csv_line[0]); + value := csv_line[1]; + + if key == { + case "active_idx"; + db.active_idx = string_to_int(value); + case "selected_idx"; + db.selected_idx = string_to_int(value); + case "modified_on"; + db.modified_on = nanoseconds_to_apollo(string_to_int(value)); + was_modified_on_parsed = true; + case "first_day_of_week"; + first_day_of_week = abs(string_to_int(value)) % NUM_WEEK_DAYS; + } + + advance(*csv_line); + } - // Make sure we are reading all the file. - buffer: u8; - success, bytes := file_read(file, *buffer, 1); - if bytes > 0 { - log_error("Unexpected content found at the end of file '%'.", path); + // Adjust selected task. + if db.selected_idx < 0 && db.tasks.count > 0 then db.selected_idx = 0; + if db.selected_idx >= db.tasks.count then db.selected_idx = db.tasks.count - 1; + + // Adjust active task. + if db.active_idx >= 0 && was_modified_on_parsed == false { + log_error("Missing 'modified_on' value on file '%': fix this by adding the 'modified_on', or removing the 'active_idx'.", st_path); + return false; + } + if db.active_idx >= db.tasks.count { + log_error("Invalid 'active_idx' '%' detected on file '%'.", db.active_idx, st_path); return false; } @@ -583,7 +670,7 @@ load_database :: (db: *Database, path: string) -> success: bool #must { // Returns success. export_to_csv :: (db: Database, path: string) -> success: bool #must { assert(xx path, ASSERT_NOT_EMPTY, "path"); - + auto_release_temp(); builder: String_Builder; @@ -601,7 +688,7 @@ export_to_csv :: (db: Database, path: string) -> success: bool #must { print_to_builder(*builder, "%,%,%,%,%,%,%,%\n", name, it.times[0], it.times[1], it.times[2], it.times[3], it.times[4], it.times[5], it.times[6]); } - + return write_entire_file(path, *builder); } @@ -611,33 +698,6 @@ import_from_csv :: (db: *Database, path: string) -> bool #must { assert(db != null, ASSERT_NOT_NULL, "db"); assert(xx path, ASSERT_NOT_EMPTY, "path"); - advance :: inline (array: *[] $T, amount: int = 1) { - assert(amount >= 0); - assert(array.count >= amount); - array.count -= amount; - array.data += amount; - } - - // Taken from Text_File_Handler module. - consume_next_line :: (sp: *string) -> string, bool { - s := << sp; - found, result, right := split_from_left(s, 10,, temporary_allocator); - - if !found { - << sp = ""; - return s, (s.count > 0); - } - - advance(sp, result.count + 1); - - if result { - // If there's a carriage return at the end, remove it by decrementing the string's length. - if result[result.count-1] == 13 result.count -= 1; - } - - return result, true; - } - data, success := read_entire_file(path); if success == false { log_error("Failed to read file '%'.", path); @@ -650,31 +710,32 @@ import_from_csv :: (db: *Database, path: string) -> bool #must { // Skip header line. consume_next_line(*csv); - + // Parse CSV lines. + CSV_SEPARATOR : u8 : #char ","; while (true) { auto_release_temp(); line, success := consume_next_line(*csv); if success == false then break; - + task: Task; - csv_values := split(line, ",",, temporary_allocator); - + csv_line := split(line, CSV_SEPARATOR,, temporary_allocator); + // Truncate and import task name. - task_name := truncate(csv_values[0], task.name.count); + task_name := truncate(csv_line[0], task.name.count); memcpy(task.name.data, task_name.data, task_name.count); - advance(*csv_values); - for csv_values + advance(*csv_line); + for csv_line task.times[it_index] = string_to_int(it); - + add_task(db, *task); } // Adjust selected task. if (db.selected_idx < 0 && db.tasks.count > 0) db.selected_idx = 0; - + return true; } @@ -1103,6 +1164,7 @@ free_memory :: () { reset_database(*archive); free(app_directory); + free(st_file_path); free(db_file_path); free(ar_file_path); } @@ -1144,7 +1206,7 @@ read_input_int :: (y: int, message: string) -> value: int, success: bool { str := read_input_string(input_pos_x, y, input_width,, temporary_allocator); - value, success := parse_int(*str); + value, success := string_to_int(str); return value, success; } @@ -1166,7 +1228,7 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { } main :: () { - + #if DEBUG { defer report_memory_leaks(); } context.logger = print_error; @@ -1205,24 +1267,31 @@ main :: () { } app_directory = join(base_path, PATH_DELIMITER, APP_FOLDER_NAME); + st_file_path = join(app_directory, PATH_DELIMITER, ST_FILE_NAME); db_file_path = join(app_directory, PATH_DELIMITER, DB_FILE_NAME); ar_file_path = join(app_directory, PATH_DELIMITER, AR_FILE_NAME); - make_directory_if_it_does_not_exist(app_directory, recursive = true); } - { // Initialize database and archive files if needed. - if (file_exists(db_file_path) == false) { - if (store_database(database, db_file_path) == false) { + { // Initialize files if needed. + if (file_exists(ar_file_path) == false) { + if (export_to_csv(archive, ar_file_path) == false) { + log_error("Failed to initialize archive."); + exit(1); + } + } + + if file_exists(db_file_path) == false { + if export_to_csv(database, db_file_path) == false { log_error("Failed to initialize database."); exit(1); } } - - if (file_exists(ar_file_path) == false) { - if (export_to_csv(archive, ar_file_path) == false) { - log_error("Failed to initialize archive."); + + if file_exists(st_file_path) == false { + if store_app_state(database, st_file_path, db_file_path) == false { + log_error("Failed to initialize app state."); exit(1); } } @@ -1230,143 +1299,143 @@ main :: () { args := get_command_line_arguments(); defer array_reset(*args); + + // Iterate over simple commands. + for 1..args.count-1 { + if is_equal_to_any(args[it], "--help", "-h") { + write_strings( + "Usage: ttt [OPTION]... [FILE]...\n", + " -i, --import [FILE] Import CSV file to database (discard first row).\n", + " -e, --export [FILE] Export database to CSV file.\n", + " -s, --start-of-week [NUMBER] Set first day of week (0=Sunday, 1=Monday...).\n", + " -n, --no-autosave Disable autosave feature (only save on exit).\n", + " -h, --help Display this help and exit.\n", + " -v, --version Output version information and exit.\n", + "\n", + "In app commands\n", + " w, W Archive duplicates and reset all tasks.\n", + " a, A Archive selected task (except if active).\n", + " r, R Restore selected task from archive.\n", + " t, T Select currently active task (if any).\n", + " d, D Duplicate selected task.\n", + " c, C Coalesce similar tasks.\n", + " n, N Create new task.\n", + " m, M Move selected task to position.\n", + " g, G Select task by position.\n", + " i, I Invert tasks order.\n", + " s, S Sort tasks by:\n", + " n name;\n", + " t total time;\n", + " 1..7 time of Nth day of week.\n", + " q, Q Save changes and exit.\n", + " F2 Rename selected task.\n", + " F5 Recalculate total times.\n", + " TAB Toggle archive view.\n", + " BACKSPACE Reset times for selected task.\n", + " DELETE Delete selected task (except if active).\n", + " SPACE, ENTER Toggle selected task as active/inactive.\n", + " 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n", + " =# sets # seconds;\n", + " -# subtracts # seconds;\n", + " # adds # seconds;\n", + " #m specifies # as minutes;\n", + " #h specifies # as hours;\n", + " #d specifies # as days;\n", + " #y specifies # as years.\n", + " UP Select task above.\n", + " DOWN Select task below.\n", + " PAGE-UP Select task 1 page above.\n", + " PAGE-DOWN Select task 1 page below.\n", + " HOME Select first/top task.\n", + " END Select last/bottom task.\n", + "\n", + "Notes\n"); + print("- All data files are stored in '%'.\n", app_directory); + print(" The app state is stored in CSV format on the '%' file.\n", ST_FILE_NAME); + print(" The database entries are stored in CSV format on the '%' file.\n", DB_FILE_NAME); + print(" The archived entries are stored in CSV format on the '%' file.\n", AR_FILE_NAME); + write_strings( + "- During intensive operations such as saving or recalculating totals times,\n", + " a diamond symbol is shown on the top left corner.\n" + ); + exit(0); + } + + if is_equal_to_any(args[it], "--version", "-v") { + print("Task Time Tracker version % \nCopyright % Daniel Martins\nLicense GPL-3.0-or-later\n", VERSION, YEAR); + exit(0); + } + + if is_equal_to_any(args[it], "--no-autosave", "-n") { + is_autosave_enabled = false; + continue; + } + + // Bypass valid commands that will be parsed later. + if is_equal_to_any(args[it], "--import", "-i", "--export", "-e", "--start-of-week", "-s") { + it += 1; + continue; + } + + log_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]); + exit(1); + } - if args.count > 1 { + // Load app state. + if load_app_state(*database, st_file_path, db_file_path) == false { + log_error("Failed to load database."); + exit(1); + } + + // Iterate over data sensitive commands. + is_exit_requested := false; + for 1..args.count-1 { - is_exit_requested := false; - for 1..args.count-1 { - if is_equal_to_any(args[it], "--help", "-h") { - write_strings( - "Usage: ttt [OPTION]... [FILE]...\n", - " -i, --import-csv [FILE] Import CSV file to database (discard first row).\n", - " -e, --export-csv [FILE] Export database to CSV file.\n", - " -s, --start-of-week [NUMBER] Set first day of week (0=Sunday, 1=Monday...).\n", - " -n, --no-autosave Disable autosave feature (only save on exit).\n", - " -h, --help Display this help and exit.\n", - " -v, --version Output version information and exit.\n", - "\n", - "In app commands\n", - " w, W Archive duplicates and reset all tasks.\n", - " a, A Archive selected task (except if active).\n", - " r, R Restore selected task from archive.\n", - " t, T Select currently active task (if any).\n", - " d, D Duplicate selected task.\n", - " c, C Coalesce similar tasks.\n", - " n, N Create new task.\n", - " m, M Move selected task to position.\n", - " g, G Select task by position.\n", - " i, I Invert tasks order.\n", - " s, S Sort tasks by:\n", - " n name;\n", - " t total time;\n", - " 1..7 time of Nth day of week.\n", - " q, Q Save changes and exit.\n", - " F2 Rename selected task.\n", - " F5 Recalculate total times.\n", - " TAB Toggle archive view.\n", - " BACKSPACE Reset times for selected task.\n", - " DELETE Delete selected task (except if active).\n", - " SPACE, ENTER Toggle selected task as active/inactive.\n", - " 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n", - " =# sets # seconds;\n", - " -# subtracts # seconds;\n", - " # adds # seconds;\n", - " #m specifies # as minutes;\n", - " #h specifies # as hours;\n", - " #d specifies # as days;\n", - " #y specifies # as years.\n", - " UP Select task above.\n", - " DOWN Select task below.\n", - " PAGE-UP Select task 1 page above.\n", - " PAGE-DOWN Select task 1 page below.\n", - " HOME Select first/top task.\n", - " END Select last/bottom task.\n", - "\n", - "Notes\n"); - print("- All data files are stored in '%'.\n", app_directory); - write_strings( - " The database tasks are stored in binary format on the 'database.bin' file.\n", - " The archived entries are stored in CSV format on the 'archive.csv' file.\n", - "- During intensive operations such as saving or recalculating totals times,\n", - " a diamond symbol is shown on the top left corner.\n" - ); - exit(0); - } - - if is_equal_to_any(args[it], "--version", "-v") { - print("Task Time Tracker version % \nCopyright % Daniel Martins\nLicense GPL-3.0-or-later\n", VERSION, YEAR); - exit(0); - } - - if is_equal_to_any(args[it], "--import-csv", "-i") { - it += 1; - if it >= args.count { - log_error("Missing CSV file path to import."); - exit(1); - } - if (load_database(*database, db_file_path) == false) { - log_error("Failed to load database during import."); - exit(1); - } - if (import_from_csv(*database, args[it]) == false) { - log_error("Failed to import CSV file."); - exit(1); - } - if (store_database(*database, db_file_path) == false) { - log_error("Failed to store database during import."); - exit(1); - } - reset_database(*database); - is_exit_requested = true; - continue; + if is_equal_to_any(args[it], "--start-of-week", "-s") { + it += 1; + if it >= args.count { + log_error("Missing number for starting day of week."); + exit(1); } - - if is_equal_to_any(args[it], "--export-csv", "-e") { - it += 1; - if it >= args.count { - log_error("Missing CSV file path to export."); - exit(1); - } - if (load_database(*database, db_file_path) == false) { - log_error("Failed to load database during export."); - exit(1); - } - if (export_to_csv(*database, args[it]) == false) { - log_error("Failed to export CSV file."); - exit(1); - } - reset_database(*database); - is_exit_requested = true; - continue; + first_day_of_week = abs(string_to_int(args[it])) % NUM_WEEK_DAYS; + continue; + } + + if is_equal_to_any(args[it], "--import", "-i") { + it += 1; + if it >= args.count { + log_error("Missing CSV file path to import."); + exit(1); } - - if is_equal_to_any(args[it], "--start-of-week", "-s") { - it += 1; - if it >= args.count { - log_error("Missing number for starting day of week."); - exit(1); - } - first_day_of_week = parse_int(*args[it]); - continue; + if import_from_csv(*database, args[it]) == false { + log_error("Failed to import CSV file."); + exit(1); } - - if is_equal_to_any(args[it], "--no-autosave", "-n") { - is_autosave_enabled = false; - continue; + if export_to_csv(*database, db_file_path) == false { + log_error("Failed to store database during import."); + exit(1); } - - log_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]); - exit(1); + is_exit_requested = true; + continue; } - if is_exit_requested { - exit(0); + if is_equal_to_any(args[it], "--export", "-e") { + it += 1; + if it >= args.count { + log_error("Missing CSV file path to export."); + exit(1); + } + if export_to_csv(*database, args[it]) == false { + log_error("Failed to export CSV file."); + exit(1); + } + is_exit_requested = true; + continue; } } - if (load_database(*database, db_file_path) == false) { - log_error("Failed to load database."); - exit(1); + if is_exit_requested { + exit(0); } initialize_user_interface(); @@ -1395,11 +1464,11 @@ main :: () { } key := TUI.get_key(INPUT_TIMEOUT_MS); - + if key == #char "q" || key == #char "Q" break; - + redraw_all = key != TUI.Keys.None; - + update_times(*database); selected_task := get_selected_task(db); @@ -1424,11 +1493,11 @@ main :: () { show_processing(); if (db == *archive) { if export_to_csv(*archive, ar_file_path) == false { - log_error("Failed to store archive during autosave."); + log_error("Failed to save archive during autosave."); } } - if store_database(database, db_file_path) == false { - log_error("Failed to store database during autosave."); + if store_app_state(database, st_file_path, db_file_path) == false { + log_error("Failed to save database during autosave."); } hide_processing(); } @@ -1809,7 +1878,7 @@ main :: () { } if (countdown_to_autosave > 0 || is_autosave_enabled == false) { while true { - if (store_database(database, db_file_path) == false) { + if (store_app_state(database, st_file_path, db_file_path) == false) { log_error("Failed to save database, retry?"); draw_error_window(); if TUI.get_key() == TUI.Keys.Escape then break; @@ -1819,6 +1888,6 @@ main :: () { } assert(TUI.reset_terminal(), "Failed to reset TUI."); - + return; } @@ -48,6 +48,74 @@ print_database :: (db: Database) { // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // +DB_FILE_SIGN_STR :: "TTT:B:02"; + +// Stores data from database into binary file. +// Returns success. +store_database :: (db: Database, path: string) -> success: bool #must { + assert(xx path, ASSERT_NOT_EMPTY, "path"); + + // Open file. + file, open_success := file_open(path, for_writing = true); + if open_success == false return false; + defer file_close(*file); + + file_write(*file, DB_FILE_SIGN_STR); + file_write(*file, *db, size_of(Database)); + file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); + + return true; +} + +// Loads data from binary file into database. +// Returns success. +load_database :: (db: *Database, path: string) -> success: bool #must { + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(xx path, ASSERT_NOT_EMPTY, "path"); + + // Open file. + file, open_success := file_open(path); + if open_success == false then return false; + defer file_close(*file); + + // Validate file signature. + file_signature: [DB_FILE_SIGN_STR.count] u8; + read_success := file_read(file, *file_signature, DB_FILE_SIGN_STR.count); + if read_success == false log_error("Failed to read file signature."); + if cast(string)file_signature != DB_FILE_SIGN_STR { + log_error("Invalid file signature while loading database."); + return false; + } + + // Read database structure. + read_success = file_read(file, db, size_of(Database)); + if read_success == false { + log_error("Failed to read database info."); + return false; + } + + // Reserve database capacity for tasks. + tasks_count := db.tasks.count; + Initialize(*db.tasks); // Cleanup whatever was read from file. + array_reserve(*db.tasks, tasks_count); + + // Read database tasks. + file_read(file, db.tasks.data, size_of(Task) * tasks_count); + db.tasks.count = tasks_count; + + // Make sure we are reading all the file. + buffer: u8; + success, bytes := file_read(file, *buffer, 1); + if bytes > 0 { + log_error("Unexpected content found at the end of file '%'.", path); + return false; + } + + return true; +} + +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // + // Average cumulative calculation. average: float64 = 0; |
