This commit is contained in:
+267
-86
@@ -6,12 +6,17 @@ use gpui::{
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use lumbridge_runtime::{
|
||||
CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions, RuntimeEvent,
|
||||
CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions,
|
||||
RuntimeCommand, RuntimeEvent, TerminalSize,
|
||||
};
|
||||
use lumbridge_spike_model::{
|
||||
ActionOutcome, FOOTER_RIGHT, FocusDirection, OutputSource, PaneId, PaneState, ShellAction,
|
||||
ShellModel, SurfaceKind, WORKSPACES,
|
||||
};
|
||||
use lumbridge_terminal::{
|
||||
KeyModifiers, TerminalDimensions, TerminalEngine, TerminalEngineOptions, TerminalKey,
|
||||
TerminalKeyEvent,
|
||||
};
|
||||
|
||||
const BG: u32 = 0x090c12;
|
||||
const PANEL: u32 = 0x101620;
|
||||
@@ -28,7 +33,9 @@ const TIMING_SAMPLE_LIMIT: usize = 256;
|
||||
const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
|
||||
const RUNTIME_DRAIN_LIMIT: usize = 64;
|
||||
const LIVE_PANE: PaneId = PaneId::CodexRuntime;
|
||||
const LIVE_PTY_SCRIPT: &str = "i=0; printf 'Lumbridge runtime actor owns this PTY\\n'; while :; do printf '[pty %06d] /bin/sh · actor output\\n' \"$i\"; i=$((i+1)); sleep 1; done";
|
||||
const LIVE_PTY_SCRIPT: &str = "printf 'Lumbridge interactive PTY · type here\\n'; exec /bin/sh -i";
|
||||
const TERMINAL_ROW_STEP: u16 = 2;
|
||||
const TERMINAL_COLUMN_STEP: u16 = 10;
|
||||
|
||||
actions!(
|
||||
lumbridge,
|
||||
@@ -45,6 +52,10 @@ actions!(
|
||||
SelectPane4,
|
||||
SelectPane5,
|
||||
SelectPane6,
|
||||
TerminalTaller,
|
||||
TerminalShorter,
|
||||
TerminalWider,
|
||||
TerminalNarrower,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -53,7 +64,7 @@ struct LumbridgeShell {
|
||||
timing: RenderTiming,
|
||||
runtime: Option<RuntimeActor>,
|
||||
runtime_status: LiveRuntimeStatus,
|
||||
runtime_lines: PtyLineFramer,
|
||||
terminal: TerminalEngine,
|
||||
last_runtime_sequence: u64,
|
||||
root_focus: FocusHandle,
|
||||
pane_focus: [FocusHandle; 6],
|
||||
@@ -106,38 +117,6 @@ impl LiveRuntimeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PtyLineFramer {
|
||||
pending: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PtyLineFramer {
|
||||
fn push(&mut self, bytes: &[u8]) -> Vec<String> {
|
||||
self.pending.extend_from_slice(bytes);
|
||||
let mut lines = Vec::new();
|
||||
while let Some(newline) = self.pending.iter().position(|byte| *byte == b'\n') {
|
||||
let mut line = self.pending.drain(..=newline).collect::<Vec<_>>();
|
||||
line.pop();
|
||||
if line.last() == Some(&b'\r') {
|
||||
line.pop();
|
||||
}
|
||||
lines.push(String::from_utf8_lossy(&line).into_owned());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Vec<String> {
|
||||
if self.pending.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut line = std::mem::take(&mut self.pending);
|
||||
if line.last() == Some(&b'\r') {
|
||||
line.pop();
|
||||
}
|
||||
vec![String::from_utf8_lossy(&line).into_owned()]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RenderTiming {
|
||||
pending_dispatch: Option<Instant>,
|
||||
@@ -228,7 +207,8 @@ impl LumbridgeShell {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let (runtime, runtime_status) = match start_live_runtime() {
|
||||
let terminal = TerminalEngine::new(TerminalEngineOptions::default());
|
||||
let (runtime, runtime_status) = match start_live_runtime(terminal.dimensions()) {
|
||||
Ok(runtime) => (Some(runtime), LiveRuntimeStatus::Starting),
|
||||
Err(error) => (None, LiveRuntimeStatus::Fault(error.to_string())),
|
||||
};
|
||||
@@ -239,7 +219,7 @@ impl LumbridgeShell {
|
||||
timing: RenderTiming::default(),
|
||||
runtime,
|
||||
runtime_status,
|
||||
runtime_lines: PtyLineFramer::default(),
|
||||
terminal,
|
||||
last_runtime_sequence: 0,
|
||||
root_focus,
|
||||
pane_focus,
|
||||
@@ -296,24 +276,20 @@ impl LumbridgeShell {
|
||||
break;
|
||||
}
|
||||
self.last_runtime_sequence = sequence;
|
||||
let lines = self.runtime_lines.push(&bytes);
|
||||
if !lines.is_empty() {
|
||||
self.dispatch(ShellAction::AppendExternalOutput {
|
||||
pane: LIVE_PANE,
|
||||
lines,
|
||||
});
|
||||
changed = true;
|
||||
let update = self.terminal.process(&bytes);
|
||||
for response in update.outbound {
|
||||
if let Err(error) =
|
||||
self.send_runtime_command(RuntimeCommand::Input(response))
|
||||
{
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.publish_terminal_snapshot();
|
||||
changed = true;
|
||||
}
|
||||
RuntimeEvent::InputClosed { .. } => {}
|
||||
RuntimeEvent::Exited { status, .. } => {
|
||||
let lines = self.runtime_lines.finish();
|
||||
if !lines.is_empty() {
|
||||
self.dispatch(ShellAction::AppendExternalOutput {
|
||||
pane: LIVE_PANE,
|
||||
lines,
|
||||
});
|
||||
}
|
||||
self.runtime_status =
|
||||
LiveRuntimeStatus::Exited(format!("PTY exited with code {}", status.code));
|
||||
changed = true;
|
||||
@@ -332,6 +308,36 @@ impl LumbridgeShell {
|
||||
changed
|
||||
}
|
||||
|
||||
fn send_runtime_command(&self, command: RuntimeCommand) -> Result<(), RuntimeActorError> {
|
||||
self.runtime
|
||||
.as_ref()
|
||||
.ok_or(RuntimeActorError::Disconnected)?
|
||||
.try_send(command)
|
||||
}
|
||||
|
||||
fn publish_terminal_snapshot(&mut self) {
|
||||
let lines = self.terminal.snapshot().plain_rows();
|
||||
self.dispatch(ShellAction::ReplaceExternalOutput {
|
||||
pane: LIVE_PANE,
|
||||
lines,
|
||||
});
|
||||
}
|
||||
|
||||
fn resize_terminal(&mut self, rows: u16, columns: u16, cx: &mut Context<Self>) {
|
||||
let dimensions = TerminalDimensions::new(rows, columns)
|
||||
.expect("resize actions always retain non-zero dimensions");
|
||||
if !self.terminal.resize(dimensions) {
|
||||
return;
|
||||
}
|
||||
let pty_size = TerminalSize::new(rows, columns)
|
||||
.expect("terminal engine dimensions are valid PTY dimensions");
|
||||
if let Err(error) = self.send_runtime_command(RuntimeCommand::Resize(pty_size)) {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
}
|
||||
self.publish_terminal_snapshot();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.dispatch(ShellAction::SelectPane(pane));
|
||||
window.focus(&self.pane_focus[pane.index()]);
|
||||
@@ -375,8 +381,66 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn terminal_taller(&mut self, _: &TerminalTaller, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
dimensions.rows().saturating_add(TERMINAL_ROW_STEP),
|
||||
dimensions.columns(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn terminal_shorter(&mut self, _: &TerminalShorter, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
dimensions.rows().saturating_sub(TERMINAL_ROW_STEP).max(2),
|
||||
dimensions.columns(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn terminal_wider(&mut self, _: &TerminalWider, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
dimensions.rows(),
|
||||
dimensions.columns().saturating_add(TERMINAL_COLUMN_STEP),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn terminal_narrower(&mut self, _: &TerminalNarrower, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
dimensions.rows(),
|
||||
dimensions
|
||||
.columns()
|
||||
.saturating_sub(TERMINAL_COLUMN_STEP)
|
||||
.max(20),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.model.command_palette().is_open() {
|
||||
if self.model.selected_pane() != LIVE_PANE || self.runtime_status.is_terminal() {
|
||||
return;
|
||||
}
|
||||
let modifiers = key_modifiers(event);
|
||||
let Some(event) = terminal_key_from_parts(
|
||||
event.keystroke.key.as_str(),
|
||||
event.keystroke.key_char.as_deref(),
|
||||
modifiers,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let bytes = self.terminal.encode_key(&event);
|
||||
if bytes.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
}
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,6 +454,11 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
"escape" => {
|
||||
self.dispatch(ShellAction::CloseCommandPalette);
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
if let Some(character) = event.keystroke.key_char.as_deref()
|
||||
&& !event.keystroke.modifiers.control
|
||||
@@ -409,6 +478,7 @@ impl LumbridgeShell {
|
||||
pane: &PaneState,
|
||||
selected: bool,
|
||||
runtime_status: LiveRuntimeStatus,
|
||||
terminal_dimensions: TerminalDimensions,
|
||||
focus: FocusHandle,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::AnyElement {
|
||||
@@ -478,7 +548,7 @@ impl LumbridgeShell {
|
||||
.child(format!("{}", id.index() + 1)),
|
||||
)
|
||||
.child(if external {
|
||||
"Shell · runtime actor"
|
||||
"Shell · VT session"
|
||||
} else {
|
||||
pane.fixture().title
|
||||
}),
|
||||
@@ -495,16 +565,45 @@ impl LumbridgeShell {
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(if external {
|
||||
runtime_status.detail()
|
||||
format!(
|
||||
"{} · {}×{} · Alt+Shift+arrows resize",
|
||||
runtime_status.detail(),
|
||||
terminal_dimensions.columns(),
|
||||
terminal_dimensions.rows()
|
||||
)
|
||||
} else {
|
||||
pane.fixture().target.to_owned()
|
||||
})
|
||||
.when(selected, |view| view.child("FOCUSED")),
|
||||
)
|
||||
.when(external && selected, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.h(px(26.0))
|
||||
.flex_none()
|
||||
.px_3()
|
||||
.border_y_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.text_xs()
|
||||
.child(div().text_color(rgb(ACCENT)).child("TERMINAL"))
|
||||
.child(div().text_color(rgb(MUTED)).child("BROWSER"))
|
||||
.child(div().text_color(rgb(MUTED)).child("TOOLS"))
|
||||
.child(div().text_color(rgb(MUTED)).child("CONTEXT"))
|
||||
.child(div().text_color(rgb(MUTED)).child("GOAL"))
|
||||
.child(div().text_color(rgb(MUTED)).child("REVIEW")),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.gap_1()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
@@ -512,6 +611,36 @@ impl LumbridgeShell {
|
||||
.text_color(rgb(TEXT))
|
||||
.children(pane.lines()[line_start..].iter().cloned()),
|
||||
)
|
||||
.when(external && selected, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.h(px(38.0))
|
||||
.flex_none()
|
||||
.px_3()
|
||||
.border_t_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.text_xs()
|
||||
.child(
|
||||
div()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("LOCAL ANALYST · OFF · suggestions only"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.text_color(rgb(TEXT))
|
||||
.child("Continue")
|
||||
.child("Review plan")
|
||||
.child("Ask…"),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -767,7 +896,7 @@ impl Render for LumbridgeShell {
|
||||
.px_3()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("1 live PTY · 5 deterministic · 1 waiting"),
|
||||
.child("1 interactive VT · 5 deterministic · 1 waiting"),
|
||||
);
|
||||
|
||||
let pane_cards = self
|
||||
@@ -779,6 +908,7 @@ impl Render for LumbridgeShell {
|
||||
pane,
|
||||
self.model.selected_pane() == pane.id(),
|
||||
self.runtime_status.clone(),
|
||||
self.terminal.dimensions(),
|
||||
self.pane_focus[pane.id().index()].clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -795,11 +925,11 @@ impl Render for LumbridgeShell {
|
||||
|
||||
let counters = self.model.counters();
|
||||
let footer_left = format!(
|
||||
"rev {} · {} focus · {} external batches · {} PTY lines · {} total lines",
|
||||
"rev {} · VT rev {} · {} focus · {} VT snapshots · {} synthetic lines",
|
||||
self.model.revision(),
|
||||
self.terminal.revision(),
|
||||
counters.focus_moves,
|
||||
counters.external_output_batches,
|
||||
counters.external_lines_appended,
|
||||
counters.external_snapshot_updates,
|
||||
counters.terminal_lines_appended
|
||||
);
|
||||
let timing = self.timing.summary();
|
||||
@@ -815,6 +945,10 @@ impl Render for LumbridgeShell {
|
||||
.on_action(cx.listener(Self::focus_down))
|
||||
.on_action(cx.listener(Self::open_palette))
|
||||
.on_action(cx.listener(Self::close_palette))
|
||||
.on_action(cx.listener(Self::terminal_taller))
|
||||
.on_action(cx.listener(Self::terminal_shorter))
|
||||
.on_action(cx.listener(Self::terminal_wider))
|
||||
.on_action(cx.listener(Self::terminal_narrower))
|
||||
.on_action(cx.listener(|shell, _: &SelectPane1, window, cx| {
|
||||
shell.select_pane(PaneId::CodexRuntime, window, cx);
|
||||
}))
|
||||
@@ -908,13 +1042,62 @@ impl Render for LumbridgeShell {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_live_runtime() -> Result<RuntimeActor, RuntimeActorError> {
|
||||
fn key_modifiers(event: &KeyDownEvent) -> KeyModifiers {
|
||||
let mut modifiers = KeyModifiers::default();
|
||||
if event.keystroke.modifiers.shift {
|
||||
modifiers = modifiers.union(KeyModifiers::SHIFT);
|
||||
}
|
||||
if event.keystroke.modifiers.alt {
|
||||
modifiers = modifiers.union(KeyModifiers::ALT);
|
||||
}
|
||||
if event.keystroke.modifiers.control {
|
||||
modifiers = modifiers.union(KeyModifiers::CONTROL);
|
||||
}
|
||||
if event.keystroke.modifiers.platform {
|
||||
modifiers = modifiers.union(KeyModifiers::PLATFORM);
|
||||
}
|
||||
modifiers
|
||||
}
|
||||
|
||||
fn terminal_key_from_parts(
|
||||
key: &str,
|
||||
key_char: Option<&str>,
|
||||
modifiers: KeyModifiers,
|
||||
) -> Option<TerminalKeyEvent> {
|
||||
let key = match key {
|
||||
"enter" => TerminalKey::Enter,
|
||||
"backspace" => TerminalKey::Backspace,
|
||||
"tab" => TerminalKey::Tab,
|
||||
"escape" => TerminalKey::Escape,
|
||||
"up" => TerminalKey::Up,
|
||||
"down" => TerminalKey::Down,
|
||||
"left" => TerminalKey::Left,
|
||||
"right" => TerminalKey::Right,
|
||||
"home" => TerminalKey::Home,
|
||||
"end" => TerminalKey::End,
|
||||
"insert" => TerminalKey::Insert,
|
||||
"delete" => TerminalKey::Delete,
|
||||
"pageup" => TerminalKey::PageUp,
|
||||
"pagedown" => TerminalKey::PageDown,
|
||||
function if function.starts_with('f') => {
|
||||
let number = function.strip_prefix('f')?.parse::<u8>().ok()?;
|
||||
TerminalKey::Function(number)
|
||||
}
|
||||
_ if !modifiers.contains(KeyModifiers::PLATFORM) => TerminalKey::Text(key_char?.to_owned()),
|
||||
_ => return None,
|
||||
};
|
||||
Some(TerminalKeyEvent { key, modifiers })
|
||||
}
|
||||
|
||||
fn start_live_runtime(dimensions: TerminalDimensions) -> Result<RuntimeActor, RuntimeActorError> {
|
||||
let command = CommandConfig::new("/bin/sh")
|
||||
.map_err(RuntimeActorError::Start)?
|
||||
.args(["-c", LIVE_PTY_SCRIPT]);
|
||||
let pty_size = TerminalSize::new(dimensions.rows(), dimensions.columns())
|
||||
.map_err(RuntimeActorError::Start)?;
|
||||
RuntimeActor::spawn(
|
||||
command,
|
||||
PtyOptions::default(),
|
||||
PtyOptions::new(pty_size),
|
||||
RuntimeActorOptions::default(),
|
||||
)
|
||||
}
|
||||
@@ -922,22 +1105,21 @@ fn start_live_runtime() -> Result<RuntimeActor, RuntimeActorError> {
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("left", FocusLeft, Some("LumbridgeShell")),
|
||||
KeyBinding::new("right", FocusRight, Some("LumbridgeShell")),
|
||||
KeyBinding::new("up", FocusUp, Some("LumbridgeShell")),
|
||||
KeyBinding::new("down", FocusDown, Some("LumbridgeShell")),
|
||||
KeyBinding::new("h", FocusLeft, Some("LumbridgeShell")),
|
||||
KeyBinding::new("l", FocusRight, Some("LumbridgeShell")),
|
||||
KeyBinding::new("k", FocusUp, Some("LumbridgeShell")),
|
||||
KeyBinding::new("j", FocusDown, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-left", FocusLeft, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-right", FocusRight, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-up", FocusUp, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-down", FocusDown, Some("LumbridgeShell")),
|
||||
KeyBinding::new("secondary-k", OpenPalette, Some("LumbridgeShell")),
|
||||
KeyBinding::new("escape", ClosePalette, Some("LumbridgeShell")),
|
||||
KeyBinding::new("1", SelectPane1, Some("LumbridgeShell")),
|
||||
KeyBinding::new("2", SelectPane2, Some("LumbridgeShell")),
|
||||
KeyBinding::new("3", SelectPane3, Some("LumbridgeShell")),
|
||||
KeyBinding::new("4", SelectPane4, Some("LumbridgeShell")),
|
||||
KeyBinding::new("5", SelectPane5, Some("LumbridgeShell")),
|
||||
KeyBinding::new("6", SelectPane6, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-1", SelectPane1, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-2", SelectPane2, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-3", SelectPane3, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-4", SelectPane4, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-5", SelectPane5, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-6", SelectPane6, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-up", TerminalTaller, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-down", TerminalShorter, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-right", TerminalWider, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-left", TerminalNarrower, Some("LumbridgeShell")),
|
||||
]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(1500.0), px(960.0)), cx);
|
||||
@@ -959,22 +1141,21 @@ fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PtyLineFramer;
|
||||
use super::{KeyModifiers, TerminalKey, terminal_key_from_parts};
|
||||
|
||||
#[test]
|
||||
fn frames_split_crlf_and_utf8_chunks_without_losing_bytes() {
|
||||
let mut framer = PtyLineFramer::default();
|
||||
assert!(framer.push(b"first\r").is_empty());
|
||||
assert_eq!(framer.push(b"\nsecond\npart"), ["first", "second"]);
|
||||
assert!(framer.push(&[0xf0, 0x9f]).is_empty());
|
||||
assert!(framer.push(&[0x91, 0xa9]).is_empty());
|
||||
assert_eq!(framer.finish(), ["part👩"]);
|
||||
fn maps_named_and_composed_gpui_keys_to_terminal_input() {
|
||||
let enter = terminal_key_from_parts("enter", None, KeyModifiers::default()).unwrap();
|
||||
assert_eq!(enter.key, TerminalKey::Enter);
|
||||
|
||||
let composed = terminal_key_from_parts("é", Some("é"), KeyModifiers::default()).unwrap();
|
||||
assert_eq!(composed.key, TerminalKey::Text("é".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replaces_invalid_utf8_only_after_a_complete_line() {
|
||||
let mut framer = PtyLineFramer::default();
|
||||
assert!(framer.push(&[0xff]).is_empty());
|
||||
assert_eq!(framer.push(b"\n"), ["�"]);
|
||||
fn reserves_platform_text_shortcuts_for_the_workspace() {
|
||||
assert!(terminal_key_from_parts("c", Some("c"), KeyModifiers::PLATFORM).is_none());
|
||||
let control = terminal_key_from_parts("c", Some("c"), KeyModifiers::CONTROL).unwrap();
|
||||
assert_eq!(control.key, TerminalKey::Text("c".into()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user