diff --git a/i3.config b/i3.config index 232dce41..4c49e4bc 100644 --- a/i3.config +++ b/i3.config @@ -1,3 +1,5 @@ +# i3 config file (v4) +# # This configuration file was written for the NEO layout. If you are using a # different layout, you should change it. diff --git a/src/cfgparse.y b/src/cfgparse.y index 2258268f..42438893 100644 --- a/src/cfgparse.y +++ b/src/cfgparse.y @@ -5,9 +5,11 @@ */ #include #include +#include #include #include #include +#include #include "all.h" @@ -46,6 +48,194 @@ int yywrap() { return 1; } +/* + * Goes through each line of buf (separated by \n) and checks for statements / + * commands which only occur in i3 v4 configuration files. If it finds any, it + * returns version 4, otherwise it returns version 3. + * + */ +static int detect_version(char *buf) { + char *walk = buf; + char *line = buf; + while (*walk != '\0') { + if (*walk != '\n') { + walk++; + continue; + } + + /* check for some v4-only statements */ + if (strncasecmp(line, "bindcode", strlen("bindcode")) == 0 || + strncasecmp(line, "# i3 config file (v4)", strlen("# i3 config file (v4)")) == 0 || + strncasecmp(line, "workspace_layout", strlen("workspace_layout")) == 0) { + printf("deciding for version 4 due to this line: %.*s\n", (int)(walk-line), line); + return 4; + } + + /* if this is a bind statement, we can check the command */ + if (strncasecmp(line, "bind", strlen("bind")) == 0) { + char *bind = strchr(line, ' '); + if (bind == NULL) + goto next; + while ((*bind == ' ' || *bind == '\t') && *bind != '\0') + bind++; + if (*bind == '\0') + goto next; + if ((bind = strchr(bind, ' ')) == NULL) + goto next; + while ((*bind == ' ' || *bind == '\t') && *bind != '\0') + bind++; + if (*bind == '\0') + goto next; + if (strncasecmp(bind, "layout", strlen("layout")) == 0 || + strncasecmp(bind, "floating", strlen("floating")) == 0 || + strncasecmp(bind, "workspace", strlen("workspace")) == 0 || + strncasecmp(bind, "focus left", strlen("focus left")) == 0 || + strncasecmp(bind, "focus right", strlen("focus right")) == 0 || + strncasecmp(bind, "focus up", strlen("focus up")) == 0 || + strncasecmp(bind, "focus down", strlen("focus down")) == 0 || + strncasecmp(bind, "border normal", strlen("border normal")) == 0 || + strncasecmp(bind, "border 1pixel", strlen("border 1pixel")) == 0 || + strncasecmp(bind, "border borderless", strlen("border borderless")) == 0) { + printf("deciding for version 4 due to this line: %.*s\n", (int)(walk-line), line); + return 4; + } + } + +next: + /* advance to the next line */ + walk++; + line = walk; + } + + return 3; +} + +/* + * Calls i3-migrate-config-to-v4.pl to migrate a configuration file (input + * buffer). + * + * Returns the converted config file or NULL if there was an error (for + * example the script could not be found in $PATH or the i3 executable’s + * directory). + * + */ +static char *migrate_config(char *input, off_t size) { + int writepipe[2]; + int readpipe[2]; + + if (pipe(writepipe) != 0 || + pipe(readpipe) != 0) { + warn("migrate_config: Could not create pipes"); + return NULL; + } + + pid_t pid = fork(); + if (pid == -1) { + warn("Could not fork()"); + return NULL; + } + + /* child */ + if (pid == 0) { + /* close writing end of writepipe, connect reading side to stdin */ + close(writepipe[1]); + dup2(writepipe[0], 0); + + /* close reading end of readpipe, connect writing side to stdout */ + close(readpipe[0]); + dup2(readpipe[1], 1); + + /* start the migration script, search PATH first */ + char *migratepath = "i3-migrate-config-to-v4.pl"; + execlp(migratepath, migratepath, NULL); + + /* if the script is not in path, maybe the user installed to a strange + * location and runs the i3 binary with an absolute path. We use + * argv[0]’s dirname */ + char *pathbuf = strdup(start_argv[0]); + char *dir = dirname(pathbuf); + asprintf(&migratepath, "%s/%s", dir, "i3-migrate-config-to-v4.pl"); + execlp(migratepath, migratepath, NULL); + +#if defined(__linux__) + /* on linux, we have one more fall-back: dirname(/proc/self/exe) */ + char buffer[BUFSIZ]; + if (readlink("/proc/self/exe", buffer, BUFSIZ) == -1) { + warn("could not read /proc/self/exe"); + exit(1); + } + dir = dirname(buffer); + asprintf(&migratepath, "%s/%s", dir, "i3-migrate-config-to-v4.pl"); + execlp(migratepath, migratepath, NULL); +#endif + + warn("Could not start i3-migrate-config-to-v4.pl"); + exit(2); + } + + /* parent */ + + /* close reading end of the writepipe (connected to the script’s stdin) */ + close(writepipe[0]); + + /* write the whole config file to the pipe, the script will read everything + * immediately */ + int written = 0; + int ret; + while (written < size) { + if ((ret = write(writepipe[1], input + written, size - written)) < 0) { + warn("Could not write to pipe"); + return NULL; + } + written += ret; + } + close(writepipe[1]); + + /* close writing end of the readpipe (connected to the script’s stdout) */ + close(readpipe[1]); + + /* read the script’s output */ + int conv_size = 65535; + char *converted = malloc(conv_size); + int read_bytes = 0; + do { + if (read_bytes == conv_size) { + conv_size += 65535; + converted = realloc(converted, conv_size); + } + ret = read(readpipe[0], converted + read_bytes, conv_size - read_bytes); + if (ret == -1) { + warn("Cannot read from pipe"); + return NULL; + } + read_bytes += ret; + } while (ret > 0); + + /* get the returncode */ + int status; + wait(&status); + if (!WIFEXITED(status)) { + fprintf(stderr, "Child did not terminate normally, using old config file (will lead to broken behaviour)\n"); + return NULL; + } + + int returncode = WEXITSTATUS(status); + if (returncode != 0) { + fprintf(stderr, "Migration process exit code was != 0\n"); + if (returncode == 2) { + fprintf(stderr, "could not start the migration script\n"); + /* TODO: script was not found. tell the user to fix his system or create a v4 config */ + } else if (returncode == 1) { + fprintf(stderr, "This already was a v4 config. Please add the following line to your config file:\n"); + fprintf(stderr, "# i3 config file (v4)\n"); + /* TODO: nag the user with a message to include a hint for i3 in his config file */ + } + return NULL; + } + + return converted; +} + void parse_file(const char *f) { SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables); int fd, ret, read_bytes = 0; @@ -160,6 +350,35 @@ void parse_file(const char *f) { } } + /* analyze the string to find out whether this is an old config file (3.x) + * or a new config file (4.x). If it’s old, we run the converter script. */ + int version = detect_version(buf); + if (version == 3) { + /* We need to convert this v3 configuration */ + char *converted = migrate_config(new, stbuf.st_size); + if (converted != NULL) { + printf("\n"); + printf("****************************************************************\n"); + printf("NOTE: Automatically converted configuration file from v3 to v4.\n"); + printf("\n"); + printf("Please convert your config file to v4. You can use this command:\n"); + printf(" mv %s %s.O\n", f, f); + printf(" i3-migrate-config-to-v4.pl %s.O > %s\n", f, f); + printf("****************************************************************\n"); + printf("\n"); + free(new); + new = converted; + } else { + printf("\n"); + printf("**********************************************************************\n"); + printf("ERROR: Could not convert config file. Maybe i3-migrate-config-to-v4.pl\n"); + printf("was not correctly installed on your system?\n"); + printf("**********************************************************************\n"); + printf("\n"); + } + } + + /* now lex/parse it */ yy_scan_string(new); context = scalloc(sizeof(struct context));