From 6dbd58faf4a4a9e974d346a249f1cbec8c309a12 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Mon, 5 May 2025 14:52:40 +0100 Subject: [PATCH] correct tests, the way god intended --- addbook.rex | 317 ++----------------------------- app/appdb.cls | 411 ++++++++++++++++++++++++++++++++++++++++ app/appui.cls | 220 +++++++++++++++++++++ app/utils.rex | 11 ++ db/test_contacts.sqlite | Bin 0 -> 7168 bytes tests/test_appdb.rexx | 326 +++++++++++++++++++++++++++++++ 6 files changed, 981 insertions(+), 304 deletions(-) create mode 100644 app/appdb.cls create mode 100644 app/appui.cls create mode 100644 app/utils.rex create mode 100644 db/test_contacts.sqlite create mode 100755 tests/test_appdb.rexx diff --git a/addbook.rex b/addbook.rex index 8f48320..5f746bf 100755 --- a/addbook.rex +++ b/addbook.rex @@ -18,7 +18,8 @@ app~run() Do forever if .environment['STOPNOW'] = 1 then do app~cleanup() /* Clean up before exiting */ - address system 'clear' + call SysCls + Say .environment["REXX_PATH"] Say "Exiting Address Book." exit 0 end @@ -27,18 +28,6 @@ end Exit -::routine SysWait - use arg seconds - address system 'sleep' seconds - return - - -::ROUTINE setEnv - .local~home = SysGetpwnam("gmgauthier", "d") - .local~projectRoot = .home||"/Projects/rexx-address-book" - .local~dbPath = .projectRoot||"/db/contacts.sqlite" - - ::CLASS AddressBookApp PUBLIC ::METHOD Init @@ -64,300 +53,20 @@ Exit db~closeDb() return +::ROUTINE setEnv + .environment~home = SysGetpwnam("gmgauthier", "d") + .environment~projectRoot = .home||"/Projects/rexx-address-book" + .environment~pkgPath = .projectRoot||"/app" + .environment~dbPath = .projectRoot||"/db" + .environment["REXX_PATH"] = .projectRoot||";"||.pkgPath||";"||.dbPath||";" -::CLASS AddressBookDB PUBLIC - - ::method init - expose db - - /* Create database directory if it doesn't exist */ - if SysFileExists(.dbPath) == .false then DO - SAY "Initializing new address book" - db = .ooSQLiteConnection~new(.dbPath) - self~createTables - END - Else Do - db = .ooSQLiteConnection~new(.dbPath,.ooSQLite~OPEN_READWRITE) - End - return - - ::METHOD getFileName - expose db - return db~fileName() - - ::METHOD closeDb - expose db - db~Close() - RETURN - - ::method createTables - expose db - - /* Contacts table */ - db~exec("CREATE TABLE IF NOT EXISTS contacts ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "first_name TEXT,"||, - "last_name TEXT,"||, - "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"||, - "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"||, - ")") - - /* Phone numbers table */ - db~exec("CREATE TABLE IF NOT EXISTS phone_numbers ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "contact_id INTEGER,"||, - "type TEXT,"||, -- home, work, mobile, etc. - "number TEXT,"||, - "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, - ")") - - /* Email addresses table */ - db~exec("CREATE TABLE IF NOT EXISTS email_addresses ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "contact_id INTEGER,"||, - "type TEXT,"||, -- home, work, etc. - "email TEXT,"||, - "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, - ")") - - /* Physical addresses table */ - db~exec("CREATE TABLE IF NOT EXISTS addresses ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "contact_id INTEGER,"||, - "type TEXT,"||, -- home, work, etc. - "street TEXT,"||, - "city TEXT,"||, - "state TEXT,"||, - "postal_code TEXT,"||, - "country TEXT,"||, - "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, - ")") - return - -/* UI handling class */ -::class AddressBookUI public - - ::method init - expose win mainMenu - return - - ::method initialize - expose win mainMenu - - win = self~DrawMainPanel(0, .true) /* default colour, draw border box */ - self~setupMainMenu(win) - return - - ::METHOD DrawMainPanel - use arg clrset, with_box - win = .window~new() /* The first invocation must be with no arguments */ - win~curs_set(0) /* Hide the cursor by default */ - win~keypad(1) /* Enable function keys and arrow keys */ - win~raw - win~noecho - win~clear - win~cbreak - - self~SetPanelColor(win, clrset) - - if with_box = .true then DO - win~box(0, 0) /* Draw box with default ACS characters */ - win~refresh - END - - return win - - ::METHOD DrawSubPanel - use arg height, width, starty, startx, clrset, title, with_box - new_win = .window~new(height, width, starty, startx) - new_win~curs_set(0) - - /* Set the panel color */ - self~SetPanelColor(new_win, clrset) - - /* Display title */ - new_win~attron(new_win~A_BOLD) - new_win~mvaddstr(starty-(starty-2), (width - title~length) % 2, title) - new_win~attroff(new_win~A_BOLD) - new_win~refresh - if with_box = .true then DO - new_win~box(0,0) - new_win~refresh - END - return new_win - - ::METHOD SetPanelColor - use arg panel, clrset - - panel~start_color() - SELECT - /* white text on blue background */ - when clrset = 1 then panel~init_pair(2, panel~COLOR_WHITE, panel~COLOR_BLUE) - /* white text on grey background */ - when clrset = 2 then panel~init_pair(3, panel~COLOR_WHITE, panel~COLOR_GREEN) - /* yellow text on red background */ - when clrset = 3 then panel~init_pair(4, panel~COLOR_YELLOW, panel~COLOR_RED) - /* black text on white background */ - when clrset = 4 then panel~init_pair(5, panel~COLOR_BLACK, panel~COLOR_WHITE) - /* black text on green background */ - when clrset = 5 then panel~init_pair(6, panel~COLOR_BLACK, panel~COLOR_GREEN) - /* black text on cyan background */ - when clrset = 6 then panel~init_pair(7, panel~COLOR_BLACK, panel~COLOR_CYAN) - /* Default to white text on black background */ - OTHERWISE DO - clrset=0 - panel~init_pair(1, panel~COLOR_WHITE, panel~COLOR_BLACK) - END - END - panel~bkgd(panel~color_pair(clrset+1)) - panel~refresh - - ::method setupMainMenu - expose mainMenu win menuwin menuItems menu_keys - use arg win - - max_y = win~lines - max_x = win~cols - - menu_height = 21 - menu_width = 40 - start_y = (max_y - menu_height) % 2 - start_x = (max_x - menu_width) % 2 - clrset=1 - title = "Rexx Address Book" - with_box = .true - - menuwin = self~DrawSubPanel(menu_height, menu_width, start_y, start_x, clrset, title, with_box) - menuItems = .array~of("[A]dd Contact", "[R]emove Contact", "[E]dit Contact", "[S]earch", "[L]ist All", "E[X]it") - menu_keys = .array~of("a", "r", "e", "s", "l", "x") - - /* Display menu items */ - selected = 1 - self~DrawMenu(menuwin, menuItems, selected, win) - /* menuwin~mvaddstr(menu_height - 2, 3, "Type 'X' to exit") */ - menuwin~refresh - RETURN - - ::method DrawMenu - expose win menuItems selected mainwin - use arg win, items, selected, mainwin - - do i = 1 to items~items - if i = selected then do - win~attron(mainwin~COLOR_PAIR(2)) - win~attron(mainwin~A_BOLD) - win~mvaddstr(i+3, 2, items[i]) - win~attroff(mainwin~COLOR_PAIR(2)) - win~attroff(mainwin~A_BOLD) - end - else do - win~mvaddstr(i+3, 2, items[i]) - end - end - win~refresh - return - - /** MAIN LOOP **/ - ::method mainLoop - expose win mainMenu menuwin - - running = .true - - do while running - menuwin~refresh - key = win~getch - - menu_keys = .array~of("a", "r", "e", "s", "l", "x") - - select - when key = win~KEY_UP then do - if selected > 1 then selected = selected - 1 - call DrawMenu menuwin, menuItems, selected, win - end - when key = win~KEY_DOWN then do - if selected < menuItems~items then selected = selected + 1 - call DrawMenu menuwin, menuItems, selected, win - end - when key = D2C(10) | key = D2C(13) then do /* Enter key - numeric codes only */ - menuwin~refresh - call ProcessSelection menu_keys[selected], home - return - end - when key = D2C(88) | key = D2C(120) | key = "x" | key = C2D("x") then do - menuwin~endwin - .environment['STOPNOW'] = 1 - RETURN - end - otherwise do - if datatype(key) = 'CHAR' then do - key = lower(key) - pos = self~findInArray(menu_keys, key) - if pos > 0 then do - menuwin~mvaddstr(19 - 1, 18, "Letter selection ["||key||"]") - menuwin~refresh - call SysWait 0.5 - self~ProcessSelection(menuwin, key) - return - end - end - end - end - end /* do while running */ - return - - /* Process selection */ - ::METHOD ProcessSelection - use arg menuwin, key_char - select - when key_char = 'a' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the ADD panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 'r' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the REMOVE panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 'e' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the EDIT panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 's' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the SEARCH panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 'l' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the LIST panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - otherwise nop - end - return - - ::METHOD findInArray - use arg array, item - do i = 1 to array~items - if array[i] = item then return i - end - return 0 /* Not found */ - - ::method cleanup - expose win - /* Clean up ncurses */ - win~endwin - return - -/* Handle program termination */ -::ROUTINE ProgramHalt - signal off halt - Say "PROGRAM TERMINATED AT THE KEYBOARD" - exit 0 /** External Libraries **/ +::requires 'app/appdb.cls' +::requires 'app/appui.cls' +::requires 'app/utils.rex' + ::requires 'ooSQLite.cls' ::requires "rxunixsys" LIBRARY ::requires 'ncurses.cls' + diff --git a/app/appdb.cls b/app/appdb.cls new file mode 100644 index 0000000..398cb66 --- /dev/null +++ b/app/appdb.cls @@ -0,0 +1,411 @@ +::requires 'ooSQLite.cls' +::requires "rxunixsys" LIBRARY +::requires 'ncurses.cls' +::requires 'app/utils.rex' + +::CLASS AddressBookDB PUBLIC + + ::method init + expose db + + /* Create database directory if it doesn't exist */ + if SysFileExists(.dbPath) == .false then DO + SAY "Initializing new address book" + db = .ooSQLiteConnection~new(.dbPath) + self~createTables + END + Else Do + Say "Initializing existing address book" + db = .ooSQLiteConnection~new(.dbPath,.ooSQLite~OPEN_READWRITE) + End + return + + ::METHOD getFileName + expose db + return db~fileName() + + ::METHOD closeDb + expose db + db~Close() + RETURN + + ::METHOD createTables + expose db + + /* Contacts table */ + db~exec("CREATE TABLE IF NOT EXISTS contacts ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "first_name TEXT,"||, + "last_name TEXT,"||, + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"||, + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"||, + ")") + + /* Phone numbers table */ + db~exec("CREATE TABLE IF NOT EXISTS phone_numbers ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "contact_id INTEGER,"||, + "type TEXT,"||, -- home, work, mobile, etc. + "number TEXT,"||, + "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, + ")") + + /* Email addresses table */ + db~exec("CREATE TABLE IF NOT EXISTS email_addresses ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "contact_id INTEGER,"||, + "type TEXT,"||, -- home, work, etc. + "email TEXT,"||, + "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, + ")") + + /* Physical addresses table */ + db~exec("CREATE TABLE IF NOT EXISTS addresses ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "contact_id INTEGER,"||, + "type TEXT,"||, -- home, work, etc. + "street TEXT,"||, + "city TEXT,"||, + "state TEXT,"||, + "postal_code TEXT,"||, + "country TEXT,"||, + "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, + ")") + return + + /* Contact CRUD Operations */ + ::METHOD addContact + expose db + use arg contactDict /* Use a Rexx 'directory' to pass multiple values around */ + sql = "INSERT INTO contacts (first_name, last_name) VALUES ('"contactDict~firstName"', '"contactDict~lastName"')" + rc = db~exec(sql) + if rc \= 0 then do + say "Error adding contact:" db~errMsg() + return -1 + end + contactId = db~lastInsertRowId() /* the row id of 'contacts' table is the master id */ + self~addPhoneNumber(contactId, contactDict["PHONE_TYPE"], contactDict["PHONE_NUMBER"]) + self~addEmailAddress(contactId, contactDict["EMAIL_TYPE"], contactDict["EMAIL_ADDRESS"]) + self~addRealAddress(contactId, contactDict~addressType, contactDict~street, contactDict~city, contactDict~state, + contactDict~postalCode, contactDict~country) + return contactId + + ::METHOD getContact + expose db + use arg contactId + returnedContent = .Directory~new() + sql1 = "SELECT * FROM contacts WHERE id = "contactId + contacts = db~exec(sql1,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES) + /* The result will be rendered as an array of Rexx 'directories' + * which are basically analogous to python dictionaries. */ + + if contacts = .nil then DO + Say 'NO CONTACTS FOUND' + return + END + if contacts~size < 1 then do + Say 'NO CONTACT FOUND' + return + END + return contacts[1] /* Rexx is 1-indexed */ +/*** + returnedContent~firstName = contact["FIRST_NAME"] + returnedContent~lastName = contact["LAST_NAME"] + sql2 = "SELECT * FROM email_addresses WHERE contact_id = "contactId + emails = db~exec(sql2,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES) + email_array = .Array~new() + do row over emails + email_array.append(row["EMAIL"]) + END + returnedContent~emailAddresses = email_array + + sql3 = "SELECT * FROM phone_numbers WHERE contact_id = "contactId + phones = db~exec(sql3,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES) + phone_dir = .Directory~new() + phone_array = .Array~new() + do phone over phones + phone_dir~put(phone["NUMBER"], phone["TYPE"]) + phone_array.append(phone_dir) + END + returnedContent~phoneNumbers = phone_array + + sql4 = "SELECT * FROM addresses WHERE contact_id = "contactId + addresses = db~exec(sql3,.true,.ooSQLite~~OO_ARRAY_OF_DIRECTORIES) + address_dir = .Dictionary~new() + address_array = .Array~new() + do address over addresses + address_dir~type = address["TYPE"] + address_dir~street = address["STREET"] + address_dir~city = address["CITY"] + address_dir~state = address["STATE"] + address_dir~postal_code = address["POSTAL_CODE"] + address_dir~country = address["COUNTRY"] + address_array.append(address_dir) + END + returnedContent~addresses = address_array + return returnedContent +***/ + + ::METHOD getAllContacts + expose db + sql = "SELECT * FROM contacts ORDER BY last_name, first_name" + contacts = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + return contacts + + ::METHOD searchContacts + expose db + use arg searchTerm + + contactsList = .Array~new() + + sql = "SELECT id FROM contacts WHERE first_name LIKE '%"searchTerm"%' OR last_name LIKE '%"searchTerm"%' ORDER BY last_name, first_name" + contactIds = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + + do contactDir over contactIds + contactId = contactDir["ID"] + contact = self~getContact(contactId) + if contact \= .nil then do + contactsList~append(contact) + end + end + return contactsList + + ::METHOD updateContact + expose db + use arg contactId, firstName, lastName + sql = "UPDATE contacts SET first_name = '"firstName"', last_name = '"lastName"' WHERE id = "contactId + rc = db~exec(sql) + if rc \= 0 then do + say "Error updating contact:" db~errMsg() + return -1 + end + return rc +/** + self~removeContactPhones(contactId) + self~removeContactEmails(contactId) + self~removeContactAddresses(contactId) + + /* Then add new entries */ + self~addPhoneNumber(contactId, contactDict["PHONE_TYPE"], contactDict["PHONE_NUMBER"]) + self~addEmailAddress(contactId, contactDict["EMAIL_TYPE"], contactDict["EMAIL_ADDRESS"]) + self~addAddress(contactId, contactDict~addressType, contactDict~street, contactDict~city, contactDict~state, + contactDict~postalCode, contactDict~country) + return 0 +**/ + + ::METHOD deleteContact + expose db + use arg contactId + + /* Delete all related records first (assuming foreign key constraints) */ + self~removeContactPhones(contactId) + self~removeContactEmails(contactId) + self~removeContactAddresses(contactId) + + /* Delete the contact record */ + sql = "DELETE FROM contacts WHERE id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error deleting contact:" db~errMsg() + return -1 + end + return 0 + + ::METHOD getPhoneNumbers + expose db + use arg contactId + + phones = .Array~new + sql = "SELECT id, type, number FROM phone_numbers WHERE contact_id = "contactId + phones = db~exec(sql,.true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + + return phones + + ::METHOD addPhoneNumber + expose db + use arg contactId, phoneType, phoneNumber + + if phoneType = .nil | phoneNumber = .nil then return 0 + + sql = "INSERT INTO phone_numbers (contact_id, type, number) VALUES ("contactId", '"phoneType"', '"phoneNumber"')" + rc = db~exec(sql) + if rc \= 0 then DO + Say "Unable to insert phone number for contact id "contactId + return rc + END + phone_id = db~lastInsertRowId() + return phone_id + + ::METHOD updatePhoneNumber + expose db + use arg phoneId, phoneType, phoneNumber + + sql = "UPDATE phone_numbers SET type = '"phoneType"', number = '"phoneNumber"' WHERE id = "phoneId + rc = db~exec(sql) + if rc \= 0 then DO + Say "Unable to update phone number or type for phone id "contactId + return rc + END + + return rc + + ::METHOD deletePhoneNumber + expose db + use arg phoneId + + stmt = db~prepare("DELETE FROM phone_numbers WHERE id = ?") + stmt~bind(1, phoneId) + stmt~step + + return db~changes() > 0 + + + + /* Email address operations */ + + ::METHOD addEmailAddress + expose db + use arg contactId, emailType, emailAddress + + if emailType = .nil | emailAddress = .nil then return 0 + + sql = "INSERT INTO email_addresses (contact_id, type, email) VALUES ("contactId", '"emailType"', '"emailAddress"')" + rc = db~exec(sql) + if rc \= 0 then do + say "Error adding email address:" db~errMsg() + return -1 + end + + email_id = db~lastInsertRowId() + + return email_id + + ::METHOD getEmailAddresses + expose db + use arg contactId + + sql = "SELECT id, type, email FROM email_addresses WHERE contact_id = "contactId + emails = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + return emails + + ::METHOD updateEmailAddress + expose db + use arg emailId, emailType, emailAddress + + sql = "UPDATE email_addresses SET type = '"emailType"', email = '"emailaddress"' WHERE id = "emailId + rc = db~exec(sql) + if rc \= 0 then do + say "Error adding email address:" db~errMsg() + return -1 + end + return rc + + + ::METHOD deleteEmailAddress + expose db + use arg emailId + + stmt = db~prepare("DELETE FROM email_addresses WHERE id = ?") + stmt~bind(1, emailId) + stmt~step + + return db~changes() > 0 + + /* Physical address operations */ + + ::METHOD addRealAddress + expose db + use arg contactId, addressType, street, city, state, postalCode, country + + if addressType = .nil | street = .nil then return 0 + + sql = "INSERT INTO addresses (contact_id, type, street, city, state, postal_code, country)", + "VALUES ("contactId", '"addressType"', '"street"', '"city"', '"state"', '"postalCode"', '"country"')" + rc = db~exec(sql) + + if rc \= 0 then do + say "Error adding address:" db~errMsg() + return -1 + end + addr_id = db~lastInsertRowId() + return addr_id + + ::METHOD getRealAddresses + expose db + use arg contactId + + sql = "SELECT id, type, street, city, state, postal_code, country" || , + " FROM addresses WHERE contact_id = " contactId + addresses = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + return addresses + + ::METHOD updateRealAddress + expose db + use arg addressId, addressType, street, city, state, postalCode, country + + sql = "UPDATE addresses SET type = '"addressType"', street = '"street"', city = '"city"'," || , + " state = '"state"', postal_code = '"postalCode"', country = '"country"' WHERE id = "addressId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error updating address:" db~errMsg() + return -1 + end + + return rc + + ::METHOD deleteAddress + expose db + use arg addressId + + sql="DELETE FROM addresses WHERE id = "addressId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing address:" db~errMsg() + return -1 + end + + return 0 + + ::METHOD removeContactPhones + expose db + use arg contactId + + sql = "DELETE FROM phone_numbers WHERE contact_id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing phone numbers:" db~errMsg() + return -1 + end + + return 0 + + ::METHOD removeContactEmails + expose db + use arg contactId + + sql = "DELETE FROM email_addresses WHERE contact_id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing email addresses:" db~errMsg() + return -1 + end + return 0 + + ::METHOD removeContactAddresses + expose db + use arg contactId + + sql = "DELETE FROM addresses WHERE contact_id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing addresses:" db~errMsg() + return -1 + end + return 0 + diff --git a/app/appui.cls b/app/appui.cls new file mode 100644 index 0000000..734ffbc --- /dev/null +++ b/app/appui.cls @@ -0,0 +1,220 @@ +::requires 'ooSQLite.cls' +::requires "rxunixsys" LIBRARY +::requires 'ncurses.cls' +::requires 'app/utils.rex' + +::class AddressBookUI public + + ::method init + expose win mainMenu + return + + ::method initialize + expose win mainMenu + + win = self~DrawMainPanel(0, .true) /* default colour, draw border box */ + self~setupMainMenu(win) + return + + ::METHOD DrawMainPanel + use arg clrset, with_box + win = .window~new() /* The first invocation must be with no arguments */ + win~curs_set(0) /* Hide the cursor by default */ + win~keypad(1) /* Enable function keys and arrow keys */ + win~raw + win~noecho + win~clear + win~cbreak + + self~SetPanelColor(win, clrset) + + if with_box = .true then DO + win~box(0, 0) /* Draw box with default ACS characters */ + win~refresh + END + + return win + + ::METHOD DrawSubPanel + use arg height, width, starty, startx, clrset, title, with_box + new_win = .window~new(height, width, starty, startx) + new_win~curs_set(0) + + /* Set the panel color */ + self~SetPanelColor(new_win, clrset) + + /* Display title */ + new_win~attron(new_win~A_BOLD) + new_win~mvaddstr(starty-(starty-2), (width - title~length) % 2, title) + new_win~attroff(new_win~A_BOLD) + new_win~refresh + if with_box = .true then DO + new_win~box(0,0) + new_win~refresh + END + return new_win + + ::METHOD SetPanelColor + use arg panel, clrset + + panel~start_color() + SELECT + /* white text on blue background */ + when clrset = 1 then panel~init_pair(2, panel~COLOR_WHITE, panel~COLOR_BLUE) + /* white text on grey background */ + when clrset = 2 then panel~init_pair(3, panel~COLOR_WHITE, panel~COLOR_GREEN) + /* yellow text on red background */ + when clrset = 3 then panel~init_pair(4, panel~COLOR_YELLOW, panel~COLOR_RED) + /* black text on white background */ + when clrset = 4 then panel~init_pair(5, panel~COLOR_BLACK, panel~COLOR_WHITE) + /* black text on green background */ + when clrset = 5 then panel~init_pair(6, panel~COLOR_BLACK, panel~COLOR_GREEN) + /* black text on cyan background */ + when clrset = 6 then panel~init_pair(7, panel~COLOR_BLACK, panel~COLOR_CYAN) + /* Default to white text on black background */ + OTHERWISE DO + clrset=0 + panel~init_pair(1, panel~COLOR_WHITE, panel~COLOR_BLACK) + END + END + panel~bkgd(panel~color_pair(clrset+1)) + panel~refresh + + ::method setupMainMenu + expose mainMenu win menuwin menuItems menu_keys + use arg win + + max_y = win~lines + max_x = win~cols + + menu_height = 21 + menu_width = 40 + start_y = (max_y - menu_height) % 2 + start_x = (max_x - menu_width) % 2 + clrset=1 + title = "Rexx Address Book" + with_box = .true + + menuwin = self~DrawSubPanel(menu_height, menu_width, start_y, start_x, clrset, title, with_box) + menuItems = .array~of("[A]dd Contact", "[R]emove Contact", "[E]dit Contact", "[S]earch", "[L]ist All", "E[X]it") + menu_keys = .array~of("a", "r", "e", "s", "l", "x") + + /* Display menu items */ + selected = 1 + self~DrawMenu(menuwin, menuItems, selected, win) + /* menuwin~mvaddstr(menu_height - 2, 3, "Type 'X' to exit") */ + menuwin~refresh + RETURN + + ::method DrawMenu + expose win menuItems selected mainwin + use arg win, items, selected, mainwin + + do i = 1 to items~items + if i = selected then do + win~attron(mainwin~COLOR_PAIR(2)) + win~attron(mainwin~A_BOLD) + win~mvaddstr(i+3, 2, items[i]) + win~attroff(mainwin~COLOR_PAIR(2)) + win~attroff(mainwin~A_BOLD) + end + else do + win~mvaddstr(i+3, 2, items[i]) + end + end + win~refresh + return + + /** MAIN LOOP **/ + ::method mainLoop + expose win mainMenu menuwin + + running = .true + + do while running + menuwin~refresh + key = win~getch + + menu_keys = .array~of("a", "r", "e", "s", "l", "x") + + select + when key = win~KEY_UP then do + if selected > 1 then selected = selected - 1 + call DrawMenu menuwin, menuItems, selected, win + end + when key = win~KEY_DOWN then do + if selected < menuItems~items then selected = selected + 1 + call DrawMenu menuwin, menuItems, selected, win + end + when key = D2C(10) | key = D2C(13) then do /* Enter key - numeric codes only */ + menuwin~refresh + call ProcessSelection menu_keys[selected], home + return + end + when key = D2C(88) | key = D2C(120) | key = "x" | key = C2D("x") then do + menuwin~endwin + .environment['STOPNOW'] = 1 + RETURN + end + otherwise do + if datatype(key) = 'CHAR' then do + key = lower(key) + pos = self~findInArray(menu_keys, key) + if pos > 0 then do + menuwin~mvaddstr(19 - 1, 18, "Letter selection ["||key||"]") + menuwin~refresh + call SysWait 0.5 + self~ProcessSelection(menuwin, key) + return + end + end + end + end + end /* do while running */ + return + + /* Process selection */ + ::METHOD ProcessSelection + use arg menuwin, key_char + select + when key_char = 'a' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the ADD panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 'r' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the REMOVE panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 'e' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the EDIT panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 's' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the SEARCH panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 'l' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the LIST panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + otherwise nop + end + return + + ::METHOD findInArray + use arg array, item + do i = 1 to array~items + if array[i] = item then return i + end + return 0 /* Not found */ + + ::method cleanup + expose win + /* Clean up ncurses */ + win~endwin + return diff --git a/app/utils.rex b/app/utils.rex new file mode 100644 index 0000000..3a86a17 --- /dev/null +++ b/app/utils.rex @@ -0,0 +1,11 @@ +::ROUTINE SysWait PUBLIC + use arg seconds + address system 'sleep' seconds + return + + +/* Handle program termination */ +::ROUTINE ProgramHalt + signal off halt + Say "PROGRAM TERMINATED AT THE KEYBOARD" + exit 0 diff --git a/db/test_contacts.sqlite b/db/test_contacts.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..b59c7b4b5ecfb381fdd564b8fce06dec923dc886 GIT binary patch literal 7168 zcmeHMO-~a+7@pbbM@S%n5Ylj{OvC~WEumkiTx2Z+R{LeUReRbj-GMG_cgyb9NIV$u zKlm^B2mS)R=*^oq6R+Mi&bHl})`oz=gf=^w?Y{GV%*^{t-*)Dm^3$whQM|7k4avd@ z-~kYVEsOyG)9mrH$0;60*pE}7LtOBl2GMUGudUFr^uu59HoSF##Z41=POBb&V09Iq zDVDUSQreIdRg`4epr%R9fu=WIBD_Gbz(33qJRrm?ii|UPf$WkJE|xMmzEs6eNEP!H zp^(Wl(;UeQk(#bqQq2|(?2r=4r%4%i z*(Mt~j0<_ZO|pbdFU^}z=X{OS{3hgY2; zug$F@=d)L=+IGr3R@paPH0g1RYBf60M17782v;D3TUmkEf#RcEz#8 zW^!Y5JvoGxKt3=xYunbD9UY=Egq7g_!H5Bsy7EX<%*un_1M~xZMIRX978d~*fssI9 z(t{QzdOOisgjV6ej+2|=&@$}Xu5ptP1);M=L3aFe=o3KS*@s(P1Y8708i4@k1rzh3 zoW7^1G?hw4@87!{-AE-9bC3z~If0D6&Kj(^L=H>4T}#t!9_u=-KfP z(PzM_TU-QO1V$Z!D4bxw5sQm6fm!>%KAGCU1?dR$FKM}HDB8ikLRC4FWjlg+tWxIf z`1gW4fYqO{z%3!}Zve(N`S~C6#N(MzAL~1i*Is{MVFA7+?bLqnJl!LFeaZbE`;yuH x?n!(2ZH{Rfl(M|OeX~