gtk2passwordapp 3.0.1 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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