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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +13 -8
- data/CHANGELOG.md +99 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +301 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +149 -0
- data/lib/mui/command_completer.rb +11 -2
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +32 -2
- data/lib/mui/command_registry.rb +21 -2
- data/lib/mui/config.rb +3 -1
- data/lib/mui/editor.rb +78 -2
- data/lib/mui/handler_result.rb +13 -7
- data/lib/mui/highlighters/search_highlighter.rb +2 -1
- data/lib/mui/highlighters/syntax_highlighter.rb +3 -1
- data/lib/mui/key_handler/base.rb +87 -0
- data/lib/mui/key_handler/command_mode.rb +68 -0
- data/lib/mui/key_handler/insert_mode.rb +10 -41
- data/lib/mui/key_handler/normal_mode.rb +24 -51
- data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
- data/lib/mui/key_handler/search_mode.rb +10 -7
- data/lib/mui/key_handler/visual_mode.rb +15 -10
- data/lib/mui/key_notation_parser.rb +152 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -0
- data/lib/mui/line_renderer.rb +52 -1
- data/lib/mui/mode_manager.rb +3 -2
- data/lib/mui/screen.rb +24 -6
- data/lib/mui/search_state.rb +61 -28
- data/lib/mui/syntax/language_detector.rb +33 -1
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
- data/lib/mui/terminal_adapter/curses.rb +13 -11
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +83 -40
- data/lib/mui/window_manager.rb +7 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +15 -0
- 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 (`&`, `<`)
|
|
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
|
-
|
|
16
|
+
all_commands = COMMANDS + plugin_command_names
|
|
17
17
|
|
|
18
|
-
|
|
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
|
data/lib/mui/command_line.rb
CHANGED
|
@@ -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
|
data/lib/mui/command_registry.rb
CHANGED
|
@@ -12,14 +12,33 @@ module Mui
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def execute(name, context, *)
|
|
15
|
-
command =
|
|
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 = {}
|