vimamsa 0.1.0 → 0.1.5

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.
@@ -0,0 +1,177 @@
1
+
2
+ def gui_find_macro_update_callback(search_str = "")
3
+ puts "gui_find_macro_update_callback: #{search_str}"
4
+ heystack = $macro.named_macros
5
+ return [] if heystack.empty?
6
+ $macro_search_list = []
7
+ files = heystack.keys.sort.collect { |x| [x, 0] }
8
+
9
+ if (search_str.size > 1)
10
+ files = fuzzy_filter(search_str, heystack.keys, 40)
11
+ end
12
+ $macro_search_list = files
13
+ return files
14
+ end
15
+
16
+ def gui_find_macro_select_callback(search_str, idx)
17
+ puts "gui_find_macro_select_callback"
18
+ selected = $macro_search_list[idx]
19
+ m = $macro.named_macros[selected[0]].clone
20
+ puts "SELECTED MACRO:#{selected}, #{m}"
21
+ id = $macro.last_macro
22
+ $macro.recorded_macros[id] = m
23
+ $macro.run_macro(id)
24
+ end
25
+
26
+ class Macro
27
+ # attr_reader :recorded_macros, :recording, :named_macros
28
+ attr_accessor :recorded_macros, :recording, :named_macros, :last_macro
29
+
30
+ def initialize()
31
+ @recording = false
32
+ # @recorded_macros = {}
33
+ @current_recording = []
34
+ @current_name = nil
35
+ @last_macro = "a"
36
+
37
+ #TODO:
38
+ @recorded_macros = vma.marshal_load("macros", {})
39
+ @named_macros = vma.marshal_load("named_macros", {})
40
+ $hook.register(:shutdown, self.method("save"))
41
+ end
42
+
43
+ def save()
44
+ vma.marshal_save("macros", @recorded_macros)
45
+ vma.marshal_save("named_macros", @named_macros)
46
+ end
47
+
48
+ def gui_name_macro()
49
+ callback = self.method("name_macro")
50
+ # gui_one_input_action("Grep", "Search:", "grep", "grep_cur_buffer")
51
+ gui_one_input_action("Name last macro", "Name:", "Set", callback)
52
+ end
53
+
54
+ def find_macro_gui()
55
+ # Ripl.start :binding => binding
56
+
57
+ l = $macro.named_macros.keys.sort.collect { |x| [x, 0] }
58
+ $macro_search_list = l
59
+ $select_keys = ["h", "l", "f", "d", "s", "a", "g", "z"]
60
+
61
+ qt_select_update_window(l, $select_keys.collect { |x| x.upcase },
62
+ "gui_find_macro_select_callback",
63
+ "gui_find_macro_update_callback")
64
+ end
65
+
66
+ def name_macro(name, id = nil)
67
+ puts "NAME MACRO #{name}"
68
+ if id.nil?
69
+ id = @last_macro
70
+ end
71
+ @named_macros[name] = @recorded_macros[id].clone
72
+ end
73
+
74
+ def start_recording(name)
75
+ @recording = true
76
+ @current_name = name
77
+ @current_recording = []
78
+ message("Start recording macro [#{name}]")
79
+
80
+ # Returning false prevents from putting start_recording to start of macro
81
+ return false
82
+ end
83
+
84
+ def end_recording()
85
+ if @recording == true
86
+ @recorded_macros[@current_name] = @current_recording
87
+ @last_macro = @current_name
88
+ @current_name = @current_recording = nil
89
+ @recording = false
90
+ message("Stop recording macro [#{@last_macro}]")
91
+ else
92
+ message("Not recording macro")
93
+ end
94
+ end
95
+
96
+ def is_recording
97
+ return @recording
98
+ end
99
+
100
+ def record_action(eval_str)
101
+ if @recording
102
+ if eval_str == "repeat_last_action"
103
+ @current_recording << $command_history.last
104
+ else
105
+ @current_recording << eval_str
106
+ end
107
+ end
108
+ end
109
+
110
+ # Allow method to specify the macro action instead of recording from keyboard input
111
+ def overwrite_current_action(eval_str)
112
+ if @recording
113
+ @current_recording[-1] = eval_str
114
+ end
115
+ end
116
+
117
+ def run_last_macro
118
+ run_macro(@last_macro)
119
+ end
120
+
121
+ def run_macro(name)
122
+ if $macro.is_recording == true
123
+ message("Can't run a macro that runs a macro (recursion risk)")
124
+ return false
125
+ end
126
+ message("Start running macro [#{name}]")
127
+ if @recorded_macros.has_key?(name)
128
+ @last_macro = name
129
+ end
130
+ acts = @recorded_macros[name]
131
+ if acts.kind_of?(Array) and acts.any?
132
+ set_last_command({ method: $macro.method("run_macro"), params: [name] })
133
+ #
134
+ # Ripl.start :binding => binding
135
+ for a in acts
136
+ ret = exec_action(a)
137
+ puts ret
138
+ if ret == false
139
+ message("Error while running macro")
140
+ break
141
+ end
142
+ end
143
+ # eval_str = m.join(";")
144
+ # debug(eval_str)
145
+ # eval(eval_str)
146
+ end
147
+ buf.set_pos(buf.pos)
148
+ end
149
+
150
+ def save_macro(name)
151
+ m = @recorded_macros[name]
152
+ return if !(m.kind_of?(Array) and m.any?)
153
+ contents = m.join(";")
154
+ dot_dir = File.expand_path("~/.vimamsa")
155
+ Dir.mkdir(dot_dir) unless File.exist?(dot_dir)
156
+ save_fn = "#{dot_dir}/macro_#{name}.rb"
157
+
158
+ Thread.new {
159
+ File.open(save_fn, "w+") do |io|
160
+ #io.set_encoding(self.encoding)
161
+
162
+ begin
163
+ io.write(contents)
164
+ rescue Encoding::UndefinedConversionError => ex
165
+ # this might happen when trying to save UTF-8 as US-ASCII
166
+ # so just warn, try to save as UTF-8 instead.
167
+ warn("Saving as UTF-8 because of: #{ex.class}: #{ex}")
168
+ io.rewind
169
+
170
+ io.set_encoding(Encoding::UTF_8)
171
+ io.write(contents)
172
+ end
173
+ end
174
+ sleep 3 #TODO:remove
175
+ }
176
+ end
177
+ end
@@ -0,0 +1,71 @@
1
+ #scriptdir=File.expand_path(File.dirname(__FILE__))
2
+ $:.unshift File.dirname(__FILE__) + "/lib"
3
+
4
+ # require 'benchmark/ips'
5
+
6
+ # load "vendor/ver/lib/ver/vendor/textpow.rb"
7
+ # load "vendor/ver/lib/ver/syntax/detector.rb"
8
+ # load "vendor/ver/config/detect.rb"
9
+
10
+ require "differ"
11
+ module Differ
12
+ class Diff
13
+ def get_raw_array()
14
+ return @raw
15
+ end
16
+ end
17
+ end
18
+
19
+ Encoding.default_external = Encoding::UTF_8
20
+ Encoding.default_internal = Encoding::UTF_8
21
+
22
+ # Globals
23
+ $command_history = []
24
+ $clipboard = []
25
+ $register = Hash.new("")
26
+ $cnf = {}
27
+ $search_dirs = []
28
+ $errors = []
29
+
30
+ $cur_register = "a"
31
+ $input_char_call_func = nil
32
+ $debuginfo = {}
33
+
34
+ $jump_sequence = []
35
+
36
+ $debug = false
37
+ $experimental = false
38
+
39
+ # Return currently active buffer
40
+ def buf()
41
+ return $buffer
42
+ end
43
+
44
+ def bufs()
45
+ return $buffers
46
+ end
47
+
48
+ def buflist()
49
+ return $buffers
50
+ end
51
+
52
+ require "vimamsa/editor.rb"
53
+
54
+ # load "qt_funcs.rb"
55
+
56
+ $vma = Editor.new
57
+ def vma()
58
+ return $vma
59
+ end
60
+
61
+ # c_startup
62
+ # run_random_jump_test
63
+ # main_loop
64
+
65
+ # debug("END")
66
+
67
+
68
+
69
+
70
+
71
+
@@ -0,0 +1,1072 @@
1
+ require "gtk3"
2
+ require "gtksourceview3"
3
+ #require "gtksourceview4"
4
+ require "ripl"
5
+ require "fileutils"
6
+ require "pathname"
7
+ require "date"
8
+ require "ripl/multi_line"
9
+ require "json"
10
+ require "listen"
11
+
12
+ puts "INIT rbvma"
13
+
14
+ require "vimamsa/util"
15
+ # require "rbvma/rbvma"
16
+ require "vimamsa/main" #
17
+ require "vimamsa/key_binding_tree" #
18
+ require "vimamsa/actions" #
19
+ require "vimamsa/macro" #
20
+ require "vimamsa/buffer" #
21
+ require "vimamsa/debug" #
22
+ require "vimamsa/constants"
23
+ require "vimamsa/easy_jump"
24
+ require "vimamsa/hook"
25
+ require "vimamsa/search"
26
+ require "vimamsa/search_replace"
27
+ require "vimamsa/buffer_list"
28
+ require "vimamsa/file_finder"
29
+ require "vimamsa/hyper_plain_text"
30
+ require "vimamsa/ack"
31
+ require "vimamsa/encrypt"
32
+ require "vimamsa/file_manager"
33
+
34
+ # load "vendor/ver/lib/ver/vendor/textpow.rb"
35
+ # load "vendor/ver/lib/ver/syntax/detector.rb"
36
+ # load "vendor/ver/config/detect.rb"
37
+
38
+ $vma = Editor.new
39
+
40
+ def vma()
41
+ return $vma
42
+ end
43
+
44
+ $idle_scroll_to_mark = false
45
+
46
+ def idle_func
47
+ # puts "IDLEFUNC"
48
+ if $idle_scroll_to_mark
49
+ # Ripl.start :binding => binding
50
+ # $view.get_visible_rect
51
+ vr = $view.visible_rect
52
+
53
+ # iter = b.get_iter_at(:offset => i)
54
+
55
+ b = $view.buffer
56
+ iter = b.get_iter_at(:offset => b.cursor_position)
57
+ iterxy = $view.get_iter_location(iter)
58
+ # puts "ITERXY" + iterxy.inspect
59
+ # Ripl.start :binding => binding
60
+
61
+ intr = iterxy.intersect(vr)
62
+ if intr.nil?
63
+ $view.set_cursor_pos($view.buffer.cursor_position)
64
+ else
65
+ $idle_scroll_to_mark = false
66
+ end
67
+
68
+ sleep(0.1)
69
+ end
70
+ sleep(0.01)
71
+ return true
72
+ end
73
+
74
+ # qt_select_update_window(l, $select_keys.collect { |x| x.upcase },
75
+ # "gui_find_macro_select_callback",
76
+ # "gui_find_macro_update_callback")
77
+ class SelectUpdateWindow
78
+ COLUMN_JUMP_KEY = 0
79
+ COLUMN_DESCRIPTION = 1
80
+
81
+ def update_item_list(item_list)
82
+ # puts item_list.inspect
83
+ # Ripl.start :binding => binding
84
+ @model.clear
85
+ for item in item_list
86
+ iter = @model.append
87
+ v = ["", item[0]]
88
+ puts v.inspect
89
+ iter.set_values(v)
90
+ end
91
+
92
+ set_selected_row(0)
93
+ end
94
+
95
+ def set_selected_row(rownum)
96
+ rownum = 0 if rownum < 0
97
+ @selected_row = rownum
98
+
99
+ if @model.count > 0
100
+ path = Gtk::TreePath.new(@selected_row.to_s)
101
+ iter = @model.get_iter(path)
102
+ @tv.selection.select_iter(iter)
103
+ end
104
+ end
105
+
106
+ def initialize(main_window, item_list, jump_keys, select_callback, update_callback)
107
+ @window = Gtk::Window.new(:toplevel)
108
+ # @window.screen = main_window.screen
109
+ @window.title = "List Store"
110
+
111
+ @selected_row = 0
112
+
113
+ puts item_list.inspect
114
+ @update_callback = method(update_callback)
115
+ @select_callback = method(select_callback)
116
+ # puts @update_callback_m.call("").inspect
117
+
118
+ vbox = Gtk::Box.new(:vertical, 8)
119
+ vbox.margin = 8
120
+ @window.add(vbox)
121
+
122
+ @entry = Gtk::SearchEntry.new
123
+ @entry.width_chars = 45
124
+ container = Gtk::Box.new(:horizontal, 10)
125
+ # container.halign = :start
126
+ container.halign = :center
127
+ container.pack_start(@entry,
128
+ :expand => false, :fill => false, :padding => 0)
129
+
130
+ # create tree view
131
+ @model = Gtk::ListStore.new(String, String)
132
+ treeview = Gtk::TreeView.new(@model)
133
+ treeview.search_column = COLUMN_DESCRIPTION
134
+ @tv = treeview
135
+ # item_list = @update_callback.call("")
136
+ update_item_list(item_list)
137
+
138
+ # Ripl.start :binding => binding
139
+ @window.signal_connect("key-press-event") do |_widget, event|
140
+ # puts "KEYPRESS 1"
141
+ @entry.handle_event(event)
142
+ end
143
+
144
+ @entry.signal_connect("key_press_event") do |widget, event|
145
+ # puts "KEYPRESS 2"
146
+ if event.keyval == Gdk::Keyval::KEY_Down
147
+ puts "DOWN"
148
+ set_selected_row(@selected_row + 1)
149
+ # fixed = iter[COLUMN_FIXED]
150
+
151
+ true
152
+ elsif event.keyval == Gdk::Keyval::KEY_Up
153
+ set_selected_row(@selected_row - 1)
154
+ puts "UP"
155
+ true
156
+ elsif event.keyval == Gdk::Keyval::KEY_Return
157
+ path = Gtk::TreePath.new(@selected_row.to_s)
158
+ iter = @model.get_iter(path)
159
+ ret = iter[1]
160
+ @select_callback.call(ret, @selected_row)
161
+ @window.destroy
162
+ # puts iter[1].inspect
163
+ true
164
+ elsif event.keyval == Gdk::Keyval::KEY_Escape
165
+ @window.destroy
166
+ true
167
+ else
168
+ false
169
+ end
170
+ end
171
+
172
+ @entry.signal_connect("search-changed") do |widget|
173
+ puts "search changed: #{widget.text || ""}"
174
+ item_list = @update_callback.call(widget.text)
175
+ update_item_list(item_list)
176
+ # label.text = widget.text || ""
177
+ end
178
+ @entry.signal_connect("changed") { puts "[changed] " }
179
+ @entry.signal_connect("next-match") { puts "[next-match] " }
180
+
181
+ label = Gtk::Label.new(<<-EOF)
182
+
183
+ Search:
184
+ EOF
185
+ vbox.pack_start(label, :expand => false, :fill => false, :padding => 0)
186
+
187
+ vbox.pack_start(container, :expand => false, :fill => false, :padding => 0)
188
+ sw = Gtk::ScrolledWindow.new(nil, nil)
189
+ sw.shadow_type = :etched_in
190
+ sw.set_policy(:never, :automatic)
191
+ vbox.pack_start(sw, :expand => true, :fill => true, :padding => 0)
192
+
193
+ sw.add(treeview)
194
+
195
+ renderer = Gtk::CellRendererText.new
196
+ column = Gtk::TreeViewColumn.new("JMP",
197
+ renderer,
198
+ "text" => COLUMN_JUMP_KEY)
199
+ column.sort_column_id = COLUMN_JUMP_KEY
200
+ treeview.append_column(column)
201
+
202
+ renderer = Gtk::CellRendererText.new
203
+ column = Gtk::TreeViewColumn.new("Description",
204
+ renderer,
205
+ "text" => COLUMN_DESCRIPTION)
206
+ column.sort_column_id = COLUMN_DESCRIPTION
207
+ treeview.append_column(column)
208
+
209
+ @window.set_default_size(280, 500)
210
+ puts "SelectUpdateWindow"
211
+ end
212
+
213
+ def run
214
+ if !@window.visible?
215
+ @window.show_all
216
+ # add_spinner
217
+ else
218
+ @window.destroy
219
+ # GLib::Source.remove(@tiemout) unless @timeout.zero?
220
+ @timeout = 0
221
+ end
222
+ @window
223
+ end
224
+ end
225
+
226
+ def center_on_current_line()
227
+ b = $view.buffer
228
+ iter = b.get_iter_at(:offset => b.cursor_position)
229
+ within_margin = 0.0 #margin as a [0.0,0.5) fraction of screen size
230
+ use_align = true
231
+ xalign = 0.0 #0.0=top 1.0=bottom, 0.5=center
232
+ yalign = 0.5
233
+ $view.scroll_to_iter(iter, within_margin, use_align, xalign, yalign)
234
+ end
235
+
236
+ def qt_select_update_window(item_list, jump_keys, select_callback, update_callback)
237
+ $selup = SelectUpdateWindow.new(nil, item_list, jump_keys, select_callback, update_callback)
238
+ $selup.run
239
+ end
240
+
241
+ # ~/Drive/code/ruby-gnome/gtk3/sample/gtk-demo/search_entry2.rb
242
+ # ~/Drive/code/ruby-gnome/gtk3/sample/gtk-demo/list_store.rb
243
+
244
+ def qt_open_file_dialog(dirpath)
245
+ dialog = Gtk::FileChooserDialog.new(:title => "Open file",
246
+ :action => :open,
247
+ :buttons => [[Gtk::Stock::OPEN, :accept],
248
+ [Gtk::Stock::CANCEL, :cancel]])
249
+ dialog.set_current_folder(dirpath)
250
+
251
+ dialog.signal_connect("response") do |dialog, response_id|
252
+ if response_id == Gtk::ResponseType::ACCEPT
253
+ open_new_file(dialog.filename)
254
+ # puts "uri = #{dialog.uri}"
255
+ end
256
+ dialog.destroy
257
+ end
258
+ dialog.run
259
+ end
260
+
261
+ def qt_file_saveas(dirpath)
262
+ dialog = Gtk::FileChooserDialog.new(:title => "Save as",
263
+ :action => :save,
264
+ :buttons => [[Gtk::Stock::SAVE, :accept],
265
+ [Gtk::Stock::CANCEL, :cancel]])
266
+ dialog.set_current_folder(dirpath)
267
+ dialog.signal_connect("response") do |dialog, response_id|
268
+ if response_id == Gtk::ResponseType::ACCEPT
269
+ file_saveas(dialog.filename)
270
+ end
271
+ dialog.destroy
272
+ end
273
+
274
+ dialog.run
275
+ end
276
+
277
+ def qt_create_buffer(id)
278
+ puts "qt_create_buffer(#{id})"
279
+ buf1 = GtkSource::Buffer.new()
280
+ view = VSourceView.new()
281
+
282
+ view.set_highlight_current_line(true)
283
+ view.set_show_line_numbers(true)
284
+ view.set_buffer(buf1)
285
+
286
+ ssm = GtkSource::StyleSchemeManager.new
287
+ ssm.set_search_path(ssm.search_path << ppath("styles/"))
288
+ # sty = ssm.get_scheme("dark")
289
+ sty = ssm.get_scheme("molokai_edit")
290
+ # puts ssm.scheme_ids
291
+
292
+ view.buffer.highlight_matching_brackets = true
293
+ view.buffer.style_scheme = sty
294
+
295
+ provider = Gtk::CssProvider.new
296
+ provider.load(data: "textview { font-family: Monospace; font-size: 11pt; }")
297
+ # provider.load(data: "textview { font-family: Arial; font-size: 12pt; }")
298
+ view.style_context.add_provider(provider)
299
+ view.wrap_mode = :char
300
+
301
+ $vmag.buffers[id] = view
302
+ end
303
+
304
+ def gui_set_file_lang(id, lname)
305
+ view = $vmag.buffers[id]
306
+ lm = GtkSource::LanguageManager.new
307
+ lang = nil
308
+ lm.set_search_path(lm.search_path << ppath("lang/"))
309
+ lang = lm.get_language(lname)
310
+
311
+ view.buffer.language = lang
312
+ view.buffer.highlight_syntax = true
313
+ end
314
+
315
+ def qt_process_deltas
316
+ end
317
+
318
+ def qt_add_image(imgpath, pos)
319
+ end
320
+
321
+ def qt_process_deltas
322
+ end
323
+
324
+ def qt_process_events
325
+ end
326
+
327
+ def qt_select_window_close(arg = nil)
328
+ end
329
+
330
+ # def set_window_title(str)
331
+ # unimplemented
332
+ # end
333
+
334
+ def render_text(tmpbuf, pos, selection_start, reset)
335
+ unimplemented
336
+ end
337
+
338
+ def qt_set_buffer_contents(id, txt)
339
+ # $vbuf.set_text(txt)
340
+ puts "qt_set_buffer_contents(#{id}, txt)"
341
+
342
+ $vmag.buffers[id].buffer.set_text(txt)
343
+ end
344
+
345
+ def qt_set_cursor_pos(id, pos)
346
+ $view.set_cursor_pos(pos)
347
+ # Ripl.start :binding => binding
348
+ end
349
+
350
+ def qt_set_selection_start(id, selection_start)
351
+ end
352
+
353
+ def qt_set_current_buffer(id)
354
+ view = $vmag.buffers[id]
355
+ puts "qt_set_current_buffer(#{id}), view=#{view}"
356
+ buf1 = view.buffer
357
+ $vmag.view = view
358
+ $vmag.buf1 = buf1
359
+ $view = view
360
+ $vbuf = buf1
361
+
362
+ $vmag.sw.remove($vmag.sw.child) if !$vmag.sw.child.nil?
363
+ $vmag.sw.add(view)
364
+
365
+ view.grab_focus
366
+ #view.set_focus(10)
367
+ view.set_cursor_visible(true)
368
+ #view.move_cursor(1, 1, false)
369
+ view.place_cursor_onscreen
370
+
371
+ #TODO:
372
+ # itr = view.buffer.get_iter_at(:offset => 0)
373
+ # view.buffer.place_cursor(itr)
374
+
375
+ # wtitle = ""
376
+ # wtitle = buf.fname if !buf.fname.nil?
377
+ $vmag.sw.show_all
378
+ end
379
+
380
+ def gui_set_window_title(wtitle,subtitle="")
381
+ $vmag.window.title = wtitle
382
+ $vmag.window.titlebar.subtitle = subtitle
383
+ end
384
+
385
+ def unimplemented
386
+ puts "unimplemented"
387
+ end
388
+
389
+ def center_where_cursor
390
+ unimplemented
391
+ end
392
+
393
+ def paste_system_clipboard()
394
+ # clipboard = $vmag.window.get_clipboard(Gdk::Selection::CLIPBOARD)
395
+ utf8_string = Gdk::Atom.intern("UTF8_STRING")
396
+ # x = clipboard.request_contents(utf8_string)
397
+
398
+ widget = Gtk::Invisible.new
399
+ clipboard = Gtk::Clipboard.get_default($vmag.window.display)
400
+ received_text = ""
401
+
402
+ target_string = Gdk::Selection::TARGET_STRING
403
+ ti = clipboard.request_contents(target_string)
404
+
405
+ # clipboard.request_contents(target_string) do |_clipboard, selection_data|
406
+ # received_text = selection_data.text
407
+ # puts "received_text=#{received_text}"
408
+ # end
409
+ if clipboard.wait_is_text_available?
410
+ received_text = clipboard.wait_for_text
411
+ end
412
+
413
+ if received_text != "" and !received_text.nil?
414
+ max_clipboard_items = 100
415
+ if received_text != $clipboard[-1]
416
+ #TODO: HACK
417
+ $paste_lines = false
418
+ end
419
+ $clipboard << received_text
420
+ # puts $clipboard[-1]
421
+ $clipboard = $clipboard[-([$clipboard.size, max_clipboard_items].min)..-1]
422
+ end
423
+ return received_text
424
+ end
425
+
426
+ def set_system_clipboard(arg)
427
+ # return if arg.class != String
428
+ # return if s.size < 1
429
+ # utf8_string = Gdk::Atom.intern("UTF8_STRING")
430
+ widget = Gtk::Invisible.new
431
+ clipboard = Gtk::Clipboard.get_default($vmag.window.display)
432
+ clipboard.text = arg
433
+ end
434
+
435
+ def get_visible_area()
436
+ view = $view
437
+ vr = view.visible_rect
438
+ startpos = view.get_iter_at_position_raw(vr.x, vr.y)[1].offset
439
+ endpos = view.get_iter_at_position_raw(vr.x + vr.width, vr.y + vr.height)[1].offset
440
+ return [startpos, endpos]
441
+ end
442
+
443
+ def page_up
444
+ $view.signal_emit("move-cursor", Gtk::MovementStep.new(:PAGES), -1, false)
445
+ return true
446
+ end
447
+
448
+ def page_down
449
+ $view.signal_emit("move-cursor", Gtk::MovementStep.new(:PAGES), 1, false)
450
+ return true
451
+ end
452
+
453
+ # module Rbvma
454
+ # # Your code goes here...
455
+ # def foo
456
+ # puts "BAR"
457
+ # end
458
+ # end
459
+ $debug = true
460
+
461
+ def scan_indexes(txt, regex)
462
+ # indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) + 1 }
463
+ indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) }
464
+ return indexes
465
+ end
466
+
467
+ $update_cursor = false
468
+
469
+ class VSourceView < GtkSource::View
470
+ def initialize(title = nil)
471
+ # super(:toplevel)
472
+ super()
473
+ puts "vsource init"
474
+ @last_keyval = nil
475
+ @last_event = [nil, nil]
476
+
477
+ signal_connect "button-press-event" do |_widget, event|
478
+ if event.button == Gdk::BUTTON_PRIMARY
479
+ # puts "Gdk::BUTTON_PRIMARY"
480
+ false
481
+ elsif event.button == Gdk::BUTTON_SECONDARY
482
+ # puts "Gdk::BUTTON_SECONDARY"
483
+ true
484
+ else
485
+ true
486
+ end
487
+ end
488
+
489
+ signal_connect("key_press_event") do |widget, event|
490
+ handle_key_event(event, :key_press_event)
491
+ true
492
+ end
493
+
494
+ signal_connect("key_release_event") do |widget, event|
495
+ handle_key_event(event, :key_release_event)
496
+ true
497
+ end
498
+
499
+ signal_connect("move-cursor") do |widget, event|
500
+ $update_cursor = true
501
+ false
502
+ end
503
+
504
+ signal_connect "button-release-event" do |widget, event|
505
+ $buffer.set_pos(buffer.cursor_position)
506
+ false
507
+ end
508
+ @curpos_mark = nil
509
+ end
510
+
511
+ def handle_key_event(event, sig)
512
+ if $update_cursor
513
+ curpos = buffer.cursor_position
514
+ puts "MOVE CURSOR: #{curpos}"
515
+ buf.set_pos(curpos)
516
+ $update_cursor = false
517
+ end
518
+ puts $view.visible_rect.inspect
519
+
520
+ puts "key event"
521
+ puts event
522
+
523
+ key_name = event.string
524
+ if event.state.control_mask?
525
+ key_name = Gdk::Keyval.to_name(event.keyval)
526
+ # Gdk::Keyval.to_name()
527
+ end
528
+
529
+ keyval_trans = {}
530
+ keyval_trans[Gdk::Keyval::KEY_Control_L] = "ctrl"
531
+ keyval_trans[Gdk::Keyval::KEY_Control_R] = "ctrl"
532
+
533
+ keyval_trans[Gdk::Keyval::KEY_Escape] = "esc"
534
+
535
+ keyval_trans[Gdk::Keyval::KEY_Return] = "enter"
536
+ keyval_trans[Gdk::Keyval::KEY_ISO_Enter] = "enter"
537
+ keyval_trans[Gdk::Keyval::KEY_KP_Enter] = "enter"
538
+ keyval_trans[Gdk::Keyval::KEY_Alt_L] = "alt"
539
+ keyval_trans[Gdk::Keyval::KEY_Alt_R] = "alt"
540
+
541
+ keyval_trans[Gdk::Keyval::KEY_BackSpace] = "backspace"
542
+ keyval_trans[Gdk::Keyval::KEY_KP_Page_Down] = "pagedown"
543
+ keyval_trans[Gdk::Keyval::KEY_KP_Page_Up] = "pageup"
544
+ keyval_trans[Gdk::Keyval::KEY_Page_Down] = "pagedown"
545
+ keyval_trans[Gdk::Keyval::KEY_Page_Up] = "pageup"
546
+ keyval_trans[Gdk::Keyval::KEY_Left] = "left"
547
+ keyval_trans[Gdk::Keyval::KEY_Right] = "right"
548
+ keyval_trans[Gdk::Keyval::KEY_Down] = "down"
549
+ keyval_trans[Gdk::Keyval::KEY_Up] = "up"
550
+ keyval_trans[Gdk::Keyval::KEY_space] = "space"
551
+
552
+ keyval_trans[Gdk::Keyval::KEY_Shift_L] = "shift"
553
+ keyval_trans[Gdk::Keyval::KEY_Shift_R] = "shift"
554
+ keyval_trans[Gdk::Keyval::KEY_Tab] = "tab"
555
+
556
+ key_trans = {}
557
+ key_trans["\e"] = "esc"
558
+ tk = keyval_trans[event.keyval]
559
+ key_name = tk if !tk.nil?
560
+
561
+ key_str_parts = []
562
+ key_str_parts << "ctrl" if event.state.control_mask? and key_name != "ctrl"
563
+ key_str_parts << "alt" if event.state.mod1_mask? and key_name != "alt"
564
+
565
+ key_str_parts << key_name
566
+ key_str = key_str_parts.join("-")
567
+ keynfo = { :key_str => key_str, :key_name => key_name, :keyval => event.keyval }
568
+ puts keynfo.inspect
569
+ # $kbd.match_key_conf(key_str, nil, :key_press)
570
+ # puts "key_str=#{key_str} key_"
571
+
572
+ if key_str != "" # or prefixed_key_str != ""
573
+ if sig == :key_release_event and event.keyval == @last_keyval
574
+ $kbd.match_key_conf(key_str + "!", nil, :key_release)
575
+ @last_event = [event, :key_release]
576
+ elsif sig == :key_press_event
577
+ $kbd.match_key_conf(key_str, nil, :key_press)
578
+ @last_event = [event, key_str, :key_press]
579
+ end
580
+ @last_keyval = event.keyval #TODO: outside if?
581
+ end
582
+
583
+ handle_deltas
584
+
585
+ # set_focus(5)
586
+ # false
587
+
588
+ end
589
+
590
+ def pos_to_coord(i)
591
+ b = buffer
592
+ iter = b.get_iter_at(:offset => i)
593
+ iterxy = get_iter_location(iter)
594
+ winw = parent_window.width
595
+ view_width = visible_rect.width
596
+ gutter_width = winw - view_width
597
+
598
+ x = iterxy.x + gutter_width
599
+ y = iterxy.y
600
+
601
+ # buffer_to_window_coords(Gtk::TextWindowType::TEXT, iterxy.x, iterxy.y).inspect
602
+ # puts buffer_to_window_coords(Gtk::TextWindowType::TEXT, x, y).inspect
603
+ (x, y) = buffer_to_window_coords(Gtk::TextWindowType::TEXT, x, y)
604
+ # Ripl.start :binding => binding
605
+
606
+ return [x, y]
607
+ end
608
+
609
+ def handle_deltas()
610
+ any_change = false
611
+ while d = buf.deltas.shift
612
+ any_change = true
613
+ pos = d[0]
614
+ op = d[1]
615
+ num = d[2]
616
+ txt = d[3]
617
+ if op == DELETE
618
+ startiter = buffer.get_iter_at(:offset => pos)
619
+ enditer = buffer.get_iter_at(:offset => pos + num)
620
+ buffer.delete(startiter, enditer)
621
+ elsif op == INSERT
622
+ startiter = buffer.get_iter_at(:offset => pos)
623
+ buffer.insert(startiter, txt)
624
+ end
625
+ end
626
+ if any_change
627
+ qt_set_cursor_pos($buffer.id, $buffer.pos) #TODO: only when necessary
628
+ end
629
+
630
+ # sanity_check #TODO
631
+ end
632
+
633
+ def sanity_check()
634
+ a = buffer.text
635
+ b = buf.to_s
636
+ # puts "===================="
637
+ # puts a.lines[0..10].join()
638
+ # puts "===================="
639
+ # puts b.lines[0..10].join()
640
+ # puts "===================="
641
+ if a == b
642
+ puts "Buffers match"
643
+ else
644
+ puts "ERROR: Buffer's don't match."
645
+ end
646
+ end
647
+
648
+ def set_cursor_pos(pos)
649
+ # return
650
+ itr = buffer.get_iter_at(:offset => pos)
651
+ itr2 = buffer.get_iter_at(:offset => pos + 1)
652
+ buffer.place_cursor(itr)
653
+
654
+ # $view.signal_emit("extend-selection", Gtk::MovementStep.new(:PAGES), -1, false)
655
+
656
+ within_margin = 0.075 #margin as a [0.0,0.5) fraction of screen size
657
+ use_align = false
658
+ xalign = 0.5 #0.0=top 1.0=bottom, 0.5=center
659
+ yalign = 0.5
660
+
661
+ if @curpos_mark.nil?
662
+ @curpos_mark = buffer.create_mark("cursor", itr, false)
663
+ else
664
+ buffer.move_mark(@curpos_mark, itr)
665
+ end
666
+ scroll_to_mark(@curpos_mark, within_margin, use_align, xalign, yalign)
667
+ $idle_scroll_to_mark = true
668
+ ensure_cursor_visible
669
+
670
+ # scroll_to_iter(itr, within_margin, use_align, xalign, yalign)
671
+
672
+ # $view.signal_emit("extend-selection", Gtk::TextExtendSelection.new, itr,itr,itr2)
673
+ # Ripl.start :binding => binding
674
+ draw_cursor
675
+
676
+ return true
677
+ end
678
+
679
+ def cursor_visible_idle_func
680
+ puts "cursor_visible_idle_func"
681
+ # From https://picheta.me/articles/2013/08/gtk-plus--a-method-to-guarantee-scrolling.html
682
+ # vr = visible_rect
683
+
684
+ # b = $view.buffer
685
+ # iter = buffer.get_iter_at(:offset => buffer.cursor_position)
686
+ # iterxy = get_iter_location(iter)
687
+
688
+ sleep(0.01)
689
+ # intr = iterxy.intersect(vr)
690
+ if is_cursor_visible == false
691
+ # set_cursor_pos(buffer.cursor_position)
692
+
693
+ itr = buffer.get_iter_at(:offset => buffer.cursor_position)
694
+
695
+ within_margin = 0.075 #margin as a [0.0,0.5) fraction of screen size
696
+ use_align = false
697
+ xalign = 0.5 #0.0=top 1.0=bottom, 0.5=center
698
+ yalign = 0.5
699
+
700
+ scroll_to_iter(itr, within_margin, use_align, xalign, yalign)
701
+
702
+ # return true # Call this func again
703
+ else
704
+ return false # Don't call this idle func again
705
+ end
706
+ end
707
+
708
+ def is_cursor_visible
709
+ vr = visible_rect
710
+ iter = buffer.get_iter_at(:offset => buffer.cursor_position)
711
+ iterxy = get_iter_location(iter)
712
+ iterxy.width = 1 if iterxy.width == 0
713
+ iterxy.height = 1 if iterxy.height == 0
714
+
715
+ intr = iterxy.intersect(vr)
716
+ if intr.nil?
717
+ puts iterxy.inspect
718
+ puts vr.inspect
719
+ # Ripl.start :binding => binding
720
+
721
+ # exit!
722
+ return false
723
+ else
724
+ return true
725
+ end
726
+ end
727
+
728
+ def ensure_cursor_visible
729
+ if is_cursor_visible == false
730
+ Thread.new {
731
+ sleep 0.01
732
+ GLib::Idle.add(proc { cursor_visible_idle_func })
733
+ }
734
+ end
735
+ end
736
+
737
+ def draw_cursor
738
+ if is_command_mode
739
+ itr = buffer.get_iter_at(:offset => buf.pos)
740
+ itr2 = buffer.get_iter_at(:offset => buf.pos + 1)
741
+ $view.buffer.select_range(itr, itr2)
742
+ elsif buf.visual_mode?
743
+ puts "VISUAL MODE"
744
+ (_start, _end) = buf.get_visual_mode_range2
745
+ puts "#{_start}, #{_end}"
746
+ itr = buffer.get_iter_at(:offset => _start)
747
+ itr2 = buffer.get_iter_at(:offset => _end + 1)
748
+ $view.buffer.select_range(itr, itr2)
749
+ else # Insert mode
750
+ itr = buffer.get_iter_at(:offset => buf.pos)
751
+ $view.buffer.select_range(itr, itr)
752
+ puts "INSERT MODE"
753
+ end
754
+ end
755
+
756
+ # def quit
757
+ # destroy
758
+ # true
759
+ # end
760
+ end
761
+
762
+ class VMAg
763
+ attr_accessor :buffers, :sw, :view, :buf1, :window
764
+
765
+ VERSION = "1.0"
766
+
767
+ HEART = "♥"
768
+ RADIUS = 150
769
+ N_WORDS = 5
770
+ FONT = "Serif 18"
771
+ TEXT = "I ♥ GTK+"
772
+
773
+ def initialize()
774
+ @show_overlay = true
775
+ @da = nil
776
+ @buffers = {}
777
+ @view = nil
778
+ @buf1 = nil
779
+ end
780
+
781
+ def run
782
+ init_window
783
+ # init_rtext
784
+ Gtk.main
785
+ end
786
+
787
+ def start_overlay_draw()
788
+ @da = Gtk::Fixed.new
789
+ @overlay.add_overlay(@da)
790
+ @overlay.set_overlay_pass_through(@da, true)
791
+ end
792
+
793
+ def clear_overlay()
794
+ if @da != nil
795
+ @overlay.remove(@da)
796
+ end
797
+ end
798
+
799
+ def overlay_draw_text(text, textpos)
800
+ # puts "overlay_draw_text #{[x,y]}"
801
+ (x, y) = @view.pos_to_coord(textpos)
802
+ # puts "overlay_draw_text #{[x,y]}"
803
+ label = Gtk::Label.new("<span background='#00000088' foreground='#ff0000' weight='ultrabold'>#{text}</span>")
804
+ label.use_markup = true
805
+ @da.put(label, x, y)
806
+ end
807
+
808
+ def end_overlay_draw()
809
+ @da.show_all
810
+ end
811
+
812
+ def toggle_overlay
813
+ @show_overlay = @show_overlay ^ 1
814
+ if !@show_overlay
815
+ if @da != nil
816
+ @overlay.remove(@da)
817
+ end
818
+ return
819
+ else
820
+ @da = Gtk::Fixed.new
821
+ @overlay.add_overlay(@da)
822
+ @overlay.set_overlay_pass_through(@da, true)
823
+ end
824
+
825
+ (startpos, endpos) = get_visible_area
826
+ s = @view.buffer.text
827
+ wpos = s.enum_for(:scan, /\W(\w)/).map { Regexp.last_match.begin(0) + 1 }
828
+ wpos = wpos[0..130]
829
+
830
+ # vr = @view.visible_rect
831
+ # # gtk_text_view_get_line_at_y
832
+ # # gtk_text_view_get_iter_at_position
833
+ # gtk_text_view_get_iter_at_position(vr.
834
+ # istart = @view.get_iter_at_position(vr.x,vr.y)
835
+ # istart = @view.get_iter_at_y(vr.y)
836
+ # startpos = @view.get_iter_at_position_raw(vr.x,vr.y)[1].offset
837
+ # endpos = @view.get_iter_at_position_raw(vr.x+vr.width,vr.y+vr.height)[1].offset
838
+ # puts "startpos,endpos:#{[startpos, endpos]}"
839
+
840
+ da = @da
841
+ if false
842
+ da.signal_connect "draw" do |widget, cr|
843
+ cr.save
844
+ for pos in wpos
845
+ (x, y) = @view.pos_to_coord(pos)
846
+
847
+ layout = da.create_pango_layout("XY")
848
+ desc = Pango::FontDescription.new("sans bold 11")
849
+ layout.font_description = desc
850
+
851
+ cr.move_to(x, y)
852
+ # cr.move_to(gutter_width, 300)
853
+ cr.pango_layout_path(layout)
854
+
855
+ cr.set_source_rgb(1.0, 0.0, 0.0)
856
+ cr.fill_preserve
857
+ end
858
+ cr.restore
859
+ false # = draw other
860
+ # true # = Don't draw others
861
+ end
862
+ end
863
+
864
+ for pos in wpos
865
+ (x, y) = @view.pos_to_coord(pos)
866
+ # da.put(Gtk::Label.new("AB"), x, y)
867
+ label = Gtk::Label.new("<span background='#00000088' foreground='#ff0000' weight='ultrabold'>AB</span>")
868
+ label.use_markup = true
869
+ da.put(label, x, y)
870
+ end
871
+
872
+ # puts @view.pos_to_coord(300).inspect
873
+
874
+ @da.show_all
875
+ end
876
+
877
+ def init_keybindings
878
+ $kbd = KeyBindingTree.new()
879
+ $kbd.add_mode("C", :command)
880
+ $kbd.add_mode("I", :insert)
881
+ $kbd.add_mode("V", :visual)
882
+ $kbd.add_mode("M", :minibuffer)
883
+ $kbd.add_mode("R", :readchar)
884
+ $kbd.add_mode("B", :browse)
885
+ $kbd.set_default_mode(:command)
886
+ require "default_key_bindings"
887
+
888
+ $macro = Macro.new
889
+
890
+ # bindkey "VC j", "buf.move(FORWARD_LINE)"
891
+ bindkey "VC j", "puts('j_key_action')"
892
+ bindkey "VC ctrl-j", "puts('ctrl_j_key_action')"
893
+
894
+ bindkey "VC l", "buf.move(FORWARD_CHAR)"
895
+ bindkey "C x", "buf.delete(CURRENT_CHAR_FORWARD)"
896
+ # bindkey "C r <char>", "buf.replace_with_char(<char>)"
897
+ bindkey "I space", 'buf.insert_txt(" ")'
898
+
899
+ bindkey "VC l", "buf.move(FORWARD_CHAR)"
900
+ bindkey "VC j", "buf.move(FORWARD_LINE)"
901
+ bindkey "VC k", "buf.move(BACKWARD_LINE)"
902
+ bindkey "VC h", "buf.move(BACKWARD_CHAR)"
903
+ end
904
+
905
+ def handle_deltas()
906
+ while d = buf.deltas.shift
907
+ pos = d[0]
908
+ op = d[1]
909
+ num = d[2]
910
+ txt = d[3]
911
+ if op == DELETE
912
+ startiter = @buf1.get_iter_at(:offset => pos)
913
+ enditer = @buf1.get_iter_at(:offset => pos + num)
914
+ @buf1.delete(startiter, enditer)
915
+ elsif op == INSERT
916
+ startiter = @buf1.get_iter_at(:offset => pos)
917
+ @buf1.insert(startiter, txt)
918
+ end
919
+ end
920
+ end
921
+
922
+ def add_to_minibuf(msg)
923
+ startiter = @minibuf.buffer.get_iter_at(:offset => 0)
924
+ @minibuf.buffer.insert(startiter, "#{msg}\n")
925
+ @minibuf.signal_emit("move-cursor", Gtk::MovementStep.new(:PAGES), -1, false)
926
+ end
927
+
928
+ def init_minibuffer()
929
+ # Init minibuffer
930
+ sw = Gtk::ScrolledWindow.new
931
+ sw.set_policy(:automatic, :automatic)
932
+ overlay = Gtk::Overlay.new
933
+ overlay.add(sw)
934
+ @vpaned.pack2(overlay, :resize => false)
935
+ # overlay.set_size_request(-1, 50)
936
+ # $ovrl = overlay
937
+ # $ovrl.set_size_request(-1, 30)
938
+ $sw2 = sw
939
+ sw.set_size_request(-1, 12)
940
+
941
+ view = VSourceView.new()
942
+ view.set_highlight_current_line(false)
943
+ view.set_show_line_numbers(false)
944
+ # view.set_buffer(buf1)
945
+ ssm = GtkSource::StyleSchemeManager.new
946
+ ssm.set_search_path(ssm.search_path << ppath("styles/"))
947
+ sty = ssm.get_scheme("molokai_edit")
948
+ view.buffer.highlight_matching_brackets = false
949
+ view.buffer.style_scheme = sty
950
+ provider = Gtk::CssProvider.new
951
+ # provider.load(data: "textview { font-family: Monospace; font-size: 11pt; }")
952
+ provider.load(data: "textview { font-family: Arial; font-size: 10pt; color:#ff0000}")
953
+ view.style_context.add_provider(provider)
954
+ view.wrap_mode = :char
955
+ @minibuf = view
956
+ # Ripl.start :binding => binding
957
+ # startiter = view.buffer.get_iter_at(:offset => 0)
958
+ message("STARTUP")
959
+ sw.add(view)
960
+ end
961
+
962
+ def init_header_bar()
963
+
964
+ header = Gtk::HeaderBar.new
965
+ @header = header
966
+ header.show_close_button = true
967
+ header.title = ""
968
+ header.has_subtitle = true
969
+ header.subtitle = ""
970
+ # Ripl.start :binding => binding
971
+
972
+
973
+ # icon = Gio::ThemedIcon.new("mail-send-receive-symbolic")
974
+ # icon = Gio::ThemedIcon.new("document-open-symbolic")
975
+ # icon = Gio::ThemedIcon.new("dialog-password")
976
+
977
+ #edit-redo edit-paste edit-find-replace edit-undo edit-find edit-cut edit-copy
978
+ #document-open document-save document-save-as document-properties document-new
979
+ # document-revert-symbolic
980
+ #
981
+
982
+ #TODO:
983
+ # button = Gtk::Button.new
984
+ # icon = Gio::ThemedIcon.new("open-menu-symbolic")
985
+ # image = Gtk::Image.new(:icon => icon, :size => :button)
986
+ # button.add(image)
987
+ # header.pack_end(button)
988
+
989
+ button = Gtk::Button.new
990
+ icon = Gio::ThemedIcon.new("document-open-symbolic")
991
+ image = Gtk::Image.new(:icon => icon, :size => :button)
992
+ button.add(image)
993
+ header.pack_end(button)
994
+
995
+ button.signal_connect "clicked" do |_widget|
996
+ open_file_dialog
997
+ end
998
+
999
+ button = Gtk::Button.new
1000
+ icon = Gio::ThemedIcon.new("document-save-symbolic")
1001
+ image = Gtk::Image.new(:icon => icon, :size => :button)
1002
+ button.add(image)
1003
+ header.pack_end(button)
1004
+ button.signal_connect "clicked" do |_widget|
1005
+ buf.save
1006
+ end
1007
+
1008
+ button = Gtk::Button.new
1009
+ icon = Gio::ThemedIcon.new("document-new-symbolic")
1010
+ image = Gtk::Image.new(:icon => icon, :size => :button)
1011
+ button.add(image)
1012
+ header.pack_end(button)
1013
+ button.signal_connect "clicked" do |_widget|
1014
+ create_new_file
1015
+ end
1016
+
1017
+ box = Gtk::Box.new(:horizontal, 0)
1018
+ box.style_context.add_class("linked")
1019
+
1020
+ button = Gtk::Button.new
1021
+ image = Gtk::Image.new(:icon_name => "pan-start-symbolic", :size => :button)
1022
+ button.add(image)
1023
+ box.add(button)
1024
+ button.signal_connect "clicked" do |_widget|
1025
+ history_switch_backwards
1026
+ end
1027
+
1028
+ button = Gtk::Button.new
1029
+ image = Gtk::Image.new(:icon_name => "pan-end-symbolic", :size => :button)
1030
+ button.add(image)
1031
+ box.add(button)
1032
+ button.signal_connect "clicked" do |_widget|
1033
+ history_switch_forwards
1034
+ end
1035
+
1036
+ button = Gtk::Button.new
1037
+ icon = Gio::ThemedIcon.new("window-close-symbolic")
1038
+ image = Gtk::Image.new(:icon => icon, :size => :button)
1039
+ button.add(image)
1040
+ box.add(button)
1041
+ button.signal_connect "clicked" do |_widget|
1042
+ bufs.close_current_buffer
1043
+ end
1044
+
1045
+ header.pack_start(box)
1046
+ @window.titlebar = header
1047
+ @window.add(Gtk::TextView.new)
1048
+ end
1049
+
1050
+ def init_window
1051
+ @window = Gtk::Window.new(:toplevel)
1052
+ @window.set_default_size(650, 850)
1053
+ @window.title = "Multiple Views"
1054
+ @window.show_all
1055
+ # vpaned = Gtk::Paned.new(:horizontal)
1056
+ @vpaned = Gtk::Paned.new(:vertical)
1057
+ @window.add(@vpaned)
1058
+
1059
+ @sw = Gtk::ScrolledWindow.new
1060
+ @sw.set_policy(:automatic, :automatic)
1061
+ @overlay = Gtk::Overlay.new
1062
+ @overlay.add(@sw)
1063
+ @vpaned.pack1(@overlay, :resize => true)
1064
+
1065
+ init_minibuffer
1066
+ init_header_bar
1067
+
1068
+ @window.show_all
1069
+
1070
+ vma.start
1071
+ end
1072
+ end