Folder support, Custom theme support, Boiler Plate Code support

This commit is contained in:
Alexander R.
2026-07-29 00:51:54 +00:00
parent 0559f7549a
commit 4aa62722c6
108 changed files with 3020 additions and 152 deletions
+1
View File
@@ -1,4 +1,5 @@
{ {
"enable_boilerplate": false,
"llm": { "llm": {
"base_url": "http://100.82.205.18:1010/", "base_url": "http://100.82.205.18:1010/",
"model": "local-model", "model": "local-model",
+1
View File
@@ -1,4 +1,5 @@
{ {
"enable_boilerplate": false,
"llm": { "llm": {
"base_url": "http://127.0.0.1:1010", "base_url": "http://127.0.0.1:1010",
"model": "local-model", "model": "local-model",
+29 -6
View File
@@ -43,19 +43,38 @@ export interface HandbookCatalog {
subjects: HandbookTopic[]; subjects: HandbookTopic[];
} }
export const getConfig = async (): Promise<{ enable_boilerplate: boolean }> => {
try {
const response = await apiClient.get('/config');
return response.data;
} catch (e) {
return { enable_boilerplate: false };
}
};
export const getChallenges = async (): Promise<Challenge[]> => { export const getChallenges = async (): Promise<Challenge[]> => {
const response = await apiClient.get('/challenges'); const response = await apiClient.get('/challenges');
return response.data.challenges; return response.data.challenges;
}; };
export const runCode = async (language: string, code: string, stdin: string = '') => { export const runCode = async (
const response = await apiClient.post('/run', { language, code, stdin }); language: string,
code: string,
stdin: string = '',
files?: Record<string, string>,
activeFile?: string
) => {
const response = await apiClient.post('/run', { language, code, stdin, files, active_file: activeFile });
return response.data; return response.data;
}; };
export const lintCode = async (
export const lintCode = async (language: string, code: string) => { language: string,
const response = await apiClient.post('/lint', { language, code }); code: string,
files?: Record<string, string>,
activeFile?: string
) => {
const response = await apiClient.post('/lint', { language, code, files, active_file: activeFile });
return response.data; return response.data;
}; };
@@ -63,13 +82,17 @@ export const getGuidance = async (
challengeId: string, challengeId: string,
language: string, language: string,
code: string, code: string,
question: string = '' question: string = '',
files?: Record<string, string>,
activeFile?: string
) => { ) => {
const response = await apiClient.post('/guide', { const response = await apiClient.post('/guide', {
challenge_id: challengeId, challenge_id: challengeId,
language, language,
code, code,
question, question,
files,
active_file: activeFile,
}); });
return response.data; return response.data;
}; };
+535 -33
View File
@@ -8,6 +8,7 @@ import {
getGuidance, getGuidance,
getHandbookCatalog, getHandbookCatalog,
getHandbookExample, getHandbookExample,
getConfig,
Challenge, Challenge,
HandbookTopic, HandbookTopic,
HandbookCatalog, HandbookCatalog,
@@ -32,18 +33,128 @@ import {
Terminal, Terminal,
GripVertical, GripVertical,
GripHorizontal, GripHorizontal,
Folder,
FolderPlus,
Plus,
Edit2,
Trash2,
FileCode,
} from 'lucide-react'; } from 'lucide-react';
interface WorkspaceProps { interface WorkspaceProps {
challengeId: string; challengeId?: string;
onBack: () => void; onBack: () => void;
} }
function getLanguageFromFilename(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase() || '';
const map: Record<string, string> = {
cpp: 'cpp',
hpp: 'cpp',
h: 'cpp',
cxx: 'cpp',
py: 'python',
java: 'java',
cs: 'csharp',
rs: 'rust',
go: 'go',
js: 'javascript',
ts: 'typescript',
html: 'html',
lua: 'lua',
json: 'json',
md: 'markdown',
};
return map[ext] || 'python';
}
function getDefaultFilesForLanguage(language?: string, enableBoilerplate: boolean = false): Record<string, string> {
const norm = (language || 'python').toLowerCase().trim();
if (!enableBoilerplate) {
if (norm === 'cpp' || norm === 'c++' || norm === 'cxx') {
return { 'main.cpp': '' };
} else if (norm === 'java') {
return { 'Main.java': '' };
} else if (norm === 'csharp' || norm === 'c#') {
return { 'Program.cs': '' };
} else if (norm === 'rust') {
return { 'main.rs': '' };
} else if (norm === 'go') {
return { 'main.go': '' };
} else if (norm === 'javascript' || norm === 'js') {
return { 'main.js': '' };
} else if (norm === 'typescript' || norm === 'ts') {
return { 'main.ts': '' };
} else if (norm === 'html') {
return { 'index.html': '' };
} else if (norm === 'lua') {
return { 'main.lua': '' };
}
return { 'main.py': '' };
}
if (norm === 'cpp' || norm === 'c++' || norm === 'cxx') {
return {
'main.cpp': '// Main implementation\n#include "main.h"\n#include <iostream>\n\nint main() {\n std::cout << "Hello from C++!" << std::endl;\n return 0;\n}\n',
'main.h': '// Main header file\n#ifndef MAIN_H\n#define MAIN_H\n\n// Declarations\n\n#endif\n',
};
} else if (norm === 'java') {
return {
'Main.java': 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello from Java!");\n }\n}\n',
};
} else if (norm === 'csharp' || norm === 'c#') {
return {
'Program.cs': 'using System;\n\nclass Program {\n static void Main() {\n Console.WriteLine("Hello from C#!");\n }\n}\n',
};
} else if (norm === 'rust') {
return {
'main.rs': 'fn main() {\n println!("Hello from Rust!");\n}\n',
};
} else if (norm === 'go') {
return {
'main.go': 'package main\nimport "fmt"\n\nfunc main() {\n fmt.Println("Hello from Go!")\n}\n',
};
} else if (norm === 'javascript' || norm === 'js') {
return {
'main.js': 'console.log("Hello from JavaScript!");\n',
};
} else if (norm === 'typescript' || norm === 'ts') {
return {
'main.ts': 'console.log("Hello from TypeScript!");\n',
};
} else if (norm === 'html') {
return {
'index.html': '<!DOCTYPE html>\n<html>\n<head>\n <title>App</title>\n</head>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>\n',
};
} else if (norm === 'lua') {
return {
'main.lua': 'print("Hello from Lua!");\n',
};
}
return {
'main.py': '# Main script\nprint("Hello from Python!")\n',
};
}
const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => { const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
const [allChallenges, setAllChallenges] = useState<Challenge[]>([]); const [allChallenges, setAllChallenges] = useState<Challenge[]>([]);
const [currentChallenge, setCurrentChallenge] = useState<Challenge | null>(null); const [currentChallenge, setCurrentChallenge] = useState<Challenge | null>(null);
const [enableBoilerplate, setEnableBoilerplate] = useState(false);
// Multi-file & Folder state
const [files, setFiles] = useState<Record<string, string>>({ 'main.py': '' });
const [folders, setFolders] = useState<string[]>([]);
const [activeFile, setActiveFile] = useState<string>('main.py');
const [openTabs, setOpenTabs] = useState<string[]>(['main.py']);
// Modals for File & Folder Operations
const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
const [newFileNameInput, setNewFileNameInput] = useState('');
const [isNewFolderModalOpen, setIsNewFolderModalOpen] = useState(false);
const [newFolderNameInput, setNewFolderNameInput] = useState('');
const [isRenameModalOpen, setIsRenameModalOpen] = useState(false);
const [renameFileNameInput, setRenameFileNameInput] = useState('');
const [code, setCode] = useState('');
const [stdinText, setStdinText] = useState(''); const [stdinText, setStdinText] = useState('');
// Resizable Layout Dimensions // Resizable Layout Dimensions
@@ -59,8 +170,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
text: 'Execution and lint output will appear here...', text: 'Execution and lint output will appear here...',
}); });
// Right sidebar state: 'mentor' | 'handbook' | 'closed' // Right sidebar state: 'mentor' | 'handbook' | 'files' | 'closed'
const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'closed'>('mentor'); const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'files' | 'closed'>('mentor');
// Mentor state // Mentor state
const [guidance, setGuidance] = useState<string>('Ask the mentor a question or click Check Answer below...'); const [guidance, setGuidance] = useState<string>('Ask the mentor a question or click Check Answer below...');
@@ -80,6 +191,15 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [isLinting, setIsLinting] = useState(false); const [isLinting, setIsLinting] = useState(false);
// Load backend config on mount
useEffect(() => {
getConfig()
.then((cfg) => {
setEnableBoilerplate(cfg.enable_boilerplate);
})
.catch(() => {});
}, []);
// Load all challenges // Load all challenges
useEffect(() => { useEffect(() => {
getChallenges() getChallenges()
@@ -95,7 +215,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
.catch((err) => console.error('Failed to load challenges:', err)); .catch((err) => console.error('Failed to load challenges:', err));
}, [challengeId]); }, [challengeId]);
// Fetch handbook catalog when current challenge language changes // Load handbook catalog when current challenge language changes
useEffect(() => { useEffect(() => {
if (currentChallenge) { if (currentChallenge) {
getHandbookCatalog(currentChallenge.language) getHandbookCatalog(currentChallenge.language)
@@ -105,8 +225,45 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
setHandbookExample(''); setHandbookExample('');
}) })
.catch((err) => console.error('Failed to load handbook catalog:', err)); .catch((err) => console.error('Failed to load handbook catalog:', err));
const defaultFiles = getDefaultFilesForLanguage(currentChallenge.language, enableBoilerplate);
setFiles(defaultFiles);
const keys = Object.keys(defaultFiles);
const first = keys[0] || 'main.py';
setActiveFile(first);
setOpenTabs(keys);
} }
}, [currentChallenge]); }, [currentChallenge, enableBoilerplate]);
const handleCreateFolder = (folderName: string) => {
const name = folderName.trim();
if (!name) return;
if (!folders.includes(name)) {
setFolders((prev) => [...prev, name]);
}
setIsNewFolderModalOpen(false);
setNewFolderNameInput('');
};
// Global keyboard shortcuts (Ctrl+1..9 for tabs, Ctrl+T for new file)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 't') {
e.preventDefault();
setNewFileNameInput('');
setIsNewFileModalOpen(true);
}
if ((e.ctrlKey || e.metaKey) && e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key, 10) - 1;
if (idx < openTabs.length) {
e.preventDefault();
setActiveFile(openTabs[idx]);
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [openTabs]);
// Global mousemove and mouseup listeners for drag-to-resize splitters // Global mousemove and mouseup listeners for drag-to-resize splitters
useEffect(() => { useEffect(() => {
@@ -177,14 +334,76 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
} }
}; };
// Multi-file tab & file operation handlers
const handleCreateFile = (filename: string) => {
const name = filename.trim();
if (!name) return;
if (files[name] !== undefined) {
if (!openTabs.includes(name)) {
setOpenTabs((prev) => [...prev, name]);
}
setActiveFile(name);
} else {
setFiles((prev) => ({ ...prev, [name]: '' }));
setOpenTabs((prev) => [...prev, name]);
setActiveFile(name);
}
setIsNewFileModalOpen(false);
setNewFileNameInput('');
};
const handleCloseTab = (tabName: string, e?: React.MouseEvent) => {
if (e) {
e.stopPropagation();
}
if (openTabs.length <= 1) return;
const nextTabs = openTabs.filter((t) => t !== tabName);
setOpenTabs(nextTabs);
if (activeFile === tabName) {
setActiveFile(nextTabs[nextTabs.length - 1]);
}
};
const handleRenameActiveFile = (newName: string) => {
const name = newName.trim();
if (!name || name === activeFile) {
setIsRenameModalOpen(false);
return;
}
const content = files[activeFile] || '';
const newFiles = { ...files };
delete newFiles[activeFile];
newFiles[name] = content;
setFiles(newFiles);
setOpenTabs((prev) => prev.map((t) => (t === activeFile ? name : t)));
setActiveFile(name);
setIsRenameModalOpen(false);
setRenameFileNameInput('');
};
const handleDeleteFile = (fileName: string) => {
if (Object.keys(files).length <= 1) return;
const newFiles = { ...files };
delete newFiles[fileName];
setFiles(newFiles);
const nextTabs = openTabs.filter((t) => t !== fileName);
setOpenTabs(nextTabs.length > 0 ? nextTabs : [Object.keys(newFiles)[0]]);
if (activeFile === fileName) {
setActiveFile(Object.keys(newFiles)[0]);
}
};
const handleRunCode = async (overrideStdin?: string) => { const handleRunCode = async (overrideStdin?: string) => {
if (!code.trim() || !currentChallenge) return; const currentCode = files[activeFile] || '';
if (!currentCode.trim() || !currentChallenge) return;
setIsRunning(true); setIsRunning(true);
setOutput({ status: 'idle', text: 'Executing code...' }); setOutput({ status: 'idle', text: 'Executing code...' });
const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText; const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText;
try { try {
const res = await runCode(getMonacoLanguage(currentChallenge.language), code, inputToSend); const lang = getLanguageFromFilename(activeFile);
const res = await runCode(lang, currentCode, inputToSend, files, activeFile);
if (res.exit_code === 0) { if (res.exit_code === 0) {
setOutput({ setOutput({
status: 'success', status: 'success',
@@ -206,11 +425,13 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
}; };
const handleLintCode = async () => { const handleLintCode = async () => {
if (!code.trim() || !currentChallenge) return; const currentCode = files[activeFile] || '';
if (!currentCode.trim() || !currentChallenge) return;
setIsLinting(true); setIsLinting(true);
setOutput({ status: 'idle', text: 'Running syntax & style lint...' }); setOutput({ status: 'idle', text: 'Running syntax & style lint...' });
try { try {
const res = await lintCode(getMonacoLanguage(currentChallenge.language), code); const lang = getLanguageFromFilename(activeFile);
const res = await lintCode(lang, currentCode, files, activeFile);
if (res.exit_code === 0) { if (res.exit_code === 0) {
setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' }); setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' });
} else { } else {
@@ -235,11 +456,14 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
setUserQuestion(''); setUserQuestion('');
try { try {
const lang = getLanguageFromFilename(activeFile);
const res = await getGuidance( const res = await getGuidance(
cid, cid,
getMonacoLanguage(currentChallenge.language), lang,
code, files[activeFile] || '',
q q,
files,
activeFile
); );
const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : ''; const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : '';
setGuidance(`${qHeader}${res.mentor_response || 'No guidance received.'}`); setGuidance(`${qHeader}${res.mentor_response || 'No guidance received.'}`);
@@ -343,7 +567,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => handleRunCode()} onClick={() => handleRunCode()}
disabled={isRunning || !code.trim()} disabled={isRunning || !(files[activeFile] || '').trim()}
className="px-3.5 py-1.5 bg-green-600 hover:bg-green-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm" className="px-3.5 py-1.5 bg-green-600 hover:bg-green-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
> >
{isRunning ? ( {isRunning ? (
@@ -356,7 +580,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
<button <button
onClick={handleLintCode} onClick={handleLintCode}
disabled={isLinting || !code.trim()} disabled={isLinting || !(files[activeFile] || '').trim()}
className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm" className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
> >
{isLinting ? ( {isLinting ? (
@@ -369,14 +593,27 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
<button <button
onClick={handleCheckAnswer} onClick={handleCheckAnswer}
disabled={isLoadingGuidance || !code.trim()} disabled={isLoadingGuidance || !(files[activeFile] || '').trim()}
className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm" className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
> >
<Sparkles size={14} /> <Sparkles size={14} />
<span>Check Answer</span> <span>Check Answer</span>
</button> </button>
{/* Dual Sidebar Toggles */} {/* Sidebar Toggles: File Browser (Folder icon), Mentor, Syntax Handbook */}
<button
onClick={() => setRightSidebarMode(rightSidebarMode === 'files' ? 'closed' : 'files')}
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
rightSidebarMode === 'files'
? 'bg-blue-500/20 text-blue-300 border-blue-500/40'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700 border-gray-700'
}`}
title="Toggle File Tree Sidebar"
>
<Folder size={14} />
<span>Files</span>
</button>
<button <button
onClick={() => setRightSidebarMode(rightSidebarMode === 'mentor' ? 'closed' : 'mentor')} onClick={() => setRightSidebarMode(rightSidebarMode === 'mentor' ? 'closed' : 'mentor')}
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${ className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
@@ -405,7 +642,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{/* Main Container */} {/* Main Container */}
<div className="flex flex-1 overflow-hidden relative"> <div className="flex flex-1 overflow-hidden relative">
{/* Full-Screen Drag Overlay to bypass Monaco Editor event capture */} {/* Full-Screen Drag Overlay */}
{draggingType && ( {draggingType && (
<div <div
onMouseMove={(e) => { onMouseMove={(e) => {
@@ -434,7 +671,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
}`} }`}
/> />
)} )}
{/* Left Column: Challenge Specifications (Scalable Width) */}
{/* Left Column: Challenge Specifications */}
<div <div
style={{ width: `${leftWidth}px` }} style={{ width: `${leftWidth}px` }}
className="border-r border-transparent bg-darkBg flex flex-col overflow-hidden shrink-0" className="border-r border-transparent bg-darkBg flex flex-col overflow-hidden shrink-0"
@@ -507,7 +745,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
</div> </div>
</div> </div>
{/* Resizable Splitter Handle: Left Column */} {/* Splitter Handle: Left */}
<div <div
onMouseDown={startDraggingLeft} onMouseDown={startDraggingLeft}
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none" className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
@@ -516,27 +754,76 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" /> <GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
</div> </div>
{/* Middle Column: Monaco Code Editor & Resizable Output Console */} {/* Middle Column: Multi-File Tabs, Monaco Code Editor & Resizable Output Console */}
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0"> <div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
<div className="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0"> {/* Multi-File Tab Bar */}
<span className="font-mono text-blue-400">{getLanguageFileName(currentChallenge?.language)}</span> <div className="bg-darkBg text-gray-400 px-2 py-1 text-xs flex items-center border-b border-darkBorder shrink-0 overflow-x-auto gap-1 select-none">
<span className="text-[11px] text-gray-500">TactiTerm IDE</span> {openTabs.map((tab, idx) => (
<div
key={tab}
onClick={() => setActiveFile(tab)}
onAuxClick={(e) => {
if (e.button === 1) handleCloseTab(tab, e);
}}
onDoubleClick={() => {
if (tab === activeFile) {
setRenameFileNameInput(tab);
setIsRenameModalOpen(true);
}
}}
className={`px-3 py-1 rounded-t-md text-xs font-mono flex items-center gap-2 cursor-pointer border-t border-x transition-colors ${
tab === activeFile
? 'bg-[#1e1e1e] text-blue-400 border-darkBorder font-semibold'
: 'bg-darkBg text-gray-400 border-transparent hover:bg-darkSurface hover:text-gray-200'
}`}
title="Double-click to rename | Middle-click to close"
>
<FileCode size={13} className={tab === activeFile ? 'text-blue-400' : 'text-gray-500'} />
<span>{tab}</span>
<span className="text-[10px] text-gray-500 opacity-60 font-sans">^{idx + 1}</span>
{openTabs.length > 1 && (
<button
onClick={(e) => handleCloseTab(tab, e)}
className="hover:text-red-400 p-0.5 rounded transition-colors ml-1"
>
<X size={12} />
</button>
)}
</div>
))}
<button
onClick={() => {
setNewFileNameInput('');
setIsNewFileModalOpen(true);
}}
className="p-1 hover:bg-gray-800 text-gray-400 hover:text-white rounded transition-colors ml-1"
title="New File Tab (Ctrl+T)"
>
<Plus size={14} />
</button>
<div className="ml-auto text-[11px] text-gray-500 font-mono pr-2">
{getLanguageFromFilename(activeFile)}
</div>
</div> </div>
{/* Code Editor */} {/* Code Editor */}
<div className="flex-1 min-h-0 relative"> <div className="flex-1 min-h-0 relative">
<Editor <Editor
height="100%" height="100%"
defaultLanguage={getMonacoLanguage(currentChallenge?.language)} defaultLanguage={getLanguageFromFilename(activeFile)}
language={getMonacoLanguage(currentChallenge?.language)} language={getLanguageFromFilename(activeFile)}
defaultValue="" defaultValue=""
value={code} value={files[activeFile] || ''}
theme="vs-dark" theme="vs-dark"
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
editorRef.current = editor; editorRef.current = editor;
registerMonacoCompletions(monaco); registerMonacoCompletions(monaco);
}} }}
onChange={(val) => setCode(val || '')} onChange={(val) =>
setFiles((prev) => ({ ...prev, [activeFile]: val || '' }))
}
options={{ options={{
minimap: { enabled: false }, minimap: { enabled: false },
fontSize: 13, fontSize: 13,
@@ -611,7 +898,111 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
</div> </div>
)} )}
{/* Right Column Mode 1: Mentor Sidebar (Scalable Width) */} {/* Right Column Mode 1: File Browser Sidebar */}
{rightSidebarMode === 'files' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-transparent bg-darkSurface flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 border-b border-darkBorder bg-darkBg flex justify-between items-center">
<h2 className="flex items-center gap-2 font-bold text-blue-400 text-xs uppercase tracking-wider">
<Folder size={16} /> File Browser
</h2>
<div className="flex items-center gap-1">
<button
onClick={() => {
setNewFolderNameInput('');
setIsNewFolderModalOpen(true);
}}
className="p-1 hover:bg-gray-800 text-gray-300 hover:text-white rounded"
title="Create Folder"
>
<FolderPlus size={16} />
</button>
<button
onClick={() => {
setNewFileNameInput('');
setIsNewFileModalOpen(true);
}}
className="p-1 hover:bg-gray-800 text-gray-300 hover:text-white rounded"
title="Create File"
>
<Plus size={16} />
</button>
<button
onClick={() => setRightSidebarMode('closed')}
className="text-gray-400 hover:text-white text-xs p-1 rounded hover:bg-gray-800"
>
<X size={16} />
</button>
</div>
</div>
<div className="p-3 flex-1 overflow-y-auto space-y-1 bg-darkBg">
{folders.map((folderName) => (
<div
key={`folder-${folderName}`}
className="flex items-center justify-between p-2 rounded-lg text-xs font-mono bg-darkSurface border border-darkBorder/50 text-amber-300 select-none"
>
<div className="flex items-center gap-2 overflow-hidden">
<Folder size={14} className="text-amber-400" />
<span className="truncate">{folderName}/</span>
</div>
</div>
))}
{Object.keys(files).map((fileName) => (
<div
key={fileName}
onClick={() => {
if (!openTabs.includes(fileName)) {
setOpenTabs((prev) => [...prev, fileName]);
}
setActiveFile(fileName);
}}
className={`flex items-center justify-between p-2 rounded-lg text-xs font-mono transition-all cursor-pointer border ${
fileName === activeFile
? 'bg-blue-600/20 border-blue-500/50 text-blue-300 font-bold'
: 'bg-darkSurface border-darkBorder/50 text-gray-300 hover:bg-gray-800'
}`}
>
<div className="flex items-center gap-2 overflow-hidden">
<FileCode size={14} className={fileName === activeFile ? 'text-blue-400' : 'text-gray-500'} />
<span className="truncate">{fileName}</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
setRenameFileNameInput(fileName);
setActiveFile(fileName);
setIsRenameModalOpen(true);
}}
className="p-1 text-gray-400 hover:text-white"
title="Rename File"
>
<Edit2 size={12} />
</button>
{Object.keys(files).length > 1 && (
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteFile(fileName);
}}
className="p-1 text-gray-400 hover:text-red-400"
title="Delete File"
>
<Trash2 size={12} />
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Right Column Mode 2: Mentor Sidebar (Scalable Width) */}
{rightSidebarMode === 'mentor' && ( {rightSidebarMode === 'mentor' && (
<div <div
style={{ width: `${rightWidth}px` }} style={{ width: `${rightWidth}px` }}
@@ -673,7 +1064,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
<button <button
onClick={handleCheckAnswer} onClick={handleCheckAnswer}
disabled={isLoadingGuidance || !code.trim()} disabled={isLoadingGuidance || !(files[activeFile] || '').trim()}
className="py-2 px-3 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1" className="py-2 px-3 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1"
title="Check Answer" title="Check Answer"
> >
@@ -689,7 +1080,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
</div> </div>
)} )}
{/* Right Column Mode 2: Handbook Sidebar (Scalable Width) */} {/* Right Column Mode 3: Handbook Sidebar (Scalable Width) */}
{rightSidebarMode === 'handbook' && ( {rightSidebarMode === 'handbook' && (
<div <div
style={{ width: `${rightWidth}px` }} style={{ width: `${rightWidth}px` }}
@@ -746,7 +1137,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
</div> </div>
</div> </div>
{/* Topic Catalog List (Fills full height when no topic selected) */} {/* Topic Catalog List */}
<div className={`p-3 bg-darkBg flex flex-col gap-1.5 overflow-y-auto ${ <div className={`p-3 bg-darkBg flex flex-col gap-1.5 overflow-y-auto ${
selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1' selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1'
}`}> }`}>
@@ -788,7 +1179,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
)} )}
</div> </div>
{/* Generated LLM Reference Card View (ONLY renders after selecting a topic) */} {/* Generated LLM Reference Card View */}
{selectedTopic && ( {selectedTopic && (
<div className="flex-1 p-4 overflow-y-auto bg-darkSurface border-t border-darkBorder"> <div className="flex-1 p-4 overflow-y-auto bg-darkSurface border-t border-darkBorder">
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner"> <div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
@@ -808,6 +1199,117 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
</div> </div>
)} )}
</div> </div>
{/* New File Modal */}
{isNewFileModalOpen && (
<div className="fixed inset-0 bg-black/60 z-[999999] flex items-center justify-center p-4">
<div className="bg-darkSurface border border-darkBorder rounded-xl p-5 w-full max-w-sm shadow-xl flex flex-col gap-4">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Plus size={16} className="text-blue-400" /> Create New File
</h3>
<input
type="text"
autoFocus
value={newFileNameInput}
onChange={(e) => setNewFileNameInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateFile(newFileNameInput);
if (e.key === 'Escape') setIsNewFileModalOpen(false);
}}
placeholder="e.g. helper.cpp, utils.h, data.json"
className="w-full bg-darkBg border border-darkBorder rounded-lg px-3 py-2 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 font-mono"
/>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsNewFileModalOpen(false)}
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white rounded-lg hover:bg-gray-800"
>
Cancel
</button>
<button
onClick={() => handleCreateFile(newFileNameInput)}
className="px-4 py-1.5 text-xs font-semibold bg-blue-600 hover:bg-blue-500 text-white rounded-lg"
>
Create File
</button>
</div>
</div>
</div>
)}
{/* New Folder Modal */}
{isNewFolderModalOpen && (
<div className="fixed inset-0 bg-black/60 z-[999999] flex items-center justify-center p-4">
<div className="bg-darkSurface border border-darkBorder rounded-xl p-5 w-full max-w-sm shadow-xl flex flex-col gap-4">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<FolderPlus size={16} className="text-amber-400" /> Create New Folder
</h3>
<input
type="text"
autoFocus
value={newFolderNameInput}
onChange={(e) => setNewFolderNameInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateFolder(newFolderNameInput);
if (e.key === 'Escape') setIsNewFolderModalOpen(false);
}}
placeholder="e.g. src, include, components"
className="w-full bg-darkBg border border-darkBorder rounded-lg px-3 py-2 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-amber-500 font-mono"
/>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsNewFolderModalOpen(false)}
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white rounded-lg hover:bg-gray-800"
>
Cancel
</button>
<button
onClick={() => handleCreateFolder(newFolderNameInput)}
className="px-4 py-1.5 text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white rounded-lg"
>
Create Folder
</button>
</div>
</div>
</div>
)}
{/* Rename File Modal */}
{isRenameModalOpen && (
<div className="fixed inset-0 bg-black/60 z-[999999] flex items-center justify-center p-4">
<div className="bg-darkSurface border border-darkBorder rounded-xl p-5 w-full max-w-sm shadow-xl flex flex-col gap-4">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Edit2 size={16} className="text-amber-400" /> Rename File
</h3>
<input
type="text"
autoFocus
value={renameFileNameInput}
onChange={(e) => setRenameFileNameInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleRenameActiveFile(renameFileNameInput);
if (e.key === 'Escape') setIsRenameModalOpen(false);
}}
placeholder="Enter new filename..."
className="w-full bg-darkBg border border-darkBorder rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500 font-mono"
/>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsRenameModalOpen(false)}
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white rounded-lg hover:bg-gray-800"
>
Cancel
</button>
<button
onClick={() => handleRenameActiveFile(renameFileNameInput)}
className="px-4 py-1.5 text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white rounded-lg"
>
Rename
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };
+61
View File
@@ -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}/!")
+27
View File
@@ -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())
+45
View File
@@ -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()
+17
View File
@@ -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)
+20
View File
@@ -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
View File
@@ -6,6 +6,7 @@ from src.core.loader import ChallengeLoader
from src.core.executor import executor from src.core.executor import executor
from src.core.linter import linter from src.core.linter import linter
from src.core.prompts import get_socratic_prompt from src.core.prompts import get_socratic_prompt
from src.core.config import config
app = FastAPI(title="Socratic Tutor API") app = FastAPI(title="Socratic Tutor API")
@@ -20,25 +21,40 @@ app.add_middleware(
loader = ChallengeLoader() loader = ChallengeLoader()
@app.get("/config")
def get_app_config():
return {
"enable_boilerplate": config.enable_boilerplate,
"llm_base_url": config.llm_base_url,
"llm_model": config.llm_model,
}
from typing import Optional, Dict
class LintRequest(BaseModel): class LintRequest(BaseModel):
language: str language: str
code: str code: Optional[str] = ""
files: Optional[Dict[str, str]] = None
active_file: Optional[str] = None
from typing import Optional
class GuideRequest(BaseModel): class GuideRequest(BaseModel):
challenge_id: str challenge_id: str
language: str language: str
code: str code: Optional[str] = ""
question: Optional[str] = "" question: Optional[str] = ""
files: Optional[Dict[str, str]] = None
active_file: Optional[str] = None
class RunRequest(BaseModel): class RunRequest(BaseModel):
language: str language: str
code: str code: Optional[str] = ""
stdin: Optional[str] = "" stdin: Optional[str] = ""
files: Optional[Dict[str, str]] = None
active_file: Optional[str] = None
@app.get("/challenges") @app.get("/challenges")
@@ -60,7 +76,7 @@ def get_challenge(challenge_id: str):
@app.post("/lint") @app.post("/lint")
def lint_code(req: LintRequest): def lint_code(req: LintRequest):
"""Runs linting on user code using Core Linter.""" """Runs linting on user code using Core Linter."""
res = linter.lint(req.language, req.code) res = linter.lint(req.language, req.code or "", files=req.files, active_file=req.active_file)
if res.get("stderr") == f"Language '{req.language}' is not supported.": if res.get("stderr") == f"Language '{req.language}' is not supported.":
raise HTTPException(status_code=404, detail=res["stderr"]) raise HTTPException(status_code=404, detail=res["stderr"])
return res return res
@@ -69,7 +85,7 @@ def lint_code(req: LintRequest):
@app.post("/run") @app.post("/run")
def run_code(req: RunRequest): def run_code(req: RunRequest):
"""Executes user code using Core Executor.""" """Executes user code using Core Executor."""
res = executor.run(req.language, req.code, stdin=req.stdin or "") res = executor.run(req.language, req.code or "", stdin=req.stdin or "", files=req.files)
if res.get("stderr") == f"Language '{req.language}' is not supported.": if res.get("stderr") == f"Language '{req.language}' is not supported.":
raise HTTPException(status_code=404, detail=res["stderr"]) raise HTTPException(status_code=404, detail=res["stderr"])
return res return res
@@ -100,7 +116,11 @@ async def guide_code(req: GuideRequest):
challenge = challenges[req.challenge_id] challenge = challenges[req.challenge_id]
guidance = await mentor.get_guidance_async( guidance = await mentor.get_guidance_async(
challenge, req.code, user_question=req.question or "" challenge,
user_code=req.code or "",
user_question=req.question or "",
files=req.files,
active_file=req.active_file,
) )
return { return {
+11 -1
View File
@@ -8,6 +8,7 @@ class Config:
"""Manages application configuration settings.""" """Manages application configuration settings."""
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"enable_boilerplate": False,
"llm": { "llm": {
"base_url": "http://localhost:8080/v1", "base_url": "http://localhost:8080/v1",
"model": "local-model", "model": "local-model",
@@ -34,6 +35,8 @@ class Config:
try: try:
with open(self.config_path, "r", encoding="utf-8") as f: with open(self.config_path, "r", encoding="utf-8") as f:
user_config = json.load(f) user_config = json.load(f)
if "enable_boilerplate" in user_config:
config["enable_boilerplate"] = bool(user_config["enable_boilerplate"])
if "llm" in user_config and isinstance(user_config["llm"], dict): if "llm" in user_config and isinstance(user_config["llm"], dict):
config["llm"].update(user_config["llm"]) config["llm"].update(user_config["llm"])
if "web" in user_config and isinstance(user_config["web"], dict): if "web" in user_config and isinstance(user_config["web"], dict):
@@ -50,8 +53,11 @@ class Config:
except Exception as e: except Exception as e:
print(f"Warning: Failed to create default {self.config_path}: {e}") print(f"Warning: Failed to create default {self.config_path}: {e}")
# Allow environment variable overrides # Allow environment variable overrides
env_boilerplate = os.getenv("TACTTERM_ENABLE_BOILERPLATE")
if env_boilerplate is not None:
config["enable_boilerplate"] = env_boilerplate.lower() in ("true", "1", "yes")
env_base_url = os.getenv("TACTTERM_LLM_BASE_URL") env_base_url = os.getenv("TACTTERM_LLM_BASE_URL")
if env_base_url: if env_base_url:
config["llm"]["base_url"] = env_base_url config["llm"]["base_url"] = env_base_url
@@ -88,6 +94,10 @@ class Config:
return config return config
@property
def enable_boilerplate(self) -> bool:
return bool(self._data.get("enable_boilerplate", False))
@property @property
def llm_base_url(self) -> str: def llm_base_url(self) -> str:
return self._data["llm"]["base_url"].rstrip("/") return self._data["llm"]["base_url"].rstrip("/")
+67 -18
View File
@@ -8,9 +8,15 @@ from src.core.registry import registry
class CodeExecutor: class CodeExecutor:
"""Executes code for all 10 supported programming languages with stdin support.""" """Executes code for all 10 supported programming languages with stdin support and multi-file projects."""
def run(self, language: str, code: str, stdin: str = "") -> Dict[str, Any]: def run(
self,
language: str,
code: str = "",
stdin: str = "",
files: Dict[str, str] = None,
) -> Dict[str, Any]:
config = registry.get_config(language) config = registry.get_config(language)
if not config: if not config:
return { return {
@@ -24,20 +30,42 @@ class CodeExecutor:
ext = config.get("ext", ".txt") ext = config.get("ext", ".txt")
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
if lang_key == "java": # 1. Write multi-file project contents or fallback single file
temp_file_name = "Main.java" if files and isinstance(files, dict) and len(files) > 0:
elif lang_key == "cpp": for rel_path, content in files.items():
temp_file_name = "main.cpp" target_path = os.path.join(tmpdir, rel_path)
elif lang_key == "rust": os.makedirs(os.path.dirname(target_path), exist_ok=True)
temp_file_name = "main.rs" with open(target_path, "w", encoding="utf-8") as f:
elif lang_key == "csharp": f.write(content)
temp_file_name = "Program.cs" # Determine main entry file
main_file_candidates = [
"main.cpp", "Main.java", "main.rs", "Program.cs", "main.py",
"main.go", "main.js", "main.ts", "main.lua", "index.html"
]
temp_file_name = None
for candidate in main_file_candidates:
if candidate in files:
temp_file_name = candidate
break
if not temp_file_name:
temp_file_name = list(files.keys())[0]
else: else:
temp_file_name = f"main{ext}" if lang_key == "java":
temp_file_name = "Main.java"
elif lang_key == "cpp":
temp_file_name = "main.cpp"
elif lang_key == "rust":
temp_file_name = "main.rs"
elif lang_key == "csharp":
temp_file_name = "Program.cs"
else:
temp_file_name = f"main{ext}"
temp_file_path = os.path.join(tmpdir, temp_file_name)
with open(temp_file_path, "w", encoding="utf-8") as f:
f.write(code)
temp_file_path = os.path.join(tmpdir, temp_file_name) temp_file_path = os.path.join(tmpdir, temp_file_name)
with open(temp_file_path, "w", encoding="utf-8") as f:
f.write(code)
try: try:
if lang_key == "python": if lang_key == "python":
@@ -51,10 +79,13 @@ class CodeExecutor:
timeout=15, timeout=15,
) )
elif lang_key == "html": elif lang_key == "html":
primary_code = code
if files and temp_file_name in files:
primary_code = files[temp_file_name]
return { return {
"language": language, "language": language,
"exit_code": 0, "exit_code": 0,
"stdout": code, "stdout": primary_code,
"stderr": "", "stderr": "",
} }
elif lang_key == "javascript": elif lang_key == "javascript":
@@ -77,7 +108,7 @@ class CodeExecutor:
) )
elif lang_key == "go": elif lang_key == "go":
result = subprocess.run( result = subprocess.run(
["go", "run", temp_file_path], ["go", "run", "."],
input=stdin, input=stdin,
capture_output=True, capture_output=True,
text=True, text=True,
@@ -94,8 +125,17 @@ class CodeExecutor:
timeout=15, timeout=15,
) )
elif lang_key == "cpp": elif lang_key == "cpp":
# Collect all .cpp files recursively in tmpdir
cpp_files = []
for root, _, fnames in os.walk(tmpdir):
for fn in fnames:
if fn.endswith(".cpp"):
cpp_files.append(os.path.relpath(os.path.join(root, fn), tmpdir))
if not cpp_files:
cpp_files = [temp_file_name]
compile_res = subprocess.run( compile_res = subprocess.run(
["g++", "-O2", temp_file_path, "-o", "main"], ["g++", "-O2"] + cpp_files + ["-o", "main"],
capture_output=True, capture_output=True,
text=True, text=True,
cwd=tmpdir, cwd=tmpdir,
@@ -140,8 +180,16 @@ class CodeExecutor:
timeout=15, timeout=15,
) )
elif lang_key == "java": elif lang_key == "java":
java_files = []
for root, _, fnames in os.walk(tmpdir):
for fn in fnames:
if fn.endswith(".java"):
java_files.append(os.path.relpath(os.path.join(root, fn), tmpdir))
if not java_files:
java_files = [temp_file_name]
compile_res = subprocess.run( compile_res = subprocess.run(
["javac", temp_file_path], ["javac"] + java_files,
capture_output=True, capture_output=True,
text=True, text=True,
cwd=tmpdir, cwd=tmpdir,
@@ -154,8 +202,9 @@ class CodeExecutor:
"stdout": compile_res.stdout, "stdout": compile_res.stdout,
"stderr": f"Javac Compilation Error:\n{compile_res.stderr}", "stderr": f"Javac Compilation Error:\n{compile_res.stderr}",
} }
main_class = os.path.splitext(temp_file_name)[0]
result = subprocess.run( result = subprocess.run(
["java", "Main"], ["java", main_class],
input=stdin, input=stdin,
capture_output=True, capture_output=True,
text=True, text=True,
+28 -12
View File
@@ -10,7 +10,13 @@ from src.core.registry import registry
class CodeLinter: class CodeLinter:
"""Lints user code for syntax and style issues for 10 languages.""" """Lints user code for syntax and style issues for 10 languages."""
def lint(self, language: str, code: str) -> Dict[str, Any]: def lint(
self,
language: str,
code: str = "",
files: Dict[str, str] = None,
active_file: str = None,
) -> Dict[str, Any]:
config = registry.get_config(language) config = registry.get_config(language)
if not config: if not config:
return { return {
@@ -24,20 +30,30 @@ class CodeLinter:
ext = config.get("ext", ".txt") ext = config.get("ext", ".txt")
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
if lang_key == "java": if files and isinstance(files, dict) and len(files) > 0:
temp_file_name = "Main.java" for rel_path, content in files.items():
elif lang_key == "cpp": target_path = os.path.join(tmpdir, rel_path)
temp_file_name = "main.cpp" os.makedirs(os.path.dirname(target_path), exist_ok=True)
elif lang_key == "rust": with open(target_path, "w", encoding="utf-8") as f:
temp_file_name = "main.rs" f.write(content)
elif lang_key == "csharp": temp_file_name = active_file if active_file and active_file in files else list(files.keys())[0]
temp_file_name = "Program.cs"
else: else:
temp_file_name = f"main{ext}" if lang_key == "java":
temp_file_name = "Main.java"
elif lang_key == "cpp":
temp_file_name = "main.cpp"
elif lang_key == "rust":
temp_file_name = "main.rs"
elif lang_key == "csharp":
temp_file_name = "Program.cs"
else:
temp_file_name = f"main{ext}"
temp_path = os.path.join(tmpdir, temp_file_name)
with open(temp_path, "w", encoding="utf-8") as f:
f.write(code)
temp_path = os.path.join(tmpdir, temp_file_name) temp_path = os.path.join(tmpdir, temp_file_name)
with open(temp_path, "w", encoding="utf-8") as f:
f.write(code)
try: try:
if lang_key == "python": if lang_key == "python":
+24 -6
View File
@@ -21,10 +21,17 @@ class MentorClient:
self.config = config self.config = config
async def get_guidance_async( async def get_guidance_async(
self, challenge: object, user_code: str, user_question: str = "" self,
challenge: object,
user_code: str = "",
user_question: str = "",
files: Optional[Dict[str, str]] = None,
active_file: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Fetch mentor guidance asynchronously from the configured LLM endpoint.""" """Fetch mentor guidance asynchronously from the configured LLM endpoint."""
prompt = get_mentor_prompt(challenge, user_code, user_question=user_question) prompt = get_mentor_prompt(
challenge, user_code, user_question=user_question, files=files, active_file=active_file
)
endpoint = get_chat_completions_url(self.config.llm_base_url) endpoint = get_chat_completions_url(self.config.llm_base_url)
user_content = ( user_content = (
@@ -92,7 +99,12 @@ class MentorClient:
} }
def get_guidance( def get_guidance(
self, challenge: object, user_code: str, user_question: str = "" self,
challenge: object,
user_code: str = "",
user_question: str = "",
files: Optional[Dict[str, str]] = None,
active_file: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Synchronous wrapper for fetching mentor guidance.""" """Synchronous wrapper for fetching mentor guidance."""
import asyncio import asyncio
@@ -103,15 +115,21 @@ class MentorClient:
import nest_asyncio import nest_asyncio
nest_asyncio.apply() nest_asyncio.apply()
return loop.run_until_complete( return loop.run_until_complete(
self.get_guidance_async(challenge, user_code, user_question=user_question) self.get_guidance_async(
challenge, user_code, user_question=user_question, files=files, active_file=active_file
)
) )
else: else:
return asyncio.run( return asyncio.run(
self.get_guidance_async(challenge, user_code, user_question=user_question) self.get_guidance_async(
challenge, user_code, user_question=user_question, files=files, active_file=active_file
)
) )
except Exception: except Exception:
return asyncio.run( return asyncio.run(
self.get_guidance_async(challenge, user_code, user_question=user_question) self.get_guidance_async(
challenge, user_code, user_question=user_question, files=files, active_file=active_file
)
) )
+80 -2
View File
@@ -41,7 +41,81 @@ Your ultimate mission is to build the user's confidence in designing and constru
SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR
def get_mentor_prompt(challenge: object, user_code: str, user_question: str = "") -> str: from typing import Dict, Optional
def compress_file_content(filename: str, content: str, max_lines: int = 30) -> str:
"""Compresses file content by extracting signatures and truncating bodies if context limit is reached."""
lines = content.splitlines()
if len(lines) <= max_lines:
return content
headers = []
for line in lines:
stripped = line.strip()
if (
stripped.startswith(("#include", "import ", "from ", "using ", "package "))
or stripped.startswith(("def ", "class ", "struct ", "fn ", "interface ", "enum ", "public ", "private "))
or stripped.endswith(("{", ":"))
):
headers.append(line)
if len(headers) >= 5:
return "\n".join(headers[:max_lines]) + f"\n... [{len(lines) - len(headers)} lines omitted]"
else:
return "\n".join(lines[:max_lines]) + f"\n... [{len(lines) - max_lines} lines omitted]"
def format_multifile_context(
user_code: str | Dict[str, str] = "",
files: Optional[Dict[str, str]] = None,
active_file: Optional[str] = None,
max_chars: int = 12000,
) -> str:
"""Formats single or multi-file project code for the LLM mentor, applying context compression if needed."""
if isinstance(user_code, dict) and files is None:
files = user_code
user_code = ""
if not files or not isinstance(files, dict):
code_str = str(user_code or "")
return code_str if code_str.strip() else "# No code written yet"
active_name = active_file if active_file and active_file in files else list(files.keys())[0]
total_len = sum(len(c) for c in files.values())
should_compress = total_len > max_chars
parts = []
file_list_summary = "Project Structure:\n" + "\n".join(
f"- {fname}{' (Active)' if fname == active_name else ''}" for fname in files.keys()
)
parts.append(file_list_summary)
# 1. Active file first (kept in full)
active_content = files[active_name]
parts.append(f"=== File: {active_name} (Active) ===\n{active_content}")
# 2. Inactive files (compressed if overall context is large)
for fname, content in files.items():
if fname == active_name:
continue
if should_compress and not fname.endswith((".h", ".hpp", ".d.ts")):
compressed = compress_file_content(fname, content)
parts.append(f"=== File: {fname} (Compressed) ===\n{compressed}")
else:
parts.append(f"=== File: {fname} ===\n{content}")
return "\n\n".join(parts)
def get_mentor_prompt(
challenge: object,
user_code: str,
user_question: str = "",
files: Optional[Dict[str, str]] = None,
active_file: Optional[str] = None,
) -> str:
"""Returns the formatted system prompt for the LLM mentor.""" """Returns the formatted system prompt for the LLM mentor."""
reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", [])) reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", []))
hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", [])) hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", []))
@@ -52,6 +126,10 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = ""
else "How should I structure my program and overcome my current blocker for this challenge?" else "How should I structure my program and overcome my current blocker for this challenge?"
) )
formatted_code = format_multifile_context(
user_code=user_code, files=files, active_file=active_file
)
return SYSTEM_PROMPT_MENTOR.format( return SYSTEM_PROMPT_MENTOR.format(
challenge_name=getattr(challenge, "name", "Coding Challenge"), challenge_name=getattr(challenge, "name", "Coding Challenge"),
language=getattr(challenge, "language", "Python"), language=getattr(challenge, "language", "Python"),
@@ -59,7 +137,7 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = ""
challenge_description=getattr(challenge, "description", ""), challenge_description=getattr(challenge, "description", ""),
requirements=reqs if reqs else "- Follow standard problem specifications", requirements=reqs if reqs else "- Follow standard problem specifications",
hints=hints if hints else "- Think through edge cases", hints=hints if hints else "- Think through edge cases",
user_code=user_code if user_code.strip() else "# No code written yet", user_code=formatted_code,
user_question=q_text, user_question=q_text,
) )
+772 -65
View File
File diff suppressed because it is too large Load Diff
+154
View File
@@ -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()
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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"
}
+12
View File
@@ -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