colorful rat Ratfactor.com > Dave's Repos

addrbook

A CLI contact management program
git clone http://ratfactor.com/repos/addrbook/addrbook.git

addrbook/addrbook

Download raw file: addrbook

1 #!/usr/bin/env ruby 2 3 # addrbook - A contact management program. 4 # 5 # See README for the required directory structure at ADDRBOOK_PATH. 6 # I'm hard-coding my path as the fallback. 7 $mydir = ( ENV['ADDRBOOK_PATH'] || '/home/dave/doc/addrbook' ).chomp('/') 8 9 # Determine text editing program from environment, vim as fallback. 10 $editor = ENV['VISUAL'] || ENV['EDITOR'] || 'vim' 11 12 # Determine text viewing program from env or less as fallback 13 $viewer = ENV['PAGER'] || 'less' 14 15 # This adds the getch method to STDIN so I can do "Press any key..." 16 require 'io/console' 17 18 # Requires installing sqlite3 extension, e.g.: 'gem install sqlite3' 19 require 'sqlite3' 20 21 db_path = "#{$mydir}/addrbook.db" 22 $db = SQLite3::Database.new db_path 23 24 def die_with(msg) 25 puts msg 26 # Because this was intended to work from within aerc and the 'term' command 27 # doesn't have an option to keep the terminal open after the given command 28 # exits (which is normally what I want), keep open until user hits a key so 29 # they can read the error message. 30 puts "Press any key to continue..." 31 STDIN.getch 32 exit 1 33 end 34 35 def find_by_email(email) 36 rows = $db.execute('SELECT contact_id FROM email WHERE email = ?', [email]) 37 38 if rows.count < 1 39 return nil 40 end 41 42 # id field from first (only) row 43 return rows[0][0] 44 end 45 46 def find_or_die(email) 47 id = find_by_email(email) 48 if !id 49 die_with "Could not find a contact with email '#{email}'" 50 end 51 52 return id 53 end 54 55 def add_contact(email, name) 56 sql = "INSERT INTO contact (name) VALUES (?) returning contact_id" 57 contact_row = $db.execute(sql, [name]) 58 id = contact_row[0][0] 59 60 sql = "INSERT INTO email (contact_id, email) VALUES (?, ?)" 61 $db.execute(sql, [id, email]) 62 63 return id 64 end 65 66 def make_contact_fname(id) 67 return "#{$mydir}/txt/#{id}.txt" 68 end 69 70 def open_as_txt(id, open_with) 71 sql = "SELECT contact_id, name, added, notes, 72 (select string_agg(email, ' % ') from email where contact_id = c.contact_id), 73 (select string_agg(phone, ' % ') from phone where contact_id = c.contact_id), 74 (select string_agg(url, ' % ') from url where contact_id = c.contact_id) 75 FROM contact c 76 WHERE c.contact_id = ?" 77 row = $db.execute(sql, [id])[0] # first row 78 id = row[0] 79 name = row[1] 80 added = row[2] 81 notes = row[3] 82 emails = row[4] 83 phones = row[5] 84 urls = row[6] 85 86 # Split relation items or make blank placeholders for empty ones 87 if !emails then emails = [''] else emails = emails.split(' % ') end 88 if !phones then phones = [''] else phones = phones.split(' % ') end 89 if !urls then urls = [''] else urls = urls.split(' % ') end 90 91 contact_fname = make_contact_fname(id) 92 File.open(contact_fname, 'w') do |f| 93 f.puts "Contact: #{id}" 94 f.puts " Name: #{name}" 95 f.puts " Added: #{added}" 96 emails.each {|email| f.puts " Email: #{email}" } 97 phones.each {|phone| f.puts " Phone: #{phone}" } 98 urls.each {|url| f.puts " URL: #{url}" } 99 f.puts 100 f.puts notes 101 end 102 103 # Now open with the supplied application or command 104 system("#{open_with} #{contact_fname}") 105 end 106 107 # Read txt file and import changes into *existing* DB record. 108 def import_txt(id) 109 contact_fname = make_contact_fname(id) 110 extract_field = /\s*([^:]+):\s*(.*)/; 111 linenum = 0 112 in_notes = false 113 name = nil 114 added = nil 115 notes = nil 116 emails = [] 117 phones = [] 118 urls = [] 119 hint = "You can fix this by editing '#{contact_fname}' and then run 'addrbook reimport #{id}' to avoid losing your changes." 120 121 # Process txt lines 122 File.foreach(contact_fname) do |line| 123 line.chomp! 124 linenum += 1 125 126 # Gather notes lines together 127 if notes then 128 notes = "#{notes}#{line}\n" 129 next 130 end 131 # Blank line starts notes 132 if line == '' then notes = '' ; next end 133 134 x = extract_field.match(line) 135 if !x 136 die_with "Did not understand line #{linenum}: #{line}\n#{hint}" 137 end 138 field = x[1] 139 value = x[2] == '' ? nil : x[2] 140 141 if field == 'Contact' 142 # Sanity check 143 if Integer(value) != Integer(id) 144 die_with "Problem with line #{linenum}: Contact '#{value}' doesn't match expected '#{id}'.\n#{hint}" 145 end 146 elsif field == 'Name' 147 name = value 148 elsif field == 'Added' 149 added = value 150 elsif field == 'Email' 151 if value then emails.push value end 152 elsif field == 'Phone' 153 if value then phones.push value end 154 elsif field == 'URL' 155 if value then urls.push value end 156 else 157 die_with "Problem with line #{linenum}: Did not recognize field '#{field}'\n#{hint}" 158 end 159 end 160 161 # Update DB! 162 # Start a SQLite3 transaction so that re-creation of entries is atomic. 163 $db.transaction do |tdb| 164 tdb.execute('DELETE FROM email WHERE contact_id = ?', [id]) 165 tdb.execute('DELETE FROM phone WHERE contact_id = ?', [id]) 166 tdb.execute('DELETE FROM url WHERE contact_id = ?', [id]) 167 sql = "UPDATE contact SET 168 name = ?, 169 added = ?, 170 notes = ? 171 WHERE contact_id = ?" 172 tdb.execute(sql, [name, added, notes, id]) 173 emails.each do |email| 174 sql = "INSERT INTO email (contact_id, email) VALUES (?, ?)" 175 tdb.execute(sql, [id, email]) 176 end 177 phones.each do |phone| 178 sql = "INSERT INTO phone (contact_id, phone) VALUES (?, ?)" 179 tdb.execute(sql, [id, phone]) 180 end 181 urls.each do |url| 182 sql = "INSERT INTO url (contact_id, url) VALUES (?, ?)" 183 tdb.execute(sql, [id, url]) 184 end 185 end 186 end 187 188 if ARGV[0] == 'add' 189 # Both params are optional and could be nil: 190 email = ARGV[1] 191 name = ARGV[2] 192 193 if email 194 id = find_by_email(email) 195 if id 196 die_with "Contact already exists with email address '#{email}'." 197 end 198 end 199 200 # Create the contact and open for editing 201 id = add_contact(email, name) 202 open_as_txt(id, $editor) 203 import_txt id 204 exit 205 end 206 207 # Used by the next three commands 208 def get_id_from_arg 209 if(Integer(ARGV[1]) rescue false) 210 id = ARGV[1] 211 else 212 id = find_or_die(ARGV[1]) 213 end 214 215 return id 216 end 217 218 219 if ARGV[0] == 'edit' 220 id = get_id_from_arg 221 open_as_txt(id, $editor) 222 import_txt id 223 exit 224 end 225 226 if ARGV[0] == 'view' 227 id = get_id_from_arg 228 open_as_txt(id, $viewer) 229 exit 230 end 231 232 if ARGV[0] == 'cat' 233 id = get_id_from_arg 234 open_as_txt(id, 'cat') 235 exit 236 end 237 238 if ARGV[0] == 'reimport' 239 id = Integer(ARGV[1]) rescue false 240 if !id 241 die_with "The id of the txt entry to import must be a number (e.g. 1, 16, ...)" 242 end 243 244 import_txt id 245 exit 246 end 247 248 if ARGV[0] == 'list' 249 # Grouping will mean we only get first email addr. 250 sql = "SELECT contact_id, name, email 251 FROM contact 252 LEFT JOIN email USING (contact_id) 253 GROUP BY contact_id" 254 $db.execute(sql) do |row| 255 puts "%3s %-30s %s" % row 256 end 257 exit 258 end 259 260 # If we got this far, no valid command given 261 die_with "Usage: addrbook <command> [params] 262 263 Available commands: 264 add [email addr] [name] Create new contact entry 265 edit <email addr | ID> Update contact as text, import changes. 266 view <email addr | ID> Display contact as text with pager 267 cat <email addr | ID> Output contact as text (to 'cat') 268 reimport <contact number> Updates contact DB entry from text. 269 list Display list of current contacts. 270 autocomplete-email <fuzzy> TODO 271 272 The 'edit' command is the normal way to update a contact. It: 273 274 1. Writes the database contents of the entry to a text file. 275 2. Opens the text file in your editor. 276 3. Imports the edited contact information back to the database. 277 278 The 'reimport' command performs only Step 3 and exists mostly for 279 recovery if the edit process was interrupted. 280 "