Sublime Text
loads all files, buffers, layout, and other data back from the last time you quit. This allows you to quickly jump into action. I'd like Todool's
save format to be just as good.
Here's how my first file format looked like:
data:
stored general info about the editortree:
stored task related info like indentation, time info, string content, special content in ^1.123123123.1^
which wasnt in the example imageold read -> new write -> new read
routeNo backwards compatibility killed a lot of my save files, usually because of my negligence when adding new data to the format.
Heres a list of some file formats. JSON
like formats offer a sense of structure through scopes. A format like that makes sense for my theme and option files.
These are just plain structs that could be dumped to JSON
. I looked into alternatives to JSON
and found out about msgpack, which is pretty much JSON
in binary with a simple spec.
I implemented msgpack and all the goodies you'd want odin-msgpack. There you can read / write entire structs and not worry too much.
Option / Theme Data that does not exist in a old / new version will simply be ignored ~ Default values will stay.
A format like this does not work for a lot of structured data like a tree though. JSON
like formats would store lots of similar data like variable names to associate back.
A custom file format makes sense here, since you can tailor that towards all you need. Or you could go for a format with schemas. These allow to predefine structures that will reappear throughout your file and be written / read back the same expected way.
Interestingly Databases work via these schemas
Some applications actually use databases as an application file format.
sqlite is a C
library that you can easily use, so I did in odin-sqlite.
Here's how you work with databases
The DB does all the hard work for you. You have to conform to the way the DB works, which then allows an easy way to store data.
Working with a DB is a painful process. The constant back and forth messaging via SQL
kills usability to me. Transitioning a functioning codebase to a DB is also terrible, since you need to restructure everything. I actually tried this out in the Wheel Reinvention Jam
where I transitioned a week old codebase to using a DB. That process took almost a week... Clearly not suited for a jam or a long existing project.
What I wanted from a new file format:
Throughout the jam I looked into how milton is setup. It actually has an interesting way to deal with savefiles, that I think I've seen before but I can't recall where exactly.
It uses versioned flat structures which don't change too often. I assume it copies them directly into / out of memory, which is pretty clever.
Here is my data layout for attemping something like milton's
approach
This binary based file format achieves all the goals I stated above. For a new version you have to declare new structs or reuse previous Data*
structs. Write always uses the most recent Data*
structs.
Thank you for reading this, turned out a bit longer than expected. Feel free to share your thoughts to me
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | TaskNode :: struct { prev, next: ^TaskNode, parent: ^TaskNode, task_head, task_tail, task_jump: ^TaskNode, text_head, text_tail: ^TextNode, ... } TextNode :: struct { parent: ^TaskNode, prev, next: ^TextNode, length: u8, bytes: [BOX_CAP]byte, } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | TaskNode :: struct { parent, prev, next: ^TaskNode, head, tail: ^TaskNode, sub_head, sub_tail: ^SubNode, ... custom_calls: map[ShortcutCall]proc(), custom_draw: proc(node: ^TaskNode), } SubNode :: struct { parent: ^TaskNode, prev, next: ^SubNode, data: SubData, ... } SubData :: union { SubText, SubButton, SubToggle, SubU8, SubInt, SubF32, SubHex, SubColor, SubPadding, ... } SubText :: struct { text: string, } SubButton :: struct { call: proc(), } SubToggle :: struct { state: ^bool, } SubU8 :: struct { ptr: ^u8, clamped: bool, } SubInt :: struct { ptr: ^int, start, end: int, // range clamped: bool, } SubF32 :: struct { ptr: ^f32, start, end: f32, // range clamped: bool, } SubColor :: struct { color: ^Color, } SubHex :: struct { ptr: ^Color, clamped: bool, } SubPadding :: struct { width: int, height: int, } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | create_option_nodes :: proc(x, y: int) -> ^TaskNode { options := task_scoped(); options.rect.l = x; options.rect.t = y; options.disabled = true; sub_init(SubText { "options" }).disabled = true;; { task_scoped().disabled = true; sub_init(SubText { "visual" }).disabled = true;; { task_scoped().disabled = true; sub_init(SubText { "tab" }).disabled = true; sub_init(SubInt { ptr = &ui.tab, start = 10, end = 100 }); } { task_scoped().disabled = true; sub_init(SubText { "line_height" }).disabled = true; sub_init(SubInt { ptr = &ui.line_height, start = 20, end = 50 }); } { task_scoped().disabled = true; sub_init(SubText { "font" }).disabled = true; font := &core.fonts[.Editor]; sub_init(SubF32 { ptr = &font.temp_size, start = font.min_size, end = font.max_size }); } } return options; } |