#!/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'