ruby-pass-qt 1.0.0
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 +7 -0
- data/.rubocop.yml +10 -0
- data/.ruby-version +1 -0
- data/LICENSE +674 -0
- data/README.md +38 -0
- data/Rakefile +12 -0
- data/exe/pass-qt +6 -0
- data/lib/pass-qt/app/components/passinfowidget.rb +235 -0
- data/lib/pass-qt/app/components/passlistwidget/treewidget.rb +240 -0
- data/lib/pass-qt/app/components/passlistwidget.rb +91 -0
- data/lib/pass-qt/app/components.rb +2 -0
- data/lib/pass-qt/app/views/browsestoresdialog.rb +111 -0
- data/lib/pass-qt/app/views/mainwindow.rb +80 -0
- data/lib/pass-qt/app/views/newonetimepassworddialog.rb +133 -0
- data/lib/pass-qt/app/views/newpassworddialog.rb +169 -0
- data/lib/pass-qt/app/views.rb +4 -0
- data/lib/pass-qt/app.rb +23 -0
- data/lib/pass-qt/config/initializers/qt6.rb +2 -0
- data/lib/pass-qt/config/settings.rb +40 -0
- data/lib/pass-qt/config.rb +9 -0
- data/lib/pass-qt/lib/contrib/process.rb +58 -0
- data/lib/pass-qt/lib/contrib.rb +1 -0
- data/lib/pass-qt/lib/pass.rb +32 -0
- data/lib/pass-qt/lib.rb +2 -0
- data/lib/pass-qt/version.rb +3 -0
- data/lib/pass-qt.rb +6 -0
- data/screenshot.png +0 -0
- metadata +83 -0
data/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Pass-Qt
|
|
2
|
+
|
|
3
|
+
A simple GUI for pass on Linux.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Ruby 3.4+](https://www.ruby-lang.org/)
|
|
8
|
+
- [Qt 6.10+](https://www.qt.io/)
|
|
9
|
+
- [pass](https://www.passwordstore.org/)
|
|
10
|
+
- [pwgen](https://sourceforge.net/projects/pwgen/)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ gem install pass-qt
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
To initialize the password store:
|
|
21
|
+
|
|
22
|
+
```console
|
|
23
|
+
$ pass init new_gpg-id_or_email
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To launch the GUI:
|
|
27
|
+
|
|
28
|
+
```console
|
|
29
|
+
$ pass-qt
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Screenshot
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
Licensed under the [GPL-3.0-only](./LICENSE).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
_ = Gem::Specification.load("pass-qt.gemspec")
|
|
5
|
+
|
|
6
|
+
require "rubocop/rake_task"
|
|
7
|
+
RuboCop::RakeTask.new
|
|
8
|
+
|
|
9
|
+
require "rspec/core/rake_task"
|
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
11
|
+
|
|
12
|
+
task default: %i[spec]
|
data/exe/pass-qt
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
module PassQt
|
|
2
|
+
class PassInfoWidget < RubyQt6::Bando::QWidget
|
|
3
|
+
q_object do
|
|
4
|
+
slot "_on_copy_action_triggered()"
|
|
5
|
+
slot "_on_copy_action_triggered_otpcode()"
|
|
6
|
+
slot "_on_view_action_triggered()"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
|
|
12
|
+
@store = QString.new
|
|
13
|
+
@passname = QString.new
|
|
14
|
+
|
|
15
|
+
initialize_form
|
|
16
|
+
initialize_otpform
|
|
17
|
+
initialize_folderform
|
|
18
|
+
initialize_infoframe
|
|
19
|
+
|
|
20
|
+
@stackedlayout = QStackedLayout.new
|
|
21
|
+
@stackedlayout.add_widget(@form)
|
|
22
|
+
@stackedlayout.add_widget(@otpform)
|
|
23
|
+
@stackedlayout.add_widget(@folderform)
|
|
24
|
+
@stackedlayout.add_widget(@infoframe)
|
|
25
|
+
|
|
26
|
+
mainlayout = QVBoxLayout.new(self)
|
|
27
|
+
mainlayout.add_spacing(76)
|
|
28
|
+
mainlayout.add_layout(@stackedlayout)
|
|
29
|
+
|
|
30
|
+
use_infoframe
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reinitialize_passfile(store, passname)
|
|
34
|
+
@store = store
|
|
35
|
+
@passname = passname
|
|
36
|
+
use_infoframe
|
|
37
|
+
|
|
38
|
+
Pass.show(@store, @passname, on_success: ->(data) {
|
|
39
|
+
formdata = h_parse_passfile(data)
|
|
40
|
+
if formdata["password"].start_with?("otpauth:")
|
|
41
|
+
use_otpform(formdata)
|
|
42
|
+
else
|
|
43
|
+
use_form(formdata)
|
|
44
|
+
end
|
|
45
|
+
}, on_failure: ->(data) {
|
|
46
|
+
errinfo = data["stderr"]
|
|
47
|
+
use_infoframe_err(errinfo)
|
|
48
|
+
})
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reinitialize_passfolder(store, passname)
|
|
52
|
+
@store = store
|
|
53
|
+
@passname = passname
|
|
54
|
+
use_infoframe
|
|
55
|
+
use_folderform
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reinitialize_infoframe
|
|
59
|
+
@store = QString.new
|
|
60
|
+
@passname = QString.new
|
|
61
|
+
use_infoframe
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def initialize_form
|
|
67
|
+
@passnameinput = initialize_form_inputfield
|
|
68
|
+
initialize_form_inputfield_copyaction(@passnameinput)
|
|
69
|
+
|
|
70
|
+
@passwordinput = initialize_form_inputfield
|
|
71
|
+
initialize_form_inputfield_copyaction(@passwordinput)
|
|
72
|
+
initialize_form_inputfield_viewaction(@passwordinput)
|
|
73
|
+
|
|
74
|
+
@usernameinput = initialize_form_inputfield
|
|
75
|
+
initialize_form_inputfield_copyaction(@usernameinput)
|
|
76
|
+
|
|
77
|
+
@websiteinput = initialize_form_inputfield
|
|
78
|
+
initialize_form_inputfield_copyaction(@websiteinput)
|
|
79
|
+
|
|
80
|
+
@form = QWidget.new
|
|
81
|
+
formlayout = QFormLayout.new(@form)
|
|
82
|
+
formlayout.add_row(initialize_form_label("File"), @passnameinput)
|
|
83
|
+
formlayout.add_row(initialize_form_label("Username"), @usernameinput)
|
|
84
|
+
formlayout.add_row(initialize_form_label("Password"), @passwordinput)
|
|
85
|
+
formlayout.add_row(initialize_form_label("Website"), @websiteinput)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def initialize_otpform
|
|
89
|
+
@otppassnameinput = initialize_form_inputfield
|
|
90
|
+
initialize_form_inputfield_copyaction(@otppassnameinput)
|
|
91
|
+
|
|
92
|
+
@otppasswordinput = initialize_form_inputfield
|
|
93
|
+
initialize_form_inputfield_copyaction(@otppasswordinput)
|
|
94
|
+
initialize_form_inputfield_viewaction(@otppasswordinput)
|
|
95
|
+
|
|
96
|
+
@otpcodeinput = initialize_form_inputfield
|
|
97
|
+
action = @otpcodeinput.add_action(QIcon.from_theme(QIcon::ThemeIcon::EditCopy), QLineEdit::LeadingPosition)
|
|
98
|
+
action.triggered.connect(self, :_on_copy_action_triggered_otpcode)
|
|
99
|
+
|
|
100
|
+
@otpform = QWidget.new
|
|
101
|
+
otpformlayout = QFormLayout.new(@otpform)
|
|
102
|
+
otpformlayout.add_row(initialize_form_label("File"), @otppassnameinput)
|
|
103
|
+
otpformlayout.add_row(initialize_form_label("OTP URI"), @otppasswordinput)
|
|
104
|
+
otpformlayout.add_row(initialize_form_label("OTP Code"), @otpcodeinput)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize_folderform
|
|
108
|
+
@folderpassnameinput = initialize_form_inputfield
|
|
109
|
+
initialize_form_inputfield_copyaction(@folderpassnameinput)
|
|
110
|
+
|
|
111
|
+
@folderform = QWidget.new
|
|
112
|
+
folderformlayout = QFormLayout.new(@folderform)
|
|
113
|
+
folderformlayout.add_row(initialize_form_label("Folder"), @folderpassnameinput)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def initialize_form_label(text)
|
|
117
|
+
label = QLabel.new(text)
|
|
118
|
+
label.set_alignment(Qt::AlignRight)
|
|
119
|
+
label.set_style_sheet("min-width: 80px; padding-right: 4px;")
|
|
120
|
+
label
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def initialize_form_inputfield
|
|
124
|
+
input = QLineEdit.new
|
|
125
|
+
input.set_read_only(true)
|
|
126
|
+
input.set_focus_policy(Qt::NoFocus)
|
|
127
|
+
input
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def initialize_form_inputfield_copyaction(input)
|
|
131
|
+
action = input.add_action(QIcon.from_theme(QIcon::ThemeIcon::EditCopy), QLineEdit::LeadingPosition)
|
|
132
|
+
action.triggered.connect(self, :_on_copy_action_triggered)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def initialize_form_inputfield_viewaction(input)
|
|
136
|
+
action = input.add_action(QIcon.from_theme(QIcon::ThemeIcon::DocumentPrintPreview), QLineEdit::TrailingPosition)
|
|
137
|
+
action.triggered.connect(self, :_on_view_action_triggered)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def initialize_infoframe
|
|
141
|
+
@infolabel = QLabel.new
|
|
142
|
+
@infoframe = QWidget.new
|
|
143
|
+
infoframelayout = QVBoxLayout.new(@infoframe)
|
|
144
|
+
infoframelayout.add_widget(@infolabel)
|
|
145
|
+
infoframelayout.add_stretch
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def use_form(formdata)
|
|
149
|
+
@passnameinput.set_text(@passname)
|
|
150
|
+
@passwordinput.set_text(formdata["password"])
|
|
151
|
+
@passwordinput.set_echo_mode(QLineEdit::Password)
|
|
152
|
+
|
|
153
|
+
@usernameinput.set_text(formdata["username"])
|
|
154
|
+
@websiteinput.set_text(formdata["website"])
|
|
155
|
+
@stackedlayout.set_current_widget(@form)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def use_otpform(formdata)
|
|
159
|
+
@otppassnameinput.set_text(@passname)
|
|
160
|
+
@otppasswordinput.set_text(formdata["password"])
|
|
161
|
+
@otppasswordinput.set_cursor_position(0)
|
|
162
|
+
@otppasswordinput.set_echo_mode(QLineEdit::Password)
|
|
163
|
+
|
|
164
|
+
@otpcodeinput.set_text("")
|
|
165
|
+
@stackedlayout.set_current_widget(@otpform)
|
|
166
|
+
|
|
167
|
+
Pass.otp(@store, @passname, on_success: ->(data) {
|
|
168
|
+
otpcode = data["stdout"].rstrip
|
|
169
|
+
@otpcodeinput.set_text(otpcode)
|
|
170
|
+
}, on_failure: ->(_) {})
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def use_folderform
|
|
174
|
+
@folderpassnameinput.set_text(@passname)
|
|
175
|
+
@stackedlayout.set_current_widget(@folderform)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def use_infoframe(info = "")
|
|
179
|
+
@infolabel.set_style_sheet("color: black;")
|
|
180
|
+
@infolabel.set_text(info)
|
|
181
|
+
@stackedlayout.set_current_widget(@infoframe)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def use_infoframe_err(info)
|
|
185
|
+
@infolabel.set_style_sheet("color: red;")
|
|
186
|
+
@infolabel.set_text(info)
|
|
187
|
+
@stackedlayout.set_current_widget(@infoframe)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def h_parse_passfile(data)
|
|
191
|
+
lines = data["stdout"].lines
|
|
192
|
+
password = lines[0][..-2]
|
|
193
|
+
|
|
194
|
+
username = ""
|
|
195
|
+
website = ""
|
|
196
|
+
lines.each do |line|
|
|
197
|
+
matched = line.match(/\A(\w+:\s*)?(.*)\n/)
|
|
198
|
+
next if matched.nil?
|
|
199
|
+
|
|
200
|
+
case matched[1]&.rstrip&.downcase
|
|
201
|
+
when "username:" then username = matched[2]
|
|
202
|
+
when "website:" then website = matched[2]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
"password" => password,
|
|
208
|
+
"username" => username,
|
|
209
|
+
"website" => website
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def _on_copy_action_triggered
|
|
214
|
+
input = sender.parent
|
|
215
|
+
QApplication.clipboard.set_text(input.text)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def _on_copy_action_triggered_otpcode
|
|
219
|
+
Pass.otp(@store, @passname, on_success: ->(data) {
|
|
220
|
+
otpcode = data["stdout"].rstrip
|
|
221
|
+
@otpcodeinput.set_text(otpcode)
|
|
222
|
+
QApplication.clipboard.set_text(otpcode)
|
|
223
|
+
}, on_failure: ->(_) {})
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def _on_view_action_triggered
|
|
227
|
+
input = sender.parent
|
|
228
|
+
case input.echo_mode
|
|
229
|
+
when QLineEdit::Normal then input.set_echo_mode(QLineEdit::Password)
|
|
230
|
+
when QLineEdit::Password then input.set_echo_mode(QLineEdit::Normal)
|
|
231
|
+
else raise "unreachable!"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
module PassQt
|
|
2
|
+
class PassListWidget < RubyQt6::Bando::QWidget
|
|
3
|
+
class TreeWidget < RubyQt6::Bando::QTreeWidget
|
|
4
|
+
DataItem = Struct.new(:passname, :treewidgetitem)
|
|
5
|
+
|
|
6
|
+
q_object do
|
|
7
|
+
signal "store_changed(QString)"
|
|
8
|
+
signal "passfile_selected(QString,QString)"
|
|
9
|
+
signal "passfolder_selected(QString,QString)"
|
|
10
|
+
slot "_on_item_clicked(QTreeWidgetItem*,int)"
|
|
11
|
+
slot "_on_new_password_action_triggered()"
|
|
12
|
+
slot "_on_new_otp_action_triggered()"
|
|
13
|
+
slot "_on_delete_action_triggered()"
|
|
14
|
+
slot "_on_refresh_action_triggered()"
|
|
15
|
+
slot "_on_open_action_triggered()"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
super
|
|
20
|
+
|
|
21
|
+
@store = QString.new
|
|
22
|
+
@dataitems = {}
|
|
23
|
+
|
|
24
|
+
initialize_actions
|
|
25
|
+
initialize_fileiconprovider
|
|
26
|
+
|
|
27
|
+
item_clicked.connect(self, :_on_item_clicked)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def context_menu_event(evt)
|
|
31
|
+
menu = QMenu.new("", self)
|
|
32
|
+
|
|
33
|
+
menu.add_action(@new_password_action)
|
|
34
|
+
menu.add_action(@new_otp_action)
|
|
35
|
+
menu.add_action(@delete_action)
|
|
36
|
+
|
|
37
|
+
menu.add_separator
|
|
38
|
+
menu.add_action(@refresh_action)
|
|
39
|
+
menu.add_action(@open_action)
|
|
40
|
+
|
|
41
|
+
enabled = !selected_items.empty?
|
|
42
|
+
@delete_action.set_enabled(enabled)
|
|
43
|
+
|
|
44
|
+
menu.exec(evt.global_pos)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reinitialize_store(store)
|
|
48
|
+
clear
|
|
49
|
+
|
|
50
|
+
@store = store
|
|
51
|
+
@dataitems = {}
|
|
52
|
+
|
|
53
|
+
store_root_path = QDir.new(@store)
|
|
54
|
+
dirs = [store]
|
|
55
|
+
until dirs.empty?
|
|
56
|
+
dir = dirs.shift
|
|
57
|
+
diritem = @dataitems[dir]&.treewidgetitem || invisible_root_item
|
|
58
|
+
|
|
59
|
+
entry_list = QDir.new(dir).entry_info_list(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)
|
|
60
|
+
entry_list.each do |entry|
|
|
61
|
+
filepath = entry.absolute_file_path
|
|
62
|
+
next if @dataitems.key?(filepath)
|
|
63
|
+
|
|
64
|
+
if entry.dir?
|
|
65
|
+
dirs << filepath
|
|
66
|
+
passname = store_root_path.relative_file_path(filepath)
|
|
67
|
+
elsif entry.file?
|
|
68
|
+
next unless h_passfile?(entry)
|
|
69
|
+
passname = store_root_path.relative_file_path(filepath)
|
|
70
|
+
passname = passname[0, passname.size - entry.suffix.size - 1]
|
|
71
|
+
else
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
item = QTreeWidgetItem.new(diritem, QStringList.new << entry.complete_base_name << filepath)
|
|
76
|
+
item.set_icon(0, @fileiconprovider.icon(entry))
|
|
77
|
+
@dataitems[filepath] = DataItem.new(passname, item)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
expand_all
|
|
82
|
+
store_changed.emit(@store)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update_passname_filter(text)
|
|
86
|
+
if text.empty?
|
|
87
|
+
@dataitems.each do |_, item|
|
|
88
|
+
item.treewidgetitem.set_hidden(false)
|
|
89
|
+
item.treewidgetitem.set_selected(false)
|
|
90
|
+
end
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
re_options = QRegularExpression::UnanchoredWildcardConversion | QRegularExpression::NonPathWildcardConversion
|
|
95
|
+
re = QRegularExpression.from_wildcard(text, nil, re_options)
|
|
96
|
+
|
|
97
|
+
filepath_matched = Set.new
|
|
98
|
+
filepath_matched_1st = nil
|
|
99
|
+
@dataitems.each do |filepath, item|
|
|
100
|
+
has_match = re.match(item.passname).has_match
|
|
101
|
+
next unless has_match
|
|
102
|
+
|
|
103
|
+
if filepath_matched_1st.nil?
|
|
104
|
+
filepath_matched_1st = filepath if h_passfile?(filepath)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
loop do
|
|
108
|
+
filepath_matched << filepath
|
|
109
|
+
filepath = QFileInfo.new(filepath).absolute_path
|
|
110
|
+
break unless @dataitems.key?(filepath)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@dataitems.each do |filepath, item|
|
|
115
|
+
visible = filepath_matched.include?(filepath)
|
|
116
|
+
item.treewidgetitem.set_hidden(!visible)
|
|
117
|
+
|
|
118
|
+
selected = filepath_matched_1st == filepath
|
|
119
|
+
item.treewidgetitem.set_selected(selected)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def initialize_actions
|
|
126
|
+
@new_password_action = initialize_actions_act(QIcon::ThemeIcon::DocumentNew, "New Password", :_on_new_password_action_triggered)
|
|
127
|
+
@new_otp_action = initialize_actions_act(QIcon::ThemeIcon::DocumentNew, "New OTP", :_on_new_otp_action_triggered)
|
|
128
|
+
@delete_action = initialize_actions_act(QIcon::ThemeIcon::EditDelete, "Delete", :_on_delete_action_triggered)
|
|
129
|
+
@refresh_action = initialize_actions_act(QIcon::ThemeIcon::ViewRefresh, "Refresh", :_on_refresh_action_triggered)
|
|
130
|
+
@open_action = initialize_actions_act(QIcon::ThemeIcon::FolderVisiting, "Open Store With File Explorer", :_on_open_action_triggered)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize_actions_act(icon, text, slot)
|
|
134
|
+
action = QAction.new(QIcon.from_theme(icon), text, self)
|
|
135
|
+
action.triggered.connect(self, slot)
|
|
136
|
+
action
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def initialize_fileiconprovider
|
|
140
|
+
@fileiconprovider = QFileIconProvider.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def reinitialize(selected_passname: "")
|
|
144
|
+
reinitialize_store(@store)
|
|
145
|
+
|
|
146
|
+
@dataitems.each do |_, item|
|
|
147
|
+
if item.passname == selected_passname
|
|
148
|
+
item.treewidgetitem.set_selected(true)
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def h_passfile?(fileinfo)
|
|
155
|
+
case fileinfo
|
|
156
|
+
when QFileInfo then fileinfo.suffix.downcase == "gpg"
|
|
157
|
+
when QString then fileinfo.ends_with(".gpg", Qt::CaseInsensitive)
|
|
158
|
+
else raise "unreachable!"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def h_folderpath(fileinfo)
|
|
163
|
+
folder = fileinfo.dir? ? fileinfo.absolute_file_path : fileinfo.absolute_path
|
|
164
|
+
QDir.new(@store).relative_file_path(folder)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def _on_item_clicked(item, _column)
|
|
168
|
+
filepath = item.data(1, Qt::DisplayRole).value
|
|
169
|
+
dataitem = @dataitems[filepath]
|
|
170
|
+
h_passfile?(filepath) ?
|
|
171
|
+
passfile_selected.emit(@store, dataitem.passname) :
|
|
172
|
+
passfolder_selected.emit(@store, dataitem.passname)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def _on_new_password_action_triggered
|
|
176
|
+
item = selected_items[0]
|
|
177
|
+
if item
|
|
178
|
+
filepath = item.data(1, Qt::DisplayRole).value
|
|
179
|
+
folder = h_folderpath(QFileInfo.new(filepath))
|
|
180
|
+
folder = "" if folder == "."
|
|
181
|
+
else
|
|
182
|
+
folder = ""
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
dialog = NewPasswordDialog.new(@store, folder, on_success: ->(passname) {
|
|
186
|
+
reinitialize(selected_passname: passname)
|
|
187
|
+
})
|
|
188
|
+
dialog.show
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def _on_new_otp_action_triggered
|
|
192
|
+
item = selected_items[0]
|
|
193
|
+
if item
|
|
194
|
+
filepath = item.data(1, Qt::DisplayRole).value
|
|
195
|
+
folder = h_folderpath(QFileInfo.new(filepath))
|
|
196
|
+
folder = "" if folder == "."
|
|
197
|
+
else
|
|
198
|
+
folder = ""
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
dialog = NewOneTimePasswordDialog.new(@store, folder, on_success: ->(passname) {
|
|
202
|
+
reinitialize(selected_passname: passname)
|
|
203
|
+
})
|
|
204
|
+
dialog.show
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def _on_delete_action_triggered
|
|
208
|
+
item = selected_items[0]
|
|
209
|
+
filepath = item.data(1, Qt::DisplayRole).value
|
|
210
|
+
dataitem = @dataitems[filepath]
|
|
211
|
+
|
|
212
|
+
message = "<p>Do you really want to delete this item?</p>#{dataitem.passname}"
|
|
213
|
+
reply = QMessageBox.question(self, "", message)
|
|
214
|
+
return if reply == QMessageBox::No
|
|
215
|
+
|
|
216
|
+
file = QFile.new(filepath)
|
|
217
|
+
file.move_to_trash
|
|
218
|
+
reinitialize
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def _on_refresh_action_triggered
|
|
222
|
+
item = selected_items[0]
|
|
223
|
+
if item
|
|
224
|
+
filepath = item.data(1, Qt::DisplayRole).value
|
|
225
|
+
dataitem = @dataitems[filepath]
|
|
226
|
+
passname = dataitem.passname
|
|
227
|
+
else
|
|
228
|
+
passname = ""
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
reinitialize(selected_passname: passname)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def _on_open_action_triggered
|
|
235
|
+
url = QUrl.from_local_file(@store)
|
|
236
|
+
QDesktopServices.open_url(url)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require_relative "passlistwidget/treewidget"
|
|
2
|
+
|
|
3
|
+
module PassQt
|
|
4
|
+
class PassListWidget < RubyQt6::Bando::QWidget
|
|
5
|
+
q_object do
|
|
6
|
+
signal "store_changed(QString)"
|
|
7
|
+
signal "passfile_selected(QString,QString)"
|
|
8
|
+
signal "passfolder_selected(QString,QString)"
|
|
9
|
+
slot "_on_combobox_current_text_changed(QString)"
|
|
10
|
+
slot "_on_searchbar_text_changed(QString)"
|
|
11
|
+
slot "_on_treewidget_store_changed(QString)"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
initialize_combobox
|
|
18
|
+
initialize_searchbar
|
|
19
|
+
initialize_treewidget
|
|
20
|
+
|
|
21
|
+
mainlayout = QVBoxLayout.new(self)
|
|
22
|
+
mainlayout.add_layout(@comboboxlayout)
|
|
23
|
+
mainlayout.add_widget(@searchbar)
|
|
24
|
+
mainlayout.add_widget(@treewidget)
|
|
25
|
+
|
|
26
|
+
update_combobox
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reinitialize_stores
|
|
30
|
+
@combobox.block_signals(true).tap do |blocked|
|
|
31
|
+
@combobox.clear
|
|
32
|
+
ensure
|
|
33
|
+
@combobox.block_signals(blocked)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
update_combobox
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def initialize_combobox
|
|
42
|
+
@combobox = QComboBox.new
|
|
43
|
+
@combobox.current_text_changed.connect(self, :_on_combobox_current_text_changed)
|
|
44
|
+
|
|
45
|
+
@comboboxlayout = QHBoxLayout.new
|
|
46
|
+
@comboboxlayout.add_widget(QLabel.new("Current Store"))
|
|
47
|
+
@comboboxlayout.add_widget(@combobox)
|
|
48
|
+
@comboboxlayout.set_stretch(0, 2)
|
|
49
|
+
@comboboxlayout.set_stretch(1, 3)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize_searchbar
|
|
53
|
+
@searchbar = QLineEdit.new
|
|
54
|
+
@searchbar.set_clear_button_enabled(true)
|
|
55
|
+
@searchbar.set_placeholder_text("List passwords that match passname...")
|
|
56
|
+
@searchbar.text_changed.connect(self, :_on_searchbar_text_changed)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize_treewidget
|
|
60
|
+
@treewidget = TreeWidget.new
|
|
61
|
+
@treewidget.set_header_hidden(true)
|
|
62
|
+
@treewidget.store_changed.connect(self, :_on_treewidget_store_changed)
|
|
63
|
+
@treewidget.passfile_selected.connect(self, :passfile_selected)
|
|
64
|
+
@treewidget.passfolder_selected.connect(self, :passfolder_selected)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update_combobox
|
|
68
|
+
PassQt.settings.GET_stores.each do |store|
|
|
69
|
+
fileinfo = QFileInfo.new(store["fullpath"])
|
|
70
|
+
@combobox.add_item(fileinfo.file_name, QVariant.new(fileinfo.absolute_file_path))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def _on_combobox_current_text_changed(_text)
|
|
75
|
+
store = @combobox.current_data.value
|
|
76
|
+
@treewidget.reinitialize_store(store)
|
|
77
|
+
|
|
78
|
+
@searchbar.clear
|
|
79
|
+
QTimer.single_shot(0, @searchbar, :set_focus)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def _on_searchbar_text_changed(text)
|
|
83
|
+
@treewidget.update_passname_filter(text)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def _on_treewidget_store_changed(store)
|
|
87
|
+
@searchbar.clear
|
|
88
|
+
store_changed.emit(store)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|