Adding a new command to tmux

2023-01-04 00:00:00 +0000 UTC

This continues the work done in my last post on tmux’s objected-oriented commands in C.

To demonstrate the function of the command system, this post will walk you through adding a new “echo” command that prints its argument out to the screen (or stdout if invoked from the command line).

The diff for this work can be viewed on Github in this PR.

Step 1: Create File cmd-echo.c

This file will include the cmd_entry instance and exec function.

static enum cmd_retval	cmd_echo_exec(struct cmd *, struct cmdq_item *);

const struct cmd_entry cmd_echo_entry = {
	.name = "echo",
	.alias = NULL,

	.args = { "", 1, 1, NULL },
	.usage = "[message-to-echo]",

	.flags = 0,
	.exec = cmd_echo_exec
};

Note in the descriptor we set the alias to NULL since we won’t have a shorter version of the word echo. We can also elide the target field sicne the command is untargeted. Finally, we set the lower and upper bounds in the .args field each to 1 since we must have exactly one argument – this argument is the message-to-echo mentioned in the usage string. Note finally we have an empty template in the args member as well, since the command accepts no flags currently.

Next we need to write the exec function. This function pulls context from the struct cmd * and struct cmdq_item * to achieve its objective.

static enum cmd_retval cmd_echo_exec(struct cmd *self, struct cmdq_item *item)
{
	struct args *args = cmd_get_args(self);
	const char *s = args_string(args, 0);

	if(s == NULL)
		return (CMD_RETURN_ERROR);

	cmdq_print(item, "%s", s);
	return (CMD_RETURN_NORMAL);
}

First, we need to access the arguments of the command. For that use, we use the accessor function cmd_get_args on our struct cmd *self. This returns a value of the listed type struct args *.

Next we can use the API for the struct args type to extract the string value of an argument at a particular index: args_string. This function returns a char*. It returns NULL when we have no argument present at that index, so we need to account for that case and return an error if no string is available to echo (even though this case should never occur due to the lower and upper bounds on argument count mentioned before).

Finally, this command uses the cmdq_print API to display the message. This API extracts a struct client from the struct cmdq_item and writes the argument specified by the format string & variadic arguments to an appropriate location based on the command’s invocation.

Step 2: declare the cmd_echo_entry as extern in cmd.c`

To let the linker know to look for our cmd_echo_entry struct definition outside of cmd.c, we need to add an externd declaration:

/* ... omitted ... */
extern const struct cmd_entry cmd_down_pane_entry;
extern const struct cmd_entry cmd_echo_entry;
extern const struct cmd_entry cmd_find_window_entry;
/* ... omitted ... */

Step 3: add a reference to the cmd_echo_entry in cmd_table[] in cmd.c

Also in cmd.c, we need to add a reference to our command descriptor cmd_echo_entry to the cmd_table:

const struct cmd_entry *cmd_table[] = {
	/* ... omitted ... */
	&cmd_display_panes_entry,
	&cmd_echo_entry,
	&cmd_find_window_entry,
	/* ... omitted ... */
};

This allows the function cmd_find to find our command by name or alias, if provided. cmd_find is called by cmd_parse, which constructs the struct cmd that will eventually be passed back into our exec function.

Step 4: add file cmd-echo.c to Makefile.am to register file with Automake

By adding cmd-echo.c to the dis_tmux_SOURCES variable in Makefile.am, automake will know to compile cmd-echo.c so that the externd declaration in cmd.c actually points to something!

# List of sources.
dist_tmux_SOURCES = \
	# ... omitted ...
	cmd-display-panes.c \
	cmd-echo.c \
	cmd-find-window.c \
	# ... omitted ...

And that’s it! We can now compile with the usual tmux compilation workflow:

$ sh autogen.sh
$ ./configure && make

Tie it together: Using our command

Having started a new tmux server and client using our freshly built tmux, we can run the command from the shell:

$ ./tmux echo "Hey! This is the echo command"
Hey! This is the echo command

We can also run it from within a tmux session by entering the keystrokes Tmux-Prefix :echo "The string to echo is here!" (the tmux prefix being C-b by default):

The string to echo is here!                                                                                                                          [0/0]






[0] 0:[tmux]* 1:bash-                                                                                                             "devenv" 07:51 04-Jan-23
Tags: c tmux tmux-internals