Skip to content

Commit 9080cc5

Browse files
feat: Add an integrated terminal.
This commit adds an integrated terminal to Helix, which can be opened by using the :term command. Co-authored-by: Andrey Tkachenko <[email protected]>
1 parent 0ad2f02 commit 9080cc5

File tree

20 files changed

+1423
-81
lines changed

20 files changed

+1423
-81
lines changed

Cargo.lock

Lines changed: 399 additions & 69 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
@@ -4,6 +4,7 @@ members = [
44
"helix-core",
55
"helix-graphics",
66
"helix-input",
7+
"helix-terminal-view",
78
"helix-view",
89
"helix-term",
910
"helix-tui",

helix-term/src/application.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,12 @@ impl Application {
678678
return true;
679679
}
680680
}
681+
EditorEvent::TerminalEvent(event) => {
682+
let needs_render = self.editor.handle_virtual_terminal_events(event).await;
683+
if needs_render {
684+
self.render().await;
685+
}
686+
}
681687
}
682688

683689
false

helix-term/src/commands.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub(crate) mod dap;
22
pub(crate) mod lsp;
33
pub(crate) mod syntax;
44
pub(crate) mod typed;
5+
pub(crate) mod vte;
56

67
pub use dap::*;
78
use futures_util::FutureExt;
@@ -18,6 +19,7 @@ use tui::{
1819
widgets::Cell,
1920
};
2021
pub use typed::*;
22+
pub use vte::*;
2123

2224
use helix_core::{
2325
char_idx_at_visual_offset,
@@ -615,6 +617,8 @@ impl MappableCommand {
615617
goto_prev_tabstop, "Goto next snippet placeholder",
616618
rotate_selections_first, "Make the first selection your primary one",
617619
rotate_selections_last, "Make the last selection your primary one",
620+
toggle_terminal, "Toggle integrated terminal",
621+
close_terminal, "Close active terminal",
618622
);
619623
}
620624

helix-term/src/commands/lsp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,7 @@ fn compute_inlay_hints_for_view(
14021402
};
14031403

14041404
let width = label.width();
1405-
let limit = limit.get().into();
1405+
let limit: usize = limit.get().into();
14061406
if width > limit {
14071407
let mut floor_boundary = 0;
14081408
let mut acc = 0;

helix-term/src/commands/typed.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,20 @@ fn new_file(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> an
571571
Ok(())
572572
}
573573

574+
fn toggle_terminal(
575+
cx: &mut compositor::Context,
576+
_args: Args,
577+
event: PromptEvent,
578+
) -> anyhow::Result<()> {
579+
if event != PromptEvent::Validate {
580+
return Ok(());
581+
}
582+
583+
cx.editor.terminals.toggle_terminal();
584+
585+
Ok(())
586+
}
587+
574588
fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
575589
if event != PromptEvent::Validate {
576590
return Ok(());
@@ -2912,6 +2926,17 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
29122926
..Signature::DEFAULT
29132927
},
29142928
},
2929+
TypableCommand {
2930+
name: "term",
2931+
aliases: &[],
2932+
doc: "Toggle the integrated terminal.",
2933+
fun: toggle_terminal,
2934+
completer: CommandCompleter::none(),
2935+
signature: Signature {
2936+
positionals: (0, Some(0)),
2937+
..Signature::DEFAULT
2938+
},
2939+
},
29152940
TypableCommand {
29162941
name: "format",
29172942
aliases: &["fmt"],

helix-term/src/commands/vte.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use super::Context;
2+
3+
pub fn toggle_terminal(cx: &mut Context) {
4+
cx.editor.terminals.toggle_terminal();
5+
}
6+
7+
pub fn close_terminal(cx: &mut Context) {
8+
cx.editor.terminals.toggle_terminal();
9+
}

helix-term/src/ui/editor.rs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub struct EditorView {
4141
pseudo_pending: Vec<KeyEvent>,
4242
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
4343
pub(crate) completion: Option<Completion>,
44+
pub(crate) terminal: super::terminal::Terminal,
4445
spinners: ProgressSpinners,
4546
/// Tracks if the terminal window is focused by reaction to terminal focus events
4647
terminal_focused: bool,
@@ -65,6 +66,7 @@ impl EditorView {
6566
pseudo_pending: Vec::new(),
6667
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
6768
completion: None,
69+
terminal: super::terminal::Terminal::new(),
6870
spinners: ProgressSpinners::default(),
6971
terminal_focused: true,
7072
}
@@ -492,7 +494,7 @@ impl EditorView {
492494
let cursor_scope = match mode {
493495
Mode::Insert => theme.find_highlight_exact("ui.cursor.insert"),
494496
Mode::Select => theme.find_highlight_exact("ui.cursor.select"),
495-
Mode::Normal => theme.find_highlight_exact("ui.cursor.normal"),
497+
_ => theme.find_highlight_exact("ui.cursor.normal"),
496498
}
497499
.unwrap_or(base_cursor_scope);
498500

@@ -1360,6 +1362,10 @@ impl Component for EditorView {
13601362
event: &Event,
13611363
context: &mut crate::compositor::Context,
13621364
) -> EventResult {
1365+
if let Some(result) = self.terminal.handle_event(event, context) {
1366+
return result;
1367+
}
1368+
13631369
let mut cx = commands::Context {
13641370
editor: context.editor,
13651371
count: None,
@@ -1541,9 +1547,21 @@ impl Component for EditorView {
15411547
editor_area = editor_area.clip_top(1);
15421548
}
15431549

1550+
let original_height = editor_area.height;
1551+
if cx.editor.terminals.visible {
1552+
editor_area = editor_area.clip_bottom(editor_area.height / 2);
1553+
}
1554+
15441555
// if the terminal size suddenly changed, we need to trigger a resize
15451556
cx.editor.resize(editor_area);
15461557

1558+
if cx.editor.terminals.visible {
1559+
let mut term_area = editor_area;
1560+
term_area.height = original_height - editor_area.height;
1561+
term_area.y += editor_area.height;
1562+
self.terminal.render(term_area, surface, cx);
1563+
}
1564+
15471565
if use_bufferline {
15481566
Self::render_bufferline(cx.editor, area.with_height(1), surface);
15491567
}
@@ -1624,18 +1642,29 @@ impl Component for EditorView {
16241642
}
16251643
}
16261644

1627-
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1628-
match editor.cursor() {
1629-
// all block cursors are drawn manually
1630-
(pos, CursorKind::Block) => {
1631-
if self.terminal_focused {
1632-
(pos, CursorKind::Hidden)
1633-
} else {
1634-
// use terminal cursor when terminal loses focus
1635-
(pos, CursorKind::Underline)
1645+
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1646+
if editor.terminals.visible {
1647+
let mut a = area;
1648+
a.y += a.height / 2;
1649+
1650+
if let Some(v) = self.terminal.get_cursor(a, editor) {
1651+
v
1652+
} else {
1653+
(None, CursorKind::Hidden)
1654+
}
1655+
} else {
1656+
match editor.cursor() {
1657+
// all block cursors are drawn manually
1658+
(pos, CursorKind::Block) => {
1659+
if self.terminal_focused {
1660+
(pos, CursorKind::Hidden)
1661+
} else {
1662+
// use terminal cursor when terminal loses focus
1663+
(pos, CursorKind::Underline)
1664+
}
16361665
}
1666+
cursor => cursor,
16371667
}
1638-
cursor => cursor,
16391668
}
16401669
}
16411670
}

helix-term/src/ui/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod popup;
1111
pub mod prompt;
1212
mod spinner;
1313
mod statusline;
14+
mod terminal;
1415
mod text;
1516
mod text_decorations;
1617

helix-term/src/ui/terminal.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use std::cell::Cell;
2+
3+
use helix_core::Position;
4+
use helix_view::{
5+
graphics::{CursorKind, Rect},
6+
input,
7+
theme::Modifier,
8+
Editor,
9+
};
10+
use tui::buffer::Buffer;
11+
12+
use crate::compositor::{Context, EventResult};
13+
14+
pub struct Terminal {
15+
size: Cell<(u16, u16)>,
16+
prev_pressed: Cell<bool>,
17+
}
18+
19+
impl Terminal {
20+
pub fn new() -> Self {
21+
Self {
22+
size: Cell::new((24, 80)),
23+
prev_pressed: Cell::new(false),
24+
}
25+
}
26+
27+
pub fn render(&self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
28+
if self.size.get() != (area.height, area.width) && area.height > 0 && area.width > 0 {
29+
self.size.set((area.height, area.width));
30+
31+
cx.editor
32+
.terminals
33+
.handle_input_event(&input::Event::Resize(self.size.get().1, self.size.get().0));
34+
}
35+
36+
if let Some((_, term)) = cx.editor.terminals.get_active() {
37+
let content = term.renderable_content();
38+
39+
surface.clear(area);
40+
41+
for cell in content.display_iter {
42+
if let Some(c) = surface.get_mut(
43+
area.left() + cell.point.column.0 as u16,
44+
area.top() + cell.point.line.0 as u16,
45+
) {
46+
let style = helix_view::theme::Style::reset()
47+
.bg(helix_view::terminal::color_from_ansi(cell.bg))
48+
.fg(helix_view::terminal::color_from_ansi(cell.fg))
49+
.add_modifier(Modifier::from_bits(cell.flags.bits()).unwrap());
50+
51+
let style = if let Some(col) = cell.underline_color() {
52+
style.underline_color(helix_view::terminal::color_from_ansi(col))
53+
} else {
54+
style
55+
};
56+
57+
c.reset();
58+
c.set_char(cell.c);
59+
c.set_style(style);
60+
}
61+
}
62+
}
63+
}
64+
65+
pub(crate) fn get_cursor(
66+
&self,
67+
area: Rect,
68+
editor: &Editor,
69+
) -> Option<(Option<Position>, CursorKind)> {
70+
editor.terminals.get_active().map(|(_, term)| {
71+
let pt = term.grid().cursor.point;
72+
(
73+
Some(Position {
74+
row: area.y as usize + pt.line.0 as usize,
75+
col: area.x as usize + pt.column.0 as usize,
76+
}),
77+
helix_view::terminal::cursor_kind_from_ansi(term.cursor_style().shape),
78+
)
79+
})
80+
}
81+
82+
pub(crate) fn handle_event(
83+
&self,
84+
event: &input::Event,
85+
context: &mut Context,
86+
) -> Option<EventResult> {
87+
if context.editor.terminals.visible {
88+
match event {
89+
input::Event::Key(input::KeyEvent {
90+
code: input::KeyCode::Char('\\'),
91+
..
92+
}) => {
93+
if self.prev_pressed.get() {
94+
context.editor.terminals.visible = false;
95+
Some(EventResult::Consumed(None))
96+
} else {
97+
self.prev_pressed.set(true);
98+
99+
if context.editor.terminals.handle_input_event(event) {
100+
Some(EventResult::Consumed(None))
101+
} else {
102+
Some(EventResult::Ignored(None))
103+
}
104+
}
105+
}
106+
107+
input::Event::Resize(_, _) => Some(EventResult::Ignored(None)),
108+
109+
event => {
110+
if let input::Event::Key(..) = &event {
111+
self.prev_pressed.set(false);
112+
}
113+
114+
if context.editor.terminals.handle_input_event(event) {
115+
Some(EventResult::Consumed(None))
116+
} else {
117+
Some(EventResult::Ignored(None))
118+
}
119+
}
120+
}
121+
} else {
122+
None
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)