mui 0.2.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +13 -8
  3. data/CHANGELOG.md +99 -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/lib/mui/command_completer.rb +11 -2
  14. data/lib/mui/command_history.rb +89 -0
  15. data/lib/mui/command_line.rb +32 -2
  16. data/lib/mui/command_registry.rb +21 -2
  17. data/lib/mui/config.rb +3 -1
  18. data/lib/mui/editor.rb +78 -2
  19. data/lib/mui/handler_result.rb +13 -7
  20. data/lib/mui/highlighters/search_highlighter.rb +2 -1
  21. data/lib/mui/highlighters/syntax_highlighter.rb +3 -1
  22. data/lib/mui/key_handler/base.rb +87 -0
  23. data/lib/mui/key_handler/command_mode.rb +68 -0
  24. data/lib/mui/key_handler/insert_mode.rb +10 -41
  25. data/lib/mui/key_handler/normal_mode.rb +24 -51
  26. data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
  27. data/lib/mui/key_handler/search_mode.rb +10 -7
  28. data/lib/mui/key_handler/visual_mode.rb +15 -10
  29. data/lib/mui/key_notation_parser.rb +152 -0
  30. data/lib/mui/key_sequence.rb +67 -0
  31. data/lib/mui/key_sequence_buffer.rb +85 -0
  32. data/lib/mui/key_sequence_handler.rb +163 -0
  33. data/lib/mui/key_sequence_matcher.rb +79 -0
  34. data/lib/mui/line_renderer.rb +52 -1
  35. data/lib/mui/mode_manager.rb +3 -2
  36. data/lib/mui/screen.rb +24 -6
  37. data/lib/mui/search_state.rb +61 -28
  38. data/lib/mui/syntax/language_detector.rb +33 -1
  39. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  40. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  41. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  42. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  43. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  44. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  45. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  46. data/lib/mui/terminal_adapter/curses.rb +13 -11
  47. data/lib/mui/version.rb +1 -1
  48. data/lib/mui/window.rb +83 -40
  49. data/lib/mui/window_manager.rb +7 -0
  50. data/lib/mui/wrap_cache.rb +40 -0
  51. data/lib/mui/wrap_helper.rb +84 -0
  52. data/lib/mui.rb +15 -0
  53. metadata +26 -3
data/docs/plugins.md ADDED
@@ -0,0 +1,285 @@
1
+ ---
2
+ title: Plugins
3
+ layout: default
4
+ nav_order: 5
5
+ ---
6
+
7
+ # Plugins
8
+ {: .no_toc }
9
+
10
+ ## Table of contents
11
+ {: .no_toc .text-delta }
12
+
13
+ 1. TOC
14
+ {:toc}
15
+
16
+ ---
17
+
18
+ ## Overview
19
+
20
+ Mui's plugin system allows you to extend the editor's functionality using Ruby gems. Plugins can:
21
+
22
+ - Add custom commands
23
+ - Define key mappings
24
+ - React to events (autocmd)
25
+ - Integrate with external tools
26
+ - Add syntax highlighting
27
+
28
+ ## Official Plugins
29
+
30
+ | Plugin | Description | Install |
31
+ |--------|-------------|---------|
32
+ | [mui-lsp](https://github.com/S-H-GAMELINKS/mui-lsp) | LSP support | `gem install mui-lsp` |
33
+ | [mui-git](https://github.com/S-H-GAMELINKS/mui-git) | Git integration | `gem install mui-git` |
34
+ | [mui-fzf](https://github.com/S-H-GAMELINKS/mui-fzf) | fzf integration | `gem install mui-fzf` |
35
+
36
+ ## Using Plugins
37
+
38
+ ### Loading Plugins
39
+
40
+ Add plugins to your `~/.muirc`:
41
+
42
+ ```ruby
43
+ Mui.use "mui-lsp"
44
+ Mui.use "mui-git"
45
+ Mui.use "mui-fzf"
46
+ ```
47
+
48
+ Plugins are automatically installed via `bundler/inline` on first startup.
49
+
50
+ ### LSP Configuration
51
+
52
+ Configure LSP servers with `Mui.lsp`:
53
+
54
+ ```ruby
55
+ Mui.use "mui-lsp"
56
+
57
+ Mui.lsp do
58
+ # Use preset configuration
59
+ use :ruby
60
+
61
+ # Custom server configuration
62
+ server :typescript,
63
+ command: ["typescript-language-server", "--stdio"],
64
+ filetypes: ["typescript", "typescriptreact"]
65
+ end
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Creating Plugins
71
+
72
+ ### Class-based Plugin
73
+
74
+ ```ruby
75
+ class MyPlugin < Mui::Plugin
76
+ name :my_plugin
77
+ version "1.0.0"
78
+ description "My awesome plugin"
79
+
80
+ # Optional: declare dependencies
81
+ depends_on :other_plugin
82
+
83
+ def setup
84
+ # Define commands, keymaps, autocmds here
85
+ end
86
+ end
87
+ ```
88
+
89
+ ### DSL-based Plugin
90
+
91
+ ```ruby
92
+ Mui.define_plugin(:my_plugin) do
93
+ # Define commands, keymaps, autocmds here
94
+ end
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Plugin API
100
+
101
+ ### Commands
102
+
103
+ ```ruby
104
+ def setup
105
+ command :greet do |ctx|
106
+ ctx.set_message("Hello!")
107
+ end
108
+
109
+ command :greet_name do |ctx, name|
110
+ ctx.set_message("Hello, #{name}!")
111
+ end
112
+ end
113
+ ```
114
+
115
+ ### Key Mappings
116
+
117
+ ```ruby
118
+ def setup
119
+ # Normal mode mapping
120
+ keymap :normal, "<Leader>g" do |ctx|
121
+ ctx.editor.execute_command("greet")
122
+ end
123
+
124
+ # Insert mode mapping
125
+ keymap :insert, "<C-g>" do |ctx|
126
+ ctx.insert_text("Generated text")
127
+ end
128
+
129
+ # Multi-key sequence
130
+ keymap :normal, "<Leader>gg" do |ctx|
131
+ ctx.set_message("Leader g g pressed!")
132
+ end
133
+ end
134
+ ```
135
+
136
+ ### Autocmd Events
137
+
138
+ ```ruby
139
+ def setup
140
+ autocmd :BufEnter, pattern: "*.rb" do |ctx|
141
+ ctx.set_message("Opened Ruby file: #{ctx.buffer.file_path}")
142
+ end
143
+
144
+ autocmd :BufWritePre do |ctx|
145
+ # Run before every file save
146
+ end
147
+
148
+ autocmd :InsertLeave do |ctx|
149
+ # Run when leaving insert mode
150
+ end
151
+ end
152
+ ```
153
+
154
+ ---
155
+
156
+ ## CommandContext API
157
+
158
+ The `ctx` object passed to handlers provides access to editor internals:
159
+
160
+ ### Messages
161
+
162
+ ```ruby
163
+ ctx.set_message("Info message")
164
+ ctx.set_error("Error message")
165
+ ```
166
+
167
+ ### Editor Access
168
+
169
+ ```ruby
170
+ ctx.editor # Editor instance
171
+ ctx.buffer # Current buffer
172
+ ctx.window # Current window
173
+ ctx.mode # Current mode (:normal, :insert, etc.)
174
+ ```
175
+
176
+ ### Mode Control
177
+
178
+ ```ruby
179
+ ctx.change_mode(:normal)
180
+ ctx.change_mode(:insert)
181
+ ```
182
+
183
+ ### Text Manipulation
184
+
185
+ ```ruby
186
+ ctx.insert_text("text") # Insert at cursor
187
+ ctx.buffer.lines # Get all lines
188
+ ctx.buffer.line(n) # Get line n
189
+ ```
190
+
191
+ ### Cursor
192
+
193
+ ```ruby
194
+ ctx.cursor_row # Current row (0-indexed)
195
+ ctx.cursor_col # Current column (0-indexed)
196
+ ctx.move_cursor(row, col) # Move cursor
197
+ ```
198
+
199
+ ### File Operations
200
+
201
+ ```ruby
202
+ ctx.buffer.file_path # Current file path
203
+ ctx.buffer.modified? # Has unsaved changes?
204
+ ctx.editor.execute_command("w") # Save file
205
+ ctx.editor.execute_command("e filename") # Open file
206
+ ```
207
+
208
+ ### Scratch Buffers
209
+
210
+ ```ruby
211
+ ctx.open_scratch_buffer("[Results]", "Content here")
212
+ ```
213
+
214
+ ### Jobs
215
+
216
+ See [Jobs]({{ site.baseurl }}/jobs) for async job execution.
217
+
218
+ ### Interactive Commands
219
+
220
+ ```ruby
221
+ if ctx.command_exists?("fzf")
222
+ result = ctx.run_interactive_command("fzf")
223
+ ctx.editor.execute_command("e #{result}") if result
224
+ end
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Publishing Plugins
230
+
231
+ ### Gem Structure
232
+
233
+ ```
234
+ mui-myplugin/
235
+ ├── lib/
236
+ │ └── mui_myplugin.rb
237
+ ├── mui-myplugin.gemspec
238
+ └── README.md
239
+ ```
240
+
241
+ ### Gemspec
242
+
243
+ ```ruby
244
+ Gem::Specification.new do |spec|
245
+ spec.name = "mui-myplugin"
246
+ spec.version = "1.0.0"
247
+ spec.summary = "My Mui plugin"
248
+ spec.files = Dir["lib/**/*.rb"]
249
+ spec.require_paths = ["lib"]
250
+
251
+ spec.add_dependency "mui", ">= 0.2.0"
252
+ end
253
+ ```
254
+
255
+ ### Plugin File
256
+
257
+ ```ruby
258
+ # lib/mui_myplugin.rb
259
+ require "mui"
260
+
261
+ class MuiMyplugin < Mui::Plugin
262
+ name :myplugin
263
+ version "1.0.0"
264
+
265
+ def setup
266
+ command :my_command do |ctx|
267
+ ctx.set_message("Hello from myplugin!")
268
+ end
269
+ end
270
+ end
271
+ ```
272
+
273
+ ### Publishing
274
+
275
+ ```bash
276
+ gem build mui-myplugin.gemspec
277
+ gem push mui-myplugin-1.0.0.gem
278
+ ```
279
+
280
+ Users can then install with:
281
+
282
+ ```ruby
283
+ # ~/.muirc
284
+ Mui.use "mui-myplugin"
285
+ ```
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: Syntax Highlighting
3
+ layout: default
4
+ nav_order: 7
5
+ ---
6
+
7
+ # Syntax Highlighting
8
+ {: .no_toc }
9
+
10
+ ## Table of contents
11
+ {: .no_toc .text-delta }
12
+
13
+ 1. TOC
14
+ {:toc}
15
+
16
+ ---
17
+
18
+ ## Overview
19
+
20
+ Mui includes built-in syntax highlighting for 9 programming languages. Highlighting is automatic based on file extension.
21
+
22
+ ## Supported Languages
23
+
24
+ | Language | Extensions |
25
+ |----------|------------|
26
+ | Ruby | `.rb`, `.rake`, `.gemspec` |
27
+ | C | `.c`, `.h`, `.y` |
28
+ | Go | `.go` |
29
+ | Rust | `.rs` |
30
+ | JavaScript | `.js`, `.mjs`, `.cjs`, `.jsx` |
31
+ | TypeScript | `.ts`, `.tsx`, `.mts`, `.cts` |
32
+ | Markdown | `.md`, `.markdown` |
33
+ | HTML | `.html`, `.htm`, `.xhtml` |
34
+ | CSS | `.css`, `.scss`, `.sass` |
35
+
36
+ ## Configuration
37
+
38
+ ### Enable/Disable
39
+
40
+ ```ruby
41
+ # ~/.muirc
42
+ Mui.set :syntax, true # Enable (default)
43
+ Mui.set :syntax, false # Disable
44
+ ```
45
+
46
+ ## Language Features
47
+
48
+ ### Ruby
49
+
50
+ - Keywords (`def`, `class`, `module`, `if`, `end`, etc.)
51
+ - Strings (single, double, heredoc)
52
+ - Comments (`#`, `=begin`/`=end`)
53
+ - Numbers
54
+ - Symbols (`:symbol`)
55
+ - Constants (`CONSTANT`, `ClassName`)
56
+ - Instance variables (`@foo`, `@@bar`)
57
+ - Global variables (`$stdout`)
58
+ - Method calls (`.to_s`, `.each`)
59
+
60
+ ### C
61
+
62
+ - Keywords (`int`, `char`, `struct`, `if`, `for`, etc.)
63
+ - Strings and character literals
64
+ - Comments (`//`, `/* */`)
65
+ - Numbers
66
+ - Preprocessor directives (`#include`, `#define`)
67
+
68
+ ### Go
69
+
70
+ - Keywords (`func`, `package`, `import`, `go`, `defer`, etc.)
71
+ - Types (`int`, `string`, `bool`, etc.)
72
+ - Constants (`true`, `false`, `nil`, `iota`)
73
+ - Strings (regular and raw backtick strings)
74
+ - Comments (`//`, `/* */`)
75
+
76
+ ### Rust
77
+
78
+ - Keywords (`fn`, `let`, `mut`, `impl`, `trait`, etc.)
79
+ - Macros (`println!`, `vec!`)
80
+ - Lifetimes (`'a`, `'static`)
81
+ - Attributes (`#[derive]`, `#[cfg]`)
82
+ - Doc comments (`///`, `//!`)
83
+ - Raw strings (`r#"..."#`)
84
+
85
+ ### JavaScript
86
+
87
+ - ES6+ keywords (`const`, `let`, `async`, `await`, `class`)
88
+ - Template literals (`` `template ${expr}` ``)
89
+ - Regex literals (`/pattern/flags`)
90
+ - BigInt (`123n`)
91
+ - Strings and comments
92
+
93
+ ### TypeScript
94
+
95
+ All JavaScript features plus:
96
+ - Type keywords (`interface`, `type`, `enum`, `declare`, `abstract`)
97
+ - Type annotations
98
+
99
+ ### Markdown
100
+
101
+ - Headings (`#`, `##`, etc.)
102
+ - Emphasis (`*italic*`, `**bold**`)
103
+ - Code blocks (fenced and indented)
104
+ - Links (`[text](url)`)
105
+ - Lists (ordered and unordered)
106
+ - Blockquotes (`>`)
107
+
108
+ ### HTML
109
+
110
+ - Tags (`<div>`, `</div>`, `<br/>`)
111
+ - Attributes (`class="..."`, `id="..."`)
112
+ - Comments (`<!-- -->`)
113
+ - DOCTYPE
114
+ - Entities (`&amp;`, `&lt;`)
115
+
116
+ ### CSS
117
+
118
+ - Selectors (`.class`, `#id`, `:pseudo`, `::pseudo-element`)
119
+ - Properties and values
120
+ - Hex colors (`#fff`, `#ffffff`)
121
+ - At-rules (`@media`, `@import`, `@keyframes`)
122
+ - Functions (`calc()`, `rgb()`, `var()`)
123
+
124
+ ## Theme Colors
125
+
126
+ Syntax colors are defined per theme. All 8 built-in themes include syntax highlighting colors:
127
+
128
+ - `mui` (default)
129
+ - `solarized_dark`
130
+ - `solarized_light`
131
+ - `monokai`
132
+ - `nord`
133
+ - `gruvbox_dark`
134
+ - `dracula`
135
+ - `tokyo_night`
136
+
137
+ Each theme defines colors for:
138
+
139
+ | Element | Description |
140
+ |---------|-------------|
141
+ | `syntax_keyword` | Language keywords |
142
+ | `syntax_string` | String literals |
143
+ | `syntax_comment` | Comments |
144
+ | `syntax_number` | Numeric literals |
145
+ | `syntax_type` | Types and classes |
146
+ | `syntax_function` | Function names |
147
+ | `syntax_variable` | Variables |
148
+ | `syntax_constant` | Constants |
149
+ | `syntax_operator` | Operators |
@@ -13,9 +13,18 @@ module Mui
13
13
  ].freeze
14
14
 
15
15
  def complete(prefix)
16
- return COMMANDS.sort if prefix.empty?
16
+ all_commands = COMMANDS + plugin_command_names
17
17
 
18
- COMMANDS.select { |cmd| cmd.start_with?(prefix) }.sort
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)
19
28
  end
20
29
  end
21
30
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Manages command history with file persistence
5
+ class CommandHistory
6
+ MAX_HISTORY = 100
7
+ HISTORY_FILE = File.expand_path("~/.mui_history")
8
+
9
+ attr_reader :history
10
+
11
+ def initialize(history_file: HISTORY_FILE)
12
+ @history_file = history_file
13
+ @history = []
14
+ @index = nil
15
+ @saved_input = nil
16
+ load_from_file
17
+ end
18
+
19
+ def add(command)
20
+ return if command.strip.empty?
21
+
22
+ @history.delete(command)
23
+ @history.push(command)
24
+ @history.shift if @history.size > MAX_HISTORY
25
+ save_to_file
26
+ end
27
+
28
+ def previous(current_input)
29
+ return nil if @history.empty?
30
+
31
+ if @index.nil?
32
+ @saved_input = current_input
33
+ @index = @history.size - 1
34
+ elsif @index.positive?
35
+ @index -= 1
36
+ else
37
+ return nil
38
+ end
39
+
40
+ @history[@index]
41
+ end
42
+
43
+ def next_entry
44
+ return nil if @index.nil?
45
+
46
+ if @index < @history.size - 1
47
+ @index += 1
48
+ @history[@index]
49
+ else
50
+ result = @saved_input
51
+ reset
52
+ result
53
+ end
54
+ end
55
+
56
+ def reset
57
+ @index = nil
58
+ @saved_input = nil
59
+ end
60
+
61
+ def browsing?
62
+ !@index.nil?
63
+ end
64
+
65
+ def size
66
+ @history.size
67
+ end
68
+
69
+ def empty?
70
+ @history.empty?
71
+ end
72
+
73
+ private
74
+
75
+ def load_from_file
76
+ return unless File.exist?(@history_file)
77
+
78
+ @history = File.readlines(@history_file, chomp: true).last(MAX_HISTORY)
79
+ rescue StandardError
80
+ @history = []
81
+ end
82
+
83
+ def save_to_file
84
+ File.write(@history_file, "#{@history.join("\n")}\n")
85
+ rescue StandardError
86
+ # Ignore write failures
87
+ end
88
+ end
89
+ end
@@ -5,11 +5,12 @@ module Mui
5
5
  # Commands that accept file path arguments
6
6
  FILE_COMMANDS = %w[e w sp split vs vsplit tabnew tabe tabedit].freeze
7
7
 
8
- attr_reader :buffer, :cursor_pos
8
+ attr_reader :buffer, :cursor_pos, :history
9
9
 
10
- def initialize
10
+ def initialize(history: CommandHistory.new)
11
11
  @buffer = ""
12
12
  @cursor_pos = 0
13
+ @history = history
13
14
  end
14
15
 
15
16
  def input(char)
@@ -27,6 +28,25 @@ module Mui
27
28
  def clear
28
29
  @buffer = ""
29
30
  @cursor_pos = 0
31
+ @history.reset
32
+ end
33
+
34
+ def history_previous
35
+ result = @history.previous(@buffer)
36
+ return false unless result
37
+
38
+ @buffer = result.dup
39
+ @cursor_pos = @buffer.length
40
+ true
41
+ end
42
+
43
+ def history_next
44
+ result = @history.next_entry
45
+ return false unless result
46
+
47
+ @buffer = result.dup
48
+ @cursor_pos = @buffer.length
49
+ true
30
50
  end
31
51
 
32
52
  def move_cursor_left
@@ -38,6 +58,9 @@ module Mui
38
58
  end
39
59
 
40
60
  def execute
61
+ command = @buffer.strip
62
+ @history.add(command) unless command.empty?
63
+ @history.reset
41
64
  result = parse(@buffer)
42
65
  @buffer = ""
43
66
  @cursor_pos = 0
@@ -127,6 +150,13 @@ module Mui
127
150
  { action: :tab_move, position: ::Regexp.last_match(1).to_i }
128
151
  when /^(\d+)tabn(?:ext)?/, /^tabn(?:ext)?\s+(\d+)/
129
152
  { action: :tab_go, index: ::Regexp.last_match(1).to_i - 1 }
153
+ when /^!(.*)$/
154
+ shell_cmd = ::Regexp.last_match(1).strip
155
+ if shell_cmd.empty?
156
+ { action: :shell_command_error, message: "E471: Argument required" }
157
+ else
158
+ { action: :shell_command, command: shell_cmd }
159
+ end
130
160
  when /^(\d+)$/
131
161
  { action: :goto_line, line_number: ::Regexp.last_match(1).to_i }
132
162
  else
@@ -12,14 +12,33 @@ module Mui
12
12
  end
13
13
 
14
14
  def execute(name, context, *)
15
- command = @commands[name.to_sym]
15
+ command = find(name)
16
16
  raise UnknownCommandError, name unless command
17
17
 
18
18
  command.call(context, *)
19
19
  end
20
20
 
21
21
  def exists?(name)
22
- @commands.key?(name.to_sym)
22
+ @commands.key?(name.to_sym) || plugin_command_exists?(name)
23
+ end
24
+
25
+ def find(name)
26
+ # Built-in commands take precedence
27
+ command = @commands[name.to_sym]
28
+ return command if command
29
+
30
+ # Fall back to plugin commands
31
+ plugin_commands[name.to_sym]
32
+ end
33
+
34
+ private
35
+
36
+ def plugin_command_exists?(name)
37
+ plugin_commands.key?(name.to_sym)
38
+ end
39
+
40
+ def plugin_commands
41
+ Mui.config.commands
23
42
  end
24
43
  end
25
44
  end
data/lib/mui/config.rb CHANGED
@@ -11,7 +11,9 @@ module Mui
11
11
  shiftwidth: 2, # Indent width for > and < commands
12
12
  expandtab: true, # Use spaces instead of tabs
13
13
  tabstop: 8, # Tab display width
14
- reselect_after_indent: false # Keep selection after > / < in visual mode
14
+ reselect_after_indent: false, # Keep selection after > / < in visual mode
15
+ leader: "\\", # Leader key for key mappings (default: backslash)
16
+ timeoutlen: 1000 # Timeout for multi-key sequences in milliseconds
15
17
  }
16
18
  @plugins = []
17
19
  @keymaps = {}