Logo

index : track

---

  • summary
  • about
  • tree
  • log
  • branches
<< path: root/public/track.git/html/src/main.jai blob: 96ae34b3d70f988c95dba7c859583873be7be7d0 [raw] [clear marker]

        
0/*
1 * MIT License
2 *
3 * Copyright (c) 2026 dev@ptrace.dev
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in all
13 * copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 * SOFTWARE.
22 *
23 */
24
25/** +Version: 1.2.0 */
26
27#import "Basic"()(
28 MEMORY_DEBUGGER = MEMORY_DEBUGGER_ENABLED
29);
30#import "File";
31#import "File_Utilities";
32#import "String";
33#import "Sort";
34#import "POSIX";
35#import "Math";
36
37#import "termcolors";
38#import "stringpad";
39Re :: #import "uniform";
40
41#load "parse.jai";
42#load "display.jai";
43
44
45
46/** Limitations: This pattern won't catch keywords inside multiline
47 comments, that have other paragraphs in front on them. Like this:
48
49 /*
50 My multiline comment that explains something.
51
52 TODO(me, 10): Check if this `TODO` is reachable!
53 */
54
55 Spoiler: This todo comment is not reachable.
56
57 It wouldn't be a problem to catch this, but this would also catch
58 stuff, that probably isn't inside a comment and set a higher risk
59 to a recursive bomb.
60*/
61
62
63Filter :: enum_flags u8 {
64 NONE :: 0x1;
65 TODO;
66 NOTE;
67 CONTINUE;
68 IDEA;
69}
70
71
72main :: () {
73 #if MEMORY_DEBUGGER_ENABLED defer report_memory_leaks();
74
75 cmd_args := get_command_line_arguments();
76 defer this_allocation_is_not_a_leak(cmd_args.data);
77
78 user_paths: [..]string;
79 defer this_allocation_is_not_a_leak(user_paths.data);
80
81 visitor: Visitor;
82 defer this_allocation_is_not_a_leak(visitor.paths.data);
83
84 init_pattern();
85 termcolors_init_allocator_and_context();
86
87 success := args_read(cmd_args, *user_paths);
88 if !success return;
89
90 if args.help {
91 print(HEEEEEELP);
92 return;
93 }
94
95 if !user_paths {
96 files_collect(".", *visitor);
97 } else {
98 for user_paths files_collect(it, *visitor);
99 }
100
101 if !visitor.paths {
102 print("No results\n");
103 return;
104 };
105
106 had_updates := false;
107
108 for visitor.paths {
109 is_updated, todo, note, idea, cont, other := update(it);
110 had_updates |= is_updated;
111
112 if is_updated {
113 display(todo, note, idea, cont, other, path_filename(it));
114 }
115
116 termcolors_reset_pool(true);
117 reset_temporary_storage();
118 }
119
120 if !had_updates print("No results\n");
121}
122
123set_filter :: () -> Filter {
124 filter: Filter;
125
126 if args.filter_todo then filter |= .TODO;
127 if args.filter_note then filter |= .NOTE;
128 if args.filter_continue then filter |= .CONTINUE;
129 if args.filter_idea then filter |= .IDEA;
130
131 if filter == 0 {
132 filter |= .TODO;
133 filter |= .NOTE;
134 filter |= .CONTINUE;
135 filter |= .IDEA;
136 }
137
138 return filter;
139}
140
141
142#scope_file
143
144
145args: Args;
146
147
148HEEEEEELP :: #string STR_END
149Displays TODO, NOTE, IDEA or CONTINUE in files.
150
151Usage:
152 track [dir or files] [options]
153
154Options:
155 -h Show (this) help menu
156 -r Recursive search
157 -o Omit 'other' results
158 -i Stops ignoring dotfiles dotdirs
159 -sl Follows symlinks
160 -sa Sorts by author instead by priority
161
162Filters by:
163 -ft Todo
164 -fn Note
165 -fc Continue
166 -fi Idea
167
168 -err-io (Debug): shows IO errors
169
170 -microslop-cringe Normalizes lines endings
171
172STR_END;
173
174
175Args :: struct {
176 help := false;
177 recursive := false;
178 omit_other := false;
179 ignore_dot := false;
180 follow_symlinks := false;
181 microslop_cringe := false;
182 sort_by_author := false;
183 show_io_errors := false;
184
185 // TODO: This is garbage
186 filter_todo := false;
187 filter_note := false;
188 filter_continue := false;
189 filter_idea := false;
190}
191
192// Letting this struct, in case I want to extend it
193Visitor :: struct {
194 paths: [..]string;
195}
196
197
198args_read :: (cmd_args: []string, user_paths: *[..]string) -> success: bool {
199
200 find :: (arg: *bool, patterns: ..string) #expand {
201 for patterns {
202 if array_find(`cmd_args, it) {
203 arg.* = true;
204 array_unordered_remove_by_value(*`cmd_args, it);
205 }
206 }
207 }
208
209 find(*args.help, "-h", "--help", "-help", "help");
210 find(*args.recursive, "-r");
211 find(*args.omit_other, "-o");
212 find(*args.ignore_dot, "-i");
213 find(*args.follow_symlinks, "-sl");
214 find(*args.microslop_cringe, "-microslop-cringe");
215 find(*args.sort_by_author, "-sa");
216 find(*args.show_io_errors, "-err-io");
217 find(*args.filter_todo, "-ft");
218 find(*args.filter_note, "-fn");
219 find(*args.filter_continue, "-fc");
220 find(*args.filter_idea, "-fi");
221
222 if cmd_args.count < 2 return true;
223 for i: 1..cmd_args.count-1 array_add(user_paths, cmd_args[i]);
224
225 return true;
226}
227
228update :: (path: string) -> (
229 is_updated: bool,
230 todo: [..]Token,
231 note: [..]Token,
232 idea: [..]Token,
233 cont: [..]Token,
234 other: [..]Token
235) {
236 does_array_has_data :: (arr: []$T) #expand {
237 `is_updated |= cast(bool, arr);
238 }
239
240
241 dummy: [..]Token;
242 dummy.allocator = temp;
243
244 source, f_success := read_entire_file(path, log_errors = args.show_io_errors);
245 defer free(source);
246
247 if !f_success {
248 // TODO: Kinda cringe, I have to come up with a way better idea
249 return false, dummy, dummy, dummy, dummy, dummy;
250 }
251
252 if args.microslop_cringe then normalize_line_endings(*source);
253 has_match, matches := matches_collect(source,, temp);
254
255 if !has_match {
256 return false, dummy, dummy, dummy, dummy, dummy;
257 }
258
259 tokens := matches_process(source, matches,, temp);
260 tokens_sorted := sort_tokens(tokens);
261 todo, note, idea, cont, other := filter_tokens(tokens_sorted,, temp);
262
263 is_updated := false;
264 does_array_has_data(todo);
265 does_array_has_data(note);
266 does_array_has_data(idea);
267 does_array_has_data(cont);
268 does_array_has_data(other);
269
270 // TODO: This is cringe too
271 return is_updated, todo, note, idea, cont, other;
272}
273
274filter_tokens :: (tokens: []Token)
275 -> (todo: [..]Token, note: [..]Token, idea: [..]Token, cont: [..]Token, other: [..]Token)
276{
277 should_add :: (expected: Filter, arr: *[..]$T) #expand {
278 if `filter & expected then array_add(arr, `it);
279 }
280
281 filter := set_filter();
282 todo, note, idea, cont, other: [..]Token;
283
284 for tokens {
285 if #complete it.identifier == {
286 case .TODO; should_add(.TODO, *todo);
287 case .NOTE; should_add(.NOTE, *note);
288 case .IDEA; should_add(.IDEA, *idea);
289 case .CONTINUE; should_add(.CONTINUE, *cont);
290 case .NONE;
291 if !args.omit_other then array_add(*other, it);
292 }
293 }
294
295 return todo, note, idea, cont, other;
296}
297
298sort_tokens :: (tokens: [..]Token) -> []Token {
299 if args.sort_by_author {
300 return quick_sort(tokens, compare_author);
301 } else {
302 return quick_sort(tokens, compare_priority);
303 }
304}
305
306compare_author :: (a: Token, b: Token) -> s64 {
307 return compare_strings(xx a.author, xx b.author);
308}
309
310compare_priority :: (a: Token, b: Token) -> s64 {
311 return compare_floats(xx b.priority, xx a.priority);
312}
313
314files_collect :: (path: string, visitor: *Visitor) {
315
316 add_if_file :: (path: string) #expand {
317 is_a_file, success := is_file(path);
318
319 if !success {
320 log_error("Not a valid path: %", path);
321 `return;
322 }
323
324 if is_a_file {
325 array_add(*`visitor.paths, path);
326 `return;
327 }
328 }
329
330 collect_files :: (using info: *File_Visit_Info, visitor: *Visitor) {
331 if args.ignore_dot && starts_with(short_name, ".") {
332 descend_into_directory = false;
333 return;
334 }
335
336 if had_error then return;
337 add_if_file(full_name);
338 }
339
340 path_trimmed := trim_right(path, "/");
341
342 if path != "." {
343 add_if_file(path_trimmed);
344 }
345
346 complete := visit_files(
347 path_trimmed,
348 recursive = args.recursive,
349 visitor,
350 collect_files,
351 visit_files = true,
352 visit_directories = true,
353 visit_symlinks = args.follow_symlinks
354 );
355
356 if !complete {
357 log_error("[Warning]: Some error occured while traversing the file tree");
358 }
359}
360
361is_file :: (path: string) -> bool, success: bool {
362 stats: stat_t;
363
364 p := to_c_string(path);
365 ret := stat(p, *stats);
366 this_allocation_is_not_a_leak(p);
367
368 if ret != 0 return false, false;
369 return S_ISREG(stats.st_mode), true;
370}
371
372
Copyright 2026  E766CB298A6D1E64 | Git-Thing heavily inspired by cgit