├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── arg.h ├── asprintf.c ├── asprintf.h ├── config.mk ├── man ├── Makefile ├── ruler.1 ├── ruler.1.html └── ruler.1.md ├── parser.y ├── ruler.c ├── ruler.h ├── rulerrc └── scanner.l /.gitignore: -------------------------------------------------------------------------------- 1 | lex.yy.c 2 | y.tab.h 3 | y.tab.c 4 | *.o 5 | ruler 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016 Tudor Ioan Roman 4 | 5 | Permission to use, copy, modify, and distribute this software for 6 | any purpose with or without fee is hereby granted, provided that the 7 | above copyright notice and this permission notice appear in all 8 | copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 16 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include config.mk 2 | 3 | VERCMD ?= git describe 2> /dev/null 4 | 5 | NAME = ruler 6 | VERSION = $(shell $(VERCMD) || cat VERSION) 7 | YACC ?= yacc 8 | LEX ?= lex 9 | 10 | all: $(NAME) 11 | 12 | $(NAME): ruler.c lex.yy.c y.tab.c 13 | $(CC) $^ $(CFLAGS) $(LDFLAGS) -DNAME=\"$(NAME)\" -DVERSION=\"$(VERSION)\" -o ruler 14 | 15 | %.tab.c %.tab.h: parser.y 16 | $(YACC) $< 17 | 18 | lex.yy.c: scanner.l 19 | $(LEX) $< 20 | 21 | install: all 22 | mkdir -p $(DESTDIR)$(PREFIX)/bin 23 | install $(NAME) $(DESTDIR)$(PREFIX)/bin/$(NAME) 24 | cd ./man; $(MAKE) install 25 | 26 | uninstall: 27 | rm -f $(DESTDIR)$(PREFIX)/bin/$(NAME) 28 | cd ./man; $(MAKE) uninstall 29 | 30 | clean: 31 | rm $(NAME) lex.yy.c y.tab.c y.tab.h 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruler 2 | ===== 3 | 4 | ruler is a program used for creating rules like in some window managers, like 5 | [i3](https://i3wm.org/) or [bspwm](https://github.com/baskerville/bspwm/). 6 | 7 | Rules are commands that are associated with a set of window properties 8 | (*descriptors*). When a 9 | window is created, `ruler` executes all rules whose descriptors match the properties of a 10 | window. 11 | 12 | Descriptors can be defined as regular expressions (POSIX Extended Regular 13 | Expressions) to avoid code repetition. 14 | 15 | Commands are executed asynchronously by default. If a command is prefixed with a 16 | semicolon, it will be run synchronously. 17 | 18 | For more information, see the included manual page (`ruler(1)`). 19 | 20 | Example configuration 21 | --------------------- 22 | 23 | ``` 24 | # move all browsers to workspace 2 25 | role="browser" 26 | wtf "$RULER_ID" && waitron group_add_window 2 27 | 28 | # drop a notification if a window containing that fifth glyph is born 29 | instance=".*e.*" 30 | echo "warning!" > /tmp/notifyd.fifo 31 | ``` 32 | 33 | Dependencies 34 | ------------ 35 | 36 | * [xcb](https://xcb.freedesktop.org/) 37 | * [xcb-util-wm](https://www.archlinux.org/packages/extra/x86_64/xcb-util-wm/) 38 | * [libwm](https://github.com/wmutils/libwm) 39 | 40 | Build time dependencies: 41 | 42 | * a yacc implementation (GNU bison, OpenBSD yacc etc.) 43 | * a lex implementation (flex) 44 | 45 | Building and installing 46 | ----------------------- 47 | 48 | ``` 49 | $ make 50 | # make install 51 | ``` 52 | 53 | The `Makefile` respects the `DESTDIR` and `PREFIX` environment variables. 54 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /arg.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copy me if you can. 3 | * by 20h 4 | */ 5 | 6 | #ifndef ARG_H__ 7 | #define ARG_H__ 8 | 9 | extern char *argv0; 10 | 11 | /* use main(int argc, char *argv[]) */ 12 | #define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ 13 | argv[0] && argv[0][1]\ 14 | && argv[0][0] == '-';\ 15 | argc--, argv++) {\ 16 | char argc_;\ 17 | char **argv_;\ 18 | int brk_;\ 19 | if (argv[0][1] == '-' && argv[0][2] == '\0') {\ 20 | argv++;\ 21 | argc--;\ 22 | break;\ 23 | }\ 24 | for (brk_ = 0, argv[0]++, argv_ = argv;\ 25 | argv[0][0] && !brk_;\ 26 | argv[0]++) {\ 27 | if (argv_ != argv)\ 28 | break;\ 29 | argc_ = argv[0][0];\ 30 | switch (argc_) 31 | 32 | /* Handles obsolete -NUM syntax */ 33 | #define ARGNUM case '0':\ 34 | case '1':\ 35 | case '2':\ 36 | case '3':\ 37 | case '4':\ 38 | case '5':\ 39 | case '6':\ 40 | case '7':\ 41 | case '8':\ 42 | case '9' 43 | 44 | #define ARGEND }\ 45 | } 46 | 47 | #define ARGC() argc_ 48 | 49 | #define ARGNUMF(base) (brk_ = 1, estrtol(argv[0], (base))) 50 | 51 | #define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ 52 | ((x), abort(), (char *)0) :\ 53 | (brk_ = 1, (argv[0][1] != '\0')?\ 54 | (&argv[0][1]) :\ 55 | (argc--, argv++, argv[0]))) 56 | 57 | #define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ 58 | (char *)0 :\ 59 | (brk_ = 1, (argv[0][1] != '\0')?\ 60 | (&argv[0][1]) :\ 61 | (argc--, argv++, argv[0]))) 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /asprintf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "asprintf.h" 6 | 7 | int 8 | asprintf(char **str, const char *format, ...) 9 | { 10 | int size = 0; 11 | va_list args; 12 | 13 | va_start(args, format); 14 | size = vsnprintf(NULL, 0, format, args); 15 | 16 | if (size < 0) 17 | size = -1; 18 | *str = malloc((size + 1) * sizeof(char)); 19 | if (str == NULL) 20 | return -1; 21 | 22 | size = vsprintf(*str, format, args); 23 | va_end(args); 24 | 25 | return size; 26 | } 27 | -------------------------------------------------------------------------------- /asprintf.h: -------------------------------------------------------------------------------- 1 | #ifndef __ASPRINTF_H 2 | 3 | #define __ASPRINTF_H 4 | 5 | int asprintf(char **str, const char *fmt, ...); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | PREFIX = /usr/local 2 | MANPREFIX = $(PREFIX)/share/man 3 | MANDIR = $(MANPREFIX)/man1 4 | 5 | CFLAGS += -std=c99 -Wall -g -D_POSIX_C_SOURCE=200112L -D_XOPEN_SOURCE=500 6 | LDFLAGS += -lxcb -lxcb-ewmh -lxcb-icccm -lwm -lxcb-randr -lxcb-cursor 7 | -------------------------------------------------------------------------------- /man/Makefile: -------------------------------------------------------------------------------- 1 | include ../config.mk 2 | 3 | MAN = ruler.1 4 | MANUAL = "Ruler Manual" 5 | ORG = "Ruler" 6 | 7 | .POSIX: 8 | 9 | all: build 10 | 11 | build: $(MAN) 12 | 13 | %.1: *.1.md 14 | ronn --manual=$(MANUAL) --organization=$(ORG) $@.md 15 | 16 | clean: 17 | rm *.1 18 | rm *.1.html 19 | 20 | install: 21 | mkdir -p $(DESTDIR)$(MANDIR)/ 22 | cp -f $(MAN) $(DESTDIR)$(MANDIR)/ 23 | 24 | uninstall: 25 | @echo "uninstalling manpages" 26 | @for page in $(MAN); do \ 27 | rm -f $(DESTDIR)$(MANDIR)/$$page; \ 28 | done 29 | -------------------------------------------------------------------------------- /man/ruler.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "RULER" "1" "February 2017" "Ruler" "Ruler Manual" 5 | . 6 | .SH "NAME" 7 | \fBruler\fR \- A window rule daemon 8 | . 9 | .SH "SYNOPSIS" 10 | \fBruler\fR [\-himopv] [\-s \fIshell\fR] \fIfilename\fR [\fIfilename\fR\.\.\.] 11 | . 12 | .SH "DESCRIPTION" 13 | \fBruler\fR is an X daemon that executes arbitrary commands for windows with specific windows, called \fIrules\fR\. 14 | . 15 | .SH "OPTIONS" 16 | . 17 | .TP 18 | \fB\-h\fR 19 | Print usage\. 20 | . 21 | .TP 22 | \fB\-i\fR 23 | Ignore case in rule descriptors\. 24 | . 25 | .TP 26 | \fB\-m\fR 27 | Apply rules when windows are mapped\. 28 | . 29 | .TP 30 | \fB\-o\fR 31 | Apply rules on windows with \fIoverride_redirect\fR set, like panels and docks\. 32 | . 33 | .TP 34 | \fB\-p\fR 35 | Apply rules when windows change their properties\. 36 | . 37 | .TP 38 | \fB\-s\fR \fIshell\fR 39 | Execute rule commands with \fIshell\fR\. 40 | . 41 | .TP 42 | \fB\-v\fR 43 | Print version information\. 44 | . 45 | .SH "BEHAVIOR" 46 | \fBruler\fR is a program that listens to X window events and applies a set of rules on windows that match them\. A rule is made from two parts: a list of descriptors and a command, that is piped to an interpreter (\fB$SHELL\fR by default)\. 47 | . 48 | .P 49 | A descriptor is a criterion \- regular expression pair\. The criterion defines the property to be matched\. 50 | . 51 | .P 52 | \fBruler\fR reads its configuration file from \fB$XDG_CONFIG_HOME/ruler/rulerrc\fR by default, or from the command line if specified\. If \fB$XDG_CONFIG_HOME\fR is not defined, \fB$HOME/\.config/ruler/rulerrc\fR is used\. 53 | . 54 | .P 55 | If \fBruler\fR receives \fBSIGUSR1\fR or \fBSIGUSR2\fR, it will reload the specified configuration files or pause rule detection respectively\. 56 | . 57 | .P 58 | Commands are executed by piping them to the interpreter\. (like \fBecho "COMMAND" | $SHELL\fR)\. The chosen shell is by default \fB$SHELL\fR\. 59 | . 60 | .P 61 | Rules are executed after a window is created\. This behavior can be changed with the \fB\-m\fR and \fB\-p\fR flags\. 62 | . 63 | .SH "CONFIGURATION" 64 | Each line of the configuration file is interpreted like so: 65 | . 66 | .IP "\(bu" 4 67 | If it starts with a \fB#\fR, it is ignored\. 68 | . 69 | .IP "\(bu" 4 70 | If it is indented with tabs and/or spaces, it is interpreted as a command\. 71 | . 72 | .IP "\(bu" 4 73 | Else, it\'s a descriptor\. 74 | . 75 | .IP "" 0 76 | . 77 | .P 78 | A descriptor list \- command pair is called a \fIrule\fR\. Rules can have newlines between them\. 79 | . 80 | .P 81 | Syntax: 82 | . 83 | .IP "" 4 84 | . 85 | .nf 86 | 87 | DESCRIPTOR 88 | [;]COMMAND 89 | 90 | DESCRIPTOR := CRITERION_1=STRING_1 CRITERION_2=STRING_2 \.\.\. 91 | CRITERION_i := class | instance | type | name | role 92 | . 93 | .fi 94 | . 95 | .IP "" 0 96 | . 97 | .P 98 | \fBSTRING_i\fR is any string enclosed between double quotes (\fB"\fR)\. 99 | . 100 | .P 101 | \fBSTRING_i\fR and \fBCOMMAND\fR can contain escaped characters, preceded by a \fB\e\fR\. You can have multi\-line commands this way\. 102 | . 103 | .P 104 | \fBSTRING_i\fR is a POSIX extended regular expression\. 105 | . 106 | .P 107 | If \fBCOMMAND\fR is preceded by a \fB;\fR, the command will be run synchronously, otherwise it will be run asynchronously\. 108 | . 109 | .P 110 | \fBCOMMAND\fR will be executed by the shell set in the \fBSHELL\fR environment variable\. The window id will be set in the \fBRULER_WID\fR environment variable\. 111 | . 112 | .P 113 | \fBCRITERION\fR can be: 114 | . 115 | .IP "\(bu" 4 116 | \fBclass\fR \- the second part of \fBWM_CLASS\fR window property\. 117 | . 118 | .IP "\(bu" 4 119 | \fBinstance\fR \- the first part of \fBWM_CLASS\fR window property\. 120 | . 121 | .IP "\(bu" 4 122 | \fBtype\fR \- the \fB_NET_WM_WINDOW_TYPE\fR window property\. \fBVALUE\fR can be a combination of \fBdesktop\fR, \fBdock\fR, \fBtoolbar\fR, \fBmenu\fR, \fButility\fR, \fBsplash\fR, \fBdialog\fR, \fBdropdown_menu\fR, \fBpopup_menu\fR, \fBtooltip\fR, \fBnotification\fR, \fBcombo\fR, \fBdnd\fR, \fBnormal\fR, separated by commas\. 123 | . 124 | .IP 125 | Example value: \fBcombo,dnd\fR\. 126 | . 127 | .IP "\(bu" 4 128 | \fBname\fR \- the X11 window title (\fB_NET_WM_NAME\fR and \fBWM_NAME\fR properties, the latter used as a fallback)\. 129 | . 130 | .IP "\(bu" 4 131 | \fBrole\fR \- window role (\fBWM_WINDOW_ROLE\fR property) 132 | . 133 | .IP "" 0 134 | . 135 | .SH "EXAMPLE" 136 | . 137 | .nf 138 | 139 | # assign all browsers to group 2 140 | role="browser" 141 | wtf "$RULER_WID" && waitron group_add_window 2 142 | 143 | # say hello if a window is created, synchronously 144 | name="\.*" 145 | ;echo "Hello!";\e 146 | echo "How are you?" 147 | . 148 | .fi 149 | . 150 | .SH "ENVIRONMENT" 151 | \fBruler\fR acts on the X display specified by the \fBDISPLAY\fR variable and executes commands through the shell specified by \fBSHELL\fR\. 152 | . 153 | .SH "AUTHOR" 154 | Tudor Roman \fB\fR 155 | . 156 | .SH "SEE ALSO" 157 | wmutils(1), regex(7) 158 | -------------------------------------------------------------------------------- /man/ruler.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ruler(1) - A window rule daemon 7 | 44 | 45 | 52 | 53 |
54 | 55 | 67 | 68 |
    69 |
  1. ruler(1)
  2. 70 |
  3. Ruler Manual
  4. 71 |
  5. ruler(1)
  6. 72 |
73 | 74 |

NAME

75 |

76 | ruler - A window rule daemon 77 |

78 | 79 |

SYNOPSIS

80 | 81 |

ruler [-himopv] [-s shell] filename [filename...]

82 | 83 |

DESCRIPTION

84 | 85 |

ruler is an X daemon that executes arbitrary commands for windows with 86 | specific windows, called rules.

87 | 88 |

OPTIONS

89 | 90 |
91 |
-h

Print usage.

92 |
-i

Ignore case in rule descriptors.

93 |
-m

Apply rules when windows are mapped.

94 |
-o

Apply rules on windows with override_redirect set, like panels and docks.

95 |
-p

Apply rules when windows change their properties.

96 |
-s shell

Execute rule commands with shell.

97 |
-v

Print version information.

98 |
99 | 100 | 101 |

BEHAVIOR

102 | 103 |

ruler is a program that listens to X window events and applies a set of rules 104 | on windows that match them. A rule is made from two parts: a list of descriptors and a 105 | command, that is piped to an interpreter ($SHELL by default).

106 | 107 |

A descriptor is a criterion - regular expression pair. The criterion defines the property to 108 | be matched.

109 | 110 |

ruler reads its configuration file from $XDG_CONFIG_HOME/ruler/rulerrc by 111 | default, or from the command line if specified. If $XDG_CONFIG_HOME is not 112 | defined, $HOME/.config/ruler/rulerrc is used.

113 | 114 |

If ruler receives SIGUSR1 or SIGUSR2, it will reload the specified 115 | configuration files or pause rule detection respectively.

116 | 117 |

Commands are executed by piping them to the interpreter. (like echo "COMMAND" | 118 | $SHELL). The chosen shell is by default $SHELL.

119 | 120 |

Rules are executed after a window is created. This behavior can be changed with 121 | the -m and -p flags.

122 | 123 |

CONFIGURATION

124 | 125 |

Each line of the configuration file is interpreted like so:

126 | 127 |
    128 |
  • If it starts with a #, it is ignored.

  • 129 |
  • If it is indented with tabs and/or spaces, it is interpreted as a command.

  • 130 |
  • Else, it's a descriptor.

  • 131 |
132 | 133 | 134 |

A descriptor list - command pair is called a rule. Rules can have newlines 135 | between them.

136 | 137 |

Syntax:

138 | 139 |
DESCRIPTOR
140 |     [;]COMMAND
141 | 
142 | DESCRIPTOR := CRITERION_1=STRING_1 CRITERION_2=STRING_2 ...
143 | CRITERION_i := class | instance | type | name | role
144 | 
145 | 146 |

STRING_i is any string enclosed between double quotes (").

147 | 148 |

STRING_i and COMMAND can contain escaped characters, preceded by a \. You 149 | can have multi-line commands this way.

150 | 151 |

STRING_i is a POSIX extended regular expression.

152 | 153 |

If COMMAND is preceded by a ;, the command will be run synchronously, 154 | otherwise it will be run asynchronously.

155 | 156 |

COMMAND will be executed by the shell set in the SHELL environment 157 | variable. The window id will be set in the RULER_WID environment variable.

158 | 159 |

CRITERION can be:

160 | 161 |
    162 |
  • class - the second part of WM_CLASS window property.

  • 163 |
  • instance - the first part of WM_CLASS window property.

  • 164 |
  • type - the _NET_WM_WINDOW_TYPE window property. VALUE can be a 165 | combination of 166 | desktop, dock, toolbar, menu, utility, splash, dialog, 167 | dropdown_menu, popup_menu, tooltip, notification, combo, dnd, 168 | normal, separated by commas.

    169 | 170 |

    Example value: combo,dnd.

  • 171 |
  • name - the X11 window title (_NET_WM_NAME and WM_NAME properties, the 172 | latter used as a fallback).

  • 173 |
  • role - window role (WM_WINDOW_ROLE property)

  • 174 |
175 | 176 | 177 |

EXAMPLE

178 | 179 |
# assign all browsers to group 2
180 | role="browser"
181 |     wtf "$RULER_WID" && waitron group_add_window 2
182 | 
183 | # say hello if a window is created, synchronously
184 | name=".*"
185 |     ;echo "Hello!";\
186 |         echo "How are you?"
187 | 
188 | 189 |

ENVIRONMENT

190 | 191 |

ruler acts on the X display specified by the DISPLAY variable and executes 192 | commands through the shell specified by SHELL.

193 | 194 |

AUTHOR

195 | 196 |

Tudor Roman <tudurom at gmail dot com>

197 | 198 |

SEE ALSO

199 | 200 |

wmutils(1), regex(7)

201 | 202 | 203 |
    204 |
  1. Ruler
  2. 205 |
  3. February 2017
  4. 206 |
  5. ruler(1)
  6. 207 |
208 | 209 |
210 | 211 | 212 | -------------------------------------------------------------------------------- /man/ruler.1.md: -------------------------------------------------------------------------------- 1 | ruler(1) -- A window rule daemon 2 | ================================ 3 | 4 | ## SYNOPSIS 5 | 6 | `ruler` [-himopv] [-s ] [...] 7 | 8 | ## DESCRIPTION 9 | 10 | `ruler` is an X daemon that executes arbitrary commands for windows with 11 | specific windows, called *rules*. 12 | 13 | ## OPTIONS 14 | 15 | * `-h`: 16 | Print usage. 17 | 18 | * `-i`: 19 | Ignore case in rule descriptors. 20 | 21 | * `-m`: 22 | Apply rules when windows are mapped. 23 | 24 | * `-o`: 25 | Apply rules on windows with *override_redirect* set, like panels and docks. 26 | 27 | * `-p`: 28 | Apply rules when windows change their properties. 29 | 30 | * `-s` : 31 | Execute rule commands with . 32 | 33 | * `-v`: 34 | Print version information. 35 | 36 | ## BEHAVIOR 37 | 38 | `ruler` is a program that listens to X window events and applies a set of rules 39 | on windows that match them. A rule is made from two parts: a list of descriptors and a 40 | command, that is piped to an interpreter (`$SHELL` by default). 41 | 42 | A descriptor is a criterion - regular expression pair. The criterion defines the property to 43 | be matched. 44 | 45 | `ruler` reads its configuration file from `$XDG_CONFIG_HOME/ruler/rulerrc` by 46 | default, or from the command line if specified. If `$XDG_CONFIG_HOME` is not 47 | defined, `$HOME/.config/ruler/rulerrc` is used. 48 | 49 | If `ruler` receives `SIGUSR1` or `SIGUSR2`, it will reload the specified 50 | configuration files or pause rule detection respectively. 51 | 52 | Commands are executed by piping them to the interpreter. (like `echo "COMMAND" | 53 | $SHELL`). The chosen shell is by default `$SHELL`. 54 | 55 | Rules are executed after a window is created. This behavior can be changed with 56 | the `-m` and `-p` flags. 57 | 58 | ## CONFIGURATION 59 | 60 | Each line of the configuration file is interpreted like so: 61 | 62 | * If it starts with a `#`, it is ignored. 63 | 64 | * If it is indented with tabs and/or spaces, it is interpreted as a command. 65 | 66 | * Else, it's a descriptor. 67 | 68 | A descriptor list - command pair is called a *rule*. Rules can have newlines 69 | between them. 70 | 71 | Syntax: 72 | 73 | ``` 74 | DESCRIPTOR 75 | [;]COMMAND 76 | 77 | DESCRIPTOR := CRITERION_1=STRING_1 CRITERION_2=STRING_2 ... 78 | CRITERION_i := class | instance | type | name | role 79 | ``` 80 | 81 | `STRING_i` is any string enclosed between double quotes (`"`). 82 | 83 | `STRING_i` and `COMMAND` can contain escaped characters, preceded by a `\`. You 84 | can have multi-line commands this way. 85 | 86 | `STRING_i` is a POSIX extended regular expression. 87 | 88 | If `COMMAND` is preceded by a `;`, the command will be run synchronously, 89 | otherwise it will be run asynchronously. 90 | 91 | `COMMAND` will be executed by the shell set in the `SHELL` environment 92 | variable. The window id will be set in the `RULER_WID` environment variable. 93 | 94 | `CRITERION` can be: 95 | 96 | * `class` - the second part of `WM_CLASS` window property. 97 | 98 | * `instance` - the first part of `WM_CLASS` window property. 99 | 100 | * `type` - the `_NET_WM_WINDOW_TYPE` window property. `VALUE` can be a 101 | combination of 102 | `desktop`, `dock`, `toolbar`, `menu`, `utility`, `splash`, `dialog`, 103 | `dropdown_menu`, `popup_menu`, `tooltip`, `notification`, `combo`, `dnd`, 104 | `normal`, separated by commas. 105 | 106 | Example value: `combo,dnd`. 107 | 108 | * `name` - the X11 window title (`_NET_WM_NAME` and `WM_NAME` properties, the 109 | latter used as a fallback). 110 | 111 | * `role` - window role (`WM_WINDOW_ROLE` property) 112 | 113 | ## EXAMPLE 114 | 115 | ```c 116 | # assign all browsers to group 2 117 | role="browser" 118 | wtf "$RULER_WID" && waitron group_add_window 2 119 | 120 | # say hello if a window is created, synchronously 121 | name=".*" 122 | ;echo "Hello!";\ 123 | echo "How are you?" 124 | ``` 125 | 126 | ## ENVIRONMENT 127 | 128 | `ruler` acts on the X display specified by the `DISPLAY` variable and executes 129 | commands through the shell specified by `SHELL`. 130 | 131 | ## AUTHOR 132 | 133 | Tudor Roman `` 134 | 135 | ## SEE ALSO 136 | 137 | wmutils(1), regex(7) 138 | -------------------------------------------------------------------------------- /parser.y: -------------------------------------------------------------------------------- 1 | %{ 2 | #include 3 | #include 4 | 5 | #include "ruler.h" 6 | 7 | #define YYSTYPE char * 8 | %} 9 | 10 | %token CRITERION EQUALS STRING NEWLINE COMMAND END 11 | %start block_list 12 | %defines 13 | %error-verbose 14 | 15 | %% 16 | block_list: block 17 | | block_list NEWLINE 18 | | block_list block 19 | | block_list END 20 | { return 0; } 21 | ; 22 | 23 | block: descriptor_list command 24 | { 25 | block(); 26 | } 27 | ; 28 | 29 | descriptor_list: descriptor 30 | | descriptor_list descriptor 31 | | descriptor_list NEWLINE 32 | ; 33 | 34 | command: COMMAND 35 | { 36 | comm($1); 37 | } 38 | ; 39 | 40 | descriptor: CRITERION EQUALS STRING 41 | { 42 | desc($1, $3); 43 | } 44 | ; 45 | %% 46 | 47 | void 48 | yyerror(const char *str) 49 | { 50 | fprintf(stderr, "error: %s\n", str); 51 | } 52 | 53 | int 54 | yywrap() 55 | { 56 | return 1; 57 | } 58 | -------------------------------------------------------------------------------- /ruler.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "arg.h" 19 | #include "asprintf.h" 20 | #include "ruler.h" 21 | 22 | extern FILE * yyin; 23 | 24 | struct list *last_d = NULL; 25 | struct list *block_list = NULL; 26 | struct list *win_list = NULL; 27 | 28 | command_t last_c; 29 | extern char **environ; 30 | 31 | const int _debug = DEBUG; 32 | struct conf conf; 33 | 34 | int state_run = 0, state_reload = 0, state_pause = 0; 35 | 36 | char *argv0; 37 | char **configs; 38 | int no_of_configs; 39 | 40 | xcb_connection_t *conn; 41 | xcb_screen_t *scrn; 42 | xcb_ewmh_connection_t *ewmh; 43 | 44 | xcb_atom_t allowed_atoms[NR_ATOMS]; 45 | 46 | void 47 | print_usage(const char *program_name, int exit_value) 48 | { 49 | fprintf(stderr, "Usage: %s [-himopv] [-s shell] filename [filename...]\n", program_name); 50 | exit(exit_value); 51 | } 52 | 53 | void print_version(void) 54 | { 55 | fprintf(stderr, "%s %s\n", NAME, VERSION); 56 | fprintf(stderr, "Copyright (c) 2017 Tudor Ioan Roman\n"); 57 | fprintf(stderr, "Released under the ISC License\n"); 58 | exit(0); 59 | } 60 | 61 | char * 62 | strip_quotes(char *str) 63 | { 64 | /* length of original string - 2 (two quotes) + 1 (a null terminator) */ 65 | int len = strlen(str) - 2; 66 | 67 | /* check if string needs to be stripped */ 68 | if (str[0] != '"' || str[len + 1] != '"') 69 | return str; 70 | 71 | char *s = malloc((len + 1) * sizeof(char)); 72 | int i; 73 | 74 | for (i = 1; i < strlen(str) - 1; i++) { 75 | s[i - 1] = str[i]; 76 | } 77 | s[len] = '\0'; 78 | free(str); 79 | 80 | return s; 81 | } 82 | 83 | struct descriptor * 84 | new_descriptor(char *criterion, char *str) 85 | { 86 | struct descriptor *d = malloc(sizeof(struct descriptor)); 87 | int status; 88 | str = strip_quotes(str); 89 | 90 | /* convert criterion from string form to enum form */ 91 | #define MATCH_CRIT(c, C) if (strcmp(criterion, #c) == 0) d->criterion = CRIT_##C 92 | MATCH_CRIT(class, CLASS); 93 | MATCH_CRIT(instance, INSTANCE); 94 | MATCH_CRIT(type, TYPE); 95 | MATCH_CRIT(name, NAME); 96 | MATCH_CRIT(role, ROLE); 97 | #undef MATCH_CRIT 98 | 99 | d->str = str; 100 | d->reg = malloc(sizeof(regex_t)); 101 | 102 | DMSG("new regex from `%s`\n", str); 103 | status = regcomp(d->reg, str, REGEX_FLAGS | (conf.case_insensitive * REG_ICASE)); 104 | if (status != 0) { 105 | warnx("couldn't compile regex for %s=\"%s\". Check your regex.", criterion, str); 106 | regfree(d->reg); 107 | free(d->reg); 108 | d->reg = NULL; 109 | } 110 | 111 | return d; 112 | } 113 | 114 | char * 115 | criterion_to_string(enum criterion c) 116 | { 117 | char *s; 118 | switch (c) { 119 | case CRIT_CLASS: s = "class"; break; 120 | case CRIT_INSTANCE: s = "instance"; break; 121 | case CRIT_TYPE: s = "type"; break; 122 | case CRIT_NAME: s = "name"; break; 123 | case CRIT_ROLE: s = "role"; break; 124 | } 125 | 126 | return strdup(s); 127 | } 128 | 129 | /* 130 | * Create descriptor and add it to the list. 131 | */ 132 | void 133 | desc(char *crit, char *str) 134 | { 135 | struct descriptor *d = new_descriptor(crit, str); 136 | list_add(&last_d, d); 137 | } 138 | 139 | /* 140 | * Free descriptor. 141 | */ 142 | void descriptor_free(struct descriptor *d) 143 | { 144 | free(d->str); 145 | free(d); 146 | } 147 | 148 | /* 149 | * Add node to the back of the list. 150 | */ 151 | void 152 | list_add(struct list **list, void *n) 153 | { 154 | struct list *to_alloc; 155 | 156 | if (*list == NULL) 157 | to_alloc = *list; 158 | else 159 | to_alloc = (*list)->prev; 160 | 161 | to_alloc = malloc(sizeof(struct list)); 162 | to_alloc->n = n; 163 | 164 | if (*list == NULL) { 165 | to_alloc->next = to_alloc->prev = NULL; 166 | } else { 167 | to_alloc->next = *list; 168 | (*list)->prev = to_alloc; 169 | to_alloc->prev = NULL; 170 | } 171 | 172 | *list = to_alloc; 173 | } 174 | 175 | /* 176 | * Find node and delete it if it exists. 177 | */ 178 | void 179 | list_delete(struct list **list, struct list *item) 180 | { 181 | struct list *prev; 182 | 183 | if (*list == NULL || item == NULL) 184 | return; 185 | 186 | prev = *list; 187 | while (prev != NULL && prev->next != item) 188 | prev = prev->next; 189 | 190 | if (prev != NULL) 191 | prev->next = item->next; 192 | if (item == *list) 193 | *list = item->next; 194 | free(item->n); 195 | free(item); 196 | } 197 | 198 | /* 199 | * Free all nodes of the list. 200 | */ 201 | void 202 | list_free(struct list **list) 203 | { 204 | struct list *d, *next; 205 | for (d = *list; d != NULL; d = next) { 206 | next = d->next; 207 | free(d); 208 | } 209 | 210 | *list = NULL; 211 | } 212 | 213 | command_t 214 | new_command(char *comm) 215 | { 216 | return comm; 217 | } 218 | 219 | void 220 | comm(char *comm) 221 | { 222 | command_t c = new_command(comm); 223 | last_c = c; 224 | } 225 | 226 | /* 227 | * Create a new block from a list of descriptors and a command. 228 | */ 229 | struct block * 230 | new_block(struct list *d, command_t c) 231 | { 232 | struct block *b = malloc(sizeof(struct block)); 233 | b->d = d; 234 | b->c = c; 235 | 236 | return b; 237 | } 238 | 239 | /* 240 | * Create a new block from the last list of descriptors 241 | * and the last command. 242 | */ 243 | void 244 | block(void) 245 | { 246 | struct block *b = new_block(last_d, last_c); 247 | last_d = NULL; 248 | last_c = NULL; 249 | list_add(&block_list, b); 250 | } 251 | 252 | /* 253 | * Free block. 254 | */ 255 | void 256 | block_free(struct block *b) 257 | { 258 | /* list of descriptors */ 259 | struct list *desc_list = b->d; 260 | struct list *node; 261 | 262 | free(b->c); 263 | for (node = desc_list; node != NULL; node = node->next) { 264 | struct descriptor *desc = node->n; 265 | descriptor_free(desc); 266 | } 267 | free(desc_list); 268 | free(b); 269 | } 270 | 271 | /* 272 | * Return empty win_props structure. 273 | */ 274 | struct win_props * 275 | new_win_props(void) 276 | { 277 | struct win_props *p = malloc(sizeof(struct win_props)); 278 | p->name = p->role = p->instance = 279 | p->class = p->type = NULL; 280 | 281 | return p; 282 | } 283 | 284 | /* 285 | * Free strings of win_props. 286 | */ 287 | void 288 | free_win_props(struct win_props *p) 289 | { 290 | free(p->class); 291 | free(p->instance); 292 | free(p->type); 293 | free(p->name); 294 | free(p->role); 295 | free(p); 296 | } 297 | 298 | /* 299 | * For debugging. Print window properties. 300 | */ 301 | void 302 | print_win_props(struct win_props *p) 303 | { 304 | DMSG("name: \"%s\"\tclass: \"%s\"\tinstance: \"%s\"\ttype: \"%s\"\trole: \"%s\"\n", p->name, 305 | p->class, p->instance, p->type, p->role); 306 | } 307 | 308 | /* 309 | * Init ewmh connection. 310 | */ 311 | void 312 | init_ewmh(void) 313 | { 314 | ewmh = malloc(sizeof(xcb_ewmh_connection_t)); 315 | if (!ewmh) 316 | warnx("couldn't set up ewmh connection"); 317 | xcb_ewmh_init_atoms_replies(ewmh, 318 | xcb_ewmh_init_atoms(conn, ewmh), NULL); 319 | } 320 | 321 | /* 322 | * Get atom by name. 323 | */ 324 | xcb_atom_t 325 | get_atom(const char *name) 326 | { 327 | xcb_intern_atom_reply_t *r = 328 | xcb_intern_atom_reply(conn, 329 | xcb_intern_atom(conn, 0, strlen(name), name), 330 | NULL); 331 | xcb_atom_t atom; 332 | 333 | if (!r) { 334 | warnx("couldn't get atom '%s'\n", name); 335 | return XCB_ATOM_STRING; 336 | } 337 | atom = r->atom; 338 | free(r); 339 | 340 | return atom; 341 | } 342 | 343 | /* 344 | * Populate the list of allowed atoms. 345 | */ 346 | void 347 | populate_allowed_atoms(void) 348 | { 349 | int i; 350 | 351 | for (i = 0; i < NR_ATOMS; i++) { 352 | allowed_atoms[i] = get_atom(atom_names[i]); 353 | } 354 | } 355 | 356 | /* 357 | * Convert window type atom to string form. 358 | */ 359 | char * 360 | window_type_to_string(xcb_ewmh_get_atoms_reply_t *reply) 361 | { 362 | int i; 363 | char *atom_name = NULL; 364 | char *str; 365 | 366 | if (reply != NULL) { 367 | str = malloc((WINDOW_TYPE_STRING_LENGTH + 1) * sizeof(char)); 368 | str[0] = '\0'; 369 | for (i = 0; i < reply->atoms_len; i++) { 370 | xcb_atom_t a = reply->atoms[i]; 371 | #define WT_STRING(type, s) if (a == ewmh->_NET_WM_WINDOW_TYPE_##type) atom_name = strdup(#s); 372 | WT_STRING(DESKTOP, desktop) 373 | WT_STRING(DOCK, dock) 374 | WT_STRING(TOOLBAR, toolbar) 375 | WT_STRING(MENU, menu) 376 | WT_STRING(UTILITY, utility) 377 | WT_STRING(SPLASH, splash) 378 | WT_STRING(DIALOG, dialog) 379 | WT_STRING(DROPDOWN_MENU, dropdown_menu) 380 | WT_STRING(POPUP_MENU, popup_menu) 381 | WT_STRING(TOOLTIP, tooltip) 382 | WT_STRING(NOTIFICATION, notification) 383 | WT_STRING(COMBO, combo) 384 | WT_STRING(DND, dnd) 385 | WT_STRING(NORMAL, normal) 386 | #undef WT_STRING 387 | 388 | if (atom_name != NULL) { 389 | if (i == 0) 390 | sprintf(str, "%s", atom_name); 391 | else 392 | sprintf(str, "%s,%s", str, atom_name); 393 | } 394 | 395 | free(atom_name); 396 | atom_name = NULL; 397 | } 398 | } else { 399 | str = strdup(""); 400 | } 401 | 402 | return str; 403 | } 404 | 405 | /* 406 | * Get string property of window by atom. 407 | */ 408 | char * 409 | get_string_prop(xcb_window_t win, xcb_atom_t prop, int utf8) 410 | { 411 | char *p, *value; 412 | int len = 0; 413 | xcb_get_property_cookie_t c; 414 | xcb_get_property_reply_t *r = NULL; 415 | xcb_atom_t type; 416 | 417 | if (utf8) 418 | type = ewmh->UTF8_STRING; 419 | else 420 | type = XCB_ATOM_STRING; 421 | 422 | c = xcb_get_property(conn, 0, win, 423 | prop, type, 0L, 4294967295L); 424 | r = xcb_get_property_reply(conn, c, NULL); 425 | 426 | if (r == NULL || xcb_get_property_value_length(r) == 0) { 427 | p = strdup(""); 428 | DMSG("unable to get window property for 0x%08x\n", win); 429 | } else { 430 | len = xcb_get_property_value_length(r); 431 | p = malloc((len + 1) * sizeof(char)); 432 | value = xcb_get_property_value(r); 433 | strncpy(p, value, len); 434 | p[len] = '\0'; 435 | } 436 | free(r); 437 | 438 | return p; 439 | } 440 | 441 | /* 442 | * Fill win_props structure. 443 | */ 444 | struct win_props * 445 | get_props(xcb_window_t win) 446 | { 447 | struct win_props *p = new_win_props(); 448 | int status; 449 | xcb_get_property_cookie_t c_class, c_type; 450 | xcb_icccm_get_wm_class_reply_t *r_class; 451 | xcb_ewmh_get_atoms_reply_t *r_type; 452 | 453 | /* WM_CLASS */ 454 | c_class = xcb_icccm_get_wm_class(conn, win); 455 | r_class = malloc(sizeof(xcb_icccm_get_wm_class_reply_t)); 456 | status = xcb_icccm_get_wm_class_reply(conn, c_class, r_class, NULL); 457 | if (status == 1) { 458 | p->class = strdup(r_class->class_name); 459 | p->instance = strdup(r_class->instance_name); 460 | } else { 461 | p->class = strdup(""); 462 | p->instance = strdup(""); 463 | } 464 | free(r_class); 465 | 466 | /* _NET_WM_WINDOW_TYPE */ 467 | c_type = xcb_ewmh_get_wm_window_type(ewmh, win); 468 | r_type = malloc(sizeof(xcb_ewmh_get_atoms_reply_t)); 469 | status = xcb_ewmh_get_wm_window_type_reply(ewmh, c_type, r_type, NULL); 470 | if (status == 1) { 471 | p->type = window_type_to_string(r_type); 472 | } else { 473 | p->type = strdup(""); 474 | } 475 | free(r_type); 476 | 477 | /* WM_NAME */ 478 | p->name = get_string_prop(win, ewmh->_NET_WM_NAME, 1); 479 | if (p->name[0] == '\0') { 480 | free(p->name); 481 | p->name = get_string_prop(win, allowed_atoms[ATOM_WM_NAME], 0); 482 | } 483 | 484 | /* WM_WINDOW_ROLE */ 485 | p->role = get_string_prop(win, get_atom("WM_WINDOW_ROLE"), 0); 486 | 487 | return p; 488 | } 489 | 490 | /* 491 | * Match window props with descriptor_list. 492 | * 493 | * `l` is a list of descriptor structures. 494 | */ 495 | int 496 | match_props(struct win_props *p, struct list *l) 497 | { 498 | struct list *node; 499 | int status; 500 | char *to_match; 501 | int matched; 502 | 503 | node = l; 504 | status = 0; 505 | matched = 0; 506 | do { 507 | struct descriptor *d = node->n; 508 | switch (d->criterion) { 509 | case CRIT_CLASS: to_match = p->class; break; 510 | case CRIT_INSTANCE: to_match = p->instance; break; 511 | case CRIT_TYPE: to_match = p->type; break; 512 | case CRIT_NAME: to_match = p->name; break; 513 | case CRIT_ROLE: to_match = p->role; break; 514 | default: warnx("this is a bug, report it ASAP. (%s: line %d)", __FILE__, __LINE__); to_match = ""; 515 | } 516 | 517 | /* avoid crash if regex failed to compile */ 518 | if (d->reg != NULL) 519 | status = regexec(d->reg, to_match, 0, NULL, 0); 520 | else 521 | status = 1; 522 | DMSG("match \"%s\" (%s): %d\n", to_match, criterion_to_string(d->criterion), status); 523 | matched += (status == 0) * 1; 524 | 525 | node = node->next; 526 | } while (node != NULL && status == 0); 527 | 528 | return (node == NULL) * matched; 529 | } 530 | 531 | /* 532 | * Find matching blocks for a window and put them in the `blocks` list. 533 | */ 534 | void 535 | find_matching_blocks(struct win_props *p, struct list *l, struct list **blocks) 536 | { 537 | struct list *node; 538 | int m; 539 | 540 | if (*blocks != NULL) 541 | list_free(blocks); 542 | 543 | for (node = l; node != NULL; node = node->next) { 544 | struct block *b = node->n; 545 | struct list *desc_list = b->d; 546 | DMSG("trying new block\n"); 547 | m = match_props(p, desc_list); 548 | if (m > 0) { 549 | list_add(blocks, b); 550 | } 551 | } 552 | } 553 | 554 | /* 555 | * Execute program with arguments. 556 | * 557 | * cmd is an array of strings representing the 558 | * executable and the arguments. The last element 559 | * of cmd must be NULL. 560 | * 561 | * This function should be called in a separate process, 562 | * otherwise the execution of the caller process ends. 563 | * 564 | * See execvp(3). 565 | */ 566 | void 567 | execute(char **cmd) 568 | { 569 | setsid(); 570 | execvp(cmd[0], cmd); 571 | err(1, "command execution failed"); 572 | } 573 | 574 | /* 575 | * Pipe command to shell. 576 | * 577 | * Creates two subprocesses. 578 | * The first prints to stdout the command. 579 | * The second is the shell that interprets the command. 580 | */ 581 | void 582 | spawn(char *shell, command_t cmd) 583 | { 584 | int desc[2]; 585 | if (pipe(desc) == -1) { 586 | warnx("pipe failed"); 587 | return; 588 | } 589 | 590 | if (fork() == 0) { 591 | close(STDOUT_FILENO); 592 | dup(desc[1]); 593 | close(desc[0]); 594 | close(desc[1]); 595 | 596 | fputs(cmd, stdout); 597 | exit(0); 598 | } 599 | 600 | if (fork() == 0) { 601 | close(STDIN_FILENO); 602 | dup(desc[0]); 603 | close(desc[1]); 604 | close(desc[0]); 605 | 606 | char *toexec[] = { shell, NULL }; 607 | execute(toexec); 608 | } 609 | 610 | close(desc[0]); 611 | close(desc[1]); 612 | wait(NULL); 613 | wait(NULL); 614 | } 615 | 616 | /* 617 | * Run command in the given shell. 618 | */ 619 | void run_command(char *shell, command_t cmd, int sync) 620 | { 621 | DMSG("will execute: `%s`\n", cmd); 622 | 623 | if (sync) { 624 | spawn(shell, cmd); 625 | } else { 626 | if (fork() == 0) { 627 | if (conn != NULL) 628 | close(xcb_get_file_descriptor(conn)); 629 | spawn(shell, cmd); 630 | exit(0); 631 | } 632 | } 633 | } 634 | 635 | /* 636 | * Find matching block for a window and execute the command. 637 | */ 638 | void 639 | execute_matching_block(struct win_props *props, struct list *blocks) 640 | { 641 | struct list *matching_blocks = NULL, *node; 642 | struct block *b; 643 | char chr; 644 | int i, skip, sync; 645 | 646 | find_matching_blocks(props, blocks, &matching_blocks); 647 | if (matching_blocks != NULL) { 648 | for (node = matching_blocks; node != NULL; node = node->next) { 649 | b = node->n; 650 | i = 0; 651 | while ((chr = b->c[i]) != '\0' && isblank(chr)) 652 | i++; 653 | 654 | if (chr == '\0') { 655 | warnx("either the supplied file is strange " 656 | "or this is a bug and you should report it ASAP " 657 | "(%s: line %d", __FILE__, __LINE__); 658 | return; 659 | } 660 | 661 | sync = 0; 662 | skip = i; 663 | if (chr == ';') { 664 | sync = 1; 665 | skip++; 666 | } 667 | run_command(conf.shell, b->c + skip, sync); 668 | } 669 | list_free(&matching_blocks); 670 | } 671 | } 672 | 673 | /* 674 | * Register events on existing windows. 675 | */ 676 | void 677 | register_events(void) 678 | { 679 | xcb_window_t *windows; 680 | int len, i; 681 | 682 | len = wm_get_windows(scrn->root, &windows); 683 | for (i = 0; i < len; i++) { 684 | if (wm_is_listable(windows[i], 0)) 685 | wm_reg_window_event(windows[i], XCB_EVENT_MASK_PROPERTY_CHANGE); 686 | } 687 | free(windows); 688 | } 689 | 690 | void 691 | set_environ(xcb_window_t win) 692 | { 693 | char *wid = malloc((2 + 8 + 1) * sizeof(char)); 694 | sprintf(wid, "0x%08x", win); 695 | setenv(ENV_VARIABLE, wid, 1); 696 | } 697 | 698 | /* 699 | * Handle X events. 700 | */ 701 | void 702 | handle_events(void) 703 | { 704 | xcb_generic_event_t *ev; 705 | xcb_window_t win; 706 | struct win_props *p; 707 | int xcb_desc = xcb_get_file_descriptor(conn); 708 | fd_set descs; 709 | 710 | /* to receive window creation notifications */ 711 | wm_reg_window_event(scrn->root, XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY); 712 | xcb_flush(conn); 713 | 714 | state_run = 1; 715 | state_reload = 0; 716 | state_pause = 0; 717 | while (state_run) { 718 | FD_ZERO(&descs); 719 | FD_SET(xcb_desc, &descs); 720 | 721 | /* 722 | * We can't use xcb_wait_for_event because that means 723 | * that after receiving a signal to exit, the program will wait 724 | * for an X event and then it will exit. 725 | * 726 | * Instead, we are checking if there are events and then handle them. 727 | * select should just fail if we abort the execution of the program, 728 | * thus restarting the loop and then exiting from it because state_run 729 | * will be 0. 730 | */ 731 | if (select(xcb_desc + 1, &descs, NULL, NULL, NULL) > 0) { 732 | while((ev = xcb_poll_for_event(conn)) != NULL) { 733 | win = -1; 734 | 735 | /* do work only if not paused */ 736 | if (state_pause == 0) { 737 | if ((ev->response_type & ~0x80) == XCB_MAP_NOTIFY) { 738 | xcb_map_notify_event_t *ec = (xcb_map_notify_event_t *)ev; 739 | 740 | if ((conf.catch_override_redirect || wm_is_listable(ec->window, 0)) 741 | && (conf.exec_on_map || is_new_window(ec->window))) { 742 | win = ec->window; 743 | DMSG("new window created: 0x%08x\n", win); 744 | 745 | /* we need to get notified for further property changes */ 746 | if (conf.exec_on_prop_change) { 747 | wm_reg_window_event(ec->window, XCB_EVENT_MASK_PROPERTY_CHANGE); 748 | } 749 | } 750 | } else if (conf.exec_on_prop_change && (ev->response_type & ~0x80) == XCB_PROPERTY_NOTIFY) { 751 | xcb_property_notify_event_t *en = (xcb_property_notify_event_t *)ev; 752 | int pos = 0; 753 | while (pos < NR_ATOMS && allowed_atoms[pos] != en->atom) 754 | pos++; 755 | 756 | if (pos < NR_ATOMS && (conf.catch_override_redirect || wm_is_listable(en->window, 0))) 757 | win = en->window; 758 | } else if ((ev->response_type & ~0x80) == XCB_DESTROY_NOTIFY) { 759 | xcb_destroy_notify_event_t *ed = (xcb_destroy_notify_event_t *)ev; 760 | struct list *l = win_list; 761 | 762 | win = ed->window; 763 | while (l != NULL && *(xcb_window_t *)l->n != win) 764 | l = l->next; 765 | 766 | if (l != NULL) { 767 | list_delete(&win_list, l); 768 | DMSG("removed window 0x%08x from list\n", win); 769 | } 770 | win = -1; 771 | } 772 | 773 | /* do the actual work. get props, find matches, execute commands */ 774 | if (win != -1 && state_pause == 0) { 775 | p = get_props(win); 776 | print_win_props(p); 777 | set_environ(win); 778 | execute_matching_block(p, block_list); 779 | free_win_props(p); 780 | } 781 | } 782 | 783 | free(ev); 784 | ev = NULL; 785 | } 786 | } 787 | 788 | if (state_reload) { 789 | reload_config(); 790 | state_reload = 0; 791 | } 792 | 793 | if (xcb_connection_has_error(conn)) { 794 | warnx("X server errored"); 795 | state_run = 0; 796 | } 797 | 798 | } 799 | } 800 | 801 | /* 802 | * Returns 1 if the windows has been created but not destroyed. 803 | * 0 otherwise. 804 | */ 805 | int 806 | is_new_window(xcb_window_t win) 807 | { 808 | struct list *l; 809 | xcb_window_t *w; 810 | 811 | l = win_list; 812 | while (l != NULL && *(w = l->n) != win) 813 | l = l->next; 814 | 815 | if (l == NULL) { 816 | w = malloc(sizeof(xcb_window_t)); 817 | *w = win; 818 | list_add(&win_list, w); 819 | 820 | return 1; 821 | } else { 822 | return 0; 823 | } 824 | } 825 | 826 | void 827 | cleanup(void) 828 | { 829 | struct list *l; 830 | for (l = block_list; l != NULL; l = l->next) { 831 | struct block *b = l->n; 832 | block_free(b); 833 | } 834 | list_free(&block_list); 835 | 836 | for (l = last_d; l != NULL; l = l->next) { 837 | struct descriptor *d = l->n; 838 | descriptor_free(d); 839 | } 840 | list_free(&last_d); 841 | } 842 | 843 | void 844 | init_conf(void) 845 | { 846 | conf.case_insensitive = 0; 847 | conf.shell = getenv("SHELL"); 848 | conf.catch_override_redirect = 0; 849 | conf.exec_on_prop_change = 0; 850 | conf.exec_on_map = 0; 851 | } 852 | 853 | /* 854 | * Signal handler. 855 | */ 856 | void 857 | handle_sig(int sig) 858 | { 859 | switch (sig) { 860 | case SIGHUP: 861 | case SIGINT: 862 | case SIGTERM: 863 | state_run = 0; 864 | break; 865 | case SIGUSR1: 866 | state_reload = 1; 867 | break; 868 | case SIGUSR2: 869 | state_pause = !state_pause; 870 | break; 871 | } 872 | } 873 | 874 | /* 875 | * Returns 0 if parsing succeeded. 876 | */ 877 | int 878 | parse_file(char *fp) 879 | { 880 | yyin = fopen(fp, "r"); 881 | if (yyin == NULL) 882 | return 1; 883 | yyparse(); 884 | fclose(yyin); 885 | yyrestart(yyin); 886 | 887 | return 0; 888 | } 889 | 890 | void 891 | reload_config(void) 892 | { 893 | int i; 894 | char *xdg_home = getenv("XDG_CONFIG_HOME"); 895 | char *xdg_cfg_path; 896 | 897 | cleanup(); 898 | if (xdg_home == NULL) 899 | asprintf(&xdg_home, "%s/.config", getenv("HOME")); 900 | 901 | asprintf(&xdg_cfg_path, "%s/ruler/rulerrc", xdg_home); 902 | if (parse_file(xdg_cfg_path) == 1 && no_of_configs == 0) 903 | errx(1, "couldn't open config file '%s' (%s). No other config files supplied, exiting", xdg_cfg_path, strerror(errno)); 904 | free(xdg_cfg_path); 905 | 906 | for (i = 0; i < no_of_configs; i++) { 907 | if (parse_file(configs[i]) != 0) 908 | err(1, "couldn't open config file '%s'", configs[i]); 909 | } 910 | 911 | DMSG("configs reloaded\n"); 912 | } 913 | 914 | int 915 | main(int argc, char **argv) 916 | { 917 | init_conf(); 918 | 919 | /* see arg.h */ 920 | ARGBEGIN { 921 | case 'i': 922 | conf.case_insensitive = 1; break; 923 | case 's': 924 | conf.shell = EARGF(( 925 | warnx("option 's' requires an argument"), 926 | print_usage(argv0, 1) 927 | )); break; 928 | case 'o': 929 | conf.catch_override_redirect = 1; break; 930 | case 'p': 931 | conf.exec_on_prop_change = 1; break; 932 | case 'm': 933 | conf.exec_on_map = 1; break; 934 | case 'h': 935 | print_usage(argv0, 0); break; 936 | case 'v': 937 | print_version(); break; 938 | } ARGEND 939 | 940 | /* the remaining arguments should be files */ 941 | no_of_configs = argc; 942 | DMSG("%d extra config files\n", no_of_configs); 943 | configs = argv; 944 | reload_config(); 945 | 946 | if (_debug) { 947 | struct list *l; 948 | for (l = block_list; l != NULL; l = l->next) { 949 | struct block *b = l->n; 950 | struct list *ld; 951 | for (ld = b->d; ld != NULL; ld = ld->next) { 952 | struct descriptor *d = ld->n; 953 | char *c = criterion_to_string(d->criterion); 954 | DMSG("%s = \"%s\" ", c, d->str); 955 | free(c); 956 | } 957 | DMSG("\n`%s`\n", b->c); 958 | } 959 | } 960 | 961 | if (wm_init_xcb() == -1) 962 | errx(1, "error while estabilishing connection to the X server"); 963 | if (wm_get_screen() == -1) 964 | errx(1, "couldn't get X screen"); 965 | init_ewmh(); 966 | 967 | /* don't let childrens become zombies. kill them for real (bwahaha) */ 968 | signal(SIGCHLD, SIG_IGN); 969 | /* more signals */ 970 | signal(SIGINT, handle_sig); 971 | signal(SIGHUP, handle_sig); 972 | signal(SIGTERM, handle_sig); 973 | signal(SIGUSR1, handle_sig); 974 | signal(SIGUSR2, handle_sig); 975 | 976 | populate_allowed_atoms(); 977 | register_events(); 978 | handle_events(); 979 | wm_kill_xcb(); 980 | return 0; 981 | } 982 | -------------------------------------------------------------------------------- /ruler.h: -------------------------------------------------------------------------------- 1 | #ifndef __RULER_H 2 | #define __RULER_H 3 | 4 | #include 5 | #include 6 | 7 | #define WINDOW_TYPE_STRING_LENGTH 110 8 | #define REGEX_FLAGS REG_EXTENDED | REG_NOSUB 9 | #define ENV_VARIABLE "RULER_WID" 10 | #define DEBUG 0 11 | 12 | #ifndef NAME 13 | #define NAME "ruler" 14 | #endif 15 | 16 | #ifndef VERSION 17 | #define VERSION "-1" 18 | #endif 19 | 20 | enum { 21 | ATOM_WM_NAME, 22 | ATOM_WM_CLASS, 23 | ATOM_WM_ROLE, 24 | ATOM_WM_TYPE, 25 | NR_ATOMS 26 | }; 27 | 28 | static const char *atom_names[] = { 29 | "WM_NAME", 30 | "WM_CLASS", 31 | "WM_WINDOW_ROLE", 32 | "_NET_WM_WINDOW_TYPE" 33 | }; 34 | 35 | #define DMSG(fmt, ...) if (_debug) { fprintf(stderr, fmt, ##__VA_ARGS__); } 36 | 37 | typedef char * command_t; 38 | 39 | enum criterion { 40 | CRIT_CLASS, 41 | CRIT_INSTANCE, 42 | CRIT_TYPE, 43 | CRIT_NAME, 44 | CRIT_ROLE 45 | }; 46 | 47 | struct descriptor { 48 | enum criterion criterion; 49 | char *str; 50 | regex_t *reg; 51 | }; 52 | 53 | struct list { 54 | void *n; 55 | struct list *next; 56 | struct list *prev; 57 | }; 58 | 59 | struct block { 60 | /* list of descriptors */ 61 | struct list *d; 62 | command_t c; 63 | }; 64 | 65 | struct win_props { 66 | char *class; 67 | char *instance; 68 | char *type; 69 | char *name; 70 | char *role; 71 | }; 72 | 73 | struct conf { 74 | int case_insensitive; 75 | char *shell; 76 | int catch_override_redirect; 77 | int exec_on_prop_change; 78 | int exec_on_map; 79 | }; 80 | 81 | void yyerror(const char *); 82 | int yywrap(void); 83 | int yylex(void); 84 | int yyparse(void); 85 | void yyrestart(FILE *); 86 | 87 | void print_usage(const char *, int); 88 | void print_version(void); 89 | char * strip_quotes(char *); 90 | 91 | struct descriptor * new_descriptor(char *, char *); 92 | void desc(char *, char *); 93 | void descriptor_free(struct descriptor *); 94 | 95 | void list_add(struct list **, void *node); 96 | void list_remove(struct list **, struct list *); 97 | void list_free(struct list **); 98 | 99 | command_t new_command(char *); 100 | void comm(char *); 101 | 102 | struct block * new_block(struct list *, command_t); 103 | void block(void); 104 | void block_free(struct block *); 105 | 106 | struct win_props * new_win_props(void); 107 | void free_win_props(struct win_props *); 108 | void print_win_props(struct win_props *); 109 | 110 | void init_ewmh(void); 111 | xcb_atom_t get_atom(const char *); 112 | void populate_allowed_atoms(void); 113 | 114 | char * window_type_to_string(xcb_ewmh_get_atoms_reply_t *); 115 | char * get_string_prop(xcb_window_t, xcb_atom_t, int); 116 | 117 | struct win_props * get_props(xcb_window_t); 118 | int match_props(struct win_props *, struct list *); 119 | void find_matching_blocks(struct win_props *, struct list *, struct list **); 120 | 121 | void execute(char **); 122 | void spawn(char *, command_t); 123 | void run_command(char *shell, command_t, int); 124 | 125 | void execute_matching_block(struct win_props *, struct list *); 126 | 127 | void register_events(void); 128 | void set_environ(xcb_window_t); 129 | void handle_events(void); 130 | 131 | int is_new_window(xcb_window_t); 132 | 133 | void cleanup(void); 134 | 135 | void init_conf(void); 136 | 137 | void handle_sig(int); 138 | int parse_file(char *); 139 | void reload_config(void); 140 | 141 | #endif 142 | -------------------------------------------------------------------------------- /rulerrc: -------------------------------------------------------------------------------- 1 | # with 2 | # comment #test 3 | role="browser" 4 | wtf "$RULER_WID" && waitron group_add_window 2 5 | 6 | # match all 7 | name=".*" 8 | echo "created window with id $RULER_WID" 9 | 10 | # only works with ignore case (-i) 11 | class="urxvt" 12 | echo "hello urxvt" 13 | -------------------------------------------------------------------------------- /scanner.l: -------------------------------------------------------------------------------- 1 | %{ 2 | #include 3 | 4 | #define YYSTYPE char * 5 | #include "y.tab.h" 6 | 7 | static void skip_comment(void); 8 | static void skip_blank(void); 9 | %} 10 | 11 | %% 12 | ("class"|"instance"|"type"|"name"|"role") yylval = strdup(yytext); return CRITERION; 13 | = return EQUALS; 14 | \"([^\"\\]*(\\.[^\"\\]*)*)\" yylval = strdup(yytext); return STRING; 15 | ^[ \t]+([^\r\n\t\f ](([^\n\\])|(\\(.|\n)))+) yylval=strdup(yytext); return COMMAND; 16 | \n return NEWLINE; 17 | [[:blank:]] skip_blank(); 18 | <> return END; 19 | "#".* skip_comment(); 20 | . { fprintf(stderr, "Ignoring unexpected character '%c'\n", *yytext); } 21 | %% 22 | 23 | static void 24 | skip_comment(void) 25 | { 26 | int c; 27 | 28 | c = input(); 29 | while (c != '\n' && c != EOF) 30 | c = input(); 31 | 32 | if (c == EOF) 33 | unput(c); 34 | } 35 | 36 | static void 37 | skip_blank(void) 38 | { 39 | int c; 40 | 41 | c = input(); 42 | while (isblank(c) && c != EOF) 43 | c = input(); 44 | 45 | unput(c); 46 | } 47 | --------------------------------------------------------------------------------