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.
- 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
|