Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa62722c6 | ||
|
|
0559f7549a |
@@ -1,7 +1,7 @@
|
|||||||
.PHONY: tui gentui tui-debug tui-test web install stop
|
.PHONY: tui gentui tui-debug tui-test web install stop
|
||||||
|
|
||||||
install:
|
install:
|
||||||
uv pip install fastapi uvicorn textual httpx tree-sitter-markdown tree-sitter-python tree-sitter-java tree-sitter-cpp tree-sitter-rust tree-sitter-go tree-sitter-javascript tree-sitter-typescript tree-sitter-c-sharp tree-sitter-html
|
uv pip install fastapi uvicorn textual httpx tree-sitter tree-sitter-markdown tree-sitter-python tree-sitter-java tree-sitter-cpp tree-sitter-rust tree-sitter-go tree-sitter-javascript tree-sitter-typescript tree-sitter-c-sharp tree-sitter-html tree-sitter-lua
|
||||||
devbox run -- bash -c "cd frontend && npm install"
|
devbox run -- bash -c "cd frontend && npm install"
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"enable_boilerplate": false,
|
||||||
|
"llm": {
|
||||||
|
"base_url": "http://127.0.0.1:1010",
|
||||||
|
"model": "local-model",
|
||||||
|
"api_key": "not-needed",
|
||||||
|
"temperature": 0.9,
|
||||||
|
"max_tokens": 8120,
|
||||||
|
"timeout_seconds": 120.0
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8000,
|
||||||
|
"public": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Editor } from '@monaco-editor/react';
|
import { Editor } from '@monaco-editor/react';
|
||||||
import { registerMonacoCompletions } from '../utils/monacoCompletions';
|
import { registerMonacoCompletions, getMonacoLanguage, getLanguageFileName } from '../utils/monacoCompletions';
|
||||||
import {
|
import {
|
||||||
getChallenges,
|
getChallenges,
|
||||||
runCode,
|
runCode,
|
||||||
@@ -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(currentChallenge.language.toLowerCase() || 'python', 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(currentChallenge.language.toLowerCase() || 'python', 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,
|
||||||
currentChallenge.language.toLowerCase() || 'python',
|
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">main.py</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={currentChallenge?.language.toLowerCase() || 'python'}
|
defaultLanguage={getLanguageFromFilename(activeFile)}
|
||||||
language={currentChallenge?.language.toLowerCase() || 'python'}
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,191 @@
|
|||||||
import type { Monaco } from '@monaco-editor/react';
|
import type { Monaco } from '@monaco-editor/react';
|
||||||
|
|
||||||
|
export function getMonacoLanguage(language?: string): string {
|
||||||
|
if (!language) return 'python';
|
||||||
|
const norm = language.toLowerCase().trim();
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'c++': 'cpp',
|
||||||
|
'cpp': 'cpp',
|
||||||
|
'cxx': 'cpp',
|
||||||
|
'c#': 'csharp',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'csharp': 'csharp',
|
||||||
|
'js': 'javascript',
|
||||||
|
'javascript': 'javascript',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'typescript': 'typescript',
|
||||||
|
'py': 'python',
|
||||||
|
'python': 'python',
|
||||||
|
'rs': 'rust',
|
||||||
|
'rust': 'rust',
|
||||||
|
'golang': 'go',
|
||||||
|
'go': 'go',
|
||||||
|
'html': 'html',
|
||||||
|
'java': 'java',
|
||||||
|
'lua': 'lua',
|
||||||
|
};
|
||||||
|
return map[norm] || norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLanguageFileName(language?: string): string {
|
||||||
|
const lang = getMonacoLanguage(language);
|
||||||
|
const extMap: Record<string, string> = {
|
||||||
|
python: 'main.py',
|
||||||
|
csharp: 'Program.cs',
|
||||||
|
cpp: 'main.cpp',
|
||||||
|
java: 'Main.java',
|
||||||
|
javascript: 'main.js',
|
||||||
|
typescript: 'main.ts',
|
||||||
|
rust: 'main.rs',
|
||||||
|
lua: 'main.lua',
|
||||||
|
html: 'index.html',
|
||||||
|
go: 'main.go',
|
||||||
|
};
|
||||||
|
return extMap[lang] || 'main.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDocumentSymbols(code: string): string[] {
|
||||||
|
const words = code.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
|
||||||
|
const unique = new Set<string>();
|
||||||
|
const reserved = new Set([
|
||||||
|
'if', 'else', 'for', 'while', 'return', 'import', 'from', 'def', 'class',
|
||||||
|
'public', 'private', 'protected', 'static', 'void', 'int', 'double', 'float',
|
||||||
|
'bool', 'boolean', 'char', 'const', 'let', 'var', 'func', 'package', 'struct',
|
||||||
|
]);
|
||||||
|
for (const w of words) {
|
||||||
|
if (w.length > 2 && !reserved.has(w)) {
|
||||||
|
unique.add(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(unique);
|
||||||
|
}
|
||||||
|
|
||||||
let registered = false;
|
let registered = false;
|
||||||
|
|
||||||
export function registerMonacoCompletions(monaco: Monaco) {
|
export function registerMonacoCompletions(monaco: Monaco) {
|
||||||
if (registered) return;
|
if (registered) return;
|
||||||
registered = true;
|
registered = true;
|
||||||
|
|
||||||
// Completion items for Python
|
// ── JAVA COMPLETER ──────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('java', {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1];
|
||||||
|
const lowerName = objName.toLowerCase();
|
||||||
|
let targetType = 'unknown';
|
||||||
|
|
||||||
|
if (new RegExp(`Map<|HashMap<|TreeMap<`, 'i').test(code) && (lowerName.includes('map') || lowerName.includes('dict'))) {
|
||||||
|
targetType = 'map';
|
||||||
|
} else if (new RegExp(`List<|ArrayList<|LinkedList<`, 'i').test(code) && (lowerName.includes('list') || lowerName.includes('arr') || lowerName.includes('items'))) {
|
||||||
|
targetType = 'list';
|
||||||
|
} else if (new RegExp(`Set<|HashSet<`, 'i').test(code) && lowerName.includes('set')) {
|
||||||
|
targetType = 'set';
|
||||||
|
} else if (lowerName === 'out' || objName === 'System') {
|
||||||
|
targetType = 'system';
|
||||||
|
} else if (lowerName.includes('str') || lowerName.includes('text') || lowerName.includes('name') || lowerName.includes('msg')) {
|
||||||
|
targetType = 'string';
|
||||||
|
} else {
|
||||||
|
if (lowerName.includes('map') || lowerName.includes('dict')) targetType = 'map';
|
||||||
|
else if (lowerName.includes('list') || lowerName.includes('arr')) targetType = 'list';
|
||||||
|
else if (lowerName.includes('set')) targetType = 'set';
|
||||||
|
else targetType = 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapMethods = [
|
||||||
|
{ label: 'put', insertText: 'put(${1:key}, ${2:value})', doc: 'Associates specified value with key in map.' },
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Returns value mapped to specified key.' },
|
||||||
|
{ label: 'containsKey', insertText: 'containsKey(${1:key})', doc: 'Returns true if map contains key.' },
|
||||||
|
{ label: 'containsValue', insertText: 'containsValue(${1:value})', doc: 'Returns true if map contains value.' },
|
||||||
|
{ label: 'size', insertText: 'size()', doc: 'Returns number of key-value mappings.' },
|
||||||
|
{ label: 'isEmpty', insertText: 'isEmpty()', doc: 'Returns true if map contains no mappings.' },
|
||||||
|
{ label: 'keySet', insertText: 'keySet()', doc: 'Returns Set view of keys in map.' },
|
||||||
|
{ label: 'values', insertText: 'values()', doc: 'Returns Collection view of values in map.' },
|
||||||
|
{ label: 'entrySet', insertText: 'entrySet()', doc: 'Returns Set view of mappings in map.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:key})', doc: 'Removes mapping for key.' },
|
||||||
|
{ label: 'clear', insertText: 'clear()', doc: 'Removes all mappings from map.' },
|
||||||
|
{ label: 'getOrDefault', insertText: 'getOrDefault(${1:key}, ${2:defaultValue})', doc: 'Returns mapped value or default.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const listMethods = [
|
||||||
|
{ label: 'add', insertText: 'add(${1:element})', doc: 'Appends element to end of list.' },
|
||||||
|
{ label: 'get', insertText: 'get(${1:index})', doc: 'Returns element at index.' },
|
||||||
|
{ label: 'size', insertText: 'size()', doc: 'Returns number of elements.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:index})', doc: 'Removes element at index.' },
|
||||||
|
{ label: 'contains', insertText: 'contains(${1:element})', doc: 'Returns true if list contains element.' },
|
||||||
|
{ label: 'indexOf', insertText: 'indexOf(${1:element})', doc: 'Returns index of element.' },
|
||||||
|
{ label: 'isEmpty', insertText: 'isEmpty()', doc: 'Returns true if list contains no elements.' },
|
||||||
|
{ label: 'clear', insertText: 'clear()', doc: 'Removes all elements.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stringMethods = [
|
||||||
|
{ label: 'length', insertText: 'length()', doc: 'Returns length of string.' },
|
||||||
|
{ label: 'substring', insertText: 'substring(${1:beginIndex})', doc: 'Returns substring starting at beginIndex.' },
|
||||||
|
{ label: 'charAt', insertText: 'charAt(${1:index})', doc: 'Returns char value at index.' },
|
||||||
|
{ label: 'toLowerCase', insertText: 'toLowerCase()', doc: 'Converts to lowercase.' },
|
||||||
|
{ label: 'toUpperCase', insertText: 'toUpperCase()', doc: 'Converts to uppercase.' },
|
||||||
|
{ label: 'trim', insertText: 'trim()', doc: 'Removes leading/trailing whitespace.' },
|
||||||
|
{ label: 'split', insertText: 'split("${1:regex}")', doc: 'Splits string around matches.' },
|
||||||
|
{ label: 'contains', insertText: 'contains("${1:str}")', doc: 'Returns true if contains string.' },
|
||||||
|
{ label: 'startsWith', insertText: 'startsWith("${1:prefix}")', doc: 'Checks if starts with prefix.' },
|
||||||
|
{ label: 'equals', insertText: 'equals(${1:anObject})', doc: 'Compares to specified object.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const systemMethods = [
|
||||||
|
{ label: 'println', insertText: 'println(${1:value});', doc: 'Prints value and terminates line.' },
|
||||||
|
{ label: 'printf', insertText: 'printf("${1:%s}\\n", ${2:args});', doc: 'Prints formatted string.' },
|
||||||
|
{ label: 'print', insertText: 'print(${1:value});', doc: 'Prints value.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let selected = mapMethods;
|
||||||
|
if (targetType === 'list') selected = listMethods;
|
||||||
|
else if (targetType === 'string') selected = stringMethods;
|
||||||
|
else if (targetType === 'system') selected = systemMethods;
|
||||||
|
else if (targetType === 'general') selected = [...mapMethods, ...listMethods, ...stringMethods];
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: selected.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
const suggestions = [
|
||||||
|
{ label: 'System.out.println', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'System.out.println(${1:value});', range },
|
||||||
|
{ label: 'HashMap', kind: monaco.languages.CompletionItemKind.Class, insertText: 'Map<${1:String}, ${2:Integer}> ${3:map} = new HashMap<>();', range },
|
||||||
|
{ label: 'ArrayList', kind: monaco.languages.CompletionItemKind.Class, insertText: 'List<${1:String}> ${2:list} = new ArrayList<>();', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
];
|
||||||
|
|
||||||
|
return { suggestions };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── PYTHON COMPLETER ────────────────────────────────────────
|
||||||
monaco.languages.registerCompletionItemProvider('python', {
|
monaco.languages.registerCompletionItemProvider('python', {
|
||||||
triggerCharacters: ['.'],
|
triggerCharacters: ['.'],
|
||||||
provideCompletionItems: (model, position) => {
|
provideCompletionItems: (model, position) => {
|
||||||
@@ -25,70 +204,194 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Returns value for key.' },
|
||||||
|
{ label: 'keys', insertText: 'keys()', doc: 'Returns dictionary keys.' },
|
||||||
|
{ label: 'values', insertText: 'values()', doc: 'Returns dictionary values.' },
|
||||||
|
{ label: 'items', insertText: 'items()', doc: 'Returns key-value pairs.' },
|
||||||
|
{ label: 'append', insertText: 'append(${1:item})', doc: 'Appends item to list.' },
|
||||||
|
{ label: 'pop', insertText: 'pop(${1:index})', doc: 'Removes and returns item.' },
|
||||||
|
{ label: 'split', insertText: 'split("${1:sep}")', doc: 'Splits string.' },
|
||||||
|
{ label: 'join', insertText: 'join(${1:iterable})', doc: 'Joins elements.' },
|
||||||
|
];
|
||||||
|
if (objName.includes('map') || objName.includes('dict')) {
|
||||||
|
methods = [
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Returns value for key.' },
|
||||||
|
{ label: 'keys', insertText: 'keys()', doc: 'Returns dictionary keys.' },
|
||||||
|
{ label: 'values', insertText: 'values()', doc: 'Returns dictionary values.' },
|
||||||
|
{ label: 'items', insertText: 'items()', doc: 'Returns key-value pairs.' },
|
||||||
|
{ label: 'update', insertText: 'update(${1:dict})', doc: 'Updates dictionary.' },
|
||||||
|
{ label: 'pop', insertText: 'pop(${1:key})', doc: 'Removes key.' },
|
||||||
|
];
|
||||||
|
} else if (objName.includes('list') || objName.includes('arr')) {
|
||||||
|
methods = [
|
||||||
|
{ label: 'append', insertText: 'append(${1:item})', doc: 'Appends item.' },
|
||||||
|
{ label: 'extend', insertText: 'extend(${1:iterable})', doc: 'Extends list.' },
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:index}, ${2:item})', doc: 'Inserts item.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:item})', doc: 'Removes item.' },
|
||||||
|
{ label: 'sort', insertText: 'sort()', doc: 'Sorts list.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
const suggestions = [
|
const suggestions = [
|
||||||
{
|
{ label: 'print', kind: monaco.languages.CompletionItemKind.Function, insertText: 'print(${1:value})', range },
|
||||||
label: 'print',
|
{ label: 'len', kind: monaco.languages.CompletionItemKind.Function, insertText: 'len(${1:obj})', range },
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
{ label: 'range', kind: monaco.languages.CompletionItemKind.Function, insertText: 'range(${1:stop})', range },
|
||||||
insertText: 'print(${1:value})',
|
{ label: 'enumerate', kind: monaco.languages.CompletionItemKind.Function, insertText: 'enumerate(${1:iterable})', range },
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
documentation: 'Prints specified objects to standard output.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'len',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'len(${1:obj})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Returns the number of items in a container.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'range',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'range(${1:stop})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Generates a sequence of numbers.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'enumerate',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'enumerate(${1:iterable})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Yields index and value tuples from an iterable.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'append',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'append(${1:item})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Appends a new item to the end of the list.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'split',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'split(${1:sep})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Splits string into a list of substrings.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'def',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: 'def ${1:function_name}(${2:params}):\n ${3:pass}',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Defines a Python function.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return { suggestions };
|
return { suggestions };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Completion items for JavaScript / TypeScript
|
// ── C++ COMPLETER ───────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('cpp', {
|
||||||
|
triggerCharacters: ['.', ':', '>'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*|\bstd\b)(\.|\:\:|->)\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'push_back', insertText: 'push_back(${1:val})', doc: 'Appends element.' },
|
||||||
|
{ label: 'pop_back', insertText: 'pop_back()', doc: 'Removes last element.' },
|
||||||
|
{ label: 'size', insertText: 'size()', doc: 'Returns number of elements.' },
|
||||||
|
{ label: 'empty', insertText: 'empty()', doc: 'Checks if container is empty.' },
|
||||||
|
{ label: 'find', insertText: 'find(${1:key})', doc: 'Finds element.' },
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:val})', doc: 'Inserts element.' },
|
||||||
|
{ label: 'clear', insertText: 'clear()', doc: 'Clears all elements.' },
|
||||||
|
];
|
||||||
|
if (objName === 'std') {
|
||||||
|
methods = [
|
||||||
|
{ label: 'cout', insertText: 'cout << ${1:value} << std::endl;', doc: 'Standard output stream.' },
|
||||||
|
{ label: 'cin', insertText: 'cin >> ${1:var};', doc: 'Standard input stream.' },
|
||||||
|
{ label: 'vector', insertText: 'vector<${1:int}> ${2:vec};', doc: 'Dynamic array container.' },
|
||||||
|
{ label: 'map', insertText: 'map<${1:string}, ${2:int}> ${3:map};', doc: 'Sorted associative container.' },
|
||||||
|
{ label: 'sort', insertText: 'sort(${1:begin}, ${2:end});', doc: 'Sorts elements.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'std::cout', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'std::cout << ${1:value} << std::endl;', range },
|
||||||
|
{ label: 'std::vector', kind: monaco.languages.CompletionItemKind.Class, insertText: 'std::vector<${1:int}> ${2:vec};', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── C# COMPLETER ────────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('csharp', {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'Add', insertText: 'Add(${1:val});', doc: 'Adds value.' },
|
||||||
|
{ label: 'Remove', insertText: 'Remove(${1:val});', doc: 'Removes value.' },
|
||||||
|
{ label: 'ContainsKey', insertText: 'ContainsKey(${1:key})', doc: 'Checks if dictionary contains key.' },
|
||||||
|
{ label: 'Count', insertText: 'Count', doc: 'Gets number of elements.' },
|
||||||
|
{ label: 'Clear', insertText: 'Clear()', doc: 'Removes all elements.' },
|
||||||
|
];
|
||||||
|
if (objName === 'console') {
|
||||||
|
methods = [
|
||||||
|
{ label: 'WriteLine', insertText: 'WriteLine(${1:value});', doc: 'Writes value to standard output.' },
|
||||||
|
{ label: 'ReadLine', insertText: 'ReadLine()', doc: 'Reads next line.' },
|
||||||
|
{ label: 'Write', insertText: 'Write(${1:value});', doc: 'Writes value.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'Console.WriteLine', kind: monaco.languages.CompletionItemKind.Method, insertText: 'Console.WriteLine(${1:value});', range },
|
||||||
|
{ label: 'Dictionary', kind: monaco.languages.CompletionItemKind.Class, insertText: 'Dictionary<${1:string}, ${2:int}> ${3:dict} = new Dictionary<${1:string}, ${2:int}>();', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── JS & TS COMPLETER ───────────────────────────────────────
|
||||||
['javascript', 'typescript'].forEach((lang) => {
|
['javascript', 'typescript'].forEach((lang) => {
|
||||||
monaco.languages.registerCompletionItemProvider(lang, {
|
monaco.languages.registerCompletionItemProvider(lang, {
|
||||||
triggerCharacters: ['.'],
|
triggerCharacters: ['.'],
|
||||||
@@ -108,73 +411,62 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lineUntilPosition.endsWith('console.')) {
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'map', insertText: 'map((${1:x}) => ${2:x})', doc: 'Creates a new array with mapped elements.' },
|
||||||
|
{ label: 'filter', insertText: 'filter((${1:x}) => ${2:true})', doc: 'Filters array.' },
|
||||||
|
{ label: 'push', insertText: 'push(${1:item})', doc: 'Appends element.' },
|
||||||
|
{ label: 'slice', insertText: 'slice(${1:start}, ${2:end})', doc: 'Returns section of array.' },
|
||||||
|
{ label: 'length', insertText: 'length', doc: 'Gets length.' },
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Gets Map element.' },
|
||||||
|
{ label: 'set', insertText: 'set(${1:key}, ${2:val})', doc: 'Sets Map element.' },
|
||||||
|
];
|
||||||
|
if (objName === 'console') {
|
||||||
|
methods = [
|
||||||
|
{ label: 'log', insertText: 'log(${1:msg});', doc: 'Outputs message.' },
|
||||||
|
{ label: 'error', insertText: 'error(${1:err});', doc: 'Outputs error.' },
|
||||||
|
{ label: 'warn', insertText: 'warn(${1:msg});', doc: 'Outputs warning.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
suggestions: [
|
suggestions: methods.map(m => ({
|
||||||
{
|
label: m.label,
|
||||||
label: 'log',
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
insertText: m.insertText,
|
||||||
insertText: 'log(${1:message});',
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
documentation: m.doc,
|
||||||
documentation: 'Outputs a message to the debugging console.',
|
range,
|
||||||
range,
|
})),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'error',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'error(${1:err});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Outputs an error message to the console.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'warn',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'warn(${1:msg});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Outputs a warning message to the console.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = [
|
const symbols = extractDocumentSymbols(code);
|
||||||
{
|
return {
|
||||||
label: 'console.log',
|
suggestions: [
|
||||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
{ label: 'console.log', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'console.log(${1:val});', range },
|
||||||
insertText: 'console.log(${1:val});',
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
],
|
||||||
documentation: 'Print debug message to console.',
|
};
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'map',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'map((${1:item}) => ${2:item})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Array map transformation function.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'filter',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'filter((${1:item}) => ${2:true})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Array filter function.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return { suggestions };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Completion items for Rust
|
// ── RUST COMPLETER ──────────────────────────────────────────
|
||||||
monaco.languages.registerCompletionItemProvider('rust', {
|
monaco.languages.registerCompletionItemProvider('rust', {
|
||||||
triggerCharacters: ['.', ':'],
|
triggerCharacters: ['.', ':'],
|
||||||
provideCompletionItems: (model, position) => {
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
const word = model.getWordUntilPosition(position);
|
const word = model.getWordUntilPosition(position);
|
||||||
const range = {
|
const range = {
|
||||||
startLineNumber: position.lineNumber,
|
startLineNumber: position.lineNumber,
|
||||||
@@ -183,38 +475,43 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
const suggestions = [
|
const code = model.getValue();
|
||||||
{
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*|\bVec|\bHashMap)(\.|\:\:)\s*$/);
|
||||||
label: 'println!',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'println!("${1:{}}", ${2:val});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Prints formatted text to stdout.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'format!',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'format!("${1:{}}", ${2:val})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Constructs a formatted String.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Vec::push',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'push(${1:val});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Appends element to back of vector.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return { suggestions };
|
if (dotMatch) {
|
||||||
|
let methods = [
|
||||||
|
{ label: 'push', insertText: 'push(${1:val});', doc: 'Appends element.' },
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:key}, ${2:val});', doc: 'Inserts key-value pair.' },
|
||||||
|
{ label: 'get', insertText: 'get(&${1:key})', doc: 'Returns reference to value.' },
|
||||||
|
{ label: 'len', insertText: 'len()', doc: 'Returns length.' },
|
||||||
|
{ label: 'is_empty', insertText: 'is_empty()', doc: 'Checks if empty.' },
|
||||||
|
{ label: 'iter', insertText: 'iter()', doc: 'Returns iterator.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'println!', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'println!("${1:{}}", ${2:val});', range },
|
||||||
|
{ label: 'Vec::new', kind: monaco.languages.CompletionItemKind.Function, insertText: 'Vec::new()', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Completion items for Go
|
// ── GO COMPLETER ────────────────────────────────────────────
|
||||||
monaco.languages.registerCompletionItemProvider('go', {
|
monaco.languages.registerCompletionItemProvider('go', {
|
||||||
triggerCharacters: ['.'],
|
triggerCharacters: ['.'],
|
||||||
provideCompletionItems: (model, position) => {
|
provideCompletionItems: (model, position) => {
|
||||||
@@ -233,49 +530,113 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lineUntilPosition.endsWith('fmt.')) {
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
let methods = [
|
||||||
|
{ label: 'Println', insertText: 'Println(${1:v})', doc: 'Writes formatted line.' },
|
||||||
|
{ label: 'Printf', insertText: 'Printf("${1:%v}\\n", ${2:v})', doc: 'Writes formatted string.' },
|
||||||
|
{ label: 'Split', insertText: 'Split(${1:s}, "${2:sep}")', doc: 'Splits string.' },
|
||||||
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
suggestions: [
|
suggestions: methods.map(m => ({
|
||||||
{
|
label: m.label,
|
||||||
label: 'Println',
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
insertText: m.insertText,
|
||||||
insertText: 'Println(${1:v})',
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
documentation: m.doc,
|
||||||
documentation: 'Formats using default formats and writes to standard output.',
|
range,
|
||||||
range,
|
})),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Printf',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'Printf("${1:%v}\\n", ${2:v})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Formats according to specifier and writes to standard output.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
return {
|
return {
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{
|
{ label: 'fmt.Println', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'fmt.Println(${1:v})', range },
|
||||||
label: 'fmt.Println',
|
{ label: 'make', kind: monaco.languages.CompletionItemKind.Function, insertText: 'make(${1:type}, ${2:len})', range },
|
||||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
insertText: 'fmt.Println(${1:v})',
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── LUA COMPLETER ───────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('lua', {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
let methods = [
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:t}, ${2:val})', doc: 'Inserts element.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:t}, ${2:pos})', doc: 'Removes element.' },
|
||||||
|
{ label: 'sub', insertText: 'sub(${1:s}, ${2:i}, ${3:j})', doc: 'Returns substring.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
documentation: 'Print line to stdout.',
|
documentation: m.doc,
|
||||||
range,
|
range,
|
||||||
},
|
})),
|
||||||
{
|
};
|
||||||
label: 'make',
|
}
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'make(${1:type}, ${2:len})',
|
const symbols = extractDocumentSymbols(code);
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
return {
|
||||||
documentation: 'Allocates slice, map, or channel.',
|
suggestions: [
|
||||||
range,
|
{ label: 'print', kind: monaco.languages.CompletionItemKind.Function, insertText: 'print(${1:value})', range },
|
||||||
},
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── HTML COMPLETER ──────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('html', {
|
||||||
|
triggerCharacters: ['<'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'div', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<div>${1}</div>', range },
|
||||||
|
{ label: 'span', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<span>${1}</span>', range },
|
||||||
|
{ label: 'p', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<p>${1}</p>', range },
|
||||||
|
{ label: 'h1', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<h1>${1}</h1>', range },
|
||||||
|
{ label: 'button', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<button>${1:Click}</button>', range },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Convert themes from convert-themes/registry.json into src/tui/themes/*.json"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REGISTRY_PATH = Path("convert-themes/registry.json")
|
||||||
|
OUTPUT_DIR = Path("src/tui/themes")
|
||||||
|
|
||||||
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not REGISTRY_PATH.exists():
|
||||||
|
print(f"Error: {REGISTRY_PATH} not found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
with open(REGISTRY_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
themes_list = data.get("themes", [])
|
||||||
|
converted_count = 0
|
||||||
|
|
||||||
|
for item in themes_list:
|
||||||
|
name = item.get("name")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dark = item.get("dark") or item.get("light") or {}
|
||||||
|
if not dark:
|
||||||
|
continue
|
||||||
|
|
||||||
|
theme_id = re.sub(r'[^a-z0-9\-]', '', name.lower().replace(' ', '-'))
|
||||||
|
if not theme_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
background = dark.get("mSurfaceVariant") or dark.get("mSurface") or "#1e1e1e"
|
||||||
|
surface = dark.get("mSurface") or "#252526"
|
||||||
|
primary = dark.get("mPrimary") or "#3b82f6"
|
||||||
|
primary_text = dark.get("mOnPrimary") or "#ffffff"
|
||||||
|
secondary = dark.get("mSecondary") or "#64748b"
|
||||||
|
accent = dark.get("mTertiary") or dark.get("mHover") or primary
|
||||||
|
folder_header = accent
|
||||||
|
folder_text = dark.get("mOnTertiary") or dark.get("mOnHover") or "#ffffff"
|
||||||
|
|
||||||
|
theme_dict = {
|
||||||
|
"id": theme_id,
|
||||||
|
"name": name,
|
||||||
|
"background": background,
|
||||||
|
"surface": surface,
|
||||||
|
"primary": primary,
|
||||||
|
"primary_text": primary_text,
|
||||||
|
"secondary": secondary,
|
||||||
|
"accent": accent,
|
||||||
|
"folder_header": folder_header,
|
||||||
|
"folder_text": folder_text
|
||||||
|
}
|
||||||
|
|
||||||
|
out_file = OUTPUT_DIR / f"{theme_id}.json"
|
||||||
|
with open(out_file, "w", encoding="utf-8") as f_out:
|
||||||
|
json.dump(theme_dict, f_out, indent=2)
|
||||||
|
converted_count += 1
|
||||||
|
|
||||||
|
print(f"Successfully converted {converted_count} themes to {OUTPUT_DIR}/!")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import asyncio
|
||||||
|
from src.tui.app import TactiTermTUI
|
||||||
|
|
||||||
|
async def test_command_palette_integration():
|
||||||
|
app = TactiTermTUI()
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
print("Available themes count registered with Textual App:", len(app.available_themes))
|
||||||
|
sample_themes = list(app.available_themes.keys())[:15]
|
||||||
|
print("Sample registered themes in App:", sample_themes)
|
||||||
|
|
||||||
|
# Test system commands
|
||||||
|
commands = list(app.get_system_commands(app.screen))
|
||||||
|
cmd_titles = [c.title for c in commands]
|
||||||
|
print("System commands in Command Palette:", cmd_titles)
|
||||||
|
|
||||||
|
assert "Keys" in cmd_titles, "Keys option missing from Command Palette"
|
||||||
|
assert "Themes" in cmd_titles, "Themes option missing from Command Palette"
|
||||||
|
|
||||||
|
# Test applying a theme from registered themes
|
||||||
|
app.apply_theme("dracula")
|
||||||
|
await pilot.pause(0.1)
|
||||||
|
print("Main box classes after applying Dracula:", app.query_one("#main-container").classes)
|
||||||
|
assert app.query_one("#main-container").has_class("theme-dracula")
|
||||||
|
print("Command Palette Theme Integration test PASSED!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_command_palette_integration())
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Integration test for multi-file code execution, linting, and prompt context formatting."""
|
||||||
|
import unittest
|
||||||
|
from src.core.executor import CodeExecutor
|
||||||
|
from src.core.linter import CodeLinter
|
||||||
|
from src.core.prompts import format_multifile_context, compress_file_content
|
||||||
|
|
||||||
|
class TestMultiFile(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.executor = CodeExecutor()
|
||||||
|
self.linter = CodeLinter()
|
||||||
|
|
||||||
|
def test_cpp_multifile_execution(self):
|
||||||
|
files = {
|
||||||
|
"main.cpp": '#include "main.h"\n#include <iostream>\nint main() { printMessage(); return 0; }\n',
|
||||||
|
"main.h": '#ifndef MAIN_H\n#define MAIN_H\n#include <iostream>\ninline void printMessage() { std::cout << "C++ Multi-File Success!"; }\n#endif\n'
|
||||||
|
}
|
||||||
|
res = self.executor.run("cpp", code="", stdin="", files=files)
|
||||||
|
self.assertEqual(res.get("exit_code"), 0)
|
||||||
|
self.assertIn("C++ Multi-File Success!", res.get("stdout"))
|
||||||
|
|
||||||
|
def test_python_multifile_execution(self):
|
||||||
|
files = {
|
||||||
|
"main.py": 'import helper\nprint(helper.get_msg())\n',
|
||||||
|
"helper.py": 'def get_msg(): return "Python Multi-File Success!"\n'
|
||||||
|
}
|
||||||
|
res = self.executor.run("python", code="", stdin="", files=files)
|
||||||
|
self.assertEqual(res.get("exit_code"), 0)
|
||||||
|
self.assertIn("Python Multi-File Success!", res.get("stdout"))
|
||||||
|
|
||||||
|
def test_multifile_context_formatting(self):
|
||||||
|
files = {
|
||||||
|
"main.cpp": '#include "main.h"\nint main() { return 0; }',
|
||||||
|
"main.h": '#define MSG "Hello"'
|
||||||
|
}
|
||||||
|
ctx = format_multifile_context(files, active_file="main.cpp")
|
||||||
|
self.assertIn("=== File: main.cpp (Active) ===", ctx)
|
||||||
|
self.assertIn("=== File: main.h ===", ctx)
|
||||||
|
|
||||||
|
def test_compression(self):
|
||||||
|
large_code = "def foo():\n pass\n" * 100
|
||||||
|
compressed = compress_file_content("test.py", large_code)
|
||||||
|
self.assertIn("def foo()", compressed)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from textual.theme import Theme
|
||||||
|
from src.tui.theme_manager import theme_manager
|
||||||
|
|
||||||
|
print("Loaded theme manager count:", len(theme_manager.themes))
|
||||||
|
t_sample = list(theme_manager.themes.values())[0]
|
||||||
|
print("Sample theme data:", t_sample)
|
||||||
|
|
||||||
|
t_obj = Theme(
|
||||||
|
name=t_sample["id"],
|
||||||
|
primary=t_sample["primary"],
|
||||||
|
secondary=t_sample.get("secondary"),
|
||||||
|
accent=t_sample.get("accent"),
|
||||||
|
background=t_sample["background"],
|
||||||
|
surface=t_sample["surface"],
|
||||||
|
dark=True
|
||||||
|
)
|
||||||
|
print("Created Textual Theme object successfully:", t_obj.name)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import asyncio
|
||||||
|
from src.tui.app import TactiTermTUI
|
||||||
|
from textual.widgets import OptionList
|
||||||
|
|
||||||
|
async def test_theme_popup():
|
||||||
|
app = TactiTermTUI()
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
print("Call action_cycle_theme()...")
|
||||||
|
app.action_cycle_theme()
|
||||||
|
await pilot.pause(0.2)
|
||||||
|
|
||||||
|
main_box = app.query_one("#main-container")
|
||||||
|
print("Is show-theme-menu in classes:", main_box.has_class("show-theme-menu"))
|
||||||
|
|
||||||
|
theme_list = app.query_one("#theme_list_popup", OptionList)
|
||||||
|
print("Theme list option count:", theme_list.option_count)
|
||||||
|
print("Focused widget ID:", getattr(app.focused, "id", None))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_theme_popup())
|
||||||
+29
-9
@@ -6,6 +6,7 @@ from src.core.loader import ChallengeLoader
|
|||||||
from src.core.executor import executor
|
from src.core.executor import executor
|
||||||
from src.core.linter import linter
|
from src.core.linter import linter
|
||||||
from src.core.prompts import get_socratic_prompt
|
from src.core.prompts import get_socratic_prompt
|
||||||
|
from src.core.config import config
|
||||||
|
|
||||||
app = FastAPI(title="Socratic Tutor API")
|
app = FastAPI(title="Socratic Tutor API")
|
||||||
|
|
||||||
@@ -20,25 +21,40 @@ app.add_middleware(
|
|||||||
loader = ChallengeLoader()
|
loader = ChallengeLoader()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/config")
|
||||||
|
def get_app_config():
|
||||||
|
return {
|
||||||
|
"enable_boilerplate": config.enable_boilerplate,
|
||||||
|
"llm_base_url": config.llm_base_url,
|
||||||
|
"llm_model": config.llm_model,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
class LintRequest(BaseModel):
|
class LintRequest(BaseModel):
|
||||||
language: str
|
language: str
|
||||||
code: str
|
code: Optional[str] = ""
|
||||||
|
files: Optional[Dict[str, str]] = None
|
||||||
|
active_file: Optional[str] = None
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class GuideRequest(BaseModel):
|
class GuideRequest(BaseModel):
|
||||||
challenge_id: str
|
challenge_id: str
|
||||||
language: str
|
language: str
|
||||||
code: str
|
code: Optional[str] = ""
|
||||||
question: Optional[str] = ""
|
question: Optional[str] = ""
|
||||||
|
files: Optional[Dict[str, str]] = None
|
||||||
|
active_file: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class RunRequest(BaseModel):
|
class RunRequest(BaseModel):
|
||||||
language: str
|
language: str
|
||||||
code: str
|
code: Optional[str] = ""
|
||||||
stdin: Optional[str] = ""
|
stdin: Optional[str] = ""
|
||||||
|
files: Optional[Dict[str, str]] = None
|
||||||
|
active_file: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/challenges")
|
@app.get("/challenges")
|
||||||
@@ -60,7 +76,7 @@ def get_challenge(challenge_id: str):
|
|||||||
@app.post("/lint")
|
@app.post("/lint")
|
||||||
def lint_code(req: LintRequest):
|
def lint_code(req: LintRequest):
|
||||||
"""Runs linting on user code using Core Linter."""
|
"""Runs linting on user code using Core Linter."""
|
||||||
res = linter.lint(req.language, req.code)
|
res = linter.lint(req.language, req.code or "", files=req.files, active_file=req.active_file)
|
||||||
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
||||||
raise HTTPException(status_code=404, detail=res["stderr"])
|
raise HTTPException(status_code=404, detail=res["stderr"])
|
||||||
return res
|
return res
|
||||||
@@ -69,7 +85,7 @@ def lint_code(req: LintRequest):
|
|||||||
@app.post("/run")
|
@app.post("/run")
|
||||||
def run_code(req: RunRequest):
|
def run_code(req: RunRequest):
|
||||||
"""Executes user code using Core Executor."""
|
"""Executes user code using Core Executor."""
|
||||||
res = executor.run(req.language, req.code, stdin=req.stdin or "")
|
res = executor.run(req.language, req.code or "", stdin=req.stdin or "", files=req.files)
|
||||||
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
if res.get("stderr") == f"Language '{req.language}' is not supported.":
|
||||||
raise HTTPException(status_code=404, detail=res["stderr"])
|
raise HTTPException(status_code=404, detail=res["stderr"])
|
||||||
return res
|
return res
|
||||||
@@ -100,7 +116,11 @@ async def guide_code(req: GuideRequest):
|
|||||||
|
|
||||||
challenge = challenges[req.challenge_id]
|
challenge = challenges[req.challenge_id]
|
||||||
guidance = await mentor.get_guidance_async(
|
guidance = await mentor.get_guidance_async(
|
||||||
challenge, req.code, user_question=req.question or ""
|
challenge,
|
||||||
|
user_code=req.code or "",
|
||||||
|
user_question=req.question or "",
|
||||||
|
files=req.files,
|
||||||
|
active_file=req.active_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+24
-3
@@ -8,13 +8,14 @@ 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",
|
||||||
"api_key": "not-needed",
|
"api_key": "not-needed",
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tokens": 512,
|
"max_tokens": 4096,
|
||||||
"timeout_seconds": 5.0,
|
"timeout_seconds": 60.0,
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
@@ -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
|
||||||
@@ -60,6 +66,17 @@ class Config:
|
|||||||
if env_model:
|
if env_model:
|
||||||
config["llm"]["model"] = env_model
|
config["llm"]["model"] = env_model
|
||||||
|
|
||||||
|
env_api_key = os.getenv("TACTTERM_LLM_API_KEY")
|
||||||
|
if env_api_key:
|
||||||
|
config["llm"]["api_key"] = env_api_key
|
||||||
|
|
||||||
|
env_timeout = os.getenv("TACTTERM_LLM_TIMEOUT")
|
||||||
|
if env_timeout:
|
||||||
|
try:
|
||||||
|
config["llm"]["timeout_seconds"] = float(env_timeout)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
env_web_host = os.getenv("TACTTERM_WEB_HOST")
|
env_web_host = os.getenv("TACTTERM_WEB_HOST")
|
||||||
if env_web_host:
|
if env_web_host:
|
||||||
config["web"]["host"] = env_web_host
|
config["web"]["host"] = env_web_host
|
||||||
@@ -77,6 +94,10 @@ class Config:
|
|||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable_boilerplate(self) -> bool:
|
||||||
|
return bool(self._data.get("enable_boilerplate", False))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def llm_base_url(self) -> str:
|
def llm_base_url(self) -> str:
|
||||||
return self._data["llm"]["base_url"].rstrip("/")
|
return self._data["llm"]["base_url"].rstrip("/")
|
||||||
|
|||||||
+67
-18
@@ -8,9 +8,15 @@ from src.core.registry import registry
|
|||||||
|
|
||||||
|
|
||||||
class CodeExecutor:
|
class CodeExecutor:
|
||||||
"""Executes code for all 10 supported programming languages with stdin support."""
|
"""Executes code for all 10 supported programming languages with stdin support and multi-file projects."""
|
||||||
|
|
||||||
def run(self, language: str, code: str, stdin: str = "") -> Dict[str, Any]:
|
def run(
|
||||||
|
self,
|
||||||
|
language: str,
|
||||||
|
code: str = "",
|
||||||
|
stdin: str = "",
|
||||||
|
files: Dict[str, str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
config = registry.get_config(language)
|
config = registry.get_config(language)
|
||||||
if not config:
|
if not config:
|
||||||
return {
|
return {
|
||||||
@@ -24,20 +30,42 @@ class CodeExecutor:
|
|||||||
ext = config.get("ext", ".txt")
|
ext = config.get("ext", ".txt")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
if lang_key == "java":
|
# 1. Write multi-file project contents or fallback single file
|
||||||
temp_file_name = "Main.java"
|
if files and isinstance(files, dict) and len(files) > 0:
|
||||||
elif lang_key == "cpp":
|
for rel_path, content in files.items():
|
||||||
temp_file_name = "main.cpp"
|
target_path = os.path.join(tmpdir, rel_path)
|
||||||
elif lang_key == "rust":
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||||
temp_file_name = "main.rs"
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
elif lang_key == "csharp":
|
f.write(content)
|
||||||
temp_file_name = "Program.cs"
|
# Determine main entry file
|
||||||
|
main_file_candidates = [
|
||||||
|
"main.cpp", "Main.java", "main.rs", "Program.cs", "main.py",
|
||||||
|
"main.go", "main.js", "main.ts", "main.lua", "index.html"
|
||||||
|
]
|
||||||
|
temp_file_name = None
|
||||||
|
for candidate in main_file_candidates:
|
||||||
|
if candidate in files:
|
||||||
|
temp_file_name = candidate
|
||||||
|
break
|
||||||
|
if not temp_file_name:
|
||||||
|
temp_file_name = list(files.keys())[0]
|
||||||
else:
|
else:
|
||||||
temp_file_name = f"main{ext}"
|
if lang_key == "java":
|
||||||
|
temp_file_name = "Main.java"
|
||||||
|
elif lang_key == "cpp":
|
||||||
|
temp_file_name = "main.cpp"
|
||||||
|
elif lang_key == "rust":
|
||||||
|
temp_file_name = "main.rs"
|
||||||
|
elif lang_key == "csharp":
|
||||||
|
temp_file_name = "Program.cs"
|
||||||
|
else:
|
||||||
|
temp_file_name = f"main{ext}"
|
||||||
|
|
||||||
|
temp_file_path = os.path.join(tmpdir, temp_file_name)
|
||||||
|
with open(temp_file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
temp_file_path = os.path.join(tmpdir, temp_file_name)
|
temp_file_path = os.path.join(tmpdir, temp_file_name)
|
||||||
with open(temp_file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(code)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if lang_key == "python":
|
if lang_key == "python":
|
||||||
@@ -51,10 +79,13 @@ class CodeExecutor:
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
elif lang_key == "html":
|
elif lang_key == "html":
|
||||||
|
primary_code = code
|
||||||
|
if files and temp_file_name in files:
|
||||||
|
primary_code = files[temp_file_name]
|
||||||
return {
|
return {
|
||||||
"language": language,
|
"language": language,
|
||||||
"exit_code": 0,
|
"exit_code": 0,
|
||||||
"stdout": code,
|
"stdout": primary_code,
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
}
|
}
|
||||||
elif lang_key == "javascript":
|
elif lang_key == "javascript":
|
||||||
@@ -77,7 +108,7 @@ class CodeExecutor:
|
|||||||
)
|
)
|
||||||
elif lang_key == "go":
|
elif lang_key == "go":
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["go", "run", temp_file_path],
|
["go", "run", "."],
|
||||||
input=stdin,
|
input=stdin,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -94,8 +125,17 @@ class CodeExecutor:
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
elif lang_key == "cpp":
|
elif lang_key == "cpp":
|
||||||
|
# Collect all .cpp files recursively in tmpdir
|
||||||
|
cpp_files = []
|
||||||
|
for root, _, fnames in os.walk(tmpdir):
|
||||||
|
for fn in fnames:
|
||||||
|
if fn.endswith(".cpp"):
|
||||||
|
cpp_files.append(os.path.relpath(os.path.join(root, fn), tmpdir))
|
||||||
|
if not cpp_files:
|
||||||
|
cpp_files = [temp_file_name]
|
||||||
|
|
||||||
compile_res = subprocess.run(
|
compile_res = subprocess.run(
|
||||||
["g++", "-O2", temp_file_path, "-o", "main"],
|
["g++", "-O2"] + cpp_files + ["-o", "main"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
cwd=tmpdir,
|
cwd=tmpdir,
|
||||||
@@ -140,8 +180,16 @@ class CodeExecutor:
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
elif lang_key == "java":
|
elif lang_key == "java":
|
||||||
|
java_files = []
|
||||||
|
for root, _, fnames in os.walk(tmpdir):
|
||||||
|
for fn in fnames:
|
||||||
|
if fn.endswith(".java"):
|
||||||
|
java_files.append(os.path.relpath(os.path.join(root, fn), tmpdir))
|
||||||
|
if not java_files:
|
||||||
|
java_files = [temp_file_name]
|
||||||
|
|
||||||
compile_res = subprocess.run(
|
compile_res = subprocess.run(
|
||||||
["javac", temp_file_path],
|
["javac"] + java_files,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
cwd=tmpdir,
|
cwd=tmpdir,
|
||||||
@@ -154,8 +202,9 @@ class CodeExecutor:
|
|||||||
"stdout": compile_res.stdout,
|
"stdout": compile_res.stdout,
|
||||||
"stderr": f"Javac Compilation Error:\n{compile_res.stderr}",
|
"stderr": f"Javac Compilation Error:\n{compile_res.stderr}",
|
||||||
}
|
}
|
||||||
|
main_class = os.path.splitext(temp_file_name)[0]
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["java", "Main"],
|
["java", main_class],
|
||||||
input=stdin,
|
input=stdin,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
|
|||||||
@@ -78,12 +78,11 @@ class ChallengeGenerator:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
raw_md = (
|
msg_obj = data.get("choices", [{}])[0].get("message", {})
|
||||||
data.get("choices", [{}])[0]
|
raw_md = msg_obj.get("content") or ""
|
||||||
.get("message", {})
|
if not raw_md.strip() and msg_obj.get("reasoning_content"):
|
||||||
.get("content", "")
|
raw_md = msg_obj.get("reasoning_content", "")
|
||||||
.strip()
|
raw_md = raw_md.strip()
|
||||||
)
|
|
||||||
|
|
||||||
# Clean up outer markdown wrapper if present
|
# Clean up outer markdown wrapper if present
|
||||||
if raw_md.startswith("```markdown"):
|
if raw_md.startswith("```markdown"):
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ class HandbookService:
|
|||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
],
|
],
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 900,
|
"max_tokens": max(config.llm_max_tokens, 4096),
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
@@ -366,12 +366,11 @@ class HandbookService:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
example_md = (
|
msg_obj = data.get("choices", [{}])[0].get("message", {})
|
||||||
data.get("choices", [{}])[0]
|
example_md = msg_obj.get("content") or ""
|
||||||
.get("message", {})
|
if not example_md.strip() and msg_obj.get("reasoning_content"):
|
||||||
.get("content", "")
|
example_md = msg_obj.get("reasoning_content", "")
|
||||||
.strip()
|
example_md = example_md.strip()
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|||||||
+28
-12
@@ -10,7 +10,13 @@ from src.core.registry import registry
|
|||||||
class CodeLinter:
|
class CodeLinter:
|
||||||
"""Lints user code for syntax and style issues for 10 languages."""
|
"""Lints user code for syntax and style issues for 10 languages."""
|
||||||
|
|
||||||
def lint(self, language: str, code: str) -> Dict[str, Any]:
|
def lint(
|
||||||
|
self,
|
||||||
|
language: str,
|
||||||
|
code: str = "",
|
||||||
|
files: Dict[str, str] = None,
|
||||||
|
active_file: str = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
config = registry.get_config(language)
|
config = registry.get_config(language)
|
||||||
if not config:
|
if not config:
|
||||||
return {
|
return {
|
||||||
@@ -24,20 +30,30 @@ class CodeLinter:
|
|||||||
ext = config.get("ext", ".txt")
|
ext = config.get("ext", ".txt")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
if lang_key == "java":
|
if files and isinstance(files, dict) and len(files) > 0:
|
||||||
temp_file_name = "Main.java"
|
for rel_path, content in files.items():
|
||||||
elif lang_key == "cpp":
|
target_path = os.path.join(tmpdir, rel_path)
|
||||||
temp_file_name = "main.cpp"
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||||
elif lang_key == "rust":
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
temp_file_name = "main.rs"
|
f.write(content)
|
||||||
elif lang_key == "csharp":
|
temp_file_name = active_file if active_file and active_file in files else list(files.keys())[0]
|
||||||
temp_file_name = "Program.cs"
|
|
||||||
else:
|
else:
|
||||||
temp_file_name = f"main{ext}"
|
if lang_key == "java":
|
||||||
|
temp_file_name = "Main.java"
|
||||||
|
elif lang_key == "cpp":
|
||||||
|
temp_file_name = "main.cpp"
|
||||||
|
elif lang_key == "rust":
|
||||||
|
temp_file_name = "main.rs"
|
||||||
|
elif lang_key == "csharp":
|
||||||
|
temp_file_name = "Program.cs"
|
||||||
|
else:
|
||||||
|
temp_file_name = f"main{ext}"
|
||||||
|
|
||||||
|
temp_path = os.path.join(tmpdir, temp_file_name)
|
||||||
|
with open(temp_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
temp_path = os.path.join(tmpdir, temp_file_name)
|
temp_path = os.path.join(tmpdir, temp_file_name)
|
||||||
with open(temp_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(code)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if lang_key == "python":
|
if lang_key == "python":
|
||||||
|
|||||||
+29
-12
@@ -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 = (
|
||||||
@@ -53,12 +60,11 @@ class MentorClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
content = (
|
msg_obj = data.get("choices", [{}])[0].get("message", {})
|
||||||
data.get("choices", [{}])[0]
|
content = msg_obj.get("content") or ""
|
||||||
.get("message", {})
|
if not content.strip() and msg_obj.get("reasoning_content"):
|
||||||
.get("content", "")
|
content = msg_obj.get("reasoning_content", "")
|
||||||
.strip()
|
content = content.strip()
|
||||||
)
|
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
return {
|
return {
|
||||||
@@ -93,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
|
||||||
@@ -104,15 +115,21 @@ class MentorClient:
|
|||||||
import nest_asyncio
|
import nest_asyncio
|
||||||
nest_asyncio.apply()
|
nest_asyncio.apply()
|
||||||
return loop.run_until_complete(
|
return loop.run_until_complete(
|
||||||
self.get_guidance_async(challenge, user_code, user_question=user_question)
|
self.get_guidance_async(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return asyncio.run(
|
return asyncio.run(
|
||||||
self.get_guidance_async(challenge, user_code, user_question=user_question)
|
self.get_guidance_async(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return asyncio.run(
|
return asyncio.run(
|
||||||
self.get_guidance_async(challenge, user_code, user_question=user_question)
|
self.get_guidance_async(
|
||||||
|
challenge, user_code, user_question=user_question, files=files, active_file=active_file
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+80
-2
@@ -41,7 +41,81 @@ Your ultimate mission is to build the user's confidence in designing and constru
|
|||||||
SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR
|
SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR
|
||||||
|
|
||||||
|
|
||||||
def get_mentor_prompt(challenge: object, user_code: str, user_question: str = "") -> str:
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def compress_file_content(filename: str, content: str, max_lines: int = 30) -> str:
|
||||||
|
"""Compresses file content by extracting signatures and truncating bodies if context limit is reached."""
|
||||||
|
lines = content.splitlines()
|
||||||
|
if len(lines) <= max_lines:
|
||||||
|
return content
|
||||||
|
|
||||||
|
headers = []
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if (
|
||||||
|
stripped.startswith(("#include", "import ", "from ", "using ", "package "))
|
||||||
|
or stripped.startswith(("def ", "class ", "struct ", "fn ", "interface ", "enum ", "public ", "private "))
|
||||||
|
or stripped.endswith(("{", ":"))
|
||||||
|
):
|
||||||
|
headers.append(line)
|
||||||
|
|
||||||
|
if len(headers) >= 5:
|
||||||
|
return "\n".join(headers[:max_lines]) + f"\n... [{len(lines) - len(headers)} lines omitted]"
|
||||||
|
else:
|
||||||
|
return "\n".join(lines[:max_lines]) + f"\n... [{len(lines) - max_lines} lines omitted]"
|
||||||
|
|
||||||
|
|
||||||
|
def format_multifile_context(
|
||||||
|
user_code: str | Dict[str, str] = "",
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
|
active_file: Optional[str] = None,
|
||||||
|
max_chars: int = 12000,
|
||||||
|
) -> str:
|
||||||
|
"""Formats single or multi-file project code for the LLM mentor, applying context compression if needed."""
|
||||||
|
if isinstance(user_code, dict) and files is None:
|
||||||
|
files = user_code
|
||||||
|
user_code = ""
|
||||||
|
|
||||||
|
if not files or not isinstance(files, dict):
|
||||||
|
code_str = str(user_code or "")
|
||||||
|
return code_str if code_str.strip() else "# No code written yet"
|
||||||
|
|
||||||
|
active_name = active_file if active_file and active_file in files else list(files.keys())[0]
|
||||||
|
|
||||||
|
total_len = sum(len(c) for c in files.values())
|
||||||
|
should_compress = total_len > max_chars
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
file_list_summary = "Project Structure:\n" + "\n".join(
|
||||||
|
f"- {fname}{' (Active)' if fname == active_name else ''}" for fname in files.keys()
|
||||||
|
)
|
||||||
|
parts.append(file_list_summary)
|
||||||
|
|
||||||
|
# 1. Active file first (kept in full)
|
||||||
|
active_content = files[active_name]
|
||||||
|
parts.append(f"=== File: {active_name} (Active) ===\n{active_content}")
|
||||||
|
|
||||||
|
# 2. Inactive files (compressed if overall context is large)
|
||||||
|
for fname, content in files.items():
|
||||||
|
if fname == active_name:
|
||||||
|
continue
|
||||||
|
if should_compress and not fname.endswith((".h", ".hpp", ".d.ts")):
|
||||||
|
compressed = compress_file_content(fname, content)
|
||||||
|
parts.append(f"=== File: {fname} (Compressed) ===\n{compressed}")
|
||||||
|
else:
|
||||||
|
parts.append(f"=== File: {fname} ===\n{content}")
|
||||||
|
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mentor_prompt(
|
||||||
|
challenge: object,
|
||||||
|
user_code: str,
|
||||||
|
user_question: str = "",
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
|
active_file: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
"""Returns the formatted system prompt for the LLM mentor."""
|
"""Returns the formatted system prompt for the LLM mentor."""
|
||||||
reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", []))
|
reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", []))
|
||||||
hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", []))
|
hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", []))
|
||||||
@@ -52,6 +126,10 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = ""
|
|||||||
else "How should I structure my program and overcome my current blocker for this challenge?"
|
else "How should I structure my program and overcome my current blocker for this challenge?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
formatted_code = format_multifile_context(
|
||||||
|
user_code=user_code, files=files, active_file=active_file
|
||||||
|
)
|
||||||
|
|
||||||
return SYSTEM_PROMPT_MENTOR.format(
|
return SYSTEM_PROMPT_MENTOR.format(
|
||||||
challenge_name=getattr(challenge, "name", "Coding Challenge"),
|
challenge_name=getattr(challenge, "name", "Coding Challenge"),
|
||||||
language=getattr(challenge, "language", "Python"),
|
language=getattr(challenge, "language", "Python"),
|
||||||
@@ -59,7 +137,7 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = ""
|
|||||||
challenge_description=getattr(challenge, "description", ""),
|
challenge_description=getattr(challenge, "description", ""),
|
||||||
requirements=reqs if reqs else "- Follow standard problem specifications",
|
requirements=reqs if reqs else "- Follow standard problem specifications",
|
||||||
hints=hints if hints else "- Think through edge cases",
|
hints=hints if hints else "- Think through edge cases",
|
||||||
user_code=user_code if user_code.strip() else "# No code written yet",
|
user_code=formatted_code,
|
||||||
user_question=q_text,
|
user_question=q_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+906
-60
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -141,7 +141,7 @@ class GenHelpModal(ModalScreen):
|
|||||||
},
|
},
|
||||||
{"role": "user", "content": question},
|
{"role": "user", "content": question},
|
||||||
],
|
],
|
||||||
"max_tokens": 1000,
|
"max_tokens": config.llm_max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
@@ -234,7 +234,7 @@ class GenTUIApp(App):
|
|||||||
border: solid $secondary;
|
border: solid $secondary;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#btn-help-modal {
|
#btn-help-modal {
|
||||||
@@ -262,6 +262,7 @@ class GenTUIApp(App):
|
|||||||
Binding("ctrl+h", "open_help_modal", "AI Helper"),
|
Binding("ctrl+h", "open_help_modal", "AI Helper"),
|
||||||
Binding("ctrl+g", "generate_ai", "Generate AI"),
|
Binding("ctrl+g", "generate_ai", "Generate AI"),
|
||||||
Binding("ctrl+s", "save_challenge", "Save File"),
|
Binding("ctrl+s", "save_challenge", "Save File"),
|
||||||
|
Binding("ctrl+w", "toggle_word_wrap", "Word Wrap (Ctrl+W)"),
|
||||||
Binding("ctrl+q", "quit", "Quit"),
|
Binding("ctrl+q", "quit", "Quit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -404,6 +405,14 @@ class GenTUIApp(App):
|
|||||||
else:
|
else:
|
||||||
self.query_one("#status-bar", Static).update(f"✗ Save Error: {res.get('message')}")
|
self.query_one("#status-bar", Static).update(f"✗ Save Error: {res.get('message')}")
|
||||||
|
|
||||||
|
def action_toggle_word_wrap(self) -> None:
|
||||||
|
"""Toggle soft word wrap in the raw markdown editor."""
|
||||||
|
editor = self.query_one("#markdown-editor", TextArea)
|
||||||
|
editor.soft_wrap = not editor.soft_wrap
|
||||||
|
status = "ON" if editor.soft_wrap else "OFF"
|
||||||
|
self.notify(f"Markdown Editor Word Wrap turned {status}", title="Word Wrap Toggled")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="GenTUI — TactiTerm AI Challenge Generator")
|
parser = argparse.ArgumentParser(description="GenTUI — TactiTerm AI Challenge Generator")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user