gtk2passwordapp 3.0.1 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +63 -0
- data/bin/gtk2passwordapp +120 -0
- data/data/VERSION +1 -0
- data/data/logo.png +0 -0
- data/lib/gtk2passwordapp.rb +19 -305
- data/lib/gtk2passwordapp/account.rb +75 -0
- data/lib/gtk2passwordapp/accounts.rb +53 -0
- data/lib/gtk2passwordapp/config.rb +158 -0
- data/lib/gtk2passwordapp/gtk2passwordapp.rb +457 -0
- data/lib/gtk2passwordapp/such_parts.rb +22 -0
- data/lib/gtk2passwordapp/version.rb +3 -0
- metadata +166 -26
- data/README.txt +0 -40
- data/bin/gtk2passwordapp3 +0 -60
- data/lib/gtk2passwordapp/appconfig.rb +0 -171
- data/lib/gtk2passwordapp/iocrypt.rb +0 -48
- data/lib/gtk2passwordapp/passwords.rb +0 -45
- data/lib/gtk2passwordapp/passwords_data.rb +0 -130
- data/lib/gtk2passwordapp/rnd.rb +0 -88
- data/pngs/icon.png +0 -0
- data/pngs/logo.png +0 -0
@@ -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
|