remove terminal

This commit is contained in:
Nikita Galaiko 2023-09-14 08:16:26 +02:00 committed by GitButler
parent 425ee0ffb1
commit 3c0d4acabe
15 changed files with 7 additions and 739 deletions

204
Cargo.lock generated
View File

@ -838,7 +838,7 @@ dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset 0.9.0",
"memoffset",
"scopeguard",
]
@ -978,12 +978,6 @@ dependencies = [
"syn 2.0.31",
]
[[package]]
name = "data-encoding"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "debugid"
version = "0.8.0"
@ -1351,21 +1345,10 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
"memoffset 0.9.0",
"memoffset",
"rustc_version",
]
[[package]]
name = "filedescriptor"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
dependencies = [
"libc",
"thiserror",
"winapi",
]
[[package]]
name = "filetime"
version = "0.2.22"
@ -1748,7 +1731,6 @@ dependencies = [
"async-trait",
"backoff",
"byteorder",
"bytes",
"chrono",
"console-subscriber",
"diffy",
@ -1758,7 +1740,6 @@ dependencies = [
"md5",
"notify",
"num_cpus",
"portable-pty",
"posthog-rs",
"rand 0.8.5",
"refinery",
@ -1780,12 +1761,10 @@ dependencies = [
"tantivy",
"tauri",
"tauri-build",
"tauri-plugin-websocket",
"tauri-plugin-window-state",
"tempfile",
"thiserror",
"tokio",
"tokio-tungstenite",
"tokio-util",
"tracing",
"tracing-appender",
@ -2328,15 +2307,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "ioctl-rs"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@ -2759,15 +2729,6 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.9.0"
@ -2812,9 +2773,9 @@ dependencies = [
"libc",
"mach2",
"memmap2 0.5.10",
"memoffset 0.9.0",
"memoffset",
"minidump-common",
"nix 0.26.4",
"nix",
"procfs-core",
"scroll",
"tempfile",
@ -2937,20 +2898,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset 0.6.5",
"pin-utils",
]
[[package]]
name = "nix"
version = "0.26.4"
@ -3584,29 +3531,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "portable-pty"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"downcast-rs",
"filedescriptor",
"lazy_static",
"libc",
"log",
"nix 0.25.1",
"serde",
"serde_derive",
"serial",
"shared_library",
"shell-words",
"winapi",
"winreg 0.10.1",
]
[[package]]
name = "posthog-rs"
version = "0.2.2"
@ -4513,48 +4437,6 @@ dependencies = [
"syn 2.0.31",
]
[[package]]
name = "serial"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
dependencies = [
"serial-core",
"serial-unix",
"serial-windows",
]
[[package]]
name = "serial-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
dependencies = [
"libc",
]
[[package]]
name = "serial-unix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
dependencies = [
"ioctl-rs",
"libc",
"serial-core",
"termios",
]
[[package]]
name = "serial-windows"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
dependencies = [
"libc",
"serial-core",
]
[[package]]
name = "serialize-to-javascript"
version = "0.1.1"
@ -4618,16 +4500,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_library"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
dependencies = [
"lazy_static",
"libc",
]
[[package]]
name = "shell-words"
version = "1.1.0"
@ -5247,22 +5119,6 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin-websocket"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5b814f56e6368fdec46c4ddb04a07e0923ff995a"
dependencies = [
"futures-util",
"log",
"rand 0.8.5",
"serde",
"serde_json",
"tauri",
"thiserror",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "tauri-plugin-window-state"
version = "0.1.0"
@ -5381,15 +5237,6 @@ dependencies = [
"utf-8",
]
[[package]]
name = "termios"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
dependencies = [
"libc",
]
[[package]]
name = "thin-slice"
version = "0.1.1"
@ -5542,20 +5389,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2"
dependencies = [
"futures-util",
"log",
"native-tls",
"tokio",
"tokio-native-tls",
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.8"
@ -5761,26 +5594,6 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"native-tls",
"rand 0.8.5",
"sha1",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.16.0"
@ -6460,15 +6273,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.50.0"

View File

@ -17,7 +17,6 @@ tauri-build = { version = "1.4", features = [] }
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4", features = [ "window-maximize", "window-unmaximize", "process-relaunch", "dialog-open", "fs-read-file", "path-all", "protocol-asset", "shell-open", "system-tray", "window-start-dragging"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-websocket = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
notify = { version = "6.0.1" }
serde_json = {version = "1.0", features = [ "std", "arbitrary_precision" ] }
uuid = "1.4.1"
@ -35,9 +34,6 @@ thiserror = "1.0.44"
tantivy = "0.20.2"
similar = { version = "2.2.1", features = ["unicode"] }
tokio = { version = "1.29.1", features = [ "full", "sync", "tracing" ] }
tokio-tungstenite = "0.20.0"
portable-pty = { version = "0.8.0", features = [ "serde_support" ] }
bytes = "1.1.0"
futures = "0.3"
serde-jsonlines = "0.4.0"
sentry-anyhow = "0.31.0"

View File

@ -3,14 +3,13 @@ use std::{collections::HashMap, ops, path, time};
use anyhow::{Context, Result};
use futures::executor::block_on;
use tauri::{AppHandle, Manager};
use tokio::task;
use crate::{
bookmarks, deltas, gb_repository,
git::{self, diff},
keys,
project_repository::{self, activity, conflicts},
projects, pty, reader, search, sessions, users,
projects, reader, search, sessions, users,
virtual_branches::{self, target},
watcher,
};
@ -60,17 +59,6 @@ impl TryFrom<&AppHandle> for App {
}
impl App {
pub fn start_pty_server(&self) -> Result<()> {
let self_ = self.clone();
task::Builder::new().name("pty-server").spawn(async move {
let port = if cfg!(debug_assertions) { 7702 } else { 7703 };
if let Err(e) = pty::start_server(port, self_).await {
tracing::error!("failed to start pty server: {:#}", e);
}
})?;
Ok(())
}
pub fn init_project(&self, project: &projects::Project) -> Result<()> {
block_on(async move {
self.watchers
@ -558,26 +546,6 @@ impl App {
self.searcher.search(query)
}
pub fn record_pty(&self, project_id: &str, typ: pty::Type, bytes: &[u8]) -> Result<()> {
let gb_repository = self.gb_repository(project_id)?;
let pty_writer = pty::Writer::new(&gb_repository)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
let record = pty::Record {
timestamp,
typ,
bytes: bytes.to_vec(),
};
pty_writer.write(&record).context("failed to append pty")?;
Ok(())
}
pub fn delete_all_data(&self) -> Result<()> {
self.searcher
.delete_all_data()

View File

@ -15,7 +15,6 @@ pub mod lock;
pub mod logs;
pub mod project_repository;
pub mod projects;
pub mod pty;
pub mod reader;
pub mod search;
pub mod sessions;

View File

@ -704,16 +704,12 @@ async fn main() {
sentry::configure_scope(|scope| scope.set_user(Some(user.clone().into())))
}
app.start_pty_server()
.context("failed to start pty server")?;
app.init().context("failed to init app")?;
app_handle.manage(app);
Ok(())
})
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.invoke_handler(tauri::generate_handler![
add_project,

View File

@ -1,199 +0,0 @@
use bytes::BytesMut;
use std::{
env,
io::{Read, Write},
};
use anyhow::{Context, Result};
use futures::{SinkExt, StreamExt};
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use tokio::net;
use tokio_tungstenite::{
self,
tungstenite::handshake::server::{Request, Response},
};
use crate::app;
use super::recorder;
const TERM: &str = "xterm-256color";
pub async fn accept_connection(app: app::App, stream: net::TcpStream) -> Result<()> {
let mut project = None;
let copy_uri_callback = |req: &Request, response: Response| {
let path = req.uri().path().to_string();
if let Some(project_id) = path.split('/').last() {
project = match app.get_project(project_id) {
Ok(p) => p,
Err(e) => {
tracing::error!("failed to get project: {}", e);
None
}
};
}
Ok(response)
};
let mut ws_stream = tokio_tungstenite::accept_hdr_async(stream, copy_uri_callback)
.await
.with_context(|| "failed to accept connection".to_string())?;
if project.is_none() {
ws_stream
.close(None)
.await
.with_context(|| "failed to close connection".to_string())?;
return Ok(());
}
let project = project.unwrap();
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
let pty_system = native_pty_system();
let pty_pair = pty_system
.openpty(PtySize {
rows: 24,
cols: 80,
// Not all systems support pixel_width, pixel_height,
// but it is good practice to set it to something
// that matches the size of the selected font. That
// is more complex than can be shown here in this
// brief example though!
pixel_width: 0,
pixel_height: 0,
})
.with_context(|| "failed to open pty".to_string())?;
let mut cmd = if cfg!(target_os = "windows") {
// CommandBuilder::new(r"powershell")
// CommandBuilder::new(r"C:\Program Files\Git\bin\bash.exe")
// CommandBuilder::new(r"ubuntu.exe") // if WSL is active
// on UI the user should have the option to choose
let mut cmd = CommandBuilder::new(r"cmd");
// this is needed only for cmd.exe
// because the prompt does not have an empty space at the end
// the prompt should be sepratared from the command being typed, for command parsing
cmd.env("PROMPT", "$P$G ");
cmd
} else {
let user_default_shell = env::var("SHELL")?;
let mut cmd = CommandBuilder::new(user_default_shell);
cmd.env("TERM", TERM);
cmd.args(["-i"]);
cmd
};
// set to project path
cmd.cwd(project.path.clone());
let mut pty_child_process = pty_pair.slave.spawn_command(cmd)?;
let mut pty_reader = pty_pair.master.try_clone_reader()?;
let mut pty_writer = pty_pair.master.take_writer()?;
let shared_project_id = project.id.clone();
let shared_app = app.clone();
// it's important to spawn a new thread for the pty reader
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let mut buffer = BytesMut::with_capacity(1024);
buffer.resize(1024, 0u8);
loop {
buffer[0] = 0u8;
let tail = &mut buffer[1..];
match pty_reader.read(tail) {
Ok(0) => {
// EOF
tracing::debug!("0 bytes read from pty. EOF.");
if let Err(e) = ws_sender
.send(tokio_tungstenite::tungstenite::Message::Close(None))
.await
{
tracing::error!(
"{}: error sending data to websocket: {:#}",
shared_project_id,
e
);
}
break;
}
Ok(n) => {
let data = &buffer[..n + 1];
if let Err(e) = ws_sender
.send(tokio_tungstenite::tungstenite::Message::Binary(
data.to_vec(),
))
.await
{
tracing::error!(
"{}: error sending data to websocket: {:#}",
shared_project_id,
e
);
}
if let Err(e) =
shared_app.record_pty(&shared_project_id, recorder::Type::Output, data)
{
tracing::error!("{}: error recording data: {:#}", shared_project_id, e);
}
}
Err(e) => {
tracing::error!("Error reading from pty: {:#}", e);
break;
}
}
}
tracing::debug!("PTY child process killed.");
});
});
while let Some(message) = ws_receiver.next().await {
match message {
Ok(tokio_tungstenite::tungstenite::Message::Binary(msg)) => {
let msg_bytes = msg.as_slice();
match (msg_bytes[0], msg_bytes[1..].to_vec()) {
(0, data) => {
if msg_bytes.len().gt(&0) {
pty_writer.write_all(&data)?;
if let Err(e) =
app.record_pty(&project.id, recorder::Type::Input, &data.to_vec())
{
tracing::error!("{}: error recording data: {:#}", &project.id, e);
}
}
}
(1, data) => {
let pty_size: PtySize = serde_json::from_slice(&data)?;
pty_pair.master.resize(pty_size)?;
}
(code, _) => tracing::error!("Unknown command {}", code),
}
}
Ok(tokio_tungstenite::tungstenite::Message::Close(_)) => {
tracing::debug!("Closing the websocket connection...");
tracing::debug!("Killing PTY child process...");
pty_child_process
.kill()
.with_context(|| "failed to kill pty child process".to_string())?;
break;
}
Ok(_) => tracing::error!("Unknown received data type"),
Err(e) => {
tracing::error!("Error receiving data: {}", e);
break;
}
}
}
Ok(())
}

View File

@ -1,8 +0,0 @@
mod connection;
mod recorder;
mod server;
mod writer;
pub use recorder::{Record, Type};
pub use server::start_server;
pub use writer::PtyWriter as Writer;

View File

@ -1,17 +0,0 @@
use serde::Serialize;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Type {
Input,
Output,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Record {
pub timestamp: u128,
#[serde(rename = "type")]
pub typ: Type,
pub bytes: Vec<u8>,
}

View File

@ -1,28 +0,0 @@
use anyhow::{Context, Result};
use tokio::{net, task};
use crate::app;
use super::connection;
pub async fn start_server(port: usize, app: app::App) -> Result<()> {
let pty_ws_address = format!("127.0.0.1:{}", port);
let listener = net::TcpListener::bind(&pty_ws_address)
.await
.with_context(|| format!("failed to bind to {}", pty_ws_address))?;
tracing::info!("pty-ws: listening on {}", pty_ws_address);
while let Ok((stream, _)) = listener.accept().await {
let app_clone = app.clone();
task::Builder::new().name("pty-connection").spawn(async {
if let Err(e) = connection::accept_connection(app_clone, stream).await {
tracing::error!("pty-ws: failed to accept connection {:#}", e);
}
})?;
}
tracing::info!("pty-ws: server stopped");
Ok(())
}

View File

@ -1,34 +0,0 @@
use anyhow::{Context, Result};
use crate::gb_repository;
use super::Record;
pub struct PtyWriter<'writer> {
repository: &'writer gb_repository::Repository,
}
impl<'writer> PtyWriter<'writer> {
pub fn new(repository: &'writer gb_repository::Repository) -> Result<Self> {
repository
.get_or_create_current_session()
.context("failed to create session")?;
Ok(Self { repository })
}
pub fn write(&self, record: &Record) -> Result<()> {
let _lock = self.repository.lock();
serde_jsonlines::append_json_lines(
self.repository.session_path().join("pty.jsonl"),
[record],
)?;
tracing::debug!(
"{}: appended pty record to session",
self.repository.project_id
);
Ok(())
}
}

View File

@ -84,18 +84,10 @@
"svelte-outclick": "^3.6.2",
"svelte-resize-observer": "^2.0.0",
"tailwindcss": "^3.3.3",
"tauri-plugin-log-api": "git+https://git@github.com/tauri-apps/tauri-plugin-log.git",
"tauri-plugin-websocket-api": "git+https://git@github.com/tauri-apps/tauri-plugin-websocket.git",
"tinykeys": "^2.1.0",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vitest": "^0.34.3",
"xterm": "^5.2.1",
"xterm-addon-canvas": "^0.4.0",
"xterm-addon-fit": "^0.7.0",
"xterm-addon-ligatures": "^0.6.0",
"xterm-addon-unicode11": "^0.5.0",
"xterm-addon-webgl": "^0.15.0"
"vitest": "^0.34.3"
}
}

View File

@ -5,7 +5,6 @@ import {
IconGitCommit,
IconFile,
IconFeedback,
IconTerminal,
IconSettings,
IconDiscord,
IconSearch,
@ -147,14 +146,6 @@ const navigateGroup = ({ project, input }: { project?: Project; input: string })
},
icon: IconGitCommit
},
{
title: 'Terminal',
hotkey: 'Meta+T',
action: {
href: `/projects/${project.id}/terminal/`
},
icon: IconTerminal
},
{
title: 'Project settings',
hotkey: 'Meta+Shift+,',

View File

@ -3,7 +3,7 @@
import { Button, Tooltip } from '$lib/components';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { IconRewind, IconSearch, IconSettings, IconTerminal } from '$lib/icons';
import { IconRewind, IconSearch, IconSettings } from '$lib/icons';
export let data: LayoutData;
const { project } = data;
@ -69,15 +69,6 @@
/>
</Tooltip>
</li>
<li>
<Tooltip label="Terminal">
<Button
on:click={() => goto(`/projects/${$project.id}/terminal`)}
kind="plain"
icon={IconTerminal}
/>
</Tooltip>
</li>
<li>
<Tooltip label="Project settings">
<Button

View File

@ -1,54 +0,0 @@
<script lang="ts">
import type { LayoutData } from '../$types';
import ResizeObserver from 'svelte-resize-observer';
import setupTerminal from './terminal';
import 'xterm/css/xterm.css';
import type { Project } from '$lib/api/ipc/projects';
import { debounce } from '$lib/utils';
import { Button, Statuses } from '$lib/components';
export let data: LayoutData;
const { project, statuses } = data;
type Unpromisify<T> = T extends Promise<infer U> ? U : T;
let term: Unpromisify<ReturnType<typeof setupTerminal>> | undefined;
const handleTerminalResize = debounce(() => term?.resize(), 5);
const runCommand = (command: string) => term?.run(command);
$: terminal = (target: HTMLElement, params: { project: Project }) => {
let setupPromise = setupTerminal(params);
setupPromise.then((terminal) => (term = terminal));
setupPromise.then((terminal) => terminal.bind(target));
return {
update: (params: { project: Project }) => {
setupPromise.then((term) => term.destroy());
setupPromise = setupTerminal(params);
setupPromise.then((terminal) => (term = terminal));
setupPromise.then((terminal) => terminal.bind(target));
}
};
};
</script>
<div class="terminal-page flex h-full w-full flex-auto flex-row">
<div class="side-panel p-4">
<h2 class="pb-4 text-lg font-bold text-zinc-300">Git Status</h2>
{#await statuses.load() then}
<Statuses statuses={$statuses} />
{/await}
<div class="mt-4 font-bold">Commands</div>
<ul class="py-2">
<Button color="purple" width="full-width" on:click={() => runCommand('git push')}>
Push Commit
</Button>
</ul>
</div>
<div class="main-content-container h-full">
<div class="flex h-full w-full flex-row">
<div class="h-full w-full" use:terminal={{ project: $project }} />
<ResizeObserver on:resize={handleTerminalResize} />
</div>
</div>
</div>

View File

@ -1,129 +0,0 @@
import type { Project } from '$lib/api/ipc/projects';
import { Terminal } from 'xterm';
import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import { FitAddon } from 'xterm-addon-fit';
import { Unicode11Addon } from 'xterm-addon-unicode11';
import WebSocket, { type Message } from 'tauri-plugin-websocket-api';
import { dev } from '$app/environment';
const isWebgl2Supported = (() => {
let isSupported = window.WebGL2RenderingContext ? undefined : false;
return () => {
if (isSupported === undefined) {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2', { depth: false, antialias: false });
isSupported = gl instanceof window.WebGL2RenderingContext;
}
return isSupported;
};
})();
const uint8ArrayToNumbers = (array: Uint8Array) => {
const numbers = [];
for (let i = 0; i < array.length; i++) {
numbers.push(array[i]);
}
return numbers;
};
const encodeString = (msg: string): Message => ({
type: 'Binary',
data: uint8ArrayToNumbers(new TextEncoder().encode(msg))
});
const userInputMessage = (data: string) => encodeString(`\x00${data}`);
const resizeMessage = (size: {
rows: number;
cols: number;
pixel_width: number;
pixel_height: number;
}) => encodeString(`\x01${JSON.stringify(size)}`);
const newSession = async (params: { project: Project }) => {
const { project } = params;
const port = dev ? 7702 : 7703;
return WebSocket.connect(`ws://localhost:${port}/${project.id}`).then((conn) => {
const sendMessage = (message: Message) => {
conn.send(message).catch((e: any) => {
console.error(`failed to send message to terminal: ${e}`);
});
};
const term = new Terminal({
cursorBlink: false,
cursorStyle: 'block',
fontSize: 13,
rows: 24,
cols: 80,
allowProposedApi: true
});
const fitAddon = new FitAddon();
term.loadAddon(new Unicode11Addon());
term.loadAddon(fitAddon);
if (isWebgl2Supported()) {
term.loadAddon(new WebglAddon());
} else {
term.loadAddon(new CanvasAddon());
}
term.unicode.activeVersion = '11';
const inputListener = term.onData((data: string) => sendMessage(userInputMessage(data)));
conn.addListener((message) => {
if (message.type === 'Binary') {
term.write(new Uint8Array(message.data));
}
});
conn.addListener((message) => {
if (message.type === 'Close') {
term.dispose();
location.reload();
}
});
return {
resize: () => {
fitAddon.fit();
const size = fitAddon.proposeDimensions();
if (!size) return;
sendMessage(
resizeMessage({
cols: size.cols,
rows: size.rows,
pixel_width: 0,
pixel_height: 0
})
);
},
destroy: () => {
term.dispose();
inputListener.dispose();
conn.disconnect();
},
run: (command: string) => {
sendMessage(userInputMessage(`${command}\n`));
},
bind: (target: HTMLElement) => {
term.open(target);
fitAddon.fit();
term.focus();
}
};
});
};
const sessions = new Map<string, ReturnType<typeof newSession>>();
export default (params: { project: Project }) => {
const { project } = params;
const session = sessions.get(project.id);
if (session) return session;
const s = newSession(params);
sessions.set(project.id, s);
return s;
};