rexx address book

This commit is contained in:
Greg Gauthier 2025-05-04 19:07:21 +01:00
commit 1571221293
4 changed files with 380 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
docs/
*.iml

14
README.md Normal file
View File

@ -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. *
**************************************************************************/
```

363
addbook.rex Executable file
View File

@ -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'

BIN
db/contacts.sqlite Normal file

Binary file not shown.