Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
use crate::terminal::{
bootstrap::init_shell_script_for_shell, event_listener::ChannelEventListener,
model::ansi::Processor, session_settings::SessionSettings, shell::ShellType,
writeable_pty::Message as EventLoopMessage, SizeInfo, TerminalModel,
};
use async_channel::Receiver;
use futures_util::SinkExt;
use parking_lot::FairMutex;
use serde::Serialize;
use std::io;
use std::sync::Arc;
use warpui::{Entity, ModelContext, SingletonEntity};
use websocket::{Message, Sink, Stream, WebSocket, WebsocketMessage as _};
const CREATE_SESSION_ENDPOINT: &str = "ws://127.0.0.1:3030/create";
/// Contains info needed to resize the SSH terminal session. Is serialized and
/// sent over the websocket as text.
///
/// The field names need to be kept the same as the `WindowSizeChange` struct in
/// https://github.com/warpdotdev/ssh-proxy-server/blob/main/src/ssh/session.rs.
#[derive(Serialize, Debug)]
struct WindowSizeChange {
width: u32,
height: u32,
width_px: u32,
height_px: u32,
}
pub(super) struct EventLoop {
terminal_model: Arc<FairMutex<TerminalModel>>,
parser: Processor,
event_loop_rx: Receiver<EventLoopMessage>,
channel_event_listener: ChannelEventListener,
}
impl EventLoop {
/// Starts the [`EventLoop`] by starting a websocket connection with the server and
/// bootstrapping the PTY.
pub(super) fn start(
model: Arc<FairMutex<TerminalModel>>,
websocket_receiver: Receiver<EventLoopMessage>,
channel_event_listener: ChannelEventListener,
size_info: SizeInfo,
ctx: &mut ModelContext<Self>,
) -> Self {
let event_loop = Self::new(model, websocket_receiver, channel_event_listener);
let url = Self::get_new_session_url(size_info);
let response = WebSocket::connect(url, None /* protocols */);
ctx.spawn(response, Self::on_ws_connection);
event_loop
}
fn new(
terminal_model: Arc<FairMutex<TerminalModel>>,
websocket_receiver: Receiver<EventLoopMessage>,
channel_event_listener: ChannelEventListener,
) -> Self {
Self {
terminal_model,
parser: Processor::default(),
event_loop_rx: websocket_receiver,
channel_event_listener,
}
}
fn get_new_session_url(size_info: SizeInfo) -> String {
let num_rows = size_info.rows;
let num_cols = size_info.columns;
format!("{CREATE_SESSION_ENDPOINT}?num_rows={num_rows}&num_cols={num_cols}")
}
/// Starts tasks to listen to and write to the websocket.
fn start_websocket_listener_and_writer_tasks(
&mut self,
mut sink: impl Sink,
stream: impl Stream,
ctx: &mut ModelContext<Self>,
) {
// TODO(alokedesai): Add a spawn_stream equivalent that runs on the background executor.
ctx.spawn_stream_local(
stream,
|event_loop, message, _| {
let message = match message {
Ok(message) => message,
Err(err) => {
log::error!("Unable to receive item: {err:?}");
return;
}
};
let Some(bytes) = message.binary() else {
log::error!("Received non binary message");
return;
};
event_loop.process_pty_bytes(bytes);
},
|_, _| {},
);
let is_honor_ps1_enabled = *SessionSettings::as_ref(ctx).honor_ps1;
let receiver = self.event_loop_rx.clone();
ctx.background_executor()
.spawn(async move {
if let Err(e) = Self::write_env_vars(&mut sink, is_honor_ps1_enabled).await {
log::error!("Failed to write env vars to pty {e:?}");
}
if let Err(e) = Self::write_zsh_init_shell_script(&mut sink).await {
log::error!("Failed to write zsh bootstrap bytes to pty {e:?}");
}
while let Ok(message) = receiver.recv().await {
match message {
EventLoopMessage::Input(bytes) => {
if let Err(e) = sink.send(Message::new_binary(bytes.to_vec())).await {
log::error!("Failed to send message to network-backed PTY {e:?}");
};
}
EventLoopMessage::Resize(size_info) => {
let size_change = WindowSizeChange {
width: size_info.columns as u32,
height: size_info.rows as u32,
width_px: size_info.pane_width_px().as_f32() as u32,
height_px: size_info.pane_height_px().as_f32() as u32,
};
let Ok(serialized) = serde_json::to_string(&size_change) else {
log::error!("Error serializing window size change info");
continue;
};
// Sending as a `Text` message implies that this is a
// control channel message. The SSH proxy server should
// make this distinction.
if let Err(e) = sink.send(Message::new_text(serialized)).await {
log::error!("Failed to send message to network-backed PTY {e:?}");
};
}
// TODO(alokedesai): Implement shutdown on the network backed PTY.
EventLoopMessage::Shutdown | EventLoopMessage::ChildExited => {}
}
}
})
.detach();
}
/// Writes the ZSH init shell script to the "PTY", mimicking how we send the init shell script
/// when there is a local pty:
/// <https://github.com/warpdotdev/warp-internal/blob/747da2df83f2caa97e781ce284ceb226fb97a66c/app/src/terminal/local_tty/unix.rs#L338-L347>.
async fn write_zsh_init_shell_script(sink: &mut impl Sink) -> anyhow::Result<()> {
let zsh_init_shell_script = init_shell_script_for_shell(ShellType::Zsh, &crate::ASSETS);
sink.send(Message::new_binary(
zsh_init_shell_script.as_bytes().to_vec(),
))
.await?;
sink.send(Message::new_binary(
ShellType::Zsh.execute_command_bytes().to_vec(),
))
.await?;
Ok(())
}
/// Writes environment variables that should be defined in the session
/// before bootstrapping. This is a subset of the environment variables
/// defined in `app/src/terminal/local_tty/unix.rs` that are necessary in
/// order to dogfood Warp on Web over the remote tty.
async fn write_env_vars(
sink: &mut impl Sink,
is_honor_ps1_enabled: bool,
) -> anyhow::Result<()> {
let honor_ps1_env_var = format!(r#"WARP_HONOR_PS1="{}";"#, is_honor_ps1_enabled as u8);
sink.send(Message::new_binary(honor_ps1_env_var.as_bytes().to_vec()))
.await?;
Ok(())
}
fn on_ws_connection(
&mut self,
connection: anyhow::Result<WebSocket>,
ctx: &mut ModelContext<Self>,
) {
let connection = match connection {
Ok(connection) => connection,
Err(e) => {
log::error!("Failed to construct websocket connection: {e:?}");
return;
}
};
ctx.spawn(connection.split(), |me, (sink, stream), ctx| {
me.start_websocket_listener_and_writer_tasks(sink, stream, ctx);
});
}
/// Processes a byte slice through the `Processor`.
fn process_pty_bytes(&mut self, bytes: &[u8]) {
let mut terminal_model = self.terminal_model.lock();
self.parser
.parse_bytes(&mut *terminal_model, bytes, &mut io::sink());
self.channel_event_listener.send_wakeup_event();
}
}
impl Entity for EventLoop {
type Event = ();
}
+4
View File
@@ -0,0 +1,4 @@
mod event_loop;
mod terminal_manager;
pub use terminal_manager::TerminalManager;
@@ -0,0 +1,196 @@
use crate::ai::blocklist::InputConfig;
use crate::context_chips::prompt_type::PromptType;
use crate::pane_group::TerminalViewResources;
use crate::persistence::ModelEvent;
use crate::terminal::event_listener::ChannelEventListener;
use crate::terminal::model::session::Sessions;
use crate::terminal::remote_tty::event_loop::EventLoop;
use crate::terminal::shell::{ShellName, ShellType};
use crate::terminal::writeable_pty::pty_controller::{EventLoopSendError, EventLoopSender};
use crate::terminal::writeable_pty::terminal_manager_util::{
init_pty_controller_model, wire_up_pty_controller_with_view,
};
use crate::terminal::ShellLaunchState;
use std::any::Any;
use crate::terminal::model_events::ModelEventDispatcher;
use crate::terminal::writeable_pty::{self, Message};
use crate::terminal::{terminal_manager, SizeInfo, TerminalModel, TerminalView};
use async_channel::{Receiver, Sender, TrySendError};
use parking_lot::FairMutex;
use pathfinder_geometry::vector::Vector2F;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use warpui::{AppContext, ModelHandle, ViewHandle, WindowId};
type PtyController = writeable_pty::PtyController<Sender<Message>>;
pub struct TerminalManager {
model: Arc<FairMutex<TerminalModel>>,
// Store a reference to the PTYController and EventLoop so the UI framework doesn't end up
// deallocating them because there are no strong references to the models.
_pty_controller: ModelHandle<PtyController>,
_event_loop: ModelHandle<EventLoop>,
view: ViewHandle<TerminalView>,
}
impl TerminalManager {
/// Creates a terminal manager model that feeds bytes to/from a remote PTY.
pub fn create_model(
resources: TerminalViewResources,
initial_size: Vector2F,
model_event_sender: Option<SyncSender<ModelEvent>>,
window_id: WindowId,
initial_input_config: Option<InputConfig>,
ctx: &mut AppContext,
) -> ModelHandle<Box<dyn crate::terminal::TerminalManager>> {
// Create all the necessary channels we need for communication.
let (wakeups_tx, wakeups_rx) = async_channel::unbounded();
let (events_tx, events_rx) = async_channel::unbounded();
let (executor_command_tx, executor_command_rx) = async_channel::unbounded();
// Use an empty pty reads broadcaster since we don't need to broadcast any PTY bytes for the
// network-backed PTY. We use 1 instead of 0 here because `async_broadcast` internally
// asserts that the capacity is at least 1.
let (pty_reads_tx, _pty_reads_rx) = async_broadcast::broadcast(1);
let channel_event_proxy = ChannelEventListener::new(wakeups_tx, events_tx, pty_reads_tx);
// Initialize the sessions model.
let sessions: ModelHandle<Sessions> =
ctx.add_model(|ctx| Sessions::new(executor_command_tx, ctx));
let model_events =
ctx.add_model(|ctx| ModelEventDispatcher::new(events_rx, sessions.clone(), ctx));
// Create the terminal model.
let model = terminal_manager::create_terminal_model(
None, /* startup_directory */
None, /* restored_blocks */
initial_size,
channel_event_proxy.clone(),
// TODO(alokedesai): Add support for other shells within the network-backed pty.
ShellLaunchState::ShellSpawned {
available_shell: None,
display_name: ShellName::blank(),
shell_type: ShellType::Zsh,
},
ctx,
);
let size_info = *model.block_list().size();
let colors = model.colors();
let model = Arc::new(FairMutex::new(model));
let (event_loop_tx, event_loop_rx) = async_channel::unbounded();
let event_loop = Self::create_and_start_event_loop(
model.clone(),
channel_event_proxy.clone(),
event_loop_rx,
size_info,
ctx,
);
// Initialize the PtyController.
let pty_controller = init_pty_controller_model(
event_loop_tx.clone(),
executor_command_rx,
model_events.clone(),
sessions.clone(),
model.clone(),
ctx,
);
let cloned_model = model.clone();
let prompt_type =
ctx.add_model(|ctx| PromptType::new_dynamic_from_sessions(sessions.clone(), ctx));
let view = ctx.add_typed_action_view(window_id, |ctx| {
TerminalView::new(
resources,
wakeups_rx,
model_events.clone(),
cloned_model,
sessions.clone(),
size_info,
colors,
model_event_sender.clone(),
prompt_type,
initial_input_config,
None, // conversation_restoration - not used for remote
None, // inactive_pty_reads_rx
ctx,
)
});
wire_up_pty_controller_with_view(
&pty_controller,
&view,
model.clone(),
sessions,
model_event_sender,
ctx,
);
// Create the terminal manager itself.
let terminal_manager = Self {
model,
view,
_pty_controller: pty_controller,
_event_loop: event_loop,
};
ctx.add_model(|_ctx| {
let manager: Box<dyn crate::terminal::TerminalManager> = Box::new(terminal_manager);
manager
})
}
fn create_and_start_event_loop(
terminal_model: Arc<FairMutex<TerminalModel>>,
channel_event_listener: ChannelEventListener,
message_receiver: Receiver<Message>,
size_info: SizeInfo,
ctx: &mut AppContext,
) -> ModelHandle<EventLoop> {
ctx.add_model(|ctx| {
EventLoop::start(
terminal_model,
message_receiver,
channel_event_listener,
size_info,
ctx,
)
})
}
}
impl super::super::TerminalManager for TerminalManager {
fn model(&self) -> Arc<FairMutex<TerminalModel>> {
self.model.clone()
}
fn view(&self) -> ViewHandle<TerminalView> {
self.view.clone()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl EventLoopSender for Sender<Message> {
fn send(&self, message: Message) -> Result<(), EventLoopSendError> {
self.try_send(message).map_err(|err| match err {
TrySendError::Closed(_) => EventLoopSendError::Disconnected,
TrySendError::Full(_) => EventLoopSendError::Other(err.into()),
})
}
}