#!/usr/bin/perl # Usage: # fvwm-theme -bg [background] # fvwm-theme -decor [window decor] # fvwm-theme -term [terminal scheme] # fvwm-theme -theme [meta-theme name] # # Leave off the extension for [background]. # # At fvwm2 startup: fvwm-theme -init # # This will add various bindable menus and functions (most importantly, the # top-level ThemesMenu menu), and set the theme from ~/.fvwm-theme (or # failing that, from the "default" meta-theme). use strict; sub my_die; ########################## Configurable Constants ############################# my $SHARE_DIR = '/usr/local/share/fvwm-theme'; my $USER_FILE = "$ENV{HOME}/.fvwm-theme"; my $BACKGROUND_SETTER = 'qiv -y'; # should set a tiled root background my $TERMINAL_PROGRAM = 'aterm'; # must have full xresources support # This image will be used to mark the current theme option in menus my $CHECKMARK_IMAGE = 'check.png'; ################################# Constants ################################### my $PROG_NAME = 'fvwm-theme'; my $BACKGROUNDS_DIR = "$SHARE_DIR/backgrounds"; my $DECORS_DIR = "$SHARE_DIR/decors"; my $TERMS_DIR = "$SHARE_DIR/terms"; my $THEMES_DIR = "$SHARE_DIR/themes"; my @TERM_COLORS = ('red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'); my @TITLE_PIXMAPS = qw(title titlebt titleat titleunder titlelt titlert titleleft titleright buttonsbg lbuttonsbg rbuttonsbg); my @PM_NAMES = qw(Main LeftMain RightMain UnderText LeftOfText RightOfText LeftEnd RightEnd Buttons LeftButtons RightButtons); my @BUTTON_SETUP = ( ['1 ActiveUp', 'options_activeup.xpm'], ['6 ActiveUp', 'iconify_activeup.xpm'], ['4 ActiveUp', 'maximize_activeup.xpm'], ['2 ActiveUp', 'close_activeup.xpm'], ['1 ActiveDown', 'options_activedown.xpm'], ['6 ActiveDown', 'iconify_activedown.xpm'], ['4 ActiveDown', 'maximize_activedown.xpm'], ['2 ActiveDown', 'close_activedown.xpm'], ['1 Inactive', 'options_inactive.xpm'], ['6 Inactive', 'iconify_inactive.xpm'], ['4 Inactive', 'maximize_inactive.xpm'], ['2 Inactive', 'close_inactive.xpm'], ); my %OPTIONS = ( 'init' => { func => \&init, args => 0 }, 'backgrounds-menu' => { func => \&backgrounds_menu, args => 0 }, 'decors-menu' => { func => \&decors_menu, args => 0 }, 'terms-menu' => { func => \&terms_menu, args => 0 }, 'meta-themes-menu' => { func => \&meta_themes_menu, args => 0 }, 'bg' => { func => \&set_background, args => 1 }, 'decor' => { func => \&set_decor, args => 1 }, 'term' => { func => \&set_term, args => 1 }, 'theme' => { func => \&set_theme, args => 1 } ); ################################## Globals #################################### my %theme; ################################# Begin Main ################################## my $option = shift or usage_abort(); $option =~ s/^-+// or usage_abort(); my $option_info = $OPTIONS{$option} or usage_abort(); @ARGV == $option_info->{args} or usage_abort(); read_hash_file($USER_FILE, \%theme) or read_hash_file("$THEMES_DIR/default", \%theme) or my_die "No $USER_FILE and no default theme found"; my $func = $option_info->{func}; &$func(@ARGV); write_hash_file($USER_FILE, \%theme) or my_die "Can't open $USER_FILE for writing ($!)"; ################################## End Main ################################### ############################### Bound Functions ############################### sub init { print <<"END"; ImagePath +:$DECORS_DIR AddToFunc SetBackground I PipeRead 'fvwm-theme -bg \$0' AddToFunc SetDecor I PipeRead 'fvwm-theme -decor \$0' AddToFunc SetTerm I PipeRead 'fvwm-theme -term \$0' AddToFunc SetTheme I PipeRead 'fvwm-theme -theme \$0' AddToMenu ThemesMenu "Themes" Title + "Backgrounds" Popup BackgroundsMenu + "Window Decorations" Popup DecorsMenu + "Terminals" Popup TermThemesMenu + "Full Themes" Popup MetaThemesMenu AddToMenu BackgroundsMenu DynamicPopupAction Function MakeBackgroundsMenu AddToFunc MakeBackgroundsMenu + I DestroyMenu recreate BackgroundsMenu + I PipeRead 'fvwm-theme -backgrounds-menu' AddToMenu DecorsMenu DynamicPopupAction Function MakeDecorsMenu AddToFunc MakeDecorsMenu + I DestroyMenu recreate DecorsMenu + I PipeRead 'fvwm-theme -decors-menu' AddToMenu TermThemesMenu DynamicPopupAction Function MakeTermThemesMenu AddToFunc MakeTermThemesMenu + I DestroyMenu recreate TermThemesMenu + I PipeRead 'fvwm-theme -terms-menu' AddToMenu MetaThemesMenu DynamicPopupAction Function MakeMetaThemesMenu AddToFunc MakeMetaThemesMenu + I DestroyMenu recreate MetaThemesMenu + I PipeRead 'fvwm-theme -meta-themes-menu' END set_theme_from_hash(); } sub backgrounds_menu { generic_menu('Backgrounds', 'BackgroundsMenu', 'bgsub', 'SetBackground', $BACKGROUNDS_DIR, $theme{background}, 0); } sub decors_menu { generic_menu('Window Decorations', 'DecorsMenu', 'dcsub', 'SetDecor', $DECORS_DIR, $theme{decor}, 1); } sub terms_menu { generic_menu('Terminal Themes', 'TermThemesMenu', 'ttsub', 'SetTerm', $TERMS_DIR, $theme{term}, 0); } sub meta_themes_menu { generic_menu('Full Themes', 'MetaThemesMenu', 'ftsub', 'SetTheme', $THEMES_DIR, '', 0); } sub set_background { my $background = shift; $theme{background} = $background; system "$BACKGROUND_SETTER $BACKGROUNDS_DIR/$background"; print "Refresh\n"; } sub set_decor { my $decor = shift; my $dir = "$DECORS_DIR/$decor"; return unless -d $dir; for (@BUTTON_SETUP) { my $target = $_->[0]; my $pixmap = $_->[1]; next unless -e "$dir/$pixmap"; print "ButtonStyle $target Pixmap $decor/$pixmap\n"; } print "ButtonStyle 1 MiniIcon\n" if -e "$DECORS_DIR/$decor/MINI_ICON"; print "ButtonStyle All -- Flat UseTitleStyle\n"; for my $state ('ActiveUp', 'ActiveDown', 'Inactive') { print "TitleStyle $state MultiPixmap "; for (my $i=0; $i < @TITLE_PIXMAPS; $i++) { my $pixmap = $TITLE_PIXMAPS[$i]; my $append = ($state eq 'Inactive' ? 'inactive' : 'active'); my $pixmap_fn = "${pixmap}_$append.xpm"; next unless -e "$dir/$pixmap_fn"; my $pm_name = $PM_NAMES[$i]; print ", " if $i > 0; print "$pm_name "; print "(stretched) " if -e "$DECORS_DIR/$decor/STRETCH" && ($pixmap eq 'title' || $pixmap eq 'titleunder' || $pixmap eq 'titlebt' || $pixmap eq 'titleat'); print "$decor/$pixmap_fn"; } print "\n"; } print "TitleStyle -- Flat\n"; open F, "<$dir/decor"; print while ; close F; $theme{decor} = $decor; } sub set_term { my $term = shift; my %term_info; read_hash_file("$TERMS_DIR/$term", \%term_info) or return; $theme{term} = $term; open XRDB, "|xrdb -merge -" or my_die "Can't open a pipe to 'xrdb -merge -' ($!)"; my $transparent = (exists $term_info{transparent} ? 'true' : 'false'); my $shading = $term_info{shading} || '100%'; print XRDB <<"END"; $TERMINAL_PROGRAM.transparent: $transparent $TERMINAL_PROGRAM.shading: $shading $TERMINAL_PROGRAM.backgroundPixmap: $term_info{backgroundPixmap} $TERMINAL_PROGRAM.background: $term_info{background} $TERMINAL_PROGRAM.foreground: $term_info{foreground} END print XRDB "$TERMINAL_PROGRAM.colorBD: $term_info{bold}\n" if $term_info{bold}; for (my $i=0; $i < @TERM_COLORS; $i++) { my $index = $i+1; my $value = $term_info{$TERM_COLORS[$i]} or next; print XRDB "$TERMINAL_PROGRAM.color${index}: $value\n"; } for (my $i=0; $i < @TERM_COLORS; $i++) { my $index = $i+9; my $value = $term_info{'bright_' . $TERM_COLORS[$i]} || $term_info{$TERM_COLORS[$i]} or next; print XRDB "$TERMINAL_PROGRAM.color${index}: $value\n"; } close XRDB or my_die "Close failed after writing to pipe 'xrdb -merge -' ($!)"; } sub set_theme { my $theme = shift; read_hash_file("$THEMES_DIR/$theme", \%theme); set_theme_from_hash(); } ############################## Helper Functions ############################### sub set_theme_from_hash { set_background($theme{background}); set_decor($theme{decor}); set_term($theme{term}); } sub generic_menu { my ($menu_title, $menu_name, $subname_prefix, $menu_function, $directory, $selected, $directories_are_items) = @_; chdir $directory or my_die "Can't chdir to $directory ($!)"; generic_submenu($menu_title, $menu_name, $subname_prefix, $menu_function, '.', $selected, $directories_are_items); } sub generic_submenu { my ($menu_title, $menu_name, $subname_prefix, $menu_function, $directory, $selected, $directories_are_items) = @_; opendir DIR, $directory or my_die "Can't opendir $directory ($!)"; my @contents = sort(grep(!/^\./, readdir(DIR))); closedir DIR; print "DestroyMenu recreate $menu_name\n"; print "AddToMenu $menu_name '$menu_title' Title\n"; my @subdirs; for my $item (@contents) { my $path = ($directory eq '.' ? $item : "$directory/$item"); my $display_name = make_display_name($item); if (-d $path && !$directories_are_items) { my $submenu_name = "${subname_prefix}_$path"; $submenu_name =~ tr|/|_|; push @subdirs, [$display_name, $submenu_name, $subname_prefix, $menu_function, $path, $selected]; print "+ '$display_name' Popup $submenu_name\n"; } else { print "+ '$display_name"; print "%$CHECKMARK_IMAGE%" if $path eq $selected; print "' $menu_function $path\n"; } } generic_submenu(@$_) for @subdirs; } sub read_hash_file { my ($file, $hash) = @_; open F, "<$file" or return 0; while () { chomp; my ($key, $val) = split(/=/, $_, 2); $key = trim_whitespace($key); $val = trim_whitespace($val); $$hash{$key} = $val; } close F; return 1; } sub write_hash_file { my ($file, $hash) = @_; open F, ">$file" or return 0; while (my ($key, $val) = each %$hash) { print F "$key=$val\n"; } close F; return 1; } sub trim_whitespace { my $str = shift; $str =~ s/^\s+//; $str =~ s/\s+$//; return $str; } sub make_display_name { my $name = shift; $name =~ s/\.[^.]+$//; # strip extension $name =~ tr/_-/ /; # convert underscores to spaces $name =~ s/\b([a-z])/\U$1/g; # capitalize words return $name; } sub usage_abort { print STDERR <<"END"; Usage: fvwm-theme -bg [background] fvwm-theme -decor [window decor] fvwm-theme -term [terminal scheme] fvwm-theme -theme [meta-theme name] Leave off the extension for [background]. At fvwm2 startup: fvwm-theme -init This will add various bindable menus and functions (most importantly, the ThemesMenu menu), and set the theme from ~/.fvwm-theme (or failing that, the "default" meta-theme). END exit(1); } sub my_die { my $msg = shift; die "$PROG_NAME: $msg\n"; }