@setoelkahfi / svara / commits / 63733a1

feat: added embedded terminal

Signed-off-by: mellobacon <mellodev@outlook.com>

mellobacon committed Jan 31, 2024 at 22:30 UTC 63733a14467a5f0c648f6f940a92123c9fbf5922
10 files changed +449 -29
src-tauri/Cargo.lock
+150 -2
@@ -580,6 +580,12 @@ version = "0.2.0"
580 source = "registry+https://github.com/rust-lang/crates.io-index"
581 checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
582
583 +[[package]]
584 +name = "downcast-rs"
585 +version = "1.2.0"
586 +source = "registry+https://github.com/rust-lang/crates.io-index"
587 +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
588 +
589 [[package]]
590 name = "dtoa"
591 version = "1.0.9"
@@ -611,7 +617,7 @@ dependencies = [
617 "rustc_version",
618 "toml 0.7.6",
619 "vswhom",
614 - "winreg",
620 + "winreg 0.11.0",
621 ]
622
623 [[package]]
@@ -686,10 +692,21 @@ version = "0.3.6"
692 source = "registry+https://github.com/rust-lang/crates.io-index"
693 checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
694 dependencies = [
689 - "memoffset",
695 + "memoffset 0.9.0",
696 "rustc_version",
697 ]
698
699 +[[package]]
700 +name = "filedescriptor"
701 +version = "0.8.2"
702 +source = "registry+https://github.com/rust-lang/crates.io-index"
703 +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
704 +dependencies = [
705 + "libc",
706 + "thiserror",
707 + "winapi",
708 +]
709 +
710 [[package]]
711 name = "filetime"
712 version = "0.2.22"
@@ -1345,6 +1362,15 @@ dependencies = [
1362 "cfg-if",
1363 ]
1364
1365 +[[package]]
1366 +name = "ioctl-rs"
1367 +version = "0.1.6"
1368 +source = "registry+https://github.com/rust-lang/crates.io-index"
1369 +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
1370 +dependencies = [
1371 + "libc",
1372 +]
1373 +
1374 [[package]]
1375 name = "itoa"
1376 version = "0.4.8"
@@ -1564,6 +1590,15 @@ version = "2.5.0"
1590 source = "registry+https://github.com/rust-lang/crates.io-index"
1591 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
1592
1593 +[[package]]
1594 +name = "memoffset"
1595 +version = "0.6.5"
1596 +source = "registry+https://github.com/rust-lang/crates.io-index"
1597 +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
1598 +dependencies = [
1599 + "autocfg",
1600 +]
1601 +
1602 [[package]]
1603 name = "memoffset"
1604 version = "0.9.0"
@@ -1629,6 +1664,20 @@ version = "1.0.4"
1664 source = "registry+https://github.com/rust-lang/crates.io-index"
1665 checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
1666
1667 +[[package]]
1668 +name = "nix"
1669 +version = "0.25.1"
1670 +source = "registry+https://github.com/rust-lang/crates.io-index"
1671 +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
1672 +dependencies = [
1673 + "autocfg",
1674 + "bitflags 1.3.2",
1675 + "cfg-if",
1676 + "libc",
1677 + "memoffset 0.6.5",
1678 + "pin-utils",
1679 +]
1680 +
1681 [[package]]
1682 name = "nodrop"
1683 version = "0.1.14"
@@ -1683,6 +1732,7 @@ dependencies = [
1732 "encoding_rs",
1733 "infer 0.3.7",
1734 "log",
1735 + "portable-pty",
1736 "serde",
1737 "serde_json",
1738 "tauri",
@@ -1691,6 +1741,7 @@ dependencies = [
1741 "tauri-plugin-log",
1742 "tauri-plugin-store",
1743 "time",
1744 + "tokio",
1745 "trash",
1746 "windows 0.32.0",
1747 ]
@@ -2038,6 +2089,27 @@ dependencies = [
2089 "miniz_oxide",
2090 ]
2091
2092 +[[package]]
2093 +name = "portable-pty"
2094 +version = "0.8.1"
2095 +source = "registry+https://github.com/rust-lang/crates.io-index"
2096 +checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be"
2097 +dependencies = [
2098 + "anyhow",
2099 + "bitflags 1.3.2",
2100 + "downcast-rs",
2101 + "filedescriptor",
2102 + "lazy_static",
2103 + "libc",
2104 + "log",
2105 + "nix",
2106 + "serial",
2107 + "shared_library",
2108 + "shell-words",
2109 + "winapi",
2110 + "winreg 0.10.1",
2111 +]
2112 +
2113 [[package]]
2114 name = "ppv-lite86"
2115 version = "0.2.17"
@@ -2477,6 +2549,48 @@ dependencies = [
2549 "syn 2.0.29",
2550 ]
2551
2552 +[[package]]
2553 +name = "serial"
2554 +version = "0.4.0"
2555 +source = "registry+https://github.com/rust-lang/crates.io-index"
2556 +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
2557 +dependencies = [
2558 + "serial-core",
2559 + "serial-unix",
2560 + "serial-windows",
2561 +]
2562 +
2563 +[[package]]
2564 +name = "serial-core"
2565 +version = "0.4.0"
2566 +source = "registry+https://github.com/rust-lang/crates.io-index"
2567 +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
2568 +dependencies = [
2569 + "libc",
2570 +]
2571 +
2572 +[[package]]
2573 +name = "serial-unix"
2574 +version = "0.4.0"
2575 +source = "registry+https://github.com/rust-lang/crates.io-index"
2576 +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
2577 +dependencies = [
2578 + "ioctl-rs",
2579 + "libc",
2580 + "serial-core",
2581 + "termios",
2582 +]
2583 +
2584 +[[package]]
2585 +name = "serial-windows"
2586 +version = "0.4.0"
2587 +source = "registry+https://github.com/rust-lang/crates.io-index"
2588 +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
2589 +dependencies = [
2590 + "libc",
2591 + "serial-core",
2592 +]
2593 +
2594 [[package]]
2595 name = "serialize-to-javascript"
2596 version = "0.1.1"
@@ -2529,6 +2643,22 @@ dependencies = [
2643 "lazy_static",
2644 ]
2645
2646 +[[package]]
2647 +name = "shared_library"
2648 +version = "0.1.9"
2649 +source = "registry+https://github.com/rust-lang/crates.io-index"
2650 +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
2651 +dependencies = [
2652 + "lazy_static",
2653 + "libc",
2654 +]
2655 +
2656 +[[package]]
2657 +name = "shell-words"
2658 +version = "1.1.0"
2659 +source = "registry+https://github.com/rust-lang/crates.io-index"
2660 +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
2661 +
2662 [[package]]
2663 name = "simd-adler32"
2664 version = "0.3.7"
@@ -3004,6 +3134,15 @@ dependencies = [
3134 "utf-8",
3135 ]
3136
3137 +[[package]]
3138 +name = "termios"
3139 +version = "0.2.2"
3140 +source = "registry+https://github.com/rust-lang/crates.io-index"
3141 +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
3142 +dependencies = [
3143 + "libc",
3144 +]
3145 +
3146 [[package]]
3147 name = "thin-slice"
3148 version = "0.1.1"
@@ -3884,6 +4023,15 @@ dependencies = [
4023 "memchr",
4024 ]
4025
4026 +[[package]]
4027 +name = "winreg"
4028 +version = "0.10.1"
4029 +source = "registry+https://github.com/rust-lang/crates.io-index"
4030 +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
4031 +dependencies = [
4032 + "winapi",
4033 +]
4034 +
4035 [[package]]
4036 name = "winreg"
4037 version = "0.11.0"
src-tauri/Cargo.toml
+2
@@ -24,6 +24,8 @@ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", br
24 time = "0.3.28"
25 log = "^0.4"
26 infer = "0.3"
27 +portable-pty = "0.8.1"
28 +tokio = { version = "1", features = ["time"] }
29
30 [dev-dependencies]
31 windows = "0.32.0"
src-tauri/src/main.rs
+53 -5
@@ -11,21 +11,45 @@ const LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::LogDir];
11
12 use std::collections::HashMap;
13 use std::fs::File;
14 -use std::io::Write;
14 +use std::io::{Write, BufReader};
15 use std::path::{Path, PathBuf};
16 use std::process::Command;
17 -use std::{env, fs};
17 +use std::{env, fs, sync::Arc};
18
19 -use tauri::{App, Manager, Wry};
19 +use tauri::{App, Manager, Wry, State, async_runtime::Mutex as AsyncMutex};
20
21 -mod encoding;
21 use encoding::BOM;
22 use log::{error, info, Level};
23 use tauri::plugin::TauriPlugin;
24 use tauri_plugin_log::{LogTarget, RotationStrategy};
25 +use portable_pty::{native_pty_system, PtySize};
26
27 use crate::encoding::convert_to_u16;
28
29 +mod encoding;
30 +mod terminal;
31 +
32 +#[tauri::command]
33 +// create a shell and add to it the $TERM env variable so we can use clear and other commands
34 +async fn async_create_shell(state: State<'_, terminal::AppState>, path: &str) -> Result<(), String> {
35 + terminal::async_create_shell(state, path).await
36 +}
37 +
38 +#[tauri::command]
39 +async fn async_write_to_pty(data: &str, state: State<'_, terminal::AppState>) -> Result<(), ()> {
40 + terminal::async_write_to_pty(data, state).await
41 +}
42 +
43 +#[tauri::command]
44 +async fn async_read_from_pty(state: State<'_, terminal::AppState>) -> Result<Option<String>, ()> {
45 + terminal::async_read_from_pty(state).await
46 +}
47 +
48 +#[tauri::command]
49 +async fn async_resize_pty(rows: u16, cols: u16, state: State<'_, terminal::AppState>) -> Result<(), ()> {
50 + terminal::async_resize_pty(rows, cols, state).await
51 +}
52 +
53 #[tauri::command]
54 fn open_in_explorer(path: &str) {
55 // FOR OTHER OS REFER - https://doc.rust-lang.org/std/env/consts/constant.OS.html
@@ -340,7 +364,27 @@ fn main() {
364 original(info);
365 error!("[FATAL]: {:?}", info.to_string());
366 }));
367 +
368 + let pty_system = native_pty_system();
369 +
370 + let pty_pair = pty_system
371 + .openpty(PtySize {
372 + rows: 24,
373 + cols: 80,
374 + pixel_width: 0,
375 + pixel_height: 0,
376 + })
377 + .unwrap();
378 +
379 + let reader = pty_pair.master.try_clone_reader().unwrap();
380 + let writer = pty_pair.master.take_writer().unwrap();
381 +
382 tauri::Builder::default()
383 + .manage(terminal::AppState {
384 + pty_pair: Arc::new(AsyncMutex::new(pty_pair)),
385 + writer: Arc::new(AsyncMutex::new(writer)),
386 + reader: Arc::new(AsyncMutex::new(BufReader::new(reader))),
387 + })
388 .invoke_handler(tauri::generate_handler![
389 open_in_explorer,
390 delete_file,
@@ -351,7 +395,11 @@ fn main() {
395 write_file,
396 open_in_default,
397 is_supported,
354 - open_terminal
398 + open_terminal,
399 + async_create_shell,
400 + async_read_from_pty,
401 + async_resize_pty,
402 + async_write_to_pty
403 ])
404 .plugin(tauri_plugin_fs_watch::init())
405 .plugin(tauri_plugin_store::Builder::default().build())
src-tauri/src/terminal.rs new
+84
@@ -0,0 +1,84 @@
1 +use std::{
2 + io::{BufRead, BufReader, Read, Write},
3 + process::exit,
4 + sync::Arc,
5 + thread::{self},
6 +};
7 +use tauri::{async_runtime::Mutex as AsyncMutex, State};
8 +use portable_pty::{CommandBuilder, PtyPair, PtySize};
9 +
10 +pub struct AppState {
11 + pub(crate) pty_pair: Arc<AsyncMutex<PtyPair>>,
12 + pub(crate) writer: Arc<AsyncMutex<Box<dyn Write + Send>>>,
13 + pub(crate) reader: Arc<AsyncMutex<BufReader<Box<dyn Read + Send>>>>,
14 +}
15 +
16 +pub async fn async_create_shell(state: State<'_, AppState>, path: &str) -> Result<(), String> {
17 + #[cfg(target_os = "windows")]
18 + let mut cmd = CommandBuilder::new("powershell.exe");
19 +
20 + #[cfg(not(target_os = "windows"))]
21 + let mut cmd = CommandBuilder::new("bash");
22 +
23 + // add the $TERM env variable so we can use clear and other commands
24 +
25 + #[cfg(target_os = "windows")]
26 + cmd.env("TERM", "cygwin");
27 +
28 + #[cfg(not(target_os = "windows"))]
29 + cmd.env("TERM", "xterm-256color");
30 +
31 + cmd.cwd(path);
32 +
33 + let mut child = state
34 + .pty_pair
35 + .lock()
36 + .await
37 + .slave
38 + .spawn_command(cmd)
39 + .map_err(|err| err.to_string())?;
40 +
41 + thread::spawn(move || {
42 + let status = child.wait().unwrap();
43 + exit(status.exit_code() as i32)
44 + });
45 + Ok(())
46 +}
47 +pub async fn async_write_to_pty(data: &str, state: State<'_, AppState>) -> Result<(), ()> {
48 + write!(state.writer.lock().await, "{}", data).map_err(|_| ())
49 +}
50 +pub async fn async_read_from_pty(state: State<'_, AppState>) -> Result<Option<String>, ()> {
51 + let mut reader = state.reader.lock().await;
52 + let data = {
53 + // Read all available text
54 + let data = reader.fill_buf().map_err(|_| ())?;
55 +
56 + // Send the data to the webview if necessary
57 + if data.len() > 0 {
58 + std::str::from_utf8(data)
59 + .map(|v| Some(v.to_string()))
60 + .map_err(|_| ())?
61 + } else {
62 + None
63 + }
64 + };
65 +
66 + if let Some(data) = &data {
67 + reader.consume(data.len());
68 + }
69 +
70 + Ok(data)
71 +}
72 +pub async fn async_resize_pty(rows: u16, cols: u16, state: State<'_, AppState>) -> Result<(), ()> {
73 + state
74 + .pty_pair
75 + .lock()
76 + .await
77 + .master
78 + .resize(PtySize {
79 + rows,
80 + cols,
81 + ..Default::default()
82 + })
83 + .map_err(|_| ())
84 +}
\ No newline at end of file
src/App.svelte
+13 -8
@@ -14,6 +14,8 @@
14 import { error } from "tauri-plugin-log-api";
15 import ToolView from "./lib/ToolView.svelte";
16 import { getExtensions } from "./config/extensionhandler";
17 + import { fitTerminal } from "./lib/Terminal.svelte";
18 + import { getThemeProperty } from "./config/themehandler";
19 let resolution = writable(0);
20
21 let minPanelSize = 10;
@@ -21,7 +23,7 @@
23 let bottomPanelSize = 20;
24
25 onMount(async () => {
24 - getExtensions();
26 + await getExtensions();
27 await loadDefaultSettings();
28 let size = await appWindow.innerSize();
29 resolution.set(size.width);
@@ -114,18 +116,16 @@
116 {/if}
117 <Pane>
118 <div class="__">
117 - <Splitpanes horizontal theme="editor-panes">
118 - <Pane>
119 + <Splitpanes horizontal theme="editor-panes" on:resize={fitTerminal} on:resized={fitTerminal} class="{!$showBottomPanel ? "hidden" : ""}">
120 + <Pane size={$showBottomPanel ? 100 - bottomPanelSize : 100}>
121 <div id="container">
122 <EditorTabList />
123 <div id="tabview" class:hidden={$hidden}></div>
124 </div>
125 </Pane>
124 - {#if $showBottomPanel}
125 - <Pane minSize={5} bind:size={bottomPanelSize} class="view-bottom-pane">
126 - <ToolView content={$editortool.content}></ToolView>
127 - </Pane>
128 - {/if}
126 + <Pane bind:size={bottomPanelSize} maxSize={90} minSize={10} class="view-bottom-pane">
127 + <ToolView content={$editortool.content} name={$editortool.name}></ToolView>
128 + </Pane>
129 </Splitpanes>
130 </div>
131 </Pane>
@@ -142,6 +142,11 @@
142 .__ {
143 height: 100%;
144 overflow: hidden;
145 + :global(.hidden) {
146 + :global(.splitpanes__splitter), :global(.view-bottom-pane) {
147 + display: none;
148 + }
149 + }
150 }
151 #main {
152 display: flex;
src/config/config.ts
+7 -2
@@ -7,6 +7,7 @@ import { Store } from "tauri-plugin-store-api";
7 import { setEditorFontFamily, setEditorFontSize } from "../lib/Editor.svelte";
8 import { watch } from "tauri-plugin-fs-watch-api";
9 import { info } from "tauri-plugin-log-api";
10 +import { setTerminalState } from "../lib/Statusbar.svelte";
11
12 export const systemfonts = writable([]);
13 export const editorfont = writable("");
@@ -82,8 +83,8 @@ export async function loadDefaultSettings() {
83 }
84 )
85
85 - loadTheme(await appSettings.get("nucleus.theme"));
86 - getShortcuts();
86 + await getShortcuts();
87 + await loadTheme(await appSettings.get("nucleus.theme"));
88
89 appSettings.onKeyChange("editor.fontSize", (value: number) => {
90 setEditorFontSize(value);
@@ -94,6 +95,10 @@ export async function loadDefaultSettings() {
95 appSettings.onKeyChange("nucleus.theme", (value: string) => {
96 loadTheme(value);
97 })
98 + appSettings.onKeyChange("nucleus.useExternalTerminal", (value: boolean) => {
99 + setTerminalState(value);
100 + })
101 + setTerminalState(await appSettings.get("nucleus.useExternalTerminal"))
102 info("Settings initialized", {file: "config.ts", line: 97});
103 }
104
src/lib/File.ts
+3
@@ -5,6 +5,7 @@ import { filetree } from "./FileTree.svelte";
5 import { watchImmediate } from "tauri-plugin-fs-watch-api";
6 import { openFileTree } from "./Sidebar.svelte";
7 import { info, trace, warn, error } from "tauri-plugin-log-api";
8 +import { homeDir } from "@tauri-apps/api/path";
9
10 export async function openFile() {
11 let newPath = await dialog.open() as string;
@@ -16,6 +17,7 @@ export async function openFile() {
17 export const workspaceName = writable("Untitled Workspace");
18 export const dirToLoad = writable("");
19 export const dirLoadFail = writable(false);
20 +export const workingDir = writable(await homeDir());
21 export async function openFolder() {
22 dirLoadFail.set(false);
23 let directory = await dialog.open({directory: true}) as string;
@@ -25,6 +27,7 @@ export async function openFolder() {
27 return
28 };
29 dirToLoad.set(directory.split(path.sep).pop());
30 + workingDir.set(directory);
31 // if file path is not in the configured scope already, add it
32 // TODO: should configure this so it doesnt access restricted paths based on user permissions
33 await invoke("attempt_file_access", {app_handle: window, p: directory});
src/lib/Statusbar.svelte
+16 -10
@@ -3,10 +3,10 @@
3 import { line_info, language, encoding } from "./Editor.svelte";
4 import { getVersion } from '@tauri-apps/api/app';
5 import { onMount } from "svelte";
6 - import { Terminal } from "carbon-icons-svelte";
7 - import { homeDir } from '@tauri-apps/api/path';
8 - import { filetree } from "./FileTree.svelte";
6 + import { Terminal as TerminalIcon } from "carbon-icons-svelte";
7 import { invoke } from "@tauri-apps/api";
8 + import Terminal from "./Terminal.svelte";
9 + import { workingDir } from "./File";
10
11 let appVersion = "";
12 onMount(async () => {
@@ -14,20 +14,22 @@
14 })
15
16 const tools = [
17 - {name: "Terminal", content: null, icon: Terminal, action: spawnTerminal}
17 + {name: "Terminal", content: Terminal, icon: TerminalIcon, action: (t) => {spawnTerminal(t)}}
18 ]
19
20 - async function spawnTerminal() {
21 - let workingdir = await homeDir();
22 - if ($filetree.length !== 0) {
23 - workingdir = $filetree[0].path;
20 + async function spawnTerminal(t) {
21 + if ($externalTerminal) {
22 + invoke("open_terminal", {path: $workingDir});
23 + }
24 + else {
25 + toggleBottomPanel(t);
26 }
25 - invoke("open_terminal", {path: workingdir});
27 }
28 </script>
29 <script lang="ts" context="module">
30 import { writable } from "svelte/store";
31 export let showBottomPanel = writable(false);
32 + export let externalTerminal = writable(false);
33 let show = false;
34
35 export let editortool = writable({name: "", content: null});
@@ -39,6 +41,10 @@
41 editortool.set({name: x.name, content: x.content});
42 }
43 }
44 + export function setTerminalState(value) {
45 + let v = value === "true" ? true : false;
46 + externalTerminal.set(v);
47 + }
48 </script>
49
50 <div id="statusbar">
@@ -49,7 +55,7 @@
55 <div class="editor-tools">
56 {#each tools as tool}
57 <!-- svelte-ignore a11y-click-events-have-key-events -->
52 - <span class="tool" title={tool.name} on:click={() => {tool.action()}}>
58 + <span class="tool" title={tool.name} on:click={() => {tool.action(tool)}}>
59 <svelte:component this={tool.icon}></svelte:component>
60 </span>
61 {/each}
src/lib/Terminal.svelte new
+88
@@ -0,0 +1,88 @@
1 +<script lang="ts">
2 + import "xterm/css/xterm.css";
3 + import { onMount } from "svelte";
4 + import {Terminal} from "xterm";
5 + import {FitAddon} from "xterm-addon-fit";
6 + import { invoke } from "@tauri-apps/api";
7 + import { get, writable } from "svelte/store";
8 + import { workingDir } from "./File";
9 + import { getThemeProperty } from "../config/themehandler";
10 +
11 + let terminalElement: HTMLElement;
12 +
13 + function initializeXterm() {
14 + terminalController.loadAddon(termFit);
15 + terminalController.open(terminalElement);
16 +
17 + initShell();
18 + terminalController.onData(writeToPty);
19 + fitTerminal();
20 +
21 + window.requestAnimationFrame(readFromPty);
22 + }
23 +
24 + onMount(async () => {
25 + initializeXterm();
26 + });
27 +</script>
28 +<script lang="ts" context="module">
29 +
30 +export const termTheme = writable();
31 +let terminalController = new Terminal({
32 + fontFamily: "Cascadia Mono",
33 + fontSize: 14
34 +});
35 +
36 +export function updateTermTheme() {
37 + terminalController.options.theme = get(termTheme);
38 +}
39 +
40 +let termFit = new FitAddon();
41 +export function fitTerminal() {
42 + termFit.fit();
43 + void invoke<string>("async_resize_pty", {
44 + rows: terminalController.rows,
45 + cols: terminalController.cols,
46 + });
47 +}
48 +function writeToTerminal(data: string) {
49 + return new Promise<void>((r) => {
50 + terminalController.write(data, () => r());
51 + });
52 +}
53 +
54 +function writeToPty(data: string) {
55 + void invoke("async_write_to_pty", {
56 + data,
57 + });
58 +}
59 +
60 +function initShell() {
61 + updateTermTheme();
62 + invoke("async_create_shell", {path: get(workingDir)}).catch((error) => {
63 + // on linux it seem to to "Operation not permitted (os error 1)" but it still works because echo $SHELL give /bin/bash
64 + console.error("Error creating shell:", error);
65 + });
66 +}
67 +async function readFromPty() {
68 + const data = await invoke<string>("async_read_from_pty");
69 + if (data) {
70 + await writeToTerminal(data);
71 + }
72 + window.requestAnimationFrame(readFromPty);
73 +}
74 +</script>
75 +
76 +<svelte:window on:resize={fitTerminal}></svelte:window>
77 +
78 +<div id="terminal" bind:this={terminalElement} />
79 +
80 +<style>
81 + #terminal {
82 + height: 100%;
83 + width: 100%;
84 + }
85 + :global(.xterm-screen) {
86 + padding: 10px;
87 + }
88 +</style>
\ No newline at end of file
src/lib/ToolView.svelte
+33 -2
@@ -1,6 +1,37 @@
1 <script lang="ts">
2 + import { showBottomPanel } from "./Statusbar.svelte";
3 +
4 export let content = null;
5 + export let name = "Tool";
6 +
7 + function closePanel() {
8 + showBottomPanel.set(false);
9 + }
10 </script>
11 <div id="toolview">
5 - {content}
6 -</div>
\ No newline at end of file
12 + <div class="header">{name} <span class="close" on:click={closePanel}></span></div>
13 + <svelte:component this={content}></svelte:component>
14 +</div>
15 +
16 +<style lang="scss">
17 +.header {
18 + height: 35px;
19 + display: flex;
20 + flex-direction: row;
21 + align-items: center;
22 + justify-content: space-between;
23 + padding: 0 8px;
24 + .close {
25 + font-size: 12px;
26 + padding: 0 7px;
27 + display: flex;
28 + align-items: center;
29 + justify-content: center;
30 + border-radius: 3px;
31 + cursor: pointer;
32 + &::before {
33 + content: "\2715";
34 + }
35 + }
36 +}
37 +</style>
\ No newline at end of file