commit 157122129367eac0f74555c40ab07a1086607f4a Author: Greg Gauthier Date: Sun May 4 19:07:21 2025 +0100 rexx address book diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d014078 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +docs/ +*.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..dff8a0d --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Rexx Address Book + +```oorexx +/*************************************************************************** +* Rexx Address Book * +* * +* A simple application for storing, maintaining, and browsing contact * +* information. * +* * +* Backend: Object Rexx with unix system, and sqlite extensions. * +* Frontend: Object Rexx with ncurses extensions. * + **************************************************************************/ +``` + diff --git a/addbook.rex b/addbook.rex new file mode 100755 index 0000000..8f48320 --- /dev/null +++ b/addbook.rex @@ -0,0 +1,363 @@ +#!/usr/bin/env rexx +/*************************************************************************** + * Rexx Address Book * + * * + * A simple application for storing, maintaining, and browsing contact * + * information. * + * * + * Backend: Object Rexx with unix system, and sqlite extensions. * + * Frontend: Object Rexx with ncurses extensions. * + **************************************************************************/ +signal on HALT name ProgramHalt +.environment['STOPNOW'] = 0 +call setEnv + +app = .AddressBookApp~new() +app~run() + +Do forever + if .environment['STOPNOW'] = 1 then do + app~cleanup() /* Clean up before exiting */ + address system 'clear' + Say "Exiting Address Book." + exit 0 + end + app~reloop() +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 + expose ui db + db = .AddressBookDB~new() + ui = .AddressBookUI~new() + return + + ::method run + expose ui + ui~initialize + ui~mainLoop + ui~cleanup + return + + ::method reloop + expose ui + ui~mainLoop + RETURN + + ::method cleanup + expose db + db~closeDb() + return + + +::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 'ooSQLite.cls' +::requires "rxunixsys" LIBRARY +::requires 'ncurses.cls' diff --git a/db/contacts.sqlite b/db/contacts.sqlite new file mode 100644 index 0000000..5fad5a9 Binary files /dev/null and b/db/contacts.sqlite differ