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
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Base class for undoable actions
5
+ class UndoableAction
6
+ def execute(buffer); end
7
+ def undo(buffer); end
8
+ end
9
+
10
+ # Insert a single character
11
+ class InsertCharAction < UndoableAction
12
+ def initialize(row, col, char)
13
+ super()
14
+ @row = row
15
+ @col = col
16
+ @char = char
17
+ end
18
+
19
+ def execute(buffer)
20
+ buffer.insert_char_without_record(@row, @col, @char)
21
+ end
22
+
23
+ def undo(buffer)
24
+ buffer.delete_char_without_record(@row, @col)
25
+ end
26
+ end
27
+
28
+ # Delete a single character
29
+ class DeleteCharAction < UndoableAction
30
+ def initialize(row, col, char)
31
+ super()
32
+ @row = row
33
+ @col = col
34
+ @char = char
35
+ end
36
+
37
+ def execute(buffer)
38
+ buffer.delete_char_without_record(@row, @col)
39
+ end
40
+
41
+ def undo(buffer)
42
+ buffer.insert_char_without_record(@row, @col, @char)
43
+ end
44
+ end
45
+
46
+ # Insert a line
47
+ class InsertLineAction < UndoableAction
48
+ def initialize(row, text)
49
+ super()
50
+ @row = row
51
+ @text = text
52
+ end
53
+
54
+ def execute(buffer)
55
+ buffer.insert_line_without_record(@row, @text)
56
+ end
57
+
58
+ def undo(buffer)
59
+ buffer.delete_line_without_record(@row)
60
+ end
61
+ end
62
+
63
+ # Delete a line
64
+ class DeleteLineAction < UndoableAction
65
+ def initialize(row, text)
66
+ super()
67
+ @row = row
68
+ @text = text
69
+ end
70
+
71
+ def execute(buffer)
72
+ buffer.delete_line_without_record(@row)
73
+ end
74
+
75
+ def undo(buffer)
76
+ buffer.insert_line_without_record(@row, @text)
77
+ end
78
+ end
79
+
80
+ # Split a line (Enter key)
81
+ class SplitLineAction < UndoableAction
82
+ def initialize(row, col)
83
+ super()
84
+ @row = row
85
+ @col = col
86
+ end
87
+
88
+ def execute(buffer)
89
+ buffer.split_line_without_record(@row, @col)
90
+ end
91
+
92
+ def undo(buffer)
93
+ buffer.join_lines_without_record(@row)
94
+ end
95
+ end
96
+
97
+ # Join lines (Backspace at line start)
98
+ class JoinLinesAction < UndoableAction
99
+ def initialize(row, col)
100
+ super()
101
+ @row = row
102
+ @col = col
103
+ end
104
+
105
+ def execute(buffer)
106
+ buffer.join_lines_without_record(@row)
107
+ end
108
+
109
+ def undo(buffer)
110
+ buffer.split_line_without_record(@row, @col)
111
+ end
112
+ end
113
+
114
+ # Delete a range of text
115
+ class DeleteRangeAction < UndoableAction
116
+ def initialize(start_row, start_col, end_row, end_col, deleted_lines)
117
+ super()
118
+ @start_row = start_row
119
+ @start_col = start_col
120
+ @end_row = end_row
121
+ @end_col = end_col
122
+ @deleted_lines = deleted_lines
123
+ end
124
+
125
+ def execute(buffer)
126
+ buffer.delete_range_without_record(@start_row, @start_col, @end_row, @end_col)
127
+ end
128
+
129
+ def undo(buffer)
130
+ buffer.restore_range(@start_row, @start_col, @deleted_lines)
131
+ end
132
+ end
133
+
134
+ # Replace line content (for cc command)
135
+ class ReplaceLineAction < UndoableAction
136
+ def initialize(row, old_text, new_text)
137
+ super()
138
+ @row = row
139
+ @old_text = old_text
140
+ @new_text = new_text
141
+ end
142
+
143
+ def execute(buffer)
144
+ buffer.replace_line_without_record(@row, @new_text)
145
+ end
146
+
147
+ def undo(buffer)
148
+ buffer.replace_line_without_record(@row, @old_text)
149
+ end
150
+ end
151
+
152
+ # Group multiple actions into one undo unit
153
+ class GroupAction < UndoableAction
154
+ def initialize(actions)
155
+ super()
156
+ @actions = actions
157
+ end
158
+
159
+ def execute(buffer)
160
+ @actions.each { |action| action.execute(buffer) }
161
+ end
162
+
163
+ def undo(buffer)
164
+ @actions.reverse_each { |action| action.undo(buffer) }
165
+ end
166
+
167
+ def empty?
168
+ @actions.empty?
169
+ end
170
+
171
+ def size
172
+ @actions.size
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Utility module for calculating display width of Unicode characters
5
+ # CJK characters and some other characters are "wide" (2 cells)
6
+ module UnicodeWidth
7
+ class << self
8
+ # Returns the display width of a single character
9
+ def char_width(char)
10
+ return 0 if char.nil? || char.empty?
11
+
12
+ ord = char.ord
13
+
14
+ # Control characters
15
+ return 0 if ord < 32
16
+
17
+ # ASCII printable characters
18
+ return 1 if ord < 127
19
+
20
+ # Non-printable
21
+ return 0 if ord == 127
22
+
23
+ # Wide characters (East Asian Wide and Fullwidth)
24
+ return 2 if wide_char?(ord)
25
+
26
+ # Default to 1 for other characters
27
+ 1
28
+ end
29
+
30
+ # Returns the display width of a string
31
+ def string_width(str)
32
+ return 0 if str.nil?
33
+
34
+ str.chars.sum { |c| char_width(c) }
35
+ end
36
+
37
+ # Returns the display width of a substring from index 0 to col (exclusive)
38
+ def width_to_col(str, col)
39
+ return 0 if str.nil? || col <= 0
40
+
41
+ str.chars.take(col).sum { |c| char_width(c) }
42
+ end
43
+
44
+ # Returns the character index for a given display width position
45
+ def col_at_width(str, target_width)
46
+ return 0 if str.nil? || target_width <= 0
47
+
48
+ current_width = 0
49
+ str.chars.each_with_index do |char, index|
50
+ return index if current_width >= target_width
51
+
52
+ current_width += char_width(char)
53
+ end
54
+ str.length
55
+ end
56
+
57
+ private
58
+
59
+ def wide_char?(ord)
60
+ # CJK ranges (simplified, covers most common cases)
61
+ # Full implementation would use Unicode East Asian Width property
62
+
63
+ # Hangul Jamo
64
+ return true if ord.between?(0x1100, 0x115F)
65
+
66
+ # CJK Radicals Supplement to Enclosed CJK Letters
67
+ return true if ord.between?(0x2E80, 0x4DBF)
68
+
69
+ # CJK Unified Ideographs
70
+ return true if ord.between?(0x4E00, 0x9FFF)
71
+
72
+ # Hangul Syllables
73
+ return true if ord.between?(0xAC00, 0xD7AF)
74
+
75
+ # CJK Compatibility Ideographs
76
+ return true if ord.between?(0xF900, 0xFAFF)
77
+
78
+ # Fullwidth Forms
79
+ return true if ord.between?(0xFF00, 0xFF60)
80
+
81
+ # CJK Unified Ideographs Extension B-F
82
+ return true if ord.between?(0x20000, 0x2FA1F)
83
+
84
+ # Halfwidth Katakana (narrow, actually 1)
85
+ return false if ord.between?(0xFF61, 0xFFDC)
86
+
87
+ # Japanese Hiragana
88
+ return true if ord.between?(0x3040, 0x309F)
89
+
90
+ # Japanese Katakana
91
+ return true if ord.between?(0x30A0, 0x30FF)
92
+
93
+ # CJK Symbols and Punctuation
94
+ return true if ord.between?(0x3000, 0x303F)
95
+
96
+ false
97
+ end
98
+ end
99
+ end
100
+ end
data/lib/mui/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mui
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/mui/window.rb ADDED
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class Window
5
+ attr_accessor :x, :y, :width, :height, :cursor_row, :cursor_col, :scroll_row
6
+ attr_reader :buffer
7
+
8
+ def initialize(buffer, x: 0, y: 0, width: 80, height: 24, color_scheme: nil)
9
+ @buffer = buffer
10
+ @x = x
11
+ @y = y
12
+ @width = width
13
+ @height = height
14
+ @cursor_row = 0
15
+ @cursor_col = 0
16
+ @scroll_row = 0
17
+ @color_scheme = color_scheme
18
+ @wrap_cache = WrapCache.new
19
+ @syntax_highlighter = Highlighters::SyntaxHighlighter.new(color_scheme, buffer:)
20
+ @line_renderer = create_line_renderer
21
+ @status_line_renderer = StatusLineRenderer.new(buffer, self, color_scheme)
22
+ end
23
+
24
+ def buffer=(new_buffer)
25
+ @buffer = new_buffer
26
+ @cursor_row = 0
27
+ @cursor_col = 0
28
+ @scroll_row = 0
29
+ @wrap_cache.clear
30
+ @syntax_highlighter.buffer = new_buffer
31
+ @line_renderer = create_line_renderer
32
+ @status_line_renderer = StatusLineRenderer.new(new_buffer, self, @color_scheme)
33
+ end
34
+
35
+ def visible_height
36
+ @height - 1 # Status line only (command line is shared by all windows)
37
+ end
38
+
39
+ def visible_width
40
+ @width
41
+ end
42
+
43
+ def ensure_cursor_visible
44
+ # Calculate screen row of cursor considering line wrapping
45
+ cursor_screen_row = screen_rows_from_scroll_to_cursor
46
+
47
+ # Scroll up if cursor is above visible area
48
+ @scroll_row -= 1 while @cursor_row < @scroll_row
49
+
50
+ # Scroll down if cursor is below visible area
51
+ while cursor_screen_row >= visible_height
52
+ @scroll_row += 1
53
+ cursor_screen_row = screen_rows_from_scroll_to_cursor
54
+ end
55
+ end
56
+
57
+ def render(screen, selection: nil, search_state: nil)
58
+ options = build_render_options(selection, search_state)
59
+ screen_row = 0
60
+ logical_row = @scroll_row
61
+
62
+ while screen_row < visible_height && logical_row < @buffer.line_count
63
+ line = @buffer.line(logical_row)
64
+ wrapped_lines = WrapHelper.wrap_line(line, visible_width, cache: @wrap_cache)
65
+
66
+ wrapped_lines.each do |wrap_info|
67
+ break if screen_row >= visible_height
68
+
69
+ render_wrapped_segment(screen, logical_row, wrap_info, screen_row, options)
70
+ screen_row += 1
71
+ end
72
+
73
+ logical_row += 1
74
+ end
75
+
76
+ # Clear remaining lines
77
+ while screen_row < visible_height
78
+ clear_line(screen, screen_row)
79
+ screen_row += 1
80
+ end
81
+
82
+ @status_line_renderer.render(screen, @y + visible_height)
83
+ end
84
+
85
+ def render_wrapped_segment(screen, logical_row, wrap_info, screen_row, options)
86
+ wrap_options = options.merge(logical_row:)
87
+ @line_renderer.render_wrapped_line(screen, @y + screen_row, @x, wrap_info, wrap_options)
88
+
89
+ # Fill remaining width with spaces if line is shorter
90
+ text_width = UnicodeWidth.string_width(wrap_info[:text])
91
+ return unless text_width < visible_width
92
+
93
+ remaining_width = visible_width - text_width
94
+ fill_text = " " * remaining_width
95
+ if @color_scheme && @color_scheme[:normal]
96
+ screen.put_with_style(@y + screen_row, @x + text_width, fill_text, @color_scheme[:normal])
97
+ else
98
+ screen.put(@y + screen_row, @x + text_width, fill_text)
99
+ end
100
+ end
101
+
102
+ def screen_cursor_x
103
+ line = @buffer.line(@cursor_row) || ""
104
+ _, screen_col = WrapHelper.logical_to_screen(line, @cursor_col, visible_width, cache: @wrap_cache)
105
+ @x + screen_col
106
+ end
107
+
108
+ def screen_cursor_y
109
+ @y + screen_rows_from_scroll_to_cursor
110
+ end
111
+
112
+ # カーソル移動
113
+ def move_left
114
+ @cursor_col -= 1 if @cursor_col.positive?
115
+ end
116
+
117
+ def move_right
118
+ @cursor_col += 1 if @cursor_col < max_cursor_col
119
+ end
120
+
121
+ def move_up
122
+ @cursor_row -= 1 if @cursor_row.positive?
123
+ clamp_cursor_col
124
+ end
125
+
126
+ def move_down
127
+ @cursor_row += 1 if @cursor_row < @buffer.line_count - 1
128
+ clamp_cursor_col
129
+ end
130
+
131
+ def clamp_cursor_to_line(buffer)
132
+ max_col = [buffer.line(@cursor_row).length - 1, 0].max
133
+ @cursor_col = max_col if @cursor_col > max_col
134
+ end
135
+
136
+ # Refresh highlighters (call when custom highlighters change)
137
+ def refresh_highlighters
138
+ @line_renderer = create_line_renderer
139
+ end
140
+
141
+ private
142
+
143
+ def clear_line(screen, screen_row)
144
+ empty_line = " " * visible_width
145
+ if @color_scheme && @color_scheme[:normal]
146
+ screen.put_with_style(@y + screen_row, @x, empty_line, @color_scheme[:normal])
147
+ else
148
+ screen.put(@y + screen_row, @x, empty_line)
149
+ end
150
+ end
151
+
152
+ # Calculates screen rows from scroll_row to cursor position
153
+ def screen_rows_from_scroll_to_cursor
154
+ screen_rows = 0
155
+
156
+ # Add screen lines for rows between scroll_row and cursor_row
157
+ (@scroll_row...@cursor_row).each do |row|
158
+ line = @buffer.line(row) || ""
159
+ screen_rows += WrapHelper.screen_line_count(line, visible_width, cache: @wrap_cache)
160
+ end
161
+
162
+ # Add the row offset within the cursor's line
163
+ cursor_line = @buffer.line(@cursor_row) || ""
164
+ row_offset, = WrapHelper.logical_to_screen(cursor_line, @cursor_col, visible_width, cache: @wrap_cache)
165
+ screen_rows + row_offset
166
+ end
167
+
168
+ # Clear wrap cache when window dimensions change
169
+ def resize(new_width, new_height)
170
+ @width = new_width
171
+ @height = new_height
172
+ @wrap_cache.clear
173
+ end
174
+
175
+ def create_line_renderer
176
+ renderer = LineRenderer.new(@color_scheme)
177
+ renderer.add_highlighter(@syntax_highlighter)
178
+ renderer.add_highlighter(Highlighters::SelectionHighlighter.new(@color_scheme))
179
+ renderer.add_highlighter(Highlighters::SearchHighlighter.new(@color_scheme))
180
+
181
+ # Add buffer-specific custom highlighters
182
+ @buffer.custom_highlighters(@color_scheme).each do |highlighter|
183
+ renderer.add_highlighter(highlighter)
184
+ end
185
+
186
+ renderer
187
+ end
188
+
189
+ def build_render_options(selection, search_state)
190
+ { selection:, search_state:, buffer: @buffer }
191
+ end
192
+
193
+ def max_cursor_col
194
+ [@buffer.line(@cursor_row).length - 1, 0].max
195
+ end
196
+
197
+ def clamp_cursor_col
198
+ @cursor_col = max_cursor_col if @cursor_col > max_cursor_col
199
+ end
200
+ end
201
+ end