aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--modules/README.md2
-rw-r--r--ttt.jai543
-rw-r--r--unused.jai68
4 files changed, 381 insertions, 240 deletions
diff --git a/README.md b/README.md
index 6167659..d0c9502 100644
--- a/README.md
+++ b/README.md
@@ -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`.
diff --git a/ttt.jai b/ttt.jai
index ca78b4b..2bbc90d 100644
--- a/ttt.jai
+++ b/ttt.jai
@@ -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;
}
diff --git a/unused.jai b/unused.jai
index 1f71b37..da8b41d 100644
--- a/unused.jai
+++ b/unused.jai
@@ -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;