diff --git a/Makefile.am b/Makefile.am index 16d4493e..fbdb155c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3,6 +3,8 @@ AUTOMAKE_OPTIONS = 1.11.3 ACLOCAL_AMFLAGS = -I libgwater ${ACLOCAL_FLAGS} +AM_YFLAGS = -d + noinst_LIBRARIES = include $(top_srcdir)/libgwater-xcb-nolibtool.mk @@ -27,6 +29,7 @@ rofi_SOURCES=\ source/helper.c\ source/timings.c\ source/history.c\ + source/theme.c\ source/widgets/box.c\ source/widgets/widget.c\ source/widgets/textbox.c\ @@ -43,6 +46,8 @@ rofi_SOURCES=\ source/dialogs/window.c\ source/dialogs/script.c\ source/dialogs/help-keys.c\ + lexer/theme-parser.y\ + lexer/theme-lexer.l\ include/xcb.h\ include/xcb-internal.h\ include/rofi.h\ @@ -55,6 +60,7 @@ rofi_SOURCES=\ include/helper.h\ include/timings.h\ include/history.h\ + include/theme.h\ include/widgets/box.h\ include/widgets/widget.h\ include/widgets/widget-internal.h\ diff --git a/configure.ac b/configure.ac index c5df0ea6..ca32299e 100644 --- a/configure.ac +++ b/configure.ac @@ -3,6 +3,10 @@ AC_INIT([rofi], [1.2.0], [https://github.com/DaveDavenport/rofi/],[],[https://fo AC_CONFIG_SRCDIR([source/rofi.c]) AC_CONFIG_HEADER([config.h]) + +AC_PROG_LEX +AC_PROG_YACC + dnl --------------------------------------------------------------------- dnl Setup automake to be silent and in foreign mode. dnl We want xz distribution diff --git a/doc/themer.md b/doc/themer.md new file mode 100644 index 00000000..46e12927 --- /dev/null +++ b/doc/themer.md @@ -0,0 +1,67 @@ + +Each widget has: + +* Class: Type of widget. + +Example: textbox, scrollbar, separator + +Class are prefixed with a `@` + +* Name: Internal name of the widget. + +Sub-widgets are {Parent}.{Child}. + +Example: Listview, Listview.Even, Listview.Uneven, Listview.Scrollbar + +Names are prefixed with a `#` + +* State: State of widget + +Optional flag(s) indicating state. +Multiple flags can be set. + +Example: Highlight Active Urgent + +States are prefixed with a `!` + +So to set color of Even entry in listview that is highlighted and urgent: + +`@textbox #Listview.Even !Urgent !Highlight` + +Or to indicate all textboxes + +`@textbox !Highlight` + +Class is manditory, name is optional. Name is split on .s. + + +# Internally: + +The theme is represented like a tree: + +class --> name --> name --> state -> state + +The states are sorted alphabetically + +So `@textbox #Listview.Even !Urgent !Highlight` becomes: + +textbox->listview->even -> highlight -> urgent. + +When searching for entries the tree is traversed until deepest node is found. +Missing states are skipped. +Then from there properties are searched going up again. + +Properties are in the form of: + +`name: value` +Each property ends with `;` +Each property has a type. (Boolean, Integer, String, Color) + +A block is enclosed by `{}` + +``` +@textbox #Listview.Even !Urgent !Highlight { + padding: 3; + foreground: #aarrggbb; +} +``` diff --git a/include/theme.h b/include/theme.h new file mode 100644 index 00000000..dae85d9c --- /dev/null +++ b/include/theme.h @@ -0,0 +1,46 @@ +#ifndef THEME_H +#define THEME_H +#include +typedef enum { + P_INTEGER, + P_FLOAT, + P_STRING, + P_BOOLEAN, + P_COLOR +} PropertyType; + +typedef struct { + char *name; + PropertyType type; + union { + int i; + double f; + char *s; + int b; + unsigned int color; + } value; +} Property; + +typedef struct _Widget { + char *name; + + unsigned int num_widgets; + struct _Widget **widgets; + + GHashTable *properties; + + struct _Widget *parent; +} Widget; + +extern Widget *rofi_theme; + +Widget *rofi_theme_find_or_create_class ( Widget *base, const char *class ); + + +void rofi_theme_print ( Widget *widget ); + +Property *rofi_theme_property_create ( PropertyType type ); +void rofi_theme_property_free ( Property *p ); +void rofi_theme_free ( Widget * ); +void rofi_theme_parse_file ( const char *file ); +#endif diff --git a/lexer/theme-lexer.l b/lexer/theme-lexer.l new file mode 100644 index 00000000..4094a256 --- /dev/null +++ b/lexer/theme-lexer.l @@ -0,0 +1,33 @@ +%option noyywrap + +%{ +#include + + +#include "lexer/theme-parser.tab.h" +int yylex(void); +#define YY_DECL int yylex() + +%} + +%% + +"@" { return CLASS;} +"\{" { return BOPEN;} +"\}" { return BCLOSE;} +":" { return PSEP; } +";" { return PCLOSE;} +"." { return NSEP; } +[ \t] ; // ignore all whitespace +[0-9]+\.[0-9]+ { yylval.fval = g_ascii_strtod(yytext, NULL); return T_FLOAT;} +[0-9]+ { yylval.ival = (int)g_ascii_strtoll(yytext, NULL, 10); return T_INT;} +(true|false) { yylval.bval= g_strcmp0(yytext, "true") == 0; return T_BOOLEAN;} +[a-zA-Z0-9]+ { yylval.sval = g_strdup(yytext); return N_STRING;} +\"[a-zA-Z0-9]+\" { yylval.sval = g_strdup(yytext); return T_STRING;} +#[0-9A-Fa-f]+ { yylval.colorval = (unsigned int)strtoull ( &yytext[1], NULL, 16); return T_COLOR;} +[\r\n]+ ; + +<*><> { + yyterminate(); +} +%% diff --git a/lexer/theme-parser.y b/lexer/theme-parser.y new file mode 100644 index 00000000..b30803cc --- /dev/null +++ b/lexer/theme-parser.y @@ -0,0 +1,163 @@ +%locations +%debug +%error-verbose + +%code requires { +#include "theme.h" +} +%{ +#include +#include + +void yyerror(const char* s); +int yylex (void ); + +#include "theme.h" +Widget *rofi_theme = NULL; +%} + +%union { + int ival; + double fval; + char *sval; + int bval; + unsigned int colorval; + Widget *theme; + GList *name_path; + Property *property; + GHashTable *property_list; +} + +%token T_INT +%token T_FLOAT +%token T_STRING +%token N_STRING +%token T_BOOLEAN +%token T_COLOR + +%token CLASS "class"; +%token BOPEN "bracket open"; +%token BCLOSE "bracket close"; +%token PSEP "property separator"; +%token PCLOSE "property close"; +%token NSEP "Name separator"; + +%type class +%type entry +%type pvalue +%type entries +%type start +%type name_path +%type property +%type property_list +%type properties +%type optional_properties +%start start + +%% + +start: + optional_properties + entries { + $$ = $2; + if ( $1 != NULL ) { + $$->properties = $1; + } + } +; +entries: + %empty { + // There is always a base widget. + $$ = rofi_theme = (Widget*)g_malloc0 (sizeof(Widget)); + rofi_theme->name = g_strdup ( "Window" ); + } +| entries entry { $$ = $1; } +; + +entry: + class + name_path + properties +{ + Widget *widget = rofi_theme_find_or_create_class ( rofi_theme , $1 ); + g_free($1); + for ( GList *iter = g_list_first ( $2 ); iter ; iter = g_list_next ( iter ) ) { + widget = rofi_theme_find_or_create_class ( widget, iter->data ); + } + g_list_foreach ( $2, (GFunc)g_free , NULL ); + g_list_free ( $2 ); + if ( widget->properties != NULL ) { + fprintf(stderr, "Properties already set on this widget.\n"); + exit ( EXIT_FAILURE ); + } + widget->properties = $3; + +}; + +/** + * properties + */ +optional_properties + : %empty { $$ = NULL; } + | property_list { $$ = $1; } +; +properties: BOPEN property_list BCLOSE { $$ = $2;} + | BOPEN BCLOSE { $$ = NULL; } + | %empty { $$ = NULL; } + ; + +property_list: + property { + $$ = g_hash_table_new_full ( g_str_hash, g_str_equal, NULL, (GDestroyNotify)rofi_theme_property_free ); + g_hash_table_replace ( $$, $1->name, $1 ); + } +| property_list property { + // Old will be free'ed, and key/value will be replaced. + g_hash_table_replace ( $$, $2->name, $2 ); + } +; + +property +: pvalue PSEP T_INT PCLOSE { + $$ = rofi_theme_property_create ( P_INTEGER ); + $$->name = $1; + $$->value.i = $3; + } +| pvalue PSEP T_FLOAT PCLOSE { + $$ = rofi_theme_property_create ( P_FLOAT ); + $$->name = $1; + $$->value.f = $3; + } +| pvalue PSEP T_COLOR PCLOSE { + $$ = rofi_theme_property_create ( P_COLOR ); + $$->name = $1; + $$->value.color = $3; + } +| pvalue PSEP T_STRING PCLOSE { + $$ = rofi_theme_property_create ( P_STRING ); + $$->name = $1; + $$->value.s = $3; + } +| pvalue PSEP T_BOOLEAN PCLOSE { + $$ = rofi_theme_property_create ( P_BOOLEAN ); + $$->name = $1; + $$->value.b = $3; + } +; + +pvalue: N_STRING { $$ = $1; } + +class: + CLASS N_STRING { $$ = $2; } +; + + +name_path: + %empty { $$ = NULL; } +| N_STRING { $$ = g_list_append ( NULL, $1 );} +| name_path NSEP N_STRING { $$ = g_list_append ( $1, $3);} +; + + +%% + diff --git a/source/rofi.c b/source/rofi.c index f89c64d4..7fb2d28d 100644 --- a/source/rofi.c +++ b/source/rofi.c @@ -64,6 +64,8 @@ #include "gitconfig.h" +#include "theme.h" + // Pidfile. char *pidfile = NULL; const char *cache_dir = NULL; @@ -896,6 +898,12 @@ int main ( int argc, char *argv[] ) // Parse command line for settings, independent of other -no-config. config_parse_cmd_options_dynamic ( ); + char *theme = NULL; + if ( find_arg_str ( "-theme", &theme ) > 0 ){ + rofi_theme_parse_file ( theme ); + rofi_theme_print ( rofi_theme ); + } + // Dump. // catch help request if ( find_arg ( "-h" ) >= 0 || find_arg ( "-help" ) >= 0 || find_arg ( "--help" ) >= 0 ) { diff --git a/source/theme.c b/source/theme.c new file mode 100644 index 00000000..7fb05816 --- /dev/null +++ b/source/theme.c @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include "theme.h" + +void yyerror ( const char *); +Widget *rofi_theme_find_or_create_class ( Widget *base, const char *class ) +{ + for ( unsigned int i = 0; i < base->num_widgets;i++){ + if ( g_strcmp0(base->widgets[i]->name, class) == 0 ){ + return base->widgets[i]; + } + } + + base->widgets = g_realloc ( base->widgets, sizeof(Widget*)*(base->num_widgets+1)); + base->widgets[base->num_widgets] = g_malloc0(sizeof(Widget)); + Widget *retv = base->widgets[base->num_widgets]; + retv->parent = base; + retv->name = g_strdup(class); + base->num_widgets++; + return retv; +} +/** + * Properties + */ +Property *rofi_theme_property_create ( PropertyType type ) +{ + Property *retv = g_malloc0 ( sizeof(Property) ); + retv->type = type; + return retv; +} +void rofi_theme_property_free ( Property *p ) +{ + if ( p == NULL ) { + return; + } + g_free ( p->name ); + if ( p->type == P_STRING ) { + g_free ( p->value.s ); + } + g_free(p); +} + +void rofi_theme_free ( Widget *widget ) +{ + if ( widget == NULL ){ + return; + } + if ( widget->properties ) { + g_hash_table_destroy ( widget->properties ); + } + for ( unsigned int i = 0; i < widget->num_widgets; i++ ){ + rofi_theme_free ( widget->widgets[i] ); + } + g_free ( widget->widgets ); + g_free ( widget->name ); + g_free ( widget ); +} + +/** + * print + */ +static void rofi_theme_print_property_index ( int depth, Property *p ) +{ + printf("%*s %s: ", depth, "", p->name ); + switch ( p->type ) + { + case P_STRING: + printf("\"%s\"", p->value.s); + break; + case P_INTEGER: + printf("%d", p->value.i); + break; + case P_FLOAT: + printf("%.2f", p->value.f); + break; + case P_BOOLEAN: + printf("%s", p->value.b?"true":"false"); + break; + case P_COLOR: + printf("#%08X", p->value.color); + break; + } + putchar ( '\n' ); +} + +static void rofi_theme_print_index ( int depth, Widget *widget ) +{ + printf ( "%*sName: %s \n", depth, "", widget->name ); + + GHashTableIter iter; + gpointer key, value; + if ( widget->properties ){ + g_hash_table_iter_init (&iter, widget->properties); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + Property *p = (Property*)value; + rofi_theme_print_property_index ( depth, p ); + } + } + for ( unsigned int i = 0; i < widget->num_widgets;i++){ + rofi_theme_print_index ( depth+2, widget->widgets[i] ); + } +} +void rofi_theme_print ( Widget *widget ) +{ + rofi_theme_print_index ( 0, widget); +} + +extern int yyparse(); +extern FILE* yyin; +extern Widget *rofi_theme; + +void rofi_theme_parse_file ( const char *file ) +{ + yyin = fopen ( file, "rb"); + if ( yyin == NULL ){ + fprintf(stderr, "Failed to open file: '%s'\n", strerror ( errno ) ); + return; + } + while ( yyparse() ); +} + +void yyerror(const char* s) { + fprintf(stderr, "Parse error: %s\n", s); + exit(EXIT_FAILURE); +}