Mayank2027 commited on
Commit
e8c82c6
·
verified ·
1 Parent(s): 261effd

Update frontend/src/components/Terminal.tsx

Browse files
Files changed (1) hide show
  1. frontend/src/components/Terminal.tsx +90 -23
frontend/src/components/Terminal.tsx CHANGED
@@ -1,21 +1,29 @@
1
- import React, { useEffect, useRef } from 'react';
2
  import { Terminal as XTerm } from 'xterm';
3
  import { FitAddon } from 'xterm-addon-fit';
4
  import 'xterm/css/xterm.css';
 
5
 
6
- const Terminal: React.FC = () => {
7
- const termRef = useRef<HTMLDivElement>(null);
8
- const xterm = useRef<XTerm>();
9
- const ws = useRef<WebSocket>();
 
10
 
11
- useEffect(() => {
 
 
 
 
 
 
 
12
  const term = new XTerm({
13
  cursorBlink: true,
14
  theme: {
15
  background: '#0d1117',
16
  foreground: '#c9d1d9',
17
  cursor: '#58a6ff',
18
- // 'selection' is not in the typings of the installed xterm version; removed.
19
  black: '#484f58',
20
  red: '#f85149',
21
  green: '#3fb950',
@@ -30,25 +38,84 @@ const Terminal: React.FC = () => {
30
  });
31
  const fit = new FitAddon();
32
  term.loadAddon(fit);
33
- if (termRef.current) {
34
- term.open(termRef.current);
35
- fit.fit();
36
- xterm.current = term;
37
- }
38
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
39
  const socket = new WebSocket(`${protocol}//${window.location.host}/ws/terminal`);
40
- ws.current = socket;
41
- socket.onmessage = (event) => {
42
- term.write(event.data);
43
- };
44
- term.onData(data => {
45
- socket.send(`exec:${data}`);
46
- });
47
- window.addEventListener('resize', () => fit.fit());
48
- return () => { socket.close(); term.dispose(); window.removeEventListener('resize', () => {}); };
 
 
49
  }, []);
50
 
51
- return <div ref={termRef} style={{ height: '100%', width: '100%', background: '#0d1117' }} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  };
53
 
54
- export default Terminal;
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
  import { Terminal as XTerm } from 'xterm';
3
  import { FitAddon } from 'xterm-addon-fit';
4
  import 'xterm/css/xterm.css';
5
+ import { FaPlus, FaTimes } from 'react-icons/fa';
6
 
7
+ interface Tab {
8
+ id: number;
9
+ terminal: XTerm;
10
+ socket: WebSocket;
11
+ }
12
 
13
+ const TerminalTabs: React.FC = () => {
14
+ const [tabs, setTabs] = useState<Tab[]>([]);
15
+ const [activeTab, setActiveTab] = useState(0);
16
+ const termRefs = useRef<Map<number, HTMLDivElement>>(new Map());
17
+ const nextId = useRef(1);
18
+
19
+ const createTerminal = () => {
20
+ const id = nextId.current++;
21
  const term = new XTerm({
22
  cursorBlink: true,
23
  theme: {
24
  background: '#0d1117',
25
  foreground: '#c9d1d9',
26
  cursor: '#58a6ff',
 
27
  black: '#484f58',
28
  red: '#f85149',
29
  green: '#3fb950',
 
38
  });
39
  const fit = new FitAddon();
40
  term.loadAddon(fit);
 
 
 
 
 
41
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
42
  const socket = new WebSocket(`${protocol}//${window.location.host}/ws/terminal`);
43
+ term.onData(data => socket.send(`exec:${data}`));
44
+ socket.onmessage = (event) => term.write(event.data);
45
+ const newTab: Tab = { id, terminal: term, socket };
46
+ setTabs(prev => [...prev, newTab]);
47
+ setActiveTab(id);
48
+ return { term, socket, fit };
49
+ };
50
+
51
+ useEffect(() => {
52
+ // Create initial terminal
53
+ createTerminal();
54
  }, []);
55
 
56
+ useEffect(() => {
57
+ const tab = tabs.find(t => t.id === activeTab);
58
+ if (tab) {
59
+ const container = termRefs.current.get(tab.id);
60
+ if (container && !tab.terminal.element) {
61
+ tab.terminal.open(container);
62
+ const fitAddon = new FitAddon();
63
+ tab.terminal.loadAddon(fitAddon);
64
+ fitAddon.fit();
65
+ window.addEventListener('resize', () => fitAddon.fit());
66
+ }
67
+ }
68
+ }, [activeTab, tabs]);
69
+
70
+ const removeTab = (id: number) => {
71
+ const tab = tabs.find(t => t.id === id);
72
+ if (tab) {
73
+ tab.socket.close();
74
+ tab.terminal.dispose();
75
+ setTabs(prev => prev.filter(t => t.id !== id));
76
+ if (activeTab === id) {
77
+ const remaining = tabs.filter(t => t.id !== id);
78
+ if (remaining.length > 0) setActiveTab(remaining[0].id);
79
+ }
80
+ }
81
+ };
82
+
83
+ return (
84
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0d1117' }}>
85
+ <div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border)' }}>
86
+ {tabs.map(tab => (
87
+ <div key={tab.id}
88
+ onClick={() => setActiveTab(tab.id)}
89
+ style={{
90
+ padding: '4px 12px',
91
+ cursor: 'pointer',
92
+ background: activeTab === tab.id ? 'var(--bg-primary)' : 'transparent',
93
+ borderRight: '1px solid var(--border)',
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ gap: 6,
97
+ fontSize: 12,
98
+ color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-secondary)',
99
+ }}>
100
+ <span>Terminal {tab.id}</span>
101
+ <FaTimes size={10} onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }} style={{ opacity: 0.6 }} />
102
+ </div>
103
+ ))}
104
+ <button onClick={() => createTerminal()} style={{ padding: '4px 8px', color: 'var(--text-secondary)' }}>
105
+ <FaPlus size={12} />
106
+ </button>
107
+ </div>
108
+ <div style={{ flex: 1, position: 'relative' }}>
109
+ {tabs.map(tab => (
110
+ <div
111
+ key={tab.id}
112
+ ref={el => { if (el) termRefs.current.set(tab.id, el); }}
113
+ style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: activeTab === tab.id ? 'block' : 'none' }}
114
+ />
115
+ ))}
116
+ </div>
117
+ </div>
118
+ );
119
  };
120
 
121
+ export default TerminalTabs;