mui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -0
  3. data/CHANGELOG.md +448 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +301 -0
  7. data/docs/getting-started.md +140 -0
  8. data/docs/index.md +55 -0
  9. data/docs/jobs.md +297 -0
  10. data/docs/keybindings.md +229 -0
  11. data/docs/plugins.md +285 -0
  12. data/docs/syntax-highlighting.md +149 -0
  13. data/exe/mui +1 -2
  14. data/lib/mui/autocmd.rb +66 -0
  15. data/lib/mui/buffer.rb +275 -0
  16. data/lib/mui/buffer_word_cache.rb +131 -0
  17. data/lib/mui/buffer_word_completer.rb +77 -0
  18. data/lib/mui/color_manager.rb +136 -0
  19. data/lib/mui/color_scheme.rb +63 -0
  20. data/lib/mui/command_completer.rb +30 -0
  21. data/lib/mui/command_context.rb +90 -0
  22. data/lib/mui/command_history.rb +89 -0
  23. data/lib/mui/command_line.rb +167 -0
  24. data/lib/mui/command_registry.rb +44 -0
  25. data/lib/mui/completion_renderer.rb +84 -0
  26. data/lib/mui/completion_state.rb +58 -0
  27. data/lib/mui/config.rb +58 -0
  28. data/lib/mui/editor.rb +395 -0
  29. data/lib/mui/error.rb +29 -0
  30. data/lib/mui/file_completer.rb +51 -0
  31. data/lib/mui/floating_window.rb +161 -0
  32. data/lib/mui/handler_result.rb +107 -0
  33. data/lib/mui/highlight.rb +22 -0
  34. data/lib/mui/highlighters/base.rb +23 -0
  35. data/lib/mui/highlighters/search_highlighter.rb +27 -0
  36. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  37. data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
  38. data/lib/mui/input.rb +17 -0
  39. data/lib/mui/insert_completion_renderer.rb +92 -0
  40. data/lib/mui/insert_completion_state.rb +77 -0
  41. data/lib/mui/job.rb +81 -0
  42. data/lib/mui/job_manager.rb +113 -0
  43. data/lib/mui/key_code.rb +30 -0
  44. data/lib/mui/key_handler/base.rb +187 -0
  45. data/lib/mui/key_handler/command_mode.rb +511 -0
  46. data/lib/mui/key_handler/insert_mode.rb +323 -0
  47. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  48. data/lib/mui/key_handler/normal_mode.rb +552 -0
  49. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  50. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  51. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  52. data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
  53. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  54. data/lib/mui/key_handler/search_mode.rb +191 -0
  55. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  56. data/lib/mui/key_handler/visual_mode.rb +402 -0
  57. data/lib/mui/key_handler/window_command.rb +112 -0
  58. data/lib/mui/key_handler.rb +16 -0
  59. data/lib/mui/key_notation_parser.rb +152 -0
  60. data/lib/mui/key_sequence.rb +67 -0
  61. data/lib/mui/key_sequence_buffer.rb +85 -0
  62. data/lib/mui/key_sequence_handler.rb +163 -0
  63. data/lib/mui/key_sequence_matcher.rb +79 -0
  64. data/lib/mui/layout/calculator.rb +15 -0
  65. data/lib/mui/layout/leaf_node.rb +33 -0
  66. data/lib/mui/layout/node.rb +29 -0
  67. data/lib/mui/layout/split_node.rb +132 -0
  68. data/lib/mui/line_renderer.rb +173 -0
  69. data/lib/mui/mode.rb +13 -0
  70. data/lib/mui/mode_manager.rb +186 -0
  71. data/lib/mui/motion.rb +139 -0
  72. data/lib/mui/plugin.rb +35 -0
  73. data/lib/mui/plugin_manager.rb +106 -0
  74. data/lib/mui/register.rb +110 -0
  75. data/lib/mui/screen.rb +103 -0
  76. data/lib/mui/search_completer.rb +50 -0
  77. data/lib/mui/search_input.rb +40 -0
  78. data/lib/mui/search_state.rb +121 -0
  79. data/lib/mui/selection.rb +55 -0
  80. data/lib/mui/status_line_renderer.rb +40 -0
  81. data/lib/mui/syntax/language_detector.rb +106 -0
  82. data/lib/mui/syntax/lexer_base.rb +106 -0
  83. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  84. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  85. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  86. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  87. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  88. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  89. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  90. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  91. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  92. data/lib/mui/syntax/token.rb +42 -0
  93. data/lib/mui/syntax/token_cache.rb +91 -0
  94. data/lib/mui/tab_bar_renderer.rb +87 -0
  95. data/lib/mui/tab_manager.rb +96 -0
  96. data/lib/mui/tab_page.rb +35 -0
  97. data/lib/mui/terminal_adapter/base.rb +92 -0
  98. data/lib/mui/terminal_adapter/curses.rb +164 -0
  99. data/lib/mui/terminal_adapter.rb +4 -0
  100. data/lib/mui/themes/default.rb +315 -0
  101. data/lib/mui/undo_manager.rb +83 -0
  102. data/lib/mui/undoable_action.rb +175 -0
  103. data/lib/mui/unicode_width.rb +100 -0
  104. data/lib/mui/version.rb +1 -1
  105. data/lib/mui/window.rb +201 -0
  106. data/lib/mui/window_manager.rb +256 -0
  107. data/lib/mui/wrap_cache.rb +40 -0
  108. data/lib/mui/wrap_helper.rb +84 -0
  109. data/lib/mui.rb +171 -2
  110. metadata +123 -5
data/lib/mui/buffer.rb ADDED
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class Buffer
5
+ attr_reader :lines, :change_count
6
+ attr_accessor :name, :modified, :undo_manager, :readonly
7
+
8
+ # Alias for autocmd pattern matching
9
+ alias file_path name
10
+
11
+ def initialize(name = "[No Name]")
12
+ @name = name
13
+ @lines = [empty_line]
14
+ @modified = false
15
+ @undo_manager = nil
16
+ @readonly = false
17
+ @custom_highlighter_map = {}
18
+ @change_count = 0
19
+ end
20
+
21
+ def readonly?
22
+ @readonly
23
+ end
24
+
25
+ # Override in subclasses to provide buffer-specific highlighters
26
+ def custom_highlighters(_color_scheme)
27
+ @custom_highlighter_map.values
28
+ end
29
+
30
+ # Add a custom highlighter with a unique key
31
+ def add_custom_highlighter(key, highlighter)
32
+ @custom_highlighter_map[key] = highlighter
33
+ end
34
+
35
+ # Remove a custom highlighter by key
36
+ def remove_custom_highlighter(key)
37
+ @custom_highlighter_map.delete(key)
38
+ end
39
+
40
+ # Check if a custom highlighter exists
41
+ def custom_highlighter?(key)
42
+ @custom_highlighter_map.key?(key)
43
+ end
44
+
45
+ # Set content directly (for scratch buffers)
46
+ def content=(text)
47
+ @lines = text.split("\n", -1)
48
+ @lines = [empty_line] if @lines.empty?
49
+ @modified = false
50
+ end
51
+
52
+ # Reload file from disk
53
+ def reload
54
+ return unless @name && File.exist?(@name)
55
+
56
+ load(@name)
57
+ end
58
+
59
+ def load(path)
60
+ @name = path
61
+ if File.exist?(path)
62
+ @lines = File.readlines(path, chomp: true)
63
+ @lines = [empty_line] if @lines.empty?
64
+ end
65
+ @modified = false
66
+ end
67
+
68
+ def save(path = @name)
69
+ File.write(path, "#{@lines.join("\n")}\n")
70
+ @name = path
71
+ @modified = false
72
+ end
73
+
74
+ def line_count
75
+ @lines.size
76
+ end
77
+
78
+ def line(n)
79
+ @lines[n] || ""
80
+ end
81
+
82
+ def insert_char(row, col, char)
83
+ with_undo(InsertCharAction.new(row, col, char)) do
84
+ insert_char_without_record(row, col, char)
85
+ end
86
+ end
87
+
88
+ def delete_char(row, col)
89
+ return unless valid_char_position?(row, col)
90
+
91
+ char = @lines[row][col]
92
+ with_undo(DeleteCharAction.new(row, col, char)) do
93
+ delete_char_without_record(row, col)
94
+ end
95
+ end
96
+
97
+ def insert_line(row, text = nil)
98
+ actual_text = text&.dup || empty_line
99
+ with_undo(InsertLineAction.new(row, actual_text)) do
100
+ insert_line_without_record(row, actual_text)
101
+ end
102
+ end
103
+
104
+ def delete_line(row)
105
+ text = @lines[row]&.dup
106
+ return unless text
107
+
108
+ with_undo(DeleteLineAction.new(row, text)) do
109
+ delete_line_without_record(row)
110
+ end
111
+ end
112
+
113
+ def split_line(row, col)
114
+ return unless @lines[row]
115
+
116
+ with_undo(SplitLineAction.new(row, col)) do
117
+ split_line_without_record(row, col)
118
+ end
119
+ end
120
+
121
+ def join_lines(row)
122
+ return if row >= line_count - 1
123
+
124
+ col = @lines[row]&.size || 0
125
+ with_undo(JoinLinesAction.new(row, col)) do
126
+ join_lines_without_record(row)
127
+ end
128
+ end
129
+
130
+ def delete_range(start_row, start_col, end_row, end_col)
131
+ deleted_lines = capture_range(start_row, start_col, end_row, end_col)
132
+ with_undo(DeleteRangeAction.new(start_row, start_col, end_row, end_col, deleted_lines)) do
133
+ delete_range_without_record(start_row, start_col, end_row, end_col)
134
+ end
135
+ end
136
+
137
+ def replace_line(row, text)
138
+ old_text = @lines[row]&.dup || empty_line
139
+ with_undo(ReplaceLineAction.new(row, old_text, text.dup)) do
140
+ replace_line_without_record(row, text)
141
+ end
142
+ end
143
+
144
+ def insert_char_without_record(row, col, char)
145
+ @lines[row] ||= empty_line
146
+ @lines[row].insert(col, char)
147
+ @modified = true
148
+ @change_count += 1
149
+ end
150
+
151
+ def delete_char_without_record(row, col)
152
+ return unless valid_char_position?(row, col)
153
+
154
+ @lines[row].slice!(col)
155
+ @modified = true
156
+ @change_count += 1
157
+ end
158
+
159
+ def insert_line_without_record(row, text = nil)
160
+ @lines.insert(row, text&.dup || empty_line)
161
+ @modified = true
162
+ @change_count += 1
163
+ end
164
+
165
+ def delete_line_without_record(row)
166
+ @lines.delete_at(row)
167
+ @lines = [empty_line] if @lines.empty?
168
+ @modified = true
169
+ @change_count += 1
170
+ end
171
+
172
+ def split_line_without_record(row, col)
173
+ return unless @lines[row]
174
+
175
+ rest = (@lines[row][col..] || "").dup
176
+ @lines[row] = (@lines[row][0...col] || "").dup
177
+ insert_line_without_record(row + 1, rest)
178
+ end
179
+
180
+ def join_lines_without_record(row)
181
+ return if row >= line_count - 1
182
+
183
+ @lines[row] = ((@lines[row] || "") + (@lines[row + 1] || "")).dup
184
+ delete_line_without_record(row + 1)
185
+ end
186
+
187
+ def delete_range_without_record(start_row, start_col, end_row, end_col)
188
+ if start_row == end_row
189
+ delete_within_line(start_row, start_col, end_col)
190
+ else
191
+ delete_across_lines(start_row, start_col, end_row, end_col)
192
+ end
193
+ @modified = true
194
+ @change_count += 1
195
+ end
196
+
197
+ def replace_line_without_record(row, text)
198
+ @lines[row] = text.dup
199
+ @modified = true
200
+ @change_count += 1
201
+ end
202
+
203
+ def restore_range(start_row, start_col, deleted_lines)
204
+ return if deleted_lines.empty?
205
+
206
+ current_line = @lines[start_row] || empty_line
207
+ if deleted_lines.size == 1
208
+ # Single line restore: insert at start_col
209
+ @lines[start_row] = current_line[0...start_col] + deleted_lines[0] + current_line[start_col..]
210
+ else
211
+ # Multi-line restore
212
+ first_part = current_line[0...start_col]
213
+ last_part = current_line[start_col..]
214
+
215
+ # Set first line
216
+ @lines[start_row] = first_part + deleted_lines[0]
217
+
218
+ # Insert middle lines
219
+ (1...(deleted_lines.size - 1)).each do |i|
220
+ @lines.insert(start_row + i, deleted_lines[i].dup)
221
+ end
222
+
223
+ # Insert last line with remainder
224
+ last_deleted = deleted_lines[-1]
225
+ @lines.insert(start_row + deleted_lines.size - 1, last_deleted + last_part)
226
+ end
227
+ @modified = true
228
+ @change_count += 1
229
+ end
230
+
231
+ private
232
+
233
+ def with_undo(action)
234
+ @undo_manager&.record(action)
235
+ yield
236
+ end
237
+
238
+ def valid_char_position?(row, col)
239
+ !col.negative? && @lines[row] && col < @lines[row].size
240
+ end
241
+
242
+ def empty_line
243
+ String.new
244
+ end
245
+
246
+ def capture_range(start_row, start_col, end_row, end_col)
247
+ if start_row == end_row
248
+ line = @lines[start_row] || ""
249
+ [line[start_col..end_col] || ""]
250
+ else
251
+ lines = []
252
+ lines << ((@lines[start_row] || "")[start_col..] || "")
253
+ ((start_row + 1)...end_row).each do |row|
254
+ lines << (@lines[row]&.dup || "")
255
+ end
256
+ lines << ((@lines[end_row] || "")[0..end_col] || "")
257
+ lines
258
+ end
259
+ end
260
+
261
+ def delete_within_line(row, start_col, end_col)
262
+ line = @lines[row] || ""
263
+ @lines[row] = (line[0...start_col] || "") + (line[(end_col + 1)..] || "")
264
+ end
265
+
266
+ def delete_across_lines(start_row, start_col, end_row, end_col)
267
+ first_part = (@lines[start_row] || "")[0...start_col] || ""
268
+ last_part = (@lines[end_row] || "")[(end_col + 1)..] || ""
269
+
270
+ (end_row - start_row).times { @lines.delete_at(start_row + 1) }
271
+
272
+ @lines[start_row] = first_part + last_part
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Caches words from buffer for fast completion
5
+ # Built once when entering Insert mode, updated incrementally on changes
6
+ class BufferWordCache
7
+ # Minimum word length to include in cache
8
+ MIN_WORD_LENGTH = 2
9
+ # Maximum candidates to return (for performance)
10
+ MAX_CANDIDATES = 50
11
+
12
+ def initialize(buffer)
13
+ @buffer = buffer
14
+ @words = Set.new
15
+ @dirty_rows = Set.new
16
+ build_cache
17
+ end
18
+
19
+ # Get completion candidates matching the prefix
20
+ def complete(prefix, cursor_row, cursor_col)
21
+ return [] if prefix.empty?
22
+
23
+ # Process any dirty rows first
24
+ process_dirty_rows(cursor_row, cursor_col)
25
+
26
+ # Get word at cursor to exclude it
27
+ word_at_cursor = word_at_position(cursor_row, cursor_col)
28
+
29
+ prefix_downcase = prefix.downcase
30
+ candidates = @words.select do |w|
31
+ w != prefix && w != word_at_cursor && w.downcase.start_with?(prefix_downcase)
32
+ end
33
+ candidates.to_a.sort.first(MAX_CANDIDATES)
34
+ end
35
+
36
+ # Get the full word at cursor position (for exclusion)
37
+ def word_at_position(row, col)
38
+ line = @buffer.line(row)
39
+ return nil if line.empty?
40
+
41
+ # Find word boundaries
42
+ start_col = col
43
+ start_col -= 1 while start_col.positive? && word_char?(line[start_col - 1])
44
+
45
+ end_col = col
46
+ end_col += 1 while end_col < line.length && word_char?(line[end_col])
47
+
48
+ word = line[start_col...end_col]
49
+ word && word.length >= MIN_WORD_LENGTH ? word : nil
50
+ end
51
+
52
+ # Mark a row as dirty (needs re-scanning)
53
+ def mark_dirty(row)
54
+ @dirty_rows << row
55
+ end
56
+
57
+ # Add a word directly (for incremental updates)
58
+ def add_word(word)
59
+ @words << word if word.length >= MIN_WORD_LENGTH
60
+ end
61
+
62
+ # Get the word prefix at cursor position
63
+ def prefix_at(row, col)
64
+ line = @buffer.line(row)
65
+ return "" if col.zero? || line.empty?
66
+
67
+ start_col = col
68
+ start_col -= 1 while start_col.positive? && word_char?(line[start_col - 1])
69
+
70
+ line[start_col...col] || ""
71
+ end
72
+
73
+ private
74
+
75
+ # Regex to match word characters (alphanumeric + underscore)
76
+ WORD_REGEX = /\w+/
77
+
78
+ def build_cache
79
+ @buffer.lines.each do |line|
80
+ extract_words_fast(line)
81
+ end
82
+ end
83
+
84
+ def process_dirty_rows(exclude_row, exclude_col)
85
+ return if @dirty_rows.empty?
86
+
87
+ @dirty_rows.each do |row|
88
+ next if row >= @buffer.line_count
89
+
90
+ line = @buffer.line(row)
91
+ extract_words_with_exclusion(line, exclude_row, exclude_col, row)
92
+ end
93
+ @dirty_rows.clear
94
+ end
95
+
96
+ # Fast word extraction using scan (for initial cache build)
97
+ def extract_words_fast(line)
98
+ line.scan(WORD_REGEX) do |word|
99
+ @words << word if word.length >= MIN_WORD_LENGTH
100
+ end
101
+ end
102
+
103
+ # Word extraction with cursor position exclusion (for dirty row processing)
104
+ def extract_words_with_exclusion(line, exclude_row, exclude_col, current_row)
105
+ line.scan(WORD_REGEX) do |word|
106
+ next if word.length < MIN_WORD_LENGTH
107
+
108
+ # Get match position
109
+ match = Regexp.last_match
110
+ word_start = match.begin(0)
111
+ word_end = match.end(0)
112
+
113
+ # Skip word at cursor position
114
+ next if exclude_row && current_row == exclude_row && word_start <= exclude_col && word_end > exclude_col
115
+
116
+ @words << word
117
+ end
118
+ end
119
+
120
+ def word_char?(char)
121
+ # Direct character check is faster than regex for single chars
122
+ return false unless char
123
+
124
+ c = char.ord
125
+ c.between?(48, 57) || # 0-9
126
+ c.between?(65, 90) || # A-Z
127
+ c.between?(97, 122) || # a-z
128
+ c == 95 # _
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Collects words from buffer for completion
5
+ class BufferWordCompleter
6
+ # Minimum word length to include in completion
7
+ MIN_WORD_LENGTH = 2
8
+ # Maximum candidates to collect (for performance)
9
+ MAX_CANDIDATES = 50
10
+
11
+ def initialize(buffer)
12
+ @buffer = buffer
13
+ end
14
+
15
+ # Get completion candidates matching the prefix
16
+ # Optimized to filter while collecting, stopping early when enough candidates found
17
+ def complete(prefix, cursor_row, cursor_col)
18
+ return [] if prefix.empty?
19
+
20
+ candidates = Set.new
21
+ prefix_downcase = prefix.downcase
22
+
23
+ @buffer.lines.each_with_index do |line, row|
24
+ extract_matching_words(line, row, cursor_row, cursor_col, prefix, prefix_downcase, candidates)
25
+ break if candidates.size >= MAX_CANDIDATES
26
+ end
27
+
28
+ candidates.to_a.sort
29
+ end
30
+
31
+ # Get the word prefix at cursor position
32
+ def prefix_at(row, col)
33
+ line = @buffer.line(row)
34
+ return "" if col.zero? || line.empty?
35
+
36
+ # Find word start
37
+ start_col = col
38
+ start_col -= 1 while start_col.positive? && word_char?(line[start_col - 1])
39
+
40
+ line[start_col...col] || ""
41
+ end
42
+
43
+ private
44
+
45
+ def extract_matching_words(line, row, exclude_row, exclude_col, prefix, prefix_downcase, candidates)
46
+ current_word = +""
47
+ word_start = 0
48
+
49
+ line.each_char.with_index do |char, col|
50
+ if word_char?(char)
51
+ current_word << char
52
+ else
53
+ check_and_add_word(current_word, word_start, row, col, exclude_row, exclude_col, prefix, prefix_downcase, candidates)
54
+ current_word = +""
55
+ word_start = col + 1
56
+ end
57
+ end
58
+
59
+ # Handle word at end of line
60
+ check_and_add_word(current_word, word_start, row, line.length, exclude_row, exclude_col, prefix, prefix_downcase, candidates)
61
+ end
62
+
63
+ def check_and_add_word(word, word_start, row, col, exclude_row, exclude_col, prefix, prefix_downcase, candidates)
64
+ return if word.length < MIN_WORD_LENGTH
65
+ return if word == prefix
66
+ return unless word.downcase.start_with?(prefix_downcase)
67
+
68
+ # Don't include the word at cursor position
69
+ at_cursor = row == exclude_row && word_start <= exclude_col && col > exclude_col
70
+ candidates << word unless at_cursor
71
+ end
72
+
73
+ def word_char?(char)
74
+ char&.match?(/\w/)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class ColorManager
5
+ # Standard 8 colors
6
+ COLOR_MAP = {
7
+ black: 0,
8
+ red: 1,
9
+ green: 2,
10
+ yellow: 3,
11
+ blue: 4,
12
+ magenta: 5,
13
+ cyan: 6,
14
+ white: 7
15
+ }.freeze
16
+
17
+ # 256-color palette extended colors
18
+ # Use https://www.ditig.com/256-colors-cheat-sheet for reference
19
+ EXTENDED_COLOR_MAP = {
20
+ # mui theme
21
+ darkgray: 235, # #262626 (~#2b2b2b)
22
+
23
+ # solarized
24
+ solarized_base03: 234, # #1c1c1c (~#002b36)
25
+ solarized_base02: 235, # #262626 (~#073642)
26
+ solarized_base01: 240, # #585858 (~#586e75)
27
+ solarized_base00: 241, # #626262 (~#657b83)
28
+ solarized_base0: 244, # #808080 (~#839496)
29
+ solarized_base1: 245, # #8a8a8a (~#93a1a1)
30
+ solarized_base2: 254, # #e4e4e4 (~#eee8d5)
31
+ solarized_base3: 230, # #ffffd7 (~#fdf6e3)
32
+ solarized_yellow: 136, # #af8700 (~#b58900)
33
+ solarized_orange: 166, # #d75f00 (~#cb4b16)
34
+ solarized_red: 160, # #d70000 (~#dc322f)
35
+ solarized_magenta: 125, # #af005f (~#d33682)
36
+ solarized_violet: 61, # #5f5faf (~#6c71c4)
37
+ solarized_blue: 33, # #0087ff (~#268bd2)
38
+ solarized_cyan: 37, # #00afaf (~#2aa198)
39
+ solarized_green: 64, # #5f8700 (~#859900)
40
+
41
+ # monokai
42
+ monokai_bg: 235, # #262626 (~#272822)
43
+ monokai_fg: 231, # #ffffff (~#f8f8f2)
44
+ monokai_pink: 197, # #ff005f (~#f92672)
45
+ monokai_green: 148, # #afd700 (~#a6e22e)
46
+ monokai_orange: 208, # #ff8700 (~#fd971f)
47
+ monokai_purple: 141, # #af87ff (~#ae81ff)
48
+ monokai_cyan: 81, # #5fd7ff (~#66d9ef)
49
+ monokai_yellow: 186, # #d7d787 (~#e6db74)
50
+
51
+ # nord
52
+ nord_polar0: 236, # #303030 (~#2e3440)
53
+ nord_polar1: 238, # #444444 (~#3b4252)
54
+ nord_polar2: 239, # #4e4e4e (~#434c5e)
55
+ nord_polar3: 240, # #585858 (~#4c566a)
56
+ nord_snow0: 253, # #dadada (~#d8dee9)
57
+ nord_snow1: 254, # #e4e4e4 (~#e5e9f0)
58
+ nord_snow2: 255, # #eeeeee (~#eceff4)
59
+ nord_frost0: 109, # #87afaf (~#8fbcbb)
60
+ nord_frost1: 110, # #87afd7 (~#88c0d0)
61
+ nord_frost2: 111, # #87afff (~#81a1c1)
62
+ nord_frost3: 68, # #5f87d7 (~#5e81ac)
63
+ nord_aurora_red: 167, # #d75f5f (~#bf616a)
64
+ nord_aurora_orange: 208, # #ff8700 (~#d08770)
65
+ nord_aurora_yellow: 179, # #d7af5f (~#ebcb8b)
66
+ nord_aurora_green: 108, # #87af87 (~#a3be8c)
67
+ nord_aurora_purple: 139, # #af87af (~#b48ead)
68
+
69
+ # gruvbox
70
+ gruvbox_bg: 235, # #262626 (~#282828)
71
+ gruvbox_fg: 223, # #ffd7af (~#ebdbb2)
72
+ gruvbox_red: 124, # #af0000 (~#cc241d)
73
+ gruvbox_green: 106, # #87af00 (~#98971a)
74
+ gruvbox_yellow: 172, # #d78700 (~#d79921)
75
+ gruvbox_blue: 66, # #5f8787 (~#458588)
76
+ gruvbox_purple: 132, # #af5f87 (~#b16286)
77
+ gruvbox_aqua: 72, # #5faf87 (~#689d6a)
78
+ gruvbox_orange: 166, # #d75f00 (~#d65d0e)
79
+ gruvbox_gray: 245, # #8a8a8a (~#928374)
80
+
81
+ # dracula
82
+ dracula_bg: 236, # #303030 (~#282a36)
83
+ dracula_fg: 231, # #ffffff (~#f8f8f2)
84
+ dracula_selection: 239, # #4e4e4e (~#44475a)
85
+ dracula_comment: 61, # #5f5faf (~#6272a4)
86
+ dracula_cyan: 117, # #87d7ff (~#8be9fd)
87
+ dracula_green: 84, # #5fdf5f (~#50fa7b)
88
+ dracula_orange: 215, # #ffaf5f (~#ffb86c)
89
+ dracula_pink: 212, # #ff87d7 (~#ff79c6)
90
+ dracula_purple: 141, # #af87ff (~#bd93f9)
91
+ dracula_red: 203, # #ff5f5f (~#ff5555)
92
+ dracula_yellow: 228, # #ffff87 (~#f1fa8c)
93
+
94
+ # tokyo night
95
+ tokyo_bg: 234, # #1c1c1c (~#1a1b26)
96
+ tokyo_fg: 146, # #afafd7 (~#a9b1d6)
97
+ tokyo_comment: 60, # #5f5f87 (~#565f89)
98
+ tokyo_cyan: 115, # #87d7af (~#7dcfff)
99
+ tokyo_blue: 75, # #5fafff (~#7aa2f7)
100
+ tokyo_purple: 140, # #af87d7 (~#bb9af7)
101
+ tokyo_green: 108, # #87af87 (~#9ece6a)
102
+ tokyo_orange: 215, # #ffaf5f (~#ff9e64)
103
+ tokyo_red: 203, # #ff5f5f (~#f7768e)
104
+ tokyo_yellow: 223 # #ffd7af (~#e0af68)
105
+ }.freeze
106
+
107
+ attr_reader :pairs
108
+
109
+ def initialize
110
+ @pair_index = 1
111
+ @pairs = {}
112
+ end
113
+
114
+ def register_pair(fg, bg)
115
+ key = [fg, bg]
116
+ return @pairs[key] if @pairs[key]
117
+
118
+ @pairs[key] = @pair_index
119
+ @pair_index += 1
120
+ @pairs[key]
121
+ end
122
+
123
+ def get_pair_index(fg, bg)
124
+ register_pair(fg, bg)
125
+ end
126
+
127
+ def color_code(color)
128
+ return -1 if color.nil?
129
+ return color if color.is_a?(Integer)
130
+
131
+ COLOR_MAP[color] || EXTENDED_COLOR_MAP[color] || -1
132
+ end
133
+
134
+ alias resolve color_code
135
+ end
136
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class ColorScheme
5
+ ELEMENTS = %i[
6
+ normal
7
+ status_line
8
+ status_line_mode
9
+ search_highlight
10
+ visual_selection
11
+ line_number
12
+ message_error
13
+ message_info
14
+ tab_bar
15
+ tab_bar_active
16
+ completion_popup
17
+ completion_popup_selected
18
+ syntax_keyword
19
+ syntax_string
20
+ syntax_comment
21
+ syntax_number
22
+ syntax_symbol
23
+ syntax_constant
24
+ syntax_operator
25
+ syntax_identifier
26
+ syntax_preprocessor
27
+ syntax_instance_variable
28
+ syntax_global_variable
29
+ syntax_method_call
30
+ syntax_type
31
+ diff_add
32
+ diff_delete
33
+ diff_hunk
34
+ diff_header
35
+ ].freeze
36
+
37
+ attr_reader :name, :colors
38
+
39
+ def initialize(name)
40
+ @name = name
41
+ @colors = {}
42
+ end
43
+
44
+ def define(element, fg:, bg: nil, bold: false, underline: false)
45
+ @colors[element] = {
46
+ fg:,
47
+ bg:,
48
+ bold:,
49
+ underline:
50
+ }
51
+ end
52
+
53
+ def [](element)
54
+ @colors[element] || default_color
55
+ end
56
+
57
+ private
58
+
59
+ def default_color
60
+ { fg: :white, bg: nil, bold: false, underline: false }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Provides command name completion
5
+ class CommandCompleter
6
+ COMMANDS = %w[
7
+ e w q wq q!
8
+ sp split vs vsplit close only
9
+ tabnew tabe tabedit tabclose tabc
10
+ tabnext tabn tabprev tabp tabprevious
11
+ tabfirst tabf tabrewind tabr tablast tabl
12
+ tabmove tabm
13
+ ].freeze
14
+
15
+ def complete(prefix)
16
+ all_commands = COMMANDS + plugin_command_names
17
+
18
+ return all_commands.uniq.sort if prefix.empty?
19
+
20
+ prefix_downcase = prefix.downcase
21
+ all_commands.select { |cmd| cmd.downcase.start_with?(prefix_downcase) }.uniq.sort
22
+ end
23
+
24
+ private
25
+
26
+ def plugin_command_names
27
+ Mui.config.commands.keys.map(&:to_s)
28
+ end
29
+ end
30
+ end