#!/bin/ash
# Simple GUI for crontab, replacement for original pschedule
# Copyright (C) James Budiono 2012
#
# License: GNU GPL Version 3 or later
#
# Note: the bits in Global Inits, which is part of GTK-server bash init code
#       is (C) Peter van Eerten. Everything else is (C) James Budiono
#
# 131121 L18L internationalisation

# std localisation stanza
export TEXTDOMAIN=fatdog
. gettext.sh
# performance tweak - use "C" if there is no localisation
! [ -e $TEXTDOMAINDIR/${LANG%.*}/LC_MESSAGES/$TEXTDOMAIN.mo ] &&
! [ -e $TEXTDOMAINDIR/${LANG%_*}/LC_MESSAGES/$TEXTDOMAIN.mo ] && LANG=C

### configuration
APPTITLE="$(gettext 'Fatdog64 Task Scheduler')"
CRONTABS=/var/spool/cron/crontabs
CRON_SERVICE=/etc/init.d/80-crond

T_CMD=0 T_SCHEDULE=1

#---------------------------------------------------------- Global Inits
### global variables for proper GTK-server
GTK= NULL="NULL"
PIPE=/tmp/gtk.bash.$$
trap 'stop-gtk-server -fifo $PIPE; exit' HUP TERM INT # stop GTK-server in case of an error

### start GTK-server
gtk-server -fifo=$PIPE &
while [ ! -p $PIPE ]; do sleep 1; continue; done

### GTK-server helper functions
# Assignment function
define() { $2 $3 $4 $5 $6 $7 $8 $9; eval $1=\""$GTK"\"; }

# Communicate with GTK-server
gtk()
{
/bin/echo "$@" > $PIPE
read GTK < $PIPE
}

#---------------------------------------------------------- GUI realization
add_missing_functions() {
	
	# add GTK+ API not defined in gtk-server.cfg
	gtk gtk_server_define gtk_builder_new NONE WIDGET 0
	gtk gtk_server_define gtk_builder_add_from_file NONE INT 3 WIDGET STRING NULL
	gtk gtk_server_define gtk_builder_add_from_string NONE INT 4 WIDGET STRING INT NULL
	gtk gtk_server_define gtk_builder_get_object NONE WIDGET 2 WIDGET STRING
	gtk gtk_server_define gtk_builder_connect_signals NONE NONE 2 WIDGET NULL	
	
	gtk gtk_server_define gtk_tree_view_get_cursor NONE NONE 3 WIDGET PTR_LONG NULL
	gtk gtk_server_define gtk_tree_path_to_string NONE STRING 1 WIDGET
	gtk gtk_server_define gtk_tree_model_get_iter_first NONE BOOL 2 WIDGET WIDGET
	
	# hack for getting unlimited string length and leak-free operation with gtk_tree_get_model
	# see below
	gtk gtk_server_define strcat NONE STRING 2 POINTER STRING
	gtk gtk_server_define memcpy NONE NONE 3 PTR_LONG POINTER INT		
}

### helper - leak-free, unlimited string length from gtk_tree_model_get with unpatched gtk-server
# works with gtk-server 2.3.1 and 2.4.5
# $1-tree $2-iter $3-index $4-result name
tree_model_get_string() {
	local PSTR # pointer to string
	gtk gtk_server_define gtk_tree_model_get NONE NONE 5 WIDGET WIDGET INT PTR_LONG INT # ensure correct definition
	define PSTR gtk gtk_tree_model_get $1 $2 $3 NULL -1 # PSTR --> *buf
	
	if [ $PSTR -ne 0 ]; then
		# with newer gtk-server 2.4.4 onwards you can use this
		#define $4 gtk gtk_server_string_from_pointer $PSTR 
		define $4 gtk strcat $PSTR \"\" # get string point by address --> **buf
		gtk free $PSTR # this was dynamically alloc-ed by gtk_tree_model_get, now free it
	else
		eval $4=\"\"
	fi
}

### signal handler - load crontab to view
# this one just load the entries from the crontab file, no parsing is done here
load_crontab() {
	local ITER
	define ITER gtk gtk_server_opaque
	
	if [ -r $CRONTABS/$USER ]; then
		while read -r p; do
			case "$p" in
				\#*|"") continue ;;
				*)
					q=${p#* }; q=${q#* }; q=${q#* }; q=${q#* }; q=${q#* }; # remove the first 5 fields
					p=${p%$q}
					gtk gtk_list_store_append $LISTSTORE $ITER NULL
					gtk gtk_list_store_set $LISTSTORE $ITER $T_CMD \""$q"\" -1
					gtk gtk_list_store_set $LISTSTORE $ITER $T_SCHEDULE \""$p"\" -1
					;;
			esac
		done < $CRONTABS/$USER
	fi	
	gtk free $ITER
}

### signal handler - add a new crontab entry
add_entry() {
	local ITER
	define ITER gtk gtk_server_opaque
	
	gtk gtk_list_store_append $LISTSTORE $ITER NULL
	gtk gtk_list_store_set $LISTSTORE $ITER $T_CMD \""/bin/true"\" -1
	gtk gtk_list_store_set $LISTSTORE $ITER $T_SCHEDULE \""* * * * *"\" -1

	gtk free $ITER
}

### signal handler - update the content of the entry details
# parsing of crontab entry is done here, on demand
update_details() {
	local ITER SELECTED_PATH CMD SCHEDULE q1 q2 q3 q4 q5
	define ITER gtk gtk_server_opaque
	
	define SELECTED_PATH gtk gtk_tree_view_get_cursor $TREEVIEW NULL
	if [ $SELECTED_PATH -ne 0 ]; then
		gtk gtk_tree_model_get_iter $LISTSTORE $ITER $SELECTED_PATH	
		gtk gtk_tree_path_free $SELECTED_PATH

		# $1-tree $2-iter $3-index $4-result name
		tree_model_get_string $LISTSTORE $ITER $T_CMD CMD
		tree_model_get_string $LISTSTORE $ITER $T_SCHEDULE SCHEDULE
		
		# parse the crontab entry
		gtk gtk_entry_set_text $CMD_ENTRY \""$CMD"\"
		q1=${SCHEDULE%% *} SCHEDULE=${SCHEDULE#* }
		q2=${SCHEDULE%% *} SCHEDULE=${SCHEDULE#* }
		q3=${SCHEDULE%% *} SCHEDULE=${SCHEDULE#* }
		q4=${SCHEDULE%% *} SCHEDULE=${SCHEDULE#* }
		q5=${SCHEDULE%% *} SCHEDULE=${SCHEDULE#* }
		# min hour day month dow
		# echo $q1 $q2 $q3 $q4 $q5
		parse_field "$q1" $AT_MINUTE $ENTRY_MINUTE 
		parse_field "$q2" $AT_HOUR $ENTRY_HOUR 
		parse_field "$q3" $AT_DAY $ENTRY_DAY 
		parse_field "$q4" $AT_MONTH $ENTRY_MONTH
		if [ "$q5" = "*" ]; then gtk gtk_combo_box_set_active $COMBO_DOW 8
		else gtk gtk_combo_box_set_active $COMBO_DOW $q5
		fi
		
		gtk gtk_widget_show $DETAILS_VBOX		
	fi
	
	gtk free $ITER
}
# simple parse. Complex crontab will get butchered here.
# $1-field value $2-at object $3-entry object
parse_field() {
	case "$1" in 
		*/*) AT=${1%/*} EVERY=${1#*/} ;;
		*) AT=$1 EVERY="" ;;
	esac
	gtk gtk_entry_set_text $2 \""$AT"\"
	gtk gtk_entry_set_text $3 \""$EVERY"\"
}

### signal handler - update entry from gui to crontab
update_entry() {
	local ITER SELECTED_PATH CMD q1 q2 q3 q4 q5

	define CMD gtk gtk_entry_get_text $CMD_ENTRY
	get_field $AT_MINUTE $ENTRY_MINUTE; q1=$ENTRY
	get_field $AT_HOUR $ENTRY_HOUR; q2=$ENTRY
	get_field $AT_DAY $ENTRY_DAY; q3=$ENTRY
	get_field $AT_MONTH $ENTRY_MONTH; q4=$ENTRY
	define q5 gtk gtk_combo_box_get_active $COMBO_DOW
	[ $q5 -eq 8 ] && q5="*"
	
	define ITER gtk gtk_server_opaque
	
	define SELECTED_PATH gtk gtk_tree_view_get_cursor $TREEVIEW NULL
	if [ $SELECTED_PATH -ne 0 ]; then
		gtk gtk_tree_model_get_iter $LISTSTORE $ITER $SELECTED_PATH	
		gtk gtk_tree_path_free $SELECTED_PATH
		
		gtk gtk_list_store_set $LISTSTORE $ITER $T_CMD \""$CMD"\" -1
		gtk gtk_list_store_set $LISTSTORE $ITER $T_SCHEDULE \""$q1 $q2 $q3 $q4 $q5"\" -1
	fi
	gtk free $ITER	
}
#$1-at object $2-entry object returns ENTRY
get_field() {
	define ENTRY gtk gtk_entry_get_text $1
	define EVERY gtk gtk_entry_get_text $2
	[ "$EVERY" ] && ENTRY=${ENTRY}/${EVERY}
}

### signal handler - remove currently selected entry
remove_entry() {
	local ITER SELECTED_PATH CMD SCHEDULE
	define ITER gtk gtk_server_opaque
	
	define SELECTED_PATH gtk gtk_tree_view_get_cursor $TREEVIEW NULL
	if [ $SELECTED_PATH -ne 0 ]; then
		gtk gtk_tree_model_get_iter $LISTSTORE $ITER $SELECTED_PATH	
		gtk gtk_tree_path_free $SELECTED_PATH
		
		gtk gtk_list_store_remove $LISTSTORE $ITER
		gtk gtk_widget_hide $DETAILS_VBOX		
	fi
	
	gtk free $ITER
}

### save the gui content back to the crontab
save_crontab() {
	local ITER CMD SCHEDULE crontmp
	define ITER gtk gtk_server_opaque
	
	crontmp=$(mktemp -p /tmp crontab.$USER.XXXXXXXX)
	gtk gtk_tree_model_get_iter_first $LISTSTORE $ITER
	while [ $GTK -eq 1 ]; do
		tree_model_get_string $LISTSTORE $ITER $T_CMD CMD
		tree_model_get_string $LISTSTORE $ITER $T_SCHEDULE SCHEDULE
		
		echo "$SCHEDULE $CMD" >> $crontmp
		gtk gtk_tree_model_iter_next $LISTSTORE $ITER
	done
	crontab $crontmp
	
	rm -f $crontmp
	gtk free $ITER
	
	[ ! -x $CRON_SERVICE ] && Xdialog --title "$(gettext 'Reminder')" --infobox \
"$(gettext 'Task Scheduler requires the Task Scheduler service to be enabled, otherwise none of the tasks will run.
The Task Scheduler service appears disabled; please enable it using Services Manager.')
" 0 0 90000	
}

################### main ###################
### gtk-server localisation stanza
gtk gtk_server_define setlocale NONE STRING 2 INT STRING
gtk gtk_server_define bindtextdomain NONE STRING 2 STRING STRING
gtk gtk_server_define textdomain NONE STRING 1 STRING
gtk setlocale 6 \"\" # 6 corresponds to LC_ALL
gtk bindtextdomain $TEXTDOMAIN $TEXTDOMAINDIR
gtk textdomain $TEXTDOMAIN

# init gtk
add_missing_functions
gtk gtk_init

define BUILDER gtk gtk_builder_new
define RESULT gtk gtk_builder_add_from_file $BUILDER "${0}.xml"
gtk gtk_builder_connect_signals $BUILDER

define MAIN_WINDOW gtk gtk_builder_get_object $BUILDER main_window
define CLOSE_BUTTON gtk gtk_builder_get_object $BUILDER close-button
define ADD_BUTTON gtk gtk_builder_get_object $BUILDER add-button
define DEL_BUTTON gtk gtk_builder_get_object $BUILDER del-button
define APPLY_BUTTON gtk gtk_builder_get_object $BUILDER apply-button

define TREEVIEW gtk gtk_builder_get_object $BUILDER treeview
define LISTSTORE gtk gtk_builder_get_object $BUILDER liststore
define DETAILS_VBOX gtk gtk_builder_get_object $BUILDER details-vbox

define CMD_ENTRY gtk gtk_builder_get_object $BUILDER cmd-entry

define ENTRY_MONTH gtk gtk_builder_get_object $BUILDER entry-month
define ENTRY_DAY gtk gtk_builder_get_object $BUILDER entry-day
define ENTRY_HOUR gtk gtk_builder_get_object $BUILDER entry-hour
define ENTRY_MINUTE gtk gtk_builder_get_object $BUILDER entry-minute
define COMBO_DOW gtk gtk_builder_get_object $BUILDER combo-dow

define AT_MONTH gtk gtk_builder_get_object $BUILDER at-month
define AT_DAY gtk gtk_builder_get_object $BUILDER at-day
define AT_HOUR gtk gtk_builder_get_object $BUILDER at-hour
define AT_MINUTE gtk gtk_builder_get_object $BUILDER at-minute

gtk g_object_unref $BUILDER

### connect signals
gtk gtk_server_connect $MAIN_WINDOW delete-event quit
gtk gtk_server_connect $CLOSE_BUTTON clicked quit
gtk gtk_server_connect $ADD_BUTTON clicked add-entry
gtk gtk_server_connect $DEL_BUTTON clicked remove-entry
gtk gtk_server_connect $APPLY_BUTTON clicked update-entry

gtk gtk_server_connect $TREEVIEW cursor-changed cursor-changed

### init app
gtk gtk_widget_hide $DETAILS_VBOX
load_crontab

### gtk main loop
gtk gtk_widget_show $MAIN_WINDOW
while true; do 
	define EVENT gtk gtk_server_callback wait
	case $EVENT in
		quit) save_crontab; break ;;
		cursor-changed) update_details ;;
				
		add-entry) add_entry ;;
		update-entry) update_entry ;;
		remove-entry) 
			if Xdialog --title "Confirm" --yesno "Are you sure you want to delete this entry?" 0 0; then
				remove_entry 
			fi ;;
	esac
done
gtk gtk_server_exit 

