1#!@SH@
2#
3# Copyright (C) 2021 Claes Nästén
4#
5
6# URL for the pekwm theme site
7PEKWM_THEMES="https://www.pekwm.se/themes"
8# URL for the theme index file
9REMOTE_THEME_INDEX="$PEKWM_THEMES/themes-v1.csv"
10# Path to local copy of the theme index file
11LOCAL_THEME_INDEX="$HOME/.pekwm/themes-v1.csv"
12# Max age of the local theme index before it is re fetchted.
13LOCAL_THEME_INDEX_MAX_AGE="86400"
14# Path to local directory where screenshots are cached, used with the
15# show command.
16SCREENSHOT_CACHE_DIR="$HOME/.cache/pekwm-themes"
17# Field delimiter
18SEP="¤"
19
20# Stop execution with exit code 1 after printing provided error
21# message
22#
23# die error message
24die()
25{
26    echo "ERROR: $@"
27    exit 1
28}
29
30# Print progress message
31#
32# progress progress message
33progress()
34{
35    echo "`date '+%Y%m%dT%H:%M:%S'`: $@"
36}
37
38# Print usage information
39#
40# usage exit-code
41usage()
42{
43    echo "usage: $0 [install|uninstall|show|search|new|update]"
44    echo ""
45    echo "  install [-a] name   install theme"
46    echo "  uninstall name      uninstall theme (removes local changes)"
47    echo "  show name           display theme preview"
48    echo "  search [pattern]    search for theme"
49    echo "  new name            create new theme"
50    echo "  update              update theme index"
51    echo ""
52    exit $1
53}
54
55# Fetch remote file using HTTP(S)/FTP.
56#
57# fetch_file url local-path
58fetch_file()
59{
60    progress "fetching $1 to $2..."
61
62    CURL=`which curl 2>/dev/null | grep '^/'`
63    WGET=`which wget 2>/dev/null | grep '^/'`
64    FTP=`which ftp 2>/dev/null | grep '^/'`
65
66    if test "x$CURL" != "x"; then
67        $CURL -L -s -o "$2" "$1"
68    elif test "$WGET" != "x"; then
69        $WGET -q -O "$2" "$1"
70    elif test "$FTP" != "x"; then
71        $FTP -h 2>&1 | grep -q http
72        if $? -eq 0; then
73            $FTP -o "$2" "$1"
74        else
75            die "unsupported ftp, no HTTP support"
76        fi
77    else
78        die "no supported HTTP client (curl, wget, BSD ftp)"
79    fi
80}
81
82# Update local copy of the theme index
83#
84# update_index
85update_index()
86{
87    mkdir -p "$HOME/.pekwm"
88    if test -e "${LOCAL_THEME_INDEX}.stamp"; then
89        created=`cat ${LOCAL_THEME_INDEX}.stamp`
90        now=`pekwm_ctrl -a util timestamp 2>/dev/null`
91        age=$(($now - $created))
92        if test $age -gt $LOCAL_THEME_INDEX_MAX_AGE; then
93            rm -f "$LOCAL_THEME_INDEX"
94        fi
95    else
96        # no stamp file exists, force a refresh
97        rm -f "$LOCAL_THEME_INDEX"
98    fi
99
100    if test ! -e "$LOCAL_THEME_INDEX"; then
101        fetch_file "$REMOTE_THEME_INDEX" "$LOCAL_THEME_INDEX"
102        pekwm_ctrl -a util timestamp >"${LOCAL_THEME_INDEX}.stamp" 2>/dev/null
103    fi
104}
105
106# Read theme data from the local theme index, populates theme_remote,
107# theme_branch, theme_author and theme_title
108#
109# read_theme name
110read_theme()
111{
112    line=`sed 1d $LOCAL_THEME_INDEX | grep "^$1$SEP"`
113    if test -z "$line"; then
114        die "no theme named $1"
115    fi
116    theme_name="$1"
117    theme_remote=`echo $line | @AWK@ -F "$SEP" '{ print $2 }'`
118    theme_branch=`echo $line | @AWK@ -F "$SEP" '{ print $3 }'`
119    theme_base_path=`echo $line | @AWK@ -F "$SEP" '{ print $4 }'`
120    theme_author=`echo $line | @AWK@ -F "$SEP" '{ print $6 }'`
121    theme_title=`echo $line | @AWK@ -F "$SEP" '{ print $7 }'`
122}
123
124# Set theme_path to local theme path
125#
126# set_theme_path name
127set_theme_path()
128{
129    theme_path="$HOME/.pekwm/themes/$1"
130}
131
132# Install theme to $HOME/.pekwm/themes, if activate is given the
133# current theme is changed.
134#
135# theme_install name [activate]
136theme_install()
137{
138    read_theme "$1"
139    set_theme_path "$1"
140
141    if test -z "$theme_base_path"; then
142        theme_install_plain
143    else
144        theme_install_base_path
145    fi
146
147    if test "x$2" = "xactivate"; then
148        @CMAKE_INSTALL_PREFIX@/share/pekwm/scripts/pekwm_themeset.sh \
149         "$HOME/.pekwm/themes" "$theme_path"
150    fi
151}
152
153# Install/update theme residing in the root of the GIT repository (use
154# the theme_ variables from read_theme)
155#
156# theme_install_plain
157theme_install_plain()
158{
159    git_update_repo "$theme_remote" "$theme_branch" "$theme_path"
160}
161
162# Install/update theme not in the root of the GIT repository (use the
163# theme_ variables from read_theme)
164#
165# theme_install_base_path
166theme_install_base_path()
167{
168    mkdir -p "$HOME/.pekwm/themes_git"
169    repo_path="$HOME/.pekwm/themes_git/`echo $theme_remote | tr -cd '[:alnum:]'`"
170    git_update_repo "$theme_remote" "$theme_branch" "$repo_path"
171
172    ln -sf "$repo_path/$theme_base_path" "$HOME/.pekwm/themes/$theme_name"
173}
174
175# Clone or update GIT repository
176#
177# remote branch local-path
178git_update_repo()
179{
180    if test -d "$3"; then
181        echo "$3 already exist, updating"
182        git -C "$3" remote set-url origin "$1"
183        git -C "$3" fetch origin
184    else
185        echo "cloning $1"
186        git clone "$1" "$3"
187    fi
188
189    git -C "$3" checkout "$2"
190    git -C "$3" reset --hard "origin/$2"
191}
192
193# Uninstall theme from $HOME/.pekwm/themes
194#
195# theme_uninstall name
196theme_uninstall()
197{
198    read_theme $1
199    set_theme_path $1
200    if test -d "$theme_path"; then
201        echo "uninstalling $1"
202        rm -rf "$theme_path"
203    fi
204}
205
206# Update local copy of theme screenshot
207#
208# update_theme_screenshot name
209update_theme_screenshot()
210{
211    mkdir -p $SCREENSHOT_CACHE_DIR
212    path="$SCREENSHOT_CACHE_DIR/$1.png"
213    if test ! -e "$path"; then
214        fetch_file $PEKWM_THEMES/$1.png $path
215    fi
216}
217
218# Search for the
219#
220# theme_search [pattern]
221theme_search()
222{
223    if test -z "$1"; then
224        echo "available themes:"
225    else
226        echo "themes matching $1:"
227    fi
228
229    search="$1"
230    if test -z "$search"; then
231        search=".*"
232    fi
233
234    sed 1d "$LOCAL_THEME_INDEX" | @AWK@ -F "$SEP" '{ print $1 }' | grep "$search"
235}
236
237# Show theme using pekwm_dialog, if install is choosen the theme is
238# installed and activated.
239#
240# theme_show name
241theme_show()
242{
243    read_theme "$1"
244    set_theme_path "$1"
245    update_theme_screenshot "$1"
246    pekwm_dialog -i "$SCREENSHOT_CACHE_DIR/$1.png" -t "pekwm theme: $1" \
247                 -o Install -o Close $theme_title
248    if test $? -eq 0; then
249        theme_install "$1" "activate"
250    fi
251}
252
253# Generate a theme directory
254#
255# theme_new name
256theme_new()
257{
258    theme_name="$1"
259
260    if test -z "$theme_name"; then
261        die "a theme name must be provided to pekwm_theme new"
262    fi
263    if test -e "$theme_name"; then
264       die "a file or directory with the name $theme_name already exist"
265    fi
266
267    mkdir $theme_name || die "failed to create $theme_name"
268    mkdir $theme_name/backgrounds \
269          || die "failed to create $theme_name/backgrounds"
270
271    cat > $theme_name/theme <<EOF
272\$FONT = "-*-fixed-*-r-*-*-*-*-*-*-*-*-iso10646-*"
273\$MENU_FONT = "-*-fixed-*-r-*-*-*-*-*-*-*-*-iso10646-*"
274
275\$BG_TEX = "Solid #ffffff"
276
277\$COLOR_FO = "#eeeeee"
278\$COLOR_FO_HI = "#ffffff"
279\$COLOR_FO_LO = "#dddddd"
280\$COLOR_BD_FO = "#000000"
281\$TEXT_FO = "#333333"
282
283\$COLOR_FO_SEL = "#efefef"
284\$COLOR_FO_SEL_HI = "#ffffff"
285\$COLOR_FO_SEL_LO = "#dddddd"
286\$TEXT_FO_SEL = "#000000"
287
288\$COLOR_UN = "#aaaaaa"
289\$COLOR_UN_HI = "#bbbbbb"
290\$COLOR_UN_LO = "#999999"
291\$COLOR_BD_UN = "#666666"
292\$TEXT_UN = "#666666"
293
294\$COLOR_UN_SEL = "#afafaf"
295\$COLOR_UN_SEL_HI = "#bbbbbb"
296\$COLOR_UN_SEL_LO = "#999999"
297\$TEXT_UN_SEL = "#333333"
298
299INCLUDE = "\$THEME_DIR/template"
300EOF
301
302    cat > $theme_name/template <<EOF
303\$TEX_FO = "SolidRaised \$COLOR_FO \$COLOR_FO_HI \$COLOR_FO_LO 1 0"
304\$TEX_FO_SEL = "SolidRaised \$COLOR_FO_SEL \$COLOR_FO_SEL_HI \$COLOR_FO_SEL_LO 1 0"
305\$TEX_UN = "SolidRaised \$COLOR_UN \$COLOR_UN_HI \$COLOR_UN_LO 1 0"
306\$TEX_UN_SEL = "SolidRaised \$COLOR_UN_SEL \$COLOR_UN_SEL_HI \$COLOR_UN_SEL_LO"
307\$TEX_SEP_FO = "Solid \$COLOR_FO_HI 2x20"
308\$TEX_SEP_UN = "Solid \$COLOR_UN_HI 2x20"
309
310# Theme format version
311Version = "2"
312
313Require {
314  Templates = "True"
315}
316
317Background {
318  Texture = "\$BG_TEX"
319}
320
321ColorMaps {
322  ColorMap = "Focused" {
323    Map = "#aaaaaa" { To = "\$COLOR_FO" }
324    Map = "#ffffff" { To = "\$COLOR_FO_LO" }
325  }
326  ColorMap = "Unfocused" {
327    Map = "#aaaaaa" { To = "\$COLOR_UN" }
328    Map = "#ffffff" { To = "\$COLOR_UN_LO" }
329  }
330  ColorMap = "Hoover" {
331    Map = "#aaaaaa" { To = "\$COLOR_FO_SEL" }
332    Map = "#ffffff" { To = "\$COLOR_FO_SEL_HI" }
333    Map = "#000000" { To = "#ffffff" }
334  }
335  ColorMap = "Pressed" {
336    Map = "#aaaaaa" { To = "\$COLOR_FO" }
337    Map = "#ffffff" { To = "\$COLOR_FO_LO" }
338    Map = "#000000" { To = "#ffffff" }
339  }
340}
341
342Define = "Border" {
343  Focused {
344    TopLeft = "Solid \$COLOR_BD_FO 2x2"
345    Top = "Solid \$COLOR_BD_FO 2x2"
346    TopRight = "Solid \$COLOR_BD_FO 2x2"
347    Left = "Solid \$COLOR_BD_FO 2x2"
348    Right = "Solid \$COLOR_BD_FO 2x2"
349    BottomLeft = "Solid \$COLOR_BD_FO 2x2"
350    Bottom = "Solid \$COLOR_BD_FO 2x2"
351    BottomRight = "Solid \$COLOR_BD_FO 2x2"
352  }
353  Unfocused {
354    TopLeft = "Solid \$COLOR_BD_UN 2x2"
355    Top = "Solid \$COLOR_BD_UN 2x2"
356    TopRight = "Solid \$COLOR_BD_UN 2x2"
357    Left = "Solid \$COLOR_BD_UN 2x2"
358    Right = "Solid \$COLOR_BD_UN 2x2"
359    BottomLeft = "Solid \$COLOR_BD_UN 2x2"
360    Bottom = "Solid \$COLOR_BD_UN 2x2"
361    BottomRight = "Solid \$COLOR_BD_UN 2x2"
362  }
363}
364
365Define = "EmptyBorder" {
366  Focused {
367    TopLeft = "Empty"
368    Top = "Empty"
369    TopRight = "Empty"
370    Left = "Empty"
371    Right = "Empty"
372    BottomLeft = "Empty"
373    Bottom = "Empty"
374    BottomRight = "Empty"
375  }
376  Unfocused {
377    TopLeft = "Empty"
378    Top = "Empty"
379    TopRight = "Empty"
380    Left = "Empty"
381    Right = "Empty"
382    BottomLeft = "Empty"
383    Bottom = "Empty"
384    BottomRight = "Empty"
385  }
386}
387
388Define = "Buttons" {
389  Right = "Close" {
390    Focused = "ImageMapped Focused button-close.xpm"
391    Unfocused = "ImageMapped Unfocused button-close.xpm"
392    Hoover = "ImageMapped Hoover button-close.xpm"
393    Pressed = "ImageMapped Pressed button-close.xpm"
394    Button = "1" { Actions = "Close" }
395    Button = "3" { Actions = "Kill" }
396  }
397}
398
399Define = "Decor" {
400  Height = "20"
401  Pad = "2 2 5 5"
402
403  Focused = "\$TEX_FO"
404  Unfocused = "\$TEX_UN"
405
406  Tab {
407    Focused = "\$TEX_FO"
408    FocusedSelected = "\$TEX_FO_SEL"
409    Unfocused = "\$TEX_UN"
410    UnfocusedSelected = "\$TEX_UN_SEL"
411  }
412  Separator {
413    Focused = "\$TEX_SEP_FO"
414    Unfocused = "\$TEX_SEP_UN"
415  }
416  Font {
417    Focused = "\$FONT#Center"
418  }
419  FontColor {
420    Focused = "\$TEXT_FO"
421    FocusedSelected = "\$TEXT_FO_SEL"
422    Unfocused = "\$TEXT_UN"
423    UnfocusedSelected = "\$TEXT_UN_SEL"
424  }
425}
426
427Define = "EmptyDecor" {
428  Height = "0"
429  Pad = "0 0 0 0"
430  Focused = "Empty"
431  Unfocused = "Empty"
432  Tab {
433    Focused = "Empty"
434    FocusedSelected = "Empty"
435    Unfocused = "Empty"
436    UnfocusedSelected = "Empty"
437  }
438  Separator {
439    Focused = "Empty"
440    Unfocused = "Empty"
441  }
442  Font {
443    Focused = "Empty"
444  }
445  FontColor {
446    Focused = "Empty"
447    FocusedSelected = "Empty"
448    Unfocused = "Empty"
449    UnfocusedSelected = "Empty"
450  }
451}
452
453Decor = "Default" {
454  @Decor
455  Border {
456    @Border
457  }
458  Buttons {
459    @Buttons
460  }
461}
462
463Decor = "InputDialog" {
464  @EmptyDecor
465  Border {
466    @Border
467  }
468}
469
470Decor = "Menu" {
471  @Decor
472  Border {
473    @Border
474  }
475}
476
477Decor = "StatusWindow" {
478  @EmptyDecor
479  Border {
480    @Border
481  }
482}
483
484Decor = "WorkspaceIndicator" {
485  @EmptyDecor
486  Border {
487    @Border
488  }
489}
490
491CmdDialog {
492  Font = "\$FONT"
493  Texture = "\$TEX_UN"
494  Text = "\$TEXT_UN"
495  Pad = "4 4 4 4"
496}
497
498Dialog {
499  Background = "Solid \$COLOR_UN"
500  TitleFont = "\$MENU_FONT"
501  TitleColor = "\$TEXT_FO_SEL"
502  Font = "\$FONT"
503  Text = "\$TEXT_FO"
504  Pad = "4 4 4 4"
505
506  Button {
507    Font = "\$FONT"
508    Text = "\$TEXT_FO_SEL"
509
510    Focused = "\$TEX_FO"
511    Unfocused = "\$TEX_UN"
512    Hoover = "\$TEX_FO_SEL"
513    Pressed = "\$TEX_UN_SEL"
514  }
515}
516
517Harbour {
518  Texture = "\$TEX_UN"
519}
520
521Menu {
522  Focused {
523    Font = "\$MENU_FONT"
524    Background = "\$TEX_FO"
525    Item = "Empty"
526    Text = "\$TEXT_FO"
527    Separator = "Solid \$COLOR_FO_HI 1x1"
528    Arrow = "Solid \$COLOR_FO_LO 5x5"
529  }
530  Unfocused {
531    Font = "\$MENU_FONT"
532    Background = "\$TEX_UN"
533    Item = "Empty"
534    Text = "\$TEXT_UN"
535    Separator = "Solid \$COLOR_UN_HI 1x1"
536    Arrow = "Solid \$COLOR_UN_LO 5x5"
537  }
538  Selected {
539    Font = "\$MENU_FONT"
540    Background = "\$TEX_FO_SEL"
541    Item = "\$TEX_FO_SEL"
542    Text = "\$TEXT_FO_SEL"
543    Arrow = "Solid \$COLOR_FO_SEL_LO 5x5"
544  }
545  Pad = "2 2 4 4"
546}
547
548Status {
549  Font = "\$FONT"
550  Texture = "Solid \$COLOR_UN"
551  Text = "\$TEXT_UN"
552  Pad = "0 0 0 0"
553}
554
555WorkspaceIndicator {
556  Font = "\$FONT#Center"
557  Background = "Solid \$COLOR_UN"
558  Workspace = "Solid \$COLOR_UN_SEL"
559  WorkspaceActive = "Solid \$COLOR_FO_SEL"
560  Text = "\$TEXT_FO"
561  EdgePadding = "2"
562  WorkspacePadding = "2"
563}
564EOF
565
566    cat > $theme_name/button-close.xpm <<EOF
567/* XPM */
568static char *button_close[] = {
569/* columns rows colors chars-per-pixel */
570"20 20 3 1 ",
571"  c #aaaaaaaaaaaa",
572". c black",
573"X c white",
574/* pixels */
575"XXXXXXXXXXXXXXXXXXXX",
576"X..          ......X",
577"X...        ...... X",
578"X...       .....   X",
579"X....     .....    X",
580"X.....   .....     X",
581"X ..... .....      X",
582"X  .........       X",
583"X   .......        X",
584"X    .....         X",
585"X    ......        X",
586"X   ........       X",
587"X  ..........      X",
588"X .....  .....     X",
589"X ....    .....    X",
590"X....      .....   X",
591"X....       ...... X",
592"X...         ......X",
593"X..           .....X",
594"XXXXXXXXXXXXXXXXXXXX"
595};
596EOF
597
598    echo "generated theme $theme_name"
599}
600
601# Update theme index
602#
603# theme_update_index
604theme_update()
605{
606    rm -f "$LOCAL_THEME_INDEX.stamp" "$LOCAL_THEME_INDEX"
607    update_index
608}
609
610if test -z "$HOME"; then
611    die "\$HOME is not set, can not proceed"
612fi
613if test -z "`which git 2>/dev/null | grep '^/'`"; then
614    die "git must be available in the path, can not proceed"
615fi
616
617if test -z "$1"; then
618    usage 1
619elif test "x$1" = "xupdate"; then
620    theme_update
621else
622    update_index
623
624    if test "x$1" = "xinstall"; then
625        if test "$2" = "-a"; then
626            theme_install "$3" "activate"
627        else
628            theme_install "$2"
629        fi
630    elif test "x$1" = "xuninstall"; then
631        theme_uninstall "$2"
632    elif test "x$1" = "xshow"; then
633        theme_show "$2" "$3"
634    elif test "x$1" = "xsearch"; then
635        theme_search "$2"
636    elif test "x$1" = "xnew"; then
637        theme_new "$2"
638    else
639        usage 1
640    fi
641fi
642