vimamsa 0.1.22 → 0.1.24

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +32 -0
  3. data/Dockerfile +45 -0
  4. data/README.md +2 -2
  5. data/custom_example.rb +38 -9
  6. data/docker_cmd.sh +7 -0
  7. data/exe/run_tests.rb +23 -0
  8. data/img/screenshot1.png +0 -0
  9. data/img/screenshot2.png +0 -0
  10. data/lib/vimamsa/actions.rb +8 -0
  11. data/lib/vimamsa/buffer.rb +165 -53
  12. data/lib/vimamsa/buffer_changetext.rb +68 -14
  13. data/lib/vimamsa/buffer_cursor.rb +9 -3
  14. data/lib/vimamsa/buffer_list.rb +14 -28
  15. data/lib/vimamsa/buffer_manager.rb +1 -1
  16. data/lib/vimamsa/conf.rb +33 -1
  17. data/lib/vimamsa/diff_buffer.rb +185 -0
  18. data/lib/vimamsa/editor.rb +149 -80
  19. data/lib/vimamsa/file_finder.rb +6 -2
  20. data/lib/vimamsa/gui.rb +330 -135
  21. data/lib/vimamsa/gui_dialog.rb +2 -0
  22. data/lib/vimamsa/gui_file_panel.rb +94 -0
  23. data/lib/vimamsa/gui_form_generator.rb +4 -2
  24. data/lib/vimamsa/gui_func_panel.rb +127 -0
  25. data/lib/vimamsa/gui_image.rb +2 -4
  26. data/lib/vimamsa/gui_menu.rb +54 -1
  27. data/lib/vimamsa/gui_select_window.rb +18 -6
  28. data/lib/vimamsa/gui_settings.rb +486 -0
  29. data/lib/vimamsa/gui_sourceview.rb +196 -8
  30. data/lib/vimamsa/gui_text.rb +0 -22
  31. data/lib/vimamsa/hyper_plain_text.rb +1 -0
  32. data/lib/vimamsa/key_actions.rb +54 -31
  33. data/lib/vimamsa/key_binding_tree.rb +154 -8
  34. data/lib/vimamsa/key_bindings_vimlike.rb +48 -35
  35. data/lib/vimamsa/langservp.rb +161 -7
  36. data/lib/vimamsa/macro.rb +54 -7
  37. data/lib/vimamsa/main.rb +1 -0
  38. data/lib/vimamsa/rbvma.rb +5 -0
  39. data/lib/vimamsa/string_util.rb +56 -0
  40. data/lib/vimamsa/test_framework.rb +137 -0
  41. data/lib/vimamsa/util.rb +3 -36
  42. data/lib/vimamsa/version.rb +1 -1
  43. data/modules/calculator/calculator.rb +318 -0
  44. data/modules/calculator/calculator_info.rb +3 -0
  45. data/modules/terminal/terminal.rb +140 -0
  46. data/modules/terminal/terminal_info.rb +3 -0
  47. data/run_tests.rb +89 -0
  48. data/styles/dark.xml +1 -1
  49. data/styles/molokai_edit.xml +2 -2
  50. data/tests/key_bindings.rb +2 -0
  51. data/tests/test_basic_editing.rb +86 -0
  52. data/tests/test_copy_paste.rb +88 -0
  53. data/tests/test_key_bindings.rb +152 -0
  54. data/tests/test_module_interface.rb +98 -0
  55. data/tests/test_undo.rb +201 -0
  56. data/vimamsa.gemspec +6 -5
  57. metadata +52 -14
@@ -0,0 +1,486 @@
1
+ SETTINGS_DEFS = [
2
+ {
3
+ :label => "Tab / Indent",
4
+ :settings => [
5
+ { :key => [:tab, :width], :label => "Tab width", :type => :int, :min => 1, :max => 16, :step => 1 },
6
+ { :key => [:tab, :to_spaces_default], :label => "Use spaces instead of tabs (default)", :type => :bool },
7
+ { :key => [:indent_based_on_last_line], :label => "Auto-indent based on last line", :type => :bool },
8
+ ],
9
+ },
10
+ {
11
+ :label => "Font",
12
+ :settings => [
13
+ { :key => [:font, :family], :label => "Font family", :type => :string },
14
+ { :key => [:font, :size], :label => "Font size (pt)", :type => :int, :min => 4, :max => 72, :step => 1 },
15
+ ],
16
+ },
17
+ {
18
+ :label => "Appearance",
19
+ :settings => [
20
+ { :key => [:match, :highlight, :color], :label => "Search highlight color", :type => :string },
21
+ { :key => [:kbd, :show_prev_action], :label => "Show previous action in toolbar", :type => :bool },
22
+ { :key => [:style_scheme], :label => "Color scheme", :type => :select,
23
+ :options => proc {
24
+ ssm = GtkSource::StyleSchemeManager.new
25
+ ssm.set_search_path(ssm.search_path << ppath("styles/"))
26
+ ssm.scheme_ids.reject { |id| [VIMAMSA_OVERLAY_SCHEME_ID, VIMAMSA_CONTRAST_SCHEME_ID].include?(id) }.sort
27
+ } },
28
+ { :key => [:color_contrast], :label => "Color scheme contrast (1.0 = original)", :type => :float, :min => 0.5, :max => 2.0, :step => 0.05 },
29
+ { :key => [:color_brightness], :label => "Color scheme brightness (0.0 = original)", :type => :float, :min => -0.5, :max => 0.5, :step => 0.01 },
30
+ ],
31
+ },
32
+ {
33
+ :label => "Behavior",
34
+ :settings => [
35
+ { :key => [:lsp, :enabled], :label => "Enable LSP (Language Server)", :type => :bool },
36
+ { :key => [:experimental], :label => "Enable experimental features", :type => :bool },
37
+ { :key => [:macro, :animation_delay], :label => "Macro animation delay (sec)", :type => :float, :min => 0.0, :max => 2.0, :step => 0.0001 },
38
+ { :key => [:paste, :cursor_at_start], :label => "Leave cursor at start of pasted text", :type => :bool },
39
+ ],
40
+ },
41
+ {
42
+ :label => "Files",
43
+ :settings => [
44
+ { :key => ["search_dirs"], :label => "Search directories (one per line)", :type => :string_list },
45
+ ],
46
+ },
47
+ ]
48
+
49
+ # Build the full settings definition list, appending a Modules section
50
+ # dynamically from whatever subdirectories exist in modules/.
51
+ def all_settings_defs
52
+ # Build one settings entry per module found in modules/*/.
53
+ # Each module may provide a <name>_info.rb file that defines <name>_info,
54
+ # returning a hash with :name (display label) and :no_restart (bool).
55
+ # Modules without an info file fall back to a capitalized directory name
56
+ # and require a restart to activate.
57
+ module_settings = Dir.glob(ppath("modules/*/")).sort.map do |mod_dir|
58
+ mod_name = File.basename(mod_dir)
59
+ info_file = File.join(mod_dir, "#{mod_name}_info.rb")
60
+ load info_file if File.exist?(info_file)
61
+ info_fn = "#{mod_name}_info"
62
+ info = respond_to?(info_fn, true) ? send(info_fn) : {}
63
+ label = info[:name] || mod_name.split(/[_\-]/).map(&:capitalize).join(" ")
64
+ no_restart = info[:no_restart] || false
65
+ restart_note = no_restart ? "" : " (requires restart)"
66
+ { :key => [:modules, mod_name.to_sym, :enabled], :label => "#{label}#{restart_note}", :type => :bool, :no_restart => no_restart }
67
+ end
68
+ return SETTINGS_DEFS unless module_settings.any?
69
+ SETTINGS_DEFS + [{ :label => "Modules", :settings => module_settings }]
70
+ end
71
+
72
+ class SettingsDialog
73
+ def initialize
74
+ @widgets = {}
75
+ @window = Gtk::Window.new
76
+ @window.set_transient_for($vmag.window) if $vmag&.window
77
+ @window.modal = true
78
+ @window.title = "Preferences"
79
+ @window.default_width = 500
80
+
81
+ outer = Gtk::Box.new(:vertical, 12)
82
+ # outer.margin = 16
83
+ @window.set_child(outer)
84
+
85
+ notebook = Gtk::Notebook.new
86
+ outer.append(notebook)
87
+
88
+ all_settings_defs.each do |section|
89
+ grid = Gtk::Grid.new
90
+ grid.row_spacing = 10
91
+ grid.column_spacing = 16
92
+ grid.margin_top = 12
93
+ grid.margin_bottom = 12
94
+ grid.margin_start = 12
95
+ grid.margin_end = 12
96
+
97
+ grid_row = 0
98
+ section[:settings].each do |s|
99
+ label = Gtk::Label.new(s[:label])
100
+ label.halign = :start
101
+ label.hexpand = true
102
+
103
+ if s[:type] == :string_list
104
+ # Label spans both columns; editor on the next row spanning both columns
105
+ grid.attach(label, 0, grid_row, 2, 1)
106
+ grid_row += 1
107
+ cur = get(s[:key])
108
+ container, get_value = build_string_list_editor(cur.is_a?(Array) ? cur : [])
109
+ @widgets[s[:key]] = { :widget => container, :type => s[:type], :get_value => get_value }
110
+ grid.attach(container, 0, grid_row, 2, 1)
111
+ else
112
+ widget = make_widget(s)
113
+ # Store prev_value so save_and_close can detect newly-enabled modules.
114
+ @widgets[s[:key]] = { :widget => widget, :type => s[:type], :no_restart => s[:no_restart], :prev_value => cnf_get(s[:key]) }
115
+ grid.attach(label, 0, grid_row, 1, 1)
116
+ grid.attach(widget, 1, grid_row, 1, 1)
117
+ end
118
+ grid_row += 1
119
+ end
120
+
121
+ notebook.append_page(grid, Gtk::Label.new(section[:label]))
122
+ end
123
+
124
+ hbox = Gtk::Box.new(:horizontal, 8)
125
+ hbox.halign = :end
126
+
127
+ cancel_btn = Gtk::Button.new(:label => "Cancel")
128
+ save_btn = Gtk::Button.new(:label => "Save")
129
+ cancel_btn.signal_connect("clicked") { @window.destroy }
130
+ save_btn.signal_connect("clicked") { save_and_close }
131
+
132
+ hbox.append(cancel_btn)
133
+ hbox.append(save_btn)
134
+ outer.append(hbox)
135
+
136
+ press = Gtk::EventControllerKey.new
137
+ press.set_propagation_phase(Gtk::PropagationPhase::CAPTURE)
138
+ @window.add_controller(press)
139
+ press.signal_connect("key-pressed") do |_g, keyval, _kc, _y|
140
+ if keyval == Gdk::Keyval::KEY_Escape
141
+ @window.destroy
142
+ true
143
+ else
144
+ false
145
+ end
146
+ end
147
+ end
148
+
149
+ def make_widget(s)
150
+ cur = cnf_get(s[:key])
151
+ case s[:type]
152
+ when :bool
153
+ w = Gtk::Switch.new
154
+ w.active = cur == true
155
+ w.valign = :center
156
+ w
157
+ when :int
158
+ adj = Gtk::Adjustment.new(cur.to_f, s[:min].to_f, s[:max].to_f, s[:step].to_f, s[:step].to_f * 5, 0.0)
159
+ Gtk::SpinButton.new(adj, s[:step].to_f, 0)
160
+ when :float
161
+ adj = Gtk::Adjustment.new(cur.to_f, s[:min].to_f, s[:max].to_f, s[:step].to_f, s[:step].to_f * 5, 0.0)
162
+ digits = s[:step].to_s.split(".").last.to_s.length
163
+ digits = 2 if digits < 2
164
+ Gtk::SpinButton.new(adj, s[:step].to_f, digits)
165
+ when :string
166
+ w = Gtk::Entry.new
167
+ w.text = cur.to_s
168
+ w
169
+ when :select
170
+ options = s[:options].is_a?(Proc) ? s[:options].call : s[:options]
171
+ cur = cnf_get(s[:key]).to_s
172
+ string_list = Gtk::StringList.new(options)
173
+ w = Gtk::DropDown.new(string_list, nil)
174
+ w.selected = [options.index(cur) || 0, 0].max
175
+ w
176
+ end
177
+ end
178
+
179
+ def build_string_list_editor(paths)
180
+ store = Gtk::ListStore.new(String)
181
+ paths.each { |p| store.append[0] = p }
182
+
183
+ tv = Gtk::TreeView.new(store)
184
+ tv.headers_visible = false
185
+ renderer = Gtk::CellRendererText.new
186
+ renderer.ellipsize = Pango::EllipsizeMode::START
187
+ col = Gtk::TreeViewColumn.new("", renderer, text: 0)
188
+ col.expand = true
189
+ tv.append_column(col)
190
+
191
+ sw = Gtk::ScrolledWindow.new
192
+ sw.set_policy(:automatic, :automatic)
193
+ sw.set_child(tv)
194
+ sw.set_size_request(-1, 120)
195
+ sw.vexpand = true
196
+
197
+ add_btn = Gtk::Button.new(label: "Add…")
198
+ add_btn.signal_connect("clicked") do
199
+ chooser = Gtk::FileChooserDialog.new(
200
+ :title => "Select directory",
201
+ :action => :select_folder,
202
+ :buttons => [["Select", :accept], ["Cancel", :cancel]],
203
+ )
204
+ chooser.set_transient_for(@window)
205
+ chooser.modal = true
206
+ chooser.signal_connect("response") do |dlg, resp|
207
+ if resp == Gtk::ResponseType::ACCEPT
208
+ iter = store.append
209
+ iter[0] = dlg.file.path
210
+ end
211
+ dlg.destroy
212
+ end
213
+ chooser.show
214
+ end
215
+
216
+ remove_btn = Gtk::Button.new(label: "Remove")
217
+ remove_btn.signal_connect("clicked") do
218
+ sel = tv.selection.selected
219
+ store.remove(sel) if sel
220
+ end
221
+
222
+ btn_box = Gtk::Box.new(:horizontal, 6)
223
+ btn_box.append(add_btn)
224
+ btn_box.append(remove_btn)
225
+
226
+ container = Gtk::Box.new(:vertical, 4)
227
+ container.vexpand = true
228
+ container.append(sw)
229
+ container.append(btn_box)
230
+
231
+ get_value = proc {
232
+ result = []
233
+ store.each { |_m, _path, iter| result << iter[0] }
234
+ result
235
+ }
236
+
237
+ [container, get_value]
238
+ end
239
+
240
+ def save_and_close
241
+ @widgets.each do |key, info|
242
+ val = case info[:type]
243
+ when :bool then info[:widget].active?
244
+ when :int then info[:widget].value.to_i
245
+ when :float then info[:widget].value.to_f
246
+ when :string then info[:widget].text
247
+ when :string_list then info[:get_value].call
248
+ when :select then info[:widget].selected_item&.string
249
+ end
250
+ cnf_set(key, val) unless val.nil?
251
+ end
252
+ save_settings_to_file
253
+ gui_refresh_font
254
+ gui_refresh_style_scheme
255
+ activate_no_restart_modules
256
+ @window.destroy
257
+ end
258
+
259
+ # For modules that support live activation (no_restart: true), apply enable
260
+ # and disable transitions immediately without requiring a restart.
261
+ # Modules that were already in the target state when the dialog opened are
262
+ # skipped (prev_value matches new value), so we never double-init or double-disable.
263
+ def activate_no_restart_modules
264
+ @widgets.each do |key, info|
265
+ next unless key.is_a?(Array) && key[0] == :modules && key[2] == :enabled
266
+ next unless info[:no_restart] # module requires restart — skip
267
+ mod_name = key[1].to_s
268
+ was_enabled = info[:prev_value]
269
+ now_enabled = cnf_get(key)
270
+ if !was_enabled && now_enabled
271
+ # Toggled on: load the module file and call <name>_init.
272
+ main_file = ppath("modules/#{mod_name}/#{mod_name}.rb")
273
+ next unless File.exist?(main_file)
274
+ load main_file
275
+ init_fn = "#{mod_name}_init"
276
+ send(init_fn) if respond_to?(init_fn, true)
277
+ elsif was_enabled && !now_enabled
278
+ # Toggled off: call <name>_disable to unregister actions and bindings.
279
+ disable_fn = "#{mod_name}_disable"
280
+ send(disable_fn) if respond_to?(disable_fn, true)
281
+ end
282
+ end
283
+ end
284
+
285
+ def run
286
+ @window.show
287
+ end
288
+ end
289
+
290
+ def show_settings_dialog
291
+ SettingsDialog.new.run
292
+ end
293
+
294
+ VIMAMSA_OVERLAY_SCHEME_ID = "vimamsa_overlay"
295
+ VIMAMSA_CONTRAST_SCHEME_ID = "vimamsa_contrast"
296
+
297
+ # ── Color utilities ────────────────────────────────────────────────────────────
298
+
299
+ def hex_to_rgb(hex)
300
+ hex = hex.delete("#")
301
+ hex = hex.chars.map { |c| c * 2 }.join if hex.length == 3
302
+ [hex[0, 2].to_i(16), hex[2, 2].to_i(16), hex[4, 2].to_i(16)]
303
+ end
304
+
305
+ def rgb_to_hsl(r, g, b)
306
+ r, g, b = r / 255.0, g / 255.0, b / 255.0
307
+ max, min = [r, g, b].max, [r, g, b].min
308
+ l = (max + min) / 2.0
309
+ if max == min
310
+ [0.0, 0.0, l]
311
+ else
312
+ d = max - min
313
+ s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min)
314
+ h = case max
315
+ when r then ((g - b) / d + (g < b ? 6 : 0)) / 6.0
316
+ when g then ((b - r) / d + 2) / 6.0
317
+ else ((r - g) / d + 4) / 6.0
318
+ end
319
+ [h, s, l]
320
+ end
321
+ end
322
+
323
+ def hsl_to_rgb(h, s, l)
324
+ return Array.new(3, (l * 255).round) if s == 0
325
+ q = l < 0.5 ? l * (1 + s) : l + s - l * s
326
+ p = 2 * l - q
327
+ [h + 1 / 3.0, h, h - 1 / 3.0].map do |t|
328
+ t += 1 if t < 0; t -= 1 if t > 1
329
+ v = if t < 1 / 6.0 then p + (q - p) * 6 * t
330
+ elsif t < 1 / 2.0 then q
331
+ elsif t < 2 / 3.0 then p + (q - p) * (2 / 3.0 - t) * 6
332
+ else p
333
+ end
334
+ (v * 255).round.clamp(0, 255)
335
+ end
336
+ end
337
+
338
+ # Adjust contrast and brightness of a hex color.
339
+ # contrast: scales lightness around 0.5 (>1 increases separation, <1 reduces it).
340
+ # brightness: fixed lightness offset applied after contrast (-1.0..1.0, 0 = no change).
341
+ # Chroma is preserved by rescaling saturation to compensate for gamut narrowing at
342
+ # extreme lightness values, preventing warm colors from washing out.
343
+ def adjust_hex_color(hex, contrast, brightness)
344
+ r, g, b = hex_to_rgb(hex)
345
+ h, s, l = rgb_to_hsl(r, g, b)
346
+ new_l = (0.5 + (l - 0.5) * contrast + brightness).clamp(0.0, 1.0)
347
+ old_chroma = s * [l, 1.0 - l].min * 2.0
348
+ max_new_chroma = [new_l, 1.0 - new_l].min * 2.0
349
+ new_s = max_new_chroma > 0 ? [old_chroma / max_new_chroma, 1.0].min : 0.0
350
+ nr, ng, nb = hsl_to_rgb(h, new_s, new_l)
351
+ "#%02X%02X%02X" % [nr, ng, nb]
352
+ end
353
+
354
+ # ── Scheme XML processing ──────────────────────────────────────────────────────
355
+
356
+ # Find the XML file on disk for a given scheme id.
357
+ def find_scheme_xml_path(scheme_id)
358
+ ssm = GtkSource::StyleSchemeManager.new
359
+ ssm.set_search_path(ssm.search_path << ppath("styles/"))
360
+ ssm.search_path.each do |dir|
361
+ next unless Dir.exist?(dir)
362
+ Dir[File.join(dir, "*.xml")].each do |f|
363
+ return f if File.read(f).include?("id=\"#{scheme_id}\"")
364
+ rescue StandardError
365
+ end
366
+ end
367
+ nil
368
+ end
369
+
370
+ # Apply contrast and brightness to every hex color value in an XML string.
371
+ def apply_contrast_to_xml(xml, contrast, brightness)
372
+ xml.gsub(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}\b/) do |hex|
373
+ adjust_hex_color(hex, contrast, brightness)
374
+ end
375
+ end
376
+
377
+ # If contrast != 1.0, generate a contrast-adjusted copy of base_scheme_id and
378
+ # return its scheme id. Otherwise return base_scheme_id unchanged.
379
+ def apply_contrast_transformation(base_scheme_id, contrast, brightness)
380
+ return base_scheme_id if contrast == 1.0 && brightness == 0.0
381
+ xml_path = find_scheme_xml_path(base_scheme_id)
382
+ return base_scheme_id if xml_path.nil?
383
+
384
+ adjusted = apply_contrast_to_xml(IO.read(xml_path), contrast, brightness)
385
+ adjusted = adjusted.sub(/(<style-scheme\b[^>]*)id="[^"]*"/, "\\1id=\"#{VIMAMSA_CONTRAST_SCHEME_ID}\"")
386
+ adjusted = adjusted.sub(/(<style-scheme\b[^>]*)name="[^"]*"/, "\\1name=\"#{VIMAMSA_CONTRAST_SCHEME_ID}\"")
387
+ adjusted = adjusted.sub(/\s*parent-scheme="[^"]*"/, "") # avoid inheritance chains
388
+ IO.write(ppath("styles/_vimamsa_contrast.xml"), adjusted)
389
+ VIMAMSA_CONTRAST_SCHEME_ID
390
+ end
391
+
392
+ # ── Overlay scheme ─────────────────────────────────────────────────────────────
393
+
394
+ # Return foreground attribute XML fragment for a style name from a scheme object.
395
+ # Falls back to default_fg if the scheme doesn't define it.
396
+ def style_fg_attr(sty, style_name, default_fg = nil)
397
+ fg = sty&.get_style(style_name)&.foreground
398
+ fg = default_fg if fg.nil? || fg.empty?
399
+ fg ? " foreground=\"#{fg}\"" : ""
400
+ end
401
+
402
+ # Generate a GtkSourceView style scheme that inherits from base_scheme_id
403
+ # (optionally contrast-adjusted) and overlays Vimamsa-specific heading/
404
+ # hyperlink styles on top. Written to styles/_vimamsa_overlay.xml.
405
+ # Foreground colors are read from the parent scheme so they are preserved.
406
+ def generate_vimamsa_overlay(base_scheme_id)
407
+ contrast = (cnf_get([:color_contrast]) || 1.0).to_f
408
+ brightness = (cnf_get([:color_brightness]) || 0.0).to_f
409
+ parent_id = apply_contrast_transformation(base_scheme_id, contrast, brightness)
410
+
411
+ # Load the parent scheme to read existing foreground colors for headings/links
412
+ ssm = GtkSource::StyleSchemeManager.new
413
+ ssm.set_search_path(ssm.search_path << ppath("styles/"))
414
+ parent_sty = ssm.get_scheme(parent_id)
415
+
416
+ xml = <<~XML
417
+ <?xml version="1.0"?>
418
+ <style-scheme id="#{VIMAMSA_OVERLAY_SCHEME_ID}" name="#{VIMAMSA_OVERLAY_SCHEME_ID}" version="1.0" parent-scheme="#{parent_id}">
419
+ <style name="def:title" scale="2.0" bold="true"#{style_fg_attr(parent_sty, "def:title")}/>
420
+ <style name="def:hyperlink" bold="true"#{style_fg_attr(parent_sty, "def:hyperlink", "#4FC3F7")}/>
421
+ <style name="def:heading0" scale="2.0" bold="true"#{style_fg_attr(parent_sty, "def:heading0")}/>
422
+ <style name="def:heading1" scale="1.75" bold="true"#{style_fg_attr(parent_sty, "def:heading1")}/>
423
+ <style name="def:heading2" scale="1.5" bold="true"#{style_fg_attr(parent_sty, "def:heading2")}/>
424
+ <style name="def:heading3" scale="1.25" bold="true"#{style_fg_attr(parent_sty, "def:heading3")}/>
425
+ <style name="def:heading4" scale="1.175" bold="true"#{style_fg_attr(parent_sty, "def:heading4")}/>
426
+ <style name="def:heading5" scale="1.1" bold="true"#{style_fg_attr(parent_sty, "def:heading5")}/>
427
+ <style name="def:heading6" scale="1.0" bold="true"#{style_fg_attr(parent_sty, "def:heading6")}/>
428
+ <style name="def:bold" bold="true"/>
429
+ </style-scheme>
430
+ XML
431
+ IO.write(ppath("styles/_vimamsa_overlay.xml"), xml)
432
+ end
433
+
434
+ # Build a StyleSchemeManager with the project styles dir appended,
435
+ # apply the overlay on top of the user's chosen base scheme,
436
+ # and return the overlay scheme object.
437
+ def load_vimamsa_scheme
438
+ base_id = cnf.style_scheme! || "molokai_edit"
439
+ generate_vimamsa_overlay(base_id)
440
+ ssm = GtkSource::StyleSchemeManager.new
441
+ ssm.set_search_path(ssm.search_path << ppath("styles/"))
442
+ ssm.get_scheme(VIMAMSA_OVERLAY_SCHEME_ID)
443
+ end
444
+
445
+ # Returns true if the given GtkSource::StyleScheme has a light background.
446
+ # Reads the background color of the "text" style and computes relative luminance.
447
+ def scheme_is_light?(sty)
448
+ text_style = sty.get_style("text")
449
+ return false if text_style.nil?
450
+ bg = text_style.background
451
+ return false if bg.nil? || !bg.start_with?("#")
452
+ hex = bg.delete("#")
453
+ hex = hex.chars.map { |c| c * 2 }.join if hex.length == 3
454
+ return false unless hex.length == 6
455
+ r = hex[0, 2].to_i(16) / 255.0
456
+ g = hex[2, 2].to_i(16) / 255.0
457
+ b = hex[4, 2].to_i(16) / 255.0
458
+ luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
459
+ luminance > 0.5
460
+ end
461
+
462
+ # Apply dark/light GTK preference to match the given style scheme.
463
+ def gui_apply_color_mode(sty)
464
+ Gtk::Settings.default.gtk_application_prefer_dark_theme = !scheme_is_light?(sty)
465
+ end
466
+
467
+ def gui_refresh_style_scheme
468
+ return unless $vmag
469
+ sty = load_vimamsa_scheme
470
+ return if sty.nil?
471
+ gui_apply_color_mode(sty)
472
+ for _k, view in $vmag.buffers
473
+ view.buffer.style_scheme = sty
474
+ end
475
+ end
476
+
477
+ def gui_refresh_font
478
+ return unless $vmag
479
+ provider = Gtk::CssProvider.new
480
+ provider.load(data: "textview { font-family: #{get(cnf.font.family)}; font-size: #{get(cnf.font.size)}pt; }")
481
+ for _k, window in $vmag.windows
482
+ view = window[:sw].child
483
+ next if view.nil?
484
+ view.style_context.add_provider(provider)
485
+ end
486
+ end