vanken 0.1.0 → 0.2.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +26 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +10 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/lib/vanken/app/capture_controller.rb +66 -1
  9. data/lib/vanken/app/document.rb +102 -39
  10. data/lib/vanken/app/document_analysis.rb +75 -0
  11. data/lib/vanken/app/document_jobs.rb +52 -9
  12. data/lib/vanken/app/expert_info.rb +26 -0
  13. data/lib/vanken/app/io_graph.rb +47 -0
  14. data/lib/vanken/app/navigation.rb +77 -0
  15. data/lib/vanken/app/search_job.rb +62 -0
  16. data/lib/vanken/app/stats_job.rb +45 -0
  17. data/lib/vanken/capture/analyzer.rb +3 -1
  18. data/lib/vanken/capture/analyzer_process.rb +9 -6
  19. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  20. data/lib/vanken/capture/filter_worker.rb +8 -2
  21. data/lib/vanken/capture/helper_main.rb +10 -3
  22. data/lib/vanken/capture/helper_options.rb +6 -2
  23. data/lib/vanken/capture/launcher.rb +1 -0
  24. data/lib/vanken/config/analysis_settings.rb +58 -0
  25. data/lib/vanken/config/columns.rb +138 -0
  26. data/lib/vanken/config/messages.rb +105 -0
  27. data/lib/vanken/config/preferences.rb +34 -8
  28. data/lib/vanken/config/profiles.rb +74 -0
  29. data/lib/vanken/config/sessions.rb +69 -0
  30. data/lib/vanken/config/yaml_file.rb +36 -0
  31. data/lib/vanken/core/coloring.rb +86 -0
  32. data/lib/vanken/core/frame_store.rb +2 -2
  33. data/lib/vanken/core/stores.rb +12 -0
  34. data/lib/vanken/gateway/detail_builder.rb +1 -1
  35. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  36. data/lib/vanken/gateway/dissector.rb +31 -6
  37. data/lib/vanken/gateway/exporter.rb +125 -0
  38. data/lib/vanken/gateway/file_writer.rb +26 -2
  39. data/lib/vanken/gateway/interfaces.rb +59 -0
  40. data/lib/vanken/gateway/resolver.rb +131 -0
  41. data/lib/vanken/gateway/statistics.rb +83 -0
  42. data/lib/vanken/gateway/stream.rb +159 -0
  43. data/lib/vanken/ui/actions.rb +54 -28
  44. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  45. data/lib/vanken/ui/application.rb +183 -9
  46. data/lib/vanken/ui/coloring_operations.rb +104 -0
  47. data/lib/vanken/ui/column_operations.rb +115 -0
  48. data/lib/vanken/ui/dialogs.rb +41 -26
  49. data/lib/vanken/ui/file_operations.rb +17 -12
  50. data/lib/vanken/ui/filter_operations.rb +43 -15
  51. data/lib/vanken/ui/main_view.rb +74 -29
  52. data/lib/vanken/ui/navigation_operations.rb +86 -0
  53. data/lib/vanken/ui/packet_source.rb +33 -1
  54. data/lib/vanken/ui/selection.rb +8 -2
  55. data/lib/vanken/ui/settings_operations.rb +233 -0
  56. data/lib/vanken/version.rb +1 -1
  57. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  58. data/sig/generated/vanken/app/document.rbs +23 -1
  59. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  60. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  61. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  62. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  63. data/sig/generated/vanken/app/navigation.rbs +31 -0
  64. data/sig/generated/vanken/app/search_job.rbs +21 -0
  65. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  66. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  67. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  68. data/sig/generated/vanken/config/columns.rbs +37 -0
  69. data/sig/generated/vanken/config/messages.rbs +14 -0
  70. data/sig/generated/vanken/config/preferences.rbs +1 -1
  71. data/sig/generated/vanken/config/profiles.rbs +29 -0
  72. data/sig/generated/vanken/config/sessions.rbs +17 -0
  73. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  74. data/sig/generated/vanken/core/coloring.rbs +52 -0
  75. data/sig/generated/vanken/core/stores.rbs +4 -0
  76. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  77. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  78. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  79. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  80. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  81. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  82. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  83. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  84. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  85. data/sig/generated/vanken/ui/application.rbs +23 -1
  86. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  87. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  88. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  89. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  90. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  91. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  92. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  93. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  94. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  95. metadata +51 -3
@@ -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
@@ -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 show_dialog(kind, title, content)
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
- @dialog = Zaniah::UI::Dialog.new(content, title: title).on_close { dismiss_dialog }.test_id("vk.#{kind}")
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("", label: title).test_id("vk.path.input")
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
- promiscuous = Zaniah::UI::Checkbox.new("プロミスキャスモード", value: @preferences.get("capture.promiscuous"))
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(%w[system dark light high_contrast], label: "テーマ", value: @preferences.get("appearance.theme"))
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(%w[relative absolute delta delta_displayed epoch], label: "時刻表示", value: @preferences.get("packet_list.time_format"))
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("↑", size: :sm).disabled(index.zero?).on_click { @table.move_column(column[:key], index - 1); columns_dialog })
83
- .child(Zaniah::UI::Button.new("↓", size: :sm).disabled(index == @table.columns.size - 1).on_click { @table.move_column(column[:key], index + 1); columns_dialog }))
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
@@ -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