Folder support, Custom theme support, Boiler Plate Code support
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"enable_boilerplate": false,
|
||||
"llm": {
|
||||
"base_url": "http://100.82.205.18:1010/",
|
||||
"model": "local-model",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"enable_boilerplate": false,
|
||||
"llm": {
|
||||
"base_url": "http://127.0.0.1:1010",
|
||||
"model": "local-model",
|
||||
|
||||
@@ -43,19 +43,38 @@ export interface HandbookCatalog {
|
||||
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[]> => {
|
||||
const response = await apiClient.get('/challenges');
|
||||
return response.data.challenges;
|
||||
};
|
||||
|
||||
export const runCode = async (language: string, code: string, stdin: string = '') => {
|
||||
const response = await apiClient.post('/run', { language, code, stdin });
|
||||
export const runCode = async (
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
export const lintCode = async (language: string, code: string) => {
|
||||
const response = await apiClient.post('/lint', { language, code });
|
||||
export const lintCode = async (
|
||||
language: string,
|
||||
code: string,
|
||||
files?: Record<string, string>,
|
||||
activeFile?: string
|
||||
) => {
|
||||
const response = await apiClient.post('/lint', { language, code, files, active_file: activeFile });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -63,13 +82,17 @@ export const getGuidance = async (
|
||||
challengeId: string,
|
||||
language: string,
|
||||
code: string,
|
||||
question: string = ''
|
||||
question: string = '',
|
||||
files?: Record<string, string>,
|
||||
activeFile?: string
|
||||
) => {
|
||||
const response = await apiClient.post('/guide', {
|
||||
challenge_id: challengeId,
|
||||
language,
|
||||
code,
|
||||
question,
|
||||
files,
|
||||
active_file: activeFile,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getGuidance,
|
||||
getHandbookCatalog,
|
||||
getHandbookExample,
|
||||
getConfig,
|
||||
Challenge,
|
||||
HandbookTopic,
|
||||
HandbookCatalog,
|
||||
@@ -32,18 +33,128 @@ import {
|
||||
Terminal,
|
||||
GripVertical,
|
||||
GripHorizontal,
|
||||
Folder,
|
||||
FolderPlus,
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
FileCode,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface WorkspaceProps {
|
||||
challengeId: string;
|
||||
challengeId?: string;
|
||||
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 [allChallenges, setAllChallenges] = useState<Challenge[]>([]);
|
||||
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('');
|
||||
|
||||
// Resizable Layout Dimensions
|
||||
@@ -59,8 +170,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
text: 'Execution and lint output will appear here...',
|
||||
});
|
||||
|
||||
// Right sidebar state: 'mentor' | 'handbook' | 'closed'
|
||||
const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'closed'>('mentor');
|
||||
// Right sidebar state: 'mentor' | 'handbook' | 'files' | 'closed'
|
||||
const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'files' | 'closed'>('mentor');
|
||||
|
||||
// Mentor state
|
||||
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 [isLinting, setIsLinting] = useState(false);
|
||||
|
||||
// Load backend config on mount
|
||||
useEffect(() => {
|
||||
getConfig()
|
||||
.then((cfg) => {
|
||||
setEnableBoilerplate(cfg.enable_boilerplate);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Load all challenges
|
||||
useEffect(() => {
|
||||
getChallenges()
|
||||
@@ -95,7 +215,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
.catch((err) => console.error('Failed to load challenges:', err));
|
||||
}, [challengeId]);
|
||||
|
||||
// Fetch handbook catalog when current challenge language changes
|
||||
// Load handbook catalog when current challenge language changes
|
||||
useEffect(() => {
|
||||
if (currentChallenge) {
|
||||
getHandbookCatalog(currentChallenge.language)
|
||||
@@ -105,8 +225,45 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
setHandbookExample('');
|
||||
})
|
||||
.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
|
||||
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) => {
|
||||
if (!code.trim() || !currentChallenge) return;
|
||||
const currentCode = files[activeFile] || '';
|
||||
if (!currentCode.trim() || !currentChallenge) return;
|
||||
setIsRunning(true);
|
||||
setOutput({ status: 'idle', text: 'Executing code...' });
|
||||
const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText;
|
||||
|
||||
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) {
|
||||
setOutput({
|
||||
status: 'success',
|
||||
@@ -206,11 +425,13 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
};
|
||||
|
||||
const handleLintCode = async () => {
|
||||
if (!code.trim() || !currentChallenge) return;
|
||||
const currentCode = files[activeFile] || '';
|
||||
if (!currentCode.trim() || !currentChallenge) return;
|
||||
setIsLinting(true);
|
||||
setOutput({ status: 'idle', text: 'Running syntax & style lint...' });
|
||||
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) {
|
||||
setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' });
|
||||
} else {
|
||||
@@ -235,11 +456,14 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
setUserQuestion('');
|
||||
|
||||
try {
|
||||
const lang = getLanguageFromFilename(activeFile);
|
||||
const res = await getGuidance(
|
||||
cid,
|
||||
getMonacoLanguage(currentChallenge.language),
|
||||
code,
|
||||
q
|
||||
lang,
|
||||
files[activeFile] || '',
|
||||
q,
|
||||
files,
|
||||
activeFile
|
||||
);
|
||||
const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : '';
|
||||
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">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{isRunning ? (
|
||||
@@ -356,7 +580,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{isLinting ? (
|
||||
@@ -369,14 +593,27 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
<span>Check Answer</span>
|
||||
</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
|
||||
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 ${
|
||||
@@ -405,7 +642,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
|
||||
{/* Main Container */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
{/* Full-Screen Drag Overlay to bypass Monaco Editor event capture */}
|
||||
{/* Full-Screen Drag Overlay */}
|
||||
{draggingType && (
|
||||
<div
|
||||
onMouseMove={(e) => {
|
||||
@@ -434,7 +671,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{/* Left Column: Challenge Specifications (Scalable Width) */}
|
||||
|
||||
{/* Left Column: Challenge Specifications */}
|
||||
<div
|
||||
style={{ width: `${leftWidth}px` }}
|
||||
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>
|
||||
|
||||
{/* Resizable Splitter Handle: Left Column */}
|
||||
{/* Splitter Handle: Left */}
|
||||
<div
|
||||
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"
|
||||
@@ -516,27 +754,76 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
|
||||
</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="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0">
|
||||
<span className="font-mono text-blue-400">{getLanguageFileName(currentChallenge?.language)}</span>
|
||||
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
|
||||
{/* Multi-File Tab Bar */}
|
||||
<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">
|
||||
{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>
|
||||
|
||||
{/* Code Editor */}
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage={getMonacoLanguage(currentChallenge?.language)}
|
||||
language={getMonacoLanguage(currentChallenge?.language)}
|
||||
defaultLanguage={getLanguageFromFilename(activeFile)}
|
||||
language={getLanguageFromFilename(activeFile)}
|
||||
defaultValue=""
|
||||
value={code}
|
||||
value={files[activeFile] || ''}
|
||||
theme="vs-dark"
|
||||
onMount={(editor, monaco) => {
|
||||
editorRef.current = editor;
|
||||
registerMonacoCompletions(monaco);
|
||||
}}
|
||||
onChange={(val) => setCode(val || '')}
|
||||
onChange={(val) =>
|
||||
setFiles((prev) => ({ ...prev, [activeFile]: val || '' }))
|
||||
}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
@@ -611,7 +898,111 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
</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' && (
|
||||
<div
|
||||
style={{ width: `${rightWidth}px` }}
|
||||
@@ -673,7 +1064,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
|
||||
<button
|
||||
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"
|
||||
title="Check Answer"
|
||||
>
|
||||
@@ -689,7 +1080,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Column Mode 2: Handbook Sidebar (Scalable Width) */}
|
||||
{/* Right Column Mode 3: Handbook Sidebar (Scalable Width) */}
|
||||
{rightSidebarMode === 'handbook' && (
|
||||
<div
|
||||
style={{ width: `${rightWidth}px` }}
|
||||
@@ -746,7 +1137,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
</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 ${
|
||||
selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1'
|
||||
}`}>
|
||||
@@ -788,7 +1179,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generated LLM Reference Card View (ONLY renders after selecting a topic) */}
|
||||
{/* Generated LLM Reference Card View */}
|
||||
{selectedTopic && (
|
||||
<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">
|
||||
@@ -808,6 +1199,117 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.linter import linter
|
||||
from src.core.prompts import get_socratic_prompt
|
||||
from src.core.config import config
|
||||
|
||||
app = FastAPI(title="Socratic Tutor API")
|
||||
|
||||
@@ -20,25 +21,40 @@ app.add_middleware(
|
||||
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):
|
||||
language: str
|
||||
code: str
|
||||
|
||||
|
||||
from typing import Optional
|
||||
code: Optional[str] = ""
|
||||
files: Optional[Dict[str, str]] = None
|
||||
active_file: Optional[str] = None
|
||||
|
||||
|
||||
class GuideRequest(BaseModel):
|
||||
challenge_id: str
|
||||
language: str
|
||||
code: str
|
||||
code: Optional[str] = ""
|
||||
question: Optional[str] = ""
|
||||
files: Optional[Dict[str, str]] = None
|
||||
active_file: Optional[str] = None
|
||||
|
||||
|
||||
class RunRequest(BaseModel):
|
||||
language: str
|
||||
code: str
|
||||
code: Optional[str] = ""
|
||||
stdin: Optional[str] = ""
|
||||
files: Optional[Dict[str, str]] = None
|
||||
active_file: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/challenges")
|
||||
@@ -60,7 +76,7 @@ def get_challenge(challenge_id: str):
|
||||
@app.post("/lint")
|
||||
def lint_code(req: LintRequest):
|
||||
"""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.":
|
||||
raise HTTPException(status_code=404, detail=res["stderr"])
|
||||
return res
|
||||
@@ -69,7 +85,7 @@ def lint_code(req: LintRequest):
|
||||
@app.post("/run")
|
||||
def run_code(req: RunRequest):
|
||||
"""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.":
|
||||
raise HTTPException(status_code=404, detail=res["stderr"])
|
||||
return res
|
||||
@@ -100,7 +116,11 @@ async def guide_code(req: GuideRequest):
|
||||
|
||||
challenge = challenges[req.challenge_id]
|
||||
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 {
|
||||
|
||||
+11
-1
@@ -8,6 +8,7 @@ class Config:
|
||||
"""Manages application configuration settings."""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"enable_boilerplate": False,
|
||||
"llm": {
|
||||
"base_url": "http://localhost:8080/v1",
|
||||
"model": "local-model",
|
||||
@@ -34,6 +35,8 @@ class Config:
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as 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):
|
||||
config["llm"].update(user_config["llm"])
|
||||
if "web" in user_config and isinstance(user_config["web"], dict):
|
||||
@@ -50,8 +53,11 @@ class Config:
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to create default {self.config_path}: {e}")
|
||||
|
||||
|
||||
# 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")
|
||||
if env_base_url:
|
||||
config["llm"]["base_url"] = env_base_url
|
||||
@@ -88,6 +94,10 @@ class Config:
|
||||
|
||||
return config
|
||||
|
||||
@property
|
||||
def enable_boilerplate(self) -> bool:
|
||||
return bool(self._data.get("enable_boilerplate", False))
|
||||
|
||||
@property
|
||||
def llm_base_url(self) -> str:
|
||||
return self._data["llm"]["base_url"].rstrip("/")
|
||||
|
||||
+67
-18
@@ -8,9 +8,15 @@ from src.core.registry import registry
|
||||
|
||||
|
||||
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)
|
||||
if not config:
|
||||
return {
|
||||
@@ -24,20 +30,42 @@ class CodeExecutor:
|
||||
ext = config.get("ext", ".txt")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
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"
|
||||
# 1. Write multi-file project contents or fallback single file
|
||||
if files and isinstance(files, dict) and len(files) > 0:
|
||||
for rel_path, content in files.items():
|
||||
target_path = os.path.join(tmpdir, rel_path)
|
||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||
with open(target_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
# 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:
|
||||
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)
|
||||
with open(temp_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
try:
|
||||
if lang_key == "python":
|
||||
@@ -51,10 +79,13 @@ class CodeExecutor:
|
||||
timeout=15,
|
||||
)
|
||||
elif lang_key == "html":
|
||||
primary_code = code
|
||||
if files and temp_file_name in files:
|
||||
primary_code = files[temp_file_name]
|
||||
return {
|
||||
"language": language,
|
||||
"exit_code": 0,
|
||||
"stdout": code,
|
||||
"stdout": primary_code,
|
||||
"stderr": "",
|
||||
}
|
||||
elif lang_key == "javascript":
|
||||
@@ -77,7 +108,7 @@ class CodeExecutor:
|
||||
)
|
||||
elif lang_key == "go":
|
||||
result = subprocess.run(
|
||||
["go", "run", temp_file_path],
|
||||
["go", "run", "."],
|
||||
input=stdin,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -94,8 +125,17 @@ class CodeExecutor:
|
||||
timeout=15,
|
||||
)
|
||||
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(
|
||||
["g++", "-O2", temp_file_path, "-o", "main"],
|
||||
["g++", "-O2"] + cpp_files + ["-o", "main"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=tmpdir,
|
||||
@@ -140,8 +180,16 @@ class CodeExecutor:
|
||||
timeout=15,
|
||||
)
|
||||
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(
|
||||
["javac", temp_file_path],
|
||||
["javac"] + java_files,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=tmpdir,
|
||||
@@ -154,8 +202,9 @@ class CodeExecutor:
|
||||
"stdout": compile_res.stdout,
|
||||
"stderr": f"Javac Compilation Error:\n{compile_res.stderr}",
|
||||
}
|
||||
main_class = os.path.splitext(temp_file_name)[0]
|
||||
result = subprocess.run(
|
||||
["java", "Main"],
|
||||
["java", main_class],
|
||||
input=stdin,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
+28
-12
@@ -10,7 +10,13 @@ from src.core.registry import registry
|
||||
class CodeLinter:
|
||||
"""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)
|
||||
if not config:
|
||||
return {
|
||||
@@ -24,20 +30,30 @@ class CodeLinter:
|
||||
ext = config.get("ext", ".txt")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
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"
|
||||
if files and isinstance(files, dict) and len(files) > 0:
|
||||
for rel_path, content in files.items():
|
||||
target_path = os.path.join(tmpdir, rel_path)
|
||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||
with open(target_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
temp_file_name = active_file if active_file and active_file in files else list(files.keys())[0]
|
||||
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)
|
||||
with open(temp_path, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
try:
|
||||
if lang_key == "python":
|
||||
|
||||
+24
-6
@@ -21,10 +21,17 @@ class MentorClient:
|
||||
self.config = config
|
||||
|
||||
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]:
|
||||
"""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)
|
||||
|
||||
user_content = (
|
||||
@@ -92,7 +99,12 @@ class MentorClient:
|
||||
}
|
||||
|
||||
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]:
|
||||
"""Synchronous wrapper for fetching mentor guidance."""
|
||||
import asyncio
|
||||
@@ -103,15 +115,21 @@ class MentorClient:
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", []))
|
||||
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?"
|
||||
)
|
||||
|
||||
formatted_code = format_multifile_context(
|
||||
user_code=user_code, files=files, active_file=active_file
|
||||
)
|
||||
|
||||
return SYSTEM_PROMPT_MENTOR.format(
|
||||
challenge_name=getattr(challenge, "name", "Coding Challenge"),
|
||||
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", ""),
|
||||
requirements=reqs if reqs else "- Follow standard problem specifications",
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
+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