# Playbook: add a `/command` End-to-end recipe for adding a new user-facing command. Example: `/foo`. ## 1. Define the handler `src/command/cmd_funcs.h`: ```c gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args); ``` `src/command/cmd_funcs.c`: ```c gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args) { if (connection_get_status() != JABBER_CONNECTED) { cons_show("You are not currently connected."); return TRUE; } if (!args[0]) { cons_bad_cmd_usage(command); return TRUE; } // ... do the thing ... return TRUE; } ``` Keep the handler thin: validate, then delegate to a domain module. ## 2. Register the command `src/command/cmd_defs.c` — add an entry to the static `Command` array: ```c { CMD_PREAMBLE("/foo", parse_args, 0, 1, NULL) CMD_MAINFUNC(cmd_foo) CMD_TAGS(CMD_TAG_CHAT) CMD_SYN("/foo []") CMD_DESC("Do the foo thing.") CMD_ARGS( { "", "Optional argument to pass." }) CMD_EXAMPLES("/foo bar") } ``` For a command with subcommands, swap `CMD_MAINFUNC(cmd_foo)` for `CMD_SUBFUNCS({"sub", cmd_foo_sub}, ...)`. ## 3. Autocompletion (optional) `src/command/cmd_ac.c`: a. (If a static token list) declare and initialise an `Autocomplete`: ```c static Autocomplete foo_ac; // in cmd_ac_init(): foo_ac = autocomplete_new(); autocomplete_add(foo_ac, "bar"); autocomplete_add(foo_ac, "baz"); // add &foo_ac to the static free-list near top of file ``` b. Implement the per-command dispatcher: ```c static char* _foo_autocomplete(ProfWin* window, const char* const input, gboolean previous) { return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous); } ``` c. Register it: ```c g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete); ``` For dynamic suggestions, use `autocomplete_param_with_func` and a stateless callback. See `patterns/autocomplete.md`. ## 4. Unit test Create `tests/unittests/test_cmd_foo.c` and `test_cmd_foo.h`: `test_cmd_foo.h`: ```c void test_cmd_foo_when_disconnected_shows_message(void** state); void test_cmd_foo_when_no_arg_shows_usage(void** state); void test_cmd_foo_happy_path(void** state); ``` `test_cmd_foo.c`: ```c #include "config.h" #include "prof_cmocka.h" #include "test_cmd_foo.h" // ... includes for stubs and the unit ... void test_cmd_foo_when_disconnected_shows_message(void** state) { will_return(connection_get_status, JABBER_DISCONNECTED); expect_string(cons_show, msg, "You are not currently connected."); gchar* args[] = { NULL }; assert_true(cmd_foo(NULL, "/foo", args)); } ``` ## 5. Register the test `tests/unittests/unittests.c`: ```c #include "test_cmd_foo.h" // ... inside the tests[] array ... cmocka_unit_test(test_cmd_foo_when_disconnected_shows_message), cmocka_unit_test(test_cmd_foo_when_no_arg_shows_usage), cmocka_unit_test(test_cmd_foo_happy_path), ``` ## 6. Stubs If `cmd_foo` calls a function that is not yet stubbed, add the stub. See `testing/stubs.md`. ## 7. Wire into Make - `cmd_defs.c`, `cmd_funcs.c`, `cmd_ac.c` are already in the build. - New test file: add `tests/unittests/test_cmd_foo.c` to the unittests sources in `tests/unittests/Makefile.am` (or whatever wires it). - New stub file (if any): add to the same Makefile. ## 8. Build & check Inside Docker (`build/docker.md`): ```sh ./autogen.sh && ./configure && make -j$(nproc) && make check ``` ## 9. Help text `/help foo` should now produce the synopsis / description / args from the `Command` entry. No separate help file to update. ## 10. Commit Single commit, conventional-commit style, English. No AI-attribution trailer.