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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +28 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +53 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/exe/vanken-setup-permissions +6 -0
  9. data/lib/vanken/app/capture_controller.rb +66 -1
  10. data/lib/vanken/app/document.rb +102 -39
  11. data/lib/vanken/app/document_analysis.rb +75 -0
  12. data/lib/vanken/app/document_jobs.rb +52 -9
  13. data/lib/vanken/app/expert_info.rb +26 -0
  14. data/lib/vanken/app/io_graph.rb +47 -0
  15. data/lib/vanken/app/navigation.rb +77 -0
  16. data/lib/vanken/app/search_job.rb +62 -0
  17. data/lib/vanken/app/stats_job.rb +45 -0
  18. data/lib/vanken/capture/analyzer.rb +3 -1
  19. data/lib/vanken/capture/analyzer_process.rb +9 -6
  20. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  21. data/lib/vanken/capture/filter_worker.rb +8 -2
  22. data/lib/vanken/capture/helper_main.rb +10 -3
  23. data/lib/vanken/capture/helper_options.rb +6 -2
  24. data/lib/vanken/capture/launcher.rb +1 -0
  25. data/lib/vanken/capture/permission_setup.rb +201 -0
  26. data/lib/vanken/config/analysis_settings.rb +58 -0
  27. data/lib/vanken/config/columns.rb +138 -0
  28. data/lib/vanken/config/messages.rb +105 -0
  29. data/lib/vanken/config/preferences.rb +34 -8
  30. data/lib/vanken/config/profiles.rb +74 -0
  31. data/lib/vanken/config/sessions.rb +69 -0
  32. data/lib/vanken/config/yaml_file.rb +36 -0
  33. data/lib/vanken/core/coloring.rb +86 -0
  34. data/lib/vanken/core/frame_store.rb +2 -2
  35. data/lib/vanken/core/stores.rb +12 -0
  36. data/lib/vanken/gateway/detail_builder.rb +1 -1
  37. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  38. data/lib/vanken/gateway/dissector.rb +31 -6
  39. data/lib/vanken/gateway/exporter.rb +125 -0
  40. data/lib/vanken/gateway/file_writer.rb +26 -2
  41. data/lib/vanken/gateway/interfaces.rb +59 -0
  42. data/lib/vanken/gateway/resolver.rb +131 -0
  43. data/lib/vanken/gateway/statistics.rb +83 -0
  44. data/lib/vanken/gateway/stream.rb +159 -0
  45. data/lib/vanken/ui/actions.rb +54 -28
  46. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  47. data/lib/vanken/ui/application.rb +183 -9
  48. data/lib/vanken/ui/coloring_operations.rb +104 -0
  49. data/lib/vanken/ui/column_operations.rb +115 -0
  50. data/lib/vanken/ui/dialogs.rb +41 -26
  51. data/lib/vanken/ui/file_operations.rb +17 -12
  52. data/lib/vanken/ui/filter_operations.rb +43 -15
  53. data/lib/vanken/ui/main_view.rb +74 -29
  54. data/lib/vanken/ui/navigation_operations.rb +86 -0
  55. data/lib/vanken/ui/packet_source.rb +33 -1
  56. data/lib/vanken/ui/selection.rb +8 -2
  57. data/lib/vanken/ui/settings_operations.rb +233 -0
  58. data/lib/vanken/version.rb +1 -1
  59. data/packaging/README.md +41 -1
  60. data/packaging/linux/org.vanken.capture.policy +14 -0
  61. data/packaging/linux/vanken-capture.wrapper +6 -0
  62. data/packaging/linux/vanken.desktop +9 -0
  63. data/packaging/linux/vanken.sudoers +2 -0
  64. data/packaging/macos/chmod-bpf +6 -0
  65. data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
  66. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  67. data/sig/generated/vanken/app/document.rbs +23 -1
  68. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  69. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  70. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  71. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  72. data/sig/generated/vanken/app/navigation.rbs +31 -0
  73. data/sig/generated/vanken/app/search_job.rbs +21 -0
  74. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  75. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  76. data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
  77. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  78. data/sig/generated/vanken/config/columns.rbs +37 -0
  79. data/sig/generated/vanken/config/messages.rbs +14 -0
  80. data/sig/generated/vanken/config/preferences.rbs +1 -1
  81. data/sig/generated/vanken/config/profiles.rbs +29 -0
  82. data/sig/generated/vanken/config/sessions.rbs +17 -0
  83. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  84. data/sig/generated/vanken/core/coloring.rbs +52 -0
  85. data/sig/generated/vanken/core/stores.rbs +4 -0
  86. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  87. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  88. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  89. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  90. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  91. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  92. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  93. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  94. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  95. data/sig/generated/vanken/ui/application.rbs +23 -1
  96. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  97. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  98. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  99. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  100. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  101. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  102. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  103. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  104. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  105. metadata +61 -3
@@ -5,11 +5,13 @@ module Vanken
5
5
  module FileOperations
6
6
  CAPTURE_FILTERS = [{label: "Capture files", patterns: %w[*.pcapng *.pcap]}].freeze
7
7
 
8
+ def capture_filters = CAPTURE_FILTERS.map { |filter| filter.merge(label: t("キャプチャファイル")) }
9
+
8
10
  def open_dialog
9
11
  if @backend == :tui
10
- path_dialog("ファイルを開く") { |path| open_file(path) }
12
+ path_dialog(t("ファイルを開く")) { |path| open_file(path) }
11
13
  else
12
- paths = @window.prompt_for_paths(filters: CAPTURE_FILTERS)
14
+ paths = @window.prompt_for_paths(filters: capture_filters)
13
15
  open_file(paths.first) if paths && !paths.empty?
14
16
  end
15
17
  rescue StandardError => error
@@ -46,8 +48,8 @@ module Vanken
46
48
  def save_dialog(after: nil)
47
49
  return unless document
48
50
  path = @backend == :tui ? nil : @window.prompt_for_paths(save: true,
49
- default_name: "capture.pcapng", filters: CAPTURE_FILTERS)&.first
50
- return path_dialog("名前を付けて保存") { |value| save_file(value, after: after) } if @backend == :tui
51
+ default_name: "capture.pcapng", filters: capture_filters)&.first
52
+ return path_dialog(t("名前を付けて保存")) { |value| save_file(value, after: after) } if @backend == :tui
51
53
  save_file(path, after: after) if path
52
54
  rescue StandardError => error
53
55
  show_error(error)
@@ -77,28 +79,31 @@ module Vanken
77
79
  def reload_file = document&.source == :file && document.path && open_file(document.path)
78
80
 
79
81
  def request_destructive(&operation)
82
+ dismiss_dialog
80
83
  if @capture.running?
84
+ @dialog_reopen = -> { request_destructive(&operation) }
81
85
  @dialog_kind = :capturing
82
- content = Zaniah::Div.new.gap(12).child(Zaniah::UI::Label.new("キャプチャを停止してから操作してください。"))
83
- .child(Zaniah::UI::Button.new("停止").on_click do
86
+ content = Zaniah::Div.new.gap(12).child(Zaniah::UI::Label.new(t("キャプチャを停止してから操作してください。")))
87
+ .child(Zaniah::UI::Button.new(t("停止")).on_click do
84
88
  @capture.stop
85
89
  dismiss_dialog
86
90
  @app.executor.background { @capture.wait; @app.executor.post { request_destructive(&operation) unless @closing } }
87
91
  end)
88
- @dialog = Zaniah::UI::Dialog.new(content, title: "キャプチャ中").test_id("vk.capture.confirm")
92
+ @dialog = Zaniah::UI::Dialog.new(content, title: t("キャプチャ中"), close_label: t("閉じる")).on_close { dismiss_dialog }.test_id("vk.capture.confirm")
89
93
  @window.request_frame
90
94
  return false
91
95
  end
92
96
  return operation.call unless document&.dirty?
97
+ @dialog_reopen = -> { request_destructive(&operation) }
93
98
  @pending_destructive = operation
94
99
  @dialog_kind = :unsaved
95
100
  content = Zaniah::Div.new.flex_col.gap(12)
96
- .child(Zaniah::UI::Label.new("このキャプチャはまだ保存されていません。"))
101
+ .child(Zaniah::UI::Label.new(t("このキャプチャはまだ保存されていません。")))
97
102
  .child(Zaniah::Div.new.flex_row.gap(8)
98
- .child(Zaniah::UI::Button.new("保存").test_id("vk.unsaved.save").on_click { save_dialog(after: @pending_destructive) })
99
- .child(Zaniah::UI::Button.new("破棄", variant: :danger).test_id("vk.unsaved.discard").on_click { discard_changes })
100
- .child(Zaniah::UI::Button.new("キャンセル", variant: :secondary).test_id("vk.unsaved.cancel").on_click { dismiss_dialog }))
101
- @dialog = Zaniah::UI::Dialog.new(content, title: "キャプチャを保存しますか?", close_on_scrim: false).test_id("vk.unsaved")
103
+ .child(Zaniah::UI::Button.new(t("保存")).test_id("vk.unsaved.save").on_click { save_dialog(after: @pending_destructive) })
104
+ .child(Zaniah::UI::Button.new(t("破棄"), variant: :danger).test_id("vk.unsaved.discard").on_click { discard_changes })
105
+ .child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).test_id("vk.unsaved.cancel").on_click { dismiss_dialog }))
106
+ @dialog = Zaniah::UI::Dialog.new(content, title: t("キャプチャを保存しますか?"), close_on_scrim: false, close_label: t("閉じる")).on_close { dismiss_dialog }.test_id("vk.unsaved")
102
107
  @window.request_frame
103
108
  false
104
109
  end
@@ -42,7 +42,7 @@ module Vanken
42
42
  message = program.warnings.join("\n")
43
43
  rescue Core::DisplayFilter::SyntaxError => error
44
44
  status = :error
45
- message = "#{error.message} (位置 #{error.position + 1})"
45
+ message = t("%{message} (位置 %{position})", message: error.message, position: error.position + 1)
46
46
  end
47
47
  @app.executor.post do
48
48
  @filter_field.status(status, message: message) if generation == @filter_generation && !@closing
@@ -51,6 +51,7 @@ module Vanken
51
51
  end
52
52
 
53
53
  def apply_filter
54
+ cancel_packet_search
54
55
  doc = document
55
56
  return unless doc
56
57
  expression = @filter_field.value
@@ -60,7 +61,7 @@ module Vanken
60
61
  @packet_source.reset
61
62
  @window.request_frame
62
63
  rescue Core::DisplayFilter::SyntaxError => error
63
- @filter_field.status(:error, message: "#{error.message} (位置 #{error.position + 1})")
64
+ @filter_field.status(:error, message: t("%{message} (位置 %{position})", message: error.message, position: error.position + 1))
64
65
  end
65
66
  def initial_filter(text)
66
67
  set_filter(text)
@@ -85,25 +86,51 @@ module Vanken
85
86
  set_filter(expression)
86
87
  apply_filter unless prepare
87
88
  end
88
- def selection_menu(expression)
89
- return Zaniah::Menu.new([]) unless expression
90
- %i[filter prepare].each do |kind|
91
- %i[selected not and and_not or or_not].each do |mode|
92
- name = :"#{kind}_#{mode}"
93
- @app.actions.register(:"context_#{name}", title: @app.actions.command(name).title) do
94
- filter_from_expression(expression, mode, prepare: kind == :prepare)
89
+ def selection_menu(expression, number: selected_number)
90
+ filters = Zaniah::Menu.new([])
91
+ if expression
92
+ %i[filter prepare].each do |kind|
93
+ %i[selected not and and_not or or_not].each do |mode|
94
+ name = :"#{kind}_#{mode}"
95
+ @app.actions.register(:"context_#{name}", title: @app.actions.command(name).title) do
96
+ filter_from_expression(expression, mode, prepare: kind == :prepare)
97
+ end
95
98
  end
96
99
  end
100
+ @app.actions.register(:context_copy_filter, title: t("フィルタとしてコピー")) { copy_text(expression) }
101
+ ui = self
102
+ filters = Zaniah::Menu.build do
103
+ submenu(ui.t("フィルタとして適用")) { %i[selected not and and_not or or_not].each { |mode| item(:"context_filter_#{mode}") } }
104
+ submenu(ui.t("フィルタを準備")) { %i[selected not and and_not or or_not].each { |mode| item(:"context_prepare_#{mode}") } }
105
+ item(:context_copy_filter)
106
+ end
107
+ end
108
+ Zaniah::Menu.new(filters.items + packet_context_menu(number).items)
109
+ end
110
+
111
+ def packet_context_menu(number)
112
+ doc = document
113
+ return Zaniah::Menu.new([]) unless doc && number.is_a?(Integer) && number.between?(1, doc.count)
114
+ enabled = ->(*) { document.equal?(doc) }
115
+ {
116
+ context_mark_packet: [:marked, :toggle_mark, "マーク", "マークを解除"],
117
+ context_ignore_packet: [:ignored, :toggle_ignore, "無視", "無視を解除"],
118
+ context_time_reference: [:time_references, :toggle_time_reference, "時刻基準にする", "時刻基準を解除"]
119
+ }.each do |action, (state, operation, label, clear_label)|
120
+ @app.actions.register(action, title: t(doc.public_send(state).include?(number) ? clear_label : label), enabled: enabled) do
121
+ update_packet_state(operation, number: number) if enabled.call
122
+ end
123
+ end
124
+ stream_enabled = ->(*) { enabled.call && doc.annotations[number][:tcp_stream] >= 0 }
125
+ @app.actions.register(:context_follow_tcp_stream, title: t("TCP ストリームを追跡"), enabled: stream_enabled) do
126
+ follow_tcp_stream(doc.annotations[number][:tcp_stream]) if stream_enabled.call
97
127
  end
98
- @app.actions.register(:context_copy_filter, title: "フィルタとしてコピー") { copy_text(expression) }
99
128
  Zaniah::Menu.build do
100
- submenu("フィルタとして適用") { %i[selected not and and_not or or_not].each { |mode| item(:"context_filter_#{mode}") } }
101
- submenu("フィルタを準備") { %i[selected not and and_not or or_not].each { |mode| item(:"context_prepare_#{mode}") } }
102
- item(:context_copy_filter)
129
+ %i[context_mark_packet context_ignore_packet context_time_reference context_follow_tcp_stream].each { |action| item(action) }
103
130
  end
104
131
  end
105
132
  def bookmark_filter
106
- path_dialog("フィルタの名前") do |name|
133
+ path_dialog(t("フィルタの名前")) do |name|
107
134
  @preferences.bookmark(name, @filter_field.value) unless name.empty?
108
135
  dismiss_dialog
109
136
  end
@@ -113,7 +140,8 @@ module Vanken
113
140
  content = Zaniah::Div.new.flex_col.gap(4).children(items.first(60).map do |label, expression|
114
141
  Zaniah::UI::Button.new(label, variant: :ghost).on_click { set_filter(expression); apply_filter; dismiss_dialog }
115
142
  end)
116
- show_dialog(:history, "表示フィルタ", content)
143
+ content.child(Zaniah::UI::Label.new(t("フィルタ履歴はまだありません。"), tone: :muted)) if items.empty?
144
+ show_dialog(:history, t("表示フィルタ"), content, reopen: -> { filter_history })
117
145
  end
118
146
  end
119
147
  end
@@ -1,34 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config/columns"
4
+
3
5
  module Vanken
4
6
  module UI
5
7
  class MainView < Zaniah::UI::Component
6
- COLUMNS = [[:no, "No.", 64], [:time, "Time", 128], [:source, "Source", 164],
7
- [:destination, "Destination", 164], [:protocol, "Protocol", 90], [:length, "Length", 76], [:info, "Info", 560]].freeze
8
+ COLUMNS = Config::Columns::BUILTINS
9
+
10
+ class ExpertBadge < Zaniah::UI::Badge
11
+ def initialize(label, variant:, &activate)
12
+ super(label, variant: variant)
13
+ @label, @activate = label, activate
14
+ end
15
+ def build(cx)
16
+ super.cursor(:pointer).on_click { @activate.call }
17
+ .focusable(context: {in_button: true}) { |action| action == :activate && (@activate.call; true) }
18
+ end
19
+ def accessibility_node(_cx) = node(:button, label: @label, actions: [:press])
20
+ def accessibility_action(_node, action) = action == :press && (@activate.call; true)
21
+ end
8
22
 
9
23
  def initialize(ui)
10
24
  super()
11
25
  @ui = ui
12
- columns = COLUMNS.map { |key, label, width| {key: key, label: label, width: width, align: %i[no time length].include?(key) ? :end : :start} }
13
- saved = ui.preferences.get("layout.columns")
14
- unless saved.empty?
15
- columns = columns.sort_by { |column| saved.index { |item| item["key"] == column[:key].to_s } || columns.size }.map do |column|
16
- value = saved.find { |item| item["key"] == column[:key].to_s }
17
- value ? column.merge(width: value["width"], visible: value["visible"]) : column
18
- end
26
+ @filter_container = Zaniah::Div.new.flex_1.focusable(context: {in_display_filter: true}).child(ui.filter_field)
27
+ @menu_bar = Zaniah::UI::MenuBar.from(ui.app.menu_bar)
28
+ settings = ui.respond_to?(:column_settings) ? ui.column_settings : Config::Columns.load(directory: ui.preferences.directory, legacy: ui.preferences.get("layout.columns"))
29
+ columns = settings.table_columns.map do |column|
30
+ default = COLUMNS.find { |key, *_| key == column[:key] }
31
+ default && column[:label] == default[1] ? column.merge(label: text(column[:label])) : column
19
32
  end
20
- table = Zaniah::UI::VirtualTable.new(ui.packet_source, columns: columns, follow_tail: ui.autoscroll?)
33
+ ui.document.custom_columns = settings.custom if ui.document
34
+ table = Zaniah::UI::VirtualTable.new(ui.packet_source, columns: columns, follow_tail: ui.autoscroll?, label: text("パケット一覧"))
21
35
  .test_id("vk.packet_list").on_select { |index, *_| number = ui.packet_source.row_id(index); ui.select_packet(number) if number.is_a?(Integer) }
22
36
  .on_sort { |key, direction, _| ui.document&.sort(key, direction) }
23
- .on_columns_change { |items, _| ui.preferences.set("layout.columns", items.map { |item| {"key" => item[:key].to_s, "width" => item[:width], "visible" => item[:visible]} }) }
24
- tree = Zaniah::UI::TreeView.new([]).test_id("vk.packet_details").on_select { |node, *_| ui.select_detail(node) }
25
- .on_context_menu { |node, _| ui.selection_menu(node.filter) }
37
+ .on_columns_change do |items, _|
38
+ if ui.respond_to?(:persist_columns_layout)
39
+ ui.persist_columns_layout(items)
40
+ else
41
+ settings.update_layout(items)
42
+ settings.save(ui.preferences.directory)
43
+ end
44
+ end
45
+ tree = Zaniah::UI::TreeView.new([], label: text("パケット詳細")).test_id("vk.packet_details").on_select { |node, *_| ui.select_detail(node) }
46
+ .on_context_menu { |node, _| ui.respond_to?(:field_menu) ? ui.field_menu(node) : ui.selection_menu(node.filter) }
26
47
  table.on_row_context_menu do |index, _|
27
48
  number = ui.packet_source.row_id(index)
28
49
  table.select(index) if number.is_a?(Integer)
29
- ui.selection_menu(number.is_a?(Integer) ? "frame.number == #{number}" : nil)
50
+ ui.selection_menu(number.is_a?(Integer) ? "frame.number == #{number}" : nil, number: number.is_a?(Integer) ? number : nil)
30
51
  end
31
- hex = Zaniah::UI::HexView.new("".b).test_id("vk.packet_bytes").on_select { |range, _| ui.select_bytes(range) }
52
+ hex = Zaniah::UI::HexView.new("".b).accessibility_label(text("パケットバイト")).test_id("vk.packet_bytes").on_select { |range, _| ui.select_bytes(range) }
32
53
  .on_copy { |range, _| ui.copy_text(ui.selected_bytes.byteslice(range).unpack1("H*").scan(/../).join(" ")) }
33
54
  ui.install_panes(table, tree, hex)
34
55
  ratios = ui.preferences.get("layout.ratios")
@@ -39,6 +60,7 @@ module Vanken
39
60
  end
40
61
 
41
62
  def build(cx)
63
+ @previous_buttons, @buttons = @buttons || {}, {}
42
64
  ui = @ui
43
65
  doc = ui.document
44
66
  doc.frame_latency = cx.window.frame_stats.fetch(:frame_ms, 0) / 1000 if doc
@@ -53,7 +75,7 @@ module Vanken
53
75
  @heights = heights
54
76
  end
55
77
  root = Zaniah::Div.new.w_full.h_full.flex_col.bg(cx.theme.colors.background)
56
- root.child(Zaniah::UI::MenuBar.from(ui.app.menu_bar)) unless RUBY_PLATFORM.include?("darwin") && ui.native?
78
+ root.child(@menu_bar) unless RUBY_PLATFORM.include?("darwin") && ui.native?
57
79
  toolbar_items = [
58
80
  button("開く", "vk.open") { ui.open_dialog },
59
81
  button("保存", "vk.save", disabled: !doc) { ui.save_dialog },
@@ -63,13 +85,22 @@ module Vanken
63
85
  if doc&.loading? && doc.source == :file
64
86
  toolbar_items << button("読み込みを中止", "vk.file.cancel") { doc.cancel }
65
87
  end
66
- root.child(Zaniah::UI::Toolbar.new(*toolbar_items))
88
+ root.child(Zaniah::UI::Toolbar.new(*toolbar_items).accessibility_label(text("キャプチャ操作")))
67
89
  root.child(Zaniah::Div.new.flex_row.items_center.gap(6).p([4, 8])
68
- .child(Zaniah::Div.new.flex_1.focusable(context: {in_display_filter: true}).child(ui.filter_field)).child(button("適用", "vk.filter.apply", disabled: !doc) { ui.apply_filter })
90
+ .child(@filter_container).child(button("適用", "vk.filter.apply", disabled: !doc) { ui.apply_filter })
69
91
  .child(button("クリア", "vk.filter.clear") { ui.clear_filter })
70
92
  .child(button("履歴", "vk.filter.history") { ui.filter_history }))
71
93
  root.child(doc ? @split.flex_1 : welcome(cx).flex_1)
72
- root.child(Zaniah::UI::StatusBar.new(Zaniah::UI::Label.new(ui.status_text, size: :xs)).test_id("vk.status"))
94
+ status = [Zaniah::UI::Label.new(ui.status_text, size: :xs).flex_1]
95
+ summary = doc.respond_to?(:expert_summary) ? doc.expert_summary : nil
96
+ if summary && summary[:count].positive?
97
+ severity = %w[note note warning error].fetch(summary[:severity])
98
+ caption = {"note" => "注記", "warning" => "警告", "error" => "エラー"}.fetch(severity)
99
+ badge = ExpertBadge.new("#{text(caption)}: #{summary[:count]}", variant: {"note" => :neutral, "warning" => :warning, "error" => :danger}.fetch(severity)) { ui.expert_info }
100
+ .test_id("vk.expert.badge")
101
+ status << badge
102
+ end
103
+ root.child(Zaniah::UI::StatusBar.new(*status).test_id("vk.status"))
73
104
  root.child(ui.dialog) if ui.dialog
74
105
  root.on_drop(types: ["text/uri-list"]) do |event, _|
75
106
  event.content.formats.fetch("text/uri-list", "").each_line do |line|
@@ -90,23 +121,37 @@ module Vanken
90
121
 
91
122
  private
92
123
 
93
- def pane(title, body) = Zaniah::Div.new.w_full.h_full.flex_col.child(Zaniah::UI::Label.new(title, size: :xs, tone: :muted).p([4, 8])).child(body)
94
- def button(label, id, disabled: false, &block) = Zaniah::UI::Button.new(label, size: :sm, variant: :secondary).disabled(disabled).test_id(id).on_click(&block)
95
- def welcome(_cx)
124
+ def text(value) = @ui.respond_to?(:t) ? @ui.t(value) : value
125
+ def pane(title, body) = Zaniah::Div.new.w_full.h_full.flex_col.child(Zaniah::UI::Label.new(text(title), size: :xs, tone: :muted).p([4, 8])).child(body)
126
+ def button(label, id, disabled: false, size: :sm, variant: :secondary, &block)
127
+ caption = text(label)
128
+ key = [id, caption, size, variant]
129
+ @buttons[key] = (@previous_buttons[key] || Zaniah::UI::Button.new(caption, size: size, variant: variant).test_id(id)).disabled(disabled).on_click(&block)
130
+ end
131
+ def welcome(cx)
96
132
  content = Zaniah::Div.new.flex_col.p(24).gap(12).test_id("vk.welcome")
97
133
  .child(Zaniah::UI::Label.new("Vanken", size: :xl))
98
- .child(Zaniah::UI::Label.new("パケットキャプチャを開くか、インタフェースを選択してキャプチャを開始します。", tone: :muted, wrap: :word))
99
- .child(Zaniah::UI::Button.new("キャプチャファイルを開く").on_click { @ui.open_dialog })
100
- .child(Zaniah::UI::Button.new("インタフェースを選択").on_click { @ui.capture_options })
134
+ .child(Zaniah::UI::Label.new(text("パケットキャプチャを開くか、インタフェースを選択してキャプチャを開始します。"), tone: :muted, wrap: :word))
135
+ .child(button("キャプチャファイルを開く", "vk.welcome.open", size: :md, variant: :primary) { @ui.open_dialog })
136
+ .child(button("インタフェースを選択", "vk.welcome.capture", size: :md, variant: :primary) { @ui.capture_options })
101
137
  @ui.preferences.recent_files.first(10).each do |path|
102
- content.child(Zaniah::UI::Button.new(path, size: :sm, variant: :ghost).on_click { @ui.open_file(path) })
138
+ content.child(button(path, "vk.recent.#{path}", variant: :ghost) { @ui.open_file(path) })
103
139
  end
104
140
  (@ui.interface_infos || []).each do |info|
105
141
  name = info.fetch(:name)
106
- content.child(Zaniah::UI::Button.new("#{name} #{info[:description]}", size: :sm, variant: :ghost)
107
- .on_click { @ui.capture_options(interface: name) })
142
+ traffic = @ui.respond_to?(:interface_traffic) ? (@ui.interface_traffic || {}) : {}
143
+ values = (traffic[:history] || {}).fetch(name, [])
144
+ values = [0] if values.empty?
145
+ rate = (traffic[:rates] || {}).fetch(name, 0)
146
+ row = Zaniah::Div.new.flex_row.items_center.gap(8)
147
+ .child(button("#{name} #{info[:description]}", "vk.interface.#{name}", variant: :ghost) { @ui.capture_options(interface: name) }.flex_1)
148
+ .child(Zaniah::UI::Sparkline.new(values, width: 100, height: 24, label: "#{name} #{text('トラフィック')}").test_id("vk.interface.traffic.#{name}"))
149
+ .child(Zaniah::UI::Label.new("#{rate} B/s", size: :xs, tone: :muted))
150
+ content.child(row)
108
151
  end
109
- content
152
+ @welcome_scroll ||= Zaniah::ScrollView.new
153
+ @welcome_scroll.children.clear
154
+ @welcome_scroll.h([cx.window.content_size.height - 132, 120].max).child(content)
110
155
  end
111
156
  end
112
157
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vanken
4
+ module UI
5
+ module NavigationOperations
6
+ def mark_packet = update_packet_state(:toggle_mark)
7
+ def ignore_packet = update_packet_state(:toggle_ignore)
8
+ def time_reference = update_packet_state(:toggle_time_reference)
9
+ def mark_all_displayed = update_packet_state(:mark_all_displayed, selected: false)
10
+ def unmark_all = update_packet_state(:unmark_all, selected: false)
11
+ def prev_in_conversation = navigate_conversation(-1)
12
+ def next_in_conversation = navigate_conversation(1)
13
+
14
+ def find_packet
15
+ mode = @search_mode || :filter
16
+ target = @search_target || :bytes
17
+ field = Zaniah::UI::TextField.new(@search_query || "", label: t("検索内容")).test_id("vk.search.query")
18
+ modes = Zaniah::UI::SegmentedControl.new([[t("表示フィルタ"), :filter], [t("16 進"), :hex], [t("文字列"), :string], [t("正規表現"), :regex]], value: mode, label: t("検索形式"))
19
+ .on_change { |value, *_| mode = value }.test_id("vk.search.mode")
20
+ targets = Zaniah::UI::Select.new([[t("パケット一覧"), :list], [t("パケット詳細"), :details], [t("パケットバイト"), :bytes]], label: t("検索対象"), value: target)
21
+ .on_change { |value, *_| target = value }.test_id("vk.search.target")
22
+ content = Zaniah::Div.new.flex_col.gap(12).child(modes).child(targets).child(field)
23
+ .child(Zaniah::Div.new.flex_row.gap(8)
24
+ .child(Zaniah::UI::Button.new(t("次を検索")).test_id("vk.search.next").on_click { search_packets(field.value, mode: mode, target: target) })
25
+ .child(Zaniah::UI::Button.new(t("前を検索"), variant: :secondary).on_click { search_packets(field.value, mode: mode, target: target, direction: -1) })
26
+ .child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).on_click { cancel_packet_search; dismiss_dialog }))
27
+ show_dialog(:search, t("パケット検索"), content, reopen: -> { @search_query, @search_mode, @search_target = field.value, mode, target; find_packet })
28
+ end
29
+
30
+ def search_result_dialog
31
+ show_dialog(:search_result, t("パケット検索"), Zaniah::UI::Label.new(t("一致するパケットはありません。"), wrap: :word), reopen: -> { search_result_dialog })
32
+ end
33
+
34
+ def find_next = repeat_packet_search(1)
35
+ def find_prev = repeat_packet_search(-1)
36
+ def cancel_packet_search
37
+ @search_generation = (@search_generation || 0) + 1
38
+ document&.cancel_search
39
+ end
40
+
41
+ def search_packets(query, mode: :filter, target: :bytes, direction: 1)
42
+ doc = document
43
+ return unless doc
44
+ cancel_packet_search
45
+ generation = @search_generation
46
+ @search_query, @search_mode, @search_target = query, mode, target
47
+ doc.search(query, mode: mode, target: target, from: selected_number, direction: direction) do |number, error|
48
+ @app.executor.post do
49
+ next unless !@closing && generation == @search_generation && document.equal?(doc)
50
+ if error
51
+ show_error(error)
52
+ elsif number
53
+ select_found_packet(number)
54
+ else
55
+ search_result_dialog
56
+ end
57
+ end
58
+ end
59
+ dismiss_dialog if @dialog_kind == :search
60
+ rescue StandardError => error
61
+ show_error(error)
62
+ end
63
+
64
+ private
65
+ def repeat_packet_search(direction)
66
+ @search_query && !@search_query.empty? ? search_packets(@search_query, mode: @search_mode, target: @search_target, direction: direction) : find_packet
67
+ end
68
+ def select_found_packet(number)
69
+ index = document&.displayed_index(number)
70
+ return unless index
71
+ @table.select(index)
72
+ @table.scroll_to(index, align: :center)
73
+ end
74
+ def navigate_conversation(direction)
75
+ number = document&.conversation_neighbor(selected_number, direction)
76
+ select_found_packet(number) if number
77
+ end
78
+ def update_packet_state(operation, selected: true, number: selected_number)
79
+ return unless document && (!selected || number)
80
+ selected ? document.public_send(operation, number) : document.public_send(operation)
81
+ @packet_source.reset
82
+ @window.request_frame
83
+ end
84
+ end
85
+ end
86
+ end
@@ -40,6 +40,14 @@ module Vanken
40
40
 
41
41
  def count = @count
42
42
  def packet_count = @packet_count
43
+ def invalidate_rows(numbers)
44
+ @generation += 1
45
+ @pending, @requests = {}, {}
46
+ @batch_scheduled = false
47
+ @refresh_pending = @refresh_again = nil
48
+ numbers.each { |number| @rows.delete(number) }
49
+ @ui.window.request_frame
50
+ end
43
51
  def row_id(index)
44
52
  return [:pending, index] unless index.is_a?(Integer) && index.between?(0, @count - 1)
45
53
  @numbers[index] ||= @document.number_at(index)
@@ -51,7 +59,21 @@ module Vanken
51
59
  text = value(index, key)
52
60
  return nil if text.nil?
53
61
  theme = @ui.app.global(:theme)
54
- Zaniah::Text.new(text.to_s, font: @ui.monospace_font, size: theme.typography.size_sm, color: theme.colors.text)
62
+ colors = row_style(index)
63
+ Zaniah::Text.new(text.to_s, font: @ui.monospace_font, size: theme.typography.size_sm, color: colors.fetch(:foreground, theme.colors.text))
64
+ end
65
+ def row_style(index)
66
+ number = row_id(index)
67
+ return {} unless number.is_a?(Integer)
68
+ if @ui.table.respond_to?(:selection) && @ui.table.selection.include?(number)
69
+ theme = @ui.app.global(:theme)
70
+ return {foreground: theme.colors.text, strikethrough: @document.ignored.include?(number)}
71
+ end
72
+ if @document.ignored.include?(number)
73
+ return {foreground: @ui.app.global(:theme).colors.text_muted, strikethrough: true}
74
+ end
75
+ return {foreground: Zaniah::Color.parse("#ffffff"), background: Zaniah::Color.parse("#000000")} if @document.marked.include?(number)
76
+ @rows[number]&.fetch(:style, {}) || {}
55
77
  end
56
78
  def value(index, key)
57
79
  number = row_id(index)
@@ -139,7 +161,17 @@ module Vanken
139
161
  def row_values(document, number, index, format, precision, previous = :current)
140
162
  row = document.row(number)
141
163
  values = row.slice(:source, :destination, :protocol, :length, :info).transform_values(&:to_s)
164
+ if @ui.respond_to?(:resolve_address)
165
+ %i[source destination].each do |key|
166
+ values[key] = @ui.resolve_address(values[key], document: document, number: number)
167
+ end
168
+ end
169
+ values.merge!(row.select { |key, _| key.to_s.start_with?("field:") })
142
170
  values[:time] = format_time(document, row, number, index, format, precision, previous)
171
+ if @ui.respond_to?(:coloring_enabled?) && @ui.coloring_enabled?
172
+ colors = @ui.coloring_rules.match(document.view(number), theme: @ui.app.global(:theme).name) || {}
173
+ values[:style] = colors.transform_values { |color| Zaniah::Color.parse(color) }
174
+ end
143
175
  values
144
176
  rescue StandardError
145
177
  nil
@@ -8,6 +8,10 @@ module Vanken
8
8
  return unless doc && number.is_a?(Integer) && number.between?(1, doc.count)
9
9
  @selection_generation += 1
10
10
  generation = @selection_generation
11
+ expanded = @pending_tree_expansion ||= @tree.expanded.dup
12
+ update { |state| state.merge!(number: number, node: nil, details: [], bytes: "".b) }
13
+ @tree.replace([])
14
+ @hex.bytes = "".b
11
15
  @app.executor.background do
12
16
  nodes = doc.details(number)
13
17
  bytes = doc.store.read(number).bytes
@@ -15,6 +19,8 @@ module Vanken
15
19
  next unless generation == @selection_generation && document.equal?(doc)
16
20
  update { |state| state.merge!(number: number, node: nil, details: nodes, bytes: bytes) }
17
21
  @tree.replace(nodes)
22
+ expanded.each { |id| @tree.expand(id) }
23
+ @pending_tree_expansion = nil
18
24
  @hex.bytes = bytes
19
25
  end
20
26
  rescue StandardError => error
@@ -65,10 +71,10 @@ module Vanken
65
71
  @table.scroll_to(document.displayed_count - 1)
66
72
  end
67
73
  def go_to_packet
68
- path_dialog("パケットへ移動") do |value|
74
+ path_dialog(t("パケットへ移動")) do |value|
69
75
  number = Integer(value, 10)
70
76
  index = document&.display_numbers&.index(number)
71
- raise Vanken::Error, "指定したパケットは表示されていません" unless index
77
+ raise Vanken::Error, t("指定したパケットは表示されていません") unless index
72
78
  @table.select(index)
73
79
  @table.scroll_to(index, align: :center)
74
80
  dismiss_dialog