feat: watch: try pager mode..

This commit is contained in:
2025-04-04 02:09:43 +00:00
parent d800266fe0
commit c5e9d9c8cb
8 changed files with 6363 additions and 202 deletions

View File

@@ -25,7 +25,7 @@ stdenv.mkDerivation {
''; '';
meta = { meta = {
description = "hectic"; description = "hmpl";
license = lib.licenses.mit; license = lib.licenses.mit;
}; };
} }

View File

@@ -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) // {{item#array}}...{{/array}}
// 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}}
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) { 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); 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]; char start_pattern[32];
snprintf(start_pattern, sizeof(start_pattern), "{{%s", prefix_start); 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 // Create a mutable copy of separator_pattern
char end_pattern[32]; char separator_copy[32];
snprintf(end_pattern, sizeof(end_pattern), "{{%s", prefix_end); strncpy(separator_copy, separator_pattern, sizeof(separator_copy) - 1);
int end_pattern_length = strlen(end_pattern); separator_copy[sizeof(separator_copy) - 1] = '\0';
Slice separator_slice = slice_create(char, separator_copy, strlen(separator_copy), 0, strlen(separator_copy));
int separator_pattern_length = strlen(separator_pattern); if (separator_slice.len == 0) {
if (!separator_pattern || separator_pattern_length == 0) {
raise_exception("Unexpected usage: separator pattern cannot be empty"); 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) { while (1) {
char *current_text = *text_ptr; // Find tag start
char *opening_tag_start = strstr(current_text + offset, start_pattern); char *text_data = (char*)text_slice.data;
if (!opening_tag_start) char *opening_tag_start = strstr(text_data + offset, (char*)start_slice.data);
break; if (!opening_tag_start) break;
int start_index = opening_tag_start - current_text;
int relative_key_start = start_index + start_pattern_length;
char *opening_tag_separator = strstr(opening_tag_start, separator_pattern); // Create slice for separator search
if (!opening_tag_start) { size_t start_index = opening_tag_start - text_data;
raise_exception("Malformed template: missing separator for section tag or not specifiet name for element"); 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); 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) { if (!opening_tag_end) {
raise_exception("Malformed template: missing closing braces for section tag"); raise_exception("Malformed template: missing closing braces for section tag");
exit(1); 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); 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; // Create pattern for closing tag
assert(element_name_length > 0); 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); // Find closing tag
substr_clone(current_text, element_name, element_name_start, element_name_length); 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; // Find the exact closing tag by checking for complete tag pattern
char *close_tag_patern = arena_alloc(arena, close_tag_patern_length + 1); char *close_tag = NULL;
snprintf(close_tag_patern, sizeof(*close_tag_patern), "%s%s%s", start_pattern, key, end_pattern); 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) { if (!close_tag) {
raise_exception("Malformed template: missing loop end for key %s", key); raise_exception("Malformed template: missing loop end for key %s", key);
exit(1); exit(1);
} }
// Get array from context
Json *arr = eval_object(arena, context, key); Json *arr = eval_object(arena, context, key);
if (arr && arr->type == JSON_ARRAY) { if (arr && arr->type == JSON_ARRAY) {
// Count array elements
size_t elem_count = 0; size_t elem_count = 0;
for (Json *e = arr->child; e; e = e->next) elem_count++; for (Json *e = arr->child; e; e = e->next) elem_count++;
// Allocate memory for replacement
char *replacement = arena_alloc(arena, MEM_KiB * elem_count); 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); // Extract template block
size_t relative_block_start = (size_t)opening_tag_end + 2 - (size_t)current_text; size_t block_start = after_opening_end;
raise_trace("relative_block_start: %p = %p - 2 - %p", opening_tag_end, current_text); size_t block_length = (close_tag - (char*)after_opening_slice.data);
size_t block_len = (size_t)opening_tag_end - (size_t)close_tag - 2; char *block_buff = arena_alloc(arena, block_length + 1);
raise_trace("block_len %p = %p - %p - 2", block_len, opening_tag_end, close_tag); substr_clone((char*)after_opening_slice.data, block_buff, block_start, block_length);
assert(block_len > 0); block_buff[block_length] = '\0';
substr_clone(current_text, block_buff, relative_block_start, block_len);
// Process each array element
for (Json *elem = arr->child; elem; elem = elem->next) { for (Json *elem = arr->child; elem; elem = elem->next) {
char *block = arena_strdup(arena, block_buff); 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); raise_trace("block after: %s", block);
size_t block_len = strlen(block); size_t block_len = strlen(block);
memcpy(replacement + offset, block, block_len); memcpy(replacement + replacement_offset, block, block_len);
offset += block_len; replacement_offset += block_len;
} }
replacement[offset] = '\0'; replacement[replacement_offset] = '\0';
raise_trace("replacement: %s", replacement); raise_trace("replacement: %s", replacement);
char *new_text = arena_repstr(arena, current_text, // Calculate replacement positions
(size_t)opening_tag_start - 1, size_t replace_start = start_index;
close_tag + close_tag_patern_length - opening_tag_start + 2, size_t replace_length = (close_tag - (char*)after_opening_slice.data) +
replacement); 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; *text_ptr = new_text;
// Update text slice
text_slice = slice_create(char, new_text, strlen(new_text), 0, strlen(new_text));
} }
offset = start_index; offset = start_index;
} }
} }

View File

@@ -2,6 +2,8 @@
# Usage: make.sh [build|check] [--norun] [--debug] [--color] # Usage: make.sh [build|check] [--norun] [--debug] [--color]
# Options: # Options:
# build Build the library and app (default if no mode is provided). # 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. # check Build tests; runs them unless --norun is specified.
# --norun (check only) Build tests but do not run them. # --norun (check only) Build tests but do not run them.
# --debug Build with -O0 (debug mode). # --debug Build with -O0 (debug mode).
@@ -9,7 +11,7 @@
# help, --help Show this help message. # help, --help Show this help message.
check_dependencies() { 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 if ! command -v "$dep" >/dev/null 2>&1; then
echo "Error: Required dependency '$dep' not found." >&2 echo "Error: Required dependency '$dep' not found." >&2
exit 1 exit 1
@@ -22,6 +24,8 @@ print_help() {
cat <<EOF cat <<EOF
Usage: $0 [build|check] [--norun] [--debug] [--color] Usage: $0 [build|check] [--norun] [--debug] [--color]
build Build the library and app (default). 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. check Build tests; runs them unless --norun is specified.
--norun (check only) Build tests but do not run them. --norun (check only) Build tests but do not run them.
--debug Build with debug flags (-O0). --debug Build with debug flags (-O0).
@@ -74,8 +78,7 @@ if [ -n "$COLOR_FLAG" ]; then
CFLAGS="$CFLAGS $COLOR_FLAG" CFLAGS="$CFLAGS $COLOR_FLAG"
fi fi
case "$MODE" in build() {
build)
mkdir -p target mkdir -p target
echo "# Build library" echo "# Build library"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
@@ -85,10 +88,20 @@ case "$MODE" in
echo "# Build app" echo "# Build app"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
cc $CFLAGS $OPTFLAGS main.c -Ltarget -lhmpl $LDFLAGS -o target/hmpl 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) check)
mkdir -p target/test mkdir -p target/test
export LOG_LEVEL=TRACE
for test_file in test/*.c; do for test_file in test/*.c; do
exe="target/test/$(basename "${test_file%.c}")" exe="target/test/$(basename "${test_file%.c}")"
# shellcheck disable=SC2086 # shellcheck disable=SC2086

6063
package/c/hmpl/test.log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -101,7 +101,7 @@
"}" "}"
#define TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE \ #define TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE \
"{{#array element}}" \ "{{#element array}}" \
" {{element.field.subfield}}" \ " {{element.field.subfield}}" \
"{{/array}}" "{{/array}}"
@@ -110,78 +110,130 @@
"value2" \ "value2" \
"value3" "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) { void test_eval_single_level_key(Arena *arena) {
raise_notice("Testing single level key evaluation");
const char *context_text = arena_strdup(arena, "{\"name\": \"world\"}"); const char *context_text = arena_strdup(arena, "{\"name\": \"world\"}");
Json *context = json_parse(arena, &context_text); Json *context = json_parse(arena, &context_text);
if (!context) { raise_exception("Malformed json"); exit(1); } if (!context) { raise_exception("Malformed json"); exit(1); }
char *result = eval_string(arena, context, "name"); 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); assert(result && strcmp(result, "world") == 0);
} }
void test_eval_nested_key(Arena *arena) { void test_eval_nested_key(Arena *arena) {
raise_notice("Testing nested key evaluation");
const char *context_text = arena_strdup(arena, "{\"person\": {\"name\": \"Alice\"}}"); const char *context_text = arena_strdup(arena, "{\"person\": {\"name\": \"Alice\"}}");
Json *context = json_parse(arena, &context_text); Json *context = json_parse(arena, &context_text);
if (!context) { raise_exception("Malformed json"); exit(1); } if (!context) { raise_exception("Malformed json"); exit(1); }
char *result = eval_string(arena, context, "person.name"); 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); assert(result && strcmp(result, "Alice") == 0);
} }
void test_render_interpolation_tags(Arena *arena) { 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); const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_CONTEXT);
Json *context = json_parse(arena, &context_text); Json *context = json_parse(arena, &context_text);
if (!context) { raise_exception("Malformed json"); exit(1); } if (!context) { raise_exception("Malformed json"); exit(1); }
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_TEMPLATE); 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, ""); 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); assert(strcmp(text, TEST_DATA_INTERPOLATION_RESULT) == 0);
} }
void test_render_interpolation_tags_with_prefix(Arena *arena) { 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); const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_CONTEXT);
Json *context = json_parse(arena, &context_text); Json *context = json_parse(arena, &context_text);
if (!context) { raise_exception("Malformed json"); exit(1); } if (!context) { raise_exception("Malformed json"); exit(1); }
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_TEMPLATE); 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, "."); hmpl_render_interpolation_tags(arena, &text, context, ".");
raise_notice("Result:\n%s", text);
assert(strcmp(text, TEST_DATA_INTERPOLATION_WITH_PREFIX_RESULT) == 0); assert(strcmp(text, TEST_DATA_INTERPOLATION_WITH_PREFIX_RESULT) == 0);
} }
void test_render_section_tags(Arena *arena) { 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); const char *context_text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_CONTEXT);
Json *context = json_parse(arena, &context_text); Json *context = json_parse(arena, &context_text);
if (!context) { raise_exception("Malformed json"); exit(1); } if (!context) { raise_exception("Malformed json"); exit(1); }
char *text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE); 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, "#", "/", " "); hmpl_render_section_tags(arena, &text, context, "#", "/", " ");
raise_notice("Result:\n%s", text);
assert(strcmp(text, TEST_DATA_SIMPLE_SECTION_ITERATION_RESULT) == 0); 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) { int main(void) {
init_logger(); init_logger();
raise_notice("Starting HMPL tests");
Arena arena = arena_init(MEM_MiB * 3); Arena arena = arena_init(MEM_MiB * 3);
// evaluation // evaluation
raise_notice("=== Testing key evaluation ===");
test_eval_single_level_key(&arena); test_eval_single_level_key(&arena);
test_eval_nested_key(&arena); test_eval_nested_key(&arena);
// interpolation tags // interpolation tags
raise_notice("=== Testing interpolation tags ===");
test_render_interpolation_tags(&arena); test_render_interpolation_tags(&arena);
test_render_interpolation_tags_with_prefix(&arena); test_render_interpolation_tags_with_prefix(&arena);
// section tags // section tags
raise_notice("=== Testing section tags ===");
test_render_section_tags(&arena); 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); arena_free(&arena);
return 0; return 0;
} }

View File

@@ -1,10 +1,10 @@
{ stdenv, gcc, lib, hectic, bash }: { stdenv, gcc, lib, bash, gdb }:
stdenv.mkDerivation { stdenv.mkDerivation {
pname = "watch"; pname = "watch";
version = "1.0"; version = "1.0";
src = ./.; src = ./.;
doCheck = true; doCheck = false;
nativeBuildInputs = [ gcc gdb ]; nativeBuildInputs = [ gcc gdb ];
@@ -17,10 +17,8 @@ stdenv.mkDerivation {
''; '';
installPhase = '' installPhase = ''
mkdir -p $out/bin $out/lib $out/include mkdir -p $out/bin
cp target/hmpl $out/bin/hmpl cp target/watch $out/bin/watch
cp target/libhmpl.a $out/lib/
cp hmpl.h $out/include/hmpl.h
''; '';
meta = { meta = {

View File

@@ -9,6 +9,8 @@
#include <fnmatch.h> #include <fnmatch.h>
#include <getopt.h> #include <getopt.h>
#include <ctype.h> #include <ctype.h>
#include <fcntl.h>
#include <signal.h>
#ifndef PATH_MAX #ifndef PATH_MAX
#define PATH_MAX 4096 #define PATH_MAX 4096
@@ -19,15 +21,27 @@
#define MAX_PATTERNS 32 #define MAX_PATTERNS 32
#define POLL_INTERVAL_MS 100 #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) { void print_usage(const char *prog_name) {
fprintf(stderr, "Usage: %s <command> [-p <pattern1>] [-p <pattern2>] ... <dir1> [dir2] ...\n", 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, " or: find . -type d | %s <command> [-p <pattern1>] [-p <pattern2>] ...\n", prog_name);
fprintf(stderr, "Options:\n"); fprintf(stderr, "Options:\n");
fprintf(stderr, " -p <pattern> File pattern to watch (can be used multiple times)\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, " -h Show this help message\n");
fprintf(stderr, "Examples:\n"); fprintf(stderr, "Examples:\n");
fprintf(stderr, " %s 'make' -p '*.c' -p '*.h' ./src\n", prog_name); 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, " 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); 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[]) { int main(int argc, char *argv[]) {
// Register signal handlers
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
if (argc < 2) { if (argc < 2) {
print_usage(argv[0]); print_usage(argv[0]);
} }
@@ -159,7 +177,7 @@ int main(int argc, char *argv[]) {
optind = 2; optind = 2;
int opt; int opt;
while ((opt = getopt(argc, argv, "p:h")) != -1) { while ((opt = getopt(argc, argv, "p:Ph")) != -1) {
switch (opt) { switch (opt) {
case 'p': case 'p':
if (num_patterns < MAX_PATTERNS) { if (num_patterns < MAX_PATTERNS) {
@@ -169,6 +187,9 @@ int main(int argc, char *argv[]) {
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
break; break;
case 'P':
pager_mode = 1;
break;
case 'h': case 'h':
print_usage(argv[0]); print_usage(argv[0]);
break; break;
@@ -183,6 +204,15 @@ int main(int argc, char *argv[]) {
print_usage(argv[0]); 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]; struct dir_info dirs[MAX_DIRS];
int num_dirs = 0; int num_dirs = 0;
struct file_hash files; struct file_hash files;
@@ -242,13 +272,13 @@ int main(int argc, char *argv[]) {
} }
// Print the directories and patterns we're watching // 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++) { 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++) { 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++) { for (int i = 0; i < num_dirs; i++) {
@@ -281,8 +311,9 @@ int main(int argc, char *argv[]) {
closedir(dir); closedir(dir);
} }
fprintf(stderr, "Initially found %d matching files\n", files.count); fprintf(output_stream, "Initially found %d matching files\n", files.count);
fprintf(stderr, "Waiting for file modifications...\n"); fprintf(output_stream, "Waiting for file modifications...\n");
fflush(output_stream);
struct timeval tv; struct timeval tv;
tv.tv_sec = 0; tv.tv_sec = 0;
@@ -302,7 +333,8 @@ int main(int argc, char *argv[]) {
int first_scan = 1; int first_scan = 1;
while (1) { // Main watch loop
while (running) {
select(0, NULL, NULL, NULL, &tv); select(0, NULL, NULL, NULL, &tv);
int any_changes = 0; int any_changes = 0;
@@ -337,14 +369,14 @@ int main(int argc, char *argv[]) {
existing->exists = 1; existing->exists = 1;
if (existing->mtime != st.st_mtime) { if (existing->mtime != st.st_mtime) {
if (!first_scan) { if (!first_scan) {
fprintf(stderr, "File modified: %s\n", filepath); fprintf(output_stream, "File modified: %s\n", filepath);
any_changes = 1; any_changes = 1;
} }
existing->mtime = st.st_mtime; existing->mtime = st.st_mtime;
} }
} else { } else {
if (!first_scan) { if (!first_scan) {
fprintf(stderr, "New file: %s\n", filepath); fprintf(output_stream, "New file: %s\n", filepath);
any_changes = 1; any_changes = 1;
} }
struct file_info *file = malloc(sizeof(struct file_info)); 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++) { for (int i = 0; i < files.size; i++) {
if (files.items[i] != NULL && files.items[i]->exists == 0) { if (files.items[i] != NULL && files.items[i]->exists == 0) {
if (!first_scan) { 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; any_changes = 1;
} }
} }
@@ -369,8 +401,42 @@ int main(int argc, char *argv[]) {
hash_remove_nonexistent(&files); 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(&current_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) { if (any_changes) {
fprintf(stderr, "Executing command: %s\n", command);
int res = system(command); int res = system(command);
if (res != 0) { if (res != 0) {
perror("system"); perror("system");
@@ -393,5 +459,6 @@ int main(int argc, char *argv[]) {
free(patterns); free(patterns);
hash_free(&files); hash_free(&files);
fprintf(output_stream, "Watch terminated.\n");
return 0; return 0;
} }

View File

@@ -2,7 +2,7 @@
pkgs.writeShellScriptBin "pager" '' pkgs.writeShellScriptBin "pager" ''
nvim -R --clean -c 'set buftype=nofile' -c 'nnoremap q :q!<CR>' -c 'set nowrap' \ nvim -R --clean -c 'set buftype=nofile' -c 'nnoremap q :q!<CR>' -c 'set nowrap' \
-c 'set runtimepath^=${pkgs.vimPlugins.vim-plugin-AnsiEsc}' \ -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 # Prevents Neovim from treating the buffer as a file
# ^^^^^^^^^^^^^^^^^^^^ # ^^^^^^^^^^^^^^^^^^^^