Skip to content

Commit ed67c91

Browse files
committed
implement legacy console API
1 parent 25f558e commit ed67c91

File tree

10 files changed

+863
-120
lines changed

10 files changed

+863
-120
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ on:
33
pull_request:
44
push:
55
env:
6-
MSRV: "1.70"
6+
MSRV: "1.71"
77
jobs:
88
check:
99
name: Check

Cargo.lock

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

Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ readme = "README.md"
77
repository = "https://github.com/helix-editor/termina"
88
edition = "2021"
99
license = "MIT OR MPL-2.0"
10-
rust-version = "1.70"
10+
rust-version = "1.71"
1111

1212
[features]
1313
default = []
1414
event-stream = ["dep:futures-core"]
15+
windows-legacy = [
16+
"windows-sys/Win32_UI_Input_KeyboardAndMouse",
17+
"windows-sys/Win32_UI_WindowsAndMessaging",
18+
]
1519

1620
[dependencies]
1721
parking_lot = "0.12"
@@ -33,7 +37,7 @@ features = [
3337

3438
[target.'cfg(windows)'.dependencies.windows-sys]
3539
# TODO: this could probably be loosened.
36-
version = ">=0.59"
40+
version = ">=0.60"
3741
default-features = false
3842
# https://microsoft.github.io/windows-rs/features/#/0.59.0/search
3943
features = [
@@ -46,3 +50,7 @@ features = [
4650
"Win32_System_Threading",
4751
"Win32_Security",
4852
]
53+
54+
[[example]]
55+
name = "event-read"
56+
required-features = ["windows-legacy"]

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,28 @@
55

66
A cross-platform "virtual terminal" (VT) manipulation library.
77

8-
Termina only "speaks text/VT" but aims to work on Windows as well as *NIX. This is made possible by Microsoft's investment into [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/). This means that Termina requires 64-bit Windows 10.0.17763 (released around Fall 2018) or later ([same as WezTerm](https://wezterm.org/install/windows.html)).
8+
Termina works on Windows as well as *NIX, but Windows requires some extra thought to choose the preferred way of handling input.
99

1010
Termina is a cross between [Crossterm](https://github.com/crossterm-rs/crossterm) and [TermWiz](https://github.com/wezterm/wezterm/blob/a87358516004a652ad840bc1661bdf65ffc89b43/termwiz/README.md) with a lower level API which exposes escape codes to consuming applications. The aim is to scale well in the long run as terminals introduce VT extensions like the [Kitty Keyboard Protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) or [Contour's Dark/Light mode detection](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) - requiring minimal changes in Termina and also allowing flexibility in how applications detect and handle these extensions. See `examples/event-read.rs` for a look at a basic API.
1111

12+
## Input handling on Windows
13+
14+
Termina is able to "speak text/VT" on Windows. This is made possible by Microsoft's investment into [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/). This means that this input mode requires 64-bit Windows 10.0.17763 (released around Fall 2018) or later ([same as WezTerm](https://wezterm.org/install/windows.html)). However, using VT mode on Windows
15+
means the terminal you use needs to support the Kitty Keyboard Protocol in order to handle some complex key combinations.
16+
Windows Terminal, notably, does not implement this protocol. However, using the VT protocol allows for some newer features to work, such as bracketed paste.
17+
This means there is no single answer for which protocol to use - it depends on your use case.
18+
19+
See feature comparison below.
20+
21+
| | VTE Mode | Legacy Console Mode |
22+
| ------------------------------------------------------------ | --------------------------------------------------------- | ------------------- |
23+
| **Kitty Keyboard Protocol** |||
24+
| **Extended key events** | terminal-dependent (must support Kitty Keyboard Protocol) ||
25+
| **Bracketed paste** |||
26+
| **[AltGr](https://en.wikipedia.org/wiki/AltGr_key) Support** |||
27+
28+
The legacy input reader can be used by enabling the `windows-legacy` feature. See the `event-read` example for usage.
29+
1230
## Credit
1331

1432
Termina contains significant code sourced and/or modified from other projects, especially Crossterm and TermWiz. See "CREDIT" comments in the source for details on what was copied and what modifications were made. Since all copied code is licensed under MIT, Termina is offered under the MIT license as well at your option.

examples/event-read.rs

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
// modifications to adapt to the termina API.
33
// <https://github.com/crossterm-rs/crossterm/blob/36d95b26a26e64b0f8c12edfe11f410a6d56a812/examples/event-read.rs>
44
use std::{
5+
env,
56
io::{self, Write as _},
67
time::Duration,
78
};
89

10+
#[cfg(windows)]
11+
use termina::windows;
912
use termina::{
1013
escape::csi::{self, KittyKeyboardFlags},
1114
event::{KeyCode, KeyEvent},
@@ -36,16 +39,36 @@ macro_rules! decreset {
3639
fn main() -> io::Result<()> {
3740
println!("{HELP}");
3841

42+
let args: Vec<_> = env::args().collect();
43+
let windows_legacy = cfg!(windows) && args.iter().any(|a| a == "--windows-legacy");
44+
45+
#[cfg(windows)]
46+
let mut terminal = PlatformTerminal::with_mode(if windows_legacy {
47+
windows::InputReaderMode::Legacy
48+
} else {
49+
windows::InputReaderMode::Vte
50+
})?;
51+
#[cfg(not(windows))]
3952
let mut terminal = PlatformTerminal::new()?;
53+
4054
terminal.enter_raw_mode()?;
4155

56+
let keyboard_flags = if windows_legacy {
57+
// Enabling the kitty keyboard protocol in a supported terminal
58+
// while using the legacy console API will result in incorrect key codes
59+
// for some key combinations.
60+
"".to_string()
61+
} else {
62+
csi::Csi::Keyboard(csi::Keyboard::PushFlags(
63+
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
64+
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS,
65+
))
66+
.to_string()
67+
};
4268
write!(
4369
terminal,
4470
"{}{}{}{}{}{}{}{}",
45-
csi::Csi::Keyboard(csi::Keyboard::PushFlags(
46-
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
47-
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
48-
)),
71+
keyboard_flags,
4972
decset!(FocusTracking),
5073
decset!(BracketedPaste),
5174
decset!(MouseTracking),
@@ -71,32 +94,43 @@ fn main() -> io::Result<()> {
7194
code: KeyCode::Char('c'),
7295
..
7396
}) => {
74-
write!(
75-
terminal,
76-
"{}",
77-
csi::Csi::Cursor(csi::Cursor::RequestActivePositionReport),
78-
)?;
79-
terminal.flush()?;
80-
let filter = |event: &Event| {
81-
matches!(
82-
event,
83-
Event::Csi(csi::Csi::Cursor(csi::Cursor::ActivePositionReport { .. }))
84-
)
85-
};
86-
if terminal.poll(filter, Some(Duration::from_millis(50)))? {
87-
let Event::Csi(csi::Csi::Cursor(csi::Cursor::ActivePositionReport {
88-
line,
89-
col,
90-
})) = terminal.read(filter)?
91-
else {
92-
unreachable!()
93-
};
94-
println!(
95-
"Cursor position: {:?}\r",
96-
(line.get_zero_based(), col.get_zero_based())
97-
);
97+
if windows_legacy {
98+
#[cfg(windows)]
99+
{
100+
let (line, col) = termina::windows::cursor_position()?;
101+
println!(
102+
"Cursor position: {:?}\r",
103+
(line.get_zero_based(), col.get_zero_based())
104+
);
105+
}
98106
} else {
99-
eprintln!("Failed to read the cursor position within 50msec\r");
107+
write!(
108+
terminal,
109+
"{}",
110+
csi::Csi::Cursor(csi::Cursor::RequestActivePositionReport),
111+
)?;
112+
terminal.flush()?;
113+
let filter = |event: &Event| {
114+
matches!(
115+
event,
116+
Event::Csi(csi::Csi::Cursor(csi::Cursor::ActivePositionReport { .. }))
117+
)
118+
};
119+
if terminal.poll(filter, Some(Duration::from_millis(50)))? {
120+
let Event::Csi(csi::Csi::Cursor(csi::Cursor::ActivePositionReport {
121+
line,
122+
col,
123+
})) = terminal.read(filter)?
124+
else {
125+
unreachable!()
126+
};
127+
println!(
128+
"Cursor position: {:?}\r",
129+
(line.get_zero_based(), col.get_zero_based())
130+
);
131+
} else {
132+
eprintln!("Failed to read the cursor position within 50msec\r");
133+
}
100134
}
101135
}
102136
Event::WindowResized(dimensions) => {
@@ -108,10 +142,15 @@ fn main() -> io::Result<()> {
108142
}
109143
}
110144

145+
let keyboard_flags = if windows_legacy {
146+
"".to_string()
147+
} else {
148+
csi::Csi::Keyboard(csi::Keyboard::PopFlags(1)).to_string()
149+
};
111150
write!(
112151
terminal,
113152
"{}{}{}{}{}{}{}{}",
114-
csi::Csi::Keyboard(csi::Keyboard::PopFlags(1)),
153+
keyboard_flags,
115154
decreset!(FocusTracking),
116155
decreset!(BracketedPaste),
117156
decreset!(MouseTracking),

src/event/source/windows.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{io, os::windows::prelude::*, ptr, sync::Arc, time::Duration};
77

88
use windows_sys::Win32::System::Threading;
99

10-
use crate::{event::Event, parse::Parser, terminal::InputHandle};
10+
use crate::{event::Event, parse::Parser, terminal::InputHandle, windows::InputReaderMode};
1111

1212
use super::{EventSource, PollTimeout};
1313

@@ -19,10 +19,10 @@ pub struct WindowsEventSource {
1919
}
2020

2121
impl WindowsEventSource {
22-
pub(crate) fn new(input: InputHandle) -> io::Result<Self> {
22+
pub(crate) fn new(input: InputHandle, mode: InputReaderMode) -> io::Result<Self> {
2323
Ok(Self {
2424
input,
25-
parser: Parser::default(),
25+
parser: Parser::with_mode(mode),
2626
waker: Arc::new(EventHandle::new()?),
2727
})
2828
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ mod terminal;
88
use std::{fmt, num::NonZeroU16};
99

1010
pub use event::{reader::EventReader, Event};
11+
#[cfg(windows)]
12+
pub use parse::windows;
1113
pub use parse::Parser;
14+
1215
pub use terminal::{PlatformHandle, PlatformTerminal, Terminal};
1316

1417
#[cfg(feature = "event-stream")]

0 commit comments

Comments
 (0)