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.
- checksums.yaml +4 -4
- data/.dockerignore +32 -0
- data/Dockerfile +45 -0
- data/README.md +2 -2
- data/custom_example.rb +38 -9
- data/docker_cmd.sh +7 -0
- data/exe/run_tests.rb +23 -0
- data/img/screenshot1.png +0 -0
- data/img/screenshot2.png +0 -0
- data/lib/vimamsa/actions.rb +8 -0
- data/lib/vimamsa/buffer.rb +165 -53
- data/lib/vimamsa/buffer_changetext.rb +68 -14
- data/lib/vimamsa/buffer_cursor.rb +9 -3
- data/lib/vimamsa/buffer_list.rb +14 -28
- data/lib/vimamsa/buffer_manager.rb +1 -1
- data/lib/vimamsa/conf.rb +33 -1
- data/lib/vimamsa/diff_buffer.rb +185 -0
- data/lib/vimamsa/editor.rb +149 -80
- data/lib/vimamsa/file_finder.rb +6 -2
- data/lib/vimamsa/gui.rb +330 -135
- data/lib/vimamsa/gui_dialog.rb +2 -0
- data/lib/vimamsa/gui_file_panel.rb +94 -0
- data/lib/vimamsa/gui_form_generator.rb +4 -2
- data/lib/vimamsa/gui_func_panel.rb +127 -0
- data/lib/vimamsa/gui_image.rb +2 -4
- data/lib/vimamsa/gui_menu.rb +54 -1
- data/lib/vimamsa/gui_select_window.rb +18 -6
- data/lib/vimamsa/gui_settings.rb +486 -0
- data/lib/vimamsa/gui_sourceview.rb +196 -8
- data/lib/vimamsa/gui_text.rb +0 -22
- data/lib/vimamsa/hyper_plain_text.rb +1 -0
- data/lib/vimamsa/key_actions.rb +54 -31
- data/lib/vimamsa/key_binding_tree.rb +154 -8
- data/lib/vimamsa/key_bindings_vimlike.rb +48 -35
- data/lib/vimamsa/langservp.rb +161 -7
- data/lib/vimamsa/macro.rb +54 -7
- data/lib/vimamsa/main.rb +1 -0
- data/lib/vimamsa/rbvma.rb +5 -0
- data/lib/vimamsa/string_util.rb +56 -0
- data/lib/vimamsa/test_framework.rb +137 -0
- data/lib/vimamsa/util.rb +3 -36
- data/lib/vimamsa/version.rb +1 -1
- data/modules/calculator/calculator.rb +318 -0
- data/modules/calculator/calculator_info.rb +3 -0
- data/modules/terminal/terminal.rb +140 -0
- data/modules/terminal/terminal_info.rb +3 -0
- data/run_tests.rb +89 -0
- data/styles/dark.xml +1 -1
- data/styles/molokai_edit.xml +2 -2
- data/tests/key_bindings.rb +2 -0
- data/tests/test_basic_editing.rb +86 -0
- data/tests/test_copy_paste.rb +88 -0
- data/tests/test_key_bindings.rb +152 -0
- data/tests/test_module_interface.rb +98 -0
- data/tests/test_undo.rb +201 -0
- data/vimamsa.gemspec +6 -5
- 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
|