feat: watch: try pager mode..
This commit is contained in:
@@ -25,7 +25,7 @@ stdenv.mkDerivation {
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "hectic";
|
||||
description = "hmpl";
|
||||
license = lib.licenses.mit;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,154 +66,116 @@ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json *
|
||||
}
|
||||
}
|
||||
|
||||
// CREATE OR REPLACE FUNCTION common.render_template_loop_blocks(result TEXT, context JSONB)
|
||||
// RETURNS TEXT LANGUAGE plpgsql AS $$
|
||||
// DECLARE
|
||||
// loop_start INT;
|
||||
// key_end INT;
|
||||
// loop_end INT;
|
||||
// loop_key TEXT;
|
||||
// block TEXT;
|
||||
// rendered_block TEXT;
|
||||
// arr JSONB;
|
||||
// item JSONB;
|
||||
// item_text TEXT;
|
||||
// BEGIN
|
||||
// LOOP
|
||||
// loop_start := strpos(result, '{{#');
|
||||
// EXIT WHEN loop_start = 0; -- Exit if no loop start found.
|
||||
//
|
||||
// -- Locate the end of the loop key marker.
|
||||
// key_end := strpos(result, '}}', loop_start);
|
||||
// IF key_end = 0 THEN
|
||||
// RAISE EXCEPTION 'Malformed template: missing closing braces for loop start';
|
||||
// END IF;
|
||||
//
|
||||
// -- Extract the key used for the loop.
|
||||
// loop_key := substr_cloneing(result from loop_start + 3 for key_end - loop_start - 3);
|
||||
//
|
||||
// RAISE DEBUG 'loop key %', loop_key;
|
||||
//
|
||||
// -- Find the matching loop end marker for this key.
|
||||
// loop_end := strpos(result, '{{/#' || loop_key || '}}', key_end);
|
||||
// IF loop_end = 0 THEN
|
||||
// RAISE EXCEPTION 'Malformed template: missing loop end for key %', loop_key;
|
||||
// END IF;
|
||||
//
|
||||
// -- Extract the inner block of the loop.
|
||||
// block := substr_cloneing(result from key_end + 2 for loop_end - key_end - 2);
|
||||
//
|
||||
// -- Retrieve the JSON array from the context for the loop key.
|
||||
// arr := eval_value(context, loop_key);
|
||||
// rendered_block := '';
|
||||
//
|
||||
// -- If an array is found, iterate over each element.
|
||||
// IF arr IS NOT NULL AND jsonb_typeof(arr) = 'array' THEN
|
||||
// FOR item IN SELECT * FROM jsonb_array_elements(arr) LOOP
|
||||
// item_text := block; -- Begin with the raw block.
|
||||
// IF jsonb_typeof(item) != 'object' THEN
|
||||
// -- Replace interpolation for primitive values.
|
||||
// item_text := replace(item_text, '{{.}}', item::text);
|
||||
// ELSE
|
||||
// -- For object values, iterate over each key/value.
|
||||
// item_text := render_template_interpolations(item_text, item, '.'::CHAR(1));
|
||||
// item_text := render_template_conditions(item_text, item, '.');
|
||||
// END IF;
|
||||
// rendered_block := rendered_block || item_text;
|
||||
// END LOOP;
|
||||
// END IF;
|
||||
//
|
||||
// -- Replace the entire loop block in the result with the rendered content.
|
||||
// result := substr_cloneing(result from 1 for loop_start - 1)
|
||||
// || rendered_block
|
||||
// || substr_cloneing(result from loop_end + char_length('{{/#' || loop_key || '}}'));
|
||||
// END LOOP;
|
||||
//
|
||||
// RETURN result;
|
||||
// END $$;
|
||||
|
||||
// {{#array_key}}
|
||||
// {{item#array}}...{{/array}}
|
||||
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern) {
|
||||
raise_debug("hmpl_render_section_tags(%p, %s, <optimized>, %s, %s, %s)", arena, *text_ptr, prefix_start, prefix_end, separator_pattern);
|
||||
|
||||
// Create search patterns
|
||||
char start_pattern[32];
|
||||
snprintf(start_pattern, sizeof(start_pattern), "{{%s", prefix_start);
|
||||
int start_pattern_length = strlen(start_pattern);
|
||||
Slice start_slice = slice_create(char, start_pattern, strlen(start_pattern), 0, strlen(start_pattern));
|
||||
|
||||
// TODO: rename close_tag_start_pattern
|
||||
char end_pattern[32];
|
||||
snprintf(end_pattern, sizeof(end_pattern), "{{%s", prefix_end);
|
||||
int end_pattern_length = strlen(end_pattern);
|
||||
|
||||
int separator_pattern_length = strlen(separator_pattern);
|
||||
if (!separator_pattern || separator_pattern_length == 0) {
|
||||
// Create a mutable copy of separator_pattern
|
||||
char separator_copy[32];
|
||||
strncpy(separator_copy, separator_pattern, sizeof(separator_copy) - 1);
|
||||
separator_copy[sizeof(separator_copy) - 1] = '\0';
|
||||
Slice separator_slice = slice_create(char, separator_copy, strlen(separator_copy), 0, strlen(separator_copy));
|
||||
if (separator_slice.len == 0) {
|
||||
raise_exception("Unexpected usage: separator pattern cannot be empty");
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
// Create slice for the text
|
||||
Slice text_slice = slice_create(char, *text_ptr, strlen(*text_ptr), 0, strlen(*text_ptr));
|
||||
size_t offset = 0;
|
||||
|
||||
while (1) {
|
||||
char *current_text = *text_ptr;
|
||||
char *opening_tag_start = strstr(current_text + offset, start_pattern);
|
||||
if (!opening_tag_start)
|
||||
break;
|
||||
int start_index = opening_tag_start - current_text;
|
||||
int relative_key_start = start_index + start_pattern_length;
|
||||
// Find tag start
|
||||
char *text_data = (char*)text_slice.data;
|
||||
char *opening_tag_start = strstr(text_data + offset, (char*)start_slice.data);
|
||||
if (!opening_tag_start) break;
|
||||
|
||||
char *opening_tag_separator = strstr(opening_tag_start, separator_pattern);
|
||||
if (!opening_tag_start) {
|
||||
raise_exception("Malformed template: missing separator for section tag or not specifiet name for element");
|
||||
// Create slice for separator search
|
||||
size_t start_index = opening_tag_start - text_data;
|
||||
Slice remaining_slice = slice_subslice(text_slice, start_index, text_slice.len - start_index);
|
||||
|
||||
// Find separator
|
||||
char *opening_tag_separator = strstr((char*)remaining_slice.data, (char*)separator_slice.data);
|
||||
if (!opening_tag_separator) {
|
||||
raise_exception("Malformed template: missing separator for section tag or not specified name for element");
|
||||
exit(1);
|
||||
}
|
||||
int separator_index = opening_tag_separator - current_text;
|
||||
int element_name_start = separator_index + separator_pattern_length;
|
||||
|
||||
char *opening_tag_end = strstr(opening_tag_separator, "}}");
|
||||
// Extract element name (now before separator)
|
||||
size_t separator_index = opening_tag_separator - (char*)remaining_slice.data;
|
||||
size_t element_name_start = start_slice.len;
|
||||
size_t element_name_length = separator_index;
|
||||
|
||||
char *element_name = arena_alloc(arena, element_name_length + 1);
|
||||
substr_clone((char*)remaining_slice.data, element_name, element_name_start, element_name_length);
|
||||
element_name[element_name_length] = '\0';
|
||||
|
||||
// Find closing braces
|
||||
Slice after_separator = slice_subslice(remaining_slice, separator_index + separator_slice.len,
|
||||
remaining_slice.len - separator_index - separator_slice.len);
|
||||
char *opening_tag_end = strstr((char*)after_separator.data, "}}");
|
||||
if (!opening_tag_end) {
|
||||
raise_exception("Malformed template: missing closing braces for section tag");
|
||||
exit(1);
|
||||
}
|
||||
assert((size_t)opening_tag_end > (size_t)opening_tag_separator);
|
||||
assert((size_t)opening_tag_separator > (size_t)opening_tag_start);
|
||||
|
||||
int key_length = (opening_tag_separator - current_text) - relative_key_start;
|
||||
assert(key_length > 0);
|
||||
|
||||
// Extract key (now after separator)
|
||||
size_t key_start = 0;
|
||||
size_t key_length = opening_tag_end - (char*)after_separator.data;
|
||||
char *key = arena_alloc(arena, key_length + 1);
|
||||
substr_clone(current_text, key, relative_key_start, key_length);
|
||||
substr_clone((char*)after_separator.data, key, key_start, key_length);
|
||||
key[key_length] = '\0';
|
||||
|
||||
int element_name_length = (opening_tag_end - current_text) - element_name_start;
|
||||
assert(element_name_length > 0);
|
||||
// Create pattern for closing tag
|
||||
char *close_tag_pattern = arena_alloc(arena, start_slice.len + key_length + 3); // +3 for "{{" and "}}"
|
||||
snprintf(close_tag_pattern, start_slice.len + key_length + 3,
|
||||
"{{%s%s}}", prefix_end, key);
|
||||
|
||||
char *element_name = arena_alloc(arena, element_name_length + 1);
|
||||
substr_clone(current_text, element_name, element_name_start, element_name_length);
|
||||
// Find closing tag
|
||||
size_t after_opening_end = (opening_tag_end - (char*)after_separator.data) + 2;
|
||||
Slice after_opening_slice = slice_subslice(after_separator, after_opening_end,
|
||||
after_separator.len - after_opening_end);
|
||||
|
||||
int close_tag_patern_length = start_pattern_length + key_length + end_pattern_length;
|
||||
char *close_tag_patern = arena_alloc(arena, close_tag_patern_length + 1);
|
||||
snprintf(close_tag_patern, sizeof(*close_tag_patern), "%s%s%s", start_pattern, key, end_pattern);
|
||||
// Find the exact closing tag by checking for complete tag pattern
|
||||
char *close_tag = NULL;
|
||||
char *search_start = (char*)after_opening_slice.data;
|
||||
while ((search_start = strstr(search_start, "{{")) != NULL) {
|
||||
if (strncmp(search_start, close_tag_pattern, strlen(close_tag_pattern)) == 0) {
|
||||
close_tag = search_start;
|
||||
break;
|
||||
}
|
||||
search_start += 2; // Move past the "{{" we found
|
||||
}
|
||||
|
||||
char *close_tag = strstr(opening_tag_end + offset + 1, close_tag_patern);
|
||||
if (!close_tag) {
|
||||
raise_exception("Malformed template: missing loop end for key %s", key);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Get array from context
|
||||
Json *arr = eval_object(arena, context, key);
|
||||
|
||||
if (arr && arr->type == JSON_ARRAY) {
|
||||
// Count array elements
|
||||
size_t elem_count = 0;
|
||||
for (Json *e = arr->child; e; e = e->next) elem_count++;
|
||||
|
||||
// Allocate memory for replacement
|
||||
char *replacement = arena_alloc(arena, MEM_KiB * elem_count);
|
||||
size_t offset = 0;
|
||||
size_t replacement_offset = 0;
|
||||
|
||||
char *block_buff = arena_alloc(arena, MEM_KiB);
|
||||
size_t relative_block_start = (size_t)opening_tag_end + 2 - (size_t)current_text;
|
||||
raise_trace("relative_block_start: %p = %p - 2 - %p", opening_tag_end, current_text);
|
||||
size_t block_len = (size_t)opening_tag_end - (size_t)close_tag - 2;
|
||||
raise_trace("block_len %p = %p - %p - 2", block_len, opening_tag_end, close_tag);
|
||||
assert(block_len > 0);
|
||||
substr_clone(current_text, block_buff, relative_block_start, block_len);
|
||||
// Extract template block
|
||||
size_t block_start = after_opening_end;
|
||||
size_t block_length = (close_tag - (char*)after_opening_slice.data);
|
||||
char *block_buff = arena_alloc(arena, block_length + 1);
|
||||
substr_clone((char*)after_opening_slice.data, block_buff, block_start, block_length);
|
||||
block_buff[block_length] = '\0';
|
||||
|
||||
// Process each array element
|
||||
for (Json *elem = arr->child; elem; elem = elem->next) {
|
||||
char *block = arena_strdup(arena, block_buff);
|
||||
|
||||
@@ -224,20 +186,26 @@ void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, cons
|
||||
raise_trace("block after: %s", block);
|
||||
|
||||
size_t block_len = strlen(block);
|
||||
memcpy(replacement + offset, block, block_len);
|
||||
offset += block_len;
|
||||
memcpy(replacement + replacement_offset, block, block_len);
|
||||
replacement_offset += block_len;
|
||||
}
|
||||
|
||||
replacement[offset] = '\0';
|
||||
replacement[replacement_offset] = '\0';
|
||||
raise_trace("replacement: %s", replacement);
|
||||
|
||||
char *new_text = arena_repstr(arena, current_text,
|
||||
(size_t)opening_tag_start - 1,
|
||||
close_tag + close_tag_patern_length - opening_tag_start + 2,
|
||||
replacement);
|
||||
// Calculate replacement positions
|
||||
size_t replace_start = start_index;
|
||||
size_t replace_length = (close_tag - (char*)after_opening_slice.data) +
|
||||
start_slice.len + key_length + 2;
|
||||
|
||||
// Perform replacement
|
||||
char *new_text = arena_repstr(arena, (char*)text_slice.data, replace_start, replace_length, replacement);
|
||||
*text_ptr = new_text;
|
||||
|
||||
// Update text slice
|
||||
text_slice = slice_create(char, new_text, strlen(new_text), 0, strlen(new_text));
|
||||
}
|
||||
|
||||
offset = start_index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Usage: make.sh [build|check] [--norun] [--debug] [--color]
|
||||
# Options:
|
||||
# build Build the library and app (default if no mode is provided).
|
||||
# watch Build the library and app and watch for changes.
|
||||
# run Build and run the app.
|
||||
# check Build tests; runs them unless --norun is specified.
|
||||
# --norun (check only) Build tests but do not run them.
|
||||
# --debug Build with -O0 (debug mode).
|
||||
@@ -9,7 +11,7 @@
|
||||
# help, --help Show this help message.
|
||||
|
||||
check_dependencies() {
|
||||
for dep in cc ar; do
|
||||
for dep in cc ar entr pager; do
|
||||
if ! command -v "$dep" >/dev/null 2>&1; then
|
||||
echo "Error: Required dependency '$dep' not found." >&2
|
||||
exit 1
|
||||
@@ -22,6 +24,8 @@ print_help() {
|
||||
cat <<EOF
|
||||
Usage: $0 [build|check] [--norun] [--debug] [--color]
|
||||
build Build the library and app (default).
|
||||
watch Build the library and app and watch for changes.
|
||||
run Build and run the app.
|
||||
check Build tests; runs them unless --norun is specified.
|
||||
--norun (check only) Build tests but do not run them.
|
||||
--debug Build with debug flags (-O0).
|
||||
@@ -74,8 +78,7 @@ if [ -n "$COLOR_FLAG" ]; then
|
||||
CFLAGS="$CFLAGS $COLOR_FLAG"
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
build)
|
||||
build() {
|
||||
mkdir -p target
|
||||
echo "# Build library"
|
||||
# shellcheck disable=SC2086
|
||||
@@ -85,10 +88,20 @@ case "$MODE" in
|
||||
echo "# Build app"
|
||||
# shellcheck disable=SC2086
|
||||
cc $CFLAGS $OPTFLAGS main.c -Ltarget -lhmpl $LDFLAGS -o target/hmpl
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
watch)
|
||||
entr -r sh ./make.sh build | pager
|
||||
;;
|
||||
build)
|
||||
build
|
||||
;;
|
||||
run)
|
||||
build && ./target/hmpl
|
||||
;;
|
||||
check)
|
||||
mkdir -p target/test
|
||||
export LOG_LEVEL=TRACE
|
||||
for test_file in test/*.c; do
|
||||
exe="target/test/$(basename "${test_file%.c}")"
|
||||
# shellcheck disable=SC2086
|
||||
|
||||
6063
package/c/hmpl/test.log
Normal file
6063
package/c/hmpl/test.log
Normal file
File diff suppressed because it is too large
Load Diff
@@ -101,7 +101,7 @@
|
||||
"}"
|
||||
|
||||
#define TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE \
|
||||
"{{#array element}}" \
|
||||
"{{#element array}}" \
|
||||
" {{element.field.subfield}}" \
|
||||
"{{/array}}"
|
||||
|
||||
@@ -110,78 +110,130 @@
|
||||
"value2" \
|
||||
"value3"
|
||||
|
||||
#define TEST_DATA_COMPLEX_SECTION_ITERATION_CONTEXT \
|
||||
"{" \
|
||||
" \"users\": [" \
|
||||
" { \"name\": \"John\", \"age\": 30 }," \
|
||||
" { \"name\": \"Jane\", \"age\": 25 }" \
|
||||
" ]" \
|
||||
"}"
|
||||
|
||||
#define TEST_DATA_COMPLEX_SECTION_ITERATION_TEMPLATE \
|
||||
"{{#user users}}" \
|
||||
" Name: {{user.name}}, Age: {{user.age}}\n" \
|
||||
"{{/users}}"
|
||||
|
||||
#define TEST_DATA_COMPLEX_SECTION_ITERATION_RESULT \
|
||||
" Name: John, Age: 30\n" \
|
||||
" Name: Jane, Age: 25\n"
|
||||
|
||||
void test_eval_single_level_key(Arena *arena) {
|
||||
raise_notice("Testing single level key evaluation");
|
||||
const char *context_text = arena_strdup(arena, "{\"name\": \"world\"}");
|
||||
Json *context = json_parse(arena, &context_text);
|
||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||
|
||||
char *result = eval_string(arena, context, "name");
|
||||
raise_debug("eval result: %s", result);
|
||||
raise_notice("Context: %s", json_to_string(arena, context));
|
||||
raise_notice("Query: name");
|
||||
raise_notice("Result: %s", result);
|
||||
assert(result && strcmp(result, "world") == 0);
|
||||
}
|
||||
|
||||
void test_eval_nested_key(Arena *arena) {
|
||||
raise_notice("Testing nested key evaluation");
|
||||
const char *context_text = arena_strdup(arena, "{\"person\": {\"name\": \"Alice\"}}");
|
||||
Json *context = json_parse(arena, &context_text);
|
||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||
|
||||
char *result = eval_string(arena, context, "person.name");
|
||||
raise_notice("context: %s, eval result: %s", json_to_string(arena, context), result);
|
||||
raise_notice("Context: %s", json_to_string(arena, context));
|
||||
raise_notice("Query: person.name");
|
||||
raise_notice("Result: %s", result);
|
||||
assert(result && strcmp(result, "Alice") == 0);
|
||||
}
|
||||
|
||||
void test_render_interpolation_tags(Arena *arena) {
|
||||
raise_trace("test_render_interpolation_tags(arena)");
|
||||
raise_notice("Testing interpolation tags without prefix");
|
||||
const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_CONTEXT);
|
||||
Json *context = json_parse(arena, &context_text);
|
||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||
|
||||
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_TEMPLATE);
|
||||
raise_notice("Template:\n%s", text);
|
||||
raise_notice("Context: %s", json_to_string(arena, context));
|
||||
|
||||
hmpl_render_interpolation_tags(arena, &text, context, "");
|
||||
raise_trace("text: %s", text);
|
||||
raise_notice("Result:\n%s", text);
|
||||
assert(strcmp(text, TEST_DATA_INTERPOLATION_RESULT) == 0);
|
||||
}
|
||||
|
||||
void test_render_interpolation_tags_with_prefix(Arena *arena) {
|
||||
raise_notice("Testing interpolation tags with prefix");
|
||||
const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_CONTEXT);
|
||||
Json *context = json_parse(arena, &context_text);
|
||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||
|
||||
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_TEMPLATE);
|
||||
raise_notice("Template:\n%s", text);
|
||||
raise_notice("Context: %s", json_to_string(arena, context));
|
||||
|
||||
hmpl_render_interpolation_tags(arena, &text, context, ".");
|
||||
raise_notice("Result:\n%s", text);
|
||||
assert(strcmp(text, TEST_DATA_INTERPOLATION_WITH_PREFIX_RESULT) == 0);
|
||||
}
|
||||
|
||||
void test_render_section_tags(Arena *arena) {
|
||||
raise_notice("Testing simple section tags");
|
||||
const char *context_text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_CONTEXT);
|
||||
Json *context = json_parse(arena, &context_text);
|
||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||
|
||||
char *text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE);
|
||||
raise_notice("Template:\n%s", text);
|
||||
raise_notice("Context: %s", json_to_string(arena, context));
|
||||
|
||||
hmpl_render_section_tags(arena, &text, context, "#", "/", " ");
|
||||
raise_notice("Result:\n%s", text);
|
||||
assert(strcmp(text, TEST_DATA_SIMPLE_SECTION_ITERATION_RESULT) == 0);
|
||||
}
|
||||
|
||||
void test_render_complex_section_tags(Arena *arena) {
|
||||
raise_notice("Testing complex section tags");
|
||||
const char *context_text = arena_strdup(arena, TEST_DATA_COMPLEX_SECTION_ITERATION_CONTEXT);
|
||||
Json *context = json_parse(arena, &context_text);
|
||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||
|
||||
char *text = arena_strdup(arena, TEST_DATA_COMPLEX_SECTION_ITERATION_TEMPLATE);
|
||||
raise_notice("Template:\n%s", text);
|
||||
raise_notice("Context: %s", json_to_string(arena, context));
|
||||
|
||||
hmpl_render_section_tags(arena, &text, context, "#", "/", " ");
|
||||
raise_notice("Result:\n%s", text);
|
||||
assert(strcmp(text, TEST_DATA_COMPLEX_SECTION_ITERATION_RESULT) == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
init_logger();
|
||||
raise_notice("Starting HMPL tests");
|
||||
Arena arena = arena_init(MEM_MiB * 3);
|
||||
|
||||
// evaluation
|
||||
raise_notice("=== Testing key evaluation ===");
|
||||
test_eval_single_level_key(&arena);
|
||||
test_eval_nested_key(&arena);
|
||||
|
||||
// interpolation tags
|
||||
raise_notice("=== Testing interpolation tags ===");
|
||||
test_render_interpolation_tags(&arena);
|
||||
test_render_interpolation_tags_with_prefix(&arena);
|
||||
|
||||
// section tags
|
||||
raise_notice("=== Testing section tags ===");
|
||||
test_render_section_tags(&arena);
|
||||
test_render_complex_section_tags(&arena);
|
||||
|
||||
printf("All tests passed.\n");
|
||||
|
||||
raise_notice("All tests passed successfully");
|
||||
arena_free(&arena);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{ stdenv, gcc, lib, hectic, bash }:
|
||||
{ stdenv, gcc, lib, bash, gdb }:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "watch";
|
||||
version = "1.0";
|
||||
src = ./.;
|
||||
doCheck = true;
|
||||
doCheck = false;
|
||||
|
||||
nativeBuildInputs = [ gcc gdb ];
|
||||
|
||||
@@ -17,10 +17,8 @@ stdenv.mkDerivation {
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin $out/lib $out/include
|
||||
cp target/hmpl $out/bin/hmpl
|
||||
cp target/libhmpl.a $out/lib/
|
||||
cp hmpl.h $out/include/hmpl.h
|
||||
mkdir -p $out/bin
|
||||
cp target/watch $out/bin/watch
|
||||
'';
|
||||
|
||||
meta = {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <fnmatch.h>
|
||||
#include <getopt.h>
|
||||
#include <ctype.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 4096
|
||||
@@ -19,15 +21,27 @@
|
||||
#define MAX_PATTERNS 32
|
||||
#define POLL_INTERVAL_MS 100
|
||||
|
||||
// Global flag to indicate if we're running in pager mode
|
||||
int pager_mode = 0;
|
||||
FILE *output_stream = NULL;
|
||||
int running = 1;
|
||||
|
||||
void signal_handler(int sig) {
|
||||
(void)sig; // Mark parameter as used to avoid warning
|
||||
running = 0;
|
||||
}
|
||||
|
||||
void print_usage(const char *prog_name) {
|
||||
fprintf(stderr, "Usage: %s <command> [-p <pattern1>] [-p <pattern2>] ... <dir1> [dir2] ...\n", prog_name);
|
||||
fprintf(stderr, " or: find . -type d | %s <command> [-p <pattern1>] [-p <pattern2>] ...\n", prog_name);
|
||||
fprintf(stderr, "Options:\n");
|
||||
fprintf(stderr, " -p <pattern> File pattern to watch (can be used multiple times)\n");
|
||||
fprintf(stderr, " -P Enable pager-friendly output (refresh mode)\n");
|
||||
fprintf(stderr, " -h Show this help message\n");
|
||||
fprintf(stderr, "Examples:\n");
|
||||
fprintf(stderr, " %s 'make' -p '*.c' -p '*.h' ./src\n", prog_name);
|
||||
fprintf(stderr, " find . -type d | %s 'echo changed' -p '*.py'\n", prog_name);
|
||||
fprintf(stderr, " %s -P 'make' -p '*.c' -p '*.h' ./src | less\n", prog_name);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
@@ -148,6 +162,10 @@ int match_any_pattern(const char *filename, char **patterns, int num_patterns) {
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Register signal handlers
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
}
|
||||
@@ -159,7 +177,7 @@ int main(int argc, char *argv[]) {
|
||||
optind = 2;
|
||||
int opt;
|
||||
|
||||
while ((opt = getopt(argc, argv, "p:h")) != -1) {
|
||||
while ((opt = getopt(argc, argv, "p:Ph")) != -1) {
|
||||
switch (opt) {
|
||||
case 'p':
|
||||
if (num_patterns < MAX_PATTERNS) {
|
||||
@@ -169,6 +187,9 @@ int main(int argc, char *argv[]) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
break;
|
||||
case 'P':
|
||||
pager_mode = 1;
|
||||
break;
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
break;
|
||||
@@ -183,6 +204,15 @@ int main(int argc, char *argv[]) {
|
||||
print_usage(argv[0]);
|
||||
}
|
||||
|
||||
// Set output stream - stderr for normal mode, stdout for pager mode
|
||||
output_stream = pager_mode ? stdout : stderr;
|
||||
|
||||
// Check if stdout is being piped and we're not in pager mode
|
||||
if (!pager_mode && !isatty(STDOUT_FILENO)) {
|
||||
pager_mode = 1;
|
||||
output_stream = stdout;
|
||||
}
|
||||
|
||||
struct dir_info dirs[MAX_DIRS];
|
||||
int num_dirs = 0;
|
||||
struct file_hash files;
|
||||
@@ -242,13 +272,13 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
// Print the directories and patterns we're watching
|
||||
fprintf(stderr, "Watching %d directories for files matching: ", num_dirs);
|
||||
fprintf(output_stream, "Watching %d directories for files matching: ", num_dirs);
|
||||
for (int i = 0; i < num_patterns; i++) {
|
||||
fprintf(stderr, "'%s'%s", patterns[i], (i < num_patterns - 1) ? ", " : "\n");
|
||||
fprintf(output_stream, "'%s'%s", patterns[i], (i < num_patterns - 1) ? ", " : "\n");
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_dirs; i++) {
|
||||
fprintf(stderr, " %s\n", dirs[i].path);
|
||||
fprintf(output_stream, " %s\n", dirs[i].path);
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_dirs; i++) {
|
||||
@@ -281,8 +311,9 @@ int main(int argc, char *argv[]) {
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
fprintf(stderr, "Initially found %d matching files\n", files.count);
|
||||
fprintf(stderr, "Waiting for file modifications...\n");
|
||||
fprintf(output_stream, "Initially found %d matching files\n", files.count);
|
||||
fprintf(output_stream, "Waiting for file modifications...\n");
|
||||
fflush(output_stream);
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 0;
|
||||
@@ -302,7 +333,8 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
int first_scan = 1;
|
||||
|
||||
while (1) {
|
||||
// Main watch loop
|
||||
while (running) {
|
||||
select(0, NULL, NULL, NULL, &tv);
|
||||
|
||||
int any_changes = 0;
|
||||
@@ -337,14 +369,14 @@ int main(int argc, char *argv[]) {
|
||||
existing->exists = 1;
|
||||
if (existing->mtime != st.st_mtime) {
|
||||
if (!first_scan) {
|
||||
fprintf(stderr, "File modified: %s\n", filepath);
|
||||
fprintf(output_stream, "File modified: %s\n", filepath);
|
||||
any_changes = 1;
|
||||
}
|
||||
existing->mtime = st.st_mtime;
|
||||
}
|
||||
} else {
|
||||
if (!first_scan) {
|
||||
fprintf(stderr, "New file: %s\n", filepath);
|
||||
fprintf(output_stream, "New file: %s\n", filepath);
|
||||
any_changes = 1;
|
||||
}
|
||||
struct file_info *file = malloc(sizeof(struct file_info));
|
||||
@@ -361,7 +393,7 @@ int main(int argc, char *argv[]) {
|
||||
for (int i = 0; i < files.size; i++) {
|
||||
if (files.items[i] != NULL && files.items[i]->exists == 0) {
|
||||
if (!first_scan) {
|
||||
fprintf(stderr, "File deleted: %s\n", files.items[i]->path);
|
||||
fprintf(output_stream, "File deleted: %s\n", files.items[i]->path);
|
||||
any_changes = 1;
|
||||
}
|
||||
}
|
||||
@@ -369,8 +401,42 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
hash_remove_nonexistent(&files);
|
||||
|
||||
// If in pager mode and any changes occur, clear screen and reprint status
|
||||
if (pager_mode && any_changes) {
|
||||
// Use ANSI escape codes to clear screen
|
||||
fprintf(output_stream, "\033[2J\033[H");
|
||||
|
||||
// Reprint header
|
||||
fprintf(output_stream, "Watching %d directories for files matching: ", num_dirs);
|
||||
for (int i = 0; i < num_patterns; i++) {
|
||||
fprintf(output_stream, "'%s'%s", patterns[i], (i < num_patterns - 1) ? ", " : "\n");
|
||||
}
|
||||
|
||||
// List current files
|
||||
fprintf(output_stream, "Currently watching %d files:\n", files.count);
|
||||
int file_count = 0;
|
||||
for (int i = 0; i < files.size && file_count < files.count; i++) {
|
||||
if (files.items[i] != NULL) {
|
||||
fprintf(output_stream, " %s\n", files.items[i]->path);
|
||||
file_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Show last action
|
||||
time_t current_time = time(NULL);
|
||||
fprintf(output_stream, "\nLast event: Command executed at %s", ctime(¤t_time));
|
||||
fprintf(output_stream, "Command: %s\n", command);
|
||||
|
||||
// Make sure output is flushed to the pager
|
||||
fflush(output_stream);
|
||||
} else if (any_changes) {
|
||||
// In normal mode, just show status messages
|
||||
fprintf(output_stream, "Executing command: %s\n", command);
|
||||
fflush(output_stream);
|
||||
}
|
||||
|
||||
// Execute command if changes detected
|
||||
if (any_changes) {
|
||||
fprintf(stderr, "Executing command: %s\n", command);
|
||||
int res = system(command);
|
||||
if (res != 0) {
|
||||
perror("system");
|
||||
@@ -393,5 +459,6 @@ int main(int argc, char *argv[]) {
|
||||
free(patterns);
|
||||
hash_free(&files);
|
||||
|
||||
fprintf(output_stream, "Watch terminated.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
pkgs.writeShellScriptBin "pager" ''
|
||||
nvim -R --clean -c 'set buftype=nofile' -c 'nnoremap q :q!<CR>' -c 'set nowrap' \
|
||||
-c 'set runtimepath^=${pkgs.vimPlugins.vim-plugin-AnsiEsc}' \
|
||||
-c 'runtime! plugin/*.vim' -c 'AnsiEsc' -
|
||||
-c 'runtime! plugin/*.vim' -c 'AnsiEsc' - $@
|
||||
# ^^^^^^^^^^^^^^^^^^^^
|
||||
# Prevents Neovim from treating the buffer as a file
|
||||
# ^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Reference in New Issue
Block a user