gtk2passwordapp 3.0.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ module Gtk2passwordapp
2
+ class Account
3
+
4
+ PASSWORD = 0
5
+ PREVIOUS = 1
6
+ USERNAME = 2
7
+ URL = 3
8
+ NOTE = 4
9
+ UPDATED = 5
10
+
11
+ def initialize(name, data)
12
+ unless data.has_key?(name)
13
+ raise "Account name must be a non-empty String." unless name.class==String and name.length > 0
14
+ data[name] = [ '', '', '', '', '', 0 ]
15
+ end
16
+ @name, @data = name, data[name]
17
+ end
18
+
19
+ ### READERS ###
20
+
21
+ def name
22
+ @name
23
+ end
24
+
25
+ def password
26
+ @data[PASSWORD]
27
+ end
28
+
29
+ def previous
30
+ @data[PREVIOUS]
31
+ end
32
+
33
+ def username
34
+ @data[USERNAME]
35
+ end
36
+
37
+ def url
38
+ @data[URL]
39
+ end
40
+
41
+ def note
42
+ @data[NOTE]
43
+ end
44
+
45
+ def updated
46
+ @data[UPDATED]
47
+ end
48
+
49
+ ### WRITTERS ###
50
+
51
+ def password=(password)
52
+ raise 'Password must be all graph.' unless password=~/^[[:graph:]]+$/
53
+ if @data[PASSWORD] != password
54
+ @data[UPDATED] = Time.now.to_i
55
+ @data[PREVIOUS] = @data[PASSWORD]
56
+ @data[PASSWORD] = password
57
+ end
58
+ end
59
+
60
+ def note=(note)
61
+ @data[NOTE]=note
62
+ end
63
+
64
+ def username=(username)
65
+ raise 'Username must be all graph.' unless username=~/^[[:graph:]]*$/
66
+ @data[USERNAME]=username
67
+ end
68
+
69
+ def url=(url)
70
+ raise 'Must be like http://site' unless url=='' or url=~/^\w+:\/\/\S+$/
71
+ @data[URL]=url
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,53 @@
1
+ module Gtk2passwordapp
2
+ class Accounts
3
+
4
+ def reset(password)
5
+ @yzb = YamlZlibBlowfish.new(password)
6
+ end
7
+
8
+ attr_reader :data
9
+ attr_accessor :dumpfile
10
+ def initialize(dumpfile=nil, password=nil)
11
+ reset(password) if password # sets @yzb
12
+ @dumpfile = dumpfile
13
+ @data = {}
14
+ end
15
+
16
+ def exist?
17
+ File.exist? @dumpfile
18
+ end
19
+
20
+ # will raise an exception on failed decryption
21
+ def load(password=nil)
22
+ reset(password) if password
23
+ data = @yzb.load(@dumpfile)
24
+ raise "Decryption error." unless data.class == Hash
25
+ @data = data
26
+ end
27
+
28
+ def save(password=nil)
29
+ reset(password) if password
30
+ @yzb.dump(@dumpfile, @data)
31
+ File.chmod(0600, @dumpfile)
32
+ end
33
+
34
+ def names
35
+ @data.keys
36
+ end
37
+
38
+ def delete(account)
39
+ @data.delete(account)
40
+ end
41
+
42
+ def get(account)
43
+ raise "Account #{account} does NOT exists!" unless @data.has_key?(account)
44
+ Account.new(account, @data)
45
+ end
46
+
47
+ def add(account)
48
+ raise "Account #{account} exists!" if @data.has_key?(account)
49
+ Account.new(account, @data)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,158 @@
1
+ module Gtk2passwordapp
2
+ help = <<-HELP
3
+ Usage:
4
+ gtk3app gtk2passwordapp [--help] [--version]
5
+ gtk2passwordapp [--no-gui [--dump [--verbose]]] [account]
6
+ HELP
7
+
8
+ APPDIR = File.dirname File.dirname __dir__
9
+
10
+ s0 = Rafini::Empty::STRING
11
+ h0 = Rafini::Empty::HASH
12
+ a0 = Rafini::Empty::ARRAY
13
+
14
+ CONFIG = {
15
+ Help: help,
16
+
17
+ # Password Data File
18
+ PwdFile: "#{XDG['CACHE']}/gtk3app/gtk2passwordapp/passwords.dat",
19
+ # Shared Secret File
20
+ # Consider using a file found in a removable flashdrive.
21
+ SharedSecretFile: "#{XDG['CACHE']}/gtk3app/gtk2passwordapp/key.ssss",
22
+ BackupFile: "#{ENV['HOME']}/Dropbox/gtk2passwordapp.bak",
23
+
24
+ # Mark Recent Selections
25
+ Recent: 7,
26
+
27
+ # Mark Old Passwords
28
+ TooOld: 60*60*24*365, # Year
29
+
30
+ # Timeout for qr-code read.
31
+ QrcTimeOut: 3,
32
+
33
+ # Password Generators
34
+ Random: 'Random',
35
+ AlphaNumeric: 'Alpha-Numeric',
36
+ Custom: 'Caps',
37
+ CustomDigits: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
38
+
39
+ # Button Labels
40
+ Go: 'Go',
41
+ Edit: 'Edit',
42
+ Add: 'Add',
43
+ Goto: 'Goto',
44
+ Current: 'Current',
45
+ Previous: 'Previous',
46
+ Show: 'Show',
47
+ Cancel: 'Cancel',
48
+ Delete: 'Delete',
49
+ Save: 'Save',
50
+
51
+ # Labels
52
+ ReTry: 'Try Again!',
53
+ HiddenPwd: ' * * * ',
54
+
55
+ # Colors
56
+ Blue: '#00F',
57
+ Red: '#F00',
58
+ Black: '#000',
59
+
60
+ # Clipboard
61
+ SwitchClipboard: false,
62
+ ClipboardTimeout: 15,
63
+
64
+ # Fields' Labels
65
+ Name: 'Account:',
66
+ FIELDS: [
67
+ [:url, 'Url:' ],
68
+ [:note, 'Note:' ],
69
+ [:username, 'Username:'],
70
+ [:password, 'Password:'],
71
+ ],
72
+ FIELD_ALIGNMENT: [0.0, 0.5],
73
+
74
+ # Such::Thing::PARAMETERS
75
+ thing: {
76
+
77
+ box: h0,
78
+ label: h0,
79
+ check_button: h0,
80
+ entry: h0,
81
+
82
+ button: {
83
+ set_width_request: 75,
84
+ },
85
+
86
+ vbox!: [[:vertical], :box, s0],
87
+ hbox!: [[:horizontal], :box, s0],
88
+
89
+ prompt: {
90
+ set_width_request: 75,
91
+ set_alignment: [1.0, 0.5],
92
+ set_padding: [4,4],
93
+ },
94
+ prompt!: [a0, :prompt],
95
+
96
+ prompted: {
97
+ set_width_request: 325,
98
+ },
99
+ prompted!: [a0, :prompted],
100
+
101
+ a!: [a0, :button],
102
+ b!: [a0, :button],
103
+ c!: [a0, :button],
104
+
105
+ window: {
106
+ set_title: 'Password Manager',
107
+ set_window_position: :center,
108
+ },
109
+
110
+ password_label!: [['Password:'], :label],
111
+ password_entry!: [a0, :entry, {set_visibility: false}],
112
+
113
+ edit_label!: [['Edit Account'], :label],
114
+ add_label!: [['Add Account'], :label],
115
+ view_label!: [['View Account'], :label],
116
+
117
+ pwd_size_check!: [:check_button],
118
+ pwd_size_spin!: [
119
+ {
120
+ set_range: [4,64],
121
+ set_increments: [1,10],
122
+ set_digits: 0,
123
+ set_value: 14,
124
+ },
125
+ ],
126
+
127
+ reset!: [['Reset Master Password'], 'activate'],
128
+ backup!: [['Backup Passwords'], 'activate'],
129
+
130
+ about_dialog: {
131
+ set_program_name: 'Password Manager',
132
+ set_version: VERSION,
133
+ set_copyright: '(c) 2014 CarlosJHR64',
134
+ set_comments: 'A Gtk3App Password Manager',
135
+ set_website: 'https://github.com/carlosjhr64/gtk2passwordapp',
136
+ set_website_label: 'See it at GitHub!',
137
+ },
138
+ HelpFile: 'https://github.com/carlosjhr64/gtk2passwordapp',
139
+ Logo: "#{XDG['DATA']}/gtk3app/gtk2passwordapp/logo.png",
140
+
141
+ backup_dialog: {
142
+ set_title: 'Backup Passwords',
143
+ set_window_position: :center_on_parent,
144
+ },
145
+
146
+ error_dialog: {
147
+ set_text: 'Backup Error',
148
+ set_window_position: :center_on_parent,
149
+ },
150
+
151
+ delete_dialog: {
152
+ set_window_position: :center_on_parent,
153
+ },
154
+ delete_label!: [['Delete?'], :label],
155
+
156
+ }
157
+ }
158
+ end
@@ -0,0 +1,457 @@
1
+ module Gtk2passwordapp
2
+ using Rafini::Exception
3
+ using Rafini::Array
4
+
5
+ RND = SuperRandom.new
6
+ H2Q = BaseConvert::FromTo.new(:hex, :qgraph)
7
+ H2W = BaseConvert::FromTo.new(:hex, :word)
8
+
9
+ def self.options=(opts)
10
+ @@options=opts
11
+ end
12
+
13
+ def self.options
14
+ @@options
15
+ end
16
+
17
+ def self.run(program)
18
+ Gtk2PasswordApp.new(program)
19
+ end
20
+
21
+ class DeleteDialog < Such::Dialog
22
+ def initialize(parent)
23
+ super([parent: parent], :delete_dialog)
24
+ add_button(Gtk::Stock::CANCEL, Gtk::ResponseType::CANCEL)
25
+ add_button(Gtk::Stock::OK, Gtk::ResponseType::OK)
26
+ Such::Label.new child, :delete_label!
27
+ end
28
+
29
+ def runs
30
+ show_all
31
+ value = false
32
+ if run == Gtk::ResponseType::OK
33
+ value = true
34
+ end
35
+ destroy
36
+ return value
37
+ end
38
+ end
39
+
40
+ class BackupDialog < Such::FileChooserDialog
41
+ def initialize(parent)
42
+ super([parent: parent], :backup_dialog)
43
+ set_action Gtk::FileChooser::Action::SAVE
44
+ add_button(Gtk::Stock::CANCEL, Gtk::ResponseType::CANCEL)
45
+ add_button(Gtk::Stock::OPEN, Gtk::ResponseType::ACCEPT)
46
+ if CONFIG[:BackupFile]
47
+ set_filename CONFIG[:BackupFile]
48
+ set_current_name File.basename CONFIG[:BackupFile]
49
+ end
50
+ end
51
+
52
+ def runs
53
+ show_all
54
+ value = nil
55
+ if run == Gtk::ResponseType::ACCEPT
56
+ value = filename
57
+ end
58
+ destroy
59
+ return value
60
+ end
61
+ end
62
+
63
+ class ErrorDialog < Such::MessageDialog
64
+ def initialize(parent)
65
+ super([parent: parent, flags: :modal, type: :error, buttons_type: :close], :error_dialog)
66
+ end
67
+
68
+ def runs
69
+ set_secondary_text $!.message
70
+ run
71
+ destroy
72
+ end
73
+ end
74
+
75
+ class Gtk2PasswordApp
76
+
77
+ def initialize(program)
78
+ @program = program
79
+ @names = @combo = nil
80
+
81
+ @blue = Gdk::RGBA.parse(CONFIG[:Blue])
82
+ @red = Gdk::RGBA.parse(CONFIG[:Red])
83
+ @black = Gdk::RGBA.parse(CONFIG[:Black])
84
+
85
+ _ = CONFIG[:CustomDigits]
86
+ @h2c = BaseConvert::FromTo.new(:hex, _.length)
87
+ @h2c.to_digits = _
88
+
89
+ if CONFIG[:SwitchClipboard]
90
+ @clipboard = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
91
+ @primary = Gtk::Clipboard.get(Gdk::Selection::CLIPBOARD)
92
+ else
93
+ @primary = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
94
+ @clipboard = Gtk::Clipboard.get(Gdk::Selection::CLIPBOARD)
95
+ end
96
+
97
+ @current, @previous = [], []
98
+
99
+ window = program.window
100
+ @page = Such::Box.new window, :vbox!
101
+ @accounts = Accounts.new(CONFIG[:PwdFile])
102
+ password_page((@accounts.exist?)? :load : :init)
103
+ window.show
104
+
105
+ # Because accounts are editable from the main window,
106
+ # minime's menu needs to be updated each time.
107
+ destroy_menu_items
108
+ program.mini.signal_connect('show'){generate_menu_items}
109
+ program.mini.signal_connect('hide'){destroy_menu_items}
110
+ end
111
+
112
+ def copy2clipboard(pwd, user)
113
+ @primary.text = pwd
114
+ @clipboard.text = user
115
+ GLib::Timeout.add_seconds(CONFIG[:ClipboardTimeout]) do
116
+ @primary.request_text{ |_, text| @primary.text = '' if text == pwd }
117
+ @clipboard.request_text{|_, text| @clipboard.text = '' if text == user }
118
+ false
119
+ end
120
+ end
121
+
122
+ def color_code(selected)
123
+ @current.unshift selected; @current.uniq!
124
+ if @current.length > CONFIG[:Recent]
125
+ popped = @current.pop
126
+ popped.override_color :normal, @black
127
+ end
128
+ selected.override_color :normal, @blue
129
+ end
130
+
131
+ def generate_menu_items
132
+ now = Time.now.to_i
133
+ @accounts.names.sort{|a,b|a.upcase<=>b.upcase}.each do |name|
134
+ account = @accounts.get name
135
+ pwd, user, updated = account.password, account.username, account.updated
136
+ too_old = ((now - updated) > CONFIG[:TooOld])
137
+ selected = Such::MenuItem.new([name], 'activate') do
138
+ color_code selected unless too_old
139
+ @combo.set_active @names.index name if @combo
140
+ copy2clipboard pwd, user
141
+ end
142
+ if too_old
143
+ selected.override_color :normal, @red
144
+ elsif @previous.include? name
145
+ @current[@previous.index(name)] = selected
146
+ selected.override_color :normal, @blue
147
+ end
148
+ @program.mini_menu.append selected
149
+ selected.show
150
+ end
151
+ @current.delete_if{|a|a.nil?}
152
+ @previous.clear
153
+ end
154
+
155
+ def destroy_menu_items
156
+ @current.each{|item| @previous.push item.label}
157
+ @current.clear
158
+ @program.mini_menu.each{|item|item.destroy}
159
+ end
160
+
161
+ def clear_page
162
+ @page.each{|w|w.destroy}
163
+ end
164
+
165
+ def process_pwd_entries(entry1, entry2)
166
+ begin
167
+ pwd1 = entry1.text.strip
168
+ if pwd1 == '' and pwd = Helpema::ZBar.qrcode(CONFIG[:QrcTimeOut])
169
+ pwd1 = pwd
170
+ end
171
+ raise 'No password given.' if pwd1 == ''
172
+ if entry2
173
+ raise 'Passwords did not match' unless entry2.text.strip==pwd1
174
+ @accounts.save pwd1
175
+ else
176
+ if pwd1=~/^\d+\-[\dabcdef]+$/ # then we probably have a shared secret...
177
+ if File.exist? CONFIG[:SharedSecretFile] # and looks like we really do...
178
+ pwd0 = File.read(CONFIG[:SharedSecretFile]).strip
179
+ pwd = Helpema::SSSS.combine(pwd0, pwd1)
180
+ pwd1 = pwd unless pwd=='' # but maybe not.
181
+ end
182
+ end
183
+ @accounts.load pwd1
184
+ end
185
+ true
186
+ rescue StandardError
187
+ $!.puts
188
+ entry1.text = ''
189
+ entry2.text = '' if entry2
190
+ false
191
+ end
192
+ end
193
+
194
+ def backup
195
+ if filename = BackupDialog.new(@program.window).runs
196
+ begin
197
+ FileUtils.cp CONFIG[:PwdFile], filename
198
+ rescue
199
+ $!.puts
200
+ ErrorDialog.new(@program.window).runs
201
+ end
202
+ end
203
+ end
204
+
205
+ # mode can be :init, :load, or :reset
206
+ def password_page(mode)
207
+ clear_page
208
+
209
+ password_label = Such::Label.new @page, :password_label!
210
+ password_entry1 = Such::Entry.new @page, :password_entry!
211
+ password_entry2 = (mode==:load)? nil : Such::Entry.new(@page, :password_entry!)
212
+
213
+ action = Such::AbButtons.new(@page, :hbox!) do |button, *_|
214
+ case button
215
+ when action.a_Button # Cancel
216
+ (mode==:reset)? view_page : @program.quit!
217
+ when action.b_Button # Go
218
+ if process_pwd_entries password_entry1, password_entry2
219
+ unless mode==:reset
220
+ @program.app_menu.append_menu_item(:reset!){password_page(:reset)}
221
+ @program.app_menu.append_menu_item(:backup!){backup}
222
+ end
223
+ view_page
224
+ else
225
+ password_label.text = CONFIG[:ReTry]
226
+ end
227
+ end
228
+ end
229
+ action.labels :Cancel, :Go
230
+
231
+ password_entry1.signal_connect('activate') do
232
+ if password_entry2
233
+ password_entry2.grab_focus
234
+ else
235
+ action.b_Button.clicked
236
+ end
237
+ end
238
+ if password_entry2
239
+ password_entry2.signal_connect('activate') do
240
+ action.b_Button.clicked
241
+ end
242
+ end
243
+
244
+ @page.show_all
245
+ end
246
+
247
+ def create_combo
248
+ combo = Such::PromptedCombo.new @page, :hbox!
249
+ combo.prompt_Label.text = CONFIG[:Name]
250
+ @combo= combo.prompted_ComboBoxText
251
+ @names = @accounts.names.sort{|a,b|a.upcase<=>b.upcase}
252
+ @names.each{|name|@combo.append_text name}
253
+ @combo.set_active @names.index(@account.name)
254
+ @combo.signal_connect('destroy'){@names = @combo = nil}
255
+ end
256
+
257
+ def create_entries
258
+ entries = {}
259
+ CONFIG[:FIELDS].each do |field, text|
260
+ entry = Such::PromptedLabel.new @page, :hbox!
261
+ entry.prompt_Label.text = text
262
+ entry.prompted_Label.text = @account.method(field).call
263
+ entry.prompted_Label.set_alignment(*CONFIG[:FIELD_ALIGNMENT])
264
+ entries[field] = entry
265
+ end
266
+ return entries
267
+ end
268
+
269
+ def any_name
270
+ names = @accounts.names
271
+ if name = ARGV.shift
272
+ unless names.include? name
273
+ like = Regexp.new name
274
+ name = names.which{|nm|nm=~like}
275
+ end
276
+ end
277
+ name = names.sample unless name
278
+ return name
279
+ end
280
+
281
+ def view_page
282
+ if @accounts.data.length == 0
283
+ edit_page(:add)
284
+ return
285
+ end
286
+ @account ||= @accounts.get any_name
287
+
288
+ clear_page
289
+
290
+ Such::Label.new @page, :view_label!
291
+ create_combo
292
+ entries = create_entries
293
+
294
+ label, hidden = entries[:password].prompted_Label, CONFIG[:HiddenPwd]
295
+ label.text = hidden
296
+
297
+ @combo.signal_connect('changed') do
298
+ @account = @accounts.get @combo.active_text
299
+ CONFIG[:FIELDS].each do |field, _|
300
+ entries[field].prompted_Label.text = @account.method(field).call
301
+ end
302
+ label.text = hidden
303
+ end
304
+
305
+ clip_box = Such::AbcButtons.new(@page, :hbox!) do |button, *_|
306
+ case button
307
+ when clip_box.a_Button # Current
308
+ copy2clipboard @account.password, @account.username
309
+ when clip_box.b_Button # Previous
310
+ copy2clipboard @account.previous, @account.password
311
+ when clip_box.c_Button # Show
312
+ label.text == hidden ?
313
+ label.text = @account.password :
314
+ label.text = hidden
315
+ end
316
+ end
317
+ clip_box.labels :Current, :Previous, :Show
318
+
319
+ edit_box = Such::AbcButtons.new(@page, :hbox!) do |button, *_|
320
+ case button
321
+ when edit_box.a_Button then edit_page
322
+ when edit_box.b_Button then edit_page(:add)
323
+ when edit_box.c_Button
324
+ system("#{Gtk3App::CONFIG[:Open]} '#{@account.url}'") if @account.url.length > 0
325
+ end
326
+ end
327
+ edit_box.labels :Edit, :Add, :Goto
328
+
329
+ @page.show_all
330
+ end
331
+
332
+ def edit_page(mode=:edit)
333
+ clear_page
334
+
335
+ edited = false
336
+ previous = @account ? @account.name : nil
337
+ name = nil
338
+
339
+ case mode
340
+ when :add
341
+ Such::Label.new @page, :add_label!
342
+ name = Such::PromptedEntryLabel.new @page, :hbox!
343
+ name.prompt_Label.text = CONFIG[:Name]
344
+ when :edit
345
+ Such::Label.new @page, :edit_label!
346
+ name = Such::PromptedLabel.new @page, :hbox!
347
+ name.prompt_Label.text = CONFIG[:Name]
348
+ name.prompted_Label.text = @account.name
349
+ end
350
+ name.prompted_Label.set_alignment(*CONFIG[:FIELD_ALIGNMENT])
351
+
352
+ entries = {}
353
+ CONFIG[:FIELDS].each do |field, text|
354
+ entry = Such::PromptedEntry.new @page, :hbox!
355
+ entry.prompt_Label.text = text
356
+ entry.prompted_Entry.text = @account.method(field).call if mode==:edit
357
+ entries[field] = entry
358
+ end
359
+
360
+ # cb and sb will be a CheckButton and SpinButton respectively.
361
+ cb = sb = nil
362
+ password = @account ? @account.password : ''
363
+ truncate = Proc.new do |p|
364
+ password = p
365
+ if cb.active?
366
+ n = sb.value.to_i
367
+ p = p[-n..-1] if p.length > n
368
+ end
369
+ p
370
+ end
371
+
372
+ pwd = entries[:password].prompted_Entry
373
+ pwd.set_visibility false
374
+ pwd.signal_connect('focus-in-event' ){pwd.set_visibility true}
375
+ pwd.signal_connect('focus-out-event'){pwd.set_visibility false}
376
+
377
+ generators = Such::AbcButtons.new(@page, :hbox!) do |button,*e,s|
378
+ hex = RND.hexadecimal
379
+ case button
380
+ when generators.a_Button
381
+ pwd.text = truncate.call H2Q.convert hex
382
+ when generators.b_Button
383
+ pwd.text = truncate.call H2W.convert hex
384
+ when generators.c_Button
385
+ pwd.text = truncate.call @h2c.convert hex
386
+ end
387
+ end
388
+ generators.labels :Random, :AlphaNumeric, :Custom
389
+
390
+ cb = Such::CheckButton.new(generators, :pwd_size_check!, 'toggled') do
391
+ pwd.text = (cb.active?) ? truncate.call(password) : password
392
+ end
393
+ sb = Such::SpinButton.new(generators, :pwd_size_spin!, 'value-changed') do
394
+ pwd.text = truncate.call password if cb.active?
395
+ end
396
+
397
+ action = Such::AbcButtons.new(@page, :hbox!) do |button, *_|
398
+ case button
399
+ when action.a_Button # Cancel
400
+ if edited
401
+ @accounts.load
402
+ @account = previous ? @accounts.get(previous) : nil
403
+ end
404
+ view_page
405
+ when action.b_Button # Delete
406
+ dialog = DeleteDialog.new(@program.window)
407
+ dialog.set_title @account.name
408
+ if dialog.runs
409
+ @accounts.delete @account.name
410
+ @accounts.save
411
+ @account = nil
412
+ view_page
413
+ end
414
+ when action.c_Button # Save
415
+ edited = true
416
+ begin
417
+ if mode==:add
418
+ @account = @accounts.add(name.prompted_Entry.text.strip)
419
+ name.prompted_Label.text = @account.name
420
+ name.prompted_Entry.hide
421
+ name.prompted_Label.show
422
+ name.prompt_Label.override_color :normal, @blue
423
+ mode = :edit
424
+ end
425
+ errors = false
426
+ entries.each do |field, entry|
427
+ begin
428
+ @account.method("#{field}=".to_sym).call(entry.prompted_Entry.text.strip)
429
+ entry.prompt_Label.override_color :normal, @blue
430
+ rescue RuntimeError
431
+ $!.puts
432
+ errors ||= true
433
+ entry.prompt_Label.override_color :normal, @red
434
+ end
435
+ end
436
+ unless errors
437
+ @accounts.save
438
+ view_page
439
+ end
440
+ rescue RuntimeError
441
+ $!.puts
442
+ name.prompt_Label.override_color :normal, @red
443
+ end
444
+ end
445
+ end
446
+ action.labels :Cancel, :Delete, :Save
447
+
448
+ @page.show_all
449
+ if mode==:add
450
+ name.prompted_Label.hide
451
+ action.b_Button.hide
452
+ end
453
+ end
454
+
455
+ end
456
+
457
+ end