From 9c38adb69efd543e20077375dc9613f6e63626a9 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:03:58 +0700 Subject: [PATCH] Add terminal text color configuration modal Introduces ModalConfig component for customizing terminal text color, accessible from DragTabs via a new settings icon. TerminalXTerm now reads the color from localStorage, allowing user-selected colors to persist across sessions. --- FRONTEND/src/components/DragTabs.tsx | 21 ++- FRONTEND/src/components/ModalConfig.tsx | 161 ++++++++++++++++++++++ FRONTEND/src/components/TerminalXTerm.tsx | 15 +- 3 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 FRONTEND/src/components/ModalConfig.tsx diff --git a/FRONTEND/src/components/DragTabs.tsx b/FRONTEND/src/components/DragTabs.tsx index c3d13b8..5b5635a 100644 --- a/FRONTEND/src/components/DragTabs.tsx +++ b/FRONTEND/src/components/DragTabs.tsx @@ -31,6 +31,7 @@ import { IconChevronRight, IconEdit, IconLogout, + IconSettings, IconSettingsPlus, IconUsersGroup, } from "@tabler/icons-react"; @@ -38,6 +39,7 @@ import classes from "./Component.module.css"; import type { TStation, TUser } from "../untils/types"; import type { Socket } from "socket.io-client"; import ModalHistory from "./ModalHistory"; +import ModalConfig from "./ModalConfig"; interface DraggableTabsProps { tabsData: TStation[]; @@ -126,6 +128,7 @@ export default function DraggableTabs({ const [isChangeTab, setIsChangeTab] = useState(false); const [isSetActive, setIsSetActive] = useState(false); const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); + const [openConfig, setOpenConfig] = useState(false); const sensors = useSensors(useSensor(PointerSensor)); @@ -229,7 +232,15 @@ export default function DraggableTabs({ w={w} > - + + setOpenConfig(true)} + > + + + el.id)} /> + + setOpenConfig(false)} + onSave={() => { + onChange(active); + }} + /> ); } diff --git a/FRONTEND/src/components/ModalConfig.tsx b/FRONTEND/src/components/ModalConfig.tsx new file mode 100644 index 0000000..1086a3f --- /dev/null +++ b/FRONTEND/src/components/ModalConfig.tsx @@ -0,0 +1,161 @@ +import { useEffect, useRef, useState } from "react"; +import { Modal, Button, ColorPicker, Group, Grid } from "@mantine/core"; +import { Terminal } from "xterm"; +import { FitAddon } from "@xterm/addon-fit"; + +interface Props { + opened: boolean; + onClose: () => void; + onSave: () => void; +} + +export default function ModalConfig({ opened, onClose, onSave }: Props) { + const [color, setColor] = useState("#41ee4a"); + const [loaded, setLoaded] = useState(false); + const xtermRef = useRef(null); + const terminal = useRef(null); + const fitRef = useRef(null); + + useEffect(() => { + // Load saved color from localStorage + const saved = localStorage.getItem("terminal-text-color"); + if (saved) setColor(saved); + }, []); + + const handleSave = () => { + localStorage.setItem("terminal-text-color", color); + onClose(); + onSave(); + }; + + useEffect(() => { + if (!loaded || fitRef.current || terminal.current || !xtermRef.current) + return; + + const textColor = localStorage.getItem("terminal-text-color") || "#41ee4a"; + terminal.current = new Terminal({ + disableStdin: true, + cursorBlink: true, + convertEol: true, + fontFamily: "Consolas, 'Courier New', monospace", + fontSize: 12, + theme: { + background: "#000000", + foreground: textColor, + cursor: "#ffffff", + }, + rows: 24, + cols: 80, + }); + const fitAddon = new FitAddon(); + fitRef.current = fitAddon; + terminal.current.loadAddon(fitAddon); + if (xtermRef.current) terminal.current.open(xtermRef.current); + + terminal.current?.write( + "Change color \nChange color\nChange color\nChange color" + ); + fitAddon.fit(); + }, [loaded, xtermRef]); + + useEffect(() => { + if (terminal.current && terminal.current.options.theme) { + terminal.current.options.theme = { + background: "#000000", + foreground: color, + cursor: "#ffffff", + }; + } + }, [color]); + + useEffect(() => { + if (opened) { + setTimeout(() => { + setLoaded(true); + }, 100); + } else { + setLoaded(false); + fitRef.current = null; + terminal.current = null; + const saved = localStorage.getItem("terminal-text-color"); + if (saved) setColor(saved); + } + }, [opened]); + + return ( + + + + + + + + + + +
+
{ + event.preventDefault(); + event.stopPropagation(); + }} + ref={xtermRef} + style={{ + width: "100%", + paddingLeft: "10px", + paddingBottom: "10px", + fontSize: "12px", + maxHeight: "220px", + height: "220px", + padding: "4px", + paddingRight: 0, + }} + onDoubleClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + }} + /> +
+ + + + ); +} diff --git a/FRONTEND/src/components/TerminalXTerm.tsx b/FRONTEND/src/components/TerminalXTerm.tsx index 972b4fe..fd2b6c8 100644 --- a/FRONTEND/src/components/TerminalXTerm.tsx +++ b/FRONTEND/src/components/TerminalXTerm.tsx @@ -56,8 +56,8 @@ const TerminalCLI: React.FC = ({ const [isInit, setIsInit] = useState(false); useEffect(() => { - if (!cliOpened || fitRef.current) return; - + if (!cliOpened || fitRef.current || terminal.current) return; + const textColor = localStorage.getItem("terminal-text-color") || "#41ee4a"; terminal.current = new Terminal({ disableStdin: isDisabled, cursorBlink: true, @@ -66,7 +66,7 @@ const TerminalCLI: React.FC = ({ fontSize: fontSize, theme: { background: "#000000", - foreground: "#41ee4a", + foreground: textColor, cursor: "#ffffff", }, rows: 24, @@ -160,6 +160,15 @@ const TerminalCLI: React.FC = ({ setIsInit(true); if (!miniSize && !isDisabled) terminal.current?.focus(); terminal.current.scrollToBottom(); + if (terminal.current.options.theme) { + const textColor = + localStorage.getItem("terminal-text-color") || "#41ee4a"; + terminal.current.options.theme = { + background: "#000000", + foreground: textColor, + cursor: "#ffffff", + }; + } } if (fitRef.current) fitRef.current?.fit(); setLoading(true);