Compare commits
1
Commits
main
...
experimental
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa62722c6 |
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"enable_boilerplate": false,
|
||||||
"llm": {
|
"llm": {
|
||||||
"base_url": "http://100.82.205.18:1010/",
|
"base_url": "http://100.82.205.18:1010/",
|
||||||
"model": "local-model",
|
"model": "local-model",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"enable_boilerplate": false,
|
||||||
"llm": {
|
"llm": {
|
||||||
"base_url": "http://127.0.0.1:1010",
|
"base_url": "http://127.0.0.1:1010",
|
||||||
"model": "local-model",
|
"model": "local-model",
|
||||||
|
|||||||
@@ -43,19 +43,38 @@ export interface HandbookCatalog {
|
|||||||
subjects: HandbookTopic[];
|
subjects: HandbookTopic[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getConfig = async (): Promise<{ enable_boilerplate: boolean }> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/config');
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
return { enable_boilerplate: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getChallenges = async (): Promise<Challenge[]> => {
|
export const getChallenges = async (): Promise<Challenge[]> => {
|
||||||
const response = await apiClient.get('/challenges');
|
const response = await apiClient.get('/challenges');
|
||||||
return response.data.challenges;
|
return response.data.challenges;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runCode = async (language: string, code: string, stdin: string = '') => {
|
export const runCode = async (
|
||||||
const response = await apiClient.post('/run', { language, code, stdin });
|
language: string,
|
||||||
|
code: string,
|
||||||
|
stdin: string = '',
|
||||||
|
files?: Record<string, string>,
|
||||||
|
activeFile?: string
|
||||||
|
) => {
|
||||||
|
const response = await apiClient.post('/run', { language, code, stdin, files, active_file: activeFile });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const lintCode = async (
|
||||||
export const lintCode = async (language: string, code: string) => {
|
language: string,
|
||||||
const response = await apiClient.post('/lint', { language, code });
|
code: string,
|
||||||
|
files?: Record<string, string>,
|
||||||
|
activeFile?: string
|
||||||
|
) => {
|
||||||
|
const response = await apiClient.post('/lint', { language, code, files, active_file: activeFile });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,13 +82,17 @@ export const getGuidance = async (
|
|||||||
challengeId: string,
|
challengeId: string,
|
||||||
language: string,
|
language: string,
|
||||||
code: string,
|
code: string,
|
||||||
question: string = ''
|
question: string = '',
|
||||||
|
files?: Record<string, string>,
|
||||||
|
activeFile?: string
|
||||||
) => {
|
) => {
|
||||||
const response = await apiClient.post('/guide', {
|
const response = await apiClient.post('/guide', {
|
||||||
challenge_id: challengeId,
|
challenge_id: challengeId,
|
||||||
language,
|
language,
|
||||||
code,
|
code,
|
||||||
question,
|
question,
|
||||||
|
files,
|
||||||
|
active_file: activeFile,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getGuidance,
|
getGuidance,
|
||||||
getHandbookCatalog,
|
getHandbookCatalog,
|
||||||
getHandbookExample,
|
getHandbookExample,
|
||||||
|
getConfig,
|
||||||
Challenge,
|
Challenge,
|
||||||
HandbookTopic,
|
HandbookTopic,
|
||||||
HandbookCatalog,
|
HandbookCatalog,
|
||||||
@@ -32,18 +33,128 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
GripHorizontal,
|
GripHorizontal,
|
||||||
|
Folder,
|
||||||
|
FolderPlus,
|
||||||
|
Plus,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
FileCode,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface WorkspaceProps {
|
interface WorkspaceProps {
|
||||||
challengeId: string;
|
challengeId?: string;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLanguageFromFilename(filename: string): string {
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
cpp: 'cpp',
|
||||||
|
hpp: 'cpp',
|
||||||
|
h: 'cpp',
|
||||||
|
cxx: 'cpp',
|
||||||
|
py: 'python',
|
||||||
|
java: 'java',
|
||||||
|
cs: 'csharp',
|
||||||
|
rs: 'rust',
|
||||||
|
go: 'go',
|
||||||
|
js: 'javascript',
|
||||||
|
ts: 'typescript',
|
||||||
|
html: 'html',
|
||||||
|
lua: 'lua',
|
||||||
|
json: 'json',
|
||||||
|
md: 'markdown',
|
||||||
|
};
|
||||||
|
return map[ext] || 'python';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultFilesForLanguage(language?: string, enableBoilerplate: boolean = false): Record<string, string> {
|
||||||
|
const norm = (language || 'python').toLowerCase().trim();
|
||||||
|
if (!enableBoilerplate) {
|
||||||
|
if (norm === 'cpp' || norm === 'c++' || norm === 'cxx') {
|
||||||
|
return { 'main.cpp': '' };
|
||||||
|
} else if (norm === 'java') {
|
||||||
|
return { 'Main.java': '' };
|
||||||
|
} else if (norm === 'csharp' || norm === 'c#') {
|
||||||
|
return { 'Program.cs': '' };
|
||||||
|
} else if (norm === 'rust') {
|
||||||
|
return { 'main.rs': '' };
|
||||||
|
} else if (norm === 'go') {
|
||||||
|
return { 'main.go': '' };
|
||||||
|
} else if (norm === 'javascript' || norm === 'js') {
|
||||||
|
return { 'main.js': '' };
|
||||||
|
} else if (norm === 'typescript' || norm === 'ts') {
|
||||||
|
return { 'main.ts': '' };
|
||||||
|
} else if (norm === 'html') {
|
||||||
|
return { 'index.html': '' };
|
||||||
|
} else if (norm === 'lua') {
|
||||||
|
return { 'main.lua': '' };
|
||||||
|
}
|
||||||
|
return { 'main.py': '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (norm === 'cpp' || norm === 'c++' || norm === 'cxx') {
|
||||||
|
return {
|
||||||
|
'main.cpp': '// Main implementation\n#include "main.h"\n#include <iostream>\n\nint main() {\n std::cout << "Hello from C++!" << std::endl;\n return 0;\n}\n',
|
||||||
|
'main.h': '// Main header file\n#ifndef MAIN_H\n#define MAIN_H\n\n// Declarations\n\n#endif\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'java') {
|
||||||
|
return {
|
||||||
|
'Main.java': 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello from Java!");\n }\n}\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'csharp' || norm === 'c#') {
|
||||||
|
return {
|
||||||
|
'Program.cs': 'using System;\n\nclass Program {\n static void Main() {\n Console.WriteLine("Hello from C#!");\n }\n}\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'rust') {
|
||||||
|
return {
|
||||||
|
'main.rs': 'fn main() {\n println!("Hello from Rust!");\n}\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'go') {
|
||||||
|
return {
|
||||||
|
'main.go': 'package main\nimport "fmt"\n\nfunc main() {\n fmt.Println("Hello from Go!")\n}\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'javascript' || norm === 'js') {
|
||||||
|
return {
|
||||||
|
'main.js': 'console.log("Hello from JavaScript!");\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'typescript' || norm === 'ts') {
|
||||||
|
return {
|
||||||
|
'main.ts': 'console.log("Hello from TypeScript!");\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'html') {
|
||||||
|
return {
|
||||||
|
'index.html': '<!DOCTYPE html>\n<html>\n<head>\n <title>App</title>\n</head>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>\n',
|
||||||
|
};
|
||||||
|
} else if (norm === 'lua') {
|
||||||
|
return {
|
||||||
|
'main.lua': 'print("Hello from Lua!");\n',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'main.py': '# Main script\nprint("Hello from Python!")\n',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||||
const [allChallenges, setAllChallenges] = useState<Challenge[]>([]);
|
const [allChallenges, setAllChallenges] = useState<Challenge[]>([]);
|
||||||
const [currentChallenge, setCurrentChallenge] = useState<Challenge | null>(null);
|
const [currentChallenge, setCurrentChallenge] = useState<Challenge | null>(null);
|
||||||
|
const [enableBoilerplate, setEnableBoilerplate] = useState(false);
|
||||||
|
|
||||||
|
// Multi-file & Folder state
|
||||||
|
const [files, setFiles] = useState<Record<string, string>>({ 'main.py': '' });
|
||||||
|
const [folders, setFolders] = useState<string[]>([]);
|
||||||
|
const [activeFile, setActiveFile] = useState<string>('main.py');
|
||||||
|
const [openTabs, setOpenTabs] = useState<string[]>(['main.py']);
|
||||||
|
|
||||||
|
// Modals for File & Folder Operations
|
||||||
|
const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
|
||||||
|
const [newFileNameInput, setNewFileNameInput] = useState('');
|
||||||
|
const [isNewFolderModalOpen, setIsNewFolderModalOpen] = useState(false);
|
||||||
|
const [newFolderNameInput, setNewFolderNameInput] = useState('');
|
||||||
|
const [isRenameModalOpen, setIsRenameModalOpen] = useState(false);
|
||||||
|
const [renameFileNameInput, setRenameFileNameInput] = useState('');
|
||||||
|
|
||||||
const [code, setCode] = useState('');
|
|
||||||
const [stdinText, setStdinText] = useState('');
|
const [stdinText, setStdinText] = useState('');
|
||||||
|
|
||||||
// Resizable Layout Dimensions
|
// Resizable Layout Dimensions
|
||||||
@@ -59,8 +170,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
text: 'Execution and lint output will appear here...',
|
text: 'Execution and lint output will appear here...',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Right sidebar state: 'mentor' | 'handbook' | 'closed'
|
// Right sidebar state: 'mentor' | 'handbook' | 'files' | 'closed'
|
||||||
const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'closed'>('mentor');
|
const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'files' | 'closed'>('mentor');
|
||||||
|
|
||||||
// Mentor state
|
// Mentor state
|
||||||
const [guidance, setGuidance] = useState<string>('Ask the mentor a question or click Check Answer below...');
|
const [guidance, setGuidance] = useState<string>('Ask the mentor a question or click Check Answer below...');
|
||||||
@@ -80,6 +191,15 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const [isLinting, setIsLinting] = useState(false);
|
const [isLinting, setIsLinting] = useState(false);
|
||||||
|
|
||||||
|
// Load backend config on mount
|
||||||
|
useEffect(() => {
|
||||||
|
getConfig()
|
||||||
|
.then((cfg) => {
|
||||||
|
setEnableBoilerplate(cfg.enable_boilerplate);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load all challenges
|
// Load all challenges
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getChallenges()
|
getChallenges()
|
||||||
@@ -95,7 +215,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
.catch((err) => console.error('Failed to load challenges:', err));
|
.catch((err) => console.error('Failed to load challenges:', err));
|
||||||
}, [challengeId]);
|
}, [challengeId]);
|
||||||
|
|
||||||
// Fetch handbook catalog when current challenge language changes
|
// Load handbook catalog when current challenge language changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentChallenge) {
|
if (currentChallenge) {
|
||||||
getHandbookCatalog(currentChallenge.language)
|
getHandbookCatalog(currentChallenge.language)
|
||||||
@@ -105,8 +225,45 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
setHandbookExample('');
|
setHandbookExample('');
|
||||||
})
|
})
|
||||||
.catch((err) => console.error('Failed to load handbook catalog:', err));
|
.catch((err) => console.error('Failed to load handbook catalog:', err));
|
||||||
|
|
||||||
|
const defaultFiles = getDefaultFilesForLanguage(currentChallenge.language, enableBoilerplate);
|
||||||
|
setFiles(defaultFiles);
|
||||||
|
const keys = Object.keys(defaultFiles);
|
||||||
|
const first = keys[0] || 'main.py';
|
||||||
|
setActiveFile(first);
|
||||||
|
setOpenTabs(keys);
|
||||||
}
|
}
|
||||||
}, [currentChallenge]);
|
}, [currentChallenge, enableBoilerplate]);
|
||||||
|
|
||||||
|
const handleCreateFolder = (folderName: string) => {
|
||||||
|
const name = folderName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
if (!folders.includes(name)) {
|
||||||
|
setFolders((prev) => [...prev, name]);
|
||||||
|
}
|
||||||
|
setIsNewFolderModalOpen(false);
|
||||||
|
setNewFolderNameInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global keyboard shortcuts (Ctrl+1..9 for tabs, Ctrl+T for new file)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 't') {
|
||||||
|
e.preventDefault();
|
||||||
|
setNewFileNameInput('');
|
||||||
|
setIsNewFileModalOpen(true);
|
||||||
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key >= '1' && e.key <= '9') {
|
||||||
|
const idx = parseInt(e.key, 10) - 1;
|
||||||
|
if (idx < openTabs.length) {
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveFile(openTabs[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [openTabs]);
|
||||||
|
|
||||||
// Global mousemove and mouseup listeners for drag-to-resize splitters
|
// Global mousemove and mouseup listeners for drag-to-resize splitters
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -177,14 +334,76 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Multi-file tab & file operation handlers
|
||||||
|
const handleCreateFile = (filename: string) => {
|
||||||
|
const name = filename.trim();
|
||||||
|
if (!name) return;
|
||||||
|
if (files[name] !== undefined) {
|
||||||
|
if (!openTabs.includes(name)) {
|
||||||
|
setOpenTabs((prev) => [...prev, name]);
|
||||||
|
}
|
||||||
|
setActiveFile(name);
|
||||||
|
} else {
|
||||||
|
setFiles((prev) => ({ ...prev, [name]: '' }));
|
||||||
|
setOpenTabs((prev) => [...prev, name]);
|
||||||
|
setActiveFile(name);
|
||||||
|
}
|
||||||
|
setIsNewFileModalOpen(false);
|
||||||
|
setNewFileNameInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseTab = (tabName: string, e?: React.MouseEvent) => {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
if (openTabs.length <= 1) return;
|
||||||
|
const nextTabs = openTabs.filter((t) => t !== tabName);
|
||||||
|
setOpenTabs(nextTabs);
|
||||||
|
if (activeFile === tabName) {
|
||||||
|
setActiveFile(nextTabs[nextTabs.length - 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameActiveFile = (newName: string) => {
|
||||||
|
const name = newName.trim();
|
||||||
|
if (!name || name === activeFile) {
|
||||||
|
setIsRenameModalOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = files[activeFile] || '';
|
||||||
|
const newFiles = { ...files };
|
||||||
|
delete newFiles[activeFile];
|
||||||
|
newFiles[name] = content;
|
||||||
|
|
||||||
|
setFiles(newFiles);
|
||||||
|
setOpenTabs((prev) => prev.map((t) => (t === activeFile ? name : t)));
|
||||||
|
setActiveFile(name);
|
||||||
|
setIsRenameModalOpen(false);
|
||||||
|
setRenameFileNameInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFile = (fileName: string) => {
|
||||||
|
if (Object.keys(files).length <= 1) return;
|
||||||
|
const newFiles = { ...files };
|
||||||
|
delete newFiles[fileName];
|
||||||
|
setFiles(newFiles);
|
||||||
|
const nextTabs = openTabs.filter((t) => t !== fileName);
|
||||||
|
setOpenTabs(nextTabs.length > 0 ? nextTabs : [Object.keys(newFiles)[0]]);
|
||||||
|
if (activeFile === fileName) {
|
||||||
|
setActiveFile(Object.keys(newFiles)[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRunCode = async (overrideStdin?: string) => {
|
const handleRunCode = async (overrideStdin?: string) => {
|
||||||
if (!code.trim() || !currentChallenge) return;
|
const currentCode = files[activeFile] || '';
|
||||||
|
if (!currentCode.trim() || !currentChallenge) return;
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
setOutput({ status: 'idle', text: 'Executing code...' });
|
setOutput({ status: 'idle', text: 'Executing code...' });
|
||||||
const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText;
|
const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await runCode(getMonacoLanguage(currentChallenge.language), code, inputToSend);
|
const lang = getLanguageFromFilename(activeFile);
|
||||||
|
const res = await runCode(lang, currentCode, inputToSend, files, activeFile);
|
||||||
if (res.exit_code === 0) {
|
if (res.exit_code === 0) {
|
||||||
setOutput({
|
setOutput({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -206,11 +425,13 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLintCode = async () => {
|
const handleLintCode = async () => {
|
||||||
if (!code.trim() || !currentChallenge) return;
|
const currentCode = files[activeFile] || '';
|
||||||
|
if (!currentCode.trim() || !currentChallenge) return;
|
||||||
setIsLinting(true);
|
setIsLinting(true);
|
||||||
setOutput({ status: 'idle', text: 'Running syntax & style lint...' });
|
setOutput({ status: 'idle', text: 'Running syntax & style lint...' });
|
||||||
try {
|
try {
|
||||||
const res = await lintCode(getMonacoLanguage(currentChallenge.language), code);
|
const lang = getLanguageFromFilename(activeFile);
|
||||||
|
const res = await lintCode(lang, currentCode, files, activeFile);
|
||||||
if (res.exit_code === 0) {
|
if (res.exit_code === 0) {
|
||||||
setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' });
|
setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' });
|
||||||
} else {
|
} else {
|
||||||
@@ -235,11 +456,14 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
setUserQuestion('');
|
setUserQuestion('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const lang = getLanguageFromFilename(activeFile);
|
||||||
const res = await getGuidance(
|
const res = await getGuidance(
|
||||||
cid,
|
cid,
|
||||||
getMonacoLanguage(currentChallenge.language),
|
lang,
|
||||||
code,
|
files[activeFile] || '',
|
||||||
q
|
q,
|
||||||
|
files,
|
||||||
|
activeFile
|
||||||
);
|
);
|
||||||
const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : '';
|
const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : '';
|
||||||
setGuidance(`${qHeader}${res.mentor_response || 'No guidance received.'}`);
|
setGuidance(`${qHeader}${res.mentor_response || 'No guidance received.'}`);
|
||||||
@@ -343,7 +567,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRunCode()}
|
onClick={() => handleRunCode()}
|
||||||
disabled={isRunning || !code.trim()}
|
disabled={isRunning || !(files[activeFile] || '').trim()}
|
||||||
className="px-3.5 py-1.5 bg-green-600 hover:bg-green-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
|
className="px-3.5 py-1.5 bg-green-600 hover:bg-green-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
|
||||||
>
|
>
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
@@ -356,7 +580,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLintCode}
|
onClick={handleLintCode}
|
||||||
disabled={isLinting || !code.trim()}
|
disabled={isLinting || !(files[activeFile] || '').trim()}
|
||||||
className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
|
className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
|
||||||
>
|
>
|
||||||
{isLinting ? (
|
{isLinting ? (
|
||||||
@@ -369,14 +593,27 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleCheckAnswer}
|
onClick={handleCheckAnswer}
|
||||||
disabled={isLoadingGuidance || !code.trim()}
|
disabled={isLoadingGuidance || !(files[activeFile] || '').trim()}
|
||||||
className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
|
className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
|
||||||
>
|
>
|
||||||
<Sparkles size={14} />
|
<Sparkles size={14} />
|
||||||
<span>Check Answer</span>
|
<span>Check Answer</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dual Sidebar Toggles */}
|
{/* Sidebar Toggles: File Browser (Folder icon), Mentor, Syntax Handbook */}
|
||||||
|
<button
|
||||||
|
onClick={() => setRightSidebarMode(rightSidebarMode === 'files' ? 'closed' : 'files')}
|
||||||
|
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
|
||||||
|
rightSidebarMode === 'files'
|
||||||
|
? 'bg-blue-500/20 text-blue-300 border-blue-500/40'
|
||||||
|
: 'bg-gray-800 text-gray-300 hover:bg-gray-700 border-gray-700'
|
||||||
|
}`}
|
||||||
|
title="Toggle File Tree Sidebar"
|
||||||
|
>
|
||||||
|
<Folder size={14} />
|
||||||
|
<span>Files</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setRightSidebarMode(rightSidebarMode === 'mentor' ? 'closed' : 'mentor')}
|
onClick={() => setRightSidebarMode(rightSidebarMode === 'mentor' ? 'closed' : 'mentor')}
|
||||||
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
|
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
|
||||||
@@ -405,7 +642,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<div className="flex flex-1 overflow-hidden relative">
|
<div className="flex flex-1 overflow-hidden relative">
|
||||||
{/* Full-Screen Drag Overlay to bypass Monaco Editor event capture */}
|
{/* Full-Screen Drag Overlay */}
|
||||||
{draggingType && (
|
{draggingType && (
|
||||||
<div
|
<div
|
||||||
onMouseMove={(e) => {
|
onMouseMove={(e) => {
|
||||||
@@ -434,7 +671,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Left Column: Challenge Specifications (Scalable Width) */}
|
|
||||||
|
{/* Left Column: Challenge Specifications */}
|
||||||
<div
|
<div
|
||||||
style={{ width: `${leftWidth}px` }}
|
style={{ width: `${leftWidth}px` }}
|
||||||
className="border-r border-transparent bg-darkBg flex flex-col overflow-hidden shrink-0"
|
className="border-r border-transparent bg-darkBg flex flex-col overflow-hidden shrink-0"
|
||||||
@@ -507,7 +745,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resizable Splitter Handle: Left Column */}
|
{/* Splitter Handle: Left */}
|
||||||
<div
|
<div
|
||||||
onMouseDown={startDraggingLeft}
|
onMouseDown={startDraggingLeft}
|
||||||
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
|
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
|
||||||
@@ -516,27 +754,76 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
|
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
|
{/* Middle Column: Multi-File Tabs, Monaco Code Editor & Resizable Output Console */}
|
||||||
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
|
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
|
||||||
<div className="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0">
|
{/* Multi-File Tab Bar */}
|
||||||
<span className="font-mono text-blue-400">{getLanguageFileName(currentChallenge?.language)}</span>
|
<div className="bg-darkBg text-gray-400 px-2 py-1 text-xs flex items-center border-b border-darkBorder shrink-0 overflow-x-auto gap-1 select-none">
|
||||||
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
|
{openTabs.map((tab, idx) => (
|
||||||
|
<div
|
||||||
|
key={tab}
|
||||||
|
onClick={() => setActiveFile(tab)}
|
||||||
|
onAuxClick={(e) => {
|
||||||
|
if (e.button === 1) handleCloseTab(tab, e);
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
if (tab === activeFile) {
|
||||||
|
setRenameFileNameInput(tab);
|
||||||
|
setIsRenameModalOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`px-3 py-1 rounded-t-md text-xs font-mono flex items-center gap-2 cursor-pointer border-t border-x transition-colors ${
|
||||||
|
tab === activeFile
|
||||||
|
? 'bg-[#1e1e1e] text-blue-400 border-darkBorder font-semibold'
|
||||||
|
: 'bg-darkBg text-gray-400 border-transparent hover:bg-darkSurface hover:text-gray-200'
|
||||||
|
}`}
|
||||||
|
title="Double-click to rename | Middle-click to close"
|
||||||
|
>
|
||||||
|
<FileCode size={13} className={tab === activeFile ? 'text-blue-400' : 'text-gray-500'} />
|
||||||
|
<span>{tab}</span>
|
||||||
|
<span className="text-[10px] text-gray-500 opacity-60 font-sans">^{idx + 1}</span>
|
||||||
|
{openTabs.length > 1 && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleCloseTab(tab, e)}
|
||||||
|
className="hover:text-red-400 p-0.5 rounded transition-colors ml-1"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setNewFileNameInput('');
|
||||||
|
setIsNewFileModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="p-1 hover:bg-gray-800 text-gray-400 hover:text-white rounded transition-colors ml-1"
|
||||||
|
title="New File Tab (Ctrl+T)"
|
||||||
|
>
|
||||||
|
<Plus size={14} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="ml-auto text-[11px] text-gray-500 font-mono pr-2">
|
||||||
|
{getLanguageFromFilename(activeFile)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Code Editor */}
|
{/* Code Editor */}
|
||||||
<div className="flex-1 min-h-0 relative">
|
<div className="flex-1 min-h-0 relative">
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
defaultLanguage={getMonacoLanguage(currentChallenge?.language)}
|
defaultLanguage={getLanguageFromFilename(activeFile)}
|
||||||
language={getMonacoLanguage(currentChallenge?.language)}
|
language={getLanguageFromFilename(activeFile)}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
value={code}
|
value={files[activeFile] || ''}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
onMount={(editor, monaco) => {
|
onMount={(editor, monaco) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
registerMonacoCompletions(monaco);
|
registerMonacoCompletions(monaco);
|
||||||
}}
|
}}
|
||||||
onChange={(val) => setCode(val || '')}
|
onChange={(val) =>
|
||||||
|
setFiles((prev) => ({ ...prev, [activeFile]: val || '' }))
|
||||||
|
}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -611,7 +898,111 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Right Column Mode 1: Mentor Sidebar (Scalable Width) */}
|
{/* Right Column Mode 1: File Browser Sidebar */}
|
||||||
|
{rightSidebarMode === 'files' && (
|
||||||
|
<div
|
||||||
|
style={{ width: `${rightWidth}px` }}
|
||||||
|
className="border-l border-transparent bg-darkSurface flex flex-col overflow-hidden shrink-0"
|
||||||
|
>
|
||||||
|
<div className="p-3 border-b border-darkBorder bg-darkBg flex justify-between items-center">
|
||||||
|
<h2 className="flex items-center gap-2 font-bold text-blue-400 text-xs uppercase tracking-wider">
|
||||||
|
<Folder size={16} /> File Browser
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setNewFolderNameInput('');
|
||||||
|
setIsNewFolderModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="p-1 hover:bg-gray-800 text-gray-300 hover:text-white rounded"
|
||||||
|
title="Create Folder"
|
||||||
|
>
|
||||||
|
<FolderPlus size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setNewFileNameInput('');
|
||||||
|
setIsNewFileModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="p-1 hover:bg-gray-800 text-gray-300 hover:text-white rounded"
|
||||||
|
title="Create File"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setRightSidebarMode('closed')}
|
||||||
|
className="text-gray-400 hover:text-white text-xs p-1 rounded hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 flex-1 overflow-y-auto space-y-1 bg-darkBg">
|
||||||
|
{folders.map((folderName) => (
|
||||||
|
<div
|
||||||
|
key={`folder-${folderName}`}
|
||||||
|
className="flex items-center justify-between p-2 rounded-lg text-xs font-mono bg-darkSurface border border-darkBorder/50 text-amber-300 select-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
|
<Folder size={14} className="text-amber-400" />
|
||||||
|
<span className="truncate">{folderName}/</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{Object.keys(files).map((fileName) => (
|
||||||
|
<div
|
||||||
|
key={fileName}
|
||||||
|
onClick={() => {
|
||||||
|
if (!openTabs.includes(fileName)) {
|
||||||
|
setOpenTabs((prev) => [...prev, fileName]);
|
||||||
|
}
|
||||||
|
setActiveFile(fileName);
|
||||||
|
}}
|
||||||
|
className={`flex items-center justify-between p-2 rounded-lg text-xs font-mono transition-all cursor-pointer border ${
|
||||||
|
fileName === activeFile
|
||||||
|
? 'bg-blue-600/20 border-blue-500/50 text-blue-300 font-bold'
|
||||||
|
: 'bg-darkSurface border-darkBorder/50 text-gray-300 hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
|
<FileCode size={14} className={fileName === activeFile ? 'text-blue-400' : 'text-gray-500'} />
|
||||||
|
<span className="truncate">{fileName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setRenameFileNameInput(fileName);
|
||||||
|
setActiveFile(fileName);
|
||||||
|
setIsRenameModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-400 hover:text-white"
|
||||||
|
title="Rename File"
|
||||||
|
>
|
||||||
|
<Edit2 size={12} />
|
||||||
|
</button>
|
||||||
|
{Object.keys(files).length > 1 && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFile(fileName);
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-400 hover:text-red-400"
|
||||||
|
title="Delete File"
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right Column Mode 2: Mentor Sidebar (Scalable Width) */}
|
||||||
{rightSidebarMode === 'mentor' && (
|
{rightSidebarMode === 'mentor' && (
|
||||||
<div
|
<div
|
||||||
style={{ width: `${rightWidth}px` }}
|
style={{ width: `${rightWidth}px` }}
|
||||||
@@ -673,7 +1064,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleCheckAnswer}
|
onClick={handleCheckAnswer}
|
||||||
disabled={isLoadingGuidance || !code.trim()}
|
disabled={isLoadingGuidance || !(files[activeFile] || '').trim()}
|
||||||
className="py-2 px-3 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1"
|
className="py-2 px-3 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1"
|
||||||
title="Check Answer"
|
title="Check Answer"
|
||||||
>
|
>
|
||||||
@@ -689,7 +1080,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Right Column Mode 2: Handbook Sidebar (Scalable Width) */}
|
{/* Right Column Mode 3: Handbook Sidebar (Scalable Width) */}
|
||||||
{rightSidebarMode === 'handbook' && (
|
{rightSidebarMode === 'handbook' && (
|
||||||
<div
|
<div
|
||||||
style={{ width: `${rightWidth}px` }}
|
style={{ width: `${rightWidth}px` }}
|
||||||
@@ -746,7 +1137,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Topic Catalog List (Fills full height when no topic selected) */}
|
{/* Topic Catalog List */}
|
||||||
<div className={`p-3 bg-darkBg flex flex-col gap-1.5 overflow-y-auto ${
|
<div className={`p-3 bg-darkBg flex flex-col gap-1.5 overflow-y-auto ${
|
||||||
selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1'
|
selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -788,7 +1179,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Generated LLM Reference Card View (ONLY renders after selecting a topic) */}
|
{/* Generated LLM Reference Card View */}
|
||||||
{selectedTopic && (
|
{selectedTopic && (
|
||||||
<div className="flex-1 p-4 overflow-y-auto bg-darkSurface border-t border-darkBorder">
|
<div className="flex-1 p-4 overflow-y-auto bg-darkSurface border-t border-darkBorder">
|
||||||
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
|
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
|
||||||
@@ -808,6 +1199,117 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* New File Modal */}
|
||||||
|
{isNewFileModalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-[999999] flex items-center justify-center p-4">
|
||||||
|
<div className="bg-darkSurface border border-darkBorder rounded-xl p-5 w-full max-w-sm shadow-xl flex flex-col gap-4">
|
||||||
|
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<Plus size={16} className="text-blue-400" /> Create New File
|
||||||
|
</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={newFileNameInput}
|
||||||
|
onChange={(e) => setNewFileNameInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleCreateFile(newFileNameInput);
|
||||||
|
if (e.key === 'Escape') setIsNewFileModalOpen(false);
|
||||||
|
}}
|
||||||
|
placeholder="e.g. helper.cpp, utils.h, data.json"
|
||||||
|
className="w-full bg-darkBg border border-darkBorder rounded-lg px-3 py-2 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 font-mono"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsNewFileModalOpen(false)}
|
||||||
|
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white rounded-lg hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCreateFile(newFileNameInput)}
|
||||||
|
className="px-4 py-1.5 text-xs font-semibold bg-blue-600 hover:bg-blue-500 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Create File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* New Folder Modal */}
|
||||||
|
{isNewFolderModalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-[999999] flex items-center justify-center p-4">
|
||||||
|
<div className="bg-darkSurface border border-darkBorder rounded-xl p-5 w-full max-w-sm shadow-xl flex flex-col gap-4">
|
||||||
|
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<FolderPlus size={16} className="text-amber-400" /> Create New Folder
|
||||||
|
</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={newFolderNameInput}
|
||||||
|
onChange={(e) => setNewFolderNameInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleCreateFolder(newFolderNameInput);
|
||||||
|
if (e.key === 'Escape') setIsNewFolderModalOpen(false);
|
||||||
|
}}
|
||||||
|
placeholder="e.g. src, include, components"
|
||||||
|
className="w-full bg-darkBg border border-darkBorder rounded-lg px-3 py-2 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-amber-500 font-mono"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsNewFolderModalOpen(false)}
|
||||||
|
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white rounded-lg hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCreateFolder(newFolderNameInput)}
|
||||||
|
className="px-4 py-1.5 text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Create Folder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rename File Modal */}
|
||||||
|
{isRenameModalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-[999999] flex items-center justify-center p-4">
|
||||||
|
<div className="bg-darkSurface border border-darkBorder rounded-xl p-5 w-full max-w-sm shadow-xl flex flex-col gap-4">
|
||||||
|
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<Edit2 size={16} className="text-amber-400" /> Rename File
|
||||||
|
</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={renameFileNameInput}
|
||||||
|
onChange={(e) => setRenameFileNameInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleRenameActiveFile(renameFileNameInput);
|
||||||
|
if (e.key === 'Escape') setIsRenameModalOpen(false);
|
||||||
|
}}
|
||||||
|
placeholder="Enter new filename..."
|
||||||
|
className="w-full bg-darkBg border border-darkBorder rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500 font-mono"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsRenameModalOpen(false)}
|
||||||
|
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white rounded-lg hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRenameActiveFile(renameFileNameInput)}
|
||||||
|
className="px-4 py-1.5 text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Convert themes from convert-themes/registry.json into src/tui/themes/*.json"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REGISTRY_PATH = Path("convert-themes/registry.json")
|
||||||
|
OUTPUT_DIR = Path("src/tui/themes")
|
||||||
|
|
||||||
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not REGISTRY_PATH.exists():
|
||||||
|
print(f"Error: {REGISTRY_PATH} not found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
with open(REGISTRY_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
themes_list = data.get("themes", [])
|
||||||
|
converted_count = 0
|
||||||
|
|
||||||
|
for item in themes_list:
|
||||||
|
name = item.get("name")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dark = item.get("dark") or item.get("light") or {}
|
||||||
|
if not dark:
|
||||||
|
continue
|
||||||
|
|
||||||
|
theme_id = re.sub(r'[^a-z0-9\-]', '', name.lower().replace(' ', '-'))
|
||||||
|
if not theme_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
background = dark.get("mSurfaceVariant") or dark.get("mSurface") or "#1e1e1e"
|
||||||
|
surface = dark.get("mSurface") or "#252526"
|
||||||
|
primary = dark.get("mPrimary") or "#3b82f6"
|
||||||
|
primary_text = dark.get("mOnPrimary") or "#ffffff"
|
||||||
|
secondary = dark.get("mSecondary") or "#64748b"
|
||||||
|
accent = dark.get("mTertiary") or dark.get("mHover") or primary
|
||||||
|
folder_header = accent
|
||||||
|
folder_text = dark.get("mOnTertiary") or dark.get("mOnHover") or "#ffffff"
|
||||||
|
|
||||||
|
theme_dict = {
|
||||||
|
"id": theme_id,
|
||||||
|
"name": name,
|
||||||
|
"background": background,
|
||||||
|
"surface": surface,
|
||||||
|
"primary": primary,
|
||||||
|
"primary_text": primary_text,
|
||||||
|
"secondary": secondary,
|
||||||
|
"accent": accent,
|
||||||
|
"folder_header": folder_header,
|
||||||
|
"folder_text": folder_text
|
||||||
|
}
|
||||||
|
|
||||||
|
out_file = OUTPUT_DIR / f"{theme_id}.json"
|
||||||
|
with open(out_file, "w", encoding="utf-8") as f_out:
|
||||||
|
json.dump(theme_dict, f_out, indent=2)
|
||||||
|
converted_count += 1
|
||||||
|
|
||||||
|
print(f"Successfully converted {converted_count} themes to {OUTPUT_DIR}/!")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import asyncio
|
||||||
|
from src.tui.app import TactiTermTUI
|
||||||
|
|
||||||
|
async def test_command_palette_integration():
|
||||||
|
app = TactiTermTUI()
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
print("Available themes count registered with Textual App:", len(app.available_themes))
|
||||||
|
sample_themes = list(app.available_themes.keys())[:15]
|
||||||
|
print("Sample registered themes in App:", sample_themes)
|
||||||
|
|
||||||
|
# Test system commands
|
||||||
|
commands = list(app.get_system_commands(app.screen))
|
||||||
|
cmd_titles = [c.title for c in commands]
|
||||||
|
print("System commands in Command Palette:", cmd_titles)
|
||||||
|
|
||||||
|
assert "Keys" in cmd_titles, "Keys option missing from Command Palette"
|
||||||
|
assert "Themes" in cmd_titles, "Themes option missing from Command Palette"
|
||||||
|
|
||||||
|
# Test applying a theme from registered themes
|
||||||
|
app.apply_theme("dracula")
|
||||||
|
await pilot.pause(0.1)
|
||||||
|
print("Main box classes after applying Dracula:", app.query_one("#main-container").classes)
|
||||||
|
assert app.query_one("#main-container").has_class("theme-dracula")
|
||||||
|
print("Command Palette Theme Integration test PASSED!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_command_palette_integration())
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Integration test for multi-file code execution, linting, and prompt context formatting."""
|
||||||
|
import unittest
|
||||||
|
from src.core.executor import CodeExecutor
|
||||||
|
from src.core.linter import CodeLinter
|
||||||
|
from src.core.prompts import format_multifile_context, compress_file_content
|
||||||
|
|
||||||
|
class TestMultiFile(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.executor = CodeExecutor()
|
||||||
|
self.linter = CodeLinter()
|
||||||
|
|
||||||
|
def test_cpp_multifile_execution(self):
|
||||||
|
files = {
|
||||||
|
"main.cpp": '#include "main.h"\n#include <iostream>\nint main() { printMessage(); return 0; }\n',
|
||||||
|
"main.h": '#ifndef MAIN_H\n#define MAIN_H\n#include <iostream>\ninline void printMessage() { std::cout << "C++ Multi-File Success!"; }\n#endif\n'
|
||||||
|
}
|
||||||
|
res = self.executor.run("cpp", code="", stdin="", files=files)
|
||||||
|
self.assertEqual(res.get("exit_code"), 0)
|
||||||
|
self.assertIn("C++ Multi-File Success!", res.get("stdout"))
|
||||||
|
|
||||||
|
def test_python_multifile_execution(self):
|
||||||
|
files = {
|
||||||
|
"main.py": 'import helper\nprint(helper.get_msg())\n',
|
||||||
|
"helper.py": 'def get_msg(): return "Python Multi-File Success!"\n'
|
||||||
|
}
|
||||||
|
res = self.executor.run("python", code="", stdin="", files=files)
|
||||||
|
self.assertEqual(res.get("exit_code"), 0)
|
||||||
|
self.assertIn("Python Multi-File Success!", res.get("stdout"))
|
||||||
|
|
||||||
|
def test_multifile_context_formatting(self):
|
||||||
|
files = {
|
||||||
|
"main.cpp": '#include "main.h"\nint main() { return 0; }',
|
||||||
|
"main.h": '#define MSG "Hello"'
|
||||||
|
}
|
||||||
|
ctx = format_multifile_context(files, active_file="main.cpp")
|
||||||
|
self.assertIn("=== File: main.cpp (Active) ===", ctx)
|
||||||
|
self.assertIn("=== File: main.h ===", ctx)
|
||||||
|
|
||||||
|
def test_compression(self):
|
||||||
|
large_code = "def foo():\n pass\n" * 100
|
||||||
|
compressed = compress_file_content("test.py", large_code)
|
||||||
|
self.assertIn("def foo()", compressed)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from textual.theme import Theme
|
||||||
|
from src.tui.theme_manager import theme_manager
|
||||||
|
|
||||||
|
print("Loaded theme manager count:", len(theme_manager.themes))
|
||||||
|
t_sample = list(theme_manager.themes.values())[0]
|
||||||
|
print("Sample theme data:", t_sample)
|
||||||
|
|
||||||
|
t_obj = Theme(
|
||||||
|
name=t_sample["id"],
|
||||||
|
primary=t_sample["primary"],
|
||||||
|
secondary=t_sample.get("secondary"),
|
||||||
|
accent=t_sample.get("accent"),
|
||||||
|
background=t_sample["background"],
|
||||||
|
surface=t_sample["surface"],
|
||||||
|
dark=True
|
||||||
|
)
|
||||||
|
print("Created Textual Theme object successfully:", t_obj.name)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import asyncio
|
||||||
|
from src.tui.app import TactiTermTUI
|
||||||
|
from textual.widgets import OptionList
|
||||||
|
|
||||||
|
async def test_theme_popup():
|
||||||
|
app = TactiTermTUI()
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
print("Call action_cycle_theme()...")
|
||||||
|
app.action_cycle_theme()
|
||||||
|
await pilot.pause(0.2)
|
||||||
|
|
||||||
|
main_box = app.query_one("#main-container")
|
||||||
|
print("Is show-theme-menu in classes:", main_box.has_class("show-theme-menu"))
|
||||||
|
|
||||||
|
theme_list = app.query_one("#theme_list_popup", OptionList)
|
||||||
|
print("Theme list option count:", theme_list.option_count)
|
||||||
|
print("Focused widget ID:", getattr(app.focused, "id", None))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_theme_popup())
|
||||||
+29
-9
@@ -6,6 +6,7 @@ from src.core.loader import ChallengeLoader
|
|||||||
from src.core.executor import executor
|
from src.core.executor import executor
|
||||||
from src.core.linter import linter
|
from src.core.linter import linter
|
||||||
from src.core.prompts import get_socratic_prompt
|
from src.core.prompts import get_socratic_prompt
|
||||||
|
from src.core.config import config
|
||||||
|
|
||||||
app = FastAPI(title="Socratic Tutor API")
|
app = FastAPI(title="Socratic Tutor API")
|
||||||
|
|
||||||
@@ -20,25 +21,40 @@ app.add_middleware(
|
|||||||
loader = ChallengeLoader()
|
loader = ChallengeLoader()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/config")
|
||||||
|
def get_app_config():
|
||||||
|
return {
|
||||||
|
"enable_boilerplate": config.enable_boilerplate,
|
||||||
|
"llm_base_url": config.llm_base_url,
|
||||||
|
"llm_model": config.llm_model,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
class LintRequest(BaseModel):
|
class LintRequest(BaseModel):
|
||||||
language: str
|
language: str
|
||||||
code: str
|
code: Optional[str] = ""
|
||||||
|
files: Optional[Dict[str, str]] = None
|
||||||
|
active_file: Optional[str] = None
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class GuideRequest(BaseModel):
|
class GuideRequest(BaseModel):
|
||||||
challenge_id: str
|
challenge_id: str
|
||||||
language: str
|
language: str
|
||||||
code: str
|
code: Optional[str] = ""
|
||||||
question: Optional[str] = ""
|
question: Optional[str] = ""
|
||||||
|
files: Optional[Dict[str, str]] = None
|
||||||
|
active_file: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class RunRequest(BaseModel):
|
class RunRequest(BaseModel):
|
||||||
language: str
|
language: str
|
||||||
code: str
|
code: Optional[str] = ""
|
||||||
stdin: Optional[str] = ""
|
stdin: Optional[str] = ""
|
||||||
|
files: Optional[Dict[str, str]] = None
|
||||||
|
active_file: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/challenges")
|
@app.get("/challenges")
|
||||||
@@ -60,7 +76,7 @@ def get_challenge(challenge_id: str):
|
|||||||
@app.post("/lint")
|
@app.post("/lint")
|
||||||
def lint_code(req: LintRequest):
|
def lint_code(req: LintRequest):
|
||||||
"""Runs linting on user code using Core Linter."""
|
"""Runs linting on user code using Core Linter."""
|
||||||
res = linter.lint(req.language, req.code)
|
res = linter.lint(req.language, req.code or "", files=req.files, active_file=req.active_file)
|
||||||
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
||||||
raise HTTPException(status_code=404, detail=res["stderr"])
|
raise HTTPException(status_code=404, detail=res["stderr"])
|
||||||
return res
|
return res
|
||||||
@@ -69,7 +85,7 @@ def lint_code(req: LintRequest):
|
|||||||
@app.post("/run")
|
@app.post("/run")
|
||||||
def run_code(req: RunRequest):
|
def run_code(req: RunRequest):
|
||||||
"""Executes user code using Core Executor."""
|
"""Executes user code using Core Executor."""
|
||||||
res = executor.run(req.language, req.code, stdin=req.stdin or "")
|
res = executor.run(req.language, req.code or "", stdin=req.stdin or "", files=req.files)
|
||||||
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
||||||
raise HTTPException(status_code=404, detail=res["stderr"])
|
raise HTTPException(status_code=404, detail=res["stderr"])
|
||||||
return res
|
return res
|
||||||
@@ -100,7 +116,11 @@ async def guide_code(req: GuideRequest):
|
|||||||
|
|
||||||
challenge = challenges[req.challenge_id]
|
challenge = challenges[req.challenge_id]
|
||||||
guidance = await mentor.get_guidance_async(
|
guidance = await mentor.get_guidance_async(
|
||||||
challenge, req.code, user_question=req.question or ""
|
challenge,
|
||||||
|
user_code=req.code or "",
|
||||||
|
user_question=req.question or "",
|
||||||
|
files=req.files,
|
||||||
|
active_file=req.active_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+11
-1
@@ -8,6 +8,7 @@ class Config:
|
|||||||
"""Manages application configuration settings."""
|
"""Manages application configuration settings."""
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
|
"enable_boilerplate": False,
|
||||||
"llm": {
|
"llm": {
|
||||||
"base_url": "http://localhost:8080/v1",
|
"base_url": "http://localhost:8080/v1",
|
||||||
"model": "local-model",
|
"model": "local-model",
|
||||||
@@ -34,6 +35,8 @@ class Config:
|
|||||||
try:
|
try:
|
||||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||||
user_config = json.load(f)
|
user_config = json.load(f)
|
||||||
|
if "enable_boilerplate" in user_config:
|
||||||
|
config["enable_boilerplate"] = bool(user_config["enable_boilerplate"])
|
||||||
if "llm" in user_config and isinstance(user_config["llm"], dict):
|
if "llm" in user_config and isinstance(user_config["llm"], dict):
|
||||||
config["llm"].update(user_config["llm"])
|
config["llm"].update(user_config["llm"])
|
||||||
if "web" in user_config and isinstance(user_config["web"], dict):
|
if "web" in user_config and isinstance(user_config["web"], dict):
|
||||||
@@ -50,8 +53,11 @@ class Config:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to create default {self.config_path}: {e}")
|
print(f"Warning: Failed to create default {self.config_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
# Allow environment variable overrides
|
# Allow environment variable overrides
|
||||||
|
env_boilerplate = os.getenv("TACTTERM_ENABLE_BOILERPLATE")
|
||||||
|
if env_boilerplate is not None:
|
||||||
|
config["enable_boilerplate"] = env_boilerplate.lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
env_base_url = os.getenv("TACTTERM_LLM_BASE_URL")
|
env_base_url = os.getenv("TACTTERM_LLM_BASE_URL")
|
||||||
if env_base_url:
|
if env_base_url:
|
||||||
config["llm"]["base_url"] = env_base_url
|
config["llm"]["base_url"] = env_base_url
|
||||||
@@ -88,6 +94,10 @@ class Config:
|
|||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable_boilerplate(self) -> bool:
|
||||||
|
return bool(self._data.get("enable_boilerplate", False))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def llm_base_url(self) -> str:
|
def llm_base_url(self) -> str:
|
||||||
return self._data["llm"]["base_url"].rstrip("/")
|
return self._data["llm"]["base_url"].rstrip("/")
|
||||||
|
|||||||
+67
-18
@@ -8,9 +8,15 @@ from src.core.registry import registry
|
|||||||
|
|
||||||
|
|
||||||
class CodeExecutor:
|
class CodeExecutor:
|
||||||
"""Executes code for all 10 supported programming languages with stdin support."""
|
"""Executes code for all 10 supported programming languages with stdin support and multi-file projects."""
|
||||||
|
|
||||||
def run(self, language: str, code: str, stdin: str = "") -> Dict[str, Any]:
|
def run(
|
||||||
|
self,
|
||||||
|
language: str,
|
||||||
|
code: str = "",
|
||||||
|
stdin: str = "",
|
||||||
|
files: Dict[str, str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
config = registry.get_config(language)
|
config = registry.get_config(language)
|
||||||
if not config:
|
if not config:
|
||||||
return {
|
return {
|
||||||
@@ -24,20 +30,42 @@ class CodeExecutor:
|
|||||||
ext = config.get("ext", ".txt")
|
ext = config.get("ext", ".txt")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
if lang_key == "java":
|
# 1. Write multi-file project contents or fallback single file
|
||||||
temp_file_name = "Main.java"
|
if files and isinstance(files, dict) and len(files) > 0:
|
||||||
elif lang_key == "cpp":
|
for rel_path, content in files.items():
|
||||||
temp_file_name = "main.cpp"
|
target_path = os.path.join(tmpdir, rel_path)
|
||||||
elif lang_key == "rust":
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||||
temp_file_name = "main.rs"
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
elif lang_key == "csharp":
|
f.write(content)
|
||||||
temp_file_name = "Program.cs"
|
# Determine main entry file
|
||||||
|
main_file_candidates = [
|
||||||
|
"main.cpp", "Main.java", "main.rs", "Program.cs", "main.py",
|
||||||
|
"main.go", "main.js", "main.ts", "main.lua", "index.html"
|
||||||
|
]
|
||||||
|
temp_file_name = None
|
||||||
|
for candidate in main_file_candidates:
|
||||||
|
if candidate in files:
|
||||||
|
temp_file_name = candidate
|
||||||
|
break
|
||||||
|
if not temp_file_name:
|
||||||
|
temp_file_name = list(files.keys())[0]
|
||||||
else:
|
else:
|
||||||
temp_file_name = f"main{ext}"
|
if lang_key == "java":
|
||||||
|
temp_file_name = "Main.java"
|
||||||
|
elif lang_key == "cpp":
|
||||||
|
temp_file_name = "main.cpp"
|
||||||
|
elif lang_key == "rust":
|
||||||
|
temp_file_name = "main.rs"
|
||||||
|
elif lang_key == "csharp":
|
||||||
|
temp_file_name = "Program.cs"
|
||||||
|
else:
|
||||||
|
temp_file_name = f"main{ext}"
|
||||||
|
|
||||||
|
temp_file_path = os.path.join(tmpdir, temp_file_name)
|
||||||
|
with open(temp_file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
temp_file_path = os.path.join(tmpdir, temp_file_name)
|
temp_file_path = os.path.join(tmpdir, temp_file_name)
|
||||||
with open(temp_file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(code)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if lang_key == "python":
|
if lang_key == "python":
|
||||||
@@ -51,10 +79,13 @@ class CodeExecutor:
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
elif lang_key == "html":
|
elif lang_key == "html":
|
||||||
|
primary_code = code
|
||||||
|
if files and temp_file_name in files:
|
||||||
|
primary_code = files[temp_file_name]
|
||||||
return {
|
return {
|
||||||
"language": language,
|
"language": language,
|
||||||
"exit_code": 0,
|
"exit_code": 0,
|
||||||
"stdout": code,
|
"stdout": primary_code,
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
}
|
}
|
||||||
elif lang_key == "javascript":
|
elif lang_key == "javascript":
|
||||||
@@ -77,7 +108,7 @@ class CodeExecutor:
|
|||||||
)
|
)
|
||||||
elif lang_key == "go":
|
elif lang_key == "go":
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["go", "run", temp_file_path],
|
["go", "run", "."],
|
||||||
input=stdin,
|
input=stdin,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -94,8 +125,17 @@ class CodeExecutor:
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
elif lang_key == "cpp":
|
elif lang_key == "cpp":
|
||||||
|
# Collect all .cpp files recursively in tmpdir
|
||||||
|
cpp_files = []
|
||||||
|
for root, _, fnames in os.walk(tmpdir):
|
||||||
|
for fn in fnames:
|
||||||
|
if fn.endswith(".cpp"):
|
||||||
|
cpp_files.append(os.path.relpath(os.path.join(root, fn), tmpdir))
|
||||||
|
if not cpp_files:
|
||||||
|
cpp_files = [temp_file_name]
|
||||||
|
|
||||||
compile_res = subprocess.run(
|
compile_res = subprocess.run(
|
||||||
["g++", "-O2", temp_file_path, "-o", "main"],
|
["g++", "-O2"] + cpp_files + ["-o", "main"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
cwd=tmpdir,
|
cwd=tmpdir,
|
||||||
@@ -140,8 +180,16 @@ class CodeExecutor:
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
elif lang_key == "java":
|
elif lang_key == "java":
|
||||||
|
java_files = []
|
||||||
|
for root, _, fnames in os.walk(tmpdir):
|
||||||
|
for fn in fnames:
|
||||||
|
if fn.endswith(".java"):
|
||||||
|
java_files.append(os.path.relpath(os.path.join(root, fn), tmpdir))
|
||||||
|
if not java_files:
|
||||||
|
java_files = [temp_file_name]
|
||||||
|
|
||||||
compile_res = subprocess.run(
|
compile_res = subprocess.run(
|
||||||
["javac", temp_file_path],
|
["javac"] + java_files,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
cwd=tmpdir,
|
cwd=tmpdir,
|
||||||
@@ -154,8 +202,9 @@ class CodeExecutor:
|
|||||||
"stdout": compile_res.stdout,
|
"stdout": compile_res.stdout,
|
||||||
"stderr": f"Javac Compilation Error:\n{compile_res.stderr}",
|
"stderr": f"Javac Compilation Error:\n{compile_res.stderr}",
|
||||||
}
|
}
|
||||||
|
main_class = os.path.splitext(temp_file_name)[0]
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["java", "Main"],
|
["java", main_class],
|
||||||
input=stdin,
|
input=stdin,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
|
|||||||
+28
-12
@@ -10,7 +10,13 @@ from src.core.registry import registry
|
|||||||
class CodeLinter:
|
class CodeLinter:
|
||||||
"""Lints user code for syntax and style issues for 10 languages."""
|
"""Lints user code for syntax and style issues for 10 languages."""
|
||||||
|
|
||||||
def lint(self, language: str, code: str) -> Dict[str, Any]:
|
def lint(
|
||||||
|
self,
|
||||||
|
language: str,
|
||||||
|
code: str = "",
|
||||||
|
files: Dict[str, str] = None,
|
||||||
|
active_file: str = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
config = registry.get_config(language)
|
config = registry.get_config(language)
|
||||||
if not config:
|
if not config:
|
||||||
return {
|
return {
|
||||||
@@ -24,20 +30,30 @@ class CodeLinter:
|
|||||||
ext = config.get("ext", ".txt")
|
ext = config.get("ext", ".txt")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
if lang_key == "java":
|
if files and isinstance(files, dict) and len(files) > 0:
|
||||||
temp_file_name = "Main.java"
|
for rel_path, content in files.items():
|
||||||
elif lang_key == "cpp":
|
target_path = os.path.join(tmpdir, rel_path)
|
||||||
temp_file_name = "main.cpp"
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||||
elif lang_key == "rust":
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
temp_file_name = "main.rs"
|
f.write(content)
|
||||||
elif lang_key == "csharp":
|
temp_file_name = active_file if active_file and active_file in files else list(files.keys())[0]
|
||||||
temp_file_name = "Program.cs"
|
|
||||||
else:
|
else:
|
||||||
temp_file_name = f"main{ext}"
|
if lang_key == "java":
|
||||||
|
temp_file_name = "Main.java"
|
||||||
|
elif lang_key == "cpp":
|
||||||
|
temp_file_name = "main.cpp"
|
||||||
|
elif lang_key == "rust":
|
||||||
|
temp_file_name = "main.rs"
|
||||||
|
elif lang_key == "csharp":
|
||||||
|
temp_file_name = "Program.cs"
|
||||||
|
else:
|
||||||
|
temp_file_name = f"main{ext}"
|
||||||
|
|
||||||
|
temp_path = os.path.join(tmpdir, temp_file_name)
|
||||||
|
with open(temp_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
temp_path = os.path.join(tmpdir, temp_file_name)
|
temp_path = os.path.join(tmpdir, temp_file_name)
|
||||||
with open(temp_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(code)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if lang_key == "python":
|
if lang_key == "python":
|
||||||
|
|||||||
+24
-6
@@ -21,10 +21,17 @@ class MentorClient:
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
async def get_guidance_async(
|
async def get_guidance_async(
|
||||||
self, challenge: object, user_code: str, user_question: str = ""
|
self,
|
||||||
|
challenge: object,
|
||||||
|
user_code: str = "",
|
||||||
|
user_question: str = "",
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
|
active_file: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Fetch mentor guidance asynchronously from the configured LLM endpoint."""
|
"""Fetch mentor guidance asynchronously from the configured LLM endpoint."""
|
||||||
prompt = get_mentor_prompt(challenge, user_code, user_question=user_question)
|
prompt = get_mentor_prompt(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
endpoint = get_chat_completions_url(self.config.llm_base_url)
|
endpoint = get_chat_completions_url(self.config.llm_base_url)
|
||||||
|
|
||||||
user_content = (
|
user_content = (
|
||||||
@@ -92,7 +99,12 @@ class MentorClient:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_guidance(
|
def get_guidance(
|
||||||
self, challenge: object, user_code: str, user_question: str = ""
|
self,
|
||||||
|
challenge: object,
|
||||||
|
user_code: str = "",
|
||||||
|
user_question: str = "",
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
|
active_file: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Synchronous wrapper for fetching mentor guidance."""
|
"""Synchronous wrapper for fetching mentor guidance."""
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -103,15 +115,21 @@ class MentorClient:
|
|||||||
import nest_asyncio
|
import nest_asyncio
|
||||||
nest_asyncio.apply()
|
nest_asyncio.apply()
|
||||||
return loop.run_until_complete(
|
return loop.run_until_complete(
|
||||||
self.get_guidance_async(challenge, user_code, user_question=user_question)
|
self.get_guidance_async(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return asyncio.run(
|
return asyncio.run(
|
||||||
self.get_guidance_async(challenge, user_code, user_question=user_question)
|
self.get_guidance_async(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return asyncio.run(
|
return asyncio.run(
|
||||||
self.get_guidance_async(challenge, user_code, user_question=user_question)
|
self.get_guidance_async(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+80
-2
@@ -41,7 +41,81 @@ Your ultimate mission is to build the user's confidence in designing and constru
|
|||||||
SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR
|
SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR
|
||||||
|
|
||||||
|
|
||||||
def get_mentor_prompt(challenge: object, user_code: str, user_question: str = "") -> str:
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def compress_file_content(filename: str, content: str, max_lines: int = 30) -> str:
|
||||||
|
"""Compresses file content by extracting signatures and truncating bodies if context limit is reached."""
|
||||||
|
lines = content.splitlines()
|
||||||
|
if len(lines) <= max_lines:
|
||||||
|
return content
|
||||||
|
|
||||||
|
headers = []
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if (
|
||||||
|
stripped.startswith(("#include", "import ", "from ", "using ", "package "))
|
||||||
|
or stripped.startswith(("def ", "class ", "struct ", "fn ", "interface ", "enum ", "public ", "private "))
|
||||||
|
or stripped.endswith(("{", ":"))
|
||||||
|
):
|
||||||
|
headers.append(line)
|
||||||
|
|
||||||
|
if len(headers) >= 5:
|
||||||
|
return "\n".join(headers[:max_lines]) + f"\n... [{len(lines) - len(headers)} lines omitted]"
|
||||||
|
else:
|
||||||
|
return "\n".join(lines[:max_lines]) + f"\n... [{len(lines) - max_lines} lines omitted]"
|
||||||
|
|
||||||
|
|
||||||
|
def format_multifile_context(
|
||||||
|
user_code: str | Dict[str, str] = "",
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
|
active_file: Optional[str] = None,
|
||||||
|
max_chars: int = 12000,
|
||||||
|
) -> str:
|
||||||
|
"""Formats single or multi-file project code for the LLM mentor, applying context compression if needed."""
|
||||||
|
if isinstance(user_code, dict) and files is None:
|
||||||
|
files = user_code
|
||||||
|
user_code = ""
|
||||||
|
|
||||||
|
if not files or not isinstance(files, dict):
|
||||||
|
code_str = str(user_code or "")
|
||||||
|
return code_str if code_str.strip() else "# No code written yet"
|
||||||
|
|
||||||
|
active_name = active_file if active_file and active_file in files else list(files.keys())[0]
|
||||||
|
|
||||||
|
total_len = sum(len(c) for c in files.values())
|
||||||
|
should_compress = total_len > max_chars
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
file_list_summary = "Project Structure:\n" + "\n".join(
|
||||||
|
f"- {fname}{' (Active)' if fname == active_name else ''}" for fname in files.keys()
|
||||||
|
)
|
||||||
|
parts.append(file_list_summary)
|
||||||
|
|
||||||
|
# 1. Active file first (kept in full)
|
||||||
|
active_content = files[active_name]
|
||||||
|
parts.append(f"=== File: {active_name} (Active) ===\n{active_content}")
|
||||||
|
|
||||||
|
# 2. Inactive files (compressed if overall context is large)
|
||||||
|
for fname, content in files.items():
|
||||||
|
if fname == active_name:
|
||||||
|
continue
|
||||||
|
if should_compress and not fname.endswith((".h", ".hpp", ".d.ts")):
|
||||||
|
compressed = compress_file_content(fname, content)
|
||||||
|
parts.append(f"=== File: {fname} (Compressed) ===\n{compressed}")
|
||||||
|
else:
|
||||||
|
parts.append(f"=== File: {fname} ===\n{content}")
|
||||||
|
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mentor_prompt(
|
||||||
|
challenge: object,
|
||||||
|
user_code: str,
|
||||||
|
user_question: str = "",
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
|
active_file: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
"""Returns the formatted system prompt for the LLM mentor."""
|
"""Returns the formatted system prompt for the LLM mentor."""
|
||||||
reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", []))
|
reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", []))
|
||||||
hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", []))
|
hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", []))
|
||||||
@@ -52,6 +126,10 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = ""
|
|||||||
else "How should I structure my program and overcome my current blocker for this challenge?"
|
else "How should I structure my program and overcome my current blocker for this challenge?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
formatted_code = format_multifile_context(
|
||||||
|
user_code=user_code, files=files, active_file=active_file
|
||||||
|
)
|
||||||
|
|
||||||
return SYSTEM_PROMPT_MENTOR.format(
|
return SYSTEM_PROMPT_MENTOR.format(
|
||||||
challenge_name=getattr(challenge, "name", "Coding Challenge"),
|
challenge_name=getattr(challenge, "name", "Coding Challenge"),
|
||||||
language=getattr(challenge, "language", "Python"),
|
language=getattr(challenge, "language", "Python"),
|
||||||
@@ -59,7 +137,7 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = ""
|
|||||||
challenge_description=getattr(challenge, "description", ""),
|
challenge_description=getattr(challenge, "description", ""),
|
||||||
requirements=reqs if reqs else "- Follow standard problem specifications",
|
requirements=reqs if reqs else "- Follow standard problem specifications",
|
||||||
hints=hints if hints else "- Think through edge cases",
|
hints=hints if hints else "- Think through edge cases",
|
||||||
user_code=user_code if user_code.strip() else "# No code written yet",
|
user_code=formatted_code,
|
||||||
user_question=q_text,
|
user_question=q_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+772
-65
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
|||||||
|
"""Dynamic Theme Loader and Manager for TactiTerm TUI.
|
||||||
|
|
||||||
|
Discovers and loads JSON theme palette files from src/tui/themes/ and user custom directories.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
|
|
||||||
|
THEME_DIR = Path(__file__).parent / "themes"
|
||||||
|
USER_THEME_DIR = Path.home() / ".config" / "tactiterm" / "themes"
|
||||||
|
|
||||||
|
|
||||||
|
class ThemeManager:
|
||||||
|
"""Manages discovery, loading, and dynamic CSS generation for TactiTerm theme palettes."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.themes: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self.load_all_themes()
|
||||||
|
|
||||||
|
def load_all_themes(self) -> None:
|
||||||
|
"""Loads all JSON themes from default theme directory and user custom directory."""
|
||||||
|
self.themes.clear()
|
||||||
|
|
||||||
|
# 1. Load built-in themes
|
||||||
|
if THEME_DIR.exists():
|
||||||
|
for filepath in THEME_DIR.glob("*.json"):
|
||||||
|
self._load_theme_file(filepath)
|
||||||
|
|
||||||
|
# 2. Load custom user themes from ~/.config/tactiterm/themes/
|
||||||
|
if USER_THEME_DIR.exists():
|
||||||
|
for filepath in USER_THEME_DIR.glob("*.json"):
|
||||||
|
self._load_theme_file(filepath)
|
||||||
|
|
||||||
|
def _load_theme_file(self, filepath: Path) -> None:
|
||||||
|
try:
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
theme_id = data.get("id") or filepath.stem.lower()
|
||||||
|
theme_name = data.get("name", theme_id.capitalize())
|
||||||
|
self.themes[theme_id] = {
|
||||||
|
"id": theme_id,
|
||||||
|
"name": theme_name,
|
||||||
|
"background": data.get("background", "#1e1e1e"),
|
||||||
|
"surface": data.get("surface", "#252526"),
|
||||||
|
"primary": data.get("primary", "#3b82f6"),
|
||||||
|
"primary_text": data.get("primary_text", "#ffffff"),
|
||||||
|
"secondary": data.get("secondary", "#64748b"),
|
||||||
|
"accent": data.get("accent", "#38bdf8"),
|
||||||
|
"folder_header": data.get("folder_header", data.get("accent", "#38bdf8")),
|
||||||
|
"folder_text": data.get("folder_text", "#ffffff"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to load theme file {filepath}: {e}")
|
||||||
|
|
||||||
|
def get_theme_ids(self) -> List[str]:
|
||||||
|
"""Returns list of all loaded theme IDs."""
|
||||||
|
return list(self.themes.keys())
|
||||||
|
|
||||||
|
def get_all_themes(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Returns sorted list of all loaded theme dictionaries."""
|
||||||
|
return sorted(list(self.themes.values()), key=lambda x: x["name"])
|
||||||
|
|
||||||
|
def get_theme(self, theme_id: str) -> Dict[str, Any]:
|
||||||
|
"""Returns theme dict for theme_id or default."""
|
||||||
|
return self.themes.get(theme_id, self.themes.get("default", {}))
|
||||||
|
|
||||||
|
def register_all(self, app: Any) -> None:
|
||||||
|
"""Registers all loaded JSON themes into Textual app.register_theme."""
|
||||||
|
from textual.theme import Theme
|
||||||
|
for t_info in self.themes.values():
|
||||||
|
try:
|
||||||
|
theme_obj = Theme(
|
||||||
|
name=t_info["name"],
|
||||||
|
primary=t_info["primary"],
|
||||||
|
secondary=t_info.get("secondary"),
|
||||||
|
accent=t_info.get("accent"),
|
||||||
|
background=t_info["background"],
|
||||||
|
surface=t_info["surface"],
|
||||||
|
dark=True,
|
||||||
|
)
|
||||||
|
app.register_theme(theme_obj)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate_css(self) -> str:
|
||||||
|
"""Generates Textual CSS dynamically for all loaded themes."""
|
||||||
|
css_blocks = []
|
||||||
|
for theme_id, theme in self.themes.items():
|
||||||
|
bg = theme["background"]
|
||||||
|
surf = theme["surface"]
|
||||||
|
pri = theme["primary"]
|
||||||
|
pri_txt = theme["primary_text"]
|
||||||
|
sec = theme["secondary"]
|
||||||
|
acc = theme["accent"]
|
||||||
|
f_hdr = theme["folder_header"]
|
||||||
|
f_txt = theme["folder_text"]
|
||||||
|
|
||||||
|
block = f"""
|
||||||
|
/* Theme Palette: {theme["name"]} ({theme_id}) */
|
||||||
|
#main-container.theme-{theme_id} {{
|
||||||
|
background: {bg};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} #challenge-header,
|
||||||
|
#main-container.theme-{theme_id} #mentor-header,
|
||||||
|
#main-container.theme-{theme_id} #handbook-header,
|
||||||
|
#main-container.theme-{theme_id} #help-title,
|
||||||
|
#main-container.theme-{theme_id} #theme-title {{
|
||||||
|
background: {pri};
|
||||||
|
color: {pri_txt};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} #folder-header {{
|
||||||
|
background: {f_hdr};
|
||||||
|
color: {f_txt};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} CodeEditor {{
|
||||||
|
background: {surf};
|
||||||
|
color: {pri_txt};
|
||||||
|
border: solid {acc};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} CodeEditor:focus {{
|
||||||
|
border: heavy {pri};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} #challenge-details-container,
|
||||||
|
#main-container.theme-{theme_id} #output-scroll-container,
|
||||||
|
#main-container.theme-{theme_id} #mentor-response-scroll,
|
||||||
|
#main-container.theme-{theme_id} #handbook-example-scroll {{
|
||||||
|
background: {surf};
|
||||||
|
border: solid {sec};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} OptionList {{
|
||||||
|
background: {surf};
|
||||||
|
border: solid {sec};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} OptionList > .option-list--option-highlighted {{
|
||||||
|
background: {pri};
|
||||||
|
color: {pri_txt};
|
||||||
|
text-style: bold;
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} #status-bar {{
|
||||||
|
background: {pri};
|
||||||
|
color: {pri_txt};
|
||||||
|
}}
|
||||||
|
#main-container.theme-{theme_id} #theme-modal-popup,
|
||||||
|
#main-container.theme-{theme_id} #help-modal-popup {{
|
||||||
|
background: {bg};
|
||||||
|
border: heavy {pri};
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
css_blocks.append(block)
|
||||||
|
return "\n".join(css_blocks)
|
||||||
|
|
||||||
|
|
||||||
|
theme_manager = ThemeManager()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "adw",
|
||||||
|
"name": "ADW",
|
||||||
|
"background": "#1e1e1e",
|
||||||
|
"surface": "#242424",
|
||||||
|
"primary": "#3584e4",
|
||||||
|
"primary_text": "#ffffff",
|
||||||
|
"secondary": "#1b467c",
|
||||||
|
"accent": "#ffffff",
|
||||||
|
"folder_header": "#ffffff",
|
||||||
|
"folder_text": "#2e3436"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "atuel",
|
||||||
|
"name": "Atuel",
|
||||||
|
"background": "#0F1B40",
|
||||||
|
"surface": "#0A1126",
|
||||||
|
"primary": "#99B6F2",
|
||||||
|
"primary_text": "#0A1126",
|
||||||
|
"secondary": "#5581D9",
|
||||||
|
"accent": "#4E6BA6",
|
||||||
|
"folder_header": "#4E6BA6",
|
||||||
|
"folder_text": "#d9e2f7"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "ayu-blue",
|
||||||
|
"name": "Ayu Blue",
|
||||||
|
"background": "#1E222A",
|
||||||
|
"surface": "#0B0E14",
|
||||||
|
"primary": "#39BAE6",
|
||||||
|
"primary_text": "#0B0E14",
|
||||||
|
"secondary": "#AAD94C",
|
||||||
|
"accent": "#E6B450",
|
||||||
|
"folder_header": "#E6B450",
|
||||||
|
"folder_text": "#0B0E14"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "ayu-green",
|
||||||
|
"name": "Ayu Green",
|
||||||
|
"background": "#1E222A",
|
||||||
|
"surface": "#0B0E14",
|
||||||
|
"primary": "#AAD94C",
|
||||||
|
"primary_text": "#0B0E14",
|
||||||
|
"secondary": "#E6B450",
|
||||||
|
"accent": "#39BAE6",
|
||||||
|
"folder_header": "#39BAE6",
|
||||||
|
"folder_text": "#0B0E14"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "ayu-red",
|
||||||
|
"name": "Ayu Red",
|
||||||
|
"background": "#1E222A",
|
||||||
|
"surface": "#0B0E14",
|
||||||
|
"primary": "#D95757",
|
||||||
|
"primary_text": "#0B0E14",
|
||||||
|
"secondary": "#E6B450",
|
||||||
|
"accent": "#39BAE6",
|
||||||
|
"folder_header": "#39BAE6",
|
||||||
|
"folder_text": "#0B0E14"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "breeze",
|
||||||
|
"name": "Breeze",
|
||||||
|
"background": "#292d32",
|
||||||
|
"surface": "#202224",
|
||||||
|
"primary": "#3daee9",
|
||||||
|
"primary_text": "#141618",
|
||||||
|
"secondary": "#1d99f3",
|
||||||
|
"accent": "#9b59b6",
|
||||||
|
"folder_header": "#9b59b6",
|
||||||
|
"folder_text": "#141618"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-frappe-blue",
|
||||||
|
"name": "Catppuccin Frappe Blue",
|
||||||
|
"background": "#414559",
|
||||||
|
"surface": "#303446",
|
||||||
|
"primary": "#8caaee",
|
||||||
|
"primary_text": "#303446",
|
||||||
|
"secondary": "#babbf1",
|
||||||
|
"accent": "#ca9ee6",
|
||||||
|
"folder_header": "#ca9ee6",
|
||||||
|
"folder_text": "#303446"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-frappe-lavender",
|
||||||
|
"name": "Catppuccin Frappe Lavender",
|
||||||
|
"background": "#414559",
|
||||||
|
"surface": "#303446",
|
||||||
|
"primary": "#babbf1",
|
||||||
|
"primary_text": "#303446",
|
||||||
|
"secondary": "#8caaee",
|
||||||
|
"accent": "#ca9ee6",
|
||||||
|
"folder_header": "#ca9ee6",
|
||||||
|
"folder_text": "#303446"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-frappe-mauve",
|
||||||
|
"name": "Catppuccin Frappe Mauve",
|
||||||
|
"background": "#414559",
|
||||||
|
"surface": "#303446",
|
||||||
|
"primary": "#ca9ee6",
|
||||||
|
"primary_text": "#303446",
|
||||||
|
"secondary": "#babbf1",
|
||||||
|
"accent": "#99d1db",
|
||||||
|
"folder_header": "#99d1db",
|
||||||
|
"folder_text": "#303446"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-frappe-pink",
|
||||||
|
"name": "Catppuccin Frappe Pink",
|
||||||
|
"background": "#414559",
|
||||||
|
"surface": "#303446",
|
||||||
|
"primary": "#f4b8e4",
|
||||||
|
"primary_text": "#303446",
|
||||||
|
"secondary": "#ea999c",
|
||||||
|
"accent": "#99d1db",
|
||||||
|
"folder_header": "#99d1db",
|
||||||
|
"folder_text": "#303446"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-frappe-rosewater",
|
||||||
|
"name": "Catppuccin Frappe Rosewater",
|
||||||
|
"background": "#414559",
|
||||||
|
"surface": "#303446",
|
||||||
|
"primary": "#f2d5cf",
|
||||||
|
"primary_text": "#303446",
|
||||||
|
"secondary": "#eebebe",
|
||||||
|
"accent": "#e5c890",
|
||||||
|
"folder_header": "#e5c890",
|
||||||
|
"folder_text": "#303446"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-frappe-sapphirejson",
|
||||||
|
"name": "Catppuccin Frappe Sapphire.json",
|
||||||
|
"background": "#414559",
|
||||||
|
"surface": "#303446",
|
||||||
|
"primary": "#85c1dc",
|
||||||
|
"primary_text": "#303446",
|
||||||
|
"secondary": "#8caaee",
|
||||||
|
"accent": "#99d1db",
|
||||||
|
"folder_header": "#99d1db",
|
||||||
|
"folder_text": "#303446"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-lavender",
|
||||||
|
"name": "Catppuccin Lavender",
|
||||||
|
"background": "#313244",
|
||||||
|
"surface": "#1e1e2e",
|
||||||
|
"primary": "#b4befe",
|
||||||
|
"primary_text": "#11111b",
|
||||||
|
"secondary": "#f5bde6",
|
||||||
|
"accent": "#c6a0f6",
|
||||||
|
"folder_header": "#c6a0f6",
|
||||||
|
"folder_text": "#11111b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-macchiato-lavender",
|
||||||
|
"name": "Catppuccin Macchiato Lavender",
|
||||||
|
"background": "#363a4f",
|
||||||
|
"surface": "#24273a",
|
||||||
|
"primary": "#b7bdf8",
|
||||||
|
"primary_text": "#181926",
|
||||||
|
"secondary": "#8aadf4",
|
||||||
|
"accent": "#c6a0f6",
|
||||||
|
"folder_header": "#c6a0f6",
|
||||||
|
"folder_text": "#181926"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-macchiato-mauve",
|
||||||
|
"name": "Catppuccin Macchiato Mauve",
|
||||||
|
"background": "#363a4f",
|
||||||
|
"surface": "#24273a",
|
||||||
|
"primary": "#c6a0f6",
|
||||||
|
"primary_text": "#181926",
|
||||||
|
"secondary": "#f5a97f",
|
||||||
|
"accent": "#8bd5ca",
|
||||||
|
"folder_header": "#8bd5ca",
|
||||||
|
"folder_text": "#181926"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-macchiato-pink",
|
||||||
|
"name": "Catppuccin Macchiato Pink",
|
||||||
|
"background": "#363a4f",
|
||||||
|
"surface": "#24273a",
|
||||||
|
"primary": "#f5bde6",
|
||||||
|
"primary_text": "#181926",
|
||||||
|
"secondary": "#f5a97f",
|
||||||
|
"accent": "#8bd5ca",
|
||||||
|
"folder_header": "#8bd5ca",
|
||||||
|
"folder_text": "#181926"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-macchiato-rosewater",
|
||||||
|
"name": "Catppuccin Macchiato Rosewater",
|
||||||
|
"background": "#363a4f",
|
||||||
|
"surface": "#24273a",
|
||||||
|
"primary": "#f4dbd6",
|
||||||
|
"primary_text": "#181926",
|
||||||
|
"secondary": "#f0c6c6",
|
||||||
|
"accent": "#e5c890",
|
||||||
|
"folder_header": "#e5c890",
|
||||||
|
"folder_text": "#181926"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-macchiato-sapphire",
|
||||||
|
"name": "Catppuccin Macchiato Sapphire",
|
||||||
|
"background": "#363a4f",
|
||||||
|
"surface": "#24273a",
|
||||||
|
"primary": "#7dc4e4",
|
||||||
|
"primary_text": "#181926",
|
||||||
|
"secondary": "#8aadf4",
|
||||||
|
"accent": "#91d7e3",
|
||||||
|
"folder_header": "#91d7e3",
|
||||||
|
"folder_text": "#181926"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-mocha-pink",
|
||||||
|
"name": "Catppuccin Mocha Pink",
|
||||||
|
"background": "#313244",
|
||||||
|
"surface": "#1e1e2e",
|
||||||
|
"primary": "#f5c2e7",
|
||||||
|
"primary_text": "#11111b",
|
||||||
|
"secondary": "#fab387",
|
||||||
|
"accent": "#94e2d5",
|
||||||
|
"folder_header": "#94e2d5",
|
||||||
|
"folder_text": "#11111b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-mocha-rosewater",
|
||||||
|
"name": "Catppuccin Mocha Rosewater",
|
||||||
|
"background": "#313244",
|
||||||
|
"surface": "#1e1e2e",
|
||||||
|
"primary": "#f5e0dc",
|
||||||
|
"primary_text": "#11111b",
|
||||||
|
"secondary": "#f2cdcd",
|
||||||
|
"accent": "#f9e2af",
|
||||||
|
"folder_header": "#f9e2af",
|
||||||
|
"folder_text": "#11111b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin-mocha-sapphire",
|
||||||
|
"name": "Catppuccin Mocha Sapphire",
|
||||||
|
"background": "#313244",
|
||||||
|
"surface": "#1e1e2e",
|
||||||
|
"primary": "#74c7ec",
|
||||||
|
"primary_text": "#11111b",
|
||||||
|
"secondary": "#89b4fa",
|
||||||
|
"accent": "#89dceb",
|
||||||
|
"folder_header": "#89dceb",
|
||||||
|
"folder_text": "#11111b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "catppuccin",
|
||||||
|
"name": "Catppuccin Mocha",
|
||||||
|
"background": "#1e1e2e",
|
||||||
|
"surface": "#313244",
|
||||||
|
"primary": "#cba6f7",
|
||||||
|
"primary_text": "#11111b",
|
||||||
|
"secondary": "#89b4fa",
|
||||||
|
"accent": "#f5c2e7",
|
||||||
|
"folder_header": "#f5c2e7",
|
||||||
|
"folder_text": "#11111b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "cherry-blossom",
|
||||||
|
"name": "Cherry Blossom",
|
||||||
|
"background": "#4D3745",
|
||||||
|
"surface": "#2A1922",
|
||||||
|
"primary": "#F2C1D4",
|
||||||
|
"primary_text": "#2A1B21",
|
||||||
|
"secondary": "#FFD6E2",
|
||||||
|
"accent": "#D4A3BD",
|
||||||
|
"folder_header": "#D4A3BD",
|
||||||
|
"folder_text": "#2A1B21"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "cream-autumn",
|
||||||
|
"name": "Cream Autumn",
|
||||||
|
"background": "#231e1a",
|
||||||
|
"surface": "#161311",
|
||||||
|
"primary": "#e5c799",
|
||||||
|
"primary_text": "#161311",
|
||||||
|
"secondary": "#cbaba0",
|
||||||
|
"accent": "#9aa887",
|
||||||
|
"folder_header": "#9aa887",
|
||||||
|
"folder_text": "#161311"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "cream",
|
||||||
|
"name": "Cream",
|
||||||
|
"background": "#26211e",
|
||||||
|
"surface": "#161311",
|
||||||
|
"primary": "#a2b574",
|
||||||
|
"primary_text": "#161311",
|
||||||
|
"secondary": "#dfb26c",
|
||||||
|
"accent": "#e09260",
|
||||||
|
"folder_header": "#e09260",
|
||||||
|
"folder_text": "#161311"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "creamy-forest",
|
||||||
|
"name": "Creamy Forest",
|
||||||
|
"background": "#252722",
|
||||||
|
"surface": "#1c1e1a",
|
||||||
|
"primary": "#a2b08d",
|
||||||
|
"primary_text": "#131411",
|
||||||
|
"secondary": "#948e74",
|
||||||
|
"accent": "#6e857b",
|
||||||
|
"folder_header": "#6e857b",
|
||||||
|
"folder_text": "#f4f6eb"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "cyberpunk",
|
||||||
|
"name": "Cyberpunk",
|
||||||
|
"background": "#11151D",
|
||||||
|
"surface": "#0C1017",
|
||||||
|
"primary": "#C4A82E",
|
||||||
|
"primary_text": "#0E1015",
|
||||||
|
"secondary": "#D14358",
|
||||||
|
"accent": "#00A66C",
|
||||||
|
"folder_header": "#00A66C",
|
||||||
|
"folder_text": "#0E1015"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"name": "TactiTerm Dark (Default)",
|
||||||
|
"background": "#1e1e1e",
|
||||||
|
"surface": "#252526",
|
||||||
|
"primary": "#3b82f6",
|
||||||
|
"primary_text": "#ffffff",
|
||||||
|
"secondary": "#64748b",
|
||||||
|
"accent": "#38bdf8",
|
||||||
|
"folder_header": "#38bdf8",
|
||||||
|
"folder_text": "#ffffff"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "doomed",
|
||||||
|
"name": "Doomed",
|
||||||
|
"background": "#282c34",
|
||||||
|
"surface": "#1c1e1e",
|
||||||
|
"primary": "#51afef",
|
||||||
|
"primary_text": "#1c1e1e",
|
||||||
|
"secondary": "#f2c481",
|
||||||
|
"accent": "#98be65",
|
||||||
|
"folder_header": "#98be65",
|
||||||
|
"folder_text": "#1c1e1e"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "dracula",
|
||||||
|
"name": "Dracula",
|
||||||
|
"background": "#282a36",
|
||||||
|
"surface": "#44475a",
|
||||||
|
"primary": "#bd93f9",
|
||||||
|
"primary_text": "#282a36",
|
||||||
|
"secondary": "#6272a4",
|
||||||
|
"accent": "#8be9fd",
|
||||||
|
"folder_header": "#ff79c6",
|
||||||
|
"folder_text": "#282a36"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "espresso-cream",
|
||||||
|
"name": "Espresso Cream",
|
||||||
|
"background": "#2c1e13",
|
||||||
|
"surface": "#1a120b",
|
||||||
|
"primary": "#e29578",
|
||||||
|
"primary_text": "#1a120b",
|
||||||
|
"secondary": "#ddb892",
|
||||||
|
"accent": "#a3b19b",
|
||||||
|
"folder_header": "#a3b19b",
|
||||||
|
"folder_text": "#1a120b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "everdeer",
|
||||||
|
"name": "Everdeer",
|
||||||
|
"background": "#291d1a",
|
||||||
|
"surface": "#1c110e",
|
||||||
|
"primary": "#ffb4a3",
|
||||||
|
"primary_text": "#621000",
|
||||||
|
"secondary": "#ffb4a3",
|
||||||
|
"accent": "#e9c258",
|
||||||
|
"folder_header": "#e9c258",
|
||||||
|
"folder_text": "#3d2e00"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "everforest-alt",
|
||||||
|
"name": "Everforest Alt",
|
||||||
|
"background": "#3d484d",
|
||||||
|
"surface": "#2d353b",
|
||||||
|
"primary": "#a7c080",
|
||||||
|
"primary_text": "#2d353b",
|
||||||
|
"secondary": "#7fbbb3",
|
||||||
|
"accent": "#e69875",
|
||||||
|
"folder_header": "#e69875",
|
||||||
|
"folder_text": "#2d353b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "everforest-material",
|
||||||
|
"name": "Everforest Material",
|
||||||
|
"background": "#202020",
|
||||||
|
"surface": "#161616",
|
||||||
|
"primary": "#A7C080",
|
||||||
|
"primary_text": "#161616",
|
||||||
|
"secondary": "#DBBC7F",
|
||||||
|
"accent": "#7FBBB3",
|
||||||
|
"folder_header": "#7FBBB3",
|
||||||
|
"folder_text": "#161616"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "everforest",
|
||||||
|
"name": "Everforest",
|
||||||
|
"background": "#2D353B",
|
||||||
|
"surface": "#232A2E",
|
||||||
|
"primary": "#A7C080",
|
||||||
|
"primary_text": "#232A2E",
|
||||||
|
"secondary": "#D3C6AA",
|
||||||
|
"accent": "#9DA9A0",
|
||||||
|
"folder_header": "#9DA9A0",
|
||||||
|
"folder_text": "#232A2E"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "flexoki",
|
||||||
|
"name": "Flexoki",
|
||||||
|
"background": "#1c1b1a",
|
||||||
|
"surface": "#100f0f",
|
||||||
|
"primary": "#879a39",
|
||||||
|
"primary_text": "#1a1e0c",
|
||||||
|
"secondary": "#4385be",
|
||||||
|
"accent": "#8b7ec8",
|
||||||
|
"folder_header": "#8b7ec8",
|
||||||
|
"folder_text": "#1a1623"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "garnet",
|
||||||
|
"name": "Garnet",
|
||||||
|
"background": "#181616",
|
||||||
|
"surface": "#121212",
|
||||||
|
"primary": "#990000",
|
||||||
|
"primary_text": "#f5eeee",
|
||||||
|
"secondary": "#c8962a",
|
||||||
|
"accent": "#7a0000",
|
||||||
|
"folder_header": "#7a0000",
|
||||||
|
"folder_text": "#f0e8e8"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "github-dark",
|
||||||
|
"name": "GitHub Dark",
|
||||||
|
"background": "#161b22",
|
||||||
|
"surface": "#010409",
|
||||||
|
"primary": "#58a6ff",
|
||||||
|
"primary_text": "#010409",
|
||||||
|
"secondary": "#bc8cff",
|
||||||
|
"accent": "#bc8cff",
|
||||||
|
"folder_header": "#bc8cff",
|
||||||
|
"folder_text": "#010409"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "gruber-darker",
|
||||||
|
"name": "Gruber Darker",
|
||||||
|
"background": "#282828",
|
||||||
|
"surface": "#181818",
|
||||||
|
"primary": "#ffdd33",
|
||||||
|
"primary_text": "#181818",
|
||||||
|
"secondary": "#96a6c8",
|
||||||
|
"accent": "#9e95c7",
|
||||||
|
"folder_header": "#9e95c7",
|
||||||
|
"folder_text": "#101010"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "gruvbox-material",
|
||||||
|
"name": "Gruvbox Material",
|
||||||
|
"background": "#3c3836",
|
||||||
|
"surface": "#32302f",
|
||||||
|
"primary": "#a9b665",
|
||||||
|
"primary_text": "#32302f",
|
||||||
|
"secondary": "#e78a4e",
|
||||||
|
"accent": "#89b482",
|
||||||
|
"folder_header": "#89b482",
|
||||||
|
"folder_text": "#32302f"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "gruvbox",
|
||||||
|
"name": "Gruvbox Dark",
|
||||||
|
"background": "#282828",
|
||||||
|
"surface": "#3c3836",
|
||||||
|
"primary": "#fabd2f",
|
||||||
|
"primary_text": "#282828",
|
||||||
|
"secondary": "#b8bb26",
|
||||||
|
"accent": "#fe8019",
|
||||||
|
"folder_header": "#fe8019",
|
||||||
|
"folder_text": "#282828"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "gruvboxalt",
|
||||||
|
"name": "GruvboxAlt",
|
||||||
|
"background": "#3c3836",
|
||||||
|
"surface": "#282828",
|
||||||
|
"primary": "#ebdbb2",
|
||||||
|
"primary_text": "#282828",
|
||||||
|
"secondary": "#8ec07c",
|
||||||
|
"accent": "#83a598",
|
||||||
|
"folder_header": "#83a598",
|
||||||
|
"folder_text": "#282828"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "hexa34c",
|
||||||
|
"name": "Hexa34C",
|
||||||
|
"background": "#101510",
|
||||||
|
"surface": "#101510",
|
||||||
|
"primary": "#9ad4a1",
|
||||||
|
"primary_text": "#003916",
|
||||||
|
"secondary": "#b7ccb6",
|
||||||
|
"accent": "#a1ced8",
|
||||||
|
"folder_header": "#a1ced8",
|
||||||
|
"folder_text": "#00363e"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "horizon",
|
||||||
|
"name": "Horizon",
|
||||||
|
"background": "#232530",
|
||||||
|
"surface": "#1A1C23",
|
||||||
|
"primary": "#25B2BC",
|
||||||
|
"primary_text": "#16161C",
|
||||||
|
"secondary": "#B877DB",
|
||||||
|
"accent": "#FAB795",
|
||||||
|
"folder_header": "#FAB795",
|
||||||
|
"folder_text": "#16161C"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "jiva-lotus",
|
||||||
|
"name": "Jiva Lotus",
|
||||||
|
"background": "#2c2e33",
|
||||||
|
"surface": "#212226",
|
||||||
|
"primary": "#debfbf",
|
||||||
|
"primary_text": "#262626",
|
||||||
|
"secondary": "#debfbf",
|
||||||
|
"accent": "#ebcece",
|
||||||
|
"folder_header": "#ebcece",
|
||||||
|
"folder_text": "#262626"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "jiva-paper",
|
||||||
|
"name": "Jiva Paper",
|
||||||
|
"background": "#2c2e33",
|
||||||
|
"surface": "#212226",
|
||||||
|
"primary": "#bfc0de",
|
||||||
|
"primary_text": "#262626",
|
||||||
|
"secondary": "#bfc0de",
|
||||||
|
"accent": "#cecfeb",
|
||||||
|
"folder_header": "#cecfeb",
|
||||||
|
"folder_text": "#262626"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "jiva",
|
||||||
|
"name": "Jiva",
|
||||||
|
"background": "#3D3B43",
|
||||||
|
"surface": "#2D2B33",
|
||||||
|
"primary": "#EDDD8E",
|
||||||
|
"primary_text": "#303030",
|
||||||
|
"secondary": "#EDDD8E",
|
||||||
|
"accent": "#f7e99e",
|
||||||
|
"folder_header": "#f7e99e",
|
||||||
|
"folder_text": "#303030"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "kanagawa-dragon",
|
||||||
|
"name": "Kanagawa Dragon",
|
||||||
|
"background": "#282727",
|
||||||
|
"surface": "#181616",
|
||||||
|
"primary": "#8a9a7b",
|
||||||
|
"primary_text": "#181616",
|
||||||
|
"secondary": "#8ea4a2",
|
||||||
|
"accent": "#c4746e",
|
||||||
|
"folder_header": "#c4746e",
|
||||||
|
"folder_text": "#181616"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "kanagawa-kasumi",
|
||||||
|
"name": "Kanagawa Kasumi",
|
||||||
|
"background": "#20272E",
|
||||||
|
"surface": "#1A2026",
|
||||||
|
"primary": "#D9A78B",
|
||||||
|
"primary_text": "#1A2026",
|
||||||
|
"secondary": "#7794A6",
|
||||||
|
"accent": "#8BA37A",
|
||||||
|
"folder_header": "#8BA37A",
|
||||||
|
"folder_text": "#1A2026"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "kanagawa-paper",
|
||||||
|
"name": "Kanagawa Paper",
|
||||||
|
"background": "#2A2A37",
|
||||||
|
"surface": "#1F1F28",
|
||||||
|
"primary": "#c4b28a",
|
||||||
|
"primary_text": "#1F1F28",
|
||||||
|
"secondary": "#8ea49e",
|
||||||
|
"accent": "#938AA9",
|
||||||
|
"folder_header": "#938AA9",
|
||||||
|
"folder_text": "#1F1F28"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "kemuri-koke",
|
||||||
|
"name": "Kemuri Koke",
|
||||||
|
"background": "#36312f",
|
||||||
|
"surface": "#242120",
|
||||||
|
"primary": "#8ba37e",
|
||||||
|
"primary_text": "#242120",
|
||||||
|
"secondary": "#a69680",
|
||||||
|
"accent": "#cb9168",
|
||||||
|
"folder_header": "#cb9168",
|
||||||
|
"folder_text": "#242120"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "kemuri-susu",
|
||||||
|
"name": "Kemuri Susu",
|
||||||
|
"background": "#282624",
|
||||||
|
"surface": "#1e1d1b",
|
||||||
|
"primary": "#cabaaa",
|
||||||
|
"primary_text": "#1e1d1b",
|
||||||
|
"secondary": "#73685F",
|
||||||
|
"accent": "#594F46",
|
||||||
|
"folder_header": "#594F46",
|
||||||
|
"folder_text": "#f0ede6"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "kemuri",
|
||||||
|
"name": "Kemuri",
|
||||||
|
"background": "#36312f",
|
||||||
|
"surface": "#242120",
|
||||||
|
"primary": "#cb9168",
|
||||||
|
"primary_text": "#242120",
|
||||||
|
"secondary": "#a69680",
|
||||||
|
"accent": "#8da388",
|
||||||
|
"folder_header": "#8da388",
|
||||||
|
"folder_text": "#242120"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "lilac-amoled",
|
||||||
|
"name": "Lilac AMOLED",
|
||||||
|
"background": "#110d1a",
|
||||||
|
"surface": "#000000",
|
||||||
|
"primary": "#b58fff",
|
||||||
|
"primary_text": "#000000",
|
||||||
|
"secondary": "#c79aff",
|
||||||
|
"accent": "#d8b4ff",
|
||||||
|
"folder_header": "#d8b4ff",
|
||||||
|
"folder_text": "#000000"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "macaron",
|
||||||
|
"name": "Macaron",
|
||||||
|
"background": "#242426",
|
||||||
|
"surface": "#18181A",
|
||||||
|
"primary": "#E5B4E2",
|
||||||
|
"primary_text": "#18181A",
|
||||||
|
"secondary": "#B4E5E5",
|
||||||
|
"accent": "#E5D0B4",
|
||||||
|
"folder_header": "#E5D0B4",
|
||||||
|
"folder_text": "#18181A"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "matecito",
|
||||||
|
"name": "Matecito",
|
||||||
|
"background": "#272e2a",
|
||||||
|
"surface": "#1e2320",
|
||||||
|
"primary": "#8da383",
|
||||||
|
"primary_text": "#1e2320",
|
||||||
|
"secondary": "#cb9b7c",
|
||||||
|
"accent": "#7f9193",
|
||||||
|
"folder_header": "#7f9193",
|
||||||
|
"folder_text": "#1e2320"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "miasma",
|
||||||
|
"name": "Miasma",
|
||||||
|
"background": "#2a2a2a",
|
||||||
|
"surface": "#222222",
|
||||||
|
"primary": "#c9a554",
|
||||||
|
"primary_text": "#222222",
|
||||||
|
"secondary": "#b36d43",
|
||||||
|
"accent": "#bb7744",
|
||||||
|
"folder_header": "#bb7744",
|
||||||
|
"folder_text": "#222222"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "mine",
|
||||||
|
"name": "Mine",
|
||||||
|
"background": "#262427",
|
||||||
|
"surface": "#1d1b1d",
|
||||||
|
"primary": "#e1e2d5",
|
||||||
|
"primary_text": "#1d1b1d",
|
||||||
|
"secondary": "#2d2a2e",
|
||||||
|
"accent": "#2d2a2e",
|
||||||
|
"folder_header": "#2d2a2e",
|
||||||
|
"folder_text": "#e1e2d5"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "mizuki-akiyama",
|
||||||
|
"name": "Mizuki-Akiyama",
|
||||||
|
"background": "#1d1a2a",
|
||||||
|
"surface": "#10111b",
|
||||||
|
"primary": "#e6a6c8",
|
||||||
|
"primary_text": "#2b1422",
|
||||||
|
"secondary": "#afa2d8",
|
||||||
|
"accent": "#7fb6d6",
|
||||||
|
"folder_header": "#7fb6d6",
|
||||||
|
"folder_text": "#071f2d"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "monochrome",
|
||||||
|
"name": "Monochrome",
|
||||||
|
"background": "#191919",
|
||||||
|
"surface": "#111111",
|
||||||
|
"primary": "#aaaaaa",
|
||||||
|
"primary_text": "#111111",
|
||||||
|
"secondary": "#a7a7a7",
|
||||||
|
"accent": "#cccccc",
|
||||||
|
"folder_header": "#cccccc",
|
||||||
|
"folder_text": "#111111"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "murasaki",
|
||||||
|
"name": "Murasaki",
|
||||||
|
"background": "#50066f",
|
||||||
|
"surface": "#300443",
|
||||||
|
"primary": "#e8bcfb",
|
||||||
|
"primary_text": "#8e0cc6",
|
||||||
|
"secondary": "#8e0cc6",
|
||||||
|
"accent": "#066f50",
|
||||||
|
"folder_header": "#066f50",
|
||||||
|
"folder_text": "#90f9d9"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "murata",
|
||||||
|
"name": "Murata",
|
||||||
|
"background": "#252627",
|
||||||
|
"surface": "#1a1b1c",
|
||||||
|
"primary": "#db6d6d",
|
||||||
|
"primary_text": "#1a1b1c",
|
||||||
|
"secondary": "#d1b394",
|
||||||
|
"accent": "#8faeb1",
|
||||||
|
"folder_header": "#8faeb1",
|
||||||
|
"folder_text": "#1a1b1c"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "naysayer",
|
||||||
|
"name": "NaySayer",
|
||||||
|
"background": "#0b3335",
|
||||||
|
"surface": "#062329",
|
||||||
|
"primary": "#2ec09c",
|
||||||
|
"primary_text": "#062329",
|
||||||
|
"secondary": "#8cde94",
|
||||||
|
"accent": "#7ad0c6",
|
||||||
|
"folder_header": "#7ad0c6",
|
||||||
|
"folder_text": "#d1b897"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "neon-surf",
|
||||||
|
"name": "Neon Surf",
|
||||||
|
"background": "#06101A",
|
||||||
|
"surface": "#000000",
|
||||||
|
"primary": "#00a8f4",
|
||||||
|
"primary_text": "#000000",
|
||||||
|
"secondary": "#22d3ee",
|
||||||
|
"accent": "#0ea5e9",
|
||||||
|
"folder_header": "#0ea5e9",
|
||||||
|
"folder_text": "#000000"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "noctalia-legacy",
|
||||||
|
"name": "Noctalia legacy",
|
||||||
|
"background": "#262130",
|
||||||
|
"surface": "#1c1822",
|
||||||
|
"primary": "#c7a1d8",
|
||||||
|
"primary_text": "#1a151f",
|
||||||
|
"secondary": "#a984c4",
|
||||||
|
"accent": "#e0b7c9",
|
||||||
|
"folder_header": "#e0b7c9",
|
||||||
|
"folder_text": "#20161f"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "nord-aurora",
|
||||||
|
"name": "Nord Aurora",
|
||||||
|
"background": "#3b4252",
|
||||||
|
"surface": "#2e3440",
|
||||||
|
"primary": "#b48ead",
|
||||||
|
"primary_text": "#2e3440",
|
||||||
|
"secondary": "#a3be8c",
|
||||||
|
"accent": "#ebcb8b",
|
||||||
|
"folder_header": "#ebcb8b",
|
||||||
|
"folder_text": "#2e3440"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "nord",
|
||||||
|
"name": "Nord Arctic",
|
||||||
|
"background": "#2e3440",
|
||||||
|
"surface": "#3b4252",
|
||||||
|
"primary": "#88c0d0",
|
||||||
|
"primary_text": "#2e3440",
|
||||||
|
"secondary": "#81a1c1",
|
||||||
|
"accent": "#ebcb8b",
|
||||||
|
"folder_header": "#ebcb8b",
|
||||||
|
"folder_text": "#2e3440"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "oasis-abyss",
|
||||||
|
"name": "Oasis Abyss",
|
||||||
|
"background": "#080808",
|
||||||
|
"surface": "#1A1A1A",
|
||||||
|
"primary": "#D06666",
|
||||||
|
"primary_text": "#1A1A1A",
|
||||||
|
"secondary": "#FFA247",
|
||||||
|
"accent": "#F0E68C",
|
||||||
|
"folder_header": "#F0E68C",
|
||||||
|
"folder_text": "#1A1A1A"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "occult-umbral",
|
||||||
|
"name": "Occult Umbral",
|
||||||
|
"background": "#14141E",
|
||||||
|
"surface": "#0A0A12",
|
||||||
|
"primary": "#8B2E2E",
|
||||||
|
"primary_text": "#1C1C28",
|
||||||
|
"secondary": "#8BAA82",
|
||||||
|
"accent": "#9A7398",
|
||||||
|
"folder_header": "#9A7398",
|
||||||
|
"folder_text": "#0A0A12"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "one-dark-two",
|
||||||
|
"name": "One Dark Two",
|
||||||
|
"background": "#282C34",
|
||||||
|
"surface": "#21252B",
|
||||||
|
"primary": "#62BAC6",
|
||||||
|
"primary_text": "#21252B",
|
||||||
|
"secondary": "#EAC786",
|
||||||
|
"accent": "#98C379",
|
||||||
|
"folder_header": "#98C379",
|
||||||
|
"folder_text": "#21252B"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "One",
|
||||||
|
"background": "#282c34",
|
||||||
|
"surface": "#1e2127",
|
||||||
|
"primary": "#61afef",
|
||||||
|
"primary_text": "#1e2127",
|
||||||
|
"secondary": "#c678dd",
|
||||||
|
"accent": "#98c379",
|
||||||
|
"folder_header": "#98c379",
|
||||||
|
"folder_text": "#1e2127"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "osaka-jade",
|
||||||
|
"name": "Osaka jade",
|
||||||
|
"background": "#0F251F",
|
||||||
|
"surface": "#081512",
|
||||||
|
"primary": "#1E9177",
|
||||||
|
"primary_text": "#B8C8C4",
|
||||||
|
"secondary": "#167A63",
|
||||||
|
"accent": "#26A589",
|
||||||
|
"folder_header": "#26A589",
|
||||||
|
"folder_text": "#B8C8C4"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "oxide",
|
||||||
|
"name": "Oxide",
|
||||||
|
"background": "#2E2623",
|
||||||
|
"surface": "#231D1B",
|
||||||
|
"primary": "#B85A30",
|
||||||
|
"primary_text": "#1D1816",
|
||||||
|
"secondary": "#8B9A5A",
|
||||||
|
"accent": "#D8A657",
|
||||||
|
"folder_header": "#D8A657",
|
||||||
|
"folder_text": "#1D1816"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "oxocarbon",
|
||||||
|
"name": "Oxocarbon",
|
||||||
|
"background": "#262626",
|
||||||
|
"surface": "#161616",
|
||||||
|
"primary": "#33b1ff",
|
||||||
|
"primary_text": "#161616",
|
||||||
|
"secondary": "#42be65",
|
||||||
|
"accent": "#be95ff",
|
||||||
|
"folder_header": "#be95ff",
|
||||||
|
"folder_text": "#161616"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "paradise",
|
||||||
|
"name": "Paradise",
|
||||||
|
"background": "#222222",
|
||||||
|
"surface": "#151515",
|
||||||
|
"primary": "#8da3b9",
|
||||||
|
"primary_text": "#151515",
|
||||||
|
"secondary": "#8aa6a2",
|
||||||
|
"accent": "#a988b0",
|
||||||
|
"folder_header": "#a988b0",
|
||||||
|
"folder_text": "#151515"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "peche",
|
||||||
|
"name": "Peche",
|
||||||
|
"background": "#192024",
|
||||||
|
"surface": "#0e1214",
|
||||||
|
"primary": "#9DD2C0",
|
||||||
|
"primary_text": "#0e1214",
|
||||||
|
"secondary": "#FADDD2",
|
||||||
|
"accent": "#E8B8A8",
|
||||||
|
"folder_header": "#E8B8A8",
|
||||||
|
"folder_text": "#0e1214"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "rose-pine-alt",
|
||||||
|
"name": "Rose Pine Alt",
|
||||||
|
"background": "#26233a",
|
||||||
|
"surface": "#191724",
|
||||||
|
"primary": "#c4a7e7",
|
||||||
|
"primary_text": "#191724",
|
||||||
|
"secondary": "#f6c177",
|
||||||
|
"accent": "#9ccfd8",
|
||||||
|
"folder_header": "#9ccfd8",
|
||||||
|
"folder_text": "#191724"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "rose-pine-moon-alt",
|
||||||
|
"name": "Rose Pine Moon Alt",
|
||||||
|
"background": "#393552",
|
||||||
|
"surface": "#232136",
|
||||||
|
"primary": "#c4a7e7",
|
||||||
|
"primary_text": "#232136",
|
||||||
|
"secondary": "#f6c177",
|
||||||
|
"accent": "#9ccfd8",
|
||||||
|
"folder_header": "#9ccfd8",
|
||||||
|
"folder_text": "#232136"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "rose-pine-moon",
|
||||||
|
"name": "Rose Pine Moon",
|
||||||
|
"background": "#393552",
|
||||||
|
"surface": "#232136",
|
||||||
|
"primary": "#ea9a97",
|
||||||
|
"primary_text": "#232136",
|
||||||
|
"secondary": "#9ccfd8",
|
||||||
|
"accent": "#3e8fb0",
|
||||||
|
"folder_header": "#3e8fb0",
|
||||||
|
"folder_text": "#e0def4"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "rosey-amoled",
|
||||||
|
"name": "Rosey AMOLED",
|
||||||
|
"background": "#1a0f16",
|
||||||
|
"surface": "#000000",
|
||||||
|
"primary": "#ff8cb3",
|
||||||
|
"primary_text": "#000000",
|
||||||
|
"secondary": "#ffb3cc",
|
||||||
|
"accent": "#ffcce0",
|
||||||
|
"folder_header": "#ffcce0",
|
||||||
|
"folder_text": "#000000"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "shien",
|
||||||
|
"name": "Shien",
|
||||||
|
"background": "#24202C",
|
||||||
|
"surface": "#15131B",
|
||||||
|
"primary": "#9B8BC1",
|
||||||
|
"primary_text": "#15131B",
|
||||||
|
"secondary": "#7866A3",
|
||||||
|
"accent": "#5D507C",
|
||||||
|
"folder_header": "#5D507C",
|
||||||
|
"folder_text": "#FFFFFF"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "shinonome",
|
||||||
|
"name": "Shinonome",
|
||||||
|
"background": "#2D3339",
|
||||||
|
"surface": "#1A1D20",
|
||||||
|
"primary": "#D5ACA9",
|
||||||
|
"primary_text": "#1A1D20",
|
||||||
|
"secondary": "#B38D97",
|
||||||
|
"accent": "#C5BAAF",
|
||||||
|
"folder_header": "#C5BAAF",
|
||||||
|
"folder_text": "#1A1D20"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "solarized-osaka",
|
||||||
|
"name": "Solarized Osaka",
|
||||||
|
"background": "#002c38",
|
||||||
|
"surface": "#001419",
|
||||||
|
"primary": "#29a298",
|
||||||
|
"primary_text": "#ffffff",
|
||||||
|
"secondary": "#db302d",
|
||||||
|
"accent": "#b28500",
|
||||||
|
"folder_header": "#b28500",
|
||||||
|
"folder_text": "#001419"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user