easydoneitCTui

Easydoneit Terminal UI
git clone https://noulin.net/git/easydoneitCTui.git
Log | Files | Refs | LICENSE

edt.c (55502B)


      1 #! /usr/bin/env sheepy
      2 
      3 #include <stdio.h>
      4 #include <stdlib.h>
      5 #include <ncurses.h>
      6 #include <unistd.h>
      7 #include <signal.h>
      8 #include "shpPackages/md/md.h"
      9 #include "shpPackages/ini/src/ini.h"
     10 #include "tui/tui.h"
     11 #include "libsheepyObject.h"
     12 #include "edCore.h"
     13 
     14 // TODO add n 'add many tasks' and m 'add many one line tasks'
     15 // TODO sort group list
     16 // TODO handle path in statusBar when it exceeds screen size
     17 
     18 /** path element
     19  */
     20 typedef struct {
     21 	/** group id */
     22 	char group[16+1];
     23 	/** position in list */
     24 	u16 pos;
     25 	/** index in list of first line in the window */
     26 	u16 firstLineOnScreen;
     27 } pathInTreeElement;
     28 
     29 /** maximum path length stored, when MAX_PATH_DEPTH is reached, the oldest path is removed */
     30 // TODO test with low MAX
     31 #define MAX_PATH_DEPTH 1000
     32 /** path in tree circular buffer */
     33 staticArrayT(pathInTreeT, pathInTreeElement, MAX_PATH_DEPTH);
     34 /** pathInTree has the path browsed without the group in WGROUP */
     35 pathInTreeT pathInTree;
     36 /** titles for group in pathInTree and currentGroup for statusBar */
     37 smallArrayt *pathTitles = NULL;
     38 
     39 /** main window */
     40 WINDOW *mainwin;
     41 /** sub windows (colummns) */
     42 void **subwins = NULL;
     43 enum { WLEFT, WGROUP, WVIEW, WSEARCH, WHELP};
     44 
     45 /** window attributes */
     46 typedef struct {
     47 	/** this windows index in subwins array */
     48 	u8 subwin;
     49 	/** coordinate */
     50 	i16 row;
     51 	/** coordinate */
     52 	i16 col;
     53 	/** size */
     54 	i16 szrow;
     55 	/** size */
     56 	i16 szcol;
     57 	/** index in list of first line on screen */
     58 	i32 indexFirstLine;
     59 } awindow;
     60 
     61 /** window array for attributes */
     62 awindow windows[5];
     63 
     64 /** screen size */
     65 int row,col;
     66 
     67 enum {SHOWING_DATABASE, SHOWING_BOOKMARKS, SEARCH, SHOWING_SEARCH, HELP};
     68 /** ui state for user input */
     69 i8 state = SHOWING_DATABASE;
     70 i8 previousState;
     71 /** state for Info (F9) function - toggle between info and text/group */
     72 bool stateInfo = false;
     73 
     74 /** group in the WGROUP window */
     75 char currentGroup[ID_LENGTH+1];
     76 /** cursor in the WGROUP window */
     77 char selectedGroup[ID_LENGTH+1];
     78 /** index of cursor in WLEFT window */
     79 u16 leftCursor   = 0;
     80 /** index if cursor in WGROUP */
     81 u16 selectedLine = 0;
     82 
     83 /** list of select items (tids) */
     84 smallArrayt *selectedList = NULL;
     85 smallArrayt *selectedListAttributes = NULL;
     86 
     87 /** list in WLEFT window */
     88 smallArrayt *lsLeft   = NULL;
     89 /** list in WGROUP window */
     90 smallArrayt *lsGroup  = NULL;
     91 /** list in WVIEW window */
     92 smallArrayt *viewText = NULL;
     93 bool viewTextIsGroup  = false;
     94 
     95 const int cursorColor       = 8;
     96 const int regularListColor  = 0;
     97 const int groupListColor    = 2;
     98 const int selectedListColor = 3;
     99 
    100 char statusSymbols[] = "AD>^# ";
    101 i8   statusColors[]  = {0, 6, 3, 14, 4, 5};
    102 typedef enum { HIDE_STATUS, SHOW_STATUS} displayStatusT;
    103 
    104 char *searchString = NULL;
    105 
    106 typedef enum { NO_PAPER, COPIED, CUT} paperfunctionT;
    107 paperfunctionT paperfunction = NO_PAPER;
    108 
    109 /* PROTOTYPES */
    110 void statusBar(void);
    111 
    112 /**
    113  * add group to pathInTree with cursor position in group list
    114  * @param
    115  *   groupid group id to add
    116  * @param
    117  *   pos     cursor position in group list
    118  */
    119 void addGroupToPath(const char *groupid, u16 pos) {
    120 	if (staticArrayIsFull(pathInTree)) {
    121 		// remove oldest path when full
    122 		staticArrayDequeue(pathInTree);
    123 	}
    124 
    125 	// allocate element
    126 	staticArrayPush(pathInTree);
    127 	// store groupid
    128 	strcpy(staticArrayLast(pathInTree).group, groupid);
    129 	// store cursor position
    130 	staticArrayLast(pathInTree).pos               = pos;
    131 	// store window state
    132 	staticArrayLast(pathInTree).firstLineOnScreen = windows[WGROUP].indexFirstLine;
    133 	// copy window state WLEFT to keep same view as WGROUP
    134 	windows[WLEFT].indexFirstLine                 = windows[WGROUP].indexFirstLine;
    135 }
    136 
    137 /**
    138  * go back in path history
    139  */
    140 void moveToParentGroup(void) {
    141 	if (not staticArrayIsEmpty(pathInTree)) {
    142 		// there is a parent
    143 		// set last path to currentGroup
    144 		strcpy(currentGroup, staticArrayLast(pathInTree).group);
    145 		// set cursor in WGROUP
    146 		selectedLine                   = staticArrayLast(pathInTree).pos;
    147 		// set first line index in window
    148 		windows[WGROUP].indexFirstLine = staticArrayLast(pathInTree).firstLineOnScreen;
    149 		// pop path
    150 		staticArrayPop(pathInTree);
    151 		if (not staticArrayIsEmpty(pathInTree)) {
    152 			// set first line in window for new parent path
    153 			windows[WLEFT].indexFirstLine = staticArrayLast(pathInTree).firstLineOnScreen;
    154 		}
    155 	}
    156 	// TODO handle situation when pathInTree is empty and currentGroup is not root (lost history in the circular buffer)
    157 }
    158 
    159 /** get first group id in pathInTree */
    160 const char *getParentGroupInPath(void) {
    161 	if (not staticArrayIsEmpty(pathInTree)) {
    162 		// return last element in pathInTree
    163 		return staticArrayLast(pathInTree).group;
    164 	}
    165 	else {
    166 		// database top or pathInTree empty
    167 		return "";
    168 	}
    169 }
    170 
    171 /** create string with status
    172  * @param
    173  *   d dictionary containing the information about the group
    174  * @return
    175  *   statusIndex index in TASK_STATUS
    176  * @return
    177  *   string to be freed in the format 'STATUS TITLE'
    178  */
    179 char *statusAndTitle(smallDictt *d, i8 *statusIndex) {
    180 	// status is one char
    181 	char statusSymbol[3] = "  ";
    182 	*statusIndex         = 0;
    183 
    184 	// convert status string to status index in TASK_STATUS array
    185 	for (; *statusIndex < TASK_STATUS_VOID+1 ; (*statusIndex)++) {
    186 		if (eqG(getG(d, rtChar, "status"), TASK_STATUS[*statusIndex])) {
    187 			break;
    188 		}
    189 	}
    190 	statusSymbol[0]      = statusSymbols[*statusIndex];
    191 	char *s              = catS(statusSymbol, getG(d, rtChar, "title"));
    192 	return s;
    193 }
    194 
    195 /** display list in window w with cursor and showing/hiding each task/group status */
    196 void displayGroupList(smallArrayt *list, u16 w, u16 cursor, displayStatusT status) {
    197 	if (lenG(list) <= windows[w].szrow) {
    198 		// the list is smaller than the window
    199 		// the list top is the first line in the window
    200 		windows[w].indexFirstLine = 0;
    201 	}
    202 	// when the list is longer than the window, check that the list end is at window bottom
    203 	#define checkEndListInWindow(list, w) ((lenG(list) > windows[w].szrow) and ((windows[w].indexFirstLine + windows[w].szrow) > lenG(list)))
    204 	if checkEndListInWindow(list, w) {
    205 		// keep list end at window bottom
    206 		windows[w].indexFirstLine = lenG(list) - windows[w].szrow -1;
    207 	}
    208 	// print list in window
    209 	enumerateSmallArray(list, L, line) {
    210 		cast(smallDictt*, d, L)
    211 		if (line < windows[w].indexFirstLine) {
    212 			// skip elements before first line on screen
    213 			goto end;
    214 		}
    215 		i8 i;
    216 		char *s;
    217 		if (status == HIDE_STATUS) {
    218 			// hide status for database list, use regularListColor
    219 			i = regularListColor;
    220 		}
    221 		else {
    222 			// color according to status
    223 			s = statusAndTitle(d, &i);
    224 		}
    225 		if (line == cursor) {
    226 			// show cursor in cursorColor
    227 			wcolor_set(subwins[w], cursorColor, NULL);
    228 			if (w == WGROUP) {
    229 				// get tid at cursor position in list
    230 				// only for list in WGROUP window
    231 				strcpy(selectedGroup, getG(d, rtChar, "tid"));
    232 			}
    233 		}
    234 		else {
    235 			// set color
    236 			if (hasG(selectedList, getG(d, rtChar, "tid"))) {
    237 				wcolor_set(subwins[w], selectedListColor, NULL);
    238 			}
    239 			else {
    240 				if (eqG(getG(d, rtChar, "group"), "GROUP")) {
    241 					wcolor_set(subwins[w], groupListColor, NULL);
    242 				}
    243 				else {
    244 					wcolor_set(subwins[w], statusColors[i], NULL);
    245 				}
    246 			}
    247 		}
    248 		if (status == HIDE_STATUS) {
    249 			// print title without status
    250 			mvwaddnstr(subwins[w], line - windows[w].indexFirstLine, 0, getG(d, rtChar, "title"), windows[w].szcol-1);
    251 		}
    252 		else {
    253 			// print title with status
    254 			mvwaddnstr(subwins[w], line - windows[w].indexFirstLine, 0, s, windows[w].szcol-1);
    255 			free(s);
    256 		}
    257 		if (line == cursor) {
    258 			// fill the cursor line with cursorColor
    259 			wchgat(subwins[w],-1, 0, cursorColor , NULL);
    260 		}
    261 end:
    262 		finishG(d);
    263 	}
    264 }
    265 
    266 /** show list in WLEFT window */
    267 void showLeft(void) {
    268 	if (not currentGroup[0]) {
    269 		// path is database list, show nothing
    270 		return;
    271 	}
    272 	if (getParentGroupInPath()[0]) {
    273 		// a group is displayed on the left
    274 		displayGroupList(lsLeft, WLEFT, leftCursor, SHOW_STATUS);
    275 	}
    276 	else {
    277 		// display database list without status
    278 		displayGroupList(lsLeft, WLEFT, leftCursor, HIDE_STATUS);
    279 	}
    280 }
    281 
    282 /**
    283  * generate database list
    284  * @return
    285  *   ls array of dictionaries, same format as list_group
    286  */
    287 void listDatabases(smallArrayt *ls) {
    288 	// list selected databases (from .easydoneit.ini)
    289 	enumerateSmallArray(selected, S, line) {
    290 		castS(s, S);
    291 		createAllocateSmallDict(task_d);
    292 		setG(task_d, "head", "element");
    293 		setG(task_d, "tid", "");
    294 		setG(task_d, "position", line);
    295 		setG(task_d, "group", "GROUP");
    296 		setG(task_d, "title", ssGet(s));
    297 		setG(task_d, "status", TASK_STATUS[TASK_STATUS_VOID]);
    298 		setG(task_d, "ctime", 0);
    299 		pushNFreeG(ls, task_d);
    300 		finishG(s);
    301 	}
    302 	createAllocateSmallDict(task_d);
    303 	setG(task_d, "head", "empty line");
    304 	setG(task_d, "tid", "");
    305 	setG(task_d, "position", 0);
    306 	setG(task_d, "group", "");
    307 	setG(task_d, "title", "");
    308 	setG(task_d, "status", "");
    309 	setG(task_d, "ctime", 0);
    310 	pushNFreeG(ls, task_d);
    311 }
    312 
    313 /**
    314  * generate and display list in WLEFT window
    315  * @param
    316  *   parent group id or "" for database root
    317  */
    318 void viewLeftParent(const char *parent) {
    319 	if (parent[0] == 0) {
    320 		// database root
    321 		if (not currentGroup[0]) {
    322 			// path is database list, show nothing
    323 			// the database list is in WGROUP, the user is selecting a database
    324 			return;
    325 		}
    326 		// show database list
    327 		leftCursor = staticArrayGet(pathInTree, pathInTree.last-1).pos;
    328 		// create list
    329 		if (lsLeft) {
    330 			terminateG(lsLeft);
    331 		}
    332 		initiateAllocateSmallArray(&lsLeft);
    333 		listDatabases(lsLeft);
    334 	}
    335 	else {
    336 		// show parent group
    337 		if (lsLeft) {
    338 			terminateG(lsLeft);
    339 		}
    340 		lsLeft = list_group(parent);
    341 		// find currentGroup in list
    342 		// TODO add a pos parameter to viewLeftParent and remove this loop
    343 		enumerateSmallArray(lsLeft, L, pI) {
    344 			cast(smallDictt*, d, L)
    345 			if (eqG(getG(d, rtChar, "tid"), currentGroup)) {
    346 				break;
    347 			}
    348 			finishG(d);
    349 		}
    350 		leftCursor = pI;
    351 	}
    352 	showLeft();
    353 }
    354 
    355 /** show list in WGROUP window */
    356 void showGroup(void) {
    357 	if (lenG(lsGroup) == 1) {
    358 		// list is empty, set invalid selectedGroup tid, no cursor in list
    359 		// (lsGroup always has an empty item at the end of the list)
    360 		selectedGroup[0] = 0;
    361 	}
    362 	displayGroupList(lsGroup, WGROUP, selectedLine, SHOW_STATUS);
    363 }
    364 
    365 /**
    366  * generate and display list in WGROUP window
    367  * @param
    368  *   currentGroup group id
    369  */
    370 void viewGroup(const char *currentGroup) {
    371 	if (state == SHOWING_SEARCH) {
    372 		goto show;
    373 	}
    374 	if (lsGroup) {
    375 		terminateG(lsGroup);
    376 	}
    377 ifState:
    378 	if (not currentGroup[0]) {
    379 		// path is database list
    380 		// show database list to select another database
    381 		initiateAllocateSmallArray(&lsGroup);
    382 		listDatabases(lsGroup);
    383 	}
    384 	else if (state == SHOWING_BOOKMARKS) {
    385 		lsGroup = listBookmarks();
    386 	}
    387 	else if (state == SEARCH) {
    388 		if (isBlankG(searchString)) {
    389 			// the search string is empty
    390 			// leave search results
    391 			state = previousState;
    392 			// show previous state: bookmark or current group
    393 			goto ifState;
    394 		}
    395 		state = SHOWING_SEARCH;
    396 		// SEARCH
    397 		smallArrayt *searchR = search_string_in_tree(currentGroup, trimG(&searchString));
    398 		uniqG(searchR, unusedV);
    399 		createAllocateSmallArray(hit_tids);
    400 		forEachSmallArray(searchR, S) {
    401 			castS(s, S);
    402 			char *id = ssGet(s);
    403 			*(id+ID_LENGTH) = 0;
    404 			pushG(hit_tids, id);
    405 			finishG(s);
    406 		}
    407 		smashG(searchR);
    408 		uniqG(hit_tids, unusedV);
    409 		initiateAllocateSmallArray(&lsGroup);
    410 		enumerateSmallArray(hit_tids, H, count) {
    411 			castS(h, H);
    412 			smallDictt *d = get_task_in_list_group_format(ssGet(h));
    413 			setG(d, "position", count);
    414 			pushNFreeG(lsGroup, d);
    415 			finishG(h);
    416 		}
    417 		terminateG(hit_tids);
    418 		createAllocateSmallDict(task_d);
    419 		setG(task_d, "head", "empty line");
    420 		setG(task_d, "tid", "");
    421 		setG(task_d, "position", 0);
    422 		setG(task_d, "group", "");
    423 		setG(task_d, "title", "");
    424 		setG(task_d, "status", "");
    425 		setG(task_d, "ctime", 0);
    426 		pushNFreeG(lsGroup, task_d);
    427 	}
    428 	else {
    429 		// show group
    430 		lsGroup = list_group(currentGroup);
    431 	}
    432 	// check the list length and adjust selectedLine when there is only 1 line
    433 	if (selectedLine+2 > lenG(lsGroup)) {
    434 		selectedLine = lenG(lsGroup)-2;
    435 	}
    436 show:
    437 	showGroup();
    438 }
    439 
    440 /** show text in WVIEW window */
    441 void showSelected(const char *selected) {
    442 	if (not currentGroup[0]) {
    443 		// path is database list, show nothing
    444 		return;
    445 	}
    446 	if (windows[WVIEW].indexFirstLine < 0) {
    447 		// keep first line index in list
    448 		windows[WVIEW].indexFirstLine = 0;
    449 	}
    450 	if (lenG(viewText) <= windows[WVIEW].szrow) {
    451 		// the list is smaller than the window
    452 		// the list top is the first line in the window
    453 		windows[WVIEW].indexFirstLine = 0;
    454 	}
    455 	//if ((lenG(viewText) > windows[WVIEW].szrow) and ((windows[WVIEW].indexFirstLine + windows[WVIEW].szrow) > lenG(viewText))) {
    456 	if checkEndListInWindow(viewText, WVIEW) {
    457 		// keep list end at window bottom
    458 		windows[WVIEW].indexFirstLine = lenG(viewText) - windows[WVIEW].szrow -1;
    459 	}
    460 	// in root show all groups
    461 	// in groups, show text for task at pos 0 (group title task)
    462 	if (viewTextIsGroup and ((selectedLine != 0) or eqG(currentGroup, "root"))) {
    463 		// show group at cursor in WGROUP window
    464 		enumerateSmallArray(viewText, L, line) {
    465 			cast(smallDictt*, d, L)
    466 			if (line < windows[WVIEW].indexFirstLine) {
    467 				// skip elements before first line on screen
    468 				goto endGroup;
    469 			}
    470 			// print status and title
    471 			i8 i;
    472 			char *s = statusAndTitle(d, &i);
    473 			// set color
    474 			if (eqG(getG(d, rtChar, "group"), "GROUP")) {
    475 				wcolor_set(subwins[WVIEW], groupListColor, NULL);
    476 			}
    477 			else {
    478 				wcolor_set(subwins[WVIEW], statusColors[i], NULL);
    479 			}
    480 			mvwaddnstr(subwins[WVIEW], line - windows[WVIEW].indexFirstLine, 0, s, windows[WVIEW].szcol-1);
    481 			free(s);
    482 endGroup:
    483 			finishG(d);
    484 		}
    485 	}
    486 	else {
    487 		// show task text
    488 		enumerateSmallArray(viewText, L, line) {
    489 			castS(l, L);
    490 			if (line < windows[WVIEW].indexFirstLine) {
    491 				// skip elements before first line on screen
    492 				goto endText;
    493 			}
    494 			mvwaddnstr(subwins[WVIEW], line - windows[WVIEW].indexFirstLine, 0, ssGet(l), windows[WVIEW].szcol-1);
    495 endText:
    496 			finishG(l);
    497 		}
    498 	}
    499 }
    500 
    501 /**
    502  * generate and display list in WVIEW window
    503  * @param
    504  *   selected tid at cursor in WGROUP
    505  */
    506 void viewSelected(const char *selected) {
    507 	if (viewText) {
    508 		terminateG(viewText);
    509 	}
    510 	if (not currentGroup[0]) {
    511 		// path is database list, show nothing
    512 		return;
    513 	}
    514 	if (state == SHOWING_BOOKMARKS) {
    515 		// setup database, bookmarks can be in any database
    516 		save_edi_core_data_location();
    517 		setup_data_location_for_tid(selected);
    518 	}
    519 	// in root show all groups
    520 	// in groups, show text for task at pos 0 (group title task)
    521 	if (is_this_task_a_group(selected) and ((selectedLine != 0) or eqG(currentGroup, "root"))) {
    522 		// show group
    523 		viewText        = list_group(selected);
    524 		viewTextIsGroup = true;
    525 	}
    526 	else {
    527 		// show task text
    528 		viewText        = display_task(selected, passThroughTitle);
    529 		viewTextIsGroup = false;
    530 	}
    531 	if (state == SHOWING_BOOKMARKS) {
    532 		restore_edi_core_data_location();
    533 	}
    534 	showSelected(selected);
    535 }
    536 
    537 void showHelp(void) {
    538 	windows[WHELP].subwin = listLength(subwins);
    539 #define margin 4
    540 	windows[WHELP].row    = margin;
    541 	windows[WHELP].col    = margin;
    542 	windows[WHELP].szrow  = row - windows[WHELP].row - margin;
    543 	windows[WHELP].szcol  = col - windows[WHELP].col - margin;
    544 	void *w;
    545 	listPush(&subwins, EVA(w, newwin(windows[WHELP].szrow, windows[WHELP].szcol, windows[WHELP].row, windows[WHELP].col)) );
    546 	setSubwindows(subwins);
    547 	wcolor_set(subwins[windows[WHELP].subwin], 1, NULL);
    548 	// fill background with color
    549 	char *b = malloc(windows[WHELP].szrow * windows[WHELP].szcol +1);
    550 	range(i, windows[WHELP].szrow * windows[WHELP].szcol) {
    551 		b[i] = ' ';
    552 	}
    553 	b[windows[WHELP].szrow * windows[WHELP].szcol] = 0;
    554 	waddstr(subwins[windows[WHELP].subwin], b);
    555 	free(b);
    556 	box(w, 0 , 0);
    557 	mvwaddnstr(subwins[windows[WHELP].subwin], 0, 2, " HELP ", windows[WHELP].szcol-2);
    558 	smallArrayt *help = createSA(
    559 			"q              quit                                 g              set active status",
    560 			"INSERT/i       new task                             h              set done status",
    561 			"DELETE/p       delete selected items                j              set ongoing status",
    562 			"z              select/deselect all items            k              set pending status",
    563 			"x              cut selected items                   l              set inactive status",
    564 			"c              copy selected items                  o              set unknown status",
    565 			"v              paste selected items                 a              attach files to cursor item",
    566 			"b              link selected items                  d              convert task to group or group to task",
    567 			"DOWN           move cursor down                     t              toggle top/bottom",
    568 			"END            page down list",
    569 			"UP             move cursor up",
    570 			"HOME           page up list",
    571 			"RIGHT/ENTER    enter group/database (or edit task)",
    572 			"LEFT/BACKSPACE browse back in history",
    573 			"PAGE DOWN      page down description",
    574 			"PAGE UP        page up description",
    575 			"SPACE          select multiple items",
    576 			"F2             deselect items",
    577 			"F3             toggle active filter",
    578 			"F4             toggle done filter",
    579 			"F5             toggle ongoing filter",
    580 			"F6             toggle pending filter",
    581 			"F7             toggle inactive filter",
    582 			"F8             view all group tasks",
    583 			"F9             show task information",
    584 			"F10            search",
    585 			"F11            add selected items to bookmarks",
    586 			"F12            show bookmarks",
    587 			"",
    588 			"List symbols: 'A' Active, 'D' Done, '>' Ongoing, '^' Pending, '#' inactive, ' ' Unknown"
    589 	);
    590 	enumerateSmallArray(help, H, line) {
    591 		castS(h, H);
    592 		mvwaddnstr(subwins[windows[WHELP].subwin], 2+line, 3, ssGet(h), windows[WHELP].szcol-2);
    593 		if (2+line == windows[WHELP].szrow-2) {
    594 			// prevent print outside the window
    595 			break;
    596 		}
    597 		finishG(h);
    598 	}
    599 	terminateG(help);
    600 }
    601 
    602 /**
    603  * first line in main window
    604  * keyboard commands
    605  */
    606 void topMenu(void) {
    607 	// print menu, inverted colors to the end of the screen
    608 	int colorPair = 1;
    609 	move(0,0);
    610 	color_set(colorPair, NULL);
    611 	attron(A_REVERSE);
    612 	addnstr("F1 Help F2 Deselect ", col-1);
    613 
    614 	i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ACTIVE);
    615 	if (*flt == DISABLE) {
    616 		color_set(cursorColor, NULL);
    617 	}
    618 	else {
    619 		color_set(colorPair, NULL);
    620 	}
    621 
    622 	addnstr("F3 Active", col-1);
    623 	color_set(colorPair, NULL);
    624 	addnstr(" ", col-1);
    625 
    626 	flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_DONE);
    627 	if (*flt == DISABLE) {
    628 		color_set(cursorColor, NULL);
    629 	}
    630 	else {
    631 		color_set(colorPair, NULL);
    632 	}
    633 
    634 	addnstr("F4 Done", col-1);
    635 	color_set(colorPair, NULL);
    636 	addnstr(" ", col-1);
    637 
    638 	flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ONGOING);
    639 	if (*flt == DISABLE) {
    640 		color_set(cursorColor, NULL);
    641 	}
    642 	else {
    643 		color_set(colorPair, NULL);
    644 	}
    645 
    646 	addnstr("F5 Ongoing", col-1);
    647 	color_set(colorPair, NULL);
    648 	addnstr(" ", col-1);
    649 
    650 	flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_PENDING);
    651 	if (*flt == DISABLE) {
    652 		color_set(cursorColor, NULL);
    653 	}
    654 	else {
    655 		color_set(colorPair, NULL);
    656 	}
    657 
    658 	addnstr("F6 Pending", col-1);
    659 	color_set(colorPair, NULL);
    660 	addnstr(" ", col-1);
    661 
    662 	flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_INACTIVE);
    663 	if (*flt == DISABLE) {
    664 		color_set(cursorColor, NULL);
    665 	}
    666 	else {
    667 		color_set(colorPair, NULL);
    668 	}
    669 
    670 	addnstr("F7 Inactive", col-1);
    671 	color_set(colorPair, NULL);
    672 	addnstr(" F8 View Group F9 Info F10 Search F11 Bookmark F12 Bookmarks", col-1);
    673 	attroff(A_REVERSE);
    674 	chgat(-1, A_REVERSE, colorPair , NULL);
    675 }
    676 
    677 /**
    678  * show pathInTree at the bottom of main window
    679  */
    680 void statusBar(void) {
    681 	// print path at bottom
    682 	move(row-1,0);
    683 	color_set(2, NULL);
    684 	smallStringt *p = joinG(pathTitles, "/");
    685 	addnstr(ssGet(p), col-1);
    686 	clrtoeol();
    687 }
    688 
    689 /**
    690  * place WLEFT, WGROUP and WVIEW on screen
    691  * at start and the terminal is resized
    692  */
    693 void placeCols(void) {
    694 	// create columns 15 35 50
    695 	int szWin1 = (col * 3) / (2 * 10);
    696 	int szWin2 = (col * 7) / (2 * 10);
    697 	int szWin3 = col - szWin1 -szWin2;
    698 
    699 	windows[WLEFT].row    = 1;
    700 	windows[WLEFT].col    = 0;
    701 	windows[WLEFT].szrow  = row-2;
    702 	windows[WLEFT].szcol  = szWin1-1;
    703 	windows[WGROUP].row   = 1;
    704 	windows[WGROUP].col   = szWin1;
    705 	windows[WGROUP].szrow = row-2;
    706 	windows[WGROUP].szcol = szWin2-1;
    707 	windows[WVIEW].row    = 1;
    708 	windows[WVIEW].col    = szWin1+szWin2;
    709 	windows[WVIEW].szrow  = row-2;
    710 	windows[WVIEW].szcol  = szWin3;
    711 }
    712 
    713 /**
    714  * create WLEFT, WGROUP and WVIEW
    715  */
    716 void createCols(void) {
    717 	free(subwins);
    718 	subwins = NULL;
    719 
    720 	placeCols();
    721 
    722 	// initialize firstLineOnScreen index
    723 	windows[WLEFT].indexFirstLine  = 0;
    724 	windows[WGROUP].indexFirstLine = 0;
    725 	windows[WVIEW].indexFirstLine  = 0;
    726 
    727 	// create 3 window colunms WLEFT, WGROUP, WVIEW
    728 	WINDOW *w;
    729 
    730 	// refresh here, otherwise the new window is invisible
    731 	refresh();
    732 	// create left window
    733 	w = newwin(windows[WLEFT].szrow, windows[WLEFT].szcol, windows[WLEFT].row, windows[WLEFT].col);
    734 	listPush(&subwins, w);
    735 
    736 	// create group window
    737 	w = newwin(windows[WGROUP].szrow, windows[WGROUP].szcol, windows[WGROUP].row, windows[WGROUP].col);
    738 	listPush(&subwins, w);
    739 
    740 	// create view window
    741 	w = newwin(windows[WVIEW].szrow, windows[WVIEW].szcol, windows[WVIEW].row, windows[WVIEW].col);
    742 	listPush(&subwins, w);
    743 
    744 	setSubwindows(subwins);
    745 }
    746 
    747 /**
    748  * resize the windows when the terminal is resized
    749  */
    750 void resizeCols(void) {
    751 
    752 	placeCols();
    753 
    754 	// erase previous content to remove all artifacts
    755 	eraseSubwindows();
    756 	// refresh to show the windows again
    757 	refresh();
    758 	enumerateType(void, subwins, w, i) {
    759 		mvwin(*w, windows[i].row, windows[i].col);
    760 		wresize(*w, windows[i].szrow, windows[i].szcol);
    761 	}
    762 }
    763 
    764 /**
    765  * fill WLEFT, WGROUP, WVIEW
    766  */
    767 void fillCols(void) {
    768 	viewLeftParent(getParentGroupInPath());
    769 	viewGroup(currentGroup);
    770 	viewSelected(selectedGroup);
    771 }
    772 
    773 /**
    774  * display again the lists in WLEFT, WGROUP and WVIEW without updates
    775  * when the terminal is resized
    776  */
    777 void showCols(void) {
    778 	showLeft();
    779 	showGroup();
    780 	showSelected(selectedGroup);
    781 }
    782 
    783 /**
    784  * detect when the terminal is resized
    785  */
    786 void winch_sigaction(int sig, siginfo_t *si, void *arg) {
    787 	endwin();
    788 	initscr();
    789 	refresh();
    790 	clear();
    791 	row = LINES;
    792 	col = COLS;
    793 	topMenu();
    794 	statusBar();
    795 	resizeCols();
    796 	showCols();
    797 	refreshAll();
    798 }
    799 
    800 void registerSigWinch(void) {
    801 	struct sigaction sa;
    802 	memset(&sa, 0, sizeof(struct sigaction));
    803 	sigemptyset(&sa.sa_mask);
    804 	sa.sa_sigaction = winch_sigaction;
    805 	sa.sa_flags   = SA_SIGINFO;
    806 	sigaction(SIGWINCH, &sa, NULL);
    807 }
    808 
    809 void pasteClipboard(const char *pasteLinkOrCopy) {
    810 	// handle multiple databases
    811 	save_edi_core_data_location();
    812 	// saved_data_location is destination database
    813 	setup_data_location_for_tid(getG(selectedList, rtChar, 0));
    814 	// data_location is source database
    815 	bool sameSrcDstDatabase = true;
    816 	const char *destination_database_name;
    817 	if (not eqG(data_location, saved_data_location)) {
    818 		sameSrcDstDatabase        = false;
    819 		destination_database_name = getDatabaseNameFromPath(saved_data_location);
    820 	}
    821 	if (paperfunction == COPIED) {
    822 		forEachSmallArray(selectedList, T) {
    823 			castS(t, T);
    824 			if (sameSrcDstDatabase) {
    825 				if (eqG(pasteLinkOrCopy, "paste")) {
    826 					copy_task_to_a_group(ssGet(t), currentGroup);
    827 				}
    828 				if (eqG(pasteLinkOrCopy, "link")) {
    829 					smallStringt *c;
    830 					add_task_reference_to_a_group(ssGet(t), EVA(c, allocG(currentGroup)));
    831 					terminateG(c);
    832 				}
    833 			}
    834 			else {
    835 				// always copy to different source/destination databases
    836 				copy_task_to_database(ssGet(t), destination_database_name, currentGroup);
    837 			}
    838 			finishG(t);
    839 		}
    840 	}
    841 	if (paperfunction == CUT) {
    842 		enumerateSmallArray(selectedList, T, i) {
    843 			castS(t, T);
    844 			if (sameSrcDstDatabase) {
    845 				smallArrayt *a = getG(selectedListAttributes, rtSmallArrayt, i);
    846 				if (getG(a, rtI32, 1) == SHOWING_DATABASE) {
    847 					move_task_to_a_group(getG(a, rtChar, 0), ssGet(t), currentGroup);
    848 				}
    849 				else {
    850 					// group in selectedListAttributes is invalid
    851 					move_task_to_a_group(find_group_containing_task(ssGet(t)), ssGet(t), currentGroup);
    852 				}
    853 				finishG(a);
    854 			}
    855 			else {
    856 				const char *new_tid = NULL;
    857 				smallArrayt *a = getG(selectedListAttributes, rtSmallArrayt, i);
    858 				if (getG(a, rtI32, 1) == SHOWING_DATABASE) {
    859 					// move to other database
    860 					new_tid = move_task_to_a_group_to_database(getG(a, rtChar, 0), ssGet(t), destination_database_name, currentGroup);
    861 				}
    862 				else {
    863 					// group in selectedListAttributes is invalid
    864 					new_tid = move_task_to_a_group_to_database(find_group_containing_task(ssGet(t)), ssGet(t), destination_database_name, currentGroup);
    865 				}
    866 				finishG(a);
    867 
    868 				if (new_tid){
    869 					ssize_t index = findG(bookmarks, ssGet(t));
    870 					if (index != -1) {
    871 							// update bookmark
    872 							setG(bookmarks, index, new_tid);
    873 					}
    874 				}
    875 			}
    876 			finishG(t);
    877 		}
    878 	}
    879 	restore_edi_core_data_location();
    880 	// deselect all items
    881 	emptyG(selectedList);
    882 	emptyG(selectedListAttributes);
    883 	paperfunction = NO_PAPER;
    884 }
    885 
    886 void saveEdiState(void) {
    887 	char *path = expandHomeG("~/.easydoneit.bin");
    888 	createAllocateSmallJson(ediState);
    889 
    890 	createAllocateSmallArray(a);
    891 
    892 	range(i, staticArrayCount(pathInTree)) {
    893 		createAllocateSmallArray(a2);
    894 		pushG(a2, staticArrayGet(pathInTree, i).group);
    895 		pushG(a2, (u32)staticArrayGet(pathInTree, i).pos);
    896 		pushG(a2, (u32)staticArrayGet(pathInTree, i).firstLineOnScreen);
    897 		pushNFreeG(a, a2);
    898 	}
    899 
    900 	setNFreeG(ediState, "pathInTree", a);
    901 
    902 	setNFreeG(ediState, "pathTitles", pathTitles);
    903 
    904 	setG(ediState, "state", (int32_t)state);
    905 	setG(ediState, "previousState", (int32_t)previousState);
    906 	setG(ediState, "stateInfo", stateInfo);
    907 	setG(ediState, "currentGroup", currentGroup);
    908 	setG(ediState, "selectedGroup", selectedGroup);
    909 	setG(ediState, "selectedLine", (u32)selectedLine);
    910 	setNFreeG(ediState, "selectedList", selectedList);
    911 	setNFreeG(ediState, "selectedListAttributes", selectedListAttributes);
    912 	setNFreeG(ediState, "lsGroup", lsGroup); // for search results, avoid crash
    913 	// TODO save view text to restore view group
    914 	//setNFreeG(ediState, "searchString", searchString);
    915 	setG(ediState, "paperfunction", paperfunction);
    916 
    917 	//logVarG(ediState);
    918 	smallBytest *B = serialG(ediState);
    919 	writeFileG(B, path);
    920 	terminateManyG(ediState, B);
    921 	free(path);
    922 }
    923 
    924 /** true when state is loaded */
    925 bool loadEdiState(void) {
    926 	bool r = false;
    927 	char *path = expandHomeG("~/.easydoneit.bin");
    928 
    929 	// use default when edi state is not found
    930 	if (fileExists(path)) {
    931 		createAllocateSmallBytes(B);
    932 		readFileG(B, path);
    933 
    934 		createAllocateSmallJson(ediState);
    935 
    936 		deserialG(ediState, B);
    937 		//logVarG(ediState);
    938 
    939 		smallArrayt *a = getG(ediState, rtSmallArrayt, "pathInTree");
    940 
    941 		enumerateSmallArray(a, P, i) {
    942 			cast(smallArrayt *, p, P);
    943 			staticArrayPush(pathInTree);
    944 			strcpy(staticArrayLast(pathInTree).group, getG(p, rtChar, 0));
    945 			staticArrayLast(pathInTree).pos               = getG(p, rtI32, 1);
    946 			staticArrayLast(pathInTree).firstLineOnScreen = getG(p, rtI32, 2);
    947 			finishG(p);
    948 		}
    949 
    950 		finishG(a);
    951 
    952 		pathTitles             = getNDupG(ediState, rtSmallArrayt, "pathTitles");
    953 		state                  = getG(ediState, rtI32, "state");
    954 		previousState          = getG(ediState, rtI32, "previousState");
    955 	    stateInfo              = getG(ediState, rtBool, "stateInfo");
    956 		strcpy(currentGroup,     getG(ediState, rtChar, "currentGroup"));
    957 		strcpy(selectedGroup,    getG(ediState, rtChar, "selectedGroup"));
    958 		selectedLine           = getG(ediState, rtI32, "selectedLine");
    959 		selectedList           = getNDupG(ediState, rtSmallArrayt, "selectedList");
    960 		selectedListAttributes = getNDupG(ediState, rtSmallArrayt,"selectedListAttributes");
    961 		lsGroup                = getNDupG(ediState, rtSmallArrayt, "lsGroup");
    962 		//getG(ediState, rtChar, "searchString");
    963 		paperfunction          = getG(ediState, rtI32, "paperfunction");
    964 
    965 		terminateManyG(ediState, B);
    966 
    967 		r = true;
    968 	}
    969 	free(path);
    970 	return r;
    971 }
    972 
    973 
    974 int main(int argc, char** argv) {
    975 
    976 	//char *c = readFileG(c, argv[1]);
    977 	//logG(md_highlight(c));
    978 
    979 	initLibsheepy(argv[0]);
    980 
    981 	//#if 0
    982 	start("tui");
    983 
    984 	mainwin = initScreen(&row, &col);
    985 
    986 	staticArrayInit(pathInTree);
    987 
    988 	// database top
    989 	// path top title is database name
    990 	// groupid parameter to addGroupToPath is "", pos is the cursor in the database list
    991 	windows[WGROUP].indexFirstLine = 0;
    992 
    993 	if (not loadEdiState()) {
    994 		initiateAllocateSmallArray(&selectedList);
    995 		initiateAllocateSmallArray(&selectedListAttributes);
    996 
    997 		addGroupToPath("", 0);
    998 		initiateAllocateSmallArray(&pathTitles);
    999 		pushG(pathTitles, getG(selected, rtChar, 0));
   1000 		strcpy(currentGroup, "root");
   1001 	}
   1002 
   1003 	topMenu();
   1004 	statusBar();
   1005 	createCols();
   1006 	registerSigWinch();
   1007 	fillCols();
   1008 
   1009 	refreshAll();
   1010 
   1011 	int c;
   1012 	while (((c = wgetch(mainwin)) != 'q') || (state == SEARCH)) {
   1013 		if (state == HELP) {
   1014 			state   = previousState;
   1015 			void *w = listPop(&subwins);
   1016 			setSubwindows(subwins);
   1017 			werase(mainwin);
   1018 			//refresh();
   1019 			wnoutrefresh(mainwin);
   1020 			delwin(w);
   1021 			topMenu();
   1022 			statusBar();
   1023 			showCols();
   1024 		}
   1025 		else if (state == SEARCH) {
   1026 			switch(c){
   1027 				case KEY_F(1):
   1028 					state = previousState;
   1029 					noecho();
   1030 					statusBar();
   1031 					break;
   1032 				case '\n': {
   1033 					// state is SEARCH, viewGroup will perform the search
   1034 
   1035 					move(row-1,0);
   1036 					color_set(cursorColor, NULL);
   1037 					clrtoeol();
   1038 					addnstr("Searching...", col-1);
   1039 					chgat(-1, 0, cursorColor, NULL);
   1040 					refresh();
   1041 
   1042 					noecho();
   1043 					statusBar();
   1044 					// scroll WVIEW window to top
   1045 					windows[WVIEW].indexFirstLine = 0;
   1046 					eraseSubwindows();
   1047 					fillCols();
   1048 					break;
   1049 				}
   1050 				case KEY_BACKSPACE:
   1051 					delG(&searchString, -1, 0);
   1052 			}
   1053 			if (c < KEY_MIN) {
   1054 				iInjectS(&searchString, -1, c);
   1055 			}
   1056 			if (state == SEARCH) {
   1057 				move(row-1,0);
   1058 				color_set(1, NULL);
   1059 				clrtoeol();
   1060 				char *s = appendS("SEARCH (F1 to cancel): ", searchString);
   1061 				addnstr(s, col-1);
   1062 				free(s);
   1063 				chgat(-1, 0, 1, NULL);
   1064 			}
   1065 		}
   1066 		else {
   1067 			if ((c != KEY_F(9)) and (stateInfo == true)) {
   1068 				// exit state info when any key is pressed
   1069 				stateInfo = false;
   1070 			}
   1071 			switch(c){
   1072 				case 'a':
   1073 					// TODO add attachments
   1074 					break;
   1075 				case 't':
   1076 					if (eqG(add_top_or_bottom, "top")) {
   1077 						free(add_top_or_bottom);
   1078 						add_top_or_bottom = strdup("bottom");
   1079 					}
   1080 					else {
   1081 						free(add_top_or_bottom);
   1082 						add_top_or_bottom = strdup("top");
   1083 					}
   1084 					break;
   1085 				case 'd':
   1086 					// convert selected items
   1087 					if (not lenG(selectedList)) {
   1088 						if (not is_this_task_a_group(selectedGroup)) {
   1089 							create_group(selectedGroup);
   1090 						}
   1091 						else {
   1092 							convert_group_to_task(selectedGroup);
   1093 						}
   1094 					}
   1095 					else {
   1096 						// selected items in selectedList
   1097 						forEachSmallArray(selectedList, T) {
   1098 							castS(t, T);
   1099 							if (not is_this_task_a_group(ssGet(t))) {
   1100 								create_group(ssGet(t));
   1101 							}
   1102 							else {
   1103 								convert_group_to_task(ssGet(t));
   1104 							}
   1105 							finishG(t);
   1106 						}
   1107 						emptyG(selectedList);
   1108 						emptyG(selectedListAttributes);
   1109 					}
   1110 					// scroll WVIEW window to top
   1111 					windows[WVIEW].indexFirstLine = 0;
   1112 					eraseSubwindows();
   1113 					fillCols();
   1114 					break;
   1115 				case 'i':
   1116 				case KEY_IC:
   1117 					// INSERT
   1118 					if (state == SHOWING_SEARCH) {
   1119 						// leave search results
   1120 						state = previousState;
   1121 					}
   1122 					create_task(currentGroup);
   1123 					// refresh screen
   1124 					system("reset");
   1125 					reInitScreen(mainwin);
   1126 					topMenu();
   1127 					statusBar();
   1128 					eraseSubwindows();
   1129 					// scroll WVIEW window to top
   1130 					windows[WVIEW].indexFirstLine = 0;
   1131 					fillCols();
   1132 					redrawwin(mainwin);
   1133 					refresh();
   1134 					showCols();
   1135 					forEachType(void, subwins, wd) {
   1136 						redrawwin((WINDOW *)(*wd));
   1137 					}
   1138 					break;
   1139 				case 'p':
   1140 				case KEY_DC:
   1141 					// DELETE
   1142 					if (state == SHOWING_SEARCH) {
   1143 						// leave search results
   1144 						state = previousState;
   1145 					}
   1146 					if (not lenG(selectedList)) {
   1147 						delete_linked_task(currentGroup, selectedGroup);
   1148 						save_edi_core_data_location();
   1149 						if (not tid_exists(selectedGroup)) {
   1150 								ssize_t index = findG(bookmarks, selectedGroup);
   1151 								if (index != -1) {
   1152 										// delete bookmark
   1153 										delG(bookmarks, index, index+1);
   1154 								}
   1155 						}
   1156 						restore_edi_core_data_location();
   1157 					}
   1158 					else {
   1159 						// selected items in selectedList
   1160 						enumerateSmallArray(selectedList, T, i) {
   1161 							castS(t, T);
   1162 							smallArrayt *a = getG(selectedListAttributes, rtSmallArrayt, i);
   1163 							if (getG(a, rtI32, 1) == SHOWING_DATABASE) {
   1164 								delete_linked_task(getG(a, rtChar, 0), ssGet(t));
   1165 							}
   1166 							else {
   1167 								// group in selectedListAttributes is invalid
   1168 								delete_task(ssGet(t));
   1169 							}
   1170 								save_edi_core_data_location();
   1171 								if (not tid_exists(ssGet(t))) {
   1172 										ssize_t index = findG(bookmarks, ssGet(t));
   1173 										if (index != -1) {
   1174 												// delete bookmark
   1175 												delG(bookmarks, index, index+1);
   1176 										}
   1177 								}
   1178 								restore_edi_core_data_location();
   1179 							finishG(a);
   1180 							finishG(t);
   1181 						}
   1182 						emptyG(selectedList);
   1183 						emptyG(selectedListAttributes);
   1184 					}
   1185 					// scroll WVIEW window to top
   1186 					windows[WVIEW].indexFirstLine = 0;
   1187 					eraseSubwindows();
   1188 					fillCols();
   1189 					break;
   1190 				case ' ':
   1191 					// select/deselect item under cursor in the group list
   1192 					if (currentGroup[0]) {
   1193 						// select only in group, not database list
   1194 						ssize_t index = findG(selectedList, selectedGroup);
   1195 						if (index == -1) {
   1196 							// add new selection
   1197 							pushG(selectedList, selectedGroup);
   1198 							createAllocateSmallArray(a);
   1199 							pushG(a, currentGroup);
   1200 							pushG(a, (int32_t)state);
   1201 							pushNFreeG(selectedListAttributes, a);
   1202 						}
   1203 						else {
   1204 							// remove selection
   1205 							delG(selectedList, index, index+1);
   1206 							delG(selectedListAttributes, index, index+1);
   1207 						}
   1208 					}
   1209 				case KEY_DOWN:
   1210 					// move cursor down in WGROUP window
   1211 					// when the cursor reaches the list bottom
   1212 					// it goes back to the top
   1213 					if (selectedLine < lenG(lsGroup)-2) {
   1214 						// move down the list and stay inside
   1215 						selectedLine++;
   1216 					}
   1217 					else {
   1218 						selectedLine                   = 0;
   1219 						windows[WGROUP].indexFirstLine = 0;
   1220 					}
   1221 					if ((selectedLine - windows[WGROUP].indexFirstLine +1) > windows[WGROUP].szrow) {
   1222 						// scroll to keep cursor on screen
   1223 						windows[WGROUP].indexFirstLine++;
   1224 					}
   1225 					// scroll WVIEW window to top
   1226 					windows[WVIEW].indexFirstLine = 0;
   1227 					eraseSubwindows();
   1228 					fillCols();
   1229 					break;
   1230 				case KEY_END:
   1231 					// page down WGROUP window
   1232 					// when the last page is reached, the cursor is moved to the window bottom
   1233 					// on next page down, the cursor is moved to the list top
   1234 					if ((windows[WGROUP].indexFirstLine + windows[WGROUP].szrow) < lenG(lsGroup)-2) {
   1235 						// list middle is on window bottom
   1236 						windows[WGROUP].indexFirstLine += windows[WGROUP].szrow;
   1237 						// keep cursor on window top
   1238 						if checkEndListInWindow(lsGroup, WGROUP) {
   1239 							selectedLine                    = lenG(lsGroup) - windows[WGROUP].szrow -1;
   1240 						}
   1241 						else {
   1242 							selectedLine                    = windows[WGROUP].indexFirstLine;
   1243 						}
   1244 					}
   1245 					else {
   1246 						// list end is on window bottom
   1247 						if (selectedLine != (lenG(lsGroup)-2)) {
   1248 							// cursor not at window bottom, move cursor to bottom
   1249 							selectedLine = lenG(lsGroup)-2;
   1250 						}
   1251 						else {
   1252 							// cursor is at bottom, move to list top
   1253 							selectedLine                   = 0;
   1254 							windows[WGROUP].indexFirstLine = 0;
   1255 						}
   1256 					}
   1257 					// scroll WVIEW window to top
   1258 					windows[WVIEW].indexFirstLine = 0;
   1259 					eraseSubwindows();
   1260 					fillCols();
   1261 					break;
   1262 				case KEY_UP:
   1263 					// move cursor up in WGROUP window
   1264 					// when cursor reaches the list top
   1265 					// it goes back to the bottom
   1266 					if (selectedLine > 0) {
   1267 						// keep cursor in list
   1268 						selectedLine--;
   1269 						if (selectedLine < windows[WGROUP].indexFirstLine) {
   1270 							// scroll to keep cursor on screen
   1271 							windows[WGROUP].indexFirstLine--;
   1272 						}
   1273 					}
   1274 					else {
   1275 						// cursor is at top, move to list bottom
   1276 						selectedLine = lenG(lsGroup)-2;
   1277 						windows[WGROUP].indexFirstLine = lenG(lsGroup) - windows[WGROUP].szrow -1;
   1278 					}
   1279 					// scroll WVIEW window to top
   1280 					windows[WVIEW].indexFirstLine = 0;
   1281 					eraseSubwindows();
   1282 					fillCols();
   1283 					break;
   1284 				case KEY_HOME:
   1285 					// page up WGROUP window
   1286 					// when the first page is reached, the cursor is moved to the window top
   1287 					// on next page up, the cursor is moved to the list bottom
   1288 					if (((windows[WGROUP].indexFirstLine - windows[WGROUP].szrow)) > 0) {
   1289 						// list middle is on window top
   1290 						windows[WGROUP].indexFirstLine -= windows[WGROUP].szrow;
   1291 						// keep cursor on window bottom
   1292 						selectedLine                    = windows[WGROUP].indexFirstLine + windows[WGROUP].szrow -1;
   1293 					}
   1294 					else {
   1295 						// list top is on window top
   1296 						i32 newIndex = 0;
   1297 						if (windows[WGROUP].indexFirstLine) {
   1298 							// currently list top is not in window
   1299 							if (selectedLine >= windows[WGROUP].szrow) {
   1300 								// cursor is outside window after page down
   1301 								// keep cursor at window bottom
   1302 								selectedLine = windows[WGROUP].szrow -1;
   1303 							}
   1304 						}
   1305 						else {
   1306 							if (selectedLine != 0) {
   1307 								// cursor not at window top, move cursor to top
   1308 								selectedLine = 0;
   1309 							}
   1310 							else {
   1311 								// cursor is at top, move to list bottom
   1312 								selectedLine = lenG(lsGroup)-2;
   1313 								newIndex     = lenG(lsGroup) - windows[WGROUP].szrow -1;
   1314 							}
   1315 						}
   1316 						// show list top at WGROUP window top
   1317 						windows[WGROUP].indexFirstLine = newIndex;
   1318 					}
   1319 					// scroll WVIEW window to top
   1320 					windows[WVIEW].indexFirstLine = 0;
   1321 					eraseSubwindows();
   1322 					fillCols();
   1323 					break;
   1324 				case '\n':
   1325 				case KEY_RIGHT:
   1326 					// move to selected item (group, database)
   1327 					if (state == SHOWING_SEARCH) {
   1328 						// leave search results
   1329 						state = previousState;
   1330 					}
   1331 					// scroll WVIEW window to top
   1332 					windows[WVIEW].indexFirstLine = 0;
   1333 					if (state == SHOWING_BOOKMARKS) {
   1334 						// currently showing bookmarks
   1335 						// stop showing bookmarks
   1336 						state = SHOWING_DATABASE;
   1337 					}
   1338 					if (!currentGroup[0]) {
   1339 						// a database is selected
   1340 						// add empty group id for root in pathInTree
   1341 						addGroupToPath("", selectedLine);
   1342 						smallDictt *d = getG(lsGroup, rtSmallDictt, selectedLine);
   1343 						char *dName = getG(d, rtChar, "title");
   1344 						select_database(dName);
   1345 
   1346 						// update database name in status bar
   1347 						free(popG(pathTitles, rtChar));
   1348 						pushG(pathTitles, dName);
   1349 
   1350 						// set current group root, database top
   1351 						strcpy(currentGroup, "root");
   1352 						selectedLine = 0;
   1353 						statusBar();
   1354 						eraseSubwindows();
   1355 						fillCols();
   1356 						break;
   1357 					}
   1358 					// in root enter all groups
   1359 					// in groups, edit text for task at pos 0 (group title task)
   1360 					if (is_this_task_a_group(selectedGroup) and ((selectedLine != 0) or eqG(currentGroup, "root"))) {
   1361 						// a group is selected
   1362 						// add currentGroup to pathInTree
   1363 						addGroupToPath(currentGroup, selectedLine);
   1364 						// set selected group as current group
   1365 						strcpy(currentGroup, selectedGroup);
   1366 						// line 0 is group title, select first task in group instead
   1367 						// viewGroup keeps the cursor in the list
   1368 						// so when the group is empty, it is moved to line 0
   1369 						selectedLine = 1;
   1370 
   1371 						// add new current group title to pathTitles for status bar
   1372 						pushNFreeG(pathTitles, trimG(get_task_title(currentGroup)));
   1373 
   1374 						statusBar();
   1375 						eraseSubwindows();
   1376 						fillCols();
   1377 					}
   1378 					else {
   1379 						// edit task, open editor
   1380 						edit_task(selectedGroup);
   1381 						// refresh screen
   1382 						system("reset");
   1383 						reInitScreen(mainwin);
   1384 						topMenu();
   1385 						statusBar();
   1386 						eraseSubwindows();
   1387 						// scroll WVIEW window to top
   1388 						windows[WVIEW].indexFirstLine = 0;
   1389 						fillCols();
   1390 						redrawwin(mainwin);
   1391 						refresh();
   1392 						showCols();
   1393 						forEachType(void, subwins, wd) {
   1394 							redrawwin((WINDOW *)(*wd));
   1395 						}
   1396 					}
   1397 					break;
   1398 				case KEY_BACKSPACE:
   1399 				case KEY_LEFT: {
   1400 					// move back in browsing history
   1401 					// scroll WVIEW window to top
   1402 					windows[WVIEW].indexFirstLine = 0;
   1403 					if (state == SHOWING_SEARCH) {
   1404 						// leave search results
   1405 						state = previousState;
   1406 						eraseSubwindows();
   1407 						fillCols();
   1408 						break;
   1409 					}
   1410 					if (state == SHOWING_BOOKMARKS) {
   1411 						// currently showing bookmarks
   1412 						// stop showing bookmarks and stay in same group
   1413 						state = SHOWING_DATABASE;
   1414 						eraseSubwindows();
   1415 						fillCols();
   1416 						break;
   1417 					}
   1418 					if ((staticArrayCount(pathInTree) == 1) and (currentGroup[0] not_eq 0)) {
   1419 						// path is root and currentGroup is not database list
   1420 						// show database list in WGROUP window
   1421 						currentGroup[0] = 0;
   1422 					}
   1423 
   1424 					moveToParentGroup();
   1425 					if (lenG(pathTitles) > 1) {
   1426 						// always keep database name in path title
   1427 						// to show current database in status bar
   1428 						free(popG(pathTitles, rtChar));
   1429 					}
   1430 
   1431 					statusBar();
   1432 					eraseSubwindows();
   1433 					fillCols();
   1434 					break;
   1435 					}
   1436 				case KEY_NPAGE:
   1437 					// page down WVIEW window
   1438 					// showSelected keeps the text in the window and adjusts indexFirstLine
   1439 					windows[WVIEW].indexFirstLine += windows[WVIEW].szrow;
   1440 					eraseSubwindows();
   1441 					/* fillCols(); */
   1442 					viewLeftParent(getParentGroupInPath());
   1443 					viewGroup(currentGroup);
   1444 					showSelected("");
   1445 					break;
   1446 				case KEY_PPAGE:
   1447 					// page up WVIEW window
   1448 					// showSelected keeps the text in the window and adjusts indexFirstLine
   1449 					windows[WVIEW].indexFirstLine -= windows[WVIEW].szrow;
   1450 					eraseSubwindows();
   1451 					/* fillCols(); */
   1452 					viewLeftParent(getParentGroupInPath());
   1453 					viewGroup(currentGroup);
   1454 					showSelected("");
   1455 					break;
   1456 				case KEY_F(1):
   1457 					if (state != HELP) {
   1458 						previousState = state;
   1459 						state         = HELP;
   1460 						showHelp();
   1461 					}
   1462 					break;
   1463 				case KEY_F(3): {
   1464 					// toggle active filter
   1465 					// deselect all items
   1466 					emptyG(selectedList);
   1467 					emptyG(selectedListAttributes);
   1468 					if (state == SHOWING_SEARCH) {
   1469 						// leave search results
   1470 						state = previousState;
   1471 					}
   1472 					i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ACTIVE);
   1473 					if (*flt == DISABLE) {
   1474 						*flt = ENABLE;
   1475 					}
   1476 					else {
   1477 						*flt = DISABLE;
   1478 					}
   1479 					// scroll WVIEW window to top
   1480 					windows[WVIEW].indexFirstLine = 0;
   1481 					eraseSubwindows();
   1482 					topMenu();
   1483 					fillCols();
   1484 					break;
   1485 				}
   1486 				case KEY_F(4): {
   1487 					// toggle done filter
   1488 					// deselect all items
   1489 					emptyG(selectedList);
   1490 					emptyG(selectedListAttributes);
   1491 					if (state == SHOWING_SEARCH) {
   1492 						// leave search results
   1493 						state = previousState;
   1494 					}
   1495 					i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_DONE);
   1496 					if (*flt == DISABLE) {
   1497 						*flt = ENABLE;
   1498 					}
   1499 					else {
   1500 						*flt = DISABLE;
   1501 					}
   1502 					// scroll WVIEW window to top
   1503 					windows[WVIEW].indexFirstLine = 0;
   1504 					eraseSubwindows();
   1505 					topMenu();
   1506 					fillCols();
   1507 					break;
   1508 				}
   1509 				case KEY_F(5): {
   1510 					// toggle ongoing filter
   1511 					// deselect all items
   1512 					emptyG(selectedList);
   1513 					emptyG(selectedListAttributes);
   1514 					if (state == SHOWING_SEARCH) {
   1515 						// leave search results
   1516 						state = previousState;
   1517 					}
   1518 					i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ONGOING);
   1519 					if (*flt == DISABLE) {
   1520 						*flt = ENABLE;
   1521 					}
   1522 					else {
   1523 						*flt = DISABLE;
   1524 					}
   1525 					// scroll WVIEW window to top
   1526 					windows[WVIEW].indexFirstLine = 0;
   1527 					eraseSubwindows();
   1528 					topMenu();
   1529 					fillCols();
   1530 					break;
   1531 				}
   1532 				case KEY_F(6): {
   1533 					// toggle pending filter
   1534 					// deselect all items
   1535 					emptyG(selectedList);
   1536 					emptyG(selectedListAttributes);
   1537 					if (state == SHOWING_SEARCH) {
   1538 						// leave search results
   1539 						state = previousState;
   1540 					}
   1541 					i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_PENDING);
   1542 					if (*flt == DISABLE) {
   1543 						*flt = ENABLE;
   1544 					}
   1545 					else {
   1546 						*flt = DISABLE;
   1547 					}
   1548 					// scroll WVIEW window to top
   1549 					windows[WVIEW].indexFirstLine = 0;
   1550 					eraseSubwindows();
   1551 					topMenu();
   1552 					fillCols();
   1553 					break;
   1554 				}
   1555 				case KEY_F(7): {
   1556 					// toggle inactive filter
   1557 					// deselect all items
   1558 					emptyG(selectedList);
   1559 					emptyG(selectedListAttributes);
   1560 					if (state == SHOWING_SEARCH) {
   1561 						// leave search results
   1562 						state = previousState;
   1563 					}
   1564 					i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_INACTIVE);
   1565 					if (*flt == DISABLE) {
   1566 						*flt = ENABLE;
   1567 					}
   1568 					else {
   1569 						*flt = DISABLE;
   1570 					}
   1571 					// scroll WVIEW window to top
   1572 					windows[WVIEW].indexFirstLine = 0;
   1573 					eraseSubwindows();
   1574 					topMenu();
   1575 					fillCols();
   1576 					break;
   1577 				}
   1578 				case KEY_F(8): {
   1579 					// show all descriptions for tasks in list
   1580 					if (viewText) {
   1581 						terminateG(viewText);
   1582 					}
   1583 					if (state == SHOWING_SEARCH) {
   1584 						// show tasks from search results
   1585 						viewText = listTasksInList(lsGroup);
   1586 						viewTextIsGroup = true;
   1587 					}
   1588 					if (state == SHOWING_DATABASE) {
   1589 						// show tasks in current group
   1590 						viewText = listTasksInGroup(currentGroup);
   1591 						viewTextIsGroup = true;
   1592 					}
   1593 					if (state == SHOWING_BOOKMARKS) {
   1594 						// show tasks from bookmarks
   1595 						viewText = listTasksInBookmarks();
   1596 						viewTextIsGroup = true;
   1597 					}
   1598 					// scroll WVIEW window to top
   1599 					windows[WVIEW].indexFirstLine = 0;
   1600 					eraseSubwindows();
   1601 					/* fillCols(); */
   1602 					viewLeftParent(getParentGroupInPath());
   1603 					viewGroup(currentGroup);
   1604 					showSelected("");
   1605 					break;
   1606 				}
   1607 				case KEY_F(9):
   1608 					// toggle info
   1609 					if (not stateInfo) {
   1610 						stateInfo = true;
   1611 						if (viewText) {
   1612 							terminateG(viewText);
   1613 						}
   1614 						//viewText = createSA("fwefw", "wfwef");
   1615 						viewText = show_group_for_task(selectedGroup);
   1616 						viewTextIsGroup = false;
   1617 						// scroll WVIEW window to top
   1618 						windows[WVIEW].indexFirstLine = 0;
   1619 						eraseSubwindows();
   1620 						/* fillCols(); */
   1621 						viewLeftParent(getParentGroupInPath());
   1622 						viewGroup(currentGroup);
   1623 						showSelected("");
   1624 					}
   1625 					else {
   1626 						stateInfo = false;
   1627 						// scroll WVIEW window to top
   1628 						windows[WVIEW].indexFirstLine = 0;
   1629 						eraseSubwindows();
   1630 						fillCols();
   1631 					}
   1632 					break;
   1633 				case '/':
   1634 				case KEY_F(10): {
   1635 					// search string in group
   1636 					if (state == SHOWING_SEARCH) {
   1637 						// leave search results
   1638 						state = previousState;
   1639 						break;
   1640 					}
   1641 					iEmptySF(&searchString);
   1642 					previousState = state;
   1643 					state = SEARCH;
   1644 					echo();
   1645 					move(row-1,0);
   1646 					color_set(1, NULL);
   1647 					clrtoeol();
   1648 					addnstr("SEARCH (F1 to cancel): ", col-1);
   1649 					chgat(-1, 0, 1, NULL);
   1650 					break;
   1651 				}
   1652 				case KEY_F(11): {
   1653 					// bookmark items
   1654 					// remove from bookmark if already bookmarked
   1655 					if (not lenG(selectedList)) {
   1656 						ssize_t index = findG(bookmarks, selectedGroup);
   1657 						if (index == -1) {
   1658 							// no duplicated bookmarks
   1659 							// remove empty strings in bookmarks
   1660 							compactG(bookmarks);
   1661 							pushG(bookmarks, selectedGroup);
   1662 						}
   1663 						else {
   1664 							// remove bookmark
   1665 							delG(bookmarks, index, index+1);
   1666 						}
   1667 					}
   1668 					else {
   1669 						// selected items in selectedList
   1670 						forEachSmallArray(selectedList, T) {
   1671 							castS(t, T);
   1672 							ssize_t index = findG(bookmarks, ssGet(t));
   1673 							if (index == -1) {
   1674 								// no duplicated bookmarks
   1675 								pushG(bookmarks, ssGet(t));
   1676 							}
   1677 							else {
   1678 								// remove bookmark
   1679 								delG(bookmarks, index, index+1);
   1680 							}
   1681 							finishG(t);
   1682 						}
   1683 						emptyG(selectedList);
   1684 						emptyG(selectedListAttributes);
   1685 					}
   1686 					break;
   1687 				}
   1688 				case KEY_F(12): {
   1689 					// show bookmarks
   1690 					if (state == SHOWING_SEARCH) {
   1691 						// leave search results
   1692 						state = previousState;
   1693 					}
   1694 					if ((state != SHOWING_BOOKMARKS) and (not eqG(getG(bookmarks, rtChar, 0), ""))) {
   1695 						state = SHOWING_BOOKMARKS;
   1696 					}
   1697 					else if (state == SHOWING_BOOKMARKS) {
   1698 						state = SHOWING_DATABASE;
   1699 					}
   1700 					// scroll WVIEW window to top
   1701 					windows[WVIEW].indexFirstLine = 0;
   1702 					eraseSubwindows();
   1703 					fillCols();
   1704 					break;
   1705 				}
   1706 				case 'g':
   1707 					// set selected item active
   1708 					if (currentGroup[0]) {
   1709 						// change status for all selected tasks
   1710 						if (not lenG(selectedList)) {
   1711 							set_status(selectedGroup, TASK_STATUS_ACTIVE);
   1712 						}
   1713 						else {
   1714 							// selected items in selectedList
   1715 							forEachSmallArray(selectedList, T) {
   1716 								castS(t, T);
   1717 								set_status(ssGet(t), TASK_STATUS_ACTIVE);
   1718 								finishG(t);
   1719 							}
   1720 							emptyG(selectedList);
   1721 							emptyG(selectedListAttributes);
   1722 						}
   1723 
   1724 						// scroll WVIEW window to top
   1725 						windows[WVIEW].indexFirstLine = 0;
   1726 						eraseSubwindows();
   1727 						fillCols();
   1728 					}
   1729 					break;
   1730 				case 'h':
   1731 					// set selected item active
   1732 					if (currentGroup[0]) {
   1733 						// change status for all selected tasks
   1734 						if (not lenG(selectedList)) {
   1735 							set_status(selectedGroup, TASK_STATUS_DONE);
   1736 						}
   1737 						else {
   1738 							// selected items in selectedList
   1739 							forEachSmallArray(selectedList, T) {
   1740 								castS(t, T);
   1741 								set_status(ssGet(t), TASK_STATUS_DONE);
   1742 								finishG(t);
   1743 							}
   1744 							emptyG(selectedList);
   1745 							emptyG(selectedListAttributes);
   1746 						}
   1747 
   1748 						// scroll WVIEW window to top
   1749 						windows[WVIEW].indexFirstLine = 0;
   1750 						eraseSubwindows();
   1751 						fillCols();
   1752 					}
   1753 					break;
   1754 				case 'j':
   1755 					// set selected item active
   1756 					if (currentGroup[0]) {
   1757 						// change status for all selected tasks
   1758 						if (not lenG(selectedList)) {
   1759 							set_status(selectedGroup, TASK_STATUS_ONGOING);
   1760 						}
   1761 						else {
   1762 							// selected items in selectedList
   1763 							forEachSmallArray(selectedList, T) {
   1764 								castS(t, T);
   1765 								set_status(ssGet(t), TASK_STATUS_ONGOING);
   1766 								finishG(t);
   1767 							}
   1768 							emptyG(selectedList);
   1769 							emptyG(selectedListAttributes);
   1770 						}
   1771 
   1772 						// scroll WVIEW window to top
   1773 						windows[WVIEW].indexFirstLine = 0;
   1774 						eraseSubwindows();
   1775 						fillCols();
   1776 					}
   1777 					break;
   1778 				case 'k':
   1779 					// set selected item active
   1780 					if (currentGroup[0]) {
   1781 						// change status for all selected tasks
   1782 						if (not lenG(selectedList)) {
   1783 							set_status(selectedGroup, TASK_STATUS_PENDING);
   1784 						}
   1785 						else {
   1786 							// selected items in selectedList
   1787 							forEachSmallArray(selectedList, T) {
   1788 								castS(t, T);
   1789 								set_status(ssGet(t), TASK_STATUS_PENDING);
   1790 								finishG(t);
   1791 							}
   1792 							emptyG(selectedList);
   1793 							emptyG(selectedListAttributes);
   1794 						}
   1795 
   1796 						// scroll WVIEW window to top
   1797 						windows[WVIEW].indexFirstLine = 0;
   1798 						eraseSubwindows();
   1799 						fillCols();
   1800 					}
   1801 					break;
   1802 				case 'l':
   1803 					// set selected item active
   1804 					if (currentGroup[0]) {
   1805 						// change status for all selected tasks
   1806 						if (not lenG(selectedList)) {
   1807 							set_status(selectedGroup, TASK_STATUS_INACTIVE);
   1808 						}
   1809 						else {
   1810 							// selected items in selectedList
   1811 							forEachSmallArray(selectedList, T) {
   1812 								castS(t, T);
   1813 								set_status(ssGet(t), TASK_STATUS_INACTIVE);
   1814 								finishG(t);
   1815 							}
   1816 							emptyG(selectedList);
   1817 							emptyG(selectedListAttributes);
   1818 						}
   1819 
   1820 						// scroll WVIEW window to top
   1821 						windows[WVIEW].indexFirstLine = 0;
   1822 						eraseSubwindows();
   1823 						fillCols();
   1824 					}
   1825 					break;
   1826 				case 'o':
   1827 					// set selected item active
   1828 					if (currentGroup[0]) {
   1829 						// change status for all selected tasks
   1830 						if (not lenG(selectedList)) {
   1831 							set_status(selectedGroup, TASK_STATUS_VOID);
   1832 						}
   1833 						else {
   1834 							// selected items in selectedList
   1835 							forEachSmallArray(selectedList, T) {
   1836 								castS(t, T);
   1837 								set_status(ssGet(t), TASK_STATUS_VOID);
   1838 								finishG(t);
   1839 							}
   1840 							emptyG(selectedList);
   1841 							emptyG(selectedListAttributes);
   1842 						}
   1843 
   1844 						// scroll WVIEW window to top
   1845 						windows[WVIEW].indexFirstLine = 0;
   1846 						eraseSubwindows();
   1847 						fillCols();
   1848 					}
   1849 					break;
   1850 				case 'z':
   1851 					// select all in list
   1852 					if (currentGroup[0]) {
   1853 						// select only in group, not database list
   1854 						if (lenG(selectedList)) {
   1855 							// deselect all items
   1856 							emptyG(selectedList);
   1857 							emptyG(selectedListAttributes);
   1858 						}
   1859 						else {
   1860 							// nothing is selected, select all
   1861 							// except group title
   1862 							enumerateSmallArray(lsGroup, D, i) {
   1863 								cast(smallDictt*, d, D);
   1864 								// in groups don't select group title
   1865 								if ((i not_eq 0) or eqG(currentGroup, "root") or (state != SHOWING_DATABASE)) {
   1866 										char *s = getG(d, rtChar, "tid");
   1867 										if (not isBlankG(s)) {
   1868 											pushG(selectedList, s);
   1869 											createAllocateSmallArray(a);
   1870 											pushG(a, currentGroup);
   1871 											pushG(a, (int32_t)state);
   1872 											pushNFreeG(selectedListAttributes, a);
   1873 										}
   1874 								}
   1875 								finishG(d);
   1876 							}
   1877 						}
   1878 						showCols();
   1879 					}
   1880 					break;
   1881 				case 'x':
   1882 					// cut selected items
   1883 					if (currentGroup[0]) {
   1884 						// select only in group, not database list
   1885 						if (not lenG(selectedList)) {
   1886 							pushG(selectedList, selectedGroup);
   1887 							createAllocateSmallArray(a);
   1888 							pushG(a, currentGroup);
   1889 							pushG(a, (int32_t)state);
   1890 							pushNFreeG(selectedListAttributes, a);
   1891 						}
   1892 						paperfunction = CUT;
   1893 
   1894 						move(row-1,0);
   1895 						color_set(cursorColor, NULL);
   1896 						clrtoeol();
   1897 						addnstr("Cut selected items, move to destination and paste (v)", col-1);
   1898 						chgat(-1, 0, cursorColor, NULL);
   1899 					}
   1900 					break;
   1901 				case 'c':
   1902 					// copy selected items
   1903 					if (currentGroup[0]) {
   1904 						// select only in group, not database list
   1905 						if (not lenG(selectedList)) {
   1906 							pushG(selectedList, selectedGroup);
   1907 							createAllocateSmallArray(a);
   1908 							pushG(a, currentGroup);
   1909 							pushG(a, (int32_t)state);
   1910 							pushNFreeG(selectedListAttributes, a);
   1911 						}
   1912 						paperfunction = COPIED;
   1913 
   1914 						move(row-1,0);
   1915 						color_set(cursorColor, NULL);
   1916 						clrtoeol();
   1917 						addnstr("Copied selected items, move to destination and paste (v) or link (b)", col-1);
   1918 						chgat(-1, 0, cursorColor, NULL);
   1919 					}
   1920 					break;
   1921 				case 'v':
   1922 					if ((currentGroup[0]) and (state == SHOWING_DATABASE) and (paperfunction != NO_PAPER)) {
   1923 						pasteClipboard("paste");
   1924 						// scroll WVIEW window to top
   1925 						windows[WVIEW].indexFirstLine = 0;
   1926 						eraseSubwindows();
   1927 						fillCols();
   1928 					}
   1929 					break;
   1930 				case 'b':
   1931 					// link selected tasks
   1932 					if ((currentGroup[0]) and (state == SHOWING_DATABASE) and (paperfunction != NO_PAPER)) {
   1933 						pasteClipboard("link");
   1934 						// scroll WVIEW window to top
   1935 						windows[WVIEW].indexFirstLine = 0;
   1936 						eraseSubwindows();
   1937 						fillCols();
   1938 					}
   1939 					break;
   1940 				/* case KEY_MOUSE: */
   1941 				/* 	mvaddstr(row-4, 0, "mouse event"); */
   1942 				/* 	if(getmouse(&event) == OK) { */
   1943 				/* 		mvprintw(row-3,0,"x %d y %d z %d bstate 0x%x", event.x, event.y, event.z, event.bstate); */
   1944 				/* 	} */
   1945 				/* 	break; */
   1946 			}
   1947 		}
   1948 		refreshAll();
   1949 	}
   1950 exit:
   1951 	finalizeScreen(mainwin);
   1952 
   1953 	//test();
   1954 
   1955 	// save bookmarks in ini file
   1956 	smallDictt *data      = getG(ini, rtSmallDictt, "data");
   1957 	smallDictt *locations = getG(ini, rtSmallDictt, "locations");
   1958 	smallDictt *filters   = getG(ini, rtSmallDictt, "filters");
   1959 	smallDictt *desktop = getG(ini, rtSmallDictt, "desktop");
   1960 
   1961 	setNFreeG(data, "location", data_location);
   1962 
   1963 	setNFreeG(locations, "add_top_or_bottom", add_top_or_bottom);
   1964 
   1965 	enumerateS(TASK_STATUS_TRIM, name, i) {
   1966 		i32 flt = getG(status_filters, rtI32, i);
   1967 		setG(filters, name, STATUS_FILTER_STATES[flt]);
   1968 	}
   1969 	if (desktop) {
   1970 		smallStringt *b     = joinG(bookmarks, "|");
   1971 		setNFreeG(desktop, "bookmarks", b);
   1972 	}
   1973 	else {
   1974 		// no desktop section in ini file, empty bookmarks
   1975 		if (lenG(bookmarks)) {
   1976 			// create a desktop section
   1977 			desktop = allocG(rtSmallDictt);
   1978 			smallStringt *b     = joinG(bookmarks, "|");
   1979 			setNFreeG(desktop, "bookmarks", b);
   1980 			setG(ini, "desktop", desktop);
   1981 		}
   1982 	}
   1983 
   1984 	char *p = expandHomeG("~/.easydoneit.ini");
   1985 	saveIni(ini, p);
   1986 	free(p);
   1987 	//logVarG(ini);
   1988 
   1989 	saveEdiState();
   1990 
   1991 	terminateG(ini);
   1992 
   1993 	finalizeLibsheepy();
   1994 
   1995 	XSUCCESS
   1996 }