vanken 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +28 -11
- data/data/coloring_rules.yml +67 -0
- data/docs/performance.md +53 -0
- data/docs/releases.md +2 -4
- data/docs/usage.md +51 -0
- data/exe/vanken-setup-permissions +6 -0
- data/lib/vanken/app/capture_controller.rb +66 -1
- data/lib/vanken/app/document.rb +102 -39
- data/lib/vanken/app/document_analysis.rb +75 -0
- data/lib/vanken/app/document_jobs.rb +52 -9
- data/lib/vanken/app/expert_info.rb +26 -0
- data/lib/vanken/app/io_graph.rb +47 -0
- data/lib/vanken/app/navigation.rb +77 -0
- data/lib/vanken/app/search_job.rb +62 -0
- data/lib/vanken/app/stats_job.rb +45 -0
- data/lib/vanken/capture/analyzer.rb +3 -1
- data/lib/vanken/capture/analyzer_process.rb +9 -6
- data/lib/vanken/capture/analyzer_worker.rb +2 -2
- data/lib/vanken/capture/filter_worker.rb +8 -2
- data/lib/vanken/capture/helper_main.rb +10 -3
- data/lib/vanken/capture/helper_options.rb +6 -2
- data/lib/vanken/capture/launcher.rb +1 -0
- data/lib/vanken/capture/permission_setup.rb +201 -0
- data/lib/vanken/config/analysis_settings.rb +58 -0
- data/lib/vanken/config/columns.rb +138 -0
- data/lib/vanken/config/messages.rb +105 -0
- data/lib/vanken/config/preferences.rb +34 -8
- data/lib/vanken/config/profiles.rb +74 -0
- data/lib/vanken/config/sessions.rb +69 -0
- data/lib/vanken/config/yaml_file.rb +36 -0
- data/lib/vanken/core/coloring.rb +86 -0
- data/lib/vanken/core/frame_store.rb +2 -2
- data/lib/vanken/core/stores.rb +12 -0
- data/lib/vanken/gateway/detail_builder.rb +1 -1
- data/lib/vanken/gateway/display_capture_filter.rb +102 -0
- data/lib/vanken/gateway/dissector.rb +31 -6
- data/lib/vanken/gateway/exporter.rb +125 -0
- data/lib/vanken/gateway/file_writer.rb +26 -2
- data/lib/vanken/gateway/interfaces.rb +59 -0
- data/lib/vanken/gateway/resolver.rb +131 -0
- data/lib/vanken/gateway/statistics.rb +83 -0
- data/lib/vanken/gateway/stream.rb +159 -0
- data/lib/vanken/ui/actions.rb +54 -28
- data/lib/vanken/ui/analysis_dialogs.rb +371 -0
- data/lib/vanken/ui/application.rb +183 -9
- data/lib/vanken/ui/coloring_operations.rb +104 -0
- data/lib/vanken/ui/column_operations.rb +115 -0
- data/lib/vanken/ui/dialogs.rb +41 -26
- data/lib/vanken/ui/file_operations.rb +17 -12
- data/lib/vanken/ui/filter_operations.rb +43 -15
- data/lib/vanken/ui/main_view.rb +74 -29
- data/lib/vanken/ui/navigation_operations.rb +86 -0
- data/lib/vanken/ui/packet_source.rb +33 -1
- data/lib/vanken/ui/selection.rb +8 -2
- data/lib/vanken/ui/settings_operations.rb +233 -0
- data/lib/vanken/version.rb +1 -1
- data/packaging/README.md +41 -1
- data/packaging/linux/org.vanken.capture.policy +14 -0
- data/packaging/linux/vanken-capture.wrapper +6 -0
- data/packaging/linux/vanken.desktop +9 -0
- data/packaging/linux/vanken.sudoers +2 -0
- data/packaging/macos/chmod-bpf +6 -0
- data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
- data/sig/generated/vanken/app/capture_controller.rbs +16 -0
- data/sig/generated/vanken/app/document.rbs +23 -1
- data/sig/generated/vanken/app/document_analysis.rbs +21 -0
- data/sig/generated/vanken/app/document_jobs.rbs +6 -0
- data/sig/generated/vanken/app/expert_info.rbs +30 -0
- data/sig/generated/vanken/app/io_graph.rbs +45 -0
- data/sig/generated/vanken/app/navigation.rbs +31 -0
- data/sig/generated/vanken/app/search_job.rbs +21 -0
- data/sig/generated/vanken/app/stats_job.rbs +18 -0
- data/sig/generated/vanken/capture/helper_options.rbs +6 -0
- data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
- data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
- data/sig/generated/vanken/config/columns.rbs +37 -0
- data/sig/generated/vanken/config/messages.rbs +14 -0
- data/sig/generated/vanken/config/preferences.rbs +1 -1
- data/sig/generated/vanken/config/profiles.rbs +29 -0
- data/sig/generated/vanken/config/sessions.rbs +17 -0
- data/sig/generated/vanken/config/yaml_file.rbs +11 -0
- data/sig/generated/vanken/core/coloring.rbs +52 -0
- data/sig/generated/vanken/core/stores.rbs +4 -0
- data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
- data/sig/generated/vanken/gateway/dissector.rbs +15 -2
- data/sig/generated/vanken/gateway/exporter.rbs +27 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
- data/sig/generated/vanken/gateway/resolver.rbs +52 -0
- data/sig/generated/vanken/gateway/statistics.rbs +104 -0
- data/sig/generated/vanken/gateway/stream.rbs +75 -0
- data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
- data/sig/generated/vanken/ui/application.rbs +23 -1
- data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
- data/sig/generated/vanken/ui/column_operations.rbs +29 -0
- data/sig/generated/vanken/ui/dialogs.rbs +5 -3
- data/sig/generated/vanken/ui/file_operations.rbs +2 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
- data/sig/generated/vanken/ui/main_view.rbs +14 -2
- data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
- data/sig/generated/vanken/ui/packet_source.rbs +4 -0
- data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
- metadata +61 -3
|
@@ -11,34 +11,52 @@ require_relative "selection"
|
|
|
11
11
|
require_relative "dialogs"
|
|
12
12
|
require_relative "actions"
|
|
13
13
|
require_relative "packet_source"
|
|
14
|
+
require_relative "navigation_operations"
|
|
15
|
+
require_relative "coloring_operations"
|
|
16
|
+
require_relative "column_operations"
|
|
17
|
+
require_relative "settings_operations"
|
|
18
|
+
require_relative "analysis_dialogs"
|
|
14
19
|
require_relative "../gateway/interfaces"
|
|
15
20
|
require_relative "../gateway/capture_filter"
|
|
21
|
+
require_relative "../gateway/resolver"
|
|
16
22
|
require_relative "../app/capture_controller"
|
|
17
23
|
require_relative "../config/log"
|
|
24
|
+
require_relative "../config/messages"
|
|
25
|
+
require_relative "../config/sessions"
|
|
18
26
|
|
|
19
27
|
module Vanken
|
|
20
28
|
module UI
|
|
21
29
|
class Application
|
|
22
30
|
include FileOperations, FilterOperations, Selection, Dialogs
|
|
31
|
+
include NavigationOperations, ColoringOperations
|
|
32
|
+
include ColumnOperations
|
|
33
|
+
include SettingsOperations
|
|
34
|
+
include AnalysisDialogs
|
|
23
35
|
attr_reader :app, :window, :preferences, :table, :tree, :hex, :filter_field, :capture,
|
|
24
|
-
:dialog_kind, :packet_source, :interface_infos
|
|
36
|
+
:dialog_kind, :packet_source, :interface_infos, :interface_traffic
|
|
25
37
|
|
|
26
|
-
def initialize(backend: nil, preferences:
|
|
27
|
-
@
|
|
38
|
+
def initialize(backend: nil, preferences: nil, debug: false, session_parent: nil)
|
|
39
|
+
@profiles = Config::Profiles.new(directory: preferences&.directory || Config::Paths.config)
|
|
40
|
+
@preferences = preferences ||= @profiles.preferences
|
|
28
41
|
@log = Config::Log.open(debug: debug, directory: File.join(preferences.directory, "logs"))
|
|
29
42
|
@app = Zaniah::App.new
|
|
30
43
|
@entity = @app.new_entity { {document: nil, number: nil, node: nil, details: [], bytes: "".b, error: nil} }
|
|
31
44
|
@selection_generation, @file_generation, @filter_generation = 0, 0, 0
|
|
32
45
|
@pool_mutex = Mutex.new
|
|
46
|
+
@resolver_mutex = Mutex.new
|
|
47
|
+
@resolution_changes = {}
|
|
33
48
|
@autoscroll = false
|
|
34
49
|
@backend = backend || (RUBY_PLATFORM.match?(/darwin/) ? :mac : RUBY_PLATFORM.match?(/mingw|mswin/) ? :windows : :linux)
|
|
35
|
-
@
|
|
50
|
+
@session_parent = session_parent || Dir.tmpdir
|
|
51
|
+
@filter_field = Zaniah::UI::TextField.new("", placeholder: t("表示フィルタ"), clearable: false).test_id("vk.filter.input")
|
|
36
52
|
@filter_field.completion(CompletionProvider.new(self)).on_change { |text, _| validate_filter(text) }
|
|
37
53
|
@packet_source = PacketSource.new(self)
|
|
38
54
|
@capture = App::CaptureController.new(preferences: preferences,
|
|
39
55
|
on_document: ->(document) { @app.executor.post { attach_document(document) unless @closing } },
|
|
40
56
|
on_update: ->(*) { @app.executor.post { changed unless @closing } })
|
|
41
57
|
@window = @app.open_window(backend: @backend, width: preferences.get("layout.width"), height: preferences.get("layout.height"), title: "Vanken")
|
|
58
|
+
coloring_rules
|
|
59
|
+
column_settings
|
|
42
60
|
unless @backend == :tui
|
|
43
61
|
@window.text_system = Zaniah::TextSystem::Renderer.new
|
|
44
62
|
@window.text_system.scale_factor = @window.scale_factor
|
|
@@ -59,6 +77,7 @@ module Vanken
|
|
|
59
77
|
change_theme(preferences.get("appearance.theme"), persist: false)
|
|
60
78
|
@main_view = MainView.new(self).test_id("vk.main")
|
|
61
79
|
@window.draw { @main_view }
|
|
80
|
+
start_interface_monitor unless @backend == :headless
|
|
62
81
|
show_error(Vanken::ConfigError.new(preferences.warning)) if preferences.warning
|
|
63
82
|
@app.executor.background do
|
|
64
83
|
infos = Gateway::Interfaces.list
|
|
@@ -71,9 +90,16 @@ module Vanken
|
|
|
71
90
|
rescue StandardError => error
|
|
72
91
|
@app.executor.post { @interface_error = error.message; changed }
|
|
73
92
|
end
|
|
93
|
+
if @backend != :headless || session_parent
|
|
94
|
+
@app.executor.background do
|
|
95
|
+
sessions = Config::Sessions.candidates(parent: @session_parent)
|
|
96
|
+
@app.executor.post { recovery_dialog(sessions) if !@closing && !sessions.empty? && !@dialog }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
74
99
|
end
|
|
75
100
|
|
|
76
101
|
def document = @app.read(@entity)[:document]
|
|
102
|
+
def t(text, **values) = Config::Messages.translate(text, language: @preferences.get("appearance.language"), **values)
|
|
77
103
|
def selected_number = @app.read(@entity)[:number]
|
|
78
104
|
def selected_node = @app.read(@entity)[:node]
|
|
79
105
|
def detail_nodes = @app.read(@entity)[:details]
|
|
@@ -83,6 +109,19 @@ module Vanken
|
|
|
83
109
|
end
|
|
84
110
|
def changed
|
|
85
111
|
return if @closing
|
|
112
|
+
apply_analysis_settings
|
|
113
|
+
if @reanalysis_in_flight && @reanalysis_task&.done? && document&.complete?
|
|
114
|
+
@reanalysis_in_flight = false
|
|
115
|
+
@packet_source.reset
|
|
116
|
+
end
|
|
117
|
+
if @reanalysis_selection && document&.complete? && !@reanalysis_in_flight
|
|
118
|
+
saved_doc, number, generation = @reanalysis_selection
|
|
119
|
+
@reanalysis_selection = nil
|
|
120
|
+
number = selected_number if generation != @selection_generation
|
|
121
|
+
select_packet(number) if document.equal?(saved_doc) && number && number <= document.count
|
|
122
|
+
end
|
|
123
|
+
begin_reanalysis if @pending_reanalysis
|
|
124
|
+
refresh_analysis
|
|
86
125
|
@packet_source.refresh do
|
|
87
126
|
next if @closing
|
|
88
127
|
error = document&.error || @capture.error
|
|
@@ -96,9 +135,15 @@ module Vanken
|
|
|
96
135
|
end
|
|
97
136
|
def attach_document(value)
|
|
98
137
|
return if @closing
|
|
138
|
+
wait_reanalysis
|
|
139
|
+
@pending_reanalysis = @reanalysis_selection = nil
|
|
140
|
+
@reanalysis_in_flight = false
|
|
141
|
+
close_analysis
|
|
142
|
+
cancel_packet_search
|
|
99
143
|
value.scanner = ->(payload) { filter_pool.submit(payload) } if value
|
|
100
144
|
@selection_generation += 1
|
|
101
145
|
update { |state| state.merge!(document: value, number: nil, node: nil, details: [], bytes: "".b) }
|
|
146
|
+
configure_document_columns if value
|
|
102
147
|
@packet_source.reset
|
|
103
148
|
@tree&.replace([])
|
|
104
149
|
@hex.bytes = "".b if @hex
|
|
@@ -108,9 +153,13 @@ module Vanken
|
|
|
108
153
|
@table.scroll_to(0) if @table && value&.displayed_count&.positive? && !@autoscroll
|
|
109
154
|
@window.title = "Vanken#{value&.path ? " — #{File.basename(value.path)}" : ""}" if @window.respond_to?(:title=)
|
|
110
155
|
end
|
|
111
|
-
def run
|
|
156
|
+
def run
|
|
157
|
+
return @app.run unless @backend == :tui
|
|
158
|
+
@window.on_tick { @app.executor.drain }
|
|
159
|
+
@window.run
|
|
160
|
+
end
|
|
112
161
|
def native? = !%i[headless tui].include?(@backend)
|
|
113
|
-
def monospace_font = @window.text_system
|
|
162
|
+
def monospace_font = @window.text_system.respond_to?(:font) ? @window.text_system.font : nil
|
|
114
163
|
def install_panes(table, tree, hex) = (@table, @tree, @hex = table, tree, hex)
|
|
115
164
|
def filter_pool
|
|
116
165
|
@pool_mutex.synchronize do
|
|
@@ -125,6 +174,11 @@ module Vanken
|
|
|
125
174
|
def close
|
|
126
175
|
return if @closed
|
|
127
176
|
@closing = true
|
|
177
|
+
stop_interface_monitor
|
|
178
|
+
reset_resolver
|
|
179
|
+
wait_reanalysis
|
|
180
|
+
close_analysis
|
|
181
|
+
cancel_packet_search
|
|
128
182
|
@packet_source.reset
|
|
129
183
|
@selection_generation += 1
|
|
130
184
|
@filter_generation += 1
|
|
@@ -147,6 +201,7 @@ module Vanken
|
|
|
147
201
|
theme = theme.with(typography: theme.typography.with(size_md: size, size_sm: size - 1, size_xs: size - 2))
|
|
148
202
|
@app.global(:theme, theme)
|
|
149
203
|
@preferences.set("appearance.theme", name) if persist
|
|
204
|
+
@packet_source.reset
|
|
150
205
|
@window.request_frame
|
|
151
206
|
end
|
|
152
207
|
def zoom(delta = nil)
|
|
@@ -187,19 +242,138 @@ module Vanken
|
|
|
187
242
|
end
|
|
188
243
|
def status_text
|
|
189
244
|
doc = document
|
|
190
|
-
return "キャプチャファイルを開くか、インタフェースを選んで開始してください" unless doc
|
|
245
|
+
return t("キャプチャファイルを開くか、インタフェースを選んで開始してください") unless doc
|
|
191
246
|
dropped = @capture.stats.fetch("dropped", @capture.stats.fetch(:dropped, 0))
|
|
192
247
|
total = doc.store.durable_count
|
|
193
|
-
"
|
|
248
|
+
t("%{label} %{count} / %{total} 表示 %{displayed} ドロップ %{dropped}%{progress}",
|
|
249
|
+
label: t(doc.loading? ? "読み込み中" : "パケット"), count: @packet_source.packet_count, total: total,
|
|
250
|
+
displayed: @packet_source.count, dropped: dropped, progress: doc.progress ? " #{(doc.progress * 100).round}%" : "")
|
|
194
251
|
end
|
|
195
252
|
def copy_text(text) = @window.write_clipboard([Zaniah::Clipboard::Item.new({"text/plain" => text})])
|
|
196
253
|
def show_error(error)
|
|
197
254
|
@log.error("#{error.class}: #{error.message}")
|
|
198
255
|
@last_error = error
|
|
256
|
+
dismiss_dialog
|
|
199
257
|
@dialog_kind = :error
|
|
200
|
-
@
|
|
258
|
+
@dialog_reopen = -> { show_error(error) }
|
|
259
|
+
@dialog = Zaniah::UI::Dialog.new(Zaniah::UI::Label.new(error.message, wrap: :word), title: t("操作を完了できません"), close_label: t("閉じる")).test_id("vk.error")
|
|
260
|
+
.on_close { dismiss_dialog }
|
|
201
261
|
@window.request_frame
|
|
202
262
|
end
|
|
263
|
+
|
|
264
|
+
def start_interface_monitor
|
|
265
|
+
return if @traffic_thread
|
|
266
|
+
@traffic_mutex, @traffic_condition = Mutex.new, ConditionVariable.new
|
|
267
|
+
@traffic_thread = Thread.new do
|
|
268
|
+
previous, history = nil, {}
|
|
269
|
+
loop do
|
|
270
|
+
break if @closing
|
|
271
|
+
sample = Gateway::Interfaces.sample_traffic(previous: previous, history: history)
|
|
272
|
+
previous, history = sample.values_at(:previous, :history)
|
|
273
|
+
post = @traffic_mutex.synchronize do
|
|
274
|
+
@traffic_sample = sample
|
|
275
|
+
!@traffic_posted && (@traffic_posted = true)
|
|
276
|
+
end
|
|
277
|
+
if post
|
|
278
|
+
@app.executor.post do
|
|
279
|
+
value = @traffic_mutex.synchronize { @traffic_posted = false; @traffic_sample }
|
|
280
|
+
next if @closing
|
|
281
|
+
@interface_traffic = value
|
|
282
|
+
@app.update(@entity) { |_state, cx| cx.notify }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
@traffic_mutex.synchronize { @traffic_condition.wait(@traffic_mutex, 1) unless @closing }
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def stop_interface_monitor
|
|
291
|
+
return unless @traffic_thread
|
|
292
|
+
@traffic_mutex.synchronize { @traffic_condition.broadcast }
|
|
293
|
+
@traffic_thread.join
|
|
294
|
+
@traffic_thread = nil
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def resolve_address(address, document:, number:)
|
|
298
|
+
resolver = @resolver_mutex.synchronize do
|
|
299
|
+
next if @closing || !@preferences.get("name_resolution.enabled")
|
|
300
|
+
@resolver ||= Gateway::Resolver.new(timeout: @preferences.get("name_resolution.timeout"), limit: @preferences.get("name_resolution.cache_size"))
|
|
301
|
+
end
|
|
302
|
+
return address unless resolver
|
|
303
|
+
resolver.request(address) do |_name|
|
|
304
|
+
post = @resolver_mutex.synchronize do
|
|
305
|
+
next false if @closing || !resolver.equal?(@resolver)
|
|
306
|
+
(@resolution_changes[document] ||= Set.new).add(number)
|
|
307
|
+
!@resolution_posted && (@resolution_posted = true)
|
|
308
|
+
end
|
|
309
|
+
next unless post
|
|
310
|
+
@app.executor.post do
|
|
311
|
+
changes = @resolver_mutex.synchronize do
|
|
312
|
+
saved, @resolution_changes = @resolution_changes, {}
|
|
313
|
+
@resolution_posted = false
|
|
314
|
+
saved
|
|
315
|
+
end
|
|
316
|
+
next if @closing
|
|
317
|
+
numbers = changes[self.document]
|
|
318
|
+
@packet_source.invalidate_rows(numbers) if numbers && !numbers.empty?
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def reset_resolver
|
|
324
|
+
old = @resolver_mutex.synchronize do
|
|
325
|
+
previous, @resolver = @resolver, nil
|
|
326
|
+
@resolution_changes = {}
|
|
327
|
+
@resolution_posted = false
|
|
328
|
+
previous
|
|
329
|
+
end
|
|
330
|
+
return unless old
|
|
331
|
+
@closing ? old.close : @app.executor.background { old.close }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def recovery_dialog(sessions)
|
|
335
|
+
content = Zaniah::Div.new.flex_col.gap(8)
|
|
336
|
+
sessions.each do |directory|
|
|
337
|
+
content.child(Zaniah::Div.new.flex_col.gap(4)
|
|
338
|
+
.child(Zaniah::UI::Label.new(directory, wrap: :word))
|
|
339
|
+
.child(Zaniah::Div.new.flex_row.gap(8)
|
|
340
|
+
.child(Zaniah::UI::Button.new(t("復旧")).on_click { recover_session(directory) })
|
|
341
|
+
.child(Zaniah::UI::Button.new(t("破棄"), variant: :danger).on_click do
|
|
342
|
+
Config::Sessions.discard(directory, parent: @session_parent)
|
|
343
|
+
remaining = sessions.reject { |value| value == directory }
|
|
344
|
+
remaining.empty? ? dismiss_dialog : recovery_dialog(remaining)
|
|
345
|
+
rescue StandardError => error
|
|
346
|
+
show_error(error)
|
|
347
|
+
end)))
|
|
348
|
+
end
|
|
349
|
+
show_dialog(:recovery, t("前回のキャプチャを復旧"), Zaniah::ScrollView.new.h(240).child(content), reopen: -> { recovery_dialog(sessions) })
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def recover_session(directory)
|
|
353
|
+
request_destructive do
|
|
354
|
+
previous = document
|
|
355
|
+
attach_document(nil)
|
|
356
|
+
dismiss_dialog
|
|
357
|
+
@file_generation += 1
|
|
358
|
+
generation = @file_generation
|
|
359
|
+
preferences = @preferences
|
|
360
|
+
@app.executor.background do
|
|
361
|
+
previous&.close
|
|
362
|
+
doc = Config::Sessions.recover(directory, parent: @session_parent, preferences: preferences,
|
|
363
|
+
on_update: ->(updated) { @app.executor.post { changed if !@closing && document.equal?(updated) } })
|
|
364
|
+
@app.executor.post do
|
|
365
|
+
if !@closing && generation == @file_generation
|
|
366
|
+
attach_document(doc)
|
|
367
|
+
changed
|
|
368
|
+
else
|
|
369
|
+
doc.close
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
rescue StandardError => error
|
|
373
|
+
@app.executor.post { show_error(error) if !@closing && generation == @file_generation }
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
203
377
|
end
|
|
204
378
|
end
|
|
205
379
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../core/coloring"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module UI
|
|
7
|
+
module ColoringOperations
|
|
8
|
+
def coloring_rules = @coloring_rules ||= load_coloring_rules
|
|
9
|
+
def reload_coloring_rules
|
|
10
|
+
@coloring_rules = load_coloring_rules
|
|
11
|
+
@packet_source.reset
|
|
12
|
+
@window.request_frame
|
|
13
|
+
end
|
|
14
|
+
def replace_coloring_rules(payload)
|
|
15
|
+
rules = Core::Coloring::RuleSet.new(payload, catalog: document&.catalog)
|
|
16
|
+
rules.save(File.join(@preferences.directory, "coloring_rules.yml"))
|
|
17
|
+
@coloring_rules = rules
|
|
18
|
+
@packet_source.reset
|
|
19
|
+
@window.request_frame
|
|
20
|
+
rules
|
|
21
|
+
end
|
|
22
|
+
def coloring_enabled? = @preferences.get("packet_list.coloring")
|
|
23
|
+
def toggle_coloring
|
|
24
|
+
@preferences.set("packet_list.coloring", !coloring_enabled?)
|
|
25
|
+
@packet_source.reset
|
|
26
|
+
@window.request_frame
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def coloring_dialog
|
|
30
|
+
@coloring_draft ||= coloring_rules.to_h
|
|
31
|
+
rules = @coloring_draft.fetch("rules")
|
|
32
|
+
@coloring_index = (@coloring_index || 0).clamp(0, [rules.size - 1, 0].max)
|
|
33
|
+
content = Zaniah::Div.new.flex_col.gap(12)
|
|
34
|
+
unless rules.empty?
|
|
35
|
+
content.child(Zaniah::UI::Select.new(rules.each_with_index.map { |rule, index| ["#{index + 1}. #{rule.fetch('name')}", index] }, label: t("ルール"), value: @coloring_index)
|
|
36
|
+
.on_change { |value, *_| @coloring_index = value; coloring_dialog }.test_id("vk.coloring.rule"))
|
|
37
|
+
rule = rules[@coloring_index]
|
|
38
|
+
content.child(Zaniah::UI::TextField.new(rule.fetch("name"), label: t("名前")).on_change { |value, *_| rule["name"] = value }.test_id("vk.coloring.name"))
|
|
39
|
+
content.child(Zaniah::UI::TextField.new(rule.fetch("filter"), label: t("表示フィルタ")).on_change { |value, *_| rule["filter"] = value }.test_id("vk.coloring.filter"))
|
|
40
|
+
content.child(Zaniah::UI::Checkbox.new(t("有効"), value: rule.fetch("enabled")).on_change { |value, *_| rule["enabled"] = value })
|
|
41
|
+
%w[light dark].each do |theme|
|
|
42
|
+
content.child(Zaniah::Div.new.flex_row.gap(12).children(%w[fg bg].map do |key|
|
|
43
|
+
label = t("%{theme} %{part}", theme: t(theme == "light" ? "ライト" : "ダーク"), part: t(key == "fg" ? "文字" : "背景"))
|
|
44
|
+
Zaniah::UI::ColorPicker.new(rule.fetch(theme).fetch(key), label: label).on_change { |value, *_| rule.fetch(theme)[key] = value }.flex_1
|
|
45
|
+
end))
|
|
46
|
+
end
|
|
47
|
+
content.child(Zaniah::Div.new.flex_row.gap(8)
|
|
48
|
+
.child(Zaniah::UI::Button.new(t("上へ移動"), variant: :secondary).disabled(@coloring_index.zero?).on_click { move_coloring_rule(-1) })
|
|
49
|
+
.child(Zaniah::UI::Button.new(t("下へ移動"), variant: :secondary).disabled(@coloring_index == rules.size - 1).on_click { move_coloring_rule(1) })
|
|
50
|
+
.child(Zaniah::UI::Button.new(t("削除"), variant: :secondary).on_click { rules.delete_at(@coloring_index); coloring_dialog }))
|
|
51
|
+
end
|
|
52
|
+
content.child(Zaniah::Div.new.flex_row.gap(8)
|
|
53
|
+
.child(Zaniah::UI::Button.new(t("追加"), variant: :secondary).on_click do
|
|
54
|
+
rules << {"name" => t("新しいルール"), "filter" => "tcp", "enabled" => true, "light" => {"fg" => "#12272E", "bg" => "#E7E6FF"}, "dark" => {"fg" => "#E7E6FF", "bg" => "#24233A"}}
|
|
55
|
+
@coloring_index = rules.size - 1
|
|
56
|
+
coloring_dialog
|
|
57
|
+
end)
|
|
58
|
+
.child(Zaniah::UI::Button.new(t("インポート"), variant: :secondary).on_click { import_coloring_rules })
|
|
59
|
+
.child(Zaniah::UI::Button.new(t("エクスポート"), variant: :secondary).on_click { export_coloring_rules }))
|
|
60
|
+
content.child(Zaniah::Div.new.flex_row.gap(8)
|
|
61
|
+
.child(Zaniah::UI::Button.new(t("適用")).test_id("vk.coloring.apply").on_click do
|
|
62
|
+
replace_coloring_rules(@coloring_draft)
|
|
63
|
+
@coloring_draft = nil
|
|
64
|
+
dismiss_dialog
|
|
65
|
+
rescue StandardError => error
|
|
66
|
+
show_error(error)
|
|
67
|
+
end)
|
|
68
|
+
.child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).on_click { @coloring_draft = nil; dismiss_dialog }))
|
|
69
|
+
viewport = Zaniah::ScrollView.new.h([(@window.content_size.height * 0.9) - 100, 120].max).child(content)
|
|
70
|
+
show_dialog(:coloring, t("色付けルール"), viewport, reopen: -> { coloring_dialog })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def import_coloring_rules
|
|
74
|
+
path_dialog(t("色付けルールの読み込み先")) do |path|
|
|
75
|
+
@coloring_draft = Core::Coloring::RuleSet.load(path, catalog: document&.catalog).to_h
|
|
76
|
+
@coloring_index = 0
|
|
77
|
+
coloring_dialog
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
def export_coloring_rules
|
|
81
|
+
path_dialog(t("色付けルールの保存先")) do |path|
|
|
82
|
+
Core::Coloring::RuleSet.new(@coloring_draft || coloring_rules.to_h, catalog: document&.catalog).save(path)
|
|
83
|
+
coloring_dialog
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
def load_coloring_rules
|
|
89
|
+
path = File.join(@preferences.directory, "coloring_rules.yml")
|
|
90
|
+
File.file?(path) ? Core::Coloring::RuleSet.load(path, catalog: document&.catalog) : Core::Coloring::RuleSet.defaults(catalog: document&.catalog)
|
|
91
|
+
rescue Vanken::ConfigError => error
|
|
92
|
+
show_error(error)
|
|
93
|
+
Core::Coloring::RuleSet.defaults(catalog: document&.catalog)
|
|
94
|
+
end
|
|
95
|
+
def move_coloring_rule(delta)
|
|
96
|
+
rules = @coloring_draft.fetch("rules")
|
|
97
|
+
target = @coloring_index + delta
|
|
98
|
+
rules[@coloring_index], rules[target] = rules[target], rules[@coloring_index]
|
|
99
|
+
@coloring_index = target
|
|
100
|
+
coloring_dialog
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config/columns"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module UI
|
|
7
|
+
module ColumnOperations
|
|
8
|
+
def column_settings
|
|
9
|
+
if !@column_settings || @columns_directory != @preferences.directory
|
|
10
|
+
@columns_directory = @preferences.directory
|
|
11
|
+
@column_settings = Config::Columns.load(directory: @columns_directory, legacy: @preferences.get("layout.columns"))
|
|
12
|
+
end
|
|
13
|
+
@column_settings
|
|
14
|
+
rescue ConfigError => error
|
|
15
|
+
show_error(error)
|
|
16
|
+
@column_settings = Config::Columns.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reload_columns
|
|
20
|
+
@column_settings = nil
|
|
21
|
+
column_settings
|
|
22
|
+
configure_document_columns
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configure_document_columns
|
|
26
|
+
document.custom_columns = column_settings.custom if document
|
|
27
|
+
@packet_source.reset
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def persist_columns_layout(items)
|
|
31
|
+
column_settings.update_layout(items)
|
|
32
|
+
column_settings.save(@preferences.directory)
|
|
33
|
+
configure_document_columns
|
|
34
|
+
rescue StandardError => error
|
|
35
|
+
show_error(error)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_field_column(field, label: field, width: 120)
|
|
39
|
+
if document && !Core::DisplayFilter::FieldResolver.new(document.catalog).resolve(field).known
|
|
40
|
+
raise Vanken::ConfigError, "unknown field: #{field}"
|
|
41
|
+
end
|
|
42
|
+
key = column_settings.add(field, label: label, width: width)
|
|
43
|
+
apply_columns
|
|
44
|
+
key
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def remove_column(key)
|
|
48
|
+
column_settings.remove(key)
|
|
49
|
+
apply_columns
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def field_menu(node)
|
|
53
|
+
menu = selection_menu(node.filter)
|
|
54
|
+
return menu unless node.field
|
|
55
|
+
@app.actions.register(:context_apply_column, title: column_text("列として適用")) { add_field_column(node.field) }
|
|
56
|
+
added = Zaniah::Menu.build { item(:context_apply_column) }
|
|
57
|
+
Zaniah::Menu.new(menu.items + added.items)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def columns_dialog
|
|
61
|
+
rows = Zaniah::Div.new.flex_col.gap(6)
|
|
62
|
+
@table.columns.each_with_index do |column, index|
|
|
63
|
+
key = column[:key]
|
|
64
|
+
width = Zaniah::UI::NumberInput.new(column[:width], min: 40, max: 4096, step: 8, label: column_text("幅"))
|
|
65
|
+
.w(100).test_id("vk.columns.width.#{key}")
|
|
66
|
+
width.on_change do |*_|
|
|
67
|
+
amount = width.number
|
|
68
|
+
if amount && amount.finite? && amount.between?(40, 4096)
|
|
69
|
+
width.status(:none)
|
|
70
|
+
@table.resize_column(key, amount)
|
|
71
|
+
else
|
|
72
|
+
width.status(:error, message: column_text("幅は40から4096の範囲で指定してください"))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
row = Zaniah::Div.new.flex_row.items_center.gap(6)
|
|
76
|
+
.child(Zaniah::UI::Checkbox.new(column[:label], value: column[:visible]).flex_1
|
|
77
|
+
.on_change { |value, *_| @table.column_visible(key, value) })
|
|
78
|
+
.child(width)
|
|
79
|
+
.child(Zaniah::UI::Button.new(column_text("上へ移動"), size: :sm).disabled(index.zero?).on_click { @table.move_column(key, index - 1); columns_dialog })
|
|
80
|
+
.child(Zaniah::UI::Button.new(column_text("下へ移動"), size: :sm).disabled(index == @table.columns.size - 1).on_click { @table.move_column(key, index + 1); columns_dialog })
|
|
81
|
+
row.child(Zaniah::UI::Button.new(column_text("削除"), size: :sm).on_click { remove_column(key); columns_dialog }) if key.to_s.start_with?("field:")
|
|
82
|
+
rows.child(row)
|
|
83
|
+
end
|
|
84
|
+
field = Zaniah::UI::TextField.new("", label: column_text("フィールド名")).test_id("vk.columns.field")
|
|
85
|
+
label = Zaniah::UI::TextField.new("", label: column_text("列名 (空欄: フィールド名)")).test_id("vk.columns.label")
|
|
86
|
+
content = Zaniah::Div.new.flex_col.gap(8)
|
|
87
|
+
.child(Zaniah::ScrollView.new.h(280).child(rows)).child(field).child(label)
|
|
88
|
+
.child(Zaniah::UI::Button.new(column_text("追加")).test_id("vk.columns.add").on_click do
|
|
89
|
+
name = field.value.strip
|
|
90
|
+
add_field_column(name, label: label.value.empty? ? name : label.value)
|
|
91
|
+
columns_dialog
|
|
92
|
+
rescue StandardError => error
|
|
93
|
+
show_error(error)
|
|
94
|
+
end)
|
|
95
|
+
show_dialog(:columns, column_text("表示する列"), content, reopen: -> { columns_dialog })
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def column_text(text) = respond_to?(:t) ? t(text) : text
|
|
101
|
+
|
|
102
|
+
def apply_columns
|
|
103
|
+
column_settings.save(@preferences.directory)
|
|
104
|
+
configure_document_columns
|
|
105
|
+
selected = selected_number
|
|
106
|
+
rebuild_view
|
|
107
|
+
index = document&.displayed_index(selected)
|
|
108
|
+
if index
|
|
109
|
+
@table.select(index)
|
|
110
|
+
@table.scroll_to(index, align: :nearest)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/vanken/ui/dialogs.rb
CHANGED
|
@@ -5,18 +5,23 @@ module Vanken
|
|
|
5
5
|
module Dialogs
|
|
6
6
|
def dialog = @dialog
|
|
7
7
|
def dismiss_dialog
|
|
8
|
+
close_analysis if respond_to?(:close_analysis)
|
|
8
9
|
@dialog&.close
|
|
9
|
-
@dialog = @dialog_kind = @pending_destructive = nil
|
|
10
|
+
@dialog = @dialog_kind = @pending_destructive = @dialog_reopen = nil
|
|
10
11
|
@window.request_frame
|
|
11
12
|
end
|
|
12
|
-
def
|
|
13
|
+
def reopen_dialog
|
|
14
|
+
@dialog_reopen&.call
|
|
15
|
+
end
|
|
16
|
+
def show_dialog(kind, title, content, reopen: nil)
|
|
13
17
|
dismiss_dialog
|
|
14
18
|
@dialog_kind = kind
|
|
15
|
-
@
|
|
19
|
+
@dialog_reopen = reopen
|
|
20
|
+
@dialog = Zaniah::UI::Dialog.new(content, title: title, close_label: t("閉じる")).on_close { dismiss_dialog }.test_id("vk.#{kind}")
|
|
16
21
|
@window.request_frame
|
|
17
22
|
end
|
|
18
|
-
def path_dialog(title, &operation)
|
|
19
|
-
field = Zaniah::UI::TextField.new(
|
|
23
|
+
def path_dialog(title, value: "", &operation)
|
|
24
|
+
field = Zaniah::UI::TextField.new(value, label: t(title)).test_id("vk.path.input")
|
|
20
25
|
content = Zaniah::Div.new.flex_col.gap(12).child(field)
|
|
21
26
|
.child(Zaniah::Div.new.flex_row.gap(8)
|
|
22
27
|
.child(Zaniah::UI::Button.new("OK").on_click do
|
|
@@ -25,32 +30,42 @@ module Vanken
|
|
|
25
30
|
rescue StandardError => error
|
|
26
31
|
show_error(error)
|
|
27
32
|
end)
|
|
28
|
-
.child(Zaniah::UI::Button.new("キャンセル", variant: :secondary).on_click { dismiss_dialog }))
|
|
29
|
-
show_dialog(:path, title, content)
|
|
33
|
+
.child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).on_click { dismiss_dialog }))
|
|
34
|
+
show_dialog(:path, t(title), content, reopen: -> { path_dialog(title, value: field.value, &operation) })
|
|
30
35
|
end
|
|
31
|
-
def capture_options(interface: nil)
|
|
36
|
+
def capture_options(interface: nil, values: {})
|
|
32
37
|
fields = {}
|
|
33
38
|
infos = @interface_infos || []
|
|
34
39
|
choices = infos.map { |item| ["#{item.fetch(:name)} #{item[:description]}", item.fetch(:name)] }
|
|
35
40
|
selected = choices.any? { |choice| choice.last == interface } ? interface : choices.first&.last
|
|
36
|
-
selector = Zaniah::UI::Select.new(choices, label: "インタフェース", value: selected)
|
|
41
|
+
selector = Zaniah::UI::Select.new(choices, label: t("インタフェース"), value: selected)
|
|
37
42
|
selector.on_change { |value, *_| selected = value }
|
|
38
43
|
content = Zaniah::Div.new.flex_col.gap(10).child(selector)
|
|
39
|
-
[["filter", "キャプチャフィルタ (BPF)", ""], ["snaplen", "Snaplen", @preferences.get("capture.snaplen").to_s],
|
|
40
|
-
["buffer_size", "バッファサイズ (bytes)", @preferences.get("capture.buffer_size").to_s]].each do |key, label, value|
|
|
41
|
-
fields[key] = Zaniah::UI::TextField.new(value, label: label)
|
|
44
|
+
[["filter", t("キャプチャフィルタ (BPF)"), ""], ["snaplen", "Snaplen", @preferences.get("capture.snaplen").to_s],
|
|
45
|
+
["buffer_size", t("バッファサイズ (bytes)"), @preferences.get("capture.buffer_size").to_s]].each do |key, label, value|
|
|
46
|
+
fields[key] = Zaniah::UI::TextField.new(values.fetch(key, value), label: label)
|
|
42
47
|
content.child(fields[key])
|
|
43
48
|
end
|
|
44
|
-
|
|
49
|
+
advanced = Zaniah::Div.new.flex_col.gap(6)
|
|
50
|
+
{"stop_count" => t("停止件数 (0: 無制限)"), "stop_duration" => t("停止までの秒数 (0: 無制限)"), "stop_bytes" => t("停止サイズ (bytes、0: 無制限)"),
|
|
51
|
+
"ring_path" => t("リング保存先 (空欄: 保存しない)"), "ring_max_bytes" => t("リングファイル上限 (bytes)"), "ring_interval" => t("リング切替間隔 (秒)"), "ring_file_count" => t("リングファイル数")}.each do |key, label|
|
|
52
|
+
fields[key] = Zaniah::UI::TextField.new(values.fetch(key, @preferences.get("capture.#{key}").to_s), label: label).test_id("vk.capture.#{key}")
|
|
53
|
+
advanced.child(fields[key])
|
|
54
|
+
end
|
|
55
|
+
content.child(Zaniah::ScrollView.new.h(160).child(advanced))
|
|
56
|
+
promiscuous = Zaniah::UI::Checkbox.new(t("プロミスキャスモード"), value: values.fetch("promiscuous", @preferences.get("capture.promiscuous")))
|
|
45
57
|
content.child(promiscuous)
|
|
46
|
-
direction = @preferences.get("capture.direction")
|
|
47
|
-
content.child(Zaniah::UI::Select.new([["送受信", "inout"], ["受信", "in"], ["送信", "out"]], label: "方向", value: direction).on_change { |value, *_| direction = value })
|
|
48
|
-
status = Zaniah::UI::Label.new(infos.empty? ? (@interface_error || "インタフェースを取得中です。") : "GUI は通常ユーザーで動作し、取得ヘルパーに必要な権限を渡します。", wrap: :word, tone: :muted)
|
|
58
|
+
direction = values.fetch("direction", @preferences.get("capture.direction"))
|
|
59
|
+
content.child(Zaniah::UI::Select.new([[t("送受信"), "inout"], [t("受信"), "in"], [t("送信"), "out"]], label: t("方向"), value: direction).on_change { |value, *_| direction = value })
|
|
60
|
+
status = Zaniah::UI::Label.new(infos.empty? ? (@interface_error || t("インタフェースを取得中です。")) : t("GUI は通常ユーザーで動作し、取得ヘルパーに必要な権限を渡します。"), wrap: :word, tone: :muted)
|
|
49
61
|
content.child(status)
|
|
50
|
-
content.child(Zaniah::UI::Button.new("開始").test_id("vk.capture.start").disabled(infos.empty?).on_click do
|
|
62
|
+
content.child(Zaniah::UI::Button.new(t("開始")).test_id("vk.capture.start").disabled(infos.empty?).on_click do
|
|
51
63
|
options = {"interface" => selected, "snaplen" => Integer(fields.fetch("snaplen").value, 10),
|
|
52
64
|
"buffer_size" => Integer(fields.fetch("buffer_size").value, 10), "filter" => fields.fetch("filter").value,
|
|
53
65
|
"promiscuous" => promiscuous.value, "direction" => direction, "backend" => @preferences.get("capture.backend")}
|
|
66
|
+
%w[stop_count stop_bytes ring_max_bytes ring_file_count].each { |key| options[key] = Integer(fields.fetch(key).value, 10) }
|
|
67
|
+
%w[stop_duration ring_interval].each { |key| options[key] = Float(fields.fetch(key).value) }
|
|
68
|
+
options["ring_path"] = fields.fetch("ring_path").value
|
|
54
69
|
# Compilation and privileged helper startup both happen away from the UI thread.
|
|
55
70
|
@app.executor.background do
|
|
56
71
|
metadata = infos.find { |item| item[:name] == selected }
|
|
@@ -62,27 +77,27 @@ module Vanken
|
|
|
62
77
|
rescue ArgumentError => error
|
|
63
78
|
show_error(error)
|
|
64
79
|
end)
|
|
65
|
-
show_dialog(:capture_options, "キャプチャオプション", content)
|
|
80
|
+
show_dialog(:capture_options, t("キャプチャオプション"), content, reopen: -> { capture_options(interface: selected, values: fields.transform_values(&:value).merge("promiscuous" => promiscuous.value, "direction" => direction)) })
|
|
66
81
|
end
|
|
67
82
|
def preferences_dialog
|
|
68
|
-
theme = Zaniah::UI::Select.new(
|
|
83
|
+
theme = Zaniah::UI::Select.new({"システム設定" => "system", "ダーク" => "dark", "ライト" => "light", "ハイコントラスト" => "high_contrast"}.map { |label, value| [t(label), value] }, label: t("テーマ"), value: @preferences.get("appearance.theme"))
|
|
69
84
|
.on_change { |value, *_| change_theme(value) }
|
|
70
|
-
time = Zaniah::UI::Select.new(
|
|
85
|
+
time = Zaniah::UI::Select.new({"相対時刻" => "relative", "絶対時刻" => "absolute", "前パケットとの差" => "delta", "前表示パケットとの差" => "delta_displayed", "エポック時刻" => "epoch"}.map { |label, value| [t(label), value] }, label: t("時刻表示"), value: @preferences.get("packet_list.time_format"))
|
|
71
86
|
.on_change { |value, *_| time_format(value) }
|
|
72
87
|
content = Zaniah::Div.new.flex_col.gap(12).child(theme).child(time)
|
|
73
|
-
.child(Zaniah::Div.new.flex_row.gap(8).child(Zaniah::UI::Button.new("文字を小さく").on_click { zoom(-1) })
|
|
74
|
-
.child(Zaniah::UI::Button.new("文字を大きく").on_click { zoom(1) }))
|
|
75
|
-
show_dialog(:preferences, "設定", content)
|
|
88
|
+
.child(Zaniah::Div.new.flex_row.gap(8).child(Zaniah::UI::Button.new(t("文字を小さく")).on_click { zoom(-1) })
|
|
89
|
+
.child(Zaniah::UI::Button.new(t("文字を大きく")).on_click { zoom(1) }))
|
|
90
|
+
show_dialog(:preferences, t("設定"), content, reopen: -> { preferences_dialog })
|
|
76
91
|
end
|
|
77
92
|
def columns_dialog
|
|
78
93
|
content = Zaniah::Div.new.flex_col.gap(8)
|
|
79
94
|
@table.columns.each_with_index do |column, index|
|
|
80
95
|
content.child(Zaniah::Div.new.flex_row.gap(8)
|
|
81
96
|
.child(Zaniah::UI::Checkbox.new(column[:label], value: column[:visible]).on_change { |value, *_| @table.column_visible(column[:key], value) }.flex_1)
|
|
82
|
-
.child(Zaniah::UI::Button.new("
|
|
83
|
-
.child(Zaniah::UI::Button.new("
|
|
97
|
+
.child(Zaniah::UI::Button.new(t("上へ移動"), size: :sm).disabled(index.zero?).on_click { @table.move_column(column[:key], index - 1); columns_dialog })
|
|
98
|
+
.child(Zaniah::UI::Button.new(t("下へ移動"), size: :sm).disabled(index == @table.columns.size - 1).on_click { @table.move_column(column[:key], index + 1); columns_dialog }))
|
|
84
99
|
end
|
|
85
|
-
show_dialog(:columns, "表示する列", content)
|
|
100
|
+
show_dialog(:columns, t("表示する列"), content, reopen: -> { columns_dialog })
|
|
86
101
|
end
|
|
87
102
|
end
|
|
88
103
|
end
|