Skip to content

Commit 278f933

Browse files
authored
Add user command view (#91)
* Define user_command_view_toggle_n user event * Add ui.user_command config * Implement user command view * Open user command view from list view * Add core.user_command config * Fix to read user commands * Extract functions * Fix core.user_commands keys * Fix first parent hash marker * Allows switching between user command views * Define area_width and area_height * Fix OptionalCoreUserCommandConfig Deserialize * Fix build_block_lines interface * Fix helps * Fix core.user_command config format * Update README * Add screenshots
1 parent 5a096ab commit 278f933

20 files changed

+1002
-98
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ rust-version = "1.87.0"
1515
exclude = ["/.github", "/img"]
1616

1717
[dependencies]
18+
ansi-to-tui = "7.0.0"
1819
arboard = "3.6.0"
1920
base64 = "0.22.1"
2021
chrono = "0.4.41"

README.md

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,17 @@ The default key bindings can be overridden. Please refer to [default-keybind.tom
195195
| <kbd>Ctrl-g</kbd> | Toggle ignore case (if searching) | `ignore_case_toggle` |
196196
| <kbd>Ctrl-x</kbd> | Toggle fuzzy match (if searching) | `fuzzy_toggle` |
197197
| <kbd>c/C</kbd> | Copy commit short/full hash | `short_copy` `full_copy` |
198+
| <kbd>d</kbd> | Toggle custom user command view | `user_command_view_toggle_1` |
198199

199200
#### Commit Detail
200201

201-
| Key | Description | Corresponding keybind |
202-
| ----------------------------------- | --------------------------- | ----------------------------- |
203-
| <kbd>Esc</kbd> <kbd>Backspace</kbd> | Close commit details | `close` `cancel` |
204-
| <kbd>Down/Up</kbd> <kbd>j/k</kbd> | Scroll down/up | `navigate_down` `navigate_up` |
205-
| <kbd>g/G</kbd> | Go to top/bottom | `go_to_top` `go_to_bottom` |
206-
| <kbd>c/C</kbd> | Copy commit short/full hash | `short_copy` `full_copy` |
202+
| Key | Description | Corresponding keybind |
203+
| ----------------------------------- | ------------------------------- | ----------------------------- |
204+
| <kbd>Esc</kbd> <kbd>Backspace</kbd> | Close commit details | `close` `cancel` |
205+
| <kbd>Down/Up</kbd> <kbd>j/k</kbd> | Scroll down/up | `navigate_down` `navigate_up` |
206+
| <kbd>g/G</kbd> | Go to top/bottom | `go_to_top` `go_to_bottom` |
207+
| <kbd>c/C</kbd> | Copy commit short/full hash | `short_copy` `full_copy` |
208+
| <kbd>d</kbd> | Toggle custom user command view | `user_command_view_toggle_1` |
207209

208210
#### Refs List
209211

@@ -215,6 +217,14 @@ The default key bindings can be overridden. Please refer to [default-keybind.tom
215217
| <kbd>Right/Left</kbd> <kbd>l/h</kbd> | Open/Close node | `navigate_right` `navigate_left` |
216218
| <kbd>c</kbd> | Copy ref name | `short_copy` |
217219

220+
#### User Command
221+
222+
| Key | Description | Corresponding keybind |
223+
| ------------------------------------------------ | ------------------ | ------------------------------ |
224+
| <kbd>Esc</kbd> <kbd>Backspace</kbd> <kbd>?</kbd> | Close user command | `close` `cancel` `help_toggle` |
225+
| <kbd>Down/Up</kbd> <kbd>j/k</kbd> | Scroll down/up | `navigate_down` `navigate_up` |
226+
| <kbd>g/G</kbd> | Go to top/bottom | `go_to_top` `go_to_bottom` |
227+
218228
#### Help
219229

220230
| Key | Description | Corresponding keybind |
@@ -253,6 +263,13 @@ ignore_case = false
253263
# type: boolean
254264
fuzzy = false
255265

266+
[core.user_command]
267+
# The command definition for generating the content displayed in the user command view.
268+
# Multiple commands can be specified in the format commands_{n}.
269+
# For details about user command, see the separate User command section.
270+
# type: object
271+
commands_1 = { name = "git diff", commands = ["git", "--no-pager", "diff", "--color=always", "{{first_parent_hash}}", "{{target_hash}}"]}
272+
256273
[ui.common]
257274
# The type of a cursor to display in the input.
258275
# If `cursor_type = "Native"` is set, the terminal native cursor is used.
@@ -324,6 +341,50 @@ background = "#00000000"
324341

325342
</details>
326343

344+
### User command
345+
346+
The User command view allows you to display the output (stdout) of your custom external commands.
347+
This allows you to do things like view commit diffs using your favorite tools.
348+
349+
To define a user command, you need to configure the following two settings:
350+
- Keybinding definition. Specify the key to display each user command.
351+
- Config: `keybind.user_command_view_toggle_{n}`
352+
- Command definition. Specify the actual command you want to execute.
353+
- Config: `core.user_command.commands_{n}`
354+
355+
<details>
356+
<summary>Configuration example</summary>
357+
358+
```toml
359+
[keybind]
360+
user_command_view_toggle_1 = ["d"]
361+
user_command_view_toggle_2 = ["shift-d"]
362+
363+
[core.user_command]
364+
commands_1 = { "name" = "git diff", commands = ["git", "--no-pager", "diff", "--color=always", "{{first_parent_hash}}", "{{target_hash}}"] }
365+
commands_2 = { "name" = "xxx", commands = ["xxx", "{{first_parent_hash}}", "{{target_hash}}", "--width", "{{area_width}}", "--height", "{{area_height}}"] }
366+
```
367+
368+
</details>
369+
370+
#### Variables
371+
372+
The following variables can be used in command definitions.
373+
They will be replaced with their respective values command is executed.
374+
375+
- `{{target_hash}}`
376+
- The hash of the selected commit.
377+
- example: `b0ce4cb9c798576af9b4accc9f26ddce5e72063d`
378+
- `{{first_parent_hash}}`
379+
- The hash of the first parent of the selected commit.
380+
- example: `c103d9744df8ebf100773a11345f011152ec5581`
381+
- `{{area_width}}`
382+
- Width of the user command display area (number of cells).
383+
- example: 80
384+
- `{{area_height}}`
385+
- Height of the user command display area (number of cells).
386+
- example: 30
387+
327388
## Compatibility
328389

329390
### Supported terminals
@@ -370,6 +431,8 @@ Contributions that do not follow these guidelines may not be accepted.
370431
<img src="./img/refs.png" width=600>
371432
<img src="./img/searching.png" width=600>
372433
<img src="./img/applied.png" width=600>
434+
<img src="./img/diff_git.png" width=600>
435+
<img src="./img/diff_difft.png" width=600>
373436

374437
The following repositories are used as these examples:
375438

assets/default-keybind.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ search = ["/"]
3030
ignore_case_toggle = ["ctrl-g"]
3131
fuzzy_toggle = ["ctrl-x"]
3232

33+
user_command_view_toggle_1 = ["d"]
34+
3335
# copy part of information, ex: copy the short commit hash not all
3436
short_copy = ["c"]
3537
full_copy = ["shift-c"]

img/diff_delta.png

852 KB
Loading

img/diff_difft.png

878 KB
Loading

img/diff_git.png

871 KB
Loading

src/app.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ pub struct App<'a> {
4040
status_line: StatusLine,
4141

4242
keybind: &'a KeyBind,
43+
core_config: &'a CoreConfig,
4344
ui_config: &'a UiConfig,
4445
color_theme: &'a ColorTheme,
4546
image_protocol: ImageProtocol,
4647
tx: Sender,
48+
4749
numeric_prefix: String,
50+
view_area: Rect,
4851
}
4952

5053
impl<'a> App<'a> {
@@ -97,11 +100,13 @@ impl<'a> App<'a> {
97100
status_line: StatusLine::None,
98101
view,
99102
keybind,
103+
core_config,
100104
ui_config,
101105
color_theme,
102106
image_protocol,
103107
tx,
104108
numeric_prefix: String::new(),
109+
view_area: Rect::default(),
105110
}
106111
}
107112
}
@@ -189,6 +194,15 @@ impl App<'_> {
189194
AppEvent::ClearDetail => {
190195
self.clear_detail();
191196
}
197+
AppEvent::OpenUserCommand(n) => {
198+
self.open_user_command(n);
199+
}
200+
AppEvent::CloseUserCommand => {
201+
self.close_user_command();
202+
}
203+
AppEvent::ClearUserCommand => {
204+
self.clear_user_command();
205+
}
192206
AppEvent::OpenRefs => {
193207
self.open_refs();
194208
}
@@ -238,6 +252,8 @@ impl App<'_> {
238252
let [view_area, status_line_area] =
239253
Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(f.area());
240254

255+
self.update_state(view_area);
256+
241257
self.view.render(f, view_area);
242258
self.render_status_line(f, status_line_area);
243259
}
@@ -305,6 +321,10 @@ impl App<'_> {
305321
}
306322

307323
impl App<'_> {
324+
fn update_state(&mut self, view_area: Rect) {
325+
self.view_area = view_area;
326+
}
327+
308328
fn open_detail(&mut self) {
309329
if let View::List(ref mut view) = self.view {
310330
let commit_list_state = view.take_list_state();
@@ -347,6 +367,108 @@ impl App<'_> {
347367
}
348368
}
349369

370+
fn open_user_command(&mut self, user_command_number: usize) {
371+
if let View::List(ref mut view) = self.view {
372+
let commit_list_state = view.take_list_state();
373+
let selected = commit_list_state.selected_commit_hash().clone();
374+
let (commit, _) = self.repository.commit_detail(&selected);
375+
self.view = View::of_user_command_from_list(
376+
commit_list_state,
377+
commit,
378+
user_command_number,
379+
self.view_area,
380+
self.core_config,
381+
self.ui_config,
382+
self.color_theme,
383+
self.image_protocol,
384+
self.tx.clone(),
385+
);
386+
} else if let View::Detail(ref mut view) = self.view {
387+
let commit_list_state = view.take_list_state();
388+
let selected = commit_list_state.selected_commit_hash().clone();
389+
let (commit, _) = self.repository.commit_detail(&selected);
390+
self.view = View::of_user_command_from_detail(
391+
commit_list_state,
392+
commit,
393+
user_command_number,
394+
self.view_area,
395+
self.core_config,
396+
self.ui_config,
397+
self.color_theme,
398+
self.image_protocol,
399+
self.tx.clone(),
400+
);
401+
} else if let View::UserCommand(ref mut view) = self.view {
402+
let commit_list_state = view.take_list_state();
403+
let selected = commit_list_state.selected_commit_hash().clone();
404+
let (commit, _) = self.repository.commit_detail(&selected);
405+
if view.before_view_is_list() {
406+
self.view = View::of_user_command_from_list(
407+
commit_list_state,
408+
commit,
409+
user_command_number,
410+
self.view_area,
411+
self.core_config,
412+
self.ui_config,
413+
self.color_theme,
414+
self.image_protocol,
415+
self.tx.clone(),
416+
);
417+
} else {
418+
self.view = View::of_user_command_from_detail(
419+
commit_list_state,
420+
commit,
421+
user_command_number,
422+
self.view_area,
423+
self.core_config,
424+
self.ui_config,
425+
self.color_theme,
426+
self.image_protocol,
427+
self.tx.clone(),
428+
);
429+
}
430+
}
431+
}
432+
433+
fn close_user_command(&mut self) {
434+
if let View::UserCommand(ref mut view) = self.view {
435+
let commit_list_state = view.take_list_state();
436+
let selected = commit_list_state.selected_commit_hash().clone();
437+
let (commit, changes) = self.repository.commit_detail(&selected);
438+
let refs = self
439+
.repository
440+
.refs(&selected)
441+
.into_iter()
442+
.cloned()
443+
.collect();
444+
if view.before_view_is_list() {
445+
self.view = View::of_list(
446+
commit_list_state,
447+
self.ui_config,
448+
self.color_theme,
449+
self.tx.clone(),
450+
);
451+
} else {
452+
self.view = View::of_detail(
453+
commit_list_state,
454+
commit,
455+
changes,
456+
refs,
457+
self.ui_config,
458+
self.color_theme,
459+
self.image_protocol,
460+
self.tx.clone(),
461+
);
462+
}
463+
}
464+
}
465+
466+
fn clear_user_command(&mut self) {
467+
if let View::UserCommand(ref mut view) = self.view {
468+
view.clear();
469+
}
470+
}
471+
350472
fn open_refs(&mut self) {
351473
if let View::List(ref mut view) = self.view {
352474
let commit_list_state = view.take_list_state();
@@ -381,6 +503,7 @@ impl App<'_> {
381503
self.image_protocol,
382504
self.tx.clone(),
383505
self.keybind,
506+
self.core_config,
384507
);
385508
}
386509

0 commit comments

Comments
 (0)