vimamsa 0.1.20 → 0.1.21

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b635655f9d50eb870f5593b8d2e63cfacff5ed0aece9b197ca8805622799ab0
4
- data.tar.gz: 8c579b33d9d5e5d48c25fef3da47ca77eb9ba9bf713cf7c2895c1af5e2cf2d30
3
+ metadata.gz: 4b44f279b048a7cd7baba9e21e70dc9666a251f9608466f9e4bd38094c486738
4
+ data.tar.gz: ef57b1ad00c480d30720ec8d272df38ec289640c7951b8a04a070837e43a261a
5
5
  SHA512:
6
- metadata.gz: 1027546a7e21d7832e546f4ccd7eb9064aece5b7c13bef4d83477082351b2cc74e136c952999c69de70581aa651af2a5e8fd14960a1b52c91a0d0a7d08bd530c
7
- data.tar.gz: 3784552302bd4f7541320785f250fe9c4aa36e6bcd7d1317f9f85b776900211a1f7d5644891de6ae224703dca6ce9c0eda12fc894d1d4f73523e6bd149b6aebf
6
+ metadata.gz: 756f7c861bff64427fac5e397f8e0b4d539f46dd9d72edf39ddbc190eefff09cb86c0107ec38664ee9a1c450c2de9912f6960aee7c990c8b99c4c914cf0f5a56
7
+ data.tar.gz: 15a6b06c01f13435a154a2b64c544e8d5b14f253bed3e4b65e845dc510f591fcd77c1e9c278d4cf7b67699205c97952ad6ab8042fffe222260d972b88db42bee
data/README.md CHANGED
@@ -1,7 +1,18 @@
1
1
  # Vimamsa
2
2
 
3
- Vi/Vim -inspired experimental GUI-oriented text editor written with Ruby and GTK.
3
+ Vi/Vim -inspired experimental GUI-oriented text editor written with Ruby and GTK.
4
4
 
5
+ <!-- toc -->
6
+
7
+ - [Requirements](#requirements)
8
+ - [Installation](#installation)
9
+ * [Other install options](#other-install-options)
10
+ - [Run](#run)
11
+ - [Screenshots](#screenshots)
12
+ - [Key bindings](#key-bindings)
13
+ - [Current limitations](#current-limitations)
14
+
15
+ <!-- tocstop -->
5
16
 
6
17
  ## Requirements
7
18
  - Ruby 3.0+
@@ -144,6 +155,10 @@ For example, to bind ctrl-n to action "create new file":
144
155
  bindkey 'C ctrl-n', 'create_new_file()'
145
156
  ```
146
157
 
158
+
159
+ ## Known issues
160
+ - Cursor sometimes vanishes when dragging or resizing the window. At least some cases are fixed in new GTK4 versions ( >= 4.18, in Ubuntu Ubuntu 25.04). Workaround is to press ctrl key twice.
161
+
147
162
  ## Current limitations
148
163
  - UTF8 only
149
164
  - Line endings with "\n"
@@ -41,6 +41,7 @@ class Buffer < String
41
41
  @module = nil
42
42
 
43
43
  @last_save = @last_asked_from_user = @file_last_cheked = Time.now
44
+ @t_modified = @last_save
44
45
 
45
46
  @crypt = nil
46
47
  @update_highlight = true
@@ -1295,6 +1296,16 @@ class Buffer < String
1295
1296
  end_visual_mode
1296
1297
  end
1297
1298
 
1299
+ def eval_whole_buf(x = 888)
1300
+ s = self.to_s
1301
+ begin
1302
+ eval(s)
1303
+ rescue Exception
1304
+ message("Error running eval")
1305
+ else
1306
+ end
1307
+ end
1308
+
1298
1309
  def convert_selected_text(converter_id)
1299
1310
  return if !@visual_mode
1300
1311
  r = get_visual_mode_range
@@ -1503,6 +1514,7 @@ class Buffer < String
1503
1514
  #TODO: show message box
1504
1515
  end
1505
1516
  @last_save = Time.now
1517
+ refresh_title
1506
1518
  debug "file saved on #{@last_save}"
1507
1519
  sleep 3
1508
1520
  }
@@ -1515,6 +1527,20 @@ class Buffer < String
1515
1527
  end
1516
1528
  end
1517
1529
 
1530
+ def unsaved_changes?
1531
+ pp [@t_modified, @last_save]
1532
+ return true if @t_modified > @last_save
1533
+ return false
1534
+ end
1535
+
1536
+ def refresh_title
1537
+ if vma.buf == self
1538
+ pfx = ""
1539
+ pfx = "+ " if vma.buf.unsaved_changes?
1540
+ gui_set_window_title(pfx + vma.buf.title, vma.buf.subtitle)
1541
+ end
1542
+ end
1543
+
1518
1544
  def check_if_modified_outside
1519
1545
  # Don't check if less than 8 seconds since last checked
1520
1546
  return false if @fname.nil?
@@ -1597,4 +1623,3 @@ def backup_all_buffers()
1597
1623
  end
1598
1624
  message("Backup all buffers")
1599
1625
  end
1600
-
@@ -5,6 +5,7 @@ class Buffer < String
5
5
 
6
6
  #TODO: change to apply=true as default
7
7
  def add_delta(delta, apply = false, auto_update_cpos = false)
8
+ @t_modified = Time.now
8
9
  return if !is_delta_ok(delta)
9
10
  if delta[1] == DELETE
10
11
  return if delta[0] >= self.size
@@ -26,6 +27,7 @@ class Buffer < String
26
27
  add_delta([self.size, INSERT, 1, "\n"], true)
27
28
  end
28
29
  reset_larger_cpos #TODO: correct here?
30
+ refresh_title
29
31
  end
30
32
 
31
33
  # TODO: rename ot auto-format. separate module?
@@ -154,7 +154,7 @@ class BufferList
154
154
  # end
155
155
  # vma.kbd.set_mode_to_default if vma.kbd.get_scope != :editor
156
156
 
157
- gui_set_window_title(vma.buf.title, vma.buf.subtitle)
157
+ vma.buf.refresh_title
158
158
 
159
159
  if vma.buf.fname
160
160
  @last_dir = File.dirname(vma.buf.fname)
@@ -139,7 +139,7 @@ class Editor
139
139
  fname = fname_
140
140
  end
141
141
  else
142
- fname = ppath("demo.txt")
142
+ # fname = ppath("demo.txt")
143
143
  end
144
144
  fname = ARGV[0] if ARGV.size >= 1 and File.file?(File.expand_path(ARGV[0]))
145
145
  # vma.add_content_search_path(Dir.pwd)
@@ -645,7 +645,7 @@ def find_project_dir_of_fn(fn)
645
645
  break
646
646
  end
647
647
  end
648
- return if !projdir.nil?
648
+ return projdir if !projdir.nil?
649
649
  end
650
650
  return projdir
651
651
  end
@@ -8,6 +8,7 @@ cnf.search_dirs = []
8
8
  class StringIndex
9
9
  def initialize()
10
10
  @idx = StrIdx::StringIndex.new
11
+ @idx.setDirSeparator("/")
11
12
  end
12
13
 
13
14
  def find(str, minChars: 2)
@@ -94,10 +95,15 @@ class FileFinder
94
95
  select_callback = self.method("gui_file_finder_select_callback")
95
96
  update_callback = self.method("gui_file_finder_update_callback")
96
97
 
98
+
99
+ opt = { :title => "Fuzzy filename search",
100
+ :desc => "Search for files in folders defined in cnf.search_dirs" ,
101
+ :columns => [{:title=>'Filename',:id=>0}]
102
+ }
103
+
97
104
  gui_select_update_window(l, $select_keys.collect { |x| x.upcase },
98
- # "gui_file_finder_select_callback",
99
105
  select_callback,
100
- update_callback)
106
+ update_callback, opt)
101
107
  end
102
108
 
103
109
  def gui_file_finder_update_callback(search_str = "")
@@ -105,8 +111,11 @@ class FileFinder
105
111
  if (search_str.size >= 3)
106
112
  files = filter_files(search_str)
107
113
  @file_search_list = files
114
+ if files.size > 1
115
+ files = files.collect{|x|[tilde_path(x[0])]}
116
+ end
117
+
108
118
  return files
109
- #debug files.inspect
110
119
  #return files.values
111
120
  end
112
121
  return []
@@ -104,9 +104,18 @@ class VSourceView < GtkSource::View
104
104
  # Sometimes a GestureClick EventController appears from somewhere
105
105
  # not initiated from this file.
106
106
 
107
- # if ctr.class == Gtk::EventControllerKey or ctr.class == Gtk::GestureClick
108
- if ctr != @click
109
- # to_remove << ctr if ctr.class != Gtk::GestureDrag
107
+ # TODO: Check which of these are needed:
108
+ # puts ctr.class
109
+ # Gtk::DropTarget
110
+ # Gtk::EventControllerFocus
111
+ # Gtk::EventControllerKey
112
+ # Gtk::EventControllerMotion
113
+ # Gtk::EventControllerScroll
114
+ # Gtk::GestureClick
115
+ # Gtk::GestureDrag
116
+ # Gtk::ShortcutController
117
+
118
+ if ctr != @click and [Gtk::DropControllerMotion, Gtk::DropTarget, Gtk::GestureDrag, Gtk::GestureClick, Gtk::EventControllerKey].include?(ctr.class)
110
119
  to_remove << ctr
111
120
  end
112
121
  }
@@ -120,20 +129,19 @@ class VSourceView < GtkSource::View
120
129
  end
121
130
 
122
131
  def focus_out()
123
- set_cursor_color(:inactive)
124
-
125
- # This does not seem to work: (TODO:why?)
126
- # self.cursor_visible = false
132
+ set_cursor_color(:inactive)
133
+
134
+ # This does not seem to work: (TODO:why?)
135
+ # self.cursor_visible = false
127
136
  end
128
-
137
+
129
138
  def focus_in()
130
- set_cursor_color(@ctype)
131
- self.cursor_visible = false
132
- self.cursor_visible = true
133
- self.grab_focus
139
+ set_cursor_color(@ctype)
140
+ self.cursor_visible = false
141
+ self.cursor_visible = true
142
+ self.grab_focus
134
143
  end
135
-
136
-
144
+
137
145
  def register_signals()
138
146
  check_controllers
139
147
 
@@ -332,7 +340,7 @@ class VSourceView < GtkSource::View
332
340
  end
333
341
  debug $view.visible_rect.inspect
334
342
 
335
- debug "key event"
343
+ debug "key event" + [keyval, @last_keyval, keyname, sig].to_s
336
344
  # debug event
337
345
 
338
346
  # key_name = event.string
@@ -353,6 +361,7 @@ class VSourceView < GtkSource::View
353
361
  keyval_trans[Gdk::Keyval::KEY_KP_Enter] = "enter"
354
362
  keyval_trans[Gdk::Keyval::KEY_Alt_L] = "alt"
355
363
  keyval_trans[Gdk::Keyval::KEY_Alt_R] = "alt"
364
+ keyval_trans[Gdk::Keyval::KEY_Caps_Lock] = "caps"
356
365
 
357
366
  keyval_trans[Gdk::Keyval::KEY_BackSpace] = "backspace"
358
367
  keyval_trans[Gdk::Keyval::KEY_KP_Page_Down] = "pagedown"
@@ -383,6 +392,15 @@ class VSourceView < GtkSource::View
383
392
  key_str_parts << "super" if vma.kbd.modifiers[:super]
384
393
  key_str_parts << keyname
385
394
 
395
+ # After remapping capslock to control in gnome-tweak tool,
396
+ # if pressing and immediately releasing the capslock (control) key,
397
+ # we get first "caps" on keydown and ctrl-"caps" on keyup (keyval 65509, Gdk::Keyval::KEY_Caps_Lock)
398
+ # If mapping capslock to ctrl in hardware, we get keyval 65507 (Gdk::Keyval::KEY_Control_L) instead
399
+ if key_str_parts[0] == "ctrl" and key_str_parts[1] == "caps"
400
+ # Replace ctrl-caps with ctrl
401
+ key_str_parts.delete_at(1)
402
+ end
403
+
386
404
  if key_str_parts[0] == key_str_parts[1]
387
405
  # We don't want "ctrl-ctrl" or "alt-alt"
388
406
  # TODO:There should be a better way to do this
@@ -415,8 +433,8 @@ class VSourceView < GtkSource::View
415
433
  $kbd.match_key_conf(key_str, nil, :key_press)
416
434
  @last_event = [keynfo, key_str, :key_press]
417
435
  end
418
- @last_keyval = keyval #TODO: outside if?
419
436
  end
437
+ @last_keyval = keyval
420
438
 
421
439
  handle_deltas
422
440
 
@@ -596,7 +614,6 @@ class VSourceView < GtkSource::View
596
614
  end
597
615
 
598
616
  def draw_cursor
599
-
600
617
  sv = vma.gui.active_window[:sw].child
601
618
  return if sv.nil?
602
619
  if sv != self # if we are not the current buffer
@@ -33,6 +33,8 @@ reg_act(:command_to_buf, proc { command_to_buf }, "Execute command, output to bu
33
33
  reg_act(:lsp_debug, proc { vma.buf.lsp_get_def }, "LSP get definition")
34
34
  reg_act(:lsp_jump_to_definition, proc { vma.buf.lsp_jump_to_def }, "LSP jump to definition")
35
35
 
36
+ reg_act(:eval_buf, proc { vma.buf.eval_whole_buf }, "Eval whole current buffer as ruby code (DANGEROUS)")
37
+
36
38
  reg_act(:enable_debug, proc { cnf.debug = true }, "Enable debug")
37
39
  reg_act(:disable_debug, proc { cnf.debug = false }, "Disable debug")
38
40
 
@@ -632,11 +632,12 @@ end
632
632
 
633
633
  # Try to clear modifiers when program loses focus
634
634
  # e.g. after alt-tab
635
- def focus_out
636
- debug "RB Clear modifiers"
637
- $kbd.clear_modifiers()
638
- end
639
-
640
- def handle_key_event(event)
641
- $kbd.handle_key_event(event)
642
- end
635
+ # TODO
636
+ # def focus_out
637
+ # debug "RB Clear modifiers"
638
+ # $kbd.clear_modifiers()
639
+ # end
640
+
641
+ # def handle_key_event(event)
642
+ # $kbd.handle_key_event(event)
643
+ # end
@@ -127,6 +127,7 @@ bindkey "C p", [:paste_after, proc { buf.paste(AFTER) }, ""] # TODO: implement a
127
127
  bindkey "V d", [:delete_selection, proc { buf.delete(SELECTION) }, ""]
128
128
  bindkey "V a d", [:delete_append_selection, proc { buf.delete(SELECTION, :append) }, "Delete and append selection"]
129
129
 
130
+
130
131
  default_keys = {
131
132
 
132
133
  # File handling
@@ -266,6 +267,7 @@ default_keys = {
266
267
  "C d <num> e" => "delete_next_word",
267
268
  "C d ' <char>" => "buf.delete2(:to_mark,<char>)",
268
269
  "C r <char>" => "buf.replace_with_char(<char>)", # TODO
270
+ "C r space" => "buf.replace_with_char(' ')", # TODO
269
271
  "C , l b" => "load_buffer_list",
270
272
  "C , l l" => "save_buffer_list",
271
273
  "C , r <char>" => "vma.set_register(<char>)", # TODO
@@ -331,7 +333,7 @@ default_keys = {
331
333
  "CV ctrl-q" => :quit,
332
334
  "CV , R" => "restart_application",
333
335
  # "I ctrl!" => "vma.kbd.to_previous_mode",
334
- "C shift!" => "buf.save",
336
+ "C shift! s" => "buf.save",
335
337
  "I ctrl-s" => "buf.save",
336
338
  "I <char>" => "buf.insert_txt(<char>)",
337
339
  "I esc || I ctrl!" => "vma.kbd.to_previous_mode",
data/lib/vimamsa/macro.rb CHANGED
@@ -118,8 +118,10 @@ class Macro
118
118
 
119
119
  # Run the provided list of actions
120
120
  def run_actions(acts)
121
+ acts = [acts] if acts.class != Array
121
122
  isok = true
122
- if acts.kind_of?(Array) and acts.any?
123
+ # if acts.kind_of?(Array) and acts.any?
124
+ if acts.any?
123
125
  @running_macro = true
124
126
  # TODO:needed?
125
127
  # set_last_command({ method: vma.macro.method("run_macro"), params: [name] })
data/lib/vimamsa/tests.rb CHANGED
@@ -1,7 +1,19 @@
1
1
  require "digest"
2
2
 
3
3
  def run_tests()
4
- tests = ["test_paste_0", "test_delete_0"]
4
+ # DelayExecutioner.exec(id: :run_tests, wait: 0.7, callable: proc { run_tests_0 })
5
+
6
+ # Reload class
7
+ # if Object.constants.include?(:EditorTests)
8
+ # Object.send(:remove_const, :EditorTests)
9
+ # end
10
+ load __FILE__
11
+
12
+ run_edit_tests
13
+ end
14
+
15
+ def run_tests_0()
16
+ tests = ["test_write_0"]
5
17
  stats = []
6
18
  for t in tests
7
19
  r = eval(t)
@@ -16,7 +28,74 @@ def run_tests()
16
28
  puts "===================="
17
29
  end
18
30
 
19
- #
31
+ def run_edit_tests()
32
+ edt = EditorTests.new
33
+ tests = edt.methods.select { |x| x.match(/test_.*/) }.collect { |x| { :method => edt.method(x), :name => x } }
34
+ for t in tests
35
+ b = create_new_buffer(file_contents = "\n", prefix = "buf", setcurrent = true)
36
+
37
+ edits = t[:method].call
38
+ # next if t[:name] != :test_create_close_buf
39
+ errors = 0
40
+ edits.each_with_index do |x, i|
41
+ if x.class == Array
42
+ (act, target) = x
43
+
44
+ vma.macro.run_actions(act)
45
+ # bufc = b.to_s
46
+ bufc = vma.buf.to_s
47
+ if bufc != target
48
+ puts "ERROR[#{t[:name]}:#{i}] act=#{act.inspect} content=#{bufc.inspect} != #{target.inspect}"
49
+ errors += 1
50
+ end
51
+ else
52
+ vma.macro.run_actions(x)
53
+ end
54
+ end
55
+ if errors == 0
56
+ puts "TEST #{t[:name]} passed"
57
+ else
58
+ end
59
+ end
60
+ end
61
+
62
+ class EditorTests
63
+ def test_write
64
+ #[[action to run, expected buffer contents after], ...]
65
+ [['buf.insert_txt("zzzz")', "zzzz\n"],
66
+ # At end of file replace should not change anything
67
+ ['buf.replace_with_char("-")', "zzzz\n"],
68
+ 'buf.insert_txt("\n")',
69
+ ['buf.insert_txt("yy")', "zzzz\nyy\n"],
70
+ :e_move_backward_char, :e_move_backward_char,
71
+ ['buf.insert_txt("0")', "zzzz\n0yy\n"],
72
+ "buf.jump(START_OF_BUFFER)",
73
+ ['buf.replace_with_char("1")', "1zzz\n0yy\n"]]
74
+ end
75
+
76
+ def test_delete
77
+ [['buf.insert_txt("abcdef")', "abcdef\n"],
78
+ 'buf.insert_txt("\n")',
79
+ ['buf.insert_txt("yy")', "abcdef\nyy\n"],
80
+ "buf.jump(START_OF_BUFFER)",
81
+ :delete_char_forward,
82
+ [:delete_char_forward, "cdef\nyy\n"],
83
+ "buf.jump(END_OF_LINE)",
84
+ "buf.delete(BACKWARD_CHAR)",
85
+ ["buf.delete(BACKWARD_CHAR)", "cd\nyy\n"]]
86
+ end
87
+
88
+ def test_create_close_buf
89
+ ['buf.insert_txt("abcdef")',
90
+ [:buf_new,"\n"],
91
+ 'buf.insert_txt("a")',
92
+ 'buf.insert_txt("b")',
93
+ 'buf.insert_txt("c")',
94
+ 'buf.insert_txt("d")',
95
+ ['buf.insert_txt("e")',"abcde\n"],
96
+ [:close_current_buffer, "abcdef\n"]]
97
+ end
98
+ end
20
99
 
21
100
  def test_paste_0(runmacro = true)
22
101
  return
@@ -19,6 +19,8 @@ end
19
19
  Converter.new(lambda { |x| x.split("\n").collect { |x| r = x.strip }.select { |y| !y.empty? }.join(" ") + "\n" }, :lambda, :joinlines)
20
20
 
21
21
  Converter.new(lambda { |x| x.split("\n").sort.join("\n") }, :lambda, :sortlines)
22
+ Converter.new(lambda { |x| x.split("\n").reverse.join("\n") }, :lambda, :reverse_lines)
23
+
22
24
  Converter.new(lambda { |x| x.split(/\s+/).sort.join(" ") }, :lambda, :sortwords)
23
25
  Converter.new(lambda { |x| x.split("\n").collect { |b| b.scan(/(\d+(\.\d+)?)/).collect { |c| c[0] }.join(" ") }.join("\n") }, :lambda, :getnums_on_lines)
24
26
 
data/lib/vimamsa/util.rb CHANGED
@@ -93,7 +93,7 @@ end
93
93
 
94
94
  # file --mime-type --mime-encoding
95
95
 
96
- # Run idle proc once
96
+ # Run idle proc once (return false)
97
97
  # Delay execution of proc until Gtk has fully processed the last calls.
98
98
  def run_as_idle(p, delay: 0.0)
99
99
  if p.class == Proc
@@ -141,6 +141,24 @@ def exec_cmd(bin_name, arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil, arg5 = ni
141
141
  return ret_str
142
142
  end
143
143
 
144
+ def pipe_to_external(program, text)
145
+ # Open a pipe to the external program
146
+ IO.popen(program, "r+") do |pipe|
147
+ # Send the text to the program
148
+ pipe.write(text)
149
+ pipe.close_write # Close the write end to signal EOF to the program
150
+ # Read and return the output from the program
151
+ output = pipe.read
152
+ return output
153
+ end
154
+ rescue Errno::ENOENT
155
+ puts "Error: The program '#{program}' was not found."
156
+ nil
157
+ rescue => e
158
+ puts "An error occurred: #{e.message}"
159
+ nil
160
+ end
161
+
144
162
  def mkdir_if_not_exists(_dirpath)
145
163
  dirpath = File.expand_path(_dirpath)
146
164
  Dir.mkdir(dirpath) unless File.exist?(dirpath)
@@ -1,3 +1,3 @@
1
1
  module Vimamsa
2
- VERSION = "0.1.20"
2
+ VERSION = "0.1.21"
3
3
  end
data/vimamsa.gemspec CHANGED
@@ -5,7 +5,7 @@ require "vimamsa/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "vimamsa"
8
- spec.version = Vimamsa::VERSION
8
+ spec.version = Vimamsa::VERSION # in lib/vimamsa/version.rb
9
9
  spec.authors = ["Sami Sieranoja"]
10
10
  spec.email = ["sami.sieranoja@gmail.com"]
11
11
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vimamsa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.20
4
+ version: 0.1.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sami Sieranoja
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-05 00:00:00.000000000 Z
11
+ date: 2025-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -295,7 +295,7 @@ homepage: https://github.com/SamiSieranoja/vimamsa
295
295
  licenses:
296
296
  - GPL-3.0+
297
297
  metadata: {}
298
- post_install_message:
298
+ post_install_message:
299
299
  rdoc_options: []
300
300
  require_paths:
301
301
  - lib
@@ -311,8 +311,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
311
311
  - !ruby/object:Gem::Version
312
312
  version: '0'
313
313
  requirements: []
314
- rubygems_version: 3.3.26
315
- signing_key:
314
+ rubygems_version: 3.4.20
315
+ signing_key:
316
316
  specification_version: 4
317
317
  summary: Vimamsa
318
318
  test_files: []