diff --git a/include/config.h b/include/config.h index c15cdb9..8e80bf0 100644 --- a/include/config.h +++ b/include/config.h @@ -29,11 +29,17 @@ struct kanshi_profile_output { enum wl_output_transform transform; }; +struct kanshi_profile_command { + struct wl_list link; + char *command; +}; + struct kanshi_profile { struct wl_list link; char *name; // Wildcard outputs are stored at the end of the list struct wl_list outputs; + struct wl_list commands; }; struct kanshi_config { diff --git a/kanshi.5.scd b/kanshi.5.scd index 9c77514..d498649 100644 --- a/kanshi.5.scd +++ b/kanshi.5.scd @@ -35,6 +35,37 @@ Directives are followed by space-separated arguments. Arguments can be quoted On *sway*(1), output names and descriptions can be obtained via *swaymsg -t get_outputs*. +*exec* + An exec directive executes a command when the profile was successfully + applied. This can be used to update the compositor state to the profile + when not done automatically. + + Commands are executed asynchronously and their order may not be preserved. + If you need to execute sequential commands, you should collect in one exec + statement or in a separate script. + + On *sway*(1) for example, *exec* can be used to move workspaces to the + right output: + +``` + multihead { + output eDP-1 enable + output DP-1 enable transform 270 + exec swaymsg workspace 1, move workspace to eDP-1 + } +``` + + Note that some extra care must be taken with outputs identified by an + output description as the real name may change: + +``` + complex { + output "Some Other Company GTBZ 2525" mode 1920x1200 + exec swaymsg workspace 1, move workspace to output '"Some Other Company GTBZ 2525"' + } +``` + + # OUTPUT DIRECTIVES *enable*|*disable* diff --git a/main.c b/main.c index 539d6fc..7de6d3f 100644 --- a/main.c +++ b/main.c @@ -1,8 +1,12 @@ #define _POSIX_C_SOURCE 200809L #include +#include #include #include #include +#include +#include +#include #include #include "config.h" @@ -71,10 +75,59 @@ static struct kanshi_profile *match(struct kanshi_state *state, } +static void exec_command(char *cmd) { + pid_t pid, child; + if ((pid = fork()) == 0) { + setsid(); + sigset_t set; + sigemptyset(&set); + sigprocmask(SIG_SETMASK, &set, NULL); + if ((child = fork()) == 0) { + execl("/bin/sh", "/bin/sh", "-c", cmd, (void *)NULL); + fprintf(stderr, "Executing command '%s' failed: %s", cmd, strerror(errno)); + exit(-1); + } + if (child < 0) { + fprintf(stderr, "Impossible to fork a new process to execute" + " command '%s': %s", cmd, strerror(errno)); + exit(0); + } + + // Try to give some meaningful information on the command success + int wstatus; + if (waitpid(child, &wstatus, 0) != child) { + perror("waitpid"); + exit(0); + } + if (WIFEXITED(wstatus)) { + fprintf(stderr, "Command '%s' returned with exit status %d.\n", + cmd, WEXITSTATUS(wstatus)); + } else { + fprintf(stderr, "Command '%s' was killed, aborted or disappeared" + " in dire circumstances.\n", cmd); + } + exit(0); // Close child process + } + + if (pid < 0) { + perror("Impossible to fork a new process"); + } +} + +static void execute_profile_commands(struct kanshi_profile *profile) { + struct kanshi_profile_command *command; + wl_list_for_each(command, &profile->commands, link) { + fprintf(stderr, "Running command '%s'\n", command->command); + exec_command(command->command); + } +} + static void config_handle_succeeded(void *data, struct zwlr_output_configuration_v1 *config) { struct kanshi_pending_profile *pending = data; zwlr_output_configuration_v1_destroy(config); + fprintf(stderr, "running commands for configuration '%s'\n", pending->profile->name); + execute_profile_commands(pending->profile); fprintf(stderr, "configuration for profile '%s' applied\n", pending->profile->name); pending->state->current_profile = pending->profile; diff --git a/parser.c b/parser.c index 8901e7b..9809603 100644 --- a/parser.c +++ b/parser.c @@ -91,6 +91,24 @@ static bool parser_read_quoted(struct kanshi_parser *parser) { } } +static bool parser_read_line(struct kanshi_parser *parser) { + while (1) { + int ch = parser_peek_char(parser); + if (ch < 0) { + return false; + } + + if (ch == '\n' || ch == '\0') { + parser->tok_str[parser->tok_str_len] = '\0'; + return true; + } + + if (!parser_append_tok_ch(parser, parser_read_char(parser))) { + return false; + } + } +} + static bool parser_read_str(struct kanshi_parser *parser) { while (1) { int ch = parser_peek_char(parser); @@ -338,9 +356,32 @@ static struct kanshi_profile_output *parse_profile_output( } } +static struct kanshi_profile_command *parse_profile_command( + struct kanshi_parser *parser) { + // Skip the 'exec' directive. + if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) { + return NULL; + } + + if (!parser_read_line(parser)) { + return NULL; + } + + if (parser->tok_str_len <= 0) { + fprintf(stderr, "Ignoring empty command in config file on line %d\n", + parser->line); + return NULL; + } + + struct kanshi_profile_command *command = calloc(1, sizeof(*command)); + command->command = strdup(parser->tok_str); + return command; +} + static struct kanshi_profile *parse_profile(struct kanshi_parser *parser) { struct kanshi_profile *profile = calloc(1, sizeof(*profile)); wl_list_init(&profile->outputs); + wl_list_init(&profile->commands); // First parse an optional profile name parser->tok_str_len = 0; @@ -391,6 +432,14 @@ static struct kanshi_profile *parse_profile(struct kanshi_parser *parser) { } else { wl_list_insert(&profile->outputs, &output->link); } + } else if (strcmp(directive, "exec") == 0) { + struct kanshi_profile_command *command = + parse_profile_command(parser); + if (command == NULL) { + return NULL; + } + // Insert commands at the end to preserve order + wl_list_insert(profile->commands.prev, &command->link); } else { fprintf(stderr, "unknown directive '%s' in profile '%s'\n", directive, profile->name);